How Injected Code Finds Exported Functions
By Gal Badishi | September 13, 2012
Sometimes, you find yourself writing code that’s going to get injected into another process. Assuming we’re not talking about DLL injection, where everything is nice and clean, your code needs to operate in an unfamiliar environment. This may happen if you’re writing a payload for an exploit, if you’re using WriteProcessMemory to inject your code, or even if you’re writing a sophisticated packer. Your code will probably need to find the memory addresses of some Windows API functions to execute properly. How can it do that?
The simplest solution is to hardcode the functions’ addresses in the call/jmp statements in your code. This technique was prominent in early exploitation of Windows XP systems, and is still viable today on Windows XP. There are two problems with hardcoded addresses:
- Every patch applied to the system might change some functions’ addresses. Thus, the hardcoded address is relevant only for a particular patch level (or a set thereof).
- On systems employing ASLR (e.g., Windows 7), hardcoded addresses simply won’t work.
A better solution is to use a combination of LoadLibrary and GetProcAddress to dynamically locate the address of the function we want to import. However, even that requires us to have the addresses of LoadLibrary and GetProcAddress, so we need to find a way of doing so. Of course, if we develop a way of finding addresses of API functions, we’re not restricted to using it only for LoadLibrary and GetProcAddress.
Fortunately, we can use the PEB (accessible through the TIB) to find the list of loaded modules through the PEB_LDR_DATA structure. The official documentation for PEB_LDR_DATA is missing some information, so to get the complete picture you should also read the unofficial documentation. The PEB_LDR_DATA struct contains three doubly-linked lists, InLoadOrder, InMemOrder, and InInitOrder, each of them pointing to a struct we call LDR_MODULE. This latter struct contains information on the loaded module, including its name and base address.
Some things to note:
- The order of the modules in each list might not be the same (otherwise, there’s no need for three lists). The first module in InInitOrder is ntdll.dll, so in our case it’s safe to skip it.
- Each pointer in the list points to a different offset in LDR_MODULE (the offset of the corresponding LIST_ENTRY for that list). Therefore, offsets in LDR_MODULE shouldn’t be calculated relative to LDR_MODULE’s starting address, but rather relative to the appropriate LIST_ENTRY struct’s starting address.
- Unicode strings are composed of 8 bytes: 2 bytes for the string’s length, 2 bytes for the string’s maximum length, and 4 bytes for the address of the actual string.
Here’s an illustration of the important relationships we talked about:
Since virtually every program loads kernel32.dll, and kernel32.dll exports both LoadLibrary and GetProcAddress, we can simply walk one of the modules’ lists, find kernel32.dll’s base address, and follow the PE structure to go to the export table and get the address of, say, GetProcAddress. Here’s some code that does just that (note that it’s not optimized):
; Find kernel32.dll (note that we skip the first entry – ntdll.dll)
MOV EBX,DWORD PTR FS:[EBX+18] ; Get TIB address (can skip this)
MOV EBX,DWORD PTR DS:[EBX+30] ; Get PEB address
MOV EBX,DWORD PTR DS:[EBX+C] ; Get LDR
MOV EBX,DWORD PTR DS:[EBX+1C] ; EBX = InInitOrder list
PUSH 6C0065 ; Push "kernel" in Unicode onto the stack
MOV EBX,DWORD PTR DS:[EBX] ; Go to the next module (InInitOrder)
MOV ESI,ESP ; ESI = "kernel" (Unicode)
MOV EDI,DWORD PTR DS:[EBX+20] ; EDI = Module's name (Unicode)
REPE CMPS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]
JNZ SHORT find_kernel32_dll
; Find kernel32.dll's exported function GetProcAddress
MOV EBX,DWORD PTR DS:[EBX+8] ; EBX = kernel32.dll's base address
MOV EDX,DWORD PTR DS:[EBX+3C] ; PE header
MOV EDX,DWORD PTR DS:[EDX+EBX+78] ; EDX = kernel32.dll's export directory (RVA)
MOV EAX,DWORD PTR DS:[EDX+EBX+1C] ; Address of exported funcs (EAX = EAT (RVA))
MOV EDX,DWORD PTR DS:[EDX+EBX+20] ; EDX = Address of function names (RVA)
ADD EDX,EBX ; EDX = Address of function names (VA)
ADD EAX,EBX ; EAX = EAT (VA)
CALL get_exports ; Put address of string on stack
"GetProcAddress" ; No null termination
MOV ESI,DWORD PTR SS:[ESP] ; "GetProcAddress"
MOV EDI,DWORD PTR DS:[EDX]
ADD EDI,EBX ; Exported function name
REPE CMPS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
JE SHORT found_set_context ; Jump if this is our function
ADD EDX,4 ; Next function name
ADD EAX,4 ; Next function address
JMP SHORT get_exports
MOV EDX,DWORD PTR DS:[EAX]
ADD EDX,EBX ; EDX = address of GetProcAddress
; We can now call GetProcAddress using a simple "call EDX"