PE头字段 = DOS头 + PE标记 + 标准PE头 + 可选PE头
我们今天分析一下PE头字段中所有重要成员的含义
DOS头
DOS头中我们需要去分析的是如下两个成员:
1.WORD e_magic:MZ标记,用于判断是否为可执行文件,即如果显示4D 5A,说明该文件是一个可执行文件,如:.sys/.dll/.exe等
2.DWORD e_lfanew:NT头(PE签名)相对于文件首地址的偏移,用于定位PE文件真正开始的地址,但该值不是固定的
PE标记
该PE标记的地址即e_Ifanew指向的地址
PE标记中我们要分析的成员只有这一个:
DWORD Signature; 该值存储的是PE两个字,用来表示该文件是PE文件,因此一个可执行文件应该同时满足MZ标记和PE标记,如果这两点不满足可能被修改过,或者就不是一个可执行文件
标准PE头
标准PE头中我们需要去分析的是如下几个成员:
1.WORD Machine:用于约定该文件能在什么样的CPU上运行:如果是0x00表示能在任何CPU上执行,如果是0x14C表示能在386及后续CPU上执行
2.WORD NumberOfSections:用于告知该PE文件中分节的总数(不包括DOS头、NT头、节表):如果要新增节或者合并节,就要修改这个值。
4.DWORD TimeDateStamp:时间戳:文件的创建时间(和操作系统的创建时间无关),当程序被编译器编译时,由编译器填写的
时间戳的使用案例:比如现在要给一个.exe文件加壳,有些加壳软件不光要提供给.exe文件,还需要其对应的.map文件。.map文件中记录了此.exe文件中的所有的函数的名字、地址、参数等信息。这两个文件由编译器编译源文件时生成,.map和.exe的时间戳是一致的。如果现在这个.exe文件被反复修改了以后,.exe文件的时间戳就会改变,但是map的时间戳没有更新,这会导致exe和map不同步。此时如果加壳还是按照map中的记录信息去加壳,就可能出现错误。所以很多加壳软件在加壳之前会检查exe和map文件的时间戳是否一致
5. WORD SizeOfOptionalHeader:可选PE头的大小,该大小不确定:32位PE文件默认E0h,64位PE文件默认为F0h,但其大小可以自定义
6. WORD Characteristics:特征:其大小16位的每个位都表示不同的特征,可执行文件值一般都是0x010F。当某位为1时,表示此文件有此位对应的特征,为0时表示没有此特征
characteristic每一位表示的含义如下图:
把内存中的值读出来后,即读作0x010F,此时化成二进制,第七位省略,其他的每一位都表示一个特征,如果为1,则表示此文件有此位对应的特征;为0表示没有此特征
可选PE头
可选PE头中我们需要去分析的是如下几个成员:
- Magic:说明文件类型:如果值为0x010B,表示是32位下的PE文件;如果值为0x020B,表示是64位下的PE文件
- SizeOfCode:所有代码节大小的和,必须是FileAlignment的整数倍
比如文件只有一个代码节,大小为100h字节,如果文件对齐粒度是200h,那么会补0填充够200h字节,所以会显示200h(编译器填的);若文件有两个代码节,两个都是10字节,这个值应为400h。但是计算机发展到现在已经不使用这个值了,改了也没事,删除它程序也可以正常运行,但是现在之所以保留下来是因为向下兼容之前的文件
3.SizeOfInitializedData:已初始化数据(比如定义一个有值的全局变量,便是已初始化的数据)大小的和,必须是FileAlignment的整数倍:由编译器填写,目前计算机已经不使用这个值了,保留下来只是为了向下兼容。
4.SizeOfUninitializedData:未初始化数据(比如定义一个没有值的全局变量,便是未初始化的数据)大小的和,必须是FileAlignment的整数倍:编译器填的,现在没用了。
5.AddressOfEntryPoint:程序入口点OEP,即程序真正执行的起始地址:这个值是相对于文件基地址偏移的程序入口地址,而不是真正运行在内存中的程序入口地址。文件装入到4GB虚拟内存中的起始基地址 +相对于文件首地址偏移的程序入口地址,即imagebase + AddressOfEntryPoint,这个值才是真正运行在4GB虚拟内存中的程序入口地址。虽然该值在不同文件中有不同的值,但一般都是0x0040000开始,这是系统的默认ImageBase值。该值我们可以自定义修改,但要保证它可以执行
注意:程序入口在默认情况下一般都在.text节.code代码块当中,且OEP不是只能在.code代码块开始的位置,也可以从此块当中的任何合理位置,或者在其他节(如自定义.tttt节等)的任意合理位置,又或者在数据中。
6.BaseOfCode:代码开始的基址:即PE文件加载到内存后,所有代码的起始地址。编译器填写的,可以改,但不影响程序执行
7.BaseOfData:内存中所有数据开始的地址:编译器填的,程序运行用不到,想改就改
8. ImageBase: 内存镜像基址:该值可以看作模块之间的对齐粒度,也是程序运行装载到自己的虚拟4GB内存后的起始位置。imagebase一般都是0x00400000(具体原因参考上一篇对齐部分),这是系统的默认值,不能超过0x80000000,这是因为我们写的程序的数据只能在内存的2GB用户区中,不能占用2GB系统区。
模块的概念:一个.exe文件可能是由一堆PE文件组成的,每个pe文件都有自己的imagebase,他们共用一个4GB虚拟内存。这是因为.exe文件本身是一个PE文件,满足PE结构,但是.exe中可能还用到了很多.dll,每一个.dll也是一个PE文件,也满足PE结构,这些.dll有自己的功能和作用,当它们拼凑到一个.exe文件中时,.exe文件就有了完整的功能。这时又称.exe文件有很多模块构成,每一个.dll都是一个模块
pe文件(各个模块)不能从0开始的原因是因为内存保护,我们前面学过,free一个动态分配内存的指针后,一定要将指针指向NULL,那么当指针指向NULL后,这个指针指向的地址就是0x0,从0开始往后偏移一定范围的地址,操作系统都把他的地址上内存空了出来,那么此时指针访问这部分的数据,编译器会立马报错,也就是说限制了指针使其不能为所欲为的访问内存。
9.SectionAlignment:内存对齐粒度:可执行文件运行时装入4GB虚拟内存中的对齐粒度,一般为0x1000字节
10.DWORD FileAlignment:文件对齐粒度:可执行文件在硬盘的对齐粒度,一般为0x200字节,还有的是0x1000字节,和内存对齐粒度相等
举例说明内存对齐和文件对齐:(day27.1-PE结构概况中详细说明过)
11. DWORD SizeOfImage:内存中整个PE文件的映射尺寸:即文件运行时在4GB虚拟内存中的整个文件数据大小。该值可以比实际的值大,但必须是SectionAlignment的整数倍
12. DWORD SizeOfHeaders:所有头+节表按照文件对齐后的大小:即DOS头 + 垃圾数据 + PE签名 + 标准PE头 + 可选PE头 + 节表,按照文件对齐后的大小。必须是FileAlignment的整数倍,否则加载会出错
举例:比如一个可执行文件的所有头和节表加起来大小为0x1800字节,但是因为要满足文件对齐粒度0x1000,示意图SizeOfHeaders的值应该为0x2000,
13. DWORD CheckSum:校验和:一些重要的系统文件、驱动文件等对此有要求,用来判断文件是否被修改(但是可以修改这个值)
校验方法举例:把PE文件中的所有数据中两个字节的值(十六进制)两两相加(如值1 2 3 4,相加:1+2,3+4),最后将所有两两相加的结果再求和得到一个数,存放到checksum表示的4字节内存中,内存可溢出
14. DWORD SizeOfStackReserve:初始化时保留的栈大小 (最大值)
15. DWORD SizeOfStackCommit:初始化时实际提交的栈大小
16. DWORD SizeOfHeapReserve:初始化时保留的堆大小 (最大值)
17. DWORD SizeOfHeapCommit:初始化时实际提交的堆大小
18. DWORD NumberOfRvaAndSizes:目录项数目“如果该值是0x10,表示下面的_IMAGE_DATA_DIRECTORY DataDirectory[16]结构体有16个,一个占8字节,这些结构体用于告诉我们编译器在编译文件时往exe文件中所添加的数据。具体如下图所示:
可执行文件的读取到装入内存过程
文件装入内存流程如图所示:
1.编译器生成.exePE文件
比如我们使用VC,编写程序以后按下F7,编译器会编译生成对应的.exe可执行文件,此时编译器计算生成PE文件的所有数据,比如imagebase或者OEP等全部字段信息,最后将.exe文件保存到硬盘中
2.文件数据读到内存(FileBuffer)
当文件数据读取到内存中时,其数据存储是完全照搬文件在硬盘上的数据。此时文件的格式并非windows运行格式,所以Windows操作系统还无法运行它
3.将文件装载到内存镜像(ImageBuffer)
将文件从FileBuffer装入ImageBuffer,即将文件从imagebase开始对齐拉伸成内存对齐形式的一块内存。这个过程就是将文件装入自己的4GB虚拟内存中,称为PE loader。我们称将文件拉伸装载后的内存为imageBuffer,即内存镜像。此时文件的格式满足windows运行格式。对于硬盘对齐粒度等于内存对齐粒度的文件不需要进行拉伸
此时文件在4GB虚拟内存中的起始地址,就是imagebase,一般为0x00400000,接着就可以通过imagebase + addressofentrypoint找到文件装载到内存后真正的程序入口地址;或者用imagebase加上一些偏移地址值就可以得到文件其他内容在运行时装入4GB内存后的地址
4.操作系统将虚拟地址转化成物理地址
FileBuffer和ImageBuffer提到的所有地址都是虚拟地址,操作系统最后会将这些虚拟地址转换为物理地址,这样才算真正的装入到真实内存中。