介绍

这篇博文是一个由三部分组成的系列中的第一篇,描述了在尝试在 WoW64 应用程序(在 64 位 Windows 平台上运行的 32 位进程)中挂接本机 NTDLL 时必须克服的挑战。正如 许多其他来源所记录的那样,WoW64 进程包含两个版本的 NTDLL。第一个是专用的 32 位版本,它将系统调用转发到 WoW64 环境,在那里它们被调整以适应 x64 ABI第二个是原生 64 位版本,由 WoW64 环境调用,最终负责用户模式到内核模式的转换。

由于挂接 64 位 NTDLL 的一些技术困难,大多数与安全相关的产品在此类过程中仅挂接 32 位模块。唉,从攻击者的角度来看,在一些众所周知的技术的帮助下,绕过这些 32 位钩子和它们提供的缓解措施是相当微不足道的。尽管如此,为了调用系统调用并执行各种其他任务,这些技术中的大多数最终都会调用 NTDLL 的本机(即 64 位)版本。因此,通过挂接本机 NTDLL,端点保护解决方案可以更好地了解进程的操作,并在一定程度上增强对绕过的弹性。

在这篇文章中,我们将介绍将 64 位模块注入 WoW64 应用程序的方法。下一篇文章将仔细研究其中一种方法,并深入探讨处理 CFG 感知系统所需的一些调整的细节。本系列的最后一篇文章将介绍为了挂接 64 位 NTDLL 而必须应用于现成的挂钩引擎的更改。

当我们开始这项研究时,我们决定将精力主要集中在 Windows 10 上。我们介绍的所有注入方法都在多个 Windows 10 版本(主要是 RS2 和 RS3)上进行了测试,如果在较旧的 Windows 版本上使用,可能需要略有不同的实现。

注射方法

将 64 位模块注入 WoW64 应用程序一直是可能的,尽管这样做时需要考虑一些限制。通常,WoW64 进程包含很少的 64 位模块,即本机ntdll.dll和构成 WoW64 环境本身的模块:wow64.dllwow64cpu.dll 和 wow64win.dll。遗憾的是,常用的 Win32 子系统 DLL(例如 kernelbase.dllkernel32.dll、user32.dll 等)的 64 位版本未加载到进程的地址空间中。强制进程加载这些模块中的任何一个都是可能的,尽管有些困难和不可靠

因此,作为我们成功和可靠注入的第一步,我们应该剥离除本机 NTDLL 之外的所有外部依赖项的候选模块。在源代码级别,这意味着对更高级别的 Win32 API(如 VirtualProtect())的调用必须替换为对其本机对应项的调用,在本例中为 NtProtectVirtualMemory()。还需要其他改编,并将在本系列的最后一部分详细讨论。

图 1 – 仅具有单个导入描述符 (NTDLL) 的简约 DLL

在创建符合这些限制的 64 位 DLL 后,我们可以继续查看一些可能的注入方法。

劫持wow64log.dll

正如 Walied Assar 之前发现的那样,在初始化时,WoW64 环境会尝试直接从 system32 目录加载名为 wow64log.dll 的 64 位 DLL。如果找到此 DLL,它将被加载到系统中的每个 WoW64 进程中,因为它导出一组特定的、定义良好的函数。由于 wow64log.dll 目前没有随 Windows 零售版本一起提供,因此这种机制实际上可以被滥用为一种注入方法,只需劫持此 DLL 并将我们自己的版本放在 system32 中即可。

图 2 – ProcMon 捕获显示 WoW64 进程尝试加载wow64log.dll

这种方法的主要优点在于其简单性——注入模块所需的只是将其部署到上述位置,然后让系统加载器完成剩下的工作。第二个优点是加载此 DLL 是 WoW64 初始化阶段的合法部分,因此所有当前可用的 64 位 Windows 平台都支持它。

但是,此方法存在一些可能的缺点:首先,名为 wow64log.dll 的 DLL 可能已经存在于 system32 目录中,即使(如上所述)默认情况下它不存在。其次,此方法几乎不提供对注入过程的控制,因为对 LdrLoadDll() 的底层调用最终由系统代码发出。这限制了我们从注入中排除某些过程、指定何时加载模块等的能力。

天堂之门

通过简单地发出对 LdrLoadDll() 的调用,而不是让内置的系统机制代表我们调用它,可以实现对注入过程的更多控制 。实际上,这并不像看起来那么简单。正如人们可以正确地假设的那样,32 位图像加载器将拒绝任何加载 64 位图像的尝试,从而停止此操作过程。因此,如果我们想将原生模块加载到 WoW64 进程中,我们必须以某种方式通过原生加载器。我们可以分两个阶段来完成此操作:

  1. 获得在目标进程中执行任意 32 位代码的能力。

  2. 构建对 64 位版本的 LdrLoadDll() 的调用,并将目标 DLL 的名称作为其参数之一传递。

鉴于能够在目标进程的上下文中执行 32 位代码(存在多种方法),我们仍然需要一种可以自由调用 64 位 API 的方法。一种方法是利用所谓的“天堂之门”。

“Heaven's Gate”是一种技术的常用名称,它允许 32 位二进制文件执行 64 位指令,而无需通过 WoW64 环境强制执行的标准流程。这通常是通过用户启动的控制传输到代码段0x33来完成的,该代码段将处理器的执行模式从 32 位兼容模式切换到 64 位长模式。

图 3 – 执行 x86 代码的线程,就在它过渡到 x64 领域之前。

跳转到 x64 领域后,直接调用 64 位 NTDLL 的选项变得很容易获得。在漏洞利用和其他潜在恶意程序的情况下,这使他们能够避免命中放置在 32 位 API 上的钩子。但是,对于DLL注入器,这解决了手头的问题,因为它打开了调用64位版本的LdrLoadDll()的可能性,该版本能够加载64位模块。

图4 -出于演示目的,我们使用Blackbone库成功地将64位模块注入到使用Heaven's Gate的WoW 64进程中。

我们将不再详细介绍“天堂之门”的具体实现,但好奇的读者可以在这里了解更多。

Injection via APC

由于能够将内核模式驱动程序加载到系统中,我们可以使用的注入方法库显着增长。在这些方法中,最流行的可能是通过APC注入它被一些AV供应商恶意软件开发人员广泛使用,甚至可能被CIA使用。

简而言之,APC(Asynchronous Procedure Call)是一种内核机制,它提供了一种在特定线程的上下文中执行自定义例程的方法。调度后,APC 会异步转移目标线程的执行流以调用所选例程。

APC 可分为两种主要类型之一:

  1. 内核模式 APC:APC 例程最终将执行内核模式代码。它们进一步分为特殊的内核模式 APC 和普通内核模式 APC,但我们不会详细介绍它们之间的细微差别

  2. 用户模式 APC:APC 例程最终将执行用户模式代码。仅当拥有用户模式 APC 的线程变得可告警时,才会调度用户模式 APC。这是我们将在本节其余部分处理的 APC 类型。

APC 主要由系统级组件用于执行各种任务(例如促进 I/O 完成),但也可以用于 DLL 注入目的。从安全产品的角度来看,从内核空间注入 APC 提供了一种方便可靠的方法,可以确保将特定模块加载到(几乎)整个系统中的每个所需进程中。

对于 64 位 NT 内核,负责初始调度用户模式 APC(对于本机 64 位进程以及 WoW64 进程)的函数是从本机 NTDLL 导出的 KiUserApcDispatcher() 的 64 位版本。除非 APC 颁发者另有明确请求(通过 PsWrapApcWow64Thread()),否则 APC 例程本身也将执行 64 位代码,因此能够加载 64 位模块。

通过 APC 实现 DLL 注入的经典方法围绕着使用所谓的“适配器 thunk”。适配器 thunk 是写入目标进程的地址空间的与位置无关的代码的简短片段。它的主要目的是从用户模式 APC 的上下文加载 DLL,因此它将根据 KNORMAL_ROUTINE 规范接收其参数:

图 5 – 用户模式 APC 过程的原型,取自 wdm.h

从上图可以看出,KNORMAL_ROUTINE 类型的函数接收三个参数,其中第一个参数是 NormalContext。与 WDM 模型中的许多其他“上下文”参数一样,此参数实际上是指向用户定义结构的指针。在我们的例子中,我们可以使用此结构将以下信息传递到 APC 过程中:

  1. 用于加载 DLL 的 API 函数的地址。在 WoW64 进程中,这必须是本机 LdrLoadDll(),因为 kernel32.dll 的 64 位版本 不会加载到进程中,因此无法使用 LoadLibrary() 及其变体。

  2. 我们希望加载到进程中的 DLL 的路径。

一旦 KiUserApcDispatcher() 调用了适配器 thunk,它就会解压缩 NormalContext,并使用给定的 DLL 路径和其他一些硬编码的参数对提供的加载程序函数发出调用:

图 6 — 设置为用户模式 APC 目标的典型“适配器 thunk”

为了利用这种技术,我们编写了一个标准的内核级 APC 注入器,并对其进行了修改,以支持将 64 位 DLL 注入到 WoW64 进程中(如附录 A 所示)。尽管很有希望,但当尝试将我们的 DLL 注入任何 CFG 感知的 WoW64 进程时,该进程因 CFG 验证错误而崩溃。

图7 — 尝试调用适配器thunk导致的CFG验证错误

下一篇:

在下一篇文章中,我们将深入研究 CFG 的一些实现细节,以帮助理解这种注入方法失败的原因,并提出几种可能的解决方案来克服这一障碍。

附录

附录 A – 带有适配器 thunk 的 APC 注入的完整源代码