<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Parag Mali - tag: etw</title><description>Posts tagged etw.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sun, 07 Jun 2026 04:13:09 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/etw/rss.xml" rel="self" type="application/rss+xml"/><item><title>eBPF vs ETW: Two Generations of Kernel Observability</title><link>https://paragmali.com/blog/ebpf-vs-etw-two-generations-of-kernel-observability/</link><guid isPermaLink="true">https://paragmali.com/blog/ebpf-vs-etw-two-generations-of-kernel-observability/</guid><description>Why Windows ETW emits events and Linux eBPF computes them -- and what eBPF-for-Windows reveals about the convergence of two operating systems.</description><pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate><content:encoded>
**ETW (Windows 2000) is event emission only.** Per-CPU lock-free ring buffers, manifest-defined providers, kernel-mediated dispatch. Sessions filter by provider, keyword, and level; every enabled event is fully serialized and crosses the kernel/user boundary.&lt;p&gt;&lt;strong&gt;eBPF (Linux 2014) inverts the model.&lt;/strong&gt; The consumer ships verified bytecode into the kernel; programs filter and aggregate at the hook site before any data crosses the boundary. JIT-compiled, with hooks across kprobe, uprobe, tracepoint, XDP, TC, and LSM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The verifier is the trust boundary -- and the catch.&lt;/strong&gt; Rice&apos;s theorem says no in-kernel verifier can be simultaneously sound, complete, and decidable. Linux&apos;s verifier trades soundness in the corner cases (CVE-2023-2163 and three predecessors); PREVAIL (the verifier used by eBPF-for-Windows) trades completeness more heavily for stronger formal grounding.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;eBPF-for-Windows is the first cross-OS-portable kernel-observability primitive.&lt;/strong&gt; PREVAIL verifies in user mode, &lt;code&gt;bpf2c&lt;/code&gt; transliterates verified bytecode to C, MSVC compiles to a signed &lt;code&gt;.sys&lt;/code&gt; driver. Networking-subset hooks only as of 2026; full kprobe-equivalent coverage is the work in progress.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;1. The SOC Analyst Sees the Same Thing Twice&lt;/h2&gt;
&lt;p&gt;A Security Operations Center analyst opens two &lt;code&gt;Sysmon/Operational&lt;/code&gt; event channels side by side. One channel is streaming from a Red Hat Enterprise Linux host; the other is streaming from a Windows Server 2022 domain controller. The XML configuration is the same. The Event IDs are the same. A &lt;code&gt;ProcessCreate&lt;/code&gt; record from either host carries the same &lt;code&gt;Image&lt;/code&gt;, &lt;code&gt;CommandLine&lt;/code&gt;, &lt;code&gt;ParentImage&lt;/code&gt;, &lt;code&gt;IntegrityLevel&lt;/code&gt;, and &lt;code&gt;Hashes&lt;/code&gt; fields. Detection rules written against one channel match the other. To the analyst, the two operating systems are interchangeable.&lt;/p&gt;
&lt;p&gt;Underneath, they are not even close.&lt;/p&gt;
&lt;p&gt;On the Windows side, every event was emitted by a kernel provider -- &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; -- before the Sysmon user-mode service ever ran its XML filter. The kernel produced a fully formatted event, dropped it into a per-CPU ring buffer, and let user space pick it up. Every enabled event made the kernel-to-user trip in full. The filter inside Sysmon&apos;s user-mode service is what kept the on-disk log small. The wire between the kernel and the consumer carried the full firehose.&lt;/p&gt;
&lt;p&gt;On the Linux side, no kernel module owned by Microsoft is running. The same Sysmon binary is attached to roughly twenty Linux kernel probes through the &lt;code&gt;SysinternalsEBPF&lt;/code&gt; library [@github-com-microsoft-sysmonforlinux]. Each probe is an eBPF program: bytecode that was compiled by clang, verified by the kernel before load, JIT-compiled to native instructions, and attached to a hook inside the kernel [@ebpf-io-is-ebpf]. When &lt;code&gt;execve&lt;/code&gt; fires, the verified program runs on the producing CPU, reads its arguments out of the kernel context, decides whether the call matches the XML configuration&apos;s predicates, and -- only then -- writes a record into a ring buffer. The events that arrive in user space were already filtered inside the kernel. The wire carries only what the configuration cares about.&lt;/p&gt;
&lt;p&gt;The output channels match because Sysmon for Linux is engineered to look exactly like Sysmon for Windows [@github-com-microsoft-sysmonforlinux]. The substrate underneath is engineered for two different decades. ETW is from 2000. eBPF is from 2014. The fourteen-year gap shows up not in features but in &lt;em&gt;how the kernel does its job&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; ETW emits. eBPF computes. That gap is the entire generation difference. Everything else in this article is a consequence of it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This article is about why those two designs exist, why the second one is strictly more powerful, why &quot;strictly more powerful&quot; cost the Linux kernel a new class of CVE, and what Microsoft&apos;s &lt;code&gt;microsoft/ebpf-for-windows&lt;/code&gt; [@github-com-for-windows] project -- now in its sixth year of development -- reveals about which design wins at the point of convergence. By the end you will know both substrates well enough to choose between them, understand their failure modes, and see why &quot;two generations&quot; is not marketing language but a literal description of the engineering arc.&lt;/p&gt;
&lt;h2&gt;2. A Tale of Two Lineages&lt;/h2&gt;
&lt;p&gt;In 1992, Van Jacobson and Steven McCanne at Lawrence Berkeley Laboratory wrote a small virtual machine for packet filtering [@tcpdump-org-bpf-usenix93pdf]. In 2000, a separate Microsoft team shipped a kernel event bus inside Windows 2000. Neither group knew the other existed. Each was solving a different version of the same problem: &lt;em&gt;how do you watch the kernel from user space without owning the kernel?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The two answers ran in parallel for twenty-two years before they collided.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1992 -- The BSD Packet Filter.&lt;/strong&gt; McCanne and Jacobson published &quot;The BSD Packet Filter: A New Architecture for User-level Packet Capture&quot; at USENIX Winter 1993, describing work that landed in 4.3BSD-Reno earlier in 1992. The motivation was painfully concrete: &lt;code&gt;tcpdump&lt;/code&gt; was copying every packet through the kernel-user boundary, then discarding the ones the user did not want. BPF moved that filter into the kernel. A tiny two-register, 32-bit virtual machine evaluated a user-supplied predicate against each packet before any copy; only matching packets crossed into user space. The architectural insight that would survive thirty years is one sentence: &lt;em&gt;filter where the data is produced, not where it is consumed.&lt;/em&gt;&lt;/p&gt;

A safe, sandboxed virtual machine inside the Linux kernel that runs user-supplied programs at attached hook points. Programs are written in restricted C, compiled to a 64-bit RISC-style bytecode, statically verified before load, and JIT-compiled to native code. The &quot;extended&quot; version, introduced in Linux 3.18 (December 2014) [@kernel-org-bpf-indexhtml], generalized BPF from a packet-filter language into a general kernel-extensibility mechanism.
&lt;p&gt;&lt;strong&gt;2000 -- Event Tracing for Windows.&lt;/strong&gt; Microsoft shipped ETW with Windows 2000. The reference portal [@learn-microsoft-com-tracing-portal] describes the design Microsoft had been refining since the late 1990s: a kernel-mediated event bus with three roles -- providers, sessions, and consumers -- and per-CPU lock-free ring buffers. ETW&apos;s architectural insight was the inverse of BPF&apos;s: &lt;em&gt;event identity and causal order are first-class. A kernel-mediated dispatch makes them cheap.&lt;/em&gt; A &lt;code&gt;tcpdump&lt;/code&gt; filter wants to throw events away. A security telemetry system wants to keep them, attribute them, and order them.&lt;/p&gt;

A kernel-mediated tracing facility shipped in Windows 2000. Providers (kernel or user-mode components) emit structured events to per-CPU ring buffers; sessions own the buffers and select which providers to enable at which level; consumers receive the event stream either in real time or by reading the on-disk `.etl` log. ETW is documented at `learn.microsoft.com/.../etw/event-tracing-portal` [@learn-microsoft-com-tracing-portal].
&lt;p&gt;&lt;strong&gt;2003-2005 -- DTrace.&lt;/strong&gt; Bryan Cantrill, Mike Shapiro, and Adam Leventhal at Sun Microsystems started work in 2003 on what would become the first production-grade dynamic tracing system. DTrace shipped publicly in Solaris 10 in January 2005 [@en-wikipedia-org-wiki-dtrace] and quickly ported to FreeBSD and macOS. Its central idea -- safe in-kernel scripts attached to probes, with a single language for tracing the entire system -- is the spiritual ancestor of every modern kernel observability tool, including eBPF.Wikipedia gives DTrace&apos;s initial public release as January 2005, with Sun&apos;s internal development starting around 2003. The &quot;DTrace 2003&quot; claim that appears in some retrospectives conflates project inception with public release; we use the 2005 ship date here and note 2003 only as a development start. Linux could not adopt it directly: DTrace is licensed under the CDDL, which is GPLv2-incompatible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2005 -- SystemTap.&lt;/strong&gt; Red Hat attempted to fill the Linux DTrace gap with SystemTap [@sourceware-org-systemtap]. The architectural compromise that doomed it: SystemTap scripts compile to a &lt;em&gt;kernel module&lt;/em&gt;, loaded at runtime. Allowing user-supplied kernel modules to be loaded on demand is a privileged operation by definition, so production SystemTap deployments restricted use to local root. That made the observability case study moot: if you already have root, you can use any debugging tool. SystemTap survives as a niche tracing system; it did not become the Linux answer to DTrace.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1992-2014 -- classic BPF stagnates.&lt;/strong&gt; The original BPF VM kept finding new jobs. Linux Socket Filtering [@kernel-org-networking-filtertxt] ported the BSD filter into the Linux kernel in 1997. seccomp-bpf in 2012 gave it a second job: filtering system calls for sandboxing. But the language remained a 32-bit two-register packet-filter VM. It could not be extended to general kernel observability without rewriting the instruction set architecture from the ground up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2014 -- eBPF.&lt;/strong&gt; Alexei Starovoitov&apos;s &quot;extended BPF&quot; patch series landed in Linux 3.18 in December 2014 [@kernel-org-bpf-indexhtml], described in LWN&apos;s contemporaneous article on Starovoitov&apos;s eBPF patch set [@lwn-net-articles-603983]. The rewrite was thorough: 64-bit instruction set, eleven registers, maps for in-kernel state, helper calls into kernel APIs, a JIT compiler, and -- the part that mattered most -- a kernel verifier that statically proves safety before any program runs. The verifier is what turned the packet filter into a general kernel extension mechanism. Without it, every BPF program would have to be trusted; with it, untrusted user code can execute in kernel mode.&lt;/p&gt;
&lt;p&gt;By the time eBPF shipped, Windows had ETW everywhere. Linux had &lt;code&gt;auditd&lt;/code&gt;&apos;s pull-based audit log and a handful of &lt;code&gt;perf&lt;/code&gt; events. Then Starovoitov rewrote BPF, and the architectural balance shifted overnight. The next decade of Linux observability was built on the new instruction set. The next decade of Windows observability stayed on ETW. The two designs ran in parallel until 2021, when Microsoft announced that eBPF would also run on Windows.&lt;/p&gt;

flowchart LR
    A[BPF -- 1992 -- LBL]
    B[ETW -- 2000 -- Windows 2000]
    C[DTrace -- 2005 -- Solaris 10]
    D[SystemTap -- 2005 -- Red Hat]
    E[seccomp-bpf -- 2012 -- Linux 3.5]
    F[eBPF -- 2014 -- Linux 3.18]
    G[BPF Trampoline -- 2019 -- Linux 5.5]
    H[BPF Ringbuf -- 2020 -- Linux 5.8]
    I[eBPF for Windows -- 2021 -- Microsoft]
    J[RFC 9669 BPF ISA -- 2024 -- IETF]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D --&amp;gt; E --&amp;gt; F --&amp;gt; G --&amp;gt; H --&amp;gt; I --&amp;gt; J
&lt;p&gt;The diagram lays the substrate stories side by side. Each arrow is an architectural decision that constrained what came after. The next two sections walk each design end to end -- ETW first, because it is older and emission-only and easier to internalize.&lt;/p&gt;
&lt;h2&gt;3. ETW: Pure Event Emission&lt;/h2&gt;
&lt;p&gt;A natural question that turns out to be the wrong one: &lt;em&gt;why didn&apos;t Microsoft just keep extending performance counters?&lt;/em&gt; By the late 1990s, Windows already had a mature counter facility -- &lt;code&gt;perfmon&lt;/code&gt;, the Windows Performance Counters portal [@learn-microsoft-com-counters-portal]. It exposed CPU percentage, page-fault rate, queue lengths, and hundreds of other scalar metrics. If you wanted to know how loaded your system was, perfmon told you.&lt;/p&gt;
&lt;p&gt;It also told you almost nothing useful for security telemetry.&lt;/p&gt;

Three structural failures of the counter model show up the moment you try to use it as the substrate for an EDR.&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sampling-rate floor.&lt;/strong&gt; A counter can only be observed at the rate the consumer queries. On a busy host -- sshd children, container init forks, a CI runner -- process-creation rates routinely exceed any sane query rate. The counter aggregates the events it cannot expose into a single integer that hides the structure of what happened.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No identity.&lt;/strong&gt; &quot;Three hundred process creations in the last second&quot; is a counter. &quot;User &lt;code&gt;bob&lt;/code&gt; ran &lt;code&gt;/tmp/.x&lt;/code&gt; with parent &lt;code&gt;/usr/sbin/cron&lt;/code&gt; at 14:33:07.221Z&quot; is an event. The security model requires identity; the counter model erases it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No causal order.&lt;/strong&gt; Two counters sampled in sequence are not causally ordered with respect to the system events they describe. ETW&apos;s per-CPU buffers with QPC timestamps preserve causal order across CPUs to within the timer&apos;s accuracy.&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;The fix was not a faster perfmon. The fix was an entirely different shape of telemetry. ETW was that shape: push-based, per-event, kernel-attributed, with stable schemas declared up front. The contrast between perfmon (a sampling counter) and ETW (an event bus) is not parametric. The two systems answer different questions. Security needs the event-bus answer.&lt;/p&gt;
&lt;h3&gt;Provider, session, consumer&lt;/h3&gt;
&lt;p&gt;ETW&apos;s data plane has three roles, every one of them a kernel-mediated object.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;provider&lt;/em&gt; is a kernel or user-mode component that calls &lt;code&gt;EventWrite&lt;/code&gt; or &lt;code&gt;EtwWrite&lt;/code&gt; to emit a structured event. Providers identify themselves by GUID. They declare the schema of their events ahead of time: classic providers via MOF, the Vista-and-later manifest format [@learn-microsoft-com-event-tracing] called &lt;code&gt;WEVT&lt;/code&gt;, or TraceLogging [@learn-microsoft-com-logging-portal] for self-describing events. The schema is part of the contract: a consumer that knows the provider&apos;s manifest knows the field layout of every event the provider will ever emit.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;session&lt;/em&gt; is a kernel object created by &lt;code&gt;StartTrace&lt;/code&gt;. It owns a set of per-CPU buffers and a list of enabled providers, with per-provider level and keyword masks. Sessions can write events to disk (&lt;code&gt;.etl&lt;/code&gt; files) or be consumed in real time.The &lt;code&gt;.etl&lt;/code&gt; file extension stands for &quot;Event Trace Log.&quot; It is the on-disk format read by Windows Performance Analyzer and by &lt;code&gt;tracerpt.exe&lt;/code&gt; for post-hoc analysis.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;consumer&lt;/em&gt; is a user-mode process that calls &lt;code&gt;OpenTrace&lt;/code&gt; and &lt;code&gt;ProcessTrace&lt;/code&gt; and receives event callbacks. EDR agents like Sysmon, Defender, and the third-party agents that ship with Microsoft Defender for Endpoint [@learn-microsoft-com-defender-endpoint] are real-time consumers.&lt;/p&gt;

ETW&apos;s three-role architecture. *Providers* emit events into per-CPU ring buffers. *Sessions* are kernel objects that own buffers and select which providers to enable. *Consumers* are user-mode processes that read the buffers in real time or open the on-disk `.etl` file. The taxonomy is defined in the ETW provider documentation [@learn-microsoft-com-event-tracing].
&lt;h3&gt;The per-CPU ring buffer&lt;/h3&gt;
&lt;p&gt;The algorithmic core of ETW is a per-CPU lock-free ring buffer. When a provider on CPU 3 calls &lt;code&gt;EventWrite&lt;/code&gt;, the kernel formats the event according to the provider&apos;s manifest, stamps it with a QPC timestamp, and &lt;code&gt;memcpy&lt;/code&gt;s the result into the per-CPU buffer for CPU 3. A kernel writer thread drains the buffer asynchronously into the session&apos;s destination -- either an &lt;code&gt;.etl&lt;/code&gt; file on disk or a consumer&apos;s callback queue. The producer-side cost is constant: a function call plus a buffered &lt;code&gt;memcpy&lt;/code&gt;, all on the local CPU, with no cross-CPU synchronization.&lt;/p&gt;

The Windows monotonic timestamp source used for ETW event timestamps. QPC is backed by hardware timers (TSC on modern x86, generic counter on ARM64) and provides a high-resolution counter that does not go backward.
&lt;p&gt;QPC guarantees monotonic timestamps per CPU.QPC is monotonic per CPU on modern hardware, but cross-CPU ordering still relies on the kernel writer thread&apos;s serialization when events from different CPUs are merged into a single output stream. Per-event timestamps from different CPUs can be ordered after the fact, but the merge happens in the writer, not in the producer.&lt;/p&gt;

flowchart LR
    P1[Provider on CPU 0]
    P2[Provider on CPU 1]
    P3[Provider on CPU 2]
    B0[Per-CPU buffer 0]
    B1[Per-CPU buffer 1]
    B2[Per-CPU buffer 2]
    W[Kernel writer thread]
    S[Session]
    F[.etl file]
    C[Real-time consumer]
    P1 -- EventWrite --&amp;gt; B0
    P2 -- EventWrite --&amp;gt; B1
    P3 -- EventWrite --&amp;gt; B2
    B0 --&amp;gt; W
    B1 --&amp;gt; W
    B2 --&amp;gt; W
    W --&amp;gt; S
    S --&amp;gt; F
    S --&amp;gt; C
&lt;h3&gt;The cost story&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s reference portal [@learn-microsoft-com-tracing-portal] describes ETW as &quot;high-volume, low-overhead.&quot; That qualitative claim has been the consensus practitioner finding for two decades. The most useful practical writeup is Bruce Dawson&apos;s &lt;em&gt;ETW Central&lt;/em&gt; index [@randomascii-wordpress-com-etw-central], which links to more than forty blog posts on real ETW deployments and measurements. The honest summary, anchored to Dawson&apos;s practical experience plus the architectural reason (per-CPU lock-free buffers and a &lt;code&gt;memcpy&lt;/code&gt; per event), is that typical telemetry configurations sit in the low single-digit-percent CPU range, and pathological &quot;log everything&quot; configurations can reach measurable user-visible slowdowns -- on the order of 5-10% in the worst cases. These are practitioner estimates, not benchmarked figures; the BenchmarkDotNet documentation [@benchmarkdotnet-org-configs-diagnosershtml] for the &lt;code&gt;EtwProfiler&lt;/code&gt; diagnoser explicitly acknowledges the cost: &lt;em&gt;&quot;In order to not affect main results we perform a separate run if any diagnoser is used.&quot;&lt;/em&gt; The overhead is small but it is not zero.&lt;/p&gt;
&lt;p&gt;The cost has a structural cause. ETW has no in-kernel filter. The producer pays the full event-formatting cost on every emission, and the only filter is the session&apos;s level and keyword mask. If you enable a provider, every event that provider emits flows through the buffer. Filtering happens at the consumer, in user mode, after the event has crossed the boundary.&lt;/p&gt;
&lt;h3&gt;The Threat-Intelligence provider&lt;/h3&gt;
&lt;p&gt;ETW providers are not equal. The most architecturally important one for security is &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt;, a kernel-only provider that emits signals only the kernel can see: image loads, remote-thread creations, &lt;code&gt;VirtualProtect&lt;/code&gt; changes that flip memory from data to executable. Only a process running under Protected Process Light with the AntiMalware signer [@learn-microsoft-com-downloads-sysmon] can subscribe. That is why Defender, CrowdStrike Falcon, SentinelOne, and Carbon Black [@github-com-providers-docs] all run as PPL-Antimalware: it is the entry ticket to the kernel-only telemetry that distinguishes serious EDR from script-level monitoring.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; ETW&apos;s biggest weakness is that providers run inside the very process they are observing. A process can patch its own copy of &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; with a &lt;code&gt;ret&lt;/code&gt; instruction and silence its own emissions before they reach the kernel buffer. EDR vendors monitor for this integrity violation out of band, treating the patch itself as a high-confidence detection signal. The very existence of the tell is an admission that ETW&apos;s original design assumed an honest user-mode producer -- a reasonable assumption in 2000, increasingly untenable in 2025.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sysmon 6.20 [@learn-microsoft-com-downloads-sysmon], released in 2018, was the version that tied ETW into the modern EDR stack as a turnkey configuration.The 2018 Sysmon 6.20 release added the configuration schema that the cybersecurity community converged on. By 2026, the same XML configuration -- including the &lt;code&gt;ProcessCreate&lt;/code&gt;, &lt;code&gt;NetworkConnect&lt;/code&gt;, &lt;code&gt;ImageLoad&lt;/code&gt;, and &lt;code&gt;FileCreate&lt;/code&gt; event IDs -- works on both Sysmon for Windows and Sysmon for Linux. Sysmon, Microsoft&apos;s own free reference consumer authored by Mark Russinovich and Thomas Garnier [@learn-microsoft-com-downloads-sysmon], demonstrated that an XML configuration plus an ETW consumer plus protected-process status was enough to build a useful EDR. Sysmon is not Defender; it is the open shape that the commercial EDR vendors built proprietary versions of.&lt;/p&gt;
&lt;h3&gt;Closing on ETW&lt;/h3&gt;
&lt;p&gt;ETW emits. Every enabled event crosses the kernel-user boundary, fully formatted, with no in-kernel filtering language whatsoever. The session&apos;s level and keyword mask is a coarse on/off switch, not a programmable filter. Aggregation, sampling, and stack-trace folding happen in user mode, after the event is already across the boundary.&lt;/p&gt;
&lt;p&gt;Now you can read the question that drove Starovoitov&apos;s 2014 rewrite: &lt;em&gt;what if you could filter in the kernel itself? What if you could compute -- not just emit?&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;4. eBPF: Programmable In-Kernel Computation&lt;/h2&gt;
&lt;p&gt;The architectural inversion is one sentence. ETW is the producer telling the consumer what happened. eBPF is the consumer telling the producer what to compute. The producer is the kernel; the consumer is a user-mode process that has compiled, verified, and attached a small program that will run inside the kernel at a chosen hook. The roles are inverted, the data flow is inverted, and the trust model is inverted.&lt;/p&gt;
&lt;h3&gt;The lifecycle&lt;/h3&gt;
&lt;p&gt;A canonical eBPF program goes through six stages before it does any useful work. The flow below is the same on every Linux kernel since 3.18, with refinements added over the years for BTF (BPF Type Format), CO-RE (Compile Once, Run Everywhere), and link primitives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. clang -target bpf -O2 -c prog.c -o prog.o            # ELF with BTF
2. fd = bpf(BPF_PROG_LOAD, &amp;amp;attr)                       # kernel verifier runs
3. for each map referenced:
       map_fd = bpf(BPF_MAP_CREATE, &amp;amp;attr)
4. link = bpf(BPF_LINK_CREATE, kprobe|tracepoint|xdp|lsm|cgroup, fd)
5. at hook fire: JIT-compiled native code runs on the
   producing CPU, reads context, calls bpf_* helpers,
   writes to map or ringbuf
6. user space mmaps the ringbuf and consumes records
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lifecycle is documented in the canonical kernel BPF documentation index [@kernel-org-bpf-indexhtml]. It is worth lingering on stage 2. Between the user-space &lt;code&gt;bpf()&lt;/code&gt; syscall and the moment the kernel hands back a file descriptor for the loaded program, a static analyzer runs. That analyzer is the most consequential piece of code in this entire article. We treat it on its own in section 5.&lt;/p&gt;

flowchart TD
    A[&quot;Restricted C source -- (prog.c)&quot;]
    B[&quot;clang -target bpf -- BPF ELF + BTF&quot;]
    C[bpf BPF_PROG_LOAD]
    D[Kernel verifier]
    E[JIT compiler]
    F[Kernel hook]
    G[bpf BPF_MAP_CREATE]
    H[&quot;BPF maps -- (arrays, hashes, ringbuf)&quot;]
    I[&quot;bpf BPF_LINK_CREATE -- (kprobe/xdp/lsm/...)&quot;]
    J[Hook fires]
    K[User space mmap ringbuf]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D
    D --&amp;gt;|reject| Z[E_INVAL to userspace]
    D --&amp;gt;|accept| E --&amp;gt; F
    C --&amp;gt; G --&amp;gt; H
    F --&amp;gt; I --&amp;gt; J
    J --&amp;gt; H
    H --&amp;gt; K
&lt;h3&gt;Hooks: where programs attach&lt;/h3&gt;
&lt;p&gt;The thing that distinguishes eBPF from a packet filter is its hook surface. A &lt;em&gt;hook&lt;/em&gt; is a place inside the kernel where a verified program can be attached, fired at the moment something happens. Linux has a lot of hooks.&lt;/p&gt;

An attachment point in kernel code where a verified eBPF program runs. Different hook types receive different context arguments: a kprobe receives the function&apos;s CPU registers; an XDP program receives a packet buffer; an LSM hook receives the security operation&apos;s parameters. The hook type also determines what helpers and map types the verifier allows.
&lt;p&gt;The hook taxonomy, drawn from the kernel BPF docs [@kernel-org-bpf-indexhtml] and Cilium&apos;s BPF architecture reference [@docs-cilium-io-bpf-architecture], is broad:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kprobe&lt;/code&gt; and &lt;code&gt;kretprobe&lt;/code&gt; -- entry and return of any non-inlined kernel function.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fentry&lt;/code&gt; and &lt;code&gt;fexit&lt;/code&gt; -- BPF trampoline replacement for kprobes, with no &lt;code&gt;int3&lt;/code&gt; trap-frame cost.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uprobe&lt;/code&gt; -- any user-space symbol in any process.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tracepoint&lt;/code&gt; -- stable kernel tracepoints with version-locked schemas.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;perf_event&lt;/code&gt; -- sampling-profile hooks tied to perf events.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XDP&lt;/code&gt; -- driver tail-call, before allocation of an &lt;code&gt;sk_buff&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TC&lt;/code&gt; -- Linux traffic-control qdisc hooks.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LSM&lt;/code&gt; -- Linux Security Module hooks (mandatory-access-control points), available since Linux 5.7.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cgroup&lt;/code&gt;, &lt;code&gt;sched&lt;/code&gt;, &lt;code&gt;sock_ops&lt;/code&gt; -- policy and socket-state hooks.&lt;/li&gt;
&lt;/ul&gt;

flowchart TD
    K[&quot;eBPF -- Programs&quot;]
    T[&quot;Tracing -- (kprobe, fentry, -- uprobe, tracepoint)&quot;]
    N[&quot;Networking -- (XDP, TC, sock_ops, -- sk_lookup)&quot;]
    S[&quot;Security -- (LSM, seccomp, -- landlock)&quot;]
    P[&quot;Policy &amp;amp; scheduling -- (cgroup, sched, -- perf_event)&quot;]
    K --&amp;gt; T
    K --&amp;gt; N
    K --&amp;gt; S
    K --&amp;gt; P
&lt;p&gt;That hook surface is what makes eBPF the universal Linux instrumentation substrate. Once a developer learns the load-verify-attach lifecycle, the same toolchain instruments a TCP retransmit, a &lt;code&gt;do_sys_open&lt;/code&gt; call, an LSM &lt;code&gt;file_open&lt;/code&gt; check, and an XDP fast-path drop -- all in the same language with the same verifier and the same JIT.&lt;/p&gt;
&lt;h3&gt;Maps: in-kernel state&lt;/h3&gt;
&lt;p&gt;The second piece of architecture eBPF adds over classic BPF is the &lt;em&gt;map&lt;/em&gt; -- a kernel-managed key-value store accessible from inside a verified program and from user space. Maps are how eBPF programs hold state between invocations and how they communicate with user space.&lt;/p&gt;

A kernel-managed data structure that an eBPF program can read and write from inside the kernel, and a user-space process can read and write through the `bpf()` syscall. Common map types include hash, array, LRU hash, per-CPU hash, ring buffer, and program array (used for tail calls). Each map has a maximum capacity declared at creation and a verifier-checked size for keys and values.
&lt;p&gt;The kernel hash-map documentation [@docs-kernel-org-bpf-maphashhtml] distinguishes shared and per-CPU variants. The decision between them is one of the consequential design choices in writing real eBPF code.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Map type&lt;/th&gt;
&lt;th&gt;Cross-CPU semantics&lt;/th&gt;
&lt;th&gt;Update cost&lt;/th&gt;
&lt;th&gt;Memory cost&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BPF_MAP_TYPE_HASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One value per key, shared across CPUs&lt;/td&gt;
&lt;td&gt;Atomic &lt;code&gt;__sync_fetch_and_add&lt;/code&gt; or &lt;code&gt;BPF_F_LOCK&lt;/code&gt; spinlock&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max_entries * (key_size + value_size)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;State that must be globally consistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BPF_MAP_TYPE_PERCPU_HASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Separate value slot per CPU&lt;/td&gt;
&lt;td&gt;Non-atomic read-modify-write&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max_entries * value_size * num_cpus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counters and histograms where rate matters and snapshot consistency does not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BPF_MAP_TYPE_RINGBUF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single MPSC ring with global FIFO order&lt;/td&gt;
&lt;td&gt;Reservation-spinlock on producer&lt;/td&gt;
&lt;td&gt;Fixed buffer&lt;/td&gt;
&lt;td&gt;Event streams whose user-space order must match cross-CPU producer order&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The per-CPU variant exists because cache-coherence cost on a contended hash slot dominates the time spent updating it; per-CPU maps remove that contention entirely at the price of cross-CPU consistency. A per-CPU counter on a 96-vCPU host occupies &lt;code&gt;96 * value_size&lt;/code&gt; bytes per key, but updates are local loads and stores. A shared counter on the same host is &lt;code&gt;value_size&lt;/code&gt; bytes per key, but every increment is an atomic.&lt;/p&gt;

A multi-producer single-consumer kernel-to-user transport added in Linux 5.8 and documented at `docs.kernel.org/bpf/ringbuf.html` [@docs-kernel-org-bpf-ringbufhtml]. Unlike the legacy `perf_event_array` (one ring per CPU), the BPF ringbuf is a single ring shared across all CPUs, with cross-CPU producer ordering preserved in the user-visible record stream.
&lt;p&gt;The ringbuf documentation [@docs-kernel-org-bpf-ringbufhtml] is explicit about why the design exists: &lt;em&gt;&quot;more efficient memory use by sharing ring buffer across CPUs; preserving ordering of events that happen sequentially in time, even across multiple CPUs (e.g., fork/exec/exit events for a task).&quot;&lt;/em&gt; A security telemetry consumer that needs to see &lt;code&gt;fork&lt;/code&gt; on CPU 0 before &lt;code&gt;kill&lt;/code&gt; on CPU 1 cannot use a per-CPU ring; it needs a single MPSC ring. The trade-off is real: the producer pays a brief spinlock for slot reservation, where a per-CPU ring would pay nothing. For event streams the trade is worth it; for histograms it is not.&lt;/p&gt;
&lt;h3&gt;The aggregation pattern&lt;/h3&gt;
&lt;p&gt;The reason eBPF is strictly more powerful than ETW is captured in one bpftrace one-liner. The DSL &lt;code&gt;bpftrace&lt;/code&gt; [@github-com-iovisor-bpftrace] -- inspired explicitly by DTrace -- compiles a single-line query into a verified eBPF program:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bpftrace&quot;&gt;kprobe:vfs_read { @[comm] = hist(arg2); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This program attaches to the &lt;code&gt;vfs_read&lt;/code&gt; kernel function. For every call, it indexes a per-CPU map by the calling process&apos;s name (&lt;code&gt;comm&lt;/code&gt;), buckets the &lt;code&gt;arg2&lt;/code&gt; value (the read length) into a power-of-two histogram, and increments the bucket. Nothing crosses the kernel-user boundary while &lt;code&gt;vfs_read&lt;/code&gt; is firing -- not at 10K calls per second, not at 10M. When the user hits Ctrl-C, bpftrace iterates the per-CPU maps from user space, merges the buckets across CPUs, and prints a histogram.&lt;/p&gt;
&lt;p&gt;ETW cannot do this. To produce the same histogram with ETW, a consumer would have to subscribe to every &lt;code&gt;vfs_read&lt;/code&gt;-equivalent kernel event, receive each one in user mode, compute its bucket, and update an in-process histogram. The kernel-user wire would carry the full firehose. eBPF carries only the final histogram.&lt;/p&gt;
&lt;p&gt;{`
// The bpftrace one-liner:
//   kprobe:vfs_read { @[comm] = hist(arg2); }
// lowers (conceptually) to this kernel-side and user-side flow.&lt;/p&gt;
&lt;p&gt;// --- inside the kernel, at every vfs_read call ---
function on_vfs_read(ctx) {
  const comm = bpf_get_current_comm();
  const len  = ctx.regs.rsi;                  // arg2: read length
  const bucket = log2(len);                   // 0..63&lt;/p&gt;
&lt;p&gt;  // per-CPU hash keyed by (comm, bucket); no cross-CPU atomics.
  const key = { comm, bucket };
  const slot = percpu_map.lookup_or_init(key, 0);
  *slot += 1;
}&lt;/p&gt;
&lt;p&gt;// --- in user space, on Ctrl-C ---
function print_histogram() {
  const merged = {};
  for (const cpu of all_cpus) {
    for (const [key, count] of percpu_map.iter(cpu)) {
      merged[key] = (merged[key] || 0) + count;
    }
  }
  render_power_of_two_histogram(merged);
}
`}&lt;/p&gt;
&lt;p&gt;The kernel-side per-event cost is a few instructions plus a non-atomic increment. The user-space cost is paid once, at print time. The wire between kernel and user carries one batch read of the entire per-CPU map. ETW&apos;s equivalent would carry every single &lt;code&gt;vfs_read&lt;/code&gt; event in full.&lt;/p&gt;
&lt;h3&gt;The instruction-count and complexity limits&lt;/h3&gt;
&lt;p&gt;Two distinct limits constrain what the verifier will accept. The constants are easy to confuse, and earlier drafts of this article confused them. The correct distinction comes straight from the kernel headers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BPF_MAXINSNS&lt;/code&gt; is defined as 4096 in &lt;code&gt;include/uapi/linux/bpf_common.h&lt;/code&gt;. This is the maximum number of bytecode instructions per program for unprivileged callers. A program longer than 4096 instructions is rejected at load time regardless of what the verifier finds.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BPF_COMPLEXITY_LIMIT_INSNS&lt;/code&gt; is defined as 1,000,000 in &lt;code&gt;kernel/bpf/verifier.c&lt;/code&gt;. This is the maximum number of &lt;em&gt;explored states&lt;/em&gt; the verifier will visit during its symbolic execution. It applies to privileged callers with &lt;code&gt;CAP_BPF&lt;/code&gt;, who are allowed to load larger programs but still bound the cost of verifying them.The two limits answer different questions. &lt;code&gt;BPF_MAXINSNS = 4096&lt;/code&gt; bounds the &lt;em&gt;size&lt;/em&gt; of an unprivileged program. &lt;code&gt;BPF_COMPLEXITY_LIMIT_INSNS = 1,000,000&lt;/code&gt; bounds the &lt;em&gt;cost&lt;/em&gt; of verification for privileged programs. Conflating them is a common error: production EDRs run with &lt;code&gt;CAP_BPF&lt;/code&gt; plus &lt;code&gt;CAP_PERFMON&lt;/code&gt; or root and load programs much longer than 4096 instructions, but the verifier&apos;s exploration is still bounded.&lt;/p&gt;
&lt;p&gt;Linux 5.16 (March 2022) [@kernel-org-bpf-indexhtml] made &lt;code&gt;kernel.unprivileged_bpf_disabled=1&lt;/code&gt; the default.The change followed a series of verifier soundness CVEs, including CVE-2020-8835 and CVE-2021-3490, that were exploitable from unprivileged user space. Production EDRs run with &lt;code&gt;CAP_BPF&lt;/code&gt; plus &lt;code&gt;CAP_PERFMON&lt;/code&gt; or full root; the unprivileged path is reserved for sandboxed workloads where the kernel team has weighed the risk.&lt;/p&gt;
&lt;h3&gt;The JIT and the trampoline&lt;/h3&gt;
&lt;p&gt;Brendan Gregg&apos;s &lt;em&gt;BPF Performance Tools&lt;/em&gt; [@brendangregg-com-tools-bookhtml], published by Addison-Wesley in 2019 (ISBN-13 9780136554820 [@pearson-com-p200000007897-9780136554820]), reports a 10x to 12x speedup of the JIT over the interpreter on x86-64. The number is qualitative -- the workload, the kernel version, and the program shape all matter -- but the order of magnitude is consistent across kernel docs and measurements. The JIT is what makes eBPF practically usable inside hot kernel paths.&lt;/p&gt;
&lt;p&gt;A second performance refinement landed in 2019 with the BPF trampoline patch series. Starovoitov&apos;s v1 cover letter [@lore-kernel-org-1-astkernelorg] introduced &lt;code&gt;fentry&lt;/code&gt; and &lt;code&gt;fexit&lt;/code&gt; -- BPF program attach points that use a tiny JIT-emitted dispatcher to call the attached programs directly, rather than relying on kprobe&apos;s &lt;code&gt;int3&lt;/code&gt; trap mechanism. The framing is worth quoting:&lt;/p&gt;

Unlike k[ret]probe there is practically zero overhead to call a set of BPF programs before or after kernel function. -- Alexei Starovoitov, BPF trampoline cover letter [@lore-kernel-org-1-astkernelorg]
&lt;p&gt;The v3 patch in the same series [@lore-kernel-org-4-astkernelorg] explains the structural reason: &lt;em&gt;&quot;To avoid the high cost of retpoline the attached BPF programs are called directly.&quot;&lt;/em&gt; kprobe goes through an indirect-jump dispatch, which on Spectre-mitigated kernels pays a retpoline penalty per call. The BPF trampoline replaces the indirect jump with a direct call patched in at attach time, eliminating that penalty entirely. The qualitative result is &quot;practically zero overhead&quot; relative to the function call itself. The exact numbers vary; the architectural reason does not.&lt;/p&gt;
&lt;h3&gt;Tail calls&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bpf_tail_call(ctx, &amp;amp;prog_array, index)&lt;/code&gt; is a helper that, when the &lt;code&gt;prog_array&lt;/code&gt; slot at &lt;code&gt;index&lt;/code&gt; contains a loaded program, replaces the current program&apos;s execution context with the target program&apos;s. The architecture is documented in the Cilium BPF architecture reference [@docs-cilium-io-bpf-architecture], which describes the 33-call nesting ceiling: &lt;em&gt;&quot;This, too, comes with an upper nesting limit of 33 calls, and is usually used to decouple parts of the program logic, for example, into stages.&quot;&lt;/em&gt; The 33-call cap bounds the worst-case execution time of a chain that the verifier cannot symbolically follow (the destination is a runtime-resolved map slot, not a static call target). We will return to the security implications of tail calls in section 7.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; eBPF inverts the observability model. ETW asks the kernel &quot;what happened?&quot; eBPF asks the kernel &quot;compute this and tell me the answer.&quot; The asymmetry is the reason a histogram of &lt;code&gt;vfs_read&lt;/code&gt; lengths costs nothing on the wire under eBPF, and costs a fully formatted event per call under ETW.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;eBPF is strictly more powerful than ETW: programmable filter, programmable aggregation, hooks everywhere. But that power has a cost that does not exist in ETW at all. The verifier.&lt;/p&gt;
&lt;h2&gt;5. The Verifier: Where Mathematics Meets the Kernel&lt;/h2&gt;
&lt;p&gt;May 2023. NIST publishes CVE-2023-2163 [@nvd-nist-gov-2023-2163]. The advisory describes the eBPF verifier in every Linux kernel since 5.4 quietly accepting programs it should have rejected: &lt;em&gt;&quot;Incorrect verifier pruning in BPF in Linux Kernel &amp;gt;=5.4 leads to unsafe code paths being incorrectly marked as safe, resulting in arbitrary read/write in kernel memory, lateral privilege escalation, and container escape.&quot;&lt;/em&gt; The fix was a small correction to a state-pruning heuristic. The lesson is bigger than the patch: &lt;em&gt;no in-kernel verifier for a Turing-complete instruction set can be simultaneously sound, complete, and decidable.&lt;/em&gt; That is not a bug. It is a theorem.&lt;/p&gt;
&lt;h3&gt;Rice&apos;s theorem in the kernel&lt;/h3&gt;
&lt;p&gt;Alan Turing proved in 1936 that the halting problem is undecidable: no algorithm can decide, for every possible program, whether that program halts on every input. Henry Gordon Rice extended the result in 1953: any &lt;em&gt;non-trivial semantic property&lt;/em&gt; of a program -- including memory safety, type safety, and bounded resource use -- is undecidable for the general case. The verifier has to decide a non-trivial semantic property: &lt;em&gt;does this eBPF program access kernel memory only through valid pointers, with valid offsets, and terminate?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It cannot. Not in general. The verifier has to give up at least one of three properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Soundness&lt;/em&gt; -- never accept an unsafe program.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Completeness&lt;/em&gt; -- never reject a safe program.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Scalability&lt;/em&gt; -- run in polynomial time on real programs.&lt;/li&gt;
&lt;/ul&gt;

The halting problem is about a single property: termination. Rice&apos;s theorem generalizes the result to all non-trivial extensional properties -- any property that depends on what a program computes rather than how it is written. Memory safety on a Turing-complete instruction set is a non-trivial extensional property: there exist programs that are safe and programs that are unsafe. Rice&apos;s theorem says no decision procedure can correctly classify every program. Any real verifier must therefore be an *approximation* -- either it sometimes rejects safe programs (loss of completeness), sometimes accepts unsafe ones (loss of soundness), or runs out of resources on hard inputs (loss of scalability).
&lt;p&gt;Jia and colleagues at HotOS 2023 [@sigops-org-papers-jiapdf] formalized this trilemma for in-kernel verifiers. The paper&apos;s title is the thesis: &lt;em&gt;&quot;Kernel Extension Verification Is Untenable.&quot;&lt;/em&gt; The authors argue that any verifier for a kernel extension language with the expressiveness of eBPF must trade off at least one of the three properties, and that real verifiers ship by trading all three approximately.&lt;/p&gt;

Kernel Extension Verification Is Untenable. -- Jia et al., HotOS 2023, `sigops.org/s/conferences/hotos/2023/papers/jia.pdf` [@sigops-org-papers-jiapdf]

flowchart TD
    A[Soundness -- never accept -- unsafe programs]
    B[Completeness -- never reject -- safe programs]
    C[Scalability -- polynomial time -- on real programs]
    A --- B
    B --- C
    C --- A
    X[&quot;No verifier can have -- all three on a -- Turing-complete ISA&quot;]
    A -.-&amp;gt; X
    B -.-&amp;gt; X
    C -.-&amp;gt; X
&lt;p&gt;The Linux verifier ships with all three approximately. PREVAIL, the verifier used by eBPF-for-Windows, ships with stronger soundness and weaker completeness. The two designs occupy different points on the triangle, and the difference shows up in production.&lt;/p&gt;
&lt;h3&gt;The Linux verifier&lt;/h3&gt;
&lt;p&gt;The kernel verifier documentation [@docs-kernel-org-bpf-verifierhtml] describes the algorithm:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The safety of the eBPF program is determined in two steps. First step does DAG check to disallow loops and other CFG validation. ... Second step starts from the first insn and descends all possible paths. It simulates execution of every insn and observes the state change of registers and stack.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The state the verifier tracks is a register-state lattice. Each register holds a type from a finite set: &lt;code&gt;PTR_TO_CTX&lt;/code&gt; (a pointer to the program&apos;s context argument), &lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt; (a pointer into a map entry), &lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt; (the return type of &lt;code&gt;bpf_map_lookup_elem&lt;/code&gt;, which can be null), &lt;code&gt;SCALAR_VALUE&lt;/code&gt; (an integer with min/max range), and so on. Each register also has a min/max range that tightens at every operation.&lt;/p&gt;

The kernel-side static analyzer that proves termination and memory safety of every eBPF program before load. The Linux verifier is documented at `docs.kernel.org/bpf/verifier.html` [@docs-kernel-org-bpf-verifierhtml]. It uses a register-state lattice plus min/max range tracking and explores all reachable program paths with state pruning to keep the cost manageable.
&lt;p&gt;Consider the canonical pattern: look up a map value, check for null, dereference. Every eBPF tracing program does some version of this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct value *v = bpf_map_lookup_elem(&amp;amp;map, &amp;amp;key);   // r0 := PTR_TO_MAP_VALUE_OR_NULL
if (!v) return 0;                                    // branch on r0 == 0
return v-&amp;gt;field;                                     // deref r0 + offset(field)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The verifier traces both branches. On the taken branch (&lt;code&gt;r0 == 0&lt;/code&gt;), the type stays nullable, and the program returns. On the not-taken branch, the verifier refines the type from &lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt; to &lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt; -- the null qualifier is gone, the dereference is bounds-checked against the map&apos;s value size, and the program is accepted.&lt;/p&gt;
&lt;p&gt;This refinement is exactly the thing that broke in CVE-2023-2163. The bug was not in the dereference logic; it was in the &lt;em&gt;state pruning&lt;/em&gt; that keeps the verifier&apos;s exploration tractable. Once the verifier has visited a program point with a given abstract state, it prunes subsequent visits from different predecessors with &quot;the same&quot; state. CVE-2023-2163 was a case where the pruner&apos;s notion of &quot;the same state&quot; was &lt;em&gt;narrower&lt;/em&gt; than the predecessor&apos;s true state. The verifier accepted a program in which a register&apos;s true type at a join point did not match the type the verifier had pruned against. The program ran with hidden type confusion. Kernel arbitrary read/write followed.&lt;/p&gt;
&lt;h3&gt;PREVAIL, the abstract-interpretation verifier&lt;/h3&gt;
&lt;p&gt;PREVAIL [@github-com-ebpf-verifier], published by Gershuni and colleagues at PLDI 2019 [@vbpf-github-io-prevail-paperpdf], takes a structurally different approach. Where Linux&apos;s verifier is a heuristic abstract interpreter with a discrete type lattice, PREVAIL uses &lt;em&gt;numerical abstract interpretation&lt;/em&gt; over the &lt;em&gt;zone domain&lt;/em&gt; plus intervals.&lt;/p&gt;

A general framework for static analysis, introduced by Patrick and Radhia Cousot in 1977. The analyzer computes over an *abstract domain* -- intervals, zones, polyhedra, octagons -- rather than concrete program states. A safe abstract operation must over-approximate every possible concrete behavior. The soundness of the analysis reduces to the soundness of the abstract domain operations, which can be proved once and reused.
&lt;p&gt;In the zone domain, the abstract state can express &lt;em&gt;relational&lt;/em&gt; constraints between registers and memory base addresses -- not just &quot;register &lt;code&gt;r0&lt;/code&gt; is in &lt;code&gt;[base, base + size)&lt;/code&gt;&quot; but &quot;&lt;code&gt;r0 - map_base&lt;/code&gt; is in &lt;code&gt;[0, value_size)&lt;/code&gt;.&quot; That extra expressiveness is what lets PREVAIL prove pointer-arithmetic safety more directly than the Linux verifier&apos;s case enumeration. Walking the same null-check program:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Program point&lt;/th&gt;
&lt;th&gt;Linux verifier (register lattice)&lt;/th&gt;
&lt;th&gt;PREVAIL (zone domain)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;After &lt;code&gt;bpf_map_lookup_elem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;r0 in {0} U [base, base+sz)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taken branch (r0 == 0)&lt;/td&gt;
&lt;td&gt;refined to NULL&lt;/td&gt;
&lt;td&gt;r0 = 0 (equality)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not-taken branch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt; (qualifier dropped)&lt;/td&gt;
&lt;td&gt;r0 - base in [0, sz)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At deref &lt;code&gt;v-&amp;gt;field&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bounds-checked deref&lt;/td&gt;
&lt;td&gt;r0 - base in [off, off+access)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Both verifiers accept the program. The difference is in the proof strategy. Linux&apos;s verifier reasons case-by-case over a finite lattice; PREVAIL reasons numerically over an abstract domain whose soundness is proved once and reused. The PREVAIL paper (Gershuni et al., PLDI 2019) [@vbpf-github-io-prevail-paperpdf] showed that the zone-domain approach is sound and runs in polynomial time per fixed abstract domain.&lt;/p&gt;

flowchart LR
    A[&quot;r0 := bpf_map_lookup_elem&quot;]
    B{&quot;r0 == 0?&quot;}
    C[&quot;return 0&quot;]
    D[&quot;return r0-&amp;gt;field&quot;]
    A --&amp;gt; B
    B -- yes --&amp;gt; C
    B -- no --&amp;gt; D
    A -. &quot;Linux: PTR_TO_MAP_VALUE_OR_NULL -- PREVAIL: r0 in {0} U [base, base+sz)&quot; .-&amp;gt; A
    C -. &quot;Linux: NULL -- PREVAIL: r0 = 0&quot; .-&amp;gt; C
    D -. &quot;Linux: PTR_TO_MAP_VALUE -- PREVAIL: r0 - base in [0, sz)&quot; .-&amp;gt; D
&lt;p&gt;The trade-off is concrete. PREVAIL accepts a broader class of programs the Linux verifier rejects (some bounded loops, some longer programs), and rejects others the Linux verifier accepts (Linux&apos;s heuristic pruning is more aggressive than zone-domain reasoning in some patterns). The contrast is a &lt;em&gt;trade&lt;/em&gt;, not a strict ordering. Each verifier is sound with respect to its own abstract domain. The Linux verifier&apos;s CVE history is what happens when the domain itself is implemented heuristically rather than from a once-and-for-all soundness proof. The work of Paul Chaignon [@pchaigno-github-io-ebpf-verifierhtml] walks through the architectural differences in more detail.&lt;/p&gt;
&lt;h3&gt;Four CVEs, one pattern&lt;/h3&gt;
&lt;p&gt;The Linux verifier has shipped four widely-disclosed soundness bugs, each one a case where the verifier accepted a program it should have rejected.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Subsystem at fault&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;CVE-2020-8835 [@nvd-nist-gov-2020-8835]&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;32-bit register bounds tracking&lt;/td&gt;
&lt;td&gt;Out-of-bounds read/write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-3490 [@nvd-nist-gov-2021-3490]&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;ALU32 bitwise-op bounds tracking&lt;/td&gt;
&lt;td&gt;Out-of-bounds R/W, arbitrary RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2022-23222 [@nvd-nist-gov-2022-23222]&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*_OR_NULL&lt;/code&gt; type-state tracking&lt;/td&gt;
&lt;td&gt;Local privilege escalation via type confusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2023-2163 [@nvd-nist-gov-2023-2163]&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Branch-pruning logic&lt;/td&gt;
&lt;td&gt;Arbitrary kernel R/W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The CVE-2020-8835 NVD entry describes a flaw where the verifier &lt;em&gt;&quot;did not properly restrict the register bounds for 32-bit operations, leading to out-of-bounds reads and writes in kernel memory.&quot;&lt;/em&gt; CVE-2021-3490, also reported on the NVD, identifies the same class of bug in the bitwise-operation paths. The CVE-2022-23222 record is tracked across the SUSE bug [@bugzilla-suse-com-showbugcgi], Debian DSA-5050 [@debian-org-dsa-5050], and the openwall oss-security disclosure thread [@openwall-com-13-1].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; All four CVEs are the same shape: the verifier&apos;s abstract state at some program point was &lt;em&gt;narrower&lt;/em&gt; than the program&apos;s true reachable state, so the verifier proved a property that did not hold. Each fix tightened the abstract operation that introduced the narrowing -- range-tracking for the 2020 and 2021 bugs, type-state for 2022, branch pruning for 2023. None of the fixes were &quot;fix the runtime&quot;; they were all &quot;fix the static analysis.&quot; That is exactly the shape Rice&apos;s theorem predicts: a heuristic abstract interpreter that occasionally drops information at a join point.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The verifier is a research-grade static analyzer running as kernel code. When it gets the abstract domain wrong, the safety guarantee is a CVE. ETW does not have this failure mode because ETW does not run user-supplied code in the kernel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ETW has driver signing as its safety mechanism. eBPF has the verifier. Microsoft&apos;s eBPF-for-Windows project asked an interesting question: &lt;em&gt;what if you want both?&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;6. eBPF for Windows: The Convergence&lt;/h2&gt;
&lt;p&gt;On May 10, 2021, Dave Thaler of Microsoft published a blog post announcing a new project. The opening line is the kind of announcement that sounds modest and is not:&lt;/p&gt;

&quot;Today we are excited to announce a new Microsoft open source project to make eBPF work on Windows 10 and Windows Server 2016 and later.&quot; -- Dave Thaler, &quot;Making eBPF work on Windows&quot; [@cloudblogs-microsoft-com-on-windows], Microsoft Open Source Blog, May 2021
&lt;p&gt;The promise was a near-source-compatible eBPF surface on NT, so that programs and toolchains written for Linux eBPF -- libbpf, bpftool, BCC, clang &lt;code&gt;-target bpf&lt;/code&gt; -- would work on Windows with minimal change. The architectural surprise, visible only once you read the design docs, is that the Linux design does not port directly. The Windows trust model is different. The Windows code-integrity story is different. The choices Microsoft made reveal which parts of eBPF &lt;em&gt;are&lt;/em&gt; genuinely portable and which parts are deeply Linux-shaped.&lt;/p&gt;
&lt;h3&gt;Three execution modes&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;microsoft/ebpf-for-windows&lt;/code&gt; README [@github-com-for-windows] decomposes the runtime into three modes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Native eBPF program (preferred, HVCI-compatible).&lt;/em&gt; PREVAIL verifies the bytecode in user mode. On success, the &lt;code&gt;bpf2c&lt;/code&gt; [@github-com-bpf2ctests-expected] tool transliterates each verified BPF instruction to equivalent C, MSVC compiles the C, and the result is a signed &lt;code&gt;.sys&lt;/code&gt; kernel driver. The signed driver is what gets loaded into the kernel.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;JIT compiler.&lt;/em&gt; A user-mode service (&lt;code&gt;eBPFSvc.exe&lt;/code&gt;) calls the uBPF [@github-com-iovisor-ubpf] JIT to produce x64 or ARM64 native code, loaded into the kernel-mode execution context. Disabled on HVCI hosts because dynamic code generation cannot be SiPolicy-signed.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Interpreter.&lt;/em&gt; uBPF&apos;s interpreter, debug-only.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The native mode is the architecturally interesting one. It treats eBPF bytecode as a &lt;em&gt;source language&lt;/em&gt; for a signed-driver compile, not as a target for a kernel-mode JIT. The choice is forced by Windows&apos; kernel-mode security model.&lt;/p&gt;

A Windows feature that uses the hypervisor to enforce that only signed code runs in kernel mode. With HVCI on, the kernel will refuse to execute any page that does not match a Code Integrity policy signature. Dynamic code generation -- the kind a JIT does -- is impossible on an HVCI host unless the JIT itself is privileged to bless the pages it produces.
&lt;h3&gt;bpf2c: the literal transliterator&lt;/h3&gt;
&lt;p&gt;The thing that makes the native pipeline work is &lt;code&gt;bpf2c&lt;/code&gt;. It takes verified eBPF bytecode and emits portable C that any modern compiler can build into a kernel driver. The transliteration is one bytecode instruction per C statement. A concrete excerpt from &lt;code&gt;droppacket_raw.c&lt;/code&gt; [@raw-githubusercontent-com-expected-droppacketrawc], the expected output for the XDP-class &lt;code&gt;droppacket.c&lt;/code&gt; [@github-com-sample-droppacketc] sample, shows the shape:&lt;/p&gt;
&lt;p&gt;{`
// Excerpt from microsoft/ebpf-for-windows
//   tests/bpf2c_tests/expected/droppacket_raw.c
// One verified BPF instruction maps to one C statement.&lt;/p&gt;
&lt;p&gt;#pragma code_seg(push, &quot;xdp&quot;)
static uint64_t
DropPacket(void* context, const program_runtime_context_t* runtime_context)
{
  uint64_t stack[(UBPF_STACK_SIZE + 7) / 8];
  register uint64_t r0 = 0;
  register uint64_t r1 = 0;
  // ... r2 .. r6, r10 declarations ...&lt;/p&gt;
&lt;p&gt;  // EBPF_OP_MOV64_REG pc=0 dst=r6 src=r1 offset=0 imm=0
  r6 = r1;
  // EBPF_OP_MOV64_IMM pc=1 dst=r1 src=r0 offset=0 imm=0
  r1 = IMMEDIATE(0);
  // EBPF_OP_STXDW pc=2 dst=r10 src=r1 offset=-8 imm=0
  WRITE_ONCE_64(r10, (uint64_t)r1, OFFSET(-8));&lt;/p&gt;
&lt;p&gt;  // ... one C statement per verified BPF instruction ...&lt;/p&gt;
&lt;p&gt;  r0 = runtime_context-&amp;gt;helper_data[0].address(r1, r2, r3, r4, r5, context);
}
`}&lt;/p&gt;

The eBPF-for-Windows transliterator from verified BPF bytecode to portable C suitable for MSVC compilation. The output is a signed-driver source file, one C statement per BPF instruction, that can be compiled and signed through the same pipeline as any other kernel driver. The golden test corpus lives at `microsoft/ebpf-for-windows/tests/bpf2c_tests/expected` [@github-com-bpf2ctests-expected].
&lt;p&gt;Four things stand out in the excerpt. &lt;em&gt;One BPF instruction maps to one C statement&lt;/em&gt;; the &lt;code&gt;// EBPF_OP_*&lt;/code&gt; comments name the opcode, and the line below it is the equivalent C. The eBPF VM&apos;s eleven registers become eleven C &lt;code&gt;uint64_t&lt;/code&gt; locals; MSVC&apos;s optimizer assigns them to native registers in the final &lt;code&gt;.sys&lt;/code&gt;. The &lt;code&gt;#pragma code_seg(push, &quot;xdp&quot;)&lt;/code&gt; directive names the program section the same way &lt;code&gt;SEC(&quot;xdp&quot;)&lt;/code&gt; does on Linux. And helper calls dispatch through a runtime table -- &lt;code&gt;runtime_context-&amp;gt;helper_data[0].address(...)&lt;/code&gt; -- so the signed driver remains portable across helper-ABI changes.&lt;/p&gt;
&lt;p&gt;The result is a kernel module that is a signed driver in every Windows sense of the term: HVCI checks pass, Kernel Mode Code Integrity (KMCI) [@learn-microsoft-com-downloads-sysmon] is satisfied, the Authenticode chain validates. eBPF-for-Windows native mode does not invent a new in-kernel trust boundary. It composes with the one Windows already has.&lt;/p&gt;

flowchart LR
    A[&quot;Restricted C source&quot;]
    B[&quot;clang -target bpf&quot;]
    C[&quot;BPF bytecode&quot;]
    D[&quot;PREVAIL verifier -- (user mode)&quot;]
    E[&quot;bpf2c -- transliterator&quot;]
    F[&quot;Portable C&quot;]
    G[&quot;MSVC compile&quot;]
    H[&quot;Signed .sys driver&quot;]
    I[&quot;Windows kernel -- (HVCI / KMCI)&quot;]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D --&amp;gt; E --&amp;gt; F --&amp;gt; G --&amp;gt; H --&amp;gt; I
&lt;h3&gt;The verifier moved&lt;/h3&gt;
&lt;p&gt;The most consequential architectural choice in eBPF-for-Windows is not visible in the binary. PREVAIL does not run inside the kernel. It runs inside the user-mode &lt;code&gt;eBPFSvc.exe&lt;/code&gt; service, which orchestrates verification and the subsequent compile-and-sign pipeline. The kernel never sees an unverified BPF program. By the time anything enters the kernel, it is either a signed driver (native mode) or a JIT-produced buffer that has already passed verification in user space (JIT mode, on non-HVCI hosts).&lt;/p&gt;
&lt;p&gt;This is a deliberate divergence from Linux. Linux runs its verifier inside the kernel because the kernel is the only place that can prevent unprivileged user space from loading unsafe programs. Windows can move the verifier out of the kernel because the kernel-mode trust boundary -- &lt;em&gt;the thing that can run&lt;/em&gt; -- is already protected by code signing. The verifier becomes a &lt;em&gt;correctness&lt;/em&gt; check rather than a &lt;em&gt;safety&lt;/em&gt; check at the kernel boundary; safety at the boundary is enforced by HVCI.&lt;/p&gt;
&lt;h3&gt;Hook coverage as of 2026&lt;/h3&gt;
&lt;p&gt;The hook surface on Windows is narrower than Linux&apos;s. As of 2026, eBPF-for-Windows exposes XDP-class network hooks, BIND, SOCK_OPS, SOCK_ADDR, and process-creation and process-exit hooks via Windows Filtering Platform callouts plus a process hook surface. There is no full kprobe surface. There are no LSM-equivalent hooks. The project README [@github-com-for-windows] labels itself &quot;work-in-progress.&quot; The networking-subset claim in this article is not marketing softening; it is the actual hook list.&lt;/p&gt;

The naive model of cross-OS eBPF says: same bytecode runtime, runs on both kernels. The actual model is more subtle and more interesting.&lt;p&gt;The bytecode is portable because both verifiers accept the same instruction encoding, now standardized at IETF as RFC 9669 [@rfc-editor-org-rfc-rfc9669html]. The verifier is portable because PREVAIL is an abstract interpreter that does not depend on Linux-specific kernel data structures. The &lt;em&gt;runtime&lt;/em&gt; is not portable: Linux runs verified bytecode through its in-kernel JIT; Windows transliterates verified bytecode to C and compiles it into a signed driver.&lt;/p&gt;
&lt;p&gt;So the cross-platform abstraction is the verifier, not the runtime. PREVAIL is the contract; each OS lifts verified bytecode into its own trust model. Linux trusts the verifier&apos;s output enough to JIT it in kernel mode; Windows distrusts in-kernel dynamic code by policy and lifts the verified bytecode out through a signed-driver compile. The portability boundary moved from &quot;same VM&quot; to &quot;same static analysis,&quot; and that is the architectural insight that makes the project work.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The runtime is not the cross-platform abstraction. The verifier is. PREVAIL is the contract; each OS lifts verified bytecode into its own trust model -- in-kernel JIT on Linux, signed-driver compile on Windows. eBPF-for-Windows is not &quot;same kernel hook, different OS&quot;; it is &quot;same bytecode contract, different OS-specific lifting.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Cross-OS eBPF works for the networking subset today. The general kernel observability case -- arbitrary kprobes, full LSM hooks, deep process introspection -- is still Linux-only because the &lt;em&gt;hooks themselves&lt;/em&gt; are Linux-internal. eBPF-for-Windows is a real convergence, but it is a &lt;em&gt;subset&lt;/em&gt; convergence. Section 7 zooms out and compares the two designs across the full set of dimensions practitioners actually use to choose.&lt;/p&gt;
&lt;h2&gt;7. Head-to-Head: Performance and Trust Models&lt;/h2&gt;
&lt;p&gt;Two designs. One emits, one computes. Practitioners need to know what each one costs, where each one&apos;s edges cut, and what attack classes each design enables. The right form for that comparison is a table.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;ETW&lt;/th&gt;
&lt;th&gt;Linux eBPF&lt;/th&gt;
&lt;th&gt;eBPF for Windows&lt;/th&gt;
&lt;th&gt;DTrace&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;In-kernel filter language&lt;/td&gt;
&lt;td&gt;None (level + keyword mask only)&lt;/td&gt;
&lt;td&gt;Verified bytecode&lt;/td&gt;
&lt;td&gt;Verified bytecode&lt;/td&gt;
&lt;td&gt;D scripting language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-kernel aggregation&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Maps (per-CPU and shared)&lt;/td&gt;
&lt;td&gt;Maps&lt;/td&gt;
&lt;td&gt;Aggregations primitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer per-event cost&lt;/td&gt;
&lt;td&gt;Constant: format + memcpy to per-CPU buffer&lt;/td&gt;
&lt;td&gt;JIT-compiled native code at hook&lt;/td&gt;
&lt;td&gt;JIT or signed-driver call at hook&lt;/td&gt;
&lt;td&gt;Probe handler call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier&lt;/td&gt;
&lt;td&gt;Driver signing only&lt;/td&gt;
&lt;td&gt;Linux in-kernel heuristic verifier&lt;/td&gt;
&lt;td&gt;PREVAIL in user mode + KMCI&lt;/td&gt;
&lt;td&gt;None (D is interpreted, safe-by-construction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier soundness incidents&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;4 widely-disclosed CVEs (2020-2023)&lt;/td&gt;
&lt;td&gt;None disclosed&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hook coverage&lt;/td&gt;
&lt;td&gt;Universal across Windows API surface&lt;/td&gt;
&lt;td&gt;Universal: kprobe, uprobe, tracepoint, XDP, TC, LSM, sched&lt;/td&gt;
&lt;td&gt;XDP, BIND, SOCK_OPS, SOCK_ADDR, process&lt;/td&gt;
&lt;td&gt;Solaris/BSD/macOS provider set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform&lt;/td&gt;
&lt;td&gt;Windows only&lt;/td&gt;
&lt;td&gt;Linux only&lt;/td&gt;
&lt;td&gt;Source-compatible with Linux subset&lt;/td&gt;
&lt;td&gt;Solaris, FreeBSD, macOS (legacy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;Per-CPU ring buffer, .etl files&lt;/td&gt;
&lt;td&gt;Ringbuf, perf_event_array, maps&lt;/td&gt;
&lt;td&gt;Ringbuf, maps&lt;/td&gt;
&lt;td&gt;Per-CPU buffers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust model&lt;/td&gt;
&lt;td&gt;Manifest registration + driver signing&lt;/td&gt;
&lt;td&gt;Verifier + CAP_BPF + CAP_PERFMON&lt;/td&gt;
&lt;td&gt;Verifier + HVCI + driver signing&lt;/td&gt;
&lt;td&gt;Privilege check + safe-by-construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adoption pattern&lt;/td&gt;
&lt;td&gt;Defender, Sysmon, CrowdStrike, SentinelOne, Carbon Black&lt;/td&gt;
&lt;td&gt;Cilium, Falco, Tetragon, Tracee, Pixie, Sysmon for Linux&lt;/td&gt;
&lt;td&gt;Pre-production; Azure test deployments&lt;/td&gt;
&lt;td&gt;Solaris/macOS legacy + bpftrace via inspiration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;Forensic capture across the entire Windows API surface&lt;/td&gt;
&lt;td&gt;Hot-path filtering and aggregation with arbitrary kernel hooks&lt;/td&gt;
&lt;td&gt;Cross-platform networking observability&lt;/td&gt;
&lt;td&gt;Interactive debugging on Solaris-lineage systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;The asymptotic argument&lt;/h3&gt;
&lt;p&gt;Two designs can be compared asymptotically. ETW carries N events of average size S; the kernel-to-user wire cost is Omega(NS) -- the unavoidable lower bound for streaming N events. eBPF can reduce that to O(M) where M is the aggregation size, for workloads that aggregate before the events cross the boundary. The bpftrace histogram from section 4 is the concrete example: &lt;code&gt;vfs_read&lt;/code&gt; can fire ten million times per second while the user-side bandwidth is zero, because the per-CPU histogram never crosses the boundary until print time.&lt;/p&gt;
&lt;p&gt;The asymmetry is the entire reason eBPF makes sense for high-frequency telemetry. It is also the reason every cloud-native observability tool from 2018 onward is on eBPF. When the producer rate exceeds the user-space consumption rate, you do not have a choice: you either drop events or aggregate them in-kernel. ETW can drop. Only eBPF can aggregate.&lt;/p&gt;
&lt;h3&gt;The tail-call attack class&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bpf_tail_call(ctx, &amp;amp;prog_array, index)&lt;/code&gt; is powerful and its power has structural consequences. From the BPF trampoline v3 cover letter [@lore-kernel-org-1-astkernelorg-2], the kernel team is explicit that the trampoline was designed in part as a &lt;em&gt;replacement&lt;/em&gt; for tail-call-based chaining: &lt;em&gt;&quot;In many cases it can be used as a replacement for bpf_tail_call-based program chaining.&quot;&lt;/em&gt; The motivation is structural -- there are three attack classes implicit in the tail-call mechanism, and the trampoline avoids them.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Branch-target injection on the tail-call dispatcher.&lt;/em&gt; Pre-mitigation kernels exposed an indirect branch from kernel mode -- the dispatcher selecting its target from a user-controllable &lt;code&gt;prog_array&lt;/code&gt; index. That is exactly the shape of a Spectre-v2 gadget. Mitigation: retpolined dispatcher and the BPF trampoline replacement that avoids the indirect branch entirely.The qualitative reason fentry beats kprobe is not a benchmark; it is the avoidance of a retpoline. The v3 patch cover letter spells this out: &lt;em&gt;&quot;To avoid the high cost of retpoline the attached BPF programs are called directly.&quot;&lt;/em&gt; Real numbers vary by microarchitecture, retpoline implementation, and the rest of the kernel-build configuration, but the structural reason is the same on every machine.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Recursion-bound bypass.&lt;/em&gt; The 33-call cap protects the verifier&apos;s termination proof for a single program from being bypassed by chaining, but it is a per-execution counter. A sequence of attached programs at different attach points can still produce arbitrary aggregate work. The mitigation lives in per-event scheduling, not in the verifier.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Speculative type confusion.&lt;/em&gt; The verifier proves a single program&apos;s register-type invariants. The target of a tail call is selected at runtime from a map, so speculative execution can execute a different program under the calling program&apos;s type-state. Mitigation: indirect-call hardening shared with the rest of the kernel.&lt;/p&gt;

flowchart LR
    A[&quot;Calling BPF program&quot;]
    B[&quot;bpf_tail_call(ctx, &amp;amp;arr, idx)&quot;]
    C[&quot;JIT dispatcher -- (indirect jump)&quot;]
    D{&quot;Map slot at idx&quot;}
    E[&quot;Target BPF program&quot;]
    F[&quot;Speculative path -- (wrong target)&quot;]
    G[&quot;Retpoline / BPF trampoline -- (direct call)&quot;]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D
    D -- correct --&amp;gt; E
    D -. speculative .-&amp;gt; F
    G -. mitigation .-&amp;gt; C
&lt;h3&gt;The ETW user-mode bypass&lt;/h3&gt;
&lt;p&gt;ETW has its own structural attack class, mentioned in section 3 and worth restating in the trust-model context. A process that wants to silence its own ETW emissions can patch &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; to a &lt;code&gt;ret&lt;/code&gt; instruction in its own address space. The kernel buffer never sees the event. EDR vendors monitor for this integrity violation out of band, and use the patch itself as a high-confidence detection signal.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; ETW&apos;s emission path runs in the calling process&apos;s own address space. A process that wants to hide its activity can patch the &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; thunk to &lt;code&gt;ret&lt;/code&gt;, silencing emissions before they reach the kernel buffer. EDR vendors monitor for this integrity violation out of band, and treat the patch as a detection in its own right. The deeper question is whether any user-mode emission primitive can be tamper-resistant under hostile user-mode code. The current answer is &quot;no&quot;: the mitigation has been to move the trust boundary into the kernel, via PPL, the kernel-only Threat-Intelligence provider, and (on Linux) LSM hooks that observe &lt;code&gt;mprotect&lt;/code&gt; and image-load operations directly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Trust models, side by side&lt;/h3&gt;
&lt;p&gt;ETW trusts manifest registration plus Code Integrity for kernel drivers. The kernel only emits events; the only adversary-controllable surface is the user-mode provider, and the integrity-violation tell catches the obvious attack.&lt;/p&gt;
&lt;p&gt;Linux eBPF trusts the verifier plus &lt;code&gt;CAP_BPF&lt;/code&gt; and &lt;code&gt;CAP_PERFMON&lt;/code&gt;. The verifier is the kernel-mode safety boundary; capabilities gate who can load programs at all. Both have been the source of soundness CVEs and exploitation paths. Defense in depth: unprivileged eBPF off by default since 5.16, hardening of the indirect-call dispatcher, ongoing verifier work.&lt;/p&gt;
&lt;p&gt;eBPF for Windows trusts PREVAIL plus HVCI driver signing. The verifier runs in user mode; the kernel only ever sees a signed driver or a JIT-emitted buffer that has already passed the verifier. The composition is &lt;em&gt;strictly more conservative&lt;/em&gt; than Linux eBPF, because it stacks the verifier on top of the signing model rather than replacing it. Microsoft is using the Windows kernel-mode trust mechanism &lt;em&gt;and&lt;/em&gt; adding the eBPF verifier to it, not choosing between them.&lt;/p&gt;
&lt;p&gt;The next layer up from the kernel substrate is the consumer layer -- the agents and SIEM pipelines practitioners actually ship. That production stack is what determines which substrate practitioners reach for first.&lt;/p&gt;
&lt;h2&gt;8. Production Adoption: The Agent Layer&lt;/h2&gt;
&lt;p&gt;The substrate matters because the consumer stack does. On Linux, eBPF is the foundation of every serious cloud-native security and observability project. On Windows, ETW is the same. The portable subset is small but real, and it is growing.&lt;/p&gt;
&lt;h3&gt;The Linux side&lt;/h3&gt;
&lt;p&gt;Cilium [@cilium-io] is the dominant eBPF-based networking project, CNCF-graduated [@falco-org-docs] and shipping Kubernetes cluster networking, NetworkPolicy enforcement, and a service mesh implementation. Falco [@falco-org], originally created by Sysdig and now CNCF-graduated, provides eBPF-based runtime threat detection driven by a rules engine. Tetragon [@tetragon-io-docs-overview], a Cilium subproject, attaches eBPF programs to kprobes and LSM hooks for in-kernel enforcement -- not just observation but the ability to block. Tracee [@github-com-aquasecurity-tracee] from Aqua Security is an eBPF runtime security tool. Pixie [@docs-px-dev], originally Pixie Labs and now under New Relic, uses eBPF for auto-instrumentation of services running in Kubernetes.&lt;/p&gt;
&lt;p&gt;Sysmon for Linux [@github-com-microsoft-sysmonforlinux] is the most architecturally interesting member of the list. Microsoft, the company that built ETW and Sysmon, ported Sysmon to Linux by replacing the ETW back end with eBPF kprobes via the &lt;code&gt;SysinternalsEBPF&lt;/code&gt; library. The XML configuration schema and Event IDs are preserved, so SOC analysts see the same channel from either OS. It is the production demonstration that ETW and eBPF can be made surface-equivalent to a consumer.&lt;/p&gt;
&lt;h3&gt;The Windows side&lt;/h3&gt;
&lt;p&gt;Sysmon [@learn-microsoft-com-downloads-sysmon] is the canonical ETW consumer reference design, authored by Mark Russinovich and Thomas Garnier and free from Microsoft. Microsoft Defender for Endpoint [@learn-microsoft-com-defender-endpoint] is the commercial Microsoft EDR product, ETW-driven and cloud-connected. CrowdStrike Falcon, SentinelOne, and Carbon Black are the major third-party EDRs, all built on ETW. krabsetw [@github-com-microsoft-krabsetw] is Microsoft&apos;s C++ ETW consumer library; the &lt;code&gt;Microsoft.Diagnostics.Tracing.TraceEvent&lt;/code&gt; package is the .NET equivalent.&lt;/p&gt;
&lt;h3&gt;The toolchain layer&lt;/h3&gt;
&lt;p&gt;The eBPF world comes with a toolchain that does not have a direct ETW counterpart. &lt;code&gt;libbpf&lt;/code&gt; [@github-com-libbpf-libbpf] is the canonical C library for loading and managing eBPF programs. &lt;code&gt;bpftool&lt;/code&gt; [@github-com-libbpf-bpftool] is the inspection utility. &lt;code&gt;BCC&lt;/code&gt; [@github-com-iovisor-bcc] is the older Python-binding toolkit. &lt;code&gt;bpftrace&lt;/code&gt; [@github-com-iovisor-bpftrace] is the DSL inspired by DTrace. &lt;code&gt;cilium/ebpf&lt;/code&gt; [@github-com-cilium-ebpf] is the Go library; &lt;code&gt;aya&lt;/code&gt; [@github-com-rs-aya] and &lt;code&gt;libbpf-rs&lt;/code&gt; [@github-com-libbpf-rs] are the Rust libraries. The toolchain coverage tells you something about the substrate: a Go developer can write an eBPF program and have it loaded by their existing service binary, because the load-verify-attach lifecycle has a Go binding.&lt;/p&gt;
&lt;p&gt;ETW has its own toolchain -- &lt;code&gt;tracerpt.exe&lt;/code&gt;, Windows Performance Analyzer, BenchmarkDotNet, krabsetw -- but the toolchain is shaped around &lt;em&gt;consuming&lt;/em&gt; events, not around emitting programs into the kernel. The asymmetry of the toolchains mirrors the asymmetry of the substrates.&lt;/p&gt;
&lt;h3&gt;The decision guide&lt;/h3&gt;

**Windows EDR or building on Microsoft Defender for Endpoint.** Use ETW plus Sysmon plus the `Microsoft-Windows-Threat-Intelligence` provider. eBPF for Windows is not yet a substitute for Defender-grade kernel telemetry; the hook surface is too narrow.&lt;p&gt;&lt;strong&gt;Linux runtime-security or cluster networking.&lt;/strong&gt; Use eBPF. Pick &lt;code&gt;libbpf&lt;/code&gt; or &lt;code&gt;cilium/ebpf&lt;/code&gt; for the language binding. Attach LSM hooks for enforcement; fentry for observability. The verifier will fight you; that is expected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-platform networking observability with one source surface.&lt;/strong&gt; Use eBPF for Windows and Linux eBPF together, restricted to the XDP, SOCK_ADDR, SOCK_OPS, and BIND hooks. The Linux source compiles unchanged on Windows for this subset.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forensic capture across the full Windows API surface.&lt;/strong&gt; Use ETW into &lt;code&gt;.etl&lt;/code&gt; files, analyzed in Windows Performance Analyzer. Nothing else covers that breadth on Windows.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Sysmon-for-Linux case study is the cleanest practical justification for the abstract-surface convergence. If your SIEM consumes Sysmon XML and matches on Event ID and field, you can run a fleet of Windows hosts on ETW and Linux hosts on eBPF and the SIEM will not know the difference. The substrate is invisible at the consumer&apos;s contract; what matters is that the contract is preserved across the back-end change. This is the production realization of the engineering pattern -- different mechanisms, identical schemas -- that the rest of the article has been describing in architectural terms.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The consumer stack has converged at the surface layer: XML configs, Event IDs, EDR vendor APIs. The substrate has not, and the open problems in the next section are what stands in the way.&lt;/p&gt;
&lt;h2&gt;9. Open Problems and the Frontier&lt;/h2&gt;
&lt;p&gt;What can we not do yet? Four open problems will shape the next five years of kernel observability.&lt;/p&gt;
&lt;h3&gt;9.1 Verifier-driven false rejection&lt;/h3&gt;
&lt;p&gt;Programs that PREVAIL and a human can both prove safe still get rejected by the Linux verifier, which returns the cryptic &lt;em&gt;&quot;verifier complexity limit reached&quot;&lt;/em&gt; error. EDR vendors end up fighting the verifier rather than writing the program they want. The workarounds are real and ugly: &lt;code&gt;__attribute__((noinline))&lt;/code&gt; annotations to force the compiler to emit function boundaries the verifier can prune around, explicit bound assertions that re-derive properties the compiler already knows, &lt;code&gt;bpf_loop()&lt;/code&gt; to externalize loops the verifier cannot trace. The HotOS 2023 thesis is exactly that this is not a bug -- it is a property of any heuristic verifier under the soundness-completeness-scalability triangle. The completeness leg is the one the Linux verifier gives up first, every time.&lt;/p&gt;
&lt;p&gt;The frontier here is twofold. On one side, the verifier is becoming more capable: bounded loops, &lt;code&gt;bpf_for_each_map_elem&lt;/code&gt;, kfuncs, and the trampoline-based attach mechanisms have all expanded what the verifier can prove. On the other side, PREVAIL&apos;s polynomial-time abstract-interpretation approach represents an alternative architectural lineage. Neither approach removes the underlying undecidability. Both make the rejection threshold higher.&lt;/p&gt;
&lt;h3&gt;9.2 Cross-OS eBPF ABI&lt;/h3&gt;
&lt;p&gt;The eBPF Foundation&apos;s RFC 9669 [@rfc-editor-org-rfc-rfc9669html], published as an IETF Independent Submission in October 2024, standardized the &lt;em&gt;instruction set architecture&lt;/em&gt; for BPF programs. The RFC describes the 64-bit ISA, the encoding of instructions, the memory model, and the verifier&apos;s basic obligations. It is the cleanest cross-OS contract eBPF has ever had.&lt;/p&gt;
&lt;p&gt;What the RFC does &lt;em&gt;not&lt;/em&gt; standardize: helpers, map types, and hook semantics. Those remain Linux-defined-in-practice. The eBPF-for-Windows helper set is a subset, with extensions for Windows-specific concepts. The FreeBSD and illumos ports have their own subsets. A single observability agent that runs everywhere needs more than a standardized ISA; it needs a standardized helper API and a standardized hook taxonomy. Today, EDR vendors writing cross-OS agents ship two distinct programs that share a build system and not much else.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; RFC 9669 is the ISA standard. It defines what BPF bytecode looks like and what the verifier must check. It does not define which helpers a program can call, what the map types are, or what hooks the program can attach to. Those are the parts that vary between Linux, Windows, and the BSDs. Standardizing them is more of a committee problem than a research problem -- a meaningful subset is achievable; a full superset probably is not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;9.3 ETW evasion at the trust boundary&lt;/h3&gt;
&lt;p&gt;The user-mode &lt;code&gt;EtwEventWrite&lt;/code&gt; patching attack class is roughly 2020-vintage but has not gone away. The kernel-emitted &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; provider is the current best mitigation: kernel signals cannot be patched from user mode, so an attacker who silences user-mode emissions still trips kernel-only signals on &lt;code&gt;mprotect&lt;/code&gt;, image load, and remote thread creation.&lt;/p&gt;
&lt;p&gt;The deeper structural question is whether any user-mode primitive can ever be tamper-resistant under hostile user-mode code. The short answer is no, which is why the answer keeps moving the trust boundary into the kernel -- through PPL, through LSM, through signed drivers. On Linux, the same pattern shows up: hostile-user-mode-resistant telemetry must run inside the kernel, which is why the LSM hooks are the part of the eBPF hook surface that matters most for EDR.&lt;/p&gt;
&lt;h3&gt;9.4 Hot-path overhead at scale&lt;/h3&gt;
&lt;p&gt;Production environments routinely run Falco, Cilium, and a vendor EDR on the same kernel, each attaching probes to the same hook. The marginal cost of an eBPF kprobe on a five-million-events-per-second syscall is not zero, and the cost compounds non-linearly when three different agents attach to the same hook with three different programs.&lt;/p&gt;
&lt;p&gt;The current partial mitigations are real. &lt;code&gt;fentry&lt;/code&gt;/&lt;code&gt;fexit&lt;/code&gt; plus the BPF trampoline removed the per-attach trap-frame cost. &lt;code&gt;kprobe.multi&lt;/code&gt;, added in Linux 5.18, lets a single program attach to multiple functions with one trampoline. BPF-link iteration lets one agent observe what another has attached. But none of these compose perfectly: three different vendors with three different agents end up with three different trampolines on the same function. The structural fix is &lt;em&gt;trampoline sharing&lt;/em&gt;, and the implementation is attach-type-specific.The multi-agent attach problem is the eBPF version of a familiar systems issue: when N independent consumers each install their own instrumentation at the same point, the cost is N times the cost of one. Linux has solved this once for kprobes (with &lt;code&gt;kprobe.multi&lt;/code&gt;) and is solving it again for the BPF trampoline. Whether the same pattern can be made cheap for fentry attaches across LSM hooks is an open implementation question.&lt;/p&gt;
&lt;p&gt;The frontier of kernel observability is not &quot;build a new substrate.&quot; It is &quot;make the existing substrates compose under multi-tenant production load.&quot;&lt;/p&gt;
&lt;h2&gt;10. Two Generations&lt;/h2&gt;
&lt;p&gt;Return to the SOC analyst from section 1. The Sysmon Operational channel looks the same on both hosts. Now you know why -- and also why the similarity is a deliberate engineering choice rather than a coincidence.&lt;/p&gt;
&lt;p&gt;ETW is mature, has full Windows coverage, is emission-only. It is a &lt;em&gt;catalog&lt;/em&gt; of events. Every Windows subsystem registers a provider, every provider declares a manifest, every event has a stable schema. A consumer that knows the manifest knows what to expect. The trust boundary is the kernel-mode driver signing model. The cost is that aggregation, sampling, and filtering all happen in user space, after the event has crossed the boundary.&lt;/p&gt;
&lt;p&gt;eBPF is programmable, has filter and aggregation in-kernel, has a verifier. It is a &lt;em&gt;language&lt;/em&gt; for asking questions of the kernel, not a catalog of pre-defined answers. The trust boundary is the verifier, which is a research-grade static analyzer running as kernel code. Linux&apos;s verifier shipped four widely-disclosed soundness bugs in four years. PREVAIL trades that soundness leg for a more conservative completeness story. The trade-offs are not finished.&lt;/p&gt;
&lt;p&gt;eBPF-for-Windows is the convergence experiment. The native mode -- PREVAIL plus &lt;code&gt;bpf2c&lt;/code&gt; plus MSVC plus a signed &lt;code&gt;.sys&lt;/code&gt; driver -- is the first cross-OS-portable kernel-observability primitive. As of 2026 it covers a networking subset of hooks, not the full Linux surface. That gap is not architectural; it is a list of hooks Microsoft has not yet exposed. The pattern is generalizable: cross-OS observability lives in the verifier, not in the runtime, and each OS lifts verified bytecode into its own trust model.&lt;/p&gt;
&lt;p&gt;The generation gap is literal. ETW (2000) is an event bus. eBPF (2014) is a programmable kernel substrate. Both will still ship in 2035. Both will still be the right answer for some workloads. The interesting work for the next decade is in the convergence layer -- helper-API standardization, hook-point taxonomy alignment, verifier completeness -- and in the multi-tenant production engineering that makes ten different agents on one kernel cheaper than ten times one agent.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Kernel observability has matured from event emission to programmable kernel computation. That generation gap is why eBPF-for-Windows -- a small, work-in-progress project -- is one of the more architecturally significant operating-system-telemetry events of the last decade. The portable abstraction is not the runtime. It is the static analyzer.&lt;/p&gt;
&lt;/blockquote&gt;

No. As of 2026, eBPF for Windows [@github-com-for-windows] covers a networking-heavy subset of hooks -- XDP, BIND, SOCK_OPS, SOCK_ADDR, and process creation and exit -- and is not yet a substitute for Defender-grade kernel telemetry. ETW remains the canonical Windows observability substrate. The convergence between the two is real for the networking subset, and is the work-in-progress for the rest of the surface.

Because it is a heuristic abstract interpreter on a Turing-complete ISA, and Rice&apos;s theorem says no such verifier can be simultaneously sound, complete, and decidable. Real verifiers ship with all three approximately, and the soundness leg fails first when state pruning loses information at a join point. CVE-2023-2163 [@nvd-nist-gov-2023-2163], CVE-2022-23222 [@nvd-nist-gov-2022-23222], CVE-2021-3490 [@nvd-nist-gov-2021-3490], and CVE-2020-8835 [@nvd-nist-gov-2020-8835] are all instances of that pattern.

For the networking subset (XDP, SOCK_ADDR, SOCK_OPS, BIND), yes -- eBPF for Windows [@github-com-for-windows] is source-compatible with Linux eBPF for those hooks. For arbitrary kprobes or LSM hooks, no -- those hooks are Linux-internal and eBPF for Windows does not expose equivalents. Cross-platform agents typically ship two binaries that share a build system.

Since Linux 5.16 (March 2022) [@kernel-org-bpf-indexhtml], `kernel.unprivileged_bpf_disabled=1` is the kernel default. Production EDRs run with `CAP_BPF` plus `CAP_PERFMON` or root. Leaving unprivileged eBPF enabled was the entry point for several verifier CVEs, so the conservative default is correct.

A kprobe is a runtime breakpoint mechanism: the kernel patches a trap instruction at the target address, and the trap handler invokes the attached eBPF program. fentry uses the BPF trampoline [@lore-kernel-org-1-astkernelorg] -- a small JIT-emitted dispatcher that calls attached BPF programs with a direct call, avoiding the retpoline penalty an indirect dispatch would pay on Spectre-mitigated kernels. Starovoitov&apos;s framing: *&quot;practically zero overhead&quot;* for fentry, relative to the kprobe trap-frame cost.

No. ETW sessions filter by provider, keyword, and level. That is it. Any per-event computation -- counting, sampling, stack-trace folding, downsampling -- runs in user mode on the consumer side, after the event has crossed the kernel-user boundary. The lack of an in-kernel filter language is the structural reason eBPF can do things ETW cannot, like aggregate ten million `vfs_read` calls per second into a histogram without saturating the wire.

Sysmon for Linux [@github-com-microsoft-sysmonforlinux] replaces the ETW back end with eBPF kprobes via Microsoft&apos;s `SysinternalsEBPF` library. The XML configuration schema, Event IDs, and Operational channel output are preserved, so a SIEM consumer sees identical telemetry from either OS. It is the production demonstration that ETW and eBPF can be made surface-equivalent to a consumer.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;ebpf-vs-etw-two-generations-of-kernel-observability&quot; keyTerms={[
  { term: &quot;ETW&quot;, definition: &quot;Event Tracing for Windows. The Windows 2000-onward kernel-mediated event bus, with providers, sessions, consumers, and per-CPU ring buffers.&quot; },
  { term: &quot;eBPF&quot;, definition: &quot;Extended Berkeley Packet Filter. A safe, sandboxed kernel virtual machine introduced in Linux 3.18 (2014) that runs verified user-supplied bytecode at attached hook points.&quot; },
  { term: &quot;Verifier&quot;, definition: &quot;The kernel-side static analyzer that proves termination and memory safety of every eBPF program before load. The Linux verifier uses a heuristic register-state lattice; PREVAIL uses zone-domain abstract interpretation.&quot; },
  { term: &quot;BPF Map&quot;, definition: &quot;A kernel-managed key-value store accessible from inside an eBPF program and from user space. Types include hash, array, per-CPU hash, and ring buffer.&quot; },
  { term: &quot;Ringbuf&quot;, definition: &quot;The BPF ring buffer map type (Linux 5.8). A multi-producer single-consumer transport that preserves cross-CPU event ordering.&quot; },
  { term: &quot;HVCI&quot;, definition: &quot;Hypervisor-enforced Code Integrity. The Windows feature that uses the hypervisor to enforce kernel-mode code signing. Blocks dynamic kernel-mode code generation by default.&quot; },
  { term: &quot;PREVAIL&quot;, definition: &quot;The user-mode eBPF verifier used by eBPF for Windows. Based on numerical abstract interpretation over the zone domain plus intervals, with formal grounding in Gershuni et al. PLDI 2019.&quot; },
  { term: &quot;bpf2c&quot;, definition: &quot;The eBPF-for-Windows transliterator that emits portable C from verified BPF bytecode, one C statement per BPF instruction. The C is compiled by MSVC into a signed .sys driver.&quot; }
]} questions={[
  { q: &quot;Why did performance counters fail for security telemetry?&quot;, a: &quot;Three structural reasons: sampling-rate floor (counters aggregate at the consumer&apos;s query rate, hiding individual events), no event identity (a count tells you N happened, not which user did what), and no causal order (two counters sampled in sequence are not causally ordered with respect to the events they describe).&quot; },
  { q: &quot;What three properties does the soundness-completeness-scalability triangle say a verifier can&apos;t have all of?&quot;, a: &quot;Soundness (never accept an unsafe program), completeness (never reject a safe program), and scalability (run in polynomial time on real programs). Rice&apos;s theorem implies no decision procedure for a non-trivial semantic property on a Turing-complete ISA can have all three. Real verifiers must trade off.&quot; },
  { q: &quot;How does eBPF for Windows lift verified bytecode into the Windows kernel?&quot;, a: &quot;In native mode, PREVAIL verifies the bytecode in user space. On success, the bpf2c tool transliterates each verified BPF instruction to one C statement, MSVC compiles the C to a signed .sys kernel driver, and the kernel loads the driver through the standard Authenticode / HVCI / KMCI signing pipeline.&quot; },
  { q: &quot;Name two structural attack-class implications of bpf_tail_call.&quot;, a: &quot;Branch-target injection on the tail-call dispatcher (an indirect jump from kernel mode selecting its target from a user-controllable map slot is a Spectre-v2 gadget) and speculative type confusion (the verifier proves a single program&apos;s register types, but a tail call&apos;s target is a runtime-resolved map slot, so speculative execution can run a different program under the wrong type-state).&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>ebpf</category><category>etw</category><category>kernel-observability</category><category>edr</category><category>verifier</category><category>windows-internals</category><category>linux-kernel</category><category>tracing</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>From `cmd.exe` to a Kusto Row in 90 Seconds: How Sysmon and Defender for Endpoint Actually Work</title><link>https://paragmali.com/blog/from-cmdexe-to-a-kusto-row-in-90-seconds-how-sysmon-and-defe/</link><guid isPermaLink="true">https://paragmali.com/blog/from-cmdexe-to-a-kusto-row-in-90-seconds-how-sysmon-and-defe/</guid><description>The seven-layer production EDR pipeline -- kernel callback, ETW publisher, MsSense.exe, SenseCncProxy, Kusto, KQL -- traced end to end for Sysmon and Defender for Endpoint.</description><pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate><content:encoded>
Modern Windows EDR is a seven-layer production pipeline. A kernel callback fires, a user-mode aggregator labels the event, an ETW publisher (Sysmon) or a TLS-pinned cloud forwarder (`SenseCncProxy.exe`) ships it, and within seconds the event surfaces as a row in a Kusto table that the analyst queries with KQL. Sysmon (Russinovich and Garnier, August 2014) is the configurable kernel-callback-then-publish reference: twenty-nine event IDs, three canonical configurations (SwiftOnSecurity, the post-rename `NextronSystems/sysmon-config`, and `olafhartong/sysmon-modular`), Antimalware-PPL hardening since v15 in June 2023. Microsoft Defender for Endpoint (Windows Defender ATP preview March 2016, MDE rename September 2020, Microsoft Defender XDR portal late 2023) is the commercial cloud-correlated counterpart: `MsSense.exe` runs as Antimalware-PPL, shares the `WdFilter.sys` / `WdBoot.sys` / `WdNisDrv.sys` Defender Antivirus kernel surface, and lands events in six `Device*` Advanced Hunting tables with 30-day in-portal retention, extended via the Microsoft Sentinel Defender XDR connector. For MDE-licensed shops with a detection-engineering team, the community pattern is Hartong&apos;s `sysmonconfig-mde-augment.xml` -- Sysmon as a complement, not a duplicate. The pipeline&apos;s four structural ceilings (pre-driver-load horizon, observation-vs-enforcement latency, MDE schema truncation, kernel-mode adversary primitive) are documented and unclosed; FalconForce&apos;s 2022 CVE-2022-23278 disclosure and InfoGuard Labs&apos; 2025 certificate-pinning bypass bookend an adversarial arc the field has not yet ended.
&lt;h2&gt;1. From &lt;code&gt;cmd.exe&lt;/code&gt; to a Kusto Row in Ninety Seconds&lt;/h2&gt;
&lt;p&gt;At 9:14 a.m. on a Monday, a SOC analyst named Maya watches a &lt;code&gt;DeviceProcessEvents&lt;/code&gt; row light up in the Advanced Hunting console of Microsoft Defender XDR. The &lt;code&gt;FileName&lt;/code&gt; is &lt;code&gt;powershell.exe&lt;/code&gt;. The &lt;code&gt;ProcessCommandLine&lt;/code&gt; reads &lt;code&gt;powershell.exe -enc JABzAD0A...&lt;/code&gt;. The &lt;code&gt;InitiatingProcessFileName&lt;/code&gt; is &lt;code&gt;WINWORD.EXE&lt;/code&gt;. The &lt;code&gt;Timestamp&lt;/code&gt; is three seconds ago [@deviceprocessevents-table].&lt;/p&gt;
&lt;p&gt;By 9:15:44 Maya has pivoted to &lt;code&gt;DeviceNetworkEvents&lt;/code&gt;, found an outbound connection from the same &lt;code&gt;InitiatingProcessId&lt;/code&gt; to a previously-unknown IP on TCP/443, clicked &lt;strong&gt;Isolate device&lt;/strong&gt; in the device page, and the endpoint is off the network. Ninety seconds, end to end. Email triage of the original message; a quarantine on the inbound &lt;code&gt;.docm&lt;/code&gt;; and -- by the time the user&apos;s coffee has cooled -- a brand-new IOC in the tenant&apos;s custom indicator list.&lt;/p&gt;
&lt;p&gt;This article is the rewind. We walk Maya&apos;s ninety seconds backwards through the seven pipeline layers that made the triage possible -- starting in ring zero, ending in the KQL query you can copy into your own tenant -- and along the way we answer the question every SOC manager has asked at least once: do we deploy Sysmon alongside Defender for Endpoint, or trust Defender alone?&lt;/p&gt;
&lt;h3&gt;The seven layers&lt;/h3&gt;
&lt;p&gt;Maya is looking at a single Kusto row. Behind that row sit seven distinct software components, each of which can fail independently:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;kernel callback&lt;/strong&gt; fired inside the &lt;code&gt;nt!PspInsertProcess&lt;/code&gt; path on the target machine the instant &lt;code&gt;WINWORD.EXE&lt;/code&gt; called &lt;code&gt;CreateProcessW&lt;/code&gt; to spawn &lt;code&gt;powershell.exe&lt;/code&gt;. The callback handler lives inside &lt;code&gt;WdFilter.sys&lt;/code&gt; (Defender Antivirus&apos;s filter driver) and inside &lt;code&gt;SysmonDrv.sys&lt;/code&gt; if Sysmon is also installed [@pssetcreateex-msdn].&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;user-mode aggregator&lt;/strong&gt; -- &lt;code&gt;MsSense.exe&lt;/code&gt; for Defender for Endpoint, or &lt;code&gt;Sysmon.exe&lt;/code&gt; (the service) for Sysmon -- received the structured callback notification, enriched it with parent-process state, file hashes, signature information, and identity data, and decided whether the event was worth shipping [@mde-ms-learn][@sysmon-ms-learn].&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;&lt;a href=&quot;https://paragmali.com/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/&quot; rel=&quot;noopener&quot;&gt;ETW publisher&lt;/a&gt;&lt;/strong&gt; -- in Sysmon&apos;s case the &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt; provider -- emitted the event to the operating system&apos;s tracing bus, and the Sysmon service wrote it to the &lt;code&gt;Microsoft/Windows/Sysmon/Operational&lt;/code&gt; event log [@sysmon-ms-learn].&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;cloud forwarder&lt;/strong&gt; -- &lt;code&gt;SenseCncProxy.exe&lt;/code&gt; -- ran the Defender payload through TLS with certificate pinning out to the regional Defender XDR ingest endpoint [@falconforce-2022].&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;cloud sensor pipeline&lt;/strong&gt; in Microsoft&apos;s regional datacenter (the US for US tenants, the EU for European tenants, the UK for UK tenants) wrote the event into the Advanced Hunting Kusto cluster [@advanced-hunting-overview][@ms-server-endpoints-learn].&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Kusto table&lt;/strong&gt; -- &lt;code&gt;DeviceProcessEvents&lt;/code&gt; -- became queryable within seconds, joined logically across roughly fifty columns to its siblings (&lt;code&gt;DeviceNetworkEvents&lt;/code&gt;, &lt;code&gt;DeviceFileEvents&lt;/code&gt;, &lt;code&gt;DeviceRegistryEvents&lt;/code&gt;, &lt;code&gt;DeviceImageLoadEvents&lt;/code&gt;, &lt;code&gt;DeviceEvents&lt;/code&gt;) [@deviceprocessevents-table].&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;KQL query&lt;/strong&gt; Maya wrote, or one of Microsoft&apos;s built-in detection rules, joined the process row to the network row on &lt;code&gt;(DeviceId, InitiatingProcessId)&lt;/code&gt;, surfaced the C2 callback inside a ninety-second window, and put the device-isolation button on her screen [@advanced-hunting-overview][@sentinel-xdr-connector].&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of these seven layers is independently failure-prone. Operating an EDR well -- which is what this article is about -- means knowing which layer produced which artifact, which layer can be tampered with, and which layer is the right one to fix when the row does not arrive.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Modern Windows EDR is a seven-layer production pipeline: kernel callback, user-mode aggregator, ETW publisher (or cloud forwarder), TLS-pinned cloud transport, regional Kusto ingest, table write, KQL read. Sysmon and Microsoft Defender for Endpoint are two implementations of the same seven layers, with different design philosophies at every layer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Why two products, not one&lt;/h3&gt;
&lt;p&gt;Sysmon and Defender for Endpoint were not designed as a pair. They evolved as competing answers to the same problem -- &lt;em&gt;when prevention fails, what evidence do you give the responder?&lt;/em&gt; -- on the same operating system, with the same kernel-callback APIs underneath, and with the same Windows Event Tracing bus as the transport layer in the middle. They converged on a shared trust model only in 2023, when both products began running as protected processes [@sysmon-ms-learn][@falconforce-2022].&lt;/p&gt;
&lt;p&gt;That convergence is not coincidence. It is the consequence of a decade of architectural pressure pushing both products toward the same answer: collect at the Microsoft-sanctioned kernel-callback boundary, normalize in user mode, ship over a tamper-resistant transport, and surface to the analyst as a queryable column family. The differences are in the configuration grammar, the cloud-side enrichment, and the trust boundary at the publisher edge. The seven layers are the same. To see why, we have to start in 2014, when Sysmon shipped with three event types.&lt;/p&gt;
&lt;h2&gt;2. Twelve Years, Two Arcs, One Convergence&lt;/h2&gt;
&lt;p&gt;Anton Chuvakin, then a research VP at Gartner, named the category in July 2013. His blog post -- preserved on his personal site after Gartner deleted its analyst blogs in late 2023 -- coined the term &lt;em&gt;Endpoint Threat Detection and Response&lt;/em&gt; (ETDR) and defined it as &quot;tools primarily focused on detecting and investigating suspicious activities (and traces of such) other problems on hosts/endpoints&quot; [@chuvakin-2013][@wikipedia-edr]. The &quot;T&quot; dropped out of the acronym within eighteen months and the field has been called EDR ever since.&lt;/p&gt;
&lt;p&gt;Chuvakin&apos;s question -- &lt;em&gt;what evidence do you give the responder when prevention fails?&lt;/em&gt; -- got two different answers from inside Microsoft over the next decade. One was free, configurable, and ran on every Windows machine the operator wanted to run it on. The other was commercial, cloud-correlated, and only worked if you paid for it. Both started in the same place: at the supported kernel-callback boundary that Microsoft had been steadily building out since Windows XP.&lt;/p&gt;
&lt;h3&gt;The Sysmon arc: August 2014 to March 2026&lt;/h3&gt;
&lt;p&gt;Mark Russinovich gave session HTA-T07R at RSA US 2014 -- &lt;em&gt;Malware Hunting with the Sysinternals Tools&lt;/em&gt; -- and the methodology he taught (process-tree pivoting, autoruns enumeration, real-time monitoring of file and registry writes) had a natural conclusion: somebody should ship a Sysinternals tool that did all of that, continuously, into the Windows event log [@russinovich-rsa-2014]. The tool shipped in August 2014, written by Russinovich and Thomas Garnier, also of Microsoft. ZDNet&apos;s contemporaneous coverage captured the introduction: &quot;Sysmon, written by Russinovich and Thomas Garnier, also of Microsoft, is the 73rd tool in the set... Note: For public release, Sysmon has been reset to version 1.00&quot; [@zdnet-sysmon-2014]. The launch SKU had three event types: process create (EID 1), file-create-time change (EID 2), and network connect (EID 3).&lt;/p&gt;
&lt;p&gt;The design philosophy is captured in a single sentence Microsoft Learn still prints on the Sysmon download page -- a sentence whose framing of Sysmon as a publisher that refuses to do detection and refuses to hide is the entire foundation of the SwiftOnSecurity-NextronSystems-Hartong configuration lineage that §5 unpacks; the verbatim quote lands as the §4 PullQuote [@sysmon-ms-learn]. Every detection-engineering corpus in the Windows field -- SwiftOnSecurity&apos;s config, Florian Roth&apos;s fork, Olaf Hartong&apos;s modular system, the SigmaHQ rule base, the Threat Hunter Playbook -- is downstream of that one design choice.&lt;/p&gt;
&lt;p&gt;The version history reads as capability accretion, not architectural change. Sysmon v6 in February 2017 added registry events (EIDs 12-14), process-access (10), file-create (11), pipe events (17-18), file-create-stream-hash (15), and the &lt;em&gt;ServiceConfigurationChange&lt;/em&gt; (16) audit of Sysmon&apos;s own settings [@sysinternals-blog-v6]. (EID 7 ImageLoad arrived earlier, in Sysmon v2.0 -- the §4 catalogue places it correctly.) Sysmon v10 in June 2019 added DNS-query observation via ETW &lt;em&gt;consumption&lt;/em&gt; of &lt;code&gt;Microsoft-Windows-DNS-Client&lt;/code&gt;; the v10 release date is recorded in the community-curated &lt;em&gt;Sysmon Version History&lt;/em&gt; repository, explicitly marked &quot;Outdated&quot; past v11.10 because its maintainer stopped updating it [@sysmon-version-history]. v13 added ClipboardChange and ProcessTampering. v14 in August 2022 added the first &lt;em&gt;preventive&lt;/em&gt; event -- FileBlockExecutable (EID 27) -- making Sysmon something subtly more than a publisher [@diversenok-2022][@hartong-sysmon14-medium].&lt;/p&gt;
&lt;p&gt;The architectural inflection landed in &lt;strong&gt;June 2023 with Sysmon v15&lt;/strong&gt;, when the Sysmon service began running as a protected process. BleepingComputer&apos;s contemporaneous coverage notes that the service ran as &lt;code&gt;PROTECTED_ANTIMALWARE_LIGHT&lt;/code&gt; and the schema bumped to 4.90 with the new &lt;code&gt;FileExecutableDetected&lt;/code&gt; event ID 29 [@bleepingcomputer-sysmon15][@hartong-sysmon15-medium]. The Microsoft Learn page now states the change verbatim: &quot;&lt;em&gt;The service runs as a protected process, thus disallowing a wide range of user mode interactions&lt;/em&gt;&quot; [@sysmon-ms-learn]. The latest published release at the time of writing is &lt;strong&gt;v15.2 on March 26, 2026&lt;/strong&gt; (per the Sysmon download page&apos;s &lt;em&gt;Published&lt;/em&gt; by-line), with twenty-nine event types plus EID 255 (Error) [@sysmon-ms-learn].&lt;/p&gt;
&lt;h3&gt;The MDE arc: March 2016 to late 2023&lt;/h3&gt;
&lt;p&gt;Microsoft announced &lt;strong&gt;Windows Defender Advanced Threat Protection&lt;/strong&gt; in a Windows Experience blog post on March 1, 2016 -- &lt;em&gt;&quot;Today, we announce the next step in our efforts to protect our enterprise customers, with a new service, Windows Defender Advanced Threat Protection&quot;&lt;/em&gt; [@ms-blog-atp-mar2016]. The service was framed as a cloud-correlated detection-and-investigation layer on top of the Windows 10 sensor, &quot;informed by anonymous information from over 1 billion Windows devices&quot; [@ms-blog-atp-mar2016]. The 2016 product was Windows-only, in-portal, and oriented to detection and investigation only.&lt;/p&gt;
&lt;p&gt;The Fall Creators Update in October 2017 broadened the product into prevention: &quot;&lt;em&gt;The Windows Fall Creators Update represents a new chapter in our product evolution as we offer a set of new prevention capabilities designed to stop attacks as they happen and before they have impact. This means that our service will expand beyond detection, investigation, and response, and will now allow companies to use the full power of the Windows security stack for preventative protection&lt;/em&gt;&quot; [@ms-blog-atp-jun2017]. Attack Surface Reduction rules, Exploit Guard, and Application Guard joined the platform. So did the &lt;strong&gt;Advanced Hunting&lt;/strong&gt; query surface in 2018 -- KQL on the same &lt;code&gt;Device*&lt;/code&gt; tables Maya uses in §1.&lt;/p&gt;
&lt;p&gt;The cross-platform reach arrived in March 2019 with macOS support (initially as Microsoft Defender ATP) and was extended to networked Linux and macOS discovery by February 2021 [@securityweek-defender-macos][@bleepingcomputer-defender-linux]. The product was renamed twice. The most-cited rename came at &lt;strong&gt;Microsoft Ignite 2020 on September 22, 2020&lt;/strong&gt;, when the Microsoft Security blog announced the product family rebrand: &quot;&lt;em&gt;Microsoft Defender for Endpoint (previously Microsoft Defender Advanced Threat Protection)&lt;/em&gt;&quot; [@ms-unified-siem-xdr-2020]. The same post renamed Microsoft Threat Protection to Microsoft 365 Defender, O365 ATP to Microsoft Defender for Office 365, and Azure ATP to Microsoft Defender for Identity. The second rename was at &lt;strong&gt;Microsoft Ignite 2023 in November 2023&lt;/strong&gt;, when Microsoft 365 Defender became &lt;strong&gt;Microsoft Defender XDR&lt;/strong&gt;, announced as part of the broader product rebrand at Ignite 2023 [@defender-xdr-ms-learn][@ms-ignite-2023-blog].The Ignite 2023 rebrand did not change the KQL substrate, the &lt;code&gt;Device*&lt;/code&gt; schema, or the Sentinel connector contract. It is a marketing relabel on top of a stable cloud surface. Detection engineering teams kept writing queries against &lt;code&gt;DeviceProcessEvents&lt;/code&gt; exactly as they did the day before the rename.&lt;/p&gt;
&lt;h3&gt;The configuration-lineage arc&lt;/h3&gt;
&lt;p&gt;A third arc ran in parallel with the two product arcs: the community-maintained Sysmon configurations that turned Sysmon from a kernel-callback publisher into a deployment-ready detection sensor.&lt;/p&gt;
&lt;p&gt;The historical root is &lt;strong&gt;SwiftOnSecurity&apos;s &lt;code&gt;sysmon-config&lt;/code&gt; repository&lt;/strong&gt;, created on February 1, 2017 per the GitHub REST API [@github-swiftonsecurity-meta]. The README&apos;s design intent is succinct: &quot;&lt;em&gt;This is a Microsoft Sysinternals Sysmon configuration file template with default high-quality event tracing&lt;/em&gt;&quot; [@github-swiftonsecurity]. The repository remains the most-cited Sysmon-configuration starting point in the SOC industry.&lt;/p&gt;
&lt;p&gt;Florian Roth, working under the handle &lt;code&gt;@Neo23x0&lt;/code&gt;, forked SwiftOnSecurity&apos;s config in January 2018 (the exact creation date is now obscured by a 2021 rename -- see the sidenote below). The fork added blocking-rule support for Sysmon v14, an actively-maintained set of community pull-request merges, and the &lt;em&gt;export-block.xml&lt;/em&gt; variant that ships the v14+ FileBlockExecutable rules. The README states the lineage verbatim: &quot;&lt;em&gt;This is a forked and modified version of @SwiftOnSecurity&apos;s sysmon config. ... We merged most of the 30+ open pull requests&lt;/em&gt;&quot; [@github-neo23x0]. The current maintainer roster lists Florian Roth, Tobias Michalski, Christian Burkard, and Nasreddine Bencherchali.&lt;/p&gt;
&lt;p&gt;Olaf Hartong&apos;s &lt;code&gt;sysmon-modular&lt;/code&gt; was created on January 13, 2018 per the GitHub REST API [@github-hartong-meta]. The repository takes a different design approach: instead of one monolithic XML config, Hartong ships a per-EID-and-per-technique module library that compiles down into one of several pre-generated artifacts -- &lt;code&gt;sysmonconfig.xml&lt;/code&gt; (default), &lt;code&gt;sysmonconfig-with-filedelete.xml&lt;/code&gt; (default plus archive), &lt;code&gt;sysmonconfig-excludes-only.xml&lt;/code&gt; (verbose), &lt;code&gt;sysmonconfig-research.xml&lt;/code&gt; (super-verbose, with the warning &quot;&lt;em&gt;really DO NOT USE IN PRODUCTION!&lt;/em&gt;&quot;), and the load-bearing &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; whose entire design intent is to fill the gaps in Defender for Endpoint&apos;s collection surface [@github-hartong-modular].Olaf Hartong and Henri Hambartsumyan, the two FalconForce researchers who reverse-engineered Defender for Endpoint in 2022 and surfaced CVE-2022-23278, also maintain &lt;code&gt;olafhartong/sysmon-modular&lt;/code&gt;. This is the dual identity that makes the &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; config uniquely informed: the same people who learned where MDE&apos;s collection truncates Sysmon&apos;s manifest also published the config that fills those gaps [@falconforce-2022][@github-hartong-modular].&lt;/p&gt;
&lt;p&gt;The Neo23x0 repository was renamed in 2021. The current &lt;code&gt;https://github.com/Neo23x0/sysmon-config&lt;/code&gt; URL HTTP-301s to &lt;code&gt;https://github.com/NextronSystems/sysmon-config&lt;/code&gt;, and the GitHub REST API returns a &lt;code&gt;created_at&lt;/code&gt; of &lt;code&gt;2021-07-24T06:19:41Z&lt;/code&gt; with a &lt;code&gt;parent&lt;/code&gt; field pointing to &lt;code&gt;SwiftOnSecurity/sysmon-config&lt;/code&gt; [@github-nextronsystems-meta]. The content lineage from SwiftOnSecurity is unchanged; only the organizational owner moved from Florian Roth&apos;s personal handle to his employer Nextron Systems.&lt;/p&gt;
&lt;p&gt;By 2023, then, two product arcs and one configuration arc had converged on the same baseline: kernel callbacks (&lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt;, &lt;code&gt;ObRegisterCallbacks&lt;/code&gt;, &lt;code&gt;CmRegisterCallbackEx&lt;/code&gt;, Filter Manager minifilters) on the input side; an Antimalware-PPL protected service on the host; an ETW or TLS-pinned cloud transport in the middle; and KQL on &lt;code&gt;Device*&lt;/code&gt; tables on the reader side. The convergence was structural, not coincidental. To see why both arcs landed in the same place, we have to start at the kernel-callback boundary -- where Sysmon&apos;s input lives.&lt;/p&gt;
&lt;h2&gt;3. Sysmon Architecture: Kernel Collection, ETW Emission, Event Log Persistence&lt;/h2&gt;
&lt;p&gt;If you have ever read that Sysmon is an &quot;ETW-based event source,&quot; you have read something that is half-true. The half that is right is the &lt;em&gt;output&lt;/em&gt; side: Sysmon publishes its events through an ETW provider called &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt;, and the rest of the system -- including the Windows Event Log service -- subscribes to that provider. The half that is wrong is the &lt;em&gt;input&lt;/em&gt; side. Sysmon does not get most of its raw observations from ETW. It gets them from five kernel-callback families and one Filter Manager minifilter, with two narrow ETW-consumer exceptions (DNS-Client for EID 22; the WMI activity provider for EIDs 19-21).&lt;/p&gt;
&lt;p&gt;This distinction is small enough that most blog posts skip it and big enough that getting it wrong leads to architectural confusion. The split between &lt;em&gt;collection&lt;/em&gt; (how data enters the Sysmon driver) and &lt;em&gt;emission&lt;/em&gt; (how data leaves the Sysmon service) is the first thing to get straight before anything else makes sense.&lt;/p&gt;

The in-kernel, low-overhead, manifest-described tracing infrastructure built into Windows since 2000. Providers publish structured events; controllers start trace sessions and select which providers to enable; consumers receive events live or read them from `.etl` files. Sysmon uses ETW as its *output* bus -- its kernel driver hands events to the user-mode service via a private ETW session -- and as a small input source for the DNS-Client kernel provider (EID 22) and the WMI activity provider (EIDs 19-21).

A Microsoft-sanctioned ring-0 API for observing operating-system events without patching the System Service Descriptor Table. The Windows kernel exposes a small set of named callback APIs -- `PsSetCreateProcessNotifyRoutineEx` for process create and exit, `PsSetLoadImageNotifyRoutine` for image load (with a `SystemModeImage` bit that distinguishes kernel drivers from user-mode DLLs), `PsSetCreateThreadNotifyRoutineEx` for thread creation (with a remote-thread flag), `ObRegisterCallbacks` for handle-rights filtering against `PsProcessType` and `PsThreadType`, `CmRegisterCallbackEx` for registry operations, and the Filter Manager minifilter framework for file-system I/O. A driver registers a function pointer; the kernel invokes it on the corresponding event with the structured context. PatchGuard tolerates kernel callbacks; it does not tolerate SSDT patching [@wikipedia-kpp][@pssetcreateex-msdn][@ms-wdk-kernel-callbacks].

The file-system filter-driver framework (`FltMgr.sys`) that hosts minifilter drivers between the I/O manager and the file-system stack. Each minifilter declares an *altitude* (a 16-bit priority) and receives notifications for pre- and post-operation hooks on file create, file write, set-information, and set-security. Both `SysmonDrv.sys` and `WdFilter.sys` are minifilters; they coexist at different altitudes without colliding [@sysmon-ms-learn].
&lt;h3&gt;Five collection mechanisms, one ETW publisher&lt;/h3&gt;
&lt;p&gt;The Microsoft Learn page for Sysmon enumerates the event IDs and describes them at the &lt;em&gt;what&lt;/em&gt; level; the &lt;em&gt;how&lt;/em&gt; (which kernel API actually produced each event) is documented partly in the API references for each callback API and partly in the source code of Sysmon&apos;s open Linux port, &lt;code&gt;microsoft/SysmonForLinux&lt;/code&gt;, which reuses Sysinternals&apos; shared C++ rule-engine for parsing the same XML schema and translating it onto eBPF instead of kernel callbacks [@github-sysmon-linux][@sysmon-ms-learn]. The Windows port is closed source, but Sysinternals&apos; design has been documented enough -- across the RSA 2014 talk, the Diversenok 2022 reverse-engineering writeup, and the SysmonForLinux source -- that the collection-mechanism inventory is unambiguous.&lt;/p&gt;
&lt;p&gt;The five mechanisms are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;API or framework&lt;/th&gt;
&lt;th&gt;Sysmon EIDs produced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Process-lifetime callback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 (ProcessCreate), 5 (ProcessTerminate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image-load callback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetLoadImageNotifyRoutine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7 (ImageLoad); 6 (DriverLoad, distinguished by the &lt;code&gt;IMAGE_INFO.SystemModeImage&lt;/code&gt; flag on the kernel-mode image)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thread-creation callback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetCreateThreadNotifyRoutineEx&lt;/code&gt; (with the &lt;code&gt;PS_CREATE_THREAD_NOTIFY_FLAG_CREATE_REMOTE&lt;/code&gt; flag in &lt;code&gt;CREATE_THREAD_NOTIFY_INFO&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;8 (CreateRemoteThread)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object Manager callback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ObRegisterCallbacks&lt;/code&gt; against &lt;code&gt;PsProcessType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 (ProcessAccess)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry callback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CmRegisterCallbackEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12 (Registry Object Create/Delete), 13 (Registry Value Set), 14 (Registry Key/Value Rename)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter Manager minifilter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FltRegisterFilter&lt;/code&gt; against &lt;code&gt;FltCreate&lt;/code&gt;/&lt;code&gt;FltClose&lt;/code&gt;/&lt;code&gt;FltSetInformation&lt;/code&gt; -- ordinary file system, &lt;em&gt;and&lt;/em&gt; the Named Pipe File System (NPFS, &lt;code&gt;\Device\NamedPipe&lt;/code&gt;) at a different altitude&lt;/td&gt;
&lt;td&gt;11 (FileCreate), 15 (FileCreateStreamHash), 17 (PipeEvent Created), 18 (PipeEvent Connected), 23 (FileDelete archived), 26 (FileDeleteDetected), 27 (FileBlockExecutable), 28 (FileBlockShredding), 29 (FileExecutableDetected)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The five-mechanism framing collapses thread-creation and &lt;a href=&quot;https://paragmali.com/blog/the-object-manager-namespace/&quot; rel=&quot;noopener&quot;&gt;Object Manager&lt;/a&gt; callbacks into one architectural family (&quot;process and thread observation via Microsoft-sanctioned callbacks&quot;); a stricter count is six (process-lifetime, image-load, thread-creation, object-handle, registry, minifilter). Either count is defensible; what matters is keeping the API attribution honest: &lt;code&gt;PsSetCreateThreadNotifyRoutineEx&lt;/code&gt; is the canonical remote-thread observer, &lt;code&gt;ObRegisterCallbacks(PsProcessType)&lt;/code&gt; is the canonical handle-rights filter, and NPFS minifiltering -- not &lt;code&gt;ObRegisterCallbacks&lt;/code&gt; -- is what observes named-pipe creation and connection.&lt;/p&gt;
&lt;p&gt;The sixth source -- the &lt;strong&gt;ETW consumer&lt;/strong&gt; path -- is special. For DNS queries (EID 22), Sysmon does not register a kernel callback. It subscribes as a consumer of the Microsoft-published &lt;code&gt;Microsoft-Windows-DNS-Client&lt;/code&gt; ETW provider, parses the structured DNS events, and republishes them through its own ETW provider with the Sysmon enrichments applied [@sysmon-version-history]. DNS-Client is the only event Sysmon consumes from a Microsoft-published &lt;em&gt;kernel&lt;/em&gt; ETW provider; the WmiEvent family (EIDs 19-21) is implemented in a similar consumer style against the WMI activity provider&apos;s user-mode tracing surface, which is why the §4 catalogue marks those rows as &quot;WMI ETW provider consumer.&quot; Either way, ETW consumption is the input-side exception, not the rule: five kernel-callback families do the bulk of the work, and ETW is the input only for a small, deliberately-chosen set of events.The Sysmon ETW provider has the GUID &lt;code&gt;{5770385F-C22A-43E0-BF4C-06F5698FFBD9}&lt;/code&gt;. Microsoft Learn does not enumerate this GUID on the Sysmon page; the authoritative on-host discovery command is &lt;code&gt;logman query providers Microsoft-Windows-Sysmon&lt;/code&gt;, which returns the GUID, the keywords mask, and the registered processes. Pavel Yosifovich&apos;s community ETW-provider catalogue &lt;code&gt;EtwExplorer&lt;/code&gt; mirrors the value [@etwexplorer-sysmon-guid], with the on-host &lt;code&gt;logman&lt;/code&gt; command remaining the authority of last resort.&lt;/p&gt;
&lt;h3&gt;The ProcessCreate path, step by step&lt;/h3&gt;
&lt;p&gt;The clearest way to see how the pieces fit is to trace one event. Sysmon&apos;s process-create handling is the most-quoted EID in the manifest -- it is the EID that produces Maya&apos;s row in §1 -- and it follows the canonical kernel-callback pattern that Microsoft codified in &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// Conceptual pseudocode for SysmonDrv&apos;s process-create path.
// Real Sysmon source for Windows is closed; the Linux port is open.
// This is the contract documented in the WDK reference for
// PsSetCreateProcessNotifyRoutineEx.

NTSTATUS SysmonDrvEntry(PDRIVER_OBJECT DriverObject, ...) {
    // 1. Register the create-process callback. PatchGuard tolerates this.
    PsSetCreateProcessNotifyRoutineEx(SysmonProcessCreateCb, FALSE);
    // ... other callbacks registered similarly ...
    return STATUS_SUCCESS;
}

VOID SysmonProcessCreateCb(
    HANDLE  ParentId,
    HANDLE  ProcessId,
    PPS_CREATE_NOTIFY_INFO  CreateInfo  // NULL on process exit
) {
    if (CreateInfo == NULL) {
        // Process exit: emit EID 5 (ProcessTerminate).
        SysmonEmitEventEID5(ProcessId);
        return;
    }
    // Process create. Apply the XML rule engine: does this process
    // match any &amp;lt;Include&amp;gt; rule, after evaluating &amp;lt;Exclude&amp;gt; overrides?
    if (!SysmonRuleMatch(EID_1, CreateInfo)) {
        return;  // Filtered: produce no event.
    }
    // Enrich with parent process, command line, image hash, integrity
    // level, user SID, ProcessGuid, and session identifiers, then ship
    // through the private Microsoft-Windows-Sysmon ETW publisher.
    SysmonEmitEventEID1(CreateInfo);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four properties of the path matter. First, the callback is invoked &lt;strong&gt;synchronously&lt;/strong&gt; on the thread that issued the &lt;code&gt;CreateProcessW&lt;/code&gt; call, before the new process&apos;s first instruction runs; the parent and child PIDs are both known, but the new process has not yet executed any user-mode code. Second, the callback is &lt;strong&gt;rate-limited only by your rule engine&lt;/strong&gt; -- there is no built-in throttle, and a verbose &lt;code&gt;&amp;lt;Include&amp;gt;&lt;/code&gt; rule on a high-process-turnover host can saturate the ETW session. Third, the callback runs at &lt;strong&gt;IRQL = PASSIVE_LEVEL&lt;/strong&gt;, so it can do file I/O (which the driver needs for hashing) but it must do that I/O carefully to avoid deadlock on the very file system it is monitoring. Fourth, the Sysmon service runs as a separate user-mode process; if the service has crashed or been suspended, the driver continues to emit ETW events into a session with no listener and they evaporate.&lt;/p&gt;

Sysmon&apos;s per-process unique identifier, formatted as a 128-bit GUID and recorded as the `ProcessGuid` field on every event that names a process. Unlike a Windows process ID, the ProcessGuid survives PID reuse and uniquely identifies a process across its lifetime [@sysmon-ms-learn]; SOC tooling commonly joins on `(DeviceId, ProcessGuid)` to reconstruct process trees and avoid the PID-reuse race condition that plagues raw `ProcessId` joins.
&lt;h3&gt;Where the events go&lt;/h3&gt;
&lt;p&gt;Once the user-mode &lt;code&gt;Sysmon.exe&lt;/code&gt; service has labelled the event, it does two things. First, it writes the event to the Windows event log -- specifically to &lt;code&gt;Applications and Services Logs/Microsoft/Windows/Sysmon/Operational&lt;/code&gt; per Microsoft Learn&apos;s verbatim statement: &quot;&lt;em&gt;On Vista and higher, events are stored in &lt;code&gt;Applications and Services Logs/Microsoft/Windows/Sysmon/Operational&lt;/code&gt;&lt;/em&gt;&quot; [@sysmon-ms-learn]. Second, the same event is also visible to any ETW real-time consumer subscribed to &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt; -- which is how downstream collectors (Windows Event Forwarding, Splunk&apos;s universal forwarder, the Elastic Endpoint integration, Wazuh&apos;s Windows agent) actually pick the events up, rather than tailing the event log XML.&lt;/p&gt;

flowchart LR
    K1[&quot;PsSetCreateProcessNotifyRoutineEx&quot;] --&amp;gt; D[SysmonDrv.sys]
    K2[&quot;PsSetLoadImageNotifyRoutine&quot;] --&amp;gt; D
    K3[&quot;PsSetCreateThreadNotifyRoutineEx&quot;] --&amp;gt; D
    K4[&quot;ObRegisterCallbacks (PsProcessType)&quot;] --&amp;gt; D
    K5[&quot;CmRegisterCallbackEx&quot;] --&amp;gt; D
    K6[&quot;FltRegisterFilter (file system + NPFS)&quot;] --&amp;gt; D
    K7[&quot;ETW consumer: DNS-Client + WMI activity&quot;] --&amp;gt; D
    D --&amp;gt; P[&quot;ETW publisher: Microsoft-Windows-Sysmon&quot;]
    P --&amp;gt; S[Sysmon.exe service]
    S --&amp;gt; L[&quot;Applications and Services Logs / Microsoft / Windows / Sysmon / Operational&quot;]
    P --&amp;gt; R[&quot;Real-time ETW consumers (WEF, Splunk UF, Wazuh, Elastic)&quot;]
&lt;p&gt;This is the first aha moment. Sysmon is not &quot;ETW based&quot; in the way most blog posts imply. Sysmon is &lt;em&gt;a kernel driver that uses ETW as its IPC bus to user mode&lt;/em&gt;, and as a special-case consumer for one provider (DNS-Client). The reason Sysmon needed a kernel driver in the first place is that ETW alone could not see what the kernel callbacks see: ETW could not, in 2014, deliver a synchronous parent-PID-and-image-hash structure at process create time. Sysmon&apos;s driver does that work; ETW transports the result.&lt;/p&gt;
&lt;p&gt;The protected-process gate added in v15 (June 2023) closed the most-trivial blinding attack -- a SYSTEM-privilege process can no longer issue &lt;code&gt;OpenProcess(PROCESS_TERMINATE)&lt;/code&gt; against the Sysmon service to silence it. Raising the bar to a kernel-mode primitive does not eliminate the attack class, but it does change the cost model. The protected-process gate is the architectural inflection that distinguishes pre-v15 Sysmon (trivially blindable) from post-v15 Sysmon (requires a kernel primitive or a BYOVD chain) [@sysmon-ms-learn][@bleepingcomputer-sysmon15].&lt;/p&gt;
&lt;p&gt;Five collection mechanisms, one ETW publisher, one event log. That is the input side. Now the catalogue.&lt;/p&gt;
&lt;h2&gt;4. The Sysmon Event Catalogue: Twenty-Nine IDs and Their Version Gating&lt;/h2&gt;
&lt;p&gt;Run &lt;code&gt;sysmon -s&lt;/code&gt; on any v15.2 host and you get an XML schema enumerating twenty-nine event types plus EID 255 (Error). Every detection-engineering corpus in the field -- SwiftOnSecurity&apos;s config, Florian Roth&apos;s fork, Hartong&apos;s modular, the SigmaHQ rule base, the Threat Hunter Playbook -- is downstream of this single schema [@sysmon-ms-learn][@github-sigma][@github-otrf-thp]. Learn the catalogue once and the rest of the Sysmon toolchain unfolds from it.&lt;/p&gt;
&lt;p&gt;A naming disambiguation is worth doing first, because the colloquial event names the field uses (and that the topic input for this article uses verbatim) differ from the canonical Microsoft Learn names. &quot;RegistrySet&quot; is a colloquial pun on &lt;code&gt;RegistryEvent (Value Set)&lt;/code&gt;, EID 13. &quot;DnsQuery&quot; is a colloquial shorthand for &lt;code&gt;DNSEvent (DNS query)&lt;/code&gt;, EID 22. &quot;NamedPipeConnect&quot; is two events at once: &lt;code&gt;PipeEvent (Pipe Created)&lt;/code&gt;, EID 17, and &lt;code&gt;PipeEvent (Pipe Connected)&lt;/code&gt;, EID 18. The article uses the canonical Microsoft Learn names from here on.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Sysmon&apos;s manifest names some events as a family with a parenthetical operation: &lt;code&gt;RegistryEvent (Object create and delete)&lt;/code&gt; (EID 12), &lt;code&gt;RegistryEvent (Value Set)&lt;/code&gt; (EID 13), &lt;code&gt;RegistryEvent (Key and Value Rename)&lt;/code&gt; (EID 14). The same pattern applies to the pipe events: &lt;code&gt;PipeEvent (Pipe Created)&lt;/code&gt; (EID 17) and &lt;code&gt;PipeEvent (Pipe Connected)&lt;/code&gt; (EID 18). When detection-rule tooling references &quot;EID 12-14&quot; or &quot;EID 17-18&quot;, these families are what it means. The colloquial single-name forms used elsewhere in the literature are not wrong; they are just less precise. The MDE schema does not preserve the parenthetical operation suffix; it surfaces these as &lt;code&gt;ActionType&lt;/code&gt; values inside &lt;code&gt;DeviceRegistryEvents&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The twenty-nine plus one catalogue&lt;/h3&gt;
&lt;p&gt;The catalogue groups naturally by the collection mechanism that produces each event:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;EID&lt;/th&gt;
&lt;th&gt;Canonical name&lt;/th&gt;
&lt;th&gt;Collection mechanism&lt;/th&gt;
&lt;th&gt;Introduced&lt;/th&gt;
&lt;th&gt;Maps to (MDE)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ProcessCreate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v1.0 (Aug 2014)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceProcessEvents&lt;/code&gt; (&lt;code&gt;ProcessCreated&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;FileCreateTime&lt;/td&gt;
&lt;td&gt;Filter Manager&lt;/td&gt;
&lt;td&gt;v1.0 (Aug 2014)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceFileEvents&lt;/code&gt; (&lt;code&gt;FileCreated&lt;/code&gt;, partial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;NetworkConnect&lt;/td&gt;
&lt;td&gt;Internal network-callout&lt;/td&gt;
&lt;td&gt;v1.0 (Aug 2014)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceNetworkEvents&lt;/code&gt; (&lt;code&gt;ConnectionSuccess&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;ServiceStateChange&lt;/td&gt;
&lt;td&gt;Sysmon-internal&lt;/td&gt;
&lt;td&gt;v1.0 (Aug 2014)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;ProcessTerminate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v1.0 (Aug 2014)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceProcessEvents&lt;/code&gt; (&lt;code&gt;ProcessTerminated&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;DriverLoad&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetLoadImageNotifyRoutine&lt;/code&gt; (kernel-mode case via &lt;code&gt;IMAGE_INFO.SystemModeImage&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;v2.0 (2015)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceEvents&lt;/code&gt; (&lt;code&gt;DriverLoad&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;ImageLoad&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetLoadImageNotifyRoutine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v2.0 (2015)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceImageLoadEvents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;CreateRemoteThread&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PsSetCreateThreadNotifyRoutineEx&lt;/code&gt; (with &lt;code&gt;CREATE_REMOTE&lt;/code&gt; flag)&lt;/td&gt;
&lt;td&gt;v3.0 (2016)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceEvents&lt;/code&gt; (truncated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;RawAccessRead&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\Device\Harddisk*&lt;/code&gt; write filter&lt;/td&gt;
&lt;td&gt;v3.0 (2016)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;ProcessAccess&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ObRegisterCallbacks&lt;/code&gt; (PsProcessType)&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceEvents&lt;/code&gt; (GrantedAccess truncated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;FileCreate&lt;/td&gt;
&lt;td&gt;Filter Manager&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceFileEvents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;RegistryEvent (Object create/delete)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CmRegisterCallbackEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceRegistryEvents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;RegistryEvent (Value Set)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CmRegisterCallbackEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceRegistryEvents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;RegistryEvent (Key/Value Rename)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CmRegisterCallbackEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceRegistryEvents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;FileCreateStreamHash&lt;/td&gt;
&lt;td&gt;Filter Manager&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;ServiceConfigurationChange&lt;/td&gt;
&lt;td&gt;Sysmon-internal&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;PipeEvent (Pipe Created)&lt;/td&gt;
&lt;td&gt;Filter Manager minifilter on NPFS (&lt;code&gt;\Device\NamedPipe&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;PipeEvent (Pipe Connected)&lt;/td&gt;
&lt;td&gt;Filter Manager minifilter on NPFS (&lt;code&gt;\Device\NamedPipe&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;v6.0 (Feb 2017)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;WmiEvent (filter)&lt;/td&gt;
&lt;td&gt;WMI ETW provider consumer&lt;/td&gt;
&lt;td&gt;v6.10 (mid-2017)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;WmiEvent (consumer)&lt;/td&gt;
&lt;td&gt;WMI ETW provider consumer&lt;/td&gt;
&lt;td&gt;v6.10 (mid-2017)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;WmiEvent (consumer-to-filter binding)&lt;/td&gt;
&lt;td&gt;WMI ETW provider consumer&lt;/td&gt;
&lt;td&gt;v6.10 (mid-2017)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;DNSEvent (DNS query)&lt;/td&gt;
&lt;td&gt;ETW consumer of &lt;code&gt;Microsoft-Windows-DNS-Client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v10.0 (Jun 2019)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceNetworkEvents&lt;/code&gt; (&lt;code&gt;DnsQuery&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;FileDelete (archive)&lt;/td&gt;
&lt;td&gt;Filter Manager&lt;/td&gt;
&lt;td&gt;v11.10 (Jun 2020)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceFileEvents&lt;/code&gt; (partial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;ClipboardChange&lt;/td&gt;
&lt;td&gt;RDP and Win32 clipboard hooks&lt;/td&gt;
&lt;td&gt;v13.0 (2021; disputed)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;ProcessTampering&lt;/td&gt;
&lt;td&gt;Image-load and &lt;code&gt;WriteProcessMemory&lt;/code&gt; heuristic&lt;/td&gt;
&lt;td&gt;v13.0 (2021; disputed)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;FileDeleteDetected&lt;/td&gt;
&lt;td&gt;Filter Manager (non-archiving)&lt;/td&gt;
&lt;td&gt;v13.30 (2022)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceFileEvents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;FileBlockExecutable&lt;/td&gt;
&lt;td&gt;Filter Manager (blocking)&lt;/td&gt;
&lt;td&gt;v14.0 (Aug 2022)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;FileBlockShredding&lt;/td&gt;
&lt;td&gt;Filter Manager (blocking)&lt;/td&gt;
&lt;td&gt;v14.10 (2022)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;FileExecutableDetected&lt;/td&gt;
&lt;td&gt;Filter Manager&lt;/td&gt;
&lt;td&gt;v15.0 (Jun 2023)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeviceFileEvents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;td&gt;Sysmon-internal&lt;/td&gt;
&lt;td&gt;v1.0 (Aug 2014)&lt;/td&gt;
&lt;td&gt;(Sysmon-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The Sysmon Version History repository&apos;s &quot;Outdated&quot; disclaimer (&quot;&lt;em&gt;I didn&apos;t find enough time to update this repo - sorry&lt;/em&gt;&quot;) means the v12 vs v13 boundary for ClipboardChange and ProcessTampering is community-disputed. The canonical Microsoft Learn page does not enumerate version-introduction metadata per event ID. The dates in the table for EIDs 24 and 25 are best-effort community attributions and should be treated as approximate until Microsoft publishes a per-EID version history [@sysmon-version-history][@sysmon-ms-learn].&lt;/p&gt;
&lt;h3&gt;The design intent, in one sentence&lt;/h3&gt;
&lt;p&gt;The catalogue exists because Sysmon&apos;s design choice -- the one Microsoft Learn still prints today -- explicitly refuses to do detection. The publisher emits structured events; the detection logic is somebody else&apos;s problem.&lt;/p&gt;

Sysmon does not provide analysis of the events it generates, nor does it attempt to hide itself from attackers.
&lt;p&gt;This is the sentence that explains the entire SwiftOnSecurity-NextronSystems-Hartong configuration lineage [@sysmon-ms-learn]. If Sysmon refuses to do detection, somebody has to write the rules. Three somebodies did, and they wrote three different sets, and the rest of §5 is about the trade-offs between them.&lt;/p&gt;
&lt;h3&gt;What EID 27 is, and what it is not&lt;/h3&gt;
&lt;p&gt;The 2022 introduction of &lt;em&gt;FileBlockExecutable&lt;/em&gt; (EID 27) was the first preventive event in Sysmon&apos;s history. Olaf Hartong&apos;s contemporaneous writeup and Diversenok&apos;s independent reproduction both describe what the event does, and the mechanism is more subtle than &quot;the I/O is denied.&quot; The Sysmon minifilter intercepts the file-handle &lt;em&gt;close&lt;/em&gt; operation. If the rule matches and the file content carries an MZ/PE header, Sysmon logs EID 27 and marks the file for deletion via &lt;code&gt;FILE_DISPOSITION_INFORMATION&lt;/code&gt; [@diversenok-2022][@hartong-sysmon14-medium]. The attacker&apos;s &lt;code&gt;cmd /c copy mimikatz.exe C:\Users\Public\&lt;/code&gt; produces no command-line error. The copy appears to succeed. The file is then deleted at handle-close time. Hartong&apos;s writeup captures the user-visible effect verbatim: &quot;*While there is no error on the command line, the file is not written to disk*&quot; [@hartong-sysmon14-medium]. Diversenok&apos;s reverse-engineering reads: &quot;*Sysmon monitors and deletes files on closing instead of writing*&quot; [@diversenok-2022]. The closing-time semantics is the structural reason Diversenok&apos;s &lt;code&gt;Bypass #1&lt;/code&gt; (split create-close from open-write-close) works at all; the bypass is incoherent under an &lt;code&gt;Access Denied&lt;/code&gt;-at-create model and obvious under the close-time-delete model.&lt;/p&gt;
&lt;p&gt;This is a confined preventive surface, and it should not be confused with the much larger Defender exploit-protection blocking surface. &lt;a href=&quot;https://paragmali.com/blog/process-mitigation-policies-cfg-acg-cig-and-the-layer-betwee/&quot; rel=&quot;noopener&quot;&gt;Defender exploit protection mitigations&lt;/a&gt; include arbitrary-code-guard, control-flow-guard enforcement, and ASR rules -- they sit inside the Defender Antivirus and MDE stacks. EID 27&apos;s blocking is one Sysmon minifilter making a file-create decision; it is not a general-purpose application-allow-list, and it is not a substitute for &lt;a href=&quot;https://paragmali.com/blog/wdac--hvci-code-integrity-at-every-layer-in-windows/&quot; rel=&quot;noopener&quot;&gt;Windows Defender Application Control&lt;/a&gt;. Hartong&apos;s writeup is explicit about the scope -- &quot;&lt;em&gt;the FileBlockExecutable event&lt;/em&gt;&quot; -- as is Diversenok&apos;s: the introduction reads &quot;&lt;em&gt;the update introduced the first preventive measure -- the FileBlockExecutable event (ID 27)&lt;/em&gt;&quot; [@diversenok-2022].&lt;/p&gt;
&lt;p&gt;Twenty-nine events, four hardening releases, one schema. The catalogue is only useful if you configure Sysmon to emit subsets of it, and configuration is where the field&apos;s three lineages diverged.&lt;/p&gt;
&lt;h2&gt;5. Three Canonical Sysmon Configurations&lt;/h2&gt;
&lt;p&gt;Every production Sysmon deployment in the field is forked from one of three repositories. The lineage matters, and one of the things this article fixes is a common attribution error -- &quot;Florian Roth wrote the canonical Sysmon config&quot; is in widespread circulation, but the canonical &lt;em&gt;root&lt;/em&gt; is SwiftOnSecurity&apos;s repository, and Roth&apos;s repo is a 2018 fork of it.&lt;/p&gt;

The open-source generic-signature-format authored by Florian Roth and his collaborators at Nextron Systems; the SIEM-and-EDR field&apos;s vendor-neutral detection-rule lingua franca. The `SigmaHQ/sigma` repository ships over 3,000 detection rules covering the Windows kernel-callback surface (heavily Sysmon-aware), Linux audit, macOS unified log, AWS CloudTrail, Microsoft 365, and other event sources. Sigma rules are written once and compiled by community converters into the per-tool query languages (KQL for Defender XDR / Sentinel, SPL for Splunk, EQL for Elastic) [@github-sigma].
&lt;h3&gt;SwiftOnSecurity/sysmon-config (February 2017)&lt;/h3&gt;
&lt;p&gt;The historical root. The pseudonymous account &lt;em&gt;SwiftOnSecurity&lt;/em&gt; published the first widely-cited Sysmon configuration template on &lt;strong&gt;February 1, 2017&lt;/strong&gt; per the GitHub REST API [@github-swiftonsecurity-meta]. The README&apos;s design intent is the single sentence still printed at the top of the repo: &quot;&lt;em&gt;This is a Microsoft Sysinternals Sysmon configuration file template with default high-quality event tracing&lt;/em&gt;&quot; [@github-swiftonsecurity]. The template emphasises clarity over coverage; the XML is heavily commented, and the rule structure follows a deliberately conservative pattern of &lt;code&gt;&amp;lt;Include&amp;gt;&lt;/code&gt; blocks per technique.&lt;/p&gt;
&lt;p&gt;SwiftOnSecurity&apos;s config is the most-cited starting point for Sysmon deployments worldwide and the one that detection-engineering tutorials default to. It is also the &lt;em&gt;parent&lt;/em&gt; of every other Sysmon-config repository on GitHub, in the literal GitHub-fork sense -- the GitHub REST API for both NextronSystems/sysmon-config and (via the historical fork-graph) other community configs returns &lt;code&gt;SwiftOnSecurity/sysmon-config&lt;/code&gt; as the parent [@github-nextronsystems-meta].&lt;/p&gt;
&lt;h3&gt;Neo23x0/sysmon-config, now NextronSystems/sysmon-config (January 2018, renamed 2021)&lt;/h3&gt;
&lt;p&gt;Florian Roth, working under his GitHub handle &lt;code&gt;@Neo23x0&lt;/code&gt;, forked SwiftOnSecurity&apos;s config in January 2018 and added blocking-rule support for Sysmon v14 plus the merged community pull-request set. The README&apos;s design intent reads: &quot;&lt;em&gt;This is a forked and modified version of @SwiftOnSecurity&apos;s sysmon config. ... We merged most of the 30+ open pull requests&lt;/em&gt;&quot; [@github-neo23x0]. The maintainer roster as of the present writing is Florian Roth (&lt;code&gt;@Neo23x0&lt;/code&gt;), Tobias Michalski (&lt;code&gt;@humpalum&lt;/code&gt;), Christian Burkard (&lt;code&gt;@phantinuss&lt;/code&gt;), and Nasreddine Bencherchali (&lt;code&gt;@nas_bench&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The repository ships a &lt;em&gt;blocking&lt;/em&gt; variant, &lt;code&gt;sysmonconfig-export-block.xml&lt;/code&gt;, that adds &lt;code&gt;&amp;lt;RuleGroup&amp;gt;&lt;/code&gt; blocks targeting EID 27 (FileBlockExecutable) and EID 28 (FileBlockShredding) for the most common malware-staging file paths. This is the variant SOC teams deploy when they want Sysmon&apos;s preventive surface to participate in the response pipeline as a hard block rather than as a detection-only artifact.&lt;/p&gt;

The legacy URL `https://github.com/Neo23x0/sysmon-config` now HTTP-301 redirects to `https://github.com/NextronSystems/sysmon-config`. The GitHub REST API for the current repository returns `created_at: 2021-07-24T06:19:41Z` with `parent: SwiftOnSecurity/sysmon-config`, which means the repository as it now exists was created in mid-2021 when Florian Roth moved it from his personal handle to his employer&apos;s organization namespace [@github-nextronsystems-meta]. The content lineage from SwiftOnSecurity is unchanged; the move is an organizational one. The exact pre-rename creation date of the original `Neo23x0/sysmon-config` repository is not reliably retrievable from the current API and is best dated as January 2018 based on the README and the fork-history.
&lt;h3&gt;olafhartong/sysmon-modular (January 13, 2018)&lt;/h3&gt;
&lt;p&gt;Olaf Hartong&apos;s &lt;code&gt;sysmon-modular&lt;/code&gt; was created on January 13, 2018 per the GitHub REST API [@github-hartong-meta]. The repository&apos;s design takes a different shape from the monolithic SwiftOnSecurity and NextronSystems configs: instead of one carefully-tuned XML, Hartong publishes a per-EID-per-technique module library that compiles into one of five pre-generated artifacts plus an arbitrary number of custom builds [@github-hartong-modular]. The pre-generated variants are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sysmonconfig.xml&lt;/code&gt; -- the default deployment baseline.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sysmonconfig-with-filedelete.xml&lt;/code&gt; -- default plus the EID 23 archive variant of file delete, which preserves the deleted file in &lt;code&gt;C:\Sysmon\&lt;/code&gt; (volume-cost trade-off; recommend dedicated drive).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sysmonconfig-excludes-only.xml&lt;/code&gt; -- the verbose variant, which captures everything except a small set of well-known exclusions; useful for detection-engineering R&amp;amp;D on a single host.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sysmonconfig-research.xml&lt;/code&gt; -- the super-verbose variant, with the README&apos;s standing warning: &quot;&lt;em&gt;really DO NOT USE IN PRODUCTION!&lt;/em&gt;&quot; -- this is for live-malware-sample analysis in a sandbox, not for fleet rollout.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; -- the variant whose entire design intent is to augment Microsoft Defender for Endpoint&apos;s collection surface &quot;&lt;em&gt;to have as little overlap as possible&lt;/em&gt;&quot; with what MDE already captures [@github-hartong-modular].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The MDE-augment config is the artifact this article keeps returning to. It is the operational answer -- maintained by a person, not by Microsoft -- to the question of which Sysmon events are worth collecting on a host that already has MDE installed. We will return to its specific contents in §10. For now, the key observation is that this config exists because of a documented absence: Microsoft has not published a per-&lt;code&gt;ActionType&lt;/code&gt; cross-walk between MDE&apos;s &lt;code&gt;Device*&lt;/code&gt; schema and Sysmon&apos;s manifest, so Hartong reverse-engineered one.&lt;/p&gt;
&lt;h3&gt;Side-by-side comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;SwiftOnSecurity/sysmon-config&lt;/th&gt;
&lt;th&gt;NextronSystems/sysmon-config (formerly Neo23x0)&lt;/th&gt;
&lt;th&gt;olafhartong/sysmon-modular&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Author / org&lt;/td&gt;
&lt;td&gt;SwiftOnSecurity (pseudonymous)&lt;/td&gt;
&lt;td&gt;Florian Roth + Nextron Systems team&lt;/td&gt;
&lt;td&gt;Olaf Hartong (and FalconForce collaborators)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Created&lt;/td&gt;
&lt;td&gt;Feb 1, 2017&lt;/td&gt;
&lt;td&gt;Forked Jan 2018; renamed Jul 24, 2021&lt;/td&gt;
&lt;td&gt;Jan 13, 2018&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution&lt;/td&gt;
&lt;td&gt;One monolithic XML&lt;/td&gt;
&lt;td&gt;Two XMLs (audit + blocking)&lt;/td&gt;
&lt;td&gt;Modular per-technique + five pre-generated builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design philosophy&lt;/td&gt;
&lt;td&gt;Quality starting point, conservative&lt;/td&gt;
&lt;td&gt;Community-maintained, blocking-aware&lt;/td&gt;
&lt;td&gt;Tunable modular, MITRE ATT&amp;amp;CK-mapped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best used for&lt;/td&gt;
&lt;td&gt;First-time Sysmon deployment&lt;/td&gt;
&lt;td&gt;Standalone Sysmon at scale&lt;/td&gt;
&lt;td&gt;Sysmon alongside MDE, or per-team customization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-generated v14+ blocking&lt;/td&gt;
&lt;td&gt;No (audit only)&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;sysmonconfig-export-block.xml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes (built from blocking modules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MDE coexistence variant&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Choosing among the three&lt;/h3&gt;
&lt;p&gt;The detection-engineering trade-off framing is short. Pick SwiftOnSecurity when you want a clean, well-commented starting point and you are not yet sure which events you actually need. Pick NextronSystems when you want a community-maintained baseline that already has the blocking rules for Sysmon v14+. Pick Hartong when you want fine-grained per-technique tunability or, more commonly, when you are running MDE and need Sysmon to augment rather than duplicate it.&lt;/p&gt;
&lt;p&gt;Tactical caution worth one inline note: Sysmon supports &lt;strong&gt;one active configuration at a time&lt;/strong&gt;. There is no aggregate-multiple-XMLs feature at the driver layer. Hartong&apos;s modular approach generates a single merged XML at build time; the production fleet receives that single XML and the driver enforces it. If you are trying to run two configurations side by side -- one for the SOC&apos;s hunting, one for the platform team&apos;s audit -- pick one, merge the rules, and ship the combined product. The deployment tooling in &lt;code&gt;sysmon-modular&lt;/code&gt; is built around exactly this constraint.&lt;/p&gt;
&lt;p&gt;All three configurations assume the same thing: either Sysmon is the only EDR on the host (a deployment posture that exists in air-gapped, regulatory-no-cloud, or unlicensed environments) or it is augmenting an EDR whose collection surface is known. The augment case is the one where the field has converged on Hartong. To understand why, we have to look at what the &lt;em&gt;other&lt;/em&gt; EDR -- Microsoft&apos;s own -- actually collects on the host.&lt;/p&gt;
&lt;h2&gt;6. Microsoft Defender for Endpoint: The Documented On-Host Surface&lt;/h2&gt;
&lt;p&gt;Two questions about MDE have very different answers. &lt;em&gt;What does Microsoft Defender for Endpoint run on this host?&lt;/em&gt; has a primary-source-quality answer from Microsoft Learn. &lt;em&gt;What does it actually do?&lt;/em&gt; has only a community-observed answer. The documented surface is the user-mode component inventory plus registry hives and event sources. The community-observed surface includes the kernel-callback inventory, the cloud TLS-pinning details, and the inter-process communication paths -- none of which Microsoft has published. Naming both halves with the right citations on each side is one of the few things this article does that other writeups skip.&lt;/p&gt;
&lt;h3&gt;The documented surface (Microsoft Learn, primary)&lt;/h3&gt;
&lt;p&gt;On every onboarded Windows endpoint, Microsoft Defender for Endpoint installs and runs a Windows service named &lt;strong&gt;&lt;code&gt;Sense&lt;/code&gt;&lt;/strong&gt;, whose display name is &quot;Microsoft Defender for Endpoint Service&quot; and whose backing executable is &lt;code&gt;MsSense.exe&lt;/code&gt;. The on-host troubleshooting page documents the canonical health-check command: &lt;code&gt;sc query sense&lt;/code&gt; [@sense-troubleshoot]. On Windows Server 2019, Server 2022, Server 2025, and Azure Stack HCI 23H2 or later, MDE is delivered as a &lt;em&gt;Feature on Demand&lt;/em&gt; with the capability name &lt;code&gt;Microsoft.Windows.Sense.Client~~~~&lt;/code&gt;. Microsoft documents the verification command verbatim: &quot;&lt;em&gt;DISM.EXE /Online /Get-CapabilityInfo /CapabilityName:Microsoft.Windows.Sense.Client~~~~&lt;/em&gt;&quot; [@sense-troubleshoot][@ms-server-endpoints-learn].&lt;/p&gt;
&lt;p&gt;Onboarding state is recorded under two registry hives that Microsoft Learn names explicitly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HKLM\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection&lt;/code&gt; -- the policy-driven configuration surface.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HKLM\SOFTWARE\Microsoft\Windows Advanced Threat Protection\Status&lt;/code&gt; -- the run-time onboarding state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Onboarding diagnostics land in the &lt;strong&gt;&lt;code&gt;WDATPOnboarding&lt;/code&gt;&lt;/strong&gt; event source under the Application event log, with documented event IDs 5, 10, 15, 30, 35, 40, 65, and 70, each of which corresponds to a specific failure mode with a specific resolution procedure [@sense-troubleshoot]. The product installs to &lt;code&gt;C:\Program Files\Windows Defender Advanced Threat Protection\&lt;/code&gt; (the legacy path is preserved even after the September 2020 rebrand).&lt;/p&gt;
&lt;p&gt;The documented surface stops here. Microsoft Learn names &lt;code&gt;MsSense.exe&lt;/code&gt;, the &lt;code&gt;Sense&lt;/code&gt; service, the registry hives, the event source, the Feature on Demand, and the four operating systems. Microsoft Learn does &lt;em&gt;not&lt;/em&gt; publish a kernel-callback inventory for the MDE EDR sensor.&lt;/p&gt;
&lt;h3&gt;The community-observed surface&lt;/h3&gt;
&lt;p&gt;Past the documented boundary, what is in field-published primary sources is the user-mode binary inventory and the cloud-side TLS path. Three companion binaries sit alongside &lt;code&gt;MsSense.exe&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SenseCncProxy.exe&lt;/code&gt;&lt;/strong&gt; is the cloud-command-and-control proxy. This is the binary that holds the TLS connection out to Defender XDR ingest, applies the certificate-pinning policy, and shuttles agent-bound commands (live-response actions, custom-detection-rule pushes, sensor-configuration updates) back down to &lt;code&gt;MsSense.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SenseIR.exe&lt;/code&gt;&lt;/strong&gt; is the live-response and investigation actions binary. When a SOC analyst clicks &lt;strong&gt;Run script&lt;/strong&gt; or &lt;strong&gt;Collect investigation package&lt;/strong&gt; in the Defender XDR portal, &lt;code&gt;SenseIR.exe&lt;/code&gt; is the process that fulfils the request on the endpoint side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SenseNdr.exe&lt;/code&gt;&lt;/strong&gt; is the network detection and response component, responsible for endpoint-side enrichment of network observations used in the &lt;code&gt;DeviceNetworkEvents&lt;/code&gt; table.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These binaries are not enumerated on Microsoft Learn in the same way the &lt;code&gt;Sense&lt;/code&gt; service itself is. They are documented in MDE incident-response runbooks, in third-party reverse-engineering posts, and in the file-system signature data on any onboarded endpoint. The article treats their existence as community-observed. &lt;code&gt;SenseIR.exe&lt;/code&gt; is corroborated by InfoGuard 2025&apos;s reverse-engineering of MDE&apos;s live-response cloud path [@infoguard-2025]; &lt;code&gt;SenseNdr.exe&lt;/code&gt; in particular lacks an explicit community primary writeup as of 2026 -- its role here is inferred from its on-disk binary metadata and the file-system signature data on onboarded endpoints.&lt;/p&gt;
&lt;p&gt;The kernel-side surface MDE shares with Defender Antivirus &lt;em&gt;is&lt;/em&gt; documented in the Defender Antivirus product line [@ms-defender-av-arch]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WdBoot.sys&lt;/code&gt;&lt;/strong&gt; is the &lt;strong&gt;Early-Launch Antimalware (ELAM)&lt;/strong&gt; driver. It is the first non-Windows driver to load at boot and gates which non-ELAM drivers are allowed to load after it. It is signed with the &lt;strong&gt;Antimalware Extended Key Usage&lt;/strong&gt;, &lt;code&gt;1.3.6.1.4.1.311.61.4.1&lt;/code&gt; [@ms-learn-elam-sample].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WdFilter.sys&lt;/code&gt;&lt;/strong&gt; is the Defender Antivirus file-system minifilter. It sits alongside &lt;code&gt;SysmonDrv.sys&lt;/code&gt; at a different Filter Manager altitude.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WdNisDrv.sys&lt;/code&gt;&lt;/strong&gt; is the Network Inspection System driver, which provides the host-firewall-augmenting NIS layer.&lt;/li&gt;
&lt;/ul&gt;

A Windows process-protection level, introduced in Vista (as Protected Process, for DRM) and extended in Windows 8.1 (for antimalware), that prevents user-mode debugger attach, code injection, and `OpenProcess` for write from any caller that does not itself run at an equal or higher PPL signer level. Antimalware-PPL (`PROTECTED_ANTIMALWARE_LIGHT`) is the level reserved for security products signed with the Antimalware EKU; `MsSense.exe` and Sysmon v15+ both run at this level.

The Windows boot-order privilege that lets a driver signed with the Antimalware EKU `1.3.6.1.4.1.311.61.4.1` [@ms-learn-elam-sample] load before any non-ELAM driver and classify subsequent boot-start drivers as `Good`, `Bad`, or `Unknown` so the kernel can decide which to load. The ELAM driver *itself* is measured (along with the bootloader, kernel, and other early-boot artefacts) into TPM PCRs by Windows&apos;s *Measured Boot*, which is a separate boot-integrity feature; ELAM&apos;s job is to classify, not to measure. Defender Antivirus&apos;s `WdBoot.sys` is the canonical ELAM driver. Sysmon&apos;s `SysmonDrv.sys` is *not* ELAM-signed; this is the pre-driver-load horizon discussed in §12.

The Authenticode Extended Key Usage `1.3.6.1.4.1.311.61.4.1` [@ms-learn-elam-sample], issued by Microsoft to security vendors after a code-signing and behavioral review. The EKU gates two distinct things: ELAM signing eligibility (so the driver loads first) and Antimalware-PPL eligibility for the user-mode service (so the service is harder to tamper with). MDE&apos;s `MsSense.exe`, Defender Antivirus&apos;s `MsMpEng.exe`, and Sysmon v15+ all carry this signature path.
&lt;h3&gt;Antimalware-PPL on MsSense.exe&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;MsSense.exe&lt;/code&gt; service runs as &lt;strong&gt;&lt;a href=&quot;https://paragmali.com/blog/protected-process-light-when-the-administrator-isnt-enough/&quot; rel=&quot;noopener&quot;&gt;Antimalware-PPL&lt;/a&gt;&lt;/strong&gt; -- &lt;code&gt;PROTECTED_ANTIMALWARE_LIGHT&lt;/code&gt; in the kernel data structure. The protection level prevents an attacker with SYSTEM privileges from attaching a user-mode debugger, suspending the service, or injecting code into its address space using ordinary Windows debugging or code-injection APIs. This is the same protection level Sysmon v15+ runs at, and it is the same level Defender Antivirus&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; has run at since Windows 8.1. The structural defense closes user-mode tampering as a class. The residual attack surface is kernel-mode primitives -- which is what FalconForce had to use in 2022 to debug MDE [@falconforce-2022].&lt;/p&gt;
&lt;h3&gt;The dispositive reverse-engineering primary: FalconForce 2022&lt;/h3&gt;
&lt;p&gt;Olaf Hartong and Henri Hambartsumyan, working at FalconForce, published the most-cited reverse-engineering writeup of MDE&apos;s on-host architecture in 2022. The post&apos;s TL;DR captures both the debug-bypass technique and the cloud vulnerability that resulted from applying it:&lt;/p&gt;

You can debug MDE running on an endpoint by running `dbgsrv.exe` and raising its PPL protection to WinTcb. This can be used to snoop on data being transmitted by MDE to the cloud. We identified a vulnerability related to missing authorization checks of data sent from the MDE endpoint to the M365 cloud, allowing anyone to send spoofed data to any M365 tenant.
&lt;p&gt;The technique is precise [@falconforce-2022]. FalconForce raised the PPL signer level of Windows&apos;s PE debug server (&lt;code&gt;dbgsrv.exe&lt;/code&gt;) to &lt;code&gt;WinTcb&lt;/code&gt; -- a signer level &lt;em&gt;higher&lt;/em&gt; than Antimalware-PPL -- and used the elevated debug server to attach to &lt;code&gt;MsSense.exe&lt;/code&gt;. From inside that debug session they instrumented &lt;code&gt;SspiCli!EncryptMessage&lt;/code&gt;, the SSPI function MDE&apos;s cloud transport uses to wrap each outbound message before TLS encryption, and captured the plaintext payloads. The plaintext capture surfaced &lt;strong&gt;CVE-2022-23278&lt;/strong&gt;: a missing-authorization vulnerability in which the M365 cloud trusted whatever device-identifying claims the endpoint asserted, with no cross-check that the asserting endpoint owned the device identity it claimed [@msrc-cve-2022-23278][@nvd-cve-2022-23278]. Microsoft patched the vulnerability on March 8, 2022, with a public acknowledgement to FalconForce: &quot;&lt;em&gt;Microsoft released a security update to address CVE-2022-23278 in Microsoft Defender for Endpoint. This important class spoofing vulnerability impacts all platforms. We wish to thank Falcon Force for the collaboration on addressing this issue through coordinated vulnerability disclosure&lt;/em&gt;&quot; [@msrc-cve-2022-23278].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The kernel-and-Defender-Antivirus surface MDE shares (&lt;code&gt;WdBoot.sys&lt;/code&gt; ELAM, &lt;code&gt;WdFilter.sys&lt;/code&gt; minifilter, &lt;code&gt;WdNisDrv.sys&lt;/code&gt; NIS) is documented. The specific callback inventory the MDE EDR sensor itself registers is not. The community&apos;s best-published primary for what &lt;code&gt;MsSense.exe&lt;/code&gt; actually does is the FalconForce 2022 reverse-engineering writeup -- and it covers a narrow slice (TLS interception and one cloud-authorization bug), not a full callback list. The Hartong &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; config exists &lt;em&gt;as a community-curated artifact&lt;/em&gt; precisely because Microsoft has not published a per-&lt;code&gt;ActionType&lt;/code&gt;-to-per-kernel-callback cross-walk. The most-cited operational config in the field is downstream of a documentation gap. This is the second aha moment of the article.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Putting the on-host pieces together&lt;/h3&gt;

flowchart TD
    B[&quot;WdBoot.sys (ELAM, Antimalware EKU)&quot;] -.boot order.-&amp;gt; F[&quot;WdFilter.sys (file minifilter)&quot;]
    B -.boot order.-&amp;gt; N[&quot;WdNisDrv.sys (Network Inspection)&quot;]
    F --&amp;gt; M[&quot;MsSense.exe (Antimalware-PPL aggregator)&quot;]
    N --&amp;gt; M
    M --&amp;gt; IR[&quot;SenseIR.exe (Live Response)&quot;]
    M --&amp;gt; NDR[&quot;SenseNdr.exe (Network Detection)&quot;]
    M --&amp;gt; P[&quot;SenseCncProxy.exe (cloud forwarder)&quot;]
    P -- &quot;TLS + certificate pinning&quot; --&amp;gt; C[&quot;Defender XDR ingest (regional Kusto)&quot;]
&lt;p&gt;The picture is asymmetric: the kernel-driver substrate at the top is documented in the Defender Antivirus product line; the user-mode service inventory in the middle is documented for &lt;code&gt;MsSense.exe&lt;/code&gt; and partly documented for the companion binaries; the cloud transport at the bottom is documented at the API-contract level (TLS, certificate pinning) but the specific endpoints and the on-the-wire payload format are reverse-engineered. The community published primaries -- FalconForce 2022 above the line, InfoGuard Labs 2025 below it -- are how the field knows what they know about the cloud-bound payload. Which is the next layer.&lt;/p&gt;
&lt;h2&gt;7. The Cloud Pipeline: SenseCncProxy.exe to Defender XDR Ingest&lt;/h2&gt;
&lt;p&gt;The wire between &lt;code&gt;MsSense.exe&lt;/code&gt; and Microsoft&apos;s cloud is TLS with certificate pinning. It is also, twice in the last four years, the place where the most interesting Defender for Endpoint vulnerabilities have lived. The 2022 round closed one of them. The 2025 round is still open as of this article&apos;s writing.&lt;/p&gt;
&lt;h3&gt;Certificate pinning and the FalconForce 2022 method&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MsSense.exe&lt;/code&gt; does not trust whatever the Windows certificate store says about the chain to Defender XDR ingest. It pins the certificate. FalconForce&apos;s bypass is the one §6 already named: raise &lt;code&gt;dbgsrv.exe&lt;/code&gt; to &lt;code&gt;WinTcb&lt;/code&gt; PPL, attach the elevated debug server to &lt;code&gt;MsSense.exe&lt;/code&gt;, instrument &lt;code&gt;SspiCli!EncryptMessage&lt;/code&gt; to capture the plaintext payload &lt;em&gt;before&lt;/em&gt; TLS encryption [@falconforce-2022].The specific PPL elevation technique is published in the same writeup. PPLKiller&apos;s &lt;code&gt;/enablePPL&lt;/code&gt; patch writes the Antimalware-PPL bit into &lt;code&gt;dbgsrv.exe&lt;/code&gt;&apos;s &lt;code&gt;_EPROCESS.Protection&lt;/code&gt; field at the highest signer level (&lt;code&gt;WinTcb&lt;/code&gt;). The result: a PE debug server running at a PPL level &lt;em&gt;above&lt;/em&gt; Antimalware-PPL, with &lt;code&gt;OpenProcess&lt;/code&gt; rights against any Antimalware-PPL target [@falconforce-2022]. This requires SYSTEM plus a kernel primitive, typically delivered via BYOVD.&lt;/p&gt;
&lt;p&gt;The InfoGuard Labs 2025 follow-up took a different route to the same problem. Instead of reading plaintext before TLS encryption, InfoGuard &lt;em&gt;patches&lt;/em&gt; the certificate-chain validation function in memory so the endpoint certificate is no longer checked at all. Any local TLS-stripping proxy can then intercept the wire. The verbatim patch is two CPU instructions written into &lt;code&gt;CRYPT32!CertVerifyCertificateChainPolicy&lt;/code&gt;: &quot;&lt;code&gt;mov eax, 1; ret&lt;/code&gt;&quot; -- which forces the function to return success without performing any actual chain check [@infoguard-2025].&lt;/p&gt;
&lt;p&gt;With the pinning gate disabled, InfoGuard&apos;s team observed the on-the-wire protocol. The cloud-bound payload goes to two endpoint families: &lt;code&gt;/edr/commands/cnc&lt;/code&gt; for command-and-control and &lt;code&gt;/senseir/v1/actions/&lt;/code&gt; for live-response actions. The vulnerability they then disclosed is that both endpoint families accept &quot;data sent from the MDE endpoint to the cloud ... without validating authentication tokens, allowing a post-breach attacker with a machine&apos;s ID to hijack the command-and-control channel&quot; [@infoguard-2025]. Microsoft&apos;s response, verbatim: &quot;&lt;em&gt;All findings were reported to the Microsoft Security Response Center (MSRC) in July 2025. However, Microsoft has classified them as low severity and has not committed to a fix&lt;/em&gt;&quot; [@infoguard-2025].&lt;/p&gt;

FalconForce 2022 found a missing-authorization bug in the cloud&apos;s trust path. CVE-2022-23278 was patched. InfoGuard Labs 2025 found a different missing-authorization pattern in different cloud endpoints -- different bug, same class -- and the disclosure record says Microsoft has not committed to a fix. The cloud trusts whatever the endpoint claims about itself far enough that the same authorization gap keeps surfacing. The arc that began with the March 2022 spoofing-CVE patch is not closed. This is the third aha moment of the article, surfaced again in §11.
&lt;h3&gt;What the cloud does on arrival&lt;/h3&gt;
&lt;p&gt;Once &lt;code&gt;SenseCncProxy.exe&lt;/code&gt; has TLS-shipped the event over the wire to the regional Defender XDR ingest endpoint, two things happen on the cloud side. First, the event lands in the Advanced Hunting Kusto cluster. Microsoft Learn&apos;s verbatim freshness claim is: &quot;&lt;em&gt;Advanced hunting receives this data almost immediately after the sensors that collect them successfully transmit it to the corresponding cloud services&lt;/em&gt;&quot; [@advanced-hunting-overview]. &quot;Almost immediately&quot; is empirically a few seconds in steady state, which is exactly what Maya saw in §1: a row with &lt;code&gt;Timestamp&lt;/code&gt; three seconds in the past.&lt;/p&gt;
&lt;p&gt;Second, the event is replicated for use by Microsoft&apos;s built-in detection rules, MITRE-mapped queries, and the cross-domain correlation surface that joins endpoint events to email events, identity events, and cloud-application events. The cross-domain join is one of the most-cited reasons enterprises stay on the licensed product rather than fall back to standalone Sysmon: KQL can join &lt;code&gt;DeviceProcessEvents&lt;/code&gt; to &lt;code&gt;EmailEvents&lt;/code&gt; to &lt;code&gt;IdentityLogonEvents&lt;/code&gt; in one query, and Sysmon-only deployments cannot do that without a separate SIEM doing the cross-source enrichment.&lt;/p&gt;
&lt;p&gt;Data residency is documented at the regional level in the MDE configure-server-endpoints page: &quot;&lt;em&gt;data is stored in the US for customers in the USA; in EU for European customers; and in the UK for customers in the United Kingdom&lt;/em&gt;&quot; [@ms-server-endpoints-learn]. Retention in-portal is the same quota for all geographies: &quot;&lt;em&gt;Advanced hunting is a query-based threat hunting tool that you use to explore up to 30 days of raw data&lt;/em&gt;&quot; [@advanced-hunting-overview]. Past 30 days, the customer has to extend the retention surface via Microsoft Sentinel&apos;s per-table archiving, which is the operational story §9 picks up.&lt;/p&gt;
&lt;h3&gt;The event&apos;s journey, end to end&lt;/h3&gt;

sequenceDiagram
    participant K as Kernel callback (WdFilter or SysmonDrv)
    participant S as MsSense.exe (Antimalware-PPL)
    participant P as SenseCncProxy.exe
    participant CP as CRYPT32!CertVerifyCertificateChainPolicy
    participant C as Defender XDR ingest (regional Kusto)
    participant Q as DeviceProcessEvents table
    K-&amp;gt;&amp;gt;S: Synchronous callback notification
    Note over S: Enrich (parent PID, hashes, identity, ProcessGuid)
    S-&amp;gt;&amp;gt;S: SspiCli!EncryptMessage (FalconForce 2022 plaintext capture point)
    S-&amp;gt;&amp;gt;P: IPC to cloud forwarder
    P-&amp;gt;&amp;gt;CP: Validate Defender XDR certificate chain
    CP--&amp;gt;&amp;gt;P: Pinned chain OK (InfoGuard 2025 bypass: patch CP to return 0 unconditionally)
    P-&amp;gt;&amp;gt;C: HTTPS POST /edr/commands/cnc or /senseir/v1/actions/
    C-&amp;gt;&amp;gt;Q: Write into Kusto cluster
    Note over Q: &quot;Almost immediately&quot; -- seconds end to end
    Q--&amp;gt;&amp;gt;K: Queryable via KQL
&lt;p&gt;The diagram is annotated with the two community-disclosed interception points because they are the two places the field has actually been able to observe what is on the wire. Between &lt;code&gt;SspiCli!EncryptMessage&lt;/code&gt; (where the plaintext payload exists) and &lt;code&gt;CRYPT32!CertVerifyCertificateChainPolicy&lt;/code&gt; (where the certificate chain gets validated), the path is otherwise opaque to external researchers. The Microsoft-published side of the story is the contractual one: TLS, certificate pinning, regional ingest, Kusto cluster, KQL exposure. The reverse-engineered side fills in the rest.&lt;/p&gt;
&lt;p&gt;Within seconds, the event appears as a row in &lt;code&gt;DeviceProcessEvents&lt;/code&gt;. The reader-side schema is where the analyst lives. So: what columns?&lt;/p&gt;
&lt;h2&gt;8. Six &lt;code&gt;Device*&lt;/code&gt; Tables and One Worked KQL Query&lt;/h2&gt;
&lt;p&gt;Every detection rule in Microsoft Defender XDR, every hunting query in Microsoft Sentinel, and every analyst pivot Maya does on her console is a KQL query against six load-bearing tables. Knowing those six tables is the price of admission to the Defender XDR field.&lt;/p&gt;

Microsoft&apos;s data-explorer query language, originally built for Azure Data Explorer (formerly Kusto). KQL reads as a pipeline of operators -- `where`, `project`, `summarize`, `join`, `order by` -- left to right. Advanced Hunting in Microsoft Defender XDR and analytics queries in Microsoft Sentinel both expose the same KQL dialect; the same query text can be moved between the two surfaces with only the table-name namespace changing [@advanced-hunting-overview][@sentinel-xdr-connector].
&lt;h3&gt;The six tables&lt;/h3&gt;
&lt;p&gt;The six tables that this article calls &quot;load-bearing&quot; are the ones that map most cleanly to Sysmon&apos;s manifest and that detection rules join against most often:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DeviceProcessEvents&lt;/code&gt;&lt;/strong&gt; -- the canonical reader-side analogue of Sysmon&apos;s EID 1 (ProcessCreate) and EID 5 (ProcessTerminate). The schema reference page names roughly fifty columns including &lt;code&gt;Timestamp&lt;/code&gt;, &lt;code&gt;DeviceId&lt;/code&gt;, &lt;code&gt;DeviceName&lt;/code&gt;, &lt;code&gt;ActionType&lt;/code&gt;, &lt;code&gt;FileName&lt;/code&gt;, &lt;code&gt;FolderPath&lt;/code&gt;, &lt;code&gt;SHA1&lt;/code&gt;, &lt;code&gt;SHA256&lt;/code&gt;, &lt;code&gt;MD5&lt;/code&gt;, &lt;code&gt;FileSize&lt;/code&gt;, &lt;code&gt;ProcessId&lt;/code&gt;, &lt;code&gt;ProcessCommandLine&lt;/code&gt;, &lt;code&gt;ProcessIntegrityLevel&lt;/code&gt;, &lt;code&gt;ProcessTokenElevation&lt;/code&gt;, &lt;code&gt;ProcessCreationTime&lt;/code&gt;, &lt;code&gt;AccountSid&lt;/code&gt;, &lt;code&gt;AccountName&lt;/code&gt;, &lt;code&gt;AccountUpn&lt;/code&gt;, &lt;code&gt;LogonId&lt;/code&gt;, and the full &lt;code&gt;InitiatingProcess*&lt;/code&gt; family of parent-process columns [@deviceprocessevents-table].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DeviceNetworkEvents&lt;/code&gt;&lt;/strong&gt; -- the analogue of Sysmon EID 3 (NetworkConnect) plus EID 22 (DNSEvent) and the MDE-only network-protection telemetry. Columns include &lt;code&gt;RemoteIP&lt;/code&gt;, &lt;code&gt;RemotePort&lt;/code&gt;, &lt;code&gt;RemoteUrl&lt;/code&gt;, &lt;code&gt;LocalIP&lt;/code&gt;, &lt;code&gt;LocalPort&lt;/code&gt;, &lt;code&gt;Protocol&lt;/code&gt;, &lt;code&gt;RemoteIPType&lt;/code&gt;, and the &lt;code&gt;InitiatingProcess*&lt;/code&gt; family [@sentinel-xdr-connector].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DeviceFileEvents&lt;/code&gt;&lt;/strong&gt; -- the analogue of Sysmon EIDs 11 (FileCreate), 15 (FileCreateStreamHash), 23 (FileDelete archived), and 26 (FileDeleteDetected).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DeviceImageLoadEvents&lt;/code&gt;&lt;/strong&gt; -- the analogue of Sysmon EID 7 (ImageLoad).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DeviceRegistryEvents&lt;/code&gt;&lt;/strong&gt; -- the analogue of Sysmon EIDs 12-14 (RegistryEvent family).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DeviceEvents&lt;/code&gt;&lt;/strong&gt; -- the miscellaneous catch-all. &lt;a href=&quot;https://paragmali.com/blog/amsi-the-pre-execution-window-defender/&quot; rel=&quot;noopener&quot;&gt;AMSI&lt;/a&gt; scan results, exploit-protection events, ASR rule fires, Network Protection blocks, and other MDE-specific events that do not fit cleanly into any of the per-event-class tables surface here as &lt;code&gt;ActionType&lt;/code&gt; discriminators.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Past the six core tables there are siblings the article does not walk in detail but that detection engineers query alongside: &lt;code&gt;DeviceLogonEvents&lt;/code&gt; (interactive, remote-interactive, network logons), &lt;code&gt;DeviceFileCertificateInfo&lt;/code&gt; (Authenticode signer information), &lt;code&gt;DeviceInfo&lt;/code&gt; and &lt;code&gt;DeviceNetworkInfo&lt;/code&gt; (asset and posture). The cross-domain tables that the Defender XDR portal exposes -- &lt;code&gt;AlertInfo&lt;/code&gt;, &lt;code&gt;AlertEvidence&lt;/code&gt;, &lt;code&gt;IdentityLogonEvents&lt;/code&gt;, &lt;code&gt;EmailEvents&lt;/code&gt;, &lt;code&gt;CloudAppEvents&lt;/code&gt; -- are also queryable from the same surface, and the cross-domain join is one of the load-bearing reasons SOC teams move queries from a standalone SIEM into Advanced Hunting [@sentinel-xdr-connector].&lt;/p&gt;
&lt;h3&gt;Sysmon EID to MDE table cross-walk&lt;/h3&gt;
&lt;p&gt;The cross-walk is the table detection engineers actually need at their desk. Every row is a Sysmon EID, the MDE table the analogous event lands in, the &lt;code&gt;ActionType&lt;/code&gt; discriminator inside that table, and a fidelity rating relative to Sysmon&apos;s manifest -- because the MDE schema does &lt;em&gt;not&lt;/em&gt; surface every Sysmon field, and the fidelity gaps are where Hartong&apos;s MDE-augment config earns its keep.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sysmon EID&lt;/th&gt;
&lt;th&gt;MDE table&lt;/th&gt;
&lt;th&gt;ActionType&lt;/th&gt;
&lt;th&gt;Fidelity vs Sysmon&lt;/th&gt;
&lt;th&gt;Hartong-augment disposition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1 ProcessCreate&lt;/td&gt;
&lt;td&gt;DeviceProcessEvents&lt;/td&gt;
&lt;td&gt;ProcessCreated&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Drop (MDE covers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 NetworkConnect&lt;/td&gt;
&lt;td&gt;DeviceNetworkEvents&lt;/td&gt;
&lt;td&gt;ConnectionSuccess&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 ImageLoad&lt;/td&gt;
&lt;td&gt;DeviceImageLoadEvents&lt;/td&gt;
&lt;td&gt;ImageLoaded&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 CreateRemoteThread&lt;/td&gt;
&lt;td&gt;DeviceEvents&lt;/td&gt;
&lt;td&gt;RemoteThreadCreated&lt;/td&gt;
&lt;td&gt;Truncated (no SourceImage hash)&lt;/td&gt;
&lt;td&gt;Keep verbose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9 RawAccessRead&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Omitted&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 ProcessAccess&lt;/td&gt;
&lt;td&gt;DeviceEvents&lt;/td&gt;
&lt;td&gt;OpenProcessApiCall&lt;/td&gt;
&lt;td&gt;Truncated (no GrantedAccess mask)&lt;/td&gt;
&lt;td&gt;Keep verbose, narrow targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11 FileCreate&lt;/td&gt;
&lt;td&gt;DeviceFileEvents&lt;/td&gt;
&lt;td&gt;FileCreated&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12-14 RegistryEvent&lt;/td&gt;
&lt;td&gt;DeviceRegistryEvents&lt;/td&gt;
&lt;td&gt;RegistryValueSet etc.&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17-18 PipeEvent&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Omitted&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19-21 WmiEvent&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Omitted&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22 DNSEvent&lt;/td&gt;
&lt;td&gt;DeviceNetworkEvents&lt;/td&gt;
&lt;td&gt;DnsQuery&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23 FileDelete (archive)&lt;/td&gt;
&lt;td&gt;DeviceFileEvents&lt;/td&gt;
&lt;td&gt;FileDeleted&lt;/td&gt;
&lt;td&gt;Partial (no archive)&lt;/td&gt;
&lt;td&gt;Keep archive variant on selected paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26 FileDeleteDetected&lt;/td&gt;
&lt;td&gt;DeviceFileEvents&lt;/td&gt;
&lt;td&gt;FileDeleted&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27 FileBlockExecutable&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Omitted (MDE has separate prevent surface)&lt;/td&gt;
&lt;td&gt;Keep if Sysmon is enforcing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The fidelity column is the operational answer to &quot;do I need Sysmon if I have MDE?&quot; Where MDE is &lt;em&gt;Full&lt;/em&gt;, Sysmon duplicates. Where MDE is &lt;em&gt;Truncated&lt;/em&gt;, Sysmon adds the fields MDE drops. Where MDE is &lt;em&gt;Omitted&lt;/em&gt;, Sysmon is the only collection mechanism in the host&apos;s telemetry surface. This is the cross-walk that Hartong&apos;s &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; implements as XML rules.&lt;/p&gt;
&lt;h3&gt;The Kusto Hunt: PowerShell instances that called out within sixty seconds of spawn&lt;/h3&gt;
&lt;p&gt;The single most-frequently-cited hunting query in the Defender XDR field is some variation of the following. The query joins &lt;code&gt;DeviceProcessEvents&lt;/code&gt; to &lt;code&gt;DeviceNetworkEvents&lt;/code&gt; on &lt;code&gt;(DeviceId, InitiatingProcessId)&lt;/code&gt; and surfaces every PowerShell instance that opened an outbound network connection within sixty seconds of being spawned. This is the query that turns Maya&apos;s hunch (&quot;that base64-encoded command looks bad&quot;) into a SIEM-routable signal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kql&quot;&gt;// The Kusto Hunt: PowerShell instances that called out within
// 60s of process create, joined on (DeviceId, InitiatingProcessId).
DeviceProcessEvents
| where Timestamp &amp;gt; ago(24h)
| where FileName =~ &quot;powershell.exe&quot; or FileName =~ &quot;pwsh.exe&quot;
| project DeviceId, ProcessId, ProcessCreationTime = Timestamp,
          ParentImage = InitiatingProcessFileName,
          ParentCmd   = InitiatingProcessCommandLine,
          ProcessCmd  = ProcessCommandLine,
          User        = AccountUpn
| join kind=inner (
    DeviceNetworkEvents
    | where Timestamp &amp;gt; ago(24h)
    | where ActionType == &quot;ConnectionSuccess&quot;
    | project DeviceId, InitiatingProcessId, NetTime = Timestamp,
              RemoteIP, RemotePort, RemoteUrl
) on DeviceId, $left.ProcessId == $right.InitiatingProcessId
| where (NetTime - ProcessCreationTime) between (0s .. 60s)
| where RemoteIP !startswith &quot;10.&quot;
    and RemoteIP !startswith &quot;192.168.&quot;
    and not(RemoteIP matches regex &quot;^172\\.(1[6-9]|2[0-9]|3[0-1])\\.&quot;)
| project DeviceId, ProcessCreationTime, NetTime,
          ParentImage, ProcessCmd, RemoteIP, RemotePort, RemoteUrl, User
| order by NetTime desc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query is twelve operative lines and exercises four of KQL&apos;s most useful primitives: &lt;code&gt;join&lt;/code&gt; (on a tuple key), &lt;code&gt;between&lt;/code&gt; (for time-window matching), &lt;code&gt;!startswith&lt;/code&gt; and the regex check (for RFC 1918 exclusion), and &lt;code&gt;project&lt;/code&gt; (for column shaping). The &lt;code&gt;between (0s .. 60s)&lt;/code&gt; is the crux. A legitimate PowerShell launched by a logon script may also produce a network connection within the same minute -- the filter is necessary but not sufficient. Adding &lt;code&gt;ParentImage in (&quot;winword.exe&quot;, &quot;excel.exe&quot;, &quot;outlook.exe&quot;)&lt;/code&gt; narrows the hunt to the Office-spawning-PowerShell pattern that fits the Emotet and Qbot families. Adding &lt;code&gt;RemoteUrl in (~CustomTI)&lt;/code&gt; narrows the hunt further to known-bad indicators from the tenant&apos;s threat-intelligence list.&lt;/p&gt;
&lt;p&gt;{`
// JavaScript that walks through the &lt;em&gt;logic&lt;/em&gt; of the KQL hunt.
// The actual query runs in Advanced Hunting; this runs in your browser
// so you can see the join semantics with a small synthetic dataset.&lt;/p&gt;
&lt;p&gt;const processEvents = [
  { DeviceId: &quot;D1&quot;, ProcessId: 7700, Timestamp: 100,
    FileName: &quot;powershell.exe&quot;,
    InitiatingProcessFileName: &quot;WINWORD.EXE&quot;,
    ProcessCommandLine: &quot;powershell.exe -enc JABzAD0A...&quot; },
  { DeviceId: &quot;D2&quot;, ProcessId: 4422, Timestamp: 200,
    FileName: &quot;powershell.exe&quot;,
    InitiatingProcessFileName: &quot;explorer.exe&quot;,
    ProcessCommandLine: &quot;powershell.exe -Help&quot; },
];&lt;/p&gt;
&lt;p&gt;const networkEvents = [
  { DeviceId: &quot;D1&quot;, InitiatingProcessId: 7700, Timestamp: 130,
    ActionType: &quot;ConnectionSuccess&quot;,
    RemoteIP: &quot;185.243.115.84&quot;, RemotePort: 443 },
  { DeviceId: &quot;D2&quot;, InitiatingProcessId: 4422, Timestamp: 215,
    ActionType: &quot;ConnectionSuccess&quot;,
    RemoteIP: &quot;10.0.0.5&quot;, RemotePort: 443 },
];&lt;/p&gt;
&lt;p&gt;function isPrivate(ip) {
  return ip.startsWith(&quot;10.&quot;)
      || ip.startsWith(&quot;192.168.&quot;)
      || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip);
}&lt;/p&gt;
&lt;p&gt;const hits = [];
for (const p of processEvents) {
  if (!/^powershell\.exe$|^pwsh\.exe$/i.test(p.FileName)) continue;
  for (const n of networkEvents) {
    if (n.DeviceId !== p.DeviceId) continue;
    if (n.InitiatingProcessId !== p.ProcessId) continue;
    if (n.ActionType !== &quot;ConnectionSuccess&quot;) continue;
    const dt = n.Timestamp - p.Timestamp;
    if (dt &amp;lt; 0 || dt &amp;gt; 60) continue;
    if (isPrivate(n.RemoteIP)) continue;
    hits.push({ DeviceId: p.DeviceId,
                Parent:   p.InitiatingProcessFileName,
                Cmd:      p.ProcessCommandLine,
                RemoteIP: n.RemoteIP,
                Latency:  dt + &quot;s&quot; });
  }
}&lt;/p&gt;
&lt;p&gt;console.log(JSON.stringify(hits, null, 2));
// Expected output: one hit on D1 (WINWORD-spawned powershell to public IP);
// D2 is filtered out (RemoteIP is RFC 1918 private).
`}&lt;/p&gt;
&lt;p&gt;The semantic of the KQL is the semantic of the JavaScript: a relational join on a composite key, filtered by a time-window predicate and a network-class predicate. The KQL query is shorter and faster; the JavaScript is what the join is actually doing. Once a reader internalizes this pattern, the rest of the Advanced Hunting surface unfolds from it -- every other detection in the field is a variant of &quot;join &lt;code&gt;Device*&lt;/code&gt; table A to &lt;code&gt;Device*&lt;/code&gt; table B on &lt;code&gt;(DeviceId, InitiatingProcessId)&lt;/code&gt;, filter by time and content.&quot;Advanced Hunting per-query quotas are 100,000 rows of returned data and 10 minutes of execution time per call [@advanced-hunting-overview]. The practical workaround for queries that exceed either limit is to pre-filter with a tighter time window (&lt;code&gt;Timestamp &amp;gt; ago(1h)&lt;/code&gt; instead of &lt;code&gt;ago(24h)&lt;/code&gt;), or to push the heavy aggregation into a Sentinel scheduled analytics rule that runs every hour and materializes the result table for further hunting.&lt;/p&gt;
&lt;p&gt;The same query, the same columns, the same six tables surface in two different places: the Defender XDR portal itself (at &lt;code&gt;security.microsoft.com&lt;/code&gt; legacy or &lt;code&gt;defender.microsoft.com&lt;/code&gt; current), and inside Microsoft Sentinel via the Defender XDR connector. The two surfaces are not the same.&lt;/p&gt;
&lt;h2&gt;9. The Microsoft Sentinel Integration Model&lt;/h2&gt;
&lt;p&gt;The same KQL query runs in two different places, but the &lt;em&gt;economics&lt;/em&gt; of the two places are not the same, and that distinction is the one that catches detection engineers off guard. In-portal Advanced Hunting and Microsoft Sentinel both expose the same &lt;code&gt;Device*&lt;/code&gt; tables. They do not expose them with the same retention, the same join surface, or the same cost.&lt;/p&gt;
&lt;h3&gt;The connector contract&lt;/h3&gt;
&lt;p&gt;Microsoft Sentinel&apos;s &lt;strong&gt;Defender XDR connector&lt;/strong&gt; (the post-Ignite-2023 successor to the legacy Microsoft 365 Defender connector) streams Microsoft Defender XDR incidents, alerts, and Advanced Hunting events into Sentinel&apos;s Log Analytics workspace. Microsoft Learn&apos;s verbatim definition is: &quot;&lt;em&gt;The Defender XDR connector allows you to stream all Microsoft Defender XDR incidents, alerts, and advanced hunting events into Microsoft Sentinel and keeps incidents synchronized between both portals&lt;/em&gt;&quot; [@sentinel-xdr-connector]. The connector exposes per-table streaming, meaning the operator picks which &lt;code&gt;Device*&lt;/code&gt; tables to bring into Sentinel and pays per-GB ingestion only on those tables.&lt;/p&gt;
&lt;p&gt;The connector also handles the legacy-connector transition: when enabled, &quot;&lt;em&gt;any Microsoft Defender components&apos; connectors that were previously connected are automatically disconnected in the background&lt;/em&gt;&quot; [@sentinel-xdr-connector]. If a tenant was using the legacy Microsoft Defender ATP connector or per-product Defender connectors, those get retired when the unified Defender XDR connector takes over. This is the cleanup detail that catches teams off guard during the migration -- they expect both connectors to coexist for the transition window, and they do not.&lt;/p&gt;
&lt;h3&gt;Three asymmetries&lt;/h3&gt;
&lt;p&gt;The in-portal Advanced Hunting surface and the Sentinel surface differ on three practitioner-level axes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;In-portal Advanced Hunting&lt;/th&gt;
&lt;th&gt;Sentinel + Defender XDR connector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Retention&lt;/td&gt;
&lt;td&gt;30 days of raw data per query [@advanced-hunting-overview]&lt;/td&gt;
&lt;td&gt;Configurable per-workspace, up to 12 years archive [@sentinel-xdr-connector][@ms-log-analytics-archive]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query surface&lt;/td&gt;
&lt;td&gt;Six core &lt;code&gt;Device*&lt;/code&gt; tables plus cross-domain &lt;code&gt;AlertInfo&lt;/code&gt; / &lt;code&gt;EmailEvents&lt;/code&gt; / &lt;code&gt;IdentityLogonEvents&lt;/code&gt; / &lt;code&gt;CloudAppEvents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Six core &lt;code&gt;Device*&lt;/code&gt; tables (per-table selection) plus the entire Log Analytics workspace -- third-party logs, custom tables, ASIM-normalized data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Included with MDE Plan 2 license&lt;/td&gt;
&lt;td&gt;Per-GB Sentinel ingestion (current GA tier) plus per-GB archive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection authoring&lt;/td&gt;
&lt;td&gt;Custom detection rules; in-portal advanced-hunting-to-alert promotion&lt;/td&gt;
&lt;td&gt;Scheduled analytics rules; SOAR playbook triggers; automation rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-tenant hunting&lt;/td&gt;
&lt;td&gt;Tenant-bound only&lt;/td&gt;
&lt;td&gt;Possible via Lighthouse / Sentinel Workspaces aggregation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live response triggers&lt;/td&gt;
&lt;td&gt;In-portal action surface&lt;/td&gt;
&lt;td&gt;Via Logic Apps / Defender API connector&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The in-portal economics are predictable: the queries are included with the license, the retention is uniform at thirty days, the surface is the six tables plus the cross-domain entity catalogue. The Sentinel economics are flexible but billable: longer retention, more table coverage, more automation, all of which carry per-GB ingestion charges. The choice is operational: which queries does the team need to run on data older than thirty days?&lt;/p&gt;
&lt;h3&gt;When each surface is the right one&lt;/h3&gt;
&lt;p&gt;For the SOC-analyst-driven, real-time threat-hunting workflow that §1 modeled with Maya -- thirty days back, six tables, cross-domain join into &lt;code&gt;AlertInfo&lt;/code&gt; -- the in-portal Advanced Hunting surface is the obvious fit. For the longer-retention, multi-source, automated-analytic-rule workflow -- where detection engineers want a scheduled rule that joins &lt;code&gt;DeviceProcessEvents&lt;/code&gt; to a third-party identity log on a normalized schema -- the Sentinel surface is the obvious fit.&lt;/p&gt;
&lt;p&gt;The two surfaces are not exclusive. The most-cited operational pattern in 2026 is to keep the in-portal surface as the SOC-analyst hunting console (retention 30 days, no cost) and to run the Defender XDR connector into Sentinel for the subset of tables the team needs longer retention or analytics-rule scheduling on. Per-table selection keeps the per-GB ingestion bill predictable.The Sentinel connector preserves table names but namespaces them inside the Log Analytics workspace; &lt;code&gt;DeviceProcessEvents&lt;/code&gt; in Sentinel is the same shape as &lt;code&gt;DeviceProcessEvents&lt;/code&gt; in the Defender XDR portal, and most queries port between the two surfaces unchanged. Some columns are renamed at the connector boundary -- the most common gotcha is the time-zone and timestamp representation -- but the join semantics and the cross-walk to Sysmon EIDs do not change.&lt;/p&gt;
&lt;h3&gt;The portal-URL transition&lt;/h3&gt;
&lt;p&gt;A small operational detail worth naming: the Defender XDR portal lives at both &lt;code&gt;security.microsoft.com&lt;/code&gt; (legacy, still functional) and &lt;code&gt;defender.microsoft.com&lt;/code&gt; (current). The new URL was announced as part of the Microsoft 365 Defender to Microsoft Defender XDR rebrand at Ignite 2023 [@defender-xdr-ms-learn][@ms-ignite-2023-blog]. The rebrand changed neither the KQL substrate nor the &lt;code&gt;Device*&lt;/code&gt; schema; queries written against the legacy URL behave identically against the new URL. This is the disambiguation §1 alluded to in its layer-7 description: the same KQL query, the same tables, against either URL.&lt;/p&gt;
&lt;p&gt;Two query surfaces, six tables, twenty-nine Sysmon EIDs, and one operational question every SOC manager has asked at least once: &lt;em&gt;do we deploy Sysmon alongside Defender for Endpoint, or trust Defender alone?&lt;/em&gt; That is §10.&lt;/p&gt;
&lt;h2&gt;10. Sysmon Plus MDE: Three Coexistence Patterns&lt;/h2&gt;
&lt;p&gt;This is the operational question of the article. The community has converged on three answers, and one of them is wrong for almost every MDE-licensed environment. The three options, in order of increasing complexity and -- in most enterprise contexts -- decreasing prevalence:&lt;/p&gt;
&lt;h3&gt;Option A: Sysmon only, no MDE&lt;/h3&gt;
&lt;p&gt;Used in air-gapped environments, unlicensed environments, and regulatory contexts that prohibit cloud-side telemetry. Sysmon on its own produces a complete event stream into the local Windows event log, which a downstream collector (Windows Event Forwarding to a central collector, Splunk&apos;s Universal Forwarder, Wazuh&apos;s Windows agent, the Elastic Endpoint integration) picks up and ships to a customer-controlled SIEM. The trade-off: no cross-tenant correlation, no cloud-side threat-intelligence join, no &lt;code&gt;EtwTi&lt;/code&gt; (kernel security ETW provider) consumption, no Microsoft-authored detection rules. The customer owns every rule themselves.&lt;/p&gt;
&lt;p&gt;This is the right answer in a small set of contexts and the wrong answer in the licensed-enterprise context where MDE is already deployed.&lt;/p&gt;
&lt;h3&gt;Option B: MDE only, no Sysmon&lt;/h3&gt;
&lt;p&gt;The Microsoft-recommended baseline for licensed environments. MDE&apos;s &lt;code&gt;Device*&lt;/code&gt; schema covers the high-value Sysmon EID surface -- 1, 3, 7, 10, 11, 12-14 -- at full or near-full fidelity, and MDE adds the layers Sysmon does not have: cloud-side correlation, cross-domain joins (email, identity, cloud apps), Microsoft-authored built-in detection rules with continuous tuning, the &lt;code&gt;AlertInfo&lt;/code&gt;/&lt;code&gt;AlertEvidence&lt;/code&gt; evidence graph, and the SOC-actionable surface (device isolation, live response, automated investigation) [@mde-ms-learn][@ms-mitre-2024-blog].&lt;/p&gt;
&lt;p&gt;For most MDE-Plan-2-licensed organizations without a mature detection-engineering team, Option B is the right baseline. The trade-off is that the truncations and omissions in the &lt;code&gt;Device*&lt;/code&gt; schema -- the &lt;code&gt;ProcessAccess&lt;/code&gt; GrantedAccess mask Sysmon EID 10 surfaces verbatim that MDE drops, the WMI consumer expressions Sysmon EIDs 19-21 capture that MDE does not surface, the RawAccessRead and PipeEvent classes Sysmon captures that MDE omits entirely -- are not available to the team&apos;s custom hunting queries. For an organization without the engineering capacity to build hunting rules on those verbose surfaces, this is rarely a binding constraint.&lt;/p&gt;
&lt;h3&gt;Option C: MDE plus tuned Sysmon (Hartong&apos;s MDE-augment)&lt;/h3&gt;
&lt;p&gt;The detection-engineering-community pattern. Run MDE as the primary EDR. Run Sysmon alongside it with &lt;code&gt;olafhartong/sysmon-modular&lt;/code&gt;&apos;s &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; configuration, whose explicit README design intent is &quot;&lt;em&gt;intended to augment the information and have as little overlap as possible&lt;/em&gt;&quot; with MDE [@github-hartong-modular]. The augment config drops the EIDs MDE covers cleanly (1, 3, 7, 11, 12-14, 22) and keeps the EIDs MDE truncates or omits (8 with full SourceImage, 9 RawAccessRead, 10 with full GrantedAccess mask, 15 FileCreateStreamHash, 17-18 PipeEvent, 19-21 WmiEvent, 23 with archive variant on narrowly-scoped paths). The result is a Sysmon event-log stream that is purpose-built to complement MDE&apos;s Kusto stream, not duplicate it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; If you are an MDE-licensed shop with a detection-engineering team and you are &lt;em&gt;not&lt;/em&gt; running Hartong&apos;s &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt;, you are paying for two EDRs and getting the coverage of one. The augment config was purpose-built to make Sysmon&apos;s verbose-field surface complementary to MDE&apos;s cloud-correlation surface, not a duplicate. Standalone Sysmon next to MDE without the augment-specific exclusions is the worst of both worlds: double telemetry volume, double licensing exposure, and no incremental detection coverage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Cost and operational complexity&lt;/h3&gt;
&lt;p&gt;The three options have different operational profiles. The summary table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;License posture&lt;/th&gt;
&lt;th&gt;Telemetry volume&lt;/th&gt;
&lt;th&gt;Operational complexity&lt;/th&gt;
&lt;th&gt;Best used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;A. Sysmon only&lt;/td&gt;
&lt;td&gt;None (free)&lt;/td&gt;
&lt;td&gt;Medium (depends on config)&lt;/td&gt;
&lt;td&gt;Low (one product, one config)&lt;/td&gt;
&lt;td&gt;Air-gapped, regulatory-no-cloud, unlicensed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B. MDE only&lt;/td&gt;
&lt;td&gt;MDE Plan 1 or Plan 2&lt;/td&gt;
&lt;td&gt;Cloud-controlled (no per-host volume bill)&lt;/td&gt;
&lt;td&gt;Low (one product, Microsoft-managed)&lt;/td&gt;
&lt;td&gt;Most MDE-licensed orgs without detection-engineering team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C. MDE + Hartong augment&lt;/td&gt;
&lt;td&gt;MDE Plan 2 + WEF or SIEM&lt;/td&gt;
&lt;td&gt;High on Sysmon side (verbose EIDs); low on MDE side&lt;/td&gt;
&lt;td&gt;High (two products, modular config, WEF or SIEM forwarder)&lt;/td&gt;
&lt;td&gt;Detection-engineering-mature SOCs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A small operational caution: standalone Sysmon next to MDE without the augment-specific exclusions is the worst of three worlds. The drivers coexist fine at different Filter Manager altitudes, but the event log and downstream collector now carry every Sysmon EID the default config emits plus everything MDE collects on the cloud side. The double-pay problem the KeyIdea calls out is not theoretical; it shows up the first month a SOC team forgets to swap the default &lt;code&gt;sysmonconfig.xml&lt;/code&gt; for &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Hartong-augment-with-MDE pattern carries a second cost: the ETW manifest-provider session cap. Windows allows up to eight trace sessions to enable and receive events from the same manifest-based provider [@ms-etw-limits]; the &lt;code&gt;EtwTi&lt;/code&gt; security provider, Microsoft Defender Antivirus auto-start sessions, and any WPR sessions a developer might spin up all compete for that shared pool. Adding Sysmon&apos;s session takes one. On a host with a third-party EDR that already consumes several sessions against the same provider, this can cause silent telemetry loss. Audit &lt;code&gt;logman query -ets&lt;/code&gt; regularly.&lt;/p&gt;
&lt;h3&gt;The volume math&lt;/h3&gt;
&lt;p&gt;For sizing, assume a typical Windows endpoint generates roughly 20,000 process-create events per day under steady state (developer workstations are in this range; server volumes are higher; air-gapped jump boxes are lower) [@github-tsale-edr-telem]. The Hartong-augment config drops the top three high-volume EIDs (1 ProcessCreate, 7 ImageLoad, 11 FileCreate) that MDE already collects, retaining only the verbose surfaces. That cuts Sysmon volume by roughly 70 to 85 percent relative to a default-config Sysmon deployment, leaving only the verbose-EID stream (8, 10, 17-18, 19-21) MDE does not surface.&lt;/p&gt;
&lt;p&gt;This is the operational answer to the question. For organisations with detection-engineering teams, Option C is the default. For organisations without, Option B is the default. Option A is correct in a narrow set of contexts and should be picked on purpose. The next two sections turn from the layered &lt;em&gt;architecture&lt;/em&gt; to the layered &lt;em&gt;attack&lt;/em&gt; surface, because every defense has an attacker.&lt;/p&gt;
&lt;h2&gt;11. The Attack Tradition: Telemetry Suppression on Both Halves of the Pipeline&lt;/h2&gt;
&lt;p&gt;If you run an EDR on a host, you have made a bet that the EDR can survive contact with an attacker who knows it is there. The history of that bet -- on both halves of the pipeline -- is a chronological story with named techniques and named CVEs. Twelve years of attack tradition reduce to a small number of attack classes plus the structural defenses that closed each one.&lt;/p&gt;
&lt;h3&gt;Sysmon-side attacks, in order&lt;/h3&gt;
&lt;p&gt;The earliest tampering technique for Sysmon was the most obvious: stop the driver. Until Sysmon v15 in June 2023, the Sysmon service was a normal Windows service, and a SYSTEM-privilege attacker had several easy options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sc stop sysmon&lt;/code&gt; and &lt;code&gt;sc delete sysmon&lt;/code&gt; to unload &lt;code&gt;SysmonDrv.sys&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rewrite the minifilter altitude so Sysmon loads &lt;em&gt;after&lt;/em&gt; a tamper hook.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wevtutil cl Microsoft-Windows-Sysmon/Operational&lt;/code&gt; to erase history.&lt;/li&gt;
&lt;li&gt;Rewrite &lt;code&gt;SYSTEM\CurrentControlSet\Services\SysmonDrv\Parameters&lt;/code&gt; to re-program Sysmon&apos;s filter without restarting it.&lt;/li&gt;
&lt;li&gt;Register a Windows event-channel ACL change to silence &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A small family of community-published tools automated this class. The structural defense, before v15, was discipline: keep SYSTEM out of attacker hands.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;June 2023 v15 protected-process gate is the structural response&lt;/strong&gt; to this entire class. Microsoft Learn states the change verbatim: &quot;&lt;em&gt;The service runs as a protected process, thus disallowing a wide range of user mode interactions&lt;/em&gt;&quot; [@sysmon-ms-learn]. A SYSTEM-privilege attacker can no longer &lt;code&gt;OpenProcess(PROCESS_TERMINATE)&lt;/code&gt; against &lt;code&gt;Sysmon.exe&lt;/code&gt;, inject code into the service&apos;s address space, or attach a user-mode debugger. The class is not closed -- a kernel primitive still works, and a BYOVD chain that can write &lt;code&gt;_EPROCESS.Protection&lt;/code&gt; defeats the gate -- but the bar moves from &quot;a &lt;code&gt;wevtutil&lt;/code&gt; command in a PowerShell window&quot; to &quot;a kernel exploit primitive.&quot;&lt;/p&gt;
&lt;h3&gt;MDE-side attacks, in order&lt;/h3&gt;
&lt;p&gt;The MDE-side attack tradition starts at the Antimalware-PPL boundary on &lt;code&gt;MsSense.exe&lt;/code&gt;. The FalconForce 2022 work this article has already cited multiple times is the dispositive primary [@falconforce-2022]. The verbatim TL;DR -- describing how raising &lt;code&gt;dbgsrv.exe&lt;/code&gt; to &lt;code&gt;WinTcb&lt;/code&gt; PPL lets researchers debug MDE and capture cloud-bound payloads, which surfaced a missing-authorization vulnerability allowing spoofed telemetry to any M365 tenant -- landed earlier as the §6 PullQuote and is the framing this section builds on.&lt;/p&gt;
&lt;p&gt;The technique used a PPLKiller-class BYOVD chain to raise &lt;code&gt;dbgsrv.exe&lt;/code&gt; to &lt;code&gt;WinTcb&lt;/code&gt; PPL, attach to &lt;code&gt;MsSense.exe&lt;/code&gt;, and capture plaintext payloads via &lt;code&gt;SspiCli!EncryptMessage&lt;/code&gt; instrumentation. The vulnerability that work disclosed, &lt;strong&gt;CVE-2022-23278&lt;/strong&gt;, was patched on March 8, 2022 [@msrc-cve-2022-23278][@nvd-cve-2022-23278]. That patch closed &lt;em&gt;one&lt;/em&gt; missing-authorization gap in the cloud-side trust model. It did not close the class.&lt;/p&gt;
&lt;p&gt;The InfoGuard Labs 2025 follow-up [@infoguard-2025] demonstrated that the broader class is still open. The technique they used was different -- in-memory patching of &lt;code&gt;CRYPT32!CertVerifyCertificateChainPolicy&lt;/code&gt; to disable certificate-pinning validation, rather than PPL-elevated debugging -- but the vulnerability they surfaced is the same class: cloud endpoints (&lt;code&gt;/edr/commands/cnc&lt;/code&gt; and &lt;code&gt;/senseir/v1/actions/&lt;/code&gt;) that do not properly validate authentication tokens on traffic claiming to originate from the endpoint. As §7 documented, the MSRC disposition was low severity, no fix committed -- the operational consequence is that the spoofed-telemetry trust pattern that produced CVE-2022-23278 in 2022 is, three years later, still exploitable along a parallel surface.&lt;/p&gt;
&lt;p&gt;The broader attack class -- ETW Threat Intelligence (&lt;code&gt;EtwTi&lt;/code&gt;) blinding -- has been studied independently of MDE. The structural answer in 2026 is HVCI plus VBL plus Antimalware-PPL plus ELAM (the four-component hardening stack). On a fully-hardened endpoint, the user-mode tamper surface that defined the 2014-to-2020 era of EDR-blinding tradecraft is largely closed; the residual attack surface is kernel-mode adversary primitives. That is the structural ceiling §12 picks up.&lt;/p&gt;
&lt;h3&gt;Cross-pipeline attacks&lt;/h3&gt;
&lt;p&gt;Some attacks affect both halves of the pipeline simultaneously. The most-cited is &lt;strong&gt;BYOVD-driven kernel-callback removal&lt;/strong&gt;: a Bring-Your-Own-Vulnerable-Driver chain loads a Microsoft-signed but vulnerable driver, exploits a known CVE in the driver, and from kernel context calls &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; with a &lt;code&gt;Remove = TRUE&lt;/code&gt; flag against the EDR sensor&apos;s registered callbacks, effectively unhooking both Sysmon and MDE at the kernel-callback layer. The structural defense Microsoft shipped in response is the &lt;strong&gt;Microsoft Vulnerable Driver Blocklist&lt;/strong&gt; with HVCI enforcement, which has been on by default since Windows 11 22H2 [@ms-driver-blocklist].&lt;/p&gt;
&lt;p&gt;A second cross-pipeline attack is &lt;strong&gt;direct-syscall bypass&lt;/strong&gt; of user-mode hook libraries -- but this attack is mostly a relic from the 2010s when EDR vendors relied on &lt;code&gt;ntdll.dll&lt;/code&gt; user-mode IAT hooks; modern Sysmon and MDE neither register nor depend on user-mode hooks for the kernel-callback events. Direct-syscall malware that bypasses the user-mode hooks of a &lt;em&gt;third-party&lt;/em&gt; EDR will still produce a Sysmon EID 1 and an MDE &lt;code&gt;DeviceProcessEvents&lt;/code&gt; row, because the kernel-callback fires whether or not the malware called &lt;code&gt;NtCreateUserProcess&lt;/code&gt; via &lt;code&gt;ntdll.dll&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;The attack-surface lattice&lt;/h3&gt;

flowchart TD
    A1[&quot;Sysmon-side: sc stop, wevtutil clear, registry altitude swap&quot;] --&amp;gt; D1[Sysmon v15 protected-process gate]
    A2[&quot;MDE-side: PPLKiller + dbgsrv WinTcb to attach MsSense&quot;] --&amp;gt; D2[&quot;Antimalware-PPL on MsSense.exe&quot;]
    A3[&quot;Cloud-side: CVE-2022-23278 spoofed cloud telemetry&quot;] --&amp;gt; D3[&quot;MSRC patch March 8 2022&quot;]
    A4[&quot;Cloud-side: InfoGuard 2025 cert-pinning bypass + missing auth&quot;] --&amp;gt; O4[&quot;OPEN: &apos;low severity, no fix committed&apos;&quot;]
    A5[&quot;Cross-pipeline: BYOVD kernel-callback unhook&quot;] --&amp;gt; D5[&quot;HVCI + Vulnerable Driver Blocklist (Win11 22H2+)&quot;]
    D1 --&amp;gt; R[&quot;Residual: kernel-mode adversary primitive that defeats HVCI + VBL&quot;]
    D2 --&amp;gt; R
    D5 --&amp;gt; R
    D3 --&amp;gt; R
    O4 -.unclosed.-&amp;gt; R
&lt;p&gt;The shape of the lattice is the shape of the field&apos;s hardening: every user-mode attack class has a structural defense, and the structural defenses converge on a single residual -- the kernel-mode adversary primitive that defeats HVCI plus the Vulnerable Driver Blocklist. On the cloud side, the InfoGuard 2025 finding is the unresolved item -- the same trust pattern that produced CVE-2022-23278 in 2022 produced a different cluster of missing-authorization bugs three years later. The attack-defense arc is still moving, and the two-sided nature of the pipeline (host + cloud) is why.&lt;/p&gt;
&lt;p&gt;Every attack surface has a structural defense. But every defense has a horizon. What is outside the horizon?&lt;/p&gt;
&lt;h2&gt;12. Theoretical Limits: What the Pipeline Cannot See&lt;/h2&gt;
&lt;p&gt;Sysmon and Microsoft Defender for Endpoint are &lt;em&gt;observation&lt;/em&gt; pipelines, not enforcement layers. That statement contains four structural ceilings the engineering cannot lift. These are not bugs to be fixed; they are properties of the architecture that follow from the choice of where the pipeline collects.&lt;/p&gt;
&lt;h3&gt;Ceiling 1: The pre-driver-load horizon&lt;/h3&gt;
&lt;p&gt;Both Sysmon&apos;s &lt;code&gt;SysmonDrv.sys&lt;/code&gt; and Defender for Endpoint&apos;s &lt;code&gt;WdBoot.sys&lt;/code&gt; are kernel drivers, but they sit at different points in the boot order. &lt;code&gt;WdBoot.sys&lt;/code&gt; is ELAM-signed and loads before any non-ELAM driver, which lets it classify subsequent boot-start drivers as &lt;code&gt;Good&lt;/code&gt;, &lt;code&gt;Bad&lt;/code&gt;, or &lt;code&gt;Unknown&lt;/code&gt; for the kernel&apos;s load decision. (Measured Boot separately hashes &lt;code&gt;WdBoot.sys&lt;/code&gt; along with the bootloader and kernel into TPM PCRs; that integrity-attestation channel is a sibling feature, not ELAM&apos;s own job.) &lt;code&gt;SysmonDrv.sys&lt;/code&gt; is &lt;code&gt;BootStart&lt;/code&gt;-ordered but not ELAM-signed -- it loads early, but not first.&lt;/p&gt;
&lt;p&gt;Events that happen before the EDR driver&apos;s &lt;code&gt;DriverEntry&lt;/code&gt; runs are &lt;em&gt;not observable&lt;/em&gt; by that driver. For Sysmon, that means rootkit-class malware that loads inside the early Windows boot path (UEFI bootkits, boot-record manipulation, very-early kernel modifications) is invisible until after Sysmon catches up. For MDE, the ELAM-signed &lt;code&gt;WdBoot.sys&lt;/code&gt; closes most of this window for non-ELAM drivers; the residual is anything that runs even earlier -- UEFI-firmware-resident malware, hardware-implant attacks, the very narrow class that targets the pre-ELAM trust boundary itself. The &lt;a href=&quot;https://paragmali.com/blog/measured-boot-the-tcg-event-log-from-srtm-to-pcr-bound-bitlo/&quot; rel=&quot;noopener&quot;&gt;Measured Boot&lt;/a&gt; plus &lt;a href=&quot;https://paragmali.com/blog/secure-boot-in-windows-the-chain-from-sector-zero-to-userini/&quot; rel=&quot;noopener&quot;&gt;Secure Boot&lt;/a&gt; stack (covered in adjacent articles in this series) is what observes the pre-ELAM region. EDR&apos;s reach does not extend below the ELAM line.&lt;/p&gt;
&lt;h3&gt;Ceiling 2: The observation-vs-enforcement latency gap&lt;/h3&gt;
&lt;p&gt;Sysmon&apos;s kernel-callback to event-log latency is sub-millisecond. The driver runs the rule engine, decides to emit, and writes through the ETW publisher to the Sysmon service. The service writes to the event log. The total path is microseconds in the best case, milliseconds under load.&lt;/p&gt;
&lt;p&gt;MDE&apos;s end-to-end latency to a queryable Kusto row is &lt;em&gt;seconds to tens of seconds&lt;/em&gt;. The endpoint side takes microseconds; the TLS hop to regional ingest takes the dominant fraction of a second; the Kusto write and per-tenant indexing takes the rest. Microsoft&apos;s own Advanced Hunting documentation phrases the freshness contract carefully: &quot;&lt;em&gt;Advanced hunting receives this data almost immediately after the sensors that collect them successfully transmit it to the corresponding cloud services&lt;/em&gt;&quot; [@advanced-hunting-overview]. &quot;Almost immediately&quot; is empirically a few seconds in steady state, longer under load, and indefinite when the endpoint cannot reach the cloud.&lt;/p&gt;
&lt;p&gt;Any payload that completes its work inside the observation window has executed &lt;em&gt;before&lt;/em&gt; the SIEM rule could fire. A &lt;code&gt;mimikatz.exe&lt;/code&gt; invocation that dumps LSA secrets in three milliseconds, exfiltrates them over a covert DNS channel in 800 milliseconds, and exits in another two milliseconds has produced a complete attack chain before MDE&apos;s event has reached Kusto, let alone before the Maya-class analyst has glanced at her console. The hybrid responses that blur this boundary -- Sysmon v14&apos;s FileBlockExecutable (EID 27), MDE&apos;s ASR rules and Network Protection -- are &lt;em&gt;kernel-callback-time&lt;/em&gt; decisions, not SIEM-rule-time decisions; they run inside the few-microsecond window the driver itself owns, and they are constrained by the rule logic baked into the host configuration rather than by the live correlation logic of the cloud-side detection engine.&lt;/p&gt;
&lt;h3&gt;Ceiling 3: MDE schema truncation versus Sysmon manifest&lt;/h3&gt;
&lt;p&gt;This is the ceiling §8 quantified column-by-column. The &lt;code&gt;Device*&lt;/code&gt; tables surface a normalized, mostly-complete cross-walk of Sysmon&apos;s manifest -- but mostly-complete is not the same as complete. The &lt;code&gt;ProcessAccess&lt;/code&gt; GrantedAccess mask is the most-cited example: Sysmon EID 10 captures the full 32-bit &lt;code&gt;PROCESS_ACCESS_MASK&lt;/code&gt; (which discriminates between &lt;code&gt;PROCESS_QUERY_INFORMATION&lt;/code&gt;, &lt;code&gt;PROCESS_VM_READ&lt;/code&gt;, &lt;code&gt;PROCESS_CREATE_THREAD&lt;/code&gt;, and so on -- the canonical malicious patterns are visible in this mask), while MDE&apos;s &lt;code&gt;DeviceEvents&lt;/code&gt; &lt;code&gt;OpenProcessApiCall&lt;/code&gt; &lt;code&gt;ActionType&lt;/code&gt; collapses the mask into a coarser categorization. The WmiEvent consumer expressions Sysmon EIDs 19-21 capture verbatim -- which are how WMI-based persistence is detected -- are not surfaced in the &lt;code&gt;Device*&lt;/code&gt; schema at all. RawAccessRead (EID 9, the canonical disk-level credential-theft observable) is omitted. PipeEvent (EIDs 17-18) is omitted.&lt;/p&gt;
&lt;p&gt;Hartong&apos;s &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; exists precisely because of this asymmetry. The augment config is a community-curated artifact whose purpose is to fill the schema-truncation gap. The cost: a second telemetry stream on the host. The benefit: detection-engineering visibility into the verbose-EID surface MDE drops.&lt;/p&gt;
&lt;h3&gt;Ceiling 4: The kernel-mode adversary primitive&lt;/h3&gt;
&lt;p&gt;A ring-0 attacker with a working kernel primitive -- a memory-write capability into the kernel data structures, typically delivered via BYOVD against a vulnerable signed driver -- can defeat the pipeline as a consequence of defeating the structural defenses that protect it. Specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Direct call to &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; with &lt;code&gt;Remove = TRUE&lt;/code&gt; unregisters the EDR sensor&apos;s callback, after which &lt;code&gt;CreateProcess&lt;/code&gt; events on that host produce no observable.&lt;/li&gt;
&lt;li&gt;A patch to the &lt;code&gt;_EPROCESS.Protection&lt;/code&gt; field of &lt;code&gt;MsSense.exe&lt;/code&gt; or &lt;code&gt;Sysmon.exe&lt;/code&gt; strips the Antimalware-PPL gate, after which user-mode attacks against the service work again.&lt;/li&gt;
&lt;li&gt;A direct write into the &lt;code&gt;EtwTi&lt;/code&gt; provider&apos;s keyword mask zero-pages the security-event-emission surface, after which the kernel-side &lt;code&gt;EtwTi&lt;/code&gt; consumer (which several EDRs subscribe to) sees no events even when the underlying behaviour fired.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &quot;Tampering with Windows Event Tracing&quot; research published by Palantir in 2018 (Matt Graeber&apos;s canonical writeup) and the follow-on &lt;code&gt;EtwTi&lt;/code&gt;-blinding tradition is the published primary for this attack class [@palantir-etw-tampering-2018]. The structural defenses are HVCI plus VBL plus Antimalware-PPL plus ELAM. But the four-component hardening stack does not prevent a kernel-mode adversary primitive from defeating the EDR; it only raises the bar to &lt;em&gt;needing&lt;/em&gt; a kernel-mode adversary primitive.&lt;/p&gt;

Observation requires execution overhead, and execution requires the observer to live in the same trust domain as the observed. A kernel-mode observer (Sysmon, MDE) lives in the same kernel trust domain as the kernel-mode attacker; a hypervisor-rooted observer (`EtwTi` running under Virtualization-Based Security) shifts the trust boundary up one level, but does not eliminate it -- the observer-in-VBS is still subject to attacks on the hypervisor itself. There is no architectural place to put the observer that is strictly outside the attacker&apos;s reach unless the observer is in different hardware, which is what hardware-rooted Root-of-Trust attestations attempt and what an Anti-Tamper Service Provider (ATSP) is being defined for. EDR sensors will always be co-resident with the adversary at *some* trust boundary. The ceiling is structural.
&lt;p&gt;Four ceilings, four sets of open questions. What is the field working on right now?&lt;/p&gt;
&lt;h2&gt;13. Open Problems and Active Work&lt;/h2&gt;
&lt;p&gt;Some questions in this article have no answer in 2026. Five of them are where the field will move next.&lt;/p&gt;
&lt;h3&gt;The MDE kernel-callback inventory&lt;/h3&gt;
&lt;p&gt;As §6&apos;s aha-moment Callout established, Microsoft has not published a kernel-callback inventory for the MDE EDR sensor, which is the structural reason Hartong&apos;s &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; exists as a community-curated artifact rather than a Microsoft-published reference. What §13 adds is the &lt;em&gt;empirical scaffolding&lt;/em&gt; the community uses in the absence of that inventory: the MITRE Engenuity Round 6 (2024) evaluation results [@ms-mitre-2024-blog] plus the Shen et al. whole-graph re-analysis [@arxiv-shen-2024] are the closest published evidence of which MDE detection paths produced an alert during a known emulated technique. Neither covers an end-to-end kernel-callback enumeration comparable to Sysmon&apos;s manifest -- they cover &lt;em&gt;outputs&lt;/em&gt; (alerts produced) rather than &lt;em&gt;mechanisms&lt;/em&gt; (callbacks registered). Closing this gap would require either Microsoft to publish a per-&lt;code&gt;ActionType&lt;/code&gt;-to-per-kernel-callback cross-walk for the &lt;code&gt;Device*&lt;/code&gt; schema, or the community to fund and publish a reverse-engineered inventory that goes meaningfully past the FalconForce 2022 and InfoGuard 2025 slices. As of 2026, neither has happened.&lt;/p&gt;
&lt;h3&gt;Defender XDR built-in detection rule logic&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;AlertInfo&lt;/code&gt; and &lt;code&gt;AlertEvidence&lt;/code&gt; table schemas are published; the underlying rule logic that produces alerts in these tables is not. Microsoft ships &quot;Microsoft-authored detection rules&quot; as part of Defender XDR Plan 2, and the rules update continuously without an obvious public changelog. The community workaround is to subscribe to the MITRE ATT&amp;amp;CK evaluation rounds (the most recent being Round 6 in 2024 [@ms-mitre-2024-blog][@arxiv-shen-2024]) and infer rule coverage from per-technique detection scores, but this is indirect and lossy. A published rule-logic catalogue would let detection-engineering teams reason about which custom rules are duplicates of Microsoft&apos;s authored content and which fill genuine gaps.&lt;/p&gt;
&lt;h3&gt;Cross-tenant hunting and data sovereignty&lt;/h3&gt;
&lt;p&gt;MSSPs (managed-security service providers) routinely need to hunt across multiple customer tenants for shared-IOC observations. Microsoft&apos;s official multi-tenant story is Microsoft Defender XDR Multitenant Management (in GA) plus Azure Lighthouse for cross-tenant Sentinel access. Both are functional and both are documented at the operational level. The deeper question -- &lt;em&gt;what is the GDPR/HIPAA/FedRAMP framework around hunting an IOC observed in Tenant A against telemetry held in Tenant B&apos;s regional Kusto cluster?&lt;/em&gt; -- is unsettled. The data-residency commitments Microsoft makes per region [@ms-server-endpoints-learn] do not directly answer the cross-tenant-hunt question. Vendor and customer guidance is still maturing.&lt;/p&gt;
&lt;h3&gt;A Microsoft-published reference MDE-augmentation Sysmon config&lt;/h3&gt;
&lt;p&gt;Hartong&apos;s config is the community answer to the question &quot;what Sysmon EIDs should I emit on a host that already has MDE?&quot; There is no Microsoft-published reference equivalent. This is the most surgical near-term improvement Microsoft could make. Publishing such a config -- even as a starting-point template, not a binding recommendation -- would compress an entire detection-engineering conversation into a single endorsed artifact. The political reason it has not happened is partly that Microsoft does not officially recommend running Sysmon alongside MDE; the operational reality is that detection-engineering-mature shops do anyway.&lt;/p&gt;
&lt;h3&gt;Cross-platform parity&lt;/h3&gt;
&lt;p&gt;Sysmon for Linux (&lt;code&gt;microsoft/SysmonForLinux&lt;/code&gt;, created October 28, 2020 and publicly announced in October 2021) ships an eBPF-based implementation of the same XML schema and emits to syslog [@github-sysmon-linux]. It is a substantial subset of the Windows manifest -- process create, file write, network connect, image load, raw access read -- with the cross-OS shared XML rule grammar going for it, so a detection-engineering team can write one Sigma-aligned rule and run it against both Windows and Linux endpoints with minor token substitutions. Full parity between the Windows kernel-callback Sysmon and the Linux eBPF Sysmon is &lt;em&gt;not&lt;/em&gt; the design intent; the Linux port intentionally captures only the EIDs that map cleanly onto eBPF observables. &lt;code&gt;BTFHub&lt;/code&gt; plus &lt;code&gt;SysinternalsEBPF&lt;/code&gt; (the in-tree CO-RE infrastructure the Linux port uses) make per-kernel-version deployments tractable, but the field has not yet converged on a single canonical Linux config the way it converged on SwiftOnSecurity for Windows.&lt;/p&gt;
&lt;p&gt;These five open problems are where the field will move in the next five years. In the meantime, what does the analyst do on Monday morning?&lt;/p&gt;
&lt;h2&gt;14. Seven Things to Do Monday Morning&lt;/h2&gt;
&lt;p&gt;Everything above has been background. Here is the operational checklist. Each step is anchored to a primary citation. Walk all seven on a single non-production host before fleet rollout; the ninety-second triage walk from §1 is best learned by reproducing it once on your own tenant.&lt;/p&gt;
&lt;h3&gt;1. Verify the MDE sensor service is healthy&lt;/h3&gt;
&lt;p&gt;Run as Administrator on the endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sc query sense
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A healthy result shows &lt;code&gt;STATE: 4 RUNNING&lt;/code&gt; and &lt;code&gt;WIN32_EXIT_CODE: 0&lt;/code&gt;. If the result is &lt;code&gt;STATE: 1 STOPPED&lt;/code&gt; or the service is missing entirely, consult the WDATPOnboarding event source in the Application event log for events 5, 10, 15, 30, 35, 40, 65, and 70 -- each has a documented resolution procedure [@sense-troubleshoot]. On Windows Server 2019, 2022, 2025, or Azure Stack HCI 23H2 or later, also verify the Feature on Demand is installed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DISM.EXE /Online /Get-CapabilityInfo /CapabilityName:Microsoft.Windows.Sense.Client~~~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result should show &lt;code&gt;State : Installed&lt;/code&gt; and &lt;code&gt;Version : 10.x.x.x&lt;/code&gt;. If &lt;code&gt;State : NotPresent&lt;/code&gt;, install the FoD before proceeding.&lt;/p&gt;
&lt;h3&gt;2. Open Advanced Hunting and run the §8 query&lt;/h3&gt;
&lt;p&gt;Navigate to &lt;code&gt;defender.microsoft.com&lt;/code&gt; (or the legacy &lt;code&gt;security.microsoft.com&lt;/code&gt;), expand &lt;strong&gt;Hunting &amp;gt; Advanced hunting&lt;/strong&gt;, paste the §8 KQL query, and run it [@advanced-hunting-overview]. On a fresh tenant the query may return zero rows -- that is the correct result for a healthy environment. Tighten the time window if it is slow (&lt;code&gt;Timestamp &amp;gt; ago(1h)&lt;/code&gt; instead of &lt;code&gt;ago(24h)&lt;/code&gt;) until the query returns within ten seconds. The point of this step is to confirm the read surface is reachable and that the user has Hunter (or higher) RBAC permission on the tenant.&lt;/p&gt;
&lt;h3&gt;3. If licensed for Sentinel, install the Defender XDR connector&lt;/h3&gt;
&lt;p&gt;In the Microsoft Sentinel workspace, navigate to &lt;strong&gt;Data connectors&lt;/strong&gt;, choose &lt;strong&gt;Microsoft Defender XDR&lt;/strong&gt;, and configure per-table streaming [@sentinel-xdr-connector]. Pick the tables your team needs longer retention or analytics-rule scheduling on; leave the others to in-portal Advanced Hunting. Be aware that enabling the connector &quot;&lt;em&gt;automatically disconnects&lt;/em&gt;&quot; any legacy Microsoft Defender component connectors during enablement; this is the cleanup detail to plan for during migration windows [@sentinel-xdr-connector].&lt;/p&gt;
&lt;h3&gt;4. If deploying Sysmon alongside MDE, start from the augment config&lt;/h3&gt;
&lt;p&gt;Clone &lt;code&gt;olafhartong/sysmon-modular&lt;/code&gt;, build the &lt;code&gt;sysmonconfig-mde-augment.xml&lt;/code&gt; variant, and deploy with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sysmon64.exe -accepteula -i sysmonconfig-mde-augment.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the active configuration with &lt;code&gt;Sysmon64.exe -c&lt;/code&gt; and confirm the rule count matches the augment config&apos;s expected output [@github-hartong-modular].&lt;/p&gt;
&lt;h3&gt;5. If deploying Sysmon standalone, start from NextronSystems or modular default&lt;/h3&gt;
&lt;p&gt;For air-gapped or unlicensed environments, clone &lt;code&gt;NextronSystems/sysmon-config&lt;/code&gt; (the post-2021-rename successor to &lt;code&gt;Neo23x0/sysmon-config&lt;/code&gt;) and deploy &lt;code&gt;sysmonconfig.xml&lt;/code&gt; or, for the blocking-rule variant, &lt;code&gt;sysmonconfig-export-block.xml&lt;/code&gt; [@github-neo23x0][@github-nextronsystems-meta]. Alternatively, &lt;code&gt;olafhartong/sysmon-modular&lt;/code&gt;&apos;s default &lt;code&gt;sysmonconfig.xml&lt;/code&gt; (built from the modular library) is the right choice if you want fine-grained per-technique tuning later [@github-hartong-modular].&lt;/p&gt;
&lt;h3&gt;6. Verify Sysmon v15.2 or later is running&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Sysmon64.exe -c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output&apos;s header line should show the binary version. Anything &lt;code&gt;v15.x&lt;/code&gt; or later has the protected-process gate enabled [@sysmon-ms-learn][@bleepingcomputer-sysmon15]. Anything older is trivially blindable by a SYSTEM-privilege attacker and is the single biggest deployment-hygiene risk in the Sysmon population today.&lt;/p&gt;
&lt;h3&gt;7. Audit the MDE onboarding registry hives&lt;/h3&gt;
&lt;p&gt;Compare the live registry values to the expected onboarding state:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reg query &quot;HKLM\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection&quot;
reg query &quot;HKLM\SOFTWARE\Microsoft\Windows Advanced Threat Protection\Status&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unexpected changes -- particularly a change to the onboarding &lt;code&gt;OrgId&lt;/code&gt; or to the policy-controlled &lt;code&gt;Disabled&lt;/code&gt; value -- are an indicator that the tenant or device has been re-targeted, possibly by an attacker who obtained admin-level access and is attempting to re-route the endpoint&apos;s telemetry to a different tenant or to disable the MDE sensor entirely [@sense-troubleshoot]. Set up a Sentinel detection rule on &lt;code&gt;DeviceRegistryEvents&lt;/code&gt; with &lt;code&gt;RegistryKey contains &quot;Windows Advanced Threat Protection&quot;&lt;/code&gt; to surface this class of tampering automatically.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Walk steps 1 and 2 on a single non-production host before fleet rollout. The ninety-second-triage walk you saw in §1 is best learned by reproducing it once on your own tenant. The cost of getting steps 4-6 wrong (deploying the wrong Sysmon config on a high-volume server fleet) is hours of operational pain; the cost of doing them right on a single test host first is twenty minutes.&lt;/p&gt;
&lt;/blockquote&gt;

The MDE sensor service has not been onboarded on this host. Two common causes: (1) the endpoint is on a Windows Server SKU and the SENSE Feature on Demand has not been installed; run the DISM `Get-CapabilityInfo` check in step 1 to confirm. (2) The onboarding script (the `WindowsDefenderATPLocalOnboardingScript.cmd` or the equivalent Group Policy / Intune / SCCM artifact) has not been run on this host. The MDE settings page in the Defender XDR portal shows the per-device onboarding artifacts under **Settings &amp;gt; Endpoints &amp;gt; Onboarding** for download [@sense-troubleshoot].
&lt;p&gt;The Defender XDR portal also exposes a &lt;strong&gt;device timeline&lt;/strong&gt; view that surfaces a chronological event stream per device without requiring KQL. This is the right view for analysts who are still learning the schema; the KQL surface is the right view for repeatable hunts and detection-rule authoring.&lt;/p&gt;
&lt;p&gt;Seven steps, one Monday. The rest of the questions are in the FAQ.&lt;/p&gt;
&lt;h2&gt;15. Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;Seven of the questions that come up every time this material is taught.&lt;/p&gt;


Yes on its output side; mostly no on its input side. Sysmon publishes its events through an ETW provider called `Microsoft-Windows-Sysmon`, which is how downstream collectors and the Windows Event Log service consume the data. On its *input* side, Sysmon is a kernel driver that collects via five different mechanisms -- `PsSetCreateProcessNotifyRoutineEx` for process create and exit, `PsSetLoadImageNotifyRoutine` for image load and driver load, `PsSetCreateThreadNotifyRoutineEx` for remote-thread creation, `ObRegisterCallbacks` for cross-process access, `CmRegisterCallbackEx` for registry, and Filter Manager minifilters for ordinary file system and NPFS named pipes. Two exceptions live on Sysmon&apos;s input side. The single kernel-ETW consumer is `Microsoft-Windows-DNS-Client` for EID 22 DNSEvent; the WmiEvent family (EIDs 19-21) is implemented in a consumer style against the WMI activity provider&apos;s user-mode tracing surface. Calling Sysmon &quot;ETW-based&quot; without that distinction is the most common architectural confusion in the field [@sysmon-ms-learn].

For most organizations licensed for MDE Plan 2 and without a mature detection-engineering team, yes -- MDE alone is the right baseline. For organizations with a detection-engineering team, the community pattern is to deploy MDE *plus* a tuned Sysmon configuration (specifically Olaf Hartong&apos;s `sysmonconfig-mde-augment.xml`) that fills the gaps where MDE&apos;s `Device*` schema truncates or omits fields that Sysmon&apos;s manifest captures verbatim -- the `ProcessAccess` GrantedAccess mask, the full WMI consumer expressions, RawAccessRead, the pipe events, and selected file-delete archival paths. The wrong answer for an MDE-licensed shop with a detection-engineering team is to do nothing on the Sysmon side; the second-wrong answer is to deploy *default* Sysmon alongside MDE, which produces double the telemetry volume for the coverage of one [@github-hartong-modular][@mde-ms-learn].

The five class-specific `Device*` tables (`DeviceProcessEvents`, `DeviceNetworkEvents`, `DeviceFileEvents`, `DeviceImageLoadEvents`, `DeviceRegistryEvents`) each map onto a single Sysmon EID family and present a normalized, per-class set of columns. `DeviceEvents` is the miscellaneous catch-all: AMSI scan results, exploit-protection events, Defender Antivirus operational events, Attack Surface Reduction rule fires, Network Protection blocks, OpenProcess API calls, and other MDE-specific telemetry surface here under different `ActionType` values. If a row&apos;s `ActionType` does not match what you expected, the row is probably in `DeviceEvents` rather than the table you searched first [@advanced-hunting-overview].

No. The historical root is SwiftOnSecurity&apos;s `sysmon-config`, created on February 1, 2017 per the GitHub REST API [@github-swiftonsecurity-meta]. Florian Roth (`@Neo23x0`) forked SwiftOnSecurity&apos;s repository in January 2018 and added blocking-rule support, community pull-request merges, and the maintainer roster that now includes Tobias Michalski, Christian Burkard, and Nasreddine Bencherchali [@github-neo23x0]. The Neo23x0 repository was renamed to `NextronSystems/sysmon-config` on July 24, 2021 [@github-nextronsystems-meta]; the old URL HTTP-301 redirects to the new one and the content lineage from SwiftOnSecurity is unchanged. Calling Roth&apos;s config &quot;the original&quot; is the inverse of the truth; calling it &quot;the canonical actively-maintained fork&quot; is closer.

No. Sysmon supports one active configuration at a time. There is no aggregate-multiple-XMLs feature at the driver layer. Olaf Hartong&apos;s modular workflow generates a single merged XML at build time from a per-technique module library; the production fleet receives that single XML and the driver enforces it. If you want two configurations -- one for the SOC team&apos;s hunting, one for the platform team&apos;s audit -- merge the rules at build time and ship the combined product [@github-hartong-modular].

Because it runs as Antimalware Protected Process Light (`PROTECTED_ANTIMALWARE_LIGHT`), the Windows kernel rejects ordinary user-mode `OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_DUP_HANDLE)` requests against the process from any caller that does not itself run at an equal or higher signer level. The published reverse-engineering technique (FalconForce 2022) is to raise the Windows PE debug server `dbgsrv.exe` to the `WinTcb` signer level via a PPLKiller-class kernel primitive, then attach the elevated debug server to `MsSense.exe`. That technique requires a kernel-mode primitive (commonly a BYOVD chain), which is itself non-trivial. The protection level is the structural defense; the debug-server technique is the dispositive community workaround [@falconforce-2022].

Thirty days of raw data in the Defender XDR portal: &quot;*Advanced hunting is a query-based threat hunting tool that you use to explore up to 30 days of raw data*&quot; [@advanced-hunting-overview]. Beyond thirty days, retention is configurable per workspace via the Microsoft Sentinel Defender XDR connector; the Log Analytics workspace archive tier supports up to twelve years of per-table archive on a per-GB-billed basis [@sentinel-xdr-connector][@ms-log-analytics-archive]. The two surfaces are not exclusive; the common operational pattern is in-portal for the hunting team (30 days, no per-GB cost) plus per-table Sentinel streaming for the analytics-rules team (extended retention, per-GB cost on selected tables).

&lt;p&gt;These are the questions. The seven layers between Maya&apos;s &lt;code&gt;cmd.exe&lt;/code&gt; at 9:14 a.m. and her Kusto row at 9:14:03 are how the answers actually work -- a kernel callback, a user-mode aggregator, an ETW publisher or TLS-pinned cloud forwarder, a regional Kusto ingest, a table write, and a KQL read, with two structural defenses (Antimalware-PPL and the Sysmon v15 protected-process gate) keeping each layer honest. Every other detection-engineering pattern in the Windows field is a configuration of those seven layers, and most of the open problems are at the seams between them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;See also.&lt;/strong&gt; The Sysmon driver&apos;s collection layer leans on the kernel-callback APIs documented in the &lt;a href=&quot;https://paragmali.com/blog/&quot; rel=&quot;noopener&quot;&gt;Windows process mitigations and Object Manager namespace&lt;/a&gt; articles in this series. The ETW transport bus that Sysmon publishes onto -- and that &lt;code&gt;EtwTi&lt;/code&gt; security events surface through -- is the subject of the dedicated ETW article in this series; the article goes deeper on provider GUIDs, manifests, and the eight-trace-session manifest-provider cap that bounds Sysmon&apos;s coexistence story in §10. The AMSI primary path that produces &lt;code&gt;DeviceEvents&lt;/code&gt; &lt;code&gt;ActionType = &quot;AmsiScriptDetection&quot;&lt;/code&gt; is the subject of the AMSI article; the two pipelines are siblings, not substitutes. And the Sigma rule corpus that compiles down into KQL for Defender XDR / Sentinel hunting is the same Sigma corpus that compiles into Splunk SPL and Elastic EQL -- the vendor-neutral query layer that sits above this article&apos;s KQL surface [@github-sigma].&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;sysmon-and-defender-for-endpoint-the-production-edr-telemetry-pipeline&quot; keyTerms={[
  { term: &quot;Sysmon&quot;, definition: &quot;Sysinternals tool by Russinovich and Garnier (August 2014, latest v15.2 March 2026) that uses Windows kernel callbacks plus a Filter Manager minifilter to collect 29 event types and publishes them via the Microsoft-Windows-Sysmon ETW provider to the Operational event log.&quot; },
  { term: &quot;Microsoft Defender for Endpoint (MDE)&quot;, definition: &quot;Microsoft&apos;s commercial cloud-correlated EDR. Renamed from Windows Defender ATP in September 2020. Runs as the Sense service (MsSense.exe) at Antimalware-PPL, shares the WdBoot ELAM and WdFilter minifilter substrate with Defender Antivirus, and lands events in the Advanced Hunting Kusto cluster.&quot; },
  { term: &quot;Microsoft Defender XDR&quot;, definition: &quot;The November 2023 rename of Microsoft 365 Defender. The unified portal at defender.microsoft.com that exposes Advanced Hunting on the Device* tables plus the cross-domain entity tables (AlertInfo, EmailEvents, IdentityLogonEvents, CloudAppEvents).&quot; },
  { term: &quot;Advanced Hunting&quot;, definition: &quot;The KQL-on-Device*-tables threat-hunting surface in Microsoft Defender XDR. 30 days of raw data, six core tables, the cross-domain entity table set, and a 100,000-row + 10-minute per-query quota.&quot; },
  { term: &quot;ProcessGuid&quot;, definition: &quot;Sysmon&apos;s per-process 128-bit GUID that survives PID reuse and uniquely identifies a process across its lifetime. The canonical join key for process-tree reconstruction.&quot; },
  { term: &quot;Antimalware-PPL&quot;, definition: &quot;Protected Process Light at the PROTECTED_ANTIMALWARE_LIGHT signer level. Prevents user-mode debugger attach, code injection, and OpenProcess-for-write from any caller not at an equal or higher PPL level. Gates MsSense.exe and Sysmon v15+.&quot; },
  { term: &quot;ELAM&quot;, definition: &quot;Early-Launch Antimalware. The Windows boot-order privilege that lets an Antimalware-EKU-signed driver (1.3.6.1.4.1.311.61.4.1) load before any non-ELAM driver and gate which non-ELAM drivers load. WdBoot.sys is ELAM; SysmonDrv.sys is not.&quot; },
  { term: &quot;DeviceProcessEvents&quot;, definition: &quot;The canonical reader-side Kusto table for MDE process-create events. ~50 columns including the InitiatingProcess* parent-process family. The MDE analogue of Sysmon EID 1.&quot; },
  { term: &quot;DeviceEvents&quot;, definition: &quot;The miscellaneous catch-all Kusto table. AMSI scan results, exploit-protection events, ASR rule fires, OpenProcess API calls, and other MDE-specific events surface here under ActionType discriminators.&quot; },
  { term: &quot;Sysmon EID 27 FileBlockExecutable&quot;, definition: &quot;Sysmon v14&apos;s (August 2022) first preventive event. The minifilter intercepts the file-handle close; if the rule matches and the content carries an MZ/PE header, Sysmon logs EID 27 and marks the file for deletion. The copy command produces no error and appears to succeed -- the file is then deleted at handle-close. Confined preventive surface; not a general-purpose application allowlist.&quot; },
  { term: &quot;sysmonconfig-mde-augment.xml&quot;, definition: &quot;Olaf Hartong&apos;s pre-generated Sysmon configuration that drops the EIDs MDE covers (1, 3, 7, 11, 12-14, 22) and keeps the EIDs MDE truncates or omits (8, 9, 10 verbose, 15, 17-18, 19-21, 23 archive). The detection-engineering-community default for MDE coexistence.&quot; },
  { term: &quot;FalconForce 2022 / CVE-2022-23278&quot;, definition: &quot;The dispositive published reverse-engineering of MsSense.exe debug techniques (dbgsrv.exe at WinTcb PPL via PPLKiller) and the disclosed cloud spoofing vulnerability patched by Microsoft on March 8 2022.&quot; },
  { term: &quot;InfoGuard Labs 2025&quot;, definition: &quot;The follow-on reverse-engineering of MDE cloud authorization. In-memory patch of CRYPT32!CertVerifyCertificateChainPolicy (mov eax,1; ret) to bypass certificate pinning, followed by disclosure of missing-authentication on /edr/commands/cnc and /senseir/v1/actions/ endpoints. MSRC classified low severity; no fix committed.&quot; }
]} questions={[
  { q: &quot;Why is calling Sysmon &apos;ETW-based&apos; only half-true?&quot;, a: &quot;ETW is Sysmon&apos;s *output* bus (Microsoft-Windows-Sysmon ETW provider feeding the user-mode service and downstream collectors), not its primary *input* source. Sysmon&apos;s driver collects via kernel callbacks: PsSetCreateProcessNotifyRoutineEx, PsSetLoadImageNotifyRoutine, PsSetCreateThreadNotifyRoutineEx, ObRegisterCallbacks(PsProcessType), CmRegisterCallbackEx, and Filter Manager minifilters (covering both ordinary file system and NPFS named pipes). Two input-side ETW-consumer exceptions exist: Microsoft-Windows-DNS-Client for EID 22 DNSEvent, and the WMI activity provider for EIDs 19-21 WmiEvent.&quot; },
  { q: &quot;Why does Hartong&apos;s sysmonconfig-mde-augment.xml exist as a community artifact rather than a Microsoft-published reference?&quot;, a: &quot;Microsoft does not publish a per-ActionType-to-per-kernel-callback cross-walk for the MDE EDR sensor. The community knows the Device* reader-side schema and the user-mode component inventory (Sense, MsSense.exe, SenseCncProxy.exe, SenseIR.exe, SenseNdr.exe), but not the kernel-callback inventory. Hartong reverse-engineered which Sysmon EIDs MDE truncates or omits and built the augment config to fill the gap.&quot; },
  { q: &quot;What architectural change does Sysmon v15 (June 2023) introduce, and what attack class does it close?&quot;, a: &quot;Sysmon v15 runs the user-mode service as PROTECTED_ANTIMALWARE_LIGHT, disallowing a wide range of user-mode interactions. The closed attack class is the SYSTEM-privilege user-mode tamper surface: sc stop, wevtutil clear of the Operational log, code injection into Sysmon.exe, ordinary debugger attach, and OpenProcess(PROCESS_TERMINATE). The residual attack surface is kernel-mode primitives, typically delivered via BYOVD.&quot; },
  { q: &quot;Where in the seven-layer pipeline does FalconForce 2022 intercept Defender for Endpoint?&quot;, a: &quot;Between layers 2 and 4. FalconForce raised dbgsrv.exe to WinTcb PPL (defeating layer 2&apos;s Antimalware-PPL protection on MsSense.exe), attached the elevated debug server to MsSense, and instrumented SspiCli!EncryptMessage to capture plaintext payloads before the TLS-with-cert-pinning transport (layer 4) ran. The plaintext capture surfaced CVE-2022-23278, which Microsoft patched March 8 2022.&quot; },
  { q: &quot;What are the four structural ceilings the EDR pipeline cannot lift?&quot;, a: &quot;(1) The pre-driver-load horizon: events before the EDR driver&apos;s DriverEntry are invisible; the ELAM boundary is the upstream bound. (2) The observation-vs-enforcement latency gap: Sysmon kernel-callback to event-log is sub-ms; MDE end-to-end to Kusto is seconds. (3) MDE schema truncation: ProcessAccess GrantedAccess masks, WMI consumer expressions, RawAccessRead, and PipeEvent are not surfaced in the Device* tables verbatim. (4) The kernel-mode adversary primitive: an attacker with a kernel write capability defeats HVCI + VBL + PPL + ELAM as a consequence of defeating the defenses themselves.&quot; },
  { q: &quot;Which Sysmon configuration is the correct starting point for a new deployment?&quot;, a: &quot;Depends on the deployment posture. For air-gapped / regulatory-no-cloud / unlicensed: NextronSystems/sysmon-config or olafhartong/sysmon-modular&apos;s default sysmonconfig.xml. For MDE-licensed environments with a detection-engineering team: olafhartong/sysmon-modular&apos;s sysmonconfig-mde-augment.xml. For MDE-licensed environments without a detection-engineering team: do not deploy Sysmon -- run MDE alone. The wrong starting point in any context is default Sysmon alongside MDE without the augment config.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows</category><category>edr</category><category>sysmon</category><category>defender-for-endpoint</category><category>etw</category><category>threat-hunting</category><category>kql</category><category>detection-engineering</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>AMSI: The Pre-Execution Window Where Defender Catches a Base64 Payload It Has Never Seen Before</title><link>https://paragmali.com/blog/amsi-the-pre-execution-window-defender/</link><guid isPermaLink="true">https://paragmali.com/blog/amsi-the-pre-execution-window-defender/</guid><description>How the Antimalware Scan Interface scans script content after deobfuscation but before execution, the seven runtimes it plugs into, and the nearly seven-year bypass arms race that followed.</description><pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate><content:encoded>
AMSI is a seven-function Win32 API plus a COM provider model that lets any script engine hand its post-deobfuscation buffer to a registered antimalware provider, synchronously, before the engine executes the buffer. Microsoft Defender&apos;s `MpOav.dll` is the default provider. It is the single most consequential malware-defense primitive Microsoft shipped between Authenticode and Smart App Control, and it is not, by Microsoft&apos;s own published position, a security boundary. This article walks the architecture, the seven-runtime call-site catalogue (PowerShell, WSH, Office VBA, Excel XLM, .NET 4.8, WMI, Windows 11 in-memory), the six bypass eras since 2016, and the open problems on the 2026 frontier.
&lt;h2&gt;1. A 200-Millisecond Story&lt;/h2&gt;
&lt;p&gt;A user opens a Word document attached to a phishing email. The macro decodes a base64 blob, XORs the result against a four-byte key cached in a worksheet cell, and pastes the cleartext into a string variable. The variable holds a single PowerShell command: an &lt;code&gt;Invoke-Expression&lt;/code&gt; of a 12-layer obfuscated stager whose final payload is &lt;code&gt;Invoke-Mimikatz&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Two hundred milliseconds later, &lt;a href=&quot;https://paragmali.com/blog/the-defenders-dilemma-microsoft-antivirus/&quot; rel=&quot;noopener&quot;&gt;Microsoft Defender&lt;/a&gt; flags the deobfuscated string &lt;code&gt;Invoke-Mimikatz&lt;/code&gt; and refuses to run it. Not the base64. Not the XOR. Not the macro. The actual deobfuscated PowerShell, in the form the PowerShell tokenizer was about to execute.&lt;/p&gt;
&lt;p&gt;No signature for this exact payload existed yesterday. The defender never read the document, never broke the encryption, and never emulated PowerShell. So how did it see the cleartext?&lt;/p&gt;
&lt;p&gt;The answer is a seven-function Win32 API called the Antimalware Scan Interface [@amsi-portal], or AMSI, and it is the single most consequential malware-defense primitive Microsoft has shipped since &lt;a href=&quot;https://paragmali.com/blog/windows-app-identity-33-year-reinvention/&quot; rel=&quot;noopener&quot;&gt;Authenticode&lt;/a&gt;. AMSI is the only Windows primitive that scans what the script engine actually decided to run, after every layer of obfuscation has been undone, and before the engine commits to running it.&lt;/p&gt;

A versatile Win32 interface standard that lets applications and services pass the post-deobfuscation buffer they are about to execute to any registered antimalware product on the machine. AMSI ships in `amsi.dll` and is integrated into PowerShell, Windows Script Host, Office VBA, Excel 4.0 macros, .NET Framework 4.8, WMI, and User Account Control, among other hosts [@amsi-portal][@msec-xlm-amsi-2021][@amsi-on-mdav].
&lt;p&gt;This article is for four audiences. Windows application developers who want to know how to integrate AMSI without introducing the usual four bugs. Detection engineers who want to know what AMSI emits, where, and how to hunt across it. Red-team operators who want to know which 2016-era bypasses still work in 2026 and which generate so much telemetry they are not worth the risk. AV and EDR vendors who want to register their own provider and not get out-competed by the default one.&lt;/p&gt;
&lt;p&gt;To understand how AMSI works, we have to understand why the 25 years of antivirus that preceded it could not.The 200-millisecond figure in the hook is approximate. Microsoft&apos;s August 2020 disclosure of Defender&apos;s pair-of-classifiers architecture [@msec-amsi-ml-2020] describes &quot;performance-optimized&quot; on-endpoint classifiers that hand off to the cloud only when content is classified as suspicious. The 200 ms in the scene above includes that cloud round trip.&lt;/p&gt;
&lt;h2&gt;2. Why Static AV Failed: 25 Years of the Obfuscation Arms Race&lt;/h2&gt;
&lt;p&gt;Consider a benign one-liner:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Write-Host &apos;pwnd!&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A signature on that exact byte string catches the lazy attacker, and only the lazy attacker. The next attacker writes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Write-Host (&apos;pwn&apos; + &apos;d!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The signature dies. So the defender starts emulating expression-evaluation; the attacker switches to &lt;code&gt;Invoke-Expression&lt;/code&gt; of a concatenated string; the defender starts emulating &lt;code&gt;Invoke-Expression&lt;/code&gt;; the attacker base64-encodes the inner script; the defender starts decoding base64 strings; the attacker XORs the base64 against a key cached in a worksheet cell; and at some point in this regress the antivirus engine is, in effect, a re-implementation of PowerShell, except slower, more buggy, and one Patch Tuesday behind. Lee Holmes called out the dead end explicitly in his June 9, 2015 disclosure: at the obfuscated leaf of this regress, &quot;we&apos;re generally past what antivirus engines will emulate or detect, so we won&apos;t necessarily detect what this script is actually doing,&quot; and even where a defender writes a signature for an obfuscator&apos;s pattern, &quot;a signature for it would generate an unacceptable number of false positives&quot; [@holmes-2015-wayback].&lt;/p&gt;
&lt;p&gt;The ladder was not theoretical. It was the operating reality of script-borne malware for 20 years.&lt;/p&gt;
&lt;p&gt;In 1995, WM/Concept [@wiki-concept] became the first widely propagated Word macro virus and established the scriptable-host-as-malware-surface architecture: a benign-looking document carrying executable VBA inside it. On May 4, 2000, a 10 KB VBScript called ILOVEYOU [@wiki-iloveyou] ran through Windows Script Host on roughly 10 percent of all internet-connected computers and caused an estimated US$10 to $15 billion in damages. ILOVEYOU made the architectural diagnosis unmistakable: built-in script engines are a malware-execution surface that defenders cannot wish away.&lt;/p&gt;
&lt;p&gt;By 2014, the surface had matured into a thriving offensive tradecraft: PowerSploit, PowerView, Invoke-Mimikatz, and the Empire C2 framework all ran fileless inside &lt;code&gt;powershell.exe&lt;/code&gt; memory after deobfuscation. On-disk antivirus saw only the encoded wrapper, not the deobfuscated payload that actually ran.&lt;/p&gt;
&lt;p&gt;Daniel Bohannon would close the file on signature-based defenses publicly at DerbyCon 6.0 on September 25, 2016 with Invoke-Obfuscation [@invoke-obfuscation], a PowerShell obfuscator that automated the regress above and turned every public-script signature into a one-bug-away walking target. Bohannon&apos;s release was a refutation, not a tool: it showed that any defender path that pattern-matched on obfuscation artifacts was a path to an unbounded backlog.&lt;/p&gt;
&lt;p&gt;The diagnosis that Holmes named in 2015 and that Bohannon proved a year later is structural. Detection must happen &lt;em&gt;after&lt;/em&gt; deobfuscation (so the obfuscation does not hide the payload), &lt;em&gt;before&lt;/em&gt; execution (so the detector can still refuse), and &lt;em&gt;in the engine that did the deobfuscation&lt;/em&gt; (because only that engine ever holds the deobfuscated bytes). In 2014, no Windows API did that. The next ten years are the story of building one.&lt;/p&gt;
&lt;h2&gt;3. The Pre-AMSI Patchwork&lt;/h2&gt;
&lt;p&gt;Before AMSI, Microsoft and the AV industry shipped four partial answers. Each one closed some of the gap. None closed all of it, because each one was wedged at the wrong place in the pipeline. Here is the timeline of what was tried, when, and what each attempt missed.&lt;/p&gt;

gantt
    title Pre-AMSI script-malware defense timeline
    dateFormat YYYY-MM
    axisFormat %Y&lt;pre&gt;&lt;code&gt;section Threats
WM/Concept (Word macro)        :done, threat1, 1995-08, 1825d
ILOVEYOU (WSH+VBScript)        :done, threat2, 2000-05, 1d
Fileless PowerShell era        :done, threat3, 2014-01, 730d
Invoke-Obfuscation release     :crit, threat4, 2016-09-25, 1d

section Defenses
IOfficeAntiVirus (file-open)   :defense1, 1997-01, 6570d
Module Logging Event 4103      :defense2, 2012-08, 1095d
Script Block Logging 4104      :defense3, 2015-07-29, 365d
AMSI in PowerShell 5.0         :crit, defense4, 2015-07-29, 1d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first attempt was &lt;code&gt;IOfficeAntiVirus&lt;/code&gt;, a COM interface Office 97 introduced in 1997 and that Office 2000 through Office 2010 carried forward. AV products implemented the interface; Office called into it at file-open time. The interface saw the document on disk, before VBA ran. It defeated the 1995-era macro virus that arrived with its payload literal in the document body. It defeated nothing once the VBA runtime started doing AutoOpen-time &lt;code&gt;Application.Run&lt;/code&gt; of strings decoded from cells, because the decoded string was never on disk. The Office 365 Threat Research team&apos;s 2018 retrospective on the limitation [@msec-vba-amsi-2018] is direct: file-open AV does not see what the VBA runtime decides to run at runtime.&lt;/p&gt;
&lt;p&gt;The second attempt was PowerShell module logging, shipped in PowerShell 3.0 in 2012 [@wiki-powershell] as Event ID 4103. It records, after the fact, that a cmdlet ran with a given parameter binding [@ps-logging-windows]. It is forensic, not preventive: by the time Event 4103 is in the Windows Event Log, the cmdlet has already returned. And it records the bound parameters, not the contents of &lt;code&gt;Invoke-Expression&lt;/code&gt;&apos;s argument string, so it sees the call but not the payload.&lt;/p&gt;
&lt;p&gt;The third attempt, shipped on July 29, 2015 alongside Windows 10 1507 and PowerShell 5.0, was Script Block Logging [@ps-logging-windows]. Script Block Logging emits Event ID 4104 with the deobfuscated script block, captured from inside the PowerShell parser on its way to the executor. This is the right artifact at the right moment in terms of &lt;em&gt;what&lt;/em&gt; it sees, but the wrong relationship in terms of &lt;em&gt;what it can do with what it sees&lt;/em&gt;: Event 4104 is asynchronous and observation-only. It cannot refuse the script that produced it. It can only tell the SOC what ran, after it ran.&lt;/p&gt;

A PowerShell 5.0 feature that records every deobfuscated script block to the `Microsoft-Windows-PowerShell/Operational` event log channel as Event 4104. It is a post-hoc forensic record: it captures the cleartext after the parser has emitted it on its way to the executor, but the executor still runs the script [@ps-logging-windows].
&lt;p&gt;The fourth attempt was the antivirus industry&apos;s own response to the gap: bring the script-engine emulators in-house. Implement a JScript emulator inside the AV engine, a VBScript emulator inside the AV engine, a PowerShell emulator inside the AV engine. Run the obfuscated source through your private emulator and inspect what comes out. This was the regress Holmes described as &quot;fragile&quot; in 2015. Every new feature in every shipped engine version was a maintenance bill the AV vendor had to pay. PowerShell shipped a new release every couple of years; JScript varied across IE6/IE7/IE8/Edge/WSH; VBScript varied across WSH and Office. The half-life of any one emulator was short.&lt;/p&gt;
&lt;p&gt;Lee Holmes summarized the dead end in one sentence in his June 9, 2015 post: &quot;antimalware software starts to do basic language emulation,&quot; but &quot;this is a fairly fragile approach&quot; [@holmes-2015-wayback]. The next paragraph in this article is the same paragraph in his.&lt;/p&gt;
&lt;h2&gt;4. The 2015 Eureka: Lee Holmes and the Birth of AMSI&lt;/h2&gt;
&lt;p&gt;On June 9, 2015, Lee Holmes published &lt;em&gt;Windows 10 to Offer Application Developers New Malware Defenses&lt;/em&gt; [@holmes-2015-wayback] on the Microsoft Security Blog. It is the most important malware-defense blog post Microsoft has ever shipped. The same day, Holmes also published &lt;em&gt;PowerShell the Blue Team&lt;/em&gt; [@holmes-blue-team], which named the assume-breach mindset that made AMSI&apos;s design possible.&lt;/p&gt;
&lt;p&gt;The architectural fix Holmes named is the one the previous section&apos;s frustration sets up. Applications hand the post-deobfuscation buffer to AMSI. AMSI hands it to a registered antimalware provider. The provider returns a verdict. If the verdict is &quot;malware,&quot; the application refuses to execute the buffer. The whole exchange happens synchronously, in the calling process, before the engine commits.&lt;/p&gt;

While the malicious script might go through several passes of deobfuscation, it ultimately needs to supply the scripting engine with plain, unobfuscated code.&lt;p&gt;-- Lee Holmes, Microsoft Security Blog, June 9, 2015
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The same observation appears verbatim on the live Microsoft Learn &lt;code&gt;how-amsi-helps&lt;/code&gt; page [@amsi-howto], which carries Holmes 2015&apos;s argument forward in Microsoft&apos;s current documentation: &quot;Script (malicious or otherwise), might go through several passes of de-obfuscation. But you ultimately need to supply the scripting engine with plain, un-obfuscated code.&quot; The dual primary-source anchor makes the citation durable against future Wayback rot.&lt;/p&gt;
&lt;p&gt;That one sentence is the design of AMSI in compressed form. The defender stops trying to reason about the obfuscated source. It reasons about what the engine decided to run. The engine&apos;s deobfuscation work is now the defender&apos;s free lunch.&lt;/p&gt;
&lt;p&gt;The release vehicle was Windows 10 1507 on July 29, 2015, paired with PowerShell 5.0 [@wiki-win10-versions]. The companion piece, &quot;PowerShell the Blue Team&quot; [@holmes-blue-team], framed the broader assume-breach posture: &quot;What did they do? What systems did they connect to? Was any dynamic code invoked, and what was it?&quot; The trio of features Holmes shipped that day -- AMSI, Script Block Logging, and the over-the-shoulder transcripts -- was designed to answer those three questions together.The companion &quot;PowerShell heart the Blue Team&quot; devblogs post is not optional reading if you want the full context. Holmes published the two posts on the same day for a reason: AMSI is the synchronous-blocking sibling, Script Block Logging is the forensic sibling, and Constrained Language Mode is the policy-denial sibling. The trio is co-designed [@holmes-blue-team].&lt;/p&gt;
&lt;p&gt;The architectural insight that closed the loop is small to state and large to absorb. For 20 years the AV industry had been arguing about what to &lt;em&gt;scan&lt;/em&gt;. Holmes pointed out that the answer was about &lt;em&gt;when&lt;/em&gt; to scan. The naive on-disk and on-event-log approaches had failed not because their pattern matching was poor but because they were inspecting the wrong artifact at the wrong moment. The only software that ever holds the deobfuscated bytes is the engine that will execute them. The only moment that artifact exists is the moment just before the executor commits. The only place a defender can stand and see the buffer is inside that engine&apos;s process.&lt;/p&gt;
&lt;p&gt;That is the answer Holmes named, and it is the answer Microsoft has spent the last ten years implementing across seven runtimes and defending against six bypass eras. The next section is the architecture of what Holmes named.&lt;/p&gt;
&lt;h2&gt;5. The AMSI Architecture: Two API Surfaces, One Provider Model&lt;/h2&gt;
&lt;p&gt;AMSI is two API surfaces (flat C and COM) and one provider model. The flat-C surface is what script-engine hosts call; the COM surface is what AV providers implement. Both surfaces converge on the same &lt;code&gt;amsi.dll&lt;/code&gt;, and &lt;code&gt;amsi.dll&lt;/code&gt; runs in the calling process. Here is the full hot path for one PowerShell command.&lt;/p&gt;

sequenceDiagram
    autonumber
    participant User
    participant PS as powershell.exe
    participant AU as AmsiUtils.ScanContent
    participant AD as amsi.dll
    participant MP as MpOav.dll (in-process)
    participant ME as MsMpEng.exe (PPL)&lt;pre&gt;&lt;code&gt;User-&amp;gt;&amp;gt;PS: iex ([Convert]::FromBase64String($stager))
PS-&amp;gt;&amp;gt;PS: tokenize, expand, deobfuscate
PS-&amp;gt;&amp;gt;AU: ScanContent(buf, name, session)
AU-&amp;gt;&amp;gt;AD: AmsiScanBuffer(ctx, buf, len, name, session, out result)
AD-&amp;gt;&amp;gt;MP: IAntimalwareProvider::Scan(stream)
MP-&amp;gt;&amp;gt;ME: local RPC: scan(stream)
ME--&amp;gt;&amp;gt;MP: AMSI_RESULT_DETECTED (&amp;gt;= 32768)
MP--&amp;gt;&amp;gt;AD: HRESULT S_OK, result set
AD--&amp;gt;&amp;gt;AU: AMSI_RESULT_DETECTED
AU--&amp;gt;&amp;gt;PS: AmsiResultIsMalware(result) == TRUE
PS--&amp;gt;&amp;gt;User: ParseException: script content is malicious
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.1 The Win32 flat-C API&lt;/h3&gt;
&lt;p&gt;The flat-C surface is seven functions, declared in &lt;code&gt;amsi.h&lt;/code&gt;, exported from &lt;code&gt;amsi.dll&lt;/code&gt;, with minimum support Windows 10 / Windows Server 2016 [@amsi-scanbuffer]. A host typically calls them in this order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;AmsiInitialize(LPCWSTR appName, HAMSICONTEXT *amsiContext)&lt;/code&gt; once at startup. The &lt;code&gt;appName&lt;/code&gt; string identifies the host: PowerShell passes &lt;code&gt;&quot;PowerShell_&amp;lt;GUID&amp;gt;&quot;&lt;/code&gt;, .NET passes &lt;code&gt;&quot;DotNet&quot;&lt;/code&gt;, Office passes its application name [@amsi-initialize]. The string later surfaces in telemetry as &lt;code&gt;DeviceEvents.AmsiProcessName&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AmsiOpenSession(HAMSICONTEXT, HAMSISESSION *session)&lt;/code&gt; per logical user command. The session handle is a correlation primitive: multiple &lt;code&gt;AmsiScanBuffer&lt;/code&gt; calls inside one session let the provider re-join partial deobfuscations into one decision [@amsi-opensession].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AmsiScanBuffer(ctx, buffer, length, contentName, session, &amp;amp;result)&lt;/code&gt; per buffer. This is the hot path. &lt;code&gt;contentName&lt;/code&gt; is a human-readable label the SOC analyst will see [@amsi-scanbuffer].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AmsiResultIsMalware(result)&lt;/code&gt; to interpret the out parameter. The macro evaluates to non-zero when the AMSI_RESULT is at or above 32768 [@amsi-resultismalware].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AmsiCloseSession&lt;/code&gt; to release the session handle.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AmsiUninitialize&lt;/code&gt; at shutdown.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The seventh function, &lt;code&gt;AmsiScanString&lt;/code&gt;, is a thin wrapper that takes a wide-character string instead of a buffer-plus-length pair. Microsoft replaced PowerShell&apos;s &lt;code&gt;AmsiScanString&lt;/code&gt; call site with &lt;code&gt;AmsiScanBuffer&lt;/code&gt; in Windows 10 1709 as part of the response to the first CyberArk in-memory patch attack [@cyberark-redux]; we will return to that in §8.&lt;/p&gt;

The flat-C Win32 function any AMSI-aware host calls to submit a buffer for scanning. Signature: `HRESULT AmsiScanBuffer(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result)`. Returns S_OK on a completed scan; the verdict is delivered through the `result` out parameter. Minimum support Windows 10 desktop / Windows Server 2016 [@amsi-scanbuffer].
&lt;p&gt;The &lt;code&gt;AMSI_RESULT&lt;/code&gt; enumeration is the interface contract for verdicts. The values are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;Value&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Semantics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;
&lt;td&gt;AMSI_RESULT_CLEAN&lt;/td&gt;
&lt;td&gt;Known clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;
&lt;td&gt;AMSI_RESULT_NOT_DETECTED&lt;/td&gt;
&lt;td&gt;Unknown but not malicious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;16384 (0x4000)&lt;/td&gt;
&lt;td&gt;AMSI_RESULT_BLOCKED_BY_ADMIN_START&lt;/td&gt;
&lt;td&gt;Policy block (range start)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;20479 (0x4FFF)&lt;/td&gt;
&lt;td&gt;AMSI_RESULT_BLOCKED_BY_ADMIN_END&lt;/td&gt;
&lt;td&gt;Policy block (range end)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;32768 (0x8000)&lt;/td&gt;
&lt;td&gt;AMSI_RESULT_DETECTED&lt;/td&gt;
&lt;td&gt;Provider verdict: malicious; &lt;code&gt;AmsiResultIsMalware&lt;/code&gt; true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Any return value at or above 32768 is malware; values 16384 to 20479 are administrative policy blocks (e.g. AppLocker / &lt;a href=&quot;https://paragmali.com/blog/wdac--hvci-code-integrity-at-every-layer-in-windows/&quot; rel=&quot;noopener&quot;&gt;WDAC&lt;/a&gt;), and values 0 and 1 are negative results [@amsi-result-enum]. The split between 16384 and 32768 lets a host distinguish &quot;the AV refused this&quot; from &quot;policy refused this,&quot; which lets the host display different error messages.&lt;/p&gt;
&lt;h3&gt;5.2 The COM surface&lt;/h3&gt;
&lt;p&gt;For streamable content (Office macros, .NET assemblies loaded from memory, large IM payloads), the flat-C buffer-plus-length call is the wrong abstraction. AMSI&apos;s COM surface, &lt;code&gt;IAmsiStream&lt;/code&gt; plus &lt;code&gt;IAntimalwareProvider&lt;/code&gt;, lets the host hand a stream callback to the provider and lets the provider pull as much content as it wants [@amsi-iantimalware]. The reference implementation is in Microsoft&apos;s Windows-classic-samples AmsiProvider [@amsi-sample] repository.&lt;/p&gt;
&lt;p&gt;Rule of thumb: COM/stream for streamable content, flat-C for one-shot buffers. Both end up at the same provider through the same in-process load.&lt;/p&gt;
&lt;h3&gt;5.3 The provider model&lt;/h3&gt;
&lt;p&gt;AMSI providers are in-process COM servers. Registration writes two registry trees [@amsi-devaudience]:&lt;/p&gt;

flowchart TD
    A[Provider DLL: vendor implements IAntimalwareProvider] --&amp;gt; B[regsvr32 vendor.dll]
    B --&amp;gt; C[&quot;HKLM Software Classes CLSID {CLSID} InprocServer32 = vendor.dll&quot;]
    B --&amp;gt; D[&quot;HKLM Software Classes CLSID {CLSID} InprocServer32 ThreadingModel = Both&quot;]
    B --&amp;gt; E[&quot;HKLM Software Microsoft AMSI Providers {CLSID} = present&quot;]
    C --&amp;gt; F[amsi.dll AmsiInitialize]
    D --&amp;gt; F
    E --&amp;gt; F
    F --&amp;gt; G[CoCreateInstance for each registered CLSID]
    G --&amp;gt; H[Provider loaded in-process; called on every AmsiScanBuffer]
&lt;p&gt;The first tree, &lt;code&gt;HKLM\SOFTWARE\Classes\CLSID\{CLSID}&lt;/code&gt;, is standard COM. It names the provider DLL and the ThreadingModel (which must be &lt;code&gt;Both&lt;/code&gt;; marshaling proxies would defeat the in-process performance assumption). The second tree, &lt;code&gt;HKLM\SOFTWARE\Microsoft\AMSI\Providers\{CLSID}&lt;/code&gt;, is the AMSI-specific opt-in. &lt;code&gt;amsi.dll&lt;/code&gt; enumerates the Providers subkey at &lt;code&gt;AmsiInitialize&lt;/code&gt; time, calls &lt;code&gt;CoCreateInstance&lt;/code&gt; for each one in-process, and then calls each provider on every subsequent &lt;code&gt;AmsiScanBuffer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Two security mitigations have hardened that load over time. Windows 10 1709 (October 17, 2017) tightened the loader rules: provider DLLs must &lt;code&gt;LoadLibrary&lt;/code&gt; their dependencies with full paths, or the DLL hijack mitigations will refuse to satisfy unqualified loads [@amsi-devaudience]. Windows 10 1903 (May 21, 2019) added an optional Authenticode signing check: when &lt;code&gt;HKLM\SOFTWARE\Microsoft\AMSI\FeatureBits&lt;/code&gt; is set to &lt;code&gt;0x2&lt;/code&gt;, unsigned provider DLLs are refused [@amsi-iantimalware].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you ship an AMSI provider, Authenticode-sign the provider DLL. Windows 10 1903 introduced an opt-in signing check at &lt;code&gt;HKLM\SOFTWARE\Microsoft\AMSI\FeatureBits = 0x2&lt;/code&gt;. Several large enterprise customers set that bit, and unsigned provider DLLs will silently refuse to load on those machines [@amsi-iantimalware].&lt;/p&gt;
&lt;/blockquote&gt;

An in-process COM server (DLL) that implements `IAntimalwareProvider` and is registered under two registry trees: the standard COM CLSID tree under `HKLM\Software\Classes\CLSID\{CLSID}` and the AMSI-specific opt-in tree under `HKLM\Software\Microsoft\AMSI\Providers\{CLSID}`. `amsi.dll` loads every registered provider into the scanning host&apos;s process at `AmsiInitialize` time [@amsi-devaudience].
&lt;p&gt;The &lt;code&gt;Both&lt;/code&gt; threading model is mandatory for AMSI providers. AMSI calls into the provider on whatever thread the host happens to be running, and marshaling proxies would add cross-apartment round trips that destroy the in-process performance assumption [@amsi-devaudience].&lt;/p&gt;
&lt;h3&gt;5.4 The default provider: MpOav.dll&lt;/h3&gt;
&lt;p&gt;Microsoft Defender&apos;s AMSI provider is &lt;code&gt;MpOav.dll&lt;/code&gt;. CLSID &lt;code&gt;{2781761E-28E0-4109-99FE-B9D127C57AFE}&lt;/code&gt;. Path &lt;code&gt;%ProgramData%\Microsoft\Windows Defender\Platform\&amp;lt;version&amp;gt;\MpOav.dll&lt;/code&gt; [@redcanary-amsi]. It loads in-process to the scanning application: into &lt;code&gt;powershell.exe&lt;/code&gt;, into &lt;code&gt;winword.exe&lt;/code&gt;, into &lt;code&gt;wscript.exe&lt;/code&gt;. It does not do the heavy lifting; it bridges out to &lt;code&gt;MsMpEng.exe&lt;/code&gt; via local RPC for the signature engine, cloud reputation lookup, and the on-endpoint machine-learning model.&lt;/p&gt;

Microsoft Defender&apos;s AMSI provider DLL, located at `%ProgramData%\Microsoft\Windows Defender\Platform\\MpOav.dll`. Loaded in-process to the scanning application; bridges to `MsMpEng.exe` via local RPC for the heavy-lifting scan [@redcanary-amsi].
&lt;p&gt;&lt;code&gt;MpOav.dll&lt;/code&gt; lives in the scanning host&apos;s address space (&lt;code&gt;powershell.exe&lt;/code&gt;, &lt;code&gt;winword.exe&lt;/code&gt;, ...), not in &lt;code&gt;MsMpEng.exe&lt;/code&gt;. Defender&apos;s &lt;a href=&quot;https://paragmali.com/blog/windows-app-identity-33-year-reinvention/&quot; rel=&quot;noopener&quot;&gt;Protected Process Light&lt;/a&gt; hardening protects &lt;code&gt;MsMpEng.exe&lt;/code&gt;&apos;s process, but it does &lt;em&gt;not&lt;/em&gt; protect the AMSI provider DLL that gets loaded into PowerShell. That asymmetry is the basis of every in-process bypass in §8 [@redcanary-amsi].&lt;/p&gt;
&lt;h3&gt;5.5 Sessions, correlation, content names&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;HAMSISESSION&lt;/code&gt; handle returned by &lt;code&gt;AmsiOpenSession&lt;/code&gt; is the correlation primitive. If a single PowerShell command produces three deobfuscation steps that yield three &lt;code&gt;AmsiScanBuffer&lt;/code&gt; calls, sharing one session across all three lets the provider join them: &quot;I just saw a base64 alphabet, then a key-rotation pattern, then &lt;code&gt;Invoke-Mimikatz&lt;/code&gt;. Verdict: malicious. Reason: the three together are the obfuscation chain.&quot; The session-shared verdict is more informative than any single buffer would be in isolation [@amsi-opensession].&lt;/p&gt;

An opaque correlation handle returned by `AmsiOpenSession`. Multiple `AmsiScanBuffer` calls that share a `HAMSISESSION` value belong to one logical user command; the provider may re-join their partial deobfuscations into a single verdict [@amsi-opensession].
&lt;p&gt;The &lt;code&gt;contentName&lt;/code&gt; argument to &lt;code&gt;AmsiScanBuffer&lt;/code&gt; is what the SOC analyst sees in &lt;code&gt;DeviceEvents.FileName&lt;/code&gt; at hunt time. Hosts that pass a meaningful &lt;code&gt;contentName&lt;/code&gt; (the script-block ID, the assembly&apos;s friendly name, the URL the macro came from) give the SOC the breadcrumb they need to triage; hosts that pass a random GUID or an empty string give the SOC a column of noise [@deviceevents-table].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; AMSI&apos;s value comes from running inside the same process as the script engine, because that is the only place that ever holds the deobfuscated bytes. Every weakness AMSI has also comes from running inside the same process, because anyone with code execution there can mute it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We now know what AMSI is. The next section walks every shipping integration in Windows 10 and 11, and reveals that AMSI was not, in 2015, where most Windows scripted-content malware actually ran.&lt;/p&gt;
&lt;h2&gt;6. The Call-Site Catalogue: Where AMSI Plugs Into Windows&lt;/h2&gt;
&lt;p&gt;AMSI shipped in &lt;code&gt;amsi.dll&lt;/code&gt; in 2015, but &lt;code&gt;amsi.dll&lt;/code&gt; exporting &lt;code&gt;AmsiScanBuffer&lt;/code&gt; does not scan anything by itself. It scans whatever any host process bothers to hand it. The story of AMSI between 2015 and 2021 is one host integration at a time. Here is the order they shipped.&lt;/p&gt;

gantt
    title AMSI integration by runtime
    dateFormat YYYY-MM
    axisFormat %Y&lt;pre&gt;&lt;code&gt;PowerShell 5.0          :ps, 2015-07, 3650d
Windows Script Host     :wsh, 2015-07, 3650d
Office VBA              :vba, 2018-09, 2555d
.NET Framework 4.8      :dn, 2019-04, 2555d
WMI scripting           :wmi, 2019-05, 2555d
Excel 4.0 macros (XLM)  :xlm, 2021-03, 1825d
Win11 in-memory scripts :w11, 2021-10, 1825d
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PowerShell 5.0 (July 29, 2015)&lt;/h3&gt;
&lt;p&gt;PowerShell is the reference integration. The PowerShell host calls &lt;code&gt;System.Management.Automation.AmsiUtils.ScanContent&lt;/code&gt;, which (after a one-time check on the &lt;code&gt;amsiInitFailed&lt;/code&gt; flag and a lazy &lt;code&gt;AmsiInitialize&lt;/code&gt;) calls &lt;code&gt;AmsiNativeMethods.AmsiScanBuffer&lt;/code&gt; on the deobfuscated script block [@psa-clr-hooking]. The integration matches Holmes&apos;s design intent verbatim: the buffer handed to AMSI is the buffer the executor is about to run.&lt;/p&gt;
&lt;h3&gt;Windows Script Host (2015)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;wscript.exe&lt;/code&gt; and &lt;code&gt;cscript.exe&lt;/code&gt;, the hosts that ran ILOVEYOU in 2000, integrate AMSI in the same release vehicle as PowerShell 5.0 [@amsi-portal]. Every JScript or VBScript source goes through &lt;code&gt;AmsiScanBuffer&lt;/code&gt; before WSH executes it, and runtime eval-style constructions (&lt;code&gt;new ActiveXObject(&apos;WScript.Shell&apos;).Run(...)&lt;/code&gt; with a dynamically built command line) get scanned at the point where the runtime resolves them.&lt;/p&gt;
&lt;h3&gt;Office VBA (September 12, 2018)&lt;/h3&gt;
&lt;p&gt;The Office VBA integration was the first non-script-engine AMSI host, and it used a new abstraction: the trigger-buffer architecture. The VBA runtime maintains a circular buffer of Win32, COM, and VBA API calls plus their arguments. When VBA observes a high-risk trigger -- &lt;code&gt;Shell&lt;/code&gt; invocation, &lt;code&gt;CreateObject(&quot;WScript.Shell&quot;)&lt;/code&gt;, &lt;code&gt;Application.Run&lt;/code&gt; of a decoded string -- it halts the macro and flushes the circular buffer through &lt;code&gt;AmsiScanBuffer&lt;/code&gt; [@amsi-howto].&lt;/p&gt;

The dispatch pattern used by Office VBA and Excel 4.0 AMSI integrations. The runtime maintains a circular buffer of API calls and arguments and flushes it through `AmsiScanBuffer` when a high-risk trigger (e.g. `CreateObject(&quot;WScript.Shell&quot;)`, `Shell()`, a file-write API) fires. The provider sees the trigger plus its prior-API context, not just one isolated call [@amsi-howto].

Office 365 client applications now integrate with Antimalware Scan Interface (AMSI), enabling antivirus and other security solutions to scan macros and other scripts at runtime to check for malicious behavior.&lt;p&gt;-- Microsoft Office 365 Threat Research, September 12, 2018
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The Office team published the design in the September 12, 2018 announcement [@msec-vba-amsi-2018]. The architectural payoff: a provider sees not just one trigger call but the macro&apos;s prior-API context, which is what distinguishes &lt;code&gt;Application.Run(&quot;notepad.exe&quot;)&lt;/code&gt; from &lt;code&gt;Application.Run(&amp;lt;base64-decoded-PowerShell&amp;gt;)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;.NET Framework 4.8 (April 2019)&lt;/h3&gt;
&lt;p&gt;The next gap was in-memory .NET. &lt;code&gt;Assembly.Load(byte[])&lt;/code&gt;, the load path Cobalt Strike&apos;s &lt;code&gt;execute-assembly&lt;/code&gt; command and Sliver&apos;s SharpLoader use, did not produce a file on disk and did not generate any of the file-system events on-disk AV depended on. .NET Framework 4.8 closed it: &quot;In previous versions of .NET Framework, Windows Defender or third-party antimalware software would automatically scan all assemblies loaded from disk for malware. However, assemblies loaded from elsewhere, such as by using &lt;code&gt;Assembly.Load(byte[])&lt;/code&gt;, would not be scanned ... .NET Framework 4.8 on Windows 10 triggers scans for those assemblies by Windows Defender and many other antimalware solutions that implement the Antimalware Scan Interface&quot; [@dotnet-48].&lt;/p&gt;
&lt;h3&gt;WMI scripting (Windows 10 1903, May 2019)&lt;/h3&gt;
&lt;p&gt;WMI is, in the abstract, an RPC protocol and a query language, but it is also a code-execution surface (&lt;code&gt;__EventConsumer&lt;/code&gt; persistence; &lt;code&gt;Win32_Process.Create&lt;/code&gt; lateral movement). The 1903 [@wiki-win10-versions] AMSI integration scans WMI scripting paths [@amsi-on-mdav], closing the persistence pivot that had been a favorite of post-exploitation toolkits since 2012.&lt;/p&gt;
&lt;h3&gt;Excel 4.0 macros (March 3, 2021)&lt;/h3&gt;
&lt;p&gt;XLM macros, the language that Microsoft Excel introduced in 1992 (one year before VBA, which arrived in 1993), is the textbook example of a runtime that never died. Attackers rediscovered XLM in 2019 and 2020: Trickbot, Zloader, and Ursnif campaigns all used XLM4 macros to bypass VBA-focused defenses. Microsoft retrofitted the trigger-buffer architecture from VBA to XLM and shipped on March 3, 2021 [@msec-xlm-amsi-2021]. The Microsoft post enumerates the full AMSI host list as of 2021: &quot;Office VBA macros; JScript; VBScript; PowerShell; WMI; Dynamically loaded .NET assemblies; MSHTA/Jscript9.&quot;&lt;/p&gt;
&lt;h3&gt;Windows 11 in-memory script scanning (2021+)&lt;/h3&gt;
&lt;p&gt;AMSI coverage has continued to expand in current Defender releases on Windows 10 and Windows 11 beyond the script-engine hosts above; the precise call-site list is documented per-Defender-release rather than in a single canonical Microsoft Learn page. The current Defender AMSI host list reads: &quot;PowerShell; JScript; VBScript; Windows Script Host (wscript.exe and cscript.exe); .NET Framework 4.8 or newer (scanning of all assemblies); Windows Management Instrumentation (WMI)&quot; [@amsi-on-mdav]. Living-Off-the-Land Binary (LOLBin) paths that bypassed the classic script-engine entry points have become a continuing focus of Defender&apos;s per-release AMSI extensions.&lt;/p&gt;

For detection engineers: the `appName` string you pass to `AmsiInitialize` becomes `DeviceEvents.AmsiProcessName` in the Defender XDR advanced-hunting schema, and the `contentName` you pass to `AmsiScanBuffer` becomes the human-readable label the SOC analyst triages [@deviceevents-table].&lt;p&gt;If you are &lt;em&gt;integrating&lt;/em&gt; a new host, set &lt;code&gt;contentName&lt;/code&gt; to the script-block ID, the assembly&apos;s friendly name, or the URL the macro came from. Never set it to a random GUID, never set it to an empty string. Future-you, hunting at 2 a.m., will thank present-you.&lt;/p&gt;
&lt;p&gt;If you are &lt;em&gt;hunting&lt;/em&gt;, the &lt;code&gt;AmsiProcessName&lt;/code&gt; column tells you which host did the scan, which lets you quickly distinguish a PowerShell payload that landed via &lt;code&gt;winword.exe&lt;/code&gt; (Office VBA -&amp;gt; Shell -&amp;gt; powershell.exe) from one that landed via &lt;code&gt;outlook.exe&lt;/code&gt; (link click -&amp;gt; Edge -&amp;gt; PowerShell). The two have completely different lateral-movement implications.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Seven runtimes, one API. The contract is that each one phones home before it runs your code. The next section is how the seven streams converge into one analyst&apos;s pane of glass.&lt;/p&gt;
&lt;h2&gt;7. AMSI Meets ETW: The Correlation Story&lt;/h2&gt;
&lt;p&gt;The architectural dichotomy fits in one sentence: AMSI is synchronous and can block; &lt;a href=&quot;https://paragmali.com/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/&quot; rel=&quot;noopener&quot;&gt;Event Tracing for Windows (ETW)&lt;/a&gt; is asynchronous and observation-only. They share the same data, the same provider, and the same calling convention, but they answer different questions. AMSI is for &lt;em&gt;decisions&lt;/em&gt;. ETW is for &lt;em&gt;correlation&lt;/em&gt; and &lt;em&gt;survives in-process bypass&lt;/em&gt;.&lt;/p&gt;

flowchart LR
    A[powershell.exe / winword.exe&lt;br /&gt;scanning host] --&amp;gt; B[amsi.dll AmsiScanBuffer prologue]
    B --&amp;gt; C[ETW provider 2A576B87-09A7-520E-C21A-4942F0271D67 emit event]
    B --&amp;gt; D[MpOav.dll IAntimalwareProvider Scan]
    D --&amp;gt; E[MsMpEng.exe verdict]
    E --&amp;gt; F[result returned synchronously to host]
    F --&amp;gt; G[host refuses or allows execution]
    C --&amp;gt; H[Defender ATP DeviceEvents AmsiScriptDetection]
    C --&amp;gt; I[Third-party EDR via Antimalware-PPL]
    C --&amp;gt; J[Sysmon SilkETW Sealighter on-host]
&lt;p&gt;The ETW provider name is &lt;code&gt;Microsoft-Antimalware-Scan-Interface&lt;/code&gt; and its GUID is &lt;code&gt;{2A576B87-09A7-520E-C21A-4942F0271D67}&lt;/code&gt; [@etw-manifest]. It emits a structured event for every &lt;code&gt;AmsiScanBuffer&lt;/code&gt; call. The event template has ten fields: &lt;code&gt;session&lt;/code&gt;, &lt;code&gt;scanStatus&lt;/code&gt;, &lt;code&gt;scanResult&lt;/code&gt;, &lt;code&gt;appname&lt;/code&gt;, &lt;code&gt;contentname&lt;/code&gt;, &lt;code&gt;contentsize&lt;/code&gt;, &lt;code&gt;originalsize&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;hash&lt;/code&gt;, &lt;code&gt;contentFiltered&lt;/code&gt;. The &lt;code&gt;content&lt;/code&gt; field is the deobfuscated buffer that just got scanned. That is the basis of every downstream telemetry product.&lt;/p&gt;

The Event Tracing for Windows provider with GUID `{2A576B87-09A7-520E-C21A-4942F0271D67}` that emits a structured event for every `AmsiScanBuffer` call. The event template carries the deobfuscated content, the AMSI result, the host&apos;s `appName`, and the host&apos;s `contentName`. Consumed by Defender, by third-party EDRs once they have Antimalware-PPL onboarded, and by community tools like SilkETW and Sealighter on individual hosts [@etw-manifest].
&lt;p&gt;Defender&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; consumes the provider; third-party EDRs consume it once they have Antimalware-PPL onboarded; on individual hosts, community tools like SilkETW and Sealighter against the GUID let an analyst capture every scan on an air-gapped machine without a cloud connection.&lt;/p&gt;
&lt;p&gt;In Microsoft Defender for Endpoint, the same event surfaces in the &lt;code&gt;DeviceEvents&lt;/code&gt; table with &lt;code&gt;ActionType == &quot;AmsiScriptDetection&quot;&lt;/code&gt;, and the &lt;code&gt;AmsiData&lt;/code&gt; column carries the deobfuscated content, &lt;code&gt;AmsiPatchedTextInResult&lt;/code&gt; carries any provider-side rewriting, and &lt;code&gt;AmsiProcessName&lt;/code&gt; carries the host&apos;s &lt;code&gt;appName&lt;/code&gt; [@deviceevents-table]. The hunting community has converged on a few canonical patterns. Here is one of them: join the AMSI detection back to its parent process command line to recover the full attack chain.&lt;/p&gt;

DeviceEvents
| where ActionType == &quot;AmsiScriptDetection&quot;
| extend Description = tostring(parse_json(AdditionalFields).Description)
| project Timestamp, DeviceName, DeviceId, InitiatingProcessCommandLine,
          InitiatingProcessParentFileName, Description, ReportId
| join kind=leftouter (
    DeviceProcessEvents
    | project ProcessCommandLine, InitiatingProcessCommandLine,
              InitiatingProcessFolderPath, DeviceId, ReportId
  ) on DeviceId
| where Timestamp &amp;gt; ago(7d)
| sort by Timestamp desc
&lt;p&gt;The query is adapted from Bert-JanP&apos;s &lt;code&gt;AMSIScriptDetections.md&lt;/code&gt; hunting pack [@bertjan-amsi-queries] and maps each detection to MITRE T1059.001 -- Command and Scripting Interpreter: PowerShell [@attack-t1059-001]. The shape of the join is the load-bearing part: AMSI gives you the &lt;em&gt;what&lt;/em&gt; (the deobfuscated buffer), and &lt;code&gt;DeviceProcessEvents&lt;/code&gt; gives you the &lt;em&gt;how&lt;/em&gt; (the parent process and its command line). Together they are the full attack chain.The ETW provider runs from inside &lt;code&gt;AmsiScanBuffer&lt;/code&gt;&apos;s prologue, not at the (possibly bypass-clobbered) return. This is why a Cornelis de Plaa / Outflank 2020 hardware-breakpoint bypass that perfectly hides the scan &lt;em&gt;result&lt;/em&gt; still leaks ETW telemetry: the prologue emit happens before the breakpoint fires. The provider sees the scan happened; only the verdict is muted [@ethicalchaos].&lt;/p&gt;
&lt;p&gt;AMSI hands out the deobfuscated buffer; ETW makes sure someone saw it happen. The attacker&apos;s job for the next seven years was to make neither happen. Here is how that went.&lt;/p&gt;
&lt;h2&gt;8. The Bypass Arms Race: Six Eras in Nearly Seven Years&lt;/h2&gt;
&lt;p&gt;In seven years, attackers have generated six distinct bypass eras. Each era was the &lt;em&gt;necessary consequence&lt;/em&gt; of AMSI&apos;s same-process trust model. Each era&apos;s defeat by Defender required a new architectural insight, not a new signature. Here is the bird&apos;s-eye view.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;Era&lt;/th&gt;
&lt;th&gt;First public&lt;/th&gt;
&lt;th&gt;Attacker / source&lt;/th&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Defender response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;
&lt;td&gt;May 2016&lt;/td&gt;
&lt;td&gt;Matt Graeber (tweet)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AmsiUtils.amsiInitFailed = true&lt;/code&gt; via reflection&lt;/td&gt;
&lt;td&gt;String signature on field-and-class proximity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;2&lt;/td&gt;
&lt;td&gt;February 2018&lt;/td&gt;
&lt;td&gt;Avi Gimpel and Zeev Ben Porat (CyberArk Labs)&lt;/td&gt;
&lt;td&gt;In-memory patch of &lt;code&gt;AmsiScanString&lt;/code&gt;; May 2018 redux patches &lt;code&gt;AmsiScanBuffer&lt;/code&gt; (&lt;code&gt;xor edi, edi&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Windows 10 1709 swaps to &lt;code&gt;AmsiScanBuffer&lt;/code&gt;; byte-pattern signature on common patches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;3&lt;/td&gt;
&lt;td&gt;2018-2019&lt;/td&gt;
&lt;td&gt;Various practitioners&lt;/td&gt;
&lt;td&gt;Obfuscated patches: hashed &lt;code&gt;GetProcAddress&lt;/code&gt;, indirect writes, randomized bytes&lt;/td&gt;
&lt;td&gt;Behavior signature: detect any write into &lt;code&gt;amsi.dll&lt;/code&gt; code section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;4&lt;/td&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;Various practitioners&lt;/td&gt;
&lt;td&gt;Register a malicious CLSID under &lt;code&gt;HKLM\...\AMSI\Providers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Windows 10 1709 DLL-hijack mitigation; 1903 Authenticode signing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;5&lt;/td&gt;
&lt;td&gt;January 29, 2020&lt;/td&gt;
&lt;td&gt;Cornelis de Plaa (Outflank)&lt;/td&gt;
&lt;td&gt;DR0 hardware breakpoint plus Vectored Exception Handler; no memory patch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Microsoft-Windows-Kernel-Audit-API-Calls&lt;/code&gt; ETW &lt;code&gt;NtSetContextThread&lt;/code&gt; event (admin-side, no PPL gate); &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; on the remote-thread subset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;6&lt;/td&gt;
&lt;td&gt;December 2022&lt;/td&gt;
&lt;td&gt;Practical Security Analytics&lt;/td&gt;
&lt;td&gt;Patch CLR-side &lt;code&gt;Assembly.Load(byte[])&lt;/code&gt; AMSI callout in &lt;code&gt;clr.dll&lt;/code&gt;; bypass at managed boundary&lt;/td&gt;
&lt;td&gt;Signature on the CLR-side patch; &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt; ETW correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Era 1: amsiInitFailed (May 2016)&lt;/h3&gt;
&lt;p&gt;The first public AMSI bypass is one line. Matt Graeber posted it as a tweet in May 2016; Sophos&apos;s June 2021 retrospective preserves the date and the tweet body for readers who cannot reach X: &quot;In May of 2016, PowerShell hacker Matt Graeber published a one-line AMSI evasion in a tweet&quot; [@sophos-bypasses]. MDSec&apos;s June 2018 walk-through reproduces the technique and its decompiled target:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;[Ref].Assembly.GetType(&apos;System.Management.Automation.AmsiUtils&apos;). `
  GetField(&apos;amsiInitFailed&apos;,&apos;NonPublic,Static&apos;). `
  SetValue($null, $true)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why does that work? Because the host&apos;s &lt;code&gt;AmsiUtils.ScanContent&lt;/code&gt; checks a private static boolean before doing anything. If the host&apos;s own managed state says &quot;AMSI failed to initialize,&quot; the host short-circuits every subsequent scan to &quot;clean.&quot; MDSec&apos;s analysis quotes the field declaration verbatim: &quot;What we are interested in is the variable &lt;code&gt;amsiInitFailed&lt;/code&gt;, which is defined as: &lt;code&gt;private static bool amsiInitFailed = false&lt;/code&gt;&quot; [@mdsec-evasion].&lt;/p&gt;
&lt;p&gt;The structural lesson is that AMSI&apos;s trust model includes the script engine&apos;s own managed state. PowerShell trusts itself. If you can flip a field in PowerShell&apos;s own memory, you have flipped PowerShell&apos;s trust in AMSI. The short-circuit logic looks like this:&lt;/p&gt;

{`// Decompiled sketch of System.Management.Automation.AmsiUtils.ScanContent.
// Source: Practical Security Analytics decompile of the PowerShell host.&lt;p&gt;function ScanContent(content, scriptName) {
  // amsiSession is AmsiUtils.amsiSession, a static field; not a parameter.
  if (amsiInitFailed) {
    // The bypass: flip this flag once via reflection, every subsequent
    // ScanContent returns AMSI_RESULT_NOT_DETECTED without ever calling
    // amsi.dll. amsiInitFailed is a PRIVATE STATIC field of AmsiUtils;
    // reflection makes it writable from any AppDomain.
    return AMSI_RESULT_NOT_DETECTED;
  }
  if (!amsiInitialized) {
    initialize();  // lazy AmsiInitialize + AmsiOpenSession
  }
  return AmsiNativeMethods.AmsiScanBuffer(
    amsiContext, content, content.length, scriptName, amsiSession);
}`}
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Defender&apos;s response was a string signature on &lt;code&gt;amsiInitFailed&lt;/code&gt; plus class-name proximity. Sophos reports that &quot;the &lt;code&gt;amsiInitFailed&lt;/code&gt; bypass still accounts for about 1 percent of detections, based on a 90-day chunk of telemetry data from February to May of 2021&quot; [@sophos-bypasses]. The bypass is detected almost everywhere it appears, yet it still appears, because some commodity actor still ships the literal string verbatim.&lt;/p&gt;
&lt;h3&gt;Era 2: In-memory &lt;code&gt;AmsiScanString&lt;/code&gt; / &lt;code&gt;AmsiScanBuffer&lt;/code&gt; patch (February 2018)&lt;/h3&gt;
&lt;p&gt;Avi Gimpel and Zeev Ben Porat at CyberArk Labs published the first canonical in-memory native-code patch in February 2018 [@cyberark-patching]. The technique: &lt;code&gt;LoadLibrary(&quot;amsi.dll&quot;)&lt;/code&gt;, &lt;code&gt;GetProcAddress(&quot;AmsiScanString&quot;)&lt;/code&gt;, &lt;code&gt;VirtualProtect&lt;/code&gt; the page to read-write-execute, overwrite the function prologue with &lt;code&gt;mov eax, E_INVALIDARG; ret&lt;/code&gt;, restore the protection. Every subsequent &lt;code&gt;AmsiScanString&lt;/code&gt; returns &lt;code&gt;E_INVALIDARG&lt;/code&gt; without ever talking to the provider.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The first canonical in-memory AMSI patch attack was published by Avi Gimpel and Zeev Ben Porat at CyberArk Labs in February 2018 [@cyberark-patching]. It is sometimes misattributed to Tal Liberman at Black Hat USA 2016; no Liberman BH USA 2016 AMSI talk was independently locatable, and the canonical CyberArk article carries the Gimpel and Ben Porat byline.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft&apos;s 1709 response was to change which function PowerShell called. Instead of &lt;code&gt;AmsiScanString&lt;/code&gt;, PowerShell 5.0 in 1709 calls &lt;code&gt;AmsiScanBuffer&lt;/code&gt;, breaking the Gimpel and Ben Porat patch outright [@cyberark-redux]. Gimpel and Ben Porat published the redux in May 2018: target the new function with a smaller, semantically equivalent patch, &lt;code&gt;xor edi, edi&lt;/code&gt;. The instruction zeros the buffer-length register, which makes &lt;code&gt;AmsiScanBuffer&lt;/code&gt; think the host handed it an empty buffer and return &lt;code&gt;AMSI_RESULT_CLEAN&lt;/code&gt; [@cyberark-redux].&lt;/p&gt;
&lt;p&gt;The on-record Microsoft response to the CyberArk disclosures is the single most quoted sentence in the AMSI corpus. We hold its full force until §9, but here is the verbatim text Gimpel and Ben Porat reproduced under the heading &quot;Microsoft Security Response Center (MSRC) Response&quot; in their May 2018 redux:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The AMSI was not designed to prevent such attacks. If an attacker can execute code in a process using AMSI to scan for malware, there are numerous ways to alter the behavior of the AMSI scan.&quot; -- Microsoft Security Response Center, quoted in CyberArk Labs (May 2018 redux) [@cyberark-redux].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Era 3: Obfuscated patches (2018-2019)&lt;/h3&gt;
&lt;p&gt;Defender&apos;s first response was a byte-pattern signature on the common patches. Practitioners replied by randomizing the patch bytes, resolving &lt;code&gt;AmsiScanBuffer&lt;/code&gt; by hash rather than by name, writing the patch from indirect memory locations to defeat string-based hunts, and so on. The defender response, in turn, was to stop chasing the byte pattern and start watching for &lt;em&gt;the write itself&lt;/em&gt;: any RWX write into &lt;code&gt;amsi.dll&lt;/code&gt;&apos;s code section, regardless of byte pattern, is suspicious. Trend Micro&apos;s bypass-techniques retrospective lists the era&apos;s techniques side by side: &quot;Obfuscation and/or encryption; PowerShell downgrade; Hooks and unhooks; Memory patching; Forcing an error; Registry modifications; DLL hijacking; Reflection&quot; [@trendmicro-bypass].&lt;/p&gt;
&lt;h3&gt;Era 4: Provider COM-hijack (2019)&lt;/h3&gt;
&lt;p&gt;A different attack class: register a malicious CLSID under &lt;code&gt;HKLM\SOFTWARE\Microsoft\AMSI\Providers&lt;/code&gt;, write your own DLL into the standard COM tree, and &lt;code&gt;amsi.dll&lt;/code&gt; will dutifully load it in-process at &lt;code&gt;AmsiInitialize&lt;/code&gt; time [@redcanary-amsi]. Your provider then returns &lt;code&gt;AMSI_RESULT_CLEAN&lt;/code&gt; for everything, regardless of what the actual antivirus would have said. Admin is required to write the keys, but admin is required for a lot of post-exploitation work, and AMSI is in the trust path for a lot of post-exploitation script.&lt;/p&gt;
&lt;p&gt;The defender response was Windows 10 1709&apos;s DLL-hijack mitigation (provider DLLs that did not load their dependencies with full paths refused to load) and Windows 10 1903&apos;s optional Authenticode signing requirement. With &lt;code&gt;HKLM\SOFTWARE\Microsoft\AMSI\FeatureBits = 0x2&lt;/code&gt;, an unsigned provider refuses to load entirely [@amsi-iantimalware].&lt;/p&gt;
&lt;h3&gt;Era 5: Hardware-breakpoint VEH bypass (January 29, 2020)&lt;/h3&gt;
&lt;p&gt;Cornelis de Plaa, posting as Cneelis at Outflank, published the patchless AMSI bypass on January 29, 2020. The technique elegantly sidesteps every prior defender response by never patching &lt;code&gt;amsi.dll&lt;/code&gt; at all. The Outflank post itself is no longer reachable from non-browser clients (the access caveat and accessible corroborators appear in the Sidenote below); the technique mechanics, attribution, and date are preserved verbatim by EthicalChaos (April 2022) [@ethicalchaos], CrowdStrike (2024) [@crowdstrike-veh], and Trend Micro (December 2022) [@trendmicro-bypass]. Steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;AddVectoredExceptionHandler(1, exceptionHandler)&lt;/code&gt; -- register a vectored exception handler.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;DR0&lt;/code&gt; to the address of &lt;code&gt;AmsiScanBuffer&lt;/code&gt; via &lt;code&gt;SetThreadContext&lt;/code&gt;. The &lt;code&gt;DR0&lt;/code&gt;-&lt;code&gt;DR3&lt;/code&gt; registers are CPU hardware breakpoints; the processor raises &lt;code&gt;EXCEPTION_SINGLE_STEP&lt;/code&gt; the moment instruction pointer hits the breakpoint address.&lt;/li&gt;
&lt;li&gt;When the breakpoint fires, the exception handler executes. It sets &lt;code&gt;RAX = AMSI_RESULT_CLEAN&lt;/code&gt;, advances &lt;code&gt;RIP&lt;/code&gt; past the function body, and returns &lt;code&gt;EXCEPTION_CONTINUE_EXECUTION&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is zero static artifact in &lt;code&gt;amsi.dll&lt;/code&gt;. The code section is untouched. Every signature that depends on a write into &lt;code&gt;amsi.dll&lt;/code&gt; is dead. EthicalChaos reproduces the technique mechanics verbatim two years later: &quot;the idea will be to register a vectored exception handler then set a breakpoint on a function within &lt;code&gt;amsi.dll&lt;/code&gt; ... &lt;code&gt;AddVectoredExceptionHandler(1, exceptionHandler) ... SetThreadContext((HANDLE)-2, &amp;amp;threadCtx)&lt;/code&gt;&quot; [@ethicalchaos].The original Outflank 2020-01-29 blog post (outflank.nl, &quot;Bypassing AMSI by manipulating the AMSI scan results&quot;) is no longer reachable from non-browser clients and has no Wayback snapshot; this article therefore cites only accessible corroborators rather than the link-rotten primary. The technique mechanics in this section are reproduced verbatim by EthicalChaos (April 2022) [@ethicalchaos], CrowdStrike (2024) [@crowdstrike-veh], and Trend Micro (December 2022) [@trendmicro-bypass].&lt;/p&gt;
&lt;p&gt;The Defender response is the one this article keeps circling back to: kernel-side ETW. The hardware-breakpoint bypass calls &lt;code&gt;SetThreadContext&lt;/code&gt; to write to &lt;code&gt;DR0&lt;/code&gt;. The &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; (EtwTi) provider&apos;s &lt;code&gt;NtSetContextThread&lt;/code&gt; event covers remote-thread context writes, but in-thread context writes (which is what the patchless bypass performs) are more reliably caught by &lt;code&gt;Microsoft-Windows-Kernel-Audit-API-Calls&lt;/code&gt;, the provider CrowdStrike documents as its primary detection path. CrowdStrike&apos;s writeup gives the framing: &quot;the &lt;code&gt;DR0&lt;/code&gt;-&lt;code&gt;DR3&lt;/code&gt; debug registers contain the addresses of hardware (HW) breakpoints ... patchless AMSI attack called VEH-squared ... mapped in the technique Impair Defenses: Disable or Modify Tools by the MITRE ATT&amp;amp;CK framework (T1562.001)&quot; [@crowdstrike-veh] -- and MITRE&apos;s T1562.001 redirect [@attack-t1562-001] now sends readers to T1685 [@attack-t1685], the unified &quot;Impair Defenses: Disable or Modify Tools&quot; technique. The catch: EtwTi is gated on Antimalware-PPL consumers. Defender&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; and a small set of onboarded third-party EDRs see it. Non-PPL products do not.&lt;/p&gt;

Antimalware Protected Process Light. A Windows signer level (introduced in Windows 8.1, hardened in Windows 10) that lets Defender&apos;s `MsMpEng.exe` and onboarded third-party EDRs consume gated ETW providers like `Microsoft-Windows-Threat-Intelligence` (EtwTi). Without PPL, an EDR cannot see EtwTi events, which means it cannot detect patchless hardware-breakpoint AMSI bypasses through ETW.
&lt;h3&gt;Era 6: CLR-DLL load-time patch (December 2022)&lt;/h3&gt;
&lt;p&gt;If patching &lt;code&gt;amsi.dll&lt;/code&gt; directly is signature-rich, patch something further up the call chain. Practical Security Analytics published the technique in December 2022 [@psa-clr-hooking]: patch the .NET CLR&apos;s own AMSI callout (the managed-side wrapper inside &lt;code&gt;clr.dll&lt;/code&gt; that calls &lt;code&gt;AmsiScanBuffer&lt;/code&gt; on &lt;code&gt;Assembly.Load(byte[])&lt;/code&gt;) rather than &lt;code&gt;amsi.dll&lt;/code&gt; itself. The technique &quot;has an advantage over other API Call Hooking techniques that target native functions such as &lt;code&gt;AMSI.dll::AmsiScanBuffer&lt;/code&gt; in that this method is more difficult to prevent with EDR or Application Protection rules&quot; -- the patched bytes live in &lt;code&gt;clr.dll&lt;/code&gt;, not &lt;code&gt;amsi.dll&lt;/code&gt;, and many defender rules only watch the latter.&lt;/p&gt;
&lt;p&gt;The defender response was twofold: signature on the CLR-side patch bytes, and correlation against the &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt; ETW provider. The DotNETRuntime provider emits an &lt;code&gt;AssemblyLoadFinished&lt;/code&gt; event for every &lt;code&gt;Assembly.Load&lt;/code&gt; call. If the CLR-side AMSI callout has been muted, the load event fires anyway, and Defender now has a &lt;code&gt;DotNETRuntime&lt;/code&gt; event with no corresponding &lt;code&gt;AmsiScanBuffer&lt;/code&gt; event in the prior microseconds. That gap is the signal.&lt;/p&gt;
&lt;h3&gt;Era 7: The behavioral era (2023+)&lt;/h3&gt;
&lt;p&gt;By 2023, the bypass families had grown beyond enumeration. Microsoft&apos;s response was structural: stop trying to enumerate bypass techniques, and start scoring the &lt;em&gt;gap&lt;/em&gt;. Defender&apos;s machine-learning models, described in the August 2020 disclosure on pairs-of-classifiers [@msec-amsi-ml-2020], feed not just on the content of AMSI events but on the &lt;em&gt;cadence&lt;/em&gt; of AMSI events per process. A &lt;code&gt;powershell.exe&lt;/code&gt; that has been alive for 90 seconds, run 14 commands, and emitted zero &lt;code&gt;AmsiScriptDetection&lt;/code&gt; ETW events when the cohort baseline expects six is suspicious regardless of the technical mechanism behind the silence. The structural insight: the win condition is no longer &quot;detect the bypass&quot; but &quot;notice that scanning has stopped.&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; AMSI&apos;s &lt;em&gt;capability&lt;/em&gt; (post-deobfuscation, synchronous, blocking) and its &lt;em&gt;failure mode&lt;/em&gt; (same-process bypass) come from the same architectural fact. Running in the script engine&apos;s process is the only way to see the post-deobfuscation bytes; it is also the only way to be muted by anything else running in that process. Defender&apos;s 2023+ response, scoring the gap rather than the bypass, is the only structurally durable answer because it works against any technique.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The bypass arms race is a symptom, not the disease. The disease is what Microsoft has been saying out loud since 2018: AMSI is not, and was never designed to be, a security boundary.&lt;/p&gt;
&lt;h2&gt;9. What AMSI Is Not: The MSRC Boundary Position&lt;/h2&gt;
&lt;p&gt;When Avi Gimpel and Zeev Ben Porat disclosed their in-memory AMSI patches to the Microsoft Security Response Center across early 2018, the response they received and reproduced verbatim under the &quot;MSRC Response&quot; heading of their May 2018 redux is the most important single sentence in the AMSI corpus:&lt;/p&gt;

The AMSI was not designed to prevent such attacks. If an attacker can execute code in a process using AMSI to scan for malware, there are numerous ways to alter the behavior of the AMSI scan.&lt;p&gt;-- Microsoft Security Response Center, quoted in CyberArk Labs (May 2018 redux)
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;That sentence is not a Microsoft retreat under pressure. It is the published structural position. The Windows Security Servicing Criteria framework, which MSRC uses to triage every bug report against Windows, asks one question to determine whether a finding is serviced as a security vulnerability: &quot;Does the vulnerability violate the goal or intent of a security boundary or a security feature? ... If the answer to either question is no, then by default the vulnerability will be considered for the next version or release of Windows but will not be addressed through a security update or guidance&quot; [@msrc-criteria]. AMSI is published as neither a boundary nor a feature in that taxonomy. Bypasses of AMSI are not security bugs in MSRC&apos;s published framework. They get fixed when Microsoft can fix them. They do not get CVEs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; AMSI is not a security boundary. It is a high-coverage telemetry seam that closes one specific evasion strategy -- pre-execution obfuscation -- and concedes everything else to the layers above and below it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So why is AMSI valuable anyway?&lt;/p&gt;

flowchart TD
    A[AMSI same-process trust model] --&amp;gt; B{Attacker has code execution in the host?}
    B -- No, the attacker is delivering an unprivileged script --&amp;gt; C[AMSI scans the deobfuscated buffer&lt;br /&gt;provider returns DETECTED&lt;br /&gt;host refuses to run]
    B -- Yes, the attacker has unrestricted code execution --&amp;gt; D[AMSI scan is mutable in-process]
    D --&amp;gt; E[ETW provider 2A576B87 emits from inside the prologue]
    E --&amp;gt; F[Defender / EDR sees the scan happened; bypass leaks telemetry]
    D --&amp;gt; G[Defender&apos;s behavioral cohort scoring]
    G --&amp;gt; H[Gap detection: process emitted 0 AmsiData events; cohort expects ~6]
    C --&amp;gt; I[AMSI as synchronous gate: WIN]
    F --&amp;gt; J[AMSI bypass leaves ETW fingerprint: WIN]
    H --&amp;gt; K[Behavioral gap detection: WIN]
&lt;p&gt;There are two trust assumptions, and both hold for most real-world attacks. The first is that the attacker is &lt;em&gt;unprivileged&lt;/em&gt;: they are delivering an obfuscated script payload inside a host process they did not control before delivery. The phishing-document case in §1 is exactly this. AMSI&apos;s synchronous gate beats them. The second is that Defender&apos;s &lt;em&gt;ETW telemetry&lt;/em&gt; of AMSI scans, including the &lt;em&gt;gaps&lt;/em&gt; in those scans, survives the bypass. Even when an in-process bypass mutes the synchronous return, the ETW provider&apos;s prologue emit still fires, and behavioral cohort scoring still notices the missing events. AMSI bypasses leak. Defender&apos;s win condition is that the leak is enough.&lt;/p&gt;
&lt;p&gt;Why can AMSI not be moved into a &lt;a href=&quot;https://paragmali.com/blog/vbs-trustlets-what-actually-runs-in-the-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;VBS Trustlet&lt;/a&gt; (the isolated, kernel-attested user-mode environment that Hyper-V&apos;s Virtual Secure Mode hosts)? Latency. A Trustlet call is a VTL switch: the CPU takes a VMEXIT into the hypervisor, saves and restores the VMCS, and returns into VTL1; the Hyper-V Top-Level Functional Specification documents the mechanism as a hypercall (Microsoft TLFS: Virtual Secure Mode [@tlfs-vsm]). AMSI is on the hot path of every script statement: PowerShell calls &lt;code&gt;AmsiScanBuffer&lt;/code&gt; per command, Office VBA calls it per trigger, .NET calls it per &lt;code&gt;Assembly.Load&lt;/code&gt;. Multiplying every per-statement scan by a VTL round trip is unacceptable. The same-process design is a deliberate latency-versus-isolation trade-off, made in 2015 and confirmed every year since.&lt;/p&gt;
&lt;p&gt;Why can AMSI not be moved out-of-process to a broker? Same answer: the broker&apos;s RPC round trip puts process context switches and ALPC marshalling on the same per-statement hot path. And a broker introduces a different problem: an in-process attacker could prevent the host from speaking to the broker (close the RPC handle, replace the proxy, set a hardware breakpoint on the marshalling thunk). The attack surface is not reduced; it is moved.&lt;/p&gt;

The pragmatic alternative to &quot;AMSI as a security boundary&quot; is *defense in depth across three trust models*, which is what Microsoft has actually shipped:&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The synchronous gate.&lt;/strong&gt; AMSI in-process. Beats the unprivileged-payload case. Cannot be a boundary because of the latency math above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The ETW correlation seam.&lt;/strong&gt; The &lt;code&gt;Microsoft-Antimalware-Scan-Interface&lt;/code&gt; provider emits the buffer to whoever can read it. Beats the in-process bypass case, because the ETW emit happens before the bypass-clobbered return [@ethicalchaos].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The policy-denial layer.&lt;/strong&gt; Constrained Language Mode under WDAC User Mode Code Integrity, and the &quot;Block Macros from Internet&quot; default. These do not scan content; they refuse to run it [@ps-clm] [@internet-macros-blocked].&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The three together cover the cases AMSI alone cannot. Each one is weak alone. None of them is a security boundary in MSRC&apos;s strict sense; together, they cover the operational space.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Now we know what AMSI is, what it is not, and how attackers have spent seven years stress-testing the difference. What is left unsolved?&lt;/p&gt;
&lt;h2&gt;10. Open Problems: The 2026 Frontier&lt;/h2&gt;
&lt;p&gt;Fred Cohen proved in 1984 that general virus detection is undecidable: &quot;In general, detection of a virus is shown to be undecidable both by a priori and runtime analysis, and without detection, cure is likely to be difficult or impossible&quot; [@cohen-1984]. AMSI does not try to solve Cohen&apos;s problem. AMSI solves an adjacent problem -- given a deobfuscated buffer, does it match patterns a provider has seen? -- which is finite-state and tractable. The first is impossible. The second is the only thing that has ever worked.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; AMSI does not try to solve the undecidable problem of &quot;is this program malicious?&quot;. It solves a tractable adjacent problem: &quot;does this deobfuscated buffer match patterns we have seen?&quot;. The first is theoretically impossible (Cohen 1984). The second is the only thing that has ever scaled.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The empirical upper bound on the second problem is now known. Danny Hendler, Shay Kels, and Amir Rubin&apos;s 2020 ACM AsiaCCS paper, &lt;em&gt;Detecting Malicious PowerShell Commands using Deep Neural Networks&lt;/em&gt; [@hendler-msr], reports on AMSI-collected PowerShell: &quot;Our best-performing model uses an architecture that enables the processing of textual signals from both the character and token levels and obtains a true-positive rate of nearly 90% while maintaining a low false-positive rate of less than 0.1%.&quot; The arXiv preprint carries the same headline figures [@hendler-arxiv]. About 90 percent true positive at under 0.1 percent false positive is the practical ceiling on AMSI-side classification. It is much better than every pre-AMSI defender alone, and it is still 10 percent away from perfect. Cohen&apos;s lower bound on the &lt;em&gt;general&lt;/em&gt; problem means perfect is not on offer; the question is what fraction of the residual 10 percent the next ten years close.&lt;/p&gt;

flowchart TD
    A[AMSI in 2026: open problems]
    A --&amp;gt; B[Patchless bypass detection without PPL]
    A --&amp;gt; C[Non-Microsoft script runtimes Python Node Ruby]
    A --&amp;gt; D[AMSI for AI-runtime LLM-generated code]
    A --&amp;gt; E[Cross-runtime correlation single chain]
    A --&amp;gt; F[IAmsiStream adoption beyond scripts]
    A --&amp;gt; G[AMSI on Linux macOS especially dotnet]&lt;pre&gt;&lt;code&gt;B -.user-mode-only detection requires polling.-&amp;gt; B1[Open: no general solution]
C -.PEP-578 audit hooks architecturally similar.-&amp;gt; C1[Open: no production bridge]
D -.does content-scan even apply to LLM output.-&amp;gt; D1[Open: design problem]
E -.no correlation_id joins macro-PowerShell-dotnet.-&amp;gt; E1[Open: per-host-app scope]
F -.designed for adoption but adoption thin.-&amp;gt; F1[Open: market problem]
G -.no shared script-engine host model.-&amp;gt; G1[Open: platform problem]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Open problem 1: patchless hardware-breakpoint bypass on unprivileged user-mode EDR.&lt;/strong&gt; The Outflank 2020 technique still works against EDR products that lack any kernel-side ETW consumer for thread-context writes [@crowdstrike-veh]. CrowdStrike&apos;s recommended detector, &lt;code&gt;Microsoft-Windows-Kernel-Audit-API-Calls&lt;/code&gt;, is available to admin-side consumers without an Antimalware-PPL gate; &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; is the stricter alternative for the remote-thread-context subset. The conjecture, stated bluntly: no reliable fully-unprivileged user-mode-only detection of the patchless bypass exists. Any such detection would have to either poll the debug registers (which defeats the bypass&apos;s whole point) or hook the syscalls the bypass uses (which any in-process bypass can in turn defeat). The path forward is to make kernel-ETW consumption table stakes for any serious EDR product on Windows; the path is administrative, not architectural.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 2: non-Microsoft script runtimes.&lt;/strong&gt; Python, Node.js, Ruby, Lua, and the JavaScript hosts embedded in WebView2 are all script-execution surfaces that AMSI does not see. Python&apos;s PEP-578 audit hooks are architecturally similar to AMSI: a callback the runtime invokes at security-relevant events. No production AMSI bridge for Python ships from Microsoft or from any major Python distributor. The architectural reason is that AMSI&apos;s contract assumes a host that has a clear &quot;about to execute deobfuscated content&quot; moment; not every runtime presents that moment to the OS in a way an external provider can intercept.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 3: AMSI for AI-runtime / LLM-generated code.&lt;/strong&gt; When Copilot or AutoGen agents generate code that an automated runner executes, is &lt;code&gt;AmsiScanBuffer&lt;/code&gt; the right seam for inspection? The architectural question is harder than the engineering one: do content-scan signatures even apply to LLM-generated code at all? The empirical answer is unknown, and the public AMSI corpus (§8 above, plus the Hendler/Kels/Rubin character- and token-level model from §10) is built on the obfuscation artefacts of human-authored attacks; whether the same signal shape persists when the author is a language model is itself the open research question. A different seam, closer to &quot;policy at agent-execution time,&quot; may be the right model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 4: cross-runtime correlation.&lt;/strong&gt; Today, each AMSI integration sees its slice of the attack. Office VBA sees the trigger buffer. PowerShell sees the deobfuscated command line. .NET sees the in-memory assembly. The provider can correlate calls within one &lt;code&gt;HAMSISESSION&lt;/code&gt;, but no single &lt;code&gt;correlation_id&lt;/code&gt; joins Office VBA&apos;s session to the PowerShell session it spawns to the .NET assembly that PowerShell loads. A SOC analyst piecing together the chain joins on parent process ID and timestamp; the join is fragile.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 5: &lt;code&gt;IAmsiStream&lt;/code&gt; adoption beyond script engines.&lt;/strong&gt; &lt;code&gt;IAmsiStream&lt;/code&gt; was designed for non-script content -- IM messages, downloaded plugins, BLOB attachments -- but the demand from non-script applications never materialized. The interface is ready; the integrations are not. This one is a market problem, not an architectural one, and there is no obvious actor whose interest is to fix it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 6: AMSI on Linux and macOS.&lt;/strong&gt; PowerShell 7 runs on Linux. .NET runs on Linux. The same &lt;code&gt;Assembly.Load(byte[])&lt;/code&gt; attack surface that drove .NET 4.8&apos;s AMSI integration exists in CoreCLR, unwatched. No equivalent of AMSI ships outside Windows. Partly that is platform: every Python and Node install on Linux is essentially its own host with its own life cycle, and there is no shared script-engine model the way &lt;code&gt;amsi.dll&lt;/code&gt; provides on Windows. Partly it is economics: the large-customer demand that drove every Windows AMSI integration since 2015 has not assembled on the other side. The PowerShell team&apos;s path forward is uncertain.&lt;/p&gt;
&lt;p&gt;If you build, hunt, attack, or defend on Windows, AMSI is not optional reading. The next section is the Monday-morning answer for each of those four roles.&lt;/p&gt;
&lt;h2&gt;11. Practical Guide: For Four Roles on Monday Morning&lt;/h2&gt;
&lt;p&gt;The rest of this section is the action-oriented closing. One numbered subsection per audience. Skip to the one that applies to you.&lt;/p&gt;
&lt;h3&gt;11.1 For an application developer&lt;/h3&gt;
&lt;p&gt;You ship a Windows application that hosts a scripting engine, an automation surface, or a plug-in loader. Here is the minimum-viable AMSI integration. The call lifecycle is exactly five functions plus one cleanup pair.&lt;/p&gt;

# Pseudocode against the Win32 flat-C AMSI surface in amsi.dll.
# A real implementation would use C++ or Rust with the actual amsi.h
# types. The lifecycle and error handling are the load-bearing parts.&lt;p&gt;amsi = ctypes.WinDLL(&quot;amsi.dll&quot;)&lt;/p&gt;
1. Once at startup. appName is what shows up in DeviceEvents.AmsiProcessName.
&lt;p&gt;ctx = HAMSICONTEXT()
hr = amsi.AmsiInitialize(&quot;MyApp_v3.2&quot;, byref(ctx))
if hr != S_OK:
    raise OSError(hr)&lt;/p&gt;
&lt;p&gt;try:
    # 2. Once per logical user command (NOT once per buffer).
    session = HAMSISESSION()
    hr = amsi.AmsiOpenSession(ctx, byref(session))
    if hr != S_OK:
        raise OSError(hr)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    # 3. Once per buffer. The contentName is what the SOC analyst sees.
    result = AMSI_RESULT()
    hr = amsi.AmsiScanBuffer(
        ctx, buffer, len(buffer), &quot;user-script.ps1&quot;, session, byref(result))
    if hr != S_OK:
        raise OSError(hr)

    # 4. Interpret the verdict.
    if amsi.AmsiResultIsMalware(result):
        raise SecurityException(&quot;AMSI flagged the content as malicious&quot;)

finally:
    # 5. Always close the session.
    amsi.AmsiCloseSession(ctx, session)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;finally:
    # 6. Always uninitialize at shutdown.
    amsi.AmsiUninitialize(ctx)
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The four common bugs to avoid: forgetting &lt;code&gt;AmsiUninitialize&lt;/code&gt; (the handle leaks until the process dies); sharing one &lt;code&gt;HAMSISESSION&lt;/code&gt; across threads (the correlation breaks and the provider sees one giant interleaved logical command); ignoring &lt;code&gt;AMSI_RESULT_DETECTED&lt;/code&gt; (defeats the entire point of integrating); and passing a meaningless &lt;code&gt;contentName&lt;/code&gt; (every SOC analyst hunting your application will quietly curse you).&lt;/p&gt;
&lt;h3&gt;11.2 For an AV or EDR vendor implementing a provider&lt;/h3&gt;
&lt;p&gt;If you are an AV vendor, the Microsoft Windows-classic-samples AmsiProvider [@amsi-sample] is your starting point. The skeleton: &lt;code&gt;DllRegisterServer&lt;/code&gt; writes the two registry trees (the CLSID tree under &lt;code&gt;HKLM\SOFTWARE\Classes\CLSID&lt;/code&gt; and the AMSI opt-in tree under &lt;code&gt;HKLM\SOFTWARE\Microsoft\AMSI\Providers&lt;/code&gt;); the IClassFactory boilerplate creates an instance; &lt;code&gt;IAntimalwareProvider::Scan&lt;/code&gt; consumes the &lt;code&gt;IAmsiStream&lt;/code&gt; and bridges it to your scan engine [@amsi-devaudience].&lt;/p&gt;
&lt;p&gt;Three operational gotchas that have bitten every vendor at least once:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Load dependencies with full paths.&lt;/strong&gt; Windows 10 1709&apos;s DLL-hijack mitigation refuses unqualified &lt;code&gt;LoadLibrary&lt;/code&gt; calls from AMSI provider DLLs. Use full paths for every secondary DLL your provider loads [@amsi-devaudience].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authenticode-sign your provider.&lt;/strong&gt; Windows 10 1903&apos;s optional signing check at &lt;code&gt;HKLM\SOFTWARE\Microsoft\AMSI\FeatureBits = 0x2&lt;/code&gt; refuses unsigned providers. Many enterprise customers set that bit by policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ThreadingModel must be &lt;code&gt;Both&lt;/code&gt;.&lt;/strong&gt; Marshaling proxies break the in-process performance assumption.&lt;/li&gt;
&lt;/ol&gt;

Defender inherits a legacy contract from the IOfficeAntiVirus era: when a full third-party AV registers itself as the active antimalware provider, Defender unregisters itself as the active AV and remains as a passive scanner. AMSI is the modern instance of that contract. Your registered provider becomes the active AV slot; Defender steps aside. The flip is not silent: Defender logs the transition, and admin tools display the new active AV in Windows Security Center. If you are the registered provider and Defender is *not* yielding, recheck your registration (both registry trees, signing, and that your provider&apos;s `IAntimalwareProvider::DisplayName` returns a non-empty string).
&lt;h3&gt;11.3 For a detection engineer&lt;/h3&gt;
&lt;p&gt;The two-pronged hunt: query the cloud telemetry (&lt;code&gt;DeviceEvents&lt;/code&gt; in Defender XDR) for the wide net, and run an on-host ETW consumer (SilkETW or Sealighter against GUID &lt;code&gt;{2A576B87-09A7-520E-C21A-4942F0271D67}&lt;/code&gt;) for the air-gapped and high-value hosts. The KQL pattern in §7 is the cloud-side join; the on-host consumer is documented in the &lt;code&gt;AMSIScriptDetections.md&lt;/code&gt; hunting pack [@bertjan-amsi-queries].&lt;/p&gt;
&lt;p&gt;Bonus rule: deploy a gap-detection alert. &quot;Any &lt;code&gt;powershell.exe&lt;/code&gt; process alive longer than 60 seconds with more than five &lt;code&gt;ProcessCommandLine&lt;/code&gt; entries and zero &lt;code&gt;AmsiScriptDetection&lt;/code&gt; events&quot; is a high-signal pattern across every bypass family in §8, including the patchless ones. It does not detect the &lt;em&gt;bypass&lt;/em&gt;; it detects the &lt;em&gt;result&lt;/em&gt; of the bypass, which is silence where there should be sound.&lt;/p&gt;
&lt;h3&gt;11.4 For a red-team operator&lt;/h3&gt;
&lt;p&gt;The viability of each bypass family in 2026 against fully-patched Windows 11 23H2 with Defender for Endpoint and a PPL-onboarded EDR is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AmsiUtils.amsiInitFailed&lt;/code&gt;: dead. String signature still in place; Sophos reports about 1 percent detection share in 2021, which means roughly 1 percent of commodity actors still ship the literal bypass and get caught [@sophos-bypasses].&lt;/li&gt;
&lt;li&gt;In-process &lt;code&gt;AmsiScanBuffer&lt;/code&gt; patch: dead. Byte-pattern signature plus behavior signature on writes to &lt;code&gt;amsi.dll&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Provider COM-hijack: dead. 1903 signing requirement plus 1709 DLL-hijack mitigation.&lt;/li&gt;
&lt;li&gt;Hardware-breakpoint VEH (Outflank 2020 family): generates &lt;code&gt;Microsoft-Windows-Kernel-Audit-API-Calls&lt;/code&gt; &lt;code&gt;NtSetContextThread&lt;/code&gt; events to any admin-side ETW consumer; the stricter &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; event fires only on remote-thread writes, so the in-thread variant is invisible to EtwTi but visible without PPL [@crowdstrike-veh].&lt;/li&gt;
&lt;li&gt;CLR-DLL patch (Practical Security Analytics, 2021): niche; the &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt; ETW correlation closes most variants.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Even when a bypass succeeds against the synchronous &lt;code&gt;AmsiScanBuffer&lt;/code&gt; return, the ETW provider still emits from inside the prologue. If your goal is silence rather than evasion, you need a bypass that prevents &lt;code&gt;amsi.dll&lt;/code&gt; from loading at all, and most modern hosts will not let you. The trade between &quot;I bypassed AMSI&quot; and &quot;I left no telemetry&quot; is rarely the same trade.&lt;/p&gt;
&lt;/blockquote&gt;

Even surviving 2026-viable bypasses emit telemetry that compounds: a provider COM-hijack attempt generates an unsigned-load failure in the Windows event log; a hardware-breakpoint VEH bypass generates an `NtSetContextThread` event in `Microsoft-Windows-Kernel-Audit-API-Calls` (and in `Microsoft-Windows-Threat-Intelligence` on the remote-thread subset); a CLR-DLL patch generates a clr.dll-write event in the kernel-mode memory-protection telemetry. The &quot;I bypassed AMSI&quot; cost is one event; the &quot;I bypassed AMSI invisibly&quot; cost is many. On a high-assurance target where the SOC is hunting on the gap and the EDR has PPL onboarded, the risk-adjusted return on most known bypasses is negative.
&lt;p&gt;AMSI is, in the end, a covenant. The script engine promises to phone home before it runs your code. The defender promises to listen. Everyone -- attacker, defender, developer, AV vendor -- has spent ten years arguing about the terms.&lt;/p&gt;
&lt;h2&gt;12. FAQ&lt;/h2&gt;

No. Per Microsoft&apos;s published Windows Security Servicing Criteria [@msrc-criteria], AMSI is not classified as a security boundary, which means AMSI-bypass bugs are not serviced as security vulnerabilities. The Microsoft Security Response Center&apos;s response to CyberArk Labs (reproduced in the May 2018 redux disclosure [@cyberark-redux]) is verbatim: &quot;the AMSI was not designed to prevent such attacks.&quot; See §9 for the architectural reasoning.

No. `MpOav.dll` loads *into the calling process* (`powershell.exe`, `winword.exe`, `wscript.exe`), not into `MsMpEng.exe`. The PPL hardening protects `MsMpEng.exe`&apos;s process from tampering, but it does not extend to the AMSI provider DLL that gets loaded into the script host&apos;s memory space [@redcanary-amsi].

Because AMSI&apos;s trust model assumes the host process is benign. A non-admin who has code execution inside a non-PPL host can patch the host&apos;s own memory (Era 2) or flip the host&apos;s own managed state via reflection (Era 1). Neither requires admin. Hardening AMSI against the unprivileged in-process attacker would require moving AMSI out of the host process, which would defeat its latency and post-deobfuscation-visibility design. See §9 [@mdsec-evasion].

The decrypted one. AMSI is called *after* `[Convert]::FromBase64String`, after XOR, after string concatenation, and after `Invoke-Expression` argument construction. The host hands AMSI the buffer that the executor was about to run. That is the entire point of the design [@holmes-2015-wayback].

No. AMSI catches `Assembly.Load(byte[])` since .NET Framework 4.8 (April 2019) [@dotnet-48]. It does *not* catch `DynamicMethod` [@dotnet-dynamicmethod] emission via `System.Reflection.Emit`, because there is no PE-load event to anchor a scan on; the IL is built up byte by byte inside the CLR. Detection of `Reflection.Emit` abuse falls under the broader &quot;Reflection&quot; bypass family Trend Micro catalogues separately from the in-memory `AmsiScanBuffer` patch family [@trendmicro-bypass].

A combination of architectural and platform reasons. Architecturally, Linux and macOS do not have a shared script-engine host model; every Python, Node.js, Ruby, and Perl install is essentially its own host. Platform-wise, the demand for an out-of-the-box scan-interface contract has not materialized, even though PowerShell 7 and .NET Core both run on Linux. See §10 for the structural argument.

AMSI is synchronous and can block; ETW is asynchronous and observation-only. Both surface the same data (the post-deobfuscation buffer) and the same provider verdict. AMSI is for *decisions* (the host refuses to run flagged content). The `Microsoft-Antimalware-Scan-Interface` ETW provider with GUID `{2A576B87-09A7-520E-C21A-4942F0271D67}` is for *correlation* and *gap detection* (the SOC can see the scan happened even when an in-process bypass mutes the synchronous return) [@etw-manifest].
&lt;p&gt;&lt;strong&gt;Key terms.&lt;/strong&gt; AMSI; &lt;code&gt;AmsiScanBuffer&lt;/code&gt;; &lt;code&gt;AmsiInitialize&lt;/code&gt;; &lt;code&gt;AmsiOpenSession&lt;/code&gt;; &lt;code&gt;HAMSISESSION&lt;/code&gt;; &lt;code&gt;AMSI_RESULT_DETECTED&lt;/code&gt; (32768); AMSI provider; &lt;code&gt;MpOav.dll&lt;/code&gt;; CLSID &lt;code&gt;{2781761E-28E0-4109-99FE-B9D127C57AFE}&lt;/code&gt;; ETW provider &lt;code&gt;{2A576B87-09A7-520E-C21A-4942F0271D67}&lt;/code&gt;; trigger-buffer architecture; Script Block Logging (Event 4104); &lt;code&gt;amsiInitFailed&lt;/code&gt;; Antimalware-PPL; &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; (EtwTi); Constrained Language Mode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Review questions.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why is &lt;code&gt;IOfficeAntiVirus&lt;/code&gt; (Office 97) architecturally unable to catch a VBA macro that does &lt;code&gt;Application.Run&lt;/code&gt; of a string decoded from a worksheet cell, even when the decoded string is malicious? (§3)&lt;/li&gt;
&lt;li&gt;State the design intent Lee Holmes named in his June 9, 2015 disclosure in one sentence. Then explain why &quot;the engine can see the actual code that will be passed to be evaluated&quot; makes the obfuscation arms race obsolete on the AMSI side specifically. (§4)&lt;/li&gt;
&lt;li&gt;Walk through every step of &lt;code&gt;AmsiInitialize&lt;/code&gt; -&amp;gt; &lt;code&gt;AmsiOpenSession&lt;/code&gt; -&amp;gt; &lt;code&gt;AmsiScanBuffer&lt;/code&gt; -&amp;gt; &lt;code&gt;AmsiResultIsMalware&lt;/code&gt; -&amp;gt; &lt;code&gt;AmsiCloseSession&lt;/code&gt; -&amp;gt; &lt;code&gt;AmsiUninitialize&lt;/code&gt; for one PowerShell command. What happens at each step, and which field in the &lt;code&gt;DeviceEvents&lt;/code&gt; table does each parameter map to? (§5, §6)&lt;/li&gt;
&lt;li&gt;Why does AMSI&apos;s same-process design produce both its capability (post-deobfuscation visibility) and its failure mode (in-process bypass)? What two trust assumptions make AMSI valuable anyway? (§5, §9)&lt;/li&gt;
&lt;li&gt;For each of the six bypass eras in §8, state the technique in one sentence, the defender response in one sentence, and the era&apos;s residual viability against Windows 11 23H2 plus Defender in 2026.&lt;/li&gt;
&lt;li&gt;Why does the &lt;code&gt;Microsoft-Antimalware-Scan-Interface&lt;/code&gt; ETW provider&apos;s prologue emit survive the patchless hardware-breakpoint bypass that mutes the synchronous &lt;code&gt;AmsiScanBuffer&lt;/code&gt; return? (§7, §8 Era 5)&lt;/li&gt;
&lt;li&gt;What is the role of Cohen&apos;s 1984 undecidability result in framing AMSI&apos;s open problems for 2026? Why does it justify the Hendler/Kels/Rubin ~90 percent / &amp;lt;0.1 percent ceiling rather than refuting it? (§10)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Further reading.&lt;/strong&gt; Lee Holmes, &lt;em&gt;Windows 10 to offer application developers new malware defenses&lt;/em&gt; [@holmes-2015-wayback] (June 9, 2015). Microsoft Office 365 Threat Research, &lt;em&gt;Office VBA + AMSI: Parting the veil on malicious macros&lt;/em&gt; [@msec-vba-amsi-2018] (September 12, 2018). Gimpel and Ben Porat, &lt;em&gt;AMSI Bypass: Patching Technique&lt;/em&gt; [@cyberark-patching] (CyberArk Labs, February 2018). Hendler, Kels, and Rubin, &lt;em&gt;Detecting Malicious PowerShell Commands using Deep Neural Networks&lt;/em&gt; [@hendler-arxiv] (ACM AsiaCCS 2020). Microsoft Windows-classic-samples AmsiProvider [@amsi-sample] (reference provider implementation).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Citation availability.&lt;/strong&gt; Two original primary sources cited by the historical record for §8 are not currently accessible from non-browser clients and are therefore omitted as live URLs from this article&apos;s reference set; all load-bearing technique mechanics, attributions, and dates are preserved through accessible secondary sources. (a) Cornelis de Plaa&apos;s Outflank post of January 29, 2020 on the hardware-breakpoint VEH bypass (outflank.nl, &quot;Bypassing AMSI by manipulating the AMSI scan results&quot;) is no longer reachable from non-browser clients and has no Wayback snapshot; the technique&apos;s mechanics, January 29, 2020 publication date, and Outflank/Cneelis attribution are reproduced verbatim by EthicalChaos (April 2022) [@ethicalchaos], CrowdStrike (2024) [@crowdstrike-veh], and Trend Micro (December 2022) [@trendmicro-bypass]. (b) Matt Graeber&apos;s May 2016 &lt;code&gt;amsiInitFailed&lt;/code&gt; tweet sits behind the Twitter/X login wall; the tweet body, the May 2016 date, and the &lt;code&gt;amsiInitFailed&lt;/code&gt; reflection technique are reproduced verbatim by Sophos (June 2021) [@sophos-bypasses] (&quot;In May of 2016, PowerShell hacker Matt Graeber published a one-line AMSI evasion in a tweet&quot;) and MDSec (June 2018) [@mdsec-evasion] (full decompilation of the targeted private static field). Readers can reach every load-bearing primary source for both Era 1 (&lt;code&gt;amsiInitFailed&lt;/code&gt;) and Era 5 (hardware-breakpoint VEH) via the corroborating links above.&lt;/p&gt;

</content:encoded><category>amsi</category><category>windows-security</category><category>defender</category><category>powershell</category><category>malware-detection</category><category>etw</category><category>bypass-arms-race</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>ETW: How Windows 2000&apos;s Performance Hack Became the EDR Substrate</title><link>https://paragmali.com/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/</link><guid isPermaLink="true">https://paragmali.com/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/</guid><description>Event Tracing for Windows is the kernel-buffered observability bus every modern Windows EDR consumes. This is the architecture, the attacks, and the one provider that survives them.</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>
Event Tracing for Windows is the high-rate, kernel-buffered observability bus that every modern Windows EDR consumes. A 2007-era architectural decision -- letting eight sessions read the same provider concurrently -- is what makes multi-vendor coexistence possible on a single host. Microsoft&apos;s `Microsoft-Windows-Threat-Intelligence` provider, gated behind Protected Process Light and an ELAM-signed Antimalware certificate since the Windows 10 RS-era, fires from the kernel side of memory-modifying syscalls and survives the user-mode `EtwEventWrite` patch class that defined red-team tradecraft from 2020 to 2022. The remaining attack surface -- BYOVD-driven kernel tampering -- is structurally narrowed by the Vulnerable Driver Blocklist enabled by default since Windows 11 22H2, with the residual sub-microsecond-payload gap remaining as ETW&apos;s irreducible &quot;observation, not enforcement&quot; limit.
&lt;h2&gt;1. Why didn&apos;t the patch silence Defender?&lt;/h2&gt;
&lt;p&gt;A red-team operator drops onto a 2026 Defender [@paragmali-com-war-it]-protected box and runs the move that worked five years ago. They locate &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; in the calling process, write the byte &lt;code&gt;0xC3&lt;/code&gt; over the function prologue, and the calling process now silently fails to emit user-mode ETW events. The .NET CLR provider goes dark. &lt;code&gt;Invoke-Mimikatz&lt;/code&gt; loads from &lt;code&gt;execute-assembly&lt;/code&gt; without lighting up &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;. Defender catches the credential dump [@paragmali-com-and-the] anyway, four seconds later, and the operator is on a SOC analyst&apos;s screen before the shellcode finishes running.&lt;/p&gt;
&lt;p&gt;The patch worked. The .NET tracing provider in that process is mute. Attach a debugger and disassemble the function prologue: the first byte is now &lt;code&gt;0xC3&lt;/code&gt;, the near-return opcode [@felixcloutier-ret] [@felixcloutier-ret], and any caller falls straight back to its return address before producing a single event. The technique is the one Adam Chester documented in March 2020 [@xpn-hiding-dotnet] [@xpn-hiding-dotnet], and to a generation of red teamers it has functioned as a near-universal ETW evasion ever since.&lt;/p&gt;
&lt;p&gt;So why did Defender still fire?&lt;/p&gt;
&lt;p&gt;Because Defender does not consume &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt; to detect a credential dump. It consumes &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; [@fluxsec-eti] [@fluxsec-eti] -- a provider whose GUID is &lt;code&gt;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&lt;/code&gt;, whose events fire from inside the kernel side of memory-modifying syscalls, and whose producer the user-mode patcher cannot reach. The patch operated on a &lt;code&gt;ntdll&lt;/code&gt; trampoline. The signal Defender used was emitted from a different layer entirely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Modern Windows EDR is layered on ETW, and the layers fail under different attacks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That single asymmetry -- one provider goes dark to a one-byte patch, another fires from a place the patcher cannot touch -- is the spine of this article. Around it sits a 26-year story of one Microsoft team accidentally building the substrate of every modern Windows endpoint security product.&lt;/p&gt;

A high-rate, kernel-buffered tracing facility built into Windows since 2000. Components called *providers* emit events tagged with a GUID; *controllers* configure trace sessions; *consumers* subscribe to live event streams or read recorded `.etl` files. ETW was designed for low-overhead developer diagnostics; it was retrofitted into the security-telemetry substrate that all modern Windows EDR products consume.

A class of endpoint security product that ingests behavioural telemetry (process creation, image load, memory allocation, network connection, registry change), correlates it against detection logic, and produces alerts and response actions. On Windows, the dominant EDRs (Microsoft Defender for Endpoint, CrowdStrike Falcon, SentinelOne, Elastic Defend, Wazuh, Sysmon-plus-SIEM) all build on ETW or on the same kernel callbacks ETW exposes to the user-mode tier.
&lt;p&gt;To understand why a one-byte patch silences one provider but not another, we have to go back to a Windows 2000 design decision about per-CPU ring buffers.&lt;/p&gt;
&lt;h2&gt;2. ETW in Windows 2000: the performance problem that started it all&lt;/h2&gt;
&lt;p&gt;Imagine a 1999 network-driver author. A customer&apos;s NT4 production server is corrupting packets under load and the only available instrumentation is &lt;code&gt;DbgPrint&lt;/code&gt;. Each call serialises through a kernel debug port, costs measurable percentage points of CPU on a busy box, and ships data to whoever happens to have the kernel debugger attached. The customer says no. The bug reproduces only at production traffic levels. You cannot ship enough printf-debugging through a debug port to find it.&lt;/p&gt;
&lt;p&gt;That is the engineering pain Insung Park and Ricky Buch&apos;s team was solving when ETW shipped with Windows 2000. Their design moves -- recorded years later in the definitive April 2007 MSDN Magazine article on the Vista upgrade [@ms-park-buch-2007] [@ms-park-buch-2007] -- still define the architecture two and a half decades later.&lt;/p&gt;
&lt;p&gt;The first move was per-CPU ring buffers. A producer on CPU 7 writes to CPU 7&apos;s buffer with no lock contention against producers on other CPUs. Hot-path tracing on a 64-core machine does not serialise. The kernel allocates at least two buffers per logical processor [@ms-event-trace-props] [@ms-event-trace-props] so a producer can keep writing while a writer thread drains the previous buffer.&lt;/p&gt;
&lt;p&gt;The second move was an asynchronous writer thread. The producer never blocks on disk I/O. It writes to its CPU&apos;s buffer and returns. A separate kernel thread drains buffers to file or hands them to a real-time consumer. ETW pushes the latency tax onto the consumer and the storage path, never onto the producer&apos;s hot loop.&lt;/p&gt;
&lt;p&gt;The third move was dynamic enable and disable. Park and Buch describe the resulting capability in one sentence:&lt;/p&gt;

ETW gives you the ability to enable and disable logging dynamically, making it easy to perform detailed tracing in production environments without requiring reboots or application restarts. -- Park &amp;amp; Buch, *MSDN Magazine*, April 2007 [@ms-park-buch-2007]
&lt;p&gt;That sentence is the entire reason ETW could later become the EDR substrate. A producer compiles its trace points into shipping code at low cost; a controller flips them on at runtime when somebody actually wants the data. Without that property, you cannot build a security product that ships universal kernel tracing on a billion endpoints.&lt;/p&gt;
&lt;p&gt;The fourth move was the trichotomy of providers, controllers, and consumers [@ms-etw-wdk] [@ms-etw-wdk]. Microsoft did not write ETW as an internal-only facility. From the start, third parties could write providers (driver authors instrumenting their own code), controllers (performance tools starting and stopping sessions), and consumers (analyzers reading event streams). The architecture is open by design.&lt;/p&gt;

A component that emits ETW events, identified by a GUID. A provider is registered with the system at runtime via the `EventRegister` API (or its predecessor `RegisterTraceGuids` for classic providers) and emits events via `EventWrite` (or `TraceEvent`). Providers ship inside Windows itself, inside Microsoft applications, and inside any third-party binary that wants to expose tracing.

A component that creates, configures, enables, and stops trace sessions. Controllers select which providers a session subscribes to and at which level and keyword bitmask. The Windows Performance Recorder, `logman`, `xperf`, and every EDR&apos;s session-management code are controllers.

A component that reads events from a session in real time or from an `.etl` file on disk. Consumers register a callback that the system invokes once per delivered event. The Windows Performance Analyzer, the krabsetw library, SilkETW, and every EDR&apos;s sensor process are consumers.

flowchart LR
    Ctl[Controller&lt;br /&gt;StartTrace + EnableTrace] --&amp;gt; Sess[Trace Session&lt;br /&gt;per-session buffer pool]
    P1[Provider on CPU 0] --&amp;gt; CPU0[CPU 0 buffer]
    P2[Provider on CPU 1] --&amp;gt; CPU1[CPU 1 buffer]
    P3[Provider on CPU N] --&amp;gt; CPUN[CPU N buffer]
    CPU0 --&amp;gt; WT[Writer thread&lt;br /&gt;asynchronous drain]
    CPU1 --&amp;gt; WT
    CPUN --&amp;gt; WT
    Sess -.governs.-&amp;gt; CPU0
    Sess -.governs.-&amp;gt; CPU1
    Sess -.governs.-&amp;gt; CPUN
    WT --&amp;gt; File[(.etl file)]
    WT --&amp;gt; RT[Real-time consumer&lt;br /&gt;OpenTrace + ProcessTrace]
&lt;p&gt;The original Windows 2000 implementation supported 32 trace sessions running simultaneously [@ms-etw-sessions] [@ms-etw-sessions], a number Microsoft later raised to 64 globally. ETW was framed as a developer-diagnostics facility -- the Windows Driver Kit primary still describes it that way [@ms-etw-wdk] [@ms-etw-wdk] -- and the security-telemetry use case did not exist for almost a decade.&lt;/p&gt;
&lt;p&gt;But the design choices that made ETW good for low-overhead production diagnostics turn out to be exactly the design choices a security telemetry bus needs. Per-CPU buffers solve the multi-core throughput problem. Asynchronous writes solve the producer-latency problem. Dynamic enable solves the always-shipping-but-mostly-off problem. The trichotomy solves the third-party-extensibility problem. Twenty-five years later, every modern Windows EDR consumes telemetry through the same four primitives.Windows 2000&apos;s 32-session global cap [@ms-etw-sessions] is preserved verbatim on the modern Microsoft Learn page: &quot;Windows 2000: Supports only 32 event tracing sessions.&quot; The cap doubled to 64 in later releases and has stayed there ever since.&lt;/p&gt;
&lt;p&gt;The 2000-era design carried one limit, however, that turned out to matter for security: only one trace session could enable a classic provider at a time. The next ten years would be defined by the consequences.&lt;/p&gt;
&lt;h2&gt;3. The MOF era: one session, one steal, one decade of coexistence pain&lt;/h2&gt;
&lt;p&gt;In 2005, a third-party performance monitor that registered a classic provider could find itself silently disabled the moment Microsoft&apos;s &lt;code&gt;wprui.exe&lt;/code&gt; started its own session against the same provider GUID. The first session got no error. It just stopped receiving events. That second-consumer-steals-first behavior is the architectural fact of the entire 2000-2007 era.&lt;/p&gt;
&lt;p&gt;Microsoft Learn still documents the rule in one sentence:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &quot;Up to eight trace sessions can enable and receive events from the same manifest-based provider. However, only one trace session can enable a classic provider. If more than one trace session tries to enable a classic provider, the first session would stop receiving events when the second session enables the provider.&quot; -- Microsoft Learn, Configuring and Starting an Event Tracing Session [@ms-etw-config] [@ms-etw-config]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That single rule made multi-EDR coexistence on classic providers structurally impossible. If Defender&apos;s predecessor and a third-party HIPS both wanted real-time process events from the same classic provider, they had to fight for it. The loser got silence with no notification.&lt;/p&gt;
&lt;p&gt;The provider class involved was &lt;em&gt;MOF-based&lt;/em&gt;, named after the schema language that described its events.&lt;/p&gt;

The schema description language inherited from WBEM (Web-Based Enterprise Management). For ETW, MOF files describe each event a classic provider can emit -- field names, types, tasks, opcodes -- and are compiled into the WMI repository at install time using `mofcomp`. Consumers decode events by querying the WMI repository for the matching MOF schema.

A synonym for *MOF provider*. The original ETW provider class introduced in Windows 2000. Registered with `RegisterTraceGuids`, emits events via `TraceEvent`, decoded against a MOF schema in the WMI repository. Capped at one trace session per provider.
&lt;p&gt;The MOF model was workable for a single-consumer world. A performance-tuning team running an in-house tool could enable the provider, capture, and disable. As the substrate of a security stack with multiple agents on the same host, it could not work. The mid-2000s had not yet produced a &quot;multiple agents on the same host&quot; world, so the limit did not bite immediately. By 2007 it would.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Era&lt;/th&gt;
&lt;th&gt;Schema location&lt;/th&gt;
&lt;th&gt;Sessions/provider&lt;/th&gt;
&lt;th&gt;Adoption in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;MOF / classic&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;WMI repository&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Niche; mostly NT Kernel Logger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPP&lt;/td&gt;
&lt;td&gt;2002&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.pdb&lt;/code&gt; (TMF)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Pervasive inside Windows internals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest-based&lt;/td&gt;
&lt;td&gt;2007 (Vista)&lt;/td&gt;
&lt;td&gt;XML manifest&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Dominant for security telemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TraceLogging&lt;/td&gt;
&lt;td&gt;2015 (Win10)&lt;/td&gt;
&lt;td&gt;Inline (TLV)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Rising for new app/service code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A handful of classic providers survived the 2007 transition and are still significant. The most important is the NT Kernel Logger [@ms-etw-sessions] [@ms-etw-sessions], the special-purpose system session that captures high-throughput kernel events: file I/O, disk I/O, registry operations, network packets. On most consumer SKUs it remains the only path to those event streams at line rate. Sysmon and most kernel-level diagnostics tools use the NT Kernel Logger or its modern descendants.The NT Kernel Logger is a system reserved logger. There is exactly one of it on a host, and the kernel itself owns the buffers. Tools that want kernel disk, file, registry, or network events at high throughput typically subscribe through it rather than through manifest providers. This is why a host can have eight &lt;code&gt;Microsoft-Windows-Kernel-File&lt;/code&gt; consumers but cannot easily have two simultaneous full-fidelity disk I/O traces.&lt;/p&gt;
&lt;p&gt;By 2007 Microsoft knew the one-session limit had to go. The fix shipped with Windows Vista in January 2007, and it was the central architectural decision of the entire ETW-as-EDR-substrate story.&lt;/p&gt;
&lt;h2&gt;4. Vista&apos;s eight sessions: the architectural decision that made the modern EDR endpoint possible&lt;/h2&gt;
&lt;p&gt;Park and Buch open their April 2007 MSDN Magazine article with the line that frames every later development:&lt;/p&gt;

On Windows Vista, ETW has gone through a major upgrade, and one of the most significant changes is the introduction of the unified event provider model and APIs. -- Park &amp;amp; Buch, *MSDN Magazine*, April 2007 [@ms-park-buch-2007]
&lt;p&gt;The new model raised the per-provider session cap from one to eight. That single number is why Defender, CrowdStrike Falcon, SentinelOne, Sysmon, and a researcher&apos;s SilkETW tap can all read &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; [@fireeye-silketw-launch] [@fireeye-silketw-launch] from the same host today without one of them stealing events from the others.&lt;/p&gt;
&lt;p&gt;The Vista model also unified two things that had been separate. ETW providers wrote to per-CPU ring buffers; the Win32 Event Log was a different facility with its own writer, its own format, and its own consumers. Park and Buch describe the unification verbatim:&lt;/p&gt;

The new unified APIs combine logging traces and writing to the Event Viewer into one consistent, easy-to-use mechanism for event providers. -- Park &amp;amp; Buch, *MSDN Magazine*, April 2007 [@ms-park-buch-2007]
&lt;p&gt;After Vista, a single &lt;code&gt;EventWrite&lt;/code&gt; call from a manifest-based provider lands both in the per-CPU ring buffer for ETW consumers &lt;em&gt;and&lt;/em&gt; in the &lt;code&gt;evtx&lt;/code&gt; channel for &lt;code&gt;wevtutil&lt;/code&gt; and Group Policy audit consumers, depending on how the manifest&apos;s channel mappings are configured. The &quot;Event Viewer&quot; the user sees is now a consumer of ETW.&lt;/p&gt;

The Vista-era ETW provider class. The provider author writes an XML manifest enumerating events, fields, tasks, opcodes, levels, keywords, and channels. The `mc.exe` message compiler turns the manifest into a binary resource embedded in the provider binary; `wevtutil im` registers the manifest with the system at install time. At runtime the provider calls `EventRegister` once per provider GUID and `EventWrite` per event. Capped at eight trace sessions per provider.

A logical destination for an event, declared in a manifest. The four standard channels are *Admin* (operational events for administrators), *Operational* (verbose events for operators), *Analytical* (high-volume events for diagnostics), and *Debug* (developer-only events). When the provider&apos;s `EventWrite` fires, the kernel demultiplexes by channel: events with channels enabled in the `evtx` configuration land in the corresponding channel log, while subscribed real-time consumers receive them through their session.
&lt;p&gt;The deployment pipeline for a manifest-based provider is heavier than for a classic provider. The author writes a manifest, compiles it, embeds the resource, and runs &lt;code&gt;wevtutil im&lt;/code&gt; at install time. Microsoft Learn calls out the distinction between provider registration and manifest installation [@ms-eventregister] [@ms-eventregister] explicitly, and notes that each process can register up to 1,024 providers [@ms-eventregister] [@ms-eventregister]. In practice few processes come close.&lt;/p&gt;

flowchart TD
    A[Author writes manifest.xml] --&amp;gt; B[mc.exe compiles to binary resource]
    B --&amp;gt; C[Resource embedded in provider .dll/.exe]
    C --&amp;gt; D[Installer runs wevtutil im manifest.xml]
    D --&amp;gt; E[System-wide manifest registry]
    F[Provider process at runtime] --&amp;gt; G[EventRegister GUID]
    G --&amp;gt; H[EventWrite per event]
    H --&amp;gt; I[Per-CPU ring buffer&lt;br /&gt;for ETW sessions]
    H --&amp;gt; J[Channel demux&lt;br /&gt;Admin / Operational / Analytical / Debug]
    J --&amp;gt; K[(.evtx log files)]
    I --&amp;gt; L[Real-time consumers]
    E -.decode metadata.-&amp;gt; L
    E -.decode metadata.-&amp;gt; K
&lt;p&gt;The cap rules now read like this: eight trace sessions can enable a manifest-based provider concurrently [@ms-about-etw] [@ms-about-etw]; up to 64 sessions can run on the system at once [@ms-etw-sessions] [@ms-etw-sessions]; &lt;code&gt;EnableTraceEx2&lt;/code&gt; returns &lt;code&gt;ERROR_NO_SYSTEM_RESOURCES&lt;/code&gt; when the per-provider cap binds [@ms-enabletraceex2] [@ms-enabletraceex2]. The 8-session number was chosen for ergonomics, not for security planning, but it is the load-bearing number in modern Windows endpoint security.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The eight-session cap on manifest-based providers is the single architectural decision that made multi-EDR coexistence on the same Windows host possible. Without it, the second EDR to subscribe to &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; would silently steal events from the first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A 2007-era driver author shipping the inaugural &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; provider, GUID &lt;code&gt;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&lt;/code&gt;, authored a manifest declaring &lt;code&gt;ProcessStart&lt;/code&gt; (event ID 1), &lt;code&gt;ProcessStop&lt;/code&gt; (event ID 2), &lt;code&gt;ImageLoad&lt;/code&gt; (event ID 5), and so on. Defender&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; could subscribe; the future CrowdStrike Falcon could subscribe; the future Sysmon could subscribe; the future SilkETW researchers could subscribe. None starves another. The Vista unification is the architectural enabler of the modern multi-EDR Windows endpoint.&lt;/p&gt;
&lt;p&gt;With multi-consumer concurrency solved, the next problems were authoring overhead and producer integrity. Two parallel paths branched off the Vista manifest model: TraceLogging for the first, the EtwTi PPL/ELAM gate for the second.&lt;/p&gt;
&lt;h2&gt;5. Two more provider classes: WPP for the kernel tree, TraceLogging for the app tier&lt;/h2&gt;
&lt;p&gt;Vista&apos;s manifest-based providers solved coexistence and decoding, but they were heavy to deploy. Microsoft shipped two more provider classes -- one older than Vista and one younger -- that traded manifest deployment for two different kinds of simplicity.&lt;/p&gt;
&lt;h3&gt;WPP: the C-preprocessor approach&lt;/h3&gt;
&lt;p&gt;WPP -- Windows software trace PreProcessor -- predates Vista. Community references and the Park &amp;amp; Buch description of ETW being &quot;abstracted into the Windows preprocessor (WPP) software tracing technology&quot; [@ms-park-buch-2007] place its first WDK ship in the Windows XP era; no Microsoft primary pins a specific build. It became the standard tracing facility inside the Windows kernel tree itself for years. The WDK page [@ms-wpp] [@ms-wpp] frames its purpose:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;WPP software tracing supplements and enhances WMI event tracing by adding ways to simplify tracing the operation of the trace provider. It is an efficient mechanism for the trace provider to log real-time binary messages.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A WPP provider is authored in C with macros that look like printf calls. The C preprocessor expands &lt;code&gt;DoTraceMessage(FlagId, &quot;Frobnicating widget %d&quot;, widgetId)&lt;/code&gt; into an &lt;code&gt;EventWrite&lt;/code&gt; call against an auto-generated provider GUID. Format strings are extracted at build time into a &lt;em&gt;Trace Message Format&lt;/em&gt; file embedded in the binary&apos;s &lt;code&gt;.pdb&lt;/code&gt;. The producer cost is the smallest of any ETW provider class: emitting an event is a function call plus a few stores into a buffer. There is no manifest to deploy, no XML to author.&lt;/p&gt;
&lt;p&gt;The corresponding decode cost is the highest. A WPP event arrives at the consumer as a binary payload referencing a TMF identifier. To turn that into a human-readable message the consumer needs the producer&apos;s &lt;code&gt;.pdb&lt;/code&gt; file. If you do not have the symbols for the binary that emitted the event, you do not know what the event means.&lt;/p&gt;
&lt;p&gt;That decode cost is why WPP did not become the EDR substrate. Sealighter&apos;s README puts the operational consequence verbatim:&lt;/p&gt;

A C-preprocessor-based ETW authoring path inherited from the XP-era WDK. Format strings are extracted to a TMF resource that lives in the producer&apos;s `.pdb`. Producer cost is minimal; decode cost requires the producer&apos;s symbol files. WPP providers inherit the classic one-session-per-provider cap and are pervasively used inside Windows itself for in-tree dev-time tracing.
&lt;blockquote&gt;
&lt;p&gt;&quot;WPP traces compounds the issues, providing almost no easy-to-find data about provider and their events.&quot; -- Sealighter README [@gh-sealighter] [@gh-sealighter]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;WPP providers also inherit the classic one-session-per-provider cap [@ms-about-etw] [@ms-about-etw], which would have made them unworkable for multi-EDR consumption even if the decode problem were solved. So WPP became the kernel-tree internal tracing facility -- ubiquitous inside Microsoft&apos;s source tree, irrelevant outside it.&lt;/p&gt;
&lt;h3&gt;TraceLogging: schema in the payload&lt;/h3&gt;
&lt;p&gt;Eight years after Vista, in Windows 10 (2015), Microsoft shipped a parallel path that solved a different problem. TraceLogging [@ms-tracelogging-about] [@ms-tracelogging-about] keeps the eight-session cap of manifest providers but eliminates the manifest deployment burden:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;TraceLogging is a system for logging events that can be decoded without a manifest.&quot; -- Microsoft Learn, About TraceLogging [@ms-tracelogging-about] [@ms-tracelogging-about]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A TraceLogging event carries its own schema inline. The event payload is a sequence of typed-length-value triples: a one-byte type tag, a length, and the data. A consumer that has never seen the provider before can still decode the event because the names and types of every field are &lt;em&gt;in the event&lt;/em&gt;. The provider author needs no XML manifest, no &lt;code&gt;mc.exe&lt;/code&gt;, no &lt;code&gt;wevtutil im&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The trade-off is per-event size. Inline schema strings cost bytes per event. For a high-volume provider emitting millions of events per minute, the per-event size matters and a manifest-based provider is correct. For a new component author who wants tracing without an install-time deployment dance, TraceLogging is the right answer.&lt;/p&gt;

A self-describing ETW provider class shipped in Windows 10. Schema is inline in each event payload as type-length-value triples; consumers decode without a manifest. Available from C/C++ via `TraceLoggingProvider.h`, from .NET via `EventSource` with `EtwSelfDescribingEventFormat`, and from WinRT via `LoggingChannel`. Inherits the eight-session cap from the manifest-based class.
&lt;p&gt;TraceLogging is also the unified path across runtimes. The same self-describing payload format is emitted from native C/C++, from .NET (when an &lt;code&gt;EventSource&lt;/code&gt; opts into &lt;code&gt;EtwSelfDescribingEventFormat&lt;/code&gt;), and from kernel-mode drivers [@ms-tracelogging-portal] [@ms-tracelogging-portal]. A consumer using TDH (the Trace Data Helper API) decodes them without distinguishing between the runtime that emitted them.&lt;/p&gt;
&lt;h3&gt;Four classes, four trade-offs&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;First Shipped&lt;/th&gt;
&lt;th&gt;Schema Location&lt;/th&gt;
&lt;th&gt;Sessions/Provider&lt;/th&gt;
&lt;th&gt;Decode without symbols/manifest?&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;MOF / classic&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;WMI repository (&lt;code&gt;mofcomp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Needs MOF&lt;/td&gt;
&lt;td&gt;Legacy components; NT Kernel Logger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPP&lt;/td&gt;
&lt;td&gt;~2002&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.pdb&lt;/code&gt; (TMF)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;No -- needs producer PDB&lt;/td&gt;
&lt;td&gt;In-tree Windows kernel dev-time tracing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest-based&lt;/td&gt;
&lt;td&gt;2007 (Vista)&lt;/td&gt;
&lt;td&gt;XML manifest, system-installed&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Needs installed manifest&lt;/td&gt;
&lt;td&gt;Shipping security telemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TraceLogging&lt;/td&gt;
&lt;td&gt;2015 (Win10)&lt;/td&gt;
&lt;td&gt;Inline TLV in payload&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;New apps and services; cross-runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sources for the table: [@ms-about-etw, @ms-etw-config, @ms-tracelogging-about, @ms-wpp].&lt;/p&gt;

For new shipping Windows components with a known event vocabulary and high volume, choose manifest-based: smallest per-event size, evtx integration, eight-consumer concurrency. For new cross-runtime open-source providers where deployment friction matters, choose TraceLogging: same eight-consumer concurrency, no XML to author, decodable everywhere. For in-source-tree dev-time tracing inside a binary you already have symbols for, WPP is fine. For new security-relevant providers, never choose classic: the one-session cap is structurally incompatible with multi-EDR coexistence.
&lt;p&gt;Four provider classes, four trade-offs. But every one of them shares a structural weakness: the producer fires from inside the calling process, and any code in that process can patch the runtime entry-point and silence the provider for itself. That is the weakness Adam Chester made famous in 2020, and the one EtwTi was built to defeat.&lt;/p&gt;
&lt;h2&gt;6. Sessions, buffers, and the autologger registry: where the telemetry actually lives&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;regedit&lt;/code&gt; on a Windows host and navigate to &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger&lt;/code&gt;. You are looking at the persistence surface of every trace session that survives a reboot on this machine -- and the persistence surface every modern EDR uses to install itself.&lt;/p&gt;
&lt;p&gt;A session is the unit ETW actually exposes to controllers. It owns a per-session pool of buffers, a writer thread, a destination (file or real-time consumer), and a list of providers it has subscribed to. The lifecycle is short. A controller fills out an &lt;code&gt;EVENT_TRACE_PROPERTIES&lt;/code&gt; structure [@ms-event-trace-props] [@ms-event-trace-props] with a session name, buffer size, logging mode, and destination, then calls &lt;code&gt;StartTrace&lt;/code&gt;. The kernel allocates the buffers -- at least two per logical processor [@ms-event-trace-props] [@ms-event-trace-props] -- and returns a session handle. The controller then calls &lt;code&gt;EnableTraceEx2&lt;/code&gt; [@ms-enabletraceex2] [@ms-enabletraceex2] for each provider it wants to subscribe to, passing &lt;code&gt;EVENT_CONTROL_CODE_ENABLE_PROVIDER&lt;/code&gt; along with the provider GUID, level, and keyword bitmask.&lt;/p&gt;
&lt;p&gt;If the provider&apos;s per-class session cap is already saturated, &lt;code&gt;EnableTraceEx2&lt;/code&gt; returns &lt;code&gt;ERROR_NO_SYSTEM_RESOURCES&lt;/code&gt;. If the caller lacks the privilege to enable that provider, it returns &lt;code&gt;ERROR_ACCESS_DENIED&lt;/code&gt;. We will see both error codes again later, on different paths.The default buffer size sweet spot is small. The Microsoft Learn primary states it explicitly: &quot;Trace sessions with large buffers (256KB or larger) should be used only for diagnostic investigations or testing, not for production tracing.&quot; [@ms-event-trace-props] Production session buffer sizes typically sit in the 32-64KB range.&lt;/p&gt;
&lt;p&gt;There are three logging modes. &lt;em&gt;File mode&lt;/em&gt; writes events to a sequential &lt;code&gt;.etl&lt;/code&gt; file on disk; the writer thread drains buffers to disk and the file grows. &lt;em&gt;Circular mode&lt;/em&gt; writes to a fixed-size file in a circular buffer; old events are overwritten when the file fills. &lt;em&gt;Real-time mode&lt;/em&gt; delivers events to a real-time consumer process via a kernel callback. Defender, EDR sensors, and Sysmon all use real-time mode for their hot paths; they may also write to file as a forensic backup.&lt;/p&gt;

A process that calls `OpenTrace` with `LogFileMode = EVENT_TRACE_REAL_TIME_MODE` and receives events live via a registered callback rather than from an `.etl` file on disk. Real-time consumers must keep up with producer rate or events are lost.
&lt;p&gt;The autologger registry path is what makes a session survive a reboot. A subkey under &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&amp;lt;SessionName&amp;gt;&lt;/code&gt; defines a session that the kernel starts at boot, before most user-mode services are running. Each subkey&apos;s values configure the session: &lt;code&gt;BufferSize&lt;/code&gt;, &lt;code&gt;MaximumBuffers&lt;/code&gt;, &lt;code&gt;LogFileMode&lt;/code&gt;, &lt;code&gt;FileName&lt;/code&gt;, plus a nested &lt;code&gt;&amp;lt;SessionName&amp;gt;\&amp;lt;ProviderGuid&amp;gt;&lt;/code&gt; subkey for each provider to enable.&lt;/p&gt;

A registry-persisted boot-time ETW session. The kernel reads `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\` at boot, creates the session, enables the configured providers, and begins capture before user-mode services start. Defender&apos;s Sense agent, CrowdStrike&apos;s Falcon sensor, and Sysmon&apos;s driver all install autologgers here.
&lt;p&gt;Defender&apos;s &lt;code&gt;DiagTrack&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Diagnosis-PCW&lt;/code&gt;, the SQM kernel logger, the EventLog-Application channel autologger -- all live here (observable via &lt;code&gt;logman query -ets&lt;/code&gt; on a stock Windows install). Third-party EDRs add their own. The Palantir CIRT taxonomy [@palantir-tampering-wayback] (about which more in section 11) frames this registry surface as the persistent-tampering target: an attacker who can write to this subtree can disable an EDR&apos;s boot-time tracing without ever interacting with the running EDR process. The events of interest never get captured because the session never starts.&lt;/p&gt;
&lt;p&gt;There is a related concept worth naming: the &lt;em&gt;Global Logger&lt;/em&gt;. This is a special autologger session whose configuration lives in &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\GlobalLogger&lt;/code&gt;. It is the boot-time tracing path that comes online before any user-mode service, including before Sense and the EDR sensor. It exists to capture early-boot kernel events that no later session can record.&lt;/p&gt;

flowchart TD
    R[HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\] --&amp;gt; S1[DiagTrack-Listener]
    R --&amp;gt; S2[Defender-Listener]
    R --&amp;gt; S3[ThirdPartyEDR-Sensor]
    R --&amp;gt; SG[GlobalLogger]
    S2 --&amp;gt; S2P[Provider GUIDs subkeys]
    S2 --&amp;gt; S2C[BufferSize / MaximumBuffers / LogFileMode]
    S2 --&amp;gt; S2F[FileName=.etl path]
    S2P --&amp;gt; KS[Kernel reads at boot]
    S2C --&amp;gt; KS
    S2F --&amp;gt; KS
    KS --&amp;gt; Started[Session started before user-mode services]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;logman query -ets&lt;/code&gt; enumerates every live trace session on the host. Cross-reference against the subkeys in &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt; to find sessions configured to start at boot. Any unauthorised entry -- a session you do not recognise, an autologger pointed at a destination outside your EDR&apos;s data path, a provider GUID you cannot account for -- belongs in your incident response queue. We return to this in section 14.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;ERROR_NO_SYSTEM_RESOURCES&lt;/code&gt; from &lt;code&gt;EnableTraceEx2&lt;/code&gt; is the runtime symptom of the eight-session cap binding [@ms-enabletraceex2]. SOC engineers debugging multi-EDR coexistence problems should look for it in their sensor&apos;s diagnostic output. Eight subscribers per manifest provider is enough for the typical Defender + third-party EDR + Sysmon + research tap arrangement, but a host running multiple research-mode tracers can saturate it.&lt;/p&gt;
&lt;p&gt;Persistence solved: a session the OS starts at every boot. But who reads it? That requires a consumer process, and consumers are where the architecture forks along the security spectrum.&lt;/p&gt;
&lt;h2&gt;7. Consumer architecture: from &lt;code&gt;OpenTrace&lt;/code&gt; to KrabsETW to a 30-line process watcher&lt;/h2&gt;
&lt;p&gt;The consumer side of ETW is mechanically simple -- three calls to open a trace, register a callback, and process events -- but the choice of library tells you almost everything about what kind of EDR you are building.&lt;/p&gt;
&lt;p&gt;The native pattern is three Win32 calls. &lt;code&gt;EnableTraceEx2&lt;/code&gt; subscribes the session to a provider GUID with a level and keyword bitmask. &lt;code&gt;OpenTrace&lt;/code&gt; returns a handle on the session for consumption. &lt;code&gt;ProcessTrace&lt;/code&gt; blocks the calling thread, drains events from the kernel&apos;s per-CPU buffers, and dispatches each one to a registered callback. Each event arrives as an &lt;code&gt;EVENT_RECORD&lt;/code&gt; containing a header (provider GUID, event ID, level, keyword, opcode, timestamp, process ID, thread ID) and a payload that the consumer decodes.&lt;/p&gt;
&lt;p&gt;For manifest providers the consumer decodes via TDH (the Trace Data Helper API) against the system-installed manifest. For TraceLogging providers the consumer decodes from the inline TLV payload. For classic and WPP providers the consumer needs the MOF schema or the producer&apos;s PDB respectively.&lt;/p&gt;

The Win32 decoder API that turns a raw `EVENT_RECORD` payload into typed fields, using the registered manifest as the schema source. `TdhGetEventInformation` returns a `TRACE_EVENT_INFO` structure with the field names, types, and offsets; `TdhFormatProperty` extracts each field. TDH is what makes manifest events self-describing at the consumer end, even though the schema lives out of band.

sequenceDiagram
    participant C as Consumer process
    participant K as Kernel ETW subsystem
    participant P as Provider process
    C-&amp;gt;&amp;gt;K: StartTrace(session)
    C-&amp;gt;&amp;gt;K: EnableTraceEx2(session, providerGuid, level, keyword)
    K--&amp;gt;&amp;gt;P: Provider notified to begin emitting
    C-&amp;gt;&amp;gt;K: OpenTrace(session)
    K--&amp;gt;&amp;gt;C: TraceHandle
    C-&amp;gt;&amp;gt;K: ProcessTrace(handle) [blocking]
    P-&amp;gt;&amp;gt;K: EventWrite(payload)
    K--&amp;gt;&amp;gt;C: callback(EVENT_RECORD)
    P-&amp;gt;&amp;gt;K: EventWrite(payload)
    K--&amp;gt;&amp;gt;C: callback(EVENT_RECORD)
    Note over C,K: ProcessTrace returns only when session ends
&lt;p&gt;In production almost no one writes the raw three-call pattern. The library universe settled into a small set of widely-used wrappers, and the choice of wrapper maps almost one-to-one onto the kind of EDR the engineering team is building.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;krabsetw&lt;/strong&gt; [@gh-krabsetw] [@gh-krabsetw] is a Microsoft-authored C++ library that simplifies session and provider management. Its README explicitly notes the production caller: a C++/CLI wrapper called &lt;code&gt;Microsoft.O365.Security.Native.ETW&lt;/code&gt;, &quot;used in production by the Office 365 Security team. It&apos;s affectionately referred to as Lobsters.&quot; If you are building an in-house EDR or a security analytics pipeline in C++ on Windows, krabsetw is the default choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microsoft.Diagnostics.Tracing.TraceEvent&lt;/strong&gt; [@nuget-traceprocessing] [@nuget-traceprocessing] is the general-purpose .NET ETW library, distributed as a NuGet package and used heavily inside the .NET diagnostics community. Microsoft&apos;s separate &lt;code&gt;Microsoft.Windows.EventTracing.Processing.All&lt;/code&gt; package is the .NET TraceProcessing API [@ms-etw-portal] [@ms-etw-portal] that the Windows engineering team uses internally to analyze ETW data from the Windows engineering system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SilkETW&lt;/strong&gt; [@gh-silketw] [@gh-silketw], originally released by Ruben Boonen at FireEye in March 2019 [@fireeye-silketw-launch] [@fireeye-silketw-launch] (now maintained by Mandiant), wraps &lt;code&gt;Microsoft.Diagnostics.Tracing.TraceEvent&lt;/code&gt; to expose ETW telemetry to detection-engineering and threat-hunting workflows. SilkETW is the canonical &quot;blue team research&quot; consumer: the tool you reach for when you want to see what events a provider actually emits without writing C++.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sealighter&lt;/strong&gt; [@gh-sealighter] [@gh-sealighter], by &lt;code&gt;pathtofile&lt;/code&gt;, is a krabsetw-wrapping C++ tool that makes multi-provider subscription and filtering tractable from a JSON config. The README states: &quot;Sealighter leverages the feature-rich Krabs ETW Library to enable detailed filtering and triage of ETW and WPP Providers and Events.&quot; Sealighter is the canonical &quot;red/blue team triage&quot; consumer: more flexible than SilkETW, less code to write than raw krabsetw.&lt;/p&gt;
&lt;p&gt;The pitfalls are universal across all four libraries. The krabsetw README spells two of them out:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The call to &apos;start&apos; on the trace object is blocking so thread management may be necessary.&quot; -- [@gh-krabsetw]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Throwing exceptions in the event handler callback ... will cause the trace to stop processing events.&quot; -- [@gh-krabsetw]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Both have caused real production outages. An EDR that throws an unhandled exception in its event callback dies silently as an ETW consumer, and the next event the provider emits goes nowhere.The &quot;throwing in the callback stops the trace&quot; pitfall is the gotcha that bites every team writing their first ETW consumer. The kernel does not catch the exception; the trace simply ends. A production-quality consumer wraps every callback in try/catch (or its language equivalent) and routes failures through a side channel, not through the trace itself.&lt;/p&gt;
&lt;p&gt;To make the structure concrete, here is what a 30-line &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; real-time consumer looks like, written in TypeScript pseudocode that mirrors the structure a Sealighter or krabsetw user would write:&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode: the structure of a krabsetw / Sealighter consumer
// for the Microsoft-Windows-Kernel-Process provider.&lt;/p&gt;
&lt;p&gt;const KERNEL_PROCESS_GUID = &quot;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&quot;;&lt;/p&gt;
&lt;p&gt;const session = new UserTraceSession(&quot;MyEdrSensor&quot;);&lt;/p&gt;
&lt;p&gt;const provider = new Provider(KERNEL_PROCESS_GUID);
provider.level = TraceLevel.Information;
provider.anyKeyword = 0xFFFFFFFFFFFFFFFFn;&lt;/p&gt;
&lt;p&gt;provider.onEvent = (event) =&amp;gt; {
  try {
    switch (event.id) {
      case 1: // ProcessStart
        const pid = event.fields.ProcessID;
        const imageName = event.fields.ImageName;
        const cmdLine = event.fields.CommandLine;
        console.log(`Process start pid=${pid} image=${imageName}`);
        break;
      case 2: // ProcessStop
        console.log(`Process stop pid=${event.fields.ProcessID}`);
        break;
      case 5: // ImageLoad
        console.log(`Image load ${event.fields.ImageName} into pid=${event.fields.ProcessID}`);
        break;
    }
  } catch (e) {
    // never let an exception escape the callback
    sideChannelLog(e);
  }
};&lt;/p&gt;
&lt;p&gt;session.enable(provider);
session.start();  // blocks until session.stop() is called
`}&lt;/p&gt;
&lt;p&gt;That code, in production form, is a working EDR sensor&apos;s process watcher. Every commercial Windows EDR has something with the same structure inside it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; krabsetw wraps the C++ surface and is the default for production in-house EDRs. TraceEvent wraps .NET and is the default for diagnostics tooling. SilkETW exposes ETW to detection engineers without C++. Sealighter wraps krabsetw with a config file for triage. Pick the library that matches the team that will own the consumer, not the one that looks most powerful.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is what Sysmon, Wazuh, and Elastic Defend look like under the hood -- a SYSTEM-privileged user-mode service consuming public providers. But there is one provider this code cannot subscribe to. Try it and &lt;code&gt;EnableTraceEx2&lt;/code&gt; returns &lt;code&gt;ERROR_ACCESS_DENIED&lt;/code&gt;. The next two sections are about the GUID that requires a passport.&lt;/p&gt;
&lt;h2&gt;8. The security provider catalogue: what EDRs actually read&lt;/h2&gt;
&lt;p&gt;There are roughly 1,300 manifest-based providers shipped on a 2026 Windows 11 24H2 install -- the community-maintained jdu2600 inventory [@gh-jdu2600] [@gh-jdu2600] tracks the count across builds, and the repnz manifest archive [@gh-repnz] [@gh-repnz] holds byte-stable copies of the manifests for cross-version diffing. Eight of those providers carry almost all the security telemetry the EDR vendors read. This is the catalogue.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Security-Auditing&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{54849625-5478-4994-A5BA-3E3B0328C30D}&lt;/code&gt;. The audit-policy-driven Security event log producer. Event ID 4624 (logon), 4625 (failed logon), 4634 (logoff), 4688 (process create with command line) [@learn-microsoft-com-event-4688] [@ms-event-4624], 4689 (process exit), and the broader subcategory audit policy events. This is the closure for the legacy Security event log: when an administrator turns on &quot;audit logon events&quot; in the local security policy, this is the provider that emits the events. EDRs that consume it are reading the same stream the Event Viewer&apos;s Security log shows.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&lt;/code&gt;. The canonical real-time process telemetry source for non-PPL EDR. Event ID 1 fires on &lt;code&gt;ProcessStart&lt;/code&gt; with PID, parent PID, image name, command line, and SID; event ID 2 on &lt;code&gt;ProcessStop&lt;/code&gt;; event ID 3 on thread create; event ID 4 on thread exit; event ID 5 on &lt;code&gt;ImageLoad&lt;/code&gt; with the loaded module name and base address. SilkETW&apos;s launch post enumerates the event record format inline [@fireeye-silketw-launch] [@fireeye-silketw-launch]. This provider is widely cited in EDR community documentation as available since Windows 7, though no Microsoft primary pins the exact build.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Kernel-File&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Kernel-Network&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Kernel-Registry&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The per-subsystem siblings of &lt;code&gt;Kernel-Process&lt;/code&gt;. &lt;code&gt;Kernel-File&lt;/code&gt; surfaces file open / close / read / write / delete operations with the file path and the operating PID. &lt;code&gt;Kernel-Network&lt;/code&gt; surfaces TCP and UDP send / receive with the local and remote endpoints. &lt;code&gt;Kernel-Registry&lt;/code&gt; surfaces registry create / open / set value / delete with the key path and value name. All three use the manifest-based class and inherit the eight-session cap. EDRs that want full-fidelity per-syscall telemetry without writing kernel callbacks subscribe to these three.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Antimalware-Scan-Interface&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{2A576B87-09A7-520E-C21A-4942F0271D67}&lt;/code&gt;, documented in the Microsoft Learn AMSI portal [@ms-amsi-portal] [@ms-amsi-portal] and surveyed in the Palantir CIRT taxonomy [@palantir-tampering-wayback] [@palantir-tampering-wayback]. This is the ETW provider that surfaces AMSI scan results: a script block submitted by PowerShell, JScript, VBA, an Office macro engine, or any other AMSI client comes through here &lt;em&gt;after deobfuscation&lt;/em&gt;. Whatever string the script engine is about to execute, the registered antimalware engine sees in plaintext, and the result of the scan is published via this provider for any listener.&lt;/p&gt;

A COM interface exposed by Windows since 2015 that script engines and runtime hosts can call into to submit content for malware scanning. The Microsoft Learn AMSI portal lists PowerShell, JScript and VBScript via Windows Script Host, Office VBA macros, and User Account Control as in-box integrators [@ms-amsi-portal]; the .NET CLR&apos;s assembly load path joined the list with .NET Framework 4.8, as documented in Adam Chester&apos;s CLR walk-through [@xpn-hiding-dotnet]. The scanned content is the post-deobfuscation form -- the actual code about to execute, not the obfuscated wrapper. Scan results surface via the `Microsoft-Antimalware-Scan-Interface` ETW provider.
&lt;p&gt;The AMSI Operational event log channel typically appears empty by default. The Palantir taxonomy [@palantir-tampering-wayback] [@palantir-tampering-wayback] notes the keyword bitmask configured for the channel does not surface scan-result events. The events fire on the ETW bus and can be consumed in real time, but they do not land in the user-visible evtx log unless the consumer reconfigures the keyword mask.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-PowerShell&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{a0c1853b-5c40-4b15-8766-3cf1c58f985a}&lt;/code&gt;. Event ID 4104 is the script-block-logging event that records each PowerShell script block before execution; event ID 4103 records pipeline execution detail; event ID 4100 records errors. The Microsoft Learn &lt;code&gt;about_Logging_Windows&lt;/code&gt; reference (Windows PowerShell 5.1) [@ms-powershell-logging] [@ms-powershell-logging] documents EID 4104 verbatim (&quot;&lt;code&gt;EventId 4104 / 0x1008&lt;/code&gt; ... &lt;code&gt;Channel Operational&lt;/code&gt; ... &lt;code&gt;Task CommandStart&lt;/code&gt;&quot;) and the script-block-logging configuration. PowerShell Core 7+ uses a separate ETW provider (&lt;code&gt;PowerShellCore&lt;/code&gt;, GUID &lt;code&gt;{f90714a8-5509-434a-bf6d-b1624c8a19a2}&lt;/code&gt;). Combined with AMSI the two providers give an EDR the executed PowerShell content twice: once at AMSI submission, once at script-block logging. Detection engineers use both as cross-checks.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{e13c0d23-ccbc-4e12-931b-d9cc2eee27e4}&lt;/code&gt;, verbatim in Adam Chester&apos;s PoC source [@xpn-hiding-dotnet] [@xpn-hiding-dotnet]. The .NET CLR provider. Surfaces assembly load events, JIT compilation, AppDomain creation, exception throws. Critical for detecting Cobalt Strike&apos;s &lt;code&gt;execute-assembly&lt;/code&gt; style of in-memory .NET payload loading. This is the provider that goes dark in the section 1 hook scene after the operator&apos;s &lt;code&gt;EtwEventWrite&lt;/code&gt; patch.This is the provider Adam Chester targeted in the canonical March 17, 2020 ETW patching post [@xpn-hiding-dotnet]. The Cobalt Strike &lt;code&gt;execute-assembly&lt;/code&gt; workflow produces a loud signal here -- &quot;assembly X loaded into PID Y from in-memory source Z&quot; -- so silencing it locally was a valuable evasion. The story comes back in section 11.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{5770385F-C22A-43E0-BF4C-06F5698FFBD9}&lt;/code&gt;, surfaced by &lt;code&gt;wevtutil gp Microsoft-Windows-Sysmon&lt;/code&gt; and inventoried in [@gh-jdu2600]; the Microsoft Learn Sysmon page by Russinovich and Garnier [@ms-sysmon] [@ms-sysmon] documents authorship, the protected-process status, and the &lt;code&gt;Microsoft-Windows-Sysmon/Operational&lt;/code&gt; channel. This is the &lt;em&gt;publishing&lt;/em&gt; side of Sysmon. Sysmon&apos;s kernel driver &lt;code&gt;SysmonDrv.sys&lt;/code&gt; collects events through &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; and friends; the user-mode service then republishes via this ETW provider so any consumer (a SIEM forwarder, a SOC dashboard, a custom analytic) can subscribe without writing its own kernel driver. Events also land in the &lt;code&gt;Microsoft-Windows-Sysmon/Operational&lt;/code&gt; evtx channel.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; (EtwTi)&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&lt;/code&gt;, verbatim in the fluxsec.red walkthrough [@fluxsec-eti] [@fluxsec-eti]. The only ETW source in the catalogue that fires from inside the kernel for memory-modifying syscalls. Ten task IDs, all prefixed &lt;code&gt;KERNEL_THREATINT_TASK_&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ALLOCVM&lt;/code&gt; (&lt;code&gt;NtAllocateVirtualMemory&lt;/code&gt; -- local and cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROTECTVM&lt;/code&gt; (&lt;code&gt;NtProtectVirtualMemory&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAPVIEW&lt;/code&gt; (section mapping; cross-process and self)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QUEUEUSERAPC&lt;/code&gt; (&lt;code&gt;NtQueueApcThread&lt;/code&gt; cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SETTHREADCONTEXT&lt;/code&gt; (&lt;code&gt;NtSetContextThread&lt;/code&gt; cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;READVM&lt;/code&gt; (&lt;code&gt;NtReadVirtualMemory&lt;/code&gt; -- local and cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WRITEVM&lt;/code&gt; (&lt;code&gt;NtWriteVirtualMemory&lt;/code&gt; -- local and cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUSPENDRESUME_THREAD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUSPENDRESUME_PROCESS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DRIVER_DEVICE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each task pairs with a 64-bit keyword bitmask that distinguishes &lt;code&gt;LOCAL&lt;/code&gt; vs &lt;code&gt;REMOTE&lt;/code&gt; (cross-process) and &lt;code&gt;KERNEL_CALLER&lt;/code&gt; vs not. The Elastic Security Labs walkthrough [@elastic-doubling-down] [@elastic-doubling-down] lists the named Win32/Nt syscalls that surface here:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The most notable addition to this visibility is the Microsoft-Windows-Threat-Intelligence Event Tracing for Windows (ETW) provider ... VirtualAlloc, VirtualProtect, MapViewOfFile, VirtualAllocEx, VirtualProtectEx, MapViewOfFile2, QueueUserAPC, SetThreadContext, WriteProcessMemory, ReadProcessMemory(lsass)&quot; -- Elastic Security Labs [@elastic-doubling-down] [@elastic-doubling-down]&lt;/p&gt;
&lt;/blockquote&gt;

The kernel-emitted ETW provider for memory-modifying syscalls. GUID `{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}`. Events are emitted from the kernel side of the syscall path (not from a user-mode trampoline), which makes the provider unreachable from a user-mode patcher in the calling process. Consumption is gated behind Protected Process Light at the Antimalware signer level, paired with an Early Launch Antimalware driver. The provider first shipped in the Windows 10 RS-era; the precise build is not stated verbatim in any Microsoft primary located, with community references converging on no later than 1709.
&lt;p&gt;The first-ship-build is hedged: the provider GUID and task inventory are well-documented in third-party reverse-engineering primaries, but no Microsoft primary located in the source verification stage pins the exact build. The community reference range is Windows 10 1607 (RS1) through 1709 (RS3). The dispositive practical evidence is Yarden Shafir&apos;s 2023 Trail of Bits walkthrough [@trailofbits-shafir] [@trailofbits-shafir], which shows live-debugger output of &lt;code&gt;CSFalconService.exe&lt;/code&gt; (CrowdStrike) holding &lt;code&gt;EtwConsumer&lt;/code&gt; handles to multiple logger IDs simultaneously. By 2023 third-party EDRs were demonstrably consuming EtwTi at scale.&lt;/p&gt;
&lt;h3&gt;The catalogue as a single screen&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider name&lt;/th&gt;
&lt;th&gt;GUID&lt;/th&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Primary source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Security-Auditing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{54849625-5478-4994-A5BA-3E3B0328C30D}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Audit-policy events (4624/4625/4688/...)&lt;/td&gt;
&lt;td&gt;None (Local Security Policy)&lt;/td&gt;
&lt;td&gt;[@ms-event-4624]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-Process&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Process / thread / image-load events&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@fireeye-silketw-launch], [@gh-jdu2600]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-File&lt;/td&gt;
&lt;td&gt;(manifest archive)&lt;/td&gt;
&lt;td&gt;File I/O syscalls&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@gh-repnz]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-Network&lt;/td&gt;
&lt;td&gt;(manifest archive)&lt;/td&gt;
&lt;td&gt;TCP/UDP send/receive&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@gh-repnz]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-Registry&lt;/td&gt;
&lt;td&gt;(manifest archive)&lt;/td&gt;
&lt;td&gt;Registry create/open/set/delete&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@gh-repnz]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Antimalware-Scan-Interface&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{2A576B87-09A7-520E-C21A-4942F0271D67}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post-deobfuscation script content&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@ms-amsi-portal], [@palantir-tampering-wayback]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-PowerShell&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{a0c1853b-5c40-4b15-8766-3cf1c58f985a}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Script-block logging (4104), pipeline&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-DotNETRuntime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{e13c0d23-ccbc-4e12-931b-d9cc2eee27e4}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CLR assembly load, JIT, exceptions&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@xpn-hiding-dotnet]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Sysmon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{5770385F-C22A-43E0-BF4C-06F5698FFBD9}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sysmon driver re-publication&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@ms-sysmon]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Threat-Intelligence&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Memory-modifying syscalls (kernel-emitted)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PPL + ELAM (Antimalware signer level)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[@fluxsec-eti], [@elastic-doubling-down]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

This is the *security* catalogue. The full Windows manifest-based provider list is roughly 1,300 entries on a current Windows 11 build; performance-tuning, diagnostic, and developer-facing providers fill out the rest. The jdu2600 inventory [@gh-jdu2600] [@gh-jdu2600] tracks the full list across Win10 versions; the repnz archive [@gh-repnz] [@gh-repnz] preserves byte-stable manifest copies for cross-version diffing.
&lt;p&gt;Nine of the ten rows in that table are accessible to any SYSTEM-privileged user-mode service. The tenth -- EtwTi -- requires a passport. The next section is about who issues the passport.&lt;/p&gt;
&lt;h2&gt;9. The PPL / ELAM gate: why EtwTi is not for everyone&lt;/h2&gt;
&lt;p&gt;To consume the one ETW provider that fires from the kernel for memory-modifying syscalls, your service must be (a) a Protected Process Light [@paragmali-com-app-ide], (b) signed at the Antimalware signer level with EKU &lt;code&gt;1.3.6.1.4.1.311.61.4.1&lt;/code&gt;, and (c) loaded from disk by an Early Launch Antimalware [@paragmali-com-to-userini] driver registered at boot. Two of those three were not possible for third parties until the Windows 10 RS-era.&lt;/p&gt;
&lt;p&gt;fluxsec.red [@fluxsec-eti] [@fluxsec-eti] gives the prerequisite list verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;In order to start receiving ETW:TI signals, we need: 1. A service running as Protected Process Light, 2. An Early Launch Antimalware driver and certificate, 3. A logging mechanism.&quot; -- [@fluxsec-eti]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Each prerequisite has a story.&lt;/p&gt;
&lt;h3&gt;Protected Process Light at the Antimalware signer level&lt;/h3&gt;
&lt;p&gt;Windows 8.1 introduced the &lt;em&gt;protected service&lt;/em&gt; concept specifically for antimalware engines. The motivation was simple: a malicious process running as administrator should not be able to inject code into the antimalware service or attach a debugger to it. The Microsoft Learn primary [@ms-protect-am] [@ms-protect-am] sets out the model:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Windows 8.1 introduced a new concept of protected services to protect anti-malware services... In addition to the existing ELAM driver certification requirements, the driver must have an embedded resource section containing the information of the certificates used to sign the user mode service binaries.&quot; -- [@ms-protect-am]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PPL is a process-protection level. A given process has a level on the PPL lattice; another process can open it for write or debug only if the requesting process&apos;s level is greater than or equal to the target&apos;s. Antimalware-PPL is a &lt;em&gt;signer level&lt;/em&gt; on that lattice. The kernel admits a process to Antimalware-PPL when its image is signed with a certificate whose EKU includes &lt;code&gt;1.3.6.1.4.1.311.61.4.1&lt;/code&gt; (Windows Antimalware) &lt;em&gt;and&lt;/em&gt; whose certificate is enrolled in an ELAM driver&apos;s allow-list at boot.&lt;/p&gt;

A Windows process-protection model. Each process has a PPL level; another process may open it for write or debug only if the requestor is at an equal or higher level. Originally introduced for DRM, the lattice was extended in Windows 8.1 to host the Antimalware signer level for protecting antimalware services from administrative-rights attackers.

A specific signer level on the PPL lattice. Reserved in Windows 8.1 for Microsoft Defender; opened to third-party EDR vendors via ELAM onboarding in the Windows 10 RS-era. Consumption of the `Microsoft-Windows-Threat-Intelligence` ETW provider is gated at the Antimalware signer level: an `EnableTraceEx2` call from a non-Antimalware-PPL caller against the EtwTi GUID returns `ERROR_ACCESS_DENIED` (the `EnableTraceEx2` [@ms-enabletraceex2] [@ms-enabletraceex2] page documents the error code for callers that lack the documented administrative groups; the per-provider PPL-signer-level check that triggers it for the EtwTi GUID specifically is described in the [@fluxsec-eti] prerequisite list).
&lt;h3&gt;Early Launch Antimalware&lt;/h3&gt;
&lt;p&gt;ELAM is a driver class that loads before any other non-Microsoft boot driver. The Microsoft Learn primary [@ms-elam] [@ms-elam] describes it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Because an ELAM service runs as a PPL (Protected Process Light), you need to debug using a kernel debugger... AM drivers are initialized first and allowed to control the initialization of subsequent boot drivers, potentially not initializing unknown boot drivers.&quot; -- [@ms-elam]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The boot sequence runs like this. Winload loads the ELAM driver as part of the early-boot path. The ELAM driver registers a callback via &lt;code&gt;IoRegisterBootDriverCallback&lt;/code&gt; and gets to inspect each subsequent boot driver, returning a verdict (initialize / do not initialize / unknown) based on the certificate inventory it carries in its embedded resource section. The kernel honours that verdict. After boot drivers settle, the SCM launches the paired user-mode antimalware service with the &lt;code&gt;LaunchProtected = SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT&lt;/code&gt; flag, and the kernel admits that service to Antimalware-PPL because its signing certificate matches an entry in the ELAM driver&apos;s allow-list.&lt;/p&gt;

A driver class that loads before any non-Microsoft boot driver. The ELAM driver registers a boot-driver callback to inspect subsequent drivers and an embedded-resource certificate inventory of permitted user-mode antimalware service signatures. Together with PPL, ELAM gates which user-mode antimalware services can pass the Antimalware-PPL admission check.
&lt;h3&gt;The 1709 onboarding&lt;/h3&gt;
&lt;p&gt;Microsoft Defender&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; ran at the Antimalware signer level by default starting around the Windows 10 1709 timeframe (October 17, 2017), and the same release is widely cited in EDR-vendor documentation as the moment the Antimalware-PPL onboarding was extended to third-party EDR vendors. The Microsoft primary that pins the 1709 third-party onboarding date is not in the public ETW documentation; we treat the date as widely-cited rather than verified.&lt;/p&gt;
&lt;p&gt;The dispositive practical evidence is the Trail of Bits 2023 walkthrough by Yarden Shafir [@trailofbits-shafir] [@trailofbits-shafir]. Shafir&apos;s WinDbg JS scripts walk the live &lt;code&gt;_ETW_REALTIME_CONSUMER&lt;/code&gt; data structures of a running Windows host and print:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Process CSFalconService.exe with ID 0x1e54 has handle 0x760 to Logger ID 3&quot; -- [@trailofbits-shafir]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is CrowdStrike&apos;s user-mode service, holding a real-time consumer handle to an EtwTi logger session. By 2023 the third-party Antimalware-PPL story is operationally complete.&lt;/p&gt;

sequenceDiagram
    participant BL as Winload (boot)
    participant EL as ELAM Driver
    participant SCM as Service Control Manager
    participant SVC as EDR Service
    participant K as Kernel ETW
    BL-&amp;gt;&amp;gt;EL: Load ELAM driver (early boot)
    EL-&amp;gt;&amp;gt;EL: Register IoRegisterBootDriverCallback then read embedded cert inventory
    Note over EL: ELAM gates subsequent boot drivers
    SCM-&amp;gt;&amp;gt;SVC: Start EDR service with PROTECTED_ANTIMALWARE_LIGHT flag
    K-&amp;gt;&amp;gt;SVC: Verify signature against ELAM allow-list
    K--&amp;gt;&amp;gt;SVC: Admit to Antimalware-PPL
    SVC-&amp;gt;&amp;gt;K: EnableTraceEx2(session, EtwTi GUID, ...)
    K-&amp;gt;&amp;gt;K: Check caller signer level ge Antimalware
    K--&amp;gt;&amp;gt;SVC: SUCCESS
    Note over SVC,K: Non-PPL caller would receive ERROR_ACCESS_DENIED here
&lt;h3&gt;Why this gate matters for the section 1 hook&lt;/h3&gt;
&lt;p&gt;The asymmetry that defines the entire generation is one sentence in the fluxsec.red walkthrough [@fluxsec-eti] [@fluxsec-eti]:&lt;/p&gt;

We cannot patch out the Threat Intelligence provider as this is emitted from within the kernel itself. To do so, you&apos;d require kernelmode execution and then to patch out those signals so no ETW signals are emitted. -- [@fluxsec-eti]
&lt;p&gt;That is the answer to the puzzle the section 1 hook posed. The Adam Chester 2020 patch operates on a user-mode trampoline in the calling process. &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; is a stub that calls down through &lt;code&gt;NtTraceEvent&lt;/code&gt; into the kernel; rewriting its first byte to &lt;code&gt;0xC3&lt;/code&gt; short-circuits the user-mode entry path and the calling process emits no events through that stub. But EtwTi does not fire from the user-mode entry path. EtwTi fires from inside the kernel implementation of &lt;code&gt;NtAllocateVirtualMemory&lt;/code&gt; and friends, after the syscall has crossed the boundary, on a path the user-mode patcher cannot reach without first achieving kernel execution.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; EtwTi is the only ETW provider in the catalogue whose producer fires from the kernel side of the syscall path -- and that is exactly why a user-mode patch in the calling process cannot silence it. The PPL+ELAM gate that controls &lt;em&gt;consumer&lt;/em&gt; admission is paired with a &lt;em&gt;producer&lt;/em&gt; location that no in-process attacker can reach.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The 2017 PPL+ELAM gate was a deliberate structural defense against the patch class that was only fully publicised three years later. By the time Chester wrote his March 2020 post, the load-bearing security signal was already structurally out of reach of his technique.&lt;/p&gt;

The combination of PPL and ELAM is not an arbitrary defense-in-depth stack. PPL gates *consumer identity* at signer level: only a binary signed with the Antimalware EKU and enrolled in an ELAM allow-list can subscribe. ELAM gates *load order*: the gate is set during early boot, before any code an attacker could load gets a chance to interfere. The signer-level check is hard because forging the signature requires breaking Microsoft&apos;s PKI; the load-order check is hard because subverting it requires compromising the boot path, which Secure Boot and the Vulnerable Driver Blocklist exist to defend.
&lt;p&gt;That is the gate. Now we walk the consumers that pass through it.&lt;/p&gt;
&lt;h2&gt;10. Six vendors, three spectra: a map of the EDR consumer architecture&lt;/h2&gt;
&lt;p&gt;Defender, CrowdStrike, SentinelOne, Sysmon, Wazuh, Elastic Defend. They look interchangeable on a vendor comparison sheet. They are not, and the differences are entirely about which substrates each one consumes.&lt;/p&gt;
&lt;p&gt;There are three axes that distinguish them.&lt;/p&gt;
&lt;h3&gt;Axis 1: kernel callbacks vs ETW&lt;/h3&gt;
&lt;p&gt;Some EDRs consume process-creation events through ETW (subscribing to &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; from a SYSTEM-privileged user-mode service). Others register kernel callbacks directly through &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; [@ms-pssetprocnotify] [@ms-pssetprocnotify] and &lt;code&gt;PsSetCreateThreadNotifyRoutine&lt;/code&gt; [@ms-pssetthreadnotify] [@ms-pssetthreadnotify] from a kernel driver they ship.&lt;/p&gt;
&lt;p&gt;The trade-off is sharp. Kernel callbacks are synchronous: the kernel calls into the driver before the operation completes, the driver runs at PASSIVE_LEVEL in the originating thread context with normal kernel APCs disabled, and the driver can deny the operation by writing a non-success status to &lt;code&gt;CreationStatus&lt;/code&gt;. ETW is asynchronous: the event is emitted from the producer&apos;s hot path, drained from a per-CPU buffer by the writer thread, and delivered to the consumer&apos;s callback at some later point. ETW cannot deny anything; it can only observe.&lt;/p&gt;

The `PsSetCreate*NotifyRoutine` family of kernel APIs. A driver calls `PsSetCreateProcessNotifyRoutineEx` (process create/exit), `PsSetCreateThreadNotifyRoutine` (thread create/exit), or `PsSetLoadImageNotifyRoutine` (image load) at boot to register a callback. The kernel invokes the callback synchronously, in the originating thread context at PASSIVE_LEVEL with normal kernel APCs disabled. The `Ex` variant of the process callback receives a `CreationStatus` field the driver can write to deny the operation.
&lt;p&gt;CrowdStrike, SentinelOne, Sysmon, and Elastic Defend ship kernel drivers and use callbacks for the latency-critical hot path. Defender uses both -- callbacks from &lt;code&gt;WdFilter.sys&lt;/code&gt; and ETW consumption from &lt;code&gt;MsMpEng.exe&lt;/code&gt; -- because as the in-box engine it has the institutional position to do so. Wazuh ships no kernel driver; it consumes ETW exclusively via SilkETW-class wrappers, which makes it less invasive but unable to deny.&lt;/p&gt;
&lt;h3&gt;Axis 2: PPL adoption&lt;/h3&gt;
&lt;p&gt;Defender (&lt;code&gt;MsMpEng.exe&lt;/code&gt; and &lt;code&gt;MsMpEngCP.exe&lt;/code&gt;) runs at Antimalware-PPL by default. CrowdStrike&apos;s &lt;code&gt;CSFalconService.exe&lt;/code&gt; runs at Antimalware-PPL, demonstrably [@trailofbits-shafir] [@trailofbits-shafir]. SentinelOne&apos;s &lt;code&gt;SentinelAgent.exe&lt;/code&gt; is widely reported to run at Antimalware-PPL via vendor documentation, although it does not appear in the Trail of Bits sample debugger output. Sysmon runs as a &lt;em&gt;protected process&lt;/em&gt; but not at the Antimalware signer level [@ms-sysmon] [@ms-sysmon] -- the Microsoft Learn page states &quot;The service runs as a protected process, thus disallowing a wide range of user mode interactions&quot; without naming Antimalware specifically.&lt;/p&gt;
&lt;p&gt;Wazuh and Elastic Defend&apos;s user-mode services run as standard SYSTEM-privileged services without PPL.&lt;/p&gt;
&lt;h3&gt;Axis 3: EtwTi consumption&lt;/h3&gt;
&lt;p&gt;This axis is determined by axis 2. Defender consumes EtwTi by design -- it is the in-box reason EtwTi exists. CrowdStrike and SentinelOne consume EtwTi (the Trail of Bits debugger output is the practical demonstration). Sysmon does not consume EtwTi: it is not Antimalware-PPL, so its &lt;code&gt;EnableTraceEx2&lt;/code&gt; calls against the EtwTi GUID would receive &lt;code&gt;ERROR_ACCESS_DENIED&lt;/code&gt;. Sysmon relies on its own &lt;code&gt;SysmonDrv.sys&lt;/code&gt; callbacks for the in-memory threat surface that EtwTi covers for the others. Wazuh and Elastic Defend do not consume EtwTi for the same reason; Elastic Defend ships its own kernel driver to compensate [@elastic-doubling-down] [@elastic-doubling-down], using Microsoft-blessed kernel-callback paths for memory events.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Process surface&lt;/th&gt;
&lt;th&gt;PPL level&lt;/th&gt;
&lt;th&gt;EtwTi?&lt;/th&gt;
&lt;th&gt;Primary source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Microsoft Defender&lt;/td&gt;
&lt;td&gt;Driver callbacks (&lt;code&gt;WdFilter.sys&lt;/code&gt;) + ETW (&lt;code&gt;MsMpEng.exe&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Antimalware-PPL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;[@ms-protect-am]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrowdStrike Falcon&lt;/td&gt;
&lt;td&gt;Driver callbacks + ETW&lt;/td&gt;
&lt;td&gt;Antimalware-PPL&lt;/td&gt;
&lt;td&gt;Yes ([@trailofbits-shafir] live evidence)&lt;/td&gt;
&lt;td&gt;[@trailofbits-shafir]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SentinelOne&lt;/td&gt;
&lt;td&gt;Driver callbacks + ETW&lt;/td&gt;
&lt;td&gt;Antimalware-PPL&lt;/td&gt;
&lt;td&gt;Widely reported&lt;/td&gt;
&lt;td&gt;-- (vendor docs; SentinelAgent.exe not in [@trailofbits-shafir] sample)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sysmon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysmonDrv.sys&lt;/code&gt; callbacks; publishes via own ETW provider&lt;/td&gt;
&lt;td&gt;Protected (not Antimalware)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;[@ms-sysmon]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wazuh&lt;/td&gt;
&lt;td&gt;ETW only (SilkETW-class)&lt;/td&gt;
&lt;td&gt;Standard SYSTEM&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elastic Defend&lt;/td&gt;
&lt;td&gt;Own kernel driver + ETW&lt;/td&gt;
&lt;td&gt;Standard SYSTEM&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;[@elastic-doubling-down]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sysmon is worth singling out as the canonical &lt;em&gt;callback-then-publish&lt;/em&gt; reference architecture. Its kernel driver registers &lt;code&gt;PsSetCreate*NotifyRoutine&lt;/code&gt; callbacks; its user-mode service consumes the events the driver delivers; and the service then publishes them via its own &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt; ETW provider for any downstream consumer (a SIEM forwarder, a SOC dashboard, a custom analytic) to read. The result is that Sysmon&apos;s events are universally consumable -- which is why Wazuh and Splunk both ship Sysmon configurations as their default kernel-event source.&lt;/p&gt;

Sysmon&apos;s design choice is the reference architecture for the callback-then-publish pattern, even though Sysmon is not itself an Antimalware-PPL EDR. By publishing through its own ETW provider rather than writing to a private channel, Sysmon makes its events consumable by any downstream pipeline. Wazuh and the Splunk Universal Forwarder can both ingest Sysmon events without any custom integration work. This is why Sysmon, despite being free, is the de facto kernel-event source for the open-source SIEM world.

flowchart LR
    K[Kernel callbacks&lt;br /&gt;synchronous, can deny] --- L1[Sysmon driver]
    K --- L2[CrowdStrike driver]
    K --- L3[SentinelOne driver]
    K --- L4[Elastic driver]
    K --- L5[Defender WdFilter.sys]
    M[ETW providers&lt;br /&gt;asynchronous, observe-only&lt;br /&gt;up to 8 consumers per provider] --- M1[Defender MsMpEng]
    M --- M2[CrowdStrike service]
    M --- M3[SentinelOne service]
    M --- M4[Sysmon service]
    M --- M5[Wazuh ETW reader]
    M --- M6[Elastic Defend service]
    K -.latency-vs-coupling axis.-&amp;gt; M
&lt;p&gt;The CrowdStrike July 2024 channel-file outage was a kernel-driver brittleness story, not an ETW story. The Falcon kernel driver&apos;s content-update parser dereferenced an out-of-bounds pointer when processing a channel file whose Rapid Response Content template had 21 input fields while the sensor&apos;s Content Interpreter expected only 20, triggering an out-of-bounds array read, BSOD-ing roughly 8.5 million Windows hosts [@ms-crowdstrike-2024][@crowdstrike-rca-2024]. That story belongs to the App Identity in Windows article [@paragmali-com-app-ide] in this series; it is mentioned here only to mark that the cost of the synchronous-kernel-driver path is a higher blast radius when the driver itself is buggy.&lt;/p&gt;
&lt;p&gt;A note on Defender&apos;s cloud schema. The events that surface in Microsoft Defender for Endpoint&apos;s hunting tables -- &lt;code&gt;DeviceProcessEvents&lt;/code&gt;, &lt;code&gt;DeviceFileEvents&lt;/code&gt;, &lt;code&gt;DeviceNetworkEvents&lt;/code&gt;, &lt;code&gt;DeviceImageLoadEvents&lt;/code&gt;, &lt;code&gt;DeviceRegistryEvents&lt;/code&gt; -- are the cloud-side abstraction over the kernel and ETW telemetry the Defender sensor collects locally. The full schema mapping from ETW provider to cloud column is out of scope here, but the substrate is the same.&lt;/p&gt;
&lt;p&gt;Six vendors, three axes, one substrate. Now we walk the attack tradition that the substrate has to survive.&lt;/p&gt;
&lt;h2&gt;11. The attack tradition: five generations of trying to blind ETW&lt;/h2&gt;
&lt;p&gt;Every generation of ETW has been attacked. Some attacks broke a single provider; some broke every user-mode provider on a host; one would, if it worked at scale, break Defender. The defense story is on the same five-generation timeline.&lt;/p&gt;
&lt;h3&gt;Gen 1 (2014-2018): autologger registry tampering&lt;/h3&gt;
&lt;p&gt;The dispositive taxonomy is Matt Graeber and Lee Christensen&apos;s December 24, 2018 Palantir CIRT post [@palantir-tampering-wayback] [@palantir-tampering-wayback], preserved in the Wayback Machine because the direct Medium URL has since returned HTTP 403 to non-browser fetchers. The opening framing is verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Event Tracing for Windows (ETW) is the mechanism Windows uses to trace and log system events. Attackers often clear event logs to cover their tracks. Though the act of clearing an event log itself generates an event, attackers who know ETW well may take advantage of tampering opportunities to cease the flow of logging temporarily or even permanently, without generating any event log entries in the process.&quot; -- [@palantir-tampering-wayback]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Graeber and Christensen split the technique into two classes. &lt;em&gt;Persistent tampering&lt;/em&gt; writes to the autologger registry path described in section 6, disabling a session before it ever starts at next boot; the events of interest are never captured because the session is never running. &lt;em&gt;Ephemeral tampering&lt;/em&gt; targets a live session: stopping the session via &lt;code&gt;ControlTrace&lt;/code&gt;, removing a provider from a session via &lt;code&gt;EnableTraceEx2(EVENT_CONTROL_CODE_DISABLE_PROVIDER, ...)&lt;/code&gt;, or directly clearing the session&apos;s buffers.&lt;/p&gt;
&lt;p&gt;The defense is direct: monitor the autologger registry surface. Sysmon Event ID 13 [@ms-sysmon] surfaces registry value-set events in &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt;; a SOC playbook that alerts on any unexpected write to that subtree catches the persistent class of attack reliably. Matt Graeber&apos;s authorship is cross-confirmed by the palantir/exploitguard repository [@gh-palantir-exploitguard] [@gh-palantir-exploitguard], which credits him as the lead researcher on the ETW work.&lt;/p&gt;
&lt;h3&gt;Gen 2 (2020): user-mode &lt;code&gt;EtwEventWrite&lt;/code&gt; 0xC3 RET patch&lt;/h3&gt;
&lt;p&gt;The technique that made ETW patching a household tradecraft term is Adam Chester&apos;s &quot;Hiding your .NET - ETW&quot;, March 17, 2020 [@xpn-hiding-dotnet] [@xpn-hiding-dotnet]. The mechanic is one byte:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Locate &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; (or in modern variants &lt;code&gt;ntdll!NtTraceEvent&lt;/code&gt;) in the calling process&apos;s memory.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;VirtualProtect&lt;/code&gt; to make the page writable.&lt;/li&gt;
&lt;li&gt;Write the byte &lt;code&gt;0xC3&lt;/code&gt; over the function&apos;s first byte.&lt;/li&gt;
&lt;li&gt;Restore the page protection.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;0xC3&lt;/code&gt; is the near-return opcode [@felixcloutier-ret] [@felixcloutier-ret]: &quot;C3 RET ZO Valid Valid Near return to calling procedure.&quot; Any caller into the function falls straight back to its return address before producing a single event. The calling process now silently fails to emit any user-mode ETW events for any provider that funnels through the patched stub -- including &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The technique has been re-implemented in every language that can call &lt;code&gt;VirtualProtect&lt;/code&gt;. The fluxsec.red Rust port [@fluxsec-etw-patching] [@fluxsec-etw-patching] explains the modern variant verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;When a ETW Provider sends a notification, it will eventually reach into ntdll.dll for the function NtTraceEvent... we can simply patch the function address to return straight from byte 0. The opcode for a ret is C3, so we can swap out the opcode 4C with C3 to immediately return out of the stub.&quot; -- [@fluxsec-etw-patching]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the structure of the patch in TypeScript pseudocode -- not actually runnable Win32, but mirroring exactly what a Windows binary would do:&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode: silence user-mode ETW for the calling process.
// This silences only the calling process and only user-mode providers
// that funnel through the patched stub.&lt;/p&gt;
&lt;p&gt;// 1. Resolve the address of ntdll!EtwEventWrite in this process.
const ntdll = getModuleHandle(&quot;ntdll.dll&quot;);
const fn = getProcAddress(ntdll, &quot;EtwEventWrite&quot;);&lt;/p&gt;
&lt;p&gt;// 2. Make the function&apos;s first page writable.
const PAGE_EXECUTE_READWRITE = 0x40;
let oldProtect = 0;
virtualProtect(fn, 1, PAGE_EXECUTE_READWRITE, /* out */ ref(oldProtect));&lt;/p&gt;
&lt;p&gt;// 3. Write 0xC3 (RET) over the first byte. Caller now returns immediately.
writeByte(fn, 0xC3);&lt;/p&gt;
&lt;p&gt;// 4. Restore original page protection.
virtualProtect(fn, 1, oldProtect, /* out */ ref(oldProtect));&lt;/p&gt;
&lt;p&gt;// Limits:
// - Silences only this process.
// - Silences only providers whose emit path funnels through this stub.
// - Cannot silence kernel-emitted providers like Microsoft-Windows-Threat-Intelligence.
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The patch operates on the calling process&apos;s user-mode trampoline. Other processes on the host are unaffected; their ETW emissions continue normally. Kernel-emitted providers like &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; are unaffected even in the patched process; they fire from the kernel side of the syscall path, after control has crossed the user/kernel boundary, on a code path the user-mode patcher cannot reach without first achieving kernel execution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Gen 3 (2021-2023): kernel-mode primitives&lt;/h3&gt;
&lt;p&gt;If a user-mode patch cannot reach EtwTi, can a kernel-mode patch? Yes -- but the attacker first needs kernel execution. The most common path is BYOVD [@paragmali-com-in-windows]: load a signed but vulnerable driver and use its primitive to read or write kernel memory. Once you can write kernel memory you can target ETW&apos;s internal data structures directly.&lt;/p&gt;
&lt;p&gt;Binarly&apos;s Black Hat Europe 2021 talk [@binarly-edr] [@binarly-edr] documents the surface verbatim:&lt;/p&gt;

Many ways to disable ETW logging are publicly available from passing a TRUE boolean parameter into a `nt!EtwpStopTrace` function to finding an ETW specific structure and dynamically modifying it or patching `ntdll!ETWEventWrite` or `advapi32!EventWrite` to return immediately thus stopping the user-mode loggers. -- [@binarly-edr]
&lt;p&gt;The kernel-side primitives Binarly enumerates target the &lt;code&gt;_ETW_GUID_ENTRY&lt;/code&gt; structure for a provider, the &lt;code&gt;EtwpRegistration&lt;/code&gt; linked list of registered providers, and the &lt;code&gt;EtwpEventTracingProhibited&lt;/code&gt; flag the kernel checks before emitting events. Yarden Shafir&apos;s 2023 Trail of Bits walkthrough [@trailofbits-shafir] [@trailofbits-shafir] provides the contemporary kernel-side data structure walk through &lt;code&gt;_ETW_REALTIME_CONSUMER&lt;/code&gt; and &lt;code&gt;_ETW_SILODRIVERSTATE&lt;/code&gt;, and notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Most recently, the Lazarus Group bypassed EDR detection by disabling ETW providers&quot; -- [@trailofbits-shafir]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The architectural-level treatment is well-documented; the specific kernel offsets that change between Windows builds are a moving target. We treat the technique class as well-established and the per-build offset details as out of scope.&lt;/p&gt;
&lt;h3&gt;Defense Gen 1 (2017): Antimalware-PPL + ELAM gate on EtwTi&lt;/h3&gt;
&lt;p&gt;Section 9 covered this in detail. The point to record here, in the attack-tradition timeline, is that the Antimalware-PPL gate predates the Adam Chester 2020 user-mode patch by three years. Microsoft did not respond to Chester&apos;s post; they had already put the load-bearing security signal structurally out of reach of any user-mode patch in the calling process. The user-mode patch class is generic against &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt; and the rest of the user-mode catalogue; it is structurally impotent against &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Defense Gen 2 (2022): Vulnerable Driver Blocklist on by default&lt;/h3&gt;
&lt;p&gt;The kernel-mode primitive class needs a kernel write. Without a vulnerability in the EDR&apos;s kernel driver, the realistic path is BYOVD: load a third-party signed driver that exposes a memory-write primitive. The structural defense is Microsoft&apos;s Vulnerable Driver Blocklist [@ms-vdb] [@ms-vdb]:&lt;/p&gt;

Since the Windows 11 2022 update, the vulnerable driver blocklist is enabled by default for all devices, and can be turned on or off via the Windows Security app... the vulnerable driver blocklist is also enforced when either memory integrity, also known as hypervisor-protected code integrity (HVCI), Smart App Control, or S mode is active... The blocklist is updated quarterly. In addition, blocklist updates are delivered through the monthly Windows updates as part of the standard servicing process. -- [@ms-vdb]
&lt;p&gt;The blocklist enumerates known-vulnerable signed drivers by hash; the kernel refuses to load anything on the list. On a Windows 11 22H2-or-later host with the default settings, the BYOVD primitive against most known-vulnerable drivers is closed. With HVCI on, the closure is enforced even against attackers who would otherwise try to load drivers via legacy paths. The empirical bound is the LOLDrivers project&apos;s catalogue of known-vulnerable drivers; the blocklist tracks public discovery with a lag of approximately one quarter, which is the residual window an attacker can exploit before a freshly disclosed driver is added.&lt;/p&gt;

The attack pattern of loading a known-vulnerable but signed driver to obtain a kernel-mode primitive (memory read, memory write, or arbitrary code execution). Used in real-world EDR-blinding attacks, including by the Lazarus Group as cited in Trail of Bits&apos; 2023 ETW walk [@trailofbits-shafir].

The Microsoft-maintained blocklist of known-vulnerable signed drivers, by hash. Enabled by default on Windows 11 22H2 and later. Enforced more strictly when HVCI, Smart App Control, or S mode is active. Updated quarterly per the Microsoft Learn primary [@ms-vdb].
&lt;p&gt;The LOLDrivers project [@loldrivers] [@loldrivers] is the empirical anchor for the BYOVD lag story. It catalogues known-vulnerable signed drivers as a community resource; the Microsoft blocklist updates quarterly, but blocklist updates are also delivered through monthly Windows servicing, so a freshly-disclosed driver can live in an exploitation window of as short as ~1 month (via Patch Tuesday) or up to a full quarter before its hash is added.&lt;/p&gt;

flowchart LR
    subgraph Attacks
        A1[&quot;Gen 1 2014-2018: Autologger registry tampering -- Palantir CIRT taxonomy&quot;]
        A2[&quot;Gen 2 2020: EtwEventWrite 0xC3 RET -- Adam Chester&quot;]
        A3[&quot;Gen 3 2021-2023: Kernel _ETW_GUID_ENTRY -- EtwpRegistration EtwpStopTrace via BYOVD&quot;]
    end
    subgraph Defenses
        D1[&quot;Sysmon Event ID 13 -- monitor Autologger subtree&quot;]
        D2[&quot;Antimalware-PPL plus ELAM -- gate on EtwTi 2017&quot;]
        D3[&quot;Vulnerable Driver Blocklist -- default-on Win11 22H2 plus HVCI&quot;]
    end
    A1 --&amp;gt; D1
    A2 --&amp;gt; D2
    A3 --&amp;gt; D3
&lt;h3&gt;The 2026 picture&lt;/h3&gt;
&lt;p&gt;User-mode patching cannot reach the kernel-mode provider that EDR cares about. The BYOVD primitive that could reach it is structurally narrowed by default on supported hardware. The remaining gap is the long tail of newly-disclosed vulnerable drivers between disclosure and blocklist update, plus any custom kernel zero-day an attacker discovers in an EDR&apos;s own driver. Both are real, both are exploited in the wild, neither is the universally-applicable evasion the 2020-era user-mode patch class was.&lt;/p&gt;
&lt;p&gt;That is the operational story. But ETW has structural limits even when no attacker is patching anything.&lt;/p&gt;
&lt;h2&gt;12. Theoretical limits: what ETW cannot see, even with every defence engaged&lt;/h2&gt;
&lt;p&gt;Even on a perfectly-configured Windows 11 box -- HVCI [@paragmali-com-in-windows] on, Vulnerable Driver Blocklist on, Antimalware-PPL Defender consuming EtwTi, third-party EDR ELAM-onboarded -- there are events ETW does not emit. Some are observed too late. Some are not observed at all.&lt;/p&gt;
&lt;p&gt;There are three structural ceilings.&lt;/p&gt;
&lt;h3&gt;Pre-ETW kernel paths&lt;/h3&gt;
&lt;p&gt;The Global Logger session is one of the earliest things to come up at boot, but it is not the first. Some early-init driver paths run before any ETW session exists; they cannot be traced via ETW. Measured Boot is the discipline that records this prefix into TPM PCRs, with attestation handled by the platform integrity layer rather than by ETW. The implication for EDR is that any malicious code executing during early boot, before the Global Logger session is up, is invisible to ETW.&lt;/p&gt;
&lt;h3&gt;Incomplete EtwTi syscall coverage&lt;/h3&gt;
&lt;p&gt;The 10 &lt;code&gt;KERNEL_THREATINT_TASK_*&lt;/code&gt; task IDs are the public surface. The underlying syscall set the kernel actually instruments is not exhaustively documented. The fluxsec.red inventory [@fluxsec-eti] [@fluxsec-eti] is the public surface, not the private one. Some syscalls are clearly covered (&lt;code&gt;NtAllocateVirtualMemory&lt;/code&gt; for cross-process allocation surfaces as &lt;code&gt;KERNEL_THREATINT_TASK_ALLOCVM&lt;/code&gt;); some have partial coverage (&lt;code&gt;MAPVIEW_LOCAL&lt;/code&gt; and &lt;code&gt;MAPVIEW_REMOTE&lt;/code&gt; keywords cover some but not all of the section-mapping primitive set across &lt;code&gt;NtCreateSection&lt;/code&gt;, &lt;code&gt;NtMapViewOfSection&lt;/code&gt;, &lt;code&gt;NtMapViewOfSectionEx&lt;/code&gt;, image-section vs file-section variants); some are not enumerated at all in the public manifest. Process-hollowing primitives that combine &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt; and &lt;code&gt;NtMapViewOfSection&lt;/code&gt; may be partially covered depending on which path the attacker takes.&lt;/p&gt;
&lt;h3&gt;The async-flush gap&lt;/h3&gt;
&lt;p&gt;ETW&apos;s per-CPU ring buffer is asynchronous. If a process allocates RWX memory, writes shellcode, executes it, and returns within one writer-thread flush interval, the event is &lt;em&gt;recorded&lt;/em&gt; but the attacker&apos;s payload has &lt;em&gt;already executed&lt;/em&gt;. The synchronous denial primitive on Windows belongs to kernel notify routines, not to ETW. The Microsoft Learn primary on About Event Tracing [@ms-about-etw] [@ms-about-etw] is explicit that events can be lost:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Events can be lost if any of the following conditions occur ... The total event size is greater than 64K ... The disk is too slow to keep up with the rate at which events are being generated. ... For real-time logging, the real-time consumer is not consuming events fast enough.&quot; -- [@ms-about-etw]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No ETW-only EDR can prevent a syscall whose payload completes inside one writer flush. EDRs that ship a kernel driver and register synchronous callbacks (CrowdStrike, SentinelOne, Sysmon, Elastic Defend) can deny operations through the &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; [@ms-pssetprocnotify] [@ms-pssetprocnotify] &lt;code&gt;CreationStatus&lt;/code&gt; field; ETW-only EDRs cannot. ETW is observation, not enforcement.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; ETW is observation, not enforcement. The synchronous denial primitive on Windows belongs to kernel notify routines, not to ETW. Sub-microsecond payloads execute before the writer thread flushes; the layered defense stack of 2026 is an empirical bar, not a theoretical guarantee.&lt;/p&gt;
&lt;/blockquote&gt;

The VBS-backed code-integrity enforcement for kernel-mode code on Windows. With HVCI enabled, the hypervisor enforces that only signed kernel pages can execute. Closes the attack class that loads unsigned drivers; combined with the Vulnerable Driver Blocklist it closes most of the realistic BYOVD primitive surface as well.
&lt;p&gt;The &quot;events can be lost&quot; enumeration in [@ms-about-etw] is the dispositive Microsoft acknowledgement of ETW&apos;s lossy substrate. SOC playbooks should treat ETW telemetry as best-effort, not as a guaranteed audit trail. Forensic claims that depend on completeness need an independent corroborating source.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A detection-only EDR can alert on a malicious operation, but only after the operation has happened. By the time the SOC sees the alert, the syscall has completed, the shellcode has executed, the credentials have been stolen. This is why the kernel-callback path (with its ability to deny via &lt;code&gt;CreationStatus&lt;/code&gt;) coexists with ETW even though ETW is more flexible: a SOC playbook needs both the speed of denial and the breadth of observation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The 2026 layered stack -- Antimalware-PPL + EtwTi + HVCI + VBL -- raises the empirical bar enormously. It does not close the architectural gap. Sub-microsecond payloads still execute before the writer thread flushes. The BYOVD primitive on a non-HVCI box still defeats the kernel-callback layer. There are still problems the substrate cannot solve in principle.&lt;/p&gt;
&lt;p&gt;Those are the limits we can describe. The next section is about the limits we cannot yet measure.&lt;/p&gt;
&lt;h2&gt;13. Open problems: keyword drift, secure kernel ETW, and the BYOVD arms race&lt;/h2&gt;
&lt;p&gt;The 2026 state of the art has five active open problems. Each has a partial workaround; none has a complete solution.&lt;/p&gt;
&lt;h3&gt;1. EtwTi keyword inventory drift across builds&lt;/h3&gt;
&lt;p&gt;Microsoft has not published a complete, current &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; keyword inventory. The community-maintained references -- the jdu2600 cross-build inventory [@gh-jdu2600] [@gh-jdu2600] and the repnz manifest archive [@gh-repnz] [@gh-repnz] -- are partial coverage and lag Microsoft&apos;s quarterly servicing cadence. EDR vendors that hard-code keyword bitmasks against an old build can silently miss events on newer builds because the keyword definitions have shifted underneath them. Detection engineers writing rules against &lt;code&gt;KERNEL_THREATINT_TASK_*&lt;/code&gt; IDs that move between builds can get false negatives.&lt;/p&gt;

There are three plausible reasons, and Microsoft has not stated which (or which combination) is operative. *Operational secrecy*: a complete keyword inventory tells attackers exactly which syscall paths are observed and which are not, narrowing the search for evasion paths. *Documentation cost*: the inventory shifts every build, and maintaining a synchronised public reference is engineering work without an obvious internal champion. *Deliberate moving target*: keeping the public surface incomplete forces attackers to reverse-engineer per build, raising the cost of stable evasion. The community references partially defeat all three rationales; the absence remains.
&lt;h3&gt;2. Secure ETW (the &lt;code&gt;EtwSi*&lt;/code&gt; family)&lt;/h3&gt;
&lt;p&gt;Windows VBS Trustlets run in the Secure Kernel (VTL1), insulated from the normal-world kernel (VTL0) by the hypervisor. The Secure Kernel exposes its own ETW family for VTL1 components; this is enumerated in fragments in Alex Ionescu&apos;s BlackHat 2015 deck on the Secure Kernel and in subsequent BlueHatIL talks. There is no public consumer-facing primary on &lt;code&gt;EtwSi*&lt;/code&gt; in 2026. Cross-link: this article&apos;s companion piece on VBS Trustlets [@paragmali-vbs-trustlets] [@paragmali-vbs-trustlets] covers the producer side of the story.&lt;/p&gt;
&lt;h3&gt;3. Forensic soundness of ETW telemetry&lt;/h3&gt;
&lt;p&gt;ETW is lossy by design (per the [@ms-about-etw] enumeration). Whether ETW-derived telemetry is &lt;em&gt;forensically sound&lt;/em&gt; -- chain-of-custody complete, lossless under load, attestable as untampered between event emission and SIEM ingestion -- is an open question. Courts have not ruled. The current best partial result is to treat ETW as supporting evidence and require independent corroboration (file-system snapshots, network captures, OS state captures) for any claim that depends on completeness. Sysmon&apos;s Event ID 16 (Sysmon configuration changed) [@ms-sysmon] and the autologger registry write events on &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt; are useful integrity signals: an attacker who silenced ETW typically leaves a footprint here.&lt;/p&gt;
&lt;h3&gt;4. The BYOVD arms race&lt;/h3&gt;
&lt;p&gt;The Vulnerable Driver Blocklist [@ms-vdb] [@ms-vdb] is hash-based and updated quarterly. The LOLDrivers project [@loldrivers] [@loldrivers] documents the public catalogue of known-vulnerable signed drivers. The gap between disclosure and blocklist update--as short as ~1 month via Patch Tuesday or up to a full quarter--is the residual exploitation window. The deeper structural issue is that the blocklist is hash-based; an attacker who finds a new vulnerability in a previously-trusted signed driver enjoys a fresh window every quarter. Closing this gap requires either a different trust model (allow-listing of known-good drivers, as Smart App Control does for executables) or behavioural detection of suspicious driver loads. Both are active areas of work.&lt;/p&gt;
&lt;h3&gt;5. Cross-process section-mapping coverage&lt;/h3&gt;
&lt;p&gt;EtwTi&apos;s &lt;code&gt;KERNEL_THREATINT_TASK_MAPVIEW&lt;/code&gt; covers some but not all section-mapping primitives. The public fluxsec.red [@fluxsec-eti] inventory lists &lt;code&gt;MAPVIEW_LOCAL&lt;/code&gt; and &lt;code&gt;MAPVIEW_REMOTE&lt;/code&gt; keywords, but the underlying syscall set (&lt;code&gt;NtMapViewOfSection&lt;/code&gt;, &lt;code&gt;NtMapViewOfSectionEx&lt;/code&gt;, &lt;code&gt;NtCreateSection&lt;/code&gt;, image-section vs file-section variants) is not exhaustively documented. Detection engineers who depend on full coverage of cross-process section mapping are working from an incomplete map.&lt;/p&gt;
&lt;h3&gt;What would a v2 ETW look like?&lt;/h3&gt;
&lt;p&gt;A theoretical ideal: synchronous kernel-emitted events on every security-relevant syscall, with the consumer running in VTL1 (Secure Kernel) so even a kernel-mode attacker in VTL0 cannot tamper with the consumer. The &lt;code&gt;EtwSi*&lt;/code&gt; family is the partial realisation. The full ideal is incompatible with x64 syscall performance: synchronous notification on every syscall would dominate the cost of the syscall itself. The pragmatic answer Microsoft has been building toward is &lt;em&gt;selective&lt;/em&gt; synchronous notification (the kernel notify routines for high-value control points) layered with &lt;em&gt;broad&lt;/em&gt; asynchronous observation (ETW for everything else), with the most security-critical of the broad observations promoted to PPL/ELAM-gated kernel-emitted producers (EtwTi). Two decades of layering, no single architectural endpoint.For the producer side of the Secure Kernel ETW story (&lt;code&gt;EtwSi*&lt;/code&gt;), see this article&apos;s companion piece on VBS Trustlets [@paragmali-vbs-trustlets] [@paragmali-vbs-trustlets] in the same series. The Trustlet-side architecture is a separate topic large enough to need its own walkthrough.&lt;/p&gt;
&lt;p&gt;Open problems are interesting but they are not actionable. The next section is about what an engineer can do on Monday morning.&lt;/p&gt;
&lt;h2&gt;14. Practical guide: five things to do Monday morning&lt;/h2&gt;
&lt;p&gt;You have read 12,000 words about ETW. Here are five concrete checks an engineer can run on a Windows host this morning.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;logman query providers&lt;/code&gt; enumerates every registered provider on the host. Cross-reference the output against the section 8 catalogue and flag any security-relevant provider your EDR is not consuming. Pay specific attention to &lt;code&gt;Microsoft-Antimalware-Scan-Interface&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-PowerShell&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;, and &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt; if Sysmon is installed. Missing coverage of any of these on a host you are responsible for is a detection-coverage gap, not a configuration issue.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Run &lt;code&gt;wevtutil gp Microsoft-Windows-Threat-Intelligence&lt;/code&gt; to confirm the provider is registered and inspect its keyword definitions. Then check whether your EDR is actually a consumer: walk the live-debugger handle enumeration in Yarden Shafir&apos;s Trail of Bits post [@trailofbits-shafir] [@trailofbits-shafir] (the WinDbg JS scripts are linked from the post). If your EDR is supposed to be ELAM-onboarded but does not appear in the consumer enumeration for an EtwTi logger session, your installation may have lost the gate. This is the difference between a configured EDR and a functional EDR.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Enumerate &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt; for unauthorised session entries. Per the Palantir CIRT taxonomy [@palantir-tampering-wayback] [@palantir-tampering-wayback], this is the persistent-tampering surface. A baseline audit should produce a known list of expected sessions (Defender, your EDR, Sysmon if installed, the standard Windows diagnostic listeners). Any subkey not on the baseline list is an investigation candidate. Sysmon Event ID 13 (registry value set) [@ms-sysmon] on this subtree is a high-signal alert in any SIEM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Run &lt;code&gt;Get-CimInstance Win32_DeviceGuard | Select-Object SecurityServicesConfigured, SecurityServicesRunning, VirtualizationBasedSecurityStatus&lt;/code&gt; to expose whether HVCI and the Vulnerable Driver Blocklist are active. Per the Microsoft Learn primary [@ms-vdb] [@ms-vdb], the BYOVD ceiling is your kernel-tampering integrity guarantee. If VBS is &lt;code&gt;Off&lt;/code&gt; on a managed endpoint, your detection coverage is structurally weaker than it should be on supported hardware. Treat it as a hardening item, not a nice-to-have.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Write a hunting query for the pattern: &quot;process X registers as ETW consumer for &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; and X is not on the EDR allow-list.&quot; The provider&apos;s PPL+ELAM gate makes this a high-signal alert: only a signed Antimalware-PPL service can pass the gate, so an unexpected process holding an &lt;code&gt;EtwConsumer&lt;/code&gt; handle to the TI logger ID is either a misconfigured tool, a legitimate research session you forgot about, or an attacker chain that has acquired Antimalware-PPL trust on your fleet. The first two are quick to triage; the third is an incident.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The structure of the check in pseudocode -- mirroring the WinDbg JS approach in [@trailofbits-shafir]:&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode: inventory providers and identify EtwTi consumers.&lt;/p&gt;
&lt;p&gt;// 1. Enumerate registered providers and find Microsoft-Windows-Threat-Intelligence.
const providers = enumerateRegisteredProviders();
const tiProvider = providers.find(p =&amp;gt; p.guid === &quot;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&quot;);
if (!tiProvider) {
  warn(&quot;EtwTi provider not registered on this host&quot;);
}&lt;/p&gt;
&lt;p&gt;// 2. Enumerate live trace sessions and find any that subscribe to TI.
const sessions = enumerateLoggerSessions();  // logman query -ets equivalent
const tiSessions = sessions.filter(s =&amp;gt;
  s.providers.some(p =&amp;gt; p.guid === tiProvider?.guid));&lt;/p&gt;
&lt;p&gt;// 3. Walk EtwConsumer handles for each TI session; identify the consuming processes.
const expectedConsumers = [&quot;MsMpEng.exe&quot;, &quot;CSFalconService.exe&quot;, &quot;SentinelAgent.exe&quot;];
for (const session of tiSessions) {
  const consumers = enumerateEtwConsumers(session.loggerId);  // Shafir WinDbg JS
  for (const consumer of consumers) {
    if (!expectedConsumers.includes(consumer.processName)) {
      alert(`Unexpected EtwTi consumer: ${consumer.processName} (PID ${consumer.pid})`);
    }
  }
}&lt;/p&gt;
&lt;p&gt;// 4. Audit autologger persistence entries against a known baseline.
const baseline = loadAutologgerBaseline();
const live = enumerateAutologgerSubkeys();  // HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger
for (const entry of live) {
  if (!baseline.includes(entry.name)) {
    alert(`Unexpected autologger entry: ${entry.name}`);
  }
}
`}&lt;/p&gt;
&lt;p&gt;With those five checks, the catalogue is no longer an abstraction. You have an inventory of what your host emits, an inventory of who consumes the most security-critical provider, an audit of the persistence surface that defines what gets emitted at all, a confirmation of the integrity layer that closes BYOVD, and a hunt for anyone who has somehow obtained the passport. Now we close with the questions every reader should expect to have.&lt;/p&gt;
&lt;h2&gt;15. Frequently asked questions&lt;/h2&gt;

Yes, for *publication*. Sysmon&apos;s kernel driver `SysmonDrv.sys` registers `PsSetCreateProcessNotifyRoutineEx` and the related thread- and image-load callbacks; the user-mode service then publishes the resulting events via its own `Microsoft-Windows-Sysmon` ETW provider GUID `{5770385F-C22A-43E0-BF4C-06F5698FFBD9}` [@ms-sysmon]. It does not consume the public catalogue providers via ETW for its kernel-event hot path; the kernel taps come straight from the callback API. This callback-then-publish architecture is why Sysmon&apos;s events are universally consumable by SIEM forwarders and downstream tools.

Because Defender consumes `Microsoft-Windows-Threat-Intelligence`, which fires from the kernel side of memory-modifying syscalls, not from the user-mode `ntdll!EtwEventWrite` trampoline. The fluxsec.red walkthrough states the asymmetry verbatim: &quot;we cannot patch out the Threat Intelligence provider as this is emitted from within the kernel itself&quot; [@fluxsec-eti]. The Adam Chester 2020 patch silences user-mode providers (like `Microsoft-Windows-DotNETRuntime`) for the patched process; it cannot silence kernel-emitted providers for any process. Defender&apos;s load-bearing security signal is structurally out of reach of the user-mode patch class.

No. The provider&apos;s security descriptor admits only Antimalware-PPL signers loaded by an ELAM driver. A non-PPL `EnableTraceEx2` call against the EtwTi GUID returns `ERROR_ACCESS_DENIED` (the Microsoft Learn primary on EnableTraceEx2 [@ms-enabletraceex2] [@ms-enabletraceex2] documents the error code for insufficient-privilege callers; the PPL-specific gate that triggers it for EtwTi is described in [@fluxsec-eti]). The gate exists because an attacker who could trivially become an EtwTi consumer would have direct visibility into the kernel&apos;s view of every memory-modifying syscall on the host -- exactly the inventory needed to evade everything else.

Schema location. Manifest-based providers ship an out-of-band XML manifest registered with `wevtutil im`; consumers decode events against the system-installed manifest using TDH. TraceLogging providers carry the schema *inline* in each event payload as type-length-value triples; consumers decode without any registered manifest. TraceLogging events are larger because the schema bytes ride in the payload; manifest events have a smaller per-event size at the cost of installation friction. Both inherit the eight-session cap [@ms-about-etw], [@ms-tracelogging-about].

Sixty-four globally per [@ms-etw-sessions], with Windows 2000 limited to 32. Per-provider, manifest-based and TraceLogging providers admit up to 8 simultaneous sessions; classic and WPP providers admit only 1 [@ms-about-etw], [@ms-etw-config]. The runtime symptom of the per-provider 8-session cap binding is `ERROR_NO_SYSTEM_RESOURCES` from `EnableTraceEx2` [@ms-enabletraceex2]; the runtime symptom of the global 64-session cap binding is the same error from `StartTrace`.

No. EventPipe is a managed-runtime cross-platform analogue to ETW that shipped in .NET Core 3.0 (September 2019) and remains available in every later release including .NET 5+. It runs on Linux and macOS as well as Windows. On Windows, the kernel-mode providers and the EtwTi security substrate have no EventPipe equivalent; EventPipe is a complement to ETW for managed workloads, not a replacement. The Windows EDR substrate remains ETW; managed-runtime tracing has acquired an additional cross-platform path that does not displace it.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;etw-event-tracing-for-windows-and-the-edr-substrate&quot; keyTerms={[
  { term: &quot;ETW&quot;, definition: &quot;Event Tracing for Windows: kernel-buffered observability bus introduced in Windows 2000.&quot; },
  { term: &quot;Provider&quot;, definition: &quot;A component that emits ETW events tagged with a GUID.&quot; },
  { term: &quot;Controller&quot;, definition: &quot;A component that creates, configures, and stops trace sessions.&quot; },
  { term: &quot;Consumer&quot;, definition: &quot;A component that reads events from a session in real time or from an .etl file.&quot; },
  { term: &quot;Manifest-based provider&quot;, definition: &quot;Vista-era ETW provider class with XML manifest schema and 8-session cap.&quot; },
  { term: &quot;TraceLogging&quot;, definition: &quot;Self-describing ETW provider class with inline TLV schema, shipped in Windows 10.&quot; },
  { term: &quot;EtwTi&quot;, definition: &quot;Microsoft-Windows-Threat-Intelligence: the kernel-emitted memory-syscall provider; PPL+ELAM-gated.&quot; },
  { term: &quot;Antimalware-PPL&quot;, definition: &quot;Signer level on the PPL lattice for antimalware services; gates EtwTi consumption.&quot; },
  { term: &quot;ELAM&quot;, definition: &quot;Early Launch Antimalware: driver class that gates the certificate inventory for permitted Antimalware-PPL binaries.&quot; },
  { term: &quot;BYOVD&quot;, definition: &quot;Bring Your Own Vulnerable Driver: load a known-vulnerable signed driver to obtain kernel primitive.&quot; },
  { term: &quot;Vulnerable Driver Blocklist&quot;, definition: &quot;Microsoft-maintained hash blocklist; default-on in Windows 11 22H2.&quot; },
  { term: &quot;Autologger&quot;, definition: &quot;Registry-persisted boot-time ETW session under HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\.&quot; }
]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;ETW is now twenty-six years old. It started as a performance facility for Windows 2000 driver authors who could not afford &lt;code&gt;DbgPrint&lt;/code&gt; on production servers, and it became the substrate of every major Windows endpoint security product through a decade of unintended consequences. The Vista team that raised the per-provider session cap from 1 to 8 was thinking about ergonomics. The Windows 8.1 team that introduced Antimalware-PPL was thinking about Defender&apos;s hardening, not about future third-party EDRs. The team that shipped EtwTi in the Windows 10 RS-era understood the security stakes precisely. By 2026 those three decisions, taken in three different Microsoft contexts a decade apart, are the architecture of detection on the Windows endpoint -- and the reason the operator in the section 1 hook scene loses the round even when the patch works exactly as it should.&lt;/p&gt;
</content:encoded><category>etw</category><category>windows-internals</category><category>edr</category><category>security</category><category>kernel</category><category>detection-engineering</category><category>threat-intelligence</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>