PE 导入表以及在逆向中的问题

参考文章:https://blog.csdn.net/chenlycly/article/details/53378427

什么是导入表:

百度百科:Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中.

当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址。

如何定位导入表

在扩展PE头是一个名为_IMAGE_OPTIONAL_HEADER的结构体

其中存在一个结构体数组为IMAGE_DATA_DIRECTORY,个数有16个,总占128字节,这里的第2个位置就是IMAGE_DATA_DIRECTORY

IMAGE_DATA_DIRECTORY的结构如下:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress; //虚拟地址,存储当前导出表的地址,这里地址是RVA
    DWORD   Size; //存储 当前导出表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

因为导入表由一堆PE文件组成,所以一般导入表都是有许多个的,导入表中存储的为当前PE需要依赖的函数等

其中关于导入表的结构体的名称为:_IMAGE_IMPORT_DESCRIPTOR,结构体如下:

导入表的具体结构为:占20个字节

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA,指向IMAGE_THUNK_DATA结构数组,也就是INT表
    };
    DWORD   TimeDateStamp;                  // 时间戳

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;                           // RVA,指向dll名字,该名字以0结尾
    DWORD   FirstThunk;                     // RVA,指向IMAGE_THUNK_DATA结构数组,也就是IAT表,这个表也是IMAGE_DIRECTORY_ENTRY_IAT所指向的结构
} IMAGE_IMPORT_DESCRIPTOR;

如何判断导入表的个数?依赖6个模块

每20个字节为一个dll的导入表的相关信息,以20个0为结束符号

第一个导入表中第四个成员,也就是Name字段,为当前导入表的名字的地址,地址为0x014592,如果文件对齐和内存对齐大小不一样那需要先转换为FOA再去查找!

当前导入表的模块为KERNEL32.dll

已经知道了各个导入表的名称,现在再继续了解下导入表中导入了哪些函数吧

IMAGE_THUNK_DATA结构体(逆向中简称IID)

其中有两个成员为相同的结构体数组OriginalFirstThunk和FirstThunk都指向IMAGE_THUNK_DATA类型的结构体数组

下面的图是PE文件加载前的情况图

IMAGE_THUNK_DATA类型结构体如下所示,发现是一个联合体,实际上该结构体的字节为4字节

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;

IMAGE_IMPORT_BY_NAME结构体

当前OriginalFirstThunk指向的地址为0x014310,结束符为4字节的0结束

那么就可以说明当前PE文件就依赖当前这个DLL文件的这么多函数!

例如图中有0x00014566,0x00014554,0x00014546

如果这些地址的最高位是为1的话,那么除去最高位的值就是函数的导出序号

如果这些地址的最高为不是为1的话,那么这个值就是一个RVA,指向IMAGE_IMPORT_BY_NAME结构体,该结构体占4个字节

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint; //可能为空,编译器决定,如果不为空,是函数在导出表的索引
    BYTE    Name[1]; //函数名称,标识符只显示函数的首字母,以0结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

可以跟下0x00014566,然后往后,最后以0结尾

那么到目前位置,就确定了如下两个

1、 如何分析该PE的导入表的DLL

2、该PE的导入表中的DLL的导入函数有哪些

OriginalFirstThunk和FirstThunk的不同点

虽然OriginalFirstThunk和FirstThunk指向的结构体都一样,但是名称不一样OriginalFirstThunk指向的表叫导入名称表(INT),FirstThunk指向的表叫导入地址表(IAT)

那么还有当程序加载的时候,操作系统是如果确定当前PE文件的导入表中导入的函数地址呢?

当程序加载的时候,操作系统会根据PE文件的导入表中每个FirstThunk

可以看到PE文件加载后的情况图如下所示

当PE文件执行后,当前导入表中的使用的函数地址都会添加IAT表中,也就是函数地址表中,这时候IAT和INT就开始有差异了!

这里可能有点抽象,我们可以通过文本编辑工具和调试器来进行互相观察,首先先找到导入表的偏移地址0x1ce80

找到第一个_IMAGE_IMPORT_DESCRIPTOR的结构体的FirstThunk中的偏移地址为0x1B054

如下图所示,这是PE未加载的情况下,此时IAT中该地址中保存的都是偏移地址对应的都是函数的名称

这里跟过去0x1B054,也就是IMAGE_THUNK_DATA结构体,此时IMAGE_THUNK_DATA中的值是一个偏移0x1D45E

跟过去0x1D45E可以看到的就是一个函数地址名称ExitThread

接下来继续看PE加载后的情况,这里我们将其载入到调试器中(此时就已经是PE在内存中拉伸加载后的情况),此时同样的步骤跟过去的0x1B054的时候可以发现IMAGE_THUNK_DATA结构体中已经不再是偏移地址了,而是直接是ExitThread对应的函数地址了,如下图所示

导入表在逆向中的用途

问题1:那为什么PE文件不直接是加载前的就好了,这样的话还可以去掉INT,直接通过IAT来寻找?

答:如果IAT中的表的地址丢失了,就需要修正,就可以通过INT表来找到函数名称,然后通过GetProcAddress来重新获得函数地址,这个用法会在以后的注入手段中用到,比如修复IAT表就需要通过遍历INT表来实现

问题2:程序一开始运行的时候,程序是怎么进行填充IAT表中的地址的呢?

PE文件运行时,Windows系统加载器首先搜索OriginalFirstThunk,如果存在,装载程序迭代搜索数组中的每个指针,GetProcAddress找到每个IMAGE_IMPORT_BY_NAME结构所指向的输入函数的地址,然后用函数入口地址来替代由FirstThunk指向的 IMAGE_THUNK_DATA 数组里的元素值(即用真实的函数地址填充到IAT里)。

问题3:如果是壳脱完之后,所谓的IAT修复的原因是什么?

如果程序加壳了,那壳自己模仿Windows装载器的工作来填充IAT中相关的数据,此时内存中就一张IAT表,输入表的其他部分是不存在的(当然不是绝对的,也有不少壳,如Aspack等,内存中会出现完整的输入表结构)。

这里就举简单的UPX的壳子,当UPX将原程序区段解密完之后,此时它就会开始根据INT表来重建IAT表,此时就有问题了,如果你将UPX脱掉然后来到了OEP,此时你将原程序转储出来,你打开就会发现程序会报C0000005的错误,原因就是当前程序已经无法修复IAT表了,你自己重新打开脱壳后的程序你会发现INT表中的数据都全没了,这也就导致操作系统加载程序的时候无法填充IAT表!

代码实现打印导入表

实现的代码如下:

void PrintfImportTable(PVOID pFileBuffer){
    PIMAGE_DOS_HEADER pDosHeader = NULL;    
    PIMAGE_NT_HEADERS pNTHeader = NULL; 
    PIMAGE_FILE_HEADER pPEHeader = NULL;    
    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;  
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
	PIMAGE_IMPORT_DESCRIPTOR pIMPORT_DESCRIPTOR;
	PIMAGE_IMPORT_BY_NAME pImage_IMPORT_BY_NAME;


	char ImportTableDllName[10] = {0};
	char FunctionName[20] = {0};

	PDWORD OriginalFirstThunk_INT = NULL;
	PDWORD FirstThunk_IAT = NULL;

	DWORD RVA = 0;
	DWORD FOA = 0;
	DWORD Original = 0;
	
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer+pDosHeader->e_lfanew);
    pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);  
    pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader+IMAGE_SIZEOF_FILE_HEADER); 
	pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + IMAGE_SIZEOF_NT_OPTIONAL_HEADER);

	//获取导入表的位置
	RVA_TO_FOA(pFileBuffer,pOptionHeader->DataDirectory[1].VirtualAddress,&FOA);

	//每个导入表的相关信息占20个字节
	pIMPORT_DESCRIPTOR = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + (DWORD)FOA);
	
	//这里可以进行while操作,这里while的判断依据为 pIMPORT_DESCRIPTOR个数

	printf("=========================================");
	
	while(pIMPORT_DESCRIPTOR->FirstThunk && pIMPORT_DESCRIPTOR->OriginalFirstThunk){
		//这里打印的是INT表
		//获取当前导入表DLL的名字
		strcpy(ImportTableDllName,(PVOID)((DWORD)pFileBuffer + (DWORD)pIMPORT_DESCRIPTOR->Name));
		
		printf("当前打印的导出表的DLL为: %s \n", ImportTableDllName);
		printf("\n");

		//printf("TimeDateStamp: %x\n",pIMPORT_DESCRIPTOR->TimeDateStamp);

		printf("INT表打印\n");
		//OriginalFirstThunk转换FOA
		RVA_TO_FOA(pFileBuffer,pIMPORT_DESCRIPTOR->OriginalFirstThunk,&FOA);
		
		OriginalFirstThunk_INT = (PDWORD)((DWORD)pFileBuffer + (DWORD)FOA);
		
		//printf("%x",*OriginalFirstThunk_INT);
		printf("\n");
		while(*OriginalFirstThunk_INT){
			//printf("%x\n ",*OriginalFirstThunk_INT);
			if((*OriginalFirstThunk_INT) & 0X80000000){
				//高位为1 则 除去最高位的值就是函数的导出序号
				Original = *OriginalFirstThunk_INT & 0x7FFFFFFF;	//去除最高标志位。
				printf("按序号导入: %08Xh -- %08dd\n", Original, Original);	//16进制 -- 10 进制
			}else{
				//高位不为1 则指向IMAGE_IMPORT_BY_NAME
				RVA_TO_FOA(pFileBuffer,*OriginalFirstThunk_INT,&FOA);
				pImage_IMPORT_BY_NAME = (PIMAGE_IMPORT_BY_NAME)FOA;
				strcpy(FunctionName,(PVOID)((DWORD)pFileBuffer + (DWORD)&(pImage_IMPORT_BY_NAME->Name)));
				printf("按函数名导入 函数名为: %s \n",FunctionName);
			}
			OriginalFirstThunk_INT++;
		}

		printf("\n");
		

		//继续如上操作进行打印操作
		//这里打印的是iat表

		printf("IAT表打印\n");
		//FirstThunk转换FOA
		RVA_TO_FOA(pFileBuffer,pIMPORT_DESCRIPTOR->FirstThunk,&FOA);

		FirstThunk_IAT = (PDWORD)((DWORD)pFileBuffer + (DWORD)FOA);
		
		//printf("%x",*OriginalFirstThunk_INT);
		printf("\n");
		while(*FirstThunk_IAT){
			//printf("%x\n ",*FirstThunk_IAT);
			if((*FirstThunk_IAT) & 0X80000000){
				//高位为1 则 除去最高位的值就是函数的导出序号
				Original = *FirstThunk_IAT & 0x7FFFFFF;	//去除最高标志位。
				printf("按序号导入: %08Xh -- %08dd\n", Original, Original);	//16进制 -- 10 进制
			}else{
				//高位不为1 则指向IMAGE_IMPORT_BY_NAME
				RVA_TO_FOA(pFileBuffer,*FirstThunk_IAT,&FOA);
				pImage_IMPORT_BY_NAME = (PIMAGE_IMPORT_BY_NAME)FOA;
				strcpy(FunctionName,(PVOID)((DWORD)pFileBuffer + (DWORD)&(pImage_IMPORT_BY_NAME->Name)));
				printf("按函数名导入 函数名为: %s \n",FunctionName);
			}
			FirstThunk_IAT++;
		}
		
		printf("=========================================");
		printf("\n");
		
		pIMPORT_DESCRIPTOR++;		
	}
}

posted @ 2020-02-18 11:59  zpchcbd  阅读(994)  评论(0编辑  收藏  举报