Description
You've stumbled upon a mysterious cash register that doesn't keep money - it keeps secrets in memory. Traverse the free list and find all the free chunks to get to the flag. Download: heapedit, heapedit.c, libc.so.6, and the Makefile.
Setup
Download heapedit, heapedit.c, libc.so.6, and the Makefile.
Read heapedit.c to understand the heap memory layout.
cat heapedit.cchmod +x heapeditSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Understand the memory layout from the sourceObservationI noticed the challenge provided heapedit.c alongside the binary, which meant the allocation size and loop count were readable without reversing; reading the source confirmed malloc(0x80) called 6 times in a loop, so I needed to account for glibc's 0x10 header to get the true per-chunk stride of 0x90.Read heapedit.c. The program creates 6 chunks of size 0x80 (128) bytes each in a loop, frees them in reverse order, then asks you to provide the address of each free chunk. Each chunk has a malloc header of 0x10 (16) bytes, so each block takes 0x90 (144) bytes total. The chunks are allocated sequentially in memory.bashcat heapedit.cWhat didn't work first
Tried: Assume each chunk is exactly 0x80 bytes apart because that is the malloc argument
malloc(0x80) returns 128 bytes of user data, but glibc prepends a 16-byte (0x10) header storing the chunk size and flags. The allocator aligns on 0x90-byte boundaries, not 0x80, so using 0x80 as the stride puts every address 16 bytes too low. Reading heapedit.c and checking sizeof shows the full block size is 0x90.
Tried: Assume the free list order matches allocation order (chunk 0 first freed, chunk 0 at head)
The source frees chunks in reverse order (chunk 5 down to chunk 0), so the tcache LIFO list head is chunk 0 and the tail is chunk 5 - the same order as allocation. The mistake is thinking LIFO reversal makes chunk 5 come first in traversal; re-reading the free loop in heapedit.c shows chunk 0 is freed last and therefore lands at the head.
Learn more
When
malloc(0x80)allocates 128 bytes, the allocator adds a 16-byte header (containing size information), making each chunk 0x90 bytes wide. Since the 6 chunks are allocated sequentially, they lie back-to-back in memory: chunk 0 at address X, chunk 1 at X + 0x90, chunk 2 at X + 0x180, etc.The tcache free list stores chunks in LIFO order. Since the chunks are freed in reverse order (chunk 5 first, chunk 0 last), the free list head is chunk 0, then chunk 1, etc. The program gives you the address of the free list head and asks you for the addresses of all 6 chunks.
Step 2
Compute the addresses by adding 0x90 repeatedlyObservationI noticed the program prints the free list head address at runtime and asks for all 6 chunk addresses in sequence, which suggested that simple arithmetic using the 0x90 stride derived from step 1 was enough to answer all 6 prompts without needing a debugger.Connect to the challenge. It tells you the starting address of the free list head. Compute each subsequent chunk address by adding 0x90. Enter all 6 addresses to get the flag.bashnc <HOST> <PORT_FROM_INSTANCE>bash# The program prints: 'Start of free list: 0x<ADDRESS>'bash# Compute: addr1 = start, addr2 = start + 0x90, addr3 = start + 0x120, etc.pythonpython3 - <<'EOF' start = int(input("Enter the free list start address (hex, e.g. 0x55a3f2b9b510): "), 16) for i in range(6): print(hex(start + i * 0x90)) EOFWhat didn't work first
Tried: Enter the addresses in decimal instead of hex because the python script prints hex strings
The program's input parser expects hexadecimal addresses (the same format it prints for the free list head). Pasting the decimal equivalents causes the server to reject each answer or compare against the wrong address range, and you never get the flag. Always submit in the 0x... hex format the challenge uses.
Tried: Use GDB to read the tcache forward pointers out of the freed chunks instead of computing addresses arithmetically
GDB works against a local copy of heapedit but the addresses on the remote server differ from your local run due to ASLR and a different heap base. The free list head address the server prints is the authoritative starting point; computing offsets from it arithmetically (adding 0x90 per chunk) is both simpler and correct regardless of ASLR.
Learn more
The key insight is that malloc allocates chunks sequentially in memory for a fresh heap. Each chunk is 0x80 (128) bytes of user data plus 0x10 (16) bytes of header = 0x90 (144) bytes per chunk. Add 0x90 to move from one chunk to the next.
The tcache is the "Thread Local Allocation Cache" - a per-thread LIFO cache for small allocations. Learning to reason about tcache free list traversal is foundational for heap exploitation challenges. This challenge teaches the basic concept before moving to tcache poisoning attacks.
Interactive tools
- pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.
- Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
Flag
Reveal flag
picoCTF{t34_c4sh_...}
The program tells you the free list start address. Compute the 6 chunk addresses by adding 0x90 each time (0x80 user data + 0x10 malloc header = 0x90 per chunk). Enter them all to get the flag.