/CVE-2022-37969

Windows LPE exploit for CVE-2022-37969

Primary LanguageC++Apache License 2.0Apache-2.0

CVE-2022-37969 Windows Local Privilege Escalation PoC

authors: Ricardo Narvaja & Daniel Kazimirow (Solid)

For demonstration purposes only. Complete exploit works on vulnerable Windows 11 21H2 systems.

Functional PoC based on previously published information by Zscaler

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

Usage

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

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 was prepared will trigger the vulnerability
    • Reading the System Token
    • Validating the token
    • Overwrite our process’s 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 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:

Creating multiple random BLF log files

Then it creates several log files with random names using a Loop.

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

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

We have to call this function twice. The first one will return an error, but it will give us the correct size of the buffer to call the second time to obtain the desired information.

Interfaz de usuario gráfica, Aplicación Descripción generada automáticamente

v5 will receive the information of the SYSTEM_BIG_POOL_INFORMATION structure.

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 SYSTEM_BIGPOOL_ENTRY.

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

It stores in an array called kernelAddrArray the VirtualAddress which is the first field of each structure that has CLFS tag and size 0x7a00. From now on, the pools that meet both conditions will be called: “right pools”.

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

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

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 de previous right pool found.

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

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

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

In this way in v32 will stores the difference between the VirtualAddress of the last two right pool found.

Then it does something similar, in this case v23 is initially zero so it does v23=v32 the first time.

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

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

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

Texto Descripción generada automáticamente

There we see that he found 6 consecutive and left the loop of creating log files.

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

Crafting the initial log file:

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

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.

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.

Preparing the methods CreatePipe() / NtFsControlFile()

It uses CreatePipe() 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

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

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

And when it finds it, it saves it in v30.Pointer 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.

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

Texto Descripción generada automáticamente

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

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

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

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

Once memory was prepared will trigger the vulnerability:

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

Texto Descripción generada automáticamente

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

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

This is how the memory is after the first call, as we said in the address around of 0x5000000

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

Reading the System Token:

This sequence will trigger the bug:

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

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

The NtSetinformationFile() is called, and the handles are closed with which the pointer is corrupted (it'll be explained later)

Interfaz de usuario gráfica, Aplicación Descripción generada automáticamente con confianza media

The HeapSpray, prevents a BSOD from occurring at this point:

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.

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

Texto Descripción generada automáticamente

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

Texto Descripción generada automáticamente con confianza media

Imagen que contiene Interfaz de usuario gráfica Descripción generada automáticamente

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

We trace from the breakpoint and see that it reaches CLFS!ClfsEarlierLsn().

Texto Descripción generada automáticamente

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

Interfaz de usuario gráfica Descripción generada automáticamente con confianza media

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

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

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

Texto Descripción generada automáticamente

We come to the second function nt!SeSetAccessStateGenericMapping()

Interfaz de usuario gráfica, Aplicación Descripción generada automáticamente

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

Interfaz de usuario gráfica, Aplicación, Teams Descripción generada automáticamente

Texto Descripción generada automáticamente

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

Interfaz de usuario gráfica, Texto Descripción generada automáticamente

Interfaz de usuario gráfica, Texto Descripción generada automáticamente con confianza media

Interfaz de usuario gráfica, Texto Descripción generada automáticamente

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

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

Then overwrites in RAX+8 the next field which is the AttributeValue()

Texto Descripción generada automáticamente

Texto Descripción generada automáticamente

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

Calendario Descripción generada automáticamente

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

That 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 from _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 the requested number of bytes and return it in the output buffer with which we can obtain in the first call the value of the SYSTEM TOKEN.

Texto Descripción generada automáticamente

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

To that he adds v14 which are the last 3 bytes of the System EPROCESS and then adds 0x4b8 which is the offset of Token for this version of Windows 11, then finds the contents of that address that will have saved the value of the System Token.

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

Texto Descripción generada automáticamente

Validating the token

Texto Descripción generada automáticamente con confianza baja

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

Overwrite our process’s token with the system one

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

Interfaz de usuario gráfica, Texto, Aplicación, Correo electrónico Descripción generada automáticamente

There we see the order in which the values are stored

Interfaz de usuario gráfica, Texto Descripción generada automáticamente

The address 0xFFFFFFFF with the value we have just found of the System Process Token.

Interfaz de usuario gráfica, Texto Descripción generada automáticamente

Texto Descripción generada automáticamente

And in the HeapSpray is the value of the Token address of my process to which I subtract 8. This value plus eight will be used as a target, remember that you wrote on the address pointed by RAX+8.

Graphical user interface, text Description automatically generated

Texto Descripción generada automáticamente

In the memory address starting on 0x5000000

Interfaz de usuario gráfica Descripción generada automáticamente

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

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

It comes again to CLFS!ClfsEarlierLsn().

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

setting RDX to 0xFFFFFFFF

Una captura de pantalla de un celular Descripción generada automáticamente

Then it comes to nt!SeSetAccessStateGenericMapping()

Texto Descripción generada automáticamente

Read the address of the Token of my process minus 8 where is going to write

Texto Descripción generada automáticamente

Then reads the SYSTEM TOKEN

Texto Descripción generada automáticamente

And writes in the address of the Token of my process (it adds 8), the System Token

And that way my process is with the System Token

Texto Descripción generada automáticamente

Once the token is written, we start a process to check the privileges, in this case, we launch Notepad.exe

Executing process as system:

Texto Descripción generada automáticamente

Interfaz de usuario gráfica, Aplicación, Word Descripción generada automáticamente

Texto Descripción generada automáticamente con confianza baja

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 explained in this blogpost.

Reversing the Patch:

Analyzing the structures

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

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

Texto Descripción generada automáticamente con confianza media

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

Escala de tiempo Descripción generada automáticamente con confianza media

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

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.

Interfaz de usuario gráfica, Texto, Aplicación, Correo electrónico Descripción generada automáticamente

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;

Interfaz de usuario gráfica, Texto Descripción generada automáticamente

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)Texto, Aplicación Descripción generada automáticamente

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:

Tabla Descripción generada automáticamente

Imagen que contiene Texto Descripción generada automáticamente

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.

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

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

Graphical user interface, application, timeline Description automatically generated

RAX will point to the _CLFS_BASE_RECORD_HEADER address

Interfaz de usuario gráfica, Texto, Aplicación, Correo electrónico Descripción generada automáticamente

Note the _CLFS_BASE_RECORD_HEADER structure in memory and the cbsymbolZone field 0x1328

bytes forward

Imagen que contiene Texto Descripción generada automáticamente

Texto Descripción generada automáticamente

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

Interfaz de usuario gráfica, Aplicación Descripción generada automáticamente

The CClfsBaseFilePersisted structure in memory:

Texto Descripción generada automáticamente

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

Tabla Descripción generada automáticamente con confianza media

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.

Escala de tiempo Descripción generada automáticamente con confianza media

Read two fields (offset 0x28 and 0x30)

Interfaz de usuario gráfica, Aplicación, Tabla Descripción generada automáticamente

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

Texto Descripción generada automáticamente

Texto Descripción generada automáticamente con confianza media

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

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

Texto Descripción generada automáticamente

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

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.

Tabla Descripción generada automáticamente

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

Texto Descripción generada automáticamente

Forma Descripción generada automáticamente con confianza media

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)

Texto Descripción generada automáticamente con confianza baja

Interfaz de usuario gráfica, Texto, Aplicación Descripción generada automáticamente

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.

Texto Descripción generada automáticamente

Interfaz de usuario gráfica, Tabla Descripción generada automáticamente

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

Aplicación, Tabla Descripción generada automáticamente con confianza media

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

Forma Descripción generada automáticamente

A picture containing calendar Description automatically generated

Image taken from the Zscaler blogpost:

Graphical user interface, application, email Description automatically generated

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

Text Description automatically generated

Graphical user interface, text, application Description automatically generated

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

Graphical user interface, text, application Description automatically generated

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

Text Description automatically generated

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

Application Description automatically generated with low confidence

The _CLFS_BASE_RECORD_HEADER on memory.

Calendar Description automatically generated

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.

Text Description automatically generated

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.

Graphical user interface, application Description automatically generated

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

Graphical user interface, text, application Description automatically generated

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

Background pattern Description automatically generated with low confidence

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.

Text Description automatically generated

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.

A picture containing application Description automatically generated

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.

A screenshot of a computer Description automatically generated with medium confidence

Graphical user interface, text, application Description automatically generated

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

Text Description automatically generated

In RAX will return the address of the _CLFS_CONTAINER_CONTEXT structure.

Graphical user interface, text, application, table Description automatically generated

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

Text Description automatically generated

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.

Graphical user interface, text, application Description automatically generated

Text, letter Description automatically generated

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.

A picture containing text Description automatically generated

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)

Text Description automatically generated with medium confidence

Graphical user interface Description automatically generated with low confidence

RBX stores the offset 0x1468.

A picture containing calendar Description automatically generated

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.

Text Description automatically generated

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.

Text Description automatically generated

A picture containing text Description automatically generated

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.

Graphical user interface, application, table Description automatically generated

Graphical user interface, application, Word Description automatically generated

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

Text, application, whiteboard Description automatically generated

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.

Calendar Description automatically generated

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.

Graphical user interface, text, application Description automatically generated

Graphical user interface, text Description automatically generated

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:

Shape Description automatically generated

MyLogxxx

A picture containing text Description automatically generated

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

Graphical user interface, text, application Description automatically generated

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.

Text Description automatically generated

This pointer Will be corrupted by overwriting the first bytes:

Text Description automatically generated

remaining pointing to a memory address previously controlled by us through HeapSpray

Text Description automatically generated

A picture containing text Description automatically generated

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

Text, application Description automatically generated with medium confidence

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.

A picture containing diagram Description automatically generated

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

Text Description automatically generated

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.

Text Description automatically generated

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

Text Description automatically generated

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.

Text Description automatically generated

Text Description automatically generated

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”.

Graphical user interface, text, application Description automatically generated

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.

Calendar Description automatically generated

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

Graphical user interface, text Description automatically generated

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.

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

Graphical user interface, table Description automatically generated

Text Description automatically generated with medium confidence

In the PoC, this value is malformed to point a fake client context object, called FakeClientContextText, whiteboard Description automatically generated

A screenshot of a computer Description automatically generated with medium confidence

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.

Text Description automatically generated with medium confidence

Chart, scatter chart Description automatically generated

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).

Graphical user interface Description automatically generated with low confidence

Text, letter Description automatically generated

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

A picture containing table Description automatically generated

Graphical user interface, text, application Description automatically generated

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.

Text Description automatically generated

Text, letter Description automatically generated

Note that the offset to the Client Context is 0x1b30

Table Description automatically generated

So, the Client Context is in offset 0x23a0.

Text Description automatically generated with medium confidence

Table Description automatically generated

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

Text, letter Description automatically generated

Table Description automatically generated with low confidence

Which would point to the string with the name

the last one is the blockattributeoffset which is 0xC before the Client Context at 0x2394.

Table Description automatically generated

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;

A picture containing diagram Description automatically generated

Text Description automatically generated

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.

A picture containing text Description automatically generated

Text Description automatically generated with medium confidence

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.

Graphical user interface, text, application, email Description automatically generated

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

Graphical user interface, text, application Description automatically generated

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 .

Graphical user interface, application Description automatically generated with medium confidence

There it is stopped with the ccoffsetArray crafted argument.

Graphical user interface, application Description automatically generated

Table Description automatically generated

The CClfsBaseFile::OffsetToAddr function returns the false Client Context

A picture containing graphical user interface Description automatically generated

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

A screenshot of a computer Description automatically generated

A picture containing text Description automatically generated

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

A screenshot of a computer Description automatically generated

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

Graphical user interface, text, application Description automatically generated with medium confidence

And finally, It compares the cidClient byte with zero

A picture containing diagram Description automatically generated

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

A screenshot of a computer Description automatically generated

The output of function r14 points to Client Context

Graphical user interface, text, application Description automatically generated

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

Text Description automatically generated

Now It reads the value of fAttributes (0x100)

Graphical user interface, text, application Description automatically generated

this function belongs to the class CClfsLogFcbPhysical

Text Description automatically generated

Graphical user interface, text, application Description automatically generated

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

Text Description automatically generated with medium confidence

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

A picture containing table Description automatically generated

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

A picture containing application Description automatically generated

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

Graphical user interface, text, application Description automatically generated

Graphical user interface, text, application, table Description automatically generated

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

Table Description automatically generated

Graphical user interface, text, application Description automatically generated

We see that in the constructor in the vtable is

Text Description automatically generated

It will check if the file is multiplexed.

Graphical user interface, application Description automatically generated

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

Graphical user interface, application Description automatically generated

Text, application, table Description automatically generated with medium confidence

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

Graphical user interface Description automatically generated with low confidence

Here retrieves the Client Context

Graphical user interface, application Description automatically generated with medium confidence

it stores the value 0xFFFFFFFF00000000.

Graphical user interface, application Description automatically generated

Graphical user interface, application Description automatically generated

A picture containing calendar Description automatically generated

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

Text Description automatically generated with medium confidence

Graphical user interface, text, letter Description automatically generated

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.

Text Description automatically generated

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

Table Description automatically generated

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

Graphical user interface, application Description automatically generated

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

Graphical user interface, application Description automatically generated

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

Graphical user interface, text, application Description automatically generated

in the iteration 0xe, RCX is 0x1a00

Graphical user interface, text Description automatically generated

A picture containing calendar Description automatically generated

That was where he had written the 0xFFFFFFFF000000.

Graphical user interface, text, application Description automatically generated

Table Description automatically generated with medium confidence

It’s reading the last two bytes FFFF

Text Description automatically generated

And it’ll copy then in R8

A picture containing calendar Description automatically generated

A picture containing calendar Description automatically generated

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.

Graphical user interface, application, table Description automatically generated

Since RCX is smaller than RDX.

Graphical user interface, application Description automatically generated

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

Table Description automatically generated

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

A screenshot of a computer Description automatically generated with low confidence

Table Description automatically generated

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

A picture containing graphical user interface Description automatically generated

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

Graphical user interface, application Description automatically generated

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.

We hope you find it useful, if you have any doubt can contact us at Ricardo.narvaja@fortra.com and Esteban.kazimiro@fortra.com

Enjoy!