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