Windows PE结构

5、导出表解析

1、课程目标

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

2、名词解释

名词 全称 解释
Export Table 导出表 记录DLL提供的函数
EAT Export Address Table 导出地址表
ENT Export Name Table 导出名称表
EOT Export 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的差异