XaFF-XaFF/BugcheckSuppressor

GitHub: XaFF-XaFF/BugcheckSuppressor

在开启 HVCI 和 kCET 的 Windows 11 内核中,通过纯数据 Hook 和 SEH 驱动的栈展开拦截蓝屏死机,使系统从致命错误中恢复并继续运行的概念验证驱动。

Stars: 15 | Forks: 2

# BugcheckSuppressor **感知 HVCI/kCET 的蓝屏抑制器概念验证** 在 Windows 内核触发蓝屏死机(BSOD)前将其拦截。使用 SEH 通过 `__finally` 处理程序展开,而非修补内核代码。 已在开启 HVCI + kCET 的 **Windows 11 24H2 build 26200** 上测试。 ## 注意事项 将此概念验证视为一个思想实验: “在 HVCI 环境下,如果不向内核运行时写入任何代码,我们能在多大程度上扭曲内核以抑制蓝屏?” 长期的动机是探索在 HVCI+kCET 系统上真正的 PatchGuard 抑制器是否可行。 这个实现是探究该问题的落脚点,而非答案。 它表明可以通过仅修改数据的 Hook 拦截蓝屏分发,恢复过程可以完全由 `RtlUnwindEx` 驱动,并且生成的控制流对影子堆栈是干净的。 这是 PatchGuard 问题中较容易的一半:捕获蓝屏。 困难的一半是在捕获后存活下来,尤其是当蓝屏源自 ntoskrnl 自身时 (例如,PatchGuard 自身通过 DPC 触发的 `KeBugCheckEx 0x109`,或是内核自身的 源自 `PspProcessDelete` 的列表项验证 `int 29h`)。 ## 工作原理 当内核蓝屏即将导致系统崩溃时, BugcheckSuppressor: 1. **Hook 分发表。** `HalPrivateDispatchTable[HalpPrepareForBugcheck]` 和 `[HalNotifyProcessorFreeze]` 是数据指针,因此我们可以 Hook 它们 而不会触发 HVCI。 2. **在入口处捕获蓝屏。** 当蓝屏触发时,Hook 会在 BSOD 显示*之前*运行。 3. **释放蓝屏全局变量**,以便在 `KiFreezeTargetExecution` 中自旋的从属 CPU 退出其等待循环。系统不再冻结。 4. **通过 `RtlUnwindEx` 展开内核栈**,从 Hook 帧回溯到错误驱动程序的调用者(通常是 `IopLoadDriver`),并在过程中运行每个 `__finally` 处理程序。这会清理 `IopLoadDriver` 端的资源。 5. **在调用者处恢复执行**,此时 `RAX = STATUS_ACCESS_VIOLATION`。 从调用者的角度来看,引发错误的函数带着错误返回了, 这与 SEH 自然捕获到违规时的返回方式相同。 错误驱动程序加载失败,用户得到 `ERROR_NOACCESS` 类别的 状态,系统保持运行。 ## 仓库结构 ``` BugcheckSuppressor/ ├── BugcheckSuppressor/ # The suppressor driver │ ├── source.cpp # Main hook + kernel state release + DPC watchdog reset │ ├── hvci_seh_recovery.cpp # SEH-driven RtlUnwindEx recovery (bad-driver and kernel-origin) │ ├── cet_stubs.asm # kCET-aware register restore + indirect JMP (asm fallback) │ ├── header.h │ └── BugcheckSuppressor.vcxproj ├── Trigger/ # Companion test driver │ ├── Source.cpp # Demo: write to RX memory section to trigger 0x50 fault │ └── Trigger.vcxproj └── README.md ``` ## 构建 **要求:** - Visual Studio 2022 及带有 Spectre 缓解的 MSVC (v143) - Windows Driver Kit (WDK) 10.0.26100.0 或更新版本 - 测试签名证书,或使用 `bcdedit /set TESTSIGNING ON` ### 准备目标环境 在核心隔离设置中启用内存完整性和内核模式硬件强制堆栈保护。 ``` bcdedit /set TESTSIGNING ON shutdown /r /t 0 ``` ### 演示:抑制错误驱动程序引起的蓝屏 ``` sc create BugcheckSuppressor type= kernel binPath= C:\path\to\BugcheckSuppressor.sys sc start BugcheckSuppressor sc create Trigger type= kernel binPath= C:\path\to\Trigger.sys sc start Trigger ``` 或者使用 OSR Loader 加载驱动程序。 `Trigger.sys` 将 `0xC3` (RET) 写入 `nt!KeBugCheckEx` 的序言中。 在 HVCI 下,EPT 强制对内核代码页执行 W=0(不可写入),因此写入 会引发 `#PF` 错误。`KiPageFault` 将未处理的异常路由到 `KeBugCheck2`。如果没有 BugcheckSuppressor,这会立刻导致 BSOD。**加载 BugcheckSuppressor 后**,蓝屏被捕获,触发驱动程序 加载失败(`sc start` 返回 `ERROR_NOACCESS`),系统保持 运行。在 DbgView 中验证(启用 Capture Kernel)。你应该会 看到类似以下内容: ``` [Suppressor] Recover: kOrigin=0 tRip=... tRsp=... tRax=00000000C0000005 ... [Suppressor/SEH] target found: ip=... frame=... (bad-driver caller, depth=N) [Suppressor/SEH] dispatching unwind to ip=... frame=... [Suppressor] Bypass! ``` ## 工作原理(技术细节) ### Hook 仅涉及数据 `HalPrivateDispatchTable` 是一个可写的数据页,导出函数 指针。HVCI 不保护数据页,只保护 .text。我们通过 `InterlockedExchangePointer` 原子性地交换了两个槽位 (偏移量为 `0x108` 的 `HalpPrepareForBugcheck`, 以及偏移量为 `0x1A8` 的 `HalNotifyProcessorFreeze`)。 ### 蓝屏路径入口 当调用 `KeBugCheckEx` 时: ``` KeBugCheckEx KeBugCheck2 HalpPrepareForBugcheck <- our hook fires here ... freezes other CPUs via IPI ... ... captures CONTEXT into PRCB+0x8FC0 ... ``` Hook 从 KPRCB 读取保存的 `CONTEXT`,识别陷阱 RIP/RSP,并判断故障是源自第三方 驱动程序(可通过 SEH 展开恢复)还是 ntoskrnl 内部(尽最大努力进行内核来源恢复)。 ### 错误驱动程序来源的恢复 (`HvciSehUnwindRecovery`) 通过 `RtlVirtualUnwind` 从 Hook 向上遍历栈帧。通过映像分类识别 “错误驱动程序帧”:从已知映像(ntoskrnl 加上抑制器)到未知映像的第一次转换就是错误 驱动程序发生故障的 RIP。再向上遍历一帧以找到错误驱动程序的 调用者(通常是 `IopLoadDriver`),然后调度 `RtlUnwindEx` 到 该帧,并设 `RAX = STATUS_ACCESS_VIOLATION`。Hook 帧与目标 之间的每个 `__finally` 都会运行,释放锁,减少 引用,恢复每线程状态。 这是抑制器能够干净利落、端到端处理的路径。 ### 内核来源的恢复 (`HvciKernelOriginUnwindRecovery`) 当蓝屏源自 ntoskrnl 内部时使用(例如 `PspProcessDelete` 内部的故障)。 没有错误驱动程序映像可作为锚点,因此跨栈解析器从保存的 RSP 中选取一个目标帧,我们 通过 `RtlUnwindEx` 向其展开,在 `RtlVirtualUnwind` 能达到的深度内 尽可能多地遍历。这最大化了运行的 `__finally` 处理程序数量。但它 受限于 ntoskrnl 的锁获取方式。 ### 汇编回退 (`cet_stubs.asm`) 如果 `RtlUnwindEx` 无法交付(`.pdata` 缺失、RSP 不匹配等),我们 将回退到感知 kCET 的汇编存根 (`HvciKcetJmpRestoreFixed`),它 通过预先计算的次数弹出影子堆栈,从 `CONTEXT` 恢复寄存器,切换 RSP,并间接 JMP 到目标。这能起作用, 但它不会运行中间的 `__finally` 块,因此被跳过帧持有的锁会泄漏。 ### 退出时的状态释放 在展开之前,Hook 会: - 释放 `KiBugCheckActive` / `KiHardwareTrigger` / `KiFreezeExecutionLock`, 以便对等 CPU 退出其冻结循环。 - 重置每个 PRCB 的 DPC 看门狗 `Count = Period`,这样恢复后的 系统不会因为累积的递减而在 10 到 20 秒后触发蓝屏 `0x133`。 - 从驱动程序初始化时拍摄的已知良好快照中恢复 `nt!HalpTimerWatchdog`, 以便时钟滴答服务恢复正常。 - 预先标记每个 `PRCB->IpiFrozen = 5`,以便冻结 IPI 的从属循环 看到“已经用我们的标记冻结”并立即退出。 这些清理工作正是系统在抑制后仍能保持可用的原因。没有它们, 你只会看到短暂的存活,随后在几秒钟后遇到 0x133 或另一次冻结。 ## 成果 1. **在 HVCI+kCET (Win11 24H2 26200) 上实现了端到端的蓝屏抑制。** 错误驱动程序来源的故障(写入 RX 页、内核异常、第三方 驱动程序代码的 fastfail)会被捕获,系统继续运行。错误驱动程序加载失败并返回 `STATUS_ACCESS_VIOLATION`, 这与 SEH 自然捕获到故障时的表现完全一致。 2. **所有 Hook 都仅针对数据。** 没有写入 ntoskrnl 的 `.text`,没有修补 任何 `.text` 字节,HVCI 不会拒绝任何操作。 3. **恢复过程使用 `RtlUnwindEx`**,这是内核自身的 SEH 分发器, 它感知影子堆栈。2022 年通过挂起线程进行 ROP 的方法已被 kCET 终止,而本方法不受影响。 ## 限制 ### 与生产相关的限制 - **构建特定的 KPRCB 偏移量。** 抑制器为 KPRCB 上下文保存区、调试器保存的 IRQL、IpiFrozen、DPC 看门狗等使用了硬编码的偏移量。 这些偏移量在 Windows 服务分支中是稳定的,但在不同 LCU 之间会偏移约 0x10 到 0x40。移植时请对照 `ntoskrnl.pdb` 进行验证。 ### 恢复边界 - **在任意点的内核来源蓝屏是“尽力而为”,而不是保证的。** 许多 ntoskrnl 函数通过显式的 `KeAcquireSpinLock` / `KeReleaseSpinLock` 对获取锁,而没有包围 `__try/__finally`。当 `RtlUnwindEx` 遍历这些帧时,本应释放它们的 `__finally` 处理程序根本不存在。锁会泄漏。任何接下来尝试获取它们的线程都会死锁。 这会影响人为的测试用例,例如通过 WinDbg 的 `r rdi=0` 将空指针解引用注入 `PspProcessDelete`。在定义良好的点上发生的现实世界内核来源 场景(通过 `KeBugCheckEx 0x109` 进行的 PG 蓝屏,kASAN 风格的不变量检查)往往更具 可恢复性,因为它们的调用者被设计为“对恢复友好的状态”。 - **在极早期或极晚期系统状态期间发生的蓝屏**(HAL 准备就绪之前的启动, 驱动程序卸载后的晚期关闭)不在范围内。 - **在极深堆栈上下文期间发生的蓝屏**可能会超出 `RtlVirtualUnwind` 的遍历能力。汇编回退可以处理这些情况,但会泄漏中间的 `__finally`。 - 如果 BugcheckSuppressor 无法恢复异常,系统将会冻结。 ### 操作方面 - **卸载驱动程序并不完全干净。** 取消 Hook 会原子性地恢复分派指针,但如果在卸载的那一刻有蓝屏正在发生,行为将是未定义的。不要在运行压力测试时卸载。 - **每台机器只能有一个抑制器实例。** 多个实例会在分派表交换时产生竞争。 - **必须使用 `/CETCOMPAT` 构建。** 否则,汇编回退的 `incsspq` 指令会触发陷阱。 ## 致谢与参考 该问题领域借鉴了之前的公开工作: - **Connor McGarr。** 原始的内核栈 ROP 抑制器 (`No Code Execution? No Problem!`,2022 年),以及解释了为什么基于 ROP 的恢复停止工作的 kCET 拆解。还有用于模糊测试 NT/SK 安全调用表面的 `SkBridge`(2025 年),这为这里的 `MmDbgCopyMemory` 分析提供了启发。 - **can1357。** 原始的 ByePG 技术,展示了蓝屏分发可以在 HAL 层被拦截。IpiFrozen 预标记模式借用了该工作。 - **Yarden Shafir。** `Secure Pool Internals`(2020 年),其中的 `ExSecurePoolUpdate` 分析为了解 VTL1 如何对 VTL0 可见内存做出写入决策提供了参考。 - **Saar Amar & Daniel King。** `Breaking VSM by Attacking SecureKernel`(Black Hat USA 2020),提供了典型的 SK 攻击面方法。 - **zer0condition。** `BusterCall` 提供了 PFN 交换技术,虽然本概念验证未使用,但它是“不写入 `.text` 即可执行内核代码”的正确参考点。
标签:BSOD, Bugcheck, HalPrivateDispatchTable, HVCI, kCET, PatchGuard, PoC, Rootkit, RtlUnwindEx, SEH, UML, Web报告查看器, Windows 11 24H2, Windows内核, Zeek, 云资产清单, 分布式计算, 子域名枚举, 异常处理, 数据Hook, 暴力破解, 栈展开, 概念验证, 白帽子, 系统安全, 网络安全监控, 蓝屏, 逆向工程, 驱动开发