一个安全研究概念验证(POC),演示了基于硬件断点(CPU 调试寄存器)的函数挂钩,作为传统内存代码修补的替代方案。
目的与范围
本仓库的发布严格用于针对 Windows 内部机制的防御性安全研究、红队/紫队教育、检测工程和学术研究。它演示了攻击者如何滥用处理器调试寄存器来中和用户态安全遥测——同样重要的是,防御者应当监测什么以检测此类技术。作者不对本代码的任何滥用负责。在未经明确授权的系统上使用此技术属违法行为,并且在大多数司法管辖区违反相关计算机欺诈和滥用法律。请勿在你不拥有或没有明确书面许可测试的任何环境中部署此代码。
mora_hwbp.c 实现了一个 DLL,一旦被加载/注入到目标进程(例如 PowerShell 主机)中,就会仅通过 CPU 硬件断点来挂钩四个用户态函数,这些断点存储在该进程每个线程的架构调试寄存器(DR0–DR7)中:
进程内的向量化异常处理程序(VEH) 接收调试寄存器引发的 EXCEPTION_SINGLE_STEP(0x80000004)异常,通过重写异常上下文模拟原函数的成功返回路径,然后恢复执行——全程不修改任何一字节的可执行内存。
这使得该技术从攻击和防御两个角度来看都特别有趣:
.text 节(经典的内联 detours、EAT/IAT 补丁或 Etwp* 桩)的完整性检查。GetThreadContext/SetThreadContext 系统调用模式),可用于检测。传统的用户态挂钩方式——内联 detours(覆盖 5–14 字节)、导入地址表(IAT)挂钩和导出地址表(EAT)挂钩——有一个共同的弱点:它们会修改完整性扫描器和 ETW 可以观察到的内存。
现代 AV/EDR 产品实现了:
pageguard/guard-page 技巧、VirtualProtect 向 PAGE_EXECUTE_READWRITE 的转换以及节哈希不匹配。硬件断点可以绕开所有这些:
.text 中没有任何可扫描的内容。SetThreadContext 以每个线程为基础设置,不会触发完整性扫描器所使用的经典“内存被修改”信号。本 POC 探索了该技术针对 AMSI(反恶意软件扫描接口,Antimalware Scan Interface)、WLDP(Windows 锁定策略,Windows Lockdown Policy) 和 ETW(Windows 事件跟踪,Event Tracing for Windows) 的有效性和可检测性——这三者是现代 Windows 安全栈中最广泛依赖的用户态安全原语。
AMSI 是 Windows 平台集成点,允许应用程序(PowerShell、Office、VBScript、.NET 主机等)向已注册的反恶意软件提供程序请求内容扫描。主要关注两个入口点:
AmsiScanBuffer(HANDLE hamsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HANDLE hamsiSession, AMSI_RESULT *pResult)AmsiScanString(HANDLE hamsiContext, LPCWSTR string, LPCWSTR contentName, HANDLE hamsiSession, AMSI_RESULT *pResult)通过将返回的 AMSI_RESULT 强制为 AMSI_RESULT_CLEAN (0),脚本引擎会认为内容已经过检查且未发现恶意,因此执行会不受阻碍地继续。
WLDP 为 Windows Defender 应用程序控制(WDAC / Device Guard)实现策略评估。WldpIsClassInApprovedList 回答给定的 COM 类(由 GUID 标识)在当前策略下是否被允许。AMSI 在内部咨询 WLDP,以决定某些脚本/内容类是否“受信任”(在批准的列表中)。如果该函数报告该类已获批准,AMSI 可能会跳过对该内容类型的额外审查。
该 DLL 将 isApproved 输出参数(RDX)设置为 TRUE 并返回 S_OK,使被评估的类看起来受信任。
ntdll.dll 中的 EtwEventWrite 是系统上几乎所有 ETW 事件发射的核心用户态接收器。抑制它会对安全监控产生广泛的副作用:
Microsoft-Windows-DotNETRuntime)该 DLL 只是返回 ERROR_SUCCESS (0),而不执行真正的函数。
┌──────────────────────────────────────────────────────────────────────────┐ │ Target Process (e.g. powershell.exe) │ │ │ │ ┌──────────────────────────┐ ┌──────────────────────────────────┐│ │ │ mora_hwbp.dll │ │ CPU / Windows ││ │ │ │ │ ││ │ │ DllMain / InstallHook │ │ Thread A Thread B ││ │ │ │ │ │ ┌────────┐ ┌────────┐ ││ │ │ ▼ │ │ │ DR0..3 │ │ DR0..3 │ ││ │ │ Resolve exports │ │ └────────┘ └────────┘ ││ │ │ (amsi/wldp/ntdll) │ │ ││ │ │ │ │ │ #DB (single-step) ││ │ │ ▼ │ │ exception ──► Windows Dispatch ││ │ │ AddVectoredException │ │ │ ││ │ │ Handler(VEH) │ │ ▼ ││ │ │ │ │ │ ┌─────────────────────────────┐ ││ │ │ ▼ │ │ │ VectoredHandler (ours) │ ││ │ │ SetHwbpOnThread(ALL) │ │ │ • match #DB address │ ││ │ │ │ │ │ │ • rewrite context (RIP/RSP)│ ││ │ │ ▼ │ │ │ • spoof return value (RAX) │ ││ │ │ MonitorThread ◄──┐ │ │ │ • continue execution │ ││ │ │ (re-hook every │ │ │ └─────────────────────────────┘ ││ │ │ 500ms) └──────┘ │ ││ │ └──────────────────────────┘ └──────────────────────────────────┘│ └──────────────────────────────────────────────────────────────────────────┘
**高级流程:**
1. DLL 被加载到目标进程中(通过任意注入技术——参见[用法](#injection--usage-example))。
2. 在 `DLL_PROCESS_ATTACH`(或通过导出的 `InstallHook`)时,使用 `GetProcAddress` 解析目标导出函数(可选地通过 `LoadLibraryW` 强制加载模块)。
3. 注册一个**向量化异常处理程序(Vectored Exception Handler)**,使其成为进程中的**第一个**处理程序(`AddVectoredExceptionHandler(1, ...)`)。
4. **当前线程**立即被挂钩,然后通过 `CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)` 枚举进程中**所有现有线程**并进行挂钩。
5. **监控线程**每 500 毫秒唤醒一次,并重新将断点应用到每个线程——包括**新创建的线程**——从而保证挂钩的持久性,即使在线程挂钩之后新产生了线程,或者断点被外部清除。
6. 当任何线程上调用任何被挂钩的函数时,CPU 会引发 `#DB` 单步异常;Windows 将其分发给 VEH,VEH 模拟一次无害返回并继续执行。
### 示例调试输出

*图 1 — 使用 Sysinternals DebugView 捕获的示例诊断输出。每一行报告已解析的目标地址及其对应调试寄存器的实时命中计数器。*
---
## 技术深入剖析
### 5.1 x64 上的硬件断点
在 x86/x64 上,每个 CPU 提供**四个硬件调试地址寄存器**(`DR0`–`DR3`)和一个控制寄存器(`DR7`)。任何线程只要在 `DR0`–`DR3` 中设置了非零断点,当指令指针到达该地址(或数据访问符合所配置的条件)时就会触发故障。状态寄存器 `DR6` 记录是哪个断点被触发。
硬件断点是**上下文相关**的:它们存储在线程的 `CONTEXT` 结构中,并且仅适用于设置它们的那个线程。这就是为什么一个健壮的实现必须将断点设置到进程的**每个线程**上(并持续为新建线程重新应用)。
### 5.2 调试寄存器布局(DR0–DR7)
`DR7` 是一个控制断点启用及其行为的位字段:
| Bits | Field | Meaning |
|--------|--------|-----------------------------------------------------|
| `0` | `L0` | 本地启用断点 0 (DR0) |
| `2` | `L1` | 本地启用断点 1 (DR1) |
| `4` | `L2` | 本地启用断点 2 (DR2) |
| `6` | `L3` | 本地启用断点 3 (DR3) |
| `8` | `LE` | 旧版本地启用(为兼容性保留) |
| `9` | `GE` | 旧版全局启用(为兼容性保留) |
| `16–17`| `R/W0` | BP0 的访问类型(`00` = 指令执行) |
| `18–19`| `Len0` | BP0 的长度(`00` = 1 字节) |
| `20–21`| `R/W1` | BP1 的访问类型(`00` = 指令执行) |
| `22–23`| `Len1` | BP1 的长度(`00` = 1 字节) |
| `24–25`| `R/W2` | BP2 的访问类型(`00` = 指令执行) |
| `26–27`| `Len2` | BP2 的长度(`00` = 1 字节) |
| `28–29`| `R/W3` | BP3 的访问类型(`00` = 指令执行) |
| `30–31`| `Len3` | BP3 的长度(`00` = 1 字节) |
四个断点均配置为**在单字节上执行(指令获取)**,这是函数入口挂钩的合适条件。
### 5.3 向量化异常处理程序(VEH)
当断点触发时,处理器会引发 `#DB` 异常。在 x64 Windows 上,`ntdll` 分发例程会在进入线程的结构化异常处理程序(SEH)链之前,将其路由到进程范围的 **VEH 链**。本项目中的处理程序:
1. **筛选** — 仅处理 `EXCEPTION_SINGLE_STEP`(`0x80000004`);其他所有异常都落入 `EXCEPTION_CONTINUE_SEARCH`。
2. **匹配** — 将 `ExceptionAddress` 与四个已知函数地址进行比较。
3. **重写上下文**:
- `RIP = *(RSP)` → 通过弹出返回地址“返回”到原始调用者。
- `RSP += 8` → 模拟 `ret`(x64 单指令展开)。
- `RAX = 0` → 伪造 `S_OK` / `ERROR_SUCCESS`(成功返回码)。
- `DR6 &= ~0xF` → 清除断点状态位,以便后续可以重新执行该指令而不会产生虚假状态。
4. **修改输出参数**(参见 [5.4](#54-per-component-interception-logic))。
5. **返回 `EXCEPTION_CONTINUE_EXECUTION`**,它告诉 Windows 使用修改后的上下文重启线程——即,执行在*调用者*处恢复,真正的目标函数**永远不会运行**。
每个拦截点还额外包裹在 SEH `__try/__except` 保护中,从而确保格式错误或意外的堆栈布局不会导致进程崩溃——这是针对恶意/加固目标的一种健壮性考量。
### 5.4 各组件拦截逻辑
**DR0 — `AmsiScanBuffer`**(x64,前 6 个参数位于 `RCX, RDX, R8, R9, [RSP+0x28], [RSP+0x30]`):```
HRESULT AmsiScanBuffer(HANDLE, PVOID, ULONG, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28] [RSP+0x30]
AMSI_RESULT_CLEAN (0) 写入第 6 个参数(pResult,位于 [RSP+0x30])。RAX 中返回 S_OK (0)。DR1 — AmsiScanString(x64,前 5 个参数位于 RCX, RDX, R8, R9, [RSP+0x28]):```
HRESULT AmsiScanString(HANDLE, LPCWSTR, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28]
- 将 `AMSI_RESULT_CLEAN (0)` 写入第 5 个参数(`pResult`,位于 `[RSP+0x28]`)。
- 在 `RAX` 中返回 `S_OK (0)`。
**DR2 — `WldpIsClassInApprovedList`**(前 3 个参数位于 `RCX, RDX, R8`):```
HRESULT WldpIsClassInApprovedList(const GUID* classId, PBOOL isApproved, DWORD evalCriteria);
RCX RDX R8
*isApproved 写入 TRUE(通过 RDX)。RAX 中返回 S_OK (0)。DR3 — EtwEventWrite(前 4 个参数在 RCX, RDX, R8, R9 中):```
ULONG EtwEventWrite(HANDLE RegHandle, PCEVENT_DESCRIPTOR EventDescriptor,
ULONG UserDataCount, PEVENT_DATA_DESCRIPTOR UserData);
- 在 `RAX` 中返回 `ERROR_SUCCESS (0)`,且不触碰任何输出参数。
- 后果:ETW 提供程序不会从被挂钩的进程收到**任何**事件,从而抑制脚本执行、模块加载、进程创建和 AMSI 遥测的日志记录。
### 5.5 线程管理与挂钩持久性
由于调试寄存器是每线程的,引擎必须持续维护这些挂钩:
1. **立即挂钩** — `DllMain`(或 `InstallHook`)通过 `SetHwbpOnThread(GetCurrentThread())` 挂钩调用线程。
2. **全线程扫描** — `HookAllThreads()` 通过 `TH32CS_SNAPTHREAD` 快照枚举进程中的每个线程,挂起每个外部线程(`SuspendThread`),应用断点(`SetHwbpOnThread`),恢复线程并关闭句柄。挂起操作可防止线程在 `GetThreadContext` 与 `SetThreadContext` 之间进行上下文切换中途出错而导致的竞态条件。
3. **持久性监视器** — `MonitorThreadProc` 以 `Sleep(500)` 循环,每 500 毫秒调用一次 `HookAllThreads()`。这会**重新启用任何已被移除的断点**(例如,由外部 `SetThreadContext` 调用、调试工具或线程销毁/创建导致),并**覆盖初始挂钩之后创建的线程**。
4. **同步** — `HookAllThreads` 在 `CRITICAL_SECTION`(`g_HookLock`)下运行,因此监视器线程和初始挂钩例程绝不会交错进行上下文切换。
5. **干净拆除** — `UninstallHook` 停止监视器,清除每个线程上的 `DR0–DR7`,并注销 VEH。
> **明确回答“挂钩持久性”问题:** 是的——如果任何线程上的断点被移除(由另一个代理、调试器或 EDR 移除),监视器线程会**在 500 毫秒内重新应用它们**。此外,DLL 加载后创建的任何线程都会在一个监视周期内被挂钩。要击败这个特定引擎,唯一可靠的方法是在同一时间窗口内终止监视器线程*并*清除 VEH*并*移除寄存器——或者从一开始就使用拒绝 `SetThreadContext` 的反调试手段。
---
## 导出 API
| 导出 | 签名 | 行为 |
|-------------------|------------------------------------|-----------------------------------------------------------------------|
| `InstallHook` | `BOOL WINAPI InstallHook(void)` | 解析目标,注册 VEH,挂钩所有线程,启动监视器。 |
| `UninstallHook` | `BOOL WINAPI UninstallHook(void)` | 停止监视器,清除所有线程上的断点,移除 VEH。 |
| `GetStats` | `void WINAPI GetStats(void)` | 输出当前挂钩状态(通过 `OutputDebugStringA`)——地址、命中计数器。 |
命中计数器(`g_HaveAmsiBuf`、`g_HaveAmsiStr`、`g_HaveWldp`、`g_HaveEtw`)通过 `InterlockedIncrement` 维护,并在调试输出中公开;这对于在实验室环境中验证拦截是否确实发生非常有用。
请注意,`DllMain` 本身会在 `DLL_PROCESS_ATTACH` 时执行完整的挂钩序列,因此这些导出函数只是为运行时加载/卸载场景提供的可选便利。
---
## 构建说明
**要求:** Windows 10/11 x64、Visual Studio Build Tools(`icx.exe`)、SDK。
编译 DLL(x64):```bat
icx.exe /nologo /O3 /MT /EHsc "mora_hwbp.c" /link /DLL /out:"mora_hwbp.dll" /LIBPATH:"C:\Program Files (x86)\Intel\oneAPI\compiler\latest\lib"
标志说明:
| Flag | Purpose |
|---|---|
/O3 | 最大优化(函数代码,非必需) |
结果是 mora_hwbp.dll,可将其加载到目标进程中。
必须将 DLL 加载到使用 AMSI/WLDP/ETW 的进程中——PowerShell 主机是典型的测试环境。可以使用任何标准 DLL 注入技术完成加载。一个最小化、自包含的演示,使用反射式/LoadLibrary 注入,可以通过一个小型 C 加载器来执行:```bat
rem Run from an x64 developer prompt (example with a generic loader)
loader.exe mora_hwbp.dll powershell.exe
或者,要进行手动实验室检查,请使用你偏好的工具进行注入,然后在 PowerShell 中进行验证:```powershell
# 1. Inject mora_hwbp.dll into the PowerShell process (via external tool).
# 2. Verify that classic AMSI test vectors now return clean.
"AmsiTestSample:7e72c3ce-861b-4339-8740-0ac1484c1386"
仅限实验室验证。 使用调试器或
GetStats/OutputDebugString观察所有四个断点在脚本内容执行时均报告命中。
此 POC 具有双重用途:使其在攻击方面有效的相同特性,正是防御者应重点搜寻的。
GetThreadContext(CONTEXT_DEBUG_REGISTERS),并审计任何在已批准的调试器配置文件之外具有非零 DR0–DR3 的线程。Microsoft-Windows-Kernel-Process + 线程跟踪,并对针对安全相关进程的 NtGetContextThread/NtSetContextThread 发出警报。EtwEventWrite 之下的 ETW 使用方钩子、内核 ETW、AMSI 使用方重新检查),而不是仅依靠 .text 完整性。SetThreadContext 视为明确的高严重性信号。RCX/RDX/R8/R9,然后是 [RSP+0x20…])。x86 变体需要 [EBP+…] 风格的参数重建。Sleep(500) 迭代之间存在一个(故意很小的)窗口;极快的线程创建加上激进的剥离,理论上可能领先监控器数百毫秒。OutputDebugStringA 的状态 — 诊断依赖调试输出通道;在完全剥离/无头环境中,应附加调试器或重定向输出以便实验室观察。本项目采用 MIT 许可证授权 — 详见 LICENSE 文件。
本项目仅用于教育和防御性研究目的。如果您是安全厂商、蓝队成员或检测工程师,我们鼓励您使用本仓库的内容来改进针对基于硬件断点的逃逸技术的检测覆盖。如果您在真实环境中发现此技术被滥用,请通过您所在组织的负责任披露流程以及相关厂商/官方渠道进行报告。
风险自负。未经授权使用此技术可能违反适用法律。
| 寄存器 | 挂钩的函数 | 模块 | 用途 |
|---|
DR0 | AmsiScanBuffer | amsi.dll | 中和 AMSI 内容扫描 |
DR1 | AmsiScanString | amsi.dll | 中和 AMSI 字符串扫描 |
DR2 | WldpIsClassInApprovedList | wldp.dll | 强制 WLDP 类批准(Device Guard / WDAC) |
DR3 | EtwEventWrite | ntdll.dll | 抑制 ETW 事件跟踪 |
/MT| 静态 CRT 链接(无运行时 DLL 依赖) |
/EHsc | C++/SEH 异常处理(__try 所需) |
/DLL | 生成带导出表的 DLL |
| 工件 | 可观察特征 |
|---|
GetThreadContext / SetThreadContext 调用 | 在其他进程/线程上的高频调试寄存器上下文切换(内核 ETW:Microsoft-Windows-Kernel-Process/Thread API)。 |
非零 DR0–DR3 | 任何 CONTEXT_DEBUG_REGISTERS 包含已知调试器工作流之外用户态地址的线程。 |
DR7 本地启用位(L0–L3)且 R/W = 00 | 在非调试器管理的线程上设置仅执行断点——一个强烈的异常信号。 |
EXCEPTION_SINGLE_STEP 数量 | 源自进程 VEH 的高频率 #DB 异常(0x80000004)。 |
| 首次机会 VEH 注册 | 在 #DB 风暴之前短时间内新增的 VEH(AddVectoredExceptionHandler)。 |
TH32CS_SNAPTHREAD + SuspendThread/ResumeThread | 重复的线程枚举+挂起模式(供 500 毫秒监控器使用)。 |
通过 LoadLibraryW 加载此前未加载的 wldp.dll/amsi.dll | 目标进程中的异常模块加载。 |
EtwEventWrite 始终未被调用 | 缺少预期的 ETW 事件(脚本运行期间 PowerShell 操作日志静默)。 |