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.
- Overview
- Background โ Why Hardware Breakpoints?
- Targeted Security Components
- Architecture
- Technical Deep Dive
- Exported API
- Build Instructions
- Injection & Usage Example
- Detection & Mitigation (Blue Team)
- Known Limitations
- References
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:
| 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 |
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:
- Offensively, it bypasses EDR/HIPS integrity checks that look for modified
.textsections (classic inline hooking,EAT/IATpatching, orEtwp*stubbing). - Defensively, hardware breakpoints leave highly distinctive forensic artifacts (debug-register contents, single-step exception density, VEH registration,
GetThreadContext/SetThreadContextsyscall 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:
- Memory scanning / AMSI scans of PowerShell and .NET CLR buffers;
- ETW-based telemetry (Microsoft-Windows-PowerShell, .NET ETW, threat intelligence providers);
- Kernel callbacks and user-mode integrity checks that detect
pageguard/guard-pagetricks,VirtualProtecttransitions toPAGE_EXECUTE_READWRITE, and section hash mismatches.
Hardware breakpoints sidestep all of this:
- They are CPU registers, not memory โ there is nothing to scan in
.text. - They are set on a per-thread basis via the Windows API
SetThreadContext, which does not trigger the classic "memory modified" signals used by integrity scanners. - The interception point is handled entirely by the processor's exception dispatch, which routes through the process VEH chain before any user-mode target function executes.
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:
- PowerShell pipeline and script-block logging events
- .NET assembly load events (
Microsoft-Windows-DotNETRuntime) - AMSI scan-result telemetry
- Threat-Intelligence provider events consumed by EDR agents
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:
- The DLL is loaded into the target process (via any injection technique โ see Usage).
- On
DLL_PROCESS_ATTACH(or via the exportedInstallHook), the target exports are resolved withGetProcAddress(optionally forcing module loads viaLoadLibraryW). - A Vectored Exception Handler is registered as the first handler in the process (
AddVectoredExceptionHandler(1, ...)). - The current thread is hooked immediately, then all existing threads in the process are enumerated via
CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)and hooked. - A monitor thread wakes every 500 ms and re-applies the breakpoints to every thread โ including newly created threads โ guaranteeing hook persistence even if a thread is spawned after hooking or if breakpoints are cleared externally.
- When any hooked function is called on any thread, the CPU raises a
#DBsingle-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:
| 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) |
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:
- Filters โ only handles
EXCEPTION_SINGLE_STEP(0x80000004); everything else falls through toEXCEPTION_CONTINUE_SEARCH. - Matches โ compares
ExceptionAddressagainst the four known function addresses. - Rewrites the context:
RIP = *(RSP)โ "return" to the original caller by popping the return address.RSP += 8โ simulate aret(x64 single-instruction unwind).RAX = 0โ spoofS_OK/ERROR_SUCCESS(successful return code).DR6 &= ~0xFโ clear the breakpoint status bits so the instruction can be re-executed later without spurious state.
- Mutates out-parameters (see 5.4).
- Returns
EXCEPTION_CONTINUE_EXECUTION, which tells Windows to restart the thread with the modified context โ i.e., execution resumes at the caller, and the real target function never runs.
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]
- Writes
AMSI_RESULT_CLEAN (0)to the 6th parameter (pResult, at[RSP+0x30]). - Returns
S_OK (0)inRAX.
DR1 โ AmsiScanString (x64, first 5 args in RCX, RDX, R8, R9, [RSP+0x28]):
HRESULT AmsiScanString(HANDLE, LPCWSTR, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28]
- Writes
AMSI_RESULT_CLEAN (0)to the 5th parameter (pResult, at[RSP+0x28]). - Returns
S_OK (0)inRAX.
DR2 โ WldpIsClassInApprovedList (first 3 args in RCX, RDX, R8):
HRESULT WldpIsClassInApprovedList(const GUID* classId, PBOOL isApproved, DWORD evalCriteria);
RCX RDX R8
- Writes
TRUEto*isApproved(viaRDX). - Returns
S_OK (0)inRAX. - Consequence: the evaluated content class is considered "approved" by the lockdown policy, and AMSI trusts that judgement for the class.
DR3 โ EtwEventWrite (first 4 args in RCX, RDX, R8, R9):
ULONG EtwEventWrite(HANDLE RegHandle, PCEVENT_DESCRIPTOR EventDescriptor,
ULONG UserDataCount, PEVENT_DATA_DESCRIPTOR UserData);
- Returns
ERROR_SUCCESS (0)inRAXwithout touching any output parameter. - Consequence: ETW providers receive no events from the hooked process, suppressing logging of script execution, module loads, process creation, and AMSI telemetry.
Because debug registers are per-thread, the engine must continuously maintain the hooks:
- Immediate hooking โ
DllMain(orInstallHook) hooks the calling thread withSetHwbpOnThread(GetCurrentThread()). - All-thread sweep โ
HookAllThreads()enumerates every thread in the process via aTH32CS_SNAPTHREADsnapshot, 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 betweenGetThreadContextandSetThreadContext. - Persistence monitor โ
MonitorThreadProcloops withSleep(500)and callsHookAllThreads()every 500 ms. This re-arms any breakpoints that were removed (e.g., by an externalSetThreadContextcall, a debugging tool, or thread teardown/creation) and covers threads created after the initial hook. - Synchronization โ
HookAllThreadsruns under aCRITICAL_SECTION(g_HookLock) so the monitor thread and the initial hooking routine never interleave context switches. - Clean teardown โ
UninstallHookstops the monitor, clearsDR0โDR7on every thread, and deregisters the VEH.
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.
| 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. |
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 |
|---|---|
/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 |
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.exeOr, 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.
| 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). |
- Watchdog/self-monitoring agents โ poll
GetThreadContext(CONTEXT_DEBUG_REGISTERS)on high-value processes and audit any thread with nonzeroDR0โDR3outside approved debugger profiles. - Kernel ETW auditing โ enable
Microsoft-Windows-Kernel-Process+ Thread tracing and alert onNtGetContextThread/NtSetContextThreadtargeting security-relevant processes. - EDR user-mode hook integrity โ since hardware hooks bypass memory checks, rely on behavioral detection (ETW consumer hooks below
EtwEventWrite, kernel ETW, AMSI consumer re-check) rather than.textintegrity alone. - Protect the monitor โ in genuinely hostile environments, treat per-thread
SetThreadContextto other processes as an explicit high-severity signal. - Endpoint hardening โ enable WDAC (which this POC explicitly bypasses for class approval โ do not treat WDAC as a standalone defense against in-memory tooling), Credential Guard, and LSASS protection where applicable.
- x64-only โ the stack-offset rewrites assume the x64 calling convention (arguments
RCX/RDX/R8/R9then[RSP+0x20โฆ]). An x86 variant would need[EBP+โฆ]-style parameter reconstruction. - Four slots only โ the x64 architecture offers exactly four breakpoint registers; you cannot hook more than four functions per thread with this method alone.
- Monitor race window โ there is an (intentionally small) window between
Sleep(500)iterations; extremely fast thread spawning combined with aggressive stripping could theoretically out-run the monitor for a few hundred milliseconds. - Anti-debug interference โ any component that actively monitors or clears debug registers (a real debugger, some sandboxes, certain EDRs) will interfere with the technique.
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.- Not a memory-persistence primitive โ this is a runtime-only, in-process technique. It provides no disk/registry persistence, no privilege escalation, and no cross-process lateral movement by itself. Its entire purpose is the controlled study of one interception primitive.
- Microsoft Learn โ Antimalware Scan Interface (AMSI)
- Microsoft Learn โ Windows Lockdown Policy (WLDP)
- Microsoft Learn โ Event Tracing for Windows (ETW)
- Microsoft Learn โ CONTEXT structure & Debug Registers
- Intelยฎ 64 and IA-32 Architectures Software Developer's Manual, Vol. 3B โ Debug Registers (Dr0โDr7, #DB exception)
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.
