在Windows可执行文件(PE)格式分析中,数据目录表和节表是两个至关重要的数据结构。作为PE文件的核心组成部分,它们承载着程序加载、内存映射和函数调用的关键信息。本文将深入剖析这两个结构的原理与实战应用。
数据目录表位于PE文件的IMAGE_OPTIONAL_HEADER末尾,是一个包含16个固定槽位的数组。每个槽位通过VirtualAddress(RVA)和Size描述特定数据结构的位置和大小。以下是关键字段的详细说明:
c复制typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 数据结构的RVA(相对虚拟地址)
DWORD Size; // 数据结构的大小(字节)
} IMAGE_DATA_DIRECTORY;
在实际分析中,我们通常通过以下步骤定位数据目录表:
c复制// 获取NT头
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pBuffer + pDosHeader->e_lfanew);
// 定位到可选头
PIMAGE_OPTIONAL_HEADER pOptionHeader = &pNtHeader->OptionalHeader;
// 访问特定目录项(如导入表)
PIMAGE_DATA_DIRECTORY pImportDir = &pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
注意:在32位和64位PE文件中,可选头结构有所不同(IMAGE_OPTIONAL_HEADER32/64),但数据目录表的布局完全一致。
数据目录表中最重要的三个目录项是:
下表展示了完整的数据目录表内容:
| 索引 | 宏定义 | 目录名称 | 说明 | 重要性 |
|---|---|---|---|---|
| 0 | IMAGE_DIRECTORY_ENTRY_EXPORT | 导出表 | 本模块导出的函数 | 高 |
| 1 | IMAGE_DIRECTORY_ENTRY_IMPORT | 导入表 | 依赖的外部函数 | ★★★★★ |
| ... | ... | ... | ... | ... |
| 5 | IMAGE_DIRECTORY_ENTRY_BASERELOC | 重定位表 | ASLR支持 | ★★★★★ |
| 12 | IMAGE_DIRECTORY_ENTRY_IAT | IAT表 | 导入函数地址 | ★★★★★ |
PE文件在磁盘和内存中的布局存在差异,这导致我们需要在RVA(内存偏移)和FOA(文件偏移)之间转换:
c复制DWORD RvaToFoa(DWORD dwRva, BYTE* pBuffer) {
PIMAGE_NT_HEADERS pNtHeader = /* 获取NT头 */;
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeader);
for (WORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++) {
if (dwRva >= pSection->VirtualAddress &&
dwRva < pSection->VirtualAddress + pSection->Misc.VirtualSize) {
return dwRva - pSection->VirtualAddress + pSection->PointerToRawData;
}
pSection++;
}
return dwRva; // 如果位于头部,则RVA=FOA
}
关键点:转换时需要检查RVA是否落在某个节区内,头部数据通常不需要转换。
节表位于可选头之后,由多个IMAGE_SECTION_HEADER结构组成,数量由文件头的NumberOfSections指定:
c复制typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8]; // 节区名称(如".text")
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 内存中的实际大小
} Misc;
DWORD VirtualAddress; // 内存中的RVA
DWORD SizeOfRawData; // 磁盘中的大小
DWORD PointerToRawData; // 磁盘中的偏移
// ... 其他字段
} IMAGE_SECTION_HEADER;
定位节表的典型代码:
c复制PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeader);
for (WORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++) {
printf("节区名: %-8s 虚拟大小: %08X 文件大小: %08X\n",
pSection->Name, pSection->Misc.VirtualSize, pSection->SizeOfRawData);
pSection++;
}
PE文件在磁盘和内存中的节区布局存在两个关键差异:
对齐方式不同:
SectionAlignment),通常4KBFileAlignment),通常512B大小可能不同:
VirtualSize:节区在内存中的实际大小SizeOfRawData:节区在文件中的占用空间典型节区布局示例:
code复制磁盘布局:
[头部][.text(文件对齐)][.data(文件对齐)][.rsrc(文件对齐)]
内存布局:
[头部(内存对齐)][.text(内存对齐)][.data(内存对齐)][.rsrc(内存对齐)]
| 节区名 | 典型特性 | 说明 |
|---|---|---|
| .text | EXECUTE, READ | 代码段,包含可执行指令 |
| .data | READ, WRITE | 初始化数据 |
| .rdata | READ | 只读数据(如字符串常量) |
| .idata | READ | 导入表数据(某些编译器) |
| .edata | READ | 导出表数据 |
| .rsrc | READ | 资源数据(图标、菜单等) |
| .reloc | READ | 重定位数据 |
导入表由多个IMAGE_IMPORT_DESCRIPTOR组成,每个描述一个DLL的导入信息:
c复制typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 指向INT(导入名称表)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // DLL名称的RVA
DWORD FirstThunk; // 指向IAT(导入地址表)
} IMAGE_IMPORT_DESCRIPTOR;
c复制// 1. 定位导入表目录
PIMAGE_DATA_DIRECTORY pImportDir = &pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
// 2. 转换为文件偏移
DWORD dwImportFoa = RvaToFoa(pImportDir->VirtualAddress, pBuffer);
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pBuffer + dwImportFoa);
// 3. 遍历每个DLL
while (pImport->Name != 0) {
// 获取DLL名称
char* szDllName = (char*)(pBuffer + RvaToFoa(pImport->Name, pBuffer));
// 处理函数导入
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(pBuffer + RvaToFoa(pImport->OriginalFirstThunk, pBuffer));
DWORD* pIAT = (DWORD*)(pBuffer + RvaToFoa(pImport->FirstThunk, pBuffer));
while (pThunk->u1.AddressOfData != 0) {
if (pThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) {
// 按序号导入
printf(" 序号导入: #%d\n", IMAGE_ORDINAL(pThunk->u1.Ordinal));
} else {
// 按名称导入
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(pBuffer + RvaToFoa(pThunk->u1.AddressOfData, pBuffer));
printf(" 名称导入: %s (Hint: %04X)\n", pName->Name, pName->Hint);
}
pThunk++;
}
pImport++;
}
RVA转换失败:
导入表损坏:
IMAGE_DIRECTORY_ENTRY_IMPORT的Size是否为0IAT与INT不一致:
快速定位节区:
.text通常位于第一个节区.rsrc的资源数据通常使用FindResource等API访问检测节区篡改:
VirtualSize和SizeOfRawData处理重定位:
IMAGE_DIRECTORY_ENTRY_BASERELOC扩展节区:
SizeOfRawData和VirtualSizePointerToRawData和VirtualAddressSizeOfImage添加导入函数:
IMAGE_IMPORT_DESCRIPTOR对齐要求:
调试技巧:
LoadLibrary和GetProcAddress验证导入表推荐工具:
掌握PE结构解析不仅能深入理解Windows加载机制,也是安全分析、逆向工程和性能优化的基础。建议通过实际修改小型PE文件来加深理解,例如尝试手动添加节区或修改导入表。