Tale of my first cve
Introduction
In a previous post regarding filesystem bugs, I mentioned that I found an interesting behaviour pertaining to the dispatchment of unhandled exceptions.
With the bug being fixed in August’s patch tuesday as CVE-2023-35359, I can finally share about it publicly.
The bug itself is pretty useless and almost always non-exploitable(afaik), but the patch is huge and worth a blog post :)
I’ll be writing about the bug itself, how I found it, my expected patch and the actual patch by Microsoft.
Executive Summary
When an unhandled exception occurs on Windows, the program will involuntarily attempt to awake the Windows Error Reporting(WER
) service for logging and analysis.
In the case where the awake call fails, for example when the service is explicitly marked as Disabled
, the faulting program will create a WerFault.exe
child to collect program specific statistics.
If by any chance the faulting program is a privileged process impersonating our current user, we will be able to hijack the process creation using a spoofed DOS device map and execute arbitrary code as high integrity.
The conditions are:
- WER service marked as disabled
- Privileged process
P
impersonating medium IL user - Unhandled exception in
P
while under impersonation
The first condition is pretty common due to privacy and storage concerns, but the other two are really difficult to satisfy.
In the next section I’ll go into the technical details and root cause analysis of the bug.
Impersonated device map technique will not be discussed since I’ve previously written about it.
Root Cause Analysis
Here’s a sample program to simulate an unhandled exception:
1 |
|
Programmers are expected to handle their own exceptions using SEH
(try-except) if using C or C++ Exception Handling
(try-catch) if using C++.
Unhandled exceptions on Windows fall back to the default exception handler KernelBase!UnhandledExceptionFilter
.
1 | LONG __stdcall UnhandledExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) |
This function checks if a debugger is attached, and breaks into it if possible.
Otherwise, execution is passed to Kernel32!BasepReportFault
to report the crash, which is a wrapper around Kernel32!WerpReportFault
and eventually calls into Kernel32!WerpReportFaultInternal
.
1 | __int64 __fastcall WerpReportFaultInternal(__int64 a1) |
This function calls into ntdll!RtlWerpReportException
, which is responsible for awaking the WER service(via an ETW trigger) and sending messages to it(via ALPC as mentioned here).
If the call succeeds, the service routine WerSvc!CWerService::SvcReportCrash
will be invoked to organize crash statistics.
Finally, before the faulting process terminates, a thread is created to launch WerFault.exe
as a child process using an auxiliary dll export Faultrep!CreateCrashVerticalProcess
which eventually calls CreateProcessAsUserW
.
In the event where the service is explicitly disabled, ntdll!RtlWerpReportException
can’t wake it up and fails with an error status.
As shown in the pseudocode above, Kernel32!WerpReportFaultInternal
takes things into its own hands and calls its own version of CreateCrash, Kernel32!StartCrashVertical
This function interestingly calls CreateProcessW
instead.
As we’ve previously discussed, CreateProcessW
is vulnerable to the impersonated device map attack because it uses its impersonation token while trying to look up the process image, but equips the eventual process with its primary token which can be of high privilege.
CreateProcessAsUserW
on the other hand already takes a token handle as an argument.
In our case, the primary token of the faulting process is passed to the function, which does not follow our spoofed device map if it’s of high privilege, and thus cannot be exploited.
Expected Patch
I thought the bug probably arised because the developer working on Kernel32!StartCrashVertical
is unaware that a similar function exists in Faultrep.dll
.
My patch would be to either call into Faultrep.dll
or replace the CreateProcessW
call with a call to CreateProcessAsUserW
.
Microsoft’s eventual patch is much more drastic.
The Patch
After receiving news of the patch, I launched procmon to view the stack traces like shown above.
To my surprise, the stack traces are exactly the same as before, which meant that the fix was on a kernel level.
I diffed ntoskrnl.exe
before and after the patch and found some additional functions and modifications.
ObpUseSystemDeviceMap
sounds really interesting as an added function, and ObpLookupObjectName
is called when loading a process image from disk so the modification to it is likely Microsoft’s patch.
1 | if ( objectType == IoFileObjectType && ObpUseSystemDeviceMap(ObjectNameRef, ObjectNameLength, a9, v14) ) |
The patch is applied to ObpLookupObjectName
to ignore the device map from the impersonation token if the object to be looked up is a file object and the call to ObpUseSystemDeviceMap
succeeds.
ObpUseSystemDeviceMap
1 | bool __fastcall ObpUseSystemDeviceMap(PUNICODE_STRING ObjectName, ULONG Length, ULONG64 x, ULONG64 y) |
NT Paths start with \??\
so this just checks if the relative path starts with C:\
, assuming C drive as system root.
In essence it means that file operations will no longer follow the device map of the impersonated token if the destination lies on a system root drive.
In my opinion this successfully eradicates the impersonated device map bug class that’s public for 8 years since James Forshaw’s discovery, how amazing is that!
Related Bugs
The first publicly documented bug using this technique should be CVE-2015-1644 by James Forshaw.
During that period attacks were prevalent on redirecting LoadLibrary
calls to load arbitrary DLLs.
Microsoft subsequently introduced the OBJ_IGNORE_IMPERSONATED_DEVICEMAP
flag but only applied it to PE loading operations.
Bugs from CreateProcessW
and even read/write operations existed for many years, with the most recent one being CVE-2023-36874.
This bug is patched a month before mine and also concerns WER.
Instead of coercing a process creation, it exploits a triggerable CreateProcessW
call under impersonation to execute arbitrary binaries.
I believe the July patch simply disabled this code path
1 | __int64 __fastcall CWerComReport::SubmitReport(CWerComReport *this, unsigned __int16 *a2, unsigned int a3, struct IWerReportSubmitCallback *a4, unsigned __int16 **a5, unsigned int *a6) |
Not sure what made them pull the trigger this time.
Bug Discovery
How did I discover this bug?
Well it’s anti-climatic but I simply rebooted my machine, launched procmon and spoofed my device map while waiting for services to initialize.
One very popular(you could argue inbuilt) third party service threw an exception while on impersonation because it couldn’t find one of its libraries, and I caught it on procmon trying to locate WerFault.exe
in my spoofed directory.
Nevertheless I’m very grateful to play a part in the mitigation of a bug class and receive my first CVE.
1337 bugs shall come.