一、导入表的结构
导入表的结构看起来复杂,其实只是套娃,不要被它吓到了。
导入表的定义如下:
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中的函数地址写死进去了。这个是绑定导入。
本文详细解析了PE文件中的导入表(Import Table)和IAT(Import Address Table)表的结构和作用。导入表用于记录程序对外部DLL函数的依赖,IAT在程序运行时被操作系统填充为实际函数地址。通过示例代码,展示了如何编程读取并打印导入表信息,揭示了绑定导入的现象。

8445

被折叠的 条评论
为什么被折叠?



