本文的是学习复现自吾爱破解dalao lyl610abc的PE文件解析系列,非本人原创,属于学习笔记(大佬yyds!)。
PE文件的两种状态
PE文件处于磁盘中与处于内存中时,两者的结构会稍微发生改变。
一个PE文件可以分为两种状态:
运行态: 当一个PE文件被打开时,PE文件的相关数据将会被装载
到内存中,根据文件对齐以及内存对齐中的区别,文件的大小以以及结构会发生相应的改变
非运行态: 当一个PE文件尚未运行时,其数据存储在磁盘中
时的一种状态
思维脑图
lyl610abc师傅太强了
PE文件整体结构表
结构 | 对应C数据结构 | 默认占用空间大小(单位字节) |
---|---|---|
DOS MZ头 | _IMAGE_DOS_HEADER | 64 |
DOS Stub | 仅在MS-DOS系统下有效,不作研究 | 不固定 |
PE文件头 | _IMAGE_NT_HEADERS | 4+20+224=248 (标志+标准头+扩展头) |
PE文件头标志 | Signature | 4 |
PE文件表头/标准PE头 | _IMAGE_FILE_HEADER | 20 |
PE文件表头可选部分/扩展PE头 | _IMAGE_OPTIONAL_HEADER | 224 |
块表/节表 | _IMAGE_SECTION_HEADER | 40 |
块/节 | 无 | 由块表/节表决定 |
非运行态
判断文件类型是否为PE格式文件
判断流程如下:
- 判断前两个字节是否为4D 5A(MZ)
- 找到3Ch
- 根据3Ch位置的2字节大小数据跳转偏移至其中,查看是否为50 45 00 00(PE..)
地址 | 长度(单位字节) | 对应C的数据结构 | 说明 | 值 | ASCII |
---|---|---|---|---|---|
0 | 2 | _IMAGE_DOS_HEADER的第一个成员e_magic | DOS MZ头的第一个成员 | 4D 5A | MZ |
3C | 2 | _IMAGE_DOS_HEADER的最后一个成员e_lfanew | 指出PE头文件偏移位置 | 不定 | 不定 |
[3C] | 4 | Signature | PE文件头标志 | 50 45 00 00 | PE.. |
利用msf生成的exe马进行实例分析
使用十六进制编辑器打开,判断文件格式类型
可以看到,00处起始位置为4D 5A,表示DOS头开头;3Ch处位置是1801 表示PE头文件偏移位置;利用Ctrl+G跳转至118,可以看到标准PE头50 45,此文件可以判断为是PE文件
对应表格数据为
地址 | 说明 | 值 | ASCII |
---|---|---|---|
0 | DOS MZ头的第一个成员 | 4D 5A | MZ |
3C | 指出PE头文件偏移位置C | 118 | |
F0 | PE文件头标志 | 50 45 00 00 | PE.. |
由此,我们可以发现以PE文件头标志为开始的PE头
根据PE结构可知,整个PE头是:PE文件头标志+标准PE头+扩展PE头
的标准,长度大小为4+20+224=248字节
根据3Ch处的118
后面所占的字节地址为:0x118+4+20+224=280+4+20+224=448=0xE0
由此可知整个PE头所占范围为0x118~0xE0
PE文件头标志和标准PE头:
0x118+2+20=200+24=0x130
扩展PE头
根据先前得到的标准PE头结束后,紧挨着的224个字节便是扩展PE头所占空间
标准pe头结束位置:0x130
扩展PE头所占空间:224字节
所以扩展PE头所占区域为: 0x130+224=304+224=528=0x210
由此可知,自0xE0开始到0x1C0结束,都是扩展PE头位置
块表
先前拿到的扩展PE头地址后16个字节为空字节,跳过,拿到第一个块表地址
从先前拿到的块表头地址继续往后看40个字节(块表大小) 拿到第二个块表的首地址
第一个块表头地址: 0x210
块表大小为 40字节
0x210+40 = 0x238
第一块表范围: 0x210~0x238
从0x238开始便是第二个块表
第一个块表: .text
第二个块表: .rdata
0x238+0x28=0x260 ,块表范围为0x238~0x260
第三个块表: .data
0x260+0x28=0x288,块表范围为0x260~0x288
第四块表 .sxdata
0x288+0x28=0x2B0,块表范围为0x288~0x2B0
第五块表 .rsrc
0x2B0+0x28=0x2D8,块表范围为0x2B0~0x2D8
第六块表 .reloc
0x2D8+0x28=0x300,块表范围为0x2D8~0x300.
汇总块表
块名称 | 块地址 |
---|---|
.text | 0x210~0x238 |
.rdata | 0x238~0x260 |
.data | 0x260~0x288 |
.sxdata | 0x288~0x2B0 |
.rsrc | 0x2B0~0x2D8 |
.reloc | 0x2D8~0x300 |
块表后的空隙
块表后跟着的按理应该是块,但在块表后和块之前,多出了一段空间
此处为300~400
存储时不存在空隙的,被称为连续存储
但在块表以及块之间是可能存在空隙的,这个空隙里面一般被填充为编译器插入的数据(也可能不存在)
这段间隙的修改并不会导致程序的不可运行,因此可以拿来写入自己想要的代码来对程序进行修改
为什么会存在这段间隙呢?
- 这段间隙是由于块表与块之间没有进行连续存储
- 这段长度的存在与否以及长度,取决于块的起始位置
- 而块的其实位置则根据
扩展PE头中的成员决定
1 | typedef struct _IMAGE_OPTIONAL_HEADER { |
结构体中的SizeOfHeaders成员(DWORD类型占4个字节)
SizeOfHeaders的含义是3个头按文件对齐后的大小
:(DOS头+PE头+块表)大小加起来后进行文件对齐
后得到的大小
自DOS头开始至块表结束,头大小和为0x248
,此处想要拿到SizeOfHeaders成员的值,需要了解文件对齐
的概念并对其进行对齐处理。
什么是文件对齐?
文件对齐的标准通过扩展PE头中定义的FileAlignment(DWORD类型 占4字节)
文件对齐的要求就是SizeOfHeaders必须为FileAlignment的整数倍
通过以上概念,我们可以理解,由于扩展PE头FileAlignment成员的限制,要求SizeOfHeaders必须为它的整数倍(不足则填充);要想知道文件对齐的标准是多少,我们需要拿到FileAlignment的值
由于前面已知扩展PE头开始地址为:0xE0
FileAlignment(DWORD 4字节)地址为:0xE0+36 = 0x104
FileAlignment为 00 02 00 00 即为 0x200
前面拿到的头大小和为0x248,显然并不是0x200的整数倍
其整数倍,也就是SizeOfHeader的大小应为: (0x248/200+1)*200=400
SizeOfHeaders大小
再通过扩展PE头成员值的方式验证计算是否正确
先前拿到的FileAlignment地址为0x104,再往后24字节,便是SizeOfHeaders存放地址
0x104+24 = 0x11C
SizeOfHeaders:na
拿到00 04 00 00 即 0x400,是FileAlignment的整数倍
可以看到,块表与块之间的间隙自0x400结束
为什么需要文件对齐?
跟内存对齐一样,都是为了使执行时的效率更高,方便内存与磁盘进行交换数据更有效率。
块结构
块的起始地址由块表中的PointerToRawData决定,第一个块
的起始地址则有上面的SizeofHeaders决定
块部分存储的是数据,如何存储由块表进行决定
块表在C中的定义
1 |
|
块的起始地址
找到结构体中的PointerToRawData成员(DWORD类型占4个字节)
PointerToRawData的含义为该块在磁盘文件中的偏移
前面拿到第一个块表的首地址0x210
从0x210开始往后找20字节,0x210+20=0x224,拿到PointerToRawData的地址,跳转过去拿到4字节的值(磁盘文件中的偏移)
00 04 00 00 即 0x400
与SizeOfHeaders得到的一致,前面说过,块的起始地址由块表中的PointerToRawData决定,而第一个块的首地址由SizeOfHeaders决定,此处一致,验证了上文的说法。
块的大小
SizeOfRawData为块的大小(文件对齐后)
SizeOfRawData(Dword 4字节)就在PointerToRawData前面
所以其在块表中的地址为PointerToRawData-4=0x224-4=0x220
SizeOfRawData:
00 72 09 00 即为 0x97200
块的大小与前面三个头(DOS+PE+块表)的大小一样,都需要满足文件对齐
先前拿到的FileAlignment为0x200 块的大小SizeOfRawData大小为0x97200满足整数倍条件,满足文件对齐。
块的结束地址(下一个块的起始地址)
块的结束地址为 块的起始地址+块的大小
即 0x400+0x97200 = 0x97600
可以看到,第一个块与第二个块之间存在空隙
,这段空隙是由于在文件中补足文件对齐产生的。
总结
在非运行态下:
- DOS首部和PE文件头与块表连续存储,中间没有空隙
- 块表与块之间存在
文件对齐
的影响,可能会存在间隙 - 块与块之间也可能由于
文件对其
的影响产生间隙
相关数据结构成员
数据结构成员 | 所属数据结构 | 说明 |
---|---|---|
SizeOfHeaders | 扩展PE头 | 头大小(文件对齐后) |
FileAlignment | 扩展PE头 | 文件对齐 |
PointerToRawData | 块表 | 第一个块表的PointerToRawData由SizeOfHeaders决定,后面块表的PointerToRawData由前一个块表的PointerToRawData+SizeOfRawData决定 |
SizeOfRawData | 块表 | 块表的大小(文件对齐后) |
记录下各个结构的起始以及结束位置方便接下来与运行态比较
结构 | 起始地址 | 结束地址 | 大小 |
---|---|---|---|
DOS部首 | 0 | 118 | 0x118=280 |
PE文件头 | 118 | 210 | 0xF8=248=4+20+224 |
块表 | 210 | 300 | 0xF0=240=6*40 |
前三个结构 | 0 | 400 | 0x400(文件对齐后) |
第一个块 | 400 | 97600 | 0x97200(文件对齐后) |
运行态
运行态,指PE文件装载入内存时,PE结构文件的状态
加载运行态PE文件
1.启动PE文件
2.使用16进制编辑器 工具->打开主内存->选定PE程序
分析运行态的PE文件
此时Dos头的偏移起始地址为00820000,结束地址为008200F0
跟进偏移,到达PE头标志起始地址008200F0,可见头标志起始地址和标准PE头
PE头标志和标准头
起始为008200F0
结束为00820108
紧随其后,便是扩展PE头,占据224字节单位
起始:00820108
结束:008201E8
块表
块表起始位置:008201E8
块表结束位置:008202AF
块表后的空隙
根据前面的信息来看,在块表前的结构在运行态以及废运行态出了起始地址不同之外,其他的并无不同
在内存中,块表与块的间隙大小与文件对齐FileAlignment无关,是由内存对齐
所决定的,内存对齐的机制与文件对齐类似,都是需为SectionAlignment所规定的的值的整数倍。
内存对齐的属性是由_IMAGE_OPTIONAL_HEADER
构造体中的成员SectionAlignment
决定的,同时,此成员其实就在FileAlignment
(文件对齐)成员的上面
扩展PE头在c中的定义
1 | typedef struct _IMAGE_OPTIONAL_HEADER { |
利用扩展PE头中的SectionAlignment值验证理论是否正确:
从扩展PE头首地址00820108开始数32(1word 2bytes 7dword =1 + 2 + 28)个字节,到820128处,拿到SectionAlignment值
00 10 00 00 即 0x1000
即内存对齐的基数为0x1000
同时,在内存中的PE文件块结构,头地址与SizeofHeaders值无关,因为SizeOfHeaders是文件对齐(FileAlignMent)专用
块
在非运行态中,块的起始位置由PointerToRawData决定,且PointerToRawData必须为FileAlignment的整数倍
但在运行态中,块的起始位置则并不由PointerToRawData决定,PointerToRawData和SizeOfHeaders一样都为文件对齐专用
运行态块存储涉及点较多,此处只对第一个块的起始地址,结束地址以及大小做计算
块的起始地址
第一个块的起始地址,取决于(DOS头+PE头+块表)总大小的和进行内存对齐后的结果。
三大头加起来的地址为8202AF为止,根据SectionAlignment的值1000h计算,需要进行补齐操作,补齐至821000达到1000的整数倍
可以看到第一个块的首地址已经发现:
首地址:821000
块表的结束地址=块表首地址+块大小,而块大小通过块表当中的SizeOfRawData可以拿到,此处再放出c中的块表定义
1 |
|
根据块表定义可以发现,SizeOfRawData
在块表中可以通过块表首地址+16字节(8x1bytes + 2DWORD ) = 8201E8 + 16 = 8201F8
拿到SizeOfRawData大小 00 92 19 00 即 199200
由此可知结束地址:821000 + 199200 = 9BA200 + E00(补0内存对齐1000)
拿到块大小第二种方法
块的大小=块的结束地址-块的起始地址=0x59B000-0x401000=0x19A000(满足内存对齐)
运行态时,块的大小满足内存对齐,而非先前的文件对齐
总结
在运行态中:
- DOS头、PE文件头、块表连续存储,中间没有空隙
- 运行态中,块表与块之间存储可能会有空隙
- 空隙的大小与SectionAlignMent有关,而非FileAlignMent
- 块与块之间也有可能会因
内存对齐
而产生空隙
相关数据结构成员:
数据结构成员 | 所属数据结构 | 说明 |
---|---|---|
SectionAlignment | 扩展PE头 | 内存对齐 |
各结构的起始和结束位置:
结构 | 起始地址 | 结束地址 | 大小 |
---|---|---|---|
DOS部首 | 00820000 | 008200F0 | 0xF0=240 |
PE文件头 | 008200F0 | 00820108 | 0xF8=244=224+40 |
块表 | 008201E8 | 008202AF | 0x1E8=200=7*40 |
前三个结构 | 00820000 | 00821000 | 0x1000(内存对齐后) |
第一个块 | 00821000 | 0x19A000 | 0x19A000(内存对齐后) |
运行态与非运行态相同点
相同
无论是运行态,还是非运行态,DOS头、PE头、块表均为连续存储,中间没有填充间隙
第一个块表首地址都三个头的大小影响,紧随着对齐后的三个头
块与块之间也需要进行对齐
不同
运行态与非运行态的起始地址不同
在非运行态中,块表与块之间、块与块之间的间隙由文件对齐(FileAlignment)
产生
在运行态中,块表与块之间、块与块之间的间隙由内存对齐(SectionAlignment)
产生