这篇文章不会深入介绍 RPC 实现的细节,请阅读进一步阅读部分中提到的资源。另外,RPC 编程不在本文的讨论范围内。我们将只讨论反向 RPC 客户端和服务器所需的概念。
您可能会在恶意软件代码中或进行安全研究时遇到 RPC 调用,因此了解 RPC 的基本工作原理对于理解代码至关重要。享受阅读。
-
远程过程调用/RPC 作为 Windows 技术的支柱,各种功能和内部 API 都构建在 RPC 之上。 -
RPC 与传统的函数调用相同,唯一的区别是调用者和被调用者代码驻留在网络中的不同计算机上。它也可以在同一台机器上。简而言之,这意味着 RPC 让我们可以通过网络调用函数。 -
RPC 遵循客户端-服务器模型,其中调用 RPC 例程的代码是客户端,而为调用提供服务的代码是服务器。服务器代码是一个独立的PE文件(exe或dll)。无论调用的性质是网络还是本地调用,RPC 调用都必须到达服务器。 -
下图显示了 RPC 通信。由于来自客户端的调用需要到达服务器,因此客户端利用端点信息来调用该调用。
-
客户端可以开始通过网络 (ncan_ip_tcp) 或使用命名管道 (ncacn_np) 与服务器进行通信,并通过高级本地远程过程(又名 ALRPC(ncalrpc))进行本地通信。这些作为 RPC 的一组协议,在客户端和服务器之间建立通信通道。当我们说网络上的 RPC 时,这意味着客户端和服务器代码位于网络环境中的不同计算机上,类似地,在本地 RPC 通信中,客户端和服务器都位于同一台计算机上。RPC 由充当运行时的 rpcrt4.dll 实现。 -
一个系统上运行着数百个RPC服务器,客户端如何识别正确的一个呢?– RPC 接口。服务器需要通知系统其存在以及它为客户端提供的功能。该信息以接口的形式整合。通过使用这些接口,客户端可以轻松地本地/远程识别 RPC 服务器。 -
当调用到达服务器接口时,服务器运行所在计算机上的 RPC 运行时可以将数据和控制权移交给服务器实现的目标 RPC 函数(exe/dll),在接下来的章节中将对此进行详细介绍。
剖析 RPC 客户端:PrintSpoofer
-
在本节中,我们将使用PrintSpoofer作为 RPC 客户端来进一步讨论。您可以在 PrintSpoofer 上找到 itm4n 的详细博客。 -
简而言之,PrintSpoofer 滥用 MS-RPRN PrinterBug 通过模拟命名管道来提升权限。Printerbug 滥用RpcRemoteFindFirstPrinterChangeNotificationEx()来强制身份验证,更多详细信息请参见此处。 -
下图显示了PrintSpoofer的代码,共有三个RPC调用RpcOpenPrinter、RpcRemoteFindFirstPrinterChangeNotificationEx和RpcClosePrinter(RpcExcept是一个宏仅供参考)。这是我们的 RPC 客户端。
-
当我们反转二进制文件时,我们看不到源代码,下图显示了调试器中的上述代码。有趣的是,在汇编代码中找不到上述 RPC 函数。您可以看到对 NdrClientCallX 的异常函数调用(X = 1、2、3 和 4),这是 RPC 调用的指示符。当你编译一个RPC程序时,所有的调用都会被转换为NdrClientCallX,这个特殊的函数调用服务器实现的适当的RPC api。
-
我们看一下NdrClientCall3的函数原型,函数参数如下所示。
-
pProxyInfo 和 nProcNum 是两个重要参数,可帮助 NdrClientCall3 api 调用服务器实现的正确 RPC 例程。
-
让我们从 nProcNum 开始,这只是一个正整数值,用于调用服务器实现的特定 RPC 函数。就逆向而言,该参数对于识别客户端正在调用的 RPC 例程非常重要。仅供参考,如果没有获取接口信息,这些数据对我们来说没有任何用处。例如,RpcOpen 的 nProcNum 为 0x01(1),RpcRemoteFindFirstPrinterChangeNotificationEx 的 nProcNum 为 0x041(65)
-
如您所见,pProxyInfo 是 MIDL_STUBLESS_PROXY_INFO 类型的指针。当然,它看起来有点吓人,但也没那么糟糕。该结构仅保存 rpc 运行时在服务器站点调用正确的 RPC 函数所需的所有有价值的信息。(例如接口信息)
-
MIDL_STUBLESS_PROXY_INFO 的定义如下所示。第一个成员是指向另一个超级重要的结构MIDL_STUB_DESC的指针。
-
MIDL_STUB_DESC结构保存接口信息。运行时从这里获取接口信息并与关联的RPC服务器建立通信通道。下图显示了 MIDL_STUB_DESC 的定义。
-
我们只对 MIDL_STUB_DESC 结构中的第一个成员感兴趣,它是一个指向 void 类型的 RpcInterfaceInfomration 的指针。这非常具有欺骗性,这个 RpcInterfaceInformation 是什么?如果您阅读此处的文档,您将看到 – 在客户端,它指向客户端 rpc 信息结构,在服务器端,它指向服务器 rpc 信息。快速谷歌搜索显示RPC_CLIENT_INTERFACE 结构的msdn页面。我正在分享 WIndows SDK 标头中 RPC_CLIENT_INTERFACE 和 RPC_SERVER_INTERFACE 的屏幕截图。
-
尽管名称相同,RPC_CLIENT_INTERFACE 和 RPC_SERVER_INTERFACE 结构是相同的。您可以清楚地看到两个结构中的第二个成员是 InterfaceId。我们想知道这里存储的值来识别服务器。长话短说,该成员拥有一个 GUID,RPC 使用该 GUID 来识别服务器。 -
现在使用这个 InterfaceId 值和 nProcNum 我们可以轻松找到服务器和客户端正在调用的 RPC 函数。
-
让我们看看传递给 NdrClientCall3 函数的参数值,在蝙蝠右侧记下 nProcNum,这是第二个参数。在我们的例子中,该值为 0x01。第一个参数是指向 MIDL_STUBLESS_PROXY_INFO pProxyInfo 的指针。让我们跟随内存转储中的指针。
-
下面的内存显示了 MIDL_STUB_DESC,pProxyInfo 的第一个成员,它是指向结构 RPC_CLIENT_INTERFACE 的指针(因为我们正在反转客户端,而在服务器端,此更改为 RPC_SERVER_INFERFACE),如下以黄色突出显示。让我们跟踪同一个内存转储中的指针。
-
最后我们到达了 RPC_CLIENT_INTERFACE。突出显示的数据是接口 GUID,它存储在 RPC_CLIENT_INTERFACE 的第二个成员 InterfaceId 中。GUID 值为12345678-1234-abcd-ef00-0123456789ab
-
现在我们有以下信息 – 接口 GUID 和 nProcNum。让我们启动RpcView应用程序,使用接口 GUID 查找服务器。我们可以在界面窗口中查找 GUID 值。瞧!目标 RPC 服务器是“spoolsv.exe”
-
我们可以在界面属性窗口中看到更多详细信息,如下所示
-
最重要的是端点窗口,您可以看到与我们在本文开头讨论的通信协议相关的所有信息。为了通过 tcp 与 spoolsv rpc 服务器通信,我们需要使用端口 55149 或命名管道“pipespoolss”。在本地,我们可以使用 LRPC-XXX..X 端点,如下所示。
-
现在,我们已经从传递给 NdrClientCall3 调用的结构中获取的 GUID 识别了 RPC 服务器。nProcNum 值怎么样?这个值是多少?简单来说,它是一个索引号。RPC服务器有一个调度表,该表存储了服务器实现的所有功能。nProcNum 用作调度表的索引值。您可以在 RpcView 中看到该表,如下图所示。由于某种原因,名称未显示。
-
有这个git hub 存储库,您可以在其中找到 RPC 服务器公开的所有功能。函数名称根据其索引值显示,如下所示
-
前面我们看到nProcNum的值为0x01,从上图中我们可以很容易地识别出这个函数——RpcOpenPrinter。也可以参考msdn,如下所示,Opnum就是nProcNum。
-
接口GUID:12345678-1234-abcd-ef00-0123456789ab -
服务器:C:WindowsSystem32spoolsv.exe
-
RPC服务器有一个称为调度表的特殊数据结构来保存它支持的所有功能,我们需要查看该表来获取我们想要分析的RPC例程的地址。 -
与我们分析客户端的方式类似,我们必须处理一些数据结构才能最终到达调度表。 -
首先,我们需要在服务器二进制文件中搜索 GUID 值。接口信息存储在rdata部分。在 IDA 中,有“字节序列..”搜索选项,如下所示。让我们搜索 GUID 的前四个字节。
-
我得到了两次点击,让我们检查第一个
-
您可以看到存储在 14008D2F4 处的 GUID,但这看起来更像是一个从 unk_14008D2F0 开始的结构。让我们通过单击 unk_14008D2F0 按 x 来交叉引用该地址。弹出一个小窗口,如下所示。单击指向 rdata (14008D5E0) 的地址
-
有趣的是,off_14008D5E0指向unk_14008D2F0,其中存在GUID,MIDL_user_allocate和MIDL_user_free表明我们正在盯着的结构肯定是MIDL_STUB_DESC,是的,再检查一下结构定义🙂。正如我们在客户端部分讨论的,服务器中 MIDL_STUB_DESC 的第一个成员持有 RPC_SERVER_INTERFACE 结构,这意味着 unk_14008D2F0 指向 RPC_SERVER_INTERFACE
-
那么转到unk_14008D2F0,这就是我们的服务器接口结构,RPC-CLIENT_INTERFACE和RPC_SERVER_INTERFACE是相同的,有一个成员InterpreterInfo,在fortinet RPC相关的一篇博客中提到InterpreterInfo指向保存DispatchTable的MIDL_SERVER_INFO。您可以在 unk_14008D2F0 的 RPC_SERVER_INTERFACE 中看到下面的 MIDL_SERVER_INFO。
-
在我们检查这个地址之前,让我们先看看 MIDL_SERVER_INFO 的定义。第二个成员是我们的 DispatchTable
-
借助上面所示的定义,我们可以轻松识别每个成员,off_14008D690是指向DispatchTable的指针。
-
通过检查off_14008D690,我们可以看到spoolsv.exe服务器实现的例程列表。
-
假设我们想在 PrintSpoofer 进行 RPC 时在调试器中分析 RpcRemoteFindFirstPrinterChangeNotificationEx 函数,但我们不知道放置断点的函数的地址。 -
别担心,让我们回到 IDA 中的调度表,而不是 rdata 中存储函数指针的地址。如下图,该地址为14008D898
-
接下来将调试器(提升的进程)附加到 spoolsv.exe 并找出程序的基地址,如下所示。地址是00007FF787EF0000
-
现在我们需要找到 RpcRemoteFindFirstPrinterChangeNotificationEx 指针存储在 rdata 部分中的偏移量。这很简单,它是 spoolsv 的基地址(在 IDA 中)和我们从调度表(在 IDA 中)获得的地址 (0000000140000000 – 14008D898) 之间的差异,这给了我们 8D898 -
最后将此值添加到调试器中 spoosv.exe 的基地址 00007FF787EF0000 + 8D898 = 7FF7 87F7 D898 。让我们在调试器中检查这个地址。下图显示了内存中的调度表,让我们跟随反汇编器中突出显示的地址。
-
瞧!反汇编程序中的 RpcRemoteFindFirstPrinterChangeNotificationEx 代码
-
如果您不确定,可以返回检查 IDA 的伪代码(如下所示)来验证上面的代码。现在,您可以在 PrintSpoofer 调用 RpcRemoteFindFirstPrinterChangeNotificationEx 时放置一个断点来中断 spoolsv.exe。
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):反向 RPC 懒惰指南