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存根,用来给链接器插入数据,随便改
1 2 3 4 5 | typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; ``
IMAGE_FILE_HEADER FileHeader; ``
IMAGE_OPTIONAL_HEADER32 OptionalHeader; ``
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
|
PE标识 Signature 4字节
<u>不可改</u>,操作系统启动程序的时候识别这个标识。
1 2 3 4 5 6 7 8 9 | typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
|
1 2 3 4 5 6 | 64 86 - > 8664 代表在x64上运行
0F 00 有 0x0F 个节区
84 D7 68 65 编译器写的时间戳,和文件无关,随便改
调试不管
F0 00 扩展PE头大小,可改
22 00 - > 0022 - > 0000 0000 0010 0010 第 2 位,第 6 位有值 对应数据位 1 , 5 分别代表文件可执行,应用程序可以处理大于 2GB 的地址(代表 64 位)
|
扩展PE头结构&不同编译器上的差异
32位上是224字节(E0)(可扩展)
64位是F0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | typedef struct _IMAGE_OPTIONAL_HEADER32 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[16];
} IMAGE_OPTIONAL_HEADER32;
|
字段名称 |
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:<u>042CE910</u>
imagebase:<u>00000010</u>
程序执行入口:(EIP)042CE910+00000010=042CE920
SectionAlignment
节区的内存对齐大小,节区在内存中的最小大小。
FileAlignment
节区的文件对齐大小,节区在磁盘文件中的最小单位。
SizeOfImage
表示在内存中整个PE文件映射的大小(包括所有节区和头信息),可比实际的值大。内存对齐以后是SectionAlignment或者FileAlignment的整数倍。
PE 文件头的大小。是FileAlignment的整数倍。
CheckSum
校验和,系统用来检测文件是否被修改
Subsystem
程序的子系统类型(例如,Windows GUI 或控制台应用程序),用来表示PE文件的特性。
节表
IMAGE_SECTION_HEADER (40字节)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER;
|
红色框出来的是扩展PE头,下面就是节表
Name
8字节,当前节的名字,可以随意更改。
当前这个节未对齐时的大小,即实际大小。
实际大小有可能会比Size of Raw Data大,因为未初始化的全局变量在文件中是不占空间的,但是在内存里是有位置的。
Q:在内存中展开时以什么为基准呢?
A:谁大按谁,如果Vitual Size>Size of Raw Data,则按照Vitual Size展开,反之则按照Size of Raw Data。
VirtualAddress(RVA)
在内存中的偏移地址,加上ImageBase则是内存中的真实地址。
Raw Size(Size of Raw Data)
文件对齐后的大小
Raw Address(File Pointer to Raw Data)
当前节在文件中起始位置
Reloc Address
节的重定位表(如果有的话)在文件中的偏移地址。
Line Numbers、Relocation Number、Line Number Numbers
与调试信息和重定位表相关。
Characteristics
节区属性
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文件组成的。
导入表(IMP):PE文件引用了哪些文件
导出表(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 字节)
相对地址 = Import Directory RVA - .rdata段的 RVA = 0x00003824 - 0x00003000 = 0x00000824
FOA = .rdata段的 Raw Address + 相对地址 =0x00001A00 + 0x00000824 = 0x00002224