Description
A two-argument program with a heap overflow (not a stack smash). It copies your first argument into a small heap buffer with an unbounded strcpy, and that buffer sits right before a second heap struct that holds an exit-time callback function pointer. Overflow the first buffer to overwrite that callback with the address of winner(), and the flag prints when the program cleans up.
Setup
Download vuln and vuln.c.
Read vuln.c to understand how the two heap structs are allocated and how your argv[1] is handled.
cat vuln.cchmod +x vulnfile vuln # 32-bit ELFSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Find the unbounded strcpy and the adjacent callbackObservationI noticed the program takes two arguments and allocates two heap structs back to back, which suggested that an unbounded strcpy on argv[1] into the first struct's buffer could overrun into the second struct's fields, including its exit-time callback pointer.The program allocates two structs on the heap, back to back. It runs strcpy(struct1->name, argv[1]) with no length check, so a long first argument overruns struct1's name buffer and writes into struct2. struct2 contains a callback function pointer that the program invokes at exit (and a name pointer it dereferences). Overwriting that callback is the win.bashgrep -nE 'malloc|strcpy|argv|\(\*' vuln.cbashobjdump -d vuln | grep -A2 '<winner>:' # note winner()'s address (32-bit)What didn't work first
Tried: Run 'nm vuln | grep winner' to get winner()'s address instead of using objdump.
nm prints the symbol's value correctly, but for a stripped binary it may show no output at all. objdump -d disassembles the text section and always reveals the function's load address even without symbol table entries, making it the safer choice here. More importantly, nm gives the address but not the surrounding instruction context you need to confirm the function boundary.
Tried: Use 'grep -nE strcpy vuln.c' without the broader pattern to find the overflow site.
A narrow grep for just 'strcpy' finds the copy call but misses the malloc lines that reveal struct sizes and the function-pointer field. Without seeing the malloc sizes and the (*callback) declaration together you cannot determine the byte distance between struct1's name buffer and struct2's callback, which is the critical measurement for building the payload.
Learn more
Why this is heap, not stack. The overflowed buffer lives in a
malloc'd chunk, so there is no saved return address nearby to clobber. Instead the interesting target is data in the next heap chunk: a function pointer the program later calls. Overwriting heap data to hijack a stored callback is a data-only attack; you never touch the stack.Step 2
Compute the offset and build the overwriteObservationI noticed from vuln.c that struct2 contains a name pointer field before the callback, which suggested I needed to compute the exact byte distance from struct1->name through all of struct2's fields and supply a valid heap address for struct2->name to avoid a segfault before the callback fires.Work out the byte distance from struct1->name to struct2's callback field. The published layout (snwau) for the 32-bit binary is: 8 bytes to fill struct1->name, then bytes that land on struct2's fields. Set struct2's name pointer to a valid readable heap address (e.g. the heap top 0x0804c040) so the program does not crash dereferencing it, and set struct2's callback to winner() (e.g. 0x080492b6). Confirm both addresses against your binary with objdump.bash# 32-bit, little-endian. Addresses are binary-specific - read them from objdump. ./vuln "$(python3 -c 'import sys; sys.stdout.buffer.write( b"A"*8 + # fill struct1->name b"BBBB" + # struct2->priority / padding b"CCCC" + # (alignment between fields) b"\x40\xc0\x04\x08" + # struct2->name = 0x0804c040 (valid ptr) b"\xb6\x92\x04\x08")')" dummy # struct2->callback = winner() 0x080492b6The exact padding and field order come from
vuln.cand the 32-bit struct layout; the0x0804c040/0x080492b6values are from the public instance. Recheck them withobjdump -d vulnagainst your binary, since a wrongnamepointer crashes before the callback fires.What didn't work first
Tried: Use a flat padding of just 8 'A' bytes followed immediately by the winner() address, treating it like a classic stack buffer overflow.
struct2 contains two fields before the callback - a priority field and a name pointer - so placing the winner() address right after 8 bytes lands it in the wrong field. The program dereferences struct2->name first; if that field holds the winner() address (a text-segment address, not heap), it segfaults before the callback fires. You must account for every intervening field in struct2's layout between the end of struct1->name and the callback offset.
Tried: Copy the hardcoded addresses 0x0804c040 and 0x080492b6 directly from the writeup without verifying them against your own binary download.
Challenge binaries can be recompiled per instance, shifting function addresses and heap base layout. If your binary has winner() at a different address, the callback field gets a garbage pointer and the program either crashes or returns normally without printing the flag. Always run 'objdump -d vuln | grep winner' on your specific downloaded binary before building the payload.
Learn more
Why the name pointer matters too. If the program prints or otherwise dereferences
struct2->namebefore calling the callback, leaving that field as garbage segfaults you before the payoff. Pointing it at a known-readable heap address keeps execution alive long enough to reach the hijacked callback. See the heap exploitation guide for adjacent-chunk overwrite patterns.Step 3
Trigger the callback and read the flagObservationI noticed that once the callback pointer in struct2 is overwritten with winner()'s address, simply running the program with the crafted argv[1] causes the overwritten callback to fire during cleanup, printing the flag.Run the program with the crafted first argument and a dummy second argument. During cleanup it calls the now-overwritten callback, jumping into winner(), which prints the flag.bash# winner() runs at program exit and prints the flagExpected output
picoCTF{...}Learn more
This is a heap-data overwrite hijacking a stored function pointer, a common alternative to stack-return-address control when the bug lives on the heap. For the pwntools form (build the argv blob, run locally first), see Pwntools for CTF.
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.
Flag
Reveal flag
picoCTF{...}
Heap overflow, not a stack smash: an unbounded strcpy of argv[1] into the first struct's name buffer overruns into the adjacent struct, whose exit-time callback pointer you overwrite with winner() (give its name pointer a valid heap address so it does not crash first). Addresses are 32-bit and binary-specific.