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

目录

[1. 为什么必须进行 RVA → FOA 转换?(核心矛盾)](#1. 为什么必须进行 RVA → FOA 转换?(核心矛盾))

[2. 磁盘布局 vs 内存布局(对比详解)](#2. 磁盘布局 vs 内存布局(对比详解))

[3. RVA、VA、FOA 精确定义与关系](#3. RVA、VA、FOA 精确定义与关系)

[4. RvaToFoa 函数完整技术详解(核心算法)](#4. RvaToFoa 函数完整技术详解(核心算法))

[5. 实战案例:导入表(Import Directory)为什么特别需要转换?](#5. 实战案例:导入表(Import Directory)为什么特别需要转换?)

[6. 记忆口诀 + 可视化图示](#6. 记忆口诀 + 可视化图示)



1. 为什么必须进行 RVA → FOA 转换?(核心矛盾)

当你编写 PE 文件解析工具 、逆向分析器、加壳/脱壳程序、恶意代码分析器,或任何直接操作 .exe / .dll 文件内容的代码时,你会立刻遇到一个无法回避的核心矛盾

  • PE 文件头(Optional Header 的 Data Directory) 中记录的所有关键表格地址(如导入表、导出表、资源表、重定位表、TLS 表等)全部使用 RVA(Relative Virtual Address,相对虚拟地址)。

  • 但你实际操作的是磁盘上的原始文件数据 (通过 fopen / fread 读取到的字节缓冲区),数据的定位方式只能是 FOA(File Offset Address,文件偏移地址,也称为 Raw Offset 或 Pointer to Raw Data)。

  • 不进行转换,你将无法正确读取任何数据结构。

  • 直接把 RVA 当作缓冲区下标使用,会导致读取完全错误的位置、程序崩溃、或解析出垃圾数据。

  • 根本原因 :PE 文件在磁盘(文件布局)内存(加载后布局) 中的组织方式完全不同。

2. 磁盘布局 vs 内存布局(对比详解)

磁盘上的 PE 文件布局(线性、紧凑、节省磁盘空间):

  • 文件从头开始依次为:DOS Header(MZ)→ DOS Stub → PE Signature(PE\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 以 ImageBase (Optional Header 中定义,通常 0x4000000x10000000)为起点,将整个文件映射到虚拟地址空间。

  • 按照 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 = 0x400000,RVA = 0x2000 → VA = 0x400000 + 0x2000 = 0x402000

  • RVA (Relative Virtual Address):相对于 ImageBase 的偏移量。

PE 头中几乎所有 Data Directory(如导入表地址)记录的都是 RVA。它描述的是"加载后离 ImageBase 多远"。

  • FOA (File Offset Address):磁盘文件中实际的字节偏移。

buffer[FOA] 即可直接读取对应数据。

RVA ≠ FOA 的根源:

  1. FileAlignment 与 SectionAlignment 的差异。

  2. VirtualSize 可能大于 SizeOfRawData。

  3. 节在内存中独立映射导致的"错位"。


4. RvaToFoa 函数完整技术详解(核心算法)

RvaToFoa 的本质是通过 Section Table 建立 RVA → FOA 的映射桥梁

每个 IMAGE_SECTION_HEADER(40 字节)包含的关键字段:

  • Name[8]:节名称(如 .text.data.rdata

  • VirtualSize:内存中实际大小(可能大于文件大小)

  • VirtualAddress:该节在内存中的起始 RVA

  • SizeOfRawData:文件中实际占用字节(按 FileAlignment 对齐)

  • PointerToRawData:该节在文件中的起始偏移(FOA)

  • Characteristics:节属性(可执行、可写等)

完整转换算法步骤

  1. 遍历 Section Table (共 NumberOfSections 个节,从 NT Headers 后开始)。

  2. 判断 RVA 是否落入当前节 (推荐使用 VirtualSize):

    复制代码
    if (dwRva >= Section.VirtualAddress && 
        dwRva < Section.VirtualAddress + Section.VirtualSize)
  3. 计算 FOA

    FOA = (dwRva - Section.VirtualAddress) + Section.PointerToRawData;

  4. 边界与特殊情况处理

    1. RVA 落在头部(< SizeOfHeaders):直接返回 RVA(头部布局几乎一致)。

    2. RVA 不在任何节内:返回 0 或报错。

    3. VirtualSize > SizeOfRawData:内存有额外零填充,但文件读取只关心 Raw 数据。

    4. 特殊目录(如 Certificate Table):VirtualAddress 可能直接是文件偏移,需按 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结构的实战技术点了。

相关推荐
浩浩测试一下7 天前
RadASM 汇编工具从下载汉化配置汇编运行 --->>>>环境详细说明
汇编·asm·radasm·pe壳
Joy T23 天前
【PE 实践】从“写提示词”到“构建高可用大模型系统”
prompt·pe·提示词工程·few shot
浩浩测试一下2 个月前
PE结构 ----> PE结构基础知识点汇总(与安全开发关联)
安全·网络安全·pe·windowspe·pe基础格式
阿杰学AI4 个月前
AI核心知识56——大语言模型之ToT(简洁且通俗易懂版)
人工智能·ai·语言模型·提示工程·tot·pe·思维树
阿杰学AI4 个月前
AI核心知识53——大语言模型之Structured CoT 超级模版(简洁且通俗易懂版)
人工智能·ai·语言模型·prompt·提示词·pe·structured cot
阿杰学AI4 个月前
AI核心知识54——大语言模型之Structured CoT(简洁且通俗易懂版)
人工智能·ai·语言模型·prompt·pe·结构化提示词·structured cot
百里香酚兰10 个月前
【AI学习笔记】Coze工作流写入飞书多维表格(即:多维表格飞书官方插件使用教程)
笔记·学习·大模型·飞书·pe·coze
百里香酚兰1 年前
【AI学习笔记】Coze平台实现将Excel文档批量导入数据库全过程
人工智能·笔记·大模型·aigc·工作流·pe·coze
爱喝矿泉水的猛男1 年前
Prompt设计技巧和高级PE
java·人工智能·prompt·cot·tot·pe·ape