Windows Dirty Pipe 漏洞 CVE-2022-22715 分析与利用
突破沙箱的旧管道 - CVE-2022-22715 Windows Dirty Pipe
作者:Cyber Kunlun 的 k0shl
2022 年 2 月,微软修补了我在 2021 年天府杯用于逃逸 Adobe Reader 沙箱的漏洞,分配了 CVE-2022-22715。该漏洞存在于命名管道文件系统中,几乎从 AppContainer 诞生至今已有近 10 年。我们称之为"Windows Dirty Pipe"。
在本文中,我将分享 Windows Dirty Pipe 的根本原因和利用方法。让我们开始我们的旅程。
背景
命名管道是一种命名的单向或双工管道,用于管道服务器和一个或多个管道客户端之间的通信。许多浏览器和应用程序使用命名管道作为浏览器进程和渲染进程之间的 IPC。当微软发布 Windows 8.1 时,引入了 AppContainer 作为一种沙箱机制,用于隔离 UWP 应用程序的资源访问。
从那时起,一些浏览器和应用程序(如旧版 Edge 或 Adobe Reader)使用 AppContainer 作为其渲染进程沙箱,当然,命名管道文件系统添加了一些支持 AppContainer 的机制。结果,它带来了 Windows Dirty Pipe -- CVE-2022-22715。
Windows Dirty Pipe 的根本原因
该漏洞存在于命名管道文件系统驱动程序 - npfs.sys 中,问题函数是 npfs!NpTranslateContainerLocalAlias。当我们使用命名管道路径调用 NtCreateFile 时,它将命中 npfs 的 IRP_MJ_CREATE 主函数,该函数调用了 NpFsdCreate。
函数根据 NtCreateFile 的参数(如 ObjectAttributes 的 RootDirectory 或 CreateDisposition)分派到不同的处理函数。如果我们创建一个新的命名管道,它将进入 NpTranslatedAlias。
我们可以控制的命名管道名称将传递到 NpTranslateAlias,该函数将获取命名管道名称的前缀并与"LOCAL"进行比较,如果我们的命名管道名称使用"LOCAL"作为前缀,这将命中 NpTranslateContainerLocalAlias 函数。这意味着我们可以使用"\Device\NamedPipe\LOCAL\xxxxx"作为命名管道名称。
最后,我们命中了有漏洞的函数,是时候展示根本原因了。
首先,npfs 检查进程令牌权限是否为 appcontainer 或 restricted,它必须至少满足两个条件之一,这意味着进程必须是 appcontainer、受限沙箱进程或两者都是。然后,函数检查命名管道名称的第一个 wchar 是否为"",如果是,npfs 将变量|ifslash|设置为 1。之后,它计算一个新的命名管道前缀长度,新的命名管道前缀包括 SID、会话号、指定字符串等,最后将新前缀长度加上命名管道名称长度和 0x14,如果变量|ifslash|为 1,总大小将加 2 到最终大小。
注意所有变量都是 ushort 类型,因此存在明显的整数溢出,如果我们使用长长度的命名管道名称,最终总大小将是一个小值。
计算后,npfs 由于总大小较小而分配一个小池,然后如果|ifslash|为 1,总大小减 2,如果总大小为 0,则存在整数下溢,unicode 字符串的 maxiumlength 将是一个大的 ushort 值 0xfffe。
函数 RtlUnciodeStringPrintf 将字符串复制到新池缓冲区中,memcpy 的长度取决于 unicode 字符串的 maxiumlength,如果我们之前触发了整数下溢,npfs 将复制一个大值到小池,触发越界写入。
Windows Dirty Pipe 的挑战
在介绍了 Windows Dirty Pipe 的根本原因后,我想在公开我的利用之前分享 CVE-2022-22715 的挑战。
当我触发崩溃并确认漏洞时,我很快意识到该漏洞不容易利用,在利用过程中我会遇到一些挑战。
虽然 npfs 计算总大小时的整数溢出可以使总大小变为小值,如 0x20\0x30\0x40...,但它必须为 0,因为我们需要触发整数下溢使 unicode 字符串的 maxiumlength 变为大的 ushort 值以进行越界写入,如果我们将总大小设置为大于 0,在总大小减 2 后,它仍然是一个小值,越界写入将不会触发。
正如我上面所说,memcpy 长度是 0xfffe,这意味着我需要将超过 16 页的池内存复制到分页池段中,这不容易形成稳定的布局。
有趣的内核池分配机制
我利用的第一步是尝试找到完成池风水的方法。在这种情况下,损坏的池必须是 0x20 分页池,这是一个内核低碎片堆(LFH)池,起初,我想喷洒 0x20 LFH 池,并损坏一些 0x20 对象以完成利用。
但有一个问题,我无法精确控制 LFH 桶中易受攻击的 0x20 池位置,且 memcpy 长度为 0xfffe,这可能会损坏一些意外对象或受保护页面,导致 BSoD。
阶段 1:准备
由于易受攻击的池是分页池,因此我选择 WNF 作为我的有限读/写原语。我使用_WNF_STATE_DATA 作为有限的越界读/写对象——管理对象,_WNF_STATE_DATA 的最大读/写范围是 0x1000。我需要找到另一个对象来完成任意地址读/写——工作对象。实际上,找到一个合适的对象并不难,该对象必须是一个分页池对象,包含一个指针字段,可用于通过 memcpy 读/写任意地址。
我最终决定使用_TOKEN 对象作为工作对象,如果我使用 TokenDefaultDacl TokenInformationClass 调用 NtSetInformationToken,nt 最终调用 nt!SepAppendDefaultDacl 将用户控制的内容复制到存储在_TOKEN 对象中的指针字段。
如果我使用 TokenBnoIsolation TokenInformationClass 调用 NtQueryInformationToken,nt 将 isolationprefix 缓冲区复制到用户模式内存。
因此,我可以使用管理对象构建假的_TOKEN 对象结构来修改相邻的工作对象,然后使用 NtSetInformationToken 和 NtQueryInformationToken 作为任意读/写原语。
我需要准备的另一个对象是 0x20 喷洒对象,它应该完全由我控制,包括分配和释放。我发现有一个名为 nt!NtRegisterThreadTerminatePort 的函数。
函数引用一个 LpcPort 对象并为存储 LpcPort 对象分配一个 0x20 分页池,然后将其存储到_ETHREAD 对象中。如果我们创建一个线程并在线程中多次调用 NtRegisterThreadTerminatePort,它可以分配大量的 0x20 分页池。
最终我脑海中形成了一个池风水计划:
喷洒 0x20 分页池以填充 LFH 子段,如果所有段都已满,后端分配将分配一个新段,我们的新 0x20 LFH 子段将位于新段中。
喷洒_TOKEN 对象和_WNF_STATE_DATA 对象以填充 VS 子段,确保它们位于同一页面中,前端分配将最终分配新的 VS 子段,它将位于在步骤 1 中创建的段中,与 LFH 子段相邻。
阶段 2:池风水
在喷洒 WNF 对象时,我发现创建了另一个名为_WNF_NAME_INSTANCES 的对象,这将导致前端分配创建另一个 LFH 段并影响我们的池风水布局。
因此,在我进行池风水之前,我创建了大量的 0xd0 池并释放它们,以创建大量的 0xd0 池洞来存储_WNF_NAME_INSTANCES 对象。
我首先分配大量的喷洒对象并喷洒_TOKEN 对象和_WNF_STATE_DATA 对象,这将在新段中创建新的 LFH 子段和 VS 子段。
布局显示,在末端 LFH 桶中有许多空闲的 LFH 池洞,新的 VS 子段紧邻 LFH 桶,如果我们现在创建易受攻击的对象,它将位于其中一个空闲的 LFH 池洞中。
注意易受攻击的对象可能不位于最后一个 LFH 页面中,但这并不必要,越界写入可能损坏 LFH 桶不会影响我们的利用。
然后在调用 RtlUnicodeStringPrintf 函数后,它将越界写入约 0xfffe 内存大小的内容,这会损坏 LFH 池空间和 VS 池空间。损坏的数据是我们可以控制的命名管道名称,我们需要计算恶意负载以修改_WNF_STAT_DATA->DataSize。
当我们创建_WNF_STATE_DATA 时,我们不能将 DataSize 设置为大于_WNF_STATE_DATA 数据区域,但在触发漏洞后,我们可以将其修改为任何值,DataSize 的最大值是 0x1000,我们可以在下一页中获得有限的越界读/写原语来修改_TOKEN 对象。
阶段 3:获得任意地址读/写
在阶段 2 中,我们进行了池风水,并使用_WNF_STATE_DATA 对象获得了有限的读/写原语,但有一个巨大的问题。我如何找到需要使用的对象句柄?
如果我损坏了对象并通过句柄使用它,损坏的对象头数据将使系统崩溃。现在,我需要找出一个有用的管理对象(_WNF_STAT_DATA)名称和工作对象(_TOKEN)句柄。
我想到了一个解决方案。对于管理对象,当我们尝试从_WNF_STATE_DATA 数据区域读取数据时,我们使用指定的长度调用 NtQueryWnfStateData,如果长度大于 DataSize,它将返回 nt 错误代码 0xc0000023。对于工作对象,当我们创建_TOKEN 对象时,_TOKEN 对象中有一个唯一的 LUID,可以通过使用 TokenStatics TokenInformationClass 的 NtQueryInformationToken 查询它,它名为 TokenId,我们可以在喷洒_TOKEN 对象时查询它们并将其存储在数组中。
因为_WNF_NAME_INSTANCES 不会被损坏,我们可以正常使用 NtUpdateWnfStateData 和 NtQueryWnfStateData。
我已经在阶段 2 损坏了一些_WNF_STATE_DATA 对象,并将 DataSize 修改为 0x1000,我们可以使用带有 0x1000 长度参数的 NtQueryWnfStateData 来找到损坏的_WNF_STATE_DATA 对象,并读取越界数据以找到最后一个损坏的页面,与损坏页面相邻的正常页面。
读取越界数据不会损坏对象结构,因此我们可以使用带有 0x1000 长度参数的 NtQueryWnfStateData,如果_WNF_STATE_DATA 对象未损坏,它将返回 0xC0000023,如果已损坏,它将返回越界数据。
如果越界数据是恶意数据,我可以确定_WNF_STATA_DATA 不在最后一个损坏的页面中,我使用这种方法找出最后一个损坏的页面,以便我可以读取具有_TOKEN 对象结构的下一个正常页面。最后一个损坏的页面中的_WNF_STATE_DATA 对象是我们的管理对象。
_TOKEN 对象中有一个 LUID 字段,我们从越界读取数据中获取它,并在我们之前创建的数组中匹配此 LUID,这样我们最终找到了工作对象。
到目前为止,我获得了管理对象名称和工作对象句柄,然后我构建了一个 0x1000 的假数据,包括假的_TOKEN 对象结构和_WNF_STATE_DATA 结构。我之前通过调用 NtQueryWnfStateData 已经获得了正常的_TOKEN 对象结构内容,我只需要更改一些值以获得任意读/写原语。
阶段 4:权限提升和修复
我们获得了任意地址读/写原语,起初,我只想将进程 TOKEN 替换为 system,它成功了,但过了一会儿,我发现它很容易崩溃。例如,我损坏了一些_TOKEN 对象,如果我打开 processexplorer,它将遍历每个进程的用户空间句柄表,当 processexplorer 访问利用进程句柄表时,将导致崩溃。
我需要在利用后进行修复,因此我决定不替换进程 TOKEN,而只是修改_ETHREAD->PreviousMode,如果我将 previous mode 设置为 0,我调用 NT API 如 NtReadVirtualMemory 和 NtWriteVirtualMemory,内核将认为线程在内核模式下运行。这是一种常见的提升权限技术,它方便我进行权限提升和修复,而不是每次构建假对象。
最后我使用工作对象将_ETHREAD->PreviousMode 设置为 0,然后使用 NtReadVirtualMemory/NtWriteVirtuaMemory 进行权限提升和修复。
在修复时我们需要做一些事情。
损坏的_Token 对象。
我触发损坏的对象崩溃并意识到它崩溃是因为我损坏了 ObjectHeader 中的 ObjectType,因此当 nt 引用该对象时,它将使系统崩溃。我可以从 nt 数据部分获取 cookie 并计算对象头中的 objecttype。我修复每个损坏的_TOKEN 对象头。
损坏的 VS 池结构。
这是我遇到的最复杂的问题,我不仅损坏了对象结构,还损坏了 VS 池结构,这将导致意外的 BSoD。我对 VS 分配进行了一些深入的反向工程,发现有一个 RBTree 来管理 VS 池,如果我知道一个 VS 池地址,我可以计算 VS 池管理器地址。
当分配新的 VS 池或释放旧的 VS 池时,它将从 VS 池管理器遍历 RBTree,如果我损坏了 VS 池地址,这意味着当 VS 池管理器从根节点遍历并访问损坏的节点时,它将崩溃。
因此我需要从 RBTree 根节点找到崩溃节点,并将其从 RBTree 中删除,如果损坏节点下有一些其他 VS 池,这可能会导致一些内存泄漏,但比使系统崩溃要好。
我计算根 VS 池,遍历 RBTree 并从 RBTree 中删除节点。
所有修复后,是时候弹出 cmd 了。因为 Adobe Reader 渲染进程在一个 Job 中,我无法从中创建进程,因此我将 shellcode 注入浏览器进程并在 C 卷中写入一个文件以完成利用。
补丁
微软在 2022 年 2 月修补了该漏洞,npfs 使用 int 类型计算总大小并检查总大小是否大于最大 ushort 值。
演示我如何使用具有可访问 SD 的 WNF API
该代码演示了如何通过查询进程令牌的安全描述符,创建一个具有可访问权限的 WNF 对象,用于漏洞利用过程中的池风水布局。更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
公众号二维码
公众号二维码







评论