PE两种状态
一个PE文件可以分为两种状态:运行态和非运行态
非运行态:当一个PE文件尚未被运行时,其数据存储在磁盘中,也就是PE个人笔记一之PE介绍中PE的状态
运行态:当一个PE文件被打开后,PE文件的相关数据将被装载到内存中,此时为运行态
在细讲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文件
接下来讲讲为何能通过之前的方法来判断PE文件
判断PE文件的流程可概括为如下三步:
- 判断头2个字节是否为4D 5A(ASCII码为MZ)
- 找到3Ch位置数据
- 根据第二步中的位置数据再找到对应的地址,判断这个地址是否为50 45 00 00(对应ACSII码为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.. |
下面结合实例再来分析:
这次使用WinHex这个工具来进行查看
此时对应的表格数据为:
地址 |
说明 |
值 |
ASCII |
0 |
DOS MZ头的第一个成员 |
4D 5A |
MZ |
3C |
指出PE头文件偏移位置 |
F0 |
|
F0 |
PE文件头标志 |
50 45 00 00 |
PE.. |
上面对一个PE文件的判断只涉及了DOS MZ头和PE文件头中的PE文件头标志
下面继续分析其它结构
后续分析
PE文件头标志和标准PE头
从先前的PE的结构继续向后看24个字节(PE文件头标志的大小+标准PE头大小 4+20)得到扩展PE头的首地址
先前的地址为:F0
后来的地址为:0xF0+24=240+24=264=0x108
所以从F0~108为PE文件头标志和标准PE头
从108开始就是扩展PE头了
PE文件头标志和标准PE头:
扩展PE头
从先前得到的扩展PE头地址继续向后看224个字节(扩展PE头大小)得到块表的首地址
先前的地址为:108
后来的地址为:0x108+224=264+224=488=0x1E8
所以从108~1E8为扩展PE头
从1E8开始就是块表了
扩展PE头:
块表
从先前得到的块表头地址继续向后看40个字节(块表大小)得到第二个块表的首地址
先前的地址为:1E8
后来的地址为:0x1E8+40=488+40=528=0x210
所以从1E8~210为第一个块表
从210开始就是第二个块表了
第一个块表:
同理可得剩下的几个块表
第二个块表:
地址从210~238
第三个块表:
地址从238~260
第四个块表:
地址从260~288
第五个块表:
地址从288~2B0
汇总块表
块名称 |
块地址 |
.text |
1E8~210 |
.rdata |
210~238 |
.data |
238~260 |
.rsrc |
260~288 |
.reloc |
288~2B0 |
块表后的空隙
块表后面跟着的应该是块,但在块表后和块之前却多出了一段空间
这里为2B0~400
先前的PE结构中间都没有空隙,为连续存储
但在块表和块之间是可能存在空隙的,这个空隙里一般被填充为编译器插入的数据(也可以没有,就是此时的情况)
这段空隙的修改并不会导致程序不可运行,因而可被拿来写入自己想要的代码来对程序进行修改
为什么会存在这段空隙?
这段空隙存在的原因在于块表和块并没有连续存储
所以这段空隙的存在与否及长度 取决于 块的起始位置
而块的起始位置则由扩展PE头中的某个成员决定
给出扩展PE头在C中的定义:
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头大小+块表大小)加完的结果进行文件对齐后得到的大小
头大小相加很好理解,按照之前得到的头大小和为:2B0,于是问题就在于文件对齐
什么是文件对齐
讲到文件对齐就涉及到扩展PE头中的另一个成员:FileAlignment(DWORD类型占4个字节)
文件对齐就是要求SizeOfHeaders必须为FileAlignment的整数倍
知道了由来以后,验证一下
前面已经得知了扩展PE头的首地址为108
从108开始往后找36个字节(中间间隔了1个WORD,2个BYTE,8个DWORD,即1*2+2*1+8*4=36)
FileAlignment的地址为:0x108+36=264+36=300=0x12C
FileAlignment:
FileAlignment为00 00 02 00=0x200(小端存储)
前面得到的头大小和为0x2B0显然不是FileAlignment的整数倍
于是将SizeOfHeaders设置为FileAlignment的整数倍:(2B0/200+1)*200=400
得出的SizeOfHeaders的大小应该为0x400
再来验证一下SizeOfHeaders的大小
前面得到的FileAlignment的地址为12C
从12C开始往后找24个字节(中间间隔了6个WORD,3个DWORD,即6*2+3*4=24)
SizeOfHeaders的地址为:0x12C+24=300+24=324=0x144
SizeOfHeaders:
SizeOfHeaders为 00 00 04 00=0x400(小端存储)
得到的大小和计算出来的大小一致,验证完毕
为什么要文件对齐
和内存对齐一样,都是为了使执行时的效率更高,有关内存对齐可参考:逆向基础笔记十八 汇编 结构体和内存对齐
PS:上面的内存对齐为程序中局部的内存对齐,主要针对的是编程时的变量、结构体等,和后面要讲的内存对齐要区别开
块
块的起始地址由块表中的PointerToRawData决定,第一个块的起始地址则由上面的SizeOfHeaders决定
块部分存储的为数据,如何存储由块表决定,这里主要探讨每个块的 起始地址、块大小、结束地址,其它留作之后的笔记
给出块表在C中的定义
#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的含义为该块在磁盘文件中的偏移
前面已经知道第一个块表的首地址为1E8
从1E8开始往后找20个字节(中间间隔了1个BYTE[8],3个DWORD,即1*8+3*4=20)
PointerToRawData的地址为:0x1E8+20=488+20=508=0x1FC
PointerToRawData:
PointerToRawData为00 00 04 00=0x400
和通过SizeOfHeaders得到的一致,验证了第一个块的PointerToRawData由SizeOfHeaders决定
块的大小
SizeOfRawData为块的大小(文件对齐后)
SizeOfRawData就在PointerToRawData前面
所以其地址为:PointerToRawData地址-4=0x1FC-4=0x1F8
SizeOfRawData:
SizeOfRawData为00 92 19 00=0x199200
块的大小和前面三个头(DOS部首+PE文件头+块表)的大小一样,也要满足文件对齐
先前得到的FileAlignment为0x200,这里的SizeOfRawData:0x199200为FileAlignment的整数倍,满足文件对齐
块的结束地址(下一个块的起始地址)
块的结束地址为块的起始地址+块的大小
即块的结束地址=0x400+0x199200=199600
可以看到第一个块和第二个块之前是存在空隙的,这段空隙也是由于文件对齐产生的
小总结
在非运行态下:
- DOS部首和PE文件头及块表连续存储,中间没有空隙
- 而块表和块之间由于文件对齐可能会存在空隙
- 块和块之间也由于文件对齐可能会存在空隙
相关数据结构成员:
数据结构成员 |
所属数据结构 |
说明 |
SizeOfHeaders |
扩展PE头 |
头大小(文件对齐后) |
FileAlignment |
扩展PE头 |
文件对齐 |
PointerToRawData |
块表 |
第一个块表的PointerToRawData由SizeOfHeaders决定,后面块表的PointerToRawData由前一个块表的PointerToRawData+SizeOfRawData决定 |
SizeOfRawData |
块表 |
块表的大小(文件对齐后) |
记录一下各结构的起始和结束位置,方便和运行态进行比较
结构 |
起始地址 |
结束地址 |
大小 |
DOS部首 |
0 |
F0 |
0xF0=240 |
PE文件头 |
F0 |
1E8 |
0xF8=244=224+40 |
块表 |
1E8 |
2B0 |
0xC8=200=5*40 |
前三个结构 |
0 |
400 |
0x400(文件对齐后) |
第一个块 |
400 |
199600 |
0x199200(文件对齐后) |
运行态
前面介绍了非运行态(硬盘状态)下PE文件的结构,现在看看运行态(内存状态)下PE文件的结构
加载运行态的PE文件
1.启动PE文件
2.然后返回WinHex,点击工具→打开RAM(R)... 或直接使用快捷键Alt+F9
3.打开后显示如下:
选中我们要分析的PE文件,使其展开
4.展开后显示如下:
选中.exe打开
5.最后确定即可
分析运行态的PE文件
按照先前分析的流程,再分析一遍运行态下的PE文件
DOS部首和PE头标志
此时的DOS部首起始地址为400000,结束地址为4000F0
PE文件头标志和标准PE头
此时PE文件头的起始地址为4000F0,结束地址为400108
扩展PE头
此时PE文件头的起始地址为400108,结束地址为4001E8
块表
此时块表的起始地址为4001E8,结束地址为4002B0
块表后的空隙
从前面的分析来看,在块表前的结构在运行态和非运行态除了起始地址不同以外,其它地方并无不同
起始地址的由来等相关内容留作之后的笔记说明
按照经验,块表后的空隙也应该是持续到先前的400+400000=400400
于是前往查看:
发现并非如此,400400仍然为空隙
先前分析得知在非运行态块表后的空隙是因为文件对齐产生的
而在运行态中,显然就不是由文件对齐决定的
接着向下查看,找到块的起始位置
发现此时块的起始位置为401000,偏移为1000,而不是 非运行态的400
于是熟悉的问题又回来了:
为什么会存在这段空隙?
和先前的文件对齐类似,当程序处于运行态时,会有另一种对齐方式:内存对齐
和内存对齐相关的属性和文件对齐(FileAlignment)类似,也是取决于扩展PE头中的一个成员
再次给出扩展PE头在C中的定义:
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;
找到结构体中的SectionAlignment成员,会发现它就在FileAlignment(文件对齐)成员的上面
知道了由来以后,验证一下
前面已经得知了扩展PE头的首地址为400108
从400108开始往后找32个字节(中间间隔了1个WORD,2个BYTE,7个DWORD,即1*2+2*1+7*4=32)
SectionAlignment的地址为:0x400108+32=0x400000+264+32=0x400000+296=0x400128
SectionAlignment:
SectionAlignment为00 10 00 00=0x1000(小端存储)
前面得到的头大小和为0x2B0显然不是SectionAlignment的整数倍
头大小应该设置为SectionAlignment的整数倍:(2B0/1000+1)*1000=1000
PS:这里的头大小不会被设置到SizeOfHeaders,因为SizeOfHeaders为文件对齐专用
块
在非运行态中,块的起始位置由PointerToRawData决定,且PointerToRawData必须为FileAlignment的整数倍
但在运行态中,块的起始位置则并不由PointerToRawData决定,PointerToRawData和SizeOfHeaders一样都为文件对齐专用
运行态块存储涉及内容较多,这里只查看一下第一个块的起始地址、结束地址和大小,不作具体探究,其它留作之后的笔记
块的起始地址
第一个块的起始地址取决于(DOS部首+PE文件头+块表)的总大小进行内存对齐后的结果
第一个块的起始地址=0x400000+0x1000=0x401000
块的结束地址
第一个块的结束地址=0x59B000
块的大小
块的大小=块的结束地址-块的起始地址=0x59B000-0x401000=0x19A000(满足内存对齐)
运行态时,块的大小满足内存对齐,非先前的文件对齐
小总结
在运行态下:
- DOS部首和PE文件头及块表连续存储,中间没有空隙
- 而块表和块之间由于内存对齐可能会存在空隙
- 块和块之间也由于内存对齐可能会存在空隙
相关数据结构成员:
数据结构成员 |
所属数据结构 |
说明 |
SectionAlignment |
扩展PE头 |
内存对齐 |
各结构的起始和结束位置:
结构 |
起始地址 |
结束地址 |
大小 |
DOS部首 |
400000 |
4000F0 |
0xF0=240 |
PE文件头 |
4000F0 |
4001E8 |
0xF8=244=224+40 |
块表 |
4001E8 |
4002B0 |
0xC8=200=5*40 |
前三个结构 |
400000 |
401000 |
0x1000(内存对齐后) |
第一个块 |
401000 |
59B000 |
0x19A000(内存对齐后) |
对比PE两种状态
本笔记主要针对PE两种状态中的文件对齐和内存对齐进行比较,其它的内容暂时没有涉及,将在后续笔记里陆续提到
相同点
无论是在运行态还是在非运行态,DOS部首、PE文件头、块表块表均为连续存储,中间没有空隙
第一个块表的首地址都受DOS部首大小+PE文件头大小+块表大小影响,都需要对齐
块和块之间也都需要对齐
不同点
运行态和非运行态的起始地址不同
在非运行态中,块表和块之间的空隙由文件对齐产生,块和块之间的空隙由文件对齐产生
在运行态中,块表和块之间的空隙由内存对齐产生,块和块之间的空隙由内存对齐产生
非运行态和运行态映射图
附件
附上本笔记中分析的EverEdit文件:点我下载