Impacket开发指南:第一部分–RPC远程过程调用深入解析

admin 2026-03-03 08:16:20 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文是Impacket开发指南首篇,聚焦WindowsRPC原理以为后续开发横向移动工具奠基。内容详解RPC架构核心,包括接口定义(IDL)、存根编组及NDR引擎机制。深入剖析客户端与服务端交互流程,涵盖字符串绑定格式、协议序列及动态端点映射原理。文章结合C++代码示例,演示了服务端接口注册、端点监听与认证配置等关键实现步骤,为安全开发人员深入理解协议底层提供了详实指导。 综合评分: 88 文章分类: 安全开发,渗透测试,内网渗透


cover_image

Impacket 开发指南:第一部分 – RPC 远程过程调用深入解析

CICADA8 CICADA8

securitainment

2026年2月17日 13:37 中国香港

| 原文链接 | 作者 | | — | — | | https://cicada-8.medium.com/impacket-developer-guide-part-1-rpc-4df4fe6d79d7 | CICADA8 |

大家好,我叫 Michael Zhmailo,是 CICADA8 团队的渗透测试专家。

我们正在开始一系列文章,介绍如何使用 Impacket 进行开发。本文是第一部分。

简介

我们认为应该从基础知识开始讲起。因此,第一部分(剧透一下,第二部分也是)将重点关注 RPC 的基础知识,而非直接编程。不过,我会积极展示 C++ 代码示例以便于理解。此外,文章中包含大量引用其他作者的研究和资料。这样,在进入使用 Impacket 开发工具的主题之前,你将对 RPC 有一个全面而深入的理解。我们将从零开始学习 Impacket,直到你能够编写自己的横向移动(Lateral Movement)自定义工具!

什么是 RPC?

RPC 即 远程过程调用(Remote Procedure Call),允许我们远程访问服务器程序的功能。此外,RPC 可以在多种不同的传输协议之上工作。所有支持的 RPC 传输可以分为两类:

  • 无连接型(Connectionless) — UDP、IPX 等..
  • 面向连接型(Connection) — TCP、SMB、HTTP、NetBIOS over TCP 等…

在 RPC 中,你有一个托管 RPC 功能的服务器,以及一个访问该功能的客户端。我甚至可以给你展示一些简单的伪代码。

# Server.cpp
intAdd(int a, int b)
{
return a+b;
}

# Client.cpp
intmain()
{
  rpc_h handle = RpcConnectToServer("10.10.10.10","1337");
int result = RpcInvokeMethod(handle, Add, 5, 2);
&nbsp; std::cout << result << std::endl; #&nbsp;7
return0;
}

接口、存根和编组

Windows 中 RPC 最重要的组成部分之一是接口(Interface)。服务器提供给客户端的所有功能都定义在接口中。这些文件包含唯一的接口标识符(UUID),以及客户端可以使用的一组方法。接口使用特殊的接口定义语言(Interface Definition Language)来描述,接口文件的扩展名为 _.idl_。让我们来看一个示例。

[
uuid(AB4ED934-1293-10DE-BC12-AE18C48DEF33),
&nbsp;version(1.0),
]
interface Calculator
{
intAdd(
&nbsp; [in]&nbsp;handle_t&nbsp;hBinding,
&nbsp; [in]&nbsp;int&nbsp;a,
&nbsp; [in]&nbsp;int&nbsp;b
&nbsp;);
}

接口文件遵循一种特殊的描述语言 MIDL(Microsoft Interface Definition Language,微软接口定义语言)。值得注意的是,接口文件不仅是我们开发者需要的,一个叫做 MIDL 编译器的特殊编译器也需要它。MIDL 编译器用于生成所谓的存根(Stub)。存根用于在客户端进程和服务器进程之间传递参数。这个过程称为编组(Marshalling)

为什么需要编组?

通常,RPC 服务器提供的函数远比我示例中的复杂得多。这些复杂函数接受许多参数,包括大型数据结构:列表、字典、完整的类和结构体。要正确传输这类数据,需要将其转换为适合通过网络传输的格式。这就是编组所负责的工作。

因此,如果你有一个描述 RPC 服务器功能的 IDL 文件,就可以生成所有必要的存根,以便正确地通过网络传输 RPC 函数所需的参数。

生成过程是自动化的,通常使用 midl.exe程序完成。

midl.exe /app_config MyInterface.idl

之后,当前文件夹中会出现几个文件。这些就是存根文件。我们稍后将看到如何在程序中使用它们。

MIDL Compiler Output

  • MyInterface_c.c

    — 客户端存根;

  • MyInterface_s.c

    — 服务器端存根;

  • MyInterface_h.h

    — 头文件。

如果查看这些文件的内部,你会看到许多以 Ndr*开头的函数。例如 NdrClientCall2()或 NdrServerCall3()。在这里我们需要了解另一个机制。它叫做网络数据表示(Network Data Representation,NDR)引擎。它负责对通过 RPC 传输的数据进行编组。

总结一下。我们有一个客户端和一个服务器,还有一个包含服务器接口定义的文件。我们可以使用 MIDL 编译这个接口,获得存根,并使用存根调用服务器上的函数。参数传递将通过编组过程完成,该过程使用 NDR 机制。此外还有 RPC Runtime(RPC 运行时)。这是一组库,实现了编组数据的传输以及所有必要的 RPC 函数。一个例子是 rpcrt4.dll.

RPC 绑定

让我再介绍两个术语。客户端连接到服务器的过程称为 RPC 绑定(RPC Binding)。服务器导出可用的接口,客户端知道它们的 UUID 后就可以连接。准确地说,客户端并不是连接到接口,而是使用一种叫做 RPC 字符串绑定(RPC String Binding)的特殊字符串连接到端点。然后,如果客户端需要的接口配置在这个端点上,就会调用所需的 RPC 方法。RPC 字符串绑定就是一个指向特定端点的字符串序列。

# Connect over TCP
ncacn_ip_tcp:192.168.0.24[49664]

# Connect over SMB
ncacn_np:\\DESKTOP-0PRT7UI[\pipe\lsass]

RPC 字符串绑定本身并不像乍看之下那么简单。该字符串具有以下格式。

InterfaceUUID@ProtocolSequence:NetworkAddress[Endpoint,Option]
  • ObjectUUID

    — 用于调用方法的接口 UUID。即 RPC 接口 ID。通常不指定;

  • ProtocolSequence

    — RPC 用于通过网络传输数据的协议。它是 RPC 协议(如 ncacn)、传输协议(如 TCP)和网络协议(如 IP)的组合。MSRPC 支持以下类型。以下是一些最常见的 RPC 协议组合的说明。

NCACN (Network Computing Architecture CoNnection oriented protocol): RPC over a TCP connection

NCADG (Network Computing Architecture Datagrame Protocol): RPC over a UDP connection

NCALRPC (Network Computing Architecture Local Remote Procedure): RPC through a local connection
  • NetworkAddress

    — 将接收 RPC 调用的系统的远程地址。此字段的格式和内容取决于指定的 ProtocolSequence

MSDN Documentation

  • Endpoint

    Options— 可选字段,用于更精确地指定端点。更详细的文档可在 MSDN 中每个特定 ProtocolSequence的说明中找到。

一旦我们有了 RPC 字符串绑定,就可以连接到 RPC 服务器。如果连接成功,我们将获得一个 RPC 绑定句柄(RPC Binding Handle),通过它可以与目标系统进行交互。

客户端 – 服务器连接算法

上层视角

客户端使用字符串绑定连接到 RPC 服务器,调用远程过程,将参数和附加信息编组到消息中,并将消息发送到服务器。服务器收到消息后,解组其内容,执行请求的操作,并将结果发送回客户端。服务器存根和客户端存根负责参数的编组和解组。远程过程调用的机制如下图所示。

+-------------------------------+ &nbsp; &nbsp; +--------------------------------+
| &nbsp; &nbsp; &nbsp; &nbsp;Client Machine &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; Server Machine &nbsp; &nbsp; &nbsp; &nbsp; |
+-------------------------------+ &nbsp; &nbsp; +--------------------------------+
| &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
| &nbsp;+-------------------------+ &nbsp;| &nbsp; &nbsp; | &nbsp;+--------------------------+ &nbsp;|
| &nbsp;| &nbsp; &nbsp; &nbsp; &nbsp;Client &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp; &nbsp; &nbsp; &nbsp; Server &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp;|
| &nbsp;| &nbsp;+-------------------+ &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;+--------------------+ &nbsp;| &nbsp;|
| &nbsp;| &nbsp;| Return &nbsp;| &nbsp;Call &nbsp; | &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;| &nbsp;Call &nbsp; | Return &nbsp; | &nbsp;| &nbsp;|
| &nbsp;| &nbsp;+-------------------+ &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;+--------------------+ &nbsp;| &nbsp;|
| &nbsp;+------------|------------+ &nbsp;| &nbsp; &nbsp; | &nbsp;+------------|-------------+ &nbsp;|
| &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
| &nbsp;+------------|------------+ &nbsp;| &nbsp; &nbsp; | &nbsp;+------------|-------------+ &nbsp;|
| &nbsp;| &nbsp; &nbsp; &nbsp;Client stub &nbsp; &nbsp; &nbsp; &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp; &nbsp; &nbsp;Server stub &nbsp; &nbsp; &nbsp; &nbsp;| &nbsp; |
| &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp; |
| &nbsp;| &nbsp;| Unpack | | &nbsp;Pack &nbsp;| &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;| Unpack | | &nbsp;Pack &nbsp;| &nbsp;| &nbsp; |
| &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp; |
| &nbsp;+------------|------------+ &nbsp;| &nbsp; &nbsp; | &nbsp;+------------|-------------+ &nbsp;|
| &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
| &nbsp;+------------|------------+ &nbsp;| &nbsp; &nbsp; | &nbsp;+------------|-------------+ &nbsp;|
| &nbsp;| &nbsp; &nbsp; &nbsp;RPC Runtime &nbsp; &nbsp; &nbsp; &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp; &nbsp; &nbsp;RPC Runtime &nbsp; &nbsp; &nbsp; &nbsp;| &nbsp; |
| &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp; |
| &nbsp;| &nbsp;|Receive | | &nbsp;Send &nbsp;| &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;|Receive | | &nbsp;Send &nbsp;| &nbsp;| &nbsp; |
| &nbsp;| &nbsp;| &nbsp; &nbsp; &nbsp; &nbsp;| | &nbsp; &nbsp; &nbsp; &nbsp;| &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;| &nbsp; &nbsp; &nbsp; &nbsp;| | &nbsp; &nbsp; &nbsp; &nbsp;| &nbsp;| &nbsp; |
| &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp;| &nbsp; &nbsp; | &nbsp;| &nbsp;+--------+ +--------+ &nbsp;| &nbsp; |
| &nbsp;+------------|------------+ &nbsp;| &nbsp; &nbsp; | &nbsp;+------------|-------------+ &nbsp;|
| &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
+---------------|----------------+ &nbsp; &nbsp; +---------------|----------------+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Call Packet &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; |------------------------------------->|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Result Packet &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; |<-------------------------------------|
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
+---------------|----------------+ &nbsp; &nbsp; +---------------|----------------+

底层视角

  • 首先,服务器开发者创建希望通过 RPC 提供的函数。
  • 然后,开发者创建一个接口文件(_.idl_),以便客户端可以生成存根。这个接口文件会传递给客户端。值得注意的是,如果客户端具备一定的 RPC 服务器分析能力,也可以自己创建 IDL 文件,但这个话题稍后再谈。
  • 服务器使用 RpcServerRegisterIf2() 函数在 RPC Runtime Library 中注册其接口
# MyInterface_s.c (Generated by MIDL)
staticconst&nbsp;RPC_SERVER_INTERFACE MyInterface___RpcServerInterface =
&nbsp; &nbsp; {
sizeof(RPC_SERVER_INTERFACE),
&nbsp; &nbsp; {{0xd6b1ad2b,0xb550,0x4729,{0xb6,0xc2,0x16,0x51,0xf5,0x84,0x80,0xc3}},{1,0}},
&nbsp; &nbsp; {{0x8A885D04,0x1CEB,0x11C9,{0x9F,0xE8,0x08,0x00,0x2B,0x10,0x48,0x60}},{2,0}},
&nbsp; &nbsp; (RPC_DISPATCH_TABLE*)&MyInterface_v1_0_DispatchTable,
0,
0,
0,
&nbsp; &nbsp; &MyInterface_ServerInfo,
0x06000000
&nbsp; &nbsp; };
RPC_IF_HANDLE MyInterface_v1_0_s_ifspec = (RPC_IF_HANDLE)& MyInterface___RpcServerInterface;

# MyInterface_h.h (Generated by MIDL)
extern&nbsp;RPC_IF_HANDLE MyInterface_v1_0_s_ifspec;

# RpcServer.cpp
RpcServerRegisterIf2(
&nbsp; MyInterface_v1_0_s_ifspec,
NULL,
NULL,
0,
&nbsp; RPC_C_LISTEN_MAX_CALLS_DEFAULT,
&nbsp; (unsigned)-1,
&nbsp; NULL);
  • 服务器填充有关端点的信息,通过这些端点可以访问接口中描述的功能。这通过 RpcServerUseProtseq() 函数完成。以下是 TCP RPC 服务器的示例。
RPC_WSTR pszProtSeq = (RPC_WSTR)L"ncacn_ip_tcp";
RPC_WSTR pszTCPPort = (RPC_WSTR)L"1337";

RpcServerUseProtseqEp(
&nbsp; pszProtSeq,
&nbsp; RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
&nbsp; pszTCPPort,
NULL
&nbsp;);

现在我们有两个选择。我们可以设置认证信息并开始等待客户端连接,或者我们可以了解动态端点机制。由于我试图做最全面的介绍,我建议先了解什么是 RPC 动态端点。

— — 底层暂停 — —

RPC 端点类型

有两种主要的端点类型:RPC 已知端点(RPC Well-Known)和 RPC 动态端点(RPC Dynamic Endpoint)。RPC 已知端点是客户端可以直接连接的端点,客户端立即知道端点的完整地址。这类端点的示例:ncacn_ip_tcp:10.10.10.10[4444]或 ncacn_np:\\10.10.10.10[\pipe\lsass]

然而,还有第二种类型的端点 – 动态端点。在这种情况下,客户端只知道使用的协议序列和地址。示例:ncacn_ip_tcp:10.10.10.10或 ncacn_np:\\10.10.10.10

在这种情况下,客户端需要联系一个特殊的 Endpoint Mapper 服务来获取完整的 RPC 字符串绑定。首先,服务器在此服务中注册,由服务分配一个具有所需协议序列的空闲端点。之后,服务器开始监听。当客户端想要连接到服务器时,它联系 epmapper 服务,提供服务器接口,服务返回连接所需的数据。例如,缺少的 TCP 端口。

让我们看一些示例代码。

以下是 RPC 已知端点注册。

RPC_STATUS rpcStatus;
// Interface Registration
rpcStatus = RpcServerRegisterIf2(...);
if&nbsp;(rpcStatus != RPC_S_OK)
return;

// Well-Known Endpoint
RpcServerUseProtseqEp(
&nbsp; &nbsp; (RPC_WSTR)L"ncacn_ip_tcp",
&nbsp; &nbsp; RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
&nbsp; &nbsp; (RPC_WSTR)L"1337",
&nbsp; &nbsp; NULL
);

// Start Listening
RpcServerListen(
1,
&nbsp; &nbsp; RPC_C_LISTEN_MAX_CALLS_DEFAULT,
FALSE
);

以下是 RPC 动态端点注册。

RPC_STATUS rpcStatus;
// Interface Registration
rpcStatus = RpcServerRegisterIf2(...);
if&nbsp;(rpcStatus != RPC_S_OK)
return;

// Well-Known Endpoint
RpcServerUseProtseqEp(
&nbsp; &nbsp; (RPC_WSTR)L"ncacn_ip_tcp",
&nbsp; &nbsp; RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
&nbsp; &nbsp; NULL &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// port missing (that's NULL for security descriptor)
);

// Registration in the epmapper service
RPC_BINDING_VECTOR* pbindingVector =&nbsp;0;
RpcServerInqBindings(&pbindingVector);

RpcEpRegister(
&nbsp; &nbsp; MyInterface_v1_0_s_ifspec,
&nbsp; &nbsp; pbindingVector,
0,
&nbsp; &nbsp; (RPC_WSTR)L"MyDynamicIFace"
);

// Start Listening
RpcServerListen(
1,
&nbsp; &nbsp; RPC_C_LISTEN_MAX_CALLS_DEFAULT,
FALSE
);

如你所见,增加了对 RpcServerInqBindings() 和 RpcEpRegister() 函数的调用,用于注册动态端点。

  • 注册端点后,服务器可以选择使用 RpcServerRegisterAuthInfo() 配置认证。

RPC 认证由称为 RPC Security Packages 的特殊提供程序提供。以下是可用提供程序的列表。

unsignedlong&nbsp;dwAuthnSvc = RPC_C_AUTHN_WINNT;
RpcServerRegisterAuthInfo(null, dwAuthnSvc,&nbsp;NULL,&nbsp;NULL);

顺便说一下,如果我没记错的话,这就是从 CoInitializeSecurity()调用的函数。而 CoInitializeSecurity()函数被用在 RemoteKrbRelay 工具中来滥用 Kerberos 票据中继。实际上,在这个函数中我们可以提供任意的 SPN,客户端就会使用这个 SPN 来连接我们。代码见此处。

  • 最后,服务器可以使用 RpcServerListen() 开始监听客户端。
RpcServerListen(
1,
&nbsp; &nbsp; RPC_C_LISTEN_MAX_CALLS_DEFAULT,
FALSE
);

现在让我们看看客户端部分!

  • 客户端通过 RpcStringBindingCompose() 和 RpcBindingFromStringBinding() 函数创建绑定句柄
// Glues into one line
RpcStringBindingComposeW(RPC_UUID, (RPC_WSTR)L"ncacn_np", (RPC_WSTR)host, InterfaceAddress, NULL, &StringBinding);
// Create Variable
RPC_WSTR bindingHandle =&nbsp;0;
RpcBindingFromStringBindingW(StringBinding, &bindingHandle);
  • 客户端调用 RPC Runtime 库,该库向 RPC 服务器发起调用并从 Endpoint Mapper 服务请求端点信息。值得注意的是,此行为仅适用于动态端点。此操作由 RPC Runtime Library 执行。我们不需要调用任何方法。但是,如果你想的话,可以调用 RpcEpResolveBinding()
RpcEpResolveBinding(
&nbsp; &nbsp; &nbsp; &nbsp;bindingHandle,
&nbsp; &nbsp; &nbsp; &nbsp;MyInterface_v1_0_c_ifspec
&nbsp;);
  • 客户端可以通过 RpcBindingSetAuthInfo() 在端点上指定认证参数;
  • 客户端调用某个 RPC 方法。RPC 方法在接口文件中声明;
RpcTryExcept {
Add(bindingHandle,&nbsp;2&nbsp;,3);
}
  • 客户端 RPC Runtime 库将传递给方法的参数编组为 NDR 格式并发送到服务器;
  • 服务器 RPC Runtime 接收数据并解组,以便将其传递给 RPC 方法代码;
  • 当服务器上被调用的方法返回控制流时,服务器存根接收声明为 [out]和 [in,out](在 IDL 文件中定义)的参数值,以及方法的返回值,将它们编组并将编组后的数据发送回客户端。

成功 : )

RPC 句柄绑定类型

在看一些代码示例之前,我想再介绍几个基本术语。在 RPC 中,还有不同类型的句柄绑定。总体上分为显式句柄绑定(Explicit Handle Binding)隐式句柄绑定(Implicit Handle Binding)

绑定类型在 idl 文件中定义。在上面的示例中,我们使用的是显式句柄绑定。

# THAT'S EXPLICIT
[
&nbsp;uuid(AB4ED999-1233-10AA-BC67-BB12C48GHM33),
&nbsp;version(1.0),
]
interface Calculator
{
&nbsp;int Add( &nbsp;[in] handle_t hBinding,
&nbsp; [in, string] int a,
&nbsp; [in] int b );
}

RpcTryExcept {
&nbsp; Add(bindingHandle, 2 ,3);
}

# THAT'S IMPLICIT
[
uuid(AB4ED999-1233-10AA-BC67-BB12C48GHM33),
&nbsp;version(1.0),
&nbsp;implicit_handle(handle_t&nbsp;hBinding)
]
interface Calculator
{
intAdd( &nbsp;[in, string]&nbsp;int&nbsp;a,
&nbsp; [in]&nbsp;int&nbsp;b );
}

RpcTryExcept {
Add(2&nbsp;,3);
}

你看到区别了吗?

显式绑定要求在每次 RPC 调用时显式传递绑定句柄。这允许在多线程应用程序中使用同一个绑定句柄。

隐式绑定用于单线程应用程序。它不在每次函数调用时传递绑定句柄。

NDR 引擎深入探讨

你可能想更深入地了解 NDR 引擎的工作原理。在那种情况下,你会遇到真正的恐惧!大量嵌套的结构体和类、指针以及完全的混乱 🙂 不过,很久以前我在 X 上(感谢 sixtyvividtails)看到了这个精美的可视化图。现在分享给你。

实际上,这是对 Ndr-函数内部发生了什么以及使用了哪些结构的描述。

NDR Engine Visualization

RPC 示例

以下是使用已知端点的客户端 – 服务器程序示例。我们有以下接口。

[
uuid(AB4ED934-1293-10DE-BC12-AE18C48DEF33),
&nbsp;version(1.0),
]
interface Calculator
{
intAdd(
&nbsp; [in]&nbsp;handle_t&nbsp;hBinding,
&nbsp; [in]&nbsp;int&nbsp;a,
&nbsp; [in]&nbsp;int&nbsp;b
&nbsp;);
}

我们打开 Visual Studio,将项目命名为 Server,将此文件添加到项目中,命名为 interface.idl。然后在解决方案中创建另一个项目。将其命名为 Client。应该如下所示

Visual Studio RPC Project Example

将以下代码粘贴到 Client.cpp 和 Server.cpp 中。

// Client.cpp
#include<windows.h>
#include<stdio.h>
#include"interface_h.h"
#pragma&nbsp;comment(lib, "rpcrt4.lib")

intmain() {
&nbsp; &nbsp; RPC_STATUS status;
&nbsp; &nbsp; RPC_WSTR stringBinding =&nbsp;NULL;
int&nbsp;result;
handle_t&nbsp;hBinding =&nbsp;NULL;

&nbsp; &nbsp; status =&nbsp;RpcStringBindingCompose(
NULL,
&nbsp; &nbsp; &nbsp; &nbsp; (RPC_WSTR)L"ncacn_ip_tcp",
&nbsp; &nbsp; &nbsp; &nbsp; (RPC_WSTR)L"localhost",
&nbsp; &nbsp; &nbsp; &nbsp; (RPC_WSTR)L"1337",
NULL,
&nbsp; &nbsp; &nbsp; &nbsp; &stringBinding);

if&nbsp;(status) {
printf("RpcStringBindingCompose failed: %d\n", status);
return&nbsp;status;
&nbsp; &nbsp; }

&nbsp; &nbsp; status =&nbsp;RpcBindingFromStringBinding(stringBinding, &hBinding);
if&nbsp;(status) {
printf("RpcBindingFromStringBinding failed: %d\n", status);
return&nbsp;status;
&nbsp; &nbsp; }

&nbsp; &nbsp; RpcTryExcept{
&nbsp; &nbsp; &nbsp; &nbsp; result =&nbsp;Add(hBinding,&nbsp;5,&nbsp;3);
printf("5 + 3 = %d\n", result);

&nbsp; &nbsp; &nbsp; &nbsp; result =&nbsp;Add(hBinding,&nbsp;10,&nbsp;20);
printf("10 + 20 = %d\n", result);
&nbsp; &nbsp; }
RpcExcept(1)
&nbsp; &nbsp; {
printf("Runtime reported exception %d\n",&nbsp;RpcExceptionCode());
&nbsp; &nbsp; }
&nbsp; &nbsp; RpcEndExcept;

RpcStringFree(&stringBinding);
RpcBindingFree(&hBinding);

return0;
}

void* __RPC_USER&nbsp;midl_user_allocate(size_t&nbsp;size) {
returnmalloc(size);
}

void&nbsp;__RPC_USER&nbsp;midl_user_free(void* p) {
free(p);
}
// Server.cpp
#include<windows.h>
#include<stdio.h>
#include"interface_h.h"
#pragma&nbsp;comment(lib, "rpcrt4.lib")

intAdd(handle_t&nbsp;hBinding,&nbsp;int&nbsp;a,&nbsp;int&nbsp;b) {
printf("Received numbers: %d and %d\n", a, b);
return&nbsp;a + b;
}

intmain() {
&nbsp; &nbsp; RPC_STATUS status;
&nbsp; &nbsp; RPC_BINDING_VECTOR* bindingVector =&nbsp;NULL;

&nbsp; &nbsp; status =&nbsp;RpcServerUseProtseqEp(
&nbsp; &nbsp; &nbsp; &nbsp; (RPC_WSTR)L"ncacn_ip_tcp",
&nbsp; &nbsp; &nbsp; &nbsp; RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
&nbsp; &nbsp; &nbsp; &nbsp; (RPC_WSTR)L"1337",
NULL
&nbsp; &nbsp; );

if&nbsp;(status) {
printf("RpcServerUseProtseq failed: %d\n", status);
return&nbsp;status;
&nbsp; &nbsp; }

&nbsp; &nbsp; status =&nbsp;RpcServerRegisterIf2(
&nbsp; &nbsp; &nbsp; &nbsp; Calculator_v1_0_s_ifspec,
NULL,
NULL,
0,
&nbsp; &nbsp; &nbsp; &nbsp; RPC_C_LISTEN_MAX_CALLS_DEFAULT,
&nbsp; &nbsp; &nbsp; &nbsp; (unsigned)-1,
NULL);

if&nbsp;(status) {
printf("RpcServerRegisterIf2 failed: %d\n", status);
return&nbsp;status;
&nbsp; &nbsp; }

printf("Calculator Server is listening...\n");
&nbsp; &nbsp; status =&nbsp;RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT,&nbsp;FALSE);
if&nbsp;(status) {
printf("RpcServerListen failed: %d\n", status);
return&nbsp;status;
&nbsp; &nbsp; }

return0;
}

void* __RPC_USER&nbsp;midl_user_allocate(size_t&nbsp;size) {
returnmalloc(size);
}

void&nbsp;__RPC_USER&nbsp;midl_user_free(void* p) {
free(p);
}
// interface_h.h (FOR REVIEW ONLY!! DO NOT ATTACH TO THE PROJECT!! U'LL COMPILE IT FROM IDL FILE)

/* this ALWAYS GENERATED file contains the definitions for the interfaces */

/* File created by MIDL compiler version 8.01.0628 */
/* at Tue Jan 19 08:14:07 2038
&nbsp;*/
/* Compiler settings for interface.idl:
&nbsp; &nbsp; Oicf, W1, Zp8, env=Win64 (32b run), target_arch=AMD64 8.01.0628
&nbsp; &nbsp; protocol : all , ms_ext, c_ext, robust
&nbsp; &nbsp; error checks: allocation ref bounds_check enum stub_data
&nbsp; &nbsp; VC __declspec() decoration level:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;__declspec(uuid()), __declspec(selectany), __declspec(novtable)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DECLSPEC_UUID(), MIDL_INTERFACE()
*/
/* @@MIDL_FILE_HEADING( &nbsp;) */

/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef&nbsp;__REQUIRED_RPCNDR_H_VERSION__
#define__REQUIRED_RPCNDR_H_VERSION__500
#endif

#include"rpc.h"
#include"rpcndr.h"

#ifndef&nbsp;__RPCNDR_H_VERSION__
#error&nbsp;this stub requires an updated version of <rpcndr.h>
#endif/* __RPCNDR_H_VERSION__ */

#ifndef&nbsp;__interface_h_h__
#define__interface_h_h__

#if&nbsp;defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma&nbsp;once
#endif

#ifndef&nbsp;DECLSPEC_XFGVIRT
#if&nbsp;defined(_CONTROL_FLOW_GUARD_XFG)
#defineDECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
#else
#defineDECLSPEC_XFGVIRT(base, func)
#endif
#endif

/* Forward Declarations */

#ifdef&nbsp;__cplusplus
extern"C"{
#endif

#ifndef&nbsp;__Calculator_INTERFACE_DEFINED__
#define__Calculator_INTERFACE_DEFINED__

/* interface Calculator */
/* [version][uuid] */

intAdd(
/* [in] */handle_t&nbsp;hBinding,
/* [in] */int&nbsp;a,
/* [in] */int&nbsp;b);

extern&nbsp;RPC_IF_HANDLE Calculator_v1_0_c_ifspec;
extern&nbsp;RPC_IF_HANDLE Calculator_v1_0_s_ifspec;
#endif/* __Calculator_INTERFACE_DEFINED__ */

/* Additional Prototypes for ALL interfaces */

/* end of Additional Prototypes */

#ifdef&nbsp;__cplusplus
}
#endif

#endif

#interface_c.c (FOR REVIEW ONLY!! DO NOT ATTACH TO THE PROJECT!! U'LL COMPILE IT FROM IDL FILE)
https://gist.github.com/MzHmO/1d7cb961771a03065364a916cacbbed8

#interface_s.c (FOR REVIEW ONLY!! DO NOT ATTACH TO THE PROJECT!! U'LL COMPILE IT FROM IDL&nbsp;FILE)
https://gist.github.com/MzHmO/56370b75a25417482789d788b53589ea

然后点击构建解决方案按钮。在你的主目录中将出现由 midl 编译器生成的文件。

MIDL Files

将 _s文件添加到服务器项目中,将 _c文件添加到客户端项目中,并在两个项目中都包含 _h头文件。

Project structure with MIDL files

最后,运行项目并检查一切是否正常工作。

RPC Server Example

我还推荐这个优秀的仓库,其中展示了相当多不同的 RPC 客户端和服务器,包括隐式和显式绑定。此外,示例还包含认证设置。我们将在第二部分讨论 RPC 安全性。不过,你可以选择性地先研究代码!

我还与你分享一个 C# RPC 客户端的仓库。另外请关注 SharpSystemTriggers。它展示了如何在 C# 中使用接口的 MIDL 表示。

C# Interface Definition Example

如果你对 Powershell 开发感兴趣,我推荐这个仓库。它之所以出色,是因为包含了在最底层实现所有 RPC 机制的代码。

Pure RPC via Powershell

一些故障排除

你可以看到从 IDL 文件成功创建了 MyInteface_c.c和 MyInterface_s.c存根,但在尝试编译时会遇到错误:C2011LNK2001

C2011 Error

C2011 Error

要解决 LNK2001 错误,请在代码中添加以下内容。

#defineWIN32_LEAN_AND_MEAN
#include<Windows.h>
#include<rpc.h>

void&nbsp;__RPC_FAR* __RPC_API&nbsp;midl_user_allocate(size_t&nbsp;cBytes);
void&nbsp;__RPC_API&nbsp;midl_user_free(void&nbsp;__RPC_FAR* p);

void&nbsp;__RPC_FAR* __RPC_API&nbsp;midl_user_allocate(size_t&nbsp;cBytes)
{
returnHeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cBytes);
}

void&nbsp;__RPC_API&nbsp;midl_user_free(void&nbsp;__RPC_FAR* p)
{
HeapFree(GetProcessHeap(),&nbsp;0, p);
}

如果提示找不到某个 Ndr 函数,只需将程序链接到 rpcrt4.lib即可。

#pragma&nbsp;comment(lib, "Rpcrt4.lib")

C2011 问题是由头文件中的类型重定义引起的。你应该尝试简单地删除 _h.h文件中重复定义的所有数据结构。

总结

在本文中,我们学习了 RPC 的基础知识,朝着使用 Impacket 开发程序的目标又迈进了一步。在下一部分中,我们将更深入地了解 RPC 安全性,以及学习如何探索系统上可用的 RPC 服务器。


免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment CICADA8 CICADA8《Impacket 开发指南:第一部分 – RPC 远程过程调用深入解析》

评论:0   参与:  0