ALPC: Advanced Local Procedure Call Internals

Windows Internals
Time it takes to read this article 6 minutes.

If you trace almost any everyday operation on Windows down through its layers — asking the Service Control Manager to start a service, authenticating against LSASS, a UWP app talking to a broker, a user-mode driver servicing a request — you eventually arrive at the same transport underneath: Advanced Local Procedure Call, or ALPC. It is the kernel’s high-performance inter-process communication mechanism, and it quietly carries a huge fraction of the messages that flow between processes on a single machine. ALPC replaced the older LPC (Local Procedure Call) mechanism in Windows Vista, keeping the same basic client/server shape but redesigning it for throughput, scalability, and richer message semantics. This article dissects how ALPC works — its ports, its connection handshake, how messages and bulk data move — and why it is such an important local security boundary.

Ports

The central abstraction in ALPC is the port, which is a first-class kernel object — the Object Manager exposes it as the ALPC Port type, secured by a security descriptor like any other object. There are two roles a port can play:

  • A connection port is the named rendezvous point a server publishes and listens on. Because it is a named object, it lives in the object namespace (typically under \RPC Control or a service-specific directory) where clients can find it by name.
  • A pair of communication ports is created for each accepted connection — a server communication port and a client communication port. These are unnamed and private to that one conversation; all subsequent messages for a session flow through them.

This split matters: the connection port is the public “front door” that any client may attempt to reach (subject to the port’s DACL), while the per-connection communication ports isolate each client’s traffic. A server can also impose a maximum message size and a connection-request handler when it creates the connection port.

The Connection Model

The lifecycle of an ALPC conversation is a deliberate handshake built on a small set of native system calls in ntdll and the kernel:

  • The server calls NtAlpcCreatePort to create a named connection port and then waits to receive connection requests on it.
  • A client calls NtAlpcConnectPort, naming the server’s connection port. This sends a connection request message that the server receives.
  • The server inspects the request — at this point it can examine who is connecting — and either accepts it with NtAlpcAcceptConnectPort or rejects it. Acceptance creates the communication-port pair that binds the two processes.

Every message that crosses an ALPC port begins with a fixed PORT_MESSAGE header. That header carries the total and data lengths, a message type, and a message ID plus the sender’s client ID (process and thread), which lets the two sides correlate requests with replies and lets the server know exactly which thread in which process sent a given message.

Message Passing

Once connected, both sides exchange messages primarily through a single versatile call, NtAlpcSendWaitReceivePort, which can send a message and wait for the reply in one transition — the request/response pattern that RPC is built on. ALPC optimizes for two very different payload sizes:

  • Small messages are copied inline. The payload rides along in the kernel-managed message buffer, which is fast and simple for the common case of compact requests.
  • Large payloads are moved through ALPC sections — shared memory. The server (or client) creates a section, and views of it are mapped into both processes; the message then references a region of that shared view instead of copying megabytes through the kernel. This is how bulk data crosses the boundary without expensive double-copies.

Beyond raw bytes, ALPC messages can carry typed message attributes. The most important is the handle attribute, which lets a sender pass a kernel handle to the receiver — the kernel duplicates the handle into the target process as part of message delivery, a clean way to hand over a file, section, or process handle. Other attributes convey security and context information used for impersonation and for correlating a message with server-side state. This attribute model is a large part of what makes ALPC “advanced” compared to the older LPC.

RPC over ALPC

Most developers never call the NtAlpc* functions directly. Instead they use RPC, and when both endpoints are on the same machine the RPC runtime selects the ncalrpc (local RPC) transport, which is implemented on top of ALPC. The RPC marshaling layer turns a function call and its parameters into an ALPC message, sends it over the port, and unmarshals the reply. This is precisely why ALPC sits underneath so much of the operating system: the Service Control Manager, the task scheduler, LSASS, the DNS client, WinRT brokers, and countless other components expose their functionality as local RPC interfaces, and every one of those calls becomes ALPC traffic. The Service Control Manager you send sc start requests to, and the LSASS that authenticates you, are both reached this way.

Security Context and Impersonation

Because a single server often handles requests from many clients at different privilege levels, ALPC is built to let the server learn and adopt the caller’s identity. The PORT_MESSAGE already identifies the sender’s process and thread, and the server can go further and impersonate the client for the duration of a request so that any resource access it performs is checked against the client’s access token rather than the server’s. This ties directly back to the Windows security model: the same tokens, SIDs, and access checks covered earlier in this series are what an ALPC server evaluates when it acts on a client’s behalf. Getting this right — impersonating at the correct level, validating what the client asked for, and reverting afterward — is the crux of writing a secure local RPC/ALPC server.

Inspecting ALPC

ALPC is observable with the same tools used elsewhere in this series. On a live system, Sysinternals WinObj shows the named connection ports sitting in the object namespace (browse \RPC Control). In a kernel debugger, dedicated extensions decode ports and their pending messages:

# WinDbg (kernel mode)

# List all ALPC ports in the system, grouped by owning process
!alpc /lpp

# Dump a specific port object: its communication peer,
# pending messages, and owning process
!alpc /m <port-address>

# The ALPC Port object type itself
dt nt!_ALPC_PORT
dt nt!_PORT_MESSAGE

For enumeration and research from user mode, James Forshaw’s NtObjectManager PowerShell module is invaluable — it can list ALPC ports, connect to them, and inspect their security descriptors, which is exactly the kind of visibility you want when mapping a machine’s local attack surface:

# PowerShell with the NtObjectManager module
Import-Module NtObjectManager

# Enumerate ALPC ports visible in the object namespace
Get-NtAlpcPort

# Inspect the security descriptor on a specific port
Get-NtObject "\RPC Control\lsasspirpc" | Format-List

!alpc /lpp is the fastest way to see which processes own which ports and how they are paired, while the NtObjectManager cmdlets let you reason about who is allowed to connect to a given server — the first question in any local privilege-escalation review.

Security Relevance

ALPC is one of the richest local attack surfaces on Windows, precisely because so many privileged services expose functionality over it and accept input from lower-privileged clients. The recurring themes a defender or a researcher (working under authorization) should understand are:

  • Input handling bugs in RPC/ALPC servers. A privileged server that mishandles a malformed message, an unexpected handle attribute, or an out-of-range offset into a shared section can be driven into memory corruption or logic errors — the source of a long line of local elevation-of-privilege vulnerabilities. The historical ALPC/Task Scheduler EoP is a well-known example of a privileged local server trusting client-supplied input it should have validated.
  • Impersonation mistakes. If a server fails to impersonate the client and instead performs an operation with its own SYSTEM token, or impersonates at too high a level, the client can trick it into acting beyond the client’s own rights — a classic confused-deputy pattern.
  • Port name squatting. If a server does not create its connection port with a sufficiently strict DACL, or a lower-privileged actor can pre-create a name a privileged component expects, clients can be redirected to a rogue endpoint or the server’s namespace can be hijacked.
  • The trust boundary itself. Every ALPC connection port that a SYSTEM service exposes to non-SYSTEM callers is a boundary that must be enforced by the port’s DACL and by rigorous validation of every message. Mapping those boundaries — which ports exist, who may connect, and what each accepts — is the essence of local attack-surface analysis.

All of this is described for defensive understanding and authorized testing only. For defenders, the practical takeaways are to keep RPC/ALPC servers patched, run them with the least privilege they can tolerate, apply restrictive port DACLs, and monitor for anomalous local RPC activity against sensitive services.

Conclusion

ALPC is the invisible plumbing of a running Windows system: named connection ports for rendezvous, per-connection communication ports for isolation, inline messages for speed and shared sections for bulk data, handle and security attributes for rich semantics, and an impersonation model that ties it all back to the Windows security reference monitor. Because local RPC rides on top of it, understanding ALPC is understanding how privileged and unprivileged code actually talk to each other — and where that conversation can go wrong. In the next article we look at how 32-bit processes run on 64-bit Windows: the WoW64 subsystem.

You Might Also Like

This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:

Comments

Copied title and URL