Vulnserver KSTET WS2_32 Recv Function Re-Use

Today, we’re going to be talking about function reuse within our shellcode. Specifically, we’ll be looking at the WS2_32.recv function and how we can use this function to read in our exploit payload and execute it.

Before we begin, I need to credit the original author / creator of this method. Like the QuickZip tutorial before it, I wanted to provide more context about how to do this method so that it’s simpler to follow. This is heavily based-on and inspired by:

We put the … at the end there as we get vulnserver to crash almost immediately on the first variable, second fuzzing iteration. Breaking down spike’s output:

Fuzzing Variable 0:1
Variablesize= 5004

This is telling us that we’re fuzzing the first variable, as most computer counting is zero-based, that’s why the first variable is referenced by the 0. On the other side of the colon, we see that it’s the second fuzzing iteration, represented by the 1. For the first thousand or so fuzzing executions, Spike will also give you the size of the fuzzing buffer that was sent. In this case, it sent a 5004 byte buffer. This can be seen in Immunity Debugger:

So it looks like Spike sent a request like:

KSTET /.:/AAAAAAAAA........

So probably 5000 A’s prefixed by the /.:/ characters.

Let’s duplicate this exploit as a Python proof-of-concept exploit.

0x02 – Proof-of-Concept Exploit

With knowledge of what the command is, we’ll start off by building our proof of concept exploit.

touch 02-poc.py
chmod +x 02-poc.py
nvim 02-poc.py

And with our file in place, it should look like this. Note that this is using Python 3, which has slightly different semantics regarding strings and their use with sockets. Specifically, our strings must be prefixed with b to indicate that they are byte strings, not character strings, which allows us to send the value over the socket.

When we run this, we see it execute:
root@kali:~/exploits/vulnserver/kstet/tutorial# ./02-poc.py
[*] creating the socket
[*] connecting to the target
[*] sending exploit
[*] cleaning up

And the following crash occurs:

Great! So we now have a working proof-of-concept exploit. Next step is to determine the type of exploit we have (vanilla EIP overwrite, structured exception handler overwrite, etc.) and what offset it occurs at.

0x03 – Determining Exploit Type and Control Offset

Looking at our image, we see that the EIP register has been overwritten by:

41414141

If you done exploitation before, you may know that a capital A in hex is the value 41. Thus, we know that in this case the overflow is an EIP overwrite exploit. So we need to figure out where this actually is within the 5000 byte buffer. Luckily, we can use Metasploit’s pattern_create.rb tool for this:

Here, we’re telling it to create a 5000 byte long unique cyclic pattern which we can use within our exploit to identify the exact offset of EIP. With our pattern in place, we end up with the following exploit (for vim users, take a look at the macro explanation for a way to format pattern_create.rb code)

This shows us that we have a working crash. We see that EIP is overwritten with our B’s, 0x42 hex, and ESP is directly after EIP. Oddly though, rather than our full 5000 bytes worth of space, we only have 20 bytes worth of space. We know this a few different ways:

Counting – each row on the stack shows four bytes, we have 5 rows, 4 * 5 = 20 bytes

As a result, it’s possible that we don’t actually need a 5000 byte buffer, we can potentially use a shorter and more targeted payload. Let’s try with only twenty bytes of C’s.

With this, we run it again and see we still get the same crash. Yay for simpler buffers!

Important Note: If you do not shorten the buffer at this point, you will hit a different issue when doing the recv command. The target socket will attempt to read in the next 512 bytes from the socket, which will be the C’s rather than your shellcode. This will cause your exploit to not work correctly, for obvious reasons (if it’s reading C’s, they don’t execute a shell…)

That works!

0x05 – Jumping to Our Buffer

Next step, getting to our buffer. Since this is a vanilla EIP overwrite, we’ll look for a JMP ESP command to get to our C’s. To do this, we can use Mona like so. Note the use of the -n flag to ignore modules which start with a null byte.

While we don’t know of any bad characters, I like to prefer ASCII characters for register values when possible. This doesn’t include SafeSEH, ASLR, or other protection methods for us to worry about, and it’s a DLL associated to vulnserver so it should be a portable exploit value.

Next, we update our exploit like so. If you aren’t familiar with the use of struct.pack to create return addresses, I’d recommend reading my article on using it as it can help you avoid typos.

When we update it, we end up with:

With this in mind, let’s restart vulnserver and Immunity Debugger. Once in Immunity, we’ll use the “Goto address in Disassembler” button to go to our JMP ESP instruction. This is the black arrow pointing at three dots. Usually it’s the last button before you get to the buttons that are letters. We’ll then set a breakpoint using F2.

When we run this, we hit our breakpoint:

We know this a breakpoint not a crash because of the “Breakpoint at essfunc.62501203” along the bottom status bar rather than an access violation or similar error.

0x06 – Getting to a Bigger Buffer

So right now we have access to our buffer, but we only have 20 bytes to work with. Not exactly spacious and it certainly won’t hold a reverse shell. As a result, we need to try to move to another location where we may have more space. The only other place we control though are the A’s above our EIP value. As a result, we need to look at jumping up there, but how?

At this point, we just took a JMP ESP command, meaning EIP currently points to the same location as ESP. We also know that 66 bytes of junk and 4 bytes of EIP above where we are would put us at the beginning of our buffer. So lets try to move 66 bytes up. We’ll do this via hex math using the registers and then performing a JMP .

Let’s take a look at how to do this.

First, where are we?

00B7FA0C 43 INC EBX

We’re currently at 00B7FA0C in memory. And the first byte of our buffer is where?

Note how in this example we used SUB DL, 70 rather than 46. This is because nasm_shell assumes values are decimal unless formatted like so:

nasm > SUB DL, 0x46
00000000 80EA46 sub dl,0x46

With this information, we can now update our exploit if we wanted to. But this is a lot of instructions for simply jumping backwards 70 bytes. Instead, we’ll use a short jump of:

\xEB\xB8

This will allow us to perform a jump of -72 bytes. You may be wondering why we need to do a -72 byte jump here when before we needed to only do a 70 byte jump back. Simply put, the first method using a register didn’t account for any instructions after we took the jump, as the first instruction preserved the address of where we were. In this case, we are using a two byte jump, and the jump will occur after those two bytes are executed. Thus, rather than a 70 byte jump, we must do a 72 byte jump.

There is no meaningful or functional difference in doing it this way, just nice to make things shorter when we can. This leaves us with:

We restart Immunity, re-set our breakpoint on the JMP ESP command, then execute the exploit and take the jump using F7. We then land at our jump:

Which when we take, we end up at the first byte of our buffer.

0x07 – Finding and Understanding the WS2_32.recv Command

Now that we are in our (slightly) larger buffer, it’s time to dig into the meat of this, which is the use of the recv command to read more data from the socket into memory which we will then execute. We’ll be using the WS2_32.recv command to do this, which is already loaded by vulnserver (and is how we send our commands to the server).

So how does WS2_32.recv work? If we look at the MSDN documentation (located here) we see the following:

int recv(
SOCKET s,
char *buf,
int len,
int flags
);

So the recv command takes four arguments: a socket file descriptor, a pointer to a buffer where the data will be stored, the length of the buffer, and a set of flags, which we won’t be using. You can read about them on MSDN though.

So we’re going to need to find our socket file descriptor so that we can re-use it. We also need to know where WS2_32.recv is located on our machine. Immunity has a way to help us with the second part, which allow us to determine the first (the socket file descriptor).

So first things first, where does vulnserver call WS2_32.recv? Well, we know that it’s going to be in the application itself somewhere. So it’ll either be in the vulnserver module or it’s DLL essfunc.dll. Luckily, this is a small application so the number of places it could be isn’t very large. Let’s check if it’s in vulnserver first.

Restart vulnserver and Immunity Debugger, and then attach the debugger to the process. Then right click in the upper left pane, highlight View, and select “Module ‘vulnserver'”

This will take us to the section of code associated with vulnserver itself. We’d need to look at essfunc.dll separately. WS2_32.recv is a call to a different module, so we can look for intermodular calls by highlighting search for and selecting intermodular calls. Note though, that similar to the goto address in disassembler functionality, sometimes the previous view command will require you to select it a second time before taking you to the correct module.

This will show you a list of all the calls to other modules. We’ll click the header for the Destination column which will sort by function name. It’s important to note, this sorts by the function name, not the module name. For example, WS2_32.bind and WS2_32.recv will be sorted on the words bind and recv, respectively.

When we sort, we can then easily identify the recv call. We’ll set a breakpoint on this command using F2 (can be done from this window when the function is highlighted). You’ll see the address should now become highlighted.

While we have the location, let’s jump to this address in the disassembly (upper left pane) and grab what memory address is being called:

You can view this using the space key to bring up the Assemble pane, or directly below the disassembly is a small slide out windows (usually starts collapsed for some reason) which will show you what the address is as well.

Let’s see what the socket file descriptor is so that we can try to find it in relationship to where we are during our exploit (hopefully it’s a short relative offset).

To find this, let’s replace our buffer of A’s at the beginning with \xCC which is a breakpoint. This will simplify the process for us. For whatever reason, not doing this can leave situations where the socket file descriptor isn’t correctly kept.

When we do this, we first hit our recv breakpoint:

If we use F7, we can step into the receive call to view what the arguments look like when the call is executed. Because this is a 32-bit exploit, we can look at the stack to view the arguments, (64-bit is pass by register, 32-bit is pass by stack).

And here, we see the following information, in relationship to what we saw in the MSDN documentation:

So our socket file descriptor we’ll want to look for when within our exploit is 0x00000080. Let’s allow execution to continue, when we should hit our breakpoint. We’ll press F9 to continue (you may need to restart immunity and unset the breakpoint on recv, sometimes this won’t allow you to continue. If that happens, rather than sending your exploit and continuing, use netcat to connect, then unset the recv breakpoint and run the exploit again).

0x08 – Finding the Socket File Descriptor

With the information about our socket, we’ll search for where this value may be located in relationship to where we our. We’ll right click on the upper left panel, Search for, Binary string.

We’ll then search for our socket file descriptor:

When we do this, we can cycle through the different results using Ctrl+L. We find three results:

So when we hit recv earlier, and when we called our exploit, the stack was located at:

00B7FA0C

Based on this, it seems the most likely to me that the 00B7FB93 would be the socket file descriptor, as it’s the closest to the stack location.

So if that’s the case, let’s make the calculations using ESP and ECX. First, we need to know how far apart our target and our current locations are. We use whichever is the highest number and subtract the smaller one to get the offset.

We can determine the opcodes for this by using Immunity Debugger’s assemble option. When we hit our breakpoint, we’ll use the spacebar to open the window, then type in the commands we want to execute. When we’re done, we’ll have this:

We can then use F7 to step through these commands and ensure they work as expected, then we can copy the lines to our clipboard and update our exploit. It’s worth noting at this stage, we’re considering the file descriptor value to be an approximation which may be off by 3 bytes forward or backwards. We’ll need 0x00000080, while we may accidentally end up with 0x00800000 and need to shift it over a bit. Easy fix, but worth being aware of this potential caveat:

0x09 – Calling WS2_32.recv to Receive Our Shellcode

With our socket file descriptor, we need to take care of the last piece, which is actually calling the WS2_32.recv function.

If we remember, when we saw it called earlier, the values looked like this (in MSDN documentation order / style):

Because we’ll be passing our arguments via the stack, we need to push them in reverse order, meaning first the flags (0x00000000 from above), then the buffer size (0x1000 from above), the buffer location (*0x003E4B28 from above), and the socket file descriptor (0x00000080 from above, currently we have a pointer to it in ECX).

So let’s take care of the flags first:

XOR EDX, EDX ; Zero out EDX
PUSH EDX ; PUSH 0X00000000

This will give us the first value. Second, we need the length of the buffer. This will be 512 bytes, or 0x00000200 hex. We can use the upper half of the lower 16-bytes of the register via the DH reference. If you aren’t familiar with this, and it sounds confusing, DL is the least significant byte of DX, and DH is the most significant byte of DX. DX in turn is the least significant word of EDX.

Next, we need to decide on our buffer location. Interestingly, this is the start of the buffer location, and grows down. In our case, we’ll choose to receive the data at

00B7F9E2 CC INT3

This gives us a bit of space (22 bytes), but we’ll need to make sure we chose the right spot once we convert our assembly into opcodes.

With this knowledge though, we can calculate this value by getting the value of ESP (since it’s two op codes will keep the amount of op codes we need to do smaller), doing some math, and then pushing the value back onto the stack.

And finally, we need to push our socket file descriptor. We have the memory address of it stored, but not the actual value (as if we stored that, it’d be hardcoded and potentially break). We can push the value of at the stored memory address using:

PUSH DWORD PTR DS:[ECX] ; Push a double-word of the value that ECX points to

And with this, we should have our full set of operations codes. All together, this looks like:

0x10 – Fixing our Mistakes

So at this point, theoretically we’re ready to start receiving stuff, but as we said some things like the socket pointer may be off by a few bytes. We need to verify before we just start trying to pop shells. You may have noticed from the above, we made four mistakes that we’ll look at fixing below:

Socket is at the wrong offset causing socket 8000 rather than 80

Our buffer value is wrong

The call instruction wasn’t sent in a reliable manner

Our arguments are being put on the stack in our line of execution in an area which will get overwritten by our buffer

To do this, set a breakpoint on your JMP ESP command and run the latest prepared exploit then step through the commands until you hit the recv command, if you then press F7 one more time to step into the function, you’ll see something like this:

From this, we see two incorrect things, it’ll take a few more executions to see the third, that the CALL WS2_32.recv wasn’t put in correctly. First, the socket isn’t aligned correct, giving us an argument of 8000 rather than 80. Second, oddly, our buffer location seems to be placed at 0x00B7FADA rather than our expected 00B7F9E2. Let’s deal with the socket first though. In this case, we’re off alignment by one full byte. So we can adjust the ADD CX operation by one byte, making it ADD CX, 188 instead of 187. Since this is little endian, the issue here is that we sent an argument of:

00800000

rather than:

80000000

As such, adding one is how we’re getting the one more byte of zero’s on the right hand side. If we look up the new opcodes using the method we did before and re-run the exploit, that successfully fixed out socket file descriptor.

So with that in place, we just have to adjust our buffer. We’re currently ending up not only after our current buffer, but after our entire buffer. Not only that, the current stack location is below us, so our target location for the payload is also going to actually get in the way. To fix this, we need to make the following changes:

Move ESP above our shellcode related to arguments so that it doesn’t interfere

Call WS2_32.recv via a register with our address in it rather than directly provided values

First, let’s deal with ESP. We can move that by using SUB ESP, 50 right above our XOR. That’ll move the stack above where we are today. We’ll also fix the buffer by removing the SUB DL, 2A and we’ll instead put ADD EDX, 50. By using 50 here, after we’ve moved the stack up for the arguments, we’ll properly align with our stage 2 buffer without our EIP overwrite interfering.

Lastly, we need to deal with the call operation. We made the mistake of encoding that via Immunity, which doesn’t actually work the way you’d think. Instead, we have to put the CALL {{ address }} into a register and call the register. Sadly, our call address has a null byte!

To fix this, we need to use a shift. A shift can be used to adjust the address by dropping certain bytes. Specifically we want a SHR, or Shift Right. The second operand, we’ll be using 8, defines the amount of bits to be shifted right. So if we have:

SHR CL, 1

This means if we start with:

CL = 42(in hex) = 0100 0010 (binary)

Shift right one time, all the bits go right. The lowest significant bit(rightmost) goes to the carry flag and a zero is appended on the highest significant bit (leftmost). The value thus becomes :

0010 0001 (21 in hex) -> and the rightmost 0 goes to the carry flag.

In our case, we’re moving an entire register 8, dropping the entire lower byte, and gaining a full 00 byte at the top / most significant byte.

Author Kevin Kirsche

Kevin is a Principal Security Architect with Verizon. He holds the OSCP, OSWP, OSCE, and SLAE certifications. He is interested in learning more about building exploits and advanced penetration testing concepts.