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, 暴力破解, 栈展开, 概念验证, 白帽子, 系统安全, 网络安全监控, 蓝屏, 逆向工程, 驱动开发