0%

PE结构-PE文件的两种状态

本文的是学习复现自吾爱破解dalao lyl610abc的PE文件解析系列,非本人原创,属于学习笔记(大佬yyds!)。

PE文件的两种状态

PE文件处于磁盘中与处于内存中时,两者的结构会稍微发生改变。

一个PE文件可以分为两种状态:

运行态: 当一个PE文件被打开时,PE文件的相关数据将会被装载到内存中,根据文件对齐以及内存对齐中的区别,文件的大小以以及结构会发生相应的改变

非运行态: 当一个PE文件尚未运行时,其数据存储在磁盘中时的一种状态

思维脑图

lyl610abc师傅太强了

image-20211116145908655

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马进行实例分析

使用十六进制编辑器打开,判断文件格式类型

image-20211116150624182

可以看到,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

image-20211116170504766

扩展PE头

根据先前得到的标准PE头结束后,紧挨着的224个字节便是扩展PE头所占空间

标准pe头结束位置:0x130

扩展PE头所占空间:224字节

所以扩展PE头所占区域为: 0x130+224=304+224=528=0x210

由此可知,自0xE0开始到0x1C0结束,都是扩展PE头位置

image-20211116170637779

块表

先前拿到的扩展PE头地址后16个字节为空字节,跳过,拿到第一个块表地址

从先前拿到的块表头地址继续往后看40个字节(块表大小) 拿到第二个块表的首地址

第一个块表头地址: 0x210

块表大小为 40字节

0x210+40 = 0x238

第一块表范围: 0x210~0x238

从0x238开始便是第二个块表

第一个块表: .text

image-20211116170758247

第二个块表: .rdata

0x238+0x28=0x260 ,块表范围为0x238~0x260

image-20211116170913559

第三个块表: .data

0x260+0x28=0x288,块表范围为0x260~0x288

image-20211116170953884

第四块表 .sxdata

0x288+0x28=0x2B0,块表范围为0x288~0x2B0

image-20211116171048772

第五块表 .rsrc

0x2B0+0x28=0x2D8,块表范围为0x2B0~0x2D8

image-20211116171132556

第六块表 .reloc

0x2D8+0x28=0x300,块表范围为0x2D8~0x300.

image-20211116171308670

汇总块表

块名称 块地址
.text 0x210~0x238
.rdata 0x238~0x260
.data 0x260~0x288
.sxdata 0x288~0x2B0
.rsrc 0x2B0~0x2D8
.reloc 0x2D8~0x300

块表后的空隙

块表后跟着的按理应该是块,但在块表后和块之前,多出了一段空间

此处为300~400

image-20211116171539285

存储时不存在空隙的,被称为连续存储

但在块表以及块之间是可能存在空隙的,这个空隙里面一般被填充为编译器插入的数据(也可能不存在)

这段间隙的修改并不会导致程序的不可运行,因此可以拿来写入自己想要的代码来对程序进行修改

为什么会存在这段间隙呢?

  • 这段间隙是由于块表与块之间没有进行连续存储
  • 这段长度的存在与否以及长度,取决于块的起始位置
  • 而块的其实位置则根据扩展PE头中的成员决定
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
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[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

结构体中的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

image-20211116164142391

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

image-20211116165632063

拿到00 04 00 00 即 0x400,是FileAlignment的整数倍

image-20211116164459021

可以看到,块表与块之间的间隙自0x400结束

为什么需要文件对齐?

跟内存对齐一样,都是为了使执行时的效率更高,方便内存与磁盘进行交换数据更有效率。

块结构

块的起始地址由块表中的PointerToRawData决定,第一个块的起始地址则有上面的SizeofHeaders决定

块部分存储的是数据,如何存储由块表进行决定

块表在C中的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
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, *PIMAGE_SECTION_HEADER;

块的起始地址

找到结构体中的PointerToRawData成员(DWORD类型占4个字节)

PointerToRawData的含义为该块在磁盘文件中的偏移

前面拿到第一个块表的首地址0x210

从0x210开始往后找20字节,0x210+20=0x224,拿到PointerToRawData的地址,跳转过去拿到4字节的值(磁盘文件中的偏移)

00 04 00 00 即 0x400image-20211116172542500

与SizeOfHeaders得到的一致,前面说过,块的起始地址由块表中的PointerToRawData决定,而第一个块的首地址由SizeOfHeaders决定,此处一致,验证了上文的说法。

块的大小

SizeOfRawData为块的大小(文件对齐后)

SizeOfRawData(Dword 4字节)就在PointerToRawData前面

所以其在块表中的地址为PointerToRawData-4=0x224-4=0x220

SizeOfRawData:

00 72 09 00 即为 0x97200

image-20211116173302128

块的大小与前面三个头(DOS+PE+块表)的大小一样,都需要满足文件对齐

先前拿到的FileAlignment为0x200 块的大小SizeOfRawData大小为0x97200满足整数倍条件,满足文件对齐。

块的结束地址(下一个块的起始地址)

块的结束地址为 块的起始地址+块的大小

即 0x400+0x97200 = 0x97600

image-20211116173621872

可以看到,第一个块与第二个块之间存在空隙,这段空隙是由于在文件中补足文件对齐产生的。

总结

在非运行态下:

  • 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文件

image-20211116192050095

2.使用16进制编辑器 工具->打开主内存->选定PE程序

image-20211116192222818

分析运行态的PE文件

此时Dos头的偏移起始地址为00820000,结束地址为008200F0

image-20211116192334866

跟进偏移,到达PE头标志起始地址008200F0,可见头标志起始地址和标准PE头

PE头标志和标准头

起始为008200F0

结束为00820108

image-20211116193032725

紧随其后,便是扩展PE头,占据224字节单位

起始:00820108

结束:008201E8

image-20211116193755733

块表

image-20211116194147123

块表起始位置:008201E8

块表结束位置:008202AF

块表后的空隙

根据前面的信息来看,在块表前的结构在运行态以及废运行态出了起始地址不同之外,其他的并无不同

在内存中,块表与块的间隙大小与文件对齐FileAlignment无关,是由内存对齐所决定的,内存对齐的机制与文件对齐类似,都是需为SectionAlignment所规定的的值的整数倍。

内存对齐的属性是由_IMAGE_OPTIONAL_HEADER构造体中的成员SectionAlignment决定的,同时,此成员其实就在FileAlignment(文件对齐)成员的上面

扩展PE头在c中的定义

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
30
31
32
33
34
35
36
37
38
39
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment; //<--- 内存对齐
DWORD FileAlignment; //<--- 文件对齐
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
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[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

利用扩展PE头中的SectionAlignment值验证理论是否正确:

从扩展PE头首地址00820108开始数32(1word 2bytes 7dword =1 + 2 + 28)个字节,到820128处,拿到SectionAlignment值

00 10 00 00 即 0x1000

image-20211116195441400

即内存对齐的基数为0x1000

同时,在内存中的PE文件块结构,头地址与SizeofHeaders值无关,因为SizeOfHeaders是文件对齐(FileAlignMent)专用

在非运行态中,块的起始位置由PointerToRawData决定,且PointerToRawData必须为FileAlignment的整数倍

但在运行态中,块的起始位置则并不由PointerToRawData决定,PointerToRawData和SizeOfHeaders一样都为文件对齐专用

运行态块存储涉及点较多,此处只对第一个块的起始地址,结束地址以及大小做计算

块的起始地址

第一个块的起始地址,取决于(DOS头+PE头+块表)总大小的和进行内存对齐后的结果。

三大头加起来的地址为8202AF为止,根据SectionAlignment的值1000h计算,需要进行补齐操作,补齐至821000达到1000的整数倍

image-20211116200746213

可以看到第一个块的首地址已经发现:

首地址:821000

image-20211116200958454

块表的结束地址=块表首地址+块大小,而块大小通过块表当中的SizeOfRawData可以拿到,此处再放出c中的块表定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_SIZEOF_SHORT_NAME              8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
2
DWORD PointerToRawData; //<--- 块在磁盘文件中的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

根据块表定义可以发现,SizeOfRawData在块表中可以通过块表首地址+16字节(8x1bytes + 2DWORD ) = 8201E8 + 16 = 8201F8

拿到SizeOfRawData大小 00 92 19 00 即 199200

image-20211116202106848

由此可知结束地址:821000 + 199200 = 9BA200 + E00(补0内存对齐1000)

image-20211116203313553

拿到块大小第二种方法

块的大小=块的结束地址-块的起始地址=0x59B000-0x401000=0x19A000(满足内存对齐)

image-20211116203344335

运行态时,块的大小满足内存对齐,而非先前的文件对齐

总结

在运行态中:

  • 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)产生

非运行态与运行态映射图

image-20211116204836412