Bootcamp
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 = 24chunk_size = 0x60→max_request = 88chunk_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 read-only • copy allowed#!/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
| Round | Size field (hex) | Chunk size (masked) | Max malloc | Response |
|---|---|---|---|---|
| 1 | 0x21 | 0x20 | 24 | YESSSS!!! |
| 2 | 0x61 | 0x60 | 88 | YESSSS!!! |
| 3 | 0x81 | 0x80 | 120 | YESSSS!!! |
| 4 | 0x351 | 0x350 | 840 | YESSSS!!! |
| 5 | 0x5a1 | 0x5a0 | 1432 | YESSSS!!! |
| |
All 15 rounds answered correctly → You won! → flag printed.