现在的位置: 首页 > 综合 > 正文

【原创】国庆PE总复习(三)

2018年04月06日 ⁄ 综合 ⁄ 共 14542字 ⁄ 字号 评论关闭
首先向大家表示道歉,昨天因为有朋友自远方来,玩到了晚上十二点才回来,写完文章一看表都已经五点了,可能让有些等着学习的朋友们等久了,呵呵,今天还好,又有时间和大家一起共同学习了,希望在国庆七天的长假里,朋友们都能我和一起对PE文件有一个更加深切的理解!!

上一篇给大家讲到了输入表,输入表真的很重要,在很多方面都有应用,如免杀,壳的编写,病毒方面输入表都很重要,因此我作为单独的一篇给大家讲解了一下输入表的原理,这篇文章我会接着上一篇给大家讲以下三个也是比较重要的区块:输出表,重定位表,资源!!

输出表
当PE文件被执行的时候,Windows装载器将文件装入内存并将输入表中登记的DLL文件一并载入,再根据DLL文件中的函数导出信息对被执行文件的IAT表进行修正。在这些包含导出函数的DLL文件中,导出信息被保存在输出表中,通过输出表,DLL文件向系统提供导出的函数的名称、序号和入口地址等信息,以便Windows装载器通过这些信息来完成动态链接的过程。

扩展名为.EXE的PE文件一般不存在输出表,但大部分的.DLL文件中都包含输出表,这不是一定的,比如用纯资源的.DLL文件就不提供导出函数,文件中也就不存在输出表,但存在同时包含输入表和输出表的.EXE文件。

输出表的位置和大小同样可以从PE文件头的数据目录中获取,与输出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY结构,从这个结构的VirtualAddress字段得到的就是输出表的RVA值。如果在磁盘上PE文件中查找输出表,那么只需要将RVA转换成文件偏移就可以了。

输出表的功能与输入表配合使用的,既然在输入表中可以用函数和序号来导入,那么可以想象,输出表中必然也可以用函数和序号这两种方法来导出函数,事实确是如此,输出表中为每个导出函数定义了导出序号,但函数名的定义是可选的,对于定义了函数名的函数来说,既可以使用名称导出,也可以使用序号导出:对于没有定义函数名的函数来说,只能使用序号来导出。但是不提倡仅仅通过序数导出函数的方法,这会带来DLL维护中的问题,一旦DLL升级或修改,调用该DLL的程序将无法工作,我曾经使用过这种方法,很显然在Win7下的DLL与XP下的DLL不同,所以就只能重定位,这种方法确实存在一定局限性,呵呵

输出表是数据目录表的第一个成员,指向IMAGE_EXPORT_DIRECTORY(简称IED)结构,IED结构定义如下:
IMAGE_EXPORT_DIRECTORY STRUCT
 Characteristics DWORD ? ;未使用,总是为0
 TimeDateStamp DWORD ? ;文件的产生时刻
 MajorVersion WORD ? ;未使用,总是为0
 MinorVersion WORD ? ;未使用,总是为0
 nName DWORD ? ;指向文件名的RVA
 nBase DWORD ? ;导出函数的起始序号
 NumberOfFunctions DWORD ? ;导出函数的总数
 NumberOfNames DWORD ? ;以名称导出的函数总数
 AddressOfFunctions DWORD ? ;指向导出函数地址表的RVA
 AddressOfNames DWORD ? ;指向函数名地址表的RVA
 AddressOfNameOrdinals DWORD ? ;指向函数名序号表的RVA
IMAGE_EXPORT_DIRECTORY ENDS

这个结构中的一些字段并没有使用,其余的有意义的字段说明如下,读者可以参考下面的图来理解这些字段之间的关系。
名称:  图1.jpg查看次数: 1016文件大小:  39.3 KB

nName字段
这个字段是一个RVA值,指向一个定义了模块名称的字符串。这个字符串说明了模块的原始文件名,比如说即使Kernel32.dll文件被改名为Ker.dll,仍然可以从这个字符串中的值得知它被编译时的文件名是“Kernel32.dll”。

NumberOfFunctions字段
文件中包含的导出函数的总数。

NumberOfNames字段
被定义了函数名称的导出函数的总数。显然,只有这个数量的函数既可以用函数名方式导出,也可以用序号方式导出,剩下的NumberOfFunctions减去NumberOfNames数量的函数只能用序号方式导出。NumberOfNames字段的值只会小于或者等于NumberOfFunctions字段的值,如果这个值是0,表示所有的函数都是以序号方式导出的

AddressOfFunctions字段
这是一个RVA值,指向包含全部导出函数入口地址的双字数组,数组中的每一项是一个RVA值,数组的项数等于NumberOfFunctions字段的值。

nBase字段
导出函数序号的起始值。将AddressOfFunctions字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出序号,举例来说,假如nBase字段的值为x,那么入口地址表指定的第一个导出函数的序号就是x,第二个导出函数的序号就是x+1,总之,一个导出函数的导出序号等于nBase字段的值加上其在入口地址表中的位置索引值。

AddressOfNames和AddressOfNameOrdinals字段
AddressOfNames字段的数值是一个RVA值,指向函数名字符串地址表,这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA,数组的项数等于NumberOfNames字段的值,所有有名称的导出函数的名称字符串都定义在这个表中。
那么这些函数名称究竟对应地址表中的那个函数呢?AddressOfNameOrdinals字段就派上用途了,这个字段也是一个RVA值,指向另一个word类型的数组(注意不是双字数组),数组的项目与文件名地址表中的项目一一对应,项目的值代表函数入口地址表的索引,这样函数名称与函数入口地址就关联起来了。

这里我仅仅介绍几个比较重要的字段解释,详细介绍请参考《加密与解密》第三版
输出表的设计是为了方便PE装载器工作,首先,模块必须保存所有输出函数的地址,供PE装载器查询,模块将这些信息保存在AddressOfFunctions域所指向的数组中,而数组无素数目存放在NumberOfFunctions域中。如果模块引出40个函数,则AddressOfFunctions指向的数组必定有40个元素,而NumberOfFunctions值为40。如果有些函数是通过名字引出的,那么模块必定也在文件中保留了这些信息。这些名字的RVA存放在一个数组中,供PE装载器查询。该数组由AddressOfNames指向,NumberOfNames包含名字数组。PE装载器知道函数名,并想以此获取这些函数的地址。至今为止,已有两个模块:名字数组和地址数组,但两者之间还没有联系的纽带,还需要一些联系函数名及其地址的东西。PE参考指出使用到地址数组的索引作为连接,因此PE装载器在名字数组中找到匹配名字的同时,它也获取指向地址表中对应元素的索引。这些索引保存在由AddressOfNamesOrdinals域所指向的另一个数组(最后一个)中。由于该数组起到联系名字和地址的作用,所以其元素数目必定和名字数组相同。例如:每个名字有且仅有一个相关地址,反过来则不一定,每个地址可以有好几个名字来对应。因此,给同一个地址取“别名”。为了起到链接作用,名字数组和索引数组必须并行成对使用,比如索引数组的第一个元素必定含有第一个名字的索引,依次类推,如下图所示,给出了输出表的格式以及三个阵列。
名称:  图2.gif查看次数: 1006文件大小:  15.6 KB

看图可能还不是很清楚,我们这里还是向前面两篇中一样,用一个实例分析来讲解输出表!
我这里就以《加密与解密》第三版中的DllDemo.DLL这个文件为实例,来详解讲解一下输出表,该指针具体位置是在PE文件头的78h偏移处(输入表在80h处),该文件的PE文件头的起始位置是100h,输出表就是在整个文件的100h+78h=178h处,因此在178h处可以发现四个字节指针00400000,倒过来就是00004000h,即输出表在内存中偏移4000h的地方。当然,这个4000h指的是内存中的偏移量,转成文件偏移地址就是0C00h。文件偏移0C00h处就是输出表的内容。
下面几个图清楚的表示的上面的过程,请大家参考:
名称:  图3.jpg查看次数: 1023文件大小:  119.9 KB
名称:  图4.jpg查看次数: 1003文件大小:  20.1 KB
名称:  图5.jpg查看次数: 1000文件大小:  33.0 KB

从上图中可以看出此文件的输出表中NumberOfFunctions为00000001,说明只有一个输出函数,DLL只有一个输出函数MsgBox,其中IMAGE_EXPORT_DIRECTORY结构如表所示
名称:  图6.jpg查看次数: 994文件大小:  25.1 KB
分析一下:
从上表可得到Name为4032h,转换为File Offset为:C32h,指向DLL名字DllDemo.DLL。
AddressOfNames:402Ch转换为File Offset为C2Ch,指向函数名的指针403Eh转换为File Offset为C3Eh,指向函数名MsgBox。
AddressOfNameOrdinals为4030h,转换为File Offset为C30h,指向输出序号数组。
具体见下图所示:
名称:  图7.jpg查看次数: 996文件大小:  69.1 KB
再来看看输出是如何实现的。PE装载器调用GetProcAddress来查找DllDemo.DLL里的API函数MsgBox,系统通过定位DllDemo.DLL的IMAGE_EXPORTS_DIRECTORY结构开始工作,从这个结构中,它获得输出函数名称表(Export Names Table,简称ENT)起始地址,进而知道这个数组一共有1个条目,它对名字进行二进制查找发现字符串"MsgBox"。
PE装载器发现MsgBox是数组的第一个条目,加载器然后从输出序数表读取相应的第一个值,这个值是MsgBox的输出序数,使用输出序数作为进入EAT的索引(并且也要考虑Base域值),它得到MsgBox的RVA是1008h,1008h加上DllDemo.DLL的装入地址得到MsgBox的实际地址。

重定位表
什么是重定位,代码又是在什么情况下才需要重定位?在32代码中,涉及直接寻址的指令都是需要重定位的(而在DOS的16位代码中,只有涉及段操作的指令才是需要重定位的。
对于操作系统来说,其任务就是在对可执行程序透明的情况下完成重定位操作,在现实中,重定位信息在编译的时候由编译器生成并被保留在可执行文件中的,在程序被执行前由操作系统跟据重定位信息修正代码,这样在开发程序的时候就不用考虑重定位问题了,重定位信息在PE文件中被存放在重定位表中。
重定位所需的数据
在开始分析重定位表的结构之前需要了解两个问题:第一,对一条指令进行重定位需要哪些信息;第二,这些信息中哪些应该被保存在重定位表中。下面举例来说明这两个问题。
请看下面的这段代码:
:00400FFC 0000 ;dwVar变量
:00401000 55    push ebp
:00401001 8BEC    mov ebp, esp
:0040100383C4FC    add esp, FFFFFFFC
:00401006 A1FC0F4000   mov eax, dword ptr [00400FFC] ;mov eax,dwVar
:0040100B 8B45FC    mov eax, dword ptr [ebp-04] ;mov eax,@dwLocal
:0040100E 8B4508    mov eax, dword ptr [ebp+08] ;mov eax,_dwParam
:00401011 C9    leave
:00401012 C20400    ret 0004
:00401015 68D2040000   push 000004D2
:0040101A E8E1FFFFFF   call 00401000 ;invoke Proc1,1234

其中地址为00401006h处的mov eax,dword ptr [00400ffc]就是一句需要重定位的指令,当整个程序的起始地址位于00400000h处的时候,这句代码是正确的,假如将它移到00500000h处的时候,这句指令必须变成mov eax,dword ptr [00500ffc]才是正确的。这就意味着它需要重定位。
让我们看看需要改变的是什么,重定位前的指令机器码是A1 FC0F 40 00,而重定位后将是A1 FC0F 50 00,也就是说00401007h开始的双字00400ffch变成了00500ffch,改变的正是起始地址的差值(00500000h-00400000h)=00100000h。
所以,重定位的算法可以描述为:将直接寻址指令中的双字地址加上模块实际装入地址与模块建议装入地址之差。为了进行这个运算,需要有3个数据,首先是需要修正的机器码地址;其次是模块的建议装入地址;最后是模块的实际装入地址。这就是第一个问题的答案。

在这3个数据中,模块的建议装入地址已经在PE文件头中定义了,而模块的实际装入地址是Windows装载器确定的,到装载文件的时候自然会知道,所以第二个问题的答案很简单,那就是应该被保存在重定位表中的仅仅是需要修正的代码的地址。

事实上正是如此,PE文件的重定位表中保存的就是一大堆需要修正的代码的地址。

重定位表的位置

重定位表一般会被单独存放在一个可丢弃的以“.reloc”命名的节中,但是和资源一样,这并不是必然的,因为重定位表放在其他节中也是合法的,惟一可以肯定的是,如果重定位表存在的话,它的地址肯定可以在PE文件头中的数据目录中找到。

重定位表的结构

虽然重定位表中的有用数据是那些需要重定位机器码的地址指针,但为了节省空间,PE文件对存放的方式做了一些优化。

在正常的情况下,每个32位的指针占用4个字节,如果有n个重定位项,那么重定位表的总大小是4×n字节大小。

直接寻址指令在程序中还是比较多的,在比较靠近的重定位表项中,32位指针的高位地址总是相同的,如果把这些相近表项的高位地址统一表示,那么就可以省略一部分的空间,当按照一个内存页来分割时,在一个页面中寻址需要的指针位数是12位(一页等于4096字节,等于2的12次方),假如将这12位凑齐16位放入一个字类型的数据中,并用一个附加的双字来表示页的起始指针,另一个双字来表示本页中重定位项数的话,那么占用的总空间会是4+4+2×n字节大小,计算一下就可以发现,当某个内存页中的重定位项多于4项的时候,后一种方法的占用空间就会比前面的方法要小。

PE文件中重定位表的组织方法就是采用类似的按页分割的方法,从PE文件头的数据目录中得到重定位表的地址后,这个地址指向的就是顺序排列在一起的很多重定位块,每一块用来描述一个内存页中的所有重定位项。

每个重定位块以一个IMAGE_BASE_RELOCATION结构开头,后面跟着在本页面中使用的所有重定位项,每个重定位项占用16位的地址(也就是一个word),结构的定义是这样的:
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd ? ;重定位内存页的起始RVA
SizeOfBlock dd ? ;重定位块的长度
IMAGE_BASE_RELOCATION ENDS

VirtualAddress字段是当前页面起始地址的RVA值,本块中所有重定位项中的12位地址加上这个起始地址后就得到了真正的RVA值。SizeOfBlock字段定义的是当前重定位块的大小,从这个字段的值可以算出块中重定位项的数量,由于SizeOfBlock=4+4+2×n,也就是sizeof IMAGE_BASE_RELOCATION+2×n,所以重定位项的数量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。

IMAGE_BASE_RELOCATION结构后面跟着的n个字就是重定位项,每个重定位项的16位数据位中的低12位就是需要重定位的数据在页面中的地址,剩下的高4位也没有被浪费,它们被用来描述当前重定位项的种类。
虽然高4位定义了多种重定位项的属性,但实际上在PE文件中只能看到0和3这两种情况。

所有的重定位块最终以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结构作为结束,读者现在一定明白了为什么可执行文件的代码总是从装入地址的1000h处开始定义的了(比如装入00400000h处的.exe文件的代码总是从00401000h开始,而装入10000000h处的.dll文件的代码总是从10001000h处开始),要是代码从装入地址处开始定义,那么第一页代码的重定位块的VirtualAddress字段就会是0,这就和重定位块的结束方式冲突了。

好了上面的理论知识讲完了,我们还是用一个实例分析来说吧!!还是那个DLL文件!
首先我们假设一下:
先看看DllDemo.DLL反汇编后的结果:
Exported fn():MsgBox - Ord:0001h
:00401008 C8000000   enter 0000,00
:0040100C 6A00       push 00000000
*Possible StringData Ref from Data Obj--->"动态链接库"
:0040100E 6800204000  push 00402000
:00401013 FF7508      push [ebp+08]
:00401016 6A00        push 00000000
:00401018 E8040000    CALL 00401021
:0040101D C9          leave
:0040101E C20400      ret 0004
*Reference To :USER32.MessageBoxA,Ord:0000h
:00401021 FF2530304000 JMP DWORD PTR[00403030]
根据上面的理论讲解,我们需要重定位的有两处:00402000和00403030
下面我们就来实例分析一下,是不是?
首先找到重定位表的指针,如下图所示:
名称:  图8.jpg查看次数: 984文件大小:  70.9 KB
名称:  图9.jpg查看次数: 986文件大小:  18.4 KB
从图中可以看出数据目录表指向重定位表的指针是5000h,换算成文件偏移地址就是0E00h,我们在定位到File Offset为0E00处,可以得到IMAGE_BASE_RELOCATION结构如下图所示:
名称:  图10.jpg查看次数: 981文件大小:  47.7 KB
从图中可以看出:
VirtualAddress:00001000h
SizeOfBlock:00000010h(有四个重定位数据,(10h-8h)/2h=4h)
重定位数据1:300Fh
重定位数据2:3023h
重定位数据3:0000h(用于对齐)
重定位数据4:0000h(用于对齐)
重定位数据计算过程如下表所示:
名称:  图11.jpg查看次数: 986文件大小:  32.1 KB
用十六进制工具查看实例文件,其中060Fh和623h分别指向402000h和403030h,如下图所示:
名称:  图12.jpg查看次数: 985文件大小:  31.2 KB
这样我们就得到了需要从重位的地址,是不是和我们上面所假设的一样!!
执行PE文件前,加载程序在进行重定位的时候,会将PE文件在内存中的实际映像地址减去PE文件所要求的映像地址,得到一个差值,再将这一差值根据重定位类型的不同添加到地址数组中。

资源
资源是PE文件中非常重要的部分,几乎所有的PE文件中都包含资源,与导入表和导出表相比,资源的组织方式要复杂得多,如果一开始就扎进一堆资源相关的数据结构中去分析各字段的含义,恐怕会越来越糊涂,要了解资源的话,重在理解资源的整体上的组织结构。
我们知道,PE文件资源中的内容包括光标、图标、位图、菜单等十几种标准的类型,除此之外还可以使用自定义的类型(这些类型的资源在第5章中已经有所介绍)。每种类型的资源中可能存在多个资源项,这些资源项用不同的ID或者名称来分辨,在某个资源ID下,还可以同时存在不同代码页的版本。

要将这么多种类型的不同ID的资源有序地组织起来,用类似于磁盘目录结构的方式是很不错的。打个比方,假如在磁盘的根目录下按照类型建立若干个第2层目录,目录名是“光标”、“图标”、“位图”和“菜单”等,就可以将各种资源分类放入这些目录中,假设现在有n个光标,那么在“光标”目录中再以光标ID为名建立n个第3层子目录,进入某个子目录后,再以代码页为名称建立不同文件,这样所有的资源就按照树型目录的方式组织起来了。现在要查找某个资源的话,那么按照根目录→资源类型→资源ID→资源代码页这样的步骤一层层地进入相应的子目录并找到正确的资源。
如图所示,PE文件中组织资源的方式与上面的构思及其相似,正是按照根目录→资源类型→资源ID的3层树型目录结构来组织资源的,只不过在第3层目录中放置的代码页“文件”不是资源本身而是一个用来描述资源的结构罢了,通过这个结构中的指针才能最后找到资源数据。
名称:  图13.jpg查看次数: 983文件大小:  57.5 KB

资源的组织方式
获取资源的位置
资源数据块的位置和大小可以从PE文件头中的IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,与资源对应的项目是数据目录中的第3个IMAGE_DATA_DIRECTORY结构,从这个结构的VirtualAddress字段得到的就是资源块地址的RVA值。

从上图中的A所示,从数据目录表中得到的资源块的起始地址就是资源根目录的起始地址,从这里开始就可以一层层地找到资源的所有信息了。
在获取资源块地址的时候,注意不要使用查找“.rsrc”节起始地址的方法,虽然在一般情况下资源总是在“.rsrc”节中,但这并不是必然的。

资源目录
资源目录树的根目录地址已经得到了,那么整个目录树上的目录是如何描述的呢?注意图中左下角的图例在整个目录树中出现的位置,这样就可以发现:不管是根目录,还是第2层或第3层中的每个目录都是由一个IMAGE_RESOURCE_ DIRECTORY结构和紧跟其后的数个IMAGE_RESOURCE _DIRECTORY_ENTRY结构组成的,这两种结构一起组成了一个目录块。

IMAGE_RESOURCE_DIRECTORY结构中包含的是本目录的各种属性信息,其中有两个字段说明了本目录中的目录项数量,也就是后面的IMAGE_RESOURCE_DIRECTORY_ ENTRY结构的数量。

IMAGE_RESOURCE_DIRECTORY结构的定义如下所示:
IMAGE_RESOURCE_DIRECTORY STRUCT
  Characteristics dd ? ;理论上为资源的属性,不过事实上总是0
  TimeDateStamp dd ? ;资源的产生时刻
  MajorVersion dw ? ;理论上为资源的版本,不过事实上总是0
  MinorVersion dw ?
  NumberOfNamedEntries dw ? ;以名称命名的入口数量
  NumberOfIdEntries dw ? ;以ID命名的入口数量
IMAGE_RESOURCE_DIRECTORY ENDS
在这个结构中,最重要的是最后两个字段,它们说明了本目录中目录项的数量,那么为什么有两个字段呢?
原因是这样的:不管是资源种类,还是资源名称都可以用名称或者ID两种方式定义,比如在*.rc文件中这样定义:
100      ICON   "Test.ico"    //(例1)
101      WAVE  "Test.wav"    //(例2)
HelpFile  HELP   "Test.chm"    //(例3)
102      12345   "Test.bin"    //(例4)
例1定义了一个ID为100的光标资源,光标的资源类型虽然写成“ICON”,但这只是一个助记符,在资源编译器里面会被换成数值型的类型ID,所有的标准类型资源都是以数值型ID定义的,在资源定义中,1到10h的ID编号保留给标准类型使用。
在例2中,标准的资源类型中并没有“WAVE”这一类型,这时资源的类型属于自定义型,类型的名称就是“WAVE”。
例3则定义了资源名称是“HelpFile”,类型名称为自定义字符串“HELP”的资源。
在例4中,资源的ID编号是102,而类型则是数值型ID,由于标准类型中并没有编号为12345的资源,所以这也是一个自定义类型的资源。
在IMAGE_RESOURCE_DIRECTORY结构中,对以ID命名和以字符串命名的情况是分别指定的:NumberOfNamedEntries字段是以字符串命名的资源数量,而NumberOfIdEntries字段的值是以ID命名的资源数量,所以两者的数量加起来才是本目录中的目录项总和,也就是当前IMAGE_RESOURCE_DIRECTORY结构后面紧跟的IMAGE_RESOURCE_DIRECTORY_ENTRY结构的数量。

现在来介绍一下IMAGE_RESOURCE_DIRECTORY_ENTRY结构,每个结构描述了一个目录项,IMAGE_RESOURCE_DIRECTORY_ENTRY结构是这样定义的:
IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT
  Name1 dd ? ;目录项的名称字符串指针或ID
  OffsetToData dd ? ;目录项指针
IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS
结构中的两个字段说明如下:

Name1字段
这个字段的名称应该是“Name”,同样是因为和关键字冲突的原因改为“Name1”,它定义了目录项的名称或者ID,这个字段的含义要看目录项用在什么地方,当结构用于第1层目录的时候(如图17.8中的B所示),这个字段定义的是资源的类型,也就是前面例子中的“ICON”,“WAVE”,“HELP”和12345等;当结构用于第2层目录的时候(如图17.8中的C1到C3),这个字段定义的是资源的名称,也就是前面例子中的100,101,“HelpFile”和102等;而当结构用于第3层目录的时候(如图17.8中的D1到D4),这里定义的是代码页编号。

你肯定会发现一个问题:当字段作为ID使用的时候,是可以放入一个双字的,如果使用字符串定义的时候,一个双字是不够的,这就需要将两种情况分别对待,区分的方法是使用字段的最高位(位31)。当位31是0的时候,表示字段的值作为ID使用;而位31为1的时候,字段的低位作为指针使用,但由于资源名称字符串是使用UNICODE来编码的,所以这个指针并不直接指向字符串,而是指向一个IMAGE_RESOURCE_DIR_STRING_U结构,这个结构包含UNICODE字符串的长度和字符串本身,其定义如下:

IMAGE_RESOURCE_DIR_STRING_U STRUCT
  Length1 dw ? ;字符串的长度
  NameString dw ? ;UNICODE字符串,由于字符串是不定长的,所以这里只能
 ;用一个dw表示,实际上当长度为100的时候,这里的数据
 ;是NameString dw 100 dup (?)
IMAGE_RESOURCE_DIR_STRING_U ENDS
这里说明一点,如果要得到ANSI类型的以0结尾的字符串,需要将NameString字段中包括的UNICODE字符串用WideCharToMultiByte函数转换一下。

OffsetToData字段
这个字段是一个指针,当它的最高位(位31)为1时,低位数据指向下一层目录块的起始地址,也就是一个IMAGE_RESOURCE_DIRECTORY结构,这种情况一般出现在第1层和第2层目录中:当字段的位31位为0时,指针指向的是用来描述资源数据块情况的IMAGE_RESOURCE_DATA_ENTRY指针,这种情况出现在第3层目录中。
当Name1字段和OffsetToData用做指针时需要注意两点:首先是不要忘记将最高位清除(可以使用and 7fffffffh来清除),其次就是这两个指针是从资源块开始的地方算起的偏移量,也就是根目录的起始位置算起的偏移量。
注意:千万不要将这两个指针作为RVA来对待,否则会得到错误的地址,正确的计算方法是将指针的值加上资源块首地址,结果才是真正的地址。
当IMAGE_RESOURCE_DIRECTORY_ENTRY用在第一层目录中的时候,它的Name1字段是作为资源类型来使用的,当资源类型以ID定义(最高位等于0),并且ID数值在1到16之间时,表示这时系统预定义的类型,如果资源是以ID定义的并且数值在16以上,表示这是一个自定义的类型。

资源数据入口
沿着资源目录树按照根目录--->资源类型----->资源ID的顺序到达第3层目录后,这一层目录的IMAGE_RESOURCE_DIRECTORY_ENTRY结构的OffsetToData字段指向的是一个IMAGE_RESOURCE_DATA_ENTRY结构。

IMAGE_RESOURCE_DATA_ENTRY结构定义如下所示:
IMAGE_RESOURCE_DATA_ENTRY STRUCT
       OffsetToData   dd   ?         ;资源数据的RVA
       Size1          dd   ?        ;资源数据的长度
       CodePage      dd   ?         ;代码页
       Reserved       dd   ?         ;保留字段
IMAGE_RESOURCE_DATA_ENTRY ENDS
IMAGE_RESOURCE_DATA_ENTRY结构描述了资源数据所处的位置和大小,换句话说,就是经过了这么多层结构的以后,终于得到了某一个资源的详细信息。
结构中的OffsetToData字段的值是指向资源数据的指针,这个指针是一个RVA值,而不是以资源块的起始地址为基址的,这里需要特别注意。Size1字段的值是资源数据的大小。结构中的第三个字段是CodePage,这个字段的名称有些奇怪,因为当前资源的代码页已经在第三层目录中指明了,在这里又定义了一次,不知道为什么,在实际应用中,这个字段好像未被使用,因为这里的值总为0。

资源的理论也介绍完了,我们研究一个实例,加深一下理解!就以《加密与解密》第三版上的实例pediy作为实例学习吧!
数据目录表的第三个成员指向资源结构,该指针具体位置是PE文件头的88h偏移处。用十六进制工具查看实例文件在PE文件头起始位置是0C0h,则资源结构在整个文件的0C0h+88h=148h处,因此在148h处可以发现资源的RVA为4000h,由于这个实例文件磁盘文件中区块对齐值等于1000h,与内存页对齐值相同,因此RVA与文件偏移地址不用转换。如下图所示:
名称:  图14.jpg查看次数: 985文件大小:  78.2 KB
我们再来看看00004000h处的内容,如下图所示:
名称:  图15.jpg查看次数: 972文件大小:  94.2 KB
从图中我们可以得到根目录的IMAGE_RESOURCE_DIRECTORY各结构成员值:Characteristics为00000000,TimeDataStamp为00000000,MajorVersion为0000,MinorVersion为0000,NumberOfNamedEntries为0000,NumberOfIdEntries为0003。NumberOfNameEntries与NumberOfIdEntries的和为3,表明这个程序有三个资源项目,也就是说,其后面紧跟着三个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,具体请看下面图
名称:  图16.jpg查看次数: 975文件大小:  37.0 KB
根据上面图所示,将这三个结构整理成如下表所示:
名称:  图17.jpg查看次数: 958文件大小:  20.7 KB
以表中的第二个IMAGE_RESOURCE_DIRECTORY_ENTRY结构为例分析资源的下一层。第一层目录,Name字段是定义资源类型,目前其ID值是04h,表明这是一个菜单资源。另外,OffsetToData字段为80000040h,第一个字节80h的二进制为10000000,最高位为1,说明还有下一层。所以OffsetToData的低位数据40h指向第二层目录块。第二层目录块的地址为资源块首地址加上40h,即为4000h+40h= 4040h。

第二层目录,偏移4040h的数据即为第二段,见图
名称:  图18.jpg查看次数: 967文件大小:  54.4 KB
从图中我们可以得出第二层IMAGE_RESOURCE_DIRECTORY结构成员:Characteristics为0,TimeDataStamp为0,MajorVersion为0,MinorVersion为0,NumberOfNamesEntries为1,NumberOfIdEntries为0。(这里好像《加密与解密》第三版上有错误)NumberOfNamesEntries与NumberOfIdEntries和为1,表明这层有一个资源数目。也就是说,其后紧跟着一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,即在文件偏移4050h处,Name是800000E8h,Offset是80000088h。如下图所示:
名称:  图19.jpg查看次数: 957文件大小:  27.8 KB
当在第二层目录时,Name字段定义的时资源的名称,Name字段第一个字节为80h,二进制为10000000h,最高位为1,表明这是一个指针,指向IMAGE_RESOURCE_DIR_STRING_U结构,其地址为资源块首地址加上Name字段低位数据0E8h,即4000h+0E8h=40E8h,具体内容见下图中所示
名称:  图20.jpg查看次数: 954文件大小:  34.9 KB
图中显示了Length是05,NameString是Unicode字符"PEDIY",即这个资源名为"PEDIY"。
OffsetToData字段是80000088h,第一个字节80h的二进制为10000000h,最高位为1,说明还有下一层,所以OffsetToData的低位数据88h指向第三层目录块。第三层目录块的地址为资源块首地址加上88h,即为4000h+88h=4088h。

第三层目录
文件偏移4088h处的数据指向第三层,具体内容参见如下图所示:
名称:  图21.jpg查看次数: 957文件大小:  32.8 KB
从上图上我们可以得到第三层的IMAGE_RESOURCE_DIRECTORY结构成员:Characteristics为0,TimeDataStamp为0,MajorVersion为0,MinorVersion为0,NumberOfIdEntries为1,NumberOfNamedEntries为0。NumberOfIdEntries和NumberOfNamedEntries之和为1,表明这层有一个资源项目。也就是说,其后面紧跟着一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,即在文件偏移4098处,Name是00000409h,OffsetToData是000000C8h。
当第三层目录时,Name字段定义的是代码页编号,00000409h表示代码页是英语。OffsetToData高地址现在为0,所以其低位数据0C8h指向IMAGE_RESOURCE_DATA_ENTRY结构,0C8h加上资源块首地址,即4000h+0C8h=40C8h,具体内容见下图所示:
名称:  图22.jpg查看次数: 956文件大小:  36.2 KB
在这里就能查看到IMAGE_RESOURCE_DATA_ENTRY结构成员值,OffsetToData是0000440,Size是0000005Ah,CodePage是00000000,Reserved是00000000h,此时菜单的真正资源数据RVA为4400h,大小为5Ah。

在这里PE文件就分析到处,如果大家还有什么不清楚明白的,可以参见我上面提到的两本书,里面讲的很详细,从明天开始我将着重讲解一下PE文件的编程问题,PE分析工具有很多,我们来看看是如何编写的,如果对前面几章不是很了解,请先把这些弄明白,才能开始学习后面的编程,不能你会觉得很难,很不好理解的!! 

抱歉!评论已关闭.