首页
论坛
课程
招聘
[原创]国庆PE总复习(1-7)合集
2010-10-1 17:44 137819

[原创]国庆PE总复习(1-7)合集

2010-10-1 17:44
137819
收藏
点赞1
打赏
分享
最新回复 (147)
雪    币: 100
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
木月 活跃值 2010-10-3 16:30
26
0
不错,支持一下。
雪    币: 232
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
aceivy 活跃值 2010-10-3 18:26
27
0
最好做一个索引!!!
雪    币: 1776
活跃值: 活跃值 (194)
能力值: ( LV12,RANK:480 )
在线值:
发帖
回帖
粉丝
熊猫正正 活跃值 9 2010-10-3 22:52
28
0
首先向大家表示道歉,昨天因为有朋友自远方来,玩到了晚上十二点才回来,写完文章一看表都已经五点了,可能让有些等着学习的朋友们等久了,呵呵,今天还好,又有时间和大家一起共同学习了,希望在国庆七天的长假里,朋友们都能我和一起对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

这个结构中的一些字段并没有使用,其余的有意义的字段说明如下,读者可以参考下面的图来理解这些字段之间的关系。


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域所指向的另一个数组(最后一个)中。由于该数组起到联系名字和地址的作用,所以其元素数目必定和名字数组相同。例如:每个名字有且仅有一个相关地址,反过来则不一定,每个地址可以有好几个名字来对应。因此,给同一个地址取“别名”。为了起到链接作用,名字数组和索引数组必须并行成对使用,比如索引数组的第一个元素必定含有第一个名字的索引,依次类推,如下图所示,给出了输出表的格式以及三个阵列。


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




从上图中可以看出此文件的输出表中NumberOfFunctions为00000001,说明只有一个输出函数,DLL只有一个输出函数MsgBox,其中IMAGE_EXPORT_DIRECTORY结构如表所示

分析一下:
从上表可得到Name为4032h,转换为File Offset为:C32h,指向DLL名字DllDemo.DLL。
AddressOfNames:402Ch转换为File Offset为C2Ch,指向函数名的指针403Eh转换为File Offset为C3Eh,指向函数名MsgBox。
AddressOfNameOrdinals为4030h,转换为File Offset为C30h,指向输出序号数组。
具体见下图所示:

再来看看输出是如何实现的。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
下面我们就来实例分析一下,是不是?
首先找到重定位表的指针,如下图所示:


从图中可以看出数据目录表指向重定位表的指针是5000h,换算成文件偏移地址就是0E00h,我们在定位到File Offset为0E00处,可以得到IMAGE_BASE_RELOCATION结构如下图所示:

从图中可以看出:
VirtualAddress:00001000h
SizeOfBlock:00000010h(有四个重定位数据,(10h-8h)/2h=4h)
重定位数据1:300Fh
重定位数据2:3023h
重定位数据3:0000h(用于对齐)
重定位数据4:0000h(用于对齐)
重定位数据计算过程如下表所示:

用十六进制工具查看实例文件,其中060Fh和623h分别指向402000h和403030h,如下图所示:

这样我们就得到了需要从重位的地址,是不是和我们上面所假设的一样!!
执行PE文件前,加载程序在进行重定位的时候,会将PE文件在内存中的实际映像地址减去PE文件所要求的映像地址,得到一个差值,再将这一差值根据重定位类型的不同添加到地址数组中。

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

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


资源的组织方式
获取资源的位置
资源数据块的位置和大小可以从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与文件偏移地址不用转换。如下图所示:

我们再来看看00004000h处的内容,如下图所示:

从图中我们可以得到根目录的IMAGE_RESOURCE_DIRECTORY各结构成员值:Characteristics为00000000,TimeDataStamp为00000000,MajorVersion为0000,MinorVersion为0000,NumberOfNamedEntries为0000,NumberOfIdEntries为0003。NumberOfNameEntries与NumberOfIdEntries的和为3,表明这个程序有三个资源项目,也就是说,其后面紧跟着三个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,具体请看下面图

根据上面图所示,将这三个结构整理成如下表所示:

以表中的第二个IMAGE_RESOURCE_DIRECTORY_ENTRY结构为例分析资源的下一层。第一层目录,Name字段是定义资源类型,目前其ID值是04h,表明这是一个菜单资源。另外,OffsetToData字段为80000040h,第一个字节80h的二进制为10000000,最高位为1,说明还有下一层。所以OffsetToData的低位数据40h指向第二层目录块。第二层目录块的地址为资源块首地址加上40h,即为4000h+40h= 4040h。

第二层目录,偏移4040h的数据即为第二段,见图

从图中我们可以得出第二层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。如下图所示:

当在第二层目录时,Name字段定义的时资源的名称,Name字段第一个字节为80h,二进制为10000000h,最高位为1,表明这是一个指针,指向IMAGE_RESOURCE_DIR_STRING_U结构,其地址为资源块首地址加上Name字段低位数据0E8h,即4000h+0E8h=40E8h,具体内容见下图中所示

图中显示了Length是05,NameString是Unicode字符"PEDIY",即这个资源名为"PEDIY"。
OffsetToData字段是80000088h,第一个字节80h的二进制为10000000h,最高位为1,说明还有下一层,所以OffsetToData的低位数据88h指向第三层目录块。第三层目录块的地址为资源块首地址加上88h,即为4000h+88h=4088h。

第三层目录
文件偏移4088h处的数据指向第三层,具体内容参见如下图所示:

从上图上我们可以得到第三层的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,具体内容见下图所示:

在这里就能查看到IMAGE_RESOURCE_DATA_ENTRY结构成员值,OffsetToData是0000440,Size是0000005Ah,CodePage是00000000,Reserved是00000000h,此时菜单的真正资源数据RVA为4400h,大小为5Ah。

在这里PE文件就分析到处,如果大家还有什么不清楚明白的,可以参见我上面提到的两本书,里面讲的很详细,从明天开始我将着重讲解一下PE文件的编程问题,PE分析工具有很多,我们来看看是如何编写的,如果对前面几章不是很了解,请先把这些弄明白,才能开始学习后面的编程,不能你会觉得很难,很不好理解的!!
上传的附件:
雪    币: 7080
活跃值: 活跃值 (11817)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
linhanshi 活跃值 2010-10-3 23:18
29
0
Thanks for share.

Программное обеспечение выпуска и Windows Crack Обучение
Нам-Dabei Guanyin Бодхисаттва Нам без митабха
雪    币: 349
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
要学会编 活跃值 2010-10-3 23:22
30
0
楼主辛苦了。非常感谢。 期待后面精彩内容
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
随风而去 活跃值 2010-10-4 00:29
31
0
收到!加油!
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
老豆荚 活跃值 2010-10-4 01:13
32
0
太谢谢了,学习
雪    币: 141
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
SunV 活跃值 2010-10-4 01:14
33
0
哈哈,LZ不嫌累啊~~
雪    币: 119
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yhcad 活跃值 2010-10-4 06:26
34
0
嗯,学习很认真~~~~~~
雪    币: 41
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sdnyzjzx 活跃值 2010-10-4 08:10
35
0
谢谢楼主,尽力去学,现在还搞不明白。
雪    币: 31
活跃值: 活跃值 (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
boystones 活跃值 2010-10-4 09:11
36
0
努力学习中,收藏了
雪    币: 31
活跃值: 活跃值 (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
boystones 活跃值 2010-10-4 10:24
37
0
收藏收藏,赞一个。
雪    币: 1115
活跃值: 活跃值 (15)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
pencil 活跃值 5 2010-10-4 12:20
38
0
PE格式复习.pdf

整理成pdf格式,方便有EBook的朋友
上传的附件:
雪    币: 246
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
blackskey 活跃值 2010-10-4 13:41
39
0
过来看看 学习下,,,,,,,,,,,
雪    币: 1377
活跃值: 活跃值 (51)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
胡雪岩 活跃值 2010-10-4 13:58
40
0
呵呵,能看懂了!!
雪    币: 223
活跃值: 活跃值 (23)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hbfp 活跃值 2010-10-4 20:35
41
0
收藏学习,这东西看了老是记不住.
雪    币: 105
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
smallyounf 活跃值 2010-10-4 22:20
42
0
收藏了,楼主真有耐心,这个真的要学习一下
雪    币: 1115
活跃值: 活跃值 (15)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
pencil 活跃值 5 2010-10-4 23:18
43
0
可否添加下参考文献?
雪    币: 1187
活跃值: 活跃值 (77)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jianshi 活跃值 2010-10-4 23:39
44
0
学习了,要是最后能总结下做成一本书就更好了
雪    币: 522
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
ddsoft 活跃值 2010-10-4 23:51
45
0
这些东西不知不觉就懂了。。。之前都是草草地看一遍。
前不久,花了一天,仔细看了遍《加密与解密3》的PE文件格式这章,居然一下子懂了,
第二天用winhex+lordPE实际弄几个PE文件,就记住了。。。。。

所以我认为,就是要多看。。。反复看。
雪    币: 92
活跃值: 活跃值 (16)
能力值: (RANK:60 )
在线值:
发帖
回帖
粉丝
沙加L 活跃值 1 2010-10-5 16:41
46
0
我觉得有本书《软件调试》里面讲PE格式,也讲得挺不错的,大家可以参考看一下
雪    币: 1776
活跃值: 活跃值 (194)
能力值: ( LV12,RANK:480 )
在线值:
发帖
回帖
粉丝
熊猫正正 活跃值 9 2010-10-5 16:55
47
0
昨天感冒了,今天鼻子也不是很舒服,不过不想失约,承诺了就一定要做下去,七篇文章一定会在国庆结束前全部献上,不管评论是好与坏,至少我懂得做为男人既已承诺,就一定要现!!

前面几篇文章中我已经对PE文件的结构作了一个比较详细的讲解,如果大家还有不清楚的,请参考相关资料,谢谢,下面我开始讲解PE文件编程方面的知识~~这样理论结合实际,我想大家会有一个更深切的理解!

首先我想对《加密与解密》第三版上的PE分析工具实例进行讲解,因为考虑到大多数人还是对C语言比较熟悉,所以先用C语言进行整体讲解,在后面的三篇中的我将着重讲解PE的Win32汇编的编程,因为必竟汇编我还是比较熟一点,C语言不是很熟,呵呵!!

其实有了上面的理论讲解,基本的算法很简单了,主要就是对PE格式的各个结构进行定位,这里我们定义一个MAP_FILE_STRUCT结构来存放有关信息,结构如下:
typedef struct _MAP_FILE_STRUCT
{
        HANDLE hFile;           ;文件句柄
        HANDLE hMapping;       ;映射文件句柄
        LPVOID ImageBase;       ;映像基址
}  MAP_FILE_STRUCT;

PE文件格式的检查
文件格式可能通过PE头开始的标志Signature来检测。检测DOS Header的Magic Mark不是也可以检测此PE文件是否合法吗?但是大家想想如果只检测一个文件的头两个字节是不是MZ,如果一个文件文件的头两个字节是MZ,那不是判断失误!所以要检查PE文件的格式有两个重要的步骤:
判断文件开始的第一个字段是否为IMAGE_DOS_SIGNATURE,即5A4Dh
再通过e_lfanew找到IMAGE_NT_HEADERS,判断Signature字段的值是否为IMAGE_NT_SIGNATURE,即00004550h,如果是IMAGE_NT_SIGNATURE,就可以认为该文件是PE格式。
具体代码实现如下:
BOOL IsPEFile(LPVOID ImageBase)
{
    PIMAGE_DOS_HEADER  pDH=NULL;
    PIMAGE_NT_HEADERS  pNtH=NULL;
  
    if(!ImageBase)                            //判断映像基址
          return FALSE;
   
    pDH=(PIMAGE_DOS_HEADER)ImageBase;
    if(pDH->e_magic!=IMAGE_DOS_SIGNATURE)    //判断是否为MZ
         return FALSE;

    pNtH=(PIMAGE_NT_HEADERS32)((DWORD)pDH+pDH->e_lfanew);     //DOS头+e_lfanew(03Ch)定位PE文件头
    if (pNtH->Signature != IMAGE_NT_SIGNATURE )               //判断是否为PE文件头PE
        return FALSE;

    return TRUE;
       
}

FileHeader和OptionalHeader内容的读取
IMAGE_NT_HEADERS STRUCT
Signature DWORD ? ;PE文件标识
FileHeader    IMAGE_FILE_HEADER    <>
OptionalHeader   IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS
从上面的结构可知,只要得到了IMAGE_NT_HEADERS,根据IMAGE_NT_HEADERS的定义,就可以找到IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER32。

首先我们要得到IMAGE_NT_HEADERS结构指针的函数:
PIMAGE_NT_HEADERS  GetNtHeaders(LPVOID ImageBase)
{
   
        if(!IsPEFile(ImageBase))               //通过文件基址来判断文件是否为PE文件
                return NULL;
        PIMAGE_NT_HEADERS  pNtH;              //定义PE文件头指针
        PIMAGE_DOS_HEADER  pDH;               //定义DOS头指针
        pDH=(PIMAGE_DOS_HEADER)ImageBase;     //得到DOS指针
        pNtH=(PIMAGE_NT_HEADERS)((DWORD)pDH+pDH->e_lfanew);      //得到PE文件头指针   
        return pNtH;

}

上面得到了IMAGE_NT_HEADERS结构的指针,下面我们来得到两个重要的结构指针:
IMAGE_FILE_HEADER结构的指针,函数如下:
PIMAGE_FILE_HEADER   GetFileHeader(LPVOID ImageBase)
{       
        PIMAGE_NT_HEADERS pNtH = NULL;   //定义PE文件头指针
        PIMAGE_NT_HEADERS pFH = NULL;   //定义映射文件头指针
        pNtH = GetNtHeaders(ImageBase);        //得到PE文件头指针
        if (!pNtH)
        return NULL;
    pFH=&pNtH->FileHeader;             //得到映射文件头指针
    return pFH;                        //返回IMAGE_FILE_HEADER指针
}

IMAGE_OPTIONAL_HEADER32结构的指针,函数如下:
PIMAGE_OPTIONAL_HEADER GetOptionalHeader(LPVOID ImageBase)
{
        PIMAGE_NT_HEADERS pNtH = NULL;   //定义PE文件头指针
        PIMAGE__OPTIONAL_HEADER pOH = NULL;   //定义可选映射头指针
        pNtH = GetNtHeaders(ImageBase);        //得到PE文件头指针
        if (!pNtH)
        return NULL;
    pOH=&pNtH->OptionalHeader;        //得到可选映像头指针
    return pOH;                       //返回IMAGE_OPTION_HEADER32指针
}

得到了这两个重要的结构指针之后,其它的事情就变得这样简单,我们只需要将FileHeader和OptionalHeader的信息显示出来,在《加密与解密》第三版中,是把FileHeader和OptionalHeader的信息以十六进制方式显示在编辑控件上,此时先用函数wsprintf将显示的值进行格式化,然后调用API函数中的SetDlgItemText即可,代码如下:
大家先先看看FileHeader的结构如下:
IMAGE_FILE_HEADER STRUCT
Machine WORD ? ;0004h - 运行平台
NumberOfSections WORD ? ;0006h - 文件的节数目
TimeDateStamp DWORD ? ;0008h - 文件创建日期和时间
PointerToSymbolTable DWORD ? ;000ch - 指向符号表(用于调试)
NumberOfSymbols DWORD ? ;0010h - 符号表中的符号数量(用于调试)
SizeOfOptionalHeader WORD ? ;0014h - IMAGE_OPTIONAL_HEADER32结构的长度
Characteristics WORD ? ;0016h - 文件属性
IMAGE_FILE_HEADER ENDS
下面编程将上面的各个信息完全显示出来了!!
void    ShowFileHeaderInfo(HWND hWnd)
{   
         char   cBuff[10];
     PIMAGE_FILE_HEADER pFH=NULL;
     
         pFH=GetFileHeader(stMapFile.ImageBase);    //得到文件头指针
     if(!pFH)
         {
                 MessageBox(hWnd,"Can't get File Header ! :(","PEInfo_Example",MB_OK);
             return;
         }
         wsprintf(cBuff, "%04lX", pFH->Machine);   //格式化输出内容
         SetDlgItemText(hWnd,IDC_EDIT_FH_MACHINE,cBuff);
         
         wsprintf(cBuff, "%04lX", pFH->NumberOfSections);
         SetDlgItemText(hWnd,IDC_EDIT_FH_NUMOFSECTIONS,cBuff);
         
         wsprintf(cBuff, "%08lX", pFH->TimeDateStamp);
         SetDlgItemText(hWnd,IDC_EDIT_FH_TDS,cBuff);
         
         wsprintf(cBuff, "%08lX", pFH->PointerToSymbolTable);
         SetDlgItemText(hWnd,IDC_EDIT_FH_PTSYMBOL,cBuff);
         
         wsprintf(cBuff, "%08lX", pFH->NumberOfSymbols);
         SetDlgItemText(hWnd,IDC_EDIT_FH_NUMOFSYM,cBuff);
         
         wsprintf(cBuff, "%04lX", pFH->SizeOfOptionalHeader);
         SetDlgItemText(hWnd,IDC_EDIT_FH_SIZEOFOH,cBuff);
         
         wsprintf(cBuff, "%04lX", pFH->Characteristics);
         SetDlgItemText(hWnd,IDC_EDIT_FH_CHARACTERISTICS,cBuff);
}

再来看看OptionalHeader结构的信息:
IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ? ;0018h 107h=ROM Image,10Bh=exe Image
MajorLinkerVersion BYTE ? ;001ah 链接器版本号
MinorLinkerVersion BYTE ? ;001bh
SizeOfCode DWORD ? ;001ch 所有含代码的节的总大小
SizeOfInitializedData DWORD? ;0020h所有含已初始化数据的节的总大小
SizeOfUninitializedData DWORD ? ;0024h 所有含未初始化数据的节的大小
AddressOfEntryPoint DWORD ? ;0028h 程序执行入口RVA
BaseOfCode DWORD ? ;002ch 代码的节的起始RVA
BaseOfData DWORD ? ;0030h 数据的节的起始RVA
ImageBase DWORD ? ;0034h 程序的建议装载地址
SectionAlignment DWORD ? ;0038h 内存中的节的对齐粒度
FileAlignment DWORD ? ;003ch 文件中的节的对齐粒度
MajorOperatingSystemVersion WORD ? ;0040h 操作系统主版本号
MinorOperatingSystemVersion WORD ? ;0042h 操作系统副版本号
MajorImageVersion WORD ? ;0044h可运行于操作系统的最小版本号
MinorImageVersion WORD ? ;0046h
MajorSubsystemVersion WORD ?;0048h 可运行于操作系统的最小子版本号
MinorSubsystemVersion WORD ? ;004ah
Win32VersionValue DWORD ? ;004ch 未用
SizeOfImage DWORD ? ;0050h 内存中整个PE映像尺寸
SizeOfHeaders DWORD ? ;0054h 所有头+节表的大小
CheckSum DWORD ? ;0058h
Subsystem WORD ? ;005ch 文件的子系统
DllCharacteristics WORD ? ;005eh
SizeOfStackReserve DWORD ? ;0060h 初始化时的堆栈大小
SizeOfStackCommit DWORD ? ;0064h 初始化时实际提交的堆栈大小
SizeOfHeapReserve DWORD ? ;0068h 初始化时保留的堆大小
SizeOfHeapCommit DWORD ? ;006ch 初始化时实际提交的堆大小
LoaderFlags DWORD ? ;0070h 未用
NumberOfRvaAndSizes DWORD ? ;0074h 下面的数据目录结构的数量
DataDirectory    IMAGE_DATA_DIRECTORY 16 dup(<>) ;0078h
IMAGE_OPTIONAL_HEADER32 ENDS
代码和上面是一样的,只是未显示完全,只是显示了几个重要的字段内容!!

得到数据目录表的信息:
数据目录表(DataDirectory)由一组数组构成,每组项目包括执行文件的重要部分的起妈RVA和长度。因为数据目录有16项,书有用了一种简单的方法,就是定义一个编辑控件ID的结构数组,用一个循环就可以了。
我们先来看一个DataDirectory的结构
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ? ;数据的起始RVA
Size DWORD ? ;数据块的长度
IMAGE_DATA_DIRECTORY ENDS
很简单就两个字段,我们就先定义一个结构体用于存放这两个字段,然后在定义一个结构体数组就于存放十六个结构体
typedef struct
{
    UINT   ID_RVA;             //用于存放DataDirectory数据块的起始RVA
    UINT   ID_SIZE;            //用于存放DataDirectory数据块的大小
} DataDir_EditID;

DataDir_EditID EditID_Array[]=
{
        {IDC_EDIT_DD_RVA_EXPORT,     IDC_EDIT_DD_SIZE_EXPORT},
    {IDC_EDIT_DD_RVA_IMPORT,     IDC_EDIT_DD_SIZE_IMPORT},
    {IDC_EDIT_DD_RVA_RES,        IDC_EDIT_DD_SZIE_RES},
    {IDC_EDIT_DD_RVA_EXCEPTION,  IDC_EDIT_DD_SZIE_EXCEPTION},
        {IDC_EDIT_DD_RVA_SECURITY,         IDC_EDIT_DD_SIZE_SECURITY},
    {IDC_EDIT_DD_RVA_RELOC,                 IDC_EDIT_DD_SIZE_RELOC},
    {IDC_EDIT_DD_RVA_DEBUG,                 IDC_EDIT_DD_SIZE_DEBUG},
        {IDC_EDIT_DD_RVA_COPYRIGHT,         IDC_EDIT_DD_SIZE_COPYRIGHT},
        {IDC_EDIT_DD_RVA_GP,                 IDC_EDIT_DD_SIZE_GP},
    {IDC_EDIT_DD_RVA_TLS,        IDC_EDIT_DD_SIZE_TLS},
        {IDC_EDIT_DD_RVA_LOADCONFIG, IDC_EDIT_DD_SIZE_LOADCONFIG},
        {IDC_EDIT_DD_RVA_IAT,                 IDC_EDIT_DD_SIZE_IAT},
        {IDC_EDIT_DD_RVA_BOUND,                 IDC_EDIT_DD_SIZE_BOUND},
        {IDC_EDIT_DD_RVA_COM,                 IDC_EDIT_DD_SIZE_COM},
        {IDC_EDIT_DD_RVA_DELAYIMPORT,IDC_EDIT_DD_SIZE_DELAYIMPORT},
        {IDC_EDIT_DD_RVA_NOUSE,                 IDC_EDIT_DD_SIZE_NOUSE}
};
上面正是定义了十六个DataDirectory,这里用一个数组表示,主要是为了方便编程时使用,以避免代码的冗长!!!
显示数据目录表的函数如下:
void ShowDataDirInfo(HWND hDlg)
{
    char   cBuff[9];
    PIMAGE_OPTIONAL_HEADER pOH=NULL;
    pOH=GetOptionalHeader(stMapFile.ImageBase);   //得到IMAGE_OPTION_HEADER32的结构指针
   if(!pOH)
        return;

        for(int i=0;i<16;i++)                         //循环显示数据目录表的十六个元素
   {
    wsprintf(cBuff, "%08lX", pOH->DataDirectory[i].VirtualAddress);  //格式化DataDirectory中数据块的RVA
    SetDlgItemText(hDlg,EditID_Array[i].ID_RVA,cBuff);       //设置DataDirectory中数据块的RVA         
   
         wsprintf(cBuff, "%08lX", pOH->DataDirectory[i].Size);  //格式化DataDirectory中数据块的Size
     SetDlgItemText(hDlg,EditID_Array[i].ID_SIZE,cBuff);    //设置DataDirectory中数据块的Size
        }
}

得到区块表信息
紧接IMAGE_NT_HEADERS以后就是区块表(Section Table)了,Section Table则是由IMAGE_SECTION_HEADER组成的数组。如何得到Section Table的位置呢?换名话说,也就是如何得到第一个IMAGE_SECTION_HEADER的位置。在Visual C++中,可以利用IMAGE_FIRST_SECTION宏来轻松得到第一个IMAGE_SECTION_HEADER的位置。(这里先讲在VC中得到区块表,到后面会具体讲在汇编中如何得到)
又因为区块的个数已经在文件头中指明了,所以只要得到第一个区块的位置,然后再利用一个循环语句就可以得到所有区块的信息了。
请看下面的函数就是利用IMAGE_FIRST_SECTION宏得到区块表的起始位置。
PIMAGE_SECTION_HEADER GetFirstSectionHeader(PIMAGE_NT_HEADERS pNtH)
{
        PIMAGE_SECTION_HEADER pSH;      //定义区块表首地址指针
        pSH = IMAGE_FIRST_SECTION(pNtH);  //得到区块表首地址指针
        return pSH;                           //返回区块首地址指针
}
这里必须强调一下,在一个PE文件中,OptionHeader的大小是可以变化的,虽然它的大小通常为E0h,但是总是有例外,原因是可选文件头的大小是由文件头的SizeOfOptionalHeader字段指定的,并不是个固定值。这也是IMAGE_FIRST_SECTION宏对于可选文件头的大小为什么不直接用固定值的原因。系统的PE加载器在加载PE文件的时候,也是利用了文件头中的SizeOfOptionalHeader字段的值来定位区块表的,而不是用固定值。能否正确定位到区块表,取决于SizeOfOptionalHeader字段的值的正确性。这是个很容易被忽略的问题,因些导致一些程序的BUG。书中使用ListView控件来显示PE文件头的区段信息。具体代码如下:
void ShowSectionHeaderInfo(HWND hDlg)
{
        LVITEM                  lvItem;
        char                    cBuff[9],cName[9];
        WORD                    i;
    PIMAGE_FILE_HEADER       pFH=NULL;      //定义映射头指针
        PIMAGE_SECTION_HEADER   pSH=NULL;       //定义区块表指针

        pFH=GetFileHeader(stMapFile.ImageBase);     //得到映射头的指针主要是为了得到区块的数目通过NumberOfSections字段
        if(!pFH)
        return;
       
        pSH=GetFirstSectionHeader(stMapFile.ImageBase);   //得到第一个区块表指针

        for( i=0;i<pFH->NumberOfSections;i++)            //循环得到各区块表的指针
        {
                memset(&lvItem, 0, sizeof(lvItem));
                lvItem.mask    = LVIF_TEXT;
                lvItem.iItem   = i;

                memset(cName,0,sizeof(cName));              //设置区块表中的各个字段的值
                memcpy(cName, pSH->Name, 8);
       
                lvItem.pszText = cName;
        SendDlgItemMessage(hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
       
        lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", pSH->VirtualAddress);
                lvItem.iSubItem = 1;
        SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
      
                wsprintf(cBuff, "%08lX", pSH->Misc.VirtualSize);
                lvItem.iSubItem = 2;
        SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);

                wsprintf(cBuff, "%08lX", pSH->PointerToRawData);
                lvItem.iSubItem = 3;
        SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
        
                wsprintf(cBuff, "%08lX", pSH->SizeOfRawData);
                lvItem.iSubItem = 4;
        SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
               
                wsprintf(cBuff, "%08lX", pSH->Characteristics);
                lvItem.iSubItem = 5;
        SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);

                ++pSH;        //指向下一个区块位置
        }
}

得到输出表信息
输出表(Export Table)中的主要成分是一个表格,内含函数名称,输出序数等。输出表是数据目录表的第一个成员,其指向IMAGE_EXPORT_DIRECTORY结构。输出函数的个数是由结构IMAGE_EXPORT_DIRECTORY的字段NumberOfFunctions来说明的。实际上,也有例外,例如在写一个DLL的时候,可以用DEF文件来制定输出函数的名称,序号等。请看下面这个DEF文件内容:
LIBRARY TEST
EXPORTS
        Func1 @1
        Func2 @12
        Func3 @18
        Func4 @23
        Func5 @31
在这个文件中,共输出了五个函数(Func1到Func5),而输出函数的序号却是1到31,如果没有考虑到这一点的话,很有可能会在这里出错,因为这时IMAGE_EXPORT_DIRECTORY的字段NumberOfFunctions的值为0x1F,即31。如果认为NumberOfFunctions值就为输出函数个数的话,就错了。
首先通过下面的两个函数来得到输出表的指针
LPVOID GetDirectoryEntryToData(LPVOID ImageBase,USHORT DirectoryEntry)
{
        DWORD dwDataStartRVA;
        LPVOID pDirData=NULL;
        PIMAGE_NT_HEADERS     pNtH=NULL;
        PIMAGE_OPTIONAL_HEADER pOH=NULL;

        pNtH=GetNtHeaders(ImageBase);
        if(!pNtH)
                return NULL;
        pOH=GetOptionalHeader(ImageBase);
        if(!pOH)
                return NULL;
    dwDataStartRVA=pOH->DataDirectory[DirectoryEntry].VirtualAddress;
      if(!dwDataStartRVA)
        return NULL;
  
        pDirData=RvaToPtr(pNtH,ImageBase,dwDataStartRVA);
   if(!pDirData)
                return NULL;         
           return  pDirData;
}

PIMAGE_EXPORT_DIRECTORY  GetExportDirectory(LPVOID ImageBase)
{
   
        PIMAGE_EXPORT_DIRECTORY pExportDir=NULL;
        pExportDir=(PIMAGE_EXPORT_DIRECTORY)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_EXPORT);
    if(!pExportDir)
                return NULL;         
           return  pExportDir;
}
PIMAGE_IMPORT_DESCRIPTOR  GetFirstImportDesc(LPVOID ImageBase)
{
        PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
        pImportDesc=(PIMAGE_IMPORT_DESCRIPTOR)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_IMPORT);
    if(!pImportDesc)
                return NULL;         
           return  pImportDesc;
}

显示输出表信息的函数如下:
void   ShowExportFuncsInfo(HWND hDlg)
{
        HWND         hList;
        LVITEM       lvItem;
        char         cBuff[10], *szFuncName;
       
        UINT                    iNumOfName=0;
        PDWORD                  pdwRvas, pdwNames;
        PWORD                   pwOrds;
        UINT                    i=0,j=0,k=0;
        BOOL                    bIsByName=FALSE;;

        PIMAGE_NT_HEADERS       pNtH=NULL;
    PIMAGE_EXPORT_DIRECTORY pExportDir=NULL;

    pNtH=GetNtHeaders(stMapFile.ImageBase);
    if(!pNtH)
                return ;
        pExportDir= (PIMAGE_EXPORT_DIRECTORY)GetExportDirectory(stMapFile.ImageBase); //调用GetExprotDirectory来得到输出表的首地址指针
        if (!pExportDir)
                    return ;

        pwOrds=(PWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfNameOrdinals);  //指向输出序列号数组
        pdwRvas=(PDWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfFunctions);   //指向函数地址数组
        pdwNames=(PDWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfNames);      //函数名字的指针地址

        if(!pdwRvas)      //如果函数地址数组为NULL,则直接返回
                return;
  
        hList=GetDlgItem(hDlg,IDC_EXPORT_LIST);
        SendMessage(hList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
               
       
        iNumOfName=pExportDir->NumberOfNames;           //得到函数名字的指针地址阵列中的元素个数

        for( i=0;i<pExportDir->NumberOfFunctions;i++)   //得到函数地址数组阵列中的元素个数
        {
                if(*pdwRvas)                                //如果函数地址数组中的值不为NULL,则继续显示,否则指向函数地址数组中下一个
                {   
                        for( j=0;j<iNumOfName;j++)        //以函数名字指针地址阵列中的元素个数为循环
                        {
                                if(i==pwOrds[j])             //如果函数地址数组的值等于函数名字的指针地址中元素j的值
                                {  
                                        bIsByName=TRUE;
                                        szFuncName=(char*)RvaToPtr(pNtH,stMapFile.ImageBase,pdwNames[j]);
                                        break;
                                }
                               
                                bIsByName=FALSE;
                        }
                
                //show funcs to listctrl
       
                memset(&lvItem, 0, sizeof(lvItem));
                lvItem.mask    = LVIF_TEXT;
                lvItem.iItem   = k;
                      
                lvItem.pszText = cBuff;
                wsprintf(cBuff, "%04lX", (UINT)(pExportDir->Base+i));
                SendDlgItemMessage(hDlg,IDC_EXPORT_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
       
        lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", (*pdwRvas));
                lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
   
                if(bIsByName)                       
                        lvItem.pszText=szFuncName;
                else
                        lvItem.pszText  = "-";
       
                lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);       
                   //
                ++k;
       
                }
       
                        ++pdwRvas;                  
}  
}
得到输入表的信息
数据目录表第二个成员指向输入表。输入表以一个IMAGE_IMPORT_DESCRIPTOR结构开始,以一个空的IMAGE_IMPORT_DESCRIPTOR结构结束。在这里可以通过GetFirstImportDesc函数得到ImportTable在文件中的位置。
GetFirstImportDesc函数的定义如下:

PIMAGE_IMPORT_DESCRIPTOR  GetFirstImportDesc(LPVOID ImageBase)
{
        PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
        pImportDesc=(PIMAGE_IMPORT_DESCRIPTOR)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_IMPORT);
    if(!pImportDesc)
                return NULL;         
           return  pImportDesc;
}
这个函数同样用到了上面所使用的GetDirectoryEntryToData,这个函数是用于专门得到区块表的各个数据块的位置而准备的,我们找到了输入表的位置,可以通过一个循环来得到整个输入表,循环终止的条件是IMAGE_IMPORT_DESCRIPTOR结构为空。
void  ShowImportDescInfo(HWND hDlg)
{
        HWND         hList;
        LVITEM       lvItem;
        char         cBuff[10], * szDllName;
       
    PIMAGE_NT_HEADERS       pNtH=NULL;
        PIMAGE_IMPORT_DESCRIPTOR  pImportDesc=NULL;

        memset(&lvItem, 0, sizeof(lvItem));
       
        hList=GetDlgItem(hDlg,IDC_IMPORT_LIST);
        SendMessage(hList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);

        pNtH=GetNtHeaders(stMapFile.ImageBase);
        pImportDesc=GetFirstImportDesc(stMapFile.ImageBase);
    if(!pImportDesc)
        {
                MessageBox(hDlg,"Can't get ImportDesc:(","PEInfo_Example",MB_OK);
                return;
        }
       
        int i=0;
    while(pImportDesc->FirstThunk)
        {
               
                memset(&lvItem, 0, sizeof(lvItem));
                lvItem.mask    = LVIF_TEXT;
                lvItem.iItem   = i;
      
                szDllName=(char*)RvaToPtr(pNtH,stMapFile.ImageBase,pImportDesc->Name);
       
                lvItem.pszText = szDllName;
                SendDlgItemMessage(hDlg,IDC_IMPORT_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
        lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", pImportDesc->OriginalFirstThunk);
                lvItem.iSubItem = 1;
                SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
   
            lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", pImportDesc->TimeDateStamp);
                lvItem.iSubItem = 2;
                SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
   
                lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", pImportDesc->ForwarderChain);
                lvItem.iSubItem = 3;
                SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
   
                lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", pImportDesc->Name);
                lvItem.iSubItem = 4;
        SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
   
                lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", pImportDesc->FirstThunk);
                lvItem.iSubItem = 5;
        SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
   
           ++i;
           ++pImportDesc;
        }
}
在ShowImportDescInfo函数中,首先用GetFirstImportDesc函数得到指向第一个IMAGE_IMPORT_DESCRIPTOR结构和指针pImportDesc,以pImportDesc-->FirstThunk为真来作为循环的条件,循环得到ImportTable的各项信息。

通过上面的ShowImportDescInfo函数,可以得到PE文件所引入的DLL的信息,接下来的任务就是如何分析得到通过DLL所输入的函数的信息,这里必须通过IMAGE_IMPORT_DESCRIPTOR所提供的信息来得到输入的函数的信息。可以通过名字和序号来引入所用的函数,怎么来区分一个函数是如何引入的呢?在于IMAGE_THUNK_DATA值的高位,如果被置位了,低31位被看作是一个序数值。如果高位没有被置位,IMAGE_THUNK_DATA值是一个指向IMAGE_IMPORT_BY_NAME的RVA。如果两者都不是,则可以认为IMAGE_THUNK_DATA值为函数的内存地址。具体参考下面的ShowImportFuncsByDllIndex(HWND hDlg,int index)函数:
void ShowImportFuncsByDllIndex(HWND hDlg,int index)
{
    HWND         hFuncList;
        LVITEM       lvItem;
        char         cBuff[30],cOrd[30],cMemAddr[30], * szFuncName;
    DWORD        dwThunk, *pdwThunk=NULL, *pdwRVA=NULL;
    int i=0;
               
        PIMAGE_NT_HEADERS         pNtH=NULL;
        PIMAGE_IMPORT_DESCRIPTOR  pFistImportDesc=NULL,pCurrentImportDesc=NULL;
    PIMAGE_IMPORT_BY_NAME     pByName=NULL;
        memset(&lvItem, 0, sizeof(lvItem));
       
        hFuncList=GetDlgItem(hDlg,IDC_IMPORTFUNCTIONS_LIST);
        SendMessage(hFuncList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
    SendMessage(hFuncList,LVM_DELETEALLITEMS ,0,0);

        pNtH=GetNtHeaders(stMapFile.ImageBase);
        pFistImportDesc=GetFirstImportDesc(stMapFile.ImageBase);
    pCurrentImportDesc=&pFistImportDesc[index];
        dwThunk=GETTHUNK(pCurrentImportDesc);

        pdwRVA=(DWORD *)dwThunk;
        pdwThunk=(DWORD*)RvaToPtr(pNtH,stMapFile.ImageBase,dwThunk);
           if(!pdwThunk)
                    return;

        while(*pdwThunk)
    {
                memset(&lvItem, 0, sizeof(lvItem));
                lvItem.mask    = LVIF_TEXT;
                lvItem.iItem   = i;

                lvItem.pszText = cBuff;
                wsprintf(cBuff, "%08lX",(DWORD)pdwRVA);
                SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
               
                lvItem.pszText  = cBuff;
                wsprintf(cBuff, "%08lX", (DWORD)(*pdwThunk));
                lvItem.iSubItem = 1;
                SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
   
                if (HIWORD(*pdwThunk)==0x8000)        //如果最高位被置位了,那么低31位是一个序数值
                {                       
                        strcpy(cBuff,"-");
                        wsprintf(cOrd, "Ord:%08lX",IMAGE_ORDINAL32(*pdwThunk));
                        szFuncName=cOrd;
                }
                else         //如果最高位没有被置位IMAGE_THUNK_DATA值是指向IMAGE_IMPORT_BY_NAME的RVA
                {
                        pByName =(PIMAGE_IMPORT_BY_NAME)RvaToPtr(pNtH,stMapFile.ImageBase,(DWORD)(*pdwThunk));
                        if(pByName)
                        {
                                wsprintf(cBuff,"%04lX",pByName->Hint);
                                szFuncName=(char *)pByName->Name;
                        }
                        else   //如果两者都不是,则可以认为IMAGE_THUNK_DATA值为函数的内存地址
                        {
                                strcpy(cBuff,"-");
                                wsprintf(cMemAddr, "MemAddr:%08lX",(DWORD)(*pdwThunk));
                                szFuncName=cMemAddr;
                        }
                }
   
                lvItem.pszText  = cBuff;               
                lvItem.iSubItem = 2;
                SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);

   
                lvItem.pszText = szFuncName;
                lvItem.iSubItem = 3;
                SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);   
       
               
                ++i;
                ++pdwRVA;
                ++pdwThunk;       
        }
}

到此,一个PE的简单工具的核心代码就基本上分析完毕了,其实当你真正了解了前面三章所讲的内容,再去看这些代码,你会觉得很简单是不是,只要你花时间,你也可以写一人简单的PE分析工具,呵呵,如果还没有弄明白的,我向大家推荐《加密与解密》第三版第十章的PE工具编写的源代码,大家可以再仔细研究研究,由于我的C不是很好,这时我就先说到这里吧,明天我会继续给大家讲解PE编程方面的问题,我相信一定会让大家对PE编程更加清楚明白!!
上篇文章有位朋友说要写上参考文献-----《加密与解密》第三版第十章PE工具的编写
雪    币: 92
活跃值: 活跃值 (16)
能力值: (RANK:60 )
在线值:
发帖
回帖
粉丝
沙加L 活跃值 1 2010-10-5 17:10
48
0
沙发,楼主真强,小弟实在佩服
雪    币: 92
活跃值: 活跃值 (16)
能力值: (RANK:60 )
在线值:
发帖
回帖
粉丝
沙加L 活跃值 1 2010-10-5 17:11
49
0
楼主,我觉得你下次写的时候,可以参考下《软件调试》这本书,我觉得这本书也讲得挺好的
雪    币: 1115
活跃值: 活跃值 (15)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
pencil 活跃值 5 2010-10-5 17:52
50
0
这个必须支持了。已经整理成pdf了,这两天走到哪都能随身看看,不错。
游客
登录 | 注册 方可回帖
返回