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 ]──
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.
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