如今,许多漏洞报告都专注于软件漏洞的利用过程。“漏洞开发者”这个术语仍然与漏洞研究同义使用,这可能源于 2000 年代早期,当时漏洞很容易被发现,而社区才刚刚开始探索利用艺术。然而,如今随着 SDL(安全开发生命周期)和持续模糊测试,在关键系统中发现未知漏洞变得越来越重要,可以说比利用过程更重要。
本文中提到的漏洞已在 2024 年 3 月更新中作为 CVE-2024-26170修补。这些漏洞是通过简单地限制非特权用户对驱动程序的访问来修补的,因此无法通过补丁差异化来识别。
cimfs.sys,复合图像文件系统驱动程序。这个驱动程序是 Windows 11 默认安装的一部分,在这之前没有任何已知漏洞。
关于复合图像文件格式 (CIM) 的少量信息:
CIM 是一种基于文件的镜像格式,在概念上类似于 WIM。
CIM 格式由一小集合平面文件组成,包括一个或多个数据和元数据区域文件、一个或多个对象 ID 文件以及一个或多个文件系统描述文件。由于它们的“平面性”,CIM 的构建、提取和删除比它们包含的等效原始目录更快。
CIM 是复合的,因为给定的镜像可以包含多个文件系统卷,这些卷可以单独挂载,同时共享相同的数据区域备份文件。
一旦构建,CIM 可以使用 CimFS 驱动程序的支持来挂载。挂载会为镜像构建一个只读磁盘和文件系统卷设备。已挂载 CIM 的内容可以使用标准的 Win32 或 NT API 文件系统接口以只读方式访问。CimFS 文件系统支持 NTFS 的许多结构,例如安全描述符、备用数据流、硬链接和重新解析点。
TLDR:另一个可以通过 Win32 API 挂载和读取的文件系统。文件请求将由驱动程序 cimfs.sys处理,以模拟一个只读文件系统。
该驱动程序暴露了一个控制设备对象 \Device\cimfs\control,以促进新 CimFS 卷的创建。用户模式客户端可以通过发出 IOCTL 来与控制设备交互。例如,IOCTL 代码 0x220004 用于挂载新 CimFS 卷。
switch ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode )
{
case 0x220004u: // 挂载卷
...
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 *)®ionSetBuf,
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)
Owner :
Group :
DiscretionaryAcl : {NT AUTHORITY\SYSTEM: AccessAllowed (GenericAll), BUILTIN\Administrators: AccessAllowed
(GenericAll)}
SystemAcl : {}
RawDescriptor : System.Security.AccessControl.CommonSecurityDescriptor
这意味着该驱动程序可能不打算暴露给非特权客户端。
然而,在设备创建期间未设置 FILE_DEVICE_SECURE_OPEN标志,这允许非特权用户通过简单地将它视为文件系统驱动程序来打开句柄并向控制设备发出 IOCTL。
hDevice = CreateFileW(
L"\\??\\CimfsControl\\something",
0,
0,
NULL,
OPEN_EXISTING,
0,
NULL
);
逻辑是,我们无法直接打开根设备 \\??\\CimfsControl,因为对象上的 DACL 设置了,但这不会传播到根设备下的任何子设备,例如 \\??\\CimfsControl\\abcdef。所有请求反正都会由控制设备处理,这允许我们绕过认证并打开攻击面。
在决定攻击计划之前,我们需要探索 cimfs.sys的工作原理,从挂载操作开始。幸运的是,cimfs 附带了一个用户模式伴侣 DLL cimfs.dll,其函数已记录。DLL 不知道认证绕过,因此我们必须修补 DLL 或手动发出调用到驱动程序。
通过反转伴侣 DLL,我们能够恢复手动调用挂载的参数:
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 中
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 进行交互。
例如:
CimManu