Loading... 源程序如下: ``` #include<windows.h> #include<tchar.h> int main(void) { MessageBox(NULL,_T("Hello World"),_T("对话框"),MB_OK); return 0; } ``` 编译为hw.exe,以此程序来学习pe结构中导入表的相关知识。 ##### 定位导入表 导入表Import: ![image.png](http://47.117.131.13/usr/uploads/2021/08/3884363243.png) 导入函数地址表ImportAddressTable: ![image.png](http://47.117.131.13/usr/uploads/2021/08/3701307168.png) 从上图可获得以下信息: - 导入表数据所在地址RVA=0x00024000,size=3Ch,FOA=0x00023000 - 导入函数地址表数据所在地址RVA=0x0002417C,size=140h,FOA=0x0002317C 在文件中定位到导入表数据: ![image.png](http://47.117.131.13/usr/uploads/2021/08/2772319184.png) 在文件中定位到导入函数地址表(IAT)数据: ![image.png](http://47.117.131.13/usr/uploads/2021/08/1843949098.png) 【注】:到现在为止,我们只要明确**IAT实际是导入表数据组织中一个重要的数据结构**。下面先从导入表数据入手,分析导入表的数据组织。 ##### 导入表描述符IMAGE_IMPORT_DESCRIPTOR **<u>导入表数据的起始是一组导入表描述符结构</u>**。<u>每组为20个字节,实例中60个字节的导入表被分成三个组。前两组代表两个动态链接库,最后一组全0结构,标识导入表描述已经结束。可以通过导入表起始地址和这个空结构计算出导入表中引用的动态链接库的个数</u>。windows在查找导入表的时候并不一定要求最后一组的20个字节都为0,只要其中的字段Name1是0就已经满足结束条件了。 ![image.png](http://47.117.131.13/usr/uploads/2021/08/3309839111.png) 导入表的每一组都是一个结构,称为导入表描述符IMAGE_IMPORT_DESCRIPTOR,该结构的定义如下: ``` typedef struct _IMAGE_IMPORT_DESCRIPTOR{ union{ DWORD Characteristics; //? DWORD OriginalFirstThunk; //桥1 指向IMAGE_THUNK_DATA(输入名称表)结构的数组 }; DWORD TimeDateStamp; //时间戳 DWORD ForwarderChain; //链表的前一个结构 DWORD Name; //指向链接库名字的指针 rva DWORD FirstThunk; //桥2 指向输入地址表(IAT)的RVA,IAT是一个IMAGE_THUNK_DATA结构的数组 }IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; ``` 以下是对每个字段的具体解释。 IMAGE_IMPORT_DESCRIPTOR.**OrigenalFirstThunk** 双字,因为它是指向另外数据结构的通路,因此简称为**桥1**。该字段指向一个包含了一系列结构的数组。<u>指向的数组中每个结构定义了一个导入函数的信息,最后以一个内容为全0的结构作为结束。</u>指向的数组中每一项为一个结构,此结构名称是IMAGE_THUNK_DATA。<u>该结构实际上只是一个双字</u>,但在不同的时刻却拥有不同的解释。该字段有两种解释: - 双字最高位为0,表示导入符号是一个数值,该值是一个RVA。(名称导入) - 双字最高位为1,表示导入符号是一个名称。(序号导入) IMAGE_IMPORT_DESCRIPTOR.**TimeDataStamp** 双字,一般不用,多为0。如果该导入表项被绑定,那么绑定后这个时间戳就被设置为对应DLL文件的时间戳。 IMAGE_IMPORT_DESCRIPTOR.**Name** 双字,<u>这里的name是一个RVA</u>,它指向该结构所对应的DLL文件的名称,而这个名称是以“\0”结尾的Ansi字符串。 我们可以知道IMAGE_IMPORT_DESCRIPTOR.**Name**字段的RVA=000242CAh ![image.png](http://47.117.131.13/usr/uploads/2021/08/523584408.png) 计算其FOA=000232CAh ![image.png](http://47.117.131.13/usr/uploads/2021/08/4069730689.png) 在文件中定位0x000232CA,可看到为动态链接库"USER32.DLL"的字符串名称,以“\0”结尾。 ![image.png](http://47.117.131.13/usr/uploads/2021/08/1943264770.png) IMAGE_IMPORT_DESCRIPTOR.**FirstThunk** 双字,与OrigenalFirstThunk相同,它指向的链表定义了针对Name这个动态链接库引入的所有导入函数,简称桥2。 ##### 导入表的双桥结构 桥1和桥2最终通向了一个目的地,都指向了引入函数的“编号 - 名称”(Hint / Name)描述部分。而从<u>桥2到目的地的过程中,还经过了另外一个很重要的结构IAT。</u> ![image.png](http://47.117.131.13/usr/uploads/2021/08/359935772.png) 以下是对hw.exe中的导入表数据的详细解释: ![image.png](http://47.117.131.13/usr/uploads/2021/08/1707246352.png) 文件中定位到导入表,size=60,每20个字节一组,分为3组,最后一组全0为结束标志,这样我们知道描述了两个动态链接库的信息。首先来看第一组(前20个字节),也即第一个动态链接库的信息。 对应结构体IMAGE_IMPORT_DESCRIPTOR: ![image.png](http://47.117.131.13/usr/uploads/2021/08/1400090419.png) **<u>桥1</u>**,最高位为0,这是一个RVA,表明函数是以字符串类型的函数名导入的。对应的FOA=0x0002314C ![image.png](http://47.117.131.13/usr/uploads/2021/08/2825427697.png) 在文件中定位到originalFirstThunk指向的数组(数组的每一项都是一个IMAGE_THUNK_DATA结构,双字,以0结束)。 ![image.png](http://47.117.131.13/usr/uploads/2021/08/1576077483.png) 每一个双字都是结构IMAGE_THUNK_DATA。该结构的详细定义如下,每个结构定义了一个导入函数的信息: ![image.png](http://47.117.131.13/usr/uploads/2021/08/1170452716.png) 连续取出的数分别为: ![image.png](http://47.117.131.13/usr/uploads/2021/08/3158596394.png) **因为这个动态链接库只调用了一个函数,所以,数组里只有两个元素。如果调用的函数多了,数组中的元素也会增加。******这组数中每一个都是一个RVA******,不过这个RVA却指向了另外的一个结构IMAGE_IMPORT_BY_NAME。这个结构大小不确定,是桥1的最终目的地。****结构的第一个为字,紧跟着的是函数的名字****。RVA=000242BC,FOA=000232BC,如下:01BEh + 函数的名称“MessageBoxA”(碰到“0”即结束):** ![image.png](http://47.117.131.13/usr/uploads/2021/08/211329463.png) 这些值组成的数据结构就是IMAGE_IMPORT_BY_NAME,详细描述如下: ![image.png](http://47.117.131.13/usr/uploads/2021/08/1027951034.png) IMAGE_IMPORT_BY_NAME.**Hint** 字,函数的编号,在DLL中对每个函数都进行了编号,访问函数时可以通过名称访问,也可通过编号访问。 IMAGE_IMPORT_BY_NAME.**Name1** 大小不确定,函数名称字符串的具体内容,以"\0"作为字符串结束的标志。 再回顾一下导入表的前20个字节,描述了第一个动态链接库的信息,对应如下: ![image.png](http://47.117.131.13/usr/uploads/2021/08/2572993445.png) FirstThunk指向的内容和OriginalFirstThunk是一样的。 ![image.png](http://47.117.131.13/usr/uploads/2021/08/4216535873.png) 在文件中,尽管通过桥1和桥2指向的数据值相同,但其存储的位置却是不同的。桥1指向的INT与桥2指向的IAT内容完全一样,但INT和IAT却存储在文件的不同位置。 ![image.png](http://47.117.131.13/usr/uploads/2021/08/1690595185.png) **<u>每一个IMAGE_IMPORT_DESCRIPTOR都对应一个唯一的动态链接库文件,以及引用了该动态链接库的多个函数,每个函数的最终“值 - 名称”描述均可以沿着桥1或者桥2找到,这种导入表结构被称为双桥结构。</u>** 双桥结构的导入表在文件在存在两分内容完全相同的地址列表。一般情况下,桥2指向的地址列表被定义为IAT,而桥1指向的地址列表则被定义为INT(Import Name Table)。有的链接程序只为导入表存储一个桥,如Borland公司的Tlink只保留桥2,这样的导入表我们称之为**单桥结构**的导入表。 ##### 导入函数地址表 PE文件中所有导入函数jmp指令操作数的集合,组成了另外一个数据结构,这个结构就是导入函数地址表(Import Address Table,IAT)。导入函数地址表是一个双字的数组,每个双字代表的是一个导入函数的VA,该地址称为导入函数地址(Import Address ,VA)。用户程序通过无条件跳转指令跳转到VA指定处,便可以运行引入函数的指令。由于IAT中定义了不知一个链接库的函数,为了区分这些从不同链接库引入的函数,规定多有引入函数按照链接库分类;相同链接库的函数地址排列在一起,最后以一个双字的0结束。IAT结构可以用下图表示: ![image.png](http://47.117.131.13/usr/uploads/2021/08/3953799446.png) 前面讲过,导入表和IAT是有紧密联系的,通过桥2即可定位到IAT。在内存中,**<u>桥1可以让你找到调用函数名称或函数的索引编号</u>**,<u>**桥2却可以帮助你找到该函数指令代码在内存空间的地址**</u>。导入表与IAT的关系如下图: ![image.png](http://47.117.131.13/usr/uploads/2021/08/3040307875.png) 当PE被加载进虚拟地址空间以后,IAT的内容会被操作系统更改为函数的VA。这个修改最终会导致通向“值 - 名称描述的桥2发生断裂,如下图所示。当桥2发生断裂以后,**如果没有桥1作为参照(因为桥1和桥2维护了两个一一对应的函数的RVA),我们就无法重新找到该地址到底是调用了哪个函数**。 ![image.png](http://47.117.131.13/usr/uploads/2021/08/1730830741.png) 此例子较为简单,只有一个导入函数,一个动态链接库。 **构造调用同一个DLL文件的多个函数的导入表** 下图为多个导入函数,多个动态链接库的情况图: ![image.png](http://47.117.131.13/usr/uploads/2021/08/3641678602.png) 在上图中,FirstThunk应该和OriginalThunk一样也有一份函数指针表,即桥2。只是这里没有画出来而已。下图是装入内存后的导入表: ![image.png](http://47.117.131.13/usr/uploads/2021/08/1950227901.png) **从现在掌握的对导入表的知识来看,定位导入函数地址表的方法有两种: 1)从导入表的最后一个导入表项IMAGE_IMPORT_DESCRIPTOR结构中的字段IMAGE_IMPORT_DESCRIPTOR.FirstThunk定位IAT。 2)通过数据目录表第13个数据项的描述直接定位IAT。 参考:《Windows.PE权威指南》 最后修改:2021 年 08 月 05 日 11 : 22 AM © 允许规范转载