An Overview of MS-RPC and Its Security Mechanisms
What is RPC?
A remote procedure call (RPC) is a form of interprocess communication (IPC). It allows a client to invoke a procedure that is exposed by an RPC server. The client calls the function as if it were a normal procedure call, without (almost) any need to code the details for the remote interaction. The server can be hosted in a different process on the same machine or on a remote machine.
In this article, we will look into Microsoft’s implementation of RPC (MS-RPC). MS-RPC is derived from the reference implementation (V1.1) of the RPC protocol at the core of the Distributed Computing Environment.
RPC is heavily used by Windows for many different services, such as task scheduling, service creation, printer and share settings, and the management of encrypted data stored remotely. Because of the nature of RPC as a remote vector, it gathers a lot of attention from a security point of view. This blog post will try to cover the basics of how MS-RPC works, with some focus on its incorporated security mechanisms.
How does MS-RPC work?
The procedures and their parameters are defined in a descriptive language called Interface Definition Language (IDL). Each interface is assigned a unique identifier, called a UUID, which is used by both server and client to address the exact interface that the server exposes.
The IDL file is then compiled using the Microsoft IDL compiler into header and source code files that contain runtime functionality. Technically speaking, those are stubs that are used by both the server and the client, and they pass control to the RPC runtime, which is implemented in rpcrt4.dll. The RPC runtime on the client side marshals the data and passes it to the RPC runtime on the other side (Figure 1).
Fig. 1: RPC runtime
You may wonder how the server and client communicate from a network (or local) point of view. The server listens for incoming RPC connections by registering a combination of a protocol sequence and an endpoint. A protocol sequence can be, for example, ncacn_ip_tcp (TCP), ncacn_np (named pipe), or ncalrpc (LPC). The endpoint can be a port such as TCP 5555 or \\pipe\\example, if a named pipe is used. Named pipes are carried over the SMB transport over TCP port 445 using the hidden IPC$ share. The full list of protocol sequences is available on the Microsoft website.
So, the server listens for connections on some endpoint. How does the client know where to connect? The answer depends on the type of endpoint it is — dynamic or well-known (aka static).
A dynamic endpoint is an endpoint registered through the endpoint mapper on the server side. The endpoint mapper (aka the epmapper) is an RPC service that maps a service to the actual endpoint. The epmapper uses TCP ports 135 and 593 for RPC over HTTP. Therefore, a client can enumerate (using designated APIs) all dynamically registered RPC servers on a remote machine using the epmapper.
A well-known endpoint is one that did not register through the epmapper. The client should know the endpoint that the server registered in advance. This can be done if the endpoint is hardcoded in the code of both client and server, or if the endpoint is present in the IDL file. Here’s an example from print spooler service IDL.
[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]
RPC represents a logical connection between a client and server through a binding handle. Both server and client use functions to manipulate the binding data — for example, when setting authentication info. When the client sets authentication on the binding, the binding is considered authenticated. When the client program calls the RPC function, the network binding happens. From a network traffic perspective, the RPC client begins the RPC interaction by sending a bind request with authentication info in the packet. The server can respond with bind_ack (acknowledged) or bind_nak (an error occurred).
Fig. 2: Wireshark snippet showing a dynamic endpoint resolution
In the Wireshark snippet in Figure 2, we can see the dynamic endpoint resolution for the Task Scheduler interface. Once the client has the endpoint information for Task Scheduler, it creates a new connection. We can then see a new connection with another binding process, including an AUTH3 packet as part of the binding authentication.
Like endpoints, bindings have types: automatic, implicit, and explicit (Figure 3). They differ in the amount of control the application has over the binding process.
- Automatic binding (obsolete) — The client and server applications do not handle the binding process and instead let the RPC runtime fully control this process.
- Implicit binding — The client has the option to configure the binding handle before the binding takes place. After the client establishes a binding, the RPC runtime library handles the rest.
- Explicit binding — The client needs to configure the binding handle. Then the RPC runtime just passes it to the server.
RPC security for remote requests
Now that we know the basics of how RPC works, we want to understand what actions, policies, and mechanisms can block a client from accessing a function. This is interesting both from an offensive and defensive perspective.
Transport authentication
Some transports, such as named pipes or HTTP, have authentication as part of their protocol. For example, named pipes are carried over SMB, which has authentication. This basically means that when a server registers a named pipe endpoint, only clients with the credentials of a valid user can connect to that endpoint. In a domain environment, having a domain user in the same domain is enough to pass the authentication check. If the machines are not part of a domain, then the client would need to have the credentials of a local user on the remote server (Figure 4). We’ll address exclusions later in this post.
Fig. 4: An example of a system error that denies access
Binding authentication
The server can have an authentication mechanism by using binding authentication. For this to happen, the server needs to register authentication info by calling RpcServerRegisterAuthInfo. The client must use the correct service principal name and Security Support Provider method that the server uses, otherwise it will receive “RPC_S_UNKNOWN_AUTHN_SERVICE” from the server.
A client can authenticate to a server by setting authentication and authorization data on the binding using APIs such as RpcBindingSetAuthInfo and RpcBindingSetAuthInfoEx. The client specifies the authentication method (e.g., NTLM, Kerberos, Negotiate, SCHANNEL, etc.) and the service principal name.
Two important things to know:
If a client sets authentication on the binding, and the server has not registered any auth info, the server will return “RPC_S_UNKNOWN_AUTHN_SERVICE”.
Just because the server registered an authentication binding, does not mean that the client must use an authenticated binding. Moreover, the RPC runtime will not dispatch client authentication binding with invalid credentials. Later in this post, we will address how the server can prevent access to unauthenticated bindings.
It’s worth noting that the RPC runtime saves the auth info globally. This means that if two RPC servers share the same process, and one of them registers authentication info, the other server will also have authentication info. A client can now authenticate the binding when accessing each one of the servers. We call this behavior SSPI multiplexing.
Endpoint security
A server can set a security descriptor on the endpoint. The security descriptor is a general access check security mechanism in Windows. It permits the creation of “rules” for who is allowed to access an object and who is denied access. When access is attempted to the object, the operating system is going to compare the access token of the caller with the security descriptor to see if access is allowed. In this case, the object is the endpoint, and the access token is the client’s access token derived from the transport protocol authentication. This check is only applicable to authenticated transports like named pipes, ALPC, and HTTP (when authentication is used). TCP is an unauthenticated transport protocol and, therefore, would not have this access check.
Similar to the SSPI multiplexing, endpoints are also multiplexed — the interface and endpoint are not bound. A server registers interfaces and endpoints separately. If a process has multiple registered endpoints, then each interface registered in this process can be accessed through each one of these endpoints.
Imagine, for example, that you register your interface and create an endpoint that listens on \\pipe\mypipe. Another RPC server hosted in the same process registered its own endpoint at TCP 7777. Your interface will be accessible through TCP as well. This will bypass security restrictions imposed on the endpoint, such as a security descriptor (Figure 5). Therefore, it’s recommended not to rely on endpoint security or to verify that the client came through the expected transport protocol.
Fig. 5: Example of bypassing endpoint security descriptor because of endpoint multiplexing
Interface security
When looking at interface security, there are three ways to secure an interface: setting a security callback, setting a security descriptor on the interface, and using interface flags.
Setting a security callback
The first interface security mechanism is a security callback. A security callback is a custom callback implemented by the server developer. The logic inside the security callback is up to the developer, and it allows the server to restrict access to the interface. If the callback returns RPC_S_OK, then the client is allowed.
If a security callback is registered, then unauthenticated clients will be automatically denied by the RPC runtime unless the flag RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH is set.
Setting a security callback and setting the RPC_IF_ALLOW_SECURE_ONLY flag does not mean that the client has high privileges. Therefore, the security callback is the place for the server to inquire regarding the client privilege level. This is done by calling RpcBindingInqAuthClient. Another common check is that of the transportation method used by the client to enforce connection through secure authenticated transportation, such as named pipes. This is done by calling RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse and comparing the Protseq (protocol sequence) parameter. This also prevents endpoint multiplexing abuse.
Security callback caching
If a security callback is registered, then it will be called during the security checks. If the security callback passes (i.e., returns RPC_S_OK), then the result will be cached, if caching is used. The next time the same client calls a function on the interface, and since the security callback result is cached, the security callback won’t be invoked again and the cached entry will be used instead.
The implementation of caching is simple but depends on a few factors.
- The caching is bound to the client’s security context, which is taken from the binding. Therefore, if the server (or any other server in the process) did not register any authentication binding, or the client did not set authentication binding, caching would be disabled for this call.
- If the server registers the interface with the flag RPC_IF_SEC_NO_CACHE, the RPC runtime forces the calling of the security callback for each call, thus disabling the caching mechanism.
- The undocumented interface flag RPC_IF_SEC_CACHE_PER_PROC also affects the caching mechanism. If the server specified this flag, then the caching will be on a call basis and not on an interface basis. That means that if the cache holds a successful value for function X on interface TEST, then a call to function Y on interface TEST will trigger the security callback again.
The caching mechanism might cause logic vulnerabilities for any security callback that is dependent on the function that the client calls. As an RPC developer or an auditor, you should be aware of the caching mechanism. We have published thorough research on caching implementation, including vulnerabilities we found because of this mechanism.
Setting a security descriptor
The second mechanism for securing an interface is setting a security descriptor on the interface. Only the function RpcServerRegisterIf3 has the option to register a security descriptor to the interface. If a security descriptor is not set, the default security descriptor is:
- NT AUTHORITY\ANONYMOUS LOGON
- Everyone
- NT AUTHORITY\RESTRICTED
- BUILTIN\Administrators
- SELF
Using the interface flags
The third way to secure an interface is to use the interface flags, which are set when creating the interface through RpcServerRegisterIf* functions. Interesting flags from a security perspective are:
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH — The security callback will be called for each invocation, no matter the transportation method used or the authentication level.
RPC_IF_ALLOW_LOCAL_ONLY — Only local requests will be allowed.
RPC_IF_ALLOW_SECURE_ONLY — Connections are limited to clients with an authorization level higher than RPC_C_AUTHN_LEVEL_NONE. Specifying this flag denies clients that come through the NULL session. This flag does not guarantee the privilege level of a calling user, it only guarantees that the client has valid credentials.
RPC_IF_SEC_NO_CACHE — This flag will completely disable caching for the interface’s security callback.
RPC_IF_SEC_CACHE_PER_PROC — Instead of disabling the whole caching mechanism, this flag will change the default behavior to be on a call basis instead of an interface basis.
Systemwide policies
There are multiple systemwide policies that are set depending on the machine type: client, server, or domain controller (DC).
An interesting system policy related to endpoint security is “Restrict Unauthenticated RPC Clients policy.” Three values can be set.
- “Authenticated” — The RPC runtime will block access to TCP clients that have not authenticated in advance if the RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH flag is not set on the interface.
- “Authenticated without exceptions” — All unauthenticated connections are blocked.
- "None" — All RPC clients are allowed to connect to RPC servers running on the machine.
Thus, an interesting attack surface for unauthenticated clients would be interfaces that are accessible through TCP and register the RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH flag. Obviously, the client will need to pass the security callback check to invoke the interfaces’ functions.
As was mentioned earlier, connections through named pipes, which are carried over SMB, have their own authentication as part of the SMB protocol. A client can use an “Anonymous login” (also known as a NULL session) when connecting through SMB. Two related system policies are “Restrict anonymous access to Named Pipes and Shares” and “Network access: Named Pipes that can be accessed anonymously”. If the first is enabled, then only named pipes that are defined in the second can be connected anonymously. For workstations, the second policy is empty, which basically means you cannot use a NULL session against workstations in the domain. For a DC machine, the list of named pipes in the policy includes “\pipe\netlogon”, “\pipe\samr”, and “\pipe\lsarpc”. This is interesting from an attacker's point of view as those are endpoints to which a machine from outside the domain can connect.
Finally, with regard to the endpoint security descriptor check, there’s a systemwide policy that is disabled by default: “Network access: Let Everyone permissions apply to anonymous users.” When enabled, the everyone security identifier is added to the token created for anonymous connections.
Procedure security checks
The server programmer can implement security checks as part of the functions that are exposed on the interface, and, therefore, has the option to change or customize the logic of the check per the function that is called. A common security check is an access check, such as the one seen in Figure 6 (taken from srvsvc):
Fig. 6: An example of a common security check, the access check
In this example, the LocalrSessionGetInfo calls SsAccessCheck. SsAccessCheck basically impersonates the client by calling RpcImpersonateClient, followed by a call to the function NtAccessCheckAndAuditAlarm, which performs the access check. This is followed by a RpcRevertToSelf call to revert to the server’s token. It’s important to remember to check the return value of RpcImpersonateClient since if it fails, the server continues to run in the security token of the server’s process and not the client’s process.
Summary
This blog post has presented a lot of information about MS-RPC and its security mechanisms. We encourage other researchers to look into different MS-RPC servers as they present a large attack surface with the potential for new vulnerabilities. We hope our post will help other researchers take their first steps in researching MS-RPC.
We are continuing to create and collate more resources on RPC for our GitHub repository. Thank you to everyone who has contributed their knowledge and experience to this topic, especially James Forshaw and Carsten Sandker.