Share
## https://sploitus.com/exploit?id=CB1EE1A3-A8A5-5B5B-AB0A-63C1AD8E73B1
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](https://www.securityweek.com/windows-zero-day-exploited-in-nokoyawa-ransomware-attacks/).  
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](https://vulners.com/cve/CVE-2023-28252)
as assigned.  
Previously, in 2022 a similar bug in the same component was researched
by us, and documented in [this
blogpost](https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe)

# 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](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-28252).

![A screenshot of a computer Description automatically generated with
medium confidence](./media/image1.png)You 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*](https://ti.qianxin.com/blog/articles/CVE-2023-28252-Analysis-of-In-the-Wild-Exploit-Sample-of-CLFS-Privilege-Escalation-Vulnerability/)
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](./media/image2.png)

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](./media/image3.png)

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 generated](./media/image4.png)Then 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](./media/image5.png)

# 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](./media/image6.png)

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](./media/image7.png)

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 confidence](./media/image8.png)In 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](./media/image9.png)

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](./media/image10.png)

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](./media/image11.png)

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](./media/image12.png)

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](./media/image13.png)

# 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](./media/image14.png)

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 confidence](./media/image15.png)The 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](./media/image16.png)

# 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](./media/image10.png)

There’s a very important structure of size 0x90 that in the previous
[blogpost of
CVE-2022-37969](https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe),
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](./media/image17.png)

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 generated](./media/image18.png)In 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](./media/image19.png)

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](./media/image20.png)

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.

![](./media/image21.png)

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](./media/image22.png)

# 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](./media/image23.png)

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](./media/image24.png)

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](./media/image25.png)

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](./media/image26.png)

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](./media/image27.png)

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

![](./media/image28.png)

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.

![](./media/image29.png)

![](./media/image30.png)Within a loop, it calls to **CreatePipe**
creating read-write pipes.

![](./media/image31.png)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](./media/image32.png)

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 generated](./media/image33.png)Note 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](./media/image34.png)

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](./media/image35.png)

Within this while the bug is triggered:

![A screenshot of a computer program Description automatically generated
with low confidence](./media/image36.png)

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](./media/image37.png)

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](./media/image38.png)

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](./media/image39.png)

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](./media/image40.png)

# 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](./media/image41.png)

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](./media/image42.png)

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](./media/image43.png)

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](./media/image44.png)

the **RCX** register points to:

![A screenshot of a computer Description automatically generated with
low confidence](./media/image45.png) ![A screenshot of a computer
Description automatically generated with low
confidence](./media/image45.png)

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](./media/image46.png)

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](./media/image47.png)

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

![A screenshot of a computer Description automatically generated with
medium confidence](./media/image48.png)

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](./media/image49.png)

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

![A picture containing text, screenshot, font, line Description
automatically generated](./media/image50.png)

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](./media/image51.png)

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](./media/image52.png)

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

![A screenshot of a computer code Description automatically generated
with low confidence](./media/image53.png) ![A screenshot of a computer
code Description automatically generated with low
confidence](./media/image53.png)

![A screenshot of a computer Description automatically
generated](./media/image54.png)

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](./media/image55.png)

When breakpoint is triggered**:**

![A picture containing text, screenshot, font, number Description
automatically generated](./media/image56.png)

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](./media/image57.png)

![A picture containing text, screenshot, font, line Description
automatically generated](./media/image58.png)

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

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

![](./media/image59.png)

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](./media/image60.png)

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](./media/image61.png)

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**

![](./media/image62.png)

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'0000**01c8**

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](./media/image63.png)

![](./media/image64.png)

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 confidence](./media/image65.png)The 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](./media/image66.png)

# 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](./media/image67.png)

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](./media/image68.png)

![A picture containing text, screenshot, font, line Description
automatically generated](./media/image69.png)

![A screenshot of a computer program Description automatically generated
with low confidence](./media/image70.png)A 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](./media/image71.png)

I will reboot and set a breakpoint there:

![A screenshot of a computer program Description automatically generated
with low confidence](./media/image72.png)

CClfsBaseFile::GetControlRecord+27 call
**CClfsBaseFile::AcquireMetadataBlock**

![A screenshot of a computer Description automatically
generated](./media/image73.png)

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](./media/image74.png).

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](./media/image75.png)

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](./media/image76.png)

**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](./media/image77.png)

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](./media/image78.png)

The **!pool** command displays the **tag** and **size** allocated.

![A picture containing text, screenshot, font, line Description
automatically generated](./media/image79.png)

![A picture containing text, screenshot, font, line Description
automatically generated](./media/image79.png)So, 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.

![](./media/image80.png)

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

![](./media/image81.png)

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](./media/image82.png)

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](./media/image83.png)

**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
generated](./media/image84.png)Then 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](./media/image85.png)

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](./media/image86.png)

# 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](./media/image87.png)

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

![A picture containing text, screenshot, display, font Description
automatically generated](./media/image88.png)

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](./media/image89.png)

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](./media/image90.png)

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

![A picture containing text, screenshot, font, line Description
automatically generated](./media/image91.png)After 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](./media/image92.png)

![A picture containing text, font, line, screenshot Description
automatically generated](./media/image93.png)

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

![A screenshot of a computer Description automatically generated with
medium confidence](./media/image94.png)

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](./media/image95.png)

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](./media/image96.png)

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](./media/image97.png)

Arriving at **WriteMetadataBlock**.

![](./media/image98.png)

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](./media/image99.png)

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](./media/image100.png)

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](./media/image101.png)

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](./media/image102.png)

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](./media/image103.png)

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](./media/image104.png)

# 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](./media/image105.png)

Then it adds 0x28 to that pointer, ( **0x58** from the beginning of the
base block of the **trigger blf)** that has the value **0x369.**
![](./media/image106.png)

![A screenshot of a computer program Description automatically generated
with medium confidence](./media/image107.png)

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](./media/image108.png)

![A close-up of a card Description automatically generated with low
confidence](./media/image109.png)GetSymbol 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](./media/image110.png)

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](./media/image111.png)

![](./media/image112.png)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**

![](./media/image113.png)

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](./media/image114.png)

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](./media/image115.png) ![A picture containing
text, font, screenshot, line Description automatically
generated](./media/image115.png)

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](./media/image116.png)

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](./media/image117.png)

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

![A screenshot of a computer Description automatically generated with
medium confidence](./media/image118.png)

![A screen shot of a computer Description automatically generated with
medium confidence](./media/image119.png)

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](./media/image120.png)

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

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

![A screenshot of a computer Description automatically
generated](./media/image121.png)

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](./media/image122.png)

The content of that attribute can be read using **NtFsControlFile.**

![A screenshot of a computer screen Description automatically generated
with medium confidence](./media/image123.png)

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](./media/image124.png)

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

![A screenshot of a computer code Description automatically generated
with medium confidence](./media/image125.png)

![A screenshot of a computer Description automatically
generated](./media/image126.png)

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

![A screenshot of a computer Description automatically
generated](./media/image127.png)

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

![A picture containing text, font, line, screenshot Description
automatically generated](./media/image128.png)

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](./media/image129.png)

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](./media/image130.png)

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

![A picture containing text, screenshot, font Description automatically
generated](./media/image131.png)

The token of my process is:

![](./media/image132.png)

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](./media/image133.png)

![A screenshot of a computer Description automatically
generated](./media/image134.png)

![A screenshot of a computer Description automatically
generated](./media/image135.png)

# 7-The real patch

BINDIFF shows a lot of changed functions

![A screenshot of a computer Description automatically
generated](./media/image136.png)

The vulnerable function is here:

![A screenshot of a computer Description automatically generated with
medium confidence](./media/image137.png)

![A screenshot of a computer Description automatically
generated](./media/image138.png)

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](./media/image139.png)

![A screenshot of a computer Description automatically
generated](./media/image140.png)

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 confidence](./media/image139.png)This 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](https://github.com/fortra/CVE-2023-28252).

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

[Ricardo.narvaja@fortra.com   
](mailto:%20 Ricardo.narvaja@fortra.com)[@ricnar456](https://twitter.com/ricnar456)

 [Esteban.kazimirow@fortra.com  
](mailto:Esteban.kazimirow@fortra.com)[@solidclt](https://twitter.com/solidclt)