PE文件导入表解析
1、前言
上一篇文章中讲到了PE文件中的导出表, 用于定位PE文件中所有的导出函数。而这一篇详细介绍一下数据目录中的导入数据表。
2、导入表
相比于导出表,导入表的设计相对复杂,共分为四种:导入表、绑定导入表、导入地址表和延迟导入表。不同导入表的作用不一,在不同的场合发挥作用。
- 导入表
IMAGE_DIRECTORY_ENTRY_IMPORT
通常所知道的导入表,在PE文件加载时,会根据这个表里的内容加载依赖库,并填充所需函数的地址。而导入表的基本结构如下所示:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 映象绑定前值 = 0
// 旧式绑定后值 = 导入模块的时间戳
// 新式绑定值 = -1(时间戳存储在绑定导入表中)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // 导入库名称的RVA
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
其中OriginalFirstThunk
和FirstThunk
都指向一个IMAGE_THUNK_DATA
的数组。在PE文件中,两个字段指向的数组中存储的数据完全一致,这种情况下,IMAGE_THUNK_DATA32
结构中实际生效的是AddressOfData
。而在加载到内存中后,FirstThunk
中的Function开始生效,他指向实际的函数地址,即FirstThunk
将指向IAT
表中的一个位置。
由于函数导入方式有按函数名导入和序号导入两种,而IMAGE_THUNK_DATA
使用一个联合体用来存储数据,因此在这里使用了一个巧妙的设计,即使用Ordinal的最高位表示是否是序号导入。如果最高位为1,就是按序号导入的,这时候,低16位就是导入序号,如果最高位是0,则AddressOfData
是指向IMAGE_IMPORT_BY_NAME
结构的RVA
,用来保存名字信息。
PIMAGE_IMPORT_DESCRIPTOR import_descriptor =
(PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)mod_base + RVA2FOA(file_header, import_rva));
// 遍历导入表
while (import_descriptor->Characteristics) {
// 获取模块名
const char* dll_name = (const char*)mod_base + RVA2FOA(file_header, import_descriptor->Name);
// 获取导入函数
IMAGE_THUNK_DATA* import_thunk =
(IMAGE_THUNK_DATA*)((DWORD_PTR)mod_base + RVA2FOA(file_header, import_descriptor->OriginalFirstThunk));
// 遍历导入函数
ExportFunctionTb function_tb;
while (import_thunk->u1.AddressOfData != 0) {
// 判断是否为序号导入
if (IMAGE_SNAP_BY_ORDINAL(import_thunk->u1.Ordinal)) {
// 对于序号导入,低16位存储了序号值
WORD ordinal = static_cast<WORD>(import_thunk->u1.Ordinal & 0xFFFF);
function_tb.push_back({ordinal, NULL, "N/A"});
} else {
// 对于名称导入,低31位存储了名称表RVA
IMAGE_IMPORT_BY_NAME* import_byname =
(IMAGE_IMPORT_BY_NAME*)((DWORD_PTR)mod_base + RVA2FOA(file_header, import_thunk->u1.AddressOfData));
function_tb.push_back({import_byname->Hint, NULL, import_byname->Name});
}
// 移动到下一个导入函数
import_thunk++;
}
// 是否绑定导入
if (import_descriptor->TimeDateStamp) {
IMAGE_THUNK_DATA* import_thunk =
(IMAGE_THUNK_DATA*)((DWORD_PTR)mod_base + RVA2FOA(file_header, import_descriptor->FirstThunk));
// 遍历导入函数
int index = 0;
while (import_thunk->u1.AddressOfData != 0) {
function_tb[index++].entry_point = import_thunk->u1.Function;
// 移动到下一个导入函数
import_thunk++;
}
}
table.push_back({dll_name, function_tb});
// 移动到下一个导入描述符
import_descriptor++;
}
- 绑定导入表
由于一般导入表中导入地址的修正是在PE加载时完成,当一个PE文件导入库或者函数过多时会影响程序的加载速度,因此出现了绑定导入的方式,文件链接时提前将函数地址写入IAT
表,加快了程序的加载速度。但是不同操作系统之间函数地址可能存在差异,因此需额外的结构来确保地址的正确性。
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp; // PE文件生成时间戳
WORD OffsetModuleName; // DLL名偏移
WORD NumberOfModuleForwarderRefs; // 重定向模块数
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD Reserved;
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
以上两个模块的结构基本一致,TimeDateStamp
存储PE文件时间戳,在加载后与对于PE文件中的时间戳对比,判断是否需要重新计算 IAT
中的值,弃用原本记录的绑定信息。OffsetModuleName
用于存储模块名偏移地址,值得注意的是这里的偏移是相对于绑定导入表的,而非模块基址。NumberOfModuleForwarderRefs
仅用于一级绑定导入,当绑定的PE文件需要导入其他的PE文件的函数,这个值就是指依赖的其他PE文件的数量。
// 获取导入表描述符
PIMAGE_BOUND_IMPORT_DESCRIPTOR bound_descriptor =
(PIMAGE_BOUND_IMPORT_DESCRIPTOR)((DWORD_PTR)mod_base + RVA2FOA(file_header, import_rva));
// 遍历导入表
for (int i = 0; bound_descriptor[i].TimeDateStamp; i++) {
const char* dll_name = (const char*)bound_descriptor + bound_descriptor[i].OffsetModuleName;
table.push_back({dll_name, bound_descriptor[i].TimeDateStamp, 0});
PIMAGE_BOUND_FORWARDER_REF bound_ref_descriptor = (PIMAGE_BOUND_FORWARDER_REF)(bound_descriptor + 1);
for (int j = 0; j < bound_descriptor[i].NumberOfModuleForwarderRefs; j++) {
const char* dll_name = (const char*)bound_descriptor + bound_ref_descriptor[i + j].OffsetModuleName;
table.push_back({dll_name, bound_ref_descriptor[i + j].TimeDateStamp, 1});
}
i += bound_descriptor[i].NumberOfModuleForwarderRefs;
}
- 延迟导入表
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
一个PE文件也许提供了很多功能,也导入了很多其他PE,但是并非每次加载都会用到它提供的所有功能,因此延迟导入就出现了,只有在一个PE文件真正用到需要的PE,这个PE才会被加载,甚至于只有真正使用某个导入函数,这个函数地址才会被修正。
typedef struct _IMAGE_DELAYLOAD_DESCRIPTOR {
union {
DWORD AllAttributes;
struct {
DWORD RvaBased : 1; // Delay load version 2
DWORD ReservedAttributes : 31;
} DUMMYSTRUCTNAME;
} Attributes;
DWORD DllNameRVA; // 导入库名称的RVA
DWORD ModuleHandleRVA; // RVA to the HMODULE caching location (PHMODULE)
DWORD ImportAddressTableRVA; // RVA to the start of the IAT (PIMAGE_THUNK_DATA)
DWORD ImportNameTableRVA; // RVA to the start of the name table (PIMAGE_THUNK_DATA::AddressOfData)
DWORD BoundImportAddressTableRVA; // RVA to an optional bound IAT
DWORD UnloadInformationTableRVA; // RVA to an optional unload info table
DWORD TimeDateStamp; // 0 if not bound,
// Otherwise, date/time of the target DLL
} IMAGE_DELAYLOAD_DESCRIPTOR, *PIMAGE_DELAYLOAD_DESCRIPTOR;
延迟导入表中ImportNameTableRVA
和ImportAddressTableRVA
都指向一个IMAGE_THUNK_DATA
的数组。不过ImportNameTableRVA
指向的数组中存储的是函数名信息,而ImportAddressTableRVA
则存储的是函数地址,与OriginalFirstThunk
和FirstThunk
类似。
// 获取导入表描述符
PIMAGE_DELAYLOAD_DESCRIPTOR delayload_descriptor =
(PIMAGE_DELAYLOAD_DESCRIPTOR)((DWORD_PTR)mod_base + RVA2FOA(file_header, import_rva));
// 遍历导入表
while (delayload_descriptor->Attributes.AllAttributes) {
// 获取模块名
const char* dll_name = (const char*)mod_base + RVA2FOA(file_header, delayload_descriptor->DllNameRVA);
// 获取导入函数
IMAGE_THUNK_DATA* import_thunk =
(IMAGE_THUNK_DATA*)((DWORD_PTR)mod_base + RVA2FOA(file_header, delayload_descriptor->ImportNameTableRVA));
// 遍历导入函数
ExportFunctionTb function_tb;
while (import_thunk->u1.AddressOfData != 0) {
// 判断是否为序号导入
if (IMAGE_SNAP_BY_ORDINAL(import_thunk->u1.Ordinal)) {
// 对于序号导入,低16位存储了序号值
WORD ordinal = static_cast<WORD>(import_thunk->u1.Ordinal & 0xFFFF);
function_tb.push_back({ordinal, NULL, "N/A"});
} else {
// 对于名称导入,低31位存储了名称表RVA
IMAGE_IMPORT_BY_NAME* import_byname =
(IMAGE_IMPORT_BY_NAME*)((DWORD_PTR)mod_base + RVA2FOA(file_header, import_thunk->u1.AddressOfData));
function_tb.push_back({import_byname->Hint, NULL, import_byname->Name});
}
// 移动到下一个导入函数
import_thunk++;
}
table.push_back({dll_name, function_tb});
// 移动到下一个导入描述符
delayload_descriptor++;
}
- 导入地址表
IMAGE_DIRECTORY_ENTRY_IAT
真正的函数地址填充的位置,通常情况下该表在PE文件中为空,但是当PE文件中存在绑定导入时,该表中会存储对应的函数地址。