导入表 与 IAT

来源:互联网 发布:数据分析平台 功能 编辑:程序博客网 时间:2024/06/03 21:48

原文连接:

http://www.feiesoft.com/win32asm/win32asm-17-9.html


WIN32汇编语言教程:第17章 PE文件 · 17.2 导入表


在开始下面几节的介绍前,先来复习一下17.1节中提出的两个概念。

首先,PE文件中的数据按照装入内存后的页面属性被划分成多个节,并由节表中的数据来描述这些节。一个节中的数据仅仅是属性相同而已,并不一定就是同一种用途的,比如导入表、导出表等就有可能和只读常量一起被放在同一个节中,因为它们的属性同是可读不可写的。

其次,由于不同用途的数据可能被放在同一个节中,仅仅依靠节表是无法确定它们的存放位置的,PE文件中依靠文件头中IMAGE_OPTIONAL_HEADER32结构内的数据目录表来指出它们的位置,可以由数据目录表来定位的数据包括导入表、导出表、资源、重定位表和TLS等15种数据。

好了,现在要引出这几节将要讲述的内容了:从数据目录表得到的是这些数据的RVA和数据块的尺寸,很明显,不同的数据块中的数据组织方式是不同的,比如导入表和资源数据块中的数据就完全是两码事情,要想深入了解PE文件就必须了解这些数据的组织方式以及系统是如何处理它们的,这就是本节以及下面几个小节的内容。

本节将首先介绍导入表的格式,下面的几个小节将逐一介绍导出表、资源和重定位表的格式和使用方法。

17.2.1 导入表简介

在Win32编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。

对于存储在磁盘上的PE文件来说,它无法得知这些导入函数会在内存的哪个地方出现,只有当PE文件被装入内存的时候,Windows装载器才将DLL装入,并将调用导入函数的指令和函数实际所处的地址联系起来,这就是“动态链接”的概念。动态链接是通过PE文件中定义的“导入表”(Import Table)来完成的,导入表中保存的正是函数名和其驻留的DLL名等动态链接所必需的信息。

1. 调用导入函数的指令

程序被执行的时候是怎样使用导入函数的呢?先将第03章中那个简单的Hello World程序反汇编一把,看看调用导入函数的指令都是什么样子的,需要反汇编的两句源代码如下:

   invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK   invoke ExitProcess,NULL

当使用W32Dasm反汇编以后,这两句代码变成了以下的指令:

:00401000 6A00                  push 00000000:00401002 6800304000                push 00403000:00401007 680F304000                push 0040300F:0040100C 6A00                   push 00000000:0040100E E807000000                Call 0040101A      ;MessageBox:00401013 6A00                   push 00000000:00401015 E806000000                Call 00401020      ;ExitProcess:0040101A FF2508204000           Jmp dword ptr [00402008]:00401020 FF2500204000           Jmp dword ptr [00402000]

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是在DLL模块中的,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr [xxxxxxxx]类型的指令,这个指令是一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。

那么在没有装载到内存之前,PE文件中的00402000地址处的内容是什么呢?使用在17.1.4节中了解的方法来查看一下。

首先,使用17.1.4节的例子文件PEInfo.exe去查看一下Hello.exe文件,会得到以下的信息:

文件名:C:\Documents and Settings\Administrator\桌面\Hello.exe----------------------------------------------------------运行平台:         0x014C节区数量:         3文件标记:         0x010F建议装入地址:     0x00400000----------------------------------------------------------节区名称 节区大小 虚拟地址 Raw_尺寸 Raw_偏移 节区属性----------------------------------------------------------.text    00000026 00001000 00000200 00000400 60000020.rdata   00000092 00002000 00000200 00000600 40000040.data    00000022 00003000 00000200 00000800 C0000040

由于建议装入地址是00400000h,所以00402000h地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移项目为600h,也就是说00402000h地址的内容实际上对应PE文件中偏移600h处的数据。

现在随便找一个16进制编辑器来看看文件0600h处的内容是什么:

0600 76 20 00 00 00 00 00 00-5C 20 00 00 00 00 00 00  v ......\ ......0610 54 20 00 00 00 00 00 00-00 00 00 00 6A 20 00 00  T ..........j ..0620 08 20 00 00 4C 20 00 00-00 00 00 00 00 00 00 00  . ..L ..........0630 84 20 00 00 00 20 00 00-00 00 00 00 00 00 00 00  . ... ..........0640 00 00 00 00 00 00 00 00-00 00 00 00 76 20 00 00  ............v ..0650 00 00 00 00 5C 20 00 00-00 00 00 00 BB 01 4D 65  ....\ ........Me0660 73 73 61 67 65 42 6F 78-41 00 55 53 45 52 33 32   ssageBoxA.USER320670 2E 64 6C 6C 00 00 75 00-45 78 69 74 50 72 6F 63  .dll..u.ExitProc0680 65 73 73 00 4B 45 52 4E-45 4C 33 32 2E 64 6C 6C  ess.KERNEL32.dll0690 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

查看的结果是00002076h,这显然不会是内存中的ExitProcess函数的地址,慢着!将它作为RVA看会怎么样呢?查看节表可以发现RVA地址00002076h也处于.rdata节内,减去节的起始地址00002000h后得到这个RVA相对于节首的偏移是76h,也就是说它对应文件0676h开始的地方,接下来可以惊奇地发现,0676h再过去两个字节的内容正是函数名字符串“ExitProcess”!

这都有点搞糊涂了,Call ExitProcess指令被编译成了Call aaaaaaaa类型的指令,而aaaaaaaa处的指令是Jmp dword ptr [xxxxxxxx],而xxxxxxxx地址的地方只是一个似乎是指向函数名字符串的RVA地址,这一系列的指令显然是无法正确执行的!

但如果告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。

接下来看看如何去获取导入表的位置,以及导入表中的数据是如何组织以便Windows装载器能够顺利地进行上面的转换工作的。

2. 获取导入表的位置

导入表的位置和大小可以从PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,对应的项目是DataDirectory字段的第2个IMAGE_DATA_DIRECTORY结构(见表17.4)。

从IMAGE_DATA_DIRECTORY结构的VirtualAddress字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址;如果在PE文件中查找导入表,那么需要首先使用17.1.4节中例举的_RVAToOffset子程序将RVA首先转换成文件偏移。


17.2.2 导入表的结构

1. PE文件中的导入表

现在得到了包含导入表的数据块,导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每个结构对应一个DLL文件,例如,如果一个PE文件从10个不同的DLL文件中引入了函数,那么就存在10个IMAGE_IMPORT_DESCRIPTOR结构来描述这些DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。

IMAGE_IMPORT_DESCRIPTOR结构的定义如下:

IMAGE_IMPORT_DESCRIPTOR STRUCT   union       Characteristics   dd  ?       OriginalFirstThunk dd  ?   ends   TimeDateStamp dd   ?   ForwarderChain dd  ?   Name1 dd           ?   FirstThunk dd      ?IMAGE_IMPORT_DESCRIPTOR ENDS

结构中的Name1字段(使用Name1作为字段名同样是因为Name一词和MASM的关键字冲突)是一个RVA,它指向此结构所对应的DLL文件的名称,这个文件名是一个以NULL结尾的字符串。

OriginalFirstThunk字段和FirstThunk字段的含义现在可以看成是相同的(使用“现在”一词的含义马上会见分晓),它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组的最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。

一个IMAGE_THUNK_DATA结构实际上就是一个双字,之所以把它定义成结构,是因为它在不同的时刻有不同的含义,结构的定义如下:

IMAGE_THUNK_DATA STRUCT   union u1       ForwarderString dd  ?       Function dd          ?       Ordinal dd           ?       AddressOfData dd    ?   endsIMAGE_THUNK_DATA ENDS

一个IMAGE_THUNK_DATA结构如何用来指定一个导入函数呢?当双字(就是指结构!)的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号。读者可以用预定义值IMAGE_ORDINAL_FLAG32(或80000000h)来对最高位进行测试,当双字的最高位为0时,表示函数以字符串类型的函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构的定义如下:

IMAGE_IMPORT_BY_NAME STRUCT   Hint dw     ?   Name1 db        ?IMAGE_IMPORT_BY_NAME ENDS

结构中的Hint字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为0,Name1字段定义了导入函数的名称字符串,这是一个以0为结尾的字符串。

整个过程听起来有些复杂,其实再看一下图17.5就很清楚了,图中示意了可执行文件导入了Kernel32.dll中的ExitProcess,ReadFile,WriteFile和lstrcmp函数的情况,其中,前面3个函数按照名称方式导入,最后的lstrcmp函数按照序号导入,这4个函数的序号分别是02f6h,0111h,002bh和0010h。


图17.5  函数的导入方法举例

现在来分析一下图17.5中的示例,导入表中IMAGE_IMPORT_DESCRIPTOR结构的Name1字段指向字符串“Kernel32.dll”,表明当前要从Kernel32.dll文件中导入函数,OriginalFirstThunk和FirstThunk字段指向两个同样的IMAGE_THUNK_DATA数组,由于要导入的是4个函数,所以数组中包含4个有效项目并以最后一个内容为0的项目作为结束。

第4个函数lstrcmp函数是以序号导入的,与其对应的IMAGE_THUNK_DATA结构的最高位等于1,和函数的序号0010h组合起来的数值就是80000010h,其余的3个函数采用的是以函数名导入的方式,所以IMAGE_THUNK_DATA结构的数值是一个RVA,分别指向3个IMAGE_IMPORT_BY_NAME结构,每个IMAGE_IMPORT_BY_NAME结构的第一个字段是函数的序号,后面就是函数的字符串名称了,一切就是这么简单!

2. 内存中的导入表

为什么需要两个一模一样的IMAGE_THUNK_DATA数组呢?答案是当PE文件被装入内存的时候,其中一个数组的值将被改作他用,还记得前面分析Hello World程序时提到的吗?Windows装载器会将指令Jmp dword ptr [xxxxxxxx]指定的xxxxxxxx处的RVA替换成真正的函数地址,其实xxxxxxxx地址正是由FirstThunk字段指向的那个数组中的一员。

实际上,当PE文件被装入内存后,内存中的映像就被Windows装载器修正成了图17.6所示的样子,其中由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,之所以在PE文件中使用两份IMAGE_THUNK_DATA数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。


图17.6 导入表被装入内存后的样子

3. 导入地址表(IAT)

IMAGE_IMPORT_DESCRIPTOR结构中FirstThunk字段指向的数组最后会被替换成导入函数的真正入口地址,暂且把这个数组称为导入地址数组。在PE文件中,所有DLL对应的导入地址数组在位置上是被排列在一起的,全部这些数组的组合也被称为导入地址表(Import Address Table,或者简称为IAT),导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址。

还有一个方法可以更方便地找到IAT的地址,那就是通过数据目录表。数据目录表中的第13项(索引值为12/ IMAGE_DIRECTORY_ENTRY_IAT)直接用来定义IAT数据块的位置和大小。


0 0
原创粉丝点击