Skip to content

Latest commit

 

History

History
1297 lines (881 loc) · 47.4 KB

README.md

File metadata and controls

1297 lines (881 loc) · 47.4 KB

Since February 2022 was reported a new ransomware that appears to be using a Windows 0-day vulnerability, according to the research conducted by Trend Micro.
More information about this ransomware can be found at this link.
According to analysis by Kaspersky, the Nokoyawa ransomware group has used other exploits targeting the Common Log File System (CLFS) driver since June 2022, with similar but distinct characteristics, all linked to a single exploit developer.
In April 2023 when Microsoft released the patch, the CVE-2023-28252 as assigned.
Previously, in 2022 a similar bug in the same component was researched by us, and documented in this blogpost

Common Log File System (CLFS) file format:

To face the analysis, it’s necessary to know the .blf file format, that is handled by the vulnerable Common Log File System driver called CLFS.sys and that is in driver’s folder within system32.

More information about this filetype can be found in the links below:

https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part

https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-the-common-log-file-system

https://github.com/ionescu007/clfs-docs/blob/main/README.md

https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe

The vulnerability:

This analysis is made for Windows 11 21H2, clfs.sys version 10.0.22000.1574 although it also works on Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 and Windows server 2022.

In previous Windows versions, it’s necessary to adjust some values, otherwise we would produce a BSOD.

Microsoft Patch Tuesday april de 2023.

A screenshot of a computer Description automatically generated with medium confidenceYou can check the driver version as shown

When the vulnerability was published, in April 2023 I started with Esteban Kazimirow to perform the reversing of the CLFS.sys driver, although in this case, just analyzing the patch was very difficult to deduce where the bug was and how to trigger it, since the exploitation is very complex.

Later, a blogpost came out whose author, from a sample of a malware, showed some parts of the code decompiled by HexRays and some information that guided where the exploitation had to be faced.

Obviously the provided info was not complete, but without this help it would have been unlikely to have come to build the PoC and later a functional exploit.

To make it easier to understand, we will first explain how to build the PoC and then we will do the vulnerability analysis.

This blogpost contains two sections:

Building the PoC:

1-Get the kernel addresses we need for exploitation

2-Preparing the Path to create the .blf files:

3-Create the "trigger blf" file using the CreateLogFile() function

4-Crafting the “trigger blf” file

5-Getting the kernel address of the BASE BLOCK of trigger blf

6-Calling AddLogContainer with the handle of trigger blf

7-Preparing the spray blf files

8-Preparing the memory to perform the spray

9-Triggering the bug

Debugging:

1-Checking the memory spray

2-Looking at the RecordOffset[12] of trigger blf

3-Looking at the iFlushBlock value in spray blf file

4-Why does it read from BLOCK 1 SHADOW instead of BLOCK 0 CONTROL ?

5-Why the checksum is equal to zero in blf spray files ?

6-Ending the exploitation.

7-The real patch

Building the PoC:

1-Get the kernel addresses we need for exploitation

I’ll create a function named InitEnvironment to obtain some necessary Kernel addresses.

Get the EPROCESS address of my process and store it in the g_EProcessAddress variable, then the EPROCESS address of the SYSTEM process, and store it in system_EPROCESS, then the EHTREAD address of the main thread of my process, and I store it in g_EThreadAddress and finally the address of the PREVIOUS MODE that in this version of the PoC will not be used.

A screenshot of a computer code Description automatically generated with low confidence

This method is well known, the GetObjectKernelAddress function, calls NtQuerySystemInformation twice with the first argument SystemExtendedHandleInformation, the first call is passed with an incorrect size and returns error, but also returns the correct size that is used in the second call and obtains the information of all the handles, then going through in a loop the information of each handle and in the field Object of the correct handleinfo gets the address searched in kernel.

A picture containing text, screenshot, font, line Description automatically generated

I also need the kernel addresses of the following functions exported by CLFS.sys:

ClfsEarlierLsn

ClfsMgmtDeregisterManagedClient

And the exported functions from NTOSKRNL.exe

RtlClearBit/PoFxProcessorNotification

SeSetAccessStateGenericMapping

To get these addresses uses a similar method that is used to get the kernel base of both modules, by calling NtQuerySystemInformation twice, but in this case the first argument will be *SYSTEM_INFORMATION_CLASS (*in the PoC we use the FindKernelModulesBase function for this purpose).

A picture containing text, font, screenshot, line Description automatically generatedThen it loads CLFS.sys and NTOSKRNL.exe as normal modules in user mode by calling to LoadLibrary, obtains the addresses in user mode with GetProcAddress and then subtracts the imagebase from each one, which obtains the offset of the function and finally adds each offset to the corresponding kernel bases and thereby obtains the kernel addresses of all the necessary functions.

A picture containing text, font, line, screenshot Description automatically generated

2-Preparing the Path to create the .blf files:

I create a function called createInitialTriggerBlfFile which will generate and write a .blf file.

The path that is used as an argument in the CreateLogFile is different from a normal path, for example to open the file 1280.blf located in the C:\Users\Public folder, we must set the path LOG:C:\Users\Public\1280. This will be saved in the stored_name_CreateLog variable.

I do this by using wsprintfW() since stored_env stores the path C:\Users\Public, previously obtained from the environment variables. To this string I will prepend the string LOG: and a random name at the end, without the .blf extension.

A screenshot of a computer Description automatically generated with medium confidence

This will be the path to my initial file that I’ll call "trigger blf". Of course, I also must save the normal path to the same file without the LOG: in front and with the BLF extension to open it and modify it with CreateFile(), WriteFIle() as any other file, this path will be, for example: C:\Users\Public\1280.blf, and it will be stored in the stored_name_fopen variable**.**

A picture containing text, font, line, screenshot Description automatically generated

Of course, both paths correspond to the same file, and I must use one or the other as appropriate.

3-Create the "trigger blf" file using the CreateLogFile() function.

The CreateLogFile function fulfills a function quite similar to CreateFile() (creates new files or open existing files and get their handle), even some arguments are similar, but CreateLogFile() only works with blf files.

In addition, when it opens an existing file, it verifies that the format is ok, even if each block has a checksum and if this is not correct it will return an error.

I’ll create 2 kinds of BLF files:

  1. The Trigger blf

  2. The Spray blf

Both are blf files but modified in a different way.

A close-up of a computer code Description automatically generated with low confidenceIn this way the PoC first creates the "trigger blf" file, using CreateLogFile, with the path for example: LOG:C:\Users\Public\1280 that I have set up before, and was stored in the stored_name_CreateLog variable.

The fifth argument fCreateDisposition, as in CreateFileA(), can take the following values:

A picture containing text, font, line, receipt Description automatically generated

In this case I’ll use the OPEN_ALWAYS argument, so the file will be created if it does not exist and if it exists it will be opened. Since the file doesn't exist yet, it will be created with a random name.

logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);

CreateLogFile() will create our "trigger blf" file with its 6 blocks and their corresponding checksums and will return the handle that will be stored in the logFile variable.

A picture containing text, screenshot, font, number Description automatically generated

Each block will have from the offset showed at left column, a header whose size is 0x70 bytes.

So, for example, the header of the CONTROL BLOCK goes from offset 0x0 to 0x70.

A screenshot of a computer Description automatically generated with low confidence

All headers of all blocks have the same structure called _CLFS_LOG_BLOCK_HEADER.

This is the header structure:

A screenshot of a computer Description automatically generated with medium confidence

At offset 0xC of the header I can find the checksum, so as the CONTROL BLOCK starts at offset 0, the checksum will be in the offset 0xC of the file and so each block will have its checksum at 0xC from the beginning of its block.

A screenshot of a computer Description automatically generated with low confidence

4-Crafting the “trigger blf” file:

To modify the trigger blf file, I must open it as a normal file either with CreateFileA or with fopen and then modify it with WriteFile or fwrite respectively, I perform this at the beginning of the fun_prepare function of the PoC.

Remember that the normal path is stored in the stored_name_fopen variable, so I use it to open the file with wfopen_s (which is a variant of fopen that supports Unicode strings).

The file is modified in the craftTriggerBlfFile function called from fun_prepare.

A picture containing text, line, font, screenshot Description automatically generated

Then I call fseek to point to the offset to be changed and then with fwrite the file is modified.

A screenshot of a computer Description automatically generated with medium confidenceThe changes to be made to the "trigger blf" file are as follows:

After making these changes, the FixCRCFile is called to calculate the new checksum and fix the checksums of the first 4 blocks. The next two blocks do not have any changes, so it is not necessary to recalculate their checksums.

A picture containing text, font, screenshot, number Description automatically generated

5-Getting the kernel address of the BASE BLOCK of trigger blf:

The CLFS.sys driver reads the six blocks of the file, and to store their content makes an allocation in the Kernel pool.

A picture containing text, screenshot, font, number Description automatically generated

There’s a very important structure of size 0x90 that in the previous blogpost of CVE-2022-37969, through reversing I found some fields and called it pool_0x90. After much more reversing, now I know that its real name is m_rgBlocks and as the controller goes allocating memory to copy from the file the contents of each block, there it saves the size of each block, the start offset, and the kernel address where it was stored.

A picture containing text, screenshot, font Description automatically generated

It has six CLFS_METADATA_BLOCK that correspond to each block by its number.

Each structure CLFS_METADATA_BLOCK is 0x18 bytes long. (0x18*6=0x90)

A picture containing text, font, line, number Description automatically generatedIn offset 0 there is a union, but at least in this exploit only the pbImage field is used, so simplifying it would be:

The allocation of that structure can be done from two different places of CLFS.sys driver, according to the creation of a new file or if an existing one is opened. In the case of when a new file is created, the driver allocates the 0x90 bytes from CClfsBaseFilePersisted::CreateImage+28A, while in the case of an existing file it allocates from CClfsBaseFilePersisted: ReadImage+6E.

After that, I’ll get the start address of block 2 that corresponds to the trigger blf file, called BASE BLOCK that begins at offset 0x800 and its length is 0x7a00.

A picture containing text, screenshot, font, number Description automatically generated

Inside the fun_prepare function below this address will be found in kernel using this piece of the code.

A screenshot of a computer code Description automatically generated with low confidence

First, the getBigPoolInfo function finds all the allocations in the pool that have the "Clfs" tag and a size of 0x7a00, then stores them in an array.

After that it opens again the trigger blf file previously modified by using CreateLogFile with the OPEN_EXISTING argument, so it opens an existing file, this will perform the allocation of its BASE BLOCK.

When getBigPoolInfo is called again, there’ll be one new “Clfs” pool of size 0x7a00, and its address is retrieved by calling NtQuerySystemInformation twice.

The address of the BASE BLOCK of trigger blf file is stored in the CLFS_kernelAddrArray variable.

Note that if the modified trigger blf file does not have the correct checksum, the CreateLogFile() function will fail.

6-Calling AddLogContainer with the handle of trigger blf:

The last part of the fun_prepare function, calls the AddLogContainer api using the handle of the trigger blf file.

A close-up of a computer code Description automatically generated with low confidence

7-Preparing the spray blf files:

In the last function of the PoC called to_trigger a second type of blf file will be created,

I’ll name it spray blf.

This kind of file will be used to fill a memory space (spray), 10 equals of this kind are needed, but initially only one is created. A picture containing text, font, line, screenshot Description automatically generated

Three arrays will be created to store the random names of this files:

stored_log_arrays: store ten new random names of .blf files that will be used with CreateLogFile.

stored_container_arrays: store random names to create ten new container files.

stored_fopen_arrays: store the log files names of the first array (stored_log_arrays variable), but with their normal path (without the “LOG:” string) and with the .blf extension.

A screenshot of a computer code Description automatically generated with medium confidence

On each iteration the blf file is copied using CopyFileW, the names that are stored in the arrays are assigned.

The fun_trigger function calls craftSprayBlfFile where modifications are made to each file and FixCRCFile will fix the CRCs.

A screenshot of a computer program Description automatically generated with medium confidence

Summarizing, I’ve created 10 similar files (spray blf) with random names with the following modifications:

A screenshot of a computer Description automatically generated with medium confidence

The last change is to copy the entire block 0 (CONTROL BLOCK) to block 1 (CONTROL BLOCK SHADOW)

A screen shot of a computer Description automatically generated with low confidence

The effect of these changes, plus those made to the trigger blf file, will be explained later in the debugging chapter.

Some of these changes are those that produce vulnerability, while others are only necessary to bypass the driver checks.

At this point the files are already created and modified, ready to perform the spray, then when they are opened with CreateLogFile, they will be located in the memory area that we want, as will show later.

8-Preparing the memory to perform the spray

In the to_trigger function, an array of 12 elements is created, containing the address of the BASE BLOCK of trigger blf file plus 0x30.

Then, in the fun_pipeSpray function, the memory is filled with a spray of pipes, inside there’s a loop that calls to CreatePipe and creates the number of pipes that is passed as a first argument, the second argument is an array that will store the handles of all the pipes created.

Within a loop, it calls to CreatePipe creating read-write pipes.

In this way first 0x5000 pipes will be created and then call again to create other 0x4000 pipes.

Then uses WriteFile to write to the first 5000 pipes, the array recently created with the addresses of BASE BLOCK + 0x30 of the trigger blf file**.**

A screenshot of a computer code Description automatically generated with low confidence

Now already has a compact block created in memory, it will release 0x667 pipes from the number 0x2000 and up to the 0x2667, since in memory the pipes are not in the same order as were created, what will happen is that there will be free spaces in this memory block.

A picture containing text, screenshot, font, software Description automatically generatedNote that the allocations of the pipes have as user size of 0x90 bytes, so when be released we’ll have

It frees the memory spaces of size 0x90 between the memory full of pipes.
Then it loops to call CreateLogFile with the 10 spray blf files.
When CreateLogFile is called to open existing files, the allocation of 0x90 bytes is performed for the m_rgBlocks one for each spray blf file**,** so these allocations will occupy gaps that were left when releasing the pipes since they are the same size.

A screenshot of a computer code Description automatically generated with medium confidence

Then repeat the process of writing in the final 0x4000 pipes the array that has the address of BASE BLOCK +0x30 of trigger blf.

9-Triggering the bug

All these manipulations creates a controlled memory space, I will show you how it is when is being debugged, but the idea is that the m_rgBlocks of each spray blf file occupy the 0x90 byte gaps that were released.

Then already in the final part, the bug is triggered within a while( 1 ) using a call to AddLogContainer to the spray blf files.

A picture containing text, font, line, number Description automatically generated

Within this while the bug is triggered:

A screenshot of a computer program Description automatically generated with low confidence

This while will exit when it finds the System token, using the NtFsControlFile function that will read the pipes attributes.

A screenshot of a computer code Description automatically generated with low confidence

Then using CreateLogFile, again overwrites the token of our process with the recently found System Token and in this way we achieve the elevation of privilege.

A picture containing text, font, line, screenshot Description automatically generated

Then restore some values, close the handles of the pipes and the blf files, and run a Notepad as System to verify that we have raised correctly.

A screenshot of a computer Description automatically generated with medium confidence

Note the blf files created on the PUBLIC folder. Remember that if you want to do another try, you must first delete the created files. some will be locked and cannot be deleted, but the PoC will still work.

A screenshot of a computer Description automatically generated with medium confidence

Debugging:

1- Checking the memory spray

Before I start with the effect of changes to trigger blf and spray blf files to perform the exploitation, I must verify that m_rgBlocks of spray blf files are located in holes that occur in memory distribution, after performing the pipe spray and the subsequent release of a fixed number of pipes.

When this procedure ends, a pipe should be located under the 0x90 bytes of m_rgBlocks, so when m_rgBlocks is used, an OUT OF BOUNDS will occur and it will read from that pipe that is below.

The PoC has an ideal point to place a breakpoint:

A screen shot of a computer code Description automatically generated with low confidence

At this point, the opening of spray blf files is complete and the AddLogContainer function is still not called.

To debug in user mode, I will use x64dbg and for kernel mode, IDA with the Windbg plugin.

A screen shot of a computer Description automatically generated with low confidence

At this point the memory should already be prepared, and I can see the distribution.

I’ll pause IDA to find an interesting point to put a breakpoint.

I’ll set up a breakpoint at CClfsBaseFilePersisted::AddContainer, which is called from AddLogContainer and at the beginning, it has the RCX register pointing to CClfsBaseFilePersisted structure and at offset 0x30 there’s a pointer to m_rgBlocks.

A screen shot of a computer Description automatically generated with medium confidence

When the breakpoint is reached, I check on call stack that AddLogContainer is being called from my PoC.

A screenshot of a computer Description automatically generated with medium confidence

the RCX register points to:

A screenshot of a computer Description automatically generated with low confidence A screenshot of a computer Description automatically generated with low confidence

The first field is the pointer to a vtable (CLFS! CClfsBaseFilePersisted::'vftable') and at offset 0x30 is the pointer to m_rgBlocks.

A screenshot of a computer code Description automatically generated with medium confidence

The blocks 0, 1, 4 and 5 have not saved the pbImage yet, while blocks 2 (BASE BLOCK) and 3 (SHADOW BLOCK) have.

Each block in m_rgBlocks table has its cbOffset which is the offset where the block starts in file, cbImage is the block size, and eBlockType is the block type.

If the spray is correct, below the m_rgBlocks there should be a pipe and within, the pointers to BASE BLOCK + 0x30 of trigger blf.

A screenshot of a computer code Description automatically generated with low confidence

The "!pool" command on windbg displays the memory distribution:

A screenshot of a computer Description automatically generated with medium confidence

Each m_rgBlocks has a “Clfs” tag and its size is 0xa0 because it is the 0x90 user size plus 0x10 header and below there’s a pipe with the “NpFr” tag that has the same 0x90 user size + 0x10 header.

Since distribution isn't an exact science, some “Clfs” were placed continuously, which is undesirable, but the one I'm working with, is correctly placed followed by a pipe.

2-Looking at the RecordOffset[12] of trigger blf

One of the first changes that affects is the one made in trigger blf file at offset 0x858, where the value 0x369 is stored.

A close-up of a sign Description automatically generated with low confidence

The BASE BLOCK starts at the offset 0x800 in the file.

A picture containing text, screenshot, font, line Description automatically generated

Inside the _CLFS_LOG_BLOCK_HEADER at offset 0x800+0x58 (0x58 from the beginning of BASE BLOCK header).

A picture containing text, screenshot, font, display Description automatically generated

At offset 0x28 the array RecordOffsets (DWORD) begins.

Moving 0x30 bytes forward, at offset 0x58 (0x828+0x30=0x858 from the beginning), is field 12 of RecordOffsets.

A picture containing text, font, screenshot, graphics Description automatically generated

I run the PoC to CreateLogFile as shown in the image below:

A screenshot of a computer code Description automatically generated with low confidence A screenshot of a computer code Description automatically generated with low confidence

A screenshot of a computer Description automatically generated

Before I enter to CreateLogFile I'm going to put a breakpoint in a place where value 0x369 hasn't been used yet.

In a case that CreateLogFile opens an existing file, the m_rgBlocks structure is allocated here:

CClfsBaseFilePersisted::ReadImage+6E

So, I’ll set a breakpoint on IDA right here:

A screenshot of a computer Description automatically generated with medium confidence

When breakpoint is triggered**:**

A picture containing text, screenshot, font, number Description automatically generated

In m_rgBlocks there is still some garbage because it’s still uninitialized, but as soon as pbImage of block 2 is allocated, the address will be saved in offset 0x30 from the start, since the first field inside each CLFS_METADATA_BLOCK is pbImage.

A screen shot of a computer Description automatically generated with medium confidence

A picture containing text, screenshot, font, line Description automatically generated

Now I set up a hardware breakpoint on write: ba w1 ffffd003'7f5bea30

After initializing to zero, it stops when it saves pbImage.

The analysis says that it corresponds to block0, because it does not consider the constant r14*8 which is 0x30 afterwards, as a result is really writing the pbImage of block 2.

A picture containing text, screenshot, font Description automatically generated

Note that CClfsBaseFilePersisted::ReadMetadataBlock is used to allocate any of the blocks, using the size passed as argument.

A picture containing text, font, screenshot, line Description automatically generated

Now set a read/write breakpoint at 0x58 from the base block, to see when it uses the value 0x369.

ba r1 FFFF978A'16ECF000+0x58

When the breakpoint is hit, reads the value 0x369 located at the RecordOffset[12], adds it to a weird pointer on r14 and increments the contents of RAX+r14.

A few lines above in the code, ESI has the value 0x13 and multiplies by 0x18, which is the size of each block in m_rgBlocks.

WINDBG>? 0x18*0x13

Evaluate expression: 456 = 00000000'000001c8

If I add the value of r8= 0x1c8 that is greater than 0x90, to the initial address of m_rgBlocks, it’ll be reading OUT OF BOUNDS.

A screenshot of a computer Description automatically generated

Below m_rgBlocks, is the pipe with the pointer to BASE BLOCK + 0x30, it reads this pointer that was strategically placed inside the pipe.

A screenshot of a computer program Description automatically generated with low confidenceThe current position in the code was called from the while(1) statement of main module.

Inside spray blf file I’ve strategically placed the value 0x13 at offset 0x48a (iFlushBlock).

A picture containing text, font, line, screenshot Description automatically generated

3-Looking at the iFlushBlock value in spray blf file.

At offset 0x8a of spray blf file, iFlushBlock of BLOCK 0 is located, whose value is 4, while offset 0x48a belongs to iFlushBlock of BLOCK 1, and its value is 0x13

A screenshot of a computer Description automatically generated with medium confidence

Now I have to find out why it reads iFlushBlock = 0x13 from BLOCK 1 instead of iFlushBlock = 4 from BLOCK 0.

4-Why does it read from BLOCK 1 SHADOW instead of BLOCK 0 CONTROL?

If I look back to find out where the 0x13 came from, I see on call stack that WriteMetadataBlock is called from CClfsBaseFilePersisted::ExtendMetadataBlock+416, there the second iFlushBlock argument is EDX=0x13, which comes from r9w.

A screenshot of a computer code Description automatically generated with low confidence

A picture containing text, screenshot, font, line Description automatically generated

A screenshot of a computer program Description automatically generated with low confidenceA couple of lines before, CClfsBaseFile::GetControlRecord was called to retrieve the address of BLOCK 0, maybe the problem is here, so I'll reboot and put a breakpoint on it.

GetControlRecord calls CClfsBaseFile::AcquireMetadataBlock who should fill the m_rgBlocks table with the address of block 0, when I step over this function gets the address of block 1, so, the problem occurs inside CClfsBaseFile::AcquireMetadataBlock.

By adding 0x8A to the address retrieved, I can confirm that the 0x13 value that belongs to BLOCK 1 is present.

A screenshot of a computer code Description automatically generated with medium confidence

I will reboot and set a breakpoint there:

A screenshot of a computer program Description automatically generated with low confidence

CClfsBaseFile::GetControlRecord+27 call CClfsBaseFile::AcquireMetadataBlock

A screenshot of a computer Description automatically generated

The second argument passed to AcquireMetadataBlock is zero, it corresponds to block 0, it is going to copy from the file and store its address in m_rgBlocks A screenshot of a computer Description automatically generated with medium confidence.

In _CLFS_METADATA_BLOCK_TYPE block type enumeration, they have different names than I used, but they are the same 6 blocks.

A picture containing text, screenshot, font, line Description automatically generated

After checking that the block type is less than the maximum m_cBlocks=6, it saves a reference value to avoid reading the same block two times.

A screenshot of a computer code Description automatically generated with low confidence

ReadMetadataBlock is called, the problem of reading block 1 instead of block 0 would be inside this function.

A picture containing text, font, number, line Description automatically generated

If everything is fine, it allocates using cbImage as size and it stores the address in field block 0-> pbImage in m_rgBlocks.

A picture containing text, font, line, screenshot Description automatically generated

The !pool command displays the tag and size allocated.

A picture containing text, screenshot, font, line Description automatically generated

A picture containing text, screenshot, font, line Description automatically generatedSo, I already have the address of pbImage of block 0 stored in m_rgBlocks, so I need to see why it copies the bytes of block 1 there instead of bytes of block 0.

I get to a call to CClfsContainer::ReadSector where a pointer to a variable containing pbImage is passed, to write the bytes.

Notice the changes made in pbimage content when stepping over ReadSector.

Adding 0x8a to pbImage I can find the value 4 which is correct value, instead of 0x13, so the problem must occur later.

After calling ClfsDecodeBlock It returns an error 0x0C01A000A.

CClfsBaseFilePersisted::ReadMetadataBlock+153 calls to ClfsDecodeBlock

After this error, it adds 1 to the type and calls CClfsBaseFilePersisted::ReadMetadataBlock again but with type 1 to read block 1.

A screenshot of a computer Description automatically generated

In CClfsBaseFilePersisted::ReadMetadataBlock It allocates and stores a new pbImage in m_rgBlocks for block 1.

A screenshot of a computer code Description automatically generated with low confidence

Blocks 0 and 1 have different addresses, now if I add 0x8a to the address of block 1 its value is 0x13.

Maybe since block 0 returned an error, it uses block 1 and returns it to GetControlRecord as Control Block.

As shown before, when it uses 0x13 value instead of 4, it goes outside the bounds of m_rgBlocks and reads the pipe spray values controlled by me.

A picture containing text, screenshot, font Description automatically generatedThen it frees the pbImage from block 0 and it copies the pointer from block 1 to block 0.

A screenshot of a computer Description automatically generated

It would be necessary to find the value that causes the error 0x0C01A000A inside ClfsDecodeBlock.

Inside ClfsDecodeBlock the checksum of the first block is zero, this is the error 0xC01A000A.

A screenshot of a computer Description automatically generated with medium confidence

5-Why the checksum is equal to zero in blf spray files?

Before calling to AddLogContainer, opening any spray blf file with a hexadecimal editor, the checksum was changed to zero.

A screenshot of a computer Description automatically generated

it should have been changed before when it was opened with CreateLogFile.

A picture containing text, screenshot, display, font Description automatically generated

For some reason spray blf files end up after exiting CreateLogFile with checksum of block 0 equal to 0 and return a valid handle, let's see why this happens.

I stop at CreateLogFile before opening some spray blf file.

A screenshot of a computer Description automatically generated with medium confidence

Note that before calling CreateLogFile, spray files have the correct checksum in block 0 and after completing the function, the checksum value changes to zero.

A screenshot of a computer code Description automatically generated with low confidence

So, I set a breakpoint on CClfsBaseFile::GetControlRecord, to look inside.

A picture containing text, screenshot, font, line Description automatically generatedAfter passing CClfsContainer::ReadSector the checksum is not zero.

Before entering to calculate the CRC32, it puts the checksum field to zero in memory to calculate the CRC, and the result is correct.

A picture containing text, font, screenshot Description automatically generated

A picture containing text, font, line, screenshot Description automatically generated

Then it checks the value of eExtendState =2 and it goes to WriteMetadataBlock.

A screenshot of a computer Description automatically generated with medium confidence

Here the checksum is still zero in memory, I just need to see when this value is written in the file.

A picture containing text, font, line, screenshot Description automatically generated

It checks some values that are crafted in blf spray file to reach CClfsBaseFilePersisted::ExtendMetadataBlock.

A screenshot of a computer program Description automatically generated with medium confidence

After a loop to read blocks that have not been read yet, block 0 continues with checksum = 0.

A white rectangle with black text Description automatically generated with low confidence

Arriving at WriteMetadataBlock.

Since I'm running before it replaces block 0 with 1, the iFlushBlock value of the blf spray file is still 4 the correct value.

A screenshot of a computer Description automatically generated with medium confidence

Now it’s working with block 4, and it will write block 4 in file, here is not the problem yet.

Then it comes to CClfsBaseFilePersisted::FlushControlRecord

A screenshot of a computer program Description automatically generated with medium confidence

Inside it reaches WriteMetadataBlock, but with argument 0, to write block 0 to file**.**

A screenshot of a computer Description automatically generated with medium confidence

Then the ClfsEncodeBlock returns error 0xC01A000A, although it will write the file with the bad block 0 in CClfsContainer::WriteSector, just below.

A screenshot of a computer Description automatically generated with medium confidence

The variable var_54 stores the 0xC01A000A error value and will be checked before exiting the function.

A screenshot of a computer Description automatically generated with medium confidence

But after calling CClfsContainer::WriteSector which returns no error, the content of var_54 is overwritten with zero.

So, the function returns zero with no error and it continues working since CreateLogFile will return a handle instead of an error value.

A screenshot of a computer Description automatically generated

6-Ending the exploitation.

The value 0x13 in iFlushBlock causes it to go out of bounds and it will read the pointer that is in the pipes that points to the Base Block +30 of trigger blf.

A screenshot of a computer Description automatically generated

Then it adds 0x28 to that pointer, ( 0x58 from the beginning of the base block of the trigger blf) that has the value 0x369.

A screenshot of a computer program Description automatically generated with medium confidence

The INC instruction will increase the value 0x14 by 1 and repeats 4 times, so 0x14 ends to 0x18.

WINDBG>db r14+369

ffffcb82'091e7397 14 00 00 00

After that, CreateLogFile is called, and reads the 0x1858 value.

A screenshot of a computer Description automatically generated with medium confidence

A close-up of a card Description automatically generated with low confidenceGetSymbol checks if the fake block previously created in trigger blf, pointed by the offset 0x1858, has the correct values.

A picture containing text, font, line, screenshot Description automatically generated

if the pointer had not been incremented several times, it would have the original value 0x1458 and will point to the right block.

After exit GetSymbol, it will use that fake block here.

A screenshot of a computer Description automatically generated with medium confidence

Then it will read the value of offset 0x18 of fake block where I place 0x05000000 and jump to content of what is there.

WINDBG>dps 0x5000000

00000000'05000000 00000000'05001000

It reads the content of 0x05000000 and its 0x05001000 and there it is ClfsEarlierLsn.

A screenshot of a computer program Description automatically generated with low confidence

This function is used to return the value 0xFFFFFFFF in RDX although this first time that value is not used.

The second call occurs here, it calls PoFxProcessorNotification which was on 0x501000 +8

A picture containing text, font, screenshot, line Description automatically generated A picture containing text, font, screenshot, line Description automatically generated

WINDBG>dps 00000000**'05001000**

00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn

00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

A screenshot of a computer screen Description automatically generated with low confidence

in this function RCX = 0x05000000 , it checks that 0x40 bytes later must be nonzero

WINDBG>dps rcx+40

00000000'05000040 00000000'05000000

The address to jump will be 0x68 later.

WINDBG>dps rcx+68

00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient

And the argument will be 0x48 bytes later.

WINDBG>dps rcx+48

00000000'05000048 00000000'05000400

The ClfsMgmtDeregisterManagedClient, it's a convenient function because I can control the argument and I also have two jumps to functions controlled by me.

A screenshot of a computer Description automatically generated

The first call is again to ClfsEarlierLsn that returned in RDX=0xFFFFFFFF.

A screenshot of a computer Description automatically generated with medium confidence

A screen shot of a computer Description automatically generated with medium confidence

it will take the source to write from the content of RDX=0xFFFFFFFF.

WINDBG>dps rdx

00000000'ffffffff ffff8005'3a4ee000

At address 0xFFFFFFFF I had stored the system_EPROCESS & 0xfffffffffffff000.

A picture containing text, font, line, number Description automatically generated

The destination is the pointer located at 0x5000400 +0x48

*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

A screenshot of a computer Description automatically generated

The PipeAttribute pointer in kernel that points to a buffer filled with “A” will be overwritten with the high part of the SYSTEM EPROCESS pointer.

This pointer was created when I previously called _NtFsControlFile with a buffer full of “A” .

A screenshot of a computer program Description automatically generated with medium confidence

The content of that attribute can be read using NtFsControlFile.

A screenshot of a computer screen Description automatically generated with medium confidence

Now the pipe attribute no longer points to the buffer with “A” but to system_EPROCESS & 0xffffffffffffff000.

A screenshot of a computer program Description automatically generated with low confidence

This code will be repeated until the system token is retrieved.

A screenshot of a computer code Description automatically generated with medium confidence

A screenshot of a computer Description automatically generated

On windows 11 the system token is at offset 0x4b8 of the EPROCESS structure recently read.

A screenshot of a computer Description automatically generated

I only need to write that system token in my process by calling CreateLogFile.

A picture containing text, font, line, screenshot Description automatically generated

To do this job, just repeat the step used to read the system token.

A screenshot of a computer Description automatically generated with medium confidence

In the double call, it first calls ClfsEarlierLsn to return 0xFFFFFFFF in RDX and then calls nt_SeSetAccessStateGenericMapping.

A picture containing text, screenshot, font, line Description automatically generated

I check that the value pointed by RDX is the System Token.

A picture containing text, screenshot, font Description automatically generated

The token of my process is:

It’s going to write there.

WINDBG>dps rax+8

ffff9b8b'fc446578 ffffc402'f601c06c

WINDBG>dps rax+8

ffff9b8b'fc446578 ffffc402'ef841919

Now my process is System I can run a Notepad to verify.

A picture containing text, screenshot, font, line Description automatically generated

A screenshot of a computer Description automatically generated

A screenshot of a computer Description automatically generated

7-The real patch

BINDIFF shows a lot of changed functions

A screenshot of a computer Description automatically generated

The vulnerable function is here:

A screenshot of a computer Description automatically generated with medium confidence

A screenshot of a computer Description automatically generated

The primary is the patched version, the secondary is the vulnerable version.

The patch tests the return value of CflsEncodeBlock, which is 0xC01A000A, stores it into the variable var_54, and since it is negative, checks it and avoids the WriteSector.

The patch, in addition to not writing the file, the function returns correctly 0xc01a000a, with which CreateLogFile does not return any handle and the exploitation cannot continue.

A screenshot of a computer Description automatically generated with medium confidence

A screenshot of a computer Description automatically generated

Only if ClfsDecodeBlock is not negative, it goes to WriteSector but leaves returning the negative value 0xC01A000A.

A screenshot of a computer Description automatically generated with medium confidenceThis is the actual patch that really prevents the exploitation using the PoC that I just attached.

At this point we have explained how the bug was exploited, it leads to controlling the functions that allows us to read the SYSTEM token and write it in our own process to achieve the local privilege escalation. You can find the functional PoC at Fortra’s GitHub.

We hope you find it useful, if you have any doubt can contact us:

[email protected] 
@ricnar456

 [email protected]
@solidclt