← Back to Writeups
CUSTOMEasypwn

Bootcamp

XESXOR9/2/20263 min read
#pwn#custom#easy#heap_chunk_size_calculation#hexdump_parsing#malloc_metadata_parsing

Bootcamp

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-03 | Status: Solved Techniques: heap_chunk_size_calculation, hexdump_parsing, malloc_metadata_parsing

Summary

Task: interactive 15-round quiz showing glibc heap hexdumps and asking for the maximum malloc() request size that produces a given chunk. Solution: parse the chunk size field from the hexdump, mask the low 3 flag bits, and compute chunk_size - 8.

Bootcamp — spbctf

Description

Go to the heap chunk training bootcamp to get used to chunk sizes

Connection: nc 109.233.56.90 11736

An interactive training challenge with 15 rounds. Each round, the server prints a glibc heap hexdump highlighting a target chunk with its size field visible (e.g. 0x21, 0x351, 0x5a1). The player must answer: what is the maximum malloc() request size that would produce a chunk of that size?

Analysis

glibc malloc chunk layout

In glibc's malloc implementation, every allocated chunk has an 8-byte header before the user data: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

+------------------+
| prev_size (8B)   |  ← only used if previous chunk is free
+------------------+
| size      (8B)   |  ← chunk size including header, with 3 flag bits
+------------------+
| user data ...    |  ← pointer returned by malloc()
+------------------+

The size field's low 3 bits encode flags:

  • Bit 0 (0x1): PREV_INUSE — previous chunk is allocated
  • Bit 1 (0x2): IS_MMAPPED — chunk was mmap'd
  • Bit 2 (0x4): NON_MAIN_ARENA — chunk belongs to a non-main arena ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Chunk size formula

When you call malloc(request), glibc computes the chunk size as:

chunk_size = align_up(request + 8, 16)    # minimum 0x20 (32 bytes)

The +8 accounts for the size field overhead (prev_size is reused by the previous chunk's data). Alignment is to 16 bytes on 64-bit systems.

Therefore, the maximum request that produces a given chunk size is:

max_request = chunk_size - 8

For example:

  • chunk_size = 0x20 → max_request = 24
  • chunk_size = 0x60 → max_request = 88
  • chunk_size = 0x350 → max_request = 840 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Hexdump parsing

The server displays a hexdump where the first line of the target chunk contains 16 bytes. Bytes 0–7 are prev_size (masked as __), and bytes 8–15 are the size field in little-endian. The solver reads these 8 bytes, reconstructs the value, masks the flag bits with & ~0x7, and computes chunk_size - 8.

Solution

python
#!/usr/bin/env python3
from pwn import *
import re

context.log_level = 'info'

r = remote('109.233.56.90', 11736)
r.recvuntil(b'Chunk identification training!')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

for rnd in range(15):
    data = r.recvuntil(b'produce this chunk?', timeout=15).decode(errors='replace')

    # Find hexdump lines (start with 0x...)
    lines = data.strip().splitlines()
    hexlines = [l for l in lines if re.match(r'^0x[0-9a-f]+', l.strip())]

    # First line: address + 16 byte columns
    # prev_size = bytes 0-7 (masked as __), size = bytes 8-15
    first = hexlines[0]
    toks = first.split()[1:]      # drop address
    bytetoks = toks[:16]          # first 16 byte tokens
    size_bytes = bytetoks[8:16]   # the size field
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    # Reconstruct little-endian value
    val = 0
    for i, b in enumerate(size_bytes):
        if b == '__':
            b = '00'
        val |= int(b, 16) << (8 * i)

    chunk_size = val & ~0x7       # mask flag bits
    max_malloc = chunk_size - 8   # maximum request size

    r.sendline(str(max_malloc).encode())
    resp = r.recvline(timeout=10)
    print(f"Round {rnd+1}: chunk_size={hex(chunk_size)} max_malloc={max_malloc} -> {resp.strip().decode()}")

print(r.recvall(timeout=10).decode(errors='replace'))

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Example rounds

RoundSize field (hex)Chunk size (masked)Max mallocResponse
10x210x2024YESSSS!!!
20x610x6088YESSSS!!!
30x810x80120YESSSS!!!
40x3510x350840YESSSS!!!
50x5a10x5a01432YESSSS!!!
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

All 15 rounds answered correctly → You won! → flag printed. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍