Introduction
A basic but core function of all Position Independent Code (PIC) for windows is to resolve the address of API functions at runtime. It’s an important task with a number of options available. Here, we’ll examine 2 popular methods using the Import Address Table (IAT) and Export Address Table (EAT) which are by far the most stable. (for this kind of code)
Since the release of Windows Vista in 2007, Address Space Layout Randomization (ASLR) is enabled for executables and dynamic link libraries specifically linked to be ASLR-enabled which mitigates exploitation of vulnerabilities. However, even long before ASLR arrived, virus writers over 20 years ago faced a similar problem with the unintentional “randomization” of the base address for kernel32.dll.
The first Windows virus called Bizatch was written by Quantum/VLAD on a beta copy of Windows 95. The virus used hardcoded API and as a result simply crashed on versions of windows that had a different base address for kernel32.dll. Mr. Sandman, Jacky Qwerty and GriYo discussed “the kernel32 problem” and “the GetModuleHandle solution” in PE infection under Win32 and weren’t aware of the Process Environment Block (PEB) under NT at the time which was discussed later by Ratter in Gaining important datas from PEB under NT boxes.. Jacky Qwerty published A GetProcAddress-alike utility which initially became a “standard” method of resolving API addressses in viruses.
At some point after this, authors started resolving the API by CRC32 checksum, presumably to hide strings of API in their code and also to reduce space. LethalMind showed in 1999 a way to resolve API using his own checksum in Retrieving API Addresses. Then of course LSD group proposed in 2002 their own ARX based algorithm in WIN32 Assembly components (shellcodes) which was the basis for many win32 shellcodes that followed.
That’s just a brief (potentially inaccurate) historical context of where most of the basic ideas for resolving API came from. Today of course, there are many more advanced challenges to overcome when exploiting vulnerabilities, but they are largely related to protection mechanisms and not what I’ll discuss here. All the structures displayed here can be found in WinNT.h from the Microsoft SDK which should be included with MSVC if you have it installed. You can find detailed description of PE/PE+ format here.
Image DOS Header
At the start of every PE file we find an MS-DOS executable or a “stub” that makes any PE file a valid MS-DOS executable.
The only field we need here is e_lfanew which when added to the current base address of module gives us a pointer to NT_IMAGE_HEADERS
// DOS .EXE header typedef struct _IMAGE_DOS_HEADER { WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Image NT Headers
Because the base address for mapped PE image in memory can be “random”, only the Relative Virtual Address (RVA) of important structures are saved in PE file.
To convert a RVA to Virtual Address (VA) we can use the following macro.
#define RVA2VA(type, base, rva) (type)((ULONG_PTR) base + rva)
Once we add e_lfanew to the base address, we then have a pointer to IMAGE_NT_HEADERS.
The following 2 structures are defined in WinNT.h but only one is used depending on architecture C code is compiled for.
We’re interested in the OptionalHeader field which contains among other things information about import and export directories.
typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64; typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Image Optional Header
At the end of Optional Header is an array of IMAGE_DATA_DIRECTORY structures.
// Directory Entries #define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
// // Optional header format. // typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Image Data Directory
Each directory holds a VA and size of directory. To access the export or import directory, simply add the VirtualAddress to base using RVA2VA macro.
// // Directory format. // typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
- VirtualAddress
- Size
RVA of the data structure. For example, if this structure is for import symbols, this field contains the RVA of the IMAGE_IMPORT_DESCRIPTOR array.
Contains the size in bytes of the data structure referred to by VirtualAddress.
Image Export Directory
Since exports are first in the list of directories, let’s examine this method of retrieval.
// // Export Format // typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
We’re interested in 5 fields.
- Name
- NumberOfNames
- AddressOfFunctions
- AddressOfNames
- AddressOfNameOrdinals
RVA of a string for DLL name.
The number of exported API by name.
RVA to array of RVAs. When each RVA is added to base address of module, they will give us the address of an exported API.
RVA to array of RVAs. When each RVA is added to base address of module, it will give us the address of a null terminated string representing an exported API.
RVA to array of ordinals. Each ordinal represents an index in AddressOfFunctions array.
The following function will retrieve an API address from the export table using CRC-32C of DLL and API name.
base parameter is obviously base address of DLL and hash is derived from the addition of 2 CRC-32C hashes. crc32c(DLL string) + crc32c(API string).
LPVOID search_exp(LPVOID base, DWORD hash) { PIMAGE_DOS_HEADER dos; PIMAGE_NT_HEADERS nt; DWORD cnt, rva, dll_h; PIMAGE_DATA_DIRECTORY dir; PIMAGE_EXPORT_DIRECTORY exp; PDWORD adr; PDWORD sym; PWORD ord; PCHAR api, dll; LPVOID api_adr=NULL; dos = (PIMAGE_DOS_HEADER)base; nt = RVA2VA(PIMAGE_NT_HEADERS, base, dos->e_lfanew); dir = (PIMAGE_DATA_DIRECTORY)nt->OptionalHeader.DataDirectory; rva = dir[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; // if no export table, return NULL if (rva==0) return NULL; exp = (PIMAGE_EXPORT_DIRECTORY) RVA2VA(ULONG_PTR, base, rva); cnt = exp->NumberOfNames; // if no api, return NULL if (cnt==0) return NULL; adr = RVA2VA(PDWORD,base, exp->AddressOfFunctions); sym = RVA2VA(PDWORD,base, exp->AddressOfNames); ord = RVA2VA(PWORD, base, exp->AddressOfNameOrdinals); dll = RVA2VA(PCHAR, base, exp->Name); // calculate hash of DLL string dll_h = crc32c(dll); do { // calculate hash of api string api = RVA2VA(PCHAR, base, sym[cnt-1]); // add to DLL hash and compare if (crc32c(api) + dll_h == hash) { // return address of function api_adr = RVA2VA(LPVOID, base, adr[ord[cnt-1]]); return api_adr; } } while (--cnt && api_adr==0); return api_adr; }
One important thing to mention is that this function does not resolve API by ordinal nor does it resolve forward references which can sometimes be a problem.
Here’s some assembly to perform the same thing.
; in: ebx = base of module to search ; ecx = hash to find ; ; out: eax = api address resolved in EAT ; search_expx: pushad ; eax = IMAGE_DOS_HEADER.e_lfanew mov eax, [ebx+3ch] ; first directory is export ; ecx = IMAGE_DATA_DIRECTORY.VirtualAddress mov ecx, [ebx+eax+78h] jecxz exp_l2 ; eax = crc32c(IMAGE_EXPORT_DIRECTORY.Name) mov eax, [ebx+ecx+0ch] add eax, ebx call crc32c mov [esp+_edx], eax ; esi = IMAGE_EXPORT_DIRECTORY.NumberOfNames lea esi, [ebx+ecx+18h] push 4 pop ecx ; load 4 RVA exp_l0: lodsd ; load RVA add eax, ebx ; eax = RVA2VA(ebx, eax) push eax ; save VA loop exp_l0 pop edi ; edi = AddressOfNameOrdinals pop edx ; edx = AddressOfNames pop esi ; esi = AddressOfFunctions pop ecx ; ecx = NumberOfNames sub ecx, ebx ; ecx = VA2RVA(NumberOfNames, base) jz exp_l2 ; exit if no api exp_l3: mov eax, [edx+4*ecx-4] ; get RVA of API string add eax, ebx ; eax = RVA2VA(eax, ebx) call crc32c ; generate crc32 of api string add eax, [esp+_edx] ; add crc32 of DLL string cmp eax, [esp+_ecx] ; found match? loopne exp_l3 ; --ecx && eax != hash jne exp_l2 ; exit if not found xchg eax, ebx xchg eax, ecx movzx eax, word [edi+2*eax] ; eax = AddressOfOrdinals[eax] add ecx, [esi+4*eax] ; ecx = base + AddressOfFunctions[eax] exp_l2: mov [esp+_eax], ecx popad ret
So that’s the basic method to search through exports. Now for the imports which is a little trickier.
Image Import Descriptor
The release of Enhanced Mitigation Experience Toolkit (EMET) by Microsoft in 2009 broke some existing shellcodes that searched the export directory for API.
EMET includes Export Address Table Access Filtering (EAF) and EAF+ since the release of 5.2, both of which serve to block read attempts of the export and import directories originating from modules commonly used to probe memory during the exploitation of vulnerabilities.
Typically, a shellcode using the IAT will resolve addresses for GetModuleHandle and GetProcAddress before resolving the rest by string.
If a PE file imports API from other modules, the import directory will contain an array of image import descriptors, each one representing a module.
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
The 3 fields we’re interested in are:
- OriginalFirstThunk
- Name
- FirstThunk
Contains offsets to the names of the imported functions.
Null terminated string of the module to import API from.
Contains offsets to the actual addresses of the functions.
Image Thunk Data
Each descriptor contains RVA that points to array of Image Thunk Data structures. Each entry represents information about the imported API.
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
In the code, I skip entries that are imported by ordinal.
The AddressOfData from OriginalFirstThunk is an RVA that points to an IMPORT_BY_NAME structure.
The Function field from FirstThunk points to actual address of API function we’re searching for.
Import By Name
Since we’re not importing by ordinal, we don’t care about the hint field, just the name which is null terminated API string.
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
- Hint
- Name
Contains an index into the export table of the DLL the function resides in. This field is for use by the PE loader so it can look up the function in the DLL’s export table quickly.This value is not essential and some linkers may set the value in this field to 0.
Contains the name of the import function. The name is an ASCIIZ string. Note that Name’s size is defined as byte but it’s really a variable-sized field. It’s just that there is no way to represent a variable-sized field in a structure. The structure is provided so that you can refer to the data structure with descriptive names.
The following code will search import address table for API address using CRC-32C hash of DLL and API strings.
LPVOID search_imp(LPVOID base, DWORD hash) { DWORD dll_h, i, rva; PIMAGE_IMPORT_DESCRIPTOR imp; PIMAGE_THUNK_DATA oft, ft; PIMAGE_IMPORT_BY_NAME ibn; PIMAGE_DOS_HEADER dos; PIMAGE_NT_HEADERS nt; PIMAGE_DATA_DIRECTORY dir; PCHAR dll; LPVOID api_adr=NULL; dos = (PIMAGE_DOS_HEADER)base; nt = RVA2VA(PIMAGE_NT_HEADERS, base, dos->e_lfanew); dir = (PIMAGE_DATA_DIRECTORY)nt->OptionalHeader.DataDirectory; rva = dir[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; // if no import table, return if (rva==0) return NULL; imp = (PIMAGE_IMPORT_DESCRIPTOR) RVA2VA(ULONG_PTR, base, rva); for (i=0; api_adr==NULL; i++) { if (imp[i].Name == 0) return NULL; dll = RVA2VA(PCHAR, base, imp[i].Name); dll_h = crc32c(dll); rva = imp[i].OriginalFirstThunk; oft = (PIMAGE_THUNK_DATA)RVA2VA(ULONG_PTR, base, rva); rva = imp[i].FirstThunk; ft = (PIMAGE_THUNK_DATA)RVA2VA(ULONG_PTR, base, rva); for (;; oft++, ft++) { if (oft->u1.Ordinal == 0) break; // skip import by ordinal if (IMAGE_SNAP_BY_ORDINAL(oft->u1.Ordinal)) continue; rva = oft->u1.AddressOfData; ibn = (PIMAGE_IMPORT_BY_NAME)RVA2VA(ULONG_PTR, base, rva); if ((crc32c(ibn->Name) + dll_h) == hash) { api_adr = (LPVOID)ft->u1.Function; break; } } } return api_adr; }
The assembly follows same alogorithm above but with some optimizations.
; in: ebx = base of module to search ; ecx = hash to find ; ; out: eax = api address resolved in IAT ; search_impx: xor eax, eax ; api_adr = NULL pushad ; eax = IMAGE_DOS_HEADER.e_lfanew mov eax, [ebx+3ch] add eax, 8 ; add 8 for import directory ; eax = IMAGE_DATA_DIRECTORY.VirtualAddress mov eax, [ebx+eax+78h] test eax, eax jz imp_l2 lea ebp, [eax+ebx] imp_l0: mov esi, ebp ; esi = current descriptor lodsd ; OriginalFirstThunk +00h xchg eax, edx ; temporarily store in edx lodsd ; TimeDateStamp +04h lodsd ; ForwarderChain +08h lodsd ; Name +0Ch test eax, eax jz imp_l2 ; if (Name == 0) goto imp_l2; add eax, ebx call crc32c mov [esp+_edx], eax lodsd ; FirstThunk mov ebp, esi ; ebp = next descriptor lea esi, [edx+ebx] ; esi = OriginalFirstThunk + base lea edi, [eax+ebx] ; edi = FirstThunk + base imp_l1: lodsd ; eax = oft->u1.Function, oft++; scasd ; ft++; test eax, eax ; if (oft->u1.Function == 0) jz imp_l0 ; goto imp_l0 js imp_l1 ; oft->u1.Ordinal & IMAGE_ORDINAL_FLAG lea eax, [eax+ebx+2] ; oft->Name_ call crc32c ; get crc of API string add eax, [esp+_edx] ; eax = api_h + dll_h cmp [esp+_ecx], eax ; found match? jne imp_l1 mov eax, [edi-4] ; ft->u1.Function imp_l2: mov [esp+_eax], eax popad ret
Process Environment Block
Another “advancement” arrived with the publication of Gaining important datas from PEB under NT boxes by Ratter/29A in 2002. There was a better way to obtain base address of KERNEL32.DLL simply by reading it from the PEB.
LPVOID get_api (DWORD dwHash) { PPEB peb; PPEB_LDR_DATA ldr; PLDR_DATA_TABLE_ENTRY dte; LPVOID api_adr=NULL; #if defined(_WIN64) peb = (PPEB) __readgsqword(0x60); #else peb = (PPEB) __readfsdword(0x30); #endif ldr = (PPEB_LDR_DATA)peb->Ldr; // for each DLL loaded for (dte=(PLDR_DATA_TABLE_ENTRY)ldr->InLoadOrderModuleList.Flink; dte->DllBase != NULL && api_adr == NULL; dte=(PLDR_DATA_TABLE_ENTRY)dte->InLoadOrderLinks.Flink) { #ifdef IMPORT api_adr=search_imp(dte->DllBase, dwHash); #else api_adr=search_exp(dte->DllBase, dwHash); #endif } return api_adr; } #endif
The assembly is purely based on same algorithm but with some minor optimizations.
; LPVOID get_apix(DWORD hash); get_apix: _get_apix: pushad mov ecx, [esp+32+4] ; ecx = hash push 30h pop eax mov eax, [fs:eax] ; eax = (PPEB) __readfsdword(0x30); mov eax, [eax+0ch] ; eax = (PPEB_LDR_DATA)peb->Ldr mov edi, [eax+0ch] ; edi = ldr->InLoadOrderModuleList.Flink jmp gapi_l1 gapi_l0: call search_expx test eax, eax jnz gapi_l2 mov edi, [edi] ; edi = dte->InLoadOrderLinks.Flink gapi_l1: mov ebx, [edi+18h] ; ebx = dte->DllBase test ebx, ebx jnz gapi_l0 xchg eax, ebx gapi_l2: mov [esp+_eax], eax popad ret
Hash algorithm
For both examples, I use CRC-32C checksum. The C stands for Castagnoli polynomial. I’ve used it simply because there were no collisions for 80,000 API tested. Some existing hash algorithms provide “good enough” results but the advantage of using CRC-32C is that it is now supported by INTEL cpus since the release of SSE4.2
It should be clear however that the OR operation of bytes with 0x20 is not part of the CRC-32C specification. This is only here to convert strings to lowercase before hashing. Sometimes kernel32.dll can appear as uppercase so it should be converted to lowercase.
In the Metasploit code, the module is converted to uppercase instead.
uint32_t crc32c(const char *s) { int i; uint32_t crc=0; do { crc ^= (uint8_t)(*s++ | 0x20); for (i=0; i<8; i++) { crc = (crc >> 1) ^ (0x82F63B78 * (crc & 1)); } } while (*(s - 1) != 0); return crc; }
Here’s the code using built in instruction.
; xor eax, eax cdq crc_l0: lodsb or al, 0x20 crc32 edx, al cmp al, 0x20 jne crc_l0
Here’s code for CPUs without the support for SSE4.2
; in: eax = s ; out: crc-32c(s) ; crc32c: pushad xchg eax, esi ; esi = s xor eax, eax ; eax = 0 cdq ; edx = 0 crc_l0: lodsb ; al = *s++ | 0x20 or al, 0x20 xor dl, al ; crc ^= c push 8 pop ecx crc_l1: shr edx, 1 ; crc >>= 1 jnc crc_l2 xor edx, 0x82F63B78 crc_l2: loop crc_l1 sub al, 0x20 ; until al==0 jnz crc_l0 mov [esp+_eax], edx popad ret
Of course, CRC-32C is not collision resistant. In some cases, you might need to consider using a cryptographic hash algorithm. The smallest I can think of would be CubeHash by Daniel Bernstein.
Although, you could also use a tiny block or stream cipher to encrypt the strings and truncate the ciphertext to 32 or 64-bits. Not sure how collision resistant that would be but it’s worth exploring.
Summary
Parsing the import and export tables isn’t a really difficult task. With all the sources and documentation available, there’s really no excuse to avoid using either in a PIC. Using hardcoded API or looking up by ordinal are recipe for a disaster.
By writing your code in C first and generating assembly output with /FAs switch of MSVC, this should make parsing in assembly much easier to understand.
Pingback: Shellcode: Multimode PIC for x86 (Reverse and Bind Shells for Windows) | modexp
Pingback: Shellcode: Fido and how it resolves GetProcAddress and LoadLibraryA | modexp
Pingback: Process Injection: Writing the payload | modexp
Pingback: Cobalt Strike 3.14 – Post-Ex Omakase Shimasu | Strategic Cyber LLC
Pingback: Cobalt Strike 3.14 – Post-Ex Omakase Shimasu
Pingback: Detect API hashing with YARA - 0xC0DECAFE.com