为可移植性付出代价¶
函数地址总是实时从 IAT 解析,所以钩住 IAT 类似于钩住 VF 表。由于函数指针存储在 IAT 中、紧挨着实际名称,无需进行逆向工程或内存扫描;只要知道要钩的 API 名称,就可以钩住它!此外,IAT 钩子让你可以轻松地按模块钩住 Windows API 调用,让你的钩子只拦截游戏主模块的 API 调用。
但这种可移植性是有代价的;放置 IAT 钩子的代码比你目前看到的复杂得多。首先,你需要定位游戏主模块的 PE 头。由于 PE 头是任何二进制中的第一个结构,你可以在每个模块的基址找到它,如清单 8-4 所示(跟着项目的 IATHookExample.cpp 文件做)。
清单 8-4:获取模块的基址
找到基址后,你必须验证 PE 头有效。这种验证可能非常重要,因为一些游戏试图通过在加载后打乱 PE 头的非必要部分来防止这类钩子。有效的 PE 头以 DOS 头为前缀,它表明文件是 DOS MZ 可执行文件;DOS 头由魔数 0x5A4D 标识。DOS 头中名为 e_lfanew 的成员然后指向可选头,可选头包含代码大小、版本号等值,由魔数 0x10B 标识。
Windows API 有名为 IMAGE_DOS_HEADER 和 IMAGE_OPTIONAL_HEADER 的 PE 结构,分别对应DOS 头和可选头。你可以用它们验证 PE 头,代码如清单 8-5。
auto dosHeader = pointMemory<IMAGE_DOS_HEADER>(base
Addr);
if (dosHeader->e_magic != 0x5A4D)
return 0;
auto optHeader =
pointMemory<IMAGE_OPTIONAL_HEADER>(baseAddr +
dosHeader->e_lfanew + 24);
if (optHeader->Magic != 0x10B)
return 0;
清单 8-5:确认 DOS 头和可选头有效
对 pointMemory() 的调用创建指向需要检查的两个头的指针。如果任一 if() 语句返回 0,相应的头就有错误的魔数,意味着 PE 头无效。
汇编中对 IAT 的引用是硬编码的,意味着汇编引用不会遍历 PE 头来定位 IAT。相反,每个函数调用有静态位置,指示在哪里找到函数地址。这意味着覆盖 PE 头以声明没有导入,是防止 IAT 钩子的可行方法,一些游戏就有这种保护。
为了应对这一点,你还需要确保游戏的 IAT 仍然存在。清单 8-6 展示了如何向清单 8-5 中的代码添加这样的检查。
auto IAT = optHeader->DataDirectory[IMAGE_DIRECTORY
_ENTRY_IMPORT];
if (IAT.Size == 0 || IAT.VirtualAddress == 0)
return 0;
清单 8-6:检查 IAT 是否实际存在
PE 头包含许多存储应用程序代码、嵌入资源、重定位等信息的部分。清单 8-6 中的代码特别关注数据部分,正如你可能猜到的,它存储许多不同类型的数据。每种类型的数据都存储在各自的目录中,IMAGE_OPTIONAL_HEADER 的 DataDirectory 成员是描述数据部分中每个目录大小和虚拟地址的目录头数组。Windows API 定义了一个名为 IMAGE_DIRECTORY_ENTRY_IMPORT 的常量,它恰好是 DataDirectory 数组中 IAT 头的索引。
因此,这段代码用 optHeader->DataDirectory[IMAGE_DIR ECTORY_ENTRY_IMPORT] 解析 IAT 的头,并检查头的 Size 和 VirtualAddress 非零,基本上确认它的存在。