MS-RPC 及其安全机制概述
什么是 RPC?
远程过程调用 (RPC) 是进程间通信 (IPC) 的一种形式。它允许客户端调用由 RPC 服务器公开的过程。客户端可以像执行普通过程调用一样来调用此函数,几乎不需要为远程交互的细节编码。服务器可以托管在同一台机器的不同进程中,也可以托管在远程机器上。
在这篇文章中,我们将研究 Microsoft 的 RPC (MS-RPC) 实施过程。MS-RPC 由处于分布式计算环境核心的 RPC 协议的参考实施 (V1.1) 衍生而来。
Windows 将 RPC 广泛用于许多不同的服务,比如任务调度、服务创建、打印机和共享设置,以及远程存储的加密数据的管理。由于 RPC 的远程媒介性质,从安全的角度来看,它吸引了很多人的注意。本博文将尝试介绍 MS-RPC 的基本工作原理,同时着重探究其中融入的安全机制。
MS-RPC 的工作原理是什么?
使用一种称为“接口定义语言 (IDL)”的描述性语言来定义过程及其参数。每个接口都被分配了一个唯一标识符(称为 UUID),服务器和客户端都使用它来处理服务器公开的确切接口。
然后,使用 Microsoft 的 IDL 编译器将 IDL 文件编译成标头文件和源代码文件,其中包含运行时功能。从技术上讲,这些是服务器和客户端都会使用的存根,它们将控制权传递给 RPC 运行时(在 rpcrt4.dll 中实施)。客户端的 RPC 运行时对数据进行整理并传递给另一端的 RPC 运行时(图 1)。
图 1:RPC 运行时
您可能想知道,从网络(或本地)的角度来看,服务器和客户端是如何通信的。服务器通过注册一个协议序列和一个端点的组合来监听传入的 RPC 连接。例如,协议序列可以是 ncacn_ip_tcp (TCP)、 ncacn_np (指定的管道),或 ncalrpc (LPC)。端点可以是一个端口,比如 TCP 5555 或 \\pipe\\example(如果使用了指定的管道)。通过 TCP 445 端口上的 SMB 传输携带指定的管道(使用隐藏的 IPC$ 共享)。协议序列的完整列表可 在 Microsoft 网站上找到。
因此,服务器在某个端点上监听连接。客户端如何知道在哪里连接?答案取决于它是哪种类型的端点——是动态的,还是已知的(又称静态的)。
动态端点是在服务器端通过端点映射器注册的端点。端点映射器(又称 epmapper)是一个 RPC 服务,它将一个服务映射到实际的端点。epmapper 通过 HTTP 使用 TCP 端口 135 和 593 进行 RPC。因此,客户端可以使用 epmapper 枚举(使用 指定的 API)远程机器上所有动态注册的 RPC 服务器。
已知的端点是没有通过 epmapper 注册的端点。客户端应该事先知道服务器注册的端点。如果端点在客户端和服务器的代码中都是硬编码的,或者端点存在于 IDL 文件中,就可以做到这一点。下面是一个来自 print spooler 服务 IDL 的示例。
[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]
RPC 使用一个绑定句柄来表示客户端与服务器之间的逻辑连接。服务器和客户端都使用函数来操作绑定数据,例如,在设置身份验证信息时。当客户端在绑定上设置身份验证时,绑定被认为已经过身份验证。当客户端程序调用 RPC 函数时,就会发生网络绑定。从网络流量的角度来看,RPC 客户端通过在数据包中发送一个带有身份验证信息的绑定请求开始 RPC 交互。服务器可以回应 bind_ack(确认)或 bind_nak(发生错误)。
图 2:显示动态端点解析的 Wireshark 代码段
在图 2 的 Wireshark 代码段中,我们可以看到任务计划程序接口的动态端点解析。一旦客户端有了任务计划程序的端点信息,它就会创建一个新连接。然后我们可以看到与另一个绑定进程的新连接,同时在绑定身份验证中包含一个 AUTH3 数据包。
像端点一样,绑定也有自动、隐式和显式三种类型(图 3)。它们的不同之处在于应用程序对绑定进程的控制程度。
- 自动绑定(废弃)——客户端和服务器应用程序不处理绑定过程,而是让 RPC 运行时完全控制此过程。
- 隐式绑定——客户端可以选择在绑定发生之前配置绑定句柄。在客户端建立绑定后,RPC 运行时库会处理其余的任务。
- 显式绑定——客户端需要配置绑定句柄。然后,RPC 运行时只是把它传递给服务器。
远程请求的 RPC 安全性
现在我们知道了 RPC 的基本工作原理,下面来了解哪些行为、策略和机制可以阻止客户端访问函数。从进攻和防守的角度来探讨这一点会非常有趣。
传输身份验证
有些传输(比如指定的管道或 HTTP)将身份验证作为其协议的一部分。例如,指定的管道通过 SMB 携带,SMB 具有身份验证功能。这基本上意味着,当服务器注册一个指定的管道端点时,只有拥有有效用户凭据的客户端才能连接到该端点。在域环境中,在同一域中拥有一个域用户就足以通过身份验证检查。如果机器不属于同一个域,则客户端需要在远程服务器上具有本地用户凭据(图 4)。我们将在本文稍后介绍排除事项。
图 4:拒绝访问的系统错误的示例
绑定身份验证
服务器可以使用绑定身份验证来建立身份验证机制。要做到这一点,服务器需要调用 RpcServerRegisterAuthInfo 来注册身份验证信息。客户端必须正确的服务主体名称和“安全支持提供商”方法,并与服务器保持一致,否则将从服务器收到“RPC_S_UNKNOWN_AUTHN_SERVICE”。
客户端可以通过使用 RpcBindingSetAuthInfo 和 RpcBindingSetAuthInfoEx 等 API 在绑定上设置身份验证和授权数据,以向服务器进行身份验证。客户端指定身份验证方法(比如 NTLM、Kerberos、Negotiate、SCHANNEL 等)和服务主体名称。
请务必注意两点:
如果客户端在绑定上设置了身份验证,而服务器没有注册任何身份验证信息,服务器将返回“RPC_S_UNKNOWN_AUTHN_SERVICE”。
仅仅因为服务器注册了身份验证绑定,并不意味着客户端必须使用身份验证绑定。此外,RPC 运行时不会调度具有无效凭据的客户端身份验证绑定。在本文的后面,我们将讨论服务器如何防止对未经身份验证的绑定的访问。
值得注意的是,RPC 运行时在全局范围内保存了身份验证信息。这意味着,如果两个 RPC 服务器共享同一个进程,并且其中一个注册了身份验证信息,则另一个服务器也会具有身份验证信息。客户端现在可以在访问每一个服务器的时候对绑定进行身份验证。我们把这种行为称为 SSPI 多路复用。
端点安全性
服务器可以在端点上设置一个安全描述符。安全描述符是 Windows 中一个通用的访问检查安全机制。它允许创建“规则”,规定谁可以访问对象、谁不能访问对象。当试图访问该对象时,操作系统会将调用者的访问令牌与安全描述符进行比较,看看是否允许访问。在这种情况下,对象是端点,而访问令牌是客户的访问令牌(派生自传输协议身份验证)。此检查只适用于经过身份验证的传输,比如指定的管道、ALPC 和 HTTP(当使用身份验证时)。TCP 是一个未经身份验证的传输协议,因此,不会有这种访问检查。
与 SSPI 多路复用类似,端点也会多路复用——接口和端点不绑定。服务器分别注册接口和端点。 如果一个进程有多个注册的端点,那么在这个进程中注册的每个接口都可以通过这些端点中的每一个进行访问。
例如,想象一下,您注册了接口并创建了一个端点,通过该端点来监听 \\pipe\mypipe。另一个托管在同一进程中的 RPC 服务器在 TCP 7777 注册了自己的端点。您的接口也将通过 TCP 进行访问。这将绕过对端点施加的安全限制,比如安全描述符(图 5)。因此,建议不要依赖端点安全或验证客户端是否通过预期的传输协议。
图 5:由于端点多路复用而绕过端点安全描述符的示例
接口安全性
在探讨接口安全性时,有三种方法可以保证接口的安全:设置安全回调、在接口上设置安全描述符以及使用接口标志。
设置安全回调
第一个接口安全机制是安全回调。安全回调是一个由服务器开发人员实施的自定义回调。安全回调内部的逻辑由开发人员决定,它允许服务器限制对接口的访问。如果回调返回 RPC_S_OK,则允许客户端访问。
如果注册了安全回调,则未经身份验证的客户端将被 RPC 运行时自动拒绝,除非设置了标志 RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH。
设置安全回调并设置 RPC_IF_ALLOW_SECURE_ONLY 标志并不意味着客户端具有高权限。因此,安全回调是服务器查询客户端权限级别的地方。通过调用 RpcBindingInqAuthClient 完成此操作。另一个常见的检查是客户端使用的传输方法中的检查,客户端通过经过身份验证的安全传输方式(比如指定的管道)来执行连接。通过调用 RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse 并比较 Protseq(协议序列)参数来完成此操作。这也可以防止滥用端点多路复用。
安全回调缓存
如果注册了一个安全回调,那么它将在安全检查期间被调用。如果安全回调通过(即返回 RPC_S_OK),那么,如果使用了缓存,结果将被缓存。下次同一个客户端在接口上调用函数时,由于安全回调的结果已缓存,安全回调不会被再次调用,而会使用缓存的条目。
缓存的实施很简单,但取决于一些因素。
- 缓存与客户端的安全上下文绑定,该上下文取自绑定。因此,如果服务器(或进程中的任何其他服务器)没有注册任何身份验证绑定,或者客户端没有设置身份验证绑定,则该调用的缓存将被禁用。
- 如果服务器使用标志 RPC_IF_SEC_NO_CACHE 来注册接口,RPC 运行时会强制为每次调用调用安全回调,从而禁用缓存机制。
- 未记录的接口标志 RPC_IF_SEC_CACHE_PER_PROC 也会影响缓存机制。如果服务器指定了这个标志,那么缓存将以 调用 为基础,而不是以 接口 为基础。这意味着,如果缓存保存了接口 TEST 上的函数 X 的成功值,则对接口 TEST 上的函数 Y 的调用将再次触发安全回调。
缓存机制可能会导致任何依赖于客户端调用的函数的安全回调出现逻辑漏洞。作为 RPC 开发人员或审核人员,您应该意识到缓存机制。 我们已经发布了关于缓存实施的全面研究,包括我们发现的该机制导致的漏洞。
设置安全描述符
确保接口安全的第二个机制是在接口上设置一个安全描述符。只有函数 RpcServerRegisterIf3 可以选择向接口注册一个安全描述符。如果没有设置安全描述符, 默认的安全描述符是:
- NT AUTHORITY\ANONYMOUS LOGON
- Everyone
- NT AUTHORITY\RESTRICTED
- BUILTIN\Administrators
- SELF
使用接口标志
确保接口安全的第三种方法是使用接口标志,通过 RpcServerRegisterIf* 函数创建接口时设置这些标志。从安全的角度来看,重要标志包括:
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH——每次调用都会调用安全回调(无论使用的传输方式或身份验证级别如何)。
RPC_IF_ALLOW_LOCAL_ONLY——只允许本地请求。
RPC_IF_ALLOW_SECURE_ONLY——连接仅限于授权级别高于 RPC_C_AUTHN_LEVEL_NONE 的客户端。指定此标志会拒绝使用 NULL 会话的客户端。此标志并不保证调用用户的权限级别,它只保证客户端具有有效的证书。
RPC_IF_SEC_NO_CACHE——此标志将完全禁用接口的安全回调的缓存。
RPC_IF_SEC_CACHE_PER_PROC——此标志不会禁用整个缓存机制,而是将默认行为改为基于调用,而不是基于接口。
全系统策略
有多个系统范围内的策略,这些策略的设置取决于机器类型:客户端、服务器或域控制器 (DC)。
与端点安全有关的一个有趣系统策略是“限制未经身份验证的 RPC 客户端”策略。可以设置三个值。
- “已经过身份验证”——如果接口上没有设置 RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH标志,RPC 运行时将阻止对没有事先进行身份验证的 TCP 客户端的访问。
- “无例外的身份验证”——所有未经身份验证的连接都会被阻止。
- 无——允许所有 RPC 客户端连接到机器上运行的 RPC 服务器。
因此,对于未经身份验证的客户端来说,一个有趣的攻击面是可通过 TCP 访问并注册 RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH 标志的接口。很明显,客户端需要通过安全回调检查,才能调用接口的函数。
正如前面提到的,通过 SMB 携带的指定管道进行的连接具有自己的身份验证,以作为 SMB 协议的一部分。通过 SMB 连接时,客户端可以使用“匿名登录”(也被称为 NULL 会话)。两个相关的系统策略是“限制对指定管道和共享的匿名访问”和“网络访问:可以匿名访问的指定管道”。如果启用第一个策略,那么只有在第二个策略中定义的指定管道可以被匿名连接。对于工作站,第二个策略是空的,这本质上意味着,您不能对域中的工作站使用 NULL 会话。对于 DC 机器,策略中的指定管道列表包括“\pipe\netlogon”、“\pipe\samr”和“\pipe\lsarpc”。从攻击者的角度来看,这很有意思,因为这些是域外机器可以连接的端点。
最后,关于端点安全描述符检查,有一个默认禁用的全系统策略:“网络访问:让‘所有人’权限适用于匿名用户。”启用后,everyone 安全标识符被添加到为匿名连接创建的令牌中。
过程安全检查
服务器程序员可以将安全检查作为接口上公开的函数的一部分进行实施,因此,可以选择更改或自定义每个调用的函数的检查逻辑。一个常见的安全检查是访问检查,如图 6(取自 srvsvc)所示:
图 6:常见的安全检查(访问检查)的示例
在此示例中,LocalrSessionGetInfo 调用了 SsAccessCheck。 SsAccessCheck 本质上是通过调用 RpcImpersonateClient来冒充客户端,然后再调用函数 NtAccessCheckAndAuditAlarm,以执行访问检查。接下来是调用 RpcRevertToSelf,以恢复到服务器的令牌。必须记得检查 RpcImpersonateClient 的返回值,这是因为,如果它失败了,服务器就会继续在服务器进程(而不是客户端的进程)的安全令牌中运行。
总结
本博文介绍了很多关于 MS-RPC 及其安全机制的信息。我们鼓励其他研究人员研究不同的 MS-RPC 服务器,因为它们呈现出一个巨大的攻击面,有可能出现新的漏洞。我们希望我们的文章能够帮助其他研究人员在研究 MS-RPC 方面迈出第一步。
我们正在继续为我们的 GitHub 存储库创建和整理更多关于 RPC 的资源。感谢所有为此主题贡献知识和经验的人,尤其是 James Forshaw 和 Carsten Sandker。