大家好,欢迎来到IT知识分享网。
PE结构简述
Windows操作系统是只能运行以内存4D 5A开头,翻译是MZ的可执行文件,该文件也叫做PE结构文件,是以.exe,.sys,.dll等等作为后缀的文件。而不同的操作系统能运行的可执行文件都是各自特有的,比如Linux可运行的可执行文件叫做elf结构文件
在以后pe结构文件的知识中,我们均以.exe文件进行pe文件的演示与讲解
32位计算机中任何一个.exe文件在运行时所在的内存叫做虚拟内存,这些文件都有自己独立的4GB的虚拟内存,其中2GB用于应用程序,其余2GB用于操作系统
.exe文件有两种状态:
一种是在硬盘(未运行时)的状态:在硬盘上的.exe文件打开后内存首地址是从0开始的(逻辑地址)
另一种是在内存中(运行时)的状态:正在运行时的.exe文件在内存(即虚拟内存)中的内存首地址是从0x开始的(物理地址),该内存首地址根据不同的文件有不同的地址
我们常见的txt文件是可以打开运行它,但它并不是可执行文件,这是因为该文件实际是在exe程序中(比如word)打开的
pe结构分节
PE结构文件内容是分节的,每一节之间以0作为空白区域,其分节有以下几种原因:
1.节省硬盘空间
在此之前我们应该了解到在早期的编译器编译的文件,其硬盘对齐是200h字节,这是因为早期硬盘很昂贵,为了节省硬盘空间导致的。而内存对齐是1000h字节。但在现代的编译器中编译器编译的文件在硬盘和内存中的对齐都是1000h字节
我们首先观察一下早期编译器生成的.exe文件(如notepad.exe)在硬盘和内存中的存储分布
如图可知,一个pe文件在存储中分为了多个段,这些段也叫做节。图中左侧是pe文件在硬盘中的存储分布,分布空间紧密。右侧是pe文件在内存中的存储分布,其分布空间稀疏,这是由于pe文件从硬盘到内存有一个拉伸的过程。由图对比可知在硬盘上存储空间更小。如此看来是符合节省硬盘空间的原因的
现在我们观察一下现代编译器生成的pe文件在硬盘和内存中的存储分布:
同一份文件在内存中和在硬盘中的内容是一样的,但是他们文件内存起始位置是不一样的,它们分节之间的空白区域大小是一样的,这似乎与我们之前所作的文件硬盘与内存分布图有所不同,这是由于对齐的机制。对齐是为了提高读写速度,比如一本书一章的结尾可能会在新的一页留一个‘完’字,这个字会单独占有一页,而不是在下一章的内容一起占据同一页,这样的设置让我们更容易的去查找我们想要的内容
到目前为止看来,pe结构分节可能并不只是因为节省硬盘空间
2.多开,比如我们挂几个
假设我们现在有一个.exe文件(比如),它的文件结构存在只读数据和可读可写数据部分,如下图表示,其每个部分都占有100兆的内存
此时我们多开一个该文件,内存分布是这样的:系统只会为我们多创建一个可读可写的数据的内存
这是我们可以发现,正是因为pe文件结构分节,我们才能占用更少的内存,发挥更大的作用
PE文件信息
如下是早期编译器编译的pe文件在硬盘和内存中的结构图
无论哪个块的内容有多大,它所被分配的大小在硬盘中都只有200h,内存中1000h,而块中的数据不论在硬盘还是内存中都是一样的,只是因为内存1000h对齐的原因,所以需要用0填充空出来的区域
如图可知:文件中我们能看到的数据(即.data,.text,.rdata)都被存储在块中,每个块在硬盘和内存中都被分配了200h和1000h的大小。
每一个节,都有一个对应的节表(图中块表)用于记录节的相关信息,如每一个节的概要性信息。这些节表是挨着存放在一个指定的区域的,所以广义上我们称这片区域为节表。
除此以外,pe文件还有两个结构:PE文件头和DOS头,这两个结构记录了该pe文件的概要性信息和特征:比如在内存中拉伸后占多大空间,或此程序启动后要分多大的堆、堆栈等
手动解析pe文件
该部分内容我们学习如何手动查找DOS头,NT头
DOS头和NT头中指定位置和宽度的数据都规定了不同含义,图中左边一列地址是相对于DOS头或PE文件头起始地址的的地址,如图所示,该图也是完整的pe结构图
DOS头
一.DOS头的作用
1.我们解析一个文件时会看最开始的两个字节(e_magic)是不是4D 5A(MZ),用于判断该文件是不是pe文件
2. 找到DOS头的最后4字节数据(e_Ifanew),它指向真正的PE文件开始的地址
3.其他DOS头中的数据可以不用理会,这是因为DOS头最初是给16位操作系统使用的,对于32位系统,DOS的作用就是上述两个
从DOS头结尾到PE签名(即NT头开始)之间,有一些空出来的空间。这个空间用于存放不同的编译器存放不同的数据,对于我们来说其实就是一些垃圾数据,而且程序本身也不会使用到这块空间。所以我们可以在这个空间加入我们自己的数据,并且该数据随着文件一起装入内存中,并分配了内存地址。因此虽然程序本身运行时不会使用这块空间,但我们可以利用一些方法访问该内存,如指针
二.手动解析DOS头
我们将ipmsg.exe程序用winhex打开,根据DOS头的结构来分析数据,找出DOS头对应的字节代码(DOS头大小为64字节,十六进制为0x40)
注意:winhex显示的文件数据是按不同含义的字段宽度顺序存的,并且其数据以小端序排列
如图便是我们打开程序所显示的文件的内存数据,接下来我们将按照DOS的结构,依次查找每个成员所对应的数据代码
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,此处是4个字节数组
0x24 WORD e_oemid; //0x0000
0x26 WORD e_oeminfo; //0x0000
0x28WORDe_res2[10]; //0x0000000000000000000000000000000000000000
0x3c DWORD e_lfanew; * //0x000000e0 表示真正的PE文件开始地址为0xe0,即PE签名所在地址
};
NT头
紧接着DOS头便是NT头,现在我们开始讲解NT头
NT头是由三部分组成:PE签名,标准PE头,可选PE头。在NT头中,首先是PE签名字段然后是标准PE头,最后紧跟着就是可选PE头
现在我们开始寻找NT头:
1.找PE签名
我们之前在找DOS头时,DOS头以0x000000e0结尾, 指向了左侧地址e0的地方,从图中可知,e0的地址数据为5045,即最右侧的PE两字,即PE签名。这种现象正好说明了此处是pe文件真正开始的地方,即NT头开始,但这个e0并不是一直固定的。
NT头不直接挨着DOS,而是在DOS头的e_lfanew数据指向e0地址开始的原因是,从DOS头结尾到NT头开始(即PE签名字段)之间,不同的编译器会存放不同的数据,但是对于我们来说就是一些垃圾数据,而且程序本身也不会使用到这块空间。所以我们可以在这个空间加入我们自己的数据,并且该数据随着文件一起装入内存中,并分配了内存地址。因此虽然程序本身运行时不会使用这块空间,但我们可以利用一些方法访问该内存,如指针
如下我们将对应PE签名结构体在上图所对应的内存地址进行展示
struct _IMAGE_NT_HEADERS {
0x00 DWORD Signature; //0x00004550 即PE的签名占4字节
0x04 _IMAGE_FILE_HEADER FileHeader; //结构体中存在结构体类型的数据,此处是标准pe头
0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader; //此处可选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可选头(大小不确定,在32位平台下为E0,在64位平台下位F0,需要根据标准PE头中的SizeOfOptionalHeader的值来判断),如下便是可选pe头对应上图的地址
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; * //0x00
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; * //0x00
0x4c DWORD SizeOfStackCommit; * //0x00001000
0x50 DWORD SizeOfHeapReserve; * //0x00
0x54 DWORD SizeOfHeapCommit; * //0x00001000
0x58 DWORD LoaderFlags; //0x00000000
0x5c DWORD NumberOfRvaAndSizes; //0x00000010
0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16]; //这个先不分析
};
作业
本次作业我们以notepad为例,进行讲解
如图便是我们notepad的硬盘内存
1.找出所有DOC头数据,并统计DOC头大小.
struct _IMAGE_DOS_HEADER {
0x00 WORD e_magic; 0x5A4D
0x02 WORD e_cblp; 0X0090
0x04 WORD e_cp; 0X0003
0x06 WORD e_crlc; 0X0000
0x08 WORD e_cparhdr; 0X0004
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 WORDe_res2[10]; 0X00000000000000000000
0x3c DWORD e_lfanew; 0X000000F0
};
64个字节
2.找出pe签名
0x00 DWORD Signature; 0X00004550
3.找出所有标准PB头数据,并统计标准PB头大小.
struct _IMAGE_FILE_HEADER {
0x00 WORD Machine; 0X8664
0x02 WORD NumberOfSections; 0X0007
0x04 DWORD TimeDateStamp; 0X4678EC68
0x08 DWORD PointerToSymbolTable; 0X00000000
0x0c DWORD NumberOfSymbols; 0X00000000
0x10 WORD SizeOfOptionalHeader; 0X00F0
0x12 WORD Characteristics; 0X0022
};
20字节
4.找出所有可选PB头数据,并统计可选PE头大小.
struct _IMAGE_OPTIONAL_HEADER {
0x00 WORD Magic; 0X020B
0x02 BYTE MajorLinkerVersion; 0X0E
0x03 BYTE MinorLinkerVersion; 0X1E
0x04 DWORD SizeOfCode; 0X00028000
0x08 DWORD SizeOfInitializedData; 0X00031000
0x0c DWORD SizeOfUninitializedData; 0X00000000
0x10 DWORD AddressOfEntryPoint; 0X000019A0
0x14 DWORD BaseOfCode; 0X00001000
0x18 DWORD BaseOfData; 0X
0x1c DWORD ImageBase; 0X00000001
0x20 DWORD SectionAlignment; 0X00001000
0x24 DWORD FileAlignment; 0X00001000
0x28 WORD MajorOperatingSystemVersion; 0X000A
0x2a WORD MinorOperatingSystemVersion; 0X0000
0x2c WORD MajorImageVersion; 0X000A
0x2e WORD MinorImageVersion; 0X0000
0x30 WORD MajorSubsystemVersion; 0X000A
0x32 WORD MinorSubsystemVersion; 0X0000
0x34 DWORD Win32VersionValue; 0X00000000
0x38 DWORD SizeOfImage; 0X0005A000
0x3c DWORD SizeOfHeaders; 0X00001000
0x40 DWORD CheckSum; 0X0005C43F
0x44 WORD Subsystem; 0X0002
0x46 WORD DllCharacteristics; 0XC160
0x48 DWORD SizeOfStackReserve; 0X00080000
0x4c DWORD SizeOfStackCommit; 0X00000000
0x50 DWORD SizeOfHeapReserve; 0X00011000
0x54 DWORD SizeOfHeapCommit; 0X00000000
0x58 DWORD LoaderFlags; 0X00
0x5c DWORD NumberOfRvaAndSizes; 0X00000000
0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16]; 这个先不分析
};
因NumberOfRvaAndSizes值为0,则DataDirectory[16]成员没有结构体,所以该可选pe头96字节。但不同的文件不同分析。
最后我们利用petool进行检查
由于该工具具有一定的bug,所以仅供参考。但由于绝大部分都可以匹配的上,所以可以判断我们手动查找dos头和nt头无误
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/118503.html











