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
Pimpersonating medium IL user - Unhandled exception in
Pwhile 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.