Skip to main content
Core Security Logo Core Security Logo
  • Contact Us
  • Support
  • All Fortra Products
  • FREE TRIALS
  • Contact Us
  • Support
  • All Fortra Products
  • FREE TRIALS
  • Cyber Threat

      Products

      • Core Impact Penetration testing software
      • Cobalt Strike Red team software
      • Outflank Security Tooling (OST) Evasive attack simulation
      • Event Manager Security information and event management
      • Powertech Antivirus Server-level virus protection
      • Product Bundles

      Solutions

      • Penetration Testing
      • Penetration Testing Services
      • Offensive Security
      • Threat Detection
      • Security Information and Event Management
    • Penetration Testing Services Security consulting services
  • Identity

      Products

      • Access Assurance Suite User provisioning and governance
      • Core Password & Secure Reset Self-service password management
      • Core Privileged Access Manager (BoKS) Privileged access management (PAM)

      Solutions

      • Privileged Access Management
      • Identity Governance & Administration
      • Password Management
    • See How to Simplify Access in Your Organization | Request a Demo
  • Industries
    • Healthcare
    • Financial Services
    • Federal Government
    • Retail
    • Utilities & Energy
    • Higher Education
    • Compliance
  • Resources
    • Upcoming Webinars & Events
    • Blogs
    • Case Studies
    • Videos
    • Datasheets
    • Guides
    • Ecourses
    • Compliance
    • All Resources
  • CoreLabs
    • Advisories
    • Exploits
    • Publications
    • Articles
    • Open Source Tools
  • About
    • Partners
    • Careers
    • Press Releases
    • Contact Us

Understanding the CVE-2022-37969 Windows Common Log File System Driver Local Privilege Escalation

In this article we would like to share the analysis and work done on CVE-2022-37969 to build a functional PoC based on previously published information by Zscaler. Here we will complement the available information by adding details, guiding the reader to the in-depth understanding of the vulnerability, exploiting it, reversing the patch, and the creation of a functional PoC.

Here is a summary of the exploitation walkthrough

  • Creating the initial BLF log file          
  • Creating multiple random BLF log files
  • Crafting the initial log file
  • Performing a controlled Heap Spray
  • Preparing the methods CreatePipe() / NtFsControlFile()
  • Once memory is prepared, the vulnerability will be triggered
  • Reading the System Token
  • Validating the token
  • Overwrite our process token with the system one
  • Executing process as system
  • Reversing the Patch: Analyzing the structures
  • Corrupting the “pContainer” pointer
  • Revisiting the Patch
  • Corrupting the SignatureOffset 
  • Corrupting more values
  • Controlling the functions that allows to read the SYSTEM token 
  • Write our own process to achieve the local privilege escalation
  • PoC source code

The scenario used here was completed in Windows 11 21H2 (OS Build 22000.918) clfs.sys v10.0.22000.918.

Creating the Initial BLF Log File

The first step is to create a file named MyLog.blf in the public folder (%public%), by using the CreateLogFile() function:

Image
mylog

 

Image

 

Image

Creating Multiple Random BLF Log Files

It will then create several log files with random names using a Loop.

And within the loop, it calls to our getBigPoolInfo() function:

Image
getBigPoolInfo

It calls the NtQuerySystemInformation(), with 0x42 (66 decimal) as the first argument. It will return the information in v5 about the raids made in the bigpool, whose structure is of type SYSTEM_BIGPOOL_INFORMATION.

Image

This function needs to be called twice. The first one will return an error, but will give us the correct size of the buffer needed to call the second time to obtain the desired information.

Image

v5 will receive the information of the SYSTEM_BIG_POOL_INFORMATION structure. 

Image

The number of allocations in the bigpool is stored in the first field called Count. In the second field there is an array of structures called SYSTEM_BIGPOOL_ENTRY.

 

Image

From there we’ll search through all the structures for the "Clfs" tag and the size 0x7a00.

Image

The VirtualAddress is stored in an array called kernelAddrArray, which is the first field of each structure that has CLFS tag and size 0x7a00.  We’ll call the pools that meet both conditions “right pools.”

Image

In addition to storing each right pool in the array, it stores the last right pool found in the content of a2 variable, which is used as an argument of the function.

Image

In this way a2 always points to the last right pool with CLFS tag and size 0x7a00 created.

 

The variable v26 always stores the previous right pool found since it is equal to v24 (v26=v24), before calling getBigPoolinfo(), but v24 is updated when leaving this call with the last right pool found, and v26 stays with the previous right pool.

Image

It then subtracts both directions, and, in case the result is negative, inverts the operands so that it is always positive.

Image

In this way v32 will store the difference between the VirtualAddress of the last two right pools found.

Then it performs a similar action. In this case v23 is initially zero so it does v23=v32 the first time.

Image

The next time it still has the same value in loop v23 and is not zero, so it breaks and goes here.

Image

V32 has the last difference. If v32 and v23 they are equal, it comes out and increments one, but resets the counter to zero.

The idea is to find six consecutive comparisons of CLFS tags and size 0x7a00 whose differences are equal. That difference will be 0x11000.  When executing this, we will see that when it finds six (since it starts from scratch) consecutive with equal distances it will display the value of difference between them.

Image

 

Image

There we see that six consecutive comparisons were found, leaving the loop of log creation files.

In the "public" folder we can see the files created:

Image

Crafting the initial log file:

Our craftFile() function opens the original file (MyLog.blf) and modifies it to trigger the bug.

Image

After modifying the file, it's necessary to change the CRC32, otherwise we'll get a corrupt file error.

This value is located at offset 0x80C of the file.

Image

Performing a Controlled Heap Spray

Next, it performs a HeapSpray, using the VirtualAlloc() function to allocate memory, at arbitrary addresses 0x10000 and 0x5000000 respectively, and saving in the second allocation (0x10000), the value 0x5000000, every 0x10 bytes.

Image

Preparing the Methods CreatePipe() / NtFsControlFile()

CreatePipe() is used to create an anonymous pipe and call NtFsControlFile() using 0x11003c as an argument to add an attribute. Later you can call this same function with the 0x110038 argument to read it.

More details of this method can be found HERE.

Image

Next we see the input buffer, which is the attribute we are adding. If we call NtFsControlFile() again with the argument 0x11038 in the output, it should return this same attribute.

Image

Search the pool for the tag of the created attribute (NpAt).

Image

When it finds it, it saves it in v30.Pointer, which is the VirtualAddress of this pool.

V30.pointer+24 points to the AttributeValueSize in the kernel pool and saves it in one of the HeapSprays we’ve made before.

Image

The idea is to write to that kernel address+8 in order to overwrite the AttributeValue.

Image

 

Image

The PipeAttribute structure has a LIST_ENTRY as its first field, which has a size of 16 bytes. It then has a pointer to the name of the attribute which has a size of 8 bytes. It then comes in 0x18 (24 decimal), which is the AttributeValueSize field that in which we are storing in the HeapSpray.

After that, we load in CLFS.sys and ntoskrnl in usermode. By using GetProcAddress(), we find the addresses of the ClfsEarlierLsn() and SeSetAccessStateGenericMapping() functions.

Image

Then we call the FindKernelModulesBase() function that will find the kernel base of both same modules using NtquerySystemInformation(), this time with the SystemModuleInformation argument needed to return the info about all the modules.

Image

In this way, we can calculate the offset of each function, and then obtain them in kernel.

Image

Once Memory is Prepared, the Vulnerability Will Be Triggered:

The pipeArbitraryWrite() function is called twice, there is a flag that initially is zero for the first call and when it is value 1 in the second call, it will change the values of the HeapSpray.

Image

In the first call in the 0x5000000 memory address, the following values are located:

Image

Remember that this value, in addition to alloc in that direction, is stored in our HeapSpray.

Image

This is the memory after the first call, in the address of around 0x5000000:

Image

And in the HeapSpray from memory 0x10000 it will store the pointer to AttributeValueSize every 0x10 bytes, besides the pointer to 0x5000000.

Image

Reading the System Token:

This sequence will trigger the bug:

Image

CreateLogFile() is called again on the crafted file and then another with a random name.

AddLogContainer() is then called using the handles of those files.

Image

The NtSetinformationFile() is called, and the handles are closed with which the pointer is corrupted. (This will be explained later.)

Image

The HeapSpray prevents a BSOD from occurring at this point:

Image

Setting a breakpoint there, we can see that the pointer is corrupt and points to our HeapSpray, with which we can handle the next two function calls of the vtable.

Image

 

Image

RAX takes the value 0x5000000 and jumps first to the function located at 0x5000000+18 and then to 0x5000000+8.

Image

 

Image

So the first jump is to fnClfsEarlierLsn() and then to fnSeSetAccessStateGenericMapping().

Tracing from the breakpoint, we can see that it reaches CLFS!ClfsEarlierLsn().

Image

This function is called exclusively because when it returns, it sets EDX to 0xFFFFFFFF.

 

Image

In the address 0xFFFFFFFF we had stored the result of the SYSTEM  _EPROCESS & 0xFFFFFFFFFFFFFFF000.

Image

As we mentioned, when returning from CLFS!ClfsEarlierLsn(), the RDX value is 0x00000000FFFFFFFF.

Image

We then come to the second function of nt!SeSetAccessStateGenericMapping().

Image

This function is useful, since RCX points to our HeapSpray and the RDX value, whose content we control, is 0xFFFFFFFF.

Image

 

Image

The content of RCX+0x48 has the pointer to AttributeValueSize that was stored in v30.Pointer+24

Image

 

Image

 

Image

That pointer value of AttributeValueSize is moved to RAX. It then reads the contents of the address 0xFFFFFFFF, where we had stored the address of the SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000.

Image

It then overwrites the next field in RAX+8, which is the AttributeValue().

Image

 

Image

Of course, the AttributeValue would normally point to the attribute we added in kernel.

Image

And now we’ll overwrite it with a pointer of the result of the system _EPROCESS &  0xFFFFFFFFFFFFFFF00.

This will mean that when we call the NtFsControlFile() function again, (this time with the 0x110038 argument to read the attribute instead of returning the "A” that were pointed by the AttributeValue pointer) it will now read the requested number of bytes from _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 and return it in the output buffer with which we can obtain in the first call the value of the SYSTEM TOKEN.

Image

v9b is the start address of the Output Buffer where the content of the result of System EPROCESS & 0xFFFFFFFFFFFFFFF000 were copied.

To that we’ll add v14, which are the last 3 bytes of the System EPROCESS. Then 0x4b8, which is the offset of Token for this version of Windows 11, will find the contents of that address that will have saved the value of the System Token.

Image

 

Image

Validating the Token

Image

Remember that the last 4 bits were changed. As this is not significant, the value still matches.

Overwrite Our Process Token With the System One

In the second call the value of the Flag is 1, since it was incremented at the end of the first call.

Image

There we see the order in which the values are stored.

Image

We can see the address 0xFFFFFFFF with the value we have just found of the System Process Token.

Image

 

Image

The value of the Token address of my process is in the HeapSpray. From this, we’ll subtract 8. This value plus eight will be used as a target. Remember that we wrote on the address pointed by RAX+8.

Image

 

Image

Here is the memory address starting on 0x5000000.

Image

We also see that it uses the name of other container, since the previous one being used by the system process cannot be opened again or deleted.

Image

Then the bug is triggered for the second time in the same way as it was on the first try.

Image

It comes again to CLFS!ClfsEarlierLsn().

Image

Then it sets RDX to 0xFFFFFFFF.

Image

Then it comes to nt!SeSetAccessStateGenericMapping().

Image

Read the address of the Token of the process (minus 8) where it is going to write.

Image

We can then read the SYSTEM TOKEN.

Image

It then writes in the address of the Token of the process (adding 8), which is the System Token.

Image

This way, the process is with the System Token.

Image

Once the token is written, we can start a process to check the privileges. In this case, we’ll launch Notepad.exe.

Executing Process as System

Image

 

Image

 

Image

Remember that this POC only works in Windows 11. In Windows 10 it will produce a BSOD, so you should make some modifications to work correctly, it is not covered in this blogpost.

Reversing the Patch:

Analyzing the Structures

We have taken the structures and most of the documentation on the CLFS file format from IONESCU's excellent work on CLFS Internals.

We can see that a check has been added in the function ClfsBaseFilePersisted::LoadContainerQ.

Image

The values that perform an addition, belongs to the _CLFS_BASE_RECORD_HEADER structure.

Image

Note that the Base Block starts at offset 0x800 of the file, and ends at offset 0x71FF, corresponding the first 0x70 bytes to the Log Block Header.

Image

As a good practice, we can add the _CLF_LOG_BLOCK_HEADER structure on IDA:

struct _CLFS_LOG_BLOCK_HEADER

{

UCHAR MajorVersion;

UCHAR MinorVersion;

UCHAR Usn;

char ClientId;

USHORT TotalSectorCount;

USHORT ValidSectorCount;

ULONG Padding;

ULONG Checksum;

ULONG Flags;

CLFS_LSN CurrentLsn;

CLFS_LSN NextLsn;

ULONG RecordOffsets[16];

ULONG SignaturesOffset;

};

Then we have the Base Record Header (_CLFS_BASE_RECORD_HEADER) that starts at the offset 0x870 from the beginning of the file and is 0x1338 bytes long.

Image

If you want to import it to IDA, before you must add the following types and missing structures:

typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;

struct _CLFS_METADATA_RECORD_HEADER

{

ULONGLONG ullDumpCount;

};

Now is ready to be added:

typedef struct _CLFS_BASE_RECORD_HEADER

{

    CLFS_METADATA_RECORD_HEADER hdrBaseRecord;

    CLFS_LOG_ID cidLog;

    ULONGLONG rgClientSymTbl[0x0b];

    ULONGLONG rgContainerSymTbl[0x0b];

    ULONGLONG rgSecuritySymTbl[0x0b];

    ULONG cNextContainer;

    CLFS_CLIENT_ID cNextClient;

    ULONG cFreeContainers;

    ULONG cActiveContainers;

    ULONG cbFreeContainers;

    ULONG cbBusyContainers;

    ULONG rgClients[0x7c];

    ULONG rgContainers[0x400];

    ULONG cbSymbolZone;

    ULONG cbSector;

    USHORT bUnused;

    CLFS_LOG_STATE eLogState;

    UCHAR cUsn;

    UCHAR cClients;

} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER; 

 

Image

After including the structures, we notice that performs an addition between the cbSymbolZone and the address where the _CLFS_BASE_RECORD_HEADER ends. (start + 1338h)

Image

Remember that cbSymbolZone was modified in the crafted log file from 0x000000F8 to 0x0001114B.

(offset 0x1b98 of the file)

 

0x800(offset of the start of the Base Block) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)

 

0x800+0x70+0x1328 = 0x1b98

 

Crafted cbsymbolZone on MyLog.blf file:

Image

 

Image

As the patch is in the CClfsBaseFilePersisted::LoadContainerQ function, we have to take a look at the CClfsBaseFilePersisted object.

Setting a breakpoint in CLFS!CClfsBaseFilePersisted::LoadContainerQ and when CreateLogFile is called with the handle of the crafted file it’ll break.

Image

Call the CClfsBaseFile::GetBaseLogRecord function  to get the address of the Base Log Record (_CLFS_BASE_RECORD_HEADER).

Image

RAX will point to the _CLFS_BASE_RECORD_HEADER address.

Image

Note the _CLFS_BASE_RECORD_HEADER structure in memory and the cbsymbolZone field 0x1328 bytes forward.

Image
Image

r14  stores the structure corresponding to the “this”, which  is CClfsBaseFilePersisted since it is the this of the function CClfsBaseFilePersisted::LoadContainerQ.

Image

The CClfsBaseFilePersisted structure in memory:

Image

So, let's create a structure with length 0x21c0 to complete its fields while we reverse it (it's an undocumented structure) we'll call it struct_CClfsBaseFilePersisted.

Image

Inside the function CClfsBaseFile::GetBaseLogRecord() gets the pointer to _CLFS_BASE_RECORD_HEADER and we know that the "this" in that function is the structure: struct_CClfsBaseFilePersisted.

Image

Read two fields (offset 0x28 and 0x30).

Image

Field 0x28 is a word and has the value 6, so we change the type to word in the structure.

Image

 

Image

 

Image

 

Image

For now, we rename it to constant 6 (const_6).

Image

According to the documentation, 6 would be the number of blocks CLFS_METADATA_BLOCK_COUNT. The field could refer to this value.

And that pointer is at offset 0x30.

Image

Note that the size shown there includes the header with length 0x10.

Image

 

Image

When the ExAllocatePoolWithTag function is called, a few bytes are requested, but the header is not included, therefore, 0x90 bytes (0xa0 – 0x10) will be requested in the call.

Searching by text +30h], the instructions that write at offset 0x30 we found a long list, but filtering the list by the type of object CClfsBaseFilePersisted leaves us with few results and immediately find where that size is allocated, and the same tag. (Tip: Create and initialize function names, are always the first to look at).

Image

 

Image

Since we still don't know the name, we'll put it pool_0x90, which is another undocumented structure, and we'll create a structure of that size.

Image

 

Image

The pool_0x90 in memory has another pointer at its own offset 0x30.

Image

This other pointer points to the base block in the file (base block starts at offset 0x800).

Image

 

Image

Image taken from the Zscaler blogpost:

Image

The allocation is huge, because it contains the entire base block.

Image

 

Image

 

Image

So, we will create a new structure of size 0x7a00 and call it BASE_BLOCK.

Image

The first 70 bytes we already knew correspond to _CLFS_LOG_BLOCK_HEADER and the following 0x1338 to _CLFS_BASE_RECORD_HEADER.

Image

So, adding the start of the Base Block with the offset to the next record (which is 0x70), we get the _CLFS_BASE_RECORD_HEADER.

Image

The _CLFS_BASE_RECORD_HEADER on memory.

Image

Looking at other methods of the same CClfsBaseFilePersisted object, in CClfsBaseFilePersisted::AddContainer you get with CClfsBaseFile::GetBaseLogRecord also the address of _CLFS_BASE_RECORD_HEADER.

Image

Next, call CClfsBaseFile::OffsetToAddr using cbOffset, it gets the address of _CLFS_CONTAINER_CONTEXT, and stores cboffset in the rgbcontainers array which is at offset 0x328 of the _CLFS_BASE_RECORD_HEADER. 

Image

CClfsBaseFile::OffsetToAddr function is used to find structures addresses from offset.

Image

At this point, the container offset that will be stored at 0x328 is still 0, because we have not added a container yet.

Image

The PoC calls CreateLogFile twice, the first time with the malformed file MyLog.blf and the second time with the normal MyLogxxx.blf file, so we must stop debugging twice in all the above places and take note in notepad of the addresses of the above structures for both files.

Image

Let us fast forward a bit to CLFS!CClfsLogFcbPhysical::AllocContainer by setting a breakpoint on it and running there.

When the AddLogContainer() is reached on the POC, we stop at the breakpoint. 

Image

Let’s also set a breakpoint on the CClfsBaseFilePersisted::AddContainer+176 where we saw before that will find the offset and pointer to the _CLFS_CONTAINER_CONTEXT structure. 

Image

 

Image

When the Debugger breaks, we can see that the offset is 0x1468.

Image

In RAX will return the address of the _CLFS_CONTAINER_CONTEXT structure.

Image

The structure is still empty because it was not added the container yet.

Image

Note that the SignatureOffset=0x50 value that we wrote at offset 0x868 to the malformed file, subtracting the 0x800 from the start of the base block, will be in the _CLFS_LOG_BLOCK_HEADER structure at offset 0x68.

Image

 

Image

When the PoC calls AddLogContainer() function using the malformed file, at offset 0x68 of _CLFS_LOG_BLOCK_HEADER, instead of the 0x50 value we wrote there, is currently a 0xFFFF0050 in memory. 

Image

At some point, that value was altered by the program, in order to see when it happened, in the next execution, we will set a memory breakpoint on write.

The offset is stored at r15 + 0x328 (r15 points to the _CLFS_BASE_RECORD_HEADER structure).

Image

 

Image

RBX stores the offset 0x1468.

Image

So, in the Base Block address + 0x70 + the offset 0x1468 that we found out, there will be the address of the CLFS_CONTAINER_CONTEXT container.

Image

In CLFS_CONTAINER_CONTEXT structure at offset 0x18 will be the pContainer pointer that will be stored there, we can set a breakpoint on write and see when it is written. 

Image

 

Image

This is the pointer that we must corrupt since in the function where the vulnerability is, it first reads the CLFS_CONTAINER_CONTEXT, then moves it to r15 and next reads the value of r15+18, which is this pointer that we have just set the Breakpoint on write.

Image

 

Image

It stores the pContainer at offset 0x1c0 of the struct_CClfsBaseFilePersisted structure. 

Image

After several times that it stops, we reach the moment where it gets corrupted. The top of the pointer address has been changed from FFs to zero. 

Image

This happens when the second AddLogContainer() of the malformed file is called, the pointer of the previous MyLogxxx is corrupted.

The problem occurs because the SignaturesOffset, which should be 0x50, is now 0xFFFF0050, so it allows writing out of bounds in the memset that follows. 

Image

 

Image

Corrupting the “pContainer” Pointer:

The memset() function is going to corrupt the _CLFS_CONTAINER_CONTEXT structure that is below, this structure corresponds to the MyLogxxx file, since when were created, it located them 0x11000 bytes away from each other.

This way, it calculates exactly where to write to the next structure and zeroes out the top of the pointer, so it points to the user heap where the HeapSspray was created.

The base block structure of the malformed file is just 0x11000 before that of the MyLogxxx file.

Malformed:

Image

MyLogxxx:

Image

 

Image

RCX is smaller than RDX since 0xFFFF0050 was added to, instead of 0x50 as it should be. 

Image

And we got to the memset() function, to set the amount of 0xb0 bytes with Zeroes, with RCX pointing to the CLFS_CONTAINER_CONTEXT structure of the MyLogxxx file, specifically to the pContainer five high bytes. 

Image

This pointer will be corrupted by overwriting the first bytes:

Image

Remaining pointing to a memory address previously controlled by us through HeapSpray:

Image

 

Image

Then, the handle of the MyLogxxx file will be closed, and reaches the CClfsBaseFilePersisted::RemoveContainer, the vulnerability finally is triggered.

Image

Revisiting the Patch

Now that we have more information, we notice that here it reads the Base_Block.LOG_BLOCK_HEADER.SignaturesOffset and the Base_Block. .LOG_BLOCK_HEADER.TotalSectorCount.

In the first part of the patch that SignaturesOffset should not be greater than 0x7a00, in ours it was originally 0x50, if it arrived with a value greater than 0x7a00 it would throw us out. 

Image

Running the PoC in the patched machine, it compares 0x50 with 0x7a00 and since it is smaller it continues. 

Image

In the following block, the malformed cbSymbolZone is added to the value of the final address of _CLFS_BASE_RECORD_HEADER and this sum is stored in result_1. 

Image

Then, the address of the Base_Block is added with the SignatureOffset value, which in a normal file is 0x7980.

Image

The maximum address of the base_block is 0x7a00, now the SymbolZone is allowed up to 0x80 before the limit.

It will store it in result_2, that is, that would be the maximum limit for the SymbolZone inside the base block, then it compares both results if the first is greater than the second, it means that it went out of bounds.

Image

 

Image

Obviously the first member will be bigger than the second and it will not continue, since the first sum of the cbSymbolZone + final address of the _CLFS_BASE_RECORD_HEADER exceeds the limit (which is the result_2) and leads in an “out of bounds.”

Image

Corrupting the SignatureOffset

The last thing we would have to figure out is where the SignatureOffset value of 0x50 becomes 0xFFFF0050.

So, let's start over, reboot and stop at CLFS!CClfsBaseFilePersisted::LoadContainerQ where the value has not yet been changed in memory and still is 0x50.

Set an access breakpoint at offset 0x68 in SignatureOffset.

Image

And after several stops, we detect the right moment when it modifies the value, in the ClfsEncodeBlockPrivate.

Image

This function is not patched, so it could be a behavior caused by the low value of 0x50 and the rest of the values being manipulated.   

Among the crafted values, we can see the ccoffsetArray value whose name in the _CLFS_BASE_RECORD_HEADER structure is rgClients and represents the array of offsets that point to the Client Context Object. 

The rgClients field is located at offset 0x138 (0x9a8-0x800-0x70) of the _CLFS_BASE_RECORD_HEADER structure. 

Image

 

Image

 

In the PoC, this value is malformed to point a fake client context object, called FakeClientContext.

Image

 

Image

This is the Client Context structure _CLFS_CLIENT_CONTEXT:

struct _CLFS_CLIENT_CONTEXT

{

CLFS_NODE_ID cidNode;

CLFS_CLIENT_ID cidClient;

USHORT fAttributes;

ULONG cbFlushThreshold;

ULONG cShadowSectors;

ULONGLONG cbUndoCommitment;

LARGE_INTEGER llCreateTime;

LARGE_INTEGER llAccessTime;

LARGE_INTEGER llWriteTime;

CLFS_LSN lsnOwnerPage;

CLFS_LSN lsnArchiveTail;

CLFS_LSN lsnBase;

CLFS_LSN lsnLast;

CLFS_LSN lsnRestart;

CLFS_LSN lsnPhysicalBase;

CLFS_LSN lsnUnused1;

CLFS_LSN lsnUnused2;

CLFS_LOG_STATE eState;

union

{

HANDLE hSecurityContext;

ULONGLONG ullAlignment;

};

};

The eState value is in offset 0x78 from the start of the structure, in the crafted file 0x23a0+0x78. 

Image

 

Image

This value shows the status of the log. 

typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED    = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED      = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE           = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE   = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE  = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN         = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED      = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE           = 0x80;

This value is set to CLFS_LOG_STATE CLFS_LOG_SHUTDOWN =0x20.

The other malformed value is fAttributes which corresponds to the set of FILE_ATTRIBUTE flags associated with the base log file (such as System and Hidden). 

Image

 

Image

Since the field starts a byte earlier at 0xa and spans two bytes, the value of fAttributes is 0x100. 

Image

 

Image

Finally, there is the blocknameoffset value that points to the offset 0x1bb8, I mean, by adding 0x78 and 0x800 points to the offset 0x2428 of the file. 

Image

 

Image

Note that the offset to the Client Context is 0x1b30.

Image

So, the Client Context is in offset 0x23a0.

Image

 

Image

And just 0x10 before, it is the value corresponding to blocknameoffset.

Image

 

Image

Which would point to the string name. The last one is the blockattributeoffset which is 0xC before the Client Context at 0x2394. 

Image

These last two values ​​belong to a structure prior to the Client Context of 0x30 bytes long, called_CLFSHASHSYM.

typedef struct _CLFSHASHSYM
{
    CLFS_NODE_ID cidNode;
    ULONG ulHash;
    ULONG cbHash;
    ULONGLONG ulBelow;
    ULONGLONG ulAbove;
    LONG cbSymName;
    LONG cbOffset;
    BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;
Image

 

Image

They are at 0x20 and 0x24 bytes from the beginning of the _CLFSHASHSYM structure, so in the _CLFSHASHSYM structure the value called blockNameOffset in the POC is the cbSymName field and the blockAttributteoffset is the cbOffset field. 

Image

 

Image

 

Those are the malformed values, now we need to see how they affect to change our SignaturesOffset from 0x50 value to 0xFFFF0050.

Let’s take a look at the CClfsBaseFile::AcquireClientContext() function, which should return the client context. 

Image

It calls the CClfsBaseFile::GetSymbol with the fourth argument which will be _CLFS_CLIENT_CONTEXT ** where it will store the pointer to Client Context. 

Image

Inside the CClfsBaseFile::GetSymbol function we pass the malformed ccoffsetArray offset to CClfsBaseFile::OffsetToAddr and get the address of the client context, let’s set a breakpoint there so it’ll stop when calling the file created with CreatelogFile. 

Image

There it is stopped with the ccoffsetArray crafted argument. 

Image

 

Image

The CClfsBaseFile::OffsetToAddr function returns the false Client Context.

Image

And checks that the value of cbOffset is not zero since 0xC is found before the _CLFS_CLIENT_CONTEXT structure that is in RAX. 

Image

 

Image

Then It compares the cbOffset with the ccoffsetArray (which is in RSI), they must be equal, otherwise we’ll get an error.

Image

It also checks that cbSymName be equal to cbOffset+0x88, if not we’ll get an error too. 

Image

And finally, It compares the cidClient byte with zero.

Image

If all those checks are successful, the client context will be saved. 

Image

The output of function r14 points to Client Context.

Image

When exiting from CClfsLogFcbPhysical::Initialize we'll have the address of CLFS_CLIENT_CONTEXT.

Image

Now It reads the value of fAttributes (0x100).

Image

This function belongs to the class CClfsLogFcbPhysical.

Image

 

Image

Which was allocated here, and its size is 0x15d0 and its tag is “ClfC.” 

Image

Let’s create a structure to store what we are reversing, we’ll call it: struct_CClfsLogFcbPhysical.

Image

Note that at 0x2b0 it saves the address of the CClfsBaseFilePersisted structure. 

Image

After saving many values in the structure, it goes to an important part, it tests the eState with 0x20. 

Image

 

Image

Since the crafted value was 0x20, the test will return 1. 

Image

 

Image

We see that in the constructor in the vtable is

Image

It will check if the file is multiplexed.  

Image

So, it goes by the desired path, reaching CClfsLogFcbPhysical::ResetLog. 

Image

 

Image

Several fields are initialized to zero except one that is initialized to 0xFFFFFFFF00000000.

Image

Here retrieves the Client Context

Image

it stores the value 0xFFFFFFFF00000000.

Image

 

Image

 

Image

It writes 0xFFFFFFFF is offset 0x5c which is the high part of CLFS_LSN lsnRestart.ullOffset.

Image

 

Image

 

Image

Now we execute the ClfsEncodeBlockPrivate() function, which is the responsible to overwrites the 0x50 with 0xFFFF0050 as we have seen before.

There it reads the value of SignatureOffset = 0x50 which is still as we put it in the malformed file and adds it to the start of CLFS_LOG_BLOCK_HEADER. 

Image

This is a loop that is writing 2 bytes, like the SignatureOffset instead of pointing to a correct value that in a normal file is a high value, for example 0x3f8 which makes it to write more forward, here it will write in the same CLFS_LOG_BLOCK_HEADER.

The idea is to change the write destination to try to corrupt the SignatureOffset value.

Normal File:

Image

At this point, it will start to loop and write two bytes. 

Image

The counter must reach the value 0x3d to exit the loop. 

Image

RCX is increasing from 0x200, we are already in the third cycle, and its value is 0x600.

Image

In the 0xe iteration , RCX is 0x1a00.

Image

 

Image

That was where he had written the 0xFFFFFFFF000000. 

Image

 

Image

It’s reading the last two bytes FFFF.

Image

And it’ll copy then in R8.

Image

 

Image

As we've seen, this value is critical as it allows you to bypass the check and write out of bounds to corrupt the pContainer pointer of the file that follows the memset() and write zeros at the top and leave it pointing to our controlled memory (HeapSpray).

In the CClfsBaseFilePersisted::AllocSymbol that the same sum that is going to get the destination of the memset which is cbSymbolZone + final address of CLFS_BASE_RECORD_HEADER compares it before against Base_block + 0xFFFF0050, so it has corrupted values on both sides of the equation.

CbSymbolZone= 0x1114B

It is the malformed value that added to the final address of CLFS_BASE_RECORD_HEADER will make it write out of bounds and the other member of the comparison that should be the address of the Base Block + SignatureOffset, remains SignatureOffset =0xFFFF0050 which allows this check to pass and write out of bounds in the memset() and zero the top of the pointer that will remain pointing to our HeapSpray. 

Image

Since RCX is smaller than RDX. 

Image

As we have seen before. (Values may differ because they belong to a previous execution) 

 

It will corrupt the pointer, setting the highest bytes to 0

Image

Leaving it pointing to a memory area that we control through HeapSpray

Image

 

Image

So, when the vulnerability is triggered, we get to CClfsBaseFilePersisted::RemoveContainer

Image

There will be the already corrupt pointer and it can be exploited as we saw previously. 

Image

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

We hope you find it useful, if you have any doubt can contact us at [email protected] and [email protected].

Ricardo Narvaja
Meet the Author

Ricardo Narvaja

Cybersecurity Specialist Developer
View Profile
Meet the Author

Esteban Kazimirow

Exploit Writer
Core Security, by Fortra
View Profile
Related Content
Article
Reversing and Exploiting Free Tools Series
Article
Analysis of CVE-2022-30136 “Windows Network File System Vulnerability“
Article
Analysis of CVE-2022-21882 "Win32k Window Object Type Confusion Exploit"
Article
Proof of Concept: CVE-2022-21907 HTTP Protocol Stack Remote Code Execution Vulnerability
  • Email Core Security Email Us
  • Twitter Find us on Twitter
  • LinkedIn Find us on LinkedIn
  • Facebook Find us on Facebook

Products

  • Access Assurance Suite
  • Core Impact
  • Cobalt Strike
  • Event Manager
  • Browse All Products

Solutions

  • Identity Governance

  • PAM
  • IGA
  • IAM
  • Password Management
  • Vulnerability Management
  • Compliance
  • Cyber Threat

  • Penetration Testing
  • Red Team
  • Phishing
  • Threat Detection
  • SIEM

Resources

  • Upcoming Webinars & Events
  • Corelabs Research
  • Blog
  • Training

About

  • Our Company
  • Partners
  • Careers
  • Accessibility

Support

Privacy Policy

Contact

Impressum

Copyright © Fortra, LLC and its group of companies. All trademarks and registered trademarks are the property of their respective owners.