一、導入表的結構
導入表的結構看起來複雜,其實只是套娃,不要被它嚇到了。
導入表的定義如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
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;
其中比較重要的是 OriginalFirstThunk, Name 和 FirstThunk。
OriginalFirstThunk 是指向INT表的RVA,FirstThunk 是指向IAT表的RVA,Name是DLL的文件名。
INT表和IAT表在文件中是完全一樣的(前提是沒有綁定導入)。
INT表的結構是這樣的:
IMAGE_THUNK_DATA 是一個4字節的數據,如果最高位是1,那麼低31位就是函數的導出序號;如果最高位是0,那麼它的值是一個RVA,指向一個 IMAGE_IMPORT_BY_NAME 結構。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
其中低地址的Hint是導出序號,然而這個值可能並不準確,有些編譯器會把它設置成0,我們只需要關注 Name,這個是一個以0結尾的字符串,表示函數名。
如果沒有提前綁定函數地址,IAT表和INT表在文件中存儲的數據是完全一樣的。
二、導入表和IAT表的作用
EXE和DLL要使用其他DLL的函數,需要說明用到了哪些函數,導入表就是存儲這些外部函數的函數名或導出序號的結構。使用了多少個DLL,就有多少個導入表。
IAT表可以認爲是導入表的“子表”,因爲導入表裏的 FirstThunk 屬性就是IAT表的RVA。EXE如果調用了DLL的函數,CALL 指令後面跟的地址其實是指向IAT表裏的某個位置,運行之前在文件中的時候,IAT表和INT表一樣,裏面存儲的要麼是函數名,要麼是導出序號。加載的時候操作系統會把IAT表裏的值修改成函數真正在DLL中的地址,具體步驟是:
操作系統首先將exe和所有dll加載到4GB虛擬內存中,然後遍歷導入表,根據DLL名字調用LoadLibrary獲取模塊句柄HMODULE,然後調用GetProcAddress獲取函數地址,然後將函數地址寫入到IAT表裏。
三、解析導入表
編程打印導入表裏的數據,包括DLL文件名,INT表和IAT表的數據。
// 打印導入表
VOID PrintImportTable(LPVOID pFileBuffer)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)(pDosHeader->e_lfanew + (DWORD)pDosHeader + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = \
(PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + \
RvaToFoa(pFileBuffer, pOptionHeader->DataDirectory[1].VirtualAddress));
// 嚴格來說應該是 sizeof(IMAGE_IMPORT_DESCRIPTOR) 個字節爲0表示結束
while (pImportTable->OriginalFirstThunk || pImportTable->FirstThunk)
{
// 打印模塊名
printf("%s\n", (LPCSTR)(RvaToFoa(pFileBuffer, pImportTable->Name) + (DWORD)pFileBuffer));
// 遍歷INT表(import name table)
printf("--------------OriginalFirstThunkRVA:%x--------------\n", pImportTable->OriginalFirstThunk);
PIMAGE_THUNK_DATA32 pThunkData = (PIMAGE_THUNK_DATA32)((DWORD)pFileBuffer + \
RvaToFoa(pFileBuffer, pImportTable->OriginalFirstThunk));
while (*((PDWORD)pThunkData) != 0)
{
// IMAGE_THUNK_DATA32 是一個4字節數據
// 如果最高位是1,那麼除去最高位就是導出序號
// 如果最高位是0,那麼這個值是RVA 指向 IMAGE_IMPORT_BY_NAME
if ((*((PDWORD)pThunkData) & 0x80000000) == 0x80000000)
{
printf("按序號導入 Ordinal:%04x\n", (*((PDWORD)pThunkData) & 0x7FFFFFFF));
}
else
{
PIMAGE_IMPORT_BY_NAME pIBN = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pFileBuffer, *((PDWORD)pThunkData)) + \
(DWORD)pFileBuffer);
printf("按名字導入 Hint:%04x Name:%s\n", pIBN->Hint, pIBN->Name);
}
pThunkData++;
}
// 遍歷IAT表(import address table)
printf("--------------FirstThunkRVA:%x--------------\n", pImportTable->FirstThunk);
pThunkData = (PIMAGE_THUNK_DATA32)((DWORD)pFileBuffer + \
RvaToFoa(pFileBuffer, pImportTable->FirstThunk));
while (*((PDWORD)pThunkData) != 0)
{
// IMAGE_THUNK_DATA32 是一個4字節數據
// 如果最高位是1,那麼除去最高位就是導出序號
// 如果最高位是0,那麼這個值是RVA 指向 IMAGE_IMPORT_BY_NAME
if ((*((PDWORD)pThunkData) & 0x80000000) == 0x80000000)
{
printf("按序號導入 Ordinal:%04x\n", (*((PDWORD)pThunkData) & 0x7FFFFFFF));
}
else
{
PIMAGE_IMPORT_BY_NAME pIBN = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pFileBuffer, *((PDWORD)pThunkData)) + \
(DWORD)pFileBuffer);
printf("按名字導入 Hint:%04x Name:%s\n", pIBN->Hint, pIBN->Name);
}
pThunkData++;
}
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pImportTable + sizeof(IMAGE_IMPORT_DESCRIPTOR));
}
}
運行結果:
打印ipmsg.exe的運行結果
打印notepad.exe的結果
可以發現notepad在文件中IAT表和INT表是不一樣的,它已經提前將DLL中的函數地址寫死進去了。這個是綁定導入。