上一章最后讲到,打印导入表时如果打印的某些 exe 比如 xp系统自带的 记事本 即notepad.exe(win10 的 notepad.exe已经没有这种情况了),那上面打印的结果就不一定是如上一章所说的了,因为我们默认了 FirstThunk 也就是 IAT 表的内容不是函数编号就是函数名的 RVA,实际上在文件中的 IAT 表仍然有第三种情况,这就是本章讲的内容了
IAT 表在文件中存储的内容未必是序号和函数名的 RVA,还有可能直接就是上章讲的文件运行加载进内存后呈现的真正的函数地址。而这个本来应该是加载后才做的事,在加载前的文件状态就已经做过了,就不用再等 exe 和所有 dll 挨个贴进内存空间,贴完后再挨个找 dll 函数地址才把 IAT 表改掉了。加快了程序的加载速度,在加载前的文件就已经把这个事做了
其实我们也可以自己做这个过程,完全是可行的。在 exe 文件加载前根据导入表已经知道他用哪些 dll,挨个找到这些 dll,查询他们的导出表依次得到每个函数的位置,当然还要 RVA 转 FOA,加上 dll 的 Imagebase(dll 之间的 Imagebase 冲突了也就是需要重定向的时候就没办法了),把这些函数绝对地址挨个贴到 IAT 表中。微软的VS有个bind.exe工具(现在最新版VS依然有),就是做这个事情的。
从前 Win 提供的 exe 大多都采用了这样的方式,这个方式就叫绑定导入。优点是极大减少程序启动时间。缺点就是要是使用的 dll 如果改动了,变动了原本的导出函数地址,那么用这种绑定导入方式调用这个 dll 的 exe 都得改,这就不太符合动态链接库的思想了。(当然调用 dll 的不止是exe,甚至其他 dll 或者任何 PE 文件,这里为了方便理解用 exe 指代调用者)另外如果绑定的过程中出现 dll 需要重定位,也就是前面说的某个 dll 占不住原本的 imagebase 的情况,也是无法绑定导入的,需要在文件加载时重新计算 IAT 表。
那么 IAT 表到底什么时候是地址,什么时候是非地址呢?需要有个标记来记录,这个标记就是上一章讲的导入表结构_IMAGE_IMPORT_DESCRIPOR 中的 时间戳 TimeDataStamp了,为 0 则表示该 dll 未绑定,即 IAT 表的值为非地址。该值为 -1 即 FFFFFFFF,表明已绑定。至于什么时候绑定的,真正的时间戳,需要看另一张表,这张表就是绑定导入表
绑定导入表位于数据目录的第12项
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp;
WORD OffsetModuleName;
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;
绑定导入表_IMAGE_BOUND_IMPORT_DESCRIPTOR 第一个成员就是真正的时间戳
在coff头/标准PE头 FILE_HEADER 中有个 TimeDateStamp字段,记录了文件生成时的时间戳
exe 绑定导入表中的时间戳,和对应 dll 标准 PE 头中的时间戳,如果吻合,证明是绑定的时候的dll,没有出错,如果不吻合证明dll已经被更新,或者 DLL 需要重定位的时候,exe 的绑定导入可能会出错,则系统在加载文件时会重新计算 IAT 中的值,不使用原本记录的绑定信息。
第二个成员 OffsetModuleName 是 dll 名
第三个成员 NumberOfModuleForwarderRefs 说明了在当前所在这个绑定导入表_IMAGE_BOUND_IMPORT_DESCRIPTOR 之后有几个 _IMAGE_BOUND_FORWARDER_REF 结构,这是个整数。如下图所示,如果是2,则后跟两个_IMAGE_BOUND_FORWARDER_REF 结构,再之后才是下一个绑定导入表_IMAGE_BOUND_IMPORT_DESCRIPTOR,以此类推。最后以绑定导入表长度的内容为全 0 结束。
至于为什么有这个结构呢?是因为一个 exe 会直接使用好几个 dll,这些 dll 如果绑定了就是绑定导入表的内容,但这些直接使用的dll可能还会再使用其他的dll,这个_IMAGE_BOUND_FORWARDER_REF 结构就是用来记录间接使用的dll 的。
因为也是描述的 dll 信息,所以_IMAGE_BOUND_FORWARDER_REF 结构 其实也是和绑定导入表一样的结构,第一个成员是时间戳,功能和绑定导入表的时间戳一样,第二个成员也一样是 dll 名,第三个成员是保留字段,无意义。
上述两个表示 dll 名的第二个成员 OffsetModuleName 不是 RVA 也不是 FOA,是使用第一个绑定导入表的RVA(即第一个_IMAGE_BOUND_IMPORT_DESCRIPTOR的RVA),加上OffsetModuleName 的值。得到的才是名字字符串真正的 RVA。那么其他的绑定导入表结构和 所有_IMAGE_BOUND_FORWARDER_REF 结构都一样,计算方法都是用当前OffsetModuleName 的值 加上 第一个绑定导入表的 RVA,注意全都是加第一个,不加别的。
这里用的是 Windows XP 中取出来的notepad.exe,可以去xp虚拟机中搞到这个exe,win10找了很多exe,都是没有绑定导入表的,没办法
彻底理解和确认思想,最好还是直接写代码,以下是打印绑定导入表的代码:
#include "Currency.h"
#include "windows.h"
#include "stdio.h"
VOID h330() //打印绑定导入表所有内容
{
char FilePath[] = "notepad.exe"; //CRACKME.EXE CrackHead.exe Dll1.dll R.DLL notepad.exe LoadDll.dll PETool.exe 打印dll用最后一个看
LPVOID pFileBuffer = NULL; //会被函数改变的 函数输出之一
LPVOID* ppFileBuffer = &pFileBuffer; //传进函数的形参
if (!ReadPEFile(FilePath, ppFileBuffer))
{
printf("文件读取失败\n");
return;
}
// 头查找
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS32 pImageNts = (PIMAGE_NT_HEADERS32)((DWORD)pFileBuffer + pDos->e_lfanew);
PIMAGE_DATA_DIRECTORY pDir = &pImageNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT];
PIMAGE_BOUND_IMPORT_DESCRIPTOR pBoundImport = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer,pDir->VirtualAddress));
//printf("VirtualAddress:%x\n", pDir->VirtualAddress);
if (pDir->VirtualAddress == 0)
{
printf("没有绑定导入表!\n");
return;
}
//全 0 判断,如果全 0 那就不循环了,只要有一个字段不等于 0 就继续循环
while (pBoundImport->NumberOfModuleForwarderRefs != 0 || pBoundImport->OffsetModuleName != 0 || pBoundImport->TimeDateStamp != 0)
{
printf("TimeDateStamp:%x\n", pBoundImport->TimeDateStamp);
// 当前 OffsetModuleName 的值 加上 第一个绑定导入表的RVA即pDir->VirtualAddress
// 上面的加法结果作为 新的 RVA, 新的 RVA 转换为绝对地址,作为字符串地址
printf("DllName:%s\n", (DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pBoundImport->OffsetModuleName + pDir->VirtualAddress));
printf("NumberOfModuleForwarderRefse:%d\n", pBoundImport->NumberOfModuleForwarderRefs);
printf("***********************************\n");
for (int i = 0; i < pBoundImport->NumberOfModuleForwarderRefs; i++)
{
pBoundImport++; //能走到这里的NumberOfModuleForwarderRefs值必不为0,由于绑定导入表和REF大小是一样的,所以这里相当于一起++
PIMAGE_BOUND_FORWARDER_REF pREF = (PIMAGE_BOUND_FORWARDER_REF)pBoundImport;
printf(" TimeDateStamp:%x\n", pREF->TimeDateStamp);
printf(" DllName:%s\n", (DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pREF->OffsetModuleName + pDir->VirtualAddress)); //和绑定导入表的字符串打印一样
printf(" ****************************\n");
}
pBoundImport++; //外循环最后别忘了导入表++
}
}
以下是运行的打印结果,与 PELoad 的对比验证
这里写代码的时候可能有个坑,如果仔细观察绑定导入表的 RVA 可以发现,这个 RVA 不在任何一个节中,即是属于头部的,这个绑定导入表是无缝紧跟在节表后的。所以如果前面写 RVA2FOA函数的时候没有考虑RVA偏移在头部的情况,那么这个程序就很可能会出错。