# Windows Service Impersonation: Which Token Does an RPC Handler Use?

> Follow process and thread tokens through a Windows service call, then apply impersonation and access checks without creating a confused deputy.

- Published: 2026-08-28
- Difficulty: advanced
- Series: Windows Service Internals
- Tags: Windows, Windows Internals, Access Control, RPC, Security Architecture
- Source: https://stevenfoerster.com/tutorials/windows-service-impersonation-and-access-tokens/

## Prerequisites

- Familiarity with Windows services and Win32 handles
- Basic understanding of users, groups, SIDs, and ACLs
- Reading comfort with MSVC structured exception handling (__try and __finally)

A Windows service can receive a request from an ordinary user and still perform the resulting work as `NT AUTHORITY\SYSTEM`. That is sometimes exactly what the service is designed to do. The service exists because the caller cannot perform the operation directly.

The security question is not simply whether the service is privileged. It is which identity Windows uses at each decision point, and whether the service has independently decided that the caller is allowed to request the operation.

This tutorial follows one request through a service process, an RPC worker thread, an impersonation token, an access check, and a protected object. The goal is to make the token transition concrete enough that code like this becomes immediately suspicious:

```c
void RpcSetMachineConfiguration(const wchar_t *name, const BYTE *data) {
    // The caller supplied the data, but this thread still has only the
    // service process token. The registry sees LocalSystem.
    WriteProtectedConfiguration(name, data);
}
```

The bug is not that the service has authority. The bug is that caller-controlled intent reached that authority without an authorization decision.

## Primary tokens and impersonation tokens

Every Windows process has a **primary access token**. It describes the security context in which the process runs: the user SID, group SIDs, privileges, integrity level, restrictions, and other attributes used in access decisions.

A thread normally has no separate token. When that thread opens a file, registry key, process, event, or other securable object, Windows uses the process primary token.

A thread can also carry an **impersonation token**. While it does, the thread has two relevant identities:

| Token | Describes | Used for |
| --- | --- | --- |
| Process primary token | The service account | Process creation and the thread's default security context |
| Thread impersonation token | The client being represented | Object access performed by that impersonating thread |

The effective token for an ordinary object access is therefore:

```text
if the current thread has an impersonation token:
    use the thread token
else:
    use the process primary token
```

That small rule explains most service-side authorization failures. A service hosted as LocalSystem starts with an extremely powerful primary token. Unless its request thread impersonates the client, a call such as `RegOpenKeyExW`, `CreateFileW`, or `OpenProcess` is evaluated as LocalSystem.

```mermaid
flowchart LR
    C[Standard-user client] -->|RPC request| T[Service worker thread]
    P[Service process token: SYSTEM] --> T
    T --> Q{Thread token present?}
    Q -->|No| S[Object access checked as SYSTEM]
    Q -->|Yes| U[Object access checked as client]
```

> [!note]
> A token contains more than a username. Group membership, disabled or deny-only SIDs, privileges, restricted SIDs, integrity level, capabilities, and token type can all change an access decision. Do not reduce a token comparison to `DOMAIN\user` strings.

## What RPC impersonation changes

An RPC server thread processing an active call can invoke `RpcImpersonateClient`. With a null binding handle, the RPC runtime selects the client currently being served by that thread.

```c
RPC_STATUS status = RpcImpersonateClient(NULL);
if (status != RPC_S_OK) {
    RpcRaiseException(status);
}

// Object access on this thread now uses the client's token.
HANDLE key = OpenConfigurationKey(KEY_SET_VALUE);

RpcRevertToSelf();
```

Before impersonation, the worker thread uses the service process token. After successful impersonation, it has a client impersonation token. After `RpcRevertToSelf`, it returns to the process identity.

The return-value check is load-bearing. Microsoft explicitly warns that if `RpcImpersonateClient` fails, the request continues in the process security context unless the server stops it. In a LocalSystem service, "impersonation failed, continue anyway" converts a failed safety mechanism into a privileged fallback.

The safe shape is:

```c
RPC_STATUS status = RpcImpersonateClient(NULL);
if (status != RPC_S_OK) {
    return ERROR_ACCESS_DENIED;
}

DWORD result;
__try {
    result = PerformOperationAsCaller();
}
__finally {
    RpcRevertToSelf();
}
return result;
```

Production C++ should use an RAII guard or an equivalent guaranteed-cleanup construct. Every successful impersonation must have a reliable revert path, including errors and exceptions. A thread returned to an RPC pool while still impersonating the previous caller creates an entirely different class of identity confusion.

### Success does not tell you which level you got

Checking the return value is necessary and not sufficient. Microsoft documents that every impersonating function tests whether the caller holds `SeImpersonatePrivilege`: if the server holds it, or the authenticated identity matches the server's own, full impersonation is allowed. Otherwise the impersonation succeeds at Identification level only.

So `RPC_S_OK` can mean "you are impersonating the client and may act as them," or it can mean "you hold an Identification-level token that can answer questions about the client but cannot open securable objects as them." A service that assumes the first and gets the second will see downstream opens fail in ways that look like a broken DACL rather than a missing privilege.

When the difference matters, ask the token rather than inferring it:

```c
SECURITY_IMPERSONATION_LEVEL level;
DWORD returned = 0;
if (GetTokenInformation(
        clientToken,
        TokenImpersonationLevel,
        &level,
        sizeof(level),
        &returned) &&
    level < SecurityImpersonation) {
    return ERROR_ACCESS_DENIED;
}
```

This privilege is also why the surrounding service context matters to an attacker. `SeImpersonatePrivilege` is held by service accounts, and a foothold in a process that has it is the starting point for a well-known family of local privilege-escalation techniques against exactly this pattern.

## The four impersonation levels

Windows distinguishes four impersonation levels:

| Level | What the server can do |
| --- | --- |
| Anonymous | Cannot obtain the client's identity |
| Identification | Can identify the client and inspect token attributes, but cannot act as the client against securable objects |
| Impersonation | Can act as the client on the local computer |
| Delegation | Can act as the client on remote computers as well as locally, when the authentication system permits it |

For a service that needs to open a local registry key or file as its caller, `SecurityImpersonation` is the relevant level. `SecurityIdentification` can support an explicit policy decision, but it is not sufficient for "let the object manager enforce the target object's DACL by performing the operation as the client."

Delegation is not a stronger default to request casually. It extends where the borrowed identity can be used and depends on the authentication path. A local service operation rarely needs it.

## Impersonation is not the same as authorization

Impersonation answers one question:

> Which token should Windows use when this thread accesses another securable object?

Authorization answers a different question:

> Is this caller allowed to request this service operation?

Sometimes object-level authorization is enough. If an RPC method exists only to read a file, impersonating the caller and opening that file with `GENERIC_READ` may correctly delegate the decision to the file's DACL.

Sometimes it is not enough. Consider a method that asks a service to rotate a system-wide key, stop a security component, or rewrite machine policy. There may be no single target object whose DACL expresses the business rule. The service needs an explicit policy check before using its own authority.

```c
DWORD RpcRotateMachineKey(void) {
    // CLIENT_CONTEXT is the service's own record of the caller: capture the
    // client token with the impersonate/OpenThreadToken/revert sequence above,
    // then keep whatever identity attributes the policy below needs.
    CLIENT_CONTEXT client;
    DWORD error = CaptureClientContext(&client);
    if (error != ERROR_SUCCESS) return error;

    if (!ClientMayRotateMachineKey(&client)) {
        return ERROR_ACCESS_DENIED;
    }

    // Deliberately privileged after authorization. Impersonating here would
    // defeat the service's purpose because the client cannot open the key.
    return RotateMachineKeyAsService();
}
```

This is not a contradiction. The correct service can authenticate the caller, authorize the requested operation, then intentionally use service authority to carry it out.

## Two sound enforcement patterns

### Pattern 1: impersonate and let the object enforce its DACL

Use this when the service is brokering access to an existing securable object and the object's security descriptor is the intended policy source.

```c
DWORD ReadDocumentAsCaller(const wchar_t *path, BYTE **buffer) {
    RPC_STATUS rpcStatus = RpcImpersonateClient(NULL);
    if (rpcStatus != RPC_S_OK) return ERROR_ACCESS_DENIED;

    DWORD result;
    __try {
        HANDLE file = CreateFileW(
            path,
            GENERIC_READ,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL
        );
        result = file == INVALID_HANDLE_VALUE
            ? GetLastError()
            : ReadAndClose(file, buffer);
    }
    __finally {
        RpcRevertToSelf();
    }
    return result;
}
```

The service must still constrain the request. Impersonation does not make path traversal, unsafe reparse-point handling, or confused namespace resolution disappear.

### Pattern 2: capture the client token and perform an explicit access check

Use this when the protected operation has an application-defined security descriptor or policy.

```c
DWORD AuthorizeClient(
    PSECURITY_DESCRIPTOR descriptor,
    DWORD requestedAccess
) {
    RPC_STATUS rpcStatus = RpcImpersonateClient(NULL);
    if (rpcStatus != RPC_S_OK) return ERROR_ACCESS_DENIED;

    HANDLE clientToken = NULL;
    BOOL opened = OpenThreadToken(
        GetCurrentThread(),
        TOKEN_QUERY,
        TRUE,
        &clientToken
    );
    DWORD openError = opened ? ERROR_SUCCESS : GetLastError();
    RpcRevertToSelf();

    if (!opened) return openError;

    GENERIC_MAPPING mapping = GetOperationGenericMapping();
    MapGenericMask(&requestedAccess, &mapping);

    PRIVILEGE_SET privileges = {0};
    DWORD privilegeBytes = sizeof(privileges);
    DWORD granted = 0;
    BOOL allowed = FALSE;

    BOOL checked = AccessCheck(
        descriptor,
        clientToken,
        requestedAccess,
        &mapping,
        &privileges,
        &privilegeBytes,
        &granted,
        &allowed
    );
    CloseHandle(clientToken);

    if (!checked) return GetLastError();
    return allowed ? ERROR_SUCCESS : ERROR_ACCESS_DENIED;
}
```

`AccessCheck` expects an impersonation token. Generic rights such as `GENERIC_READ` must be mapped to the resource's specific rights before evaluation. A multi-right request is evaluated as a unit: either every requested right is granted or the check reports failure. The `granted` mask becomes interesting when the request is `MAXIMUM_ALLOWED`, where it reports which rights the token actually resolved to.

For complex policy engines, the Authz API provides client contexts, callback ACEs, and cached access checks. The underlying design obligation remains the same: evaluate the caller's security context, not the service's.

## Group checks are narrower than access checks

`CheckTokenMembership` answers whether a SID is enabled in a token. It is useful for a policy such as "members of the local Administrators group may perform this action," but it does not evaluate a full security descriptor.

```c
BOOL isAdministrator = FALSE;
CheckTokenMembership(clientToken, administratorsSid, &isAdministrator);
```

Here, `clientToken` is the impersonation token captured with `OpenThreadToken`. When the token argument is non-`NULL`, `CheckTokenMembership` requires an impersonation token; a primary token returned by `OpenProcessToken` is not interchangeable. Passing `NULL` instead asks Windows to use the calling thread's impersonation token, or to derive an impersonation token from its primary token when the thread is not impersonating.

That check ignores allow and deny ACE ordering, object-specific rights, owner behavior, restricted-token intersections, and other rules represented by a DACL. Use a membership test when membership is genuinely the policy. Do not use it as a hand-written substitute for `AccessCheck` when the policy already exists as a security descriptor.

UAC adds another nuance. An administrator running without elevation usually has a filtered token in which administrative SIDs may be deny-only and powerful privileges are removed or disabled. A robust decision evaluates the token the client actually presented, not the account's theoretical group membership.

## A request traced end to end

Consider a service running as LocalSystem with an RPC method that changes a machine setting stored in a registry key writable only by SYSTEM and administrators.

### Incorrect flow

```text
1. Standard user sends RPC request.
2. Service dispatches the request on a worker thread.
3. Worker validates syntax but never examines caller identity.
4. Worker opens the protected key.
5. Thread has no impersonation token.
6. Registry evaluates the open against the LocalSystem process token.
7. Access succeeds.
8. User-controlled change is committed with service authority.
```

The registry DACL worked correctly. It was asked whether LocalSystem could open the key, and the answer was yes. The missing decision belonged in the service.

### Correct flow with explicit authorization

```text
1. Standard user sends RPC request.
2. Service captures the authenticated client token.
3. Service evaluates the operation's security descriptor or policy.
4. Unauthorized caller receives ACCESS_DENIED.
5. Authorized caller passes the gate.
6. Service deliberately reverts to its own identity.
7. Service performs the protected operation as LocalSystem.
8. Service records caller identity and result in its audit event.
```

The privileged step is not hidden. It is separated from, and preceded by, the authorization step.

## Failure patterns worth recognizing

### Local means trusted

`ncalrpc` restricts communication to the same computer. It does not mean the caller is an administrator, a service, or even a desktop application. Locality reduces the transport boundary; it does not supply an authorization policy. The companion tutorial [Windows Local RPC and ALPC](https://stevenfoerster.com/tutorials/windows-local-rpc-and-alpc/) develops that distinction.

### Impersonation failure falls through

```c
RpcImpersonateClient(NULL); // return value ignored
WriteProtectedObject();     // may execute as the service
RpcRevertToSelf();
```

Treat failed impersonation as a failed authorization prerequisite. Stop.

### One branch checks, sibling branches do not

A dispatcher can validate one operation and route adjacent operations directly to privileged workers. Authorization belongs at a common choke point when the policy is common, or at every operation when the policies differ. A correct check in one sibling does not protect the rest. [CVE-2026-69321](https://stevenfoerster.com/notes/cve-2026-69321/) is a worked example: one `umpo.dll` RPC writer impersonated the caller and ran the access check, while six sibling procedures wrote to the same registry hive under the SYSTEM token without it.

### Authorization occurs after the side effect

Checking membership after creating a key, opening a destructive handle, or staging a write is too late. Put the decision before the first privileged side effect.

### The service checks its own token

Calling `OpenProcessToken(GetCurrentProcess(), ...)` inside a privileged service answers questions about the service account. It says nothing about the RPC caller. Client authorization normally starts with the authenticated RPC context or a thread impersonation token.

### The service caches a client decision without its context

An authorization result can depend on token restrictions, groups, privileges, claims, resource attributes, and the requested access mask. Cache only when the cache key captures everything the policy depends on, and invalidate when the policy changes.

## Design checklist

For every privileged service method, write down:

1. Who can bind to or invoke the interface?
2. How is the client authenticated?
3. What operation is being authorized?
4. Which token is evaluated?
5. Is policy expressed by the target object's DACL or by service-specific rules?
6. Does impersonation failure stop the request?
7. Is reversion guaranteed on every exit path?
8. Does any privileged side effect occur before authorization?
9. Are all sibling operations covered?
10. Does the audit record preserve the original caller identity?

If the answer to "which token is evaluated?" is unclear, the design is not finished.

## 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
- [Access tokens](https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens), Microsoft Learn
- [Impersonation tokens](https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-tokens), Microsoft Learn
- [`RpcImpersonateClient`](https://learn.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-rpcimpersonateclient), Microsoft Learn
- [Client impersonation](https://learn.microsoft.com/en-us/windows/win32/rpc/client-impersonation), Microsoft Learn
- [Checking access with the Authz API](https://learn.microsoft.com/en-us/windows/win32/secauthz/checking-access-with-authz-api), Microsoft Learn
