我们再上一章节简要介绍了IAT表,我们知道如果程序调用dll中的函数时,必须通过IAT表来找到函数,我们基本了解了IAT表之后,我们今天来讲解一下导入表,通过本章节的学习,我们可以了解导入表,也能对IAT表有一个更深入的了解。
一.导入表是什么?
我们可以将导入表比作餐厅的菜单,而dll中函数的地址,就可以被比作为菜单上的菜品。
也就是说,导入表记录了dll中的函数名称和地址,exe在调用dll中的函数的时候,就可以通过导入表来找到函数。这里注意区分导入表和IAT表,IAT表是exe在调用dll中的函数时,通过IAT表中记录的地址来找到函数,而导入表记录了dll的名称,还有dll中函数的地址和名称(也有函数的导出序号)。
那么我们来看看导入表的结构:
typedef struct_IMAGE_IMPORRT_DESCRIPTOR{ union{ DWORD Characteristics; DWORD OriginaFirstThunk;//指向IMAGE_THUNK_DATA(输入名称表)结构数组的RVA } DWORD TimeDateStamp;//时间戳(当可执行文件不与被输入的DLL进行绑定时,此字段为0) DWORD ForwarderChain;//第一个被转向的API索引 DWORD Name;//指向被输入的DLL的名称字符串的第一个字符RVA DWORD FirstThunk;//指向输入地址表(IAT表)的RVA,IAT是一个IMAGE_THUNK_DATA结构的数组 }IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPOTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
解析:
该导入表结构中:
OriginaFIrstThunk中存储的是一个地址,指向IMAGE_THUNK_DATA(导入名称表)该表中记录了导入函数的名称,也被叫做INT表,该表的结束标志和很多表一样:最后一张表的数据全为0。
Name中存储的是指向该dll文件名的RVA
FirstThunk中存储的是该文件的IAT(import address table)表的RVA,因为导入表和IAT表都和调用的dll文件中的函数有密切关系,所以他俩总是在一起,该表的结束标志和很多表一样:最后一张表的数据全为0。
找到IAT表有两种方式:一种是通过数据目录的第13个项目的VirtualAddress找到,另一种方式是通过导入表的FirstTrunk找到
我们知道一个程序可能调用了很多dll文件,所以导入表也不只一张,VirtualAddress(数据目录中的第二个元素为导入表) 指向多个一样的导入表结构。
二.导入表详解
我们想要了解导入表,必须通过程序运行前和程序运行中两种状态来了解,也就是程序未被加载前和程序和调用的dll都被加载到独立4GB空间两种状态下:
我们给出图来帮助我们理解:
程序未被加载前:
我们可以很清楚地观察到,在程序未被加载前,INT(导入名称表)和IAT(导入地址表)中存储的地址都指向同一个地方:该处存储了函数名称字符串或函数导出序号,我们在上一章中也在程序二进制文件中观察到存储了MessageBoxA(上一章节存储了MessageBoxA是因为我写的程序调用了MessageBox函数),那么我们如何判断INT(导入名称表)中的元素到底是函数导出名称还是导出序号呢?
这里给出判断INT表中某一项方法:
在INT(导入名称)表中,若该项最高位为1,那么去除最高位,剩下的就是该函数的导出序号
若该项最高位为0,那么这一项就是RVA,指向IMAGE_TRUNK_BY_NAME。
typedef struct _IMAGE_THUNK_DATA32 { union { PBYTE ForwarderString; PDWORD Function; DWORD Ordinal; //序号 PIMAGE_IMPORT_BY_NAME AddressOfData;//指向IMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32; typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; //可能为空,编译器决定 如果不为空 是函数在导出表中的索引 BYTE Name[1]; //函数名称,以0结尾 } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
IMAGE_IMPORT_BY_NAME 中的 Name 并不只有 1 个字节,而是一直遍历到元素为 ‘\0’ 为止,OriginalFirstThunk 和 FirstThunk 的遍历 具体详见下图(这俩的遍历都一样的,其实都是遍历的 IMAGE_THUNK_DATA)。
程序和调用的dll被加载后:
我们来思考一个问题:既然INT表和IAT表都指向同一个地方,那么我们为什么需要两张表呢?
我们再上一章简单解析了IAT表,我们知道程序在调用dll中的函数时,要通过IAT表来间接寻址,如果只用一张表的话,在程序被加载后,IAT表中的项目被改为函数地址,那么程序不就不知道函数名称了吗?这就是为什么要有INT表和IAT表的原因。
我们可以观察到:在程序和调用的dll被加载后,IAT表中的项目不再指向函数名称,通过上一章节的简析,我们知道它指向了函数被加载后的真正地址,当程序调用函数时,通过IAT表就可以找到函数真正的位置了。
IAT 表在文件加载完成后系统会调用 GetProcAddress() 函数,就是做的我们前面写过的根据导出表函数序号或函数名字找到函数地址的功能。系统会循环 INT 表,根据表内的名字或序号调用 GetProcAddr() 得到地址,依次添加到 IAT 表中。
注意 IMAGE_IMPORT_BY_NAME 中的 Hint 不是导出序号,而是当前这个函数在导出表函数地址表中的索引。但基本没用,所以不一定是准确的,可以全部为0。而Name并不是一字节,而是以’\0’结尾的不定长字符串。
三.打印一个程序的导入表的信息(作业思路)
这里先给出基本思路(这里是打印程序未被加载前的导入表):
1.通过数据目录的第二项的VirtualAddress转换为FOA在文件中找到导入表
2.输出dll名(导入表中Name字段转换为FOA在文件中找到)
3.遍历OriginalFirstTrunk(OriginalFirstTrunk字段转换为FOA在文件中找到),打印导出序号或导出名称(需要转换为FOA在文件中找到)
4.遍历FirstTrunk(导入表中FirstTrunk字段转换为FOA在文件中找到)
5.找到下一张导入表,循环2,3,4,直到某张表的导入表内数据全为零结束