1.2 二进制文件概述
1.2.1 PE文件格式
PE(Portable Executable)是Win32平台下可执行文件遵守的数据格式。常见的可执行文件(如“*.exe”文件和“*.dll”文件)都是典型的PE文件。
一个可执行文件不光包含了二进制的机器代码,还会自带许多其他信息,如字符串、菜单、图标、位图、字体等。PE文件格式规定了所有的这些信息在可执行文件中如何组织。在程序被执行时,操作系统会按照PE文件格式的约定去相应的地方准确地定位各种类型的资源,并分别装入内存的不同区域。如果没有这种通用的文件格式约定,试想可执行文件装入内存将会变成一件多么困难的事情!
PE文件格式把可执行文件分成若干个数据节(section),不同的资源被存放在不同的节中。一个典型的PE文件中包含的节如下。
.text 由编译器产生,存放着二进制的机器代码,也是我们反汇编和调试的对象。
.data 初始化的数据块,如宏定义、全局变量、静态变量等。
.idata 可执行文件所使用的动态链接库等外来函数与文件的信息。
.rsrc 存放程序的资源,如图标、菜单等。
除此以外,还可能出现的节包括“.reloc”、“.edata”、“.tls”、“.rdata”等。
题外话:如果是正常编译出的标准PE文件,其节信息往往是大致相同的。但这些section的名字只是为了方便人的记忆与使用,使用Microsoft Visual C++中的编译指示符#pragma data_seg()可以把代码中的任意部分编译到PE的任意节中,节名也可以自己定义。如果可执行文件经过了“加壳”处理,PE的节信息就会变得非常“古怪”。在Crack和反病毒分析中需要经常处理这类古怪的PE文件。
1.2.2 虚拟内存
Windows的内存可以被分为两个层面:物理内存和虚拟内存。其中,物理内存比较复杂,需要进入Windows内核级别ring0才能看到。通常,在用户模式下,我们用调试器看到的内存地址都是虚拟内存。
如图1.2.1所示,Windows让所有的进程都“相信”自己拥有独立的4GB内存空间。但是,我们计算机中那根实际的内存条可能只有512MB,怎么可能为所有进程都分配4GB的内存呢?这一切都是通过虚拟内存管理器的映射做到的。
图1.2.1 Windows虚拟内存与物理内存示意图
虽然每个进程都“相信”自己拥有4GB的空间,但实际上它们运行时真正能用到的空间根本没有那么多。内存管理器只是分给进程了一片“假地址”,或者说是“虚拟地址”,让进程们“认为”这些“虚拟地址”都是可以访问的。如果进程不使用这些“虚拟地址”,它们对进程来说就只是一笔“无形的数字财富”;当需要进行实际的内存操作时,内存管理器才会把“虚拟地址”和“物理地址”联系起来。
Windows的内存管理机制在很大程度上与日常生活中银行所起的金融作用有一定的相似性,我们可以通过一个形象的比方来理解虚拟内存。
·进程相当于储户。
·内存管理器相当于银行。
·物理内存相当于钞票。
·虚拟内存相当于存款。
·进程可能拥有大片的内存,但使用的往往很少;储户拥有大笔的存款,但实际生活中的开销并没有多少。
·进程不使用虚拟内存时,这些内存只是一些地址,是虚拟存在的,是一笔无形的数字财富。
·进程使用内存时,内存管理器会为这个虚拟地址映射实际的物理内存地址,虚拟内存地址和最终被映射到的物理内存地址之间没有什么必然联系;储户需要用钱时,银行才会兑换一定的现金给储户,但物理钞票的号码与储户心目中的数字存款之间可能并没有任何联系。
·操作系统的实际物理内存空间可以远远小于进程的虚拟内存空间之和,仍能正常调度;银行中的现金准备可以远远小于所有储户的储蓄额总和,仍能正常运转,因为很少会出现所有储户同时要取出全部存款的现象;社会上实际流通的钞票也可以远远小于社会的财富总额。
题外话:实际上,金融学、经济学、管理学中有很多概念和理论与计算机科学中的知识出奇相似。有时将这些知识互相类比一下会获得一种融会贯通的清爽。
进程所拥有的4GB虚拟内存中包含了程序运行时所必需的资源,比如代码、栈空间、堆空间、资源区、动态链接库等。在后面的章节中,我们将不停地辗转于虚拟内存中的这些区域。
提示:操作系统原理中也有“虚拟内存”的概念,那是指当实际的物理内存不够时,有时操作系统会把“部分硬盘空间”当做内存使用从而使程序得到装载运行的现象。请不要将用硬盘充当内存的“虚拟内存”与这里介绍的“虚拟内存”相混淆。此外,本书除第4篇内核安全外,其余所述之“内存”均指Windows用户态内存映射机制下的虚拟内存。
1.2.3 PE文件与虚拟内存之间的映射
在调试漏洞时,可能经常需要做这样两种操作。
(1)静态反汇编工具看到的PE文件中某条指令的位置是相对于磁盘文件而言的,即所谓的文件偏移,我们可能还需要知道这条指令在内存中所处的位置,即虚拟内存地址(VA)。
(2)反之,在调试时看到的某条指令的地址是虚拟内存地址,我们也经常需要回到PE文件中找到这条指令对应的机器码。
为此,我们需要弄清楚PE文件地址和虚拟内存地址之间的映射关系。首先,我们先看几个重要的概念。
(1)文件偏移地址(File Offset)
数据在PE文件中的地址叫文件偏移地址,个人认为叫做文件地址更加准确。这是文件在磁盘上存放时相对于文件开头的偏移。
(2)装载基址(Image Base)
PE装入内存时的基地址。默认情况下,EXE文件在内存中的基地址是0x00400000,DLL文件是0x10000000。这些位置可以通过修改编译选项更改。
(3)虚拟内存地址(Virtual Address,VA)
PE文件中的指令被装入内存后的地址。
(4)相对虚拟地址(Relative Virtual Address,RVA)
相对虚拟地址是内存地址相对于映射基址的偏移量。
虚拟内存地址、映射基址、相对虚拟内存地址三者之间有如下关系。
VA=Image Base+RVA
如图1.2.2所示,在默认情况下,一般PE文件的0字节将对映到虚拟内存的0x00400000位置,这个地址就是所谓的装载基址(Image Base)。
图1.2.2 PE文件与虚拟内存的映射关系
文件偏移是相对于文件开始处0字节的偏移,RVA(相对虚拟地址)则是相对于装载基址0x00400000处的偏移。由于操作系统在进行装载时“基本”上保持PE中的各种数据结构,所以文件偏移地址和RVA有很大的一致性。
之所以说“基本”上一致是因为还有一些细微的差异。这些差异是由于文件数据的存放单位与内存数据存放单位不同而造成的。
(1)PE文件中的数据按照磁盘数据标准存放,以0x200字节为基本单位进行组织。当一个数据节(section)不足0x200字节时,不足的地方将被0x00填充;当一个数据节超过0x200字节时,下一个0x200块将分配给这个节使用。因此PE数据节的大小永远是0x200的整数倍。
(2)当代码装入内存后,将按照内存数据标准存放,并以0x1000字节为基本单位进行组织。类似的,不足将被补全,若超出将分配下一个0x1000为其所用。因此,内存中的节总是0x1000的整数倍。
表1-2-1列出的文件偏移地址和RVA之间的对应关系可以让您更直接地理解这种“细微的差异”。
表1-2-1 文件偏移地址和RVA之间的对应关系
由于内存中数据节相对于装载基址的偏移量和文件中数据节的偏移量有上述差异,所以进行文件偏移到虚拟内存地址之间的换算时,还要看所转换的地址位于第几个节内。
我们把这种由存储单位差异引起的节基址差称做节偏移,在上例中:
.text节偏移=0x1000-0x400=0xc00 .rdata节偏移=0x7000-0x6200=0xE00 .data节偏移=0x9000-0x7400=0x1C00 .rsrc节偏移=0x2D000-0x7800=0x25800
那么文件偏移地址与虚拟内存地址之间的换算关系可以用下面的公式来计算。
文件偏移地址=虚拟内存地址(VA)-装载基址(Image Base)-节偏移=RVA-节偏移
以表1-2-1为例,如果在调试时遇到虚拟内存中0x00404141处的一条指令,那么要换算出这条指令在文件中的偏移量,则有:
文件偏移量=0x00404141-0x00400000-(0x1000-0x400)=0x3541
一些PE工具提供了这类地址转换,Lord PE就是其中出色的一款,如图1.2.3所示。单击“PE Editor”按钮,选择需要查看的PE文件,如图1.2.4所示。
图1.2.3 LordPE使用1
图1.2.4 LordPE使用2
用这个工具可以方便地查看PE文件中的节信息,对应于前面表格中的例子,如图1.2.5所示。也可以方便地换算虚拟内存地址,文件偏移地址和RVA,如图1.2.6所示。
图1.2.5 LordPE使用3
图1.2.6 LordPE使用4