导入表
在数据目录的第2项,下标为1的位置,记录了可执行文件导入了哪些动态库和函数,INT(导入函数名称表)和IAT(导入函数地址表),由于一个可执行文件可能要多个PE文件支持,所以此结构可能有多个,导入表就是一个结构体数组,以一个全零元素为结尾,每一个数组的元素,代表一个PE文件的导入信息
导入表格式
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
字段介绍
DWORD OriginalFirstThunk;
指向一个结构体数组的一个相对虚拟地址,结构体数组叫输入名称表(INT:import Name table)
DWORD FirstThunk;
指向一个结构体数组的一个相对虚拟地址,结构体数组叫输入地址表 (IAT:import address table)
注:这两个字段在PE文件未加载到内存中以前,都指向了相同类型的结构体_IMAGE_THUNK_DATA32 ,在文件加载到内存以后输入地址表(IAT)会由加载器把相应的PE文件的函数地址覆盖到这里,这时,输入地址表才是真正的函数地址
DWORD Name;
导入的PE文件的名字的相对虚拟地址(RVA)
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
结构体介绍
结构体为联合体 也就是说只有四个字节
此结构体在PE文件未加载到内存的时候 IMAGE_IMPORT_DESCRIPTOR结构体的OriginalFirstThunk和FirstThunk存的数据是一样的 也就是都指向了这个结构体
字段介绍
Ordinal :加入函数时序号导入的 去掉最高位 也就是函数的序号了
(注:如何判断函数是序号导入的 通过查看Ordinal最高位 是否为1 是1去掉这个最高位的1 剩下的就是函数的序号
为什么这个字段的最高位为1就是以序号导入的?
这是因为 一个进程有4GB的虚拟内存 低两个G由用户使用 高两G为操作系统使用 也就是说一个函数最大的内存地址也就只能是 0x7fff ffff(转换成二进制最高位是0),超过这个内存就是操作系统的范围了,我们是不能使用的 所以最高位不可能为1)
AddressOfData: 最高位不是1 这个字段指向了IMAGE_IMPORT_BY_NAME结构体
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
字段介绍
Hint:函数导出时序号
Name[1]:函数的名字字符数组,0结尾
文件中找导入表信息
导入表地址
一共导入了三个库 ,最后是全0结尾
举例找ExitProcess的地址
这个函数在kernel32.dll中 那我们现在就找这个dll
1.4行为一组 首先看0x0000227c(注:此地址为name字段,RVA 需转换FOA)指向了 导入动态库的名字
这个库为user32.dll
接着找下一项 还是看0x000022ba(name字段 rva需转换)
动态库名称为kernel32.dll 我们要找的函数就在这个动态库里面
接着找OriginalFirstThunk( 地址0x000020f0 rva需转换) 三个IMAGE_THUNK_DATA32 结构
我们先找第一个 0x00002296的位置 首先最高位不是0 说明不是函数序号导入 所以这个rva指向了
IMAGE_IMPORT_BY_NAME结构
此结构为 IMAGE_IMPORT_BY_NAME 前两个字节是函数导出时的序号 后面为函数的名字 以0结尾 这个函数不是我们要找的函数继续
下一个函数名字的地址为0x2288 函数名称就是我们要找的 这个函数在函数名称表(INT)下标为1的地方 相对应的在函数地址表(IAT)下标为1的地方找函数的地址
我们再看导入表 函数地址表(iat)的地址为0x20f0 我们转过去查看
惊奇的发现和函数名称表存储的信息一模一样 正如我们之前所说在文件未加载到内存之前 是无法得到真正的函数地址的,所以两个表是一模一样的
内存中找导入表信息
定位导入表
内存中和文件中有所不同的是IAT表真正填写函数地址了
所有的rva不用转换了 直接加基地址即可
老规矩还是找ExitProcess 还是在kernel32.dll中,和在文件中找的方法是一样的先看name字段为0x0000227c加基地址为0x0040227c 这个库为user32.dll
找下一个动态库 我们看name的字段地址为0x22ba 加基地址为0x004022ba
第二项导入目录表 我们看INT为0x000020f0加基地址0x004020f0
三个指向函数名的rva 我们直接找0x00002288加基地址为0x00402288 下标项为1
我们接着找IAT表0x00002010加基地址0x00402010 跳过来以后我们把内存转为地址查看
这就是我们的IAT函数地址表 我们要找的函数就在函数地址表的下标为1的位置
windows加载导入表的流程
1.首先会检查IAT表是否为空,如果为空则循环遍历到下一个导入表
2.如果不为空则读取导入表的name字段得到dll名称,并调用lodalibaray得到动态库的模块句柄
3.接着找INT表,判断函数是否为序号导出
(1)如果是序号导出直接调用GetProcAddress获取函数地址填写进IAT
(2)如果名称导出获取每一个函数名称,并调用GetProcAddress函数获取函数地址
4.把函数的地址填写进IAT表
导入表的两种情况
情况1:IAT表为空:
IAT表为空,说明这个导入表是无效的,应继续循环找下一个函数导入表
情况2:INT表为空
INT表为空则读取IAT表,把IAT表所指示的位置当函数名称在获取函数地址,并填写到IAT表,覆盖这个函数名称的地址