Understanding and Parsing PE Export Tables
Overview of PE Export Tables
In Win32 environments, PE (Portable Executable) files often consist of multiple modules including executable files (.exe) and dynamic link libraries (.dll). When an .exe file uses functions exported from a .dll, the import table records information about these external dependencies.
Conversely, export tables document which functions a given PE module makes available for use by other modules. While executables typically don't expose exports for security reasons, DLLs commonly provide both export and import tables to enable bidirectional function sharing.
Locating Export Tables
The export table can be found through the optional PE header's data directory. Specifically, the first entry in this directory array points to the export table via its Relative Virtual Address (RVA) and size fields. Converting this RVA to a File Offset Address (FOA) reveals the actual location within the file's sections.
Export Table Structure
struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // Reserved, not used
DWORD TimeDateStamp; // Timestamp of compilation
WORD MajorVersion; // Version info (unused)
WORD MinorVersion; // Version info (unused)
DWORD Name; // RVA to module name string
DWORD Base; // Starting ordinal number
DWORD NumberOfFunctions; // Total count of exported functions
DWORD NumberOfNames; // Count of named exports
DWORD AddressOfFunctions; // RVA to function address table
DWORD AddressOfNames; // RVA to function name table
DWORD AddressOfNameOrdinals;// RVA to ordinal table
};
Note that the reported size may exceed 40 bytes due to additional sub-tables referenced by pointers within the main structure.
Key Export Table Fields Explained
Name Field: Points to a null-terminated ASCII string containing the module's filename (e.g., "kernel32.dll").
Base Field: Represents the lowest ordinal value among all exported functions. For instance, if ordinals include 14, 6, 10, and 8, then Base equals 6.
NumberOfFunctions: Reflects total exported entries calculated as (maximum_ordinal - minimum_ordinal + 1). Gaps in ordinal sequences will result in higher counts than actual defined functions.
NumberOfNames: Counts only those exports assigned explicit names during definition. Functions marked with NONAME are excluded.
AddressOfFunctions: References a table storing RVAs for every exported function. Each slot corresponds to an ordinal relative to the base:
| Index | Ordinal | Function RVA |
|---|---|---|
| 0 | Base+0 | 0x12345678 |
| 1 | Base+1 | 0x00000000 |
| 2 | Base+2 | 0xABCDEF00 |
Empty slots contain zero values indicating unused ordinals.
AddressOfNames: Points to a sorted list (by ASCII value) of RVAs pointing to function name strings. Only named exports appear here, excluding any NONAME entries.
AddressOfNameOrdinals: Contains two-byte relative ordinals corresponding positionally to entries in AddressOfNames. To get the actual ordinal: relative_ordinal + Base = true_ordinal.
Retrieving Function Addresses
Windows provides GetProcAddress() for retrieving addresses:
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
Two methods exist for lookup:
By Name Lookup Process
- Convert AddressOfNames RVA → FOA to access name table
- Iterate comparing target name against each pointed-to string until match found at index
i - Use same index
ion AddressOfNameOrdinals to retrieve relative ordinaln - Access element
[n]in AddressOfFunctions to obtain function's RVA
Example: Searching for "mul" finds it at name_table[2], whose corresponding ordinal_table[2] yields 0x0004. Then function_address_table[4] gives the final RVA.
By Ordinal Lookup Process
- Calculate relative index:
given_ordinal - Base = i - Retrieve RVA directly from function_address_table[i]
For example, finding ordinal 17 where Base=13 means accessing function_address_table[4]. Note that ordinal-based searches bypass the ordinal/name mapping tables entirely.
Traversing Export Tables
void EnumerateExports(char* buffer) {
PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)(buffer + dosHdr->e_lfanew);
PIMAGE_OPTIONAL_HEADER optHdr = &ntHdr->OptionalHeader;
PIMAGE_DATA_DIRECTORY exportDir = &optHdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (exportDir->VirtualAddress == 0) {
printf("No export table present\n");
return;
}
PIMAGE_EXPORT_DIRECTORY exportTbl = (PIMAGE_EXPORT_DIRECTORY)(ConvertRvaToFoa(exportDir->VirtualAddress, buffer) + buffer);
char* moduleName = (char*)(ConvertRvaToFoa(exportTbl->Name, buffer) + buffer);
printf("Module Name: %s\n", moduleName);
printf("Export Table Offset: %08X\n", ConvertRvaToFoa(exportDir->VirtualAddress, buffer));
printf("Base Ordinal: %08X\n", exportTbl->Base);
printf("Total Exports: %08X\n", exportTbl->NumberOfFunctions);
printf("Named Exports: %08X\n", exportTbl->NumberOfNames);
PDWORD funcAddrTbl = (PDWORD)(ConvertRvaToFoa(exportTbl->AddressOfFunctions, buffer) + buffer);
PDWORD namePtrTbl = (PDWORD)(ConvertRvaToFoa(exportTbl->AddressOfNames, buffer) + buffer);
PWORD ordinalIdxTbl = (PWORD)(ConvertRvaToFoa(exportTbl->AddressOfNameOrdinals, buffer) + buffer);
for (DWORD idx = 0; idx < exportTbl->NumberOfFunctions; ++idx) {
if (funcAddrTbl[idx] == 0) continue;
DWORD currentOrdinal = exportTbl->Base + idx;
BOOL hasName = FALSE;
for (DWORD nameIdx = 0; nameIdx < exportTbl->NumberOfNames; ++nameIdx) {
if (ordinalIdxTbl[nameIdx] == idx) {
char* funcName = (char*)(ConvertRvaToFoa(namePtrTbl[nameIdx], buffer) + buffer);
printf("Ordinal: %X Address: 0x%08X Name: [%s]\n", currentOrdinal, funcAddrTbl[idx], funcName);
hasName = TRUE;
break;
}
}
if (!hasName) {
printf("Ordinal: %X Address: 0x%08X Name: [NULL]\n", currentOrdinal, funcAddrTbl[idx]);
}
}
}