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

本文详细分析了Windows 11中CimFS驱动程序的漏洞发现与利用过程,包括权限绕过、模糊测试策略、多个内核漏洞的发现,以及最终实现从管理员到内核权限提升的完整攻击链。

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

引言

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

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

开始

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)

所有者 : 组 : 自由访问控制列表 : {NT AUTHORITY\SYSTEM: AccessAllowed (GenericAll), BUILTIN\Administrators: AccessAllowed (GenericAll)} 系统访问控制列表 : {} 原始描述符 : System.Security.AccessControl.CommonSecurityDescriptor

这意味着驱动程序可能不打算暴露给非特权客户端。 然而,在设备创建期间未设置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中存储元数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
VolumeDeviceObject_1->Extension.ChildOnly = MountImageFlags & 1;
VolumeDeviceObject_1->Extension.DirectAccess = (MountImageFlags & 2) != 0;
VolumeDeviceObject_1->DeviceObject.StackSize = ModifiedStackSize + 1;
VolumeDeviceObject_1->DeviceObject.Flags |= ModifiedFlags;
VolumeDeviceObject_1->DeviceObject.Flags &= 0xFFFFFF7F;// &~ DO_DEVICE_INITIALIZING
VolumeDeviceObjectRef1 = VolumeDeviceObject_1;
VolumeDeviceObject_1 = 0LL;
DiskDeviceObject->Extension.VolumeDevice = VolumeDeviceObjectRef1;
DiskDeviceObject->DeviceObject.Flags &= 0xFFFFFF7F;// &~ DO_DEVICE_INITIALIZING
DiskDeviceObject = 0LL;
StackSize = DeviceObject->StackSize;
if ( (char)(ModifiedStackSize + 1) >= StackSize )
  StackSize = ModifiedStackSize + 1;
DeviceObject->StackSize = StackSize;
v85 = CimFs::NotifyMountManager(&outputDeviceNameWchar, 1);

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

例如:

1
2
3
4
5
6
7
8
CimManualMountImage(ImageContainingPath, ImageName, MountImageFlags, VolumeId);

StringFromGUID2(VolumeId, &volumeIdString, GUID_BUFFER_SIZE_WCHAR);
wsprintfW(&mountedVolumeRoot, L"\\\\?\\Volume%s\\", volumeIdString);

// Attempt to create a handle to the file1(hardlink ADS)
wsprintfW(commonPathBuffer, L"%s%s", mountedVolumeRoot, Name1);
hFile = FsOpenReadonlyFile(commonPathBuffer);

我们现在可以像普通文件一样使用句柄hFile。所有文件操作将沿文件系统堆栈转发,直到到达挂载期间创建的卷设备。cimfs通过检查设备扩展来检测它是卷设备,然后通过解析区域文件完成文件系统请求。

攻击计划

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

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

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

主要逻辑如下:

  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
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Create initial corpus files
FuzzerCreateInitialCorpus(ATTRIBUTES_COPY_FILE, FILESYSTEM_FILE_NAME, FILESYSTEM_HARDLINK, FILESYSTEM_FILE_ADS, IMAGE_CONTAINING_PATH, IMAGE_NAME);

// Generate volume GUID for mount
UuidCreate(&volumeId);

// Read initial corpus into memory

// Start by reading on disk headers in cim file to obtain region GUID
hCimfile = FsOpenReadonlyFile(initialCimFile);
if (hCimfile == INVALID_HANDLE_VALUE) {
    FATAL("[-] main fail: FsOpenReadonlyFile(0x%08X)\n", GetLastError());
}
if (!CimGetRegionGUID(hCimfile, &regionId)) {
    FATAL("[-] main fail: CimGetRegionGUID(0x%08X)\n", GetLastError());
}
CloseHandle(hCimfile);

// Now load corpus
StringFromGUID2(&regionId, &regionIdString, GUID_BUFFER_SIZE_WCHAR);
// Remove braces {}
regionIdString[37] = 0;
wsprintfW(initialCorpusPath, IMAGE_CONTAINING_PATH L"region_%s_0", &regionIdString[1]);
FuzzerLoadCorpusInMem(initialCorpusPath);

// Dry run
status = FuzzerTryMountAndCreateHandles(IMAGE_CONTAINING_PATH, IMAGE_NAME, CIM_MOUNT_CHILD_ONLY, &volumeId, FILESYSTEM_HARDLINK_ADS, FILESYSTEM_HARDLINK, &hHardlinkAds, &hFile);
if (!status) {
    FATAL("[-] Dry run fail: FuzzerTryMountAndCreateHandles(0x%08X)\n", GetLastError());
}
FuzzerQueryHandle(hFile);
FuzzerQueryHandle(hHardlinkAds);
puts("[+] Dry run success");

// Dry run cleanup
CloseHandle(hFile);
CloseHandle(hHardlinkAds);
CimDismountImage(&volumeId);

for (LARGE_INTEGER effectiveOffset = { 0 } ;;) {
    MutatorMutate(&from);
    nullOrMutate = MutatorGetRandomOffset(1, 10);

    // Count from end
    // We don't use SEEK_END because we want to compute effectiveOffset right now to check whether it's mutable
    if (from & 1) {
        curOffset = MutatorGetRandomOffset(0, EFFECTIVE_FUZZ_BACK);
        effectiveOffset.QuadPart = maxOffset - curOffset;
    }
    else {
        curOffset = MutatorGetRandomOffset(0, EFFECTIVE_FUZZ_FRONT);
        effectiveOffset.QuadPart = FUZZ_FRONT_START + curOffset;
    }

    // Checks on current offset
    if (BitmapIsUntouchable(effectiveOffset.QuadPart))
        continue;

    // Open file and set to proper offset to mutate
    hRegionFile = FsOpenWriteonlyFile(initialCorpusPath);
    if (GetLastError() == 0x20) {
        // Sometimes dismount operation takes a while
        Sleep(5000);
        hRegionFile = FsOpenWriteonlyFile(initialCorpusPath);
    }
    if (hRegionFile == INVALID_HANDLE_VALUE) {
        FATAL("[-] Write sample fail: FsOpenWriteonlyFile(0x%08X)\n", GetLastError());
    }

    status = SetFilePointerEx(hRegionFile, effectiveOffset, NULL, FILE_BEGIN);
    if (!status) {
        FATAL("[-] Write sample fail: SetFilePointerEx(0x%08X)\n", GetLastError());
    }

    // Write mutated byte
    if (nullOrMutate > 8)
        mutated = 0; // 20% chance of nulling
    else
        MutatorMutate(&mutated);

    status = WriteFile(hRegionFile, &mutated, sizeof(BYTE), &written, NULL);
    if (!status) {
        FATAL("[-] Write sample fail: WriteFile(0x%08X)\n", GetLastError());
    }
    CloseHandle(hRegionFile);

    printf("[*] Mutated BYTE <%llu> from <0x%hhx> to <0x%hhx>\n", effectiveOffset.QuadPart, initialCorpusInMem[effectiveOffset.QuadPart], mutated);

    // Verify mutation doesn't affect mount and create
    status = FuzzerTryMountAndCreateHandles(IMAGE_CONTAINING_PATH, IMAGE_NAME, CIM_MOUNT_IMAGE_NONE, &volumeId, FILESYSTEM_HARDLINK_ADS, FILESYSTEM_FILE_NAME, &hHardlinkAds, &hFile);
    if (!status) {
        // Mutation caused either mount or create to fail

        printf("[*] Untouchable BYTE: <%llu>\n", effectiveOffset.QuadPart);
        // Make sure to not mutate in future
        BitmapSetUntouchable(effectiveOffset.QuadPart);
        // Revert mutation
        hRegionFile = FsOpenWriteonlyFile(initialCorpusPath);
        if (GetLastError() == 0x20) {
            // Sometimes dismount operation takes a while
            Sleep(5000);
            hRegionFile = FsOpenWriteonlyFile(initialCorpusPath);
        }
        if (hRegionFile == INVALID_HANDLE_VALUE) {
            FATAL("[-] Write sample fail: FsOpenWriteonlyFile(0x%08X)\n", GetLastError());
        }

        mutated = initialCorpusInMem[effectiveOffset.QuadPart];

        status = SetFilePointerEx(hRegionFile, effectiveOffset, NULL, FILE_BEGIN);
        if (!status) {
            FATAL("[-] Write sample fail: SetFilePointerEx(0x%08X)\n", GetLastError());
        }

        status = WriteFile(hRegionFile, &mutated, sizeof(BYTE), &written, NULL);
        if (!status) {
            FATAL("[-] Write sample fail: WriteFile(0x%08X)\n", GetLastError());
        }
        CloseHandle(hRegionFile);

        continue;
    }

    // Perform filesystem query on the two handles to fuzz
    FuzzerQueryHandle(hFile);
    FuzzerQueryHandle(hHardlinkAds);
    // Finally, cleanup
    CloseHandle(hFile);
    CloseHandle(hHardlinkAds);
    CimDismountImage(&volumeId);
}

out:
    CimDismountImage(&volumeId);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Create main CIM image
hs = CimCreateImage(ImageContainingPath, NULL, ImageName, &hImage);
if (hs != S_OK) {
    FATAL("[-] Create corpus fail: CimCreateImage(0x%08X)\n", hs);
}
wprintf(L"[+] Created CIM file <%s> at %s\n", ImageName, ImageContainingPath);

// Create a filesystem file with filled attributes for maximum entropy

// First open a dummy file to Retrieve attributes
// Lazy to code so use explorer to set bunch of attributes
hAttributesFile = FsOpenReadonlyFile(AttributesFile);
if (hAttributesFile == INVALID_HANDLE_VALUE) {
    FATAL("[-] Create corpus fail: FsOpenReadonlyFile(0x%08X)\n", GetLastError());
}

// Set basic info
status = FsGetBasicFileInfo(hAttributesFile, &attributesInfo);
if (!status) {
    FATAL("[-] Create corpus fail: FsGetBasicFileInfo(0x%08X)\n", GetLastError());
}
// Add some random attributes
metadata.Attributes = FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_EA | FILE_ATT
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计