背景
Windows在内核中通过clfs.sys驱动来实现Common Log File System(CLFS)。由于日志文件由驱动解析以及其本身结构的复杂性,clfs.sys在这些年来出现了很多安全问题,也成为Windows Kernel中一个常见的攻击面。CVE-2022-35803是微软在2022年9月修复的CLFS本地提权漏洞,这个漏洞可以认为是CVE-2022-24481的补丁绕过。本文将从分析CVE-2022-24481开始,并介绍其不完善的补丁修复是如何导致CVE-2022-35803的出现。
CVE-2022-24481
CLFS的前置知识在公众号之前分析另外一个漏洞CVE-2022-24521[1]提及,本文将不再赘述。对于该漏洞的关键结构为_CLFS_BASE_RECORD_HEADER,结构体中rgClients和rgContainers字段用来表示存放每个客户端上下文和容器上下文的偏移的32位数组,用户可以在磁盘上做修改它们。漏洞的根本原因是缺乏对于客户端偏移的有效校验,使其能够与容器上下文重叠,因此,由CLFS.sys 引起的客户端上下文的修改将同时更改与之相邻的容器上下文。而使客户端上下文出现修改操作的函数是CClfsLogFcbPhysical::FlushMetadata:
__int64 __fastcall CClfsLogFcbPhysical::FlushMetadata(__int64 CClfsLogFcbPhy){ClfsClientContext = 0i64; v2 = CClfsBaseFile::AcquireClientContext(*(ClfsLogFcbPhyObj + 0x2A8), 0, &ClfsClientContext); if ( v2 >= 0 && (ClfsClientContext_1 = ClfsClientContext) != 0i64 ) { eState = ClfsClientContext->eState; v5 = *(ClfsLogFcbPhyObj + 0x15C); ClfsClientContext->llCreateTime = *(ClfsLogFcbPhyObj + 0x1A0); ClfsClientContext_1->llAccessTime = *(ClfsLogFcbPhyObj + 0x1A8); ClfsClientContext_1->llWriteTime = *(ClfsLogFcbPhyObj + 0x1B0); ClfsClientContext_1->lsnOwnerPage.ullOffset = *(ClfsLogFcbPhyObj + 0x538); ClfsClientContext_1->lsnArchiveTail.ullOffset = *(ClfsLogFcbPhyObj + 0x1E0); ClfsClientContext_1->lsnBase.ullOffset = *(ClfsLogFcbPhyObj + 0x1D8); ClfsClientContext_1->lsnLast.ullOffset = *(ClfsLogFcbPhyObj + 0x1E8); ClfsClientContext_1->lsnRestart.ullOffset = *(ClfsLogFcbPhyObj + 0x1F0); ClfsClientContext_1->cbFlushThreshold = *(ClfsLogFcbPhyObj + 0x16C); HIWORD(ClfsClientContext_1->cidClient) = *(ClfsLogFcbPhyObj + 0x168); v6 = eState | 8; if ( (v5 & 0x10) == 0 ) v6 = eState; ClfsClientContext_1->eState = v6;...}...}
伪代码中命名的ClfsLogFcbPhyObj变量指针是一个CClfsLogFcbPhysical对象,在CClfsLogFcbPhysical::Initialize函数中初始化,打开日志文件时调用:
__int64 __fastcall CClfsLogFcbPhysical::Initialize(){...CClfsBaseFile::AcquireClientContext(*(ClfsLogFcbPhyObj + 0x2A8), 0, &ClfsClientContext);...if ( (ClfsClientContext->eState & 0x20) == 0 || (*(*ClfsLogFcbPhyObj + 0x138i64))(ClfsLogFcbPhyObj) ) { ClfsClientContext_1 = ClfsClientContext; *(ClfsLogFcbPhyObj + 0x1A0) = ClfsClientContext->llCreateTime; *(ClfsLogFcbPhyObj + 0x1A8) = ClfsClientContext_1->llAccessTime; *(ClfsLogFcbPhyObj + 0x1B0) = ClfsClientContext_1->llWriteTime; *(ClfsLogFcbPhyObj + 0x1C8) = 0i64; *(ClfsLogFcbPhyObj + 0x538) = ClfsClientContext_1->lsnOwnerPage.ullOffset; *(ClfsLogFcbPhyObj + 0x1E0) = ClfsClientContext_1->lsnArchiveTail.ullOffset; *(ClfsLogFcbPhyObj + 0x1D8) = ClfsClientContext_1->lsnBase.ullOffset; *(ClfsLogFcbPhyObj + 0x1E8) = ClfsClientContext_1->lsnLast.ullOffset; *(ClfsLogFcbPhyObj + 0x1F0) = ClfsClientContext_1->lsnRestart.ullOffset; *(ClfsLogFcbPhyObj + 0x16C) = ClfsClientContext_1->cbFlushThreshold;...}...}
若将客户端上下文的llCreateTime字段设置为用户空间地址,并将llCreateTime字段与容器上下文的pContainer字段重叠。调用函数CClfsLogFcbPhysical::FlushMetadata后,容器上下文的pContainer字段(该字段存储指向CClfsContainer对象的内核指针)将被修改为用户空间地址。
FlushMetadata函数可能会在处理日志的多个时期触发,其中包括在关闭日志文件时,在执行完FlushMetadata后,clfs.sys会调用函数CClfsLogFcbPhysical::CloseContainers来关闭容器:
__int64 __fastcall CClfsLogFcbPhysical::CloseContainers(__int64 ClfsLogFcbPhyObj){ ClfsContainerContext = 0i64; v1 = 0; v2 = *(ClfsLogFcbPhyObj + 0x554); if ( v2 >= *(ClfsLogFcbPhyObj + 0x550) ) return v1; while ( 1 ) { v1 = CClfsBaseFile::AcquireContainerContext( *(ClfsLogFcbPhyObj + 0x2A8), *(ClfsLogFcbPhyObj + 4i64 * (v2 & 0x3FF) + 1368), &ClfsContainerContext); if ( v1 < 0 ) break; v4 = ClfsContainerContext; if ( !ClfsContainerContext ) break; pContainer = ClfsContainerContext->pContainer; <<-------- 2 if ( pContainer ) { CClfsContainer::Close(pContainer); <<-------- 3 (*(*v4->pContainer + 8i64))(v4->pContainer); v4->pContainer = 0i64; } CClfsBaseFile::ReleaseContainerContext(*(ClfsLogFcbPhyObj + 0x2A8), &ClfsContainerContext); if ( ++v2 >= *(ClfsLogFcbPhyObj + 0x550) ) return v1; } return 0xC01A000Di64;}
在[2]之后,pContainer将指向攻击者控制的内存空间(可位于用户空间中)。当调用CClfsContainer::Close[3]时,会在内部触发函数ObfDereferenceObject,这将导致任意地址递减。通过在用户空间伪造pContainer可以将PreviousMode减少为零,这将导致调用线程从UserMode切换到为KernelMode,使用户可以通过NtWriteVirtualMemory/NtReadVirtualMemory函数实现内核地址空间任意读写。接着用户可以通过将当前进程_EPROCESS对象中的token字段覆盖为System进程_EPROCESS对象中token的地址(可以通过NtQuerySystemInformation获取这些地址),实现权限提升。
补丁分析
通过diff工具可以寻找到漏洞补丁在CClfsBaseFile::ValidateRgOffsets函数中加入了对rgClients和rgContainers偏移的检查:
__int64 __fastcall CClfsBaseFile::ValidateRgOffsets(CClfsBaseFile *this, unsigned int *rgObject){ v2 = 0; v3 = 0; v6 = 0; extraNum = 0; logBlockPtr = *(*(this + 6) + 0x30i64); // * _CLFS_LOG_BLOCK_HEADER if ( !logBlockPtr ) return 0xC01A000Di64; signOffset = logBlockPtr + *(logBlockPtr + 0x68); if ( signOffset < logBlockPtr ) return 0xC01A000Di64; qsort(rgOffsetArray, 0x47Cui64, 4ui64, CompareOffsets); while ( 1 ) { currentOffset = *rgOffsetArray; if ( *rgOffsetArray - 1 <= 0xFFFFFFFD ) { currentContext = CClfsBaseFile::OffsetToAddr(this); if ( !currentContext ) break; if ( currentOffset < 0x30 ) break; v12 = currentOffset - 0x30; v13 = extraNum * 4 + v3 + 0x30; if ( v13 < v3 || v3 && v13 > v12 ) <<-------- 4 break; v3 = v12; if ( *currentContext == 0xC1FDF008 ) // CLFS_NODE_TYPE_CONTAINER_CONTEXT { extraNum = 12; } else { if ( *currentContext != 0xC1FDF007 ) // CLFS_NODE_TYPE_CLIENT_CONTEXT return 0xC01A000D; extraNum = 34; } v1 = ¤tContext[extraNum]; if ( v1 < currentContext || v1 > signOffset ) break; } ++v6; ++rgOffsetArray; if ( v6 >= 0x47C ) return v2; } return 0xC01A000D;}
该补丁在[4]处引入了一个重要检测,这要求容器上下文与其相邻的下一个上下文间隔至少为0x30+12*4=0x60,客户端上下文与其相邻的下一个上下文间隔至少为0x30+34*4=0xb8,而在CClfsLogFcbPhysical::FlushMetadata函数中,最多只能修改偏移量为0x78字节的字段(ClfsClientContext->eState),所以直接通过修改客户端上下文偏移这种办法被修复了。
CVE-2022-35803
通过补丁分析可以看出,直接修改客户端上下文偏移的问题已经修复,但是否有另一种方法可以重叠字段并导致漏洞?
研究发现,从ValidateRgOffsets验证偏移到FlushMetadata修改数据,再到最后CloseContainers关闭容器期间都未对客户端上下文的类型值cidNode.cType字段(其用于表示上下文的类型)进行合法校验,这里存在可能被类型混淆的缺陷,如果将客户端上下文cidNode.cType修改为0xC1FDF008,那么在ValidateRgOffsets计算上下文间隔时,会错误地将客户端上下文与下一个上下文间隔限制为0x60,这将导致客户端上下文0x60~0x88偏移的区域与下一个上下文0~0x28偏移的区域重叠,新的漏洞出现,这便是CVE-2022-35803。
将客户端上下文的eState字段与容器上下文的pContainer字段重叠,然后触发FlushMetadata函数,便可将该字段进行或运算并重新存储:
__int64 __fastcall CClfsLogFcbPhysical::FlushMetadata(__int64 CClfsLogFcbPhy){ ClfsClientContext = 0i64; v2 = CClfsBaseFile::AcquireClientContext(*(ClfsLogFcbPhyObj + 0x2A8), 0, &ClfsClientContext); if ( v2 >= 0 && (ClfsClientContext_1 = ClfsClientContext) != 0i64 ) { eState = ClfsClientContext->eState; v5 = *(ClfsLogFcbPhyObj + 0x15C); ... v6 = eState | 8; if ( (v5 & 0x10) == 0 ) v6 = eState; ClfsClientContext_1->eState = v6; ... } ...}
由于原pContainer以0x10对齐,这将导致pContainer字段指向原Container对象向后偏移8的位置,而这个位置刚好存放着容器文件大小值,这个大小是由用户调用AddLogContainer函数时通过参数pcbContainer指定:
[in, optional] pcbContainer
The optional parameter that specifies the size of the container, in bytes.
The minimum size is 512 KB for normal logs and 1024 KB for multiplexed logs. The maximum size is approximately 4 gigabytes.
当数据被认为是地址时可以位于用户空间,完全可以利用此漏洞在用户空间伪造出容器对象的虚函数表,如下图所示:
混淆数据之前:
混淆数据之后:
这样通过合理构造日志文件便可以在内核空间调用任意函数,从而实现权限提升。
CVE-2022-35803补丁分析
在CVE-2022-35803的补丁中,在CClfsBaseFile::GetSymbol中加入了对上下文cType字段的校验,避免了此类类型混淆问题的发生:
__int64 __fastcall CClfsBaseFile::GetSymbol( CClfsBaseFile *this, unsigned int a2, char ClientContextId, struct _CLFS_CLIENT_CONTEXT **a4){...v12 = ClfsQuadAlign(0x88u); if ( *(_DWORD *)(ClientContextAddr - 16) != (unsigned __int64)(ClientContextOffset_1 + v12) || *(_DWORD *)ClientContextAddr != 0xC1FDF007 || *(_DWORD *)(ClientContextAddr + 4) != v12 || *(_BYTE *)(ClientContextAddr + 8) != ClientContextId ) {LABEL_12: v8 = 0xC01A000D; goto LABEL_13; }...}
参考
[1]https://mp.weixin.qq.com/s/Uk0NocnYXMouwxz6OsuKUA
绿盟科技天元实验室专注于新型实战化攻防对抗技术研究。
研究目标包括:漏洞利用技术、防御绕过技术、攻击隐匿技术、攻击持久化技术等蓝军技术,以及攻击技战术、攻击框架的研究。涵盖Web安全、终端安全、AD安全、云安全等多个技术领域的攻击技术研究,以及工业互联网、车联网等业务场景的攻击技术研究。通过研究攻击对抗技术,从攻击视角提供识别风险的方法和手段,为威胁对抗提供决策支撑。
M01N Team公众号
聚焦高级攻防对抗热点技术
绿盟科技蓝军技术研究战队
官方攻防交流群
网络安全一手资讯
攻防技术答疑解惑
扫码加好友即可拉群
还没有评论,来说两句吧...