Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding and Parsing PE Export Tables

Tech 1

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

  1. Convert AddressOfNames RVA → FOA to access name table
  2. Iterate comparing target name against each pointed-to string until match found at index i
  3. Use same index i on AddressOfNameOrdinals to retrieve relative ordinal n
  4. 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

  1. Calculate relative index: given_ordinal - Base = i
  2. 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]);
        }
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.