tea-cash picoCTF 2026 Solution

Published: March 20, 2026

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.

Download heapedit, heapedit.c, libc.so.6, and the Makefile.

Read heapedit.c to understand the heap memory layout.

bash
cat heapedit.c
bash
chmod +x heapedit

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Understand the memory layout from the source
    Observation
    I 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.
    bash
    cat heapedit.c
    What 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.

  2. Step 2
    Compute the addresses by adding 0x90 repeatedly
    Observation
    I 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.
    bash
    nc <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.
    python
    python3 - <<'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))
    EOF
    What 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.

Key takeaway

The heap allocator stores metadata (chunk size, free-list pointers) in headers adjacent to user data, so each allocated region is larger than the size you requested. Understanding that a malloc(0x80) call produces a 0x90-byte chunk (0x80 data + 0x10 header) is the foundational arithmetic behind every heap exploitation technique. The tcache is a per-thread LIFO singly-linked list that glibc uses to accelerate small allocations; corrupting its forward pointer through a use-after-free or overflow redirects the next malloc to attacker-controlled memory, which is the primitive underlying tcache poisoning and house-of-spirit attacks.

Related reading

Want more picoCTF 2026 writeups?

Useful tools for Binary Exploitation

What to try next