Win32 Hacks: Loading API functions from a process' Process Environment Block (PEB)
Preliminary
Last time in part 1 we figured out how to find the base address of persistent DLLs via the PEB. By reading the DLL image at the base address, we can read its export directory table and get the address to its functions. That's what we're going to do now.
PE memory layout
At the DLL base address, the DLL image is almost laid out the same way as in the DLL file on disk. The only difference is that sections are aligned and padded to larger sizes than in the file, and not all sections get mapped to memory at runtime. The latest PE specification can be found here. At runtime, the layout looks like this:
- MSDOS header (starts with the magic value "MZ")
- MSDOS stub
- PE header
- Optional header
- Data directory table
- Section headers
- Section 1
- Section 2
- ...
- Section N
IMAGE_NT_HEADERS32 contains both the PE file header and the optional header.
Our goal is to get the address to a function inside the library given the function's name, and that information is found in the export directory table. The address to the export directory table is found in the data directory table.
Validating the headers
We need to validate the headers in case some of the required fields don't exist, or the image is corrupt for some reason.
void* slGetModuleProcAddress(void* moduleBase, LPCSTR procName){
//The MSDOS header starts directly at the first byte of the base address
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)moduleBase;
//MSDOS images start with Magic value 'M' 'Z'
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
//The field e_lfanew in the MSDOS header contains the
//PE header offset
PIMAGE_NT_HEADERS32 headers32 =
(PIMAGE_NT_HEADERS32)((char*)moduleBase + dosHeader->e_lfanew);
if (headers32->Signature != IMAGE_NT_SIGNATURE)
return NULL;
//A normal PE32 file has an optional header which is 96
//bytes long
//NumberOfRvaAndSizes holds the number of entries in the
//data directory table. We need at least one.
if (headers32->FileHeader.SizeOfOptionalHeader < 96 ||
headers32->OptionalHeader.NumberOfRvaAndSizes == 0)
return NULL;
DWORD EdtOffset =
headers32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!EdtOffset)
return NULL;
/* More code follows .. */
Pretty straight forward. The addresses and sizes of different tables are stored in the data directory table at specific positions. The size and address of the export directory table is located at index 0 of the data directory table. This is why NumberOfRvaAndSizes must be at least one. We checked all the magic values in the different headers, and we found the relative virtual address to the export directory table, which is EdtOffset. So what does the EDT look like?
Looking up a symbol from the Export Directory Table
typedef struct _EXPORT_DIRECTORY_TABLE {
DWORD ExportFlags;
DWORD TimeStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD NameRVA;
DWORD OrdinalBase;
DWORD ExportAddressTableSize;
DWORD NamePointerTableSize;
DWORD ExportAddressTableRVA;
DWORD NamePointerTableRVA;
DWORD OrdinalTableRVA;
} EXPORT_DIRECTORY_TABLE, *PEXPORT_DIRECTORY_TABLE;
The interesting fields here are NameRVA, ExportAddressTableRVA, OrdinalTableRVA and OrdinalBase. The RVAs are relative addresses to the address-, ordinal- and string tables respectively, while OrdinalBase is an offset to add to ordinals.
NameRVA is an array of null-terminated ISO-8859-1 strings, and it is lexicographically sorted. Finding a symbol in the list is therefore O(log n) and pretty fast. If the symbol you searched for is at index 'i' in the name table, the symbol's ordinal is found at index 'i' in the ordinal table via OrdinalTableRVA. You use the ordinal as an index into the export address table, and that gives you the function address.
But all of these fields are relative virtual addresses. How do we turn them into absolute virtual addresses? Well, we did it before when we set up the MSDOS and PE headers. They are relative to the DLL image base, so just add the image base address to them.
//slGetModuleProcAddress() from previous code snippet
//continues here.
//moduleBase + offset gives us the real VA to the EDT
PEXPORT_DIRECTORY_TABLE EdtPtr =
(PEXPORT_DIRECTORY_TABLE)((char*)moduleBase + EdtOffset);
//Again, add base address to all the RVAs
PVOID OrdinalTable = (PBYTE)moduleBase + EdtPtr->OrdinalTableRVA;
PVOID NamePointerTable = (PBYTE)moduleBase + EdtPtr->NamePointerTableRVA;
PVOID ExportAddressTable = (PBYTE)moduleBase + EdtPtr->ExportAddressTableRVA;
//We're lazy and do a linear search for the function name
//Real code should probably use binary search
for (DWORD i = 0; i < EdtPtr->NamePointerTableSize; i++){
DWORD NameRVA = ((PDWORD)NamePointerTable)[i];
const char* NameAddr = (char*)moduleBase + NameRVA;
//If string comparison fails, skip to next iteration
//Note that these strings are ISO-8859-1, not UTF-8
//or UTF-16/USC2!
if (slStrCompare(NameAddr, procName))
continue;
//The weird stuff with OrdinalBase is explained later
WORD Ordinal = ((PWORD)OrdinalTable)[i] + (WORD)EdtPtr->OrdinalBase;
WORD RealOrdinal = Ordinal - (WORD)EdtPtr->OrdinalBase;
DWORD ExportAddress = ((PDWORD)ExportAddressTable)[RealOrdinal];
void* FinalAddr = (char*)moduleBase + ExportAddress;
return FinalAddr;
}
return NULL;
}
In the code above, we start with computing the actual virtual addresses to all the tables. Then for simplicity's sake we do a linear search as opposed to binary search. When we find a string match in the string table, we use the current index to look up the ordinal, and the ordinal is used as lookup in the export address table. Finally, we add the image base to the export address to make a real virtual address.
But one thing remains to explain. What's up with the OrdinalBase variable? As it's used in the code, it looks completely superfluous.
According to Microsoft's COFF/PE specification, one is supposed to subtract OrdinalBase from the ordinal value prior lookup into the export address table. But in reality, the entries in the ordinal array is already offset with OrdinalBase. So if you want to compare ordinal values with other tools, you have to add OrdinalBase to the ordinal. In the code above Ordinal is the ordinal value you would compare with other programs, while RealOrdinal is the ordinal you use for lookup.
The final code combined with a simple example looks like this (structures omitted):
void* slGetModuleBase(LPCWSTR moduleName){
void* pebPtr = slGetPEB();
PPEB32 peb = (PPEB32)pebPtr;
PLIST_ENTRY moduleListTail = &peb->Ldr->InMemoryOrderModuleList;
PLIST_ENTRY moduleList = moduleListTail->Flink;
do {
char* modulePtrWithOffset = (char*)moduleList;
PLDR_DATA_TABLE_ENTRY module =
(PLDR_DATA_TABLE_ENTRY)modulePtrWithOffset;
if (!slWStrCompare(module->FullDllName.Buffer, moduleName)){
void* DllBase = module->Reserved2[0];
return DllBase;
}
moduleList = moduleList->Flink;
} while (moduleList != moduleListTail);
return NULL;
}
void* slGetModuleProcAddress(void* moduleBase, LPCSTR procName){
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)moduleBase;
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
PIMAGE_NT_HEADERS32 headers32 =
(PIMAGE_NT_HEADERS32)((char*)moduleBase + dosHeader->e_lfanew);
if (headers32->Signature != IMAGE_NT_SIGNATURE)
return NULL;
if (headers32->FileHeader.SizeOfOptionalHeader < 96 ||
headers32->OptionalHeader.NumberOfRvaAndSizes == 0)
return NULL;
DWORD EdtOffset =
headers32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!EdtOffset)
return NULL;
PEXPORT_DIRECTORY_TABLE EdtPtr =
(PEXPORT_DIRECTORY_TABLE)((char*)moduleBase + EdtOffset);
PVOID OrdinalTable = (PBYTE)moduleBase + EdtPtr->OrdinalTableRVA;
PVOID NamePointerTable = (PBYTE)moduleBase + EdtPtr->NamePointerTableRVA;
PVOID ExportAddressTable = (PBYTE)moduleBase + EdtPtr->ExportAddressTableRVA;
for (DWORD i = 0; i < EdtPtr->NamePointerTableSize; i++){
DWORD NameRVA = ((PDWORD)NamePointerTable)[i];
const char* NameAddr = (char*)moduleBase + NameRVA;
if (slStrCompare(NameAddr, procName))
continue;
WORD Ordinal = ((PWORD)OrdinalTable)[i] + (WORD)EdtPtr->OrdinalBase;
WORD RealOrdinal = Ordinal - (WORD)EdtPtr->OrdinalBase;
DWORD ExportAddress = 0;
ExportAddress = ((PDWORD)ExportAddressTable)[RealOrdinal];
void* FinalAddr = (char*)moduleBase + ExportAddress;
return FinalAddr;
}
return NULL;
}
//kernel32
typedef HMODULE (WINAPI *LOADLIBRARYWPROC)(LPCWSTR);
typedef BOOL (WINAPI *FREELIBRARYPROC)(HMODULE);
typedef PVOID (WINAPI *GETPROCADDRESSSPROC)(HMODULE, LPCSTR);
typedef VOID (WINAPI *EXITPROCESSPROC)(UINT);
//user32
typedef int (WINAPI *MESSAGEBOXWPROC)(HWND, LPCWSTR, LPCWSTR, UINT);
void main(void){
//kernel32
EXITPROCESSPROC slExitProcess = NULL;
LOADLIBRARYWPROC slLoadLibraryW = NULL;
FREELIBRARYPROC slFreeLibrary = NULL;
GETPROCADDRESSSPROC slGetProcAddress = NULL;
//user32
MESSAGEBOXWPROC slMessageBoxW = NULL;
HMODULE m;
void* kernel32 = slGetModuleBase(L"KERNEL32.DLL");
slExitProcess = slGetModuleProcAddress(kernel32, "ExitProcess");
slLoadLibraryW = slGetModuleProcAddress(kernel32, "LoadLibraryW");
slFreeLibrary = slGetModuleProcAddress(kernel32, "FreeLibrary");
slGetProcAddress = slGetModuleProcAddress(kernel32, "GetProcAddress");
m = slLoadLibraryW(L"USER32.DLL");
slMessageBoxW = slGetProcAddress(m, "MessageBoxW");
slMessageBoxW(0, L"SelfLoader Example", L"Test", MB_OK);
slFreeLibrary(m);
slExitProcess(0);
}
Hopefully it's clear now that we use this trick to only get a few functions from kernel32.dll, and let GetProcAddress() do the rest. Our program has no external dependencies at link time.