PE文件节表位于IMAGE_NT_HEADERS之后,数量由FileHeader.NumberOfSections决定,每个IMAGE_SECTION_HEADER固定40字节;VirtualAddress需加基址转为内存地址,PointerToRawData仅适用于文件上下文。

如何用 IMAGE_SECTION_HEADER 解析 PE 文件的节表
PE 文件的节(Section)信息不存于磁盘文件头的“显式目录”里,而是紧接在 IMAGE_OPTIONAL_HEADER 之后的一组连续 IMAGE_SECTION_HEADER 结构。从内存映像中读取时,关键不是“找节表起始”,而是确认当前映像是否已按对齐粒度加载、且节表未被运行时修改(如 ASLR 后偏移变化但结构布局不变)。
- 内存映像中节表地址 =
NT_HEADERS->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress不相关 —— 这是证书位置;正确路径是:先定位DOS_HEADER→ 跳到NT_HEADERS→ 读FileHeader.NumberOfSections→ 节表起始地址 =(BYTE*)NT_HEADERS + sizeof(IMAGE_NT_HEADERS) - 注意
NumberOfSections是文件头字段,内存中依然有效;它决定后续要解析多少个IMAGE_SECTION_HEADER - 每个
IMAGE_SECTION_HEADER固定 40 字节(x86/x64 一致),不随架构扩展
// 示例:从 HMODULE 或基址 base 得到节表指针
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)base + dos->e_lfanew);
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sec) {
printf("%s: %.8X %.8X\n", sec->Name, sec->VirtualAddress, sec->Misc.VirtualSize);
}
为什么 VirtualAddress 和 PointerToRawData 在内存中不能直接当指针用
这两个字段是 RVA(Relative Virtual Address)和文件偏移,不是绝对地址。在内存映像中,VirtualAddress 必须加上模块基址才能得到真实内存地址;而 PointerToRawData 属于文件布局概念,在纯内存映像中可能无效(例如资源被解压、节被重定位或丢弃)。
- 如果模块是正常加载的(非手动 map),
VirtualAddress+base就是该节内容在内存中的起始地址 -
PointerToRawData仅在原始文件上下文中有意义;若你手头只有内存 dump(无原始文件),这个字段基本不可信,尤其遇到 packed 或 patch 后的模块 -
SizeOfRawData在内存中常与Misc.VirtualSize不等:前者是磁盘对齐后大小,后者是运行时实际占用的虚拟内存页数
常见误判:把 .reloc 节当成空节或忽略它
很多工具看到 Misc.VirtualSize == 0 或 VirtualAddress == 0 就跳过该节,但 Windows 加载器会为 ASLR 生成/填充 .reloc 节 —— 它可能在文件中为空,但在内存中真实存在且含有效重定位块。
- 检查
Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA或IMAGE_SCN_MEM_READ才能判断节是否可读,不能只看大小 -
.reloc的VirtualAddress通常非零,其内容是IMAGE_BASE_RELOCATION链表,每个块以VirtualAddress+SizeOfBlock描述一段需修复的地址范围 - 若用
VirtualQuery发现某节所在内存区域是PAGE_READWRITE且含大量小端 16 位数据,大概率就是重定位节
手动解析时容易崩的边界条件
节表本身没有校验和,也没有长度字段,全靠 NumberOfSections 驱动循环。一旦该字段被篡改(如加壳器故意写错)、或 NT 头被破坏,就会越界读取内存,触发 AV。
立即学习“C++免费学习笔记(深入)”;
- 解析前必须验证
dos->e_magic == IMAGE_DOS_SIGNATURE且nt->Signature == IMAGE_NT_SIGNATURE - 计算节表结束地址:
(BYTE<em>)sec + sizeof(IMAGE_SECTION_HEADER) </em> nt->FileHeader.NumberOfSections,不能超过VirtualQuery返回的区域大小 -
sec->Name是 8 字节数组,不一定以\0结尾;用std::string_view(sec->Name, strnlen((char*)sec->Name, 8))安全取名
真正麻烦的从来不是结构体定义,而是你拿到的那块内存到底“算不算一个干净加载的 PE 映像”——加壳、补丁、DLL 注入、内存马都会让节的语义和布局变得模糊。别迷信字段值,多结合 VirtualQuery 和 ReadProcessMemory(如需跨进程)交叉验证。



















