
Hardware Breakpoint (DR0-DR7) based patch-less user-mode hooking & telemetry instrumentation engine (AMSI, WLDP & ETW PoC).
A security-research Proof-of-Concept (POC) demonstrating hardware-breakpoint (CPU debug register) based function hooking as an alternative to traditional in-memory code patching.
Purpose & Scope
This repository is published strictly for defensive security research, red-team/purple-team education, detection engineering, and academic study of Windows internals. It demonstrates how an attacker could abuse processor debug registers to neutralize user-mode security telemetry — and, equally important, what defenders should monitor for in order to detect such techniques. The author is not responsible for any misuse of this code. Usage of this technique against systems without explicit authorization is illegal and violates the relevant computer-fraud and abuse laws in most jurisdictions. Do not deploy this in any environment you do not own or have explicit written permission to test.
mora_hwbp.c implements a DLL that, once loaded/injected into a target process (e.g., a PowerShell host), hooks four user-mode functions exclusively through CPU hardware breakpoints stored in the architectural debug registers (DR0–DR7) of every thread in the process:
A per-process Vectored Exception Handler (VEH) receives the EXCEPTION_SINGLE_STEP (0x80000004) faults raised by the debug registers, simulates the original function's successful return path by rewriting the exception context, and resumes execution — all without modifying a single byte of executable memory.
This makes the technique particularly interesting from both offensive and defensive perspectives:
.text sections (classic inline hooking, EAT/IAT patching, or Etwp* stubbing).GetThreadContext/SetThreadContext syscall patterns) that can be used for detection.Traditional user-mode hooking approaches — inline detours (5–14 byte overwrites), import address table (IAT) hooking, and export address table (EAT) hooking — share a common weakness: they modify memory that integrity scanners and ETW can observe.
Modern AV/EDR products implement:
pageguard/guard-page tricks, VirtualProtect transitions to PAGE_EXECUTE_READWRITE, and section hash mismatches.Hardware breakpoints sidestep all of this:
.text.SetThreadContext, which does not trigger the classic "memory modified" signals used by integrity scanners.This POC explores the efficacy and detectability of this technique against AMSI (Antimalware Scan Interface), WLDP (Windows Lockdown Policy), and ETW (Event Tracing for Windows) — the three most widely relied-upon user-mode security primitives in the modern Windows security stack.
AMSI is the Windows platform integration point that allows applications (PowerShell, Office, VBScript, .NET hosts, etc.) to request content scanning from registered antimalware providers. Two entry points are of primary interest:
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)By forcing the returned AMSI_RESULT to AMSI_RESULT_CLEAN (0), the script engine believes the content was inspected and found benign, so execution continues uninterrupted.
WLDP implements policy evaluation for Windows Defender Application Control (WDAC / Device Guard). WldpIsClassInApprovedList answers whether a given COM class (identified by GUID) is permitted under the current policy. AMSI internally consults WLDP to decide whether certain script/content classes are "trusted" (in the approved list). If the function reports the class as approved, AMSI may skip additional scrutiny for that content type.
The DLL sets the isApproved output parameter (RDX) to TRUE and returns S_OK, making the evaluated class appear trusted.
EtwEventWrite in ntdll.dll is the core user-mode sink for virtually all ETW event emission on the system. Suppressing it has broad side effects relevant to security monitoring:
Microsoft-Windows-DotNETRuntime)The DLL simply returns ERROR_SUCCESS (0) without executing the real function.
┌──────────────────────────────────────────────────────────────────────────┐
│ 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) └──────┘ │ ││
│ └──────────────────────────┘ └──────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────┘
High-level flow:
DLL_PROCESS_ATTACH (or via the exported InstallHook), the target exports are resolved with GetProcAddress (optionally forcing module loads via LoadLibraryW).AddVectoredExceptionHandler(1, ...)).CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) and hooked.#DB single-step exception; Windows dispatches it to the VEH, which simulates a benign return and continues execution.
Figure 1 — Sample diagnostics output captured with Sysinternals DebugView. Each line reports the resolved target address and the live hit counter for its corresponding debug register.
On x86/x64, each CPU provides four hardware debug-address registers (DR0–DR3) and a control register (DR7). Any thread executing with a non-zero breakpoint in DR0–DR3 will fault whenever the instruction pointer reaches that address (or data access matches the configured conditions). The status register DR6 records which breakpoint fired.
Hardware breakpoints are context-sensitive: they are stored in the thread's CONTEXT structure and apply only to the thread on which they are set. This is why a robust implementation must set breakpoints on every thread of the process (and continuously re-apply them for new threads).
DR7 is a bitfield controlling breakpoint enablement and behavior:
All four breakpoints are configured for execute (instruction-fetch) on a single byte, which is the appropriate condition for function-entry hooks.
When a breakpoint triggers, the processor raises a #DB exception. On x64 Windows the ntdll dispatch routine routes it through the process-wide VEH chain before the thread's Structured Exception Handler (SEH) chain. The handler in this project:
EXCEPTION_SINGLE_STEP (0x80000004); everything else falls through to EXCEPTION_CONTINUE_SEARCH.ExceptionAddress against the four known function addresses.RIP = *(RSP) → "return" to the original caller by popping the return address.RSP += 8 → simulate a ret (x64 single-instruction unwind).RAX = 0 → spoof S_OK / ERROR_SUCCESS (successful return code).DR6 &= ~0xF → clear the breakpoint status bits so the instruction can be re-executed later without spurious state.EXCEPTION_CONTINUE_EXECUTION, which tells Windows to restart the thread with the modified context — i.e., execution resumes at the , and the real target function .Each interception site is additionally wrapped in an SEH __try/__except guard so that a malformed or unexpected stack layout cannot crash the process — a robustness consideration for hostile/hardened targets.
DR0 — AmsiScanBuffer (x64, first 6 args in 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) to the 6th parameter (pResult, at [RSP+0x30]).S_OK (0) in RAX.DR1 — AmsiScanString (x64, first 5 args in RCX, RDX, R8, R9, [RSP+0x28]):
HRESULT AmsiScanString(HANDLE, LPCWSTR, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28]
AMSI_RESULT_CLEAN (0) to the 5th parameter (pResult, at [RSP+0x28]).S_OK (0) in RAX.DR2 — WldpIsClassInApprovedList (first 3 args in RCX, RDX, R8):
HRESULT WldpIsClassInApprovedList(const GUID* classId, PBOOL isApproved, DWORD evalCriteria);
RCX RDX R8
TRUE to *isApproved (via RDX).S_OK (0) in RAX.DR3 — EtwEventWrite (first 4 args in RCX, RDX, R8, R9):
ULONG EtwEventWrite(HANDLE RegHandle, PCEVENT_DESCRIPTOR EventDescriptor,
ULONG UserDataCount, PEVENT_DATA_DESCRIPTOR UserData);
ERROR_SUCCESS (0) in RAX without touching any output parameter.Because debug registers are per-thread, the engine must continuously maintain the hooks:
DllMain (or InstallHook) hooks the calling thread with SetHwbpOnThread(GetCurrentThread()).HookAllThreads() enumerates every thread in the process via a TH32CS_SNAPTHREAD snapshot, suspends each foreign thread (SuspendThread), applies the breakpoints (SetHwbpOnThread), resumes it, and closes the handle. Suspension prevents a race where the thread faults mid-context-swap between GetThreadContext and SetThreadContext.MonitorThreadProc loops with Sleep(500) and calls HookAllThreads() every 500 ms. This re-arms any breakpoints that were removed (e.g., by an external call, a debugging tool, or thread teardown/creation) and .Answer to the "hook persistence" question explicitly: yes — if the breakpoints are stripped from any thread (by another agent, a debugger, or an EDR), the monitor thread re-applies them within 500 ms. Additionally, any thread created after DLL load is hooked within one monitor cycle. The only reliable way to defeat this specific engine is to terminate the monitor thread and clear the VEH and strip the registers within the same window — or to use anti-debugging that denies
SetThreadContextfrom the outset.
Hit counters (g_HaveAmsiBuf, g_HaveAmsiStr, g_HaveWldp, g_HaveEtw) are maintained with InterlockedIncrement and are exposed in the debug output, which is useful for validating that interception is actually occurring in a lab environment.
Note that DllMain itself performs the full hooking sequence on DLL_PROCESS_ATTACH, so the exports are optional conveniences for runtime (un)loading scenarios.
Requirements: Windows 10/11 x64, Visual Studio Build Tools (icx.exe), SDK.
Compile the DLL (x64):
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"
Flags explained:
| Flag | Purpose |
|---|---|
The result is mora_hwbp.dll, which can be loaded into a target process.
The DLL must be loaded into a process that uses AMSI/WLDP/ETW — a PowerShell host is the canonical test bed. Loading can be done with any standard DLL-injection technique. A minimal, self-contained demonstration using Reflective/LoadLibrary injection can be performed with a small C loader:
rem Run from an x64 developer prompt (example with a generic loader)
loader.exe mora_hwbp.dll powershell.exe
Or, for a manual lab check, inject with your preferred tooling and then validate from 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"
Lab validation only. Observe with a debugger or
GetStats/OutputDebugStringthat all four breakpoints report hits as script content is executed.
This POC is dual-purpose: the same characteristics that make it effective offensively are precisely what defenders should hunt for.
GetThreadContext(CONTEXT_DEBUG_REGISTERS) on high-value processes and audit any thread with nonzero DR0–DR3 outside approved debugger profiles.Microsoft-Windows-Kernel-Process + Thread tracing and alert on NtGetContextThread/NtSetContextThread targeting security-relevant processes.EtwEventWrite, kernel ETW, AMSI consumer re-check) rather than .text integrity alone.SetThreadContext to other processes as an explicit high-severity signal.RCX/RDX/R8/R9 then [RSP+0x20…]). An x86 variant would need [EBP+…]-style parameter reconstruction.Sleep(500) iterations; extremely fast thread spawning combined with aggressive stripping could theoretically out-run the monitor for a few hundred milliseconds.OutputDebugStringA-based status — diagnostics rely on a debug output channel; in a fully stripped/headless environment you should attach a debugger or redirect the output for lab observation.This project is licensed under the MIT License - see the LICENSE file for details.
This project is released for educational and defensive research purposes only. If you are a security vendor, blue team, or detection engineer, you are encouraged to use the contents of this repository to improve your detection coverage for hardware-breakpoint-based evasion. If you discovered this technique being abused in the wild, report it through your organization's responsible-disclosure process and the relevant vendor/authority channels.
Use at your own risk. Unauthorized use of this technique may violate applicable laws.
| Register | Hooked Function | Module | Purpose |
|---|
DR0 | AmsiScanBuffer | amsi.dll | Neutralize AMSI content scanning |
DR1 | AmsiScanString | amsi.dll | Neutralize AMSI string scanning |
DR2 | WldpIsClassInApprovedList | wldp.dll | Force WLDP class approval (Device Guard / WDAC) |
DR3 | EtwEventWrite | ntdll.dll | Suppress ETW event tracing |
| Bits | Field | Meaning |
|---|
0 | L0 | Local enable for breakpoint 0 (DR0) |
2 | L1 | Local enable for breakpoint 1 (DR1) |
4 | L2 | Local enable for breakpoint 2 (DR2) |
6 | L3 | Local enable for breakpoint 3 (DR3) |
8 | LE | Legacy local enable (kept for compatibility) |
9 | GE | Legacy global enable (kept for compatibility) |
16–17 | R/W0 | Access type for BP0 (00 = instruction execution) |
18–19 | Len0 | Length for BP0 (00 = 1 byte) |
20–21 | R/W1 | Access type for BP1 (00 = instruction execution) |
22–23 | Len1 | Length for BP1 (00 = 1 byte) |
24–25 | R/W2 | Access type for BP2 (00 = instruction execution) |
26–27 | Len2 | Length for BP2 (00 = 1 byte) |
28–29 | R/W3 | Access type for BP3 (00 = instruction execution) |
30–31 | Len3 | Length for BP3 (00 = 1 byte) |
SetThreadContextHookAllThreads runs under a CRITICAL_SECTION (g_HookLock) so the monitor thread and the initial hooking routine never interleave context switches.UninstallHook stops the monitor, clears DR0–DR7 on every thread, and deregisters the VEH.| Export | Signature | Behavior |
|---|
InstallHook | BOOL WINAPI InstallHook(void) | Resolves targets, registers VEH, hooks all threads, starts monitor. |
UninstallHook | BOOL WINAPI UninstallHook(void) | Stops monitor, clears breakpoints on all threads, removes VEH. |
GetStats | void WINAPI GetStats(void) | Emits current hook status (via OutputDebugStringA) — address, hit counters. |
/O3| Maximum optimization (function code, not required) |
/MT | Static CRT linkage (no runtime DLL dependency) |
/EHsc | C++/SEH exception handling (needed for __try) |
/DLL | Produce a DLL with an export table |
| Artifact | Observable |
|---|
GetThreadContext / SetThreadContext calls | High-frequency debug-register context switches on other processes/threads (kernel ETW: Microsoft-Windows-Kernel-Process/Thread APIs). |
Nonzero DR0–DR3 | Any thread whose CONTEXT_DEBUG_REGISTERS contain a user-mode address outside known debugger workflows. |
DR7 local-enable bits (L0–L3) with R/W = 00 | Execute-only breakpoints on non-debugger-managed threads — a strong anomaly. |
EXCEPTION_SINGLE_STEP volume | High rates of #DB faults (0x80000004) originating from a process's VEH. |
| First-chance VEH registration | Newly added VEH (AddVectoredExceptionHandler) shortly before #DB storm. |
TH32CS_SNAPTHREAD + SuspendThread/ResumeThread | Repeated thread enumeration + suspension patterns (used by the 500 ms monitor). |
Load of wldp.dll/amsi.dll via LoadLibraryW when not previously loaded | Anomalous module loads in the target process. |
EtwEventWrite never reached | Absence of expected ETW events (PowerShell operational logs silent while scripts run). |