sice_cream picoCTF 2019 Solution

Published: April 2, 2026

Description

Just pwn this heap challenge. Connect with nc to get the flag.

Download the binary, libc, and connect to the server.

bash
wget <url>/sice_cream
bash
wget <url>/libc.so.6
bash
chmod +x sice_cream
bash
nc <HOST> <PORT_FROM_INSTANCE>
  1. Step 1Understand the heap menu
    Run the binary locally. It is a heap-based challenge with options like: create ice cream (malloc), eat ice cream (free), and read flavors (read). Understand the data structures used and look for a use-after-free or heap overflow vulnerability.
    bash
    ./sice_cream
    bash
    checksec sice_cream
    Learn more

    Heap exploitation targets the dynamic memory allocator (glibc malloc). Common vulnerabilities: use-after-free (accessing freed memory), double-free (freeing the same chunk twice), heap overflow (writing past the end of an allocated chunk). These can corrupt allocator metadata (size fields, free lists) to redirect future allocations.

    Run checksec to identify protections: ASLR, PIE, NX, RELRO, Stack Canary. These affect which exploitation techniques are feasible.

  2. Step 2Identify the vulnerability
    Analyze the binary in Ghidra. Look for: index bounds checking (or lack thereof), double-free potential, use-after-free in the read/write functions. The vulnerability is likely a tcache poisoning setup.
    bash
    ghidra sice_cream &
    Learn more

    Heap chunk layout (glibc, 64-bit). Every malloc'd chunk has a 16-byte header. Once freed and placed in tcache, the first 8 bytes of user data become the fd (forward pointer) to the next chunk in the same-sized bin. The allocator does not validate fd on tcache pop until glibc 2.32 added pointer mangling.

    Allocated chunk:                Freed chunk in tcache (pre-2.32):
      +-----------------+             +-----------------+
      | prev_size (8)   |             | prev_size (8)   |
      +-----------------+             +-----------------+
      | size | A|M|P    |   <- meta   | size | A|M|P    |
      +-----------------+             +-----------------+
      | user data ...   |             | fd  -> next     |  <- attacker target
      | ...             |             | (rest is junk)  |
      +-----------------+             +-----------------+

    tcache poisoning recipe:

    • x = malloc(0x20); free(x); -- x lands in tcache[0x20]. Its fd is NULL (single entry).
    • Trigger UAF or overflow to overwrite x->fd with &__free_hook (or any 8-byte writable target).
    • malloc(0x20) returns x -- tcache pops the head, advances head to x->fd.
    • malloc(0x20) again returns &__free_hook. Whatever you write into the "chunk" lands at __free_hook.
    • Write a one_gadget address into __free_hook. Next free() jumps there with rdi pointing at the chunk being freed -- shell.

    The libc base leak (step 3) is the prerequisite: __free_hook, __malloc_hook, and one_gadgets all live at fixed offsets within libc, so once you know the libc base you know all of them.

  3. Step 3Develop the exploit
    Write a pwntools exploit script. Leak a libc address (via unsorted bin or stdout), compute the addresses of __free_hook or __malloc_hook, poison the tcache to overwrite the hook with a one-gadget, then trigger it.
    python
    python3 << 'EOF'
    from pwn import *
    
    # Load binary and libc
    elf = ELF('./sice_cream')
    libc = ELF('./libc.so.6')
    
    # Start process or connect to remote
    p = process('./sice_cream')
    # p = remote('<HOST>', <PORT>)
    
    # --- Exploit skeleton ---
    # 1. Leak heap/libc address
    # 2. Overwrite tcache fd
    # 3. Allocate to arbitrary address
    # 4. Write one-gadget over __free_hook
    # 5. Trigger free() to get shell
    
    p.interactive()
    EOF
    Learn more

    Leaking libc. If you can free a chunk into the unsorted bin (size > 0x408 or all tcache slots full), its fd/bk point at main_arena+88 inside libc. Reading the chunk back via the menu's "view" option discloses that pointer. Subtract the known offset of main_arena+88 within the provided libc to compute the libc base:

    leaked = u64(p.recv(8))            # main_arena+88
    libc.address = leaked - 0x3ebca0   # offset of main_arena+88 in libc.so.6
    free_hook   = libc.sym['__free_hook']
    one_gadget  = libc.address + 0x4f432   # check constraints with one_gadget tool

    One-gadget constraints. Each one_gadget candidate has constraints like [rsp+0x40] == NULL or r12 == NULL. __free_hook is called as hook(ptr), so when the gadget fires rdi is the chunk pointer. Pick a gadget whose constraints are satisfied at __free_hook entry, or trigger via __malloc_hook (called with size in rdi) if those constraints fit better.

Flag

picoCTF{...}

Exploit a heap vulnerability (likely tcache poisoning) to overwrite __free_hook with a one-gadget and get a shell.

Want more picoCTF 2019 writeups?

Useful tools for Binary Exploitation

Related reading

What to try next