Guess the Size
Guess the Size
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-03 | Status: Solved Techniques: glibc_chunk_size_calculation, little_endian_packing, prev_inuse_bit
Summary
Task: service calls malloc(N) with random sizes for 42 rounds and asks for the glibc chunk size field value. Solution: implement request2size macro (add SIZE_SZ, align to 16, enforce MINSIZE 0x20, set PREV_INUSE bit), send as 8 raw little-endian bytes.
Guess the Size — spbctf
Description
"Guess what is missed?"
nc 109.233.56.90 11800
The service presents 42 rounds. Each round calls malloc(N) with a random size and asks: "What will be the size field of this chunk?" You must send 8 raw bytes (no newline) representing the correct glibc heap chunk size field value.
Analysis
Upon connecting, the server prints:
There is no FREE here!
You should send 8 (EIGHT) bytes to each question without newline at the end!
challenge 0/42
i call malloc(130)! What will be size field of this chunk?(Send raw 8 bytes!)
If you answer wrong, it reveals the correct answer: nope. it was 91 (hex without 0x prefix). For malloc(130), the correct size field is 0x91.
Key insight — glibc request2size macro
The challenge tests knowledge of how glibc internally computes the chunk size for a given malloc() request on a 64-bit system. The relevant constants and formula:
- SIZE_SZ = 8 (sizeof(size_t) on x86-64)
- MALLOC_ALIGNMENT = 16
- MINSIZE = 0x20 (minimum chunk = 32 bytes = 4 × SIZE_SZ)
The calculation:
- Add overhead:
x = request + SIZE_SZ(8 bytes for the chunk header — theprev_sizefield is reused by the previous chunk's data, so onlysizeneeds space) - Align up: round
xto the next multiple of 16 - Enforce minimum: if result < MINSIZE (0x20), use MINSIZE
- Set PREV_INUSE bit: OR with 1 (bit 0)
Semantic clues from the challenge
- "Guess what is missed?" — hints at the PREV_INUSE bit, the commonly forgotten
| 1when computing chunk sizes - "There is no FREE here!" — since no chunks are ever freed, the PREV_INUSE bit is always set (there are no freed chunks to clear it)
Worked example
malloc(130):
- 130 + 8 = 138
- Round up to 16: 144 = 0x90
- Set PREV_INUSE: 0x90 | 1 = 0x91
Solution
python read-only • copy allowed#!/usr/bin/env python3 import socket, re, struct, sys HOST, PORT = "109.233.56.90", 11800 def chunksize(req): """Replicate glibc request2size macro for 64-bit systems.""" SIZE_SZ = 8 # sizeof(size_t) on x86-64 ALIGN = 16 # MALLOC_ALIGNMENT MINSIZE = 0x20 # minimum chunk size (4 * SIZE_SZ) x = req + SIZE_SZ if x < MINSIZE: x = MINSIZE x = (x + (ALIGN - 1)) & ~(ALIGN - 1) return x | 1 # PREV_INUSE bit def main(): s = socket.socket() s.connect((HOST, PORT)) s.settimeout(15) buf = b"" answered_for = None while True: try: data = s.recv(4096) except socket.timeout: print("[timeout] buffer:", buf.decode(errors='replace')) break if not data: print("[closed] buffer:", buf.decode(errors='replace')) break buf += data text = buf.decode(errors='replace') sys.stdout.write(data.decode(errors='replace')) sys.stdout.flush() matches = list(re.finditer( r"i call malloc\((\d+)\)!.*?Send raw 8 bytes!\)", text, re.S )) if matches: m = matches[-1] key = (m.start(), int(m.group(1))) if key != answered_for: req = int(m.group(1)) sz = chunksize(req) packed = struct.pack("<Q", sz) # little-endian 8 bytes s.sendall(packed) print(f"\n>>> malloc({req}) size=0x{sz:x} sent={packed.hex()}\n") buf = b"" answered_for = None if __name__ == "__main__": main()
The script correctly answers all 42 rounds. The answer must be sent as 8 raw bytes in little-endian format (x86-64 memory layout).