PE结构 ---> 9.RvaToFoa 内存状体到文件状态

张开发
2026/4/4 18:46:47 15 分钟阅读
PE结构 ---> 9.RvaToFoa 内存状体到文件状态
目录1. 为什么必须进行 RVA → FOA 转换核心矛盾2. 磁盘布局 vs 内存布局对比详解3. RVA、VA、FOA 精确定义与关系4. RvaToFoa 函数完整技术详解核心算法5. 实战案例导入表Import Directory为什么特别需要转换6. 记忆口诀 可视化图示1. 为什么必须进行 RVA → FOA 转换核心矛盾当你编写PE 文件解析工具、逆向分析器、加壳/脱壳程序、恶意代码分析器或任何直接操作.exe/.dll文件内容的代码时你会立刻遇到一个无法回避的核心矛盾PE 文件头Optional Header 的 Data Directory中记录的所有关键表格地址如导入表、导出表、资源表、重定位表、TLS 表等全部使用 RVARelative Virtual Address相对虚拟地址。但你实际操作的是磁盘上的原始文件数据通过fopen/fread读取到的字节缓冲区数据的定位方式只能是FOAFile Offset Address文件偏移地址也称为 Raw Offset 或 Pointer to Raw Data。不进行转换你将无法正确读取任何数据结构。直接把 RVA 当作缓冲区下标使用会导致读取完全错误的位置、程序崩溃、或解析出垃圾数据。根本原因PE 文件在磁盘文件布局和内存加载后布局中的组织方式完全不同。2. 磁盘布局 vs 内存布局对比详解磁盘上的 PE 文件布局线性、紧凑、节省磁盘空间文件从头开始依次为DOS HeaderMZ→ DOS Stub → PE SignaturePE\0\0→ File Header → Optional Header →Section Table节表→ 各个 Section 的原始数据。头部直到 Section Table 结束在磁盘和内存中大小基本一致对齐后差异极小。每个 Section 的数据按 FileAlignment 对齐通常0x200 512 字节可能存在填充padding但整体是连续的线性字节流。使用fseek(fp, FOA)或buffer FOA即可直接定位。内存中的 PE 镜像布局由 Windows Loader 映射虚拟化、对齐、保护Loader 以ImageBaseOptional Header 中定义通常0x400000或0x10000000为起点将整个文件映射到虚拟地址空间。按照 Section Table 的指示把每个 Section 独立映射到对应的 RVA 位置。每个 Section 在内存中按 SectionAlignment 对齐通常0x1000 4KB 页对齐以便设置不同的内存保护属性.text可执行、.data可读写、.rdata只读等。可能出现VirtualSize SizeOfRawData未初始化数据如.bss在内存中分配空间但文件中不占字节或空洞未映射区域。简单总结维度磁盘布局FOA内存布局RVA组织方式线性连续字节流按节独立映射 页对齐对齐方式FileAlignment通常 512 字节SectionAlignment通常 4KB空间目标节省磁盘空间便于内存保护与分页头部大小与内存基本一致与磁盘基本一致典型差异可能有填充、紧凑存放可能有空洞、VirtualSize 更大PE 设计者的智慧程序运行时只关心“加载到内存后我在哪里”因此所有内部结构地址都使用RVA相对于 ImageBase 的偏移。这就产生了经典口诀“手在读文件脑得想内存RvaToFoa 就是那座桥。”3. RVA、VA、FOA 精确定义与关系VA (Virtual Address)程序加载到内存后的绝对虚拟地址。示例ImageBase 0x400000RVA 0x2000→ VA 0x400000 0x2000 0x402000。RVA (Relative Virtual Address)相对于 ImageBase 的偏移量。PE 头中几乎所有 Data Directory如导入表地址记录的都是 RVA。它描述的是“加载后离 ImageBase 多远”。FOA (File Offset Address)磁盘文件中实际的字节偏移。buffer[FOA]即可直接读取对应数据。RVA ≠ FOA的根源FileAlignment 与 SectionAlignment 的差异。VirtualSize 可能大于 SizeOfRawData。节在内存中独立映射导致的“错位”。4. RvaToFoa 函数完整技术详解核心算法RvaToFoa 的本质是通过 Section Table 建立 RVA → FOA 的映射桥梁。每个IMAGE_SECTION_HEADER40 字节包含的关键字段Name[8]节名称如.text、.data、.rdataVirtualSize内存中实际大小可能大于文件大小VirtualAddress该节在内存中的起始 RVASizeOfRawData文件中实际占用字节按 FileAlignment 对齐PointerToRawData该节在文件中的起始偏移FOACharacteristics节属性可执行、可写等完整转换算法步骤遍历 Section Table共NumberOfSections个节从 NT Headers 后开始。判断 RVA 是否落入当前节推荐使用VirtualSizeif (dwRva Section.VirtualAddress dwRva Section.VirtualAddress Section.VirtualSize)计算 FOAFOA (dwRva - Section.VirtualAddress) Section.PointerToRawData;边界与特殊情况处理RVA 落在头部SizeOfHeaders直接返回 RVA头部布局几乎一致。RVA 不在任何节内返回 0 或报错。VirtualSize SizeOfRawData内存有额外零填充但文件读取只关心 Raw 数据。特殊目录如 Certificate TableVirtualAddress 可能直接是文件偏移需按 PE 规范区分处理。生产级伪代码DWORD RvaToFoa(PIMAGE_NT_HEADERS pNt, DWORD dwRva) { if (dwRva 0) return 0; PIMAGE_SECTION_HEADER pSection IMAGE_FIRST_SECTION(pNt); for (WORD i 0; i pNt-FileHeader.NumberOfSections; i) { DWORD secStart pSection-VirtualAddress; // 使用 max 更安全兼容 VirtualSize SizeOfRawData 的情况 DWORD secEnd secStart max(pSection-VirtualSize, pSection-SizeOfRawData); if (dwRva secStart dwRva secEnd) { return (dwRva - secStart) pSection-PointerToRawData; } pSection; } // 落在 PE 头部 if (dwRva pNt-OptionalHeader.SizeOfHeaders) return dwRva; return 0; // 无效 RVA }5. 实战案例导入表Import Directory为什么特别需要转换BYTE* buffer ...; // fread 读取的整个文件 PIMAGE_NT_HEADERS pNt ...; // 错误写法直接用 RVA PIMAGE_IMPORT_DESCRIPTOR pImport (PIMAGE_IMPORT_DESCRIPTOR)(buffer importRVA); // 正确写法 DWORD foa RvaToFoa(pNt, importRVA); PIMAGE_IMPORT_DESCRIPTOR pImport (PIMAGE_IMPORT_DESCRIPTOR)(buffer foa);后续的 DLL 名称、Import Lookup Table (ILT)、Import Address Table (IAT) 等字段也都是 RVA必须反复调用RvaToFoa才能正确读取。所有 PE 结构导出表、资源、异常表、重定位等都遵循同一原则。6. 记忆口诀 可视化图示终极口诀“RVA 是内存视角FOA 是文件视角手在读文件脑得想内存RvaToFoa 就是桥。”可视化对比图示磁盘布局线性紧凑 内存布局按页对齐 ┌────────────────────┐ ┌─────────────────────────────┐ │ PE Headers │ │ ImageBase │ ├────────────────────┤ Loader映射 │ RVA .text (0x1000) │ │ .text raw │ ───────────► │ ... 代码 ... │ │ (FOA 0x400) │ ├─────────────────────────────┤ ├────────────────────┤ │ RVA .data (0x2000) │ │ .data raw │ │ ... 数据 ... │ │ (FOA 0x0A00) │ └─────────────────────────────┘ └────────────────────┘ 示例计算 RVA 0x2100落在 .data 节 FOA (0x2100 - 0x2000) 0x0A00 0x0B00好了到此为止PE结构的基础部分更新就OK了 后面是与安全以及对抗相关PE结构的实战技术点了。

更多文章