# Windows Local RPC and ALPC: From Client Call to Service Dispatcher

> Understand Windows local RPC bindings, interfaces, NDR stubs, endpoints, service dispatch, and the authorization boundaries the transport does not provide.

- Published: 2026-08-21
- Difficulty: advanced
- Series: Windows Service Internals
- Tags: Windows, Windows Internals, RPC, ALPC, IPC
- Source: https://stevenfoerster.com/tutorials/windows-local-rpc-and-alpc/

## Prerequisites

- Familiarity with Windows services and dynamic-link libraries
- Basic C knowledge, including function pointers and structures

Calling a Windows service often looks like calling an ordinary function. A client passes typed arguments to a generated stub, waits, and receives a return value. Between those two points, the RPC runtime has encoded the arguments, selected a local endpoint, crossed a process boundary, decoded the request, selected an interface and procedure, and dispatched a server routine on a worker thread.

The convenience is the point. Microsoft RPC lets software define an interface once and keep most transport machinery outside application code. It also makes several different objects easy to collapse into one vague idea called "the RPC endpoint."

They are not the same thing:

- The **protocol sequence** selects a transport family, such as `ncalrpc` for same-machine calls.
- The **endpoint** identifies where a server receives calls for that protocol sequence.
- The **interface UUID and version** identify a contract offered by the server.
- The **procedure number** selects an operation within that interface.
- The **binding handle** packages the information a client needs to reach the server.
- The **RPC authentication context** describes how the client and server authenticate and protect the call.

Understanding those layers makes local RPC behavior much less mysterious.

## The call path

At a high level, a local RPC call travels through this pipeline:

```mermaid
flowchart LR
    A[Client application] --> B[Generated or handwritten client stub]
    B --> C[Microsoft RPC runtime]
    C --> D[ncalrpc local endpoint]
    D --> E[Server RPC runtime]
    E --> F[Server stub and procedure table]
    F --> G[Manager or worker routine]
```

The application-facing client function may look like this:

```c
DWORD SettingsGetValue(
    RPC_BINDING_HANDLE binding,
    const GUID *settingId,
    BYTE **value,
    DWORD *valueBytes
);
```

The client stub does not normally implement the operation. It describes the call to the RPC runtime: which interface, which procedure, how each argument is represented, and where returned data should go.

The server stub performs the inverse operation and calls the registered implementation.

## IDL, MIDL, and NDR

RPC interfaces are commonly authored in Interface Definition Language (IDL). A simplified interface might be:

```text
[
    uuid(11111111-2222-3333-4444-555555555555),
    version(1.0),
    pointer_default(unique)
]
interface Settings
{
    error_status_t GetValue(
        [in] handle_t binding,
        [in] GUID *settingId,
        [out, size_is(,*valueBytes)] byte **value,
        [out] unsigned long *valueBytes
    );
}
```

The Microsoft IDL compiler generates client and server artifacts. Among other things, those artifacts describe:

- Interface identity and version
- Procedure ordering
- Parameter direction (`in`, `out`, or both)
- Pointer behavior
- Array lengths and correlations
- Wire representation
- Allocation and cleanup responsibilities

Arguments are encoded using Network Data Representation (NDR). NDR gives the two processes a shared representation for integers, structures, pointers, arrays, unions, and other IDL types.

The procedure number, often called an **opnum**, is an index into the interface's procedure contract. It is not a process-wide RPC method number. Opnum 3 on one interface is unrelated to opnum 3 on another.

> [!note]
> A UUID identifies an RPC interface contract. An RPC object UUID is a separate, optional concept used to select an object or manager implementation behind an interface. Do not assume every UUID visible near binding code serves the same role.

## Bindings: how the client names the server

An RPC string binding has this conceptual form:

```text
ObjectUUID@ProtocolSequence:NetworkAddress[Endpoint,Options]
```

For a same-machine service, a simple binding might use:

```text
ncalrpc:[example.settings]
```

The network-address field is unnecessary for `ncalrpc` because both parties are on the local computer. The endpoint can be explicit, or the client can use a partially bound handle and ask the endpoint mapper to resolve the server that registered the interface.

Client code commonly composes and converts a string binding:

```c
RPC_WSTR stringBinding = NULL;
RPC_BINDING_HANDLE binding = NULL;

RpcStringBindingComposeW(
    NULL,
    (RPC_WSTR)L"ncalrpc",
    NULL,
    (RPC_WSTR)L"example.settings",
    NULL,
    &stringBinding
);

RpcBindingFromStringBindingW(stringBinding, &binding);
RpcStringFreeW(&stringBinding);
```

Real clients may hide this inside a library, use automatic handles generated by MIDL, cache a binding, or obtain endpoint information from another service.

## What `ncalrpc` means

`ncalrpc` is the Microsoft RPC protocol sequence for calls between programs on the same computer. It avoids a network transport and is the preferred RPC path for local communication.

On Windows, local interprocess communication is built on kernel facilities including Advanced Local Procedure Call (ALPC). You may see ALPC port objects, messages, and native calls when observing a local RPC exchange. That does not make RPC and ALPC interchangeable:

| Layer | Responsibility |
| --- | --- |
| Application RPC interface | Typed procedures and application semantics |
| Microsoft RPC runtime | Binding, authentication, NDR marshaling, dispatch, errors |
| Local transport | Same-machine message delivery |
| ALPC facilities | Kernel-supported local port and message primitives |

Application code should ordinarily use the documented RPC APIs rather than native ALPC system calls. The RPC runtime owns details that an application should not reimplement, including marshaling and interface dispatch.

The other important boundary is semantic: **local is a location, not an authorization decision**. `ncalrpc` excludes remote machines. It does not imply that the local caller is trusted.

## How a server becomes reachable

A standalone RPC server or service typically performs three kinds of registration.

### 1. Register a protocol sequence and endpoint

```c
RpcServerUseProtseqEpW(
    (RPC_WSTR)L"ncalrpc",
    RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
    (RPC_WSTR)L"example.settings",
    endpointSecurityDescriptor
);
```

This tells the runtime where to receive local calls. Related APIs can request a dynamic endpoint or register all supported protocol sequences.

### 2. Register an interface

```c
RpcServerRegisterIfEx(
    Settings_v1_0_s_ifspec,
    NULL,
    NULL,
    RPC_IF_ALLOW_SECURE_ONLY,
    RPC_C_LISTEN_MAX_CALLS_DEFAULT,
    SecurityCallback
);
```

The interface specification connects the UUID, version, stub metadata, and dispatch table. Registration flags and a security callback can impose interface-level requirements.

### 3. Listen for calls

```c
RpcServerListen(
    1,
    RPC_C_LISTEN_MAX_CALLS_DEFAULT,
    FALSE
);
```

Windows services may arrange these steps inside a service DLL loaded by a shared host. The Service Control Manager starts the service, the host loads the module, and service-specific initialization registers RPC and other IPC surfaces. The eventual worker routine may be several layers away from `ServiceMain`.

## Endpoint security and operation authorization

An `ncalrpc` endpoint can have a security descriptor. That descriptor controls access to the endpoint object and can prevent some callers from connecting.

It should not be treated as the entire service authorization model. Microsoft discourages this directly: the [`RpcServerUseAllProtseqs`](https://learn.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-rpcserveruseallprotseqs) documentation states that "using a security descriptor on the endpoint in order to make a server secure is not recommended." There are several reasons:

- Connection permission is coarser than operation permission.
- One endpoint can support multiple interfaces with different policies.
- A caller allowed to use a read operation may not be allowed to use a write operation.
- Authentication and operation-specific context may not be available at endpoint creation time.
- Server code can expose additional protocol sequences or bindings.

The last reason is stronger than it looks. Interfaces are not bound to the endpoint they were registered on: every interface in a process is callable on every endpoint that process listens on, so a second RPC server sharing the process can expose a path around a carefully written endpoint descriptor. Microsoft's guidance for constraining this is a security callback that checks the protocol sequence the call actually arrived on, not an endpoint ACL.

Think of the layers this way:

```text
Endpoint security: may this token connect here?
Interface security: may this authenticated context call this interface?
Procedure authorization: may this caller request this operation on this object?
Object security: may the effective token access the downstream resource?
```

All four can matter. Passing the first does not imply passing the rest.

For the token mechanics behind the final two decisions, see [Windows Service Impersonation](https://stevenfoerster.com/tutorials/windows-service-impersonation-and-access-tokens/).

## Authentication is not authorization

RPC supports authentication services, authentication levels, server principal names, and client identity queries. These controls answer questions such as:

- Is the peer authenticated?
- Is packet integrity protected?
- Is packet privacy required?
- Which principal made the call?
- Is the caller local?

None of those questions alone says whether that principal may perform a specific operation.

Authentication also runs in the other direction, and it is the half most local designs skip. A client that binds by endpoint name alone has verified nothing about who is listening. Since the endpoint namespace is claimed at registration time, a service that starts late, or that a caller can be induced to reach, may not be the service the client meant to call. The client-side answer is to require authentication with an expected server principal, so the identity of the server is checked rather than assumed.

Server-side, `I_RpcBindingIsClientLocal` can establish that a binding represents a client on the same machine. That can be a useful constraint for a local management surface. It cannot replace a policy such as "administrators may update machine configuration" or "the owner may modify this object."

Note that this function lives in `rpcdcep.h`, the private RPC runtime header, which states that these routines exist for the stubs and support libraries and that applications must not call them. It appears here because it appears in real service binaries and is worth recognizing during analysis, not because it is a supported way to build one.

```c
unsigned int isLocal = 0;
RPC_STATUS status = I_RpcBindingIsClientLocal(binding, &isLocal);
if (status != RPC_S_OK || !isLocal) {
    return ERROR_ACCESS_DENIED;
}

// This proves locality only. An authorization decision is still required.
return AuthorizeAndPerformOperation(binding, request);
```

## Dispatch: from interface to worker

Once a call reaches the server runtime, interface identity and version select the registered interface. The procedure number selects a stub entry. The stub unmarshals arguments and invokes the manager routine or another dispatch function.

A conceptual server table looks like:

```c
typedef DWORD (*RPC_METHOD)(void *callContext);

RPC_METHOD SettingsDispatch[] = {
    SettingsGetVersion,  // opnum 0
    SettingsGetValue,    // opnum 1
    SettingsSetValue,    // opnum 2
    SettingsResetValue   // opnum 3
};
```

Generated code is more complex because it carries NDR format information, exception handling, allocation hooks, and architecture-specific metadata. The useful mental model remains an indexed contract followed by a normal native function call.

This is why an RPC method can be secure at the transport layer and still be unsafe in its implementation. Once dispatch enters the manager routine, the operation is ordinary service code running in the server process.

## Context handles and object identity

Many stateful interfaces return a **context handle**. The client sees an opaque value; the server associates it with server-side state.

This is a second, alternative shape for the same `Settings` example. The interface sketched earlier passes a binding handle on every call and keeps no server-side state between them. The variant below opens a session first and then addresses that session, so its procedures are not the ones listed in the dispatch table above.

```text
typedef [context_handle] void *SETTINGS_CONTEXT;

error_status_t OpenSettings(
    [in] handle_t binding,
    [out] SETTINGS_CONTEXT *context
);

error_status_t SetValue(
    [in] SETTINGS_CONTEXT context,
    [in] GUID *settingId,
    [in] unsigned long value
);
```

Context handles solve lifetime and state-management problems, but they are not automatically an application authorization policy. The server still needs to decide:

- Which caller may create the context?
- Is the context bound to the creating identity?
- May it be reused from another authenticated call?
- What happens when the client disconnects?
- Does each operation revalidate mutable policy?

The RPC runtime can help manage rundown when a client disappears. Application code owns the security meaning of the state.

## Failures by layer

RPC failures are easier to interpret when grouped by layer.

| Layer | Representative failure |
| --- | --- |
| Binding | Invalid string binding or unavailable endpoint |
| Transport | Server unavailable or connection closed |
| Interface | Unknown interface or unsupported version |
| Stub/NDR | Bad stub data or bounds/correlation failure |
| Security | Authentication failure or access denied |
| Application | A method-specific Win32 or domain error |

The distinction matters for diagnostics. An application-level `ERROR_FILE_NOT_FOUND` implies the request passed binding, transport, interface selection, stub decoding, and at least enough application logic to look for a file. It does not by itself prove that every required authorization check occurred.

## A service-oriented mental model

When reading or designing a Windows service that exposes local RPC, keep four diagrams separate.

### Hosting

```text
Service Control Manager
    -> service host process
        -> service DLL
            -> initialization
```

### Communication

```text
client stub -> binding -> ncalrpc endpoint -> server runtime -> server stub
```

### Dispatch

```text
interface UUID/version -> procedure number -> manager routine -> worker
```

### Authority

```text
client identity -> interface/procedure policy -> effective thread token
    -> file/registry/object access check
```

Most confusion comes from drawing only one of these. A service can be hosted correctly, bound correctly, and dispatched correctly while making the wrong authority decision.

## Where Ghidra fits

In a compiled service DLL, these layers leave different artifacts:

- Service exports and control-handler registration describe hosting.
- Calls to `RpcServerUseProtseq*` and `RpcServerRegisterIf*` describe RPC registration.
- Interface specifications, UUID data, and dispatch tables describe the contract.
- Imports such as `RpcImpersonateClient`, `AccessCheck`, and resource APIs describe potential authority transitions.

[Anatomy of a Windows Service DLL in Ghidra](https://stevenfoerster.com/tutorials/anatomy-of-a-windows-service-dll-in-ghidra/) explains how to organize those artifacts without treating the decompiler as source code.

## Design checklist

For each local RPC interface, record:

1. Which service process hosts it?
2. Which protocol sequences and endpoints expose it?
3. Which interface UUID and versions are registered?
4. How does the client authenticate the server?
5. How does the server authenticate the client?
6. What does the endpoint security descriptor restrict?
7. What does the interface security callback restrict?
8. What authorization policy applies to each procedure?
9. Which token is effective during downstream object access?
10. How are context handles tied to identity and lifetime?

If a design has only an answer for the endpoint DACL, it does not yet have an operation-level authorization model.

## References

- [CVE-2026-69321: umpo.dll Power Dependency Coordinator analysis](https://stevenfoerster.com/notes/cve-2026-69321/), a worked example of a missing authorization check in a SYSTEM service's RPC handlers
- [Selecting a protocol sequence](https://learn.microsoft.com/en-us/windows/win32/rpc/selecting-a-protocol-sequence), Microsoft Learn
- [String binding](https://learn.microsoft.com/en-us/windows/win32/rpc/string-binding), Microsoft Learn
- [Making the server available](https://learn.microsoft.com/en-us/windows/win32/rpc/making-the-server-available-on-the-network), Microsoft Learn
- [`RpcServerUseAllProtseqs`](https://learn.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-rpcserveruseallprotseqs), Microsoft Learn
- [Be wary of other RPC endpoints running in the same process](https://learn.microsoft.com/en-us/windows/win32/rpc/be-wary-of-other-rpc-endpoints-running-in-the-same-process), Microsoft Learn
- [Microsoft RPC protocol extensions overview](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/fb1ee8ad-3d98-4a17-a4d6-3d1693626410), Microsoft Open Specifications
- [Service startup](https://learn.microsoft.com/en-us/windows/win32/services/service-startup), Microsoft Learn
