查看原文
其他

PE 文件浅析

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:16004488


微信公众号:计算机与网络安全

ID:Computer-network

一个可执行文件不但包含可执行的二进制码,还会有例如菜单结构、图标、位图、字符串等信息。PE文件格式很好地规定了这些信息在文件中是如何组织的。因此当程序被执行时,系统便会按照PE文件的格式约定准确地定位文件中各种类型的资源,并将其准确地装入内存的不同区域中。那么PE文件究竟是如何组织存放这些资源的呢?这就要讲到PE文件的格式了。


一、PE文件格式


下面来简略地了解一下PE文件的构成。

为了便于大家形象地理解PE文件结构,先来看看图1。

图1  PE文件整体结构概览

图1中各部分含义如下。


DOS MZ header:又称DOS文件头或DOS MZ文件头,它是一段以关键字MZ开头的数据,偏移量3C处包含着PE文件头的起始位置信息。


DOS stub:即DOS加载模块这个区块以一段This program cannot be run in DOS mode为标志,当运行环境不匹配时则会弹出这句话。对于WIN32位的操作系统来讲,它存在的意义不大,完全可以删除。


PE header:是需要我们着重研究的PE文件头,它是一段以关键字PE为开头的数据,默认大小为224字节,里面包含着许多信息,不过前期对我们有用的就是描述自身大小的一个字段。


Section table:区段表,又称节表,这是一段记录着整个文件中区段的大小、位置与属性等信息的表。


Section 1:区段,又称为节,可以将其理解为一个个存放数据的抽屉,每个抽屉都有自己不同的名字,通过名字就可以判断出里面包含着什么样的数据。由于这些区段的数量有可能会不止一个,所以使用Section 2、Section……、Section n来表示剩余部分。


根据上面的介绍可以感觉到,PE文件的构成非常科学,它将文件分成了若干个区段(Section),不同的资源被放在了不同的区段中。一个典型的PE文件包含的区段如下。


.text:存放可执行的二进制机器码,这也是免杀的主要战场。


.data:初始化的数据块,比如全局变量等。


.idata:可执行文件所使用的动态链接库等外接函数与文件信息。


.rsrc:存放程序的资源,如图标、菜单等。


虽然我们可以根据区段名来判定各区段的用途与功能,但是这并不可靠,因为在编译程序时区段名等信息可以被任意更改或指定,虽然大多数情况下没有人那么做。

通过以上的介绍,您应该对PE文件有了一个比较初步的了解。除此之外,对于免杀技术,还有输入表也是我们必须了解的,这也是比较复杂的一部分内容。


输入表又称导入表,要想了解输入表,还要从导入函数讲起。


我们知道,如果一个程序要运行的话,它执行的就是文件的内部代码,而导入函数恰恰不属于这个定义,也就是说,虽然导入函数代表的是能被程序调用执行的代码,但其执行的代码并不在程序中,往往来源于第三方文件。这些函数的真正可执行代码位于某些DLL文件中,调用者的程序中只保留了一些调用这些代码的函数信息,包括需要调用的函数名与DLL文件的名称等。但是对于这些硬盘上静态的PE文件来说,在自己被映射到内存之前,是无法得知这些导入函数会在哪个地方出现的,只有当文件被装入到内存后Windows才会将相应的DLL文件装入,并将导入函数与DLL中的实际函数地址联系起来,这便是“动态链接”的概念,也就是DLL文件会被称为“动态链接库文件”的原因。


那么PE文件在被装入内存中究竟会产生怎样的变化,又要经过一个怎样的流程呢?这就要先简单地向大家介绍一下虚拟内存了。


二、虚拟内存的简单介绍


计算机中的内存分为物理内存和虚拟内存,一般情况下,物理内存对于我们是不可见的,而且其原理也非常复杂,需要进入内核层(Ring0)才可能与物理内存打交道。通常我们所看到的全都是虚拟内存,如图2所示。

图2  虚拟内存映射关系

通过图2不难发现,系统为每一个正在运行的进程都分配了4GB的虚拟内存,在虚拟内存与物理内存之间是虚拟内存管理器控制着它们之间的调度,虚拟内存与进程都是用户层的概念,因此我们可以轻易接触之,而虚拟内存管理器则处于用户层与系统内核层之间,物理内存则完全处于系统内核层。


我们来粗略地算算,就算是一般的家用计算机同时运行的进程最少也要有20个,那么就需要80GB的虚拟内存了,但是我们真正的物理内存可能只有512MB,使用这么点空间怎么能满足那么巨大的需求呢?就算是虚拟内存,这相差的也未免有些悬殊了。其实不然,虚拟内存只是进程的一笔虚拟财富,或者说是虚拟空间,只有当需要进行实际的内存操作时,虚拟内存管理才会将“虚拟地址”与“物理地址”联系起来。


举一个通俗些的例子:进程就相当于公民,内存管理器就相当于政府,物理内存就相当于这个国家的实际空间,虚拟内存就相当于这个国家对公民依法律规定可用空间的总大小。


进程不使用虚拟内存时,这些虚拟内存只是一些地址,是虚拟存在的,比如人们不去旅游或做其他事情时,这些空间资源都是公用的,是不属于他以及某一个人的;而进程使用虚拟内存时,虽然它理论上可以应用4GB的虚拟内存空间,但是内存管理器只会将它目前所需要的虚拟内存地址映射到物理内存地址上,而且它们之间没有什么必然的联系,比如人们想要出游,虽然他可以到全国任何一处游览并暂时居住,但是政府只保证他得到目前所去的地点的游览权,而不会因此将全国的游览空间与住宿空间向其开放(并且他目前所浏览的景区与全国可以浏览的景区也没有必然的联系)。操作系统的实际物理空间虽然远远小于进程的虚拟内存空间之和,但仍能正常调度,这就像全国的实际可用空间虽然远远小于13亿公民理论可使用的生存空间之和,但是政府仍然可以正常调度一样。


需要注意的是,Windows系统中还有一个叫硬盘虚拟内存的概念,如图3所示。

图3  硬盘虚拟内存

所谓的虚拟内存,其作用就是将硬盘划分出一块区域,使系统将其当作物理内存来使用。因为它不是真真正正的内存,而是由硬盘虚拟出来的,所以将其称为“虚拟内存”。为了便于区分,我们将其称之为“硬盘虚拟内存”。硬盘虚拟内存在系统中所扮演的角色与真正的物理内存是一样的,同属于“这个国家的实际空间”,只不过这块空间没有在地球上,而是在火星上而已(一块属于这个国家的飞地)。


因此大家不要将这两个虚拟内存的概念混淆。在知道了虚拟内存的概念后,下面就可以介绍PE文件与虚拟内存之间的映射了。


三、PE文件的内存映射


在进行免杀时,我们最经常接触的就是寻找文件偏移地址与寻找内存地址这两项工作,而正是这两项简单的工作能帮助大家更好地理解这部分内容。


如果想理解PE文件与虚拟内存之间的映射关系,我们首先需要学习以下重要的概念。


文件偏移地址(File Offset):PE文件数据在硬盘中存放的地址就称为文件偏移地址,例如我们使用WinHex查看文件时的Offset就是文件偏移地址。所谓的文件偏移地址就是指文件在磁盘上存放时相对于文件开头的偏移。


装载基址(Image Base):装载基址是指PE文件装入内存时的基地址,如果非要将其与文件偏移地址关联,我们就可以理解为“文件偏移地址的装载基址为0x00000000”,当然这是错误的,但是这样更有利于您的理解。一般情况下,EXE文件的装载基址都为0x00400000,而DLL文件一般都是0x10000000,但这并不是绝对的,都是可以更改的,特别是DLL文件更是如此。


虚拟内存地址(Virtual Address,VA):虚拟内存地址就是指PE文件被装入内存之后的地址。


相对虚拟地址(Relative Virtual Address,RVA):相对虚拟地址指的是在没有计算装载基址的情况下的内存地址。


虚拟内存地址、装载基址与相对虚拟地址之间的关系如下:

我们来仔细看一下图4。

图4  PE文件映射到虚拟内存

由图4不难看出,PE文件在映射到虚拟内存后是有很大变化的。首先,PE文件的0字节(第一个字节)所对应的虚拟内存地址为0x00400000,这个就是装载基址(Image Base)了。


我们知道,文件偏移地址(File Offset)是相对于文件开始处0字节的偏移,而相对虚拟地址(RVA)则是相对于装载基址0x00400000处的偏移。由于操作系统在装载PE文件时仍然是按照PE文件中的各种数据结构来映射的,所以说文件偏移地址与相对虚拟地址(RVA)是有一定的相似之处的。它们之间的差异则是由于数据单位存放的位置不同而造成的。


首先,PE文件的数据一般是以0x200字节为基本单位进行组织存放的。当一个数据区段(Section)不足0x200字节时,余下的空间则用“00”填充。当一个数据区段超过0x200字节时,下一个0x200数据块将被分配给这个空间使用,余下的空间仍然是用“00”填充。因此PE文件的区段大小永远是0x200的整数倍。其次,当PE文件装入内存时,PE文件的数据存放一般将以0x1000字节为基本单位进行组织存放。其分配标准与磁盘数据组织方式一样,不足0x1000字节的则用“00”填充,超过0x1000的就新分配一个0x1000的数据块。因此内存中的区段大小永远是0x1000的整数倍。


下表展示的是区段信息,可有助于大家理解。

那么,我们怎么样才能将虚拟内存地址转换为文件偏移呢?


由于区段装入到内存之后的偏移与文件偏移存在差异,所以当我们进行文件偏移与虚拟内存地址之间的换算时,就要看所转换的地址位于第几个区段内了。由此可以得出以下公式:

计算出RVA后我们就知道参与运算的VA属于哪一个区段了,如果这个值位于0x0F000~0x18000之间,那么就是属于.data区段的,如果大于等于0x18000,那么就是属于.rsrc区段的。


知道位于哪个区段后,我们就可以用这个计算后的RVA减去区段的起始虚拟偏移,算出VA相对于此区段的起始偏移量,最后加上此区段的文件偏移即可得出VA的文件偏移地址了。具体推导如下:

由此我们得出以下最终结果:

举一个例子,假如我们要由一个PE文件虚拟内存地址算出一个文件偏移地址,现在知道此PE文件的Image Base为0x00400000,PE区段的情况为上表中所示,已知的虚拟内存地址为0x00411210。


RVA=0x00411210-0x00400000=0x11210,由于0x11210小于.rsrc的起始虚拟偏移,大于.data的起始虚拟偏移,因此我们判定此数据位于.data内。


由上表中我们知道,区段.data的起始文件偏移为0x600,起始虚拟偏移为0xF000。


因此可以得出文件偏移Offset=0x600+(0x11210-0xF000)=0x11810。


就这样,我们计算出了0x00411210这个虚拟内存地址所对应的文件偏移是0x00011810。


到此,PE文件相关的知识就告一段落了。

微信公众号:计算机与网络安全

ID:Computer-network

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存