Windows PE结构

5、导出表解析

1、课程目标

  1. 理解PE导出机制的工作原理
  2. 掌握导出表结构及其解析方法
  3. 理解导出转发机制
  4. 学会通过导出表查找函数

2、名词解释

名词全称解释
Export Table导出表记录DLL提供的函数
EATExport Address Table导出地址表
ENTExport Name Table导出名称表
EOTExport Ordinal Table导出序号表
Ordinal序号函数的数字标识
Ordinal Base序号基数起始序号值
Forwarder转发器将导出重定向到其他DLL

3、使用工具

  • CFF Explorer
  • PE-bear
  • Dependency Walker
  • dumpbin /exports

4、技术原理

4.1、导出表结构

导出表结构图:

IMAGE_DATA_DIRECTORY[0] (Export)
     |
     v
+------------------------+
| IMAGE_EXPORT_DIRECTORY |
+------------------------+
     |
     +---> Name ----------> "kernel32.dll"
     |
     +---> AddressOfFunctions (EAT)
     |          |
     |          v
     |     +------------+
     |     | RVA 1      | --> 函数代码
     |     | RVA 2      | --> 函数代码
     |     | RVA 3      | --> 转发字符串
     |     +------------+
     |
     +---> AddressOfNames (ENT)
     |          |
     |          v
     |     +------------+
     |     | Name RVA 1 | --> "FunctionA"
     |     | Name RVA 2 | --> "FunctionB"
     |     +------------+
     |
     +---> AddressOfNameOrdinals (EOT)
                |
                v
           +------------+
           | Index 1    | (相对于Base的索引)
           | Index 2    |
           +------------+

4.2、导出目录结构

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;        // 保留,通常为0
    DWORD   TimeDateStamp;          // 时间戳
    WORD    MajorVersion;           // 主版本号
    WORD    MinorVersion;           // 次版本号
    DWORD   Name;                   // DLL名称RVA
    DWORD   Base;                   // 序号基数
    DWORD   NumberOfFunctions;      // 导出函数总数
    DWORD   NumberOfNames;          // 按名称导出的函数数
    DWORD   AddressOfFunctions;     // EAT RVA
    DWORD   AddressOfNames;         // ENT RVA
    DWORD   AddressOfNameOrdinals;  // EOT RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

4.3、按名称查找函数流程

1. 在ENT中二分查找函数名
2. 获取索引位置 i
3. 从EOT[i]获取序号索引 ordIndex
4. 从EAT[ordIndex]获取函数RVA
5. 检查RVA是否在导出表范围内(转发)
6. 返回VA = ImageBase + RVA

5、代码实现

5.1、解析导出表

#include <windows.h>
#include <stdio.h>

// 解析导出表
void ParseExportTable(LPVOID fileData) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    BOOL is64 = (ntHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC);
    
    // 获取导出表数据目录
    PIMAGE_DATA_DIRECTORY exportDir;
    if (is64) {
        PIMAGE_OPTIONAL_HEADER64 opt = (PIMAGE_OPTIONAL_HEADER64)&ntHeaders->OptionalHeader;
        exportDir = &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    } else {
        PIMAGE_OPTIONAL_HEADER32 opt = (PIMAGE_OPTIONAL_HEADER32)&ntHeaders->OptionalHeader;
        exportDir = &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    }
    
    if (exportDir->VirtualAddress == 0) {
        printf("[-] 没有导出表\n");
        return;
    }
    
    printf("\n========== Export Table ==========\n");
    printf("RVA: 0x%08X, Size: 0x%08X\n\n", 
           exportDir->VirtualAddress, exportDir->Size);
    
    // 获取导出目录
    DWORD exportFoa = RvaToFoa(fileData, exportDir->VirtualAddress);
    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(
        (PBYTE)fileData + exportFoa
    );
    
    // DLL名称
    DWORD nameFoa = RvaToFoa(fileData, exportDirectory->Name);
    char* dllName = (char*)((PBYTE)fileData + nameFoa);
    
    printf("DLL Name:           %s\n", dllName);
    printf("Characteristics:    0x%08X\n", exportDirectory->Characteristics);
    printf("TimeDateStamp:      0x%08X\n", exportDirectory->TimeDateStamp);
    printf("Version:            %d.%d\n", 
           exportDirectory->MajorVersion, exportDirectory->MinorVersion);
    printf("Ordinal Base:       %d\n", exportDirectory->Base);
    printf("NumberOfFunctions:  %d\n", exportDirectory->NumberOfFunctions);
    printf("NumberOfNames:      %d\n", exportDirectory->NumberOfNames);
    printf("AddressOfFunctions: 0x%08X\n", exportDirectory->AddressOfFunctions);
    printf("AddressOfNames:     0x%08X\n", exportDirectory->AddressOfNames);
    printf("AddressOfNameOrdinals: 0x%08X\n", exportDirectory->AddressOfNameOrdinals);
    
    printf("\n--- Exported Functions ---\n");
    
    // 获取三个表
    DWORD* eatArray = (DWORD*)((PBYTE)fileData + 
                               RvaToFoa(fileData, exportDirectory->AddressOfFunctions));
    DWORD* entArray = (DWORD*)((PBYTE)fileData + 
                               RvaToFoa(fileData, exportDirectory->AddressOfNames));
    WORD* eotArray = (WORD*)((PBYTE)fileData + 
                             RvaToFoa(fileData, exportDirectory->AddressOfNameOrdinals));
    
    // 计算导出表范围(用于检测转发)
    DWORD exportStart = exportDir->VirtualAddress;
    DWORD exportEnd = exportStart + exportDir->Size;
    
    // 遍历所有导出函数
    for (DWORD i = 0; i < exportDirectory->NumberOfFunctions; i++) {
        if (eatArray[i] == 0) {
            continue;  // 跳过空条目
        }
        
        DWORD ordinal = exportDirectory->Base + i;
        DWORD funcRva = eatArray[i];
        
        // 查找函数名(如果有)
        char* funcName = NULL;
        for (DWORD j = 0; j < exportDirectory->NumberOfNames; j++) {
            if (eotArray[j] == i) {
                DWORD nameFoa = RvaToFoa(fileData, entArray[j]);
                funcName = (char*)((PBYTE)fileData + nameFoa);
                break;
            }
        }
        
        printf("[%4d] Ordinal: %5d  RVA: 0x%08X", i, ordinal, funcRva);
        
        // 检查是否是转发
        if (funcRva >= exportStart && funcRva < exportEnd) {
            DWORD fwdFoa = RvaToFoa(fileData, funcRva);
            char* forwarder = (char*)((PBYTE)fileData + fwdFoa);
            printf("  -> FORWARD: %s", forwarder);
        }
        
        if (funcName) {
            printf("  Name: %s", funcName);
        } else {
            printf("  (no name)");
        }
        
        printf("\n");
    }
    
    printf("\n[*] 共导出 %d 个函数 (%d 有名称)\n", 
           exportDirectory->NumberOfFunctions, exportDirectory->NumberOfNames);
}

5.2、按名称/序号查找导出函数

// 按名称查找导出函数(二分查找)
DWORD FindExportByName(LPVOID fileData, const char* funcName) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY exportDir = &ntHeaders->OptionalHeader
                                       .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    
    if (exportDir->VirtualAddress == 0) {
        return 0;
    }
    
    DWORD exportFoa = RvaToFoa(fileData, exportDir->VirtualAddress);
    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(
        (PBYTE)fileData + exportFoa
    );
    
    DWORD* eatArray = (DWORD*)((PBYTE)fileData + 
                               RvaToFoa(fileData, exportDirectory->AddressOfFunctions));
    DWORD* entArray = (DWORD*)((PBYTE)fileData + 
                               RvaToFoa(fileData, exportDirectory->AddressOfNames));
    WORD* eotArray = (WORD*)((PBYTE)fileData + 
                             RvaToFoa(fileData, exportDirectory->AddressOfNameOrdinals));
    
    // 二分查找(导出名称表是有序的)
    int left = 0;
    int right = exportDirectory->NumberOfNames - 1;
    
    while (left <= right) {
        int mid = (left + right) / 2;
        
        DWORD nameFoa = RvaToFoa(fileData, entArray[mid]);
        char* name = (char*)((PBYTE)fileData + nameFoa);
        
        int cmp = strcmp(funcName, name);
        
        if (cmp == 0) {
            // 找到了
            WORD ordIndex = eotArray[mid];
            return eatArray[ordIndex];  // 返回函数RVA
        } else if (cmp < 0) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    
    return 0;  // 未找到
}

// 按序号查找导出函数
DWORD FindExportByOrdinal(LPVOID fileData, WORD ordinal) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY exportDir = &ntHeaders->OptionalHeader
                                       .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    
    if (exportDir->VirtualAddress == 0) {
        return 0;
    }
    
    DWORD exportFoa = RvaToFoa(fileData, exportDir->VirtualAddress);
    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(
        (PBYTE)fileData + exportFoa
    );
    
    // 计算索引
    DWORD index = ordinal - exportDirectory->Base;
    
    if (index >= exportDirectory->NumberOfFunctions) {
        return 0;
    }
    
    DWORD* eatArray = (DWORD*)((PBYTE)fileData + 
                               RvaToFoa(fileData, exportDirectory->AddressOfFunctions));
    
    return eatArray[index];
}

// 模拟GetProcAddress(从内存映像获取函数地址)
FARPROC MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName) {
    LPBYTE base = (LPBYTE)hModule;
    
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        base + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY exportDir = &ntHeaders->OptionalHeader
                                       .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    
    if (exportDir->VirtualAddress == 0) {
        return NULL;
    }
    
    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(
        base + exportDir->VirtualAddress
    );
    
    DWORD* eatArray = (DWORD*)(base + exportDirectory->AddressOfFunctions);
    DWORD* entArray = (DWORD*)(base + exportDirectory->AddressOfNames);
    WORD* eotArray = (WORD*)(base + exportDirectory->AddressOfNameOrdinals);
    
    DWORD funcRva = 0;
    
    // 判断是序号还是名称
    if (HIWORD(lpProcName) == 0) {
        // 按序号查找
        WORD ordinal = LOWORD(lpProcName);
        DWORD index = ordinal - exportDirectory->Base;
        
        if (index < exportDirectory->NumberOfFunctions) {
            funcRva = eatArray[index];
        }
    } else {
        // 按名称查找(二分)
        int left = 0;
        int right = exportDirectory->NumberOfNames - 1;
        
        while (left <= right) {
            int mid = (left + right) / 2;
            char* name = (char*)(base + entArray[mid]);
            
            int cmp = strcmp(lpProcName, name);
            
            if (cmp == 0) {
                funcRva = eatArray[eotArray[mid]];
                break;
            } else if (cmp < 0) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
    }
    
    if (funcRva == 0) {
        return NULL;
    }
    
    // 检查转发
    DWORD exportStart = exportDir->VirtualAddress;
    DWORD exportEnd = exportStart + exportDir->Size;
    
    if (funcRva >= exportStart && funcRva < exportEnd) {
        // 这是一个转发
        char* forwarder = (char*)(base + funcRva);
        
        // 解析转发字符串 "DLL.Function"
        char dllName[256];
        strcpy(dllName, forwarder);
        char* dot = strchr(dllName, '.');
        if (dot) {
            *dot = '\0';
            strcat(dllName, ".dll");
            
            HMODULE hFwdDll = LoadLibraryA(dllName);
            if (hFwdDll) {
                return MyGetProcAddress(hFwdDll, dot + 1);
            }
        }
        return NULL;
    }
    
    return (FARPROC)(base + funcRva);
}

5.3、导出表遍历器

// 导出函数信息
typedef struct _EXPORT_INFO {
    DWORD   ordinal;
    DWORD   rva;
    char    name[256];
    char    forwarder[256];
    BOOL    isForwarder;
} EXPORT_INFO;

// 枚举所有导出函数
int EnumerateExports(LPVOID fileData, EXPORT_INFO* exports, int maxCount) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY exportDir = &ntHeaders->OptionalHeader
                                       .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    
    if (exportDir->VirtualAddress == 0) {
        return 0;
    }
    
    DWORD exportFoa = RvaToFoa(fileData, exportDir->VirtualAddress);
    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(
        (PBYTE)fileData + exportFoa
    );
    
    DWORD* eatArray = (DWORD*)((PBYTE)fileData + 
                               RvaToFoa(fileData, exportDirectory->AddressOfFunctions));
    DWORD* entArray = (DWORD*)((PBYTE)fileData + 
                               RvaToFoa(fileData, exportDirectory->AddressOfNames));
    WORD* eotArray = (WORD*)((PBYTE)fileData + 
                             RvaToFoa(fileData, exportDirectory->AddressOfNameOrdinals));
    
    DWORD exportStart = exportDir->VirtualAddress;
    DWORD exportEnd = exportStart + exportDir->Size;
    
    int count = 0;
    
    for (DWORD i = 0; i < exportDirectory->NumberOfFunctions && count < maxCount; i++) {
        if (eatArray[i] == 0) continue;
        
        EXPORT_INFO* info = &exports[count];
        info->ordinal = exportDirectory->Base + i;
        info->rva = eatArray[i];
        info->name[0] = '\0';
        info->forwarder[0] = '\0';
        info->isForwarder = FALSE;
        
        // 查找名称
        for (DWORD j = 0; j < exportDirectory->NumberOfNames; j++) {
            if (eotArray[j] == i) {
                DWORD nameFoa = RvaToFoa(fileData, entArray[j]);
                strcpy(info->name, (char*)((PBYTE)fileData + nameFoa));
                break;
            }
        }
        
        // 检查转发
        if (info->rva >= exportStart && info->rva < exportEnd) {
            info->isForwarder = TRUE;
            DWORD fwdFoa = RvaToFoa(fileData, info->rva);
            strcpy(info->forwarder, (char*)((PBYTE)fileData + fwdFoa));
        }
        
        count++;
    }
    
    return count;
}

6、课后作业

  1. 编写完整的导出表解析器

    • 显示所有导出函数
    • 区分名称导出和序号导出
    • 检测和解析转发函数
  2. 实现自定义GetProcAddress

    • 从内存映像查找函数
    • 支持名称和序号查找
    • 处理转发情况
  3. 分析系统DLL导出表

    • 分析kernel32.dll的导出
    • 分析ntdll.dll的导出
    • 对比不同版本Windows的差异