首页
论坛
专栏
课程

[调试逆向] [系统底层] [原创][原创]EXE导入表解析

ilyzqe 2018-11-4 21:46 943
最近又在准备CTF,也是最近三四天才想起来PE还差几个表就解析完了,也算是在论坛上做笔记吧,这次来打印导入表,导入表的作用就不详细说了……IDE:VS2013
系统:win7
首先在数据目录项中找到找到导入表的偏移,导入表是数据目录项的第二位,里边的VirtualAddress是给出了导入表的RVA:
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

自己编写一个从RVA转成FOA的函数:

DWORD RVATOFOA(IN DWORD RVA,OUT DWORD FOA,IN LPVOID pFileBuffer){ 
PIMAGE_DOS_HEADER pDosHeader = NULL;   
PIMAGE_NT_HEADERS pNtHeaders = NULL;    
PIMAGE_FILE_HEADER pFileHeader = NULL;   
PIMAGE_OPTIONAL_HEADER pOptionalHeader = NULL;  
PIMAGE_SECTION_HEADER pSectionHeader = NULL;  
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    
pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);    
pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNtHeaders) + 4);    
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);    size_t count = 0;    //先判断在哪个节里边    
for (count = 1; RVA >(pSectionHeader->VirtualAddress+pSectionHeader->Misc.VirtualSize);count++,pSectionHeader++);    
FOA = RVA - pSectionHeader->VirtualAddress + pSectionHeader->PointerToRawData;    
return FOA;
}

转换完成之后就能在文件中定位导入表的位置,当看到导入表的格式之后觉得有点诡异:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

这个OriginalFirstThunk跟这个FirstThunk这么像没有道理吧,到了后边有更加诡异的事情发生,在OriginalFirstThunk里边存储的是一个RVA指向的是这个结构体:
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

但是!!!FIrstThunk指向的也是这个结构体,刚开始看的时候就觉得这特码有病吧,一个导入表结构体里边有两个指针指向同一个数据结构?等到后来把所有的导入表打印出来之后就有更诡异的事情发生了,先不说,嘿嘿。这个THUNK结构体里边是一个联合体我们就可以把这个结构体看成是一个双字四字节的数据,最高位是标志位如果是1那么代表的是按序号导入,除去最高位之外的31位代表的是导出序号,如果是0代表的是按照名称导入,除去最高位之外的31位代表的是一个偏移指向一个新的结构体:
typedef struct _IMAGE_IMPORT_BY_NAME {   
 WORD    Hint; 
   CHAR   Name[1]
;} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

这个时候自己刚开始想到的判断方式是&0x80000000,但是!!!windows给出了一个函数来直接判断该函数的定义是:
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG32) != 0)

是不是相当简单了,下面给出整个打印的函数跟相当诡异的地方:
VOID PrintImportTable(){

	//定义PE头文件
	PIMAGE_DOS_HEADER pDosHeader=NULL;
	PIMAGE_NT_HEADERS pNtHeader=NULL;
	PIMAGE_FILE_HEADER pFileHeader=NULL;
	PIMAGE_OPTIONAL_HEADER pOptionalHeader=NULL;
	PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
	PIMAGE_SECTION_HEADER pSectionHeader=NULL;
	PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor=NULL;


	//定义文件缓冲区
	LPVOID pFileBuffer = NULL;
	ReadPEFile(INEXEPATH, &pFileBuffer);

	if (pFileBuffer != 0){
		printf("文件打开成功!\n");
	}
	else
	{
		printf("文件打开失败!\n");
		free(pFileBuffer);
		return;
	}
	pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
	pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
	pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNtHeader) + 4);
	pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);
	pDataDirectory = (PIMAGE_DATA_DIRECTORY)(((DWORD)pOptionalHeader) + 96);
	
	//定位重定位表数据目录项第二个就是导入表
	
		pDataDirectory++;
	

	//找到导出表在文件中的地址
	DWORD ImportTableFoa = 0;
	
	ImportTableFoa = RVATOFOA(pDataDirectory->VirtualAddress, ImportTableFoa, pFileBuffer);

	//定位导入表1
	pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + ImportTableFoa);

	//用来做导入表计数
	DWORD ImportCount = 0;
	
	while (pImportDescriptor->Name)
	{
		DWORD NameFoA = 0;
		NameFoA = RVATOFOA(pImportDescriptor->Name, NameFoA, pFileBuffer);
		char* pName = (char*)((DWORD)pFileBuffer + NameFoA);
		printf("---------------------------------------------------------------------\n");
		printf("导入表模块的RVA:%x, FOA:%x,模块名称%s\n", pImportDescriptor->Name,NameFoA,pName);
		DWORD OriginalFirstThunkFOA = 0;
		OriginalFirstThunkFOA = RVATOFOA(pImportDescriptor->OriginalFirstThunk, OriginalFirstThunkFOA, pFileBuffer);
		//定位到OriginalFirstThunk
		PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((DWORD)pFileBuffer + OriginalFirstThunkFOA);
		DWORD ThunkCount = 0;
		//结束标志也是根据里边的数据是否是0来判断
		printf("------------------------OriginalFirstThunk--------------------------\n");
		while (pThunk->u1.AddressOfData){
			//判断导入方式使用内置的定义函数来判断
			if (IMAGE_SNAP_BY_ORDINAL(pThunk->u1.AddressOfData)){
				//如果是1 那么表明是序号导入
				printf("按序号导入\t\t%04X \n", pThunk->u1.Ordinal & 0x7FFFFFFF);
			}
			else
			{
				//如果是0那么就是按照名字导入
				DWORD ThunkNameFOA = 0;
				ThunkNameFOA = RVATOFOA(pThunk->u1.AddressOfData, ThunkNameFOA, pFileBuffer);
				PIMAGE_IMPORT_BY_NAME pThunkName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + ThunkNameFOA);
				printf("按名称导入\t\t[HINT]:%04X  [NAME]:%s \n", pThunkName->Hint, pThunkName->Name);
			}
			pThunk++;
			ThunkCount++;
		}
		printf("按名称导出THUNK数目是:%d\n", ThunkCount);
		printf("------------------------FirstThunk--------------------------\n");
		//通过结构体的RVA定位到在文件中FirstThunk的地址
		DWORD FirstThunkFOA = 0;
		FirstThunkFOA = RVATOFOA(pImportDescriptor->FirstThunk, FirstThunkFOA, pFileBuffer);
		PIMAGE_THUNK_DATA pFirstThunk = (PIMAGE_THUNK_DATA)((DWORD)pFileBuffer + FirstThunkFOA);
		//判断FirstThunk的数目
		size_t FirstThunkCount = 0;
		while (pFirstThunk->u1.AddressOfData){
			//判断导入方式使用内置的定义函数来判断
			if (IMAGE_SNAP_BY_ORDINAL(pFirstThunk->u1.AddressOfData)){
				//如果是1 那么表明是序号导入
				printf("按序号导入\t\t%04X \n", pFirstThunk->u1.Ordinal & 0x7FFFFFFF);
			}
			else
			{
				//如果是0那么就是按照名字导入
				DWORD FirstThunkNameFOA = 0;
				FirstThunkNameFOA = RVATOFOA(pThunk->u1.AddressOfData, FirstThunkNameFOA, pFileBuffer);
				PIMAGE_IMPORT_BY_NAME pFirstThunkName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + FirstThunkNameFOA);
				printf("按名称导入\t\t[HINT]:%04X  [NAME]:%x \n", pFirstThunkName->Hint, pFirstThunkName->Name);
			}
			FirstThunkCount++;
			pFirstThunk++;
		}
		printf("按序号导入Thunks数目:%d\n\n", FirstThunkCount);
		pImportDescriptor++;
		ImportCount++;
	}
	printf("解析结束,导入表个数为%d\n", ImportCount);
	free(pFileBuffer);
}


当我打印完OriginalFirstThunk之后想接着打印FirstThunk的时候就出了问题,刚开始以为OriginalFirstThunk指向的是函数的名字那么FirstThunk指向的就是导出序号,逻辑上感觉毫无问题,但是问题就产生了!这两个指向的是同一个数据结构,难道说同一个数据结构还能有两个不同的值?尤其是这个数据结构是一个联合体union,那么就更不可能发生这种情况了!但是事实又是当打印FirstThunk的时候发现打印出来的数据不正确!于是到网上查资料发现一个有趣的事情:

OriginalFirstThunk与FirstThunk指向的是同一个数据结构,在PE文件中既可以通过OriginalFirstThunk来找到函数名,也可以通过FirstThunk来找到函数名,为什么会出现两个指针指向同一个数据结构的现象呢,其实这个与PE文件的加载有关第一个数组(由 OriginalFirstThunk 所指向)是单独的一项,而且不能被改写,我们前边称为 INT。第二个数组(由 FirstThunk 所指向)事实上是由 PE 装载器重写的。PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,也就是说此时的FirstThunk 不在指向这个INAGE_IMPORT_BY_NAME结构,而是真实的函数的RVA。因此我们称为输入地址表(IAT)。所以,当我们的 PE 文件装载内存后准备执行时,刚刚的图就会转化为下图:

附上一张网上的图 PE文件详解 (六)


到这就已经明白了。IAT这个在脱壳的大牛口中经常出现的术语其实就是导入表的东西,而且之所以分开就是为了避免在文件加载的过程中产生地址被占用问题。
(但是这个地方跟重定位表是两个东西!!!一定不要弄混)
当我打印出这个导入表来的时候已经被Windows的设计者深深的折服了。
小白发文,如有不当欢迎指正。


[防守篇]2018看雪.TSRC CTF 挑战赛(团队赛)11月1日征题开启!

最新回复 (4)
supersoar 2018-11-4 22:11
2

0

感谢分享
ilyzqe 2018-11-4 22:16
3

0

supersoar 感谢分享
客气客气共同学习
kanxue 8 2018-11-5 11:19
4

0

推荐这种学习方法,将学习过程的记录放在论坛分享,利已利人~
sunlintaoo 3天前
5

0

感谢分享,学习了。
返回