问:导入表是干什么的?
答:当我们PE文件运行的时候,需要调用一些外部接口,导入表就是记录导入的那些外部接口的。
问:调用外部接口的大概流程是怎么样的?
答:当操作系统装载可执行文件的时候,首先会分析可执行文件需要哪些动态链接库,然后会分析每个动态链接库需要哪些函数,当把这些函数加载进内存后,会把这些函数加载到内存的所在地址填到操作系统和编译器约好的位置。在约好的地方填写完所需函数的加载地址后,当编译器在编译产生代码的时候要调用操作系统函数或者调用第三方函数时就会到约好的地方去间接的访问所需函数的地址,这样就完成了一次外部接口的调用。
问:编译器间接调用所需API地址是怎么样的?
答:如下图所示,7743F9C0便是编译器间接调用所需API地址:
问:操作系统与编译器约好存放API地址的位置是什么?
答:IAT(import address table/导入地址表)
问:操作系统怎么知道在与编译器约好的地方填入什么API?
答:这是一个数据关系问题,如果要将动态链接库中的API填入到操作系统与编译器约好的地方,我们就得想明白操作系统需要将哪些API填入到与编译器约好的位置。首先API被包含在动态链接库当中,API和包含API的动态链接库之间是一种一对多的数据关系,就好像是大学专业和大学生之间的关系,一个大学生只能选择一个专业,一个专业可以包含多个大学生。弄明白了动态链接库和API之间的数据关系,那我们还得理清楚在此种数据关系下怎么样才能有效的填入API;想要有效的填入API我们就得告诉操作系统需要哪些动态链接库,动态链接库当中又需要装载哪些API,那么我们就可以通过动态链接库信息表来查找PE文件需要调用动态链接库中的哪些API。所以当操作系统装载可执行文件的时候,首先会分析可执行文件需要哪些动态链接库,然后会分析每个动态链接库需要哪些函数,最后只要把需要装载的API往操作系统和编译器约好的地方填就完事了。
问:导入表的基本结构?
答:
上图看起来很蒙,别急,听我慢慢道来:
首先我们已知数据目录中记录的导入表位置为相对于可选标头起始位置偏移104字节,数据目录中存储导入表RVA和大小的结构体大小为8字节,如下图所示:
此处记录的导入表大小仅供参考,不能盲目信任。
我们根据数据目录中记录的导入表的RVA跳转到92000位置,导入表是由一个或多个IMAGE_IMPORT_DESCRIPTO结构(上面那张导入表结构图最左边的结构体)组成的,每个IMAGE_IMPORT_DESCRIPTO结构总大小为20字节,每个IMAGE_IMPORT_DESCRIPTO结构记录一个需要导入到内存的动态链接库的信息,当IMAGE_IMPORT_DESCRIPTO结构为零时表示导入表结束,所以我们可以通过IMAGE_IMPORT_DESCRIPTO结构是否为零来判断导入表是否结束。
如上图所示,每一种颜色表示导入表中一个需要导入到内存中的动态链接库的信息,只标了四个是因为加载到内存中的动态链接库太多了,总之记住当IMAGE_IMPORT_DESCRIPTO结构为零时表示结束;这个结构体由五个字段组成,每个字段各4字节大小,其中比较重要的是第一个字段——导入名称表、第四个字段——动态链接库名称、第五个字段——导入地址表。
导入表大概流程:
编译器在编译过程中会把所需要的动态链接库和动态链接库当中的API存储到导入表当中,编译完成后,运行PE文件时,操作系统会将PE文件加载到内存中,会先定位到动态链接库信息,然后获取动态链接库名称,再使用LoadLibrary()这个系统API去动态加载这个动态链接库到内存当中,成功加载动态链接库后,接着访问导入名称表字段提供的RVA。我们跟着红线所划的IMAGE_IMPORT_DESCRIPTO结构导入名称表字段所记录的RVA,去看看导入名称表的模样:
我们来到了RVA为92274的位置,这里的数据是非常多的,我们该怎么下手呢?其实很简单,以每四个字节为一组指向该动态链接库当中一个需要加载到内存的API名称所在位置的RVA,在跟这个RVA之前,我们先来看看这个导入名称表每一组的结构:
这是官方给出的导入名称表每一组存储API名称的RVA数据的结构图,导入查阅表格就是导入名称表。导入名称表为PE32格式,那么会将导入名称表中每一组数据和0x80000000进行与(and)运行,以判断最高位是零还是1;PE32+结构则是和0x8000000000000000进行与运算,当导入名称表中一组数据的最高位为1时便按照序号字段进行导入,最高位为零时按照名称导入。
接下来我们跟一个过去看看,就跟着第一个9392C去一探究竟:
上图所划的区域是一个需要导入到内存的API名称,这个名称是IMAGE_IMPORT_BY_NAME结构,我们来看一下IMAGE_IMPORT_BY_NAME结构:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 编译时需导入的函数序号,但操作系统不参考
BYTE Name[1]; // 需导入的函数名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
// 在该结构后面会尾随零填充字节(如有必要出现在尾随 null 字节之后),以在偶数边界上对齐下一个条目。
看完IMAGE_IMPORT_BY_NAME结构,我们再去看导入的API名称,就可以看到前两个字节为编译时函数序号,操作系统不参考,可以随意修改,而后面的字节十分重要,为导入函数的名称。
我们获取到该动态链接库中一个要加载到内存的API名称后,将该API加载到内存中,通过GetProcAddress()系统API来获取该API加载到内存的地址,那这个地址存放在哪里呢?那就需要读取IMAGE_IMPORT_DESCRIPTO结构最后一个字段——导入地址表,我们还是跟着红线所划的IMAGE_IMPORT_DESCRIPTO结构导入地址表字段所记录的RVA,去一探究竟:
看上图,当获取到API加载到内存的地址后,会访问IMAGE_IMPORT_DESCRIPTO结构导入地址表字段所记录的RVA,然后将API的加载地址存放在此处。之前说过IMAGE_IMPORT_DESCRIPTO结构导入名称表字段所记录的RVA,每四个字节为一组指向该动态链接库当中一个需要加载到内存的API名称所在位置的RVA,而导入地址表也是以这种格式来存储API加载地址,每四个字节记录一个API加载到内存的地址,最后一条记录设置为零以标识表的结尾。当该API加载到内存中的地址填入到导入地址表后,继续循环下一个API名称,通过它将指定API加载到内存中来,直到值为零时,这个动态链接库里需要加载到内存的API就全部加载完毕。接下来就会到下一个动态链接库去执行这一套流程。