Chapter 47

ETW-TI: Why Userland Bypasses Stop Here

Chapter 46 showed how to silence ETW by patching three bytes in ntdll. That works because those ETW writes originate in your process's memory, which you can modify. ETW-TI (Threat Intelligence) is categorically different: it's emitted by the Windows kernel itself — ntoskrnl.exe — in response to security-sensitive operations. No amount of userland patching touches it. This chapter explains what ETW-TI covers, why the architectural boundary matters, what the partial bypasses look like, and what implications this has for real implant design.

What ETW-TI Actually Is

ETW-TI vs userland ETW — the kernel boundary
  Userland ETW (Ch46 — patchable):
  ─────────────────────────────────────────────────────────────────────────
  Your process
    │
    ├─ calls VirtualAllocEx()
    │    └─ ntdll!EtwEventWrite()   ← IN YOUR PROCESS, patchable
    │         └─ NtTraceEvent() → kernel (writes to ETW buffer)
    │
    └─ Your patch: replace EtwEventWrite with ret → event never sent
  
  ETW-TI (NOT patchable from userland):
  ─────────────────────────────────────────────────────────────────────────
  Your process calls NtAllocateVirtualMemory()
    │
    └─ syscall → kernel (ring 0)
         │
         ├─ NtAllocateVirtualMemory does its work
         │
         └─ Before returning: kernel checks if operation is monitored
              │
              └─ EtwTiLogAllocateVirtualMemory() — IN KERNEL MEMORY
                   │
                   └─ ETW write from kernel → ETW-TI log
                              │
                              └─ consumed by:
                                   EDR kernel callbacks
                                   Microsoft Defender for Endpoint (MDE)
                                   Elastic Defend kernel driver
  
  Why you cannot patch ETW-TI from userland:
  ─────────────────────────────────────────────────────────────────────────
  EtwTiLog* functions are in ntoskrnl.exe.
  ntoskrnl.exe is in kernel memory (ring 0).
  User-mode code (ring 3) cannot write to ring 0 memory.
  VirtualProtect/WriteProcessMemory only affect your process's address space.
  
  The only way to patch ETW-TI: you need ring 0 code execution.
  That means: a kernel driver (Chapter 15+), a kernel exploit, or BYOVD.
  (BYOVD = Bring Your Own Vulnerable Driver — covered in Part 15)

What ETW-TI Monitors

ETW-TI event catalog — operations that generate kernel-level telemetry
  ETW-TI Event                              API that triggers it
  ─────────────────────────────────────────────────────────────────────────
  ALLOCATE_VIRTUAL_MEMORY                   NtAllocateVirtualMemory()
  PROTECT_VIRTUAL_MEMORY                    NtProtectVirtualMemory()
  MAP_VIEW_OF_SECTION                       NtMapViewOfSection()
  QUEUE_USER_APC                            NtQueueApcThread()
  CREATE_THREAD                             NtCreateThreadEx()
  SET_CONTEXT_THREAD                        NtSetContextThread()
  OPEN_PROCESS                              NtOpenProcess()
  OPEN_THREAD                               NtOpenThread()
  READ_VIRTUAL_MEMORY                       NtReadVirtualMemory()
  WRITE_VIRTUAL_MEMORY                      NtWriteVirtualMemory()
  CREATE_PROCESS                            NtCreateUserProcess()
  DEVICE_CONTROL                            NtDeviceIoControlFile()
  
  Additional ETW-TI event categories (Windows 10 1903+):
  ─────────────────────────────────────────────────────────────────────────
  LOAD_IMAGE                                Loading any PE into memory
  QUEUE_USER_APC_SPECIAL                    Special-user APC (Win10 19H1+)
  DUPLICATE_HANDLE                          NtDuplicateObject()
  
  Key insight: these events include the CALL STACK:
  ─────────────────────────────────────────────────────────────────────────
  Each ETW-TI event also records the userland call stack at the time of
  the syscall. This is how ETW-TI catches:
    - Direct syscall stubs not in module memory (Ch41)
    - Indirect syscalls landing at mid-function offsets (Ch43)
    - Injection attempts even when using direct/indirect syscalls
  
  MDE uses ETW-TI to detect:
    - WriteVirtualMemory events with destination in another process
    - CreateThread events with start address in non-module memory
    - AllocateVirtualMemory events with RWX protection (MEM_COMMIT + PAGE_EXECUTE_READWRITE)
    - Suspicious call stack patterns (return address not in any known module)

Partial Bypasses and Their Limits

/* ── ETW-TI "bypass" approaches — and why they're partial ─────────── */

/*
 * Approach 1: Run in an unmonitored session
 * 
 * ETW-TI consumers (like MDE) register callbacks for specific processes or
 * session scopes. Services running in Session 0 are typically monitored.
 * Interactive user sessions (Session 1+) are also monitored.
 * BUT: in some lab environments, ETW-TI consumers may not be running at all.
 * In production environments with MDE, they always are.
 * 
 * This isn't a real bypass — it's hoping the consumer isn't there.
 */

/*
 * Approach 2: Use shared section injection (NtCreateSection) to avoid
 * WriteVirtualMemory events.
 * 
 * Chapter 26 showed mapping injection: write payload via shared memory section,
 * no NtWriteVirtualMemory call. But ETW-TI still fires for:
 *   - NtMapViewOfSection (remote mapping = inject attempt signal)
 *   - NtCreateThreadEx or NtQueueApcThread (to execute the payload)
 * 
 * Reducing the ETW-TI surface, not eliminating it.
 */

/*
 * Approach 3: Use existing threads (thread hijacking, APC in alertable thread)
 * to avoid CreateThread events.
 * 
 * Thread hijacking (Ch32): modifies existing thread via SetContextThread.
 * SetContextThread IS in the ETW-TI catalog — you still get an event.
 * APC injection (Ch30): QueueUserAPC. Also in ETW-TI catalog.
 * 
 * Tradeoff: reduces the number of ETW-TI events vs a full injection chain.
 * Still detected by a tuned ETW-TI consumer.
 */

/*
 * Approach 4: Kernel bypass via BYOVD (Bring Your Own Vulnerable Driver)
 * 
 * A legitimate, signed-by-WHQL driver with a memory disclosure or write primitive
 * can be used to patch kernel code from ring 3 (using the driver's IOCTL interface).
 * This is the only real ETW-TI bypass from userland — not patching ETW-TI itself,
 * but using kernel write access through a legitimately-signed conduit.
 * 
 * Examples from threat intel:
 *   - RobbinHood ransomware: abused GIGABYTE driver (GDRV.sys)
 *   - BlackMatter: abused MSI Afterburner driver (RTCore64.sys)
 *   - AvosLocker: abused AV driver
 * 
 * BYOVD is Part 15 content (kernel topics).
 */

void etw_ti_bypass_status_report(void) {
    printf("ETW-TI bypass from pure userland: NOT POSSIBLE\n\n");
    printf("What is possible from userland:\n");
    printf("  1. Reduce ETW-TI events by choosing lower-noise injection techniques\n");
    printf("  2. Blind userland ETW (Ch46) to hide from SIEM log-based detections\n");
    printf("  3. BYOVD for kernel write access (Part 15)\n\n");
    printf("What MDE sees even with all userland bypasses applied:\n");
    printf("  - Every NtAllocateVirtualMemory with RWX or cross-process scope\n");
    printf("  - Every NtWriteVirtualMemory to another process\n");
    printf("  - Every NtCreateThreadEx in a remote process\n");
    printf("  - The call stack at each of those events\n");
    printf("Mitigation: combine low-noise techniques + BYOVD for full bypass\n");
}

The Full Detection Layer Stack

What each bypass chapter silences — and what remains
  Detection layer         Bypass            What still sees you
  ───────────────────────────────────────────────────────────────────────────
  ntdll hooks (EDR)       Ch38-40 unhook    EDR loses argument visibility
                                            but ETW-TI still fires for syscalls
  
  Userland ETW            Ch46 patch        SIEM log-based detections go silent
                                            (PS script logs, DotNET runtime,
                                            kernel-file/process/registry providers)
  
  AMSI                    Ch45 patch        Script content no longer scanned
                                            before execution
  
  WDAC/WLDP              Ch45 patch        PS Constrained Language Mode disabled
  
  Call stack detection    Ch43 indirect     Kernel sees ntdll as syscall origin
                          syscalls          (return address in ntdll, not heap)
  
  ETW-TI                  NOT BYPASSED      Kernel emits events for ALL:
                          from userland     - cross-process memory writes
                                            - remote thread creation
                                            - APC queuing
                                            - handle duplication
                                            MDE, Elastic, SentinelOne all consume
                                            these via kernel callbacks
  
  File system AV scan     Obfuscation /     Dropped files still scanned on write;
                          in-memory only    encrypt payloads, decrypt in memory
  
  Behavior detection      Choose low-       Pattern: RWX alloc + write + exec
  (heuristics)            noise chains      in remote process = high confidence
                                            No single bypass defeats heuristics
  
  Complete bypass state (all userland layers applied):
  ───────────────────────────────────────────────────────────────────────────
  ✓ EDR API hooks bypassed (syscalls / unhooking)
  ✓ AMSI silenced
  ✓ Userland ETW silenced
  ✓ Call stack shows ntdll origin (indirect syscalls)
  ✗ ETW-TI active (requires kernel access to suppress)
  ✗ File AV active (keep payloads encrypted / fileless)
  ✗ Behavioral heuristics may trigger on injection sequence

Questions & Answers

Which Windows version introduced ETW-TI and when did it become widely used by EDRs?

ETW-TI was introduced in Windows 10 version 1703 (Creators Update, April 2017) with the Microsoft-Windows-Threat-Intelligence provider GUID. The initial API surface was limited: NtAllocateVirtualMemory and NtWriteVirtualMemory cross-process events. Over subsequent releases, coverage expanded significantly: Windows 10 1809 added call stack capture per event, and 1903 added image load events and APC events. Microsoft Defender for Endpoint (formerly MDATP) began consuming ETW-TI via their kernel sensor around 2018, and CrowdStrike Falcon adopted ETW-TI supplementally (alongside their own kernel callbacks) around the same period. By 2020, any EDR not consuming ETW-TI was considered behind the curve. Today (2026), every major enterprise EDR relies on ETW-TI as a primary detection channel.

Can you disable ETW-TI by disabling the ETW service or clearing the ETW consumer list?

No. ETW-TI is not mediated by the Event Log service or any userland service. The events flow from the kernel (ntoskrnl.exe) directly to registered kernel-mode ETW consumers via a kernel callback chain. These consumers are EDR kernel drivers that loaded before your process started and registered with the kernel's ETW subsystem via EtwRegister() in kernel mode. Stopping the Windows Event Log service (EventLog) kills the consumer that writes events to .evtx files on disk — but it doesn't affect kernel-to-driver ETW channels at all. Your process doesn't appear in the consumer list because you don't have permission to enumerate or modify kernel callback registrations from ring 3.

Does ETW-TI fire for every VirtualAlloc or only cross-process ones?

Both — but with different severity weights. Self-allocation (allocating memory in your own process) does generate ETW-TI events, but these are lower priority and typically filtered by EDR detection logic. The high-fidelity signals are: cross-process NtWriteVirtualMemory (writing to another process's memory), cross-process NtCreateThreadEx (starting a thread in another process), NtMapViewOfSection with a remote target handle, and NtQueueApcThread targeting an external thread. These cross-process operations are the classic injection chain, and their ETW-TI events include both source and target process information — making the injection chain trivially detectable even without any userland hooks. Self-process RWX allocations are also flagged if they're followed by execution, but the bar is higher.

If BYOVD is the only way to bypass ETW-TI from userland, what's the operational cost of using it?

Significant. BYOVD requires: (1) shipping a known-vulnerable, WHQL-signed driver as part of your payload — this driver itself is a static detection target (its hash is in most EDR block lists after public disclosure); (2) loading the driver, which generates an image load event and requires privileges to call NtLoadDriver (typically requires SeLoadDriverPrivilege, meaning you need to be an admin or SYSTEM before the BYOVD stage); (3) using the driver's vulnerability (memory write, physical memory access, etc.) to reach kernel structures — this requires specific kernel version knowledge and offsets. In practice, BYOVD is used by ransomware groups that have already achieved high privilege and are in a "kill the EDR" phase before deploying the encryptor. It's not a technique for initial access — it's a post-exploitation nuclear option.

Can you use process injection techniques that don't trigger ETW-TI at all?

The ETW-TI catalog covers all the injection-relevant syscalls: allocate, write, map, create thread, set context, queue APC. There's no injection technique that achieves code execution in another process without using at least one of these. The closest thing to a low-ETW-TI approach is Pool Party (Ch36) or PROPagate (Ch37) — they still require cross-process memory manipulation (mapped sections, shared properties), which generates ETW-TI events. The mitigation isn't to avoid ETW-TI events entirely but to avoid the high-confidence detection signatures: specifically, the pattern of "write foreign process → create thread in foreign process → thread starts in non-module memory" all within seconds is the canonical injection detection. Spreading the events across time, using existing threads (APC, thread hijack), and writing only to pre-existing module pages (module stomping, Ch28) reduces the correlation quality — but doesn't eliminate the telemetry.