原文链接:
https://csandker.io/2021/02/21/Offensive-Windows-IPC-2-RPC.html
该系列
这是我的系列文章的第 2 部分:Offensive Windows IPC Internals。
如果您错过了第一部分并想看一看,可以在此处找到它:Offensive Windows IPC Internals 1:Named Pipes。
第2部分最初计划是关于LPC和ALPC的,但事实证明,挖掘出关于这些技术的所有未记录的比特和技巧是相当耗时的。因此,我首先讨论了如何发表我对RPC的知识,然后再将头转向ALPC。
我最初计划在RPC之前发布LPC和ALPC的原因是,RPC在本地使用时在后台使用ALPC,甚至更多:RPC是快速本地进程间通信的预期解决方案,因为RPC可以被指示通过特殊的ALPC协议序列处理本地通信(但是您在阅读时会发现这一点)。
无论如何,这里的教训是(我猜)有时最好在一件事上停下来,让你的头脑清醒,并在你迷失在尚未准备好向你揭示其奥秘的事情中之前,在其他事情上取得进展。
喝杯咖啡和一把舒适的椅子,然后系好安全带进行RPC…
介绍
Remote Procedure Calls (RPC) 是一种跨进程和机器边界(网络通信)实现客户端和服务器之间数据通信的技术。因此,RPC 是一种进程间通信 (IPC) 技术。此类别中的其他技术包括 LPC、ALPC 或命名管道。
顾名思义,此类别意味着 RPC 用于调用远程服务器以交换/传递数据或触发远程例程。在这种情况下,术语“远程”并不描述通信的要求。RPC 服务器不必位于远程计算机上,理论上甚至不必位于不同的进程中(尽管这是有道理的)。
从理论上讲,您可以在 DLL 中实现 RPC 服务器和客户端,将它们加载到相同的进程中并交换消息,但您不会获得太多收益,因为消息仍将通过进程外部的其他组件(例如内核,但稍后会详细介绍)进行路由,并且您将尝试使用“内部”进程通信技术进行“内部”进程通信。
此外,RPC 服务器不需要位于远程计算机上,但也可以从本地客户端调用。
在这篇博客文章中,您可以和我一起发现RPC的内部,它是如何工作的和操作,以及如何实现和攻击RPC客户端和服务器。
这篇文章是从进攻的角度出发的,试图从攻击者的角度涵盖RPC攻击面最相关的方面。例如,关于RPC的更具防御性的观点可以在Jonathan Johnson的 https://ipc-research.readthedocs.io/en/latest/subpages/RPC.html 中找到。
下面的帖子将包含对我的示例实现中的代码的一些引用,所有这些代码都可以在这里找到:
https://github.com/csandker/InterProcessCommunication-Samples/tree/master/RPC/CPP-RPC-Client-Server
历史
微软的RPC实现基于开放软件基金会(OSF)于1993年开发的分布式计算环境(DCE)标准的RPC实现。
“为DCE实施做出贡献的关键公司之一是Apollo Computer,它引入了NCA – ‘网络计算架构’,后来成为网络计算系统(NCS),然后是DCE / RPC本身的主要部分”
来源:https://kganugapati.wordpress.com/tag/msrpc/
微软聘请了Paul Leach(1991年),他是Apollo的创始工程师之一,这可能是RPC进入Windows的方式。
微软调整了DCE模型以适应他们的编程方案,基于命名管道上的RPC通信,并在Windows 95中将其实现带到了日光之下。
回到过去,你可能想知道为什么他们基于命名管道进行通信,因为微软在1994年刚刚提出了一种名为本地过程调用(LPC)的新技术,听起来将一种称为远程过程调用的技术建立在称为本地过程调用的东西上是有意义的,对吧?…好吧,是的,LPC将是合乎逻辑的选择(我猜他们最初使用LPC),但是LPC有一个关键的缺陷:它不支持(并且仍然不支持)异步调用(当我最终完成我的LPC / ALPC帖子时,会对此进行更多介绍…),这就是为什么Microsoft基于Name Pipes的原因。
正如我们稍后将看到的(RPC 协议序列部分),在使用 RPC 实现例程时,开发人员需要告诉 RPC 库使用什么“协议”进行传输。最初的DCE / RCP标准已经为TCP和UDP连接定义了“ncacn_ip_tcp”和“ncadg_ip_udp”。微软添加了“ncacn_np”,用于基于命名管道(通过SMB协议传输)的实现。
RPC 消息传递
RPC 是一种客户端-服务器技术,其消息传递体系结构类似于 COM(组件对象模型),在较高级别上由以下三个组件组成:
- 负责注册 RPC 接口和关联绑定信息的服务器和客户端进程(稍后将对此进行详细介绍)
- 负责编组传入和传出数据的服务器和客户端存根
- 服务器和客户端的 RPC 运行时库 (rpcrt4.dll),它获取存根数据并使用指定的协议通过网络发送它们(示例和详细信息将遵循)
可以在 https://docs.microsoft.com/en-us/windows/win32/rpc/how-rpc-works 中找到此消息体系结构的可视化概述,如下所示:
稍后,在 RPC 通信流部分中,我将概述从创建 RPC 服务器到发送消息所涉及的步骤,但在深入研究之前,我们需要澄清一些 RPC 术语位。
在这里裸露着我,而我们深入研究RPC的内部。为了与RPC相处,以下事项至关重要。
如果您迷失在新术语和API调用中,而您无法排队,您可以随时跳到RPC通信流部分,以了解这些东西在通信链中的位置。
RPC 协议序列
RPC 协议序列是一个常量字符串,用于定义 RPC 运行时应使用哪个协议来传输消息。
此字符串定义应使用哪个 RPC 协议、传输和网络协议。
微软支持以下三种RPC协议:
- 网络计算体系结构面向连接的协议 (NCACN)
- 网络计算架构数据报协议 (NCADG)
- 网络计算体系结构本地远程过程调用 (NCALRPC)
在跨系统边界建立连接的大多数情况下,您会发现 NCACN,而建议将 NCALRPC 用于本地 RPC 通信。
协议序列是从上述部分组装而成的已定义常量字符串,例如,ncacn_ip_tcp用于基于 TCP 数据包的面向连接的通信。
RPC 协议序列常量的完整列表可在以下位置找到:https://docs.microsoft.com/en-us/windows/win32/rpc/protocol-sequence-constants。
最相关的协议序列如下所示:
常量/值 | 描述 |
---|---|
ncacn_ip_tcp | 面向连接的传输控制协议/互联网协议 (TCP/IP) |
ncacn_http | 面向连接的 TCP/IP 使用 Microsoft Internet Information Server 作为 HTTP 代理 |
ncacn_np | 面向连接的命名管道(通过 SMB)。 |
ncadg_ip_udp | 数据报(无连接)用户数据报协议/互联网协议 (UDP/IP) |
ncalrpc | 本地过程调用(通过 ALPC 发布 Windows Vista) |
RPC 接口
为了建立通信通道,RPC 运行时需要知道哪些方法(也称为.“函数”)和服务器提供的参数以及客户端正在发送的数据。这些信息在所谓的“接口”中定义。
附注:如果您熟悉COM中的接口,这是一回事。
为了了解如何定义接口,让我们从我的示例代码中获取此示例:
接口1.idl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[ // UUID: A unique identifier that distinguishes this // interface from other interfaces. uuid(9510b60a-2eac-43fc-8077-aaefbdf3752b), // This is version 1.0 of this interface. version(1.0), // Using an implicit handle here named hImplicitBinding: implicit_handle(handle_t hImplicitBinding) ] interface Example1 // The interface is named Example1 { // A function that takes a zero-terminated string. int Output( [in, string] const char* pszOutput); void Shutdown(); } |
首先要注意的是,接口是在接口定义语言 (IDL) 文件中定义的。稍后,Microsoft IDL 编译器 (midl.exe) 将编译为可供服务器和客户端使用的标头和源代码文件。
接口头对给定的注释是相当不言自明的 – 现在忽略implicit_handle指令,我们很快就会进入隐式和显式句柄。
接口的主体描述此接口公开的方法、其返回值及其参数。Output 函数的参数定义中的语句不是必需的,但有助于理解此参数的用途。[in, string]
附注:您还可以在应用程序配置文件 (ACF) 中指定各种接口属性。其中一些(如绑定类型(显式与隐式))可以放在 IDL 文件中,但对于更复杂的接口,您可能希望为每个接口添加一个额外的 ACF 文件。
RPC 绑定
一旦您的客户端连接到RPC服务器(我们稍后将介绍如何完成此操作),您就会创建Microsoft所谓的“绑定”。或者用微软的话来说:
绑定是在客户端程序和服务器程序之间创建逻辑连接的过程。组成客户端和服务器之间绑定的信息由称为绑定句柄的结构表示。
一旦我们添加了一些上下文,绑定句柄的术语就会变得更加清晰。从技术上讲,有三种类型的绑定句柄:
- 含蓄
- 明确
- 自动
旁注:您可以实现自定义绑定句柄,如此处所述,但我们在本文中忽略了这一点,因为这种情况并不常见,并且您对默认类型感到满意。
隐式绑定句柄允许客户端连接到特定的 RPC 服务器并与之通信(由 IDL 文件中的 UUID 指定)。缺点是隐式绑定不是线程安全的,因此多线程应用程序应使用显式绑定。隐式绑定句柄在 IDL 文件中定义,如上面的示例 IDL 代码或我的示例隐式接口中所示。
显式绑定句柄允许客户端连接到多个 RPC 服务器并与之通信。建议使用显式绑定句柄,因为它是线程安全的,并且允许多个连接。显式绑定句柄定义的示例可以在此处的代码中找到。
对于懒惰的开发人员来说,自动绑定是介于两者之间的解决方案,他们不想摆弄绑定句柄并让 RPC 运行时找出所需的内容。我的建议是使用显式句柄只是为了知道你在做什么。
为什么我需要绑定句柄,首先你可能会问。
想象一下,绑定句柄表示客户端和服务器之间的通信通道,就像罐头电话中的电线一样(我想知道有多少人知道这些“设备”…)。假设你有一个通信香奈儿(“线”)的表示,你可以为这个通信渠道添加属性,比如画你的线,使它更加独特。
就像绑定句柄允许您保护客户端和服务器之间的连接一样(因为您获得了可以向其添加安全性的东西),从而形成Microsoft术语“经过身份验证”的绑定。
匿名和认证绑定
假设您正在运行一个简单明了的 RPC 服务器,现在客户端连接到您的服务器。如果您没有指定任何期望的最低限度(我很快就会列出),客户端和服务器之间的这种连接被称为匿名或未经身份验证的绑定,因为您的服务器不知道谁连接到它。
为了避免任何客户端连接并增强服务器的安全性,您可以转动三个齿轮:
- 您可以在注册服务器接口时设置注册标志;和/或
- 您可以使用自定义例程设置安全回调,以检查是否应允许或拒绝请求客户端;和/或
- 可以设置与绑定句柄关联的身份验证信息,以指定安全服务提供程序,并设置 SPN 来表示 RPC 服务器。
让我们一步一步来看看这三个档位。
注册标志
首先,当您创建服务器时,您需要注册接口,例如调用RpcServerRegisterIf2 – 我将在RPC通信流部分中向您展示此调用的作用。作为 RpcServerRegisterIf2 的第四个参数,您可以指定接口注册标志,例如RPC_IF_ALLOW_LOCAL_ONLY以仅允许本地连接。
附注:将此内容解读为RPC_我ace_ALLOW_LOCAL_ONLY
示例调用可能如下所示:
1 2 3 4 5 6 7 8 9 10 |
RPC_STATUS rpcStatus = RpcServerRegisterIf2( Example1_v1_0_s_ifspec, // Interface to register. NULL, // NULL type UUID NULL, // Use the MIDL generated entry-point vector. RPC_IF_ALLOW_LOCAL_ONLY, // Only allow local connections RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls. (unsigned)-1, // Infinite max size of incoming data blocks. NULL // No security callback. ); |
安全回调
列表中的下一个是安全回调,您可以将其设置为上述调用的最后一个参数。始终允许的回调可能如下所示:
1 2 3 4 5 6 |
// Naive security callback. RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE hInterface, void* pBindingHandle) { return RPC_S_OK; // Always allow anyone. } |
要包含此安全回调,只需将 RpcServerRegisterIf2 函数的最后一个参数设置为安全回调函数的名称,在本例中,该函数仅命名为“SecurityCallback”,如下所示:
1 2 3 4 5 6 7 8 9 10 |
RPC_STATUS rpcStatus = RpcServerRegisterIf2( Example1_v1_0_s_ifspec, // Interface to register. NULL, // Use the MIDL generated entry-point vector. NULL, // Use the MIDL generated entry-point vector. RPC_IF_ALLOW_LOCAL_ONLY, // Only allow local connections RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls. (unsigned)-1, // Infinite max size of incoming data blocks. SecurityCallback // No security callback. ); |
此回调函数可以以您喜欢的任何方式实现,例如,您可以基于IP允许/拒绝连接。
经过身份验证的绑定
好了,我们越来越接近RPC术语和背景部分的结尾了…在我们深入研究最后的概念时,请与我保持联系。
由于我能感受到跟进所有这些术语的人的痛苦,让我们花点时间回顾一下:
好的,到目前为止,您应该知道您可以创建隐式和显式接口,并使用一些Windows API调用来设置RPC服务器。在上一节中,我添加了一个注册服务器,您可以设置注册标志和(如果需要)回调函数来保护服务器并过滤可以访问服务器的客户端。拼图中的最后一块现在是一个额外的Windows API,它允许服务器和客户端验证您的绑定(请记住,拥有绑定句柄的好处之一是您可以验证绑定,例如“为罐头手机涂上电线”)。
…但是你为什么要/应该这样做呢?
经过身份验证的绑定与正确的注册标志 (RPC_IF_ALLOW_SECURE_ONLY) 相结合,使 RPC 服务器能够确保只有经过身份验证的用户才能连接。并且 – 如果客户端允许 – 使服务器能够通过模拟客户端来确定谁连接到它。
要备份您之前学到的内容:您也可以使用 SecurityCallback 来拒绝任何匿名客户端进行连接,但您需要根据您控制的属性自行实现过滤机制。
示例:例如,您将无法确定客户端是否为有效的域用户,因为您无权访问这些帐户信息。
好的,那么如何指定经过身份验证的绑定呢?
您可以在服务器和客户端对绑定进行身份验证。在服务器端,您希望实现此项以确保安全的连接,而在客户端,您可能需要实现此连接才能连接到服务器(正如我们稍后将在访问矩阵中看到的那样)。)
在服务器端对绑定进行身份验证:[取自此处的示例代码]
1 2 3 4 5 6 7 |
RPC_STATUS rpcStatus = RpcServerRegisterAuthInfo( pszSpn, // Server principal name RPC_C_AUTHN_WINNT, // using NTLM as authentication service provider NULL, // Use default key function, which is ignored for NTLM SSP NULL // No arg for key function ); |
在客户端对绑定进行身份验证:[取自此处的示例代码]
1 2 3 4 5 6 7 8 9 10 |
RPC_STATUS status = RpcBindingSetAuthInfoEx( hExplicitBinding, // the client's binding handle pszHostSPN, // the server's service principale name (SPN) RPC_C_AUTHN_LEVEL_PKT, // authentication level PKT RPC_C_AUTHN_WINNT, // using NTLM as authentication service provider NULL, // use current thread credentials RPC_C_AUTHZ_NAME, // authorization based on the provided SPN &secQos // Quality of Service structure ); |
客户端的有趣之处在于,您可以使用经过身份验证的绑定句柄设置服务质量 (QOS) 结构。例如,此QOS结构可以在客户端用于确定模拟级别(有关背景信息,请查看我之前的IPC帖子),我们稍后将在客户端模拟部分中介绍。
重要提示:
在服务器端设置经过身份验证的绑定不会在客户端强制执行身份验证。
例如,如果在服务器端未设置标志或仅设置了RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH,则未经身份验证的客户端仍可以连接到 RPC 服务器。
但是,设置RPC_IF_ALLOW_SECURE_ONLY标志可防止未经身份验证的客户端绑定,因为如果不创建经过身份验证的绑定,客户端就无法设置身份验证级别(使用此标志检查的级别)。
众所周知与动态端点
最后但并非最不重要的一点是,我们必须阐明RPC通信的最后一个重要方面:众所周知的与动态端点。
我会尽量使这个简短,因为它也很容易理解……
当您启动RPC服务器时,服务器注册一个接口(正如我们在上面的RpcServerRegisterIf2代码示例中看到的那样),并且它还需要定义它想要侦听的协议序列(例如“ncacn_ip_tcp”,“ncacn_np”等)。
现在,您在服务器中指定的协议序列字符串不足以打开 RPC 端口连接。想象一下,您指定“ncacn_ip_tcp”作为协议序列,这意味着您指示服务器打开一个RPC连接,该连接接受通过TCP / IP连接的连接。但。。。服务器应该在哪个TCP端口上实际打开连接?
与ncacn_ip_tcp其他协议序列也需要更多关于在何处打开连接对象的信息:
- ncacn_ip_tcp需要一个 TCP 端口号,例如 9999
- ncacn_np需要命名管道名称,例如“\pipe\FRPC-NP”
- ncalrpc 需要一个 ALPC 端口名称,例如 “\RPC Control\FRPC-LRPC”
让我们假设您指定了ncacn_np作为协议序列,并选择命名管道名称为“\pipe\FRPC-NP”。
您的 RPC 服务器将愉快地启动,现在正在等待客户端连接。另一方面,客户端需要知道它应该连接到哪里。告诉客户端服务器的名称,指定要ncacn_np的协议序列,并将命名管道名称设置为您在服务器中定义的相同名称(“\pipe\FRPC-NP”)。客户端连接成功,就像你基于已知终结点构建了 RPC 客户端和服务器一样…在这种情况下是:“\pipe\FRPC-NP”。
使用众所周知的 RPC 终结点仅意味着你预先知道所有绑定信息(协议序列和终结点地址),并且如果需要,还可以在客户端和服务器中对这些信息进行硬编码。使用已知终结点是建立第一个 RPC 客户端/服务器连接的最简单方法。
那么什么是动态端点,为什么要使用它们?
在上面的示例中,我们选择ncacn_np,只是选择任意命名管道名称来打开我们的服务器,这工作得很好,因为我们知道(至少我们希望)我们用这个名字打开的命名管道在服务器端还不存在,因为我们刚刚做了一个名字。如果我们现在选择ncacn_ip_tcp作为协议序列,我们如何知道哪个TCP端口仍然可用?好吧,我们可以指定我们的程序需要端口9999才能正常工作,并将其留给管理员以确保此端口未使用,但我们也可以要求Windows为我们分配一个免费的端口。这就是动态端点。容易。。。案件已结案,让我们去喝啤酒
等一下:如果我们动态地分配了一个端口,客户如何知道连接到哪里?!…
这是动态终结点的另一件事:如果你选择了动态终结点,则需要有人告诉你的客户端你获得了哪个端口,并且有人是 RPC 终结点映射程序服务(默认情况下在 Windows 系统上启动和运行)。如果服务器使用的是动态终结点,则需要调用 RPC 终结点映射程序,以告知它注册其接口和函数(在 IDL 文件中指定)。一旦客户端尝试创建绑定,它将查询服务器的 RPC 终结点映射程序以查找匹配的接口,终结点映射程序将填充缺少的信息(例如 TCP 端口)以创建绑定。
动态终结点的主要优点是在终结点地址空间有限时自动查找可用的终结点地址,就像 TCP 端口一样。命名管道和基于 ALPC 的连接也可以安全地使用已知终结点完成,因为地址空间(也称为您选择的任意管道或端口名称)足够大以避免冲突。
我们将使用来自服务器端的代码片段来总结这一点,以确定我们对已知和动态端点的理解。
众所周知的端点实施
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
RPC_STATUS rpcStatus; // Create Binding Information rpcStatus = RpcServerUseProtseqEp( (RPC_WSTR)L"ncacn_np", // using Named Pipes here RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Ignored for Named Pipes (only used for ncacn_ip_tcp, but set this anyway) (RPC_WSTR)L"\\pipe\\FRPC-NP", // example Named Pipe name NULL // No Secuirty Descriptor ); // Register Interface rpcStatus = RpcServerRegisterIf2(...) // As shown in the examples above // OPTIONAL: Register Authentication Information rpcStatus = RpcServerRegisterAuthInfo(...) // As shown in the example above // Listen for incoming client connections rpcStatus = RpcServerListen( 1, // Recommended minimum number of threads. RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Recommended maximum number of threads. FALSE // Start listening now. ); |
动态端点实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
RPC_STATUS rpcStatus; RPC_BINDING_VECTOR* pbindingVector = 0; // Create Binding Information rpcStatus = RpcServerUseProtseq( (RPC_WSTR)L"ncacn_ip_tcp", // using Named Pipes here RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Backlog queue length for the ncacn_ip_tcp protocol sequenc NULL // No Secuirty Descriptor ); // Register Interface rpcStatus = RpcServerRegisterIf2(...) // As shown in the examples above // OPTIONAL: Register Authentication Information rpcStatus = RpcServerRegisterAuthInfo(...) // As shown in the example above // Get Binding vectors (dynamically assigend) rpcStatus = RpcServerInqBindings(&pbindingVector); // Register with RPC Endpoint Mapper rpcStatus = RpcEpRegister( Example1_v1_0_s_ifspec, // your interface as defined via IDL pbindingVector, // your dynamic binding vectors 0, // We don't want to register the vectors with UUIDs (RPC_WSTR)L"MyDyamicEndpointServer" // Annotation used for information purposes only, max 64 characters ); // Listen for incoming client connections rpcStatus = RpcServerListen( 1, // Recommended minimum number of threads. RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Recommended maximum number of threads. FALSE // Start listening now. ); |
注意:如果您使用的是众所周知的端点,则还可以通过调用 RpcServerInqBindings & RpcEpRegister(如果需要)将 RPC 服务器注册到本地 RPC 端点映射程序。您不需要这样做即可使您的客户能够连接,但您可以。
如果您想阅读有关此内容的更多信息,可以在此处找到有关此主题的 Microsoft 文档:
https://docs.microsoft.com/en-us/windows/win32/rpc/specifying-endpoints
RPC 通信流
总结以上所有内容,通信流程可以总结如下:
- 服务器注册接口,例如使用 RpcServerRegisterIf2
- 服务器使用 RpcServerUseProtseq & RpcServerInqBindings 创建绑定信息(RpcServerInqBinding 对于已知端点是可选的))
- 服务器使用 RpcEpRegister 注册终结点(对于已知终结点是可选的))
- 服务器可以使用 RpcServerRegisterAuthInfo 注册身份验证信息(可选)
- 服务器侦听使用 RpcServerListen 的客户端连接
- 客户端使用 RpcStringBindingCompose & RpcBindingFromStringBinding 创建一个绑定句柄
- 客户RPC 运行时库通过查询服务器主机系统上的终结点映射程序来查找服务器进程(仅动态终结点需要))
- 客户端可以使用 RpcBindingSetAuthInfo(可选)对绑定句柄进行身份验证
- 客户端通过调用在使用的接口中定义的函数之一进行 RPC 调用
- 客户RPC 运行时库在 NDR 运行时的帮助下以 NDR 格式封送参数,并将其发送到服务器,
- 服务器的 RPC 运行时库将封送参数提供给存根,存根取消封送参数,然后将其传递给服务器例程。
- 当 Server 例程返回时,存根选取 [out] 和 [in, out] 参数(在接口 IDL 文件中定义)和返回值,封送它们,并将封送的数据发送到服务器的 RPC 运行时库,后者将它们传输回客户端。
示例实现
如开头所述,上面的示例取自我的示例实现,可在以下位置公开获取:
https://github.com/csandker/InterProcessCommunication-Samples/tree/master/RPC/CPP-RPC-Client-Server。
在此存储库中,您将找到以下示例实现:
- 支持未经身份验证的隐式绑定的基本未经身份验证的服务器
- 支持未经身份验证的隐式绑定的基本未经身份验证的客户端
- 支持未经身份验证的显式绑定的基本服务器
- 支持经过身份验证的显式绑定的基本服务器
- 基本客户端支持未经身份验证的显式绑定,无需 QOS
- 支持使用 QOS 进行经过身份验证的显式绑定的基本客户端
下面可以看到这些 PoC 的外观示例:
访问矩阵
好吧,如果您了解了上述所有术语,下面是访问矩阵,它可视化了哪个客户端可以连接到哪个服务器。
注: 只能将隐式客户机连接到隐式服务器,将显式客户机连接到显式服务器。否则,您会收到错误 1717 (RPC_S_UNKNOWN_IF)
攻击面
最后。。。在谈到RPC内部之后,让我们谈谈RPC的攻击面。
显然,在RPC通信链的任何地方都可能存在错误和0天,这总是归结为逐个案例分析以了解其利用潜力,但一般RPC设计概念也有一些利用潜力,我将在下面重点介绍。
附注:如果您知道有趣的RPC CVE,请向我发送邮件/0xcsandker
寻找有趣的目标
好吧,在我们考虑我们可以用RPC玩什么进攻游戏之前,我们需要先找到合适的目标。
让我们深入了解如何在您的系统上找到 RPC 服务器和客户端。
RPC 服务器
回顾一下,服务器是通过指定所需的信息(协议序列和终结点地址)并调用Windows API来构建必要的内部对象并启动服务器来构建的。考虑到这一点,在本地系统上查找RPC服务器的最简单方法是查找导入这些RPC Windows API的程序。
一种简单的方法是使用现在随Visual Studio一起提供的DumpBin实用程序。
在下面可以找到在最近的Windows10上搜索的示例Powershell代码段:C:\Windows\System32\
1 2 |
Get-ChildItem -Path "C:\Windows\System32\" -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:rpcrt4.dll $_.VersionInfo.FileName); If($out -like "*RpcServerListen*"){ Write-Host "[+] Exe starting RPC Server: $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath EXEs_RpcServerListen.txt -Append } } |
此代码段将可执行文件的名称打印到控制台,并将整个 DumpBin 输出输出到EXEs_RpcServerListen.txt的文件(以便您可以查看 DumpBin 实际为您提供的内容)。
查找感兴趣的 RPC 服务器的另一种方法是在本地或任何远程系统上查询 RPC 终结点映射程序。
微软有一个名为PortQry的测试实用程序来做到这一点(该工具还有一个GUI版本可用),你可以像这样使用它:C:\PortQryV2\PortQry.exe -n <HostName> -e 135
此工具为您提供有关终结点映射程序知道的远程 RPC 接口的一些信息(请记住,已知终结点不必通知终结点映射程序有关其接口的信息)。
另一种选择是通过调用 RpcMgmtEpEltInqBegin 并通过 RpcMgmtEpEltInqNext 迭代接口来直接查询终结点管理器。这种方法的一个名为RPCDump的示例实现包含在Chris McNab的惊人书“网络安全评估”中,O’Reilly在这里发布了用C编写的工具(根据注释注释,此代码的信用应该交给Todd Sabin)。
我已将此很酷的工具移植到VC ++,并进行了一些轻微的可用性更改。我已经在 https://github.com/csandker/RPCDump 上发布了我的分叉。
如图所示,此工具还列出了找到的 RPC 终结点的接口以及其他一些信息。我不会详细介绍所有这些字段,但如果您有兴趣,请查看代码并阅读Windows API文档。例如,通过调用 RpcMgmtInqStats 来检索统计信息,其中返回的值在“备注”部分中引用。
再次记住,只有 RPC 接口注册到目标的终结点映射程序。
RPC 客户端
查找连接到远程或本地 RPC 服务器的客户端也可能是一个有趣的目标。
没有一个单一的机构知道哪些 RPC 客户端当前正在运行,因此您有两个选择来查找客户端:
- 查找使用客户端 RPC API 的可执行文件/进程;或
- 在行动中抓住客户
查找导入客户端 RPC API 的本地可执行文件类似于我们已经使用 DumpBin 查找服务器所做的工作。一个好的Windows API是RpcStringBindingCompose:
1 2 |
Get-ChildItem -Path "C:\Windows\System32\" -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:rpcrt4.dll $_.VersionInfo.FileName); If($out -like "*RpcStringBindingCompose*"){ Write-Host "[+] Exe creates RPC Binding (potential RPC Client) : $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath EXEs_RpcClients.txt -Append } } |
查找 RPC 客户端的另一个选项是在它们连接到目标时发现它们。如何发现客户端的一个示例是通过检查通过两个系统之间的网络发送的流量。Wireshark有一个“DCERPC”滤波器,可用于发现连接。
客户端连接到服务器的示例如下所示:
绑定请求是我们可以查找以标识客户端的内容之一。在选择包中,我们可以看到一个客户端尝试绑定到UUID为“d6b1ad2b-b550-4729-b6c2-1651f58480c3”的服务器接口。
未经授权的访问
一旦您确定了一个 RPC 服务器,该服务器公开了可能对您的攻击链有用的有趣功能,那么最明显的检查就是您是否可以未经授权访问该服务器。
您可以实现自己的客户端,例如,基于我的示例实现,也可以参考访问矩阵来检查您的客户端是否可以连接到服务器。
如果您已经深入研究了 RPC 服务器的逆向工程,并发现服务器通过调用 RpcServerRegisterAuthInfo 及其 SPN 和指定的服务提供程序来设置身份验证信息,请注意,经过身份验证的服务器绑定不会强制客户端使用经过身份验证的绑定。换句话说:仅仅因为服务器设置了身份验证信息并不意味着客户端需要通过经过身份验证的绑定进行连接。此外,在连接到设置身份验证信息的服务器时,请注意运行时库 (rpcrt4.dll 不会调度具有无效凭据的客户端调用,但是,将调度没有凭据的客户端调用。或者用微软的话来说:
请记住,默认情况下,安全性是可选的
源:https://docs.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-rpcserverregisterifex
一旦您连接到服务器,就会出现“下一步该做什么”的问题……
好吧,然后您就可以调用接口函数,坏消息是:您需要首先识别函数名称和参数,这归结为对目标服务器进行逆向工程。
如果你运气好的话,你看到的不是纯粹的RPC服务器,而是COM服务器(COM,特别是DCOM,在引擎盖下使用RPC),那么服务器可能会附带一个类型库(.tlb),你可以用它来查找接口功能。
我不会在这里更深入地介绍类型库或其他任何内容(博客文章已经很长了),但我对在这种情况下的人的一般建议是:获取我的示例RPC客户端和服务器代码,编译它并使用您知道的示例代码开始您的逆向工程之旅。在这种特定情况下,让我添加另一个线索:我的示例接口在IDL文件中定义了一个“输出”函数,这个“输出”函数以print语句开头,例如,您可以通过搜索子字符串开始,以确定此特定接口函数的位置。printf("[~] Client Message: %s\n", pszOutput);
[~] Client Message
客户端模拟
客户端模拟还提供了有趣的攻击面。我已经在本系列的最后一部分对什么是冒充以及它是如何工作的进行了一些阐述,如果您错过了这一点并且需要对冒充进行重新了解,您会发现在我上一篇文章的模拟部分中对此进行了解释。
模拟客户端的配方如下:
- 您需要一个连接到服务器的 RPC 客户端
- 客户端必须使用经过身份验证的绑定(否则,您将没有可以模拟的安全信息)
- 客户端不得在“安全模拟”下设置“模拟级别”经过身份验证“绑定
- …就是这样
冒充过程就像:
- 从服务器接口函数
中调用 RpcImpersonateClient 请注意,此函数将绑定句柄作为输入,因此您需要一个显式绑定服务器来使用模拟(这有意义) - 如果该调用成功,服务器的线程上下文将更改为客户端的安全上下文,您可以调用 GetCurrentThread & OpenThreadToken 来接收客户端的模拟令牌。
如果您现在喜欢“WTF安全上下文更改?!”,如果您更喜欢“WTF模拟令牌?”,您将在IPC命名管道帖子
中找到答案?!“您会在我的Windows授权指南中找到答案 - 调用 DuplicateTokenEx 将模拟令牌转换为主令牌后,您可以通过调用 RpcRevertToSelfEx 愉快地返回到原始服务器线程上下文
- 最后,您可以调用CreateProcessWithTokenW来使用客户端的令牌创建新进程。
请注意,这只是使用客户端令牌创建流程的一种方法,但在我看来,它很好地描绘了执行这些事情的方式,因此我在这里使用这种方法。可以在此处找到此代码的示例实现。
顺便说一句,这与我在上一篇文章中用于模拟命名管道客户端的过程相同。
如上面的配方步骤中所述,您只需要一个连接到服务器的客户端,并且该客户端必须使用经过身份验证的绑定。
如果客户端未对其绑定进行身份验证,则对 RpcImpersonateClient 的调用将导致错误 1764 (RPC_S_BINDING_HAS_NO_AUTH)。
查找可以连接到服务器的合适客户端归结为查找 RPC 客户端(请参阅查找 RPC 客户端一节)并找到可以连接到服务器的客户端。好吧,后者可能是这个漏洞利用链中棘手的部分,我不能在这里给出有关如何找到这些连接的一般建议。其中一个原因是因为它取决于客户端使用的协议序列,其中未应答的TCP调用在网络上嗅探时可能是最好的检测,其中未应答的命名管道连接尝试也可能在客户端或服务器的主机系统上被发现。
在本系列的第一部分(关于命名管道)中,我对客户冒充进行了更大的关注,因此我将在这里保护自己几句话。但是,如果您还没有这样做,我建议您阅读实例创建竞争条件以及实例创建特殊类型。同样的原则也适用于此。
更有趣的方面是我故意在上面写的:“客户端不得在SecurityImpersonation* 下面设置模拟级别身份验证绑定……这听起来有点像选择退出的过程,这正是它。
请记住,在创建经过身份验证的绑定时,可以在客户端设置服务质量 (QOS) 结构吗?如“经过身份验证的绑定”一节中所述,在连接到服务器时,可以使用该结构来确定模拟级别。有趣的是,如果您没有设置任何QOS结构,则默认值将是SecurityImpersonation,只要客户端没有在SecurityImpassion下显式设置模拟级别,它就允许任何服务器模拟RPC客户端。
然后,模拟的结果可能如下所示:
服务器非模拟
冒充的另一面经常被遗漏,但从攻击者的角度来看,这同样有趣。
在本系列的第 1 部分中,我详细介绍了模拟客户端时涉及的步骤,这些步骤同样适用于 RPC 模拟(以及所有其他类似技术),其中以下两个步骤特别有趣:
>>步骤 8:然后将服务器的线程上下文更改为客户端的安全上下文。
>> 步骤 9:服务器在客户端的安全上下文中执行的任何操作和服务器调用的任何功能都是使用客户端的标识进行的,从而模拟客户端。
来源: 冒犯性 Windows IPC 内部 1: 命名管道
服务器的线程上下文被更改,然后执行的所有操作都是使用客户端的安全上下文进行的。在上面的部分(以及我的示例代码中),我用它来获取当前线程令牌,然后它是客户端的令牌,并将其转换为主令牌以使用该令牌启动新进程。我也可以直接调用我想执行的任何操作,因为我已准备好在客户端的安全上下文中操作。根据章节标题,您现在可能已经猜到了它的发展方向……如果模拟失败并且服务器未检查该错误,该怎么办?
对 RpcImpersonateClient(为您执行所有模拟魔术的 API 函数)的调用将返回模拟操作的状态,服务器必须检查这一点。
如果模拟成功,则之后您将处于客户端的安全上下文中,但如果失败,则您处于调用 RpcImpersonateClient 的同一旧安全上下文中。
现在,RPC 服务器可能以其他用户身份运行(通常也在安全性较高的上下文中),在这些情况下,它可能会尝试模拟其客户端以在较低且可能更安全的客户端安全上下文中运行客户端操作。作为攻击者,您可以通过在服务器端强制执行失败的模拟尝试,从而导致服务器执行在服务器安全性较高的上下文中操作的客户端,从而将这些情况用于权限提升攻击媒介。
此攻击场景的配方很简单:
- 您需要一个服务器来模拟其客户端,并且在执行进一步操作之前不会仔细检查 RpcImpersonateClient 的返回状态。
- 从客户端的角度来看,服务器在模拟尝试后执行的操作必须是可利用的。
- 您需要强制模拟尝试失败。
如果您阅读前面的部分并记下如何使用 DumpBin,则查找尝试模拟客户端的本地服务器是一项简单的任务。
找到一个在“假定的模拟”上下文中运行操作的服务器,可以从攻击者的角度使用,这几乎是对服务器功能的创造性逐案分析。分析这些案例的最佳建议是跳出框框思考,并可能准备好将多个事件和操作联系起来。一个相当简单但功能强大的示例可能是服务器执行的文件操作;也许您可以使用联结在写保护系统路径中创建文件,或者您可以使服务器打开命名管道而不是文件,然后使用命名管道模拟来模拟服务器…
清单上的最后一个是导致服务器的模拟尝试失败,这是工作中最简单的部分。有两种方法可以实现此目的:
- 您可以从未经身份验证的绑定进行连接;或
- 您可以从经过身份验证的绑定进行连接,并将 QOS 结构的模拟级别设置为安全匿名
此操作中的任何一个都将安全地导致模拟尝试失败。
顺便说一句,这种技术并不是一件新鲜事,它广为人知……只是有时被遗忘了。也许还有一个更花哨的名字来形容这种技术,我还没有遇到过。Microsoft甚至在RpcImpersonateClient函数的“备注”部分特别提醒您这一点(他们甚至给了它一个特殊的“Securtiy备注”标题):
如果对 RpcImpersonateClient 的调用由于任何原因而失败,则不会模拟客户端连接,而是在进程的安全上下文中发出客户端请求。如果进程作为高特权帐户(如 LocalSystem)或管理组的成员运行,则用户可能能够执行否则将被禁止执行的操作。因此,重要的是要始终检查调用的返回值,如果失败,则引发错误;不要继续执行客户端请求。
来源: RpcImpersonateClient: Security 备注
MITM 身份验证的 NTLM 连接
最后两节介绍了这样一个事实,即RPC可以用作远程网络通信技术,因此在网络端也带有一个有趣的攻击面。
附注:我故意这样说;你最初可能是“哎呀,你还打算使用一种叫做远程程序调用的技术吗?!”但事实上,RPC也非常适合纯粹在本地用作ALPC的包装器(一旦我弄清楚了ALPC的所有奥秘,我就在本系列的第3部分中回到这一点)。
无论如何,如果您通过网络使用 RPC,并且希望对绑定进行身份验证,则需要一个为您进行身份验证的网络协议。这就是为什么 RpcServerRegisterAuthInfo 的第二个参数 (AuthnSvc),它是您在服务器端调用以创建经过身份验证的绑定的 API 函数,让您定义要使用的身份验证服务。例如,您可以指定 Kerberos 的常量值 RPC_C_AUTHN_GSS_KERBEROS,也可以指定RPC_C_AUTHN_DEFAULT使用默认身份验证服务,有趣的是,NTLM (RPC_C_AUTHN_WINNT)。
自 Windows 2000 以来,Kerberos 被设置为默认身份验证方案,但 RPC 仍默认为 NTLM。
因此,如果您在网络上处于合适的位置,并且看到NTLM连接通过,则可以执行两件有趣的事情:
- 您可以从网络上获取NTLM(v2)质询响应哈希值,然后离线暴力破解用户的密码;和/或
- 您可以截获并中继 NTLM 连接,以访问另一个系统。
我不想深入探讨这两个主题(如果你直到这里,你肯定已经读够了),所以我在这里只补充两点:
- NTLM(v2) 挑战暴力破解是众所周知的, 所以你不应该有麻烦找到如何做到这一点。例如,请查看 https://hashcat.net/wiki/doku.php?id=example_hashes 上的哈希猫模式5600。
- NTLM Relay被伟大的Pixis在 https://en.hackndo.com/ntlm-relay/ 很好地描述。根据所使用的协议,有几件事需要注意,因此如果您有兴趣,请务必查看该帖子。
MITM 身份验证GSS_NEGOTIATE连接
最后但同样重要的。。。
除了基于NTLM的网络身份验证方案,如果您在 RpcServerRegisterAuthInfo 调用中选择RPC_C_AUTHN_WINNT或RPC_C_AUTHN_DEFAULT作为身份验证服务,您将获得的,经常使用的RPC_C_AUTHN_GSS_NEGOTIATE常量也是一个有趣的目标。
如果选择了RPC_C_AUTHN_GSS_NEGOTIATE Microsoft 的 Negotiate SSP 用于指示客户端和服务器自行协商是否应使用 NTLM 或 Kerberos 对用户进行身份验证。默认情况下,如果客户端和服务器支持此协商,则始终会导致 Kerberos。
可以从拦截网络位置攻击此协商,以强制在 Kerberos 上使用 NTLM,从而有效地降级身份验证方案。需要注意的是,此攻击需要合适的网络位置和缺少签名。在这一点上,我不会深入研究这个问题,主要是因为我已经在这里的旧帖子中详细介绍了该过程和攻击:降级SPNEGO身份验证。
顺便说一句,这里提到的身份验证服务常量可以在这里找到:https://docs.microsoft.com/en-us/windows/win32/rpc/authentication-service-constants。
就是这样。。你成功了!
引用
- 微软的RPC文档:https://docs.microsoft.com/en-us/windows/win32/rpc/overviews
- 乔纳森·约翰逊(Jonathan Johnson)对RPC的评论:https://ipc-research.readthedocs.io/en/latest/subpages/RPC.html
- 亚当·切斯特RPC的审查:https://blog.xpnsec.com/analysing-rpc-with-ghidra-neo4j/
- 有关如何开始使用 RPC 编程的代码项目:https://www.codeproject.com/Articles/4837/Introduction-to-RPC-Part-1#Implicitandexplicithandles17