本节书摘来自异步社区出版社《c++ 黑客编程揭秘与防范(第2版)》一书中的第6章,第6.3节,作者:冀云,更多章节内容可以访问云栖社区“异步社区”公众号查看。
c++ 黑客编程揭秘与防范(第2版)
在上一章中用od调试器调试程序时看到的地址与本章使用c32asm以十六进制形式查看程序时的地址形式有所差异。程序在内存中与在文件中有着不同的地址形式,而且pe相关的地址不只有这两种形式。与pe结构相关的地址形式有3种,且这3种地址形式可以进行转换。
与pe结构相关的3种地址是va(虚拟地址)、rva(相对虚拟地址)和fileoffset(文件偏移地址)。
va(虚拟地址):pe文件映射到内存后的地址。
rva(相对虚拟地址):内存地址相对于映射基地址的偏移地址。
fileoffset(文件偏移地址):相对pe文件在磁盘上的文件开头的偏移地址。
这3种地址都是和pe文件结构密切相关的,前面简单地引用过这几个地址,但是前面只是个概念。从了解节表开始,这3种地址的概念就非常重要了,否则后面的很多内容都将无法理解。
这3个概念之所以重要,是因为后面要不断地使用它们,而且三者之间的关系也很重要。每个地址之间的转换也很重要,尤其是va和fileoffset的转换、rva和fileoffset之间的转换。这两个转换不能说复杂,但是需要一定的公式。va和rva的转换就非常简单了。
pe文件在磁盘上和在内存中的结构是一样的。所不同的是,在磁盘上,文件是按照image_optional_header的filealignment值进行对齐的。而在内存中,映像文件是按照image_optional_header的sectionalignment进行对齐的。这两个值前面已经介绍过了,这里再进行简单的回顾。filealignment是以磁盘上的扇区为单位的,也就是说,filealignment最小为512字节,十六进制的0x200字节。而sectionalignment是以内存分页为单位来对齐的,通常win32平台一个内存分页为4k,也就是十六进制的0x1000字节。一般情况下,filealignment的值会与sectionalignment的值相同,这样磁盘文件和内存映像的结构是完全一样的。当filealig-nment的值和sectionalignment的值不相同的时候,就存在一些细微的差异了,其主要区别在于,根据对齐的实际情况而多填充了很多0值。pe文件映射如图6-15所示。

图6-15 pe文件映射图
除了文件对齐与内存对齐的差异以外,文件的起始地址从0地址开始,用c32asm的十六进制模式查看pe文件时起始位置是0x00000000。而在内存中,它的起始地址为image_optional_header结构体的imagebase字段(该说法只针对exe文件,dll文件的映射地址不一定固定,但是绝对不会是0x00000000地址)。
当filealignment和sectionalignment的值不相同时,磁盘文件与内存映像的同一节表数据在磁盘和内存中的偏移也不相同,这样两个偏移就发生了一个需要转换的问题。当知道某数据的rva,想要在文件中读取同样的数据的时候,就必须将rva转换为fileoffset。反之,也是同样的情况。
下面用一个例子来介绍如何进行转换。还记得前面为了分析pe文件结构而写的那个用messagebox()输出“hello world”的例子程序吗?用peid打开它,查看它的节表情况,如图6-16所示。
图6-16 peid显示的节表内容
从图6-16的标题栏可以看到,这里不叫“节表”,而叫“区段”。还有别的资料上称之为“区块”或“节区”,只是叫法不同,内容都是一样的。
从图6-16中可以看到,节表的第一个节区的节名称为“.text”。通常情况下,第一个节表项都是代码区,入口点也通常落在这个节表项。在早期壳不流行时,通过判断入口点是否在第一个节区就可以判断该程序是否被病毒感。如今,由于壳的流行,这种判断方法就不可靠了。关键要看的是“r.偏移”,表明了该节区在文件中的起始位置。pe头部包括dos头、pe头和节表,通常不会超过512字节,也就是说,不会超过0x200的大小。如果这个“r.偏移”为0x00001000,那么通常情况下可以确定该文件的磁盘对齐大小为0x1000(注意:这个测试程序是笔者自己写的,因此比较熟悉程序的pe结构。而且这也是一种经验的判断。严格来讲,还是要去查看image_optional_header的sectionalignment和filealignment两个成员变量的值)。测试验证一下这个程序,看到“v.偏移”与“r.偏移”相同,则说明磁盘对齐与内存对齐是一样的,这样就没办法完成演示转换的工作了。不过,可以人为地修改文件对齐大小。也可以通过工具来修改文件对齐的大小。这里借助lordpe来修改其文件对齐大小。修改方法很简单,先将要修改的测试文件复制一份,以与修改后的文件做对比。打开lordpe,单击“重建pe”按钮,然后选择刚才复制的那个测试文件,如图6-17和图6-18所示。
图6-17 lordpe界面
图6-18 重建pe功能结果
pe重建功能中有压缩文件大小的功能,这里的压缩也就是修改磁盘文件的对齐值,避免过多地因对齐而进行补0,使其少占用磁盘空间。用peid查看这个进行重建的pe文件的节表,如图6-19所示。
现在可以看到“v.偏移”与“r.偏移”的值不相同了,它们的对齐值也不相同了,大家可以自己验证一下filealignment和sectionalignment的值是否相同。
图6-19 重建pe文件后的节表
现在有两个功能完全一样,而且pe结构也一样的两个文件了,唯一的不同就是其磁盘对齐大小不同。现在在这两个程序中分别寻找一个节表中的数据,学习不同地址之间的转换。
先用od打开未进行重建pe结构的测试程序,找到反汇编中调用messagebox()处要弹出对话框的两个字符串参数的地址,如图6-20和图6-21所示。
从图6-20和图6-21中可以看到,字符串“hello world !”的地址为0x00406030,字符串“hello”的地址为0x00406040。这两个地址都是虚拟地址,也就是va。
将va(虚拟地址)转换为rva(相对虚拟地址)是很容易的,rva(相对虚拟地址)为va(虚拟地址)减去image_optional_header结构体中的imagebase(映像文件的装载虚拟地址)字段的值,即rva = va – imagebase = 0x00406030 – 0x00400000 = 0x0000 6030。由于image_optional_header中的sectionalignment和filealignment的值相同,因此其fileoffset的值也为0x00006030。用c32asm打开该文件查看文件偏移地址0x00006030处的内容,如图6-22所示。
图6-22 文件偏移0x00006030处的内容为“hello world!”字符串
从这个例子中可以看出,当sectionalignment和filealignment相同时,同一节表项中数据的rva(相对虚拟地址)和fileoffset(文件偏移地址)是相同的。rva的值是用va – imagebase计算得到的。
再用od打开“重建pe”后的测试程序,同样找到反汇编中调用messagebox()函数使用的那个字符串“hello world !”,看其虚拟地址是多少。它的虚拟地址仍然是0x00406030。同样,用虚拟地址减去装载地址,相对虚拟地址的值仍然为0x00006030。不过用c32asm打开该文件查看的话会有所不同。用c32asm看一下0x00006030地址处的内容,如图6-23所示。
图6-23 文件偏移0x00006030处没有“hello world!”字符串
从图6-23中可以看到,用c32asm打开该文件后,文件偏移0x00006030处并没有“hello world!”和“hello”字符串。这就是由文件对齐与内存对齐的差异所引起的。这时就要通过一些简单的计算把rva转换为fileoffset。
把rva转换为fileoffset的方法很简单,首先看一下当前的rva或者是fileoffset属于哪个节。0x00006030这个rva属于.data节。0x00006030这个rva相对于该节的起始rva地址0x00006000来说偏移0x30字节。再看.data节在文件中的起始位置为0x00004000,以.data节的文件起始偏移0x00004000加上0x30字节的值为0x00004030。用c32asm看一下0x00004030地址处的内容,如图6-24所示。
图6-24 0x00004030文件偏移处的内容
从图6-24中可以看出,该文件偏移处保存着“hello world !”字符串,也就是说,将rva转换为fileoffset是正确的。通过lordpe工具来验证一下,如图6-25所示。
图6-25 用lordpe计算rva为0x00006030的文件偏移
再来回顾一下这个过程。
某数据的文件偏移 = 该数据所在节的起始文件偏移 + (某数据的rva –该数据所在节的起始rva)。
除了上面的计算方法以外,还有一种计算方法,即用节的起始rva值减去节的起始文件偏移值,得到一个差值,再用rva减去这个得到的差值,就可以得到其所对应的fileoffset。读者可以使用例子程序进行手工计算,然后通过lordpe进行验证。
知道如何通过rva转换为文件偏移,那么通过文件偏移转换为rva的方法也就不难了。这3种地址相互的转换方法就介绍完了。读者如果没有理解,就可以反复地按照公式进行学习和计算。只要在头脑中建立关于磁盘文件和内存映像的结构,那么理解起来就不会太吃力。在后面的例子中,将会写一个类似lordpe中转换3种地址的程序,以帮助读者加强理解。