By Arav Garg
This post analyzes a use-after-free vulnerability in clfs.sys, the kernel driver that implements the Common Logging File System, a general-purpose logging service that can be used by user-space and kernel-space processes in Windows. A method to exploit this vulnerability to achieve privilege escalation in Windows is also outlined.
Along with two other similar vulnerabilities, Microsoft patched this vulnerability in September 2021 and assigned the CVEs CVE-2021-36955, CVE-2021-36963, and CVE-2021-38633 to them. In the absence of any public information separating the three CVEs, we’ve decided to use CVE-2021-36955 to refer to the vulnerability described herein.
The Preliminaries section describes CLFS structures, Code Analysis explains the vulnerability with the help of code snippets, and the Exploitation section outlines the steps that lead to a functional exploit.
Common Log File System (CLFS) provides a high-performance, general-purpose log file subsystem that dedicated client applications can use and multiple clients can share to optimize log access. Any user-mode application that needs logging or recovery support can use CLFS. The following structures are taken from both the official documentation and a third-party’s unofficial documentation.
Every Base Log File is made up various records. These records are stored in sectors, which are written to in units of I/O called log blocks. These log blocks are always read and written in an atomic fashion to guarantee consistency.
Every Base Log File is made up of various records. These records are stored in sectors, which are written to in units of I/O called log blocks. The Base Log File is composed of 6 different metadata blocks (3 of which are shadows), which are all examples of log blocks.
The three types of records that exist in such blocks are:
Three metadata records were defined above, yet six metadata blocks exist (and each metadata block only contains one record). This is due to shadow blocks, which are yet another technique used for consistency. Shadow blocks contain the previous copy of the metadata that was written, and by using the dump count in the record header, can be used to restore previously known good data in case of torn writes.
The following enumeration describes the six types of metadata blocks.
typedef enum _CLFS_METADATA_BLOCK_TYPE { ClfsMetaBlockControl, ClfsMetaBlockControlShadow, ClfsMetaBlockGeneral, ClfsMetaBlockGeneralShadow, ClfsMetaBlockScratch, ClfsMetaBlockScratchShadow } CLFS_METADATA_BLOCK_TYPE, *PCLFS_METADATA_BLOCK_TYPE;
The Control Record is always composed of two sectors, as defined by the constant below:
const USHORT CLFS_CONTROL_BLOCK_RAW_SECTORS = 2;
The Control Record is defined by the structure CLFS_CONTROL_RECORD, which is shown below:
typedef struct _CLFS_CONTROL_RECORD { CLFS_METADATA_RECORD_HEADER hdrControlRecord; ULONGLONG ullMagicValue; UCHAR Version; CLFS_EXTEND_STATE eExtendState; USHORT iExtendBlock; USHORT iFlushBlock; ULONG cNewBlockSectors; ULONG cExtendStartSectors; ULONG cExtendSectors; CLFS_TRUNCATE_CONTEXT cxTruncate; USHORT cBlocks; ULONG cReserved; CLFS_METADATA_BLOCK rgBlocks[ANYSIZE_ARRAY]; } CLFS_CONTROL_RECORD, *PCLFS_CONTROL_RECORD;
After Version, the next set of fields are all related to CLFS Log Extension. This data could potentially be non-zero in memory, but for a stable Base Log File on disk, all of these fields are expected to be zero. This does not, of course, imply the CLFS driver or code necessarily makes this assumption.
The first CLFS Log Extension field, eExtendState, identifies the current extend state for the file using the enumeration below:
typedef enum _CLFS_EXTEND_STATE { ClfsExtendStateNone, ClfsExtendStateExtendingFsd, ClfsExtendStateFlushingBlock } CLFS_EXTEND_STATE, *PCLFS_EXTEND_STATE;
The next two values iExtendBlock and iFlushBlock identify the index of the block being extended, followed by the block being flushed, the latter of which will normally be the shadow block. Next, the sector size of the new block is stored in cNewBlockSectors and the original sector size before the extend operation is stored in cExtendStartSectors. Finally, the number of sectors that were added is saved in cExtendSectors.
Each array entry is identified by the CLFS_METADATA_BLOCK structure, shown below:
typedef struct _CLFS_METADATA_BLOCK { union { PUCHAR pbImage; ULONGLONG ullAlignment; }; ULONG cbImage; ULONG cbOffset; CLFS_METADATA_BLOCK_TYPE eBlockType; } CLFS_METADATA_BLOCK, *PCLFS_METADATA_BLOCK;
On disk, the cbOffset field indicates the offset, starting from the control metadata block (i.e.: the first sector in the Base Log File). Of where the metadata block can be found. The cbImage field, on the other hand, contains the size of the corresponding block, while the eBlockType
corresponds to the previously shown enumeration of possible metadata block types.
In memory, an additional field, pbImage, is used to store a pointer to the data in kernel-mode memory.
Once in memory, a CLFS Base Log File is represented by a CClfsBaseFile class, which can be further extended by a CClfsBaseFilePersisted. The definition for the former can be found in public symbols and is shown below:
struct _CClfsBaseFile { ULONG m_cRef; PUCHAR m_pbImage; ULONG m_cbImage; PERESOURCE m_presImage; USHORT m_cBlocks; PCLFS_METADATA_BLOCK m_rgBlocks; PUSHORT m_rgcBlockReferences; CLFSHASHTBL m_symtblClient; CLFSHASHTBL m_symtblContainer; CLFSHASHTBL m_symtblSecurity; ULONGLONG m_cbContainer; ULONG m_cbRawSectorSize; BOOLEAN m_fGeneralBlockReferenced; } CClfsBaseFile, *PCLFSBASEFILE;
These fields mainly represent data seen earlier, such as the size of the container, the sector size, the array of metadata blocks and their number, as well as the size of the whole Base Log File and its location in kernel mode memory. Additionally, the class is reference counted, and almost any access to any of its fields is protected by the m_presImage lock, which is an executive resource accessed in either shared or exclusive mode. Finally, each block itself is also referenced in the m_rgcBlockReferences array, noting there’s a limit of 65535 references. When the general block has been referenced at least once, the m_fGeneralBlockReferenced boolean is used to indicate the fact.
All code listings show decompiled C code; source code is not available in the affected product.
Structure definitions are obtained by reverse engineering and may not accurately reflect structures defined in the source code.
The CreateLogFile() function in the Win32 API can be used to open an existing log. This function triggers a call to CClfsBaseFilePersisted::OpenImage() in clfs.sys. The pseudocode of CClfsBaseFilePersisted::OpenImage() is listed below:
long __thiscall CClfsBaseFilePersisted::OpenImage (CClfsBaseFilePersisted *this,_UNICODE_STRING *ExtFileName,_CLFS_FILTER_CONTEXT *ClfsFilterContext, unsigned_char param_3,unsigned_char *param_4) { [Truncated] [1] Status = CClfsContainer::Open(this->CclfsContainer,ExtFileName,ClfsFilterContext,param_3,local_48); if ((int)Status < 0) { LAB_fffff801226897c8: StatusDup = Status; if (Status != 0xc0000011) goto LAB_fffff80122689933; } else { StatusDup = Status; UVar4 = CClfsContainer::GetRawSectorSize(this->field_0x98_CclfsContainer); this->rawsectorSize = UVar4; if ((0xfff < UVar4 - 1) || ((UVar4 & 0x1ff) != 0)) { Status = 0xc0000098; StatusDup = Status; goto LAB_fffff80122689933; } [2] Status = ReadImage(this,&ClfsControlRecord); if ((int)Status < 0) goto LAB_fffff801226897c8; StatusDup = Status; Status = CClfsContainer::GetContainerSize(this->CclfsContainer,&this->ContainerSize); StatusDup = Status; if ((int)Status < 0) goto LAB_fffff80122689933; ClfsBaseLogRecord = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); ControlRecord = ClfsControlRecord; if (ClfsBaseLogRecord != NULL) { [Truncated] [3] if (ClfsControlRecord->eExtendState == 0) goto LAB_fffff80122689933; Block = ClfsControlRecord->iExtendBlock; if (((((Block != 0) && (Block < this->m_cBlocks)) && (Block < 6)) && ((Block = ClfsControlRecord->iFlushBlock, Block != 0 && (Block < this->m_cBlocks)))) && ((Block < 6 && (m_cbContainer = CClfsBaseFile::GetSize((CClfsBaseFile *)this), ControlRecord->cExtendStartSectors < m_cbContainer >> 9 || ControlRecord->cExtendStartSectors == m_cbContainer >> 9)))) { cExtendSectors>>1 = ControlRecord->cExtendSectors >> 1; uVar8 = (this->m_rgBlocks[ControlRecord->iExtendBlock].cbImage >> 9) + cExtendSectors>>1; if (ControlRecord->cNewBlockSectors < uVar8 || ControlRecord->cNewBlockSectors == uVar8) { [4] Status = ExtendMetadataBlock(this,(uint)ControlRecord->iExtendBlock,cExtendSectors>>1); StatusDup = Status; goto LAB_fffff80122689933; } } } } [Truncated] }
After initializing some in-memory data structures, CClfsContainer:Open() is called to open the existing Base Log File at [1]. ReadImage() is then called to read the Base Log File at [2]. If the current extend state in the Extend Context is not ClfsExtendStateNone(0) at [3], the
possibility to expand the Base Log File is explored.
If the original sector size before the previous extension (ControlRecord->cExtendStartSectors) is less than or equal to the current sector size of
the Base Log File (m_cbContainer), and the sector size of the Block (to be expanded) after the previous extension (ControlRecord->cNewBlockSectors) is less than or equal to the latest required sector size (current sector size of the Block to be expanded this->m_rgBlocks[ControlRecord->iExtendBlock].cbImage >> 9 plus the number of sectors previously added cExtendSectors >> 1), the Base Log File needs expansion. ExtendMetadataBlock() is duly called at [4].
Note:
The CClfsBaseFilePersisted::ReadImage() function called at [2] is responsible for reading the Base Log File from disk. The pseudocode of this function is listed below:
int CClfsBaseFilePersisted::ReadImage (CClfsBaseFilePersisted *BaseFilePersisted,_CLFS_CONTROL_RECORD **ClfsControlRecordPtr) { [Truncated] [5] BaseFilePersisted->m_cBlocks = 6; m_rgBlocks = (CLFS_METADATA_BLOCK *)ExAllocatePoolWithTag(0x200,0x90,0x73666c43); BaseFilePersisted->m_rgBlocks = m_rgBlocks; [Truncated] [6] memset(BaseFilePersisted->m_rgBlocks,0,(ulonglong)BaseFilePersisted->m_cBlocks * 0x18); memset(BaseFilePersisted->m_rgcBlockReferences,0,(ulonglong)BaseFilePersisted->m_cBlocks * 2); [7] BaseFilePersisted->m_rgBlocks->cbOffset = 0; BaseFilePersisted->m_rgBlocks->cbImage = BaseFilePersisted->m_cbRawSectorSize * 2; BaseFilePersisted->m_rgBlocks[1].cbOffset = BaseFilePersisted->m_cbRawSectorSize * 2; BaseFilePersisted->m_rgBlocks[1].cbImage = BaseFilePersisted->m_cbRawSectorSize * 2; [8] local_48 = CClfsBaseFile::GetControlRecord((CClfsBaseFile *)BaseFilePersisted,ClfsControlRecordPtr); [9] p_Var2 = BaseFilePersisted->m_rgBlocks->pbImage; for (; (uint)indexIter < (uint)BaseFilePersisted->m_cBlocks; indexIter = (ulonglong)((uint)indexIter + 1)) { ControlRecorD = *ClfsControlRecordPtr; pCVar3 = BaseFilePersisted->m_rgBlocks; pCVar1 = ControlRecorD->rgBlocks + indexIter; uVar6 = *(undefined4 *)((longlong)&pCVar1->pbImage + 4); uVar5 = pCVar1->cbImage; uVar7 = pCVar1->cbOffset; m_rgBlocks = pCVar3 + indexIter; *(undefined4 *)&m_rgBlocks->pbImage = *(undefined4 *)&pCVar1->pbImage; *(undefined4 *)((longlong)&m_rgBlocks->pbImage + 4) = uVar6; m_rgBlocks->cbImage = uVar5; m_rgBlocks->cbOffset = uVar7; pCVar3[indexIter].eBlockType = ControlRecorD->rgBlocks[indexIter].eBlockType; BaseFilePersisted->m_rgBlocks[indexIter].pbImage = NULL; } BaseFilePersisted->m_rgBlocks->pbImage = p_Var2; BaseFilePersisted->m_rgBlocks[1].pbImage = p_Var2; [10] local_48 = CClfsBaseFile::AcquireMetadataBlock(BaseFilePersisted,ClfsMetaBlockGeneral); if (-1 < (int)local_48) { BaseFilePersisted->field_0x94 = '\x01'; } goto LAB_fffff80654e09f9e; [Truncated] }
The in-memory buffer of the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File, is allocated (m_rgBlocks) at [5]. Each array entry is identified by the CLFS_METADATA_BLOCK structure, which is of size 0x18. The cBlocks field, which indicates the number of blocks in the array, is set to the default value 6 (Hence, the size of allocation for m_rgBlocks is 0x18 * 6 = 0x90).
The content in m_rgBlocks is initialized to 0 at [6]. The first two entries in m_rgBlocks are for the Control Record and its shadow, both of which have a fixed size of 0x400. The sizes and offsets for these blocks are duly set at [7].
At this stage, m_rgBlocks looks like the following in memory:
ffffc60c`53904380 00000000`00000000 00000000`00000400 ffffc60c`53904390 00000000`00000000 00000000`00000000 ffffc60c`539043a0 00000400`00000400 00000000`00000000 ffffc60c`539043b0 00000000`00000000 00000000`00000000 ffffc60c`539043c0 00000000`00000000 00000000`00000000 ffffc60c`539043d0 00000000`00000000 00000000`00000000 ffffc60c`539043e0 00000000`00000000 00000000`00000000 ffffc60c`539043f0 00000000`00000000 00000000`00000000 ffffc60c`53904400 00000000`00000000 00000000`00000000
CClfsBaseFile::GetControlRecord() is called to retrieve the Control Record from the Base Log File at [8]. The pbImage field in the first two entries in m_rgBlocks are duly populated. More on this below.
At this stage, m_rgBlocks contains the following values:
ffffc60c`53904380 ffffb60a`b7c79b40 00000000`00000400 ffffc60c`53904390 00000000`00000000 ffffb60a`b7c79b40 ffffc60c`539043a0 00000400`00000400 00000000`00000000 ffffc60c`539043b0 00000000`00000000 00000000`00000000 ffffc60c`539043c0 00000000`00000000 00000000`00000000 ffffc60c`539043d0 00000000`00000000 00000000`00000000 ffffc60c`539043e0 00000000`00000000 00000000`00000000 ffffc60c`539043f0 00000000`00000000 00000000`00000000 ffffc60c`53904400 00000000`00000000 00000000`00000000
The rgBlocks array from the Control Record is copied into m_rgBlocks at [9]. Thus, the sizes and corresponding offsets of each of the metadata blocks is saved.
ffffc60c`53904380 ffffb60a`b7c79b40 00000000`00000400 ffffc60c`53904390 00000000`00000000 ffffb60a`b7c79b40 ffffc60c`539043a0 00000400`00000400 00000000`00000001 ffffc60c`539043b0 00000000`00000000 00000800`00007a00 ffffc60c`539043c0 00000000`00000002 00000000`00000000 ffffc60c`539043d0 00008200`00007a00 00000000`00000003 ffffc60c`539043e0 00000000`00000000 0000fc00`00000200 ffffc60c`539043f0 00000000`00000004 00000000`00000000 ffffc60c`53904400 0000fe00`00000200 00000000`00000005
AcquireMetadataBlock() is called with the second parameter set to ClfsMetaBlockGeneral to read in the General Metadata Block from the Base Log File at [10]. The pbImage field for the corresponding entry and its shadow in m_rgBlocks are duly populated. More on this below.
At this stage, m_rgBlocks looks like the following in memory:
ffffc60c`53904380 ffffb60a`b7c79b40 00000000`00000400 ffffc60c`53904390 00000000`00000000 ffffb60a`b7c79b40 ffffc60c`539043a0 00000400`00000400 00000000`00000001 ffffc60c`539043b0 ffffb60a`b9ead000 00000800`00007a00 ffffc60c`539043c0 00000000`00000002 ffffb60a`b9ead000 ffffc60c`539043d0 00008200`00007a00 00000000`00000003 ffffc60c`539043e0 00000000`00000000 0000fc00`00000200 ffffc60c`539043f0 00000000`00000004 00000000`00000000 ffffc60c`53904400 0000fe00`00000200 00000000`00000005
It is important to note that the pbImage field for the General Metadata Block and its shadow point to the same memory (refer [17] and [20]).
Internally, CClfsBaseFilePersisted::ReadImage() calls CClfsBaseFile::GetControlRecord() to retrieve the Control Record. The pseudocode of
CClfsBaseFile::GetControlRecord() is listed below:
long __thiscall CClfsBaseFile::GetControlRecord(CClfsBaseFile *this,_CLFS_CONTROL_RECORD **ClfsControlRecordptr) { uint iVar4; astruct_12 *lVar3; _CLFS_LOG_BLOCK_HEADER *pbImage; uint RecordOffset; uint cbImage; *ClfsControlRecordptr = NULL; [11] iVar4 = AcquireMetadataBlock((CClfsBaseFilePersisted *)this,0); if (-1 < (int)iVar4) { cbImage = this->m_rgBlocks->cbImage; ControlMetadataBlock = this->m_rgBlocks->pbImage; RecordOffset = pbImage->RecordOffsets[0]; if (((RecordOffset < cbImage) && (0x6f < RecordOffset)) && (0x67 < cbImage - RecordOffset)) { [12] *ClfsControlRecordptr = (_CLFS_CONTROL_RECORD *)((longlong)ControlMetadataBlock->RecordOffsets + ((ulonglong)RecordOffset - 0x28)); } else { iVar4 = 0xc01a000d; } } return (long)iVar4; }
AcquireMetadataBlock() is called with the second parameter of type _CLFS_METADATA_BLOCK_TYPE set to ClfsMetaBlockControl (0) to acquire the
Control MetaData Block at [11]. The record offset is retrieved and used to calculate the address of the Control Record, which is saved at [12].
The CClfsBaseFile::AcquireMetadataBlock() function is used to acquire a metadata block. The pseudocode of this function is listed below:
int CClfsBaseFile::AcquireMetadataBlock (CClfsBaseFilePersisted *ClfsBaseFilePersisted,_CLFS_METADATA_BLOCK_TYPE BlockType) { ulong lVar1; longlong BlockTypeDup; lVar1 = 0; if (((int)BlockType < 0) || ((int)(uint)ClfsBaseFilePersisted->m_cBlocks <= (int)BlockType)) { lVar1 = 0xc0000225; } else { BlockTypeDup = (longlong)(int)BlockType; [13] ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] = ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] + 1; [14] if ((ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] == 1) && (lVar1 = (*ClfsBaseFilePersisted->vftable->field_0x8)(ClfsBaseFilePersisted,BlockType), (int)lVar1 < 0)) { ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] = ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] - 1; } } return (int)lVar1; }
The m_rgcBlockReferences entry for the Control Metadata Block is increased by 1 to signal its usage at [13].
If the reference count is 1, it is clear the Control Metadata Block was not being actively used (prior to this). In this case, it needs to be read from disk. The second entry in the virtual function table is set to CClfsBaseFilePersisted::ReadMetadataBlock(), which is duly called at [14].
The CClfsBaseFilePersisted::ReadMetadataBlock() function is used to read a metadata block from disk. The pseudocode of this function is listed below:
ulong __thiscall CClfsBaseFilePersisted::ReadMetadataBlock(CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE BlockType) { [Truncated] [15] cbImage = this->m_rgBlocks[(longlong)_BlockTypeDup].cbImage; cbOffset = (PIRP)(ulonglong)this->m_rgBlocks[(longlong)_BlockTypeDup].cbOffset; if (cbImage == 0) { uVar3 = 0; } else { if (0x6f < cbImage) { [16] ClfsMetadataBlock = (_CLFS_LOG_BLOCK_HEADER *)ExAllocatePoolWithTag(PagedPoolCacheAligned,(ulonglong)cbImage,0x73666c43); if (ClfsMetadataBlock == NULL) { uVar1 = 0xc000009a; } else { [17] this->m_rgBlocks[(longlong)_BlockTypeDup].pbImage = ClfsMetadataBlock; memset(ClfsMetadataBlock,0,(ulonglong)cbImage); *(undefined4 *)&this->field_0xc8 = 0; this->field_0xd0 = 0; local_40 = local_40 & 0xffffffff00000000; local_50 = CONCAT88(local_50._8_8_,ClfsMetadataBlock); [18] uVar5 = CClfsContainer::ReadSector ((ULONG_PTR)this->CclfsContainer,this->ObjectBody,NULL,(longlong *)local_50, cbImage >> 9,&cbOffset); uVar3 = (ulong)uVar5; if (((int)uVar3 < 0) || (uVar3 = KeWaitForSingleObject(this->ObjectBody,Executive,'\0','\0',NULL), (int)uVar3 < 0)) goto LAB_fffff801226841ad; this_00 = (CClfsBaseFilePersisted *)ClfsMetadataBlock; [19] uVar3 = ClfsDecodeBlock(ClfsMetadataBlock,cbImage >> 9,*(unsigned_char *)&ClfsMetadataBlock->UpdateCount ,(unsigned_char)0x10,local_res20); [Truncated] ShadowBlockType = BlockType + ClfsMetaBlockControlShadow; uVar6 = (ulonglong)ShadowBlockType; this->m_rgBlocks[ShadowBlockTypeDup].pbImage = NULL; this->m_rgBlocks[ShadowBlockTypeDup].cbImage = this->m_rgBlocks[(longlong)_BlockTypeDup].cbImage; [20] this->m_rgBlocks[ShadowBlockTypeDup].pbImage = this->m_rgBlocks[(longlong)_BlockTypeDup].pbImage; [Truncated]
The size of the Metadata block to be read is retrieved and saved in cbImage. Note that these sizes are stored in the Control Record of the Base Log File.
To read the Control Record, the hardcoded value is taken at [15], as the Control Record is of a fixed size. Memory (ClfsMetadataBlock) is allocated to read the metadata block from disk at [16]. The corresponding pbImage entry in m_rgBlocks is filled in and ClfsMetadataBlock is initialized to 0 at [17]. The function CClfsContainer::ReadSector() is called to read the specified number of sectors from disk at [18].
ClfsMetadataBlock now contains the exact contents of the metadata Block as present in the file. It is important to note that the Control Metadata Block contains the Control Context as described earlier. Thus, the contents of the Control Context are fully controlled by the attacker.
ClfsMetadataBlock is decoded via a call to ClfsDecodeBlock at [19]. It is also important to note that in the case of the Control Metadata Block, this does not modify any field in the Control Context. The corresponding shadow pbImage entry in m_rgBlocks is also set to ClfsMetadataBlock at [20].
The call to ExtendMetadataBlock() at [4] is used to extend the size of a particular metadata block in the Base Log File. The pseudocode of this function pertaining to when the current extend state is ClfsExtendStateFlushingBlock(2) is listed below:
long __thiscall CClfsBaseFilePersisted::ExtendMetadataBlock (CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE BlockType,unsigned_long cExtendSectors>>1) { [Truncated] do { ret = retDup; if (*ExtendPhasePtr != 2) break; iFlushBlockPtr = &ClfsControlRecordDup->iFlushBlock; iExtendBlockPtr = &ClfsControlRecordDup->iExtendBlock; [21] iFlushBlock = *iFlushBlockPtr; iFlushBlockDup = (ulonglong)iFlushBlock; iExtendBlock = *iExtendBlockPtr; iFlushBlockDup2 = (uint)iFlushBlock; [22] if (((iFlushBlock == iExtendBlock) || (uVar4 = IsShadowBlock(this_00,iFlushBlockDup2,(uint)iExtendBlock), uVar4 != (unsigned_char)0x0)) && (this->m_rgBlocks[iFlushBlockDup].cbImage >> 9 < ClfsControlRecordDup->cNewBlockSectors)) { ExtendMetadataBlockDescriptor (this,iFlushBlockDup2, ClfsControlRecordDup->cExtendSectors >> 1); iFlushBlockDup = (ulonglong)*iFlushBlockPtr; } WriteMetadataBlock(this,(uint)iFlushBlockDup & 0xffff,(unsigned_char)0x0); if (*puVar1 == *puVar2) { *ExtendPhasePtr = 0; } else { *puVar1 = *puVar1 - 1; [23] ret = ProcessCurrentBlockForExtend(this,ClfsControlRecordDup); retDup = ret; if ((int)ret < 0) break; } [Truncated]
The index of the block being extended (iFlushBlock) and the block being flushed (iExtendBlock) are extracted from the Extend Context in the Control Record of the Base Log File at [21]. With specially crafted values of the above fields and cNewBlockSectors at [22], code execution reaches ProcessCurrentBlockForExtend() at [23]. ProcessCurrentBlockForExtend() internally calls ExtendMetadataBlockDescriptor(), whose pseudocode is listed below:
long __thiscall CClfsBaseFilePersisted::ExtendMetadataBlockDescriptor (CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE iFlushBlock,unsigned_long cExtendSectors>>1) { [Truncated] iFlushBlockDup = (ulonglong)iFlushBlock; NewMetadataBlock = NULL; RecordHeader = NULL; iVar13 = 0; uVar3 = this->m_cbRawSectorSize; if (uVar3 == 0) { NewSize = 0; } else { NewSize = (uVar3 - 1) + this->m_rgBlocks[iFlushBlockDup].cbImage + cExtendSectors>>1 * 0x200 & -uVar3; } RecordsParamsPtr = this->m_rgBlocks; pCVar1 = RecordsParamsPtr + iFlushBlockDup; uVar4 = *(undefined4 *)&pCVar1->pbImage; uVar5 = *(undefined4 *)((longlong)&pCVar1->pbImage + 4); uVar3 = pCVar1->cbImage; uVar6 = pCVar1->cbOffset; CVar2 = RecordsParamsPtr[iFlushBlockDup].eBlockType; ShadowIndex._0_4_ = iFlushBlock + ClfsMetaBlockControlShadow; ShadowIndex = (ulonglong)(uint)ShadowIndex; uVar7 = IsShadowBlock((CClfsBaseFilePersisted *)ShadowIndex,iFlushBlock,(uint)ShadowIndex); if ((uVar7 == (unsigned_char)0x0) && (uVar7 = IsShadowBlock((CClfsBaseFilePersisted *)ShadowIndex,(unsigned_long)ShadowIndex,iFlushBlock), uVar7 != (unsigned_char)0x0)) { if (RecordsParamsPtr[iFlushBlockDup].pbImage != NULL) { [24] ExFreePoolWithTag(RecordsParamsPtr[iFlushBlockDup].pbImage,0); this->m_rgBlocks[iFlushBlockDup].pbImage = NULL; RecordsParamsPtr = this->m_rgBlocks; ShadowIndex = (ulonglong)(iFlushBlock + ClfsMetaBlockControlShadow); } [25] RecordsParamsPtr[iFlushBlockDup].cbImage = RecordsParamsPtr[ShadowIndex].cbImage; m_rgBlocksDup = this->m_rgBlocks; m_rgBlocksDup[iFlushBlockDup].pbImage = m_rgBlocks[ShadowIndex].pbImage; [Truncated]
With carefully crafted values in the Control Record of the Base Log File, code execution reaches [24].
At this stage, m_rgBlocks looks like the following in memory:
ffffc60c`53904380 ffffb60a`b7c79b40 00000000`00000400 ffffc60c`53904390 00000000`00000000 ffffb60a`b7c79b40 ffffc60c`539043a0 00000400`00000400 00000000`00000001 ffffc60c`539043b0 ffffb60a`b9ead000 00000800`00007a00 ffffc60c`539043c0 00000000`00000002 ffffb60a`b9ead000 ffffc60c`539043d0 00008200`00007a00 00000000`00000003 ffffc60c`539043e0 00000000`00000000 0000fc00`00000200 ffffc60c`539043f0 00000000`00000004 00000000`00000000 ffffc60c`53904400 0000fe00`00000200 00000000`00000005
It is important to note that the pbImage field for the General Metadata Block and its shadow point to the same memory (refer to [17] and [20]). The pbImage field of the iFlushBlock index in m_rgBlocks is freed, and the corresponding entry is cleared at [24]. For example, if iFlushBlock is set to 2, m_rgBlocks looks like the following in memory:
ffffc60c`53904380 ffffb60a`b7c79b40 00000000`00000400 ffffc60c`53904390 00000000`00000000 ffffb60a`b7c79b40 ffffc60c`539043a0 00000400`00000400 00000000`00000001 ffffc60c`539043b0 00000000`00000000 00000800`00007a00 ffffc60c`539043c0 00000000`00000002 ffffb60a`b9ead000 ffffc60c`539043d0 00008200`00007a00 00000000`00000003 ffffc60c`539043e0 00000000`00000000 0000fc00`00000200 ffffc60c`539043f0 00000000`00000004 00000000`00000000 ffffc60c`53904400 0000fe00`00000200 00000000`00000005
The entry is then repopulated with the shadow index entry at [25].
ffffc60c`53904380 ffffb60a`b7c79b40 00000000`00000400 ffffc60c`53904390 00000000`00000000 ffffb60a`b7c79b40 ffffc60c`539043a0 00000400`00000400 00000000`00000001 ffffc60c`539043b0 ffffb60a`b9ead000 00000800`00007a00 ffffc60c`539043c0 00000000`00000002 ffffb60a`b9ead000 ffffc60c`539043d0 00008200`00007a00 00000000`00000003 ffffc60c`539043e0 00000000`00000000 0000fc00`00000200 ffffc60c`539043f0 00000000`00000004 00000000`00000000 ffffc60c`53904400 0000fe00`00000200 00000000`00000005
Since the original entry and the shadow index entry pointed to the same memory, the repopulation leaves a reference to freed memory. Any use of the General Metadata Block will refer to this freed memory, resulting in a Use After Free.
The vulnerability can be converted to a double free by closing the handle to the Base Log File. This will trigger a call to FreeMetadataBlock, which will free all pbImage entries in m_rgBlocks.
A basic understanding of the segment heap in the windows kernel introduced since the 19H1 update is required to understand the exploit mechanism. The paper titled “Scoop the Windows 10 pool!” from SSTIC 2020 describes this mechanism in detail.
Objects from Windows Notification Facility (WNF) are used to groom the heap and convert the use after free into a full exploit. A good understanding of WNF is thus required to understand the exploit. The details about WNF described below are taken from the following sources:
When a WNF State Name is created via a call to NtCreateWnfStateName(), ExpWnfCreateNameInstance() is called internally to create a name
instance. The pseudocode of ExpWnfCreateNameInstance() is listed below:
ExpWnfCreateNameInstance (_WNF_SCOPE_INSTANCE *ScopeInstance,_WNF_STATE_NAME *StateName,undefined4 *param_3,_KPROCESS *param_4, _EX_RUNDOWN_REF **param_5) { [Truncated] uVar23 = (uint)((ulonglong)StateName >> 4) & 3; if ((PsInitialSystemProcess == Process) || (uVar23 != 3)) { SVar20 = 0xb8; if (*(longlong *)(param_3 + 2) == 0) { SVar20 = 0xa8; } NameInstance = (_WNF_NAME_INSTANCE *)ExAllocatePoolWithTag(PagedPool,SVar20,0x20666e57); } else { SVar20 = 0xb8; if (*(longlong *)(param_3 + 2) == 0) { [1] SVar20 = 0xa8; } [2] NameInstance = (_WNF_NAME_INSTANCE *)ExAllocatePoolWithQuotaTag(9,SVar20,0x20666e57); }
A chunk of size 0xa8 (as seen at [1]) is allocated from the Paged Pool at [2] as a structure of type _WNF_NAME_INSTANCE. This results in an allocation of size 0xc0 from the LFH. This structure is listed below:
struct _WNF_NAME_INSTANCE { struct _WNF_NODE_HEADER Header; //0x0 struct _EX_RUNDOWN_REF RunRef; //0x8 struct _RTL_BALANCED_NODE TreeLinks; //0x10 // [3] struct _WNF_STATE_NAME_STRUCT StateName; //0x28 struct _WNF_SCOPE_INSTANCE* ScopeInstance; //0x30 struct _WNF_STATE_NAME_REGISTRATION StateNameInfo; //0x38 struct _WNF_LOCK StateDataLock; //0x50 // [4] struct _WNF_STATE_DATA* StateData; //0x58 ULONG CurrentChangeStamp; //0x60 VOID* PermanentDataStore; //0x68 struct _WNF_LOCK StateSubscriptionListLock; //0x70 struct _LIST_ENTRY StateSubscriptionListHead; //0x78 struct _LIST_ENTRY TemporaryNameListEntry; //0x88 // [5] struct _EPROCESS* CreatorProcess; //0x98 LONG DataSubscribersCount; //0xa0 LONG CurrentDeliveryCount; //0xa4 };
Relevant entries in the _WNF_NAME_INSTANCE structure include:
The StateData is headed by a structure of type _WNF_STATE_DATA. This structure is listed below:
struct _WNF_STATE_DATA { // [6] struct _WNF_NODE_HEADER Header; //0x0 // [7] ULONG AllocatedSize; //0x4 ULONG DataSize; //0x8 ULONG ChangeStamp; //0xc };
The StateData pointer is referred to when the WNF State Data is updated and queried. The variable-size data immediately follows the _WNF_STATE_DATA structure.
When the WNF State Data is updated via a call to NtUpdateWnfStateData(), ExpWnfWriteStateData() is called internally to write to the StateData pointer. The pseudocode of ExpWnfWriteStateData() is listed below.
void ExpWnfWriteStateData (_WNF_NAME_INSTANCE *NameInstance,void *InputBuffer,ulonglong Length,int MatchingChangeStamp, int CheckStamp) { [Truncated] if (NameInstance->StateData != (_WNF_STATE_DATA *)0x1) { [8] StateData = NameInstance->StateData; } LengtH = (uint)(Length & 0xffffffff); [9] if (((StateData == NULL) && ((NameInstance->PermanentDataStore != NULL || (LengtH != 0)))) || [10] ((StateData != NULL && (StateData->AllocatedSize < LengtH)))) { [Truncated] [11] StateData = (_WNF_STATE_DATA *)ExAllocatePoolWithQuotaTag(9,(ulonglong)(LengtH + 0x10),0x20666e57); [Truncated] [12] StateData->Header = (_WNF_NODE_HEADER)0x100904; StateData->AllocatedSize = LengtH; [Truncated] [13] RtlCopyMemory(StateData + 1,InputBuffer,Length & 0xffffffff); StateData->DataSize = LengtH; StateData->ChangeStamp = uVar5; [Truncated] __security_check_cookie(local_30 ^ (ulonglong)&stack0xffffffffffffff08); return; }
The InputBuffer and Length parameters to the function contain the contents and size of the data. It is important to note that these can be controlled by a user.
The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at [8]. If the StateData pointer is NULL (as is the case initially) at [9], or if the current size is lesser than the size of the new data at [10], memory is allocated from the Paged Pool for the new StateData pointer at [11]. It important to note that the size of allocation is the size of the new data (Length) plus 0x10, to account for the _WNF_STATE_DATA header. The Header and AllocateSize fields shown at [6] and [7] of the _WNF_STATE_DATA header are then initialized at [12].
Note that if the current StateData pointer is large enough for the new data, code execution from [8] jumps directly to [13]. Length bytes from the InputBuffer parameter are then copied into the StateData pointer at [13]. The DataSize field in the _WNF_STATE_DATA header is also filled at [13].
A WNF State Name can be deleted via a call to NtDeleteWnfStateName(). Among other things, this function frees the associated name instance and StateData buffers described above.
When WNF State Data is queried via a call to NtQueryWnfStateData(), ExpWnfReadStateData() is called internally to read from the StateData pointer. The pseudocode of ExpWnfReadStateData() is listed below.
undefined4 ExpWnfReadStateData(_WNF_NAME_INSTANCE *NameInstance,undefined4 *param_2,void *OutBuf,uint OutBufSize,undefined4 *param_5) { [Truncated] [14] StateData = NameInstance->StateData; if (StateData == NULL) { *param_2 = 0; } else { if (StateData != (_WNF_STATE_DATA *)0x1) { *param_2 = StateData->ChangeStamp; *param_5 = StateData->DataSize; [15] if (OutBufSize < StateData->DataSize) { local_48 = 0xc0000023; } else { [16] RtlCopyMemory(OutBuf,StateData + 1,(ulonglong)StateData->DataSize); local_48 = 0; } goto LAB_fffff8054ce2383f; } *param_2 = NameInstance->CurrentChangeStamp; }
The OutBuf and OutBufSize parameters to the function are provided by the user to store the queried data. The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at [14]. If the output buffer is large enough to store the data (which is checked at [15]), StateData->DataSize bytes starting right after the StateData header are copied into the output buffer at [16].
After the creation of a pipe, a user has the ability to add attributes to the pipe. The attributes are a key-value pair, and are stored into a linked list. The PipeAttribute object is allocated in the PagedPool, and has the following structure:
struct PipeAttribute { LIST_ENTRY list; char * AttributeName; uint64_t AttributeValueSize; char * AttributeValue; char data[0]; };
The size of the allocation and the data is fully controlled by an attacker. The AttributeName and AttributeValue are pointers pointing at different offsets of the data field. A pipe attribute can be created on a pipe using the NtFsControlFile syscall, and the 0x11003C control code.
The attribute’s value can then be read using the 0x110038 control code. The AttributeValue pointer and the AttributeValueSize will be used to read the attribute value and return it to the user.
Exploitation of the vulnerability involves the following steps:
We hope you enjoyed reading the deep dive into a use-after-free in CLFS, and if you did, go ahead and check out our other blog posts on vulnerability analysis and exploitation. If you haven’t already, make sure to follow us on Twitter to keep up to date with our work. Happy hacking!