Windows PE结构
5、导出表解析
1、课程目标
- 理解PE导出机制的工作原理
- 掌握导出表结构及其解析方法
- 理解导出转发机制
- 学会通过导出表查找函数
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、课后作业
-
编写完整的导出表解析器
- 显示所有导出函数
- 区分名称导出和序号导出
- 检测和解析转发函数
-
实现自定义GetProcAddress
- 从内存映像查找函数
- 支持名称和序号查找
- 处理转发情况
-
分析系统DLL导出表
- 分析kernel32.dll的导出
- 分析ntdll.dll的导出
- 对比不同版本Windows的差异