PPLdump 的终结

前几天, GitHub 上的PPLdump出现了一个issue,指出它不再适用于 Windows 10 21H2 Build 19044.1826。起初我持怀疑态度,所以我启动了一个新的虚拟机并开始调查。这是我发现的……

原文链接:https://itm4n.github.io/the-end-of-ppldump/

简而言之 PPLdump

如果您正在阅读本文,我会假设您已经知道 PPLdump 是什么以及它的作用。但以防万一,这里有一个非常简短的总结。

PPLdump是一个用 C/C++ 编写的工具,它实现了一个用户态漏洞利用,以管理员身份将任意代码注入 PPL。这项技术是 Alex Ionescu 和 James Forshaw 对受保护进程(PPs 和 PPLs)进行的深入研究的众多发现之一。

提醒一下,它的工作原理是这样的:

  1. 调用APIDefineDosDevice以诱使 CSRSS 服务创建\KnownDlls指向任意位置的符号链接。
  2. 创建一个新的 Section 对象(由前面的符号链接指向)来托管包含我们要注入的代码的自定义 DLL 的内容。
  3. 由作为 PPL 运行的可执行文件导入的 DLL 被劫持,我们的代码被执行。

这里要记住的最重要的事情是,整个漏洞利用依赖于 PPL 中存在但不存在于 PP 中的弱点。实际上,PPL 可以从\KnownDlls目录加载 DLL ,而 PP 总是从磁盘加载 DLL。这是一个关键的区别,因为只有在最初从磁盘读取 DLL 以创建新的 Section 对象时才会检查 DLL 的数字签名。映射到Process的虚拟地址空间时,事后不检查。

构建 19044.1826 发生了什么?

PPLdump 的调试输出已在 GitHub问题中提供,但我在带有 2022 年 7 月更新包(Windows 10 21H2 Build 19044.1826)的 Windows 10 VM 中复制了它。

总的来说,输出看起来相当不错,符号链接被正确创建,\KnownDlls所以乍一看,这个DefineDosDevice技巧仍然可以正常工作。这可以通过 WinObj 轻松确认,因为如果不能在“Windows TCB”级别执行 PPL 中的代码,就无法删除符号链接。

WinObj - 在 \KnownDlls 中创建的符号链接

然后使用我们自定义 DLL 的内容创建一个新部分,但该工具[-] The DLL was not loaded.在尝试劫持后最终失败并出现错误,该错误EventAggregation.dll通常由services.exe.

在这种情况下,显而易见的做法是启动 Process Monitor,看看我们是否能发现任何看起来不正确的地方。

使用进程监视器进行 PPLdump 调试

从最初的事件中,我们已经可以看到某些事情并没有按计划进行。由于services.exe作为 PPL 执行,我们不应该在 DLL 上看到任何文件操作(例如 CreateFileCreateFileMappingkernel32.dllKernelBase.dll因为这些是已知的 DLL。相反,它们应该直接从各自的部分\KnownDlls\kernel32.dll\KnownDlls\kernelbase.dll.

结论是 PPL 现在看起来就像 PP 一样,因此不再依赖于Known DLL

NTDLL 中的补丁?

PPL 流程的创建方式显然发生了一些变化。我已经知道去哪里看,但为了这篇文章,我将通过二进制差异以正确的方式做到这一点。

我首先在 Winbindex 上获得了 Windows 10 21H2 的最后两个版本,ntdll.dll使用Windows SDK下载了公共符号。symchk.exe

要比较的 NTDLL 文件

在加载文件并分析它们之后,我只是使用 Ghidra 的BinDiff 扩展来以适当的格式导出结果。

Ghidra - 文件被导出

然后可以将两个“BinExport”文件导入 BinDiff 以比较两个版本的ntdll.dll. 通过“相似度”对函数进行排序,我们可以立即看到 7 个函数有一些细微的差异,但其中一个非常突出:LdrpInitializeProcess. 这正是我期望找到一些变化的地方。

BinDiff - 加载器已修改

我们还可以看到有一个不匹配的功能,它是在最新版本中添加的:Feature_Servicing_2206c_38427506__private_IsEnabled.

BinDiff - 添加了一个函数

加载程序中已知的 DLL 处理

最初,当创建一个新进程时,只加载 NTDLL。在 NTDLL 中实现的图像加载器负责加载其他 DLL(以及许多其他事情)。要确定它是否应该使用已知 DLL ,它只需检查进程环境块PEB)中的几个标志。

此检查在以下屏幕截图(构建版本10.0.19044.1741)中突出显示。

保护标志检查

PEB结构已部分记录,但我们不会在官方文档中找到我们需要的信息。另一方面,Process Hacker包含一个更详细的定义。

在偏移量 3 处(peb + 3if语句中),我们可以找到一个包含一组 8 位标志的字节值。最低有效位保存ImageUsesLargePages标志的值,而最高有效位保存IsLongPathAwareProcess标志的值。

位域

有了这些知识,我们就可以将代码翻译*(byte *)(peb + 3)peb->BitField. 然后,该值0x42是一个掩码,允许加载程序隔离和检查标志IsProtectedProcessIsProtectedProcessLight. 因此,反编译后的代码if ((*(byte *)(peb + 3) & 0x42) == 2)可以解释如下。

换句话说,只有当进程是PP时, Known DLL才会被忽略,因此PPL的行为就像正常进程一样。这是对我们已经知道的内容的确认,所以让我们找出构建版本中的变化。10.0.19044.1806

如果我们搜索同一行代码,我们会立即意识到还有一个额外的检查取决于Feature_Servicing_2206c_38427506__private_IsEnabled(). 多么巧合!

Ghidra - 在加载程序中添加了检查

在该else块中,我们可以看到以下检查。

Ghidra - PEB 检查已修改

因此,Ghidra 生成的反编译代码可以总结如下。

如果我们应用我之前详述的相同逻辑,我们可以将上面的代码翻译成这个更易读的版本。

补丁现在看起来很清晰。首先,检查“功能服务”值。如果禁用此功能,加载程序会退回到以前版本的代码,因此 PPL 会加载Known DLL。另一方面,如果启用此功能,加载程序只需检查标志是否peb->IsProtectedProcess已设置。因此,受保护的进程(无论是 PP 还是 PPL)都不会使用Known DLLs

装载机中的新检查

在上一部分中,我们看到 的结果Feature_Servicing_2206c_38427506__private_IsEnabled()决定了加载器将使用的有关受保护进程已知 DLL的逻辑。乍一看,这个函数似乎并不复杂,所以让我们看看我们能从中学到什么。

Ghidra - 新的功能服务检查

根据 Ghidra 生成的反编译代码,该函数似乎首先检索全局变量的值Feature_Servicing_2206c_38427506__private_featureState,如果尚未初始化,则对其进行初始化,然后返回其第四位 ( uVar1 >> 3 & 1) 的值。

因此,看起来全局变量Feature_Servicing_..._featureState包含一组位标志,用于确定是否启用了特定功能。借助几行 C/C++ 和调试器,我们可以很容易地验证这一点。

运行上面的代码会产生以下输出。

Feature_Servicing_..._featureStateis的值0x0000001b,转换为0001 1011二进制。第四位被设置,返回值为11111 0111在第二部分中,我使用掩码( )使用按位与操作手动取消设置第四位0xf7。在这种情况下,返回值为0,这倾向于证实我对代码的解释。

最后,为了更好的衡量,我们还可以手动设置Feature_Servicing_..._featureStateto的值0并检查返回的值wil_..._ReevaluateCachedFeatureEnabledState(...)以确保它是0x1b.

WinDbg - 缓存值重新评估

返回值(见RAX)实际上是0x7ff700000000001bEAX寄存器(前32位RAX)用于以下操作(mov ebx,eax)所以有效值确实是0x0000001b

结论

我不确定是什么促使微软首先区分关于已知 DLL的 PP 和 PPL。也许这是一个性能问题,我不知道。无论如何,他们已经意识到了这个潜在的弱点,否则我猜他们不会对 PP 破例。问题是,这个安全漏洞现在已经修补,这是向前迈出的一大步。我喜欢认为我在这个变化中扮演了一个小角色,尽管我完全知道所有的工作都已经由 Alex 和 James 完成。

总之,这确实是“ PPLdump 的终结”。然而,这个工具只利用了 PPL 的一个弱点,但我们可能仍然可以利用其他几个用户空间问题。所以,从我的角度来看,这也是一个开始研究另一个旁路的机会……

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部