复现环境:Windows 10 19041.1766
简介
Windows通用日志文件系统驱动程序(CLFS.sys
)是一个Windows内核组件,用于管理日志文件。在Windows系统中,日志文件是记录系统事件和错误信息的关键组成部分。CVE-2022-37969通过构造BLF文件利用越界写(OOB)漏洞:BLF日志块头的SignaturesOffset字段在分配Symbol时可导致越界写,并破坏某些对象的虚拟函数表指针。攻击者可利用此漏洞来实现本地权限提升。
文件格式简介
CLFS的元数据块总数默认为6个,也就是如下的元数据类型
数据块类型 |
元数据块类型 |
描述 |
Control Record |
Control Metadata Block |
包含了有关布局(layout)、扩展(extend)区域以及截断(truncate)区域的信息 |
Base Record |
General Metadata Block |
包含了符号表信息,其中包括该BLF有关的客户端、容器和安全上下文信息 |
Truncate Record |
Scratch Metadata Block |
包含了因为截断操作而需要对扇区进行更改的客户端信息,以及具体更改的扇区字节. |
另外三个实际上是上面三个元数据的影子块。
CLFS.sys驱动调用CClfsBaseFilePersisted::ReadImage
读取并解析文件,首先读取头部0x400
固定大小的块,这个块包含了文件所有的元数据块配置。
在这个块中,最终要的是以下两个结构
-
CLFS_LOG_BLOCK_HEADER
typedef struct {
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn<format=hex>;
UCHAR ClientId;
USHORT TotalSectorCount<comment="Number of Sectors, Size = Num * 512">;
USHORT ValidSectorCount;
DWORD Reserved1<format=hex>;
DWORD Checksum<format=hex>;
CLFS_LOG_BLOCK_FLAGS Flags;
DWORD Reserved2<format=hex, comment="Unknown (empty value) 0x00">;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
DWORD RecordOffsets[16]<format=hex>;
DWORD SignaturesOffset<format=hex>;
DWORD Reserved3<format=hex>; // TODO: PADDING
} CLFS_LOG_BLOCK_HEADER<fgcolor=cPurple>;
-
CLFS_CONTROL_RECORD
typedef struct {
CLFS_METADATA_RECORD_HEADER RecordHeader;
ULONGLONG Magic<comment="MAGIC",format=hex, fgcolor=cLtBlue>;
if (Magic != 0xC1F5C1F500005F1C) {
Printf("[!] CLFS_CONTROL_RECORD Magic Error: 0x016X\n", Magic);
}
UCHAR Version;
UCHAR Reserved1;
UCHAR Reserved2;
UCHAR Reserved3;
CLFS_EXTEND_STATE ExtendState;
USHORT ExtendBlock;
USHORT FlushBlock;
DWORD NewBlockSectors;
DWORD ExtendStartSectors;
DWORD ExtendSectors;
CLFS_TRUNCATE_CONTEXT Truncate;
DWORD Blocks;
DWORD Reserved4;
CLFS_METADATA_BLOCK RgBlocks[Blocks];
} CLFS_CONTROL_RECORD<bgcolor=cLtPurple>;
在得到CLFS_LOG_BLOCK_HEADER的解析后,我们随即就可以根据RecordOffsets
解析得到CLFS_CONTROL_RECORD
、再根据CLFS_CONTROL_RECORD
中的RgBlocks
解析三大块。
具体文件结构的解释可以参考,这里就不作叙述了。
漏洞成因分析
简单分析利用样本,能看到其构造blf patch如下
地址 |
原始值 |
目标值 |
备注 |
0x80C |
?? ?? ?? ?? |
?? ?? ?? ?? |
CRC32 CheckSum |
0x868 |
80 79 00 00 |
50 00 00 00 |
SignatureOffset |
0x9A8 |
68 13 00 00 |
30 1B 00 00 |
ClientContextOffset <ClientArray[0]> |
0x1B98 |
F8 00 00 00 |
4b 11 01 00 |
cbSymbolZone |
0x2390 |
00 00 00 00 |
B8 1B 00 00 |
Symbol Name Offset |
0x2394 |
00 00 00 00 |
30 1B 00 00 |
Symbol Context Offset |
0x23a0 |
00 – |
07 F0 FD C1 88 00 00 00 00 00 00 01 |
Fake Client Context Part 1 |
0x2418 |
00 – |
20 00 00 00 |
Fake Client Context Part 2 |
简单介绍下修改的字段
CRC32 CheckSum
这个是在我们构造完BLF文件后,需要重新计算CheckSum绕过文件校验。
SignatureOffset
配合我们构造的Fake Client Context Part 1完成SignatureOffset的覆写,实现OOB(详细见后)
ClientContextOffset
用于指向我们构造的Fake Client Context
Symbol
这里注意到0x2394
-0x2398
这块内容的修改在模板匹配的文件格式上似乎并没有什么关联。
首先在正常情况下,ClientSymbolTable与ClientContext是相邻的,下图展示了一个正常的BLF文件。
看到这里其实已经有了大概的猜想了,究其原因,自然离不开CLFS驱动本身对BLF文件的解析
之所以样本设置0x2394
上的内容,是因为CLFS.sys
中获取符号(CClfsBaseFile::GetSymbol
)是通过相对Context
的偏移实现的。
具体来讲,CClfsBaseFile::GetSymbol
是通过获取CLFS_CLIENT_CONTEXT
后往前推0xC个字节获得对应符号(CLFS_HASH_SYM
)中的Offset
以正常BLF文件举例,ClientContext-0xC
对应的是Symbol的Offset,也就是对应ClientContext相对CLFS_BASE_RECORD_HEADER
的偏移
样本通过修改ClientContextOffset将ClientContext指向我们构造的fakeClientContext,通过patch 0x2394
处的内容绕过CClfsBaseFile::GetSymbol
中对ClientContextOffset的验证。
同样的,样本通过修改0x2390
处的内容绕过校验。
Context Part 1
0x23a0
处的patch实际上就是伪造了一个ClientContext
,通过构造State为CLFS_LOG_SHUTDOWN
使其通过CClfsLogFcbPhysical::Initialize
进入CClfsLogFcbPhysical::ResetLog
这个函数会将ClientContext+0x58
上的内容覆写
在这里即下图所示
显而易见这会覆盖掉对应索引为13的chunk末尾两字节的signature
即覆盖10 01
为 FF FF
而后CClfsLogFcbPhysical::Initialize
将会执行CClfsLogFcbPhysical::FlushMetadata
继而执行CClfsBaseFilePersisted::FlushImage
、CClfsBaseFilePersisted::WriteMetadataBlock
、ClfsEncodeBlock
、ClfsEncodeBlockPrivate
ClfsEncodeBlockPrivate
函数将每个chunk末尾两字节的signature放置到SignaturesOffset
偏移对应的位置上,如下图所示
在这里用上了在此之前构造的SignaturesOffset
,简单计算下我们会发现之前通过CClfsLogFcbPhysical::ResetLog
构造的FF FF
会被覆盖到0x86A
上,即0x800 + 0x50 + 0x2 * 13
cbSymbolZone
如下图所示,我们需要调用AddLogContainer
触发OOB Write
上面提到,样本通过fakeClientContext将SignaturesOffset
覆盖为0xffff0050
,绕过了CClfsBaseFilePersisted::AllocSymbol
中的大小校验,从而实现任意位置的大小为0xB0
的置零操作
样本将其SymbolZone设置为0x01114B
实际上这个值是通过一系列操作计算得到与下一个LogFile的pContainer
之间的偏移,完成对pContainer
内核指针的覆盖。
对于打开和创建的BLF文件,在之后内存池空间没有被占位的情况下,Base Block
和下个BLF文件的Base Block
几个间隔块大小经调试得出的结构偏移是常量0x11000
,样本通过Windows提供的API查询SystemBigPoolInformation
具体的堆地址和TAG,反复调用查询两者在Pool上的位置,保证偏移恒定后(即0x11000
),当我们关闭这个两个占位文件,在这之后再次创建BLF,两者偏移量即为之前所获得的偏移量。
参见下图
调试后可以看到symbolzone偏移对应的内存处-0x1b
就是另一个blf文件的CLFS_CONTAINER_CONTEXT
0x1b的偏移+0x18
对应的即为pContainer
指针
我们覆盖了pContainer
的高位5个字节为0,将内核指针pContainer
改到我们自己伪造的用户态地址上,即范围0x000000
~0xFFFFFF
上
做完这些,用户调用CloseHandle
关闭文件,触发CClfsBaseFilePersisted::RemoveContainer
调用pContainer
指向的vftable对应的函数
对应调用链如下图
在CloseHandle
时, 会调用CClfsBaseFilePersisted::RemoveContainer
,这个函数会获取CClfsContainer
结构体, 并根据结构体虚表执行函数
通过结合先前OOB,我们构造一个虚表引用,结合堆(池)喷射完成gadgets调用。
这里调用两个gadgets都是精心构造的
结合这两个gadget我们可以通过以下方式,进行内核的任意写操作
- 将我们需要写入的数据放在
0xFFFFFFFF
上
- 将需要写入的地址
-0x8
然后放在我们构造的vftable的引用位置上,放置位置要求需要满足offset % 8 == 0, offset ≠ 0
而后执行堆喷。
CLFS部分的漏洞利用告一段落,下面我们就来分析利用样本
样本利用概述
- 版本识别校验
- 指定
Token
在_EPROCESS
中的Offset
- 指定
PreviousMode
在_ETHREAD
中的Offset
- 还有一些基本的初始化
- 进程ID、进程句柄、进程在内核中的地址
- 线程ID、进程句柄、线程在内核中的地址
- Windows 10 / 11 的区分
- Win API:
NtQuerySystemInformation
、NtWriteVirtualMemory
- 获取进程、内核进程(System)的_EBPROCESS及Token所在地址
- 调用
OpenProcessToken
并校验ProcessToken
- 调用VirtualAlloc,初始化空间
- 堆喷射,写入伪造的vftable ptr
- 循环创建BLF文件,寻找满足条件(偏移量恒定)时机,而后获取到偏移
0x110000
- 创建BLF文件,构造文件
- 创建另一个BLF文件,添加Container
- Windows 11
- 利用
PipeAttribute
实现内核任意位置读, 结合CLFS中存在的任意位置写,实现System进程的_EPROCESS
读取,得到System进程Token
- 再次利用CLFS漏洞替换进程Token为System Token
- 提权完成,调用
system("cmd")
- Windows 10
- 利用CLFS中发现的任意位置写漏洞,将用户线程
_ETHREAD.Token.PreviousMode
修改为0
- 调用
NtWriteVirtualMemory
替换进程Token为System Token
- 恢复
PreviousMode
- 提权完成,调用
system("cmd")
下面这个图非常全面的展示了样本的利用过程
样本分析
下面来详细分析一下,其是如何一步一步利用上述漏洞实现内核提权。
首先是win 10 & 11共同部分
-
版本校验
在版本校验的函数中,其不仅校验版本,而且初始化了一些其他的全局变量
初始化win api
获取当前进程、线程的Handle
获取当前进程、线程在内核中的地址
通过注册表获取UBR(修补号)
通过PEB获取系统版本号,结合UBR匹配到对应Windows中_EPROCESS
结构体中Token
的偏移
如果系统是Windows10,还会多出一个PreviousMode
在_ETHREAD
中的偏移
且配备了一个系统版本区分
-
获取进程、内核进程(System)的_EBPROCESS及Token所在地址
- 调用
OpenProcess
获取读取进程信息的权限
- 调用
NtQuerySystemInformation
获得SystemExtendedHandleInformation
- 遍历
HandleInformation
获取样本进程对应在内核中的地址
- 与上述方法相同,遍历获取进程号为4,即
System
进程的对应在内核中的地址
对应代码如下:
UINT GetEBPROCESSINFO() {
HANDLE hProcess;
DWORD CurrentProcessId;
HMODULE hNtdll;
NTQUERYSYSTEMINFORMATION pNtQuerySystemInformation;
ULONG dwBytes;
HGLOBAL hGlobal;
NTSTATUS nStatus;
PVOID Object;
PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo;
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId);
if (!hProcess) {
fprintf(stderr, "[-] OpenProcess failed: %d\n", GetLastError());
ExitProcess(-1);
}
CurrentProcessId = GetCurrentProcessId();
hNtdll = GetModuleHandleW(L"ntdll.dll");
if (!hNtdll) {
fprintf(stderr, "[-] GetModuleHandleW failed: %d\n", GetLastError());
ExitProcess(-1);
}
pNtQuerySystemInformation = reinterpret_cast<NTQUERYSYSTEMINFORMATION>(GetProcAddress(hNtdll, "NtQuerySystemInformation"));
if (!pNtQuerySystemInformation) {
fprintf(stderr, "[-] GetProcAddress failed: %d\n", GetLastError());
ExitProcess(-1);
}
dwBytes = 20;
while (1) {
dwBytes *= 2;
hGlobal = GlobalAlloc(GMEM_ZEROINIT, dwBytes);
nStatus = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, hGlobal, dwBytes, &dwBytes);
if (nStatus != STATUS_INFO_LENGTH_MISMATCH) {
break;
}
}
if (nStatus) {
fprintf(stderr, "[-] NtQuerySystemInformation failed: 0x%X\n", nStatus);
ExitProcess(-1);
}
Object = NULL;
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)hGlobal;
for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++) {
if (pHandleInfo->Handles[i].UniqueProcessId == CurrentProcessId) {
HANDLE hHandle = (HANDLE)pHandleInfo->Handles[i].HandleValue;
if (hHandle == hProcess) {
Object = pHandleInfo->Handles[i].Object;
break;
}
}
}
if (Object == NULL) {
fprintf(stderr, "[-] Failed to get current process token\n");
ExitProcess(-1);
}
pProcessKernelAddress = Object;
// GET SystemObject
dwBytes = 20;
while (1) {
dwBytes *= 2;
hGlobal = GlobalAlloc(GMEM_ZEROINIT, dwBytes);
nStatus = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, hGlobal, dwBytes, &dwBytes);
if (nStatus != STATUS_INFO_LENGTH_MISMATCH) {
break;
}
}
if (nStatus) {
fprintf(stderr, "[-] NtQuerySystemInformation failed: 0x%X\n", nStatus);
ExitProcess(-1);
}
Object = NULL;
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)hGlobal;
for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++) {
if (pHandleInfo->Handles[i].UniqueProcessId == 4) {
Object = pHandleInfo->Handles[i].Object;
break;
}
}
if (Object == NULL) {
fprintf(stderr, "[-] Failed to get current process token\n");
ExitProcess(-1);
}
pSystemKernelAddress = Object;
//getchar();
pProcessToken = (char*)pProcessKernelAddress + uTokenOffset;
pSystemToken = (char*)pSystemKernelAddress + uTokenOffset;
return 0;
}
获取ProcessToken并校验存在于SystemHandleInformation
中
申请shellcode利用空间
堆喷,指向构造的虚表
循环创建BLF文件,寻找满足条件(偏移量恒定)时机,而后获取到偏移(0x110000
)
patch 文件,构造漏洞,具体分析见上文漏洞成因分析。
验证池上的偏移量并创建BLF文件、添加Container
下面分析不同系统版本的利用过程
windows 10
通过clfs漏洞构造调用gadgets实现将样本进程上的PreviousMode置为0xFFFFFFFF
上的值,即0x00
在CloseHandle
时触发
内核调试下,下图是覆盖PreviousMode
前
驱动执行完SeSetAccessStateGenericMapping
后完成PreviousMode
置0
当我们替换PreviousMode
为0
后,这意味着我们可以使用NtReadVirtualMemory
和NtWriteVirtualMemory
在整个内核内存中进行不受约束的RW。
随及进行Token替换
在Token替换完后,我们将PreviousMode
还原为1
自此完成提权。
下图很好的解释了在Windows 10上的利用过程
Windows 11
Windows 11这里相对复杂,利用了两次CLFS漏洞
首先是利用PipeAttribute
结合CLFS漏洞将System进程的_EPORCESS复制到我们的构造的变量上,随即完成读取Token操作。
其使用到了一个PipeAttribute
结构如下
struct PipeAttribute
{
LIST_ENTRY list; // + 0x00
char *AttributeName; // + 0x10
ULONGLONG AttributeValueSize; // + 0x18
char *AttributeValue; // + 0x20
char data[]; // + 0x24
};
先是调用了CreatePipe
创建读写管道
调用NtFsControlFile执行写操作,使内核申请到PipeAttribute
NtFsControlFile(
Pipe.WritePipe,
0i64,
0i64,
0i64,
(PIO_STATUS_BLOCK)&IoStatusBlock,
0x11003Cu,
pSystemEPROCESS,
0xFD8u,
Dst,
0x100);
而后遍历SystemBigPoolInformation
拿到PipeAttribute
在内核上的地址+0x18偏移写入并堆喷
这里做的一些操作跟我们选择的gadget SeSetAccessStateGenericMapping
相关
先前提到SeSetAccessStateGenericMapping
其实就做了这个操作:poi(poi(rcx+0x48)+0x8)
poi(rcx+0x48)
也就意味着是0x010000
~0xFFFFFF
任意0x8*N
(N>0)上的内容
这就是为什么堆喷如此操作,并且这里为什么是+0x18
而不是直接+0x20
定位到AttributeValue
也是此原因。
做完这些,我们需要再次利用CLFS漏洞替换Token
自此完成提权。
下图很好的总结了在Windows 11中的利用过程
参考
https://github.com/fortra/CVE-2022-37969
https://vul.360.net/archives/438
https://www.freebuf.com/articles/network/339537.html
https://github.com/ionescu007/clfs-docs
https://bbs.kanxue.com/thread-275566.htm
https://blog.qwerdf.com/2022/11/30/CVE-2022-37969/
https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part
https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part2-exploit-analysis
https://www.slideshare.net/PeterHlavaty/deathnote-of-microsoft-windows-kernel
https://www.pixiepointsecurity.com/blog/nday-cve-2022-24521.html
https://paper.seebug.org/1743/
https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion
https://www.freebuf.com/vuls/317380.html