1. 程式人生 > >匯入表的解析及遍歷

匯入表的解析及遍歷

        一個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;
}