PE结构(10)绑定导入表

Last updated on May 10, 2024 pm

IAT中的奇怪地址

在之前的文章:PE结构(9)导入表 中,我们已对IAT结构做出了基本的解析。

目前可以达成的认识是:

在PE文件加载之前,IAT与INT指向同一块函数名称空间,当PE文件加载之后,OS会将所需导入函数的绝对地址贴到IAT的地址中。

那么有没有一种情况,即在PE文件加载之前,IAT中已经存储了导入函数的地址呢?

事实上这事儿完全有可能的,例如古早时期的Windows XP版本中,系统自带的画图、记事本、计算器等EXE执行文件,都是通过这种方式直接在IAT中存储了导入函数地址。

IAT中函数地址的加载

当PE文件被装载到内存,开始计算IAT中所需的函数地址时,一个显而易见的问题就在于:依赖的DLL文件,并不能保证自身一定位于某个固定的ImageBase。

即使在OS层面没有开启入口地址随机化,也存在某个DLL计划好的ImageBase被其他DLL Module抢占,导致程序基地址发生偏差。此时OS将根据DLL内的Reloc表对DLL内的地址进行重定位修复,最终是将修复后的函数地址填入EXE文件的IAT中。

那么如果EXE里的IAT是被提前绑定的呢?当然需要重新计算了。

同理,如果DLL文件本身被修改了,导致函数地址变更,此时也需要重新计算IAT中存储的地址。

绑定导入表

如何进行这种计算呢?就需要绑定导入表的参与了。

TimeStamp比对

我们在之前的文章中探讨过导入表的结构:

1
2
3
4
5
6
7
8
9
10
typedef struct _DATA_IMPORT_DIRECTORY {
union {
DWORD Characteristics; //导入表结束标志
DWORD OriginalFirstThunk; //RVA指向一个结构体数组(INT表)
};
DWORD TimeDateStamp; //时间戳,该值为0时,说明IAT表未绑定,为-1时IAT已被绑定导入函数地址
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; //RVA指向dll名字,以0结尾
DWORD FirstThunk; //RVA指向一个结构体数组(IAT表)
} DATA_IMPORT_DIRECTORY, * PDATA_IMPORT_DIRECTORY;

在当时我们未对我TimeDateStamp成员进行过多深究。

此时则需要重新审视该成员,当该成员存储0值时,意味着IAT中尚未绑定函数地址,当该成员存储-1时,意味着IAT已绑定函数地址。

对于使用了绑定导入表技术的PE文件来说,在程序加载之前,TimeDateStamp的值就已经是-1了。

此时我们解决了第一个问题:如何判断IAT表中已经绑定了导入函数?
但是还未解决另一个问题,在IAT被提前(加载前)绑定时,如何确认绑定的地址是真实有效的呢?

结构概述

1
2
3
4
5
6
7
8
9
10
11
typedef struct _DATA_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp; //表示绑定的时间戳,如果和PE头中的TimeDateStamp不同则可能被修改过
WORD OffsetModuleName; //dll名称地址
WORD NumberOfModuleForwarderRefs; //依赖dll个数
} DATA_BOUND_IMPORT_DESCRIPTOR, *PDATA_BOUND_IMPORT_DESCRIPTOR;

typedef struct _DATA_BOUND_FORWARDER_REF {
DWORD TimeDateStamp; //时间戳,同样的作用检查更新情况
WORD OffsetModuleName; //dll名称地址
WORD Reserved; //保留
} DATA_BOUND_FORWARDER_REF, *PDATA_BOUND_FORWARDER_REF;

该接口即为绑定导入表结构,当DATA_BOUND_IMPORT_DESCRIPTOR中存储的TimeDateStamp成员,与对应的DLL文件的标准PE头中存储的TimeDateStamp一致的情况下,我们认为导入函数的地址不需要重新计算(即DLL文件并未发生修改)。

很明显,为了确认这件事情,OS会需要遍历全部的绑定导入表结构,来判断是否需要将某个DLL的导出函数地址重新计算后填入EXE的IAT中。

遍历方法

DATA_BOUND_IMPORT_DESCRIPTOR结构中的NumberOfModuleForwarderRefs成员,描述了该dll依赖多少个其他dll。

若该成员为0,则下一个结构体仍然是DATA_BOUND_IMPORT_DESCRIPTOR。

若该成员不为0,则下N个结构体为DATA_BOUND_FORWARDER_REF结构。

DLL名称的奇怪设计

尝试获取DATA_BOUND_FORWARDER_REF结构或DATA_BOUND_IMPORT_DESCRIPTOR结构的名称时,使用第一个DATA_BOUND_IMPORT_DESCRIPTOR结构的地址加上OffsetModuleName(无论当前遍历到第几个),即是DLL名称字符串所在地址(RVA)。

编码实现打印绑定导入表

很不容易找到一个还在用这古老技术的文件。

以下示例使用从winxp中提取的记事本程序演示。

notepad.exe

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void work07() {
//将指定文件加载到内存,并解析PE信息到PEFILE结构体
PEFILE peFile{ 0 };
FILE* pFile = fopen("D:\\notepad.exe", "rb");

int fileLength = LoadFileToMemory(pFile, &peFile);
int r = fclose(pFile);

if (peFile.h_pe == nullptr) {
return;
}

//获取绑定导入表
PDATA_BOUND_IMPORT_DESCRIPTOR pTable = nullptr;
GetBoundImportTable(&pTable, &peFile);

PrintBoundImportTable(pTable, &peFile);
}

打印绑定导入表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void PrintBoundImportTable(PDATA_BOUND_IMPORT_DESCRIPTOR table, lpPEFILE lpPefile) {
if (table == NULL) {
return;
}
DWORD firstTableAddress = (DWORD)table;

while (table->TimeDateStamp != NULL) {
printf("\n");
DWORD address = (DWORD)table;
char* dllName = (char*)(table->OffsetModuleName + firstTableAddress);
DWORD timeStamp = table->TimeDateStamp;
WORD numberOfRefs = table->NumberOfModuleForwarderRefs;
printf("address: 0x%x, dllName: %s, timeStamp: 0x%x, numberOfRefs: %d \n",
address, dllName, timeStamp, numberOfRefs);

if (numberOfRefs > 0) {
printf("Refs:\n");
PrintBoundImportRef(
(PDATA_BOUND_FORWARDER_REF)(table + 1), firstTableAddress, numberOfRefs, lpPefile);
}

table += numberOfRefs > 0 ? numberOfRefs + 1 : 1;
printf("\n");
printf("*********************************************************************\n");
}
}

打印Ref(其实是完全相同的结构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void PrintBoundImportRef(
PDATA_BOUND_FORWARDER_REF table, DWORD firstTableAddress, DWORD size, lpPEFILE lpPefile) {
int index = 0;
while (table->TimeDateStamp != NULL && index < size) {
DWORD address = (DWORD)table;
char* dllName = (char*)(table->OffsetModuleName + firstTableAddress);
DWORD timeStamp = table->TimeDateStamp;
WORD Reserved = table->Reserved;
printf(" address: 0x%x, dllName: %s, timeStamp: 0x%x, Reserved: %d \n",
address, dllName, timeStamp, Reserved);

table++;
index++;
}
}

测试结果


PE结构(10)绑定导入表
http://dubhehub.github.io/blogs/2024051015000029832.html
Author
Sin
Posted on
May 10, 2024
Licensed under