第四章 导入表
导入表是PE数据组织中的一个很重要的组成部分,它是为实现代码重用而设置的。通过分析导入表数据,可以获得诸如OE文件的指令中调用了多少外来函数,以及这些外来函数都存在于哪些动态链接库里等信息。Windows加载器在运行PE时会将导入表中声明的动态链接库一并加载到进程的地址空间,并修正指令代码中调用的函数地址。在数据目录中一共有四种类型的数据与导入表数据有关: 导入表、导入函数地址表、绑定导入表、延迟加载导入表。
4.1何为导入表
当程序调用了动态链接库的相关函数,在进行编译和链接的时候,编译程序和链接程序就会将调用的相关信息写入最终生成的PE文件中,以告诉操作系统这些函数的执行指令字节码从哪里能够获取。这些信息就是导入表所要描述的内容。
4.2导入函数
程序开发者在基于汇编语言的源程序中,通过invoke指令调用用户自定义的函数,或者从其他动态链接库中导入的函数。
4.2.1 invoke指令分解
在汇编语言中,程序一旦被编译,编译器会对invoke指令进行适当分解。分解后的指令中将会包含指向导入函数的地址的操作数。当PE文件被装载到内存中时,该操作数就会变成导入函数所在虚拟地址真实的VA。
使用OD打开HelloWorld.exe程序,查看汇编后的字节码以及相关调用如下:
书上的OD结果:
自己本地C++写的一个的OD结果:
VS2012 反汇编结果:
以书上的为解释例子:
将原代码中两个导入函数MessageBoxA和ExitProcess的调用语句解释成字节码分别为:
从指令的反汇编代码中可以看出,以第一个调用为例,对invoke指令的分解操作包含以下三步:
1.压栈。即先将要调用的所有参数push到栈中。(反向顺序压栈)。
2.段内调用。即通过指令call调用一个段内地址,既call 00401018。
3.无条件转移。call指令操作数0x00401018处的值是:FF25 28204000,该字节码反汇编,得到一个无条件跳转指令,跳转到了位置0x00402008处。
(从位置00402008处获取的值是导入函数MessageBoxA在内存中的VA。)
4.2.2 导入函数地址
导入函数是从动态链接库引入的函数,所以,导入函数地址位于被加载的进程地址空间的相应的动态链接库模块内。系统在执行用户程序对导入函数的调用语句时,会跳转到该地址处执行导入函数代码。
使用OD打开HelloWorld.exe,选择地址0x0040101E所在行,在其上单机鼠标右键,选择“数据窗口中跟随”|“内存地址”。OD(3)区就会显示内存从00402000开始的数据:
我的程序得到的是这个:
如上图所示,加粗部分既为导入表数据(大小为3Ch字节)。到目前为止。感觉两个jmp指令中的操作数0x00402008和0x402000都不在该导入表(黑体部分)的范围内,API函数调用好像与导入表无关。其实不是,jmp指令中的操作数虽然不在导入表范围内,但导入表的数据结构中有一个字段是指向这个操作数所在位置的。从跳转指令的操作所指向的位置0x00402008获取的值为77D507EAh。
该值是MessageBoxA这个导入函数在进程HelloWorld.exe中的VA。
现在来对比一下磁盘文件和内存映象的导入函数的地址数据,看看是否存在差别。
4.2.3 导入函数宿主
指令要运行,就必须将指令字节码调入到内存中。既然程序中调用了动态链接库的有关函数,那么程序进程地址空间也一定会有这些函数的指令代码。也就是说,操作系统会在加载时根据导入表的描述将调用的函数指令字节码复制到进程地址空间中。
事实上,操作系统总是会将该函数所处的动态链接库全部复制到地址空间,这些动态链接库便是导入函数的指令宿主。如果一个动态链接库在一个进程中加载过,且在其他进程中也引用了该链接库的函数,操作系统不会再次加载这个动态链接库,而是通过页面调用机制使两个进程同时访问一个动态链接库。也就是说,为了节约内存资源,操作系统只保证有一份代码存在于物理内存中,大家看到的在每个进程中加载的不同地址的相同动态链接库,其实只是在页面存取机制下的一个映射而已。
之后作者证明了这个,做了一些测试和数据说明。这里就直接省略了。直接把结论整理下好了:
编译程序在编译汇编语言源文件时,会把程序中的invoke语句分解成三部分:将参数压栈、call指令、jmp指令部分
call的操作是jmp指令所在的地址;而jmp指令的操作数则是该导入函数在导入表的地址。在程序中所有的导入函数可以排列在一起,组成IAT,动过这样的分解操作配合导入表实现对外部函数的调用。
4.3 PE中的导入表
导入表是数据目录中注册的数据类型之一,其描述信息位于数据目录第2个目录项中。IAT也是数据目录中注册的数据类型之一,其描述信息位于数据目录的第13个目录项中。使用OEDump小工具获取helloworld.exe的数据目录内容如下:
加黑部分为数据目录表中的导入表项,加框部分为导入函数地址表项。
其中下划线部分为导入表数据,共60个字节。方框部分为IAT数据,共16个字节。
4.3.2 导入表描述符IMAGE_IMPORT_DESCRIPTOR
导入表数据的起始是一组导入表描述符结构。没组20个字节,实例中60个字节的导入表数据被分成三个组。前两组均代表两个动态链接库,最后一组为全0结构,表示导入表描述已经结束。可以通过导入表起始地址和这个空结构计算出导入表中引用的动态链接库的个数。
其实,windows在查找导入表的时候并不一定要求最后一组的20个字节都为0,只要其中的字段Name1是0就已经满足结束条件了。导入表的每一组都是一个结构,成为导入表描述符IMAGE_IMPORT_DESCRIPTOR,该结构的具体定义如下:
54.IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk
+0000h,双字。因为它是指向另外数据结构的通路,因此简称为桥1.该字段指向一个包含一系列结构的数组。
指向的数组中的每个结构定义了一个导入函数的信息,最后以一个全0的结构作为结束。指向的数组中每一项为一个结构,此结构的名称是IMAGE_THUNK_DATA。该结构实际上只是一个双字,但在不同的时刻却拥有不同的解释。该字段有两种解释:
双字最高位为0,表示导入符号是一个数值,该数值是一个RVA。
双字最高位为1,表示导入符号是一个名称。
55.IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp
+0004h,双字。时间戳,一般不用,多数为0。如果该导入表被绑定,那么绑定后的这个时间戳就是被设置为对应dll文件的时间戳。操作系统在加载时,可以通过这个时间戳来判断绑定的信息是否过时。
56.IMAGE_IMPORT_DESCRIPTOR.ForwarderChain
+0008h,双字。这个字段的含义和名称并不一致,这里的Name1是一个RVA,它指向该结构所对应的DLL文件的名称,而这个名称是以” ”结尾的Ansi字符串。
58.IMAGE_IMPORT_DESCRIPTOR.FirstThunk
+0010h,双字。与OriginalFirstThunk相同,它指向的连接表定义了对Name1这个动态链接库引入的所有导入函数,简称桥2。
4.3.3 导入表的双桥结构
桥1和桥2最终指向了一个目的地,都指向了引入函数的“编号-名称”(Hint/Name)描述部分。而从桥2到目的地的过程中,还经理了另外一个很重要的结构IAT。
下图为引入了ExitProcess等3个函数的kernel32.dll的导入表描述符结构示意图。
以下是对helloworld.exe中的导入表数据的详细解释:
>>54 20 00 00
桥1,最高位为0,这是一个RVA,表明函数是以字符串类型的函数名导入的。先将RVA转换为FOA,值为0x00000654,从文件的该位置开始读取双字,知道去除的双字为“0”结束。每一个双字都是结构IMAGE_THUNK_DATA。该结构的详细定义如下:
因为这个动态链接库只调用了一个函数,所以,数组里只有两个元素。这组数中每个都是一个RVA,不过这个RVA却指向了另外一个结构IMAGE_IMPORT_BY_NAME。这个结构大小不确定,是桥1的最终目的地。结构的第一个为字,紧跟着是函数的名字。
从文件偏移0x0000065C开始的数据是(碰到“0”既结束):
9D 01 4D 65 73 73 61 67 65 42 6F 78 41 00
这些值组成的数据结构就是IMAGE_IMPORT_BY_NAME,详细描述如下:
59.IMAGE_IMPORT_BY_NAME.Hint
+0000h,双字。函数的编号,在DLL中对每个函数都进行了编号,访问函数时可以通过名称访问,也可以通过编号访问。
60.IMAGE_IMPORT_BY_NAME.Name1
+0004h,大小不确定。函数名字字符串的具体内容,以“