By Arav Garg
Overview
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.
Preliminaries
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.
Metadata Blocks
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:
- Control Record that contains info about layout, extend area and truncate area.
- Base Record that contains symbol tables and info about the client, container and security contexts.
- Truncate Record that contains info on every client that needs to have sectors changed as a result of a truncate operation.
Shadow Blocks
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;
Control Record
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.
- Block Context: The control record ends with the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File. Although this is expected to be 6, there could potentially exist additional metadata blocks, and so for forward support, the cBlocks field
indicates the number of blocks in the array.
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.
CLFS In-Memory Class
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.
Code Analysis
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.
Opening a Log File
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:
- In all non-malicious cases, the current extend state is expected to be ClfsExtendStateNone(0) when the log file is written to disk.
- Since the Extend Context is under attacker control (described below), all the fields discussed above can be set by the attacker.
Reading Base Log File
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]).
Reading Control Record
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].
Acquiring Metadata Block
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].
Read Metadata Block
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].
Extending Metadata Block
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.
Exploitation
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.
Windows Notification Facility
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:
- The Windows Notification Facility The Most Undocumented Kernel Attack Surface Yet
- Playing with the Windows Notification Facility (WNF)
- CVE-2021-31956 Exploiting the Windows Kernel (NTFS with WNF) Part 1
Creating WNF State Name
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:
- StateName: Uniquely identifies the name instance, shown at [3].
- StateData: Stores the data associated with the instance, shown at [4].
- CreatorProcess: Stores the address of the _EPROCESS structure of the process that created the name instance, shown at [5].
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.
Updating WNF State Data
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].
Deleting WNF State Name
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.
Querying WNF State Data
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].
Pipe Attributes
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.
Steps for Exploitation
Exploitation of the vulnerability involves the following steps:
- Spray large number of Pipe Attributes of size 0x7a00 to use up all fragmented chunks in VS backend and allocate new ones. The last few will each be allocated on separate segments of size 0x11000, with the last (0x11000-0x7a00) bytes of each segment unused.
- Delete one of the later Pipe Attributes. This will consolidate the first 0x7a00 bytes with the remaining bytes in the rest of the segment, and put the entire segment back in the VS backend.
- Allocate the vulnerable chunk of size 0x7a00 by opening the malicious Base Log File. This will get allocated from the freed segment in Step 2. Similar to Step 1, the last (0x11000-0x7a00) bytes will be unused. The vulnerable chunk will be freed for the first time shortly afterwards. Similar to Step 2, the entire segment will be back in the VS backend.
- Spray large number of WNF_STATE_DATA objects of size 0x1000. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 3. Note that no size lesser than 0x1000 (and maximum is 0x1000 for WNF_STATE_DATA objects) can be used because that will have an additional header that will corrupt the header in the vulnerable chunk, blocking a double free.
- Free the vulnerable chunk for the second time. This will end up freeing the memory of one of the WNF_STATE_DATA objects allocated in Step 4, without actually releasing the object.
- Allocate a WNF_STATE_DATA object of size 0x1000 over the freed chunk in Step 5. This will create 2 entirely overlapping WNF_STATE_DATA objects of size 0x1000.
- Free all the WNF_STATE_DATA objects allocated in Step 4. This will once again put the entire vulnerable segment (of size 0x11000) back in the VS backend.
- Spray large number of WNF_STATE_DATA objects of size 0x700, each with unique data. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 7. Each page in the freed segment is now split as (0x700,0x700,0x1d0 (remaining)). Note, here size 0x700 can be used because the rest of the exploit doesn’t require any more freeing of the vulnerable chunk. This now creates 2 overlapping WNF_STATE_DATA objects, one of size 0x1000 (allocated in Step 6) and other of size 0x700 (allocated here). Size 0x700 is specifically chosen for 2 reasons. The first reason being that the additional chunk header (of size 0x10) in the 0x700-sized object means that the StateData header of the 0x1000-sized object is 0x10 bytes before the StateData header of the 0x700-sized object. Thus, the StateData header of the 0x700-sized object overlaps with the StateData data of the 0x1000-sized object.
- Update the StateData of the 0x1000-sized object to corrupt the StateData header of the 0x700-sized object such that the AllocatedSize and DataSize fields of the 0x700-sized object is increased from 0x6c0 (0x700-0x40) to 0x7000 each. Now, querying or updating the 0x700-sized object will result in an out-of-bounds read/write into adjacent 0x700-sized WNF_STATE_DATA objects allocated in Step 8.
- Identify the corrupted 0x700-sized WNF_STATE_DATA object by querying all of them with a Buffer size of 0x700. All will return successfully except for the corrupted one, which will return with an error indicating that the buffer size is too small. This is because the DataSize field was increased (refer to Step 9).
- Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to further identify the next 2 adjacent WNF_STATE_DATA objects using the OOB read. The first of these will be at offset 0x710 and the second will be at offset 0x1000 from the corrupted 0x700-sized WNF_STATE_DATA object.
- Free the second newly identified WNF_STATE_DATA object of size 0x700.
- Create a new process, which will run with the same privileges as the exploit process. The token of this new process is allocated over the freed WNF_STATE_DATA object in Step 12. This is the second reason for choosing size 0x700, as the size of the token object is also 0x700.
- Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to identify the contents of the token allocated in Step 13 using the OOB read. Calculate the offset to the Privileges.Enabled and Privileges.Present fields in the token*object.
- Update the corrupted 0x700-sized WNF_STATE_DATA object to corrupt the first adjacent object (identified in Step 11) using the OOB write. Increase the AllocatedSize and DataSize fields in the StateData pointer (refer to Step 9).
- Update the most recent corrupted WNF_STATE_DATA object (Step 15) to corrupt the adjacent token object using the OOB write. Overwrite the Privileges.Enabled and Privileges.Presentfields in the token object to 0xffffffffffffffff, thereby setting all the privileges. This completes the LPE.
Conclusion
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!