# Every UAC Prompt Is an ALPC Handshake: A Field Guide to Windows' Most-Attacked Local IPC Fabric

> 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.

*Published: 2026-05-27*
*Canonical: https://paragmali.com/blog/every-uac-prompt-is-an-alpc-handshake-a-field-guide-to-windo*
*License: CC BY 4.0 - https://creativecommons.org/licenses/by/4.0/*

---
<TLDR>
Every Windows service that exposes a local API does so through **LRPC**, the RPC runtime's local-only transport, and LRPC rides on top of **ALPC**, the kernel'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.
</TLDR>

## 1. Every UAC Prompt Is an ALPC Handshake

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 [User Account Control](/blog/adminless-how-windows-finally-made-elevation-a-security-boun/) 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.

Trace the call from the user side. The Explorer shell invokes `ShellExecuteEx` with the verb set to `runas`. That call does not magically elevate the process; it sends a request *to another process*, the **Application Information service** (`appinfo`) running as `svchost.exe -k netsvcs` 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 `ncalrpc` protocol sequence -- "Local procedure call" in Microsoft's own protocol-sequence reference [@msdocs-protseq]. Underneath that string is the LRPC transport in `rpcrt4.dll`, and underneath the LRPC transport is a kernel ALPC port that lives at the [Object Manager name](/blog/the-object-manager-namespace/) `\RPC Control\appinfo`. The kernel resolves the name, the handshake completes, and a single syscall named `NtAlpcSendWaitReceivePort` [@ntdoc-ntalpc] carries the request message into the SYSTEM-context server and the reply back.

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 `ntdoc.m417z.com` [@ntdoc-ntalpc] that lists all eight parameters of the function. The kernel object behind the call is the `_ALPC_PORT`, and the per-connection structure layouts are documented only on Geoff Chappell's site [@chappell-alpc] [@chappell-alpcp] and inside the chapter named *Advanced local procedure call (ALPC)* of *Windows Internals 7e Part 2* [@wininternals-7e].

<Definition term="Advanced Local Procedure Call (ALPC)">
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].
</Definition>

<Definition term="Local RPC (LRPC)">
The Microsoft RPC runtime'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.
</Definition>

<Sidenote>The abbreviation collision is real and bites every newcomer. **LPC** is the original Windows NT 3.1 kernel primitive. **LRPC** is the RPC runtime'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.</Sidenote>

Two layers sit on top of one kernel object. The kernel layer is what `Nt*Alpc*` syscalls touch. The user-mode layer is the RPC runtime's interface dispatch -- the IDL stubs, the NDR encoders, the per-interface security callback the application registers with `RpcServerRegisterIf2` [@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.

<Mermaid caption="The four-phase ALPC handshake that runs every time UAC asks for consent. Only the connection port has an Object Manager name; the per-connection communication ports created by NtAlpcAcceptConnectPort are unnamed and exist only as handles.">
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->>ConnPort: NtAlpcConnectPort (CONNECT)
    ConnPort->>Server: ALPC connect message queued
    Server->>CommPort: NtAlpcAcceptConnectPort (ACCEPT, returns paired handles)
    Client->>CommPort: NtAlpcSendWaitReceivePort (REQUEST)
    CommPort->>Server: ALPC message with NDR-encoded args
    Server->>CommPort: NtAlpcSendWaitReceivePort (REPLY)
    CommPort->>Client: NDR-encoded reply delivered
    Client->>CommPort: NtAlpcDisconnectPort (CLOSE)
</Mermaid>

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's correctness rests on a structural fact almost every secondary writeup gets wrong, and Section 4 spells it out in full.

If this primitive is everywhere, why does nobody talk about it? Because nobody had to, for thirteen years.

## 2. Origins -- Cutler's NT and the Birth of LPC (1989-1993)

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.

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 `CreateWindow` 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 **Local Procedure Call**, or LPC, with the first release of Windows NT in July 1993. Helen Custer documented the design that same year in *Inside Windows NT* [@custer-print], the canonical first-edition print primary.

<Definition term="Local Procedure Call (LPC)">
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].
</Definition>

The classic LPC mechanism worked like this. A server process calls `NtCreatePort` to create a *connection port* under an Object Manager name (for example, `\Windows\ApiPort` for CSRSS). The server then waits on the connection port. A client process opens the connection port by name and calls `NtConnectPort` to request a session. The kernel creates two new, unnamed *communication ports* -- one the client holds, one the server holds -- and ties them to the connection through the kernel'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.

<Mermaid caption="The classic LPC three-port model. The connection port is named in the Object Manager namespace; the per-connection communication ports are unnamed and exist only as handles. Vista's ALPC inherits this topology and adds asynchronous semantics and per-message attributes.">
flowchart LR
    A[Client process] -- "NtConnectPort by name" --> B[Connection port \Windows\ApiPort -- NAMED]
    B -- "NtAcceptConnectPort" --> C[Server process]
    C -- "issues a pair of handles" --> D[Client comm port -- UNNAMED]
    C -- "issues a pair of handles" --> E[Server comm port -- UNNAMED]
    A -- "NtRequestWaitReplyPort" --> D
    D -- "kernel routes the message" --> E
    E -- "delivered to" --> C
</Mermaid>

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: `NtRequestWaitReplyPort` 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 `NtMapViewOfSection` dance to set up a shared section the server would then peek into. The split between "short message in the syscall" and "long payload in a shared section" was awkward, racy, and a perennial source of off-by-one bugs in the server stubs.

The third pinch-point was security, and it is the one Cesar Cerrudo will name in 2006. LPC's access check happened once, at `NtConnectPort`, against the connection port'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.

<Aside label="Why microkernels needed cheap IPC">
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'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's job was to make it cost microseconds, and the team'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's problem, because they controlled both ends of every conversation.
</Aside>

The 1993 design assumed the only callers of CSRSS were Win32 client processes the team controlled. That assumption held for thirteen years.

## 3. The First Reckoning -- LPC's Failure Modes and Cerrudo's WLSI 2006

In March 2006, at Black Hat Europe in Amsterdam, Cesar Cerrudo gave a talk titled *WLSI -- Windows Local Shellcode Injection*. 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'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.

Cerrudo's paper, archived at Exploit-DB under the title *WLSI Windows Local Shellcode Injection* and dated March 14, 2006 [@cerrudo-exploitdb], with the speaker deck mirrored on Black Hat'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'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.

<Mermaid caption="Cerrudo 2006: the three-clause class that every later ALPC and LRPC EoP inherits. Each clause is necessary; the conjunction of all three produces a local elevation primitive whose root cause is the kernel's once-and-done trust model, not any single bug in any single server.">
flowchart LR
    A[Port is reachable -- the connection port DACL admits the attacker] --> D[Local elevation-of-privilege primitive]
    B[Server trusts the message -- no per-message identity check or per-procedure authorization] --> D
    C[Channel survives the access check -- LPC checks the DACL once at NtConnectPort, then forgets] --> D
</Mermaid>

Clause one: *the port is reachable*. The LPC connection port has a DACL; the attacker happens to be inside it. For CSRSS's `\Windows\ApiPort`, that means "any Win32 process on the desktop", which is exactly what NT was supposed to permit. Clause two: *the DACL is permissive*. Every authenticated user is in scope of the LPC servers that brokered the user-mode Win32 API surface, by design. Clause three: *the server trusts the message*. The LPC kernel object exposes a `PORT_MESSAGE` 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's own address space.

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.

<Definition term="Discretionary Access Control List (DACL)">
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's structural class.
</Definition>

<PullQuote>
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.
</PullQuote>

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's three clauses. They are: (1) the synchronous-only design forced the RPC runtime to layer its own asynchronous wrapper around `NtRequestWaitReplyPort`, 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.

<Sidenote>One LPC failure mode that did not make Cerrudo's slide and that Microsoft has never publicly discussed in detail was the reply-port confusion class. In classic LPC, a server's reply traveled back over the client'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 *Windows Internals* editions and the occasional aside in csandker [@csandker-alpc]. The public security community did not catch the bug class at the time.</Sidenote>

In November 2006 -- eight months after WLSI -- Windows Vista shipped. The new kernel called the replacement primitive **Advanced LPC**. The redesign closed half of Cerrudo's structural class -- the *permissive port DACL* 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.

The naive read of Cerrudo's paper is "Microsoft will fix the bug." 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.

## 4. The Breakthrough -- ALPC, the Vista Redesign, and the Message-Attribute System

The Vista kernel team's answer to Cerrudo was not a patch. It was a complete replacement of the kernel object.

ALPC re-cast the LPC port as an **asynchronous, message-and-attribute-based** primitive. The classic LPC quartet -- `NtRequestPort`, `NtReplyPort`, `NtRequestWaitReplyPort`, `NtReplyWaitReplyPort` -- collapsed into a single syscall, `NtAlpcSendWaitReceivePort` [@ntdoc-ntalpc], with eight parameters whose combinations express every variant the older quartet supported. The kernel object behind the syscall is the `_ALPC_PORT`. The structure layout is documented only in the chapter named *Advanced local procedure call (ALPC)* of *Windows Internals 7e Part 2* [@wininternals-7e], in the reverse-engineered header dumps on Geoff Chappell's site [@chappell-alpc] [@chappell-alpcp], and in the community-maintained `phnt` headers that the Process Hacker project ships. None of those is a Microsoft Learn page.

<Definition term="ALPC port object (_ALPC_PORT)">
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's site [@chappell-alpc].
</Definition>

The user-mode syscall surface, enumerated as exhaustively as anyone outside Microsoft can: `NtAlpcCreatePort`, `NtAlpcConnectPort`, `NtAlpcAcceptConnectPort`, `NtAlpcSendWaitReceivePort`, `NtAlpcDisconnectPort`, `NtAlpcCancelMessage`, `NtAlpcCreatePortSection`, `NtAlpcCreateResourceReserve`, plus the `PORT_ATTRIBUTES` 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 *Windows Internals 7e Part 2* chapter is the de facto architectural reference.

<Aside label="Why ALPC is not on Microsoft Learn">
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'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.
</Aside>

The structural break with classic LPC is the **message-attribute** 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.

<Definition term="Message attribute">
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.
</Definition>

**The Context attribute** 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.

**The Handle attribute** 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 `DuplicateHandle` with the receiver's process handle, hope the receiver hadn'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.

**The Security attribute** 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 -- "trust the channel because the kernel checked the DACL at connect" -- gets replaced by "ask the kernel who is actually sending this message right now."

**The View attribute** is the shared-section dance, rewritten. In classic LPC, payloads larger than the inline budget required the sender to call `NtCreateSection`, both parties to call `NtMapViewOfSection`, 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.

<Mermaid caption="The four ALPC message attributes and the LPC awkwardness each one retires. Each attribute moves a workflow that LPC servers had to coordinate out of band into a single in-message kernel transaction.">
flowchart TD
    A[Context attribute] --> A1[Replaces: server-side client-to-state map indexed by PID]
    B[Handle attribute] --> B1[Replaces: out-of-band DuplicateHandle dance]
    C[Security attribute] --> C1[Replaces: trust the channel because DACL was checked at connect]
    D[View attribute] --> D1[Replaces: NtCreateSection plus NtMapViewOfSection dance for large payloads]
</Mermaid>

The handshake topology survives from classic LPC and tightens. The server creates a named connection port with `NtAlpcCreatePort`. The client opens the connection port by name with `NtAlpcConnectPort` and sends an initial connect message; the kernel queues the connect on the server's port. The server calls `NtAlpcAcceptConnectPort`, and the kernel returns a *pair* 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 `NtAlpcSendWaitReceivePort`. 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.

<Mermaid caption="From NtAlpcConnectPort to a paired set of unnamed communication ports. The named connection port is what shows up in Object Manager listings; the per-connection communication ports never do.">
flowchart LR
    A[Client process] -- "NtAlpcConnectPort by name" --> B[Connection port -- NAMED in \RPC Control]
    B -- "kernel queues the connect" --> C[Server process]
    C -- "NtAlpcAcceptConnectPort" --> D[Paired comm ports -- UNNAMED]
    A -- "NtAlpcSendWaitReceivePort" --> D
    D -- "kernel routing" --> C
</Mermaid>

Here is the structural correction the input premise to this article got wrong, and that almost every secondary writeup gets wrong. **Only the named connection port has an Object Manager name.** The per-connection communication ports created by `NtAlpcAcceptConnectPort` are unnamed. They have no path under `\RPC Control` or `\BaseNamedObjects` 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.

> **Key idea:** ALPC'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's structural class the Vista redesign actually closed.

> **Note:** A statement like "every ALPC port has an Object Manager name" 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'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.

<Sidenote>Microsoft'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 `Microsoft-Windows-Kernel-ALPC` ETW provider [@msdocs-etwsys], and even that provider is gated behind `EVENT_TRACE_SYSTEM_LOGGER_MODE`, which a non-SYSTEM caller cannot enable. The structural opacity of the kernel layer is partly an artefact of the deliberate "no public WDK developer-facing reference" position.</Sidenote>

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.

The Vista redesign closed the *permissive port DACL* half of the structural problem. It left the *interface callback returns RPC_S_OK when it should return RPC_S_ACCESS_DENIED* half completely intact.<MarginNote>The Vista kernel team's collective attribution stops short of naming individual ALPC architects. *Windows Internals 7e Part 2* [@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.</MarginNote> That intact half is the rest of this article.

## 5. The Universalisation -- ALPC as the Local IPC Fabric (2009-2013)

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 *replaced*; it had been *adopted*.

The transition was technically backwards-compatible. Pre-Vista binaries that called `NtCreatePort` and `NtRequestWaitReplyPort` 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.

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, `services.exe`) accepts service-control commands over an LRPC interface. The DCOM activation service (`rpcss`) marshals every local COM activation request through an LRPC pipeline. Windows Error Reporting, the audio service (`audiosrv`), Task Scheduler (`schedsvc`/`schrpc`), the Application Information service (`appinfo`) that brokers UAC, the Encrypting File System extension (`efslsaext`, the EFSRPC server documented in the [MS-EFSR] specification [@ms-efsr]), the print spooler (`spoolsv`), and the Background Intelligent Transfer Service (BITS) all expose at least one LRPC interface for client communication [@csandker-rpc].

<Mermaid caption="By Windows 8.1, every major Windows service exposes at least one LRPC interface backed by a kernel ALPC port. The kernel ALPC layer is the single rectangle every service sits on top of.">
flowchart TD
    K[Kernel ALPC layer -- _ALPC_PORT objects, NtAlpcSendWaitReceivePort dispatcher]
    K --> CSRSS[CSRSS -- Win32 subsystem]
    K --> LSASS[LSASS -- logon and token issuance]
    K --> SCM[Service Control Manager]
    K --> RPCSS[RPCSS -- DCOM activator and epmapper]
    K --> APPINFO[AppInfo -- UAC consent broker]
    K --> SPOOL[Print Spooler]
    K --> SCHRPC[Task Scheduler -- schrpc and schedsvc]
    K --> BITS[BITS -- background transfers]
    K --> AUDIO[Audio service -- audiosrv]
    K --> EFS[EFS -- efslsaext]
</Mermaid>

That fan-out is the article'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 `RpcServerRegisterIf2` [@msdocs-rpcregisterif2] or `RpcServerRegisterIf3` [@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 "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" [@tob-rpcinv-blog].

<PullQuote>
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]
</PullQuote>

To see the fabric in operation, walk one call. An unprivileged user invokes `StartServiceW` from the SCM client library inside `sechost.dll`. The library binds to the SCM's local RPC endpoint -- the `\RPC Control\ntsvcs` 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 `NdrClientCall3`. `rpcrt4.dll` crosses into the kernel through `NtAlpcSendWaitReceivePort`. The kernel routes the ALPC message to the SCM's blocked worker thread inside `services.exe`. The worker, running as SYSTEM, unpacks the NDR body with `NdrStubCall3` 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's token holds `SC_MANAGER_CONNECT` and the target service's DACL grants `SERVICE_START`. If the callback returns `RPC_S_OK`, the SCM starts the service. The reply -- an NDR-encoded error code -- rides another `NtAlpcSendWaitReceivePort` back to the client. One user call, five layers crossed, and the kernel never knew it was running an RPC.

<Sidenote>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 `NtCreatePort` 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 `Nt*Alpc*` developer-facing reference: as long as everyone is supposed to use the RPC runtime, the kernel object can keep evolving.</Sidenote>

Once the transport was universal, enumeration became valuable. If only LSASS used ALPC, listing LSASS'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.

## 6. The Eureka Year -- Public Tooling and the Interface-Callback Class (2017-2019)

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 `RPC_S_OK` for a caller that should have received `RPC_S_ACCESS_DENIED`. The kernel ALPC layer behaved correctly in every one of them. The application code did not.

<Mermaid caption="Eighteen months that turned ALPC from plumbing into attack surface. The four publications below taught the public security community how to enumerate the LRPC interface inventory and named the structural bug class that the interface-callback layer produces.">
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
</Mermaid>

The first publication is **Clement Rouault and Thomas Imbert's "A view into ALPC-RPC"**, 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's deliverable was a working NDR-aware fuzzer named **RPCForge** [@rpcforge]. RPCForge surfaced **CVE-2017-11783** [@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 "the way it handles calls to Advanced Local Procedure Call (ALPC)" -- the canonical "ALPC EoP" classification that NVD reuses for every later instance.

The second is **James Forshaw's `NtObjectManager` tooling**, distributed through the `sandbox-attacksurface-analysis-tools` repository at Google Project Zero [@forshaw-saatools]. The tooling is a PowerShell module backed by a .NET library originally called `NtApiDotNet` and renamed to `NtCoreLib` in 2024. Forshaw introduced the design intent in a December 17, 2019 Project Zero post titled *Calling Local Windows RPC Servers from .NET* [@forshaw-rpc-2019], opening with what amounts to a personal manifesto: *"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."* The post named a gap in his own methodology -- *"one of my big blind spots was anything which directly interacted with a Local RPC server"* -- and introduced `Get-RpcServer`, `Get-NtAlpcServer`, and `New-RpcClient` as the cmdlets that closed it.

<PullQuote>
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]
</PullQuote>

The conceptual workflow Forshaw'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 "every local RPC procedure callable on this Windows install." Diff the inventory across a Patch Tuesday and the changes -- new procedures, retired procedures, changed security descriptors -- become a research backlog.

<RunnableCode lang="js" title="Get-RpcServer pseudocode: enumerate local RPC interfaces (PowerShell equivalent)">{`
// PowerShell equivalent (run inside an elevated session with NtObjectManager installed):
//   Install-Module NtObjectManager
//   Get-RpcServer -DbgHelpPath 'C:\\\\Program Files\\\\Debugging Tools for Windows\\\\dbghelp.dll' |
//     Where-Object { $_.Endpoints.ProtocolSequence -eq 'ncalrpc' } |
//     Select-Object Name, InterfaceId, @{N='ProcCount';E={$_.Procedures.Count}}

// The runnable below mirrors the same logic in plain JS so the in-browser engine can execute it.
const interfaces = [
  { name: 'AppInfo',        interfaceId: '201ef99a-7fa0-444c-9399-19ba84f12a1a', protocolSequence: 'ncalrpc', procedures: 12 },
  { name: 'schrpc',         interfaceId: '86d35949-83c9-4044-b424-db363231fd0c', protocolSequence: 'ncalrpc', procedures: 27 },
  { name: 'spoolss',        interfaceId: '12345678-1234-abcd-ef00-0123456789ab', protocolSequence: 'ncacn_np', procedures: 96 },
  { name: 'lsarpc-local',   interfaceId: '12345778-1234-abcd-ef00-0123456789ab', protocolSequence: 'ncalrpc', procedures: 81 },
  { name: 'epmapper',       interfaceId: 'e1af8308-5d1f-11c9-91a4-08002b14a0fa', protocolSequence: 'ncalrpc', procedures: 5  },
];

const local = interfaces
  .filter(i => i.protocolSequence === 'ncalrpc')
  .map(i => ({ name: i.name, interfaceId: i.interfaceId, procCount: i.procedures }));

console.log('Local RPC interfaces (ncalrpc only):');
local.forEach(i => console.log(\`  \${i.name.padEnd(16)} \${i.interfaceId}  procs=\${i.procCount}\`));
console.log(\`Total: \${local.length}\`);
`}</RunnableCode>

The third publication is **SandboxEscaper's CVE-2018-8440** [@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 "being exploited in the wild" [@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.

<Mermaid caption="CVE-2018-8440: NULL callback, SYSTEM-context ACL change, SYSTEM execution. The kernel ALPC layer behaved correctly; the Task Scheduler interface registered SchRpcSetSecurity without a security callback, and the SYSTEM-context service performed the requested operation on the attacker-chosen file.">
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->>Sch: NtAlpcConnectPort plus LRPC SchRpcSetSecurity request
    Sch->>Srv: dispatch -- IfCallbackFn is NULL, no security callback runs
    Srv->>FS: SetSecurityInfo as SYSTEM, grant Everyone:F to attacker-chosen path
    Srv->>Att: RPC_S_OK
    Att->>FS: overwrite the now-writable file
    Note over Att,FS: next call into the modified binary executes attacker code as SYSTEM
</Mermaid>

The Task Scheduler service exposes an LRPC interface containing a procedure named `SchRpcSetSecurity`, registered through `RpcServerRegisterIf2` with `IfCallbackFn` set to NULL. NULL has a specific meaning, documented verbatim on Microsoft Learn: *"IfCallbackFn: Security-callback function, or NULL for no callback"* [@msdocs-rpcregisterif2]. No callback means the RPC runtime dispatches the call without asking the application whether the caller should be allowed.

Once dispatched, `SchRpcSetSecurity` 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.

The 0patch micropatch writeup named the structural pattern as "the Task Scheduler fails to impersonate the requesting client" [@0patch-micropatch] -- which is to say, the service did the operation in its own privileged identity instead of the caller's. CERT/CC framed the same bug in transport terms: a vulnerability "in the handling of ALPC" that lets an authenticated user overwrite an arbitrary file [@cert-vu906424].

> **Note:** A NULL `IfCallbackFn` 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 `RpcServerRegisterIf2(..., NULL)` in production code as a finding.

The fourth is **Tavis Ormandy's CVE-2019-1162** [@nvd-cve-2019-1162], disclosed in the August 13, 2019 Project Zero post *Down the Rabbit-Hole...* [@ormandy-ctf-2019]. The bug class Ormandy named is the structural exemplar of "shared system ALPC ports that ignore caller integrity." 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 "shared ALPC ports without caller-integrity enforcement" open.

A partially-overlapping fifth example -- the same interface-callback class expressed through DCOM activation rather than direct LRPC -- is **Forshaw's October 18, 2018 Project Zero post** *Injecting Code into Windows Protected Processes using COM* [@forshaw-com-ppl-2018]. The post documented a class of [Protected Process Light](/blog/protected-process-light-when-the-administrator-isnt-enough/) (PPL) bypass in which a DCOM activator marshalled an impersonated client token into a privileged COM server, and the server'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.

<Aside label="What Forshaw's tooling unlocked">
Before `NtObjectManager`, a researcher looking at an LRPC service had to disassemble the service'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's December 2019 post named it explicitly: he wrote the tools because the tools were the bottleneck.
</Aside>

<Definition term="Interface security callback">
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'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 "permit every call that reaches the runtime."
</Definition>

<Definition term="NDR / NDR64">
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.
</Definition>

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.

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.

## 7. The LRPC Overlay -- Interface Registration and the Asymmetry the OS Cannot Fix

Look at the signature of `RpcServerRegisterIf2`. The seventh parameter is named `IfCallbackFn`. Microsoft's own reference page documents that NULL is a legal value, and that NULL means "no callback" [@msdocs-rpcregisterif2]. That parameter is the asymmetry the rest of this section is about.

A canonical server-side LRPC startup sequence looks like this. The service compiles an IDL file with MIDL; MIDL emits an `RPC_SERVER_INTERFACE` structure that pins down the interface's UUID, version, and procedure table. The service calls `RpcServerUseProtseqEp` with the protocol sequence `"ncalrpc"`, 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 `\RPC Control`. The service calls `RpcServerRegisterIf2` or, since Windows 8, `RpcServerRegisterIf3` [@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 `RpcServerListen`, and worker threads in the RPC runtime block inside `NtAlpcSendWaitReceivePort`.

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.

<Mermaid caption="One full LRPC call lifecycle. The kernel ALPC dispatcher routes the message; the user-mode RPC runtime invokes the application's security callback; the application's MIDL stub executes the procedure. The kernel cannot inspect the body of the callback; the runtime trusts whatever the callback returns.">
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->>Kernel: NtAlpcSendWaitReceivePort (REQUEST with NDR body)
    Kernel->>Worker: deliver message to blocked worker
    Worker->>Cb: invoke IfCallbackFn (if registered)
    Cb->>Worker: return RPC_S_OK or RPC_S_ACCESS_DENIED
    Worker->>Stub: dispatch to MIDL procedure (if callback returned OK)
    Stub->>Worker: result returned through NDR encoder
    Worker->>Kernel: NtAlpcSendWaitReceivePort (REPLY)
    Kernel->>Client: deliver reply
</Mermaid>

The kernel's job ends at "deliver the message to a worker thread." Everything after that is application code. The RPC runtime is a DLL that the service loads into its own address space, and the runtime's notion of authorization is whatever the callback returns. If the callback returns `RPC_S_OK`, the call proceeds. If the callback is NULL, the call proceeds without ever asking the application. The kernel has no notion of "this call requires `SeImpersonatePrivilege`" or "this call requires the caller to be in the local Administrators group", because those notions are policy choices the application makes, not properties of the IPC primitive.

<Definition term="Endpoint Mapper (epmapper)">
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.
</Definition>

<Definition term="MIDL (Microsoft Interface Definition Language)">
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's implementation.
</Definition>

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.

| Flag | What Microsoft says it does | What it closes | What it leaves open |
|---|---|---|---|
| `RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH` | "the RPC runtime invokes the registered security callback for all calls, regardless of identity, protocol sequence, or authentication level of the client" | Forces the callback to run even for unauthenticated calls | The correctness of the callback's return value |
| `RPC_IF_ALLOW_SECURE_ONLY` | rejects callers that did not authenticate at the runtime's minimum authentication level | Unauthenticated callers | Authenticated-but-unauthorized callers; Microsoft notes verbatim that "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" [@msdocs-ifflags] |
| `RPC_IF_SEC_NO_CACHE` | "Disables security callback caching, forcing a security callback for each RPC call on a given interface" | Stale cached approval after a token-state change | The correctness of the callback's body |
| `RPC_IF_ALLOW_LOCAL_ONLY` | rejects remote callers at the runtime layer | Cross-machine reachability | Local elevation primitives |

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 *force the callback to run*. It cannot be configured to *make the callback return the right answer*.

> **Key idea:** 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.

> **Note:** Microsoft Learn's verbatim note on `IfCallbackFn` reads: *"Security-callback function, or NULL for no callback. Each registered interface can have a different callback function."* [@msdocs-rpcregisterif2] A NULL callback means "anyone who can open the connection port can call any procedure on this interface." Many in-house services interpret the parameter as if NULL meant "default deny." It does not. NULL is a default *allow*, 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.

`RpcServerRegisterIf3`, 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: *"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."* The `If3` API also bakes in an [AppContainer](/blog/appcontainer-and-lowbox-tokens-windowss-capability-sandbox/) 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 "this caller is allowed to invoke this procedure with these arguments" -- is delegated to an application function the kernel cannot inspect.

<Sidenote>The kernel-vs-application boundary inside `rpcrt4.dll` 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 `NtAlpcSendWaitReceivePort` 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'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.</Sidenote>

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 `appinfo` happens to be listening on, opens the well-known ALPC port `\RPC Control\epmapper`, sends a query containing the UUID, and gets back the endpoint name. The endpoint mapper is itself an LRPC service running inside `rpcss`. It bootstraps the rest of the local-IPC fabric.

NDR and NDR64 are the wire format. `NdrClientCall3` on the client side packs the call arguments into the NDR representation Microsoft documents on Learn [@msdocs-ndr64]; the bytes ride inside an ALPC `PORT_MESSAGE` body to the server; `NdrStubCall3` 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.

<Aside label="Why the OS cannot just validate the callback">
The intuitive question -- "if the callback is the problem, why doesn't the kernel just check it?" -- 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 "correct" means for an arbitrary application's authorization policy. "Correct" is the application'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.
</Aside>

The structural punchline is that the RPC runtime is application code -- the callback runs in user mode in the server's address space, the runtime trusts whatever the callback returns, and the OS cannot validate the callback'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 *both* structural instances of this asymmetry; no kernel change could have prevented them.

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.

## 8. Competing Approaches -- Named Pipes, COM, Filter Ports, and the Potato Disambiguation

Roughly half the time a defender reads "Potato" 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 `SeImpersonatePrivilege` policy.

Before the Potato classification, four local-IPC primitives sit alongside LRPC-on-ALPC and deserve a brief tour.

**Named pipes** [@msdocs-protseq] [@msdocs-impnp] [@csandker-np] are the first-class alternative that works both locally *and* across machines over SMB. The Windows RPC runtime supports a `ncacn_np` (Network Computing Architecture, Connection-oriented, Named Pipe) protocol sequence that lets an RPC interface be reached either through `\\.\pipe\name` locally or through an SMB tree-connect remotely. The load-bearing security primitive for the named-pipe-Potato class is `ImpersonateNamedPipeClient` [@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 `SeImpersonatePrivilege`. 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 "a service running with `SeImpersonatePrivilege` is tricked into connecting to a named pipe the attacker controls, and the attacker calls `ImpersonateNamedPipeClient` to inherit the service's token."

<Definition term="SeImpersonatePrivilege">
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: *"if you have SeAssignPrimaryToken or SeImpersonate privilege, you are SYSTEM"* [@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.
</Definition>

<Definition term="OXID resolver">
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.
</Definition>

**Shared sections plus events** is the lowest-level local-IPC pattern. Two processes call `NtCreateSection` 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.

**COM local activation** [@forshaw-com-ppl-2018] [@roguepotato-blog] is not a competitor. It is a higher-level overlay. The DCOM activation service (`rpcss`) takes a CoCreateInstance-style activation request and, for local activations, marshals into LRPC under the hood. This is why DCOM-activation attacks are *also* LRPC attacks: the trigger transport is DCOM, but the impersonation primitive ends up being the LRPC `RpcImpersonateClient` machinery that runs inside the activated server.

**Filter Communication Ports** [@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 `FltCreateCommunicationPort` to set up the server side; a user-mode application calls `FilterConnectCommunicationPort` to attach to it; the kernel-side `FltSendMessage` and the user-side `FilterReplyMessage` 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 "any named local IPC endpoint" with ALPC, and they should not.

Now the Potato disambiguation. The [Potato family](/blog/windows-access-control-25-years-of-attacks/) 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.

| Axis | DCOM-activation Potato | Named-pipe Potato |
|---|---|---|
| Triggering protocol | DCOM `CoGetInstanceFromIStorage` activation against `127.0.0.1` plus the local OXID resolver | Service connects out to a named pipe controlled by the attacker (often via UNC or by tricking a print or EFS hook) |
| Impersonation primitive | `RpcImpersonateClient` invoked by the activated COM server during the LRPC dispatch | `ImpersonateNamedPipeClient` invoked by the attacker on the receiving end of the pipe |
| Required attacker privilege | `SeImpersonatePrivilege` or `SeAssignPrimaryTokenPrivilege` | `SeImpersonatePrivilege` plus the ability to direct the service to connect to the attacker's pipe |
| Canonical exemplars | RoguePotato (May 2020) [@roguepotato-blog] [@roguepotato-repo], JuicyPotato, RottenPotato | PrintSpoofer (2020) [@itm4n-printspoofer], EfsPotato, PetitPotam |
| Post-KB5004442 status | OXID redirection to remote hosts blocked by `RPC_C_AUTHN_LEVEL_PKT_INTEGRITY` enforcement, March 2023 [@mssupport-kb5004442] | Unchanged at the OS level; mitigation is `SeImpersonatePrivilege` hygiene |
| Underlying IPC fabric | LRPC on ALPC | Named pipes |

The HITB Amsterdam 2021 talk *The Rise of Potatoes: Privilege Escalation in Windows Services* 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 "RogueWinRM" precursor work [@roguewinrm-blog] in which they obtained a SYSTEM identification token but not yet a usable impersonation token.

> **Note:** Does the writeup say `ImpersonateNamedPipeClient` or `RpcImpersonateClient`? 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.

<Sidenote>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 `RPC_C_AUTHN_LEVEL_PKT_INTEGRITY` 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.</Sidenote>

<Aside label="Why WNF and ETW are not in this comparison">
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 "state names" and the kernel delivers updates. Event Tracing for Windows (ETW) is the kernel's one-way event-streaming substrate [@tob-etw]; providers emit structured events, controllers configure sessions, and consumers read the events back. Yarden Shafir'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.
</Aside>

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?

## 9. The Limits -- Three Things ALPC and LRPC Structurally Cannot Enforce

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.

**The interface-callback gate cannot be enforced by the OS.** The `RpcServerRegisterIf2` contract [@msdocs-rpcregisterif2] accepts a function pointer into the application's address space; the runtime trusts whatever the callback returns. The OS-side enforcement available without an ABI change is at most "invoke the callback" (which `RPC_IF_SEC_NO_CACHE` [@msdocs-ifflags] already enforces on every call). The OS cannot read the callback's source, cannot infer its policy, and cannot decide whether the callback's verdict matches what the application'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.

**There is no transitive caller identity.** ALPC's Security message attribute captures the caller'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 "the message came from caller A, was forwarded by proxy B, and the original token is still attached." 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's specification said it should be.

**The kernel routing path is in the trusted computing base.** The ALPC dispatcher runs in Ring 0. Any bug in `_ALPC_PORT` object lifecycle, in `_ALPC_HANDLE_DATA` reference counting, in message-attribute marshalling, or in any of the dozens of structures Geoff Chappell'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 "improperly handles calls to Advanced Local Procedure Call (ALPC)" 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.

<Mermaid caption="The three impossibility results and the Patch-Tuesday treadmill they each feed. Almost every Patch Tuesday since 2018 has shipped fixes inside one of these three rectangles. None of the three is closable without a new ABI.">
flowchart TD
    A[The interface-callback gate -- the OS cannot validate the callback body] --> 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] --> D
    C[The kernel routing path is in the TCB -- any _ALPC_PORT or attribute bug is a direct kernel EoP] --> D
</Mermaid>

There is a fourth observation that is not an impossibility result but is worth stating in the same breath: **the practical upper bound on local authentication strength**. `RPC_C_AUTHN_LEVEL_PKT_INTEGRITY` is the practical ceiling for local LRPC; the `ncalrpc` transport supports only `RPC_C_AUTHN_WINNT` 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 *minimum* for DCOM activations to `PKT_INTEGRITY` in March 2023; it did not change the *ceiling*. 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.

> **Key idea:** 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.

> **Note:** 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 `RpcServerRegisterIf2` cannot exclude. Almost every subsequent year of Patch Tuesdays has shipped further instances of the same class, and 2026's count is on track to be no smaller than 2018's.

<Sidenote>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'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'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.</Sidenote>

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.

The Patch-Tuesday treadmill is the *expected* 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.

## 10. Open Problems and a Practical Field Guide (2024-2026)

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.

**Open problem 1: public RPC fuzzing at Microsoft-internal scale.** The public ceiling is RPCForge [@rpcforge] for NDR-aware fuzzing, Forshaw's `NtObjectManager` for interface inventory and client generation [@forshaw-saatools] [@forshaw-rpc-2019], and the November 2023 PoC talk *Building More Windows RPC Tooling for Security Research* [@forshaw-poc2023] for the latest research-tooling continuation. Microsoft's internal pipeline is not public; whether a coverage-guided NDR64 fuzzer can become a small-team repeatable Patch-Tuesday tool is open.

**Open problem 2: auditing the interface-registration model for structural permissiveness.** A defender using `Get-RpcServer` can enumerate every LRPC interface on a Windows install and dump each interface's procedures and security descriptor. The defender cannot tell, without per-interface manual review, whether a registered callback is correct. Heuristic detection of NULL `IfCallbackFn` is mechanical; detection of *semantically* permissive callbacks -- callbacks whose body trusts a field the caller controls -- is open and probably AI-shaped.

**Open problem 3: `RPC_IF_SEC_NO_CACHE` adoption and cost.** 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.

**Open problem 4: the local-COM-over-LRPC bypass class.** Forshaw'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.

**Open problem 5: ALPC as covert channel.** The CVE-2019-1162 MSCTF fix [@ormandy-ctf-2019] narrowed the MSCTF subsystem's exposure. The general class of "shared system ALPC ports that ignore caller integrity" is structural; identifying others requires the kind of systematic audit Open Problem 2 names.

**Open problem 6: defender SOC integration of the `Microsoft-Windows-Kernel-ALPC` [ETW provider](/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/)** [@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 [EDR vendors](/blog/from-cmdexe-to-a-kusto-row-in-90-seconds-how-sysmon-and-defe/) that gate it behind antimalware-PPL processes.

**Open problem 7: AppContainer-aware RPC capability checking.** `RpcServerRegisterIf3` [@msdocs-rpcregisterif3] introduces an AppContainer default-deny, but there is no standard pattern for in-house service authors who want to express "this procedure requires capability X." Service authors roll their own; some get it right.

| Tool | Purpose | Author / Org | Reference |
|---|---|---|---|
| `NtObjectManager` / `NtCoreLib` (formerly `NtApiDotNet`) | LRPC interface enumeration, decompilation, and client generation from PowerShell or .NET | James Forshaw, Project Zero | [@forshaw-saatools] [@forshaw-rpc-2019] |
| RpcView | Qt5/C++ GUI for browsing RPC servers and decompiled interface metadata across Windows versions | silverf0x | [@rpcview-repo] |
| RPC Investigator | .NET Forms UI built on `NtApiDotNet` for enumeration, client workbench, and an "RPC Sniffer" ETW-backed live view | Trail of Bits, January 2023 | [@tob-rpcinv-blog] [@rpcinv-repo] |
| RPCMon | ETW-based GUI for scanning RPC communication, built like Sysinternals Procmon, depending on Forshaw's library | CyberArk Labs | [@rpcmon-repo] |
| RPCForge | NDR-aware local Python fuzzer for ALPC-exposed RPC interfaces | Clement Rouault and Thomas Imbert, Sogeti ESEC | [@rpcforge] |
| Forshaw NDR64 / RPC research pipeline (2023) | Continued research tooling and conference materials | James Forshaw | [@forshaw-poc2023] |

**The practical field guide.** Eight numbered actions for the defender or in-house RPC service author. Each cites a verified source the reader can re-read in full.

> **Note:** 1. **Enumerate registered LRPC interfaces** with `Install-Module NtObjectManager; Get-RpcServer ... | Where-Object { $_.Endpoints.ProtocolSequence -eq 'ncalrpc' }` [@forshaw-saatools] [@forshaw-rpc-2019]. Snapshot before and after Patch Tuesday and diff on (UUID, procedure list, security descriptor). 2. **Enumerate live ALPC server ports** with `Get-NtAlpcServer`. The cmdlet returns the named connection ports; the unnamed per-connection ports are not enumerable by design (see Section 4) [@forshaw-saatools]. 3. **Reach a local RPC server from PowerShell** with Forshaw's `New-RpcClient` cmdlet, which generates a `[NtCoreLib.Win32.Rpc.Client]`-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. **Audit your own RPC service** for the canonical mistake: any `RpcServerRegisterIf2` or `RpcServerRegisterIf3` call with a NULL `IfCallbackFn` argument is "anyone who can open the port can call any procedure on the interface" [@msdocs-rpcregisterif2] [@msdocs-rpcregisterif3]. Treat NULL callbacks as a finding, not a default. 5. **Harden an exposed LRPC interface** with the flag combination `RPC_IF_ALLOW_SECURE_ONLY | RPC_IF_SEC_NO_CACHE` plus an explicit callback that validates `I_RpcBindingInqLocalClientPID` and the caller's token integrity level [@msdocs-ifflags]. The Microsoft Learn note that "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" [@msdocs-ifflags] makes the explicit callback non-optional. 6. **For DCOM-activated services**, accept the KB5004442 default (`RPC_C_AUTHN_LEVEL_PKT_INTEGRITY` minimum) and do not invoke registry overrides. The override path was removed in the March 14, 2023 phase 3 rollout [@mssupport-kb5004442]. 7. **For runtime visibility**, enable the Microsoft-Windows-RPC ETW provider via RPCMon [@rpcmon-repo] or RPC Investigator's RPC Sniffer [@tob-rpcinv-blog] [@rpcinv-repo]; correlate per-process per-procedure call rates against the service inventory from step 1. 8. **For per-message kernel-level visibility**, enable the Microsoft-Windows-Kernel-ALPC system provider from an `EVENT_TRACE_SYSTEM_LOGGER_MODE` 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.

<RunnableCode lang="js" title="Patch-Tuesday RPC interface diff: pseudocode">{`
// Real shell pipeline that produces the inputs:
//   Get-RpcServer | Export-Clixml -Path C:\\\\Snaps\\\\rpc-pre-patch.xml
//   <Install Patch Tuesday updates and reboot>
//   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.

const pre = new Map([
  ['201ef99a-7fa0-444c-9399-19ba84f12a1a', ['Activate','Cancel','Continue','GetElevationType']],
  ['86d35949-83c9-4044-b424-db363231fd0c', ['SchRpcRegisterTask','SchRpcRetrieveTask','SchRpcSetSecurity']],
  ['e1af8308-5d1f-11c9-91a4-08002b14a0fa', ['ept_lookup','ept_map','ept_insert']],
]);

const post = new Map([
  ['201ef99a-7fa0-444c-9399-19ba84f12a1a', ['Activate','Cancel','Continue','GetElevationType','RequestElevation2']],
  ['86d35949-83c9-4044-b424-db363231fd0c', ['SchRpcRegisterTask','SchRpcRetrieveTask','SchRpcSetSecurityV2']],
  ['e1af8308-5d1f-11c9-91a4-08002b14a0fa', ['ept_lookup','ept_map','ept_insert']],
]);

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 => !a.has(p));
  const removed = [...a].filter(p => !b.has(p));
  if (added.length || removed.length) {
    console.log(\`Interface \${uuid}\`);
    if (added.length)   console.log('  + added:   ' + added.join(', '));
    if (removed.length) console.log('  - removed: ' + removed.join(', '));
  }
}
`}</RunnableCode>

<Sidenote>RPCMon ships a hard-coded RPC interface dictionary named `RPC_UUID_Map_Windows10_1909_18363.1977.rpcdb.json` [@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 *running* the regeneration is what stays on the defender.</Sidenote>

<Spoiler kind="solution" label="A one-liner you can paste into PowerShell right now">
Install Forshaw's module and dump every local-only RPC interface on the current Windows install, one row per interface, sorted by procedure count:

```powershell
Install-Module NtObjectManager -Scope CurrentUser
Get-RpcServer -DbgHelpPath "$env:ProgramFiles\Debugging Tools for Windows\dbghelp.dll" |
  Where-Object { $_.Endpoints.ProtocolSequence -eq 'ncalrpc' } |
  Sort-Object { $_.Procedures.Count } -Descending |
  Select-Object Name, InterfaceId, @{N='Procs';E={$_.Procedures.Count}} |
  Format-Table -AutoSize
```

Expect dozens of named interfaces on a clean Windows 11 install. Save the output, install Patch Tuesday, run it again, and `Compare-Object` the two snapshots. That diff is the canonical research workflow that the December 2019 Project Zero post [@forshaw-rpc-2019] introduced.
</Spoiler>

<Aside label="What an in-house RPC author should change first">
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'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).
</Aside>

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?

## 11. FAQ -- Six Misconceptions, Removed

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.

<FAQ title="Frequently asked misconceptions about ALPC and LRPC">

<FAQItem question="Is every Windows service reachable over ALPC?">
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 "every Windows service that wants to be reachable locally with first-class kernel-mediated transport uses LRPC on ALPC", not "every service uses ALPC."
</FAQItem>

<FAQItem question="Are all Potato-family attacks ALPC attacks?">
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.
</FAQItem>

<FAQItem question='Does "every ALPC port has an Object Manager name" hold?'>
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.
</FAQItem>

<FAQItem question="Is ALPC documented by Microsoft?">
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's site [@chappell-alpc] [@chappell-alpcp]. Microsoft *does* document ALPC architecturally in *Windows Internals 7th Edition Part 2* [@wininternals-7e], Chapter 8 section "Advanced local procedure call (ALPC)"; 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'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.
</FAQItem>

<FAQItem question='Is "Local RPC" the same as "Local Procedure Call"?'>
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'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.
</FAQItem>

<FAQItem question="Where is Yarden Shafir's 'ALPC Internals' series at Trail of Bits?">
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 "ALPC Internals" 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'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.
</FAQItem>

</FAQ>

> **Note:** Three sources are worth the rest of an afternoon. Christian Sandker's three-part *Offensive Windows IPC* 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. *Windows Internals 7th Edition Part 2* Chapter 8 section *Advanced local procedure call (ALPC)* [@wininternals-7e] is the Microsoft-blessed architectural reference; cite by ISBN 978-0-13-546238-6. James Forshaw's December 17, 2019 Project Zero post *Calling Local Windows RPC Servers from .NET* [@forshaw-rpc-2019] is the canonical introduction to the `NtObjectManager` tooling and the methodology change it unlocked. For the sister-article context in this series: the Object Manager Namespace post explains the `\RPC Control` 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.

<PullQuote>
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.
</PullQuote>

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.

<StudyGuide slug="alpc-and-lrpc-the-local-ipc-fabric-under-every-windows-service" keyTerms={[
  { term: "ALPC", definition: "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." },
  { term: "LRPC", definition: "The Microsoft RPC runtime's local-only transport, selected when the protocol sequence is `ncalrpc`. Implemented in `rpcrt4.dll`; rides on top of ALPC ports." },
  { term: "LPC", definition: "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." },
  { term: "Connection port (ALPC)", definition: "The named ALPC port a server creates so clients can find it. Lives in the Object Manager namespace, typically under `\\RPC Control`." },
  { term: "Communication port (ALPC)", definition: "The unnamed per-connection ALPC port created by `NtAlpcAcceptConnectPort`. Exists only as handles in the connecting and accepting processes; not reachable by name." },
  { term: "Message attribute", definition: "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." },
  { term: "Interface security callback", definition: "The application-supplied `IfCallbackFn` passed to `RpcServerRegisterIf2`/`RpcServerRegisterIf3`. The kernel cannot inspect or constrain it. NULL is a legal value and means 'no callback'." },
  { term: "Endpoint mapper", definition: "The well-known LRPC service at `\\RPC Control\\epmapper` that translates an interface UUID into the endpoint name a service is listening on. Hosted by `rpcss`." },
  { term: "NDR / NDR64", definition: "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." },
  { term: "SeImpersonatePrivilege", definition: "Windows user-right that permits a thread to impersonate another security principal via APIs such as `ImpersonateNamedPipeClient`. The privilege the named-pipe-Potato family abuses." }
]} questions={[
  { q: "Why does the per-connection ALPC communication port have no Object Manager name?", a: "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's 2006 structural class the Vista redesign closed." },
  { q: "Why can the OS not enforce the correctness of an interface security callback?", a: "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 'correct' means for an arbitrary application's authorization policy. Closing the gap requires either a declarative authorization ABI or a sandbox; Microsoft has not publicly committed to either." },
  { q: "What distinguishes a DCOM-activation Potato from a named-pipe Potato?", a: "The impersonation primitive. DCOM-activation Potatoes (RoguePotato, JuicyPotato, RottenPotato) use `RpcImpersonateClient` inside an LRPC-on-ALPC dispatch path. Named-pipe Potatoes (PrintSpoofer, EfsPotato, PetitPotam) use `ImpersonateNamedPipeClient` 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." },
  { q: "What changed in March 2023 for DCOM-activated services?", a: "KB5004442 phase 3 enabled the DCOM hardening with no override path. `RPC_C_AUTHN_LEVEL_PKT_INTEGRITY` 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." },
  { q: "Where can a defender see ALPC traffic at the per-message level?", a: "From the `Microsoft-Windows-Kernel-ALPC` system ETW provider, enabled in an `EVENT_TRACE_SYSTEM_LOGGER_MODE` 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." }
]} />
