PE文件:导入表

0x00 导入表的作用

一个PE文件中的导入表,简单来说就是代表了该模块调用了哪些外部的API。
导入表是逆向和病毒分析中比较重要的一个表,在分析病毒时几乎第一时间都要看一下程序的导入表的内容,判断程序大概用了哪些功能。

0x01 导入表结构

struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics; 
        DWORD   OriginalFirstThunk; //指向INT        
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp; 
    DWORD   ForwarderChain;         
    DWORD   Name;				   //dll名称
    DWORD   FirstThunk;            //指向IAT  
} IMAGE_IMPORT_DESCRIPTOR;

其中OriginalFirstThunk和FirstThunk指向相同的结构体_IMAGE_THUNK_DATA。这俩个结构体又指向同一个结构体IMAGE_THUNK_DATA32,也就是双桥结构。

struct _IMAGE_THUNK_DATA32{
    union {
        DWORD ForwarderString 
        DWORD Function ; //被输入的函数的内存地址
        DWORD Ordinal ; //被输入的API的序数值
       DWORD AddressOfData ; //高位为0则指向IMAGE_IMPORT_BY_NAME 结构体二
    }u1;
}IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME{
    WORD Hint;      //序号
    BYTE NAME[1];   //函数名
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

双桥结构
在PE文件加载前,INT和IAT都指向IMAGE_IMPORT_BY_NAME。
在磁盘中时,INT和IAT都一样,
在内存中时,INT,联合体中的AddressOfData起作用,因此指向的是IMAGE_IMPORT_BY_NAME数组的RVA。IAT存放着函数真实地址。
在这里插入图片描述
但是在PE加载的时候,双桥结构会断裂,IAT 会被PE加载器重写,PE加载器先搜索INT,PE加载器迭代搜索INT数组中的每个指针,找出 INT所指向的IMAGE_IMPORT_BY_NAME结构中的函数在内存中的真正的地址,并把它替代原来IAT中的值。此时IAT中存放的就是函数的真实地址。
在这里插入图片描述

0x02 总结

1.“双桥结构”是导入表非常重要的结构,修正导入表时经常用到这个概念。
2.PE中导入表,也就是IMAGE_IMPORT_DESCRIPTOR结构在一个数组中,意味着一个PE文件中可能有多个导入表,每个导入表中只有一个OriginalFirstThunk和FirstThunk,但是他们指向的IMAGE_THUNK_DATA是一个数组,数组的元素个数代表函数的个数,如果是IMAGE_THUNK_DATA中的AddressOfData字段生效,它指向的是一个IMAGE_IMPORT_BY_NAME数组,这个数组中的元素个数跟IMAGE_THUNK_DATA中的可能不一样,因为有的函数没有名字。

3.凡是数组的最后一定是以0填充,长度是数组元素的大小,字符串以00作为结束。
在这里插入图片描述

0x03 获得导入表代码实现

//获得导入函数表
VOID GetImportDescriptor()
{
	HANDLE FileHandle						= NULL;
	HANDLE FileMappingHandle			= NULL;
	PVOID    BaseAddress					= NULL;
	PIMAGE_DOS_HEADER					DosHeader  = NULL;
	PIMAGE_NT_HEADERS					NtHeader   = NULL;
	PIMAGE_FILE_HEADER					FileHeader  = NULL;
	PIMAGE_OPTIONAL_HEADER        OptionalHeader   = NULL;
	PIMAGE_DATA_DIRECTORY          DataDirectory      = NULL;
	PIMAGE_IMPORT_DESCRIPTOR    ImportDescriptor = NULL;
	DWORD  ImportDirectoryRva = 0;

	FileHandle = CreateFile(FileFullPath,
										GENERIC_ALL,
										FILE_SHARE_WRITE, 
										NULL, 
										OPEN_EXISTING,
										NULL,
										NULL);
	if (FileHandle == INVALID_HANDLE_VALUE)
	{
		_tprintf(_T("CreateFile() Failed\r\n"));
		goto Exit;
	}

	FileMappingHandle = CreateFileMapping(FileHandle, 
																	NULL, 
																	PAGE_READWRITE, 
																	0,
																	0,
																	NULL);
	if (FileMappingHandle == NULL)
	{
		_tprintf(_T("CreateFileMapping() Failed\r\n"));;
		goto Exit;
	}

	BaseAddress = MapViewOfFile(FileMappingHandle,	
													FILE_MAP_ALL_ACCESS, 
													0,
													0,
													0);
	if (BaseAddress == NULL)
	{
		_tprintf(_T("MapViewOfFile() Failed\r\n"));
		goto Exit;
	}

	DosHeader = (PIMAGE_DOS_HEADER)BaseAddress;
	if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		// 如果不是则提示用户,并立即结束
		MessageBox(NULL, TEXT("这不是一个有效PE文件"), TEXT("提示"), MB_OK);
		goto Exit;
	}

	NtHeader = (PIMAGE_NT_HEADERS)((TCHAR*)DosHeader + DosHeader->e_lfanew);
	if (NtHeader->Signature != IMAGE_NT_SIGNATURE)
	{
		// 如果不是则提示用户,并立即结束
		MessageBox(NULL, TEXT("这不是一个有效PE文件"), TEXT("提示"), MB_OK);
		goto Exit;
	}

	FileHeader = &NtHeader->FileHeader;


	//32位和64位的NT头大小不一样
	//所以获取DataDirectory需要加上不同的偏移量
	if (FileHeader->Machine == IMAGE_FILE_MACHINE_I386)
	{
		DataDirectory = (PIMAGE_DATA_DIRECTORY)((TCHAR*)NtHeader + 120);
	}
	if (FileHeader->Machine == IMAGE_FILE_MACHINE_IA64 ||
		FileHeader->Machine == IMAGE_FILE_MACHINE_AMD64)
	{
		DataDirectory = (PIMAGE_DATA_DIRECTORY)((TCHAR*)NtHeader + 136);
	}

	//获得Rva
	ImportDirectoryRva = DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;

	//获得文件地址
	ImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)ImageRvaToVa(NtHeader, BaseAddress, ImportDirectoryRva, NULL);

	IMAGE_IMPORT_DESCRIPTOR Null_IID;	
	memset(&Null_IID, 0, sizeof(Null_IID));


	//每个元素代表了一个引入的DLL。
	for (int i = 0; memcmp(ImportDescriptor + i, &Null_IID, sizeof(Null_IID)) != 0; i++)
	{
		LPCSTR DllName = (LPCSTR)ImageRvaToVa(NtHeader, 
																			BaseAddress,
																			ImportDescriptor[i].Name, //DLL名称的RVA
																			NULL);

		//拿到了DLL的名字
		_tprintf(_T("-----------------------------------------\r\n"));
		_tprintf(_T("[%d]: %s\r\n"), i, DllName);
		_tprintf(_T("-----------------------------------------\r\n"));

		//现在去看看从该DLL中引入了哪些函数
		//我们来到该DLL的 IMAGE_THUNK_DATA 数组(IAT:导入地址表)前面
		

		if (FileHeader->Machine == IMAGE_FILE_MACHINE_I386)
		{
			IMAGE_THUNK_DATA32 Null_Thunk;
			//memset是计算机中C/C++语言初始化函数。
			//作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。
			memset(&Null_Thunk, 0, sizeof(Null_Thunk));

			PIMAGE_THUNK_DATA32 pThunkData32 = (PIMAGE_THUNK_DATA32)ImageRvaToVa(
																	NtHeader, 
																	BaseAddress,
																	ImportDescriptor[i].OriginalFirstThunk, //【注意】这里使用的是OriginalFirstThunk
																	NULL);
			for (int j = 0; memcmp(pThunkData32 + j, &Null_Thunk, sizeof(Null_Thunk)) != 0; j++)
			{
				//这里通过RVA的最高位判断函数的导入方式,
				//如果最高位为1,按序号导入,否则按名称导入
				if (pThunkData32[j].u1.AddressOfData & IMAGE_ORDINAL_FLAG32)
				{
					_tprintf(_T("\t [%d] \t %ld \t 按序号导入\n"), j, pThunkData32[j].u1.AddressOfData & 0xffff);
				}
				else
				{
				    //按名称导入,我们再次定向到函数序号和名称
				    //注意其地址不能直接用,因为仍然是RVA!
					PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)ImageRvaToVa(
																						NtHeader, 
																						BaseAddress,
																						pThunkData32[j].u1.AddressOfData,
																						NULL);

					_tprintf(_T("\t [%d] \t %ld \t %s\n"), j, pFuncName->Hint, pFuncName->Name);
				}
			}
		}
		if (FileHeader->Machine == IMAGE_FILE_MACHINE_IA64 ||
			FileHeader->Machine == IMAGE_FILE_MACHINE_AMD64)
		{
			IMAGE_THUNK_DATA64 Null_Thunk;
			memset(&Null_Thunk, 0, sizeof(Null_Thunk));

			PIMAGE_THUNK_DATA64 pThunkData64 = (PIMAGE_THUNK_DATA64)ImageRvaToVa(
																				NtHeader, 
																				BaseAddress,
																				ImportDescriptor[i].OriginalFirstThunk, //【注意】这里使用的是OriginalFirstThunk
																				NULL);
			for (int j = 0; memcmp(pThunkData64 + j, &Null_Thunk, sizeof(Null_Thunk)) != 0; j++)
			{
				//这里通过RVA的最高位判断函数的导入方式,
				//如果最高位为1,按序号导入,否则按名称导入
				if (pThunkData64[j].u1.AddressOfData & IMAGE_ORDINAL_FLAG64)
				{
					_tprintf(_T("\t [%d] \t %ld \t 按序号导入\n"), j, pThunkData64[j].u1.AddressOfData & 0xffff);
				}
				else
				{
					//按名称导入,我们再次定向到函数序号和名称
					//注意其地址不能直接用,因为仍然是RVA!
					PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)ImageRvaToVa(
																						NtHeader, 
																						BaseAddress,
																						pThunkData64[j].u1.AddressOfData,
																						NULL);

					_tprintf(_T("\t [%d] \t %ld \t %s\n"), j, pFuncName->Hint, pFuncName->Name);
				}
			}
		}	
	}

Exit:
	if (FileHandle != NULL)
	{
		CloseHandle(FileHandle);
	}
	if (FileMappingHandle != NULL)
	{
		CloseHandle(FileMappingHandle);
	}
	if (BaseAddress != NULL)
	{
		UnmapViewOfFile(BaseAddress);
	}


}
发布了19 篇原创文章 · 获赞 21 · 访问量 986

猜你喜欢

转载自blog.csdn.net/weixin_43742894/article/details/105155489