上一篇文章探讨了 helloworld.exe 映像的 import table 和 .idata 节的情况,现在接着看一看 windows 是如何使用导入的函数。
我重申一下我的环境:helloworld.exe 是以 Win32 debug 选项编译的,但是我的电脑是 64 位的 Win7 系统,所以 32 位的 helloworld.exe 运行在 64 位的 windows 下。
前面的文章已经提过:helloworld.exe 的 ImageBase 已经不是 0x00400000 ,在我的实验上是 0x011b0000,这个 ImageBase 可能每次都不一样的。
我不想再回头弄个 64 位的 helloworld.exe 再重新探索。:)
1. 在程序中调用 API
从我的 helloworld.cpp 示例程序中提取几条调用 API 语句:
1.1 语句 1
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); |
代码调用了 User32.dll 的 API 函数,那么 LoadString() 将作为 import 函数存在于 helloworld.exe 的 import table
这条语句,被编译为:
011C1490 6A 64 push 64h |
这条 call 指令里从地址 [0x011c83c0] 取出 IP 来执行, 这个地址里面装的就是 LoadStringW() 的真实地址。
1.2 语句 2
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_HELLOWORLD)); |
代码调用了 User32.dll 的 API 函数:LoadIcon(),这条语句被编译为:
011C1647 8B F4 mov esi,esp |
地址 [0x011C83B4] 放在 LoadIcon() 函数的真实地址。
1.3 语句 3
wcex.hCursor = LoadCursor(NULL, IDC_ARROW); |
代码调用了 User32.dll 的 API 函数:LoadCursor(),这条语句被编译为:
011C1661 68 00 7F 00 00 push 7F00h |
地址 [0x011C83B8] 放在 LoadCursor() 函数的真实地址。
2. helloworld.exe 的 import table
现在来看一看 helloworld.exe 的 import table
OriginalFirstThunk hint/name RVA |
上面是前面提到过的一组由 OriginalFirstThunk 指向的 import lookup table,所不同的是:我已经将基于 0x00400000 的地址值转化为基于 0x011b0000 的地址值。
注意上面红色粗体标注的是 LoadString() 函数的 Hint/Name table 的 RVA 值。
下面再来看一看一组由 FirstThunk 引出的 Import Address Table:
FirstThunk Hint/Name table RVA |
这也是前面提到过的由 FirstThunk 得出的 Import Address Table,同样地址值已经转化为基于 0x011b0000 的地址值
上面的 Import Lookup Table 和 Import Address Table 红色标注的是一一对应的,下面是其一个函数的情况:
- 由 OriginalFirstThunk 值 0x011C81E0 指向的 Hint/Name Table 的 RVA:0x00018472
- 由 FirstThunk 值 0x011C83C0 指向的 Hint/Name Table 的 RVA:0x00018472
由 OriginalFirstThunk 得到 Hint/Name table 和 FirstThunk 得到 Hint/Name table 存在着差异,虽然在函数绑定之前是相同的。 由 OriginalFirstThunk 得到 Hint/Name 这个途径是不变的。
但是由 FirstThunk 得到 Hint/Name table 这个途径在函数绑定之后,发生了改变。FirstThunk 得到的是函数的真实地址,而不是 Hint/Name table
3. 函数的绑定
回到指令1来:
011C149D FF 15 C0 83 1C 01 call dword ptr [__imp__LoadStringW@16 (11C83C0h)] |
这个地址 0x011C83C0 就是上面所例了 FirstThunk 值,此时它被绑定为 LoadStringW() 函数的真实地址:
76E67C12 8B FF mov edi,edi |
上面这段是 LoadString() 函数的代码,入口地址是:0x76E67C12
指令2:
011C164F FF 15 B4 83 1C 01 call dword ptr [__imp__LoadIconW@8 (11C83B4h)] |
函数 LoadIconW() 的真实地址是:
76E69EAF 8B FF mov edi,edi |
指令3:
011C1668 FF 15 B8 83 1C 01 call dword ptr [__imp__LoadCursorW@8 (11C83B8h)] |
函数 LoadCursor() 的真实地址是:
76E689EE 8B FF mov edi,edi |
下面看看 FirstThunk 经过绑定后:
Import Address Table 绑定前 绑定后 |
上面所示,由 FirstThunk 指向的 Import Address Table 已经是导入函数的真实地址。
4. 绑定过程
经过绑定后的 Import Address Table 已经是被改变, 但是 OriginalFirstThunk 指向的 Import Lookup Table 是保持不变的。
函数的绑定过程很容易推出:先以 Hint/Name table 中的 Hint 在 DLL export table 进行索引,找到匹配的函数后,然后从 FirstThunk 找到 Import Address Table,用找到的匹配函数入口地址来改写 Import Address table 相应的表项。
这个绑定过程只是我简单的推论,未经过深入探索,并不能作准。:)