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);
}
}