PE导入表和IAT表的原理及工作关系

PE导入表和IAT表的原理及工作关系

0.说明

观看滴水逆向视频总结(部分截图来自于滴水课件)

编译器:vc++6.0

编写语言:c

观看滴水逆向视频总结(部分截图来自于滴水课件)

欢迎大家留言交流😃^ __ ^

可以加我qq一起学习:1245885144😄🤣

1.PE导入表和IAT表大致工作关系

(1)为什么会有IAT表

IAT:Import Address Table 导入(函数)地址表

一般程序在调用自身函数的时候,自身函数地址RAV是固定的;但是当程序在调用dll里的函数的时候,由于dll的地址会发生重定位,导致dll里的函数地址每次都会发生变化。

为了每次都能准确的调用dll函数的地址,就特意构建了一张表,用于存储每次程序运行,dll发生重定位之后,dll的函数的地址。

而这样之后,那我自身程序在调用dll函数的时候,就可以用“类似指针”指向这张表格,取其值为函数新的地址。即可准确调用dll的函数。

//类似这样的调用函数。这里的0x401234就是IAT的地址,
CALL DWORD PTR DS:[0x401234]

CALL DWORD PTR DS:[<&KERNEL32.GetVersion>;  kernel32.GetVersion

(2)为什么会有导入表

说到导入表,肯定就是对应导出表。而导出表的作用是自身提供一张清单,表明自己又哪些函数,大多是dll为exe提供函数,存在导出表。那相反,exe为了表明自身需要哪些dll的函数,也会生成一张表,那这张表就是导入表。

有些dll一会需要使用别的dll的函数,那这类dll也会有导入表。

同样,导出表是提供的自身要导出的函数的地址,对应导入表,也是为了提供的要导入的dll的函数的地址,只不过由于这个地址不固定,所以每次都会重新修正。(大致,具体看下面原理)

2.导入表和IAT表的真正关系(原理)

导入表再目录项中的第二项(导出表之后)。对应目录项中的VirtualAddress(RVA)即指向的导入表。

image-20200714021426590

这个结构体有5个4字节数据,占20字节,但只需特别记住这三个RVA即可,下面会分别详细说明。

三个RVA所指向的地址大概是这样的:

image-20200714021657868

注意这是PE文件在加载前的样子!

上面涉及到的IMAGE_THUNK_DATA这个结构数组,其实就是一个4字节数,本来是一个union类型,能表示4个数,但我们只需掌握两种即可,其余两种已经成为历史遗留了。

(1)OriginalFirstThunk

OriginalFirstThunk这个RVA所指向的是INT表(Import Name Table),这个表每个数据占4个字节。顾名思义就是表示要导入的函数的名字表。

但是之前学导出表有了解到,导出函数可以以名字导出,亦可以序号导出。所以为了方便区分,就将这INT表的每个值做了细微调整:

INT:如果这个4字节数的最高位(二进制)为1,那么抹去这个最高位之后,所表示的数就是要导入的函数的序号;如果最高位是0,那这个数就也是一个RVA,指向IMAGE_IMPORT_BY_NAME结构体(包含真正的导入函数的名字字符串,以0结尾)。INT表以4字节0结尾。

IMAGE_IMPORT_BY_NAME:前两个字节是一个序号,不是导入序号,一般无用,后面接着就是导入函数名字的字符串,以0结尾。

image-20200714021657868

(2)Name

这个结构体变量也是一个RVA,直接指向一个字符串,这个字符串就是这个导入表对应的DLL的名字。说到这,大家明白,一个导入表只对应一个DLL。那肯定会有多个导入表。所以对应目录项里的VirtualAddress(RVA)指向的是所有导入表的首地址,每个导入表占20字节,挨着。最后以一个空结构体作为结尾(20字节全0结构体)。

(3)FirstAddress

FirstAddress(RVA)指向的就是IAT表!IAT表也是每个数据占4个字节。最后以4字节0结尾。

注意上图PE文件加载前,IAT表和INT表的完全相同的,所以此时IAT表也可以判断函数导出序号,或指向函数名字结构体。

而在加载后,差别就是IAT表发生变化,系统会先根据结构体变量Name加载对应的dll(拉伸),读取dll的导出表,对应原程序的INT表,匹配dll导出函数的地址,返回其地址,贴在对应的IAT表上,挨个修正地址(也就是GetProcAddress的功能)。

所以上文说到,IAT表会存储dll的函数的地址,方便调用该函数时,直接取IAT表这个地址内的值,作为函数地址,去CALL。

image-20200714025103175

(注意IAT表发生变化)

(4)关键点

实际上,PE文件在加载过程中,只会检索IAT表里存储的数据,而不会管INT表是否有数据。(也就是说,即使INT表置零,只要IAT表数据还在,都能正常识别导入函数,从而正常运行;而如果IAT表置零了,即使INT表还在,都无法正常运行)

3.C语言解析导出表

(1)建议

解析的时候要注意,每个地址都是RVA,RVA转FOA会很多,比较绕。

有两个点很容易错

  1. IMAGE_IMPORT_BY_NAME这个结构体前两个字节是一个数,不用管,但是在读取函数名字的时候,一定要记住这里是个结构体,要跳过前两个字节的数再读取。
  2. 由于RVA互转FOA很多,可能会误把INT表直接当做IMAGE_IMPORT_BY_NAME给读取了。

建议

  1. 使用PIMAGE_IMPORY_DESCRIPTOR定义导入表结构体指针pImportDescriptor,然后直接pImportDescriptor++,即可实现导入表的遍历。
  2. 同理,也可以直接使用四字节指针去指向INT表或IAT表,指针++,即可实现遍历表结构。

(2)C源代码

这是功能子函数,定义的变量名称都挺直白的。顾名思义即可。

//功能:解析、打印重定位表
//参数:指向该程序被读取到内存的指针
//返回值:无
void ReadPE( LPVOID pFileBuffer )
{
	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_NT_HEADERS32 pNtHeader = NULL;
	PIMAGE_FILE_HEADER pFileHeader = NULL;
	PIMAGE_OPTIONAL_HEADER pOptionHeader = NULL;
	
	PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
	PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = NULL;

	PDWORD pThunkINT = NULL;//INT表
	PDWORD pThunkIAT = NULL;//IAT表

	PIMAGE_IMPORT_BY_NAME pImportByName = NULL;//名称结构体

	pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer ;
	pNtHeader = (PIMAGE_NT_HEADERS32)( (DWORD)pDosHeader + pDosHeader->e_lfanew );
	pFileHeader = (PIMAGE_FILE_HEADER)( (DWORD)pNtHeader + 4 );
	pOptionHeader = (PIMAGE_OPTIONAL_HEADER)( (DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER );

	pDataDirectory = (PIMAGE_DATA_DIRECTORY )pOptionHeader->DataDirectory ;

	//导入表寻址
	pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)( (DWORD)pDosHeader + ConvertRvaToFoa( pDataDirectory[1].VirtualAddress , pFileBuffer ) );

	while( pImportDescriptor->Name )
	{
		printf("**************************************************\n");
		printf("Dll Name : %s\n",(PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->Name ,pFileBuffer) )  );
		
		pThunkINT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->OriginalFirstThunk , pFileBuffer) );
		while( *pThunkINT )
		{	
			
			if( *pThunkINT & 0x80000000 )
				printf("Import by series number : %d\n", (DWORD)pThunkINT & 0x7fffffff);
			else
			{	//寻址函数名字结构体
				pImportByName = (PIMAGE_IMPORT_BY_NAME)( (DWORD)pDosHeader + ConvertRvaToFoa( *pThunkINT , pFileBuffer)  );
				printf("Import by name!       Hint: %-10XFunction Name: %s\n",pImportByName->Hint ,pImportByName->Name   );
				
			}
			pThunkINT++;	
	//		getchar();
		}

		putchar('\n');

		pImportDescriptor++;
	}
}

4.移动导出表到新增节区

(1)流程

image-20200714031113446

(2)我遇到的困难

  1. IAT表不能移动,因为在调用dll函数时,call命令指向的地址是IAT表,再取IAT表内的值。除了IAT表,其余都可以移动。
  2. 新增节区的属性必须包含可读可写(0xC0000000),不然系统无法正常解析导入表。PE文件:节表(区块表) 节表属性格式分析
  3. 有的PE在文件状态下,实际大小(查看二进制数据)比PE结构表明的大小(最后一个节区PointerToRawData + SizeOfRawData)要大。造成问题:RVA转FOA出错;新增节区的与原始最后一个节区不连贯。原因:那一部分多出的数据是无效的,可以覆盖0或直接删除(可能是编译时自带的)。解决:(新增节区时)可以在遍历节表头的时候,根据最后一个节表头的PointToRawData+SizeOfRawData修正FileSize。

(3)C源代码

主要功能子函数。

//功能:移动导入表到新增节
//参数:指向文件内存的指针
//返回值:指向新增节区后再移动导出表过后的PE内存的指针
LPVOID MoveImportDescriptorToNewSection( LPVOID pFileBuffer )
{
	DWORD Offset = FileSize;//复制导入表时的偏移等于新增节区前的文件大小
	DWORD SIZEOF_INT = 0;

	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_NT_HEADERS32 pNtHeader = NULL;
	PIMAGE_FILE_HEADER pFileHeader = NULL;
	PIMAGE_OPTIONAL_HEADER pOptionHeader = NULL;
	
	PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
	PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = NULL;

	PDWORD pThunkINT = NULL;//INT表
	PDWORD pThunkIAT = NULL;//IAT表

	PIMAGE_IMPORT_BY_NAME pImportByName = NULL;

	//第一步:	新增一个节区(注意新增节区的大小是不装的下导入表)					
	pFileBuffer = IncreaseNewSection( pFileBuffer , 0x3000 );//第二个参数是新增节的大小

	//初始化PE头
	pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
	pNtHeader = (PIMAGE_NT_HEADERS32)( (DWORD)pDosHeader + pDosHeader->e_lfanew );
	pFileHeader = (PIMAGE_FILE_HEADER)( (DWORD)pNtHeader + 4);
	pOptionHeader = (PIMAGE_OPTIONAL_HEADER)( (DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);

	pDataDirectory = (PIMAGE_DATA_DIRECTORY)pOptionHeader->DataDirectory;


	//第二步:	根据目录项里的RVA和Size复制整块导入表到到新增节区(Size大小内同时包多个dll的导入表)														
	memcpy((PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa(pDataDirectory[1].VirtualAddress , pFileBuffer ) ) , pDataDirectory[1].Size );
	//同时修正目录下里的RVA	
	pDataDirectory[1].VirtualAddress = ConvertFoaToRva(Offset  , pFileBuffer );
	
	//修改偏移
	Offset += pDataDirectory[1].Size ;

	//初始化指向导入表的指针
	pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)( (DWORD)pDosHeader + ConvertRvaToFoa( pDataDirectory[1].VirtualAddress , pFileBuffer ) );
	
	
	//第六步:	遍历其余dll的导入表,重复以上第三步至第五步(写的时候这一步要通过while循环放在第二步之后)				
	while(pImportDescriptor->Name )
	{

		//第三步:	根据第一块导入表的OriginalFirstThunk(RVA),复制整块INT表(先遍历计算大小,再通过memcpy复制)					
		pThunkINT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->OriginalFirstThunk ,pFileBuffer) );
		do
			SIZEOF_INT+=4;
		while( *(pThunkINT++) );//让SIZEOF_INT多加一个4的大小,为了给末尾留4字节0作为结尾
		memcpy( (PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa(pImportDescriptor->OriginalFirstThunk ,pFileBuffer)  ) , SIZEOF_INT);					
		//同时修正导入表中OriginFirstThunk(RVA)				
		pImportDescriptor->OriginalFirstThunk = ConvertFoaToRva( Offset , pFileBuffer );

		//修改偏移
		Offset += SIZEOF_INT;
		
		//第四步:	遍历INT表,判断,复制对应IMAGE_IMPORT_BY_NAME这个结构体到新增节区(注意:所有涉及到的字符串末尾要留一个0)	
		//同时修正INT表中的RVA														
		//同时遍历IAT表,修正IAT表中的RVA														
		//(IAT表不能移动,因为在调用dll函数时,call命令指向的地址是IAT表,再取IAT表内的值)
		pThunkINT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->OriginalFirstThunk , pFileBuffer) );
		pThunkIAT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->FirstThunk , pFileBuffer) );						
		for( ; *pThunkINT ; pThunkINT++ , pThunkIAT++)
			if( !(*pThunkINT & 0x80000000) )
			{
				memcpy( (PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa(*pThunkINT ,pFileBuffer) ) , 2 + strlen( (PBYTE)( (DWORD)pDosHeader + ConvertRvaToFoa(*pThunkINT+2 , pFileBuffer) )  )   );
				//同时修正INT表中的RVA		
				*pThunkINT = ConvertFoaToRva( Offset , pFileBuffer);
				//同时修正IAT表中的RVA
				*pThunkIAT = *pThunkINT;

				//修改偏移
				Offset += 2 + strlen( (PBYTE)( (DWORD)pDosHeader + ConvertRvaToFoa(*pThunkINT+2 , pFileBuffer) )  ) + 1;//多加一个1的大小,给字符串末尾留一个0的结尾符。
			}
		
		//第五步:	根据导入表的Name(RVA),复制dll名字符串到新增节区					
		memcpy( (PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->Name , pFileBuffer) ), strlen( (PBYTE)(DWORD)pDosHeader +ConvertRvaToFoa(pImportDescriptor->Name ,pFileBuffer)  )   );	
		//同时修正导入表中的Name(RVA)					
		pImportDescriptor->Name = ConvertFoaToRva( Offset , pFileBuffer);

		//修改偏移
		Offset += strlen( (PBYTE)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->Name , pFileBuffer) )  ) + 1;//多加一个1的大小,给字符串末尾留一个0的结尾符。


		pImportDescriptor++;//遍历导入表。
	}	

	return pFileBuffer ;
}

欢迎大家留言评论交流。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值