Tale of my first cve
我的第一个 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.
随着该错误在 8 月的补丁星期二作为 CVE-2023-35359 修复,我终于可以公开分享它了。
The bug itself is pretty useless and almost always non-exploitable(afaik), but the patch is huge and worth a blog post 🙂
该错误本身非常无用,几乎总是不可利用的(afaik),但是该补丁很大,值得:)博客文章
I’ll be writing about the bug itself, how I found it, my expected patch and the actual patch by Microsoft.
我将写关于错误本身,我是如何找到它的,我预期的补丁和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.
当 Windows 上发生未经处理的异常时,程序将非自愿地尝试唤醒 Windows 错误报告( WER
) 服务以进行日志记录和分析。
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.
在唤醒调用失败的情况下,例如当服务被显式标记为 Disabled
时,错误程序将创建一个 WerFault.exe
子级来收集特定于程序的统计信息。
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.
如果错误程序是冒充我们当前用户的特权进程,我们将能够使用欺骗性的DOS设备映射劫持进程创建,并以高完整性执行任意代码。
The conditions are: 条件是:
- WER service marked as disabled
标记为已禁用的 WER 服务 - Privileged process
P
impersonating medium IL user
特权进程P
模拟中型 IL 用户 - Unhandled exception in
P
while under impersonation
模拟时未P
处理的异常
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 2 3 4 5 6 7 8 9 |
int main(int argc, char **argv) { getchar(); *(int *)0 = 0; return 0; } |
Programmers are expected to handle their own exceptions using SEH
(try-except) if using C or C++ Exception Handling
(try-catch) if using C++.
如果使用 C 语言,程序员应该使用 (try-except) 来处理自己的异常, C++ Exception Handling
如果使用 C++则使用 SEH
(try-catch)。
Unhandled exceptions on Windows fall back to the default exception handler KernelBase!UnhandledExceptionFilter
.
Windows 上未经处理的异常回退到默认的异常处理程序 KernelBase!UnhandledExceptionFilter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
LONG __stdcall UnhandledExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) { ... if ( v3->ExceptionCode == -1073740791 && ((unsigned int)BasepIsKernelDebuggerPresent() || (unsigned int)BasepIsDebugPortPresent()) ) { DbgPrint_0("\r\nSTATUS_STACK_BUFFER_OVERRUN encountered\r\n"); __debugbreak(); } ... if ( !v14 || v14 == 126 ) { v8 = BasepReportFault(ExceptionInfo, 1i64); v28 = v8; v29 = 1; } } ... } |
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
.
否则,将执行传递给 以 Kernel32!BasepReportFault
报告崩溃,这是一个包装器 Kernel32!WerpReportFault
并最终调用 Kernel32!WerpReportFaultInternal
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
__int64 __fastcall WerpReportFaultInternal(__int64 a1) { ... status = RtlWerpReportException(v17, v39, v46, v32, 0, &v33); ... if ( status >= 0 ) { v20 = 1769483; if ( status == 1769483 ) { DbgPrintEx( 0x96u, 0, "WER/ReportFault:%u: ERROR RtlWerpReportException failed: too many concurrent workers\n", 952i64); goto LABEL_49; } goto LABEL_26; } ... v21 = StartCrashVertical(v6, v46, v32, &v33); ... LABEL_26: ... if ( v34 ) NtClose(v34); if ( TargetHandle ) NtClose(TargetHandle); if ( v5 ) NtClose(v5); if ( v4 ) NtClose(v4); if ( v1 ) UnmapViewOfFile(v1); if ( v6 ) NtClose(v6); if ( v33 ) NtClose(v33); return MapReturnCode(v20); } |
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).
此函数调用 ntdll!RtlWerpReportException
,它负责唤醒 WER 服务(通过 ETW 触发器)并向其发送消息(通过此处提到的 ALPC)。
If the call succeeds, the service routine WerSvc!CWerService::SvcReportCrash
will be invoked to organize crash statistics.
如果调用成功,将调用服务例程 WerSvc!CWerService::SvcReportCrash
来组织崩溃统计信息。
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
.
最后,在错误进程终止之前,将创建一个线程,以使用最终调用 CreateProcessAsUserW
的辅助 dll 导出 Faultrep!CreateCrashVerticalProcess
作为子进程 WerFault.exe
启动。

In the event where the service is explicitly disabled, ntdll!RtlWerpReportException
can’t wake it up and fails with an error status.
如果服务被显式禁用, ntdll!RtlWerpReportException
则无法唤醒它并失败并显示错误状态。
As shown in the pseudocode above, Kernel32!WerpReportFaultInternal
takes things into its own hands and calls its own version of CreateCrash, Kernel32!StartCrashVertical
如上面的伪代码所示,将事情掌握在自己手中并 Kernel32!WerpReportFaultInternal
调用自己的 CreateCrash 版本, Kernel32!StartCrashVertical

This function interestingly calls CreateProcessW
instead.
有趣的是,此函数会改为调用 CreateProcessW
。

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.
如前所述,容易受到模拟设备映射攻击,因为它在尝试查找进程映像时使用其模拟令牌, CreateProcessW
但为最终进程配备其主令牌,该令牌可能具有高特权。
CreateProcessAsUserW
on the other hand already takes a token handle as an argument.
CreateProcessAsUserW
另一方面,已经将令牌句柄作为参数。
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
.
我认为该错误可能是因为正在处理 Kernel32!StartCrashVertical
的开发人员不知道 . Faultrep.dll
My patch would be to either call into Faultrep.dll
or replace the CreateProcessW
call with a call to CreateProcessAsUserW
.
我的补丁是调用 Faultrep.dll
或用调用 CreateProcessAsUserW
替换 CreateProcessW
呼叫。
Microsoft’s eventual patch is much more drastic.
Microsoft的最终补丁要激烈得多。
The Patch 补丁
After receiving news of the patch, I launched procmon to view the stack traces like shown above.
收到补丁的消息后,我启动了 procmon 查看堆栈跟踪,如上所示。
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.
我在补丁 ntoskrnl.exe
之前和之后进行了不同,发现了一些额外的功能和修改。


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.
ObpUseSystemDeviceMap
作为一个附加功能听起来非常有趣,并且在 ObpLookupObjectName
从磁盘加载进程映像时调用,因此对它的修改很可能是Microsoft的补丁。
1 2 |
if ( objectType == IoFileObjectType && ObpUseSystemDeviceMap(ObjectNameRef, ObjectNameLength, a9, v14) ) finalAttribute = initialAttribute | OBJ_IGNORE_IMPERSONATED_DEVICEMAP; |
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
成功,则应用修补程序 ObpLookupObjectName
以忽略模拟令牌中的设备映射。
ObpUseSystemDeviceMap ObpUse系统设备地图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
bool __fastcall ObpUseSystemDeviceMap(PUNICODE_STRING ObjectName, ULONG Length, ULONG64 x, ULONG64 y) { WCHAR *v5; // rax WCHAR v6; // di WCHAR v7; // si WCHAR *v8; // rcx bool result; // al result = 0; if ( (*(_DWORD *)(&KeGetCurrentThread()[1].SwapListEntry + 1) & 8) != 0 && ObjectName->Length >= 0xEu ) { v5 = (WCHAR *)RtlGetNtSystemRoot(); v6 = RtlUpcaseUnicodeChar(*v5); v7 = RtlUpcaseUnicodeChar(ObjectName->Buffer[4]); if ( (unsigned int)Feature_MSRC79577_ObpDriveRemappingMitigation__private_IsEnabled() ) { v8 = ObjectName->Buffer; if ( v8[5] == ':' && v8[6] == '\\' && v6 == v7 ) result = 1; } } return result; } |
NT Paths start with \??\
so this just checks if the relative path starts with C:\
, assuming C drive as system root.
NT 路径以 开头 \??\
,因此仅检查相对路径是否以 开头 C:\
,假设 C 驱动器为系统根目录。
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!
在我看来,这成功地消除了自 James Forshaw 发现以来公开了 8 年的模拟设备映射错误类,这是多么神奇!
Related Bugs 相关错误
The first publicly documented bug using this technique should be CVE-2015-1644 by James Forshaw.
使用这种技术的第一个公开记录的错误应该是James Forshaw的CVE-2015-1644。
During that period attacks were prevalent on redirecting LoadLibrary
calls to load arbitrary DLLs.
在此期间,攻击在重定向 LoadLibrary
调用以加载任意 DLL 时很普遍。
Microsoft subsequently introduced the OBJ_IGNORE_IMPERSONATED_DEVICEMAP
flag but only applied it to PE loading operations.
Microsoft随后引入了该标志, OBJ_IGNORE_IMPERSONATED_DEVICEMAP
但仅将其应用于 PE 加载操作。
Bugs from CreateProcessW
and even read/write operations existed for many years, with the most recent one being CVE-2023-36874.
来自甚至读/写操作的错误 CreateProcessW
已经存在多年,最近的一次是 CVE-2023-36874。
This bug is patched a month before mine and also concerns WER.
这个错误比我早一个月修补,也涉及 WER。
Instead of coercing a process creation, it exploits a triggerable CreateProcessW
call under impersonation to execute arbitrary binaries.
它不是强制创建进程,而是利用模拟下的可 CreateProcessW
触发调用来执行任意二进制文件。
I believe the July patch simply disabled this code path
我相信七月补丁只是禁用了此代码路径
1 2 3 4 5 6 7 8 9 10 11 12 13 |
__int64 __fastcall CWerComReport::SubmitReport(CWerComReport *this, unsigned __int16 *a2, unsigned int a3, struct IWerReportSubmitCallback *a4, unsigned __int16 **a5, unsigned int *a6) { ... if ( (unsigned __int8)wil::details::FeatureImpl<__WilFeatureTraits_Feature_MSRC80633_DisableWerCplSupport>::__private_IsEnabled(&`wil::Feature<__WilFeatureTraits_Feature_MSRC80633_DisableWerCplSupport>::GetImpl'::`2'::impl) ) return 0x80004001i64; v11 = CAutoImpersonate::ImpersonateUserHighestPrivs((CAutoImpersonate *)v12); ... } |
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.
好吧,这是反气候的,但我只是重新启动了我的机器,启动了procmon并在等待服务初始化时欺骗了我的设备地图。
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.
一个非常流行(你可以说是内置的)第三方服务在模拟时抛出了一个异常,因为它找不到它的库之一,我在 procmon 上发现了它,试图在我的欺骗目录中找到 WerFault.exe
它。
Nevertheless I’m very grateful to play a part in the mitigation of a bug class and receive my first CVE.
尽管如此,我非常感谢在缓解错误类方面发挥了作用,并收到了我的第一个 CVE。
1337 bugs shall come.
1337个虫子会来。
原文始发于cp:CVE-2023-35359 analysis