Challenge Setup
This writeup follows a 32-bit Linux ELF binary compiled with a vulnerablegets() call. The same concepts apply to 64-bit binaries and strcpy(); differences are called out explicitly where they matter.
Start with your recon commands:
win() function that the binary already contains.
Finding the Vulnerability
Open the binary in GDB or a decompiler. The vulnerable code typically looks like one of these patterns:sub esp, 0xNN instruction at the function prologue — that tells you how much stack space is reserved for locals. The buffer starts somewhere in that space, and the saved return address sits just above it (at a higher address on x86, meaning it’s reached by writing past the end of the buffer).
Determining the Offset
You need to know exactly how many bytes of padding to send before your controlled return address begins. The fastest way is a cyclic pattern — a non-repeating sequence where every 4-byte (or 8-byte) chunk is unique, so you can look up the crash address and immediately know the offset.1
Generate and send the pattern
In pwntools, Or do it interactively in GDB:
cyclic(200) generates 200 bytes of De Bruijn sequence. Send it as input and let the program crash.2
Calculate the offset from the crash address
Feed the crashed EIP value back into For 64-bit binaries, use
cyclic_find():cyclic_find(crashed_rip, n=8). If GDB shows only a partial overwrite (e.g., 0x00007f6161616166), strip the upper bytes first.3
Verify the offset
Confirm by sending exactly
offset A-bytes followed by four B-bytes and checking that EIP lands on 0x42424242:Technique 1 — Ret2Win (Jump to a Hidden Function)
Many CTF challenges include awin() function — never called by normal program flow — that either prints the flag directly or spawns a shell. All you need to do is overwrite the return address with win’s address.
1
Find the win function address
2
Build and send the payload
p32() packs the address as a 4-byte little-endian integer, matching x86 memory layout. Use p64() for 64-bit binaries.3
Run against remote
Swap
process('./challenge') for remote('challenge.ctf.site', 1337) and send the same payload. Remote offsets and addresses are identical as long as the binary matches.Technique 2 — Ret2Libc (64-bit, NX Enabled)
When there’s nowin() function and NX is enabled (you can’t run shellcode), you pivot to ret2libc: call system("/bin/sh") using the libc that the binary already loads. On 64-bit, this requires a two-stage exploit — first leak a libc address to defeat ASLR, then make the actual system() call.
On 64-bit x86-64, function arguments are passed in registers (
rdi, rsi, rdx, …), not on the stack. To call system("/bin/sh"), you must control rdi. You do this with a pop rdi; ret gadget — see the ROP Chains writeup for full detail. The pwntools ROP class handles this automatically.1
Stage 1 — Leak a libc address via puts
Call After sending, read the leaked address:
puts(puts@got) to print the runtime address of puts in libc, then return to main so you get a second input opportunity.2
Stage 2 — Call system('/bin/sh')
With
libc_base known, compute the runtime addresses of system and the /bin/sh string. Because system is a raw integer address (not a named symbol in the binary), build the payload manually with flat() so that the pop rdi gadget is inserted explicitly — this guarantees rdi is set to the /bin/sh pointer before system is called:GDB Workflow Reference
These are the GDB commands you reach for most often during a buffer overflow investigation:Complete Exploit Reference
- Ret2Win (32-bit)
- Ret2Libc (64-bit)