一系列日益复杂的程序演示了 64 位 Windows 上的函数挂钩。
我在自学函数挂钩如何工作的同时编写了这些程序。它们可能对其他尝试做同样事情的人(或者在我忘记这一切如何再次运作后的未来我)有帮助。它们旨在按顺序查看。第一次在程序中使用函数时,它将包含在该特定示例程序的 .cpp 文件中,其名称带有下划线前缀。使用相同函数的任何后续示例都将使用 hooking_common.h 中包含的该函数的副本,以最大限度地减少代码重复并保持后面的示例程序足够小以便仍然易于阅读。
我已经做了一些工作来清理它们,但是后面的示例仍然有点混乱。需要注意的是,此存储库中的最终结果不足以编写功能齐全的挂钩库,但足以开始沿着这条路走。
在运行时,某些项目可能依赖于正在构建的其他项目(注入dll需要已经构建了dll),或者同时运行(挂钩目标程序需要它已经在运行)。
所有示例均使用 Visual Studio 2019 (v142) 和 Windows SDK 10.0.17763.0 构建。我不认为这里有任何与 VS 或 SDK 版本相关的东西,但我在这里列出它以防万一。几乎可以肯定,有些东西是 MSVC 特有的。
最后,最后一个蹦床示例在 mspaint 中安装了一个钩子。我假设在将来的某个时候,对 mspaint 的更新将导致此示例崩溃。在撰写本文时,mspaint 的当前版本是 1909(操作系统内部版本 18363.1016)。
这些示例分为两类:使用蹦床的示例和不使用蹦床的示例。非蹦床示例仅用于演示在不同情况下将程序流从一个函数重定向到另一个函数。构建蹦床很复杂,当我试图弄清楚函数挂钩如何工作时,首先构建非蹦床示例非常有帮助。此外,还有 4 个“目标程序”,用于演示如何在不同(已运行)进程中安装挂钩的示例。
这些示例中的大多数都会泄漏与挂钩相关的内存。我真的不在乎,因为这些示例只是为了演示挂钩概念,而且因为这些“泄漏”的分配无论如何都需要存在直到程序终止。
虽然函数挂钩技术的标准术语似乎没有太多,但此存储库中的代码(和自述文件)使用以下术语:
由于这些示例在安装钩子时不会创建蹦床,因此我认为这些函数展示了“破坏性”钩子,因为原始函数在被钩子后完全无法使用。
一个使用跳转指令覆盖函数起始字节的小示例,该跳转指令将程序流重定向到同一程序中的不同函数。由于没有构建蹦床,因此此操作具有破坏性,并且原始函数不再可调用。这是存储库中唯一的 32 位示例。
上一个示例的 64 位版本。在 64 位应用程序中,函数可能位于内存中足够远的位置,无法通过 32 位相对跳转指令访问。由于没有 64 位相对跳转指令,该程序首先创建一个“中继”函数,其中包含可以到达内存中任何位置的绝对 jmp 指令的字节(并跳转到有效负载函数)。安装在目标函数中的 32 位跳转跳转到该中继函数,而不是立即跳转到有效负载。
提供一个使用上一个项目中的技术来挂钩成员函数而不是自由函数的示例。
与前面的示例略有不同,此程序演示了如何通过对象的 vtable 获取该函数的地址来将挂钩安装到虚拟成员函数中。没有其他示例涉及虚拟函数,但我认为它很有趣,可以包含在这里。
将挂钩安装到另一个正在运行的进程中的最简单示例。此示例使用 DbgHelp 库通过字符串名称在目标进程(A - 具有自由函数的目标)中查找函数。这是唯一可能的,因为目标程序是在启用调试符号的情况下构建的。虽然简单,但此示例比以前的程序稍长一些,因为它引入了大量新函数(用于定位和操作远程进程)。
此示例演示如何挂接另一个进程从 dll 导入的函数。由于 ASLR 的工作原理,如何获取远程进程中 dll 函数的地址存在一些细微差别,如此处所示。除此之外,此示例与前一个示例几乎相同。
此示例演示如何在未由 dll 导入且不在符号表中的函数中安装挂钩(可能是因为远程进程没有调试符号)。这意味着没有(简单)方法可以通过字符串名称查找目标函数。相反,此示例假设您已使用 x64dbg 等反汇编程序来获取要挂钩的函数的相对虚拟地址 (RVA)。该程序使用该 RVA 来安装钩子。
与上面类似,不同之处在于此示例使用 dll 注入来安装有效负载函数,而不是编写原始机器代码字节。这更容易使用,因为您的有效负载可以再次用 C++ 编写。此示例的有效负载包含在项目 08B-DLL-Payload 中。
下面的例子在hook时安装了trampolines,这意味着在安装hook后程序仍然可以执行目标函数中的逻辑。由于安装钩子至少会覆盖目标函数中的前5个字节,因此这5个字节中包含的指令将被移动到trampoline函数中。因此,调用trampoline函数有效地执行了目标函数的原始逻辑。
与示例#2 等效的蹦床安装。这个例子有点奇怪,因为我想演示如何创建一个蹦床而不需要使用反汇编引擎。在本例中,目标函数被创建为在开头具有已知的 5 字节指令,因此我们只需将该函数的前 5 个字节复制到蹦床函数即可。这意味着创建蹦床非常容易,因为我们知道它的确切大小并且它不使用任何需要修复的相对寻址。如果您正在为一个非常特定的用例编写一个蹦床,那么您可能只需对此进行一些变体即可逃脱。
此示例显示了与上一个示例类似的场景,只不过这次我使用反汇编程序(capstone)来获取我们需要从目标函数中窃取的字节。这使得挂钩代码可以用于任何函数,而不仅仅是我们知道将是简单情况的函数。实际上,这个示例中发生了很多事情,因为它从目标挂钩(如前一个)跳转到构建通用挂钩函数。蹦床必须将相对调用/跳转转换为使用绝对地址的指令,这使事情变得更加复杂。这也不是一个 100% 完美的通用挂钩示例,它会因循环指令而失败,并且如果您尝试使用少于 5 个字节的指令来挂钩函数。
基本上与上面相同,只是此示例包含在安装挂钩时暂停所有正在执行的线程的代码。这不能保证在所有情况下都是线程安全的,但它肯定比什么都不做要安全得多。
这扩展了前两个示例中使用的挂钩/蹦床代码,以支持将多个函数重定向到同一有效负载,并允许有效负载函数调用安装在其中的挂钩的其他函数。
这是第一个在不同进程中安装挂钩的蹦床示例(在本例中为目标应用程序 B - 具有来自 DLL 的自由函数的目标)。所有挂钩逻辑都包含在 dll 有效负载 13B - Trampoline Imported Func DLL Payload 中。这里没有太多新内容,这个示例只是将已经完成的蹦床挂钩内容与之前展示的用于挂钩从 dll 导入的函数的技术结合起来。
回购皇冠上的宝石。此示例将 dll 有效负载(14B - Trampoline Hook MSPaint Payload)注入到正在运行的 mspaint 实例中(您必须在运行此实例之前自行启动 mspaint)。无论您在 MSPaint 中实际选择什么颜色,安装的挂钩都会导致画笔绘制为红色。老实说,前面的示例中没有显示任何内容,看到它在非人为的程序上运行真是太酷了。
在循环中调用自由函数的简单目标应用程序。编译时包含调试信息。
目标应用程序在循环中调用从 dll (B2 - GetNum-DLL) 导入的自由函数。
在循环中调用非虚拟成员函数的目标应用程序。
在循环中调用虚拟成员函数的目标应用程序。