PE文件架构学习(一)
PE文件结构
一.PE文件基本概念
PE文件是windows系统中遵循PE结构的文件,比如以.exe .dll .ocx .sys .com为后缀名的文件以及系统驱动文件。PE文件是微软Windows操作系统上的程序文件(可能是间接被执行的,如DLL)
官方解释链接 PE 格式 - Win32 apps | Microsoft Learn
顾名思义,世界上各种东西都有自己特定的结构,最简单和直观理解的例子就是我们自己身体的结构,由一个各个部位和器官关节来组成的,而同样的,PE文件也有着它自己所独特的结构。只有有了特定的结构之后,整个文件才能更加有序和规律地去处理代码以及数据,去执行操作等一系列的过程。
PE文件的大致宏观结构图如下:
通过上面的图示,我们可以看到一个PE文件的结构大致分为PE文件头、区块数据,(上图紫色部分为PE文件头部分,橙色部分为区块数据部分),PE文件使用的是一个平面地址空间,所有代码和数据都合并在一起,组成了一个很大的结构,区块中包含代码或数据,各个区块按页边界对齐,区块没有大小限制,是一个连续的结构,每个块都有它自己在内存中的一套属性,例如这个块是否包含代码、是否只读或可读/写,详细图如下:
其中核心部分为PE文件头、区块表、区块三部分,其中的DOS部首部分是16位程序的一个残留,而如今计算机程序都已经发展到了32位甚至64位,所以DOS部首不咋重要了。
PE文件并非就单放在磁盘中不起作用,都说了是可执行文件,肯定是要被装在到内存当中去执行的,当PE文件被装载到内存当中运行起来的时候,它会被拉伸,具体的位置表现在块中,同时磁盘上的数据结构布局和内存中的数据结构布局就是一致的。具体如下图:
而当这个PE文件被运行起来之后就会被叫做模块(Module),映射文件的起始地址称为模块句柄,也叫做基地址。在32位的Windows系统中可以直接调用 GetModuleHandle 的api以取得指向模块的指针,通过该指针直接访问该模块的内容。该函数结构如下:
1 | HMODULE GetModuleHandleA( |
调用该函数时所需的参数是一个可执行文件或者是DLL文件名字符串,如果系统找到了文件,则会返回该可执行文件或DLL文件映像所加载的基地址。(指定路径时,请务必使用反斜杠 \ ,而不是使用 /),如果该函数成功,则返回值是指定模块的句柄,失败则返回NULL。
二.相对虚拟地址
在windows系统中,PE文件被系统加载器映射到内存中,每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址(VA),EXE默认的加载基址是400000h,DLL文件默认基址是10000000h。需要注意的是基地址不是程序的入口点。 为了避免在PE文件中出现绝对内存中的地址所以引入了相对虚拟地址(RVA)的概念。RVA只是内存中的一个简单的相对于PE文件载入地址的偏移位置,而在winodws系统当中,每个系统版本的偏移量都有可能不同。例如,假设一个EXE文件从400000h处载入,而且它的代码区块开始于401000h处,故代码区块的RVA计算方法如下:
$$
目标地址 401000h - 载入地址 400000h = 相对虚拟地址 1000h
$$
而将一个RVA转换成真实的地址只是简单地翻转一下上述过程,即用实际的载入地址加上RVA,得到实际的内存地址,他们之间的关系如下:
$$
虚拟地址(VA) = 基地址(ImageBase) + 相对虚拟地址(RVA)
$$
入口点(OEP):首先明确一个概念就是OEP是一个RVA,然后使用OEP +Imagebase ==入口点的VA,通常情况下,OEP指向的不是main函数。
文件偏移地址(FOA):当PE文件储存在某个磁盘当中的时候,某个数据的位置相对于文件头的偏移量。
三.分部解析PE结构的各个部分
先用一张图来概括一下各个部分的关系:
一.DOS头部
DOS系统是相当古老的,是一个16位的操作系统(现在windows环境下的命令提示符窗口用的就是DOS的界面),前言是微软当时在设计PE文件的时候目的是为了让其兼容区间更大,兼容除了向上兼容以外还有向下兼容的需求。因此,为了能够兼容16位程序的运行,PE文件格式在设计的时候,设计了MS-DOS头部,它位于PE文件架构的最头部位置,有了它,一但程序在DOS下执行,DOS就能识别出这是一个有效的执行体。
而DOS头又称之为DOS部首,由 DOS stub 和 DOS ‘MZ’ HEADER两部分组成。DOS头结构图如下:
上图标红的两个部分尤为重要,其余部分在初步学习中可以忽略不计,分别是 e_magic 和 e_lfanew,e_magic 字段(一个字大小)的值需要被设置为5A4Dh,在ASCII表示法里它的ASCII值为“MZ”,是MS-DOS的创建者之一的名字缩写,而 e_lfanew 字段是真正的PE文件头的相对偏移(RVA),其指出了真正的PE头的文件偏移位置,占用四字节,其位于从文件开始的3Ch字节处。
下面用010(16进制编辑器)查看一下一个exe示例:
上图中E8至50之间的杂七杂八的是DOS头的一个残留,它们组成了DOS stub这个部分,其中还存了一个错误提示,是为16位的程序准备的,所以可以不用理会,可以直接在010中清除掉,我们的原程序运行也不会受影响。
二.PE文件头
紧跟着DOS stub 的是PE文件头(PE Header)。“PE Header”是PE相关结构的NT映像头(IMAGE_NT_HEADERS)的简称,(NT操作系统在windows操作系统的发展当中是相当重要的一个环节),其中包含许多PE装载器都能用到的重要字段。实际上有两个版本的IMAGE_NT_HEADER结构一个是为PE32(32位版本)可执行文件准备的,另一个是PE32+(64位版本),但二者几乎没有区别。当执行体在支持PE文件结构的操作系统中执行时,PE装载器将从上一DOS部分中的IMAGE_DOS_HEADER结构的 e_lfanew 字段中找到PE Header的起始偏移量,用其加上基址便得到了PE文件头的指针,如上图中的箭头所示。公式如下:
$$
PNTHeader = ImageBase + dosHeader -> e.lfanew
$$
PE文件头由三个部分组成,下图是IMAGE_NT_HEADER64的结构,左边的数字是到PE文件头的偏移量,而第一个字段singnature即是PE文件的标识。所以下文分为三个部分开讲。
一.Signature和第二大部分DOS头中的DOS-MZ的作用差不多,都是作为一个标记,且被填充为0x00004550,ASCII码中即为PE,DOS部分中的IMAGE_DOS_HEADER结构的 e_lfanew 字段正是指向“PE\0\0”。(具体请见上面第二幅图中)
二.映像文件头 IMAGE_FILE_HEADER 结构如下:
上图中包含了PE文件的一些基本信息,最重要的是其中一个域指出了 IMAGE_OPTIONAL_HEADER的大小,上图左侧的偏移量是基于PE文件头(IMAGE_NT_HEADERS)的。下图给出一个示例(使用studyPE查看,该工具会在文末给出):左图是从010中查看的,右图是StudyPE中查看的
1.Machine:可执行文件的目标CPU类型,不同平台的机器码不同。
2.NumberOfSections:区块的数目,也就是节表数,即上图中的0C。
3.TimeDateStamp:表示文件的创建时间。
三.可选映像头 IMAGE_OPTIONAL_HEADER
IMAGE_OPTIONAL_HEADER(可选映像头)是一个可选的机构,实际上 IMAGE_FILE_HEADER 结构不足以定义PE文件属性,因此可选映像头中定义了更多的数据。已知NT头开始处加14h处是文件属性的大小图中为E0,且蓝色区块即为文件属性全部内容,从NT头处加16h(切记是16进制数,对应到10进制的话往后数22位即可)即是文件属性的开始,如下图:
下图是 IMAGE_OPTIONAL_HEADER 结构图,左边对应的是其偏移量:
感兴趣的师傅可以对照上表去研究一下。
三.小结:
本篇大概论述了PE文件中的DOS头和PE头的基本结构,如有错误还请师傅们指正,最后贴出一个宏观查看PE整个结构的工具OpenArk,以及文中所使用到的工具PEstudy的下载链接,有需要的师傅自取。
https://wwz.lanzouj.com/b05l2evti 密码:99ax
OpenArk的使用截图如下,将选项卡选择到红框处即可拖拽使用: