Bypassing ETW Patchlessly with Hardware Breakpoints

When executing unmanaged payloads or offensive tooling in userland, Event Tracing for Windows (ETW) remains one of the primary telemetry sources feeding endpoint detection controls and Windows event logs. Traditional methods to silence ETW involve byte patching EtwEventWrite in ntdll.dll—typically replacing the function prologue with a simple xor eax, eax; ret sequence.
However, byte patching leaves obvious signatures. Modern security agents frequently scan system DLL memory regions in memory for unexpected modifications. Furthermore, calling VirtualProtect to flip memory permissions from PAGE_EXECUTE_READ to PAGE_EXECUTE_READWRITE creates a high-entropy API call sequence that detection engineers explicitly hunt for.
To suppress ETW telemetry cleanly without touching memory permissions or altering executable bytes, we can leverage x64 hardware debug registers (DR0-DR7) alongside Vectored Exception Handling (VEH).
Leveraging Hardware Debug Registers
The x86/x64 architecture provides dedicated hardware debug registers designed for debugging without modifying code bytes in memory. Registers DR0 through DR3 hold the linear addresses of breakpoints, while DR7 controls breakpoint execution flags (such as whether to trigger on execution, read, or write).
When a thread reaches an address stored in an active debug register, the CPU raises a hardware breakpoint exception (STATUS_SINGLE_STEP / 0x80000004) before executing the instruction.
Because these registers are part of the thread’s CPU context, setting a hardware breakpoint requires no modification to the underlying page tables or memory contents of ntdll.dll. To memory scanners, ntdll.dll remains entirely pristine.
Catching the Exception with VEH
To turn a hardware breakpoint into an execution bypass, we register a Vectored Exception Handler (VEH) using AddVectoredExceptionHandler. Unlike structured exception handling (SEH), VEH handles exceptions globally across the process before frame-based handlers receive them.
Here is the operational workflow:
- Register the VEH: Set up a top-level exception handler to intercept
STATUS_SINGLE_STEP. - Locate Target: Obtain the module base of
ntdll.dlland fetch the export address ofEtwEventWrite. - Set Debug Registers: Retrieve the target thread’s context using
GetThreadContext, populateDR0with the address ofEtwEventWrite, configureDR7to enable an execution breakpoint onDR0, and apply the context viaSetThreadContext.
When any telemetry source inside your process attempts to call EtwEventWrite, the CPU triggers the hardware breakpoint. Control immediately transfers to your registered VEH function.
Inside the exception handler, we manipulate the thread context pointers:
LONG WINAPI EtwBypassHandler(PEXCEPTION_POINTERS ExceptionInfo) {
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
if (ExceptionInfo->ExceptionRecord->ExceptionAddress == g_EtwEventWriteAddr) {
// Set return value (RAX) to STATUS_SUCCESS (0)
ExceptionInfo->ContextRecord->Rax = 0;
// Read the return address from the top of the stack
DWORD64 returnAddress = *(DWORD64*)(ExceptionInfo->ContextRecord->Rsp);
// Adjust instruction pointer to return to caller directly
ExceptionInfo->ContextRecord->Rip = returnAddress;
// Adjust stack pointer (pop return address)
ExceptionInfo->ContextRecord->Rsp += 8;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
By forcing RIP to the caller’s return address and simulating a ret instruction while returning zero in RAX, EtwEventWrite exits prematurely without emitting any event payload.
Tradeoffs and Operational Realities
While this technique bypasses basic memory integrity checks and avoids suspicious memory protection API calls, it introduces specific operational trade-offs:
- Thread Context Scope: Debug registers are stored per-thread. Applying hardware breakpoints to a single thread will not suppress telemetry generated on newly created worker threads. You must apply the debug context to newly spawned threads or hook thread initialization routines.
- Context API Monitoring: Calling thread context manipulation APIs on distant threads can be flagged if telemetry agents monitor
NtSetContextThread. Calling it onGetCurrentThread()reduces this footprint. - Register Contention: If an agent or analyst attaches a debugger that relies on hardware registers, clearing or overwriting
DR0-DR7can lead to process instability or immediate detection.
By shifting the bypass logic from physical byte patching to hardware-assisted execution redirection, you effectively neuter userland ETW telemetry while keeping ntdll.dll clean on disk and in memory.
Photo by Markus Spiske on Unsplash.