拒绝模糊测试:Windows内核中的Rust组件
摘要
Check Point Research(CPR)在2025年1月发现了一个影响Windows中新的基于Rust的图形设备接口(GDI)内核组件的安全漏洞。我们及时向微软报告了此问题,他们在2025年5月28日发布的KB5058499更新预览版中从OS Build 26100.4202开始修复了该漏洞。在以下部分中,我们详细介绍了针对Windows图形组件通过元文件进行的模糊测试活动方法,该方法导致了此安全漏洞的发现。
元文件模糊测试
测试环境
模糊测试是一种广泛使用的软件测试技术,涉及向被测试程序提供无效、意外或随机数据以识别错误。模糊测试是我们主动安全测试方法的关键部分,我们定期将其应用于广泛使用的系统,如微软的Windows操作系统,以便在恶意行为者利用之前识别和解决潜在漏洞。
可靠的测试环境对于此类工作至关重要。WinAFL是一款功能强大的模糊测试工具,以其多年来识别众多公开承认的漏洞而闻名,并专门针对Windows二进制文件进行了适配。为了有效进行中等规模的模糊测试,我们需要使用像WinAFL Pet这样的管理工具。这些工具简化了模糊测试作业的创建、配置和监控,同时简化了对应用程序中检测到的任何崩溃的评估。
目标选择
GDI是Windows操作系统中著名的核心组件,提供二维矢量图形、成像和排版功能。它通过引入新功能和优化现有功能,增强了早期Windows版本中的图形设备接口。
增强型元文件格式(EMF)包含调用GDI函数的指令。增强型元文件格式Plus(EMF+)是EMF元文件的一种变体,其中EMF+记录嵌入在EMF记录中。这种嵌入之所以成为可能,是因为EMF能够在EMR_COMMENT_EMFPLUS记录中包含任意私有数据。此外,多个EMF+记录可以嵌入到单个EMF记录中。
EMF文件代表了重要的攻击面。由于其紧凑的文件大小,这些文件特别适合模糊测试。虽然EMF文件过去曾是多个漏洞披露的焦点,但向EMF+格式的转变研究分析较少。EMF+格式引入了各种新的元文件记录,增加了处理这些文件的复杂性。
测试执行
我们启动了一个模糊测试活动,初始种子文件语料库仅包含16个文件,其中包括几个基于EMF+格式的样本。在仅仅几天的测试中,模糊测试器就识别了几个潜在的安全漏洞,其可能影响范围从信息泄露到任意代码执行。在模糊测试活动期间,我们遇到了反复的系统崩溃——我们称之为"拒绝模糊测试"条件——这干扰了我们的研究并导致了意外的发现。
经过一周的测试,测试系统由于BugCheck而崩溃并重启。这表明模糊测试器遇到了影响Windows内核的错误。鉴于我们的主要焦点是用户空间模糊测试,在这种情况下没有直接的方法可用于重现崩溃。然而,重新启动测试活动导致了相同的结果:系统在几天测试后再次崩溃,确认了Windows内核中存在由初始种子语料库的广泛突变触发的错误。
追踪BugCheck
准备工作
这使我们的焦点从发现额外漏洞转向追踪Windows内核中的这个特定错误并一致地重现崩溃。为了实现这一点,我们的第一步是启用内存转储捕获,以便我们可以分析崩溃时操作系统的状态。然而,由于我们使用RAM磁盘进行文件存储,并且模糊测试器实例运行在共享内存模式下(通过WinAFL中的-s选项启用)以提高测试速度,确定崩溃时正在处理哪个样本仍然是一个挑战,就像大海捞针一样。
重新启动模糊测试活动确认系统崩溃在几天测试后持续发生,并允许我们收集多个内存快照来分析崩溃模糊测试器实例处理的突变样本。
接近目标
尽管取得了进展,我们仍然无法随意重现错误。最终,我们实现了一个设置,其中单个模糊测试器实例可以在30分钟内使用836个文件的数据集触发导致错误的突变。这一进展使我们能够修改模糊测试工具,将突变的测试文件通过网络传输到远程服务器。
这种方法的主要目标是对模糊测试器的影响最小,并确保不会对性能或稳定性产生负面影响。为了实现这一点,我们为工具添加了以下send_data()函数,旨在将每个测试的样本传输到远程服务器。建立连接后,函数发送数据大小,然后是实际数据,在每个步骤处理任何潜在错误,必要时进行清理并返回错误代码。
|
|
清单1. 客户端修改,将每个突变发送到服务器
在服务器端,以下Python脚本主动监听传入连接。每个连接在单独的线程中处理,脚本从模糊测试工具接收样本并将其保存为单独的文件。收集5,000个文件后,脚本将它们压缩成ZIP存档并删除原始文件以优化存储使用。
|
|
清单2. 服务器端Python脚本,用于捕获工具发送的突变样本
这种修改使我们能够重现错误,即使它是由处理多个不同测试文件引起的。在更新模糊测试工具后的第一次测试运行中,系统在30分钟后成功崩溃。根据服务器上记录的样本文件,该活动在令人印象深刻的380,000次突变后达到了负责生成导致崩溃文件的突变。
目标
问题出现时
我们通过系统服务异常发现了一个崩溃,该异常发生在win32kbase_rs.sys驱动程序版本10.0.26100.3037中执行NtGdiSelectClipPath系统调用期间。此崩溃的堆栈跟踪显示了微软通过使用Rust在Windows内核中重新实现GDI子系统的REGION数据类型来增强安全的努力,正如BlueHat IL 2023会议上关于默认安全的演示所讨论的那样。这种转变在win32kbase.sys驱动程序中的Win32kRS::RegionCore_set_from_path()函数如何调用新的win32kbase_rs.sys驱动程序中具有相同名称的函数中显而易见。
值得注意的是,系统崩溃是由这个旨在提高安全性的新内核组件触发的,正如堆栈跟踪中引用的panic_bounds_check()函数的名称所暗示的那样。
根据易受攻击版本的win32kbase_rs.sys内核驱动程序的反编译源代码,当v109索引超过允许的v86范围时系统崩溃,触发内核恐慌。这个问题很可能是由v88和v95循环变量在没有适当保护措施的情况下递增超出有效限制引起的。
错误背后的原因
当region_from_path_mut()函数将路径转换为区域时,它将轮廓表示为边块的单向链表。程序通过core::panicking::panic_bounds_check()检测到越界内存访问并触发SYSTEM_SERVICE_EXCEPTION。
崩溃样本中驱动执行流进入region_from_path_mut()函数的第一个元文件记录是EmfPlusDrawBeziers记录。由此记录产生的路径几何图形产生了负责内存损坏的特定边块。当系统播放此记录时,EmfPlusObject记录中指定的几何笔使笔触变宽。当路径被加宽、展平并转换为内核中的区域时,畸形路径数据最终导致越界条件。
以下是指定要使用的笔的EmfPlusObject的相关摘录。如果设置了PenDataStartCap,则在EmfPlusPenData对象的OptionalData字段中指定起始线帽的样式。类似地,PenDataNonCenter指示是否在EmfPlusPenData对象的OptionalData字段中指定笔对齐方式。
|
|
清单3. 定义Pen对象的元文件记录
以下结构显示了触发漏洞的EmfPlusDrawBeziers记录。它包含通过突变产生的值,包括17个点,尽管声明只有4个标称计数,这在处理过程中被忽略。这种不匹配,连同坐标数据,似乎足以给路径解析逻辑带来压力,并暴露内核中的这种边缘情况行为。
|
|
清单4. 定义具有17个绝对点的贝塞尔曲线的元文件记录
漏洞演示
额外的分析表明,当Metafile对象被传递给Graphics::FromImage()以创建Graphics对象时,边界检查特别失败,尽管该方法被记录为仅接受用于绘图的Image对象,如Bitmap。这种误用使得执行能够到达易受攻击的代码路径。通过在从Metafile创建的Graphics对象上调用DrawImage()方法,可以触发产生的BugCheck。
以下PowerShell脚本在$b变量中嵌入了一个元文件,其中包含一个特别制作的EmfPlusDrawBeziers记录,具有畸形的边数据。这种方法在标准用户会话中的低完整性级别下工作,并影响x86和x64系统,因为易受攻击的例程位于win32kbase_rs.sys驱动程序中。
|
|
清单5. 用于重现漏洞的概念验证PowerShell脚本
所示的概念验证元文件只有在到达内核的边块产生特定路径几何图形时才能触发崩溃。以下是三个独立的记录级编辑,可以阻止该布局形成,因此不会执行有错误的代码路径:
- 翻转C/P标志,使PointData字段作为EmfPlusPointF对象数组读取:
$b[0x15f]=0; - 增加Size以添加额外的平面点:
$b[0x160]=84;$b=$b[0..351]+(0,0,0,0)+$b[352..($b.Length-1)]; - 将DataSize减少到64以删除最后一个点:
$b[0x164]=64;
这个问题因易受攻击的win32kbase_rs.sys组件在Windows Server版本上不存在而得到缓解。我们向微软报告了此漏洞,但MSRC将其分类为中等严重性,表示不需要立即服务。然而,他们在2025年5月补丁星期二之后的几周内作为功能更新的一部分修复了此漏洞。根据他们的评估:
“Rust代码正确捕获了越界数组访问并触发了恐慌,导致蓝屏死机(BSOD),正如预期的那样”
然而,观察到的行为与更广泛的漏洞定义一致。通过处理用户控制输入的用户空间函数触发BSOD应被视为需要安全修复的漏洞。失败的安全检查不应导致系统崩溃。
更重要的是,正如MSRC所确认的,威胁行为者可以利用此缺陷创建恶意元文件,当显示元文件时旨在崩溃目标系统。此类中断可能使企业环境暂时无法操作,导致意外停机并干扰关键业务流程。此外,还可能有数据丢失或损坏。
微软如何修复漏洞
微软确定这是一个中等严重性的拒绝服务问题,因此选择在非安全更新中解决它。根据微软的说法,修复程序首先在2025年5月28日发布的win32kbase_rs.sys内核模块版本10.0.26100.4202中随KB5058499一起提供,并在6月23日当周达到全球全面发布状态。值得注意的是,版本4202引入了对模块的重大更新,反映在文件大小从148 KB增加到164 KB,表明可能与漏洞修复相关的重大内部更改。
此更新中最重大修改的组件之一是region_from_path_mut()函数,该函数进行了一些重构。
其中最显著的变化是引入了两个不同的边处理例程:add_edge_original()和add_edge_new()。将两个顶点转换为边记录并将其插入内存边表的GlobalEdgeTable::add_edge()函数现在以这两种形式存在。微软保留了原始逻辑作为add_edge_original(),并实现了一个新的、边界强化的版本称为add_edge_new()。虽然两种实现产生相同的功能输出,但新版本解决了旧例程中存在的几个角落情况和潜在内存处理问题。
功能标志Feature_Servicing_Win32kRSPathToRegion_IsEnabled()在运行时确定调用哪个版本。尽管修复程序已经存在于代码库中,但我们在初始测试期间发现此功能标志被禁用。我们只能在调试器中确认修复程序的存在,并且只能在2025年7月补丁星期二之后在生产中稍后验证修复程序。
结论
我们在最近发布的Windows图形组件的Rust代码中发现了一个安全漏洞,可能对系统安全产生严重影响。虽然Rust在Windows内核中的采用标志着安全和可靠性的重要一步,但重要的是要认识到软件工程挑战不能仅通过语言选择来克服。Rust提供了关于内存安全和类型正确性的强大保证,有助于防止整个类别的错误,如缓冲区溢出和无效指针解引用。然而,严格的安全测试和周密的软件设计仍然至关重要,因为问题仍然可能出现。
在Rust中实现GDI区域和相关函数的情况下,失败的安全检查触发了故意的内核恐慌,也称为蓝屏死机(BSOD)。虽然这次崩溃最初是作为安全机制设计的,例如在出现问题时作为紧急停止,但这突显了一个更大的担忧。一个恰当的类比可能是家庭报警系统通过炸毁房屋来阻止窃贼。虽然威胁在技术上被中和了,但附带损害过于昂贵。
我们应该旨在保护而不冒系统范围故障风险的安全解决方案。还值得记住的是,这些问题并非Rust独有,而是可能发生在任何其他软件项目中。因此,虽然看到使用内存安全语言重写操作系统关键部分的这一步令人鼓舞,但这个例子也必须提醒人们注意所涉及的困难以及使用极其彻底的工程标准和原则的必要性。即使严格的标准也不能保证一帆风顺。我们仍然应该预期会遇到意外的错误和漏洞。
我们的发现可能构成了Rust集成到Windows内核后第一个公开披露的涉及基于Rust的内核组件的安全问题。此后不久,我们确定了一个漏洞,可能允许攻击者使用特别制作的元文件崩溃Windows 11版本24H2,如一行概念验证PowerShell脚本所演示的那样。问题仍然是我们是否会在内核中继续看到更多这样的错误。