Description
A classic 32-bit ret2win challenge. The binary has a vulnerable input function and a win() function that prints the flag but is never called normally.
Your goal: overflow the stack buffer, find the exact offset to the saved return address, and overwrite it with the address of win().
Setup
Download the binary and make it executable.
Install pwntools if needed: pip install pwntools.
Use cyclic patterns, objdump, and pwntools to build the exploit.
wget https://artifacts.picoctf.net/c/188/vuln && chmod +x vulnpip install pwntoolscyclic 100 | ./vulnobjdump -d vuln | grep -A5 winSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Find the offset with a cyclic pattern
ObservationThe binary calls gets() into a fixed 32-byte buffer with no bounds check, so the saved return address sits at a predictable offset from the start of your input. A cyclic De Bruijn pattern reads that exact offset off the EIP value at the crash.Generate a De Bruijn cyclic pattern, send it as input, then read the value in EIP from the crash to find the exact offset.bashcyclic 100 | ./vulnbashcyclic -l <EIP_VALUE_FROM_CRASH>What didn't work first
Tried: Guess a round-number offset like 32 or 40 instead of using the cyclic pattern.
The buffer is declared as 32 bytes, but the compiler may add alignment padding or extra locals that push the saved return address further out. Guess 32 and you land inside the buffer rather than on EIP, so the binary either runs on normally or crashes with a different instruction pointer. The cyclic pattern removes the guessing: the unique 4-byte substring at EIP encodes the exact offset, assuming nothing about layout.
Tried: Run cyclic -l on the raw ASCII characters shown in the crash message rather than the hex EIP value.
cyclic -l wants the 4-byte little-endian integer the CPU loaded into EIP, not the ASCII text. Read the characters off the terminal and pass them as a string and you silently query the wrong sequence position, getting a wrong offset back. Pass the hex value printed under eip in the register dump.
Learn more
A De Bruijn sequence (called a cyclic pattern in pwntools) is a string where every possible 4-byte substring appears exactly once. When it overwrites the saved return address, the value in EIP after the crash is a unique 4-byte substring, and you can look up exactly how far into the sequence that substring appears - giving you the precise offset from the start of your input to the saved return address.
Stack frame layout (32-bit cdecl) when vuln() is about to ret:
high addr +-------------+ | saved eip | <- payload[44:48] = p32(win) +-------------+ | saved ebp | <- payload[40:44] +-------------+ | local vars | <- payload[32:40] (e.g. 8-byte gap) +-------------+ | char buf[32]| <- payload[0:32] "AAAA...AAAA" low addr +-------------+ <- esp at gets()gets()writes from low to high address, no length check. After 32 bytes you spill into the local-var slots (8 bytes), then the 4-byte savedebp(offset 40), then the 4-byte savedeip(offset 44). Bytes 44-47 of your input become the new return address.retpops them intoeipand jumps.cyclic 100generates a 100-byte De Bruijn sequence.cyclic -l 0x61616164(replace with your actual EIP value) tells you the offset. Alternatively, run under GDB (gdb ./vulnthenrun, paste the cyclic, andinfo registersafter the crash).The typical offset for this challenge's 32-byte buffer is ~44 bytes (32-byte buffer + 8 bytes of padding + 4-byte saved EBP), but treat that as an estimate, not a fact. Compiler version, optimization level, and any added locals shift the layout. Always confirm with a local cyclic run before you start guessing remote.
Step 2Find the address of win()
ObservationThe description says win() is a real function that never gets called on the normal path. Its address is therefore already in the ELF symbol table, readable with objdump or nm, with nothing to leak at runtime.Use objdump to disassemble the binary and locate the address of win(). This is the address you will overwrite EIP with.bashobjdump -d vuln | grep '<win>'bashnm vuln | grep winExpected output
picoCTF{addr3ss3s_ar3_3asy_c1...}What didn't work first
Tried: Use readelf -s vuln to find the win address and copy the hex value as big-endian into the payload.
readelf -s prints the right address, but x86 stores multi-byte integers little-endian. Copy the address as a big-endian hex string and append it directly, and EIP loads the bytes reversed and jumps somewhere meaningless. Pack it with p32() in pwntools, which reverses the bytes for you.
Tried: Grep for 'win' in strings vuln output to find the address.
strings extracts printable ASCII runs from the binary data. The address of win() lives as a binary integer in the ELF symbol table, not as readable text, so strings will never print it. objdump -d and nm parse the ELF structure and decode symbol addresses properly.
Learn more
objdump -ddisassembles all executable sections of the binary. The output shows each function, its starting address, and the machine instructions. Looking for the symbolwingives you the target address directly.nmlists symbol names and their addresses from the symbol table. It's faster when you just need the address of a named function without the full disassembly.In 32-bit ELF binaries without PIE, addresses are fixed at link time, so
elf.symbols['win']from one run is valid for every run.checksec --file=vulnconfirms PIE status (also shows NX, canary, RELRO).Why PIE matters here. If PIE were enabled,
win()'s address would be randomized per process by ASLR; the address you read fromobjdumpon your laptop would not match the address loaded on the challenge server. The exploit would fail silently - you'd crash, with no easy way to know it was an address mismatch versus an offset miscount. See ASLR & PIE Bypass for CTF for the leak-then-jump pattern when PIE is on.Step 3Write and run the pwntools exploit
ObservationBoth pieces are now in hand: the exact offset to EIP from the cyclic pattern, and the fixed address of win() from objdump on a non-PIE binary. A pwntools script pads to the offset and overwrites EIP with that address to redirect execution remotely.Pad 44 bytes, then append the little-endian packed address of win(). Send via pwntools to the remote service.pythonpython3 -c " from pwn import * elf = ELF('./vuln') win_addr = elf.symbols['win'] payload = b'A' * 44 + p32(win_addr) p = remote('saturn.picoctf.net', <PORT_FROM_INSTANCE>) p.sendlineafter(b'Please enter your string:', payload) print(p.recvall().decode()) "What didn't work first
Tried: Test the payload locally with ./vuln and see it print the flag, then send the same payload to the remote and get no output.
The binary reads the flag from a flag.txt in the current directory. Remotely, win() reads a flag.txt that only exists on that server. With no such file locally, the run prints nothing or errors while the binary still returns normally. The exploit mechanics are fine; the difference is a missing local file, not a wrong offset or address.
Tried: Use p64() instead of p32() to pack the win address because the system is 64-bit.
The binary is a 32-bit ELF, as the file command confirms. Even on a 64-bit host it runs in 32-bit mode: registers are 32 bits wide and the saved return address occupies exactly 4 bytes. p64() packs 8, overwriting 4 bytes past EIP, which trashes memory beyond the return address and crashes before win() is ever reached.
Learn more
pwntools is the standard Python library for binary exploitation CTFs.
ELF()parses the binary and lets you look up symbol addresses withelf.symbols['name'].p32(addr)packs the address as a 4-byte little-endian integer, which is how x86 stores multi-byte values in memory.Little-endian means the least-significant byte is stored at the lowest address. So the address
0x0804930fbecomes the bytes\x0f\x93\x04\x08in the payload. This matches how x86 reads values off the stack when it loads the return address into EIP.remote()opens a TCP connection.sendlineafter(prompt, data)blocks untilpromptappears on the wire, then sendsdata+ newline.recvall()reads until the peer closes - which is whenwin()finishes and the process exits.Watch out for prompt mismatches. If the binary's prompt string differs from
b'Please enter your string:'by even one character,sendlineafterhangs forever. Verify the prompt locally first: run the binary by hand, copy the exact byte sequence (including punctuation and trailing space). When in doubt usestrings vuln | grep -i enterorstrace -e trace=write ./vulnto see what gets written to stdout. More tactics in pwntools for CTF.
Interactive tools
- Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
- 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{addr3ss3s_ar3_3asy_c1...}
Overwrite the 32-bit saved return address (EIP) at offset 44 with the address of win(). pwntools automates packing and delivery.