PE文件结构学习
资料来源:《逆向工程核心原理》和PE文件结构格式详解(完整版)【逆向编程】 (youtube.com)
一、PE文件基础
1.可执行文件
Windows:PE
Linux: elf
2.PE文件特征
PE文件指纹
3.PE结构
DOS头
- DOS MZ头 IMAGE_DOS_HEADER(64字节)
e_magic:4D5A是DOS签名,不可改
e_lfanew:78指向PE头开始位置,要改要一起改。
上面两个是PE指纹,操作系统用来识别是否是PE文件,其他地方可以随便改,因为IMAGE_DOS_HEADER是给16位平台看的,而我们现在的环境大部分是32位或者64位。
- MS_DOS Stu,DOS存根,用来给链接器插入数据,随便改
NT头 IMAGE_NT_HEADERS
1 | typedef struct _IMAGE_NT_HEADERS { |
PE标识 Signature 4字节
不可改,操作系统启动程序的时候识别这个标识。
标准PE头 IMAGE_FILE_HEADER(20字节)
1 | typedef struct _IMAGE_FILE_HEADER { |
1 | 64 86 ->8664 代表在x64上运行 |
扩展PE头 IMAGE_OPTIONAL_HEADER
扩展PE头结构&不同编译器上的差异
32位上是224字节(E0)(可扩展)
64位是F0
1 | //32位为例 |
字段名称 | 32 位 PE(PE32) | 64 位 PE(PE32+) | 描述 |
---|---|---|---|
Magic |
0x10B |
0x20B |
标识 PE 文件是 32 位(PE32)还是 64 位(PE32+)。 |
AddressOfEntryPoint |
存在 | 存在 | 程序入口点的 RVA(相对虚拟地址)。 |
BaseOfCode |
存在 | 存在 | 代码段的起始 RVA。 |
BaseOfData |
存在 | 不存在 | 数据段的起始 RVA,仅在 PE32 中存在。 |
ImageBase |
32 位地址(默认 0x00400000) | 64 位地址(默认 0x0000000140000000) | 可执行文件加载到内存中的首地址。 |
SizeOfStackReserve |
32 位值 | 64 位值 | 为线程的堆栈预留的大小。 |
SizeOfHeapReserve |
32 位值 | 64 位值 | 为堆分配的保留大小。 |
- Magic
2个字节,文件的标志
32 位:10B
64 位:20B
- AddressOfEntryPoint
4个字节,程序的入口点地址,即执行开始的位置。
- ImageBase
4个字节,程序加载的基地址。
AddressOfEntryPoint:042CE910
imagebase:00000010
程序执行入口:(EIP)042CE910+00000010=042CE920
- SectionAlignment
节区的内存对齐大小,节区在内存中的最小大小。
- FileAlignment
节区的文件对齐大小,节区在磁盘文件中的最小单位。
- SizeOfImage
表示在内存中整个PE文件映射的大小(包括所有节区和头信息),可比实际的值大。内存对齐以后是SectionAlignment或者FileAlignment的整数倍。
- SizeOfHeaders
PE 文件头的大小。是FileAlignment的整数倍。
- CheckSum
校验和,系统用来检测文件是否被修改
- Subsystem
程序的子系统类型(例如,Windows GUI 或控制台应用程序),用来表示PE文件的特性。
节表
IMAGE_SECTION_HEADER (40字节)
1 | typedef struct _IMAGE_SECTION_HEADER { |
红色框出来的是扩展PE头,下面就是节表
8字节,当前节的名字,可以随意更改。
当前这个节未对齐时的大小,即实际大小。
实际大小有可能会比Size of Raw Data大,因为未初始化的全局变量在文件中是不占空间的,但是在内存里是有位置的。
Q:在内存中展开时以什么为基准呢?
A:谁大按谁,如果Vitual Size>Size of Raw Data,则按照Vitual Size展开,反之则按照Size of Raw Data。
在内存中的偏移地址,加上ImageBase则是内存中的真实地址。
文件对齐后的大小
当前节在文件中起始位置
节的重定位表(如果有的话)在文件中的偏移地址。
与调试信息和重定位表相关。
节区属性
PE文件的两种状态
文件对齐和内存对齐的差异:
4、RVA和FOA的转换
VA:虚拟内存的绝对地址。
RVA:相对虚拟地址,从ImageBase开始的相对地址。
FOA:文件偏移地址
Q:想改边一个全局变量的初始值,应该怎么做?
A:先区分全局变量有无初始值。如果有初始值,全局变量储存在文件中,如果没有初始值,在文件里就没有位置,在内存展开时才会分配位置。
<1>、判断RVA是否在头部,在的话直接返回
FOA=RVA
<2>、判断RVA在哪一个节
RVA>=节.VA
RVA<=节.VA+当前节内存对其后大小
差值=RVA-节.VA
<4>、FOA=节.PointerToRawData+差值
看一下书上的例子,实例下面导入表的计算也有提到
算完RAW记得查看是否和内存中在同一节区!!!如上图Q3
5、手撕PE文件
(1)在空白区添加代码
(2)扩大节
(3)删除节
(4)新添节
(5)合并节
6、导出表&导入表
前置知识
首先明白,一个可执行程序是有多个pe文件组成的。
导入表(IMP):PE文件引用了哪些文件
- 导入地址表IAT:储存导入函数在内存里的实际应用。
- 导入名称表INT:每个dll导入描述符,储存函数名或者序号,用于加载解析函数地址。
组件 | 内容(磁盘) | 内容(内存) | 作用 |
---|---|---|---|
导入表(IMP) | 所有导入DLL的描述信息 | 不变 | 管理所有导入的DLL和函数引用 |
导入名称表(INT) | 函数名称/序号的RVA | 不变(或不存在于内存) | 提供加载时解析函数地址的线索 |
导入地址表(IAT) | 初始为函数名称/序号的RVA | 实际函数地址 | 运行时跳转到目标函数的地址表 |
1 | Import Table (IMP) |
导出表(EAT):当前的PE文件储存了哪些函数给其他文件用。
Q:导出表在哪?
A:再扩展PE头最后一个成员
Dll
动态链接库
加载DLL的两种方式
- 显式链接:程序使用DLL时候加载,使用完释放内存。
- 隐式链接:程序开始时一同加载DLL,程序终止时释放内存。
导出表
先找到导出表位置
导入表
确定依赖的函数
导入表位置
导入表结构
Name
字符串指针,指向导入函数所属的库文件名字。
RVA要转成FOA,参考下面的实际计算
因为指向的是assic码的字符串,所以到第一个00结束
OringinalFirstThunk-INT
导入名称表
FirstThunk-IAT
导入地址表
实际计算
Export Directory RVA:93 5D 82 09->0x09825D93(imagebase:0x00000010)查了一下再rdata段->FOA:0x09825D83
Export Directory Size:00033669
看了010半天不对,dumpbin /headers看了一下,然后又开了个exe,发现这个爆红的意思是typora.exe没有导入表导出表。。。(也有可能有加壳?die看了一下没有,但是这个地址太大了不正常)
1 | dumpbin /headers "D:\Typora\Typora\Typora.exe" |
换个文件来
Import Directory RVA:0x00003824,在.rdata段,rdata段的RVA是0x00003000,所以相对地址就是0x00000824,rdata段的raw address是0x00001A00,所以FOA是0x00002224,大小是C8字节
Import Directory RVA:0x00003824
.rdata段的 RVA:0x00003000
.rdata段的 Raw Address:0x00001A00
.rdata段的 Raw Size:C8 字节(即 200 字节)
1 | 相对地址 = Import Directory RVA - .rdata段的 RVA = 0x00003824 - 0x00003000 = 0x00000824 |