滴水逆向三期实践16:IAT表和导入表

IAT表

在我们调用 dll 函数的时候,发现代码中的汇编,是通过间接寻址的。也就是不直接 call 函数地址,而是通过一个中间地址再跳转的。

比如调用MessageBox这类系统函数的时候(这也是个 dll 中的函数),那么它的汇编会为 call dword ptr [ (004322d4) ] 通过间接寻址,004322d4里面存的是X就可以跳到X,这个X变量可以任意指定。004322d4是.exe领空的,而间接寻址的X可以不是.exe领空,比如这次运行中X会变为77d5050b,就跳到了.dll 的领空了

前面也讲到,因为 dll 是随着其他程序加载进4GB内存的,就可能占不住自己的 ImageBase,因而它提供的导出函数位置就会随之变化,所以 call 的时候不能把相应 dll 函数的位置写死,得根据dll加载情况而动态的变化。所以采用间接寻址的方式来实现动态变化。(因为你不知道这个函数的领空和实际地址在哪,只能先写自己代码领空的位置在 call 后面,操作系统根据 dll 实际调用情况,把真正函数地址填进类似这个 004322d4 的间接区域)

那么当编译的.exe 是未运行的文件状态时,这个 004322d4 指向的地址(还要根据 RVA 转 FOA,减去 Imagebase 才能在文件中找到这个位置),只是一个 MessageBox.USER32.dll 字符串。但内存中这 004322d4 指向的地址却是一个 77d5050b 绝对地址。那么他是什么时候改变的呢?是当.exe 和用到的所有.dll 都贴进4GB空间(并且都修复了各自的重定位表)的时候,这个值就从字符串被修改为绝对地址。

而这些存放着.dll 和函数信息并将要被转换为绝对地址的类似 004322d4 的中间地址,连起来就是IAT表(import name table,导入名称表)

(看完导入表后后面再有综合讲具体是怎么把字符串为转换地址的)

导入表

导出表是给别人用的函数的清单,相当于饭店提供的菜单

导入表相当于客人点菜选择的菜品单。上面记录了exe要使用的dll和dll中的函数

数据目录第一项伟导出表,第二项表为导入表

定位导入表:

 和之前的好多表一样,VirtualAddress 指向多个一样的导入表结构。

最后 sizeOf ( IMAGE_IMPORT_DESCRIPTOR ) 个 0  代表导入表结束

导入表的union只用第二个 OriginalFirstThunk,时间戳 TimeDateStamp 记录了当前程序编译生成的时间

Name 是指向 dll 名的 RVA,只记录了一个 dll 名,所以导入表会有若干张,顺序无缝存放,一张导入表就是一个要使用的 dll,以全0作为结尾

OriginalFirstThunk(导入表结构第一个成员)指向一张叫 INT 的表(import name table,导入名称表),存的是若干个 IMAGE_THUNK_DATA 结构,最后以0结尾作结束标志。

FirstThunk(导入表结构最后一个成员)也指向一张表,IAT表(import address table,导入地址表),找到这张表有两种方式,一种就是通过导入表这里找到,第二种就是通过数据目录表,倒数第三个项就是指向的 IAT 表,IMAGE_DIRECTORY_ENTRY_IAT,也是以0结束。在文件加载前,这两个表存的内容完全一致,都是存储的IMAGE_THUNK_DATA 结构(IMAGE_THUNK_DATA32)(详见下下图)。注意虽然一致,但 INT 表和 IAT 表是两块不同的空间,分别记录的都是程序中使用到的 dll 函数,不使用的 dll 函数不会记录。

以上描述下图有更具象的图:

 上文提到的 IMAGE_THUNK_DATA 结构其实只是一个四字节的 union,如下图。

在加载前,IMAGE_THUNK_DATA 只存一个 RVA,这个 RVA 指向上图的IMAGE_IMPORT_BY_NAME 结构(具体结构格式在下图也有)

在加载后,IAT 表变为存储函数的地址了,所以才有文章最前面提到的,在调用 MassageBox 文件加载前call间接寻址找的是一个字符串,而文件加载后这个间接寻址变为了函数的真正地址。而 INT 表不变。

IAT 表在文件加载完成后系统会调用 GetProcAddress() 函数,就是做的我们前面写过的根据导出表函数序号或函数名字找到函数地址的功能。系统会循环 INT 表,根据表内的名字或序号调用 GetProcAddr() 得到地址,依次添加到 IAT 表中。

注意 IMAGE_IMPORT_BY_NAME 中的 Hint 不是导出序号,而是当前这个函数在导出表函数地址表中的索引。但基本没用,所以不一定是准确的,可以全部为0。而Name并不是一字节,而是以’\0’结尾的不定长字符串。

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)

下面就是定位导入表的过程:

第一层循环遍历所有导入表,以0结尾

第二层循环先处理OriginalFirstThunk,其中的表项IMAGE_THUNK_DATA32就当做一个DWORD 四字节数据处理即可,为0则表结束

表项IMAGE_THUNK_DATA32 有可能是个序号,也有可能是个偏移,根据图中说明来处理。

以下是遍历导入表并打印的代码,前面有不理解或不够深入的地方直接看代码能直接理解到位

#include "Currency.h"
#include "windows.h"
#include "stdio.h"

VOID h327()		//打印导入表所有内容
{
	char FilePath[] = "PETool.exe";	//CRACKME.EXE        CrackHead.exe     Dll1.dll		R.DLL	LoadDll.dll  PETool.exe  //打印dll用最后一个看
	LPVOID pFileBuffer = NULL;				//会被函数改变的 函数输出之一
	LPVOID* ppFileBuffer = &pFileBuffer;	//传进函数的形参
	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
	PIMAGE_IMPORT_DESCRIPTOR pImportTable = NULL;

	BYTE zero[sizeof(IMAGE_IMPORT_DESCRIPTOR)] = { 0 };	//用于全0判断
	PDWORD pIATItem;
	PDWORD pINTItem;
	PIMAGE_IMPORT_BY_NAME pImportByName;

	if (!ReadPEFile(FilePath, ppFileBuffer))
	{
		printf("文件读取失败\n");
		return;
	}      
	//Dos头
	pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;	// 强转 DOS_HEADER 结构体指针
	//可选PE头	  简化后的处理
	pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
	//导入表
	printf("导入表地址 pImportTable VirtualAddress:%x\n", pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
	pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));
	
	
	
	for (int i = 1; memcmp(zero, pImportTable, sizeof(zero)); i++)
	{
		printf("&&&&&&&&&&&&&&&&&&&&第%d个导入表&&&&&&&&&&&&&&&&&&&&\n",i);
		printf("INT地址:OriginalFirstThunk:%x\n", pImportTable->OriginalFirstThunk);
		printf("时间戳: TimeDateStamp:%x\n", pImportTable->TimeDateStamp);
		printf("DLL名: Name:%s\n", (DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pImportTable->Name));
		printf("IAT地址: FirstThunk:%x\n", pImportTable->FirstThunk);
		printf("可选PE头中的IAT地址:%x\n", pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress);

		printf("*********INT表*********\n");
		pINTItem = (PDWORD)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pImportTable->OriginalFirstThunk));
		for (int j = 1; *pINTItem;j++)
		{
			if (*pINTItem & 0x80000000)	//==1为函数序号
			{
				printf("第%d个函数序号:%x\n", j, *pINTItem & 0x7FFFFFFF);
			}
			else   //为函数名字,此时*pINTItem为指向函数名地址的RVA
			{
				pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, *pINTItem));	//需转为FOA加上偏移得到绝对地址
				printf("第%d个函数名:%x-%s\n", j, pImportByName->Hint, pImportByName->Name);
			}
			pINTItem++;
		}

		printf("*********IAT表*********\n");
		pIATItem = (PDWORD)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pImportTable->OriginalFirstThunk));
		for (int j = 1; *pIATItem; j++)
		{
			
			if (*pIATItem & 0x80000000)	//==1为函数序号
			{
				printf("第%d个函数序号:%x\n", j, *pIATItem & 0x7FFFFFFF);
			}
			else   //为函数名字,此时*pINTItem为指向函数名地址的RVA
			{
				pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, *pIATItem));	//需转为FOA加上偏移得到绝对地址
				printf("第%d个函数名:%x-%s\n", j, pImportByName->Hint, pImportByName->Name);
			}
			pIATItem++;
		}
		pImportTable++;
		printf("\n\n");
	}
}

随便找个.exe 读入打印,最好是有序号形式又有函数名形式的导入表(没有也没关系,为了测试到位)正确性可打开 PE 工具查看同一个.exe,和该程序的打印信息对照正确即可

(但注意,这里如果打印的是其他一些 exe 比如系统自带的 记事本.exe 即notepad.exe,那上面打印的结果就不一定是如上所说的了,因为我们默认了 FirstThunk 也就是 IAT 表的内容不是函数编号就是函数名的RVA,实际上在文件中的 IAT 表 仍然有第三种情况,这就是下章讲的内容了 )

拓展:只有在PE文件内隐式调用(静态使用)时才会在调用者PE文件的导入表内出现该 dll 的名字和相关函数,若为显示调用(动态使用),则这个被调用的 dll 名和相关函数不会在调用者导入表内出现!

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值