匯入表的解析及遍歷
一個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;
}