一、引入PE结构
1.硬盘上的与加载到内存的文件结构异同
- 通过昨天的作业,我们可以得出在硬盘上的.exe文件的结构和正在运行加载到内存中的.exe文件结构是稍有区别的:
- 在硬盘上的exe打开后首地址是从0开始的(逻辑地址);但是正在运行时在内存中是从0x1000000开始的(物理地址)
- 还可以发现最开始的一大段数据都是相同的,直到出现了空白区(一堆00),空白区过后又是一大段数据…
- 在硬盘上的文件空白区比运行时在内存中的空白区小,但是两者的结构都满足这种分节的方式(数据–空白区–数据–空白–…)
2.可执行文件与不可执行文件
- 我们发现像.exe、.sys、.dll这种可执行文件的PE结构开头都是
4D 5A
;但是对于.jpg、.txt、.doc等文件不是可执行文件,结构开头是不统一的 - 如何理解不是可执行文件双击后可以打开:
- 比如我们平时在电脑上打开一个.txt文件,发现双击它后它就打开了,但是这种文件不是可执行文件,是没办法运行的,.txt文件之所以能被打开,是因为notepad.exe这个程序运行后帮我们打开了.txt的文件。就像WPS这种可执行软件,它可以帮我们打开其他的.doc、.ppt等文件
- 综上,不可执行文件是无法单独运行的,可执行文件才是真正能执行的文件
- 每一个不同的操作系统,都有自己规定的文件格式,只有满足规定格式的文件,才能在此操作系统上运行(所以在不同的操作系统上存储的文件格式不尽相同)
- 所以这种可执行文件才有PE结构,对于.txt等文件是没有PE结构的
二、PE结构
1.PE文件的两种状态
- 一种是在硬盘(外存)中的状态;一种是在内存中(运行时)的状态
2.分节(段)
-
PE文件在硬盘和内存中都是分节存储的。把数据分成一段一段的
比如我们开始观察到的,一段数据,再是一段空白0,再是一段数据,然后又是一堆0…
-
为什么需要分节:
-
节省内存空间:
- 举例:一个应用程序多开,分节可以按照每一段功能的不同分节,比如这一段数据是可读的,那一段数据是可读可写的。现在运行了一个QQ应用程序登录大号,假设我们把此程序在内存中的数据简化成2段,一段只读,一段可读可写;如果现在又运行了一个QQ程序登录了我的小号。假如没有分段,开启了两个qq.exe程序,就需要占用两个同样大小的内存。如果分节了,由于只读的这一段数据多个程序是可以共用的,因为这两个进程都无法对其内容进行修改,且都是相同的,只需要读取当中的数据,所以共用一个即可;只需要把每个进程可能需要修改的可读可写段再在装入在内存中即可。所以每开一个小号,只需要把可读可写段装入内存即可,可以节省内存很多空间。
-
节省硬盘空间:文件在内存中段与段之间的空隙很大;而文件在硬盘上段与段之间的空隙比较小,即节省了硬盘空间(这一结论只对以前的编译器成立,即内存对齐与硬盘对齐不相等时成立。详见下面的对齐讲解)
但是为什么不节省内存空间呢?明明内存条更贵
- 我们要知道这里的文件运行时所在内存和我们说的内存条不是一个概念,任何一个exe文件在32位计算机上运行时都有自己独立的4GB(232,即寻址范围最大是4GB)虚拟内存----其中有2GB是供应用程序使用的,另外2GB是操作系统用的。
- 我们可以想象成凡是运行后的程序虚拟上会有这样的4GB内存结构,但是实际上程序的数据都要经过操作系统帮我们管理按照特定的方式存到真实的内存条中
-
3.硬盘对齐与内存对齐
1)硬盘对齐内存对齐粒度不相等
-
我们打开xp上的notepad.exe,因为编译器很老的原因,它选择了节省硬盘放弃查找速度(因为以前硬盘的存储空间很小,如果都按照在内存中这样空隙这么大的原样存储到硬盘上,就会很浪费),所以它规定内存对齐粒度为1000h,硬盘对齐粒度为200h,这样就达到了节省硬盘空间的效果(硬盘对齐又称为文件对齐)
比如如果一个数据段在内存中的存储起始地址为0x100000,大小为0x150字节,由于为这一段大小没有1000h字节,那么下一个数据段只能空出来850h大小的内存,从0x101000地址开始存储,后面以此类推
-
优点:节省了硬盘空间
-
缺点:减低了硬盘、内存之间数据相互传输的读写速度,因为内存和硬盘的对齐粒度不同,需要换算,定位查找就需要花费一定的时间
2)硬盘对齐内存对齐粒度相等
-
现在科技的发展,硬盘的存储空间很大,编译器为了更快的追寻读写速度,舍弃一点硬盘的存储空间,规定内存对齐粒度和硬盘对齐粒度都是1000h。此时文件在内存中和硬盘中的存储状态如下图:
-
优点:由于对齐粒度一样了,当把文件从硬盘装入到内存中时可以省去很多运算,只需要确定好首地址
-
缺点:浪费了一定的硬盘空间
-
误区:如果一个可执行文件的文件对齐和内存对齐相同的话,不是说不管在硬盘上还是文件中它的每一个节的起始偏移地址都是相同的,即原封不动的从硬盘到内存/从内存到硬盘。比如ipmsg.exe飞鸽程序,它的文件对齐粒度和内存对齐粒度都是0x1000,我们发现此程序有四个节,第一个节在文件还是内存中的偏移起始地址都是0x1000,第二个节在文件还是内存中的偏移地址都是0x22000;第三个节同理;但是第四个节会发现在文件和内存中的偏移起始地址不一样了,原因是第三个节中可能含有未初始化数据,内存中大小(VirtualSize)会大于文件中对齐后的大小(SizeOfRawData),那么内存中第三个节起始偏移地址0x27000 + 内存中大小0x884C = 0x2F84C,由于要满足内存对齐,则第四个节应该从0x30000偏移地址处开始存储;而硬盘上第三个节起始偏移地址0x27000 + 文件对齐后大小0x5000 = 0x2C000,所以第四个节应该从0x2C000偏移地址处开始存储。所以即使一个可执行文件的内存对齐和文件对齐一样,他们也不是原封不动的照搬,千万不能有这个误区,还是要具体分析
现在看理解不了没事,后面学到day34再转头来看看
4.记录文件信息的PE结构
-
PE文件中有很多个节,那每个节在文件中从哪里开始?有多大?在内存中从哪里开始,有多大?由谁记录呢?PE已经规定了这些节的相关信息都记录在节表
每一个节都有一个对应的节表,只是这些节表是挨着存放在一个指定的区域的,所以广义上我们称这片区域为节表
-
还有两个结构:PE文件头和DOS头,这两个结构记录了关于此可执行文件的概要性信息和特征:比如在内存中拉伸后占多大空间,或此程序启动后要分多大的堆、堆栈等
-
所以一个文件的PE结构除了有存储数据的节,还要有存储文件和节的相关信息及特征的结构
5.PE文件结构组成部分
-
综上那么一个文件完整的PE结构,如下图:
-
即一个可执行文件虽然是一堆二进制数,但是这些数应该满足PE结构,即所有的可执行文件都是有DOS头,NT头,节表,.idata节,.text节,.data节等节这几部分构成
-
而NT头又是由三部分组成:PE签名,标准PE头(PE文件头),可选PE头
-
每一个结构都是由一定或者不定的长度数据组成,这些结构中也规定了哪个或几个字节的数据表示的含义是什么
三、手动解析DOS头和NT头
今天我们先解析DOS头和NT头部分
1.DOS头与NT头整体框架
-
DOS头和NT头中指定位置和宽度的数据都规定了不同含义,图中左边一列地址是相对地址,即这个字段的地址是相对于DOS头或者PE文件头起始地址的地址
比如文件开始的地址0x0(逻辑地址)往后数2字节(WORD)宽度的数据就表示e_magic;接着从0x02开始往后的两个字节数据表示的信息是e_cblp,以此类推
-
NT头是由三部分组成:PE签名,PE文件头,PE可选头。图中为了更清晰的区分开不同的PE组成部分,就单独把PE文件头和PE可选头拿出来展示,其实是很顺溜的往下排的,即NT头中,PE签名字段完了就是PE文件头,PE文件头完了紧跟着就是PE可选头
可以理解成NT_HEADERS结构体中的成员有4字节的Signature、FILE_HEADER结构体、OPTIONAL_HEADER结构体。所以NT头中的字段应该为:Signature、Machine、NumberOfSections…Characteristics、Magic…
2.找出DOS头数据并计算大小
1)DOS头的作用
-
我们解析一个exe文件时会看最开始的两个字节(e_magic)是不是
4D 5A
(MZ) -
找到DOS头的最后4字节数据(e_Ifanew),去找真正的PE文件开始的地址
其他DOS头中的数据可以不关心,因为DOS头最初是给16位操作系统使用的,对于32位系统,DOS的作用就是上述两个
-
从DOS头结尾到PE签名(即NT头开始)之间,为什么留出来一些空间?
- 不同的编译器会往里塞一些不同的数据,大小和内容都是不同的,取决于编译器,而且程序也不会使用到这块空间。但对于我们来说其实就是一些垃圾数据,想往里放什么就放什么,且大小是不确定的,但是我们也可以在这做手脚
- 那么既然想放什么放什么,我们其实可以往里赛我们想塞的数据,既然属于PE文件的一部分,那么这段空间肯定也会随着文件一起装入内存中,既然装入内存中了,就有了分配的内存地址,那么就可以想办法让程序去访问这个地址中的数据,所以即使程序自身运行时不会使用这块空间,但是我们可以想办法访问(想想函数指针那里)
2)手动解析DOS头
-
我们将ipmsg.exe程序用winhex打开,来分析数据,找出DOS头
注意:winhex显示的文件数据是按不同含义的字段宽度顺序存的,但是每一个含义的字段数据内容是以字节为单位倒着存的。比如DOS头中前两个字节先存e_magic表示的字段,接着再顺序存e_cblp表示的字段。但是每一个含义的字段数据是倒着存的,比如最开始的WORD宽度数据表示e_magic,内存中显式
4D 5A
,我们应该读作0x5A 4D;再比如我们知道DOS头部的最后四个字节表示e_Ifanew,内存中显式E0 00 00 00
,我们应该读作0x00 00 00 E0 -
DOS头的结构如下(大小为64字节,十六进制为0x40)
struct _IMAGE_DOS_HEADER { 0x00 WORD e_magic; * //0x5A4D MZ,即表示此文件是可执行文件 0x02 WORD e_cblp; //0x0090 0x04 WORD e_cp; //0x0003 0x06 WORD e_crlc; //0x0000 0x08 WORD e_cparhdr; //0x0040 0x0a WORD e_minalloc; //0x0000 0x0c WORD e_maxalloc; //0xffff 0x0e WORD e_ss; //0x0000 0x10 WORD e_sp; //0x00B8 0x12 WORD e_csum; //0x0000 0x14 WORD e_ip; //0x0000 0x16 WORD e_cs; //0x0000 0x18 WORD e_lfarlc; //0x0040 0x1a WORD e_ovno; //0x0000 0x1c WORD e_res[4]; //0x0000000000000000 0x24 WORD e_oemid; //0x0000 0x26 WORD e_oeminfo; //0x0000 0x28 WORD e_res2[10]; //0x0000000000000000000000000000000000000000 0x3c DWORD e_lfanew; * //0x000000e0 表示真正的PE文件开始地址为0xe0,即PE签名所在地址 };
3.找出NT头数据
以notepad.exe文件的PE结构中的数据举例
0)NT头组成
-
NT头由三部分组成:PE签名 + PE文件头 + PE可选头
struct _IMAGE_NT_HEADERS { 0x00 DWORD Signature; 0x04 _IMAGE_FILE_HEADER FileHeader; //结构体中还可以是结构体类型的数据 0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader; };
1)找PE签名
-
PE签名(4字节)
0x00 DWORD Signature; //0x00004550 即PE
2)找标准PE头
-
PE文件头(大小为20字节,0x12)
struct _IMAGE_FILE_HEADER { 0x00 WORD Machine; * //0x014c 0x02 WORD NumberOfSections; * //0x0004 0x04 DWORD TimeDateStamp; * //0x4d74bc7e 0x08 DWORD PointerToSymbolTable; //0x00000000 0x0c DWORD NumberOfSymbols; //0x00000000 0x10 WORD SizeOfOptionalHeader; * //0x00e0 0x12 WORD Characteristics; * //0x010f };
3)找可选PE头
-
PE可选头(大小不确定,需要根据标准PE头中的SizeOfOptionalHeader的值来判断)
struct _IMAGE_OPTIONAL_HEADER { 0x00 WORD Magic; * //0x010b 0x02 BYTE MajorLinkerVersion; //0x06 0x03 BYTE MinorLinkerVersion; //0x00 0x04 DWORD SizeOfCode; * //0x00021000 0x08 DWORD SizeOfInitializedData; * //0x0001b000 0x0c DWORD SizeOfUninitializedData; * //0x00000000 0x10 DWORD AddressOfEntryPoint; * //0x0001d26f 0x14 DWORD BaseOfCode; * //0x00001000 0x18 DWORD BaseOfData; * //0x00022000 0x1c DWORD ImageBase; * //0x00400000 0x20 DWORD SectionAlignment; * //0x00001000 0x24 DWORD FileAlignment; * //0x00001000 0x28 WORD MajorOperatingSystemVersion; //0x0004 0x2a WORD MinorOperatingSystemVersion; //0x0000 0x2c WORD MajorImageVersion; //0x0000 0x2e WORD MinorImageVersion; //0x0000 0x30 WORD MajorSubsystemVersion; //0x0004 0x32 WORD MinorSubsystemVersion; //0x0000 0x34 DWORD Win32VersionValue; //0x00000000 0x38 DWORD SizeOfImage; * //0x0003d000 0x3c DWORD SizeOfHeaders; * //0x00001000 0x40 DWORD CheckSum; * //0x00000000 0x44 WORD Subsystem; //0x0002 0x46 WORD DllCharacteristics; //0x0000 0x48 DWORD SizeOfStackReserve; * //0x00100000 0x4c DWORD SizeOfStackCommit; * //0x00001000 0x50 DWORD SizeOfHeapReserve; * //0x00100000 0x54 DWORD SizeOfHeapCommit; * //0x00001000 0x58 DWORD LoaderFlags; //0x00000000 0x5c DWORD NumberOfRvaAndSizes; //0x00000010 0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16]; //这个先不分析 };