突破沙箱的旧管道 - CVE-2022-22715 Windows Dirty Pipe
作者:Cyber Kunlun的k0shl
2022年2月,微软修复了一个由我在2021年天府杯上用于逃逸Adobe Reader沙箱的漏洞,该漏洞被分配为CVE-2022-22715。此问题存在于命名管道文件系统中,自AppContainer机制诞生以来已存在近十年之久。我们将这一技术缺陷称为“Windows Dirty Pipe”。
本文将深入剖析Windows Dirty Pipe的根本成因及其利用方式,开启一段内核攻防之旅。
背景介绍
命名管道是一种支持单向或双向通信的命名通道,通常用于管道服务器与一个或多个客户端之间的数据交换。许多现代浏览器和桌面应用(如旧版Edge或Adobe Reader)均采用命名管道作为主进程与渲染子进程间的IPC(进程间通信)机制。
随着Windows 8.1的发布,微软引入了AppContainer这一轻量级沙箱技术,旨在限制UWP应用对系统资源的访问权限。此后,部分传统应用程序也借鉴此机制,将其应用于渲染进程的隔离保护中。为了支持AppContainer环境下的命名管道操作,系统在npfs(命名管道文件系统驱动)层面添加了若干适配逻辑。然而,正是这些新增逻辑埋下了安全隐患——最终导致了CVE-2022-22715的出现,即我们所称的“Windows Dirty Pipe”。
漏洞成因分析
该漏洞位于Windows内核驱动 npfs.sys 中,核心问题函数为 npfs!NpTranslateContainerLocalAlias。当用户调用 NtCreateFile 并传入特定格式的命名管道路径时,IRP_MJ_CREATE 请求会被分发至 NpFsdCreate 处理流程。
根据传入参数(例如 ObjectAttributes 中的 RootDirectory 或 CreateDisposition),系统会将请求路由到不同的处理分支。若尝试创建新的命名管道实例,则流程将进入 NpTranslatedAlias 函数。
在此过程中,用户可控的管道名称会被传递至 NpTranslateAlias,该函数首先提取名称前缀并与字符串 "LOCAL" 进行比对。一旦匹配成功(即使用形如 \Device\NamedPipe\LOCAL\xxxxx 的路径),便会触发对 NpTranslateContainerLocalAlias 的调用。
[此处为图片1]
接下来是关键环节:
- 系统首先检查当前进程令牌是否属于 AppContainer 或受限沙箱类型,至少需满足其一;
- 随后判断管道名称首字符是否为反斜杠(
\),若是,则设置标志位 ifslash = 1;
- 然后计算新生成的管道前缀长度,该前缀包含SID、会话ID及固定标识等信息;
- 最终总长度 = 新前缀长度 + 用户指定的管道名长度 + 0x14,若
ifslash == 1,则额外再加2。
值得注意的是,所有参与计算的变量均为 ushort 类型(16位无符号整数),因此极易发生整数溢出。通过构造超长的管道名称,可使最终计算结果回绕为极小值(如0x20、0x30等)。
基于此计算值,系统分配相应大小的内核池空间。紧接着,若 ifslash == 1,程序将执行减2操作。此时若原始总大小恰好为2,减后变为0,从而引发整数下溢。
这一异常会导致后续构建的Unicode字符串结构体中 MaximumLength 字段被赋值为0xFFFE(最大有效值)。当调用 RtlUnicodeStringPrintf 执行字符串复制时,底层 memcpy 操作将以 MaximumLength 作为拷贝长度,从而向一个极小的缓冲区写入高达约64KB的数据,造成严重的越界写入(OOB Write)。
利用过程中的挑战
尽管漏洞原理清晰,但在实际利用过程中仍面临多重障碍:
首要难点在于:必须精确控制整数溢出后的总大小为0,才能在减2后触发下溢(即从0变为0xFFFF)。若仅使总大小略大于0(如0x20),减2后仍为非零小值,无法达成 MaximumLength 被置为0xFFFE 的条件,进而无法触发大规模越界写入。
其次,由于 memcpy 长度可达0xFFFE字节(约64KB),这意味着需要连续向分页池中写入超过16个内存页的内容。这不仅对内核堆布局稳定性提出极高要求,也极大增加了实现可靠利用的难度。
内核池分配机制的探索
为应对上述挑战,我的利用策略第一步便是研究如何有效构造内核池风水(Pool Feng Shui),以提高堆喷射的成功率与稳定性。
此次受影响的池块大小为0x20字节,属于分页池中的低碎片堆(Low Fragmentation Heap, LFH)管理范畴。初期设想是大量喷射0x20大小的对象,期望越界写入能覆盖某个敏感结构体,从而获得进一步控制权。
然而,由于LFH具有较强的随机化与隔离特性,单纯喷射难以保证目标对象落在预期位置。因此,后续工作重点转向寻找更稳定的内存布局方法,并结合其他辅助手段提升利用成功率。
在利用过程中,存在一个关键挑战:无法精准控制LFH桶中易受攻击的0x20池块的具体位置。由于memcpy操作的长度达到0xfffe,极有可能覆盖到非目标区域,例如某些敏感对象或受保护的内存页,从而引发系统蓝屏(BSoD)。
阶段1:环境准备
考虑到目标池位于分页池中,我选择Windows Notification Facility(WNF)机制作为实现有限读/写能力的基础手段。具体而言,使用_WNF_STATE_DATA结构体作为管理对象,该对象支持最大0x1000字节范围的越界访问权限。然而,仅凭此尚不足以达成任意地址读写,因此还需引入另一个合适的工作对象。
理想的工作对象应满足以下条件:属于分页池分配、具备可被操控的指针字段,并能通过用户态调用触发对指定内存区域的读写操作。经过分析,_TOKEN对象成为首选目标。
当以TokenDefaultDacl为信息类调用NtSetInformationToken时,内核函数nt!SepAppendDefaultDacl会被触发,将用户提供的数据复制至_TOKEN结构内部的指针所指向的位置;而使用TokenBnoIsolation类调用NtQueryInformationToken时,isolationprefix缓冲区内容将被拷贝回用户空间。
基于上述行为,可通过构造伪造的_TOKEN结构体来影响相邻的真实_TOKEN对象,进而结合两个系统调用来构建稳定的任意地址读写原语。
此外,还需要一种完全可控的0x20大小喷洒对象,用于精确布局池内存状态。我发现nt!NtRegisterThreadTerminatePort函数恰好满足需求——该函数会引用LpcPort对象并为其分配一个0x20大小的分页池块,随后将其存储于_ETHREAD结构中。通过在线程内反复调用该系统调用,即可大量喷洒0x20池块。
综合以上要素,制定如下池风水策略:
- 首先进行0x20大小的池喷射,填满LFH子段;若当前段已满,则触发后端分配创建新段,确保新的LFH子段落位于全新分配的内存段中。
- 接着喷射_TOKEN与_WNF_STATE_DATA对象,促使它们填充VS子段,并尽量保证这些对象集中于同一物理页面。前端分配最终会产生新的VS子段,其位置将紧邻前一步生成的LFH子段,形成有利的相邻布局。
阶段2:执行池风水布局
在实际喷射WNF相关对象时,观察到系统会额外创建名为_WNF_NAME_INSTANCES的辅助对象,这可能导致前端分配再次开辟新的LFH段,干扰原有的内存排布计划。
为缓解这一问题,在正式开始风水操作前,预先大量分配并释放0xd0大小的池块,制造出多个0xd0尺寸的“空洞”,以便后续_WNF_NAME_INSTANCES自动落入其中,避免打乱主布局结构。
接下来按序执行以下步骤:
- 批量分配喷洒对象;
- 同时喷射_TOKEN和_WNF_STATE_DATA对象;
此举将促使系统在新分配的段中建立独立的LFH与VS子段。[此处为图片1]
此时内存布局显示:LFH桶末端存在大量空闲的小型池块空洞,而新的VS子段紧挨其后。若此刻分配易受攻击的对象,它大概率会落入某个现有的LFH空洞中。
需注意的是,该对象未必落在LFH段的最后一个页面上,但这并不影响整体利用效果——即使越界写入破坏了部分LFH元数据,也不会直接导致崩溃,因为我们已经完成了关键对象的布局。
在调用RtlUnicodeStringPrintf函数之后,会发生约0xfffe字节长度的越界写入,覆盖LFH与VS池区域。写入内容为我们可控的命名管道名称字符串,需精心构造该负载,使其恰好修改相邻_WNF_STATE_DATA对象中的DataSize字段。
初始状态下,_WNF_STATE_DATA的DataSize不可设置为超出其原始数据区范围的值;但利用漏洞成功改写后,可将其扩大至最大0x1000。借此,我们获得跨页的有限越界读写能力,可用于进一步篡改邻近的_TOKEN对象,拓展控制范围。
阶段3:实现任意地址读写
经过第二阶段的池风水操作,已成功建立基于_WNF_STATE_DATA的有限读写原语。然而,仍面临一个核心难题:如何准确定位所需使用的对象句柄?
一旦关键对象被破坏,再通过原有句柄访问该对象,极可能因对象头校验失败而导致系统崩溃。因此,必须设法获取有效的管理对象(即_WNF_STATE_DATA)名称以及工作对象(即_TOKEN)的有效句柄。
只有在准确掌握这两个关键标识的前提下,才能安全地实施后续读写操作,而不触发异常或中断利用流程。
我构思了一种可行的解决方案,用于识别和利用特定内核对象之间的差异行为。对于管理对象,在尝试从 _WNF_STATE_DATA 区域读取数据时,我们调用 NtQueryWnfStateData 并传入指定长度。若该长度超过实际的 DataSize,系统将返回 NTSTATUS 错误码 0xC0000023(缓冲区太小)。而对于工作对象,在创建 _TOKEN 对象后,其结构中包含一个唯一的 LUID 值,称为 TokenId。我们可以通过使用 TokenStatistics 作为 TokenInformationClass 调用 NtQueryInformationToken 来获取此值,并在喷洒大量 _TOKEN 对象时记录这些 LUID 到数组中以供后续匹配。
由于 _WNF_NAME_INSTANCES 结构未被破坏,我们可以正常调用 NtUpdateWnfStateData 和 NtQueryWnfStateData 进行状态更新与查询操作。
[此处为图片1]
在之前的阶段2中,我已经对部分 _WNF_STATE_DATA 对象进行了破坏,并将其 DataSize 字段修改为 0x1000。现在,我可以使用长度参数为 0x1000 的 NtQueryWnfStateData 调用来探测哪些对象已被破坏。如果对象未受损,函数会因请求长度过大而返回 0xC0000023;而如果对象已损坏,则会泄露越界内存数据。通过分析这些泄露的数据,我可以定位到最后一个被破坏的页面,以及与其相邻的、仍保持完整的正常页面。
这种越界读取不会进一步破坏对象结构,因此是安全的。只要判断返回的是错误码还是有效数据,就能区分正常与损坏的对象。若越界读取到的是可控或恶意构造的数据,则说明当前 _WNF_STATE_DATA 不位于最后一个损坏页中。利用这一机制,我能够准确找出最后一个被破坏的页面,从而确保接下来读取的是紧随其后的、含有完整 _TOKEN 对象结构的合法页面。该页面中的损坏 _WNF_STATE_DATA 即为我们所控制的管理对象。
接着,我从越界读取的结果中提取出 _TOKEN 对象内的 LUID 字段,并在先前存储的 LUID 数组中进行比对,成功匹配后即可确认对应的工作对象身份。
至此,我已经获得了管理对象的名称和工作对象的有效句柄。下一步是构建一段大小为 0x1000 的伪造数据区域,其中包含伪造的 _TOKEN 结构和 _WNF_STATE_DATA 结构。此前我已经通过合法调用 NtQueryWnfStateData 获取了真实的 _TOKEN 结构内容,只需修改其中若干关键字段,即可实现任意地址读写原语。
[此处为图片2]
阶段4:权限提升与系统修复
在获得任意读写能力之后,最初的想法是直接替换目标进程的 Token 为 SYSTEM 权限的 Token。虽然这种方法在技术上可行,但很快我发现系统变得极不稳定,容易引发崩溃。例如,当我损坏了某些 _TOKEN 对象后,一旦运行如 Process Explorer 这类工具,它会遍历所有进程的用户态句柄表,当访问到被污染的句柄时,就会触发内核异常导致蓝屏。
为了保证稳定性,我决定放弃简单粗暴的 Token 替换方式,转而采用更稳健的权限提升策略——修改当前线程的 _ETHREAD->PreviousMode 字段。将该值设为 0(即 KernelMode),可使内核在执行诸如 NtReadVirtualMemory 或 NtWriteVirtualMemory 等系统调用时,误认为调用者处于内核模式,从而绕过访问检查。这是一种成熟且广泛使用的提权手段,同时也有利于后续的漏洞修复工作,避免频繁构造假对象带来的复杂性。
具体实现上,我利用之前获得的工作对象来精准修改 _ETHREAD->PreviousMode 为 0,随后便可通过标准 NT API 实现任意内存读写,完成权限提升及必要的修复操作。
修复过程中的关键任务
1. 损坏的 _TOKEN 对象修复:
在利用过程中,部分 _TOKEN 对象遭到破坏。我通过触发异常并观察崩溃路径发现,问题根源在于 ObjectHeader 中的 ObjectType 字段被篡改。当内核引用该对象时,因类型校验失败而导致系统崩溃。为此,我从内核模块的 .data 段中提取出用于保护对象头的 Cookie 值,并据此重新计算出正确的 ObjectType,逐一对损坏的 _TOKEN 对象头进行恢复。
2. VS 池结构的修复:
这是整个修复流程中最复杂的部分。不仅对象本身被破坏,连带管理它们的 VS(Variable Size)池元数据结构也受到了影响,极易引发意外蓝屏。经过深入逆向分析 VS 内存分配器的行为,我发现其内部使用一棵红黑树(RBTree)来追踪所有活跃的 VS 池块。若能获知任一合法 VS 池地址,便可反推出 VS 池管理器的基址。
每当有新的 VS 池分配或释放时,系统都会从管理器出发,遍历 RBTree。如果某个节点指向已被破坏的池块地址,那么在遍历过程中访问该节点就会导致系统崩溃。因此,我的解决方案是:从 RBTree 的根节点开始遍历,定位到所有可能导致崩溃的非法节点,并将其从树中移除。尽管这可能导致部分子节点内存无法回收(造成轻微内存泄漏),但相比系统宕机而言,这是完全可以接受的代价。
我首先计算出根 VS 池的位置,然后递归遍历整棵 RBTree,识别并剔除所有危险节点,确保后续内存操作的安全性。
[此处为图片3]
最终利用与执行
完成所有修复步骤后,进入最后的命令执行阶段。由于 Adobe Reader 的渲染进程运行在一个 Job 对象限制之下,无法直接创建新进程。因此,我选择将 shellcode 注入一个不受 Job 限制的浏览器进程中,并通过该进程在 C 盘根目录下写入一个可执行文件,以此完成完整的利用链闭环。
补丁分析
微软于 2022 年 2 月发布的安全更新中修复了此漏洞。修补的核心在于 NPFS 文件系统中对总长度的计算逻辑:原先使用 int 类型进行累加,存在整数溢出风险;修复后增加了检查机制,防止总大小超过 USHORT 的最大表示范围,从根本上阻断了溢出路径。
本演示展示了如何结合具有可访问安全描述符(SD)的 WNF API 接口,实现从信息泄露到权限提升的完整攻击链条。
该代码展示了如何利用查询进程令牌的安全描述符来创建一个具备访问权限的WNF(Windows Notification Facility)对象,从而在漏洞利用过程中实现池风水(Pool Feng Shui)的布局。通过精确控制内存分配与释放,攻击者可以借此构造有利的内存布局,提升 exploit 的稳定性和成功率。
[此处为图片1]
此技术常用于高级漏洞利用场景中,特别是在绕过现代操作系统的安全防护机制(如堆随机化、隔离等)时发挥关键作用。实现这一过程的核心在于对内核对象行为的深入理解以及对系统底层机制的精准操控。