<index> / <windows-internals> / etw-patching
[ en | fr ]
┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
ETW Patching
~ quentiin19
Written by quentiin19 — https://github.com/quentiin19

CONTENTS
  0  what is etw
  1  etw architecture
  2  how edrs use etw — the ti provider
  3  userland patch — etwEventWrite
  4  per-process trace disabling
  5  kernel-level patch — etwti provider
  6  detection

──[ 0. What is ETW ]──

ETW (Event Tracing for Windows) is a high-performance, kernel-level tracing 
framework that's been in Windows since XP. Originally for diagnostics and perf
monitoring, but security products lean on it heavily now for behavioural
detection. Faster and lower-overhead than polling or file-based logging. Events get written
straight into shared memory buffers and only flushed to consumers when the
buffer fills or a timeout hits — no disk I/O on the hot path.
──[ 1. ETW Architecture ]──
flowchart LR
    P["ETW Provider\n(OS, drivers, apps)"]
    K["ETW kernel\n(ntoskrnl / ntdll)"]
    S["Tracing Session\n(logman, WPR…)"]
    C["ETW Consumer\n(EDR, WEF, Sysmon…)"]

    P -->|"EtwEventWrite / EtwWrite"| K
    K -->|"ring buffer"| S
    S -->|"ETL / real-time"| C
Three actors:
    Provider    — anything that emits events. Each provider has a GUID.
                  Examples: Microsoft-Windows-Kernel-Process,
                  Microsoft-Windows-Threat-Intelligence.

    Session     — kernel object that takes events from one or more
                  providers and routes them to consumers. Created with
                  StartTrace() / EnableTraceEx2().

    Consumer    — reads events in real time or from a .etl file. EDRs
                  typically open a real-time session with OpenTrace() +
                  ProcessTrace().

──[ 2. How EDRs Use ETW — The TI Provider ]──

The most security-relevant ETW provider:
    Microsoft-Windows-Threat-Intelligence
    GUID: {F4E1897C-BB5D-5668-F1D8-040F4D8DD344}

Also called the EtwTi provider. Runs entirely in the kernel and emits events 
for:
    Executable memory allocation    VirtualAlloc(PAGE_EXECUTE_*)
    Remote memory writes            WriteProcessMemory
    Remote thread creation          CreateRemoteThread
    Handle duplication              DuplicateHandle on processes
    LSASS access                    OpenProcess on lsass.exe
    Kernel object operations        Driver loads, registry writes

EtwTi is kernel-mode, so you can't silence it from userland by patching 
ntdll.dll. You need Ring 0 to mess with it. Defender ATP (MDE), CrowdStrike Falcon, and most enterprise EDRs consume EtwTi
as a primary detection signal.
──[ 3. Userland Patch — EtwEventWrite ]──

Every user-mode ETW event eventually calls EtwEventWrite (or its alias 
NtTraceEvent) in ntdll.dll. Patch the function to return immediately, and the
current process stops emitting any user-mode ETW events.
    // Load ntdll, locate EtwEventWrite
    HMODULE ntdll   = GetModuleHandleA("ntdll.dll");
    FARPROC target  = GetProcAddress(ntdll, "EtwEventWrite");

    // Patch: overwrite first byte with 0xC3 (RET)
    DWORD old;
    VirtualProtect(target, 1, PAGE_EXECUTE_READWRITE, &old);
    *(BYTE*)target = 0xC3;
    VirtualProtect(target, 1, old, &old);
Effect: every EtwEventWrite call in this process silently returns. Providers 
that call EtwEventWrite directly (PowerShell, .NET runtime, WMI host, etc.) go
quiet. Limit: only affects the current process, only user-mode providers. Kernel-mode
providers (EtwTi) keep going untouched. Alternative: patch EtwEventWriteFull or NtTraceEvent to cover more of ntdll's
internal call paths.
──[ 4. Per-Process Trace Disabling ]──

More targeted: poke the ETW registration struct inside the current process to 
disable tracing for a specific provider GUID without patching executable code. Each provider registered in a process is tracked by a REGHANDLE (returned by
EventRegister). The handle points to an internal _ETW_REG_ENTRY struct. Set the
tracing flag to disabled and events stop being written. Via the public API (needs the session handle — usually inaccessible):
    // Conceptually: disable the Threat-Intelligence provider
    // in the current session
    EnableTraceEx2(sessionHandle, &TIproviderGUID,
                   EVENT_CONTROL_CODE_DISABLE_PROVIDER, 0, 0, 0, 0, NULL);
In practice the session handle is rarely in your hands. The usual workaround: 
find the provider's REGHANDLE inside ntdll!EtwpRegistrationTable and clear the
IsEnabled flag directly.
──[ 5. Kernel-Level Patch — EtwTi Provider ]──

EtwTi events come from callback functions registered inside ntoskrnl. Patching 
them needs Ring 0 write access — in practice, via BYOVD. The technique (used in tools like FireWalker and similar PoCs): Step 1 — Locate EtwTi registration
    The EtwTi provider is registered in the kernel via EtwRegister(). The
    resulting _ETW_REG_ENTRY gets linked into a global provider list in
    ntoskrnl. Pattern-scan for the provider GUID to find the entry.

Step 2 — Find the callback
    _ETW_REG_ENTRY holds a pointer to the EnableCallback function — the
    one invoked when a consumer enables the provider.
    struct _ETW_REG_ENTRY {
        ...
        PLIST_ENTRY  Links;
        LPGUID       ProviderId;
        ...
        PETWENABLECALLBACK EnableCallback;
        ...
    };
Step 3 — Patch the callback
    Overwrite the EnableCallback pointer with a stub that returns straight
    away, or patch the first bytes of the callback function itself with
    a RET:
    // Via BYOVD kernel write primitive:
    KernelWrite(enableCallbackAddr, "\xC3", 1);  // RET
Now consumers can still "enable" EtwTi but the callback never fires.
    No events come out. The provider is blind for the whole boot session.
sequenceDiagram
    participant A as Attacker (Ring 3)
    participant V as Vulnerable Driver (Ring 0)
    participant K as ntoskrnl

    A->>V: IOCTL — scan for EtwTi GUID
    V->>K: read kernel memory
    K-->>V: _ETW_REG_ENTRY address
    V-->>A: address returned

    A->>V: IOCTL — write 0xC3 at EnableCallback
    V->>K: patch callback
    Note over K: EtwTi now silent
Step 4 — Silence individual event callbacks
    Some implementations go further and patch individual event-write
    functions (EtwWriteEx, EtwpWriteUserEvent) to cover edge cases where
    the global callback approach misses certain event types.

──[ 6. Detection ]──

ETW patching is hard to spot from inside a patched process (that's the point), 
but defenders have options: Integrity scan from a separate process
    A trusted process can read the first bytes of ntdll!EtwEventWrite in
    the target's memory via ReadProcessMemory and compare with the
    on-disk copy. A leading 0xC3 is a strong signal.
    // Similar to inline-hook detection (see DLL Hooking article)
    ReadProcessMemory(hTarget, etwEventWriteAddr, buf, 5, NULL);
    if (buf[0] == 0xC3) { /* patched */ }
ETW provider health checks
    Microsoft-Windows-Kernel-Audit-API-Calls (among others) can be
    monitored by an out-of-process consumer. If the provider goes silent
    while the process is still alive, something's been patched.

Kernel integrity (EtwTi callback)
    HVCI (Hypervisor-Protected Code Integrity) blocks writes to kernel
    code pages, killing Step 3 on modern systems with HVCI on. Also
    blocks unsigned kernel modules — kills most BYOVD.

PatchGuard (KPP)
    KernelPatchGuard periodically checks kernel code and data structures.
    Patching the EtwTi callback pointer can trigger a BSOD
    (CRITICAL_STRUCTURE_CORRUPTION, bugcheck 0x109) if PatchGuard scans
    the region before you're done.

Patch scope vs. complexity:
    EtwEventWrite patch     Ring 3   easy      user-mode providers only
    Per-process disable     Ring 3   medium    per-provider, no code patch
    EtwTi callback patch    Ring 0   hard      full kernel telemetry blind