Example 19-6 shows a full implementation of the findSymbolByHash function that can be used to find exported symbols in
loaded DLLs.
Example 19-6. findSymbolByHash implementation
; __stdcall DWORD findSymbolByHash(DWORD dllBase, DWORD symHash);
findSymbolByHash:
pushad
mov ebp, [esp + 0x24] ; load 1st arg: dllBase
mov eax, [ebp + 0x3c] ❶ ; get offset to PE signature
; load edx w/ DataDirectories array: assumes PE32
mov edx, [ebp + eax + 4+20+96] ❷
add edx, ebp ; edx:= addr IMAGE_EXPORT_DIRECTORY
mov ecx, [edx + 0x18] ❸ ; ecx:= NumberOfNames
mov ebx, [edx + 0x20] ; ebx:= RVA of AddressOfNames
add ebx, ebp ; rva->va
.search_loop:
jecxz .error_done ; if at end of array, jmp to done
dec ecx ; dec loop counter
; esi:= next name, uses ecx*4 because each pointer is 4 bytes
mov esi, [ebx+ecx*4]
add esi, ebp ; rva->va
push esi
call hashString ❹ ; hash the current string
; check hash result against arg #2 on stack: symHash
cmp eax, [esp + 0x28] ❺
jnz .search_loop
; at this point we found the string in AddressOfNames
mov ebx, [edx+0x24] ; ebx:= ordinal table rva
add ebx, ebp ; rva->va
; turn cx into ordinal from name index.
; use ecx*2: each value is 2 bytes
mov cx, [ebx+ecx*2] ❻
mov ebx, [edx+0x1c] ; ebx:= RVA of AddressOfFunctions
add ebx, ebp ; rva->va
; eax:= Export function rva. Use ecx*4: each value is 4 bytes
mov eax, [ebx+ecx*4] ❼
add eax, ebp ; rva->va
jmp near .done
.error_done:
xor eax, eax ; clear eax on error
.done:
mov [esp + 0x1c], eax ❽ ; overwrite eax saved on stack
popad
retn 8The function takes as arguments a pointer to the base of the DLL and a 32-bit hash value
that corresponds to the symbol to find. It returns the pointer to the requested function in register
EAX. Remember that all addresses in a PE file are stored as RVAs, so code needs to continuously add
the dllBase value (kept in register EBP in this example) to the
RVAs retrieved from PE structures to create pointers it can actually use.
The code begins parsing the PE file at ❶ to get
the pointer to the PE signature. A pointer to IMAGE_EXPORT_DIRECTORY is created at ❷ by
adding the correct offset, assuming this is a 32-bit PE file. The code begins parsing the IMAGE_EXPORT_DIRECTORY structure at ❸, loading the NumberOfNames value and the AddressOfNames pointer. Each string pointer in AddressOfNames is passed to the hashString function at
❹, and the result of this calculation is compared
against the value passed as the function argument at ❺.
Once the correct index into AddressOfNames is found, it is
used as an index into the AddressOfNameOrdinals array at location
❻ to obtain the corresponding ordinal value, which is
used as an index into the AddressOfFunctions array at ❼. This is the value the user wants, so it is written to the stack
at ❽, overwriting the EAX value saved by the pushad instruction so that this value is preserved by the following
popad instruction.
Example 19-7 shows a complete Hello World shellcode
example that uses the previously defined findKernel32Base and
findSymbolByHash functions, instead of relying on hard-coded API
locations.
Example 19-7. Position-independent Hello World
mov ebp, esp
sub esp, 24h
call sub_A0 ❶ ; call to real start of code
db 'user32',0 ❷
db 'Hello World!!!!',0
sub_A0:
pop ebx ; ebx gets pointer to data
call findKernel32Base ❸
mov [ebp-4], eax ; store kernel32 base address
push 0EC0E4E8Eh ; LoadLibraryA hash
push dword ptr [ebp-4]
call findSymbolByHash ❹
mov [ebp-14h], eax ; store LoadLibraryA location
lea eax, [ebx] ❺ ; eax points to "user32"
push eax
call dword ptr [ebp-14h] ; LoadLibraryA
mov [ebp-8], eax ; store user32 base address
push 0BC4DA2A8h ❻ ; MessageBoxA hash
push dword ptr [ebp-8] ; user32 dll location
call findSymbolByHash
mov [ebp-0Ch], eax ; store MessageBoxA location
push 73E2D87Eh ; ExitProcess hash
push dword ptr [ebp-4] ; kernel32 dll location
call findSymbolByHash
mov [ebp-10h], eax ; store ExitProcess location
xor eax, eax
lea edi, [ebx+7] ; edi:= "Hello World!!!!" pointer
push eax ; uType: MB_OK
push edi ; lpCaption
push edi ; lpText
push eax ; hWnd: NULL
call dword ptr [ebp-0Ch] ; call MessageBoxA
xor eax, eax
push eax ; uExitCode
call dword ptr [ebp-10h] ; call ExitProcessThe code begins by using a call/pop at ❶ to obtain a pointer
to the data starting at ❷. It then calls findKernel32Base at ❸ to find
kernel32.dll and calls findSymbolByHash at
❹ to find the export in
kernel32.dll with the hash 0xEC0E4E8E. This is the ror-13-additive hash of the
string LoadLibraryA. When this function returns EAX, it will
point to the actual memory location for LoadLibraryA.
The code loads a pointer to the "user32" string at
❺ and calls the LoadLibraryA function. It then finds the exported function MessageBoxA at ❻ and calls it to display the
“Hello World!!!!” message. Finally, it calls ExitProcess to cleanly exit.