导入表的解析及遍历

        一个PE文件中的导入表,简单来说就是代表了该模块调用了哪些外部的API。当模块加载到内存后,PE加载器会修改该表,将导入地址表也就是常说的IAT修改为外部API重定位后的真实地址。下面结合实际PE文件来详细分析下导入表中的每一项。以及通过代码来对一个PE文件的导入表进行遍历,将其调用的函数显示出来。

         首先解析导入表之前,先放三个结构体。

typedef struct _IMAGE_IMPORT_DESCRIPTOR{
    union{
        DWORD Characteristics;
        DWORD OriginalFirstThunk;//导入名称表
    };
    DWORD TimeDateStamp;        //时间戳
    DWORD ForwarderChain;       
    DWORD Name;                 //dll名称
    DWORD FirstThunk;           //导入地址表
}IMAGE_IMPORT_DESCRIPTOR;
//OriginalFirstThunk和FirstThunk都指向的是_IMAGE_THUNK_DATA32结构体 
//导入名称表最高位是0,就是名称导入
//最高位是1,就是序号导入
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE
        DWORD Function;             // PDWORD,导入函数的地址,在加载到内存后,这里才起作用
        DWORD Ordinal;              // 假如是序号导入的,会用到这里
        DWORD AddressOfData;         //PIMAGE_IMPORT_BY_NAME,假如是函数名导入的,用到这                   里 ,它指向另外一个结 构体:PIMGE_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文件来一项一项的看。我这里随便拖了一个crackme的程序,使用CFF Explorer的一个PE查看工具进行查看。

左边可以看到在NT头中的扩展头(Optional Header)的最后一个成员数据目录表,第二个成员即为导入表结构体的信息。文件0x138偏移处存储着导入表的RVA,0x13c偏移处存储着导入表大小0x28

蓝色的框就是数据目录表的16个项,每一项以RVA和size两个子项组成,之后会挨个分析重要的一些项,现在就着重看导入表。

拿到RVA,这里就涉及到一个RVA转换FOA,0x35B4是一个在内存中的相对虚拟地址,(所谓的相对虚拟地址,就是PE文件加载到内存中后,某一个内存地址减去加载的基址,结果为这个内存地址相对于加载基址的偏移),想一下,虽然内存中对齐可能比文件中对齐的粒度大,但是这里内存地址相对于区段的起始位置的偏移是不会改变的,也就是说假设一个地址在文件中相对于起始区段的偏移是0x100,那么加载到内存中,它相对于起始区段偏移仍然是0x100。理解这个就比较好转换了。

所以就查看这个RVA落在下面哪两个节区之间。从下图中看到,红框中的是每个节区在内存中的起始位置,0x35B4正好落在.text和.data区段之间(0x1000~0x4000)。所以0x35B4-0x1000=0x25B4,这个算出来的是RVA在内存中相对于起始区段的偏移,也就是相对于.text段的偏移是0x25B4,那么这个偏移在内存中和在文件中是相同的,所以在文件中的位置,就很容易算出,看.text下面的字段,0x3000是文件中对齐后的大小,0x1000是文件中的起始位置。所以导入表在文件中的位置就可以算出0x1000+0x25B4=0x35B4。

跳转到0x35B4的位置,这时可以看第一个结构体了,_IMAGE_IMPORT_DESCRIPTOR,这个结构体有五个字段,每个字段四字节,所以大小为20B,其中比较有用的有三个,第一个字段,第四个字段,第五个字段。下面分别来看。要注意一点,导入表的结束是以同样结构体大小的全0来表示结束。看蓝色框的五个全为0的字段,就理解了。

第一个字段是个联合体   0x35DC,但一般用的是OriginalFirstThunk,也就是所谓的INT(导入名称表)。网上有很多关于导入地址表导入名称表的关系图,我就不贴了。可以对照着看。第四个字段 0x36B0,指向DLL的名称,也就是字符串,第五个字段FirstThunk  0x1000  这个就是所谓的IAT(导入地址表)。

这里需要说明的是OriginalFirstThunk和FirstThunk所指向的都是一个_IMAGE_THUNK_DATA32的结构体。同样,这个结构体也是以全0为结束

上面两图分别是导入名称表和导入地址表。先看导入名称表。

因为是指向一个_IMAGE_THUNK_DATA32结构体,可以看到结构体中成员是一个联合体,联合体中每个字段是一个DWORD,所以看导入名称表的第一个 0x36BE,这里面的所有地址都是RVA,之前也说过RVA转化FOA,所以跳到文件中的位置

可以看到之所以叫导入名称表,是它所指向的是每一个导入函数的名称,第四个字段,是指向DLL名称,也就是这些导入函数所在哪个DLL模块,0x36B0位置看到DLL的名称为MSVBVM60.DLL。之前说过,_IMAGE_IMPORT_DESCRIPTOR这个字段是以相同大小的结构体全0字段结束,说白了,就是每一个_IMAGE_IMPORT_DESCRIPTOR结构体,就代表是一个导入的DLL,如果全0结束,那说明导入的DLL完毕。下面这些每一个导入的函数,都和导入的DLL相对应,也就是说,一个DLL,导入的函数是属于这个DLL的,下一个DLL,导入的函数是属于下一个DLL。这里导入的DLL就一个。

然后再看 0x000036BE,它的最高位是0,所以它是以函数名导入的,假如它的最高位是1,那么它就是以序号导入的,序号导入,就直接使用第三个字段Ordinal;代表导入的序号,而名称导入就指向第三个结构体 _IMAGE_IMPORT_BY_NAME,也就是我们看到的函数名字符串。

最后再看FirstThunk指向的地址0x1000,它所指向的地址,在加载到内存中后,会再做调整,PE加载器做的填充IAT,就是在填充它。可能在内存中开始的时候,它和导入名称表都是指向函数名字符串,但加载到内存后,因为DLL加载的基址不定,所以需要使用GetProcAddress和LoadLibrary来动态获取实际导入函数的地址,导入函数名以及导入的DLL名称都存放在INT中,直接从中取出,然后再逐一获取每一个函数的实际地址,最后填充到IAT中,就完成了IAT的填充。

放到OD里面来看一下。

首先PE文件加载基址0x400000,扩展头的ImageBase得到

OD中查找0x400000的位置,看到文件已经被加载到内存中去。

往下翻找到导入表的偏移,0x35B4.

加上基址0x400000,就是0x4035B4

这时可以看到_IMAGE_IMPORT_DESCRIPTOR结构体的五个字段,和在PE工具中解析的数据基本一致,就看最后一个字段导入地址表,加载到内存中是什么东西。同样0x400000+0x1000=0x401000

返回头可以看下,在文件中的导入地址表那些数据都是填充使用,到内存中它会真正填充成函数实际地址。至此导入表的解析就先到这里。然后通过代码来遍历一个文件的导入表。

#include <iostream>
#include<windows.h>
#include<stdlib.h>

DWORD dwFileSize;
BYTE* g_pFileImageBase = 0;
PIMAGE_NT_HEADERS g_pNt = 0;  //NT头

DWORD RVAtoFOA(DWORD dwRVA)
{
	//区块数目,在文件头中
	int nCountOfSection = g_pNt->FileHeader.NumberOfSections;
	//第一个区段
	PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(g_pNt);
	//内存中对齐大小
	DWORD dwSecAligment = g_pNt->OptionalHeader.SectionAlignment;
	for (int i = 0; i < nCountOfSection; i++)
	{
		//因为在区段这个结构体中,内存中的大小是没有对齐的,
		//所以需要计算出它在内存中对齐后的大小
		//VirtualSize记录的是在区段真实大小,并没有对齐,这里用VirtualSize%对齐粒度
		//模等于0说明对齐了,直接取该值就可以,否则的话,VirtualSize/对齐粒度,计算出
		//已经对齐的部分的大小,然后再加一个粒度的大小,完成对齐
		DWORD dwVirFlieSize = pSec->Misc.VirtualSize%dwSecAligment ?
			pSec->Misc.VirtualSize / dwSecAligment * dwSecAligment + dwSecAligment :
			pSec->Misc.VirtualSize;
		//VirtualAddress记录的是区段起始内存位置,该位置加上区段在内存中的对齐大小
		//就是下一区段的起始位置。这里就是判断当前这个RVA是否落在这两个区段的范围内
		if (dwRVA >= pSec->VirtualAddress&&dwRVA <= pSec->VirtualAddress + dwVirFlieSize)
		{
			//如果落在这个范围,RVA-内存起始算出了偏移,加上文件中区段的起始位             置,
			//得到在文件中的位置
			return dwRVA - pSec->VirtualAddress + pSec->PointerToRawData;
		}
		pSec++;
	}
        return 0;
}

void ShowIAT()
{
	OPENFILENAME stOF{};                //打开文件的结构体  
	HANDLE hFile = NULL;                //文件句柄
	WCHAR szFileName[MAX_PATH]{};      //要打开的文件路径及名称名
	
	RtlZeroMemory(&stOF, sizeof(stOF));
	stOF.lStructSize = sizeof(stOF);
	stOF.lpstrFile = szFileName;
	stOF.nMaxFile = MAX_PATH;
	stOF.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
	if (GetOpenFileName(&stOF))
	{
		hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
			OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL);
		if (hFile == INVALID_HANDLE_VALUE)
		{
			printf("打开文件失败!\n");
			return;
		}
		dwFileSize = GetFileSize(hFile, NULL);
		g_pFileImageBase = new BYTE[dwFileSize]{};
		DWORD dwRead = 0;
		bool bRet = ReadFile(hFile, g_pFileImageBase, dwFileSize, &dwRead, NULL);
		//如果读取失败,释放内存,关闭句柄退出
		if (!bRet)
		{
			delete[] g_pFileImageBase;
			CloseHandle(hFile);
			return;
		}
		//读取成功也关闭掉句柄,因为文件已经读到buffer中
		CloseHandle(hFile);
	}
	//ODS头,DOS头+e_lfanew=PE标记位置
	PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)g_pFileImageBase;
	if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
	{
		//如果不是MZ标记
		delete[] g_pFileImageBase;
		return;
	}
	//NT 头
	g_pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + g_pFileImageBase);
	if (g_pNt->Signature != IMAGE_NT_SIGNATURE)
	{
		//不是PE标记
		delete[] g_pFileImageBase;
		return;
	}

	PIMAGE_OPTIONAL_HEADER32 option = &(g_pNt->OptionalHeader);
	//导入表的RVA
	DWORD dwImportRVA = option->DataDirectory[1].VirtualAddress;
	if (dwImportRVA == 0)
	{
		printf("没有导入表\n");
		delete[] g_pFileImageBase;
		return;
	}
	//导入表在文件中的位置
	DWORD dwImportInFile = (DWORD)(RVAtoFOA(dwImportRVA) + g_pFileImageBase);
	PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)dwImportInFile;

	while (pImport->Name)
	{
		//导入地址表
		PIMAGE_THUNK_DATA pIAT =
			(PIMAGE_THUNK_DATA)(RVAtoFOA(pImport->FirstThunk) + g_pFileImageBase);
		//导入名称表
		PIMAGE_THUNK_DATA pINT =
			(PIMAGE_THUNK_DATA)(RVAtoFOA(pImport->OriginalFirstThunk) + g_pFileImageBase);


		//DLL名
		char *pName = (char*)(RVAtoFOA(pImport->Name) + g_pFileImageBase);
		printf("导入模块名:%s\n", pName);
		while (pINT->u1.AddressOfData)
		{
			if (IMAGE_SNAP_BY_ORDINAL32(pINT->u1.AddressOfData)) 
			{
				//序号导入
				printf("  序号为:%-30x  地址:%X\n", pINT->u1.Ordinal & 0xFFFF, pIAT->u1.Function);
			}
			else
			{
				PIMAGE_IMPORT_BY_NAME pImport =
					(PIMAGE_IMPORT_BY_NAME)(RVAtoFOA(pINT->u1.AddressOfData)+g_pFileImageBase);
				printf("  名称为:%-30s  地址:%X\n", pImport->Name, pIAT->u1.Function);
			}
			pIAT++;
			pINT++;
		}
		pImport++;
	}
	delete[] g_pFileImageBase;
	return;
}
int main()
{
	ShowIAT();
	system("pause");
	return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_42489582/article/details/84800464