1.关于IAT(import address table)表
当exe程序中调用dll中的函数时,反汇编可以看到,call后面并不是跟的实际函数的地址,而是给了一个地址;
这些连起来就是一张表,就是IAT表;
1)内存镜像中的dll中函数的调用;
例如:一个exe中调用系统提供的dll中的MessageBox函数时:
可以看到call的地址是42d2c4;
查看42d2c4中的内容:
可以看到:42d2c4中保存的是762b0026;
762b0026是dll的领空;也就是dll中MessageBox函数的地址;
总结:
exe程序调用dll中的函数时,会使用FF15call;
call的并不是实际的函数地址,而是该函数对应的IAT表的地址;
通过IAT表来找到实际的函数地址;
2)文件镜像中的IAT相关
接下来取文件中找42d2c4中保存的内容;
需要将RVA转成FOA;
42d2c4-400000 = 2d2c4;在.idata节;
foa = 2d2c4 - 2d000 + 2b000 = 2b2c4;
可以看到:文件镜像中保存的值为2d2f4;
继续追到2d2f4,对应的foa为2b2f4
看到并不是MessageBox的地址,而是一MessageBox先关的描述;
结论:IAT表在文件和内存中是不一样的;
3)原因分析
对于一个exe程序,在运行时会加载到独立的4gb空间;
exe程序是可以按ImageBase来加载的;
但该exe引用的dll并不能保证,因为ImageBase可能被占用;
因此对于dll来说有重定位表来记录dll中的可能需要修改绝对地址;
对于引用dll的exe程序来说,因为无法确定dll中引用的函数的地址,所以不会再文件镜像中将引用dll的函数的地址写死;
而是保存的是这些函数相关的信息;
当程序运行后,并且所有dll都加载到该程序的4gb空间后,此时dll中函数的地址才确定;
IAT表中将保存dll中函数在内存中的真实地址;
因此IAT表中在文件和内存中不同;
4)IAT表总结
IAT表在程序执行前和程序执行后是不一样的;
程序在编译时给分了一个空间,保存一个偏移,这个偏移指向的是引用dll中的函数的函数名,而不是真正的函数地址;
当程序加载完毕,所有的dll都贴到程序的4gb空间后,会把IAT表中的值改为真正要用的函数的地址;因为dll的地址因不是按ImageBase加载而不一样;
2.导入表
比如说当去餐馆吃饭时,餐馆会提供一个菜单,告诉谁提供了哪些菜;
自己在点餐时也需要给餐馆提供一个单,来告诉自己点了哪些菜;
对于程序来说dll提供导出表,导出表中保存的是导出函数的清单以及地址;
程序也需要提供一个导入表,来说明使用了哪些函数;
1)导入表的结构
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组 }; DWORD TimeDateStamp; //时间戳 DWORD ForwarderChain; DWORD Name; //RVA,指向dll名字,该名字已0结尾 DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组 } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
数据目录的第二项就是导入表;
导入表结构可能有多个,一个dll对应一个导入表结构;
其中比较重要的两个属性:
OriginalFirstThunk ->指向INT表(import name table);
表中存的是一些IMAGE_THUNK_DATA结构;
这个表以0结尾;
FirstThunk ->指向IAT表(import address table);
IAT表有两种方式可以找到:1】通过数据目录的第13个结构;2】导入表结构的这个属性;
2)导入表在文件加载前和文件加载后有区别
1】pe文件加载前:
IAT表和INT表中保存的结构都是IMAGE_THUNK_DATA,且都是以0结尾;
在文件加载前两张表里的内容完全一样;
IMAGE_THUNK_DATA用来保存函数名以及函数序号等信息;
IMAGE_THUNK_DATA结构中只有一个联合,占4字节的空间;
IMAGE_THUNK_DATA结构:
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;
IMAGE_IMPORT_BY_NAME结构:
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; //可能为空,编译器决定 如果不为空 是函数在导出表中的索引 BYTE Name[1]; //函数名称,以0结尾 } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
2】pe文件加载后:
文件加载后,系统会调用通过函数名或函数序号找函数地址的函数GetProcAddr() ;
得到函数地址后,用函数地址替换IAT表中的数据;
使用两张表INT、IAT的原因:
如果只有IAT表,在程序加载完后,IAT表中的函数名将替换成函数地址,就无法知道函数名,也就是留一个函数名的备份;
3.解析导入表
主要步骤:
1】定位导入表:
数据目录的第二个结构为导入表;
通过该结构,可以找到导入表rva,转成foa可以在文件中找到导入表;
每一个引用的dll都对应一个导入表,需要循环解析,直到遇到全0的导入表结构为止;
也就是sizeOf(IMAGE_IMPORT_DESCRIPTOR) 个 0 代表导入表结束;
2】解析dll名:
导入表中储存了引用的dll的名字信息;
在导入表结构的Name属性中;
3】解析OriginalFirstThunk:
OriginalFirstThunk指向INT表;
INT表中保存了IMAGE_THUNK_DATA结构,该结构只有一个联合,占4字节;
判断最高位是否为1,如果是那么除去最高位的值就是函数的导出序号;有些函数匿名导出的,不能用函数名找地址;
如果不是,那么这个值是一个RVA 指向IMAGE_IMPORT_BY_NAME ;
IMAGE_IMPORT_BY_NAME结构中有2个属性;
Hint ->是函数在导出表中的索引,并不是函数的序号,没有实际作用;
Name ->函数名,只有1个字节,也就是函数名的开始,因为字符串长度不固定,从字符串开始到0结束为一个函数名;
调用 GetProcAddr(m,函数的名字或者导出序号) 函数可以找到函数地址;
当IMAGE_THUNK_DATA结构为0时,表示该INT表结束;
4】解析FirstThunk:
FirstThunk指向IAT表,IAT表在程序加载前和INT表是一样的;
输出导入表信息:
代码:
#include "stdafx.h" #include "PeTool.h" #include "string.h" #define SRC "C:\\Users\\Administrator\\Desktop\\Hello.exe" //解析导入表 void printImport(){ //定义pe头结构指针 PIMAGE_DOS_HEADER dosHeader = NULL; //dos头指针 PIMAGE_FILE_HEADER peHeader = NULL; //pe头指针 PIMAGE_OPTIONAL_HEADER32 opHeader = NULL; //可选pe头指针 PIMAGE_DATA_DIRECTORY dataDir = NULL; //数据目录指针 PIMAGE_IMPORT_DESCRIPTOR importDir= NULL; //导入表指针 //1.将文件读入内存 LPVOID pFileBuffer = NULL; DWORD fileSize = ReadPEFile(SRC, &pFileBuffer); if(!pFileBuffer){ printf("读取dll文件失败\n"); return; } //2.初始化头结构指针 dosHeader = (PIMAGE_DOS_HEADER) pFileBuffer; peHeader = (PIMAGE_FILE_HEADER) ((DWORD)pFileBuffer + dosHeader->e_lfanew + 4); opHeader = (PIMAGE_OPTIONAL_HEADER32) ((DWORD)peHeader + IMAGE_SIZEOF_FILE_HEADER); dataDir = opHeader ->DataDirectory; importDir = (PIMAGE_IMPORT_DESCRIPTOR) ((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, (dataDir + 1)->VirtualAddress )); //3.输出导入表信息 while(importDir->OriginalFirstThunk){ printf("按enter键显示下一个dll导入表信息:\n"); getchar(); LPSTR name = (LPSTR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, importDir->Name )); printf("================%s================\n", name); //解析INT表 PDWORD thunk =(PDWORD) ((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, (DWORD)importDir->OriginalFirstThunk)) ; while(*thunk){ if((*thunk & 0x80000000) >> 31 == 1){ //如果第一位为1,表示为序号导入 DWORD ord = *thunk & 0x7fffffff; printf("按序号导入:%d\n", ord); }else{ PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *thunk)); printf("按函数名导入:%s\n",pName->Name); } //下一个INT表项 thunk++; } //下一张导入表 importDir++; printf("\n"); } } int main(int argc, char* argv[]) { //输出导入表信息 printImport(); getchar(); }
结果: