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.
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 1Find the unbounded strcpy and the adjacent callback
ObservationThe program takes two arguments and allocates two heap structs back to back. An unbounded strcpy into the first struct's buffer runs straight into the second struct's fields, callback pointer included.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 value correctly, and prints nothing at all on a stripped binary. objdump disassembles the text section and shows the load address regardless, along with the surrounding instructions you need to confirm the function boundary.
Tried: Use 'grep -nE strcpy vuln.c' without the broader pattern to find the overflow site.
Grepping only for strcpy finds the copy and misses the malloc lines that give the struct sizes and the function-pointer field. Without both in view you cannot measure the distance from the first buffer to the callback, which is the whole 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 2Compute the offset and build the overwrite
Observationvuln.c shows a name pointer sitting before the callback in the second struct. Measure the exact distance across both structs, and give that pointer a valid heap address, or the program faults before the callback ever runs.Work out the byte distance from struct1->name to struct2's callback field. One verified layout 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 any address that is guaranteed readable (e.g. 0x0804c040, inside the binary's own data segment, since this ELF is non-PIE) 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 come from one verified run of 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.
Two fields sit before the callback, a priority value and a name pointer, so writing the address eight bytes in lands it in the wrong one. The program dereferences that name pointer first, and the 'A' bytes that land there form an unmapped address, so it segfaults before the callback runs. Count every intervening field.
Tried: Copy the hardcoded addresses 0x0804c040 and 0x080492b6 directly as fixed values without verifying them against your own binary download.
Challenge binaries get recompiled per instance, which moves function addresses and heap layout. With winner() at a different address, the callback gets a garbage pointer and the program crashes or exits quietly. Read the address off your own download 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 address keeps execution alive long enough to reach the hijacked callback. See the heap exploitation guide for adjacent-chunk overwrite patterns.Step 3Trigger the callback and read the flag
ObservationWith the callback pointing at winner(), running the program with the crafted argument fires it during cleanup.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{h34p_0v3rfl0w_...}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{h34p_0v3rfl0w_...}
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.