CVE-2022-37969

简介

Windows通用日志文件系统驱动程序(CLFS.sys)中存在越界写漏洞,在解析精心构造的BLF文件时,BLF日志块头的SignaturesOffset字段,会导致分配Symbol时的越界写,并破坏某些对象的虚拟函数表指针,成功利用此漏洞可实现本地权限提升。

成因

zscaler已于2022年10月将细节公布出来了, 该漏洞为已在野使用的漏洞,且内容较为详细。
下图为该文章原文:

文章内容简单总结下,该漏洞问题出在Initialize函数,ResetLog函数,AllocSymbol函数与CClfsContainer结构体:

Initialize函数会根据文件内容调用ResetLog, ResetLog函数会在特定情况下对指定位置的赋值0xffffffff, 从而绕过AllocSymbol函数写位置的校验, 最终造成越界写CClfsContainer结构体的指针, 从而在关闭Handle的时候任意代码执行。

基本流程

blf文件中需要修改如下值, 其中偏移0x800为文件中修改块的偏移

地址 原始值 目标值 备注
0x868 80 79 00 00 50 00 00 00 SignatureOffset
0x9a8 68 13 00 00 30 1b 00 00 ClientContextOffset
0x1b98 f8 00 00 00 4b 11 01 00 cbSymbolZone
0x2390 00 – b8 1b 00 00 30 1b 00 00 Client Context0
0x23a0 00 – 07 fd fd c1 88 00[0x03] 00 00 00 01 Client Context1
0x2418 00 – 20 00 00 00 Client Context2

step1

其中ClientContextOffset修改后, 指向了我们的fakeClientContext, 根据已有的文档我们可以发现, 因为fakeClientContext->eState=CLFS_LOG_SHUTDOWN, 所以在Initialize函数初始化的时候根据该值, 会调用ResetLog函数,

其中在ResetLog中会将指定位置赋值为0xffffffff00000000

step2

其中SignaturesOffset修改后, 在Initialize函数初始化的时候根据该值, 将指定位置的0x2*0x3d长度的值, 展开写到固定的位置, 并在Initialize函数的结尾, 将数据回写到上述指定的位置

其中原始位置与展开位置在Initialize函数的结尾时, 分别如下
原始位置的内容如下

展开位置的内容如下

其中在展开位置0x0110即代表该块已回写完毕.
在step1中写入值0xffffffff0000的位置, 既是由SignaturesOffset展开的位置中某一项

step3

其中cbSymbolZone修改后, 会在AllocSymbol函数中使用, 并造成任意位置写,
其中0x1328即是cbSymbolZone的偏移, 0x1338既是结构体Header头的大小

通过调试, 我们可以在指定位置进行长度为0xb0的清零

该函数中同时存在校验, 其中校验判断是在如下图的范围内,
其中0x68 即是 SignaturesOffset的偏移

那么即是判断写入末尾的位置是否小于等于SignaturesOffset指定的值

在step2中修改了SignaturesOffset, 使得其值大于0xff00, 所以该函数的校验可以被绕过, 使得其可以任意写长度为0xb0\x000x70+cbSymbolZone+0x1338

step4

CloseHandle时, 会获取CClfsContainer结构体, 并根据结构体执行函数.

其中在函数中会依次执行CClfsContainer结构体中+0x18+0x08位置处的函数

其默认指向的是CClfsContainer::vftable

也就是说实际上默认调用的是RemoveRelease这两个函数

在step3中可以修改CClfsContainer结构体的指针, 我们构造两个ClfsLogFile, 并使前一个结构体越界写入到下一个结构体中, 最终我们可以控制我们要执行的函数.

step5

我们可以由step4执行任何我们可以控制的内核函数地址, 再使用一些通用的泄露内核地址的方法, 既可成功利用

Exp: https://github.com/0x79H/CVE-2022-37969

提权技术参考

PreviousMode

PreviousMode位于KTHREAD中, 若PreviousMode为0, 则可以直接使用NtWriteVirtualMemory读写内核地址

Token

Token位于Eprocess中, 指定了进程的权限

PipeAttribute

类似于Bitmap的读写源语, 当NtFsControlFile的参数FsControlCode0x110038/0x11003C时, 会对内核中的PipeAttribute->AttributeValue(+0x20)进行读写操作
不同的是这个一般用来读.

泄露内核地址

使用SystemExtendedHandleInformationSystemHandleInformation, 可以获得handle对应的kernel地址

参考ProcessHacker的实现即可, 其中Process是需要duplicate之后才能显示地址的

SeSetAccessStateGenericMapping

SeSetAccessStateGenericMapping实际上就是将poi(rdx)写到poi(poi(rcx+0x48)+0x8)

ClfsEarlierLsn

主要用处就是写rdx为0xffffffff

RtlClearBit

RtlClearBit即清除比特位


使用RtlClearBitpoi(rcx+8)rdx指定位清零

技术逻辑

原始技术逻辑

首先使用ClfsEarlierLsnSeSetAccessStateGenericMapping的组合, 将0xffffffff处的东西写到poi(poi(rcx+0x48)+0x8)
然后构造rcx+0x48位置的为我们控制的地址, 这样我们就可以吧任意内容写到任意位置了, 我们使用PipeAttribute来将其转换为任意读写源语
之后替换Token即可完成提权

1
2
3
4
5
*(QWORD*)0x00010000(+0x01000000) = {0x05000000, WHERE_TO_WRITE}
*(QWORD*)0x05000000(+0x00100000) = {random_str, _ptr_fun2, random_str, _ptr_fun1}
*(char*)0xffffffff[0x10] = {WHAT_TO_WRITE}
_ptr_fun1=ClfsEarlierLsn
_ptr_fun2=SeSetAccessStateGenericMapping

新的技术逻辑

直接使用RtlClearBitPreviousMode清零, 其中标志位rdx在调用时恰好为0x0
完成后可以直接使用NtWriteVirtualMemory, 可以把任意内容写到任意位置了, 同时也可以读任意位置了
之后替换Token即可完成提权

1
2
3
4
*(QWORD*)0x00010000(+0x01000000) = {0x05000000, WHERE_TO_WRITE}
*(QWORD*)0x05000000(+0x00100000) = {random_str, _ptr_fun2, random_str, _ptr_fun1}
_ptr_fun1=RtlClearBit
_ptr_fun2=unknow

补丁修复方式

在新版本中, 在上文的Step1时, 即直接在打开文件调用Initialize函数时, 精心构造的fakeClientContext在解析的时候对相应数据增加了两条判断

检测的是CLFS_CLIENT_CONTEXT->CLFS_NODE_ID中的cTypecbNode, 其中前者是类型, 后者是大小.

cType == CLFS_NODE_TYPE_CONTAINER_CONTEXT(0xC1FDF007)cbNode == 0x88那么直接报错退出

如上图所示, 在已修补的版本中, 其通过新增的判断条件, 返回0xc01a000d(Log service encountered a corrupted metadata file.)这一错误代码, 直接终止流程.

参考文章:

Windows CLFS Zero-Day Vulnerability CVE-2022-37969 | Zscaler
CVE-2022-37969 | Windows CLFS Zero-Day - Zscaler Blog
Unofficial Common Log File System (CLFS) Documentation (by ionescu007)
BLF.bt (by CERTCC)
ProcessHacker
PipeAttribute (by hzshang)