简介
在了解PE结构之前,先简要回顾下可执行文件的概念。可执行文件是可以被操作系统加载并运行的文件。
Windows与Linux的可执行文件格式:
- Windows平台的可执行文件格式是 PE(Portable Executable) 文件结构。
- Linux平台的可执行文件格式是 ELF(Executable and Linking Format) 文件结构。
命名含义
- PE 的全称是 Portable Executable,意为“便携的可执行文件”。
- ELF 的全称是 Executable and Linking Format,意为“可执行和链接格式”。
差异点
- PE文件结构 是跨版本兼容的。例如,Windows 7上的可执行文件可以在Windows 8、Windows 10等版本中正常运行。
- ELF文件结构 则依赖于内核版本,使用不同内核编译的程序无法在不兼容的环境中运行。
应用场景
PE文件结构在以下领域具有重要意义:
- 病毒和反病毒技术:病毒感染机制和杀毒软件的检测都需要深入理解PE结构。
- 外挂与反外挂:外挂注入与反外挂保护的关键技术点通常基于PE文件操作。
- 加壳与脱壳:保护软件代码的壳技术,以及针对壳保护的破解技术都依赖于对PE结构的掌握。
- 逆向与修改:在没有源码的情况下对程序功能进行修改、汉化或调试也需要了解PE文件的内部结构。
PE结构表

PE结构分析
DOS_HANDER

32位操作系统,每个进程的虚拟地址空间只能访问4GB的内存,其中2GB是在用户空间,另外2GB是在内核空间,对于64位操作系统,每个进程的虚拟地址空间可以访问更大的内存,一般将用户空间分配给进程可用的全部内存,而内核空间仍然是2GB。(题外话)
用16进制编辑器打开程序,一行16进制有16个字节,而DOS_HANDER是给16位计算机用的,到现在的32位和64位程序依然保留了这个数据结构,详细参数可参考以上的PE结构表,在 DOS_HANDER 唯一重要的两个参数是 e_magic 和 e_lfanew,一个是标记用来判断是否是可执行程序,第二个是用来定位PE头位置的,其余的参数都可以用 00,nop 填充因为用不到。
DOS_SUB

DOS_SUB是在DOS_HANDER和PE标准头的中间这一块,这里面的数据也可以00,nop填充掉当然也可以插入自己的数据,如恶意代码。
PE_HANDER

以上 DOS_HANDER 中的 e_lfanew 参数是存储的是 08 01 在PE结构中存在大小端,要反着来 08 01 反过来是 0108 也就是 108 对应的文件偏移定位到 50 48 00 00 这里就是PE头了,PE标识占 4 个字节,而标准PE头占 20 个字节,可以参考以上PE表,第一个参数是 Machine 这个参数可以区分程序是多少位的,可以看到是 4C 01 反过来是 014c 也就是 14c 那么是 32位程序了,64位程序就是 8664。
PE扩展头


65 C8 05 00 反过来 0005c865,就是这个 AddressOfEntryPoint 程序入口地址,ImageBase 是 00 00 40 00 反过来就是 00400000,剩下的结构可以参考PE结构表。
PE地址转换介绍
FOV (File Offset Value):文件偏移量值,指的是 PE 文件中某个数据在文件中的偏移地址。
RVA (Relative Virtual Address):相对虚拟地址,指的是 PE 文件中某个数据在内存中的地址与该模块基地址之间的相对距离。
VA (Virtual Address):虚拟地址,指的是 PE 文件中某个数据在内存中的绝对地址。
FOV文件偏移计算
FOV = RVA - Virtual Offset + Raw Offset在Windows可执行文件中,RVA(Relative Virtual Address)是一个虚拟地址相对于程序映像基址的偏移量,程序映像基址是可执行文件在内存中加载时分配的基地址,通常是0x400000或0x10000000,有了FOV偏移就能够定位,在PE文件中代码的位置,直接修改二进制文件实现程序逻辑的修改。
好,我们先来找一下 AddressOfEntryPoint 程序入口,运行后机器码的位置。

通过studyPE+这个工具,可以对程序的PE结构进行展示,这里的 0001500 就是RVA。

然后就是区段了,Virtual Address 是用来描述PE文件中数据段在程序运行时被加载到内存中的虚拟地址,而 Raw Address 是用来数据段在PE文件中的物理地址或字节偏移量。



通过上面的东西进行公式计算,就能得到二进制文件的FOV偏移,与动态调试的入口地址机器码所对应。
VA虚拟地址计算
VA(Virtual Address,虚拟地址)是指被操作系统分配给进程的内存地址空间中的地址,也称为“逻辑地址”,在现代操作系统中,每个进程都有自己的虚拟地址空间,该地址空间通常从0开始,可以映射到物理内存中的实际地址。
只要明白有了VA就能定位到程序运行后的虚拟内存地址了。
VA = FOV + Virtual Offset - Raw Offset # 两个都能用
VA = RVA + ImageBase

通过上面的公式就能拿到 400000 + 1500 = 401500,在程序运行后的虚拟内存地址。
地址重定位计算
这是在逆向工程中地址计算的一种常见方法,通常用于调试和静态分析工具之间的地址转换。下面是具体的解释和场景说明:
在动态调试器(如 x64dbg)和静态分析工具(如 IDA Pro)中,二进制文件的地址表示方式会有所不同:
动态调试器
- 动态调试时,程序运行在内存中,操作系统会为程序的模块分配一个动态的加载基址(Base Address)。
- 具体地址通常由模块加载地址加上文件中的偏移量(Offset)计算得出。
静态分析工具
- 静态分析工具基于文件本身的结构,不依赖运行时的加载基址。它直接将文件的Offset视为相对地址,结合用户手动输入的加载基址,推算出实际的运行地址。
x64dbg 中的地址计算
- 当前运行的内存地址(Current Address)
- 模块加载地址(Module Base Address)
公式:
偏移量(Offset) = 当前地址 - 模块加载地址
意义在于,找到一个地址相对于模块加载基址的偏移值,这个偏移值与模块在静态文件中的位置一致。
在 IDA 中的地址计算
- 假设在调试器中计算出某个地址的偏移量(Offset)。
- 在IDA中,我们需要知道模块加载基址(Module Base Address)来还原其运行时的实际内存地址。
公式:
当前地址 = 模块加载基址 + 偏移量(Offset)
这个公式的意义是,利用偏移量和加载基址还原实际内存中动态分配的地址。
假设我们正在调试一个程序,模块加载基址是 0x400000,并且我们在 x64dbg 中发现当前执行的指令位于地址 0x402345。
Step 1: 计算Offset
使用公式:
Offset = 当前地址 - 模块加载地址
Offset = 0x402345 - 0x400000
Offset = 0x2345Step 2: 转换到IDA中的地址
假设我们在IDA中对这个模块进行分析,并假设它的加载基址为 0x500000。则在IDA中对应的实际地址为:
当前地址 = 模块加载基址 + Offset
当前地址 = 0x500000 + 0x2345
当前地址 = 0x502345改全局变量
用C写了简单的例子,输出变量数值和虚拟内存地址。
#include <stdio.h>
int value = 1000;
int main () {
printf("Address = 0x%p \n", &value);
printf("value = %d", value);
getchar();
return 0;
}有全局变量的虚拟内存地址,就可以直接可以计算出RVA相对虚拟内存地址,然后通过节表的内存偏移与RVA进行计算,就能得到文件的FOV。(为什么不修改局部变量?首先局部变量存储在堆栈中,需要通过动态修改值,在PE文件结构中并没有堆栈,堆栈是程序运行时分配出来的,所以不能通过修改PE文件结构来改变局部变量的数据。)
RVA = VA - ImageBase
FOV = RVA - Virtual Offset + Raw Offset



通过上面的公式 403010 - 400000 = 3010 得到RVA,然后算区间按照上面的第二个公式就能得出FOV,对照区段的虚拟地址可以发现,是在 .data 节表中。


拿到文件偏移就可以通过进行定位了,用二进制的方式修改全局变量的数据。
小知识

范围 = 基址 + 大小通过基址 + 大小就可以计算出这个 exe 中的内存范围,现在验证下这里面的内存地址是否全是静态地址。

当然,除了exe 也有 dll 的,有了这些静态地址,就可以借助加偏移找到动态地址。
2024-12-20 08:53:14 星期五 更新过
评论 (0)