About this vulnerability: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-36900.
What is CLFS.sys: 1, 2
The function at address 0x1C005107C has been patched:
This cmp has been added:
After the cmp instruction, the small block of code is executed and function goes to return:
The small block of code in disassembler:
And in decompiler:
This code is typical for drivers. It is WPP tracing.
So it is not interesting to us. But we should pay attention to the return code. It is 0xC0000095 that means STATUS_INTEGER_OVERFLOW.
That's a good hint for us.
Pseudocode of the patched function without details:
MACRO_STATUS CClfsBaseFilePersisted::ExtendMetadataBlockDescriptor(
CClfsBaseFilePersisted *this,
CLFS_METADATA_BLOCK_TYPE *iFlushBlock,
unsigned int num_of_extendSectors_div_2)
{
// size_of_extend_sectors_div2 = num_of_extendSectors_div_2 * 0x200u
if ( RtlULongMult(num_of_extendSectors_div_2, 0x200u, &size_of_extend_sectors_div2) < 0 )
return 0xC01A000Di64;
//size_of_flushBlockSectors_and_extendSecorsDiv2 = size_of_extend_sectors_div2 * this->CClfsBaseFile.m_rgBlocks[*iFlushBlock].cbImage
sum_ntstatus = RtlULongAdd(
size_of_extend_sectors_div2,
this->CClfsBaseFile.m_rgBlocks[*iFlushBlock].cbImage,
&size_of_flushBlockSectors_and_extendSecorsDiv2);
if ( sum_ntstatus < 0 )
return 0xC01A000Di64;
m_cbRawSectorSize = this->CClfsBaseFile.m_cbRawSectorSize;// always 512 (0x200)
// Round the number up
if ( m_cbRawSectorSize )
rounded_sum = -m_cbRawSectorSize & (m_cbRawSectorSize + size_of_flushBlockSectors_and_extendSecorsDiv2 - 1);
else
rounded_sum = 0;
// Cmp that was added by the patch
if ( rounded_sum >= size_of_flushBlockSectors_and_extendSecorsDiv2 )
{/* main body of the funcion */}
else{
/* something with WPP */
return STATUS_INTEGER_OVERFLOW;
}
...}
The vulnerability is that the variable rounded_sum
may be less than size_of_flushBlockSectors_and_extendSecorsDiv2
.
If size_of_flushBlockSectors_and_extendSecorsDiv2
will be great than 0xffffffff - 0x200 + 1
we'll end up in the 'else' block and leave
the function with STATUS_INTEGER_OVERFLOW.
But in the unpatched version, we will continue execution and get BSOD.
The variable size_of_flushBlockSectors_and_extendSecorsDiv2
depends on the value of size_of_extend_sectors_div2
and the value of iFlushBlock
. Both of them are function parameters. So, we need to manipulate these parameters so that we end up in the 'else' block.
This function is called only from two other functions:
- CClfsBaseFilePersisted::ProcessCurrentBlockForExtend
- CClfsBaseFilePersisted::ExtendMetadataBlock
The arguments submitted to the function are identical in both cases:
status = CClfsBaseFilePersisted::ExtendMetadataBlockDescriptor(
this,
control_record->iFlushBlock,
control_record->cExtendSectors / 2);
That is control_record in this call?
The clfs.sys driver keeps data about it's logs in files with .blf extension. When you call CreateLogFile
WinAPI function, clfs.sys creates a new log file. This file contains an area called control record. The control record contains information about the layout of the blf file.
Here we see the fields that are passed as arguments to the vulnerable function. How this fields are used in the function? The field cExtendSectors
is divided by two when submitted to the function and multiplied by 0x200 in the function itself:
RtlULongMult(num_of_extendSectors_div_2, 0x200u, &size_of_extend_sectors_div2)
The field iFlushBlock
is used as an index in rgBlocks
array that reside in control record too, but in the code below, it is already loaded into the CClfsBaseFile
structure, that is an in-memory representation of the blf file.
RtlULongAdd(size_of_extend_sectors_div2,
this->CClfsBaseFile.m_rgBlocks[*iFlushBlock].cbImage,
&size_of_flushBlockSectors_and_extendSecorsDiv2);
A blf file consists of six blocks, each with its own purpose. The rgBlocks
array stores the location and size of the blocks. The cbImage
field sets the size of the block in the file.
So, in the code above we multiply size of the block that has been determine by iFlushBlock
field and value that we get from operations with cExtendSectors
.
Then, the obtained value is rounded up to the disk sector boundary (512 bytes):
m_cbRawSectorSize = this->CClfsBaseFile.m_cbRawSectorSize;// always 512 (0x200)
// Round the number up
if ( m_cbRawSectorSize )
rounded_sum = -m_cbRawSectorSize & (m_cbRawSectorSize + size_of_flushBlockSectors_and_extendSecorsDiv2 - 1);
else
rounded_sum = 0;
And if we run unpatched version of clfs.sys, the rounded up value will be passed to ExAllocatePoolWithTag
function, what will allocate kernel memory. Then from block with index iFlushBlock
into the new memory will be copied rgBlock[iFlushBlock].cbImage
number of bytes.
allocated_mem = ExAllocatePoolWithTag(PagedPoolCacheAligned, rounded_up_size, 'sflC');
memmove(allocated_mem, pbImage, cbImage);
But if value of size_of_flushBlockSectors_and_extendSecorsDiv2
is 0xfffffe01
, it will be rounded up to 0. And 0 will be passed to ExAllocatePoolWithTag. Then ExAllocatePoolWithTag will allocate number of bytes less than rgBlock[iFlushBlock].cbImage
, because the minimum value of rgBlock[iFlushBlock].cbImage
is 0x200. So, chunks that lie after the allocated chunk will be corrupted. But that's not all. Then the following code will be called:
difference = rounded_up_size - cbImage;
memset(allocated_mem + rgBlocks[iFlushBlock].cbImage, 0, difference);
It means that 0 - 0x200 = 0xfffffe00
additional kernel memory will be corrupted.
All paths to the vulnerable function starts from CClfsRequest::Create
function.
This function is called when we use CreateLogFile
WinAPI. When we open a log first time after it's creation, we can reach the vulnerable function via this path:
CClfsRequest::Create->
CClfsLogFcbPhysical::Initialize ->
CClfsBaseFilePersisted::OpenImage ->
CClfsBaseFilePersisted::ExtendMetadataBlock ->
CClfsBaseFilePersisted::ExtendMetadataBlockDescriptor
The function CClfsBaseFilePersisted::OpenImage
has the following constraints:
if ( control_record->eExtendState == ClfsExtendStateNone ) // ClfsExtendStateNone = 0
goto END;
iExtendBlock = control_record->iExtendBlock;
if ( iExtendBlock )
{
m_cBlocks = this->CClfsBaseFile.m_cBlocks;// = 6
if ( iExtendBlock < m_cBlocks && ((iExtendBlock - 2) & 0xFFFD) == 0 )
{
iFlushBlock = control_record->iFlushBlock;
if ( iFlushBlock )
{
if ( iFlushBlock < m_cBlocks
&& iFlushBlock < 6u
&& iFlushBlock >= iExtendBlock
&& control_record->cExtendStartSectors <= CClfsBaseFile::GetSize(this) / 0x200 )
{
cExtendSectors_count = control_record->cExtendSectors / 2;
if ( control_record->cNewBlockSectors <= cExtendSectors_count
+ (this->CClfsBaseFile.m_rgBlocks[control_record->iExtendBlock].cbImage / 0x200) )
{
status = CClfsBaseFilePersisted::ExtendMetadataBlock(
this,
control_record->iExtendBlock,
cExtendSectors_count);
status_1 = status;
goto END;
}
}
}
}
}
So, to reach the vulnerable function we need to change in the blf file the following values:
eExtendState = 2
iExtendBlock = 4
iFlushBlock = 5
cNewBlockSectors = 2
To get BSOD we will set cExtendSectors
to 0xfffffc
and rgBlock[5].cbImage
to 0x201
.
An another constraint in this call:
CClfsBaseFilePersisted::ExtendMetadataBlock ->
CClfsBaseFile::AcquireMetadataBlock ->
CClfsBaseFilePersisted::ReadMetadataBlock
It prevents us to set rgBlock[5].cbImage
to 0x201
because it compares blocks. The blf file contain three types of blocks, and one shadow block for each type:
- Control Metadata Block
- Shadow Control Metadata Block
- General Metadata Block
- Shadow General Metadata Block
- Scratch Metadata Block
- Shadow Scratch Metadata Block
A shadow block is a backup copy of the original block. When clfs.sys sees that a block and its shadow copy are different, it selects the older block. So, the block rgBlock[5]
is shadow copy of the rgBlock[4]
. If we want to set rgBlock[5].cbImage
to 0x201
, we need to make this block older. To do this, we will change the first byte after its CLFS_LOG_BLOCK_HEADER
(each block starts with this structure) to a value, that is greater than the first byte after CLFS_LOG_BLOCK_HEADER
of the block rgBlock[4]
. And we need to copy CLFS_LOG_BLOCK_HEADER
of rgBlock[4]
to rgBlock[5]
.
After all changes we also need to recalculate the checksum of each block. It can be found in the CLFS_LOG_BLOCK_HEADER
of each block.
And with new crafted blf file we can get BSOD.