← Back to Writeups
CUSTOMEasypwn

Guess the Size

XESXOR9/2/20263 min read
#pwn#custom#easy#glibc_chunk_size_calculation#little_endian_packing#prev_inuse_bit

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: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Add overhead: x = request + SIZE_SZ (8 bytes for the chunk header — the prev_size field is reused by the previous chunk's data, so only size needs space)
  2. Align up: round x to the next multiple of 16
  3. Enforce minimum: if result < MINSIZE (0x20), use MINSIZE
  4. 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 | 1 when 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
#!/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). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍