Rpc学习笔记

0x00

感觉回到了学习c语言的时光,嘿嘿,下面是理论部分,本菜鸡初学,如果错误请指出。

Rpc(远程过程调用):基于网络端口而实现,支持协议:https://docs.microsoft.com/en-us/windows/win32/rpc/string-binding。其实Rpc是广义的,RPC可以发生在不同的主机之间,也可以发生在同一台主机上,发生在同一台主机上就是LPC。

而本地过程调用(LPC):是压栈直接调函数,远程过程调用也是调函数,但是在调用另一个进程的函数,而为了区分调用哪个函数设置了一些标识,这些表示则对应两个进程的对应的函数,所以客户端传给服务端不仅仅需要传递函数的参数还需要给那些标识表示调用哪个函数。

IDL文件:为了统一客户端与服务端不同平台处理不同的实现,于是有了 IDL语言。IDL文件由一个或多个接口定义组成,每一个接口定义都有一个接口头和一个接口体,接口头包含了使用此接口的信息(UUID和接口版本 ),接口体包含了接口函数的原型

UUID:通常为一个16长度的标识符,具有唯一性,在Rpc通信模型中,Rpc运行时库使用UUID来检查客户端和服务器之间的兼容性,也使用它在注册表中配置自身。UUID是众所周知的标识符,对于每个服务都是唯一的,并且在所有平台上都很常见。通常平台会提供生成的API。用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字.

ACF文件(RPC 应用程序配置文件):Rpc应用程序使用 ACF 文件来描述特定于硬件和操作系统的接口的特性,和IDL文件一起由MIDL编译,所以MIDL 编译器(vs自带)可以为不同的平台和系统版本生成代码,它不是必须的,我试过。

由它们(ACF/IDL)编译生成后的文件用于描述调用方和被调用过程之间的数据交换和函数原型和参数传递机制。

提到传递机制,就要说到Rpc序列化,(其实就是将传输给服务端的参数转成二进制流,服务端接受到后在反序列化一遍),而这些实现在RpcNDR引擎,而RpcNDR引擎发送的数据依赖于idl/.acf 文件编译后生成的存根文件。

Microsoft Rpc序列化内容:过程序列化(序列化绑定句柄),类型序列化(序列化接口函数参数)。

序列化过程必须使用序列化句柄才能使用序列化服务,序列化句柄在Rpc通信非常重要,如果把这个参数弄差错了,会影响到编译和通信,序列化句柄分为显示句柄和隐式句柄, ACF 中可以设置它,如果没有ACF文件标头中写上 implicit_handle(handle_t test_Binding),那么句柄类型为显示句柄。如果写上explicit_handle(handle_t test_Binding),那么句柄类型为隐式句柄。

对于显示句柄,MIDL 编译器将生成使用显式句柄作为序列化句柄的支持例程。调用序列化过程或序列化类型支持例程时,必须提供有效的序列化句柄。

对于隐式句柄,MIDL 编译器将生成全局序列化句柄变量。具有隐式句柄的序列化过程使用此全局变量来访问有效的序列化上下文(在生成的.h文件里面,一个给客户端用,一个给服务端用)。。

不知道为什么,从我根据MSDN描述和MIDL 默认生成的代码来看,不管是声明explicit_handle还是implicit_handle或者干脆去掉没有.acf文件,MIDL 编译器每次会生成类型为 handle_t 的全局句柄变量,都是隐式句柄。

Hello Rpc

在微软的RPC模型中,接口,服务端和客户端这三样是必须的,而服务端和客户端都依赖接口,所以先得开发接口(.idl/.acf )。

vs新建空项目,添加.idl和.acf 文件 (.acf文件可有可无)

rpc.acf

//接口头
[
	implicit_handle(handle_t test_Binding)
]
//接口体
interface HelloWorld
{
}

idl.idl

import "oaidl.idl";
import "ocidl.idl";
//接口头
[
	//进入此网址获得一个uuid    https://www.guidgen.com
	uuid("c8f409b7-0461-4374-b06a-3bfda33e968f"),
	version(1.0)
]
//接口体
interface HelloWorld
{
	int intAdd(int x, int y);
}

注意标头里的的语法是以逗号结束,最后一句加不加逗号都可以。

编译后会在idl文件所在目录生成idl_s.c,idl_s.h,idl_s.c文件,这里生产的文件感觉就像是实现java rpc中的代理功能,接口和序列化实现都在这些文件里,如果没有这文件,rpc和传统的socket没有区别,都是传输内存而已,但是有了接口才能方便的调用服务器的函数,而不用关心具体实现,也许这就是rpc的好处吧。

接着将idl_s.c,idl_s.h放到服务端,将idl_s.c,idl_s.h放到客户端就可以开始编写相应代码了,

第一次编写通信的代码抄的https://blog.csdn.net/herojuice/article/details/81015325,可能需要注意的会在接口函数原型前面会生成一个handle_t,服务端和客户端实现里对应位置修改最前面加个参数即可。

在这里记录下api最简单的调用过程

服务端调用过程:

RpcServerUseProtseqEp 函数告诉 RPC 运行时库使用指定的协议序列与指定的终结点组合来接收远程过程调用。

RpcServerRegisterIfEx函数使用 RPC 运行时库注册接口。

RpcServerListen函数向 RPC 运行时库发出信号,以监听远程过程调用。

客户端调用过程:


RpcStringBindingCompose	生成绑定句柄的字符串。
RpcBindingFromStringBinding 绑定函数从绑定句柄的字符串表示形式返回绑定句柄。

因为远程过程调用出错的几率比本地过程调用的几率高,而且传统的sockre编程中,一个良好的代码肯定是要有异常处理的,Rpc也不例外
try-except

    RpcTryExcept{
               //检查的代码
   }
   RpcExcept(1){
      printf("Runtime reported exception:%d,except=%d\n",GetLastError(),RpcExceptionCode());
   }

代码实现参考:
Rpc教程
https://docs.microsoft.com/en-us/windows/win32/rpc/tutorial

Rpc绑定函数:
https://docs.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-rpcstringbindingcompose

返回错误值的意思
https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-return-values

Rpc教程
https://www.cnblogs.com/xxj-bigshow/p/9155162.html

关于句柄
https://docs.microsoft.com/en-us/windows/win32/rpc/implicit-versus-explicit-handles

RpcView探索

不同的厂商实现了不同的Rpc协议,这里主要捣鼓下windows内部使用的Rpc,对用户来说,它是隐藏的,你并不知道这个调用的方法是部署哪里。不过这并不代表我们无法让它们暴露出来。

我们完全可以使用RpcView来反编译idl接口
下载地址:https://github.com/silverf0x/RpcView 修改头文件编译也提示rpcrt4.dll也不适用
然后我找到了来自中国网友cbwang先生发布的去除rpcrt4.dll验证的版本https://github.com/cbwang505/RpcViewVS

按照指示搭好环境后再修改RpcCore.c路径为RpcCore4
Rpc学习笔记

然后再RpcInternals.h加了一些自己一些镜像的Rpcrt4.dll版本

	0xA00003FAB000FLL,  //10.0.16299.15
	0xA00003FAB00C0LL,  //10.0.16299.192
	0xA00003FAB0135LL,  //10.0.16299.309
	0xA00003FAB0173LL,  //10.0.16299.371
	0xA00003FAB01ECLL,  //10.0.16299.492
	0xA00003FAB02D6LL,  //10.0.16299.726
	0xA0000427903E8LL,  //10.0.17017.1000
	0xA0000428103E8LL,  //10.0.17025.1000
	0xA000042B203EALL,  //10.0.17074.1002
	0xA000042EE0001LL,  //10.0.17134.1
	0xA00004A610001LL,	//10.0.19041.1
	0xA00004A6101A7LL,	//10.0.19041.423
	0xA00004A6201FCLL,  //10.0.19042.508

Rpc学习笔记

其中Address是接口函数偏移,前面的Index分别对应interface接口函数声明里的proc。而Location则是目标rpc所在的文件位置。

请注意,反编译后重要的不是接口函数名字,重要的是函数参数和uuid以及版本,端口协议信息,因为名字和通信是否成功没有任何关系。

测试和上图接口函数通信,编译idl后绑定代码如下

	RPC_WSTR pszStringBinding = NULL;
	//绑定端口和UUid
	if (RpcStringBindingCompose(
		NULL,
		(RPC_WSTR)L"ncalrpc",
		0,//(RPC_WSTR)L"127.0.0.1",
		(RPC_WSTR)L"OLE707BE753EEB4C057129CB5C48E88",
		NULL,
		&pszStringBinding
	) == RPC_S_OK)
		printf("绑定成功");
	else
		printf("绑定失败");
	// 设置DefaultIfName_v0_0_c_ifspec  初始化隐式全局变量句柄,调用必须提供有效的序列化句柄。 也是真正接口函数原型的第一个参数
	if(RpcBindingFromStringBinding(pszStringBinding, &DefaultIfName_v0_0_c_ifspec)== RPC_S_OK)
		printf("绑定句柄成功");
	else
		printf("绑定句柄失败");

	short arg_1={1};
	long* arg_2 = { 1 };
	Struct_26_t** arg_3 = {1 };
    Struct_26_t** arg_4 = { 1 };
	// 下面是调用服务端的函数
	RpcTryExcept
	{
			
			Proc0(DefaultIfName_v0_0_c_ifspec, arg_1, arg_2, arg_3, arg_4);
	}
		RpcExcept(1)
	{
		printf("RPC Exception %d\n", RpcExceptionCode());
		Sleep(2000);
	}

处理接口处理函数
Rpc学习笔记

编译客户端运行后查看结果:

Rpc学习笔记

提示拒绝访问,以管理员运行还是拒绝访问。
Rpc学习笔记

后来刚好在网上看到一句话:从Windows XP SP2开始,增强了安全性的要求,如果用RpcServerRegisterIf()注册接口,客户端调用时会出现 // RpcExceptionCode() == 5,必须用RpcServerRegisterIfEx带RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH标志才能调用成功。

这个软件有uuid但是提供rpc接口路径是C:\Windows\System32\combase.dll,在我看过的百分之95以上的软件只要有uuid就是这个路径,所以基本不用试了,而windows自身倒是会启动非常多的的rpc服务。

Rpc通信原理探索

客户端和服务端是一台机器:不会发送任何数据包。这时使用的机制就是LPC。LPC不涉及网络控制,而是跨进程通信,关于LPC可参考《Windows内核情景分析》6.8节

当客户端和服务端不是一台机器时:
Rpc学习笔记

客户端绑定服务端时,并不会发送任何数据包,只有当调用服务端的函数时才会发送数据包,当远程过程调用首次调用时一共有九次交互:前三次是三次握手,第四次,五次互相发送一个数据包(未知意思,猜测是获取接口列表),第六,七次互相发送一个数据包(传参数,调函数),第八次是客户端向服务端发个包表示我接受到了(和三次握手中的第三次一样)。最后第九次则是客户端取消绑定时向服务端发送的最后一个数据包。
Rpc学习笔记
Rpc学习笔记

发送的数据都是经过序列化,当首次调用成功后每次通信触发远程调用时只会互相发送一个数据包,像第六,七次那样(传参数,调函数)

如果用套接字编程能实现远程过程调用它的接口吗?首先尝试用套接字绑定注册rpc服务时的端口和服务端ip,当发送数据时,可以看到通信数据是没有进行加密的,因为我们没有对序列化。

Rpc学习笔记
服务端没有返回特别的东西。

如果把数据换成rpc通信时第四次通信(客户端发送获取接口列表的数据)会怎么样?下面将rpc通信时的数据第四次通信拷贝成代码数组然后绑定发送。

Rpc学习笔记

        char buf[160]=
        {
            0x05,0x00,0x0B,0x03,0x10,0x00,0x00,0x00,
            0xA0,0x00,0x00,0x00,0x02,0x00,0x00,0x00,
            0xD0,0x16,0xD0,0x16,0x00,0x00,0x00,0x00,
            0x03,0x00,0x00,0x00,0x00,0x00,0x01,0x00,
            0x70,0x07,0xF7,0x18,0x64,0x8E,0xCF,0x11,
            0x9A,0xF1,0x00,0x20,0xAF,0x6E,0x72,0xF4,
            0x00,0x00,0x00,0x00,0x04,0x5D,0x88,0x8A,
            0xEB,0x1C,0xC9,0x11,0x9F,0xE8,0x08,0x00,
            0x2B,0x10,0x48,0x60,0x02,0x00,0x00,0x00,
            0x01,0x00,0x01,0x00,0x70,0x07,0xF7,0x18,
            0x64,0x8E,0xCF,0x11,0x9A,0xF1,0x00,0x20,
            0xAF,0x6E,0x72,0xF4,0x00,0x00,0x00,0x00,
            0x33,0x05,0x71,0x71,0xBA,0xBE,0x37,0x49,
            0x83,0x19,0xB5,0xDB,0xEF,0x9C,0xCC,0x36,
            0x01,0x00,0x00,0x00,0x02,0x00,0x01,0x00,
            0x70,0x07,0xF7,0x18,0x64,0x8E,0xCF,0x11,
            0x9A,0xF1,0x00,0x20,0xAF,0x6E,0x72,0xF4,
            0x00,0x00,0x00,0x00,0x2C,0x1C,0xB7,0x6C,
            0x12,0x98,0x40,0x45,0x03,0x00,0x00,0x00,
            0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00

        };
        send(clientSocket, buf, 160, 0);
        char revdata[100];
        ...

将返回结果和进行对比,下图左边是原来rpc通信,右边是传统套接字通信
Rpc学习笔记

能发现不同的地方在于只是传输控制协议的序列号和确认号以及互联网协议的标志位,而返回的数据和rpc通信时返回的数据几乎一模一样,如果再发送一个传参数调函数的数据,会怎么样呢,继续将原来客户端接着后面发送的那个包数据拷贝。

        char buf[160] =
        {
            0x05,0x00,0x0B,0x03,0x10,0x00,0x00,0x00,
            0xA0,0x00,0x00,0x00,0x02,0x00,0x00,0x00,
            0xD0,0x16,0xD0,0x16,0x00,0x00,0x00,0x00,
            0x03,0x00,0x00,0x00,0x00,0x00,0x01,0x00,
            0x70,0x07,0xF7,0x18,0x64,0x8E,0xCF,0x11,
            0x9A,0xF1,0x00,0x20,0xAF,0x6E,0x72,0xF4,
            0x00,0x00,0x00,0x00,0x04,0x5D,0x88,0x8A,
            0xEB,0x1C,0xC9,0x11,0x9F,0xE8,0x08,0x00,
            0x2B,0x10,0x48,0x60,0x02,0x00,0x00,0x00,
            0x01,0x00,0x01,0x00,0x70,0x07,0xF7,0x18,
            0x64,0x8E,0xCF,0x11,0x9A,0xF1,0x00,0x20,
            0xAF,0x6E,0x72,0xF4,0x00,0x00,0x00,0x00,
            0x33,0x05,0x71,0x71,0xBA,0xBE,0x37,0x49,
            0x83,0x19,0xB5,0xDB,0xEF,0x9C,0xCC,0x36,
            0x01,0x00,0x00,0x00,0x02,0x00,0x01,0x00,
            0x70,0x07,0xF7,0x18,0x64,0x8E,0xCF,0x11,
            0x9A,0xF1,0x00,0x20,0xAF,0x6E,0x72,0xF4,
            0x00,0x00,0x00,0x00,0x2C,0x1C,0xB7,0x6C,
            0x12,0x98,0x40,0x45,0x03,0x00,0x00,0x00,
            0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00

        };

        char buf2[26] =
        {
             0x05,0x00,0x00,0x03,0x10,0x00,0x00,0x00,
             0x1A,0x00,0x00,0x00,0x02,0x00,0x00,0x00,
             0x02,0x00,0x00,0x00,0x01,0x00,0x00,0x00,
             0xFF,0x00
        };
        send(clientSocket, buf, 160, 0);
        send(clientSocket, buf2, 26, 0);;

神奇的事情发生了:我成功调用了服务端的接口函数

我有一个奇怪的想法:我在想能否调用服务端未导出的接口函数,就是想call哪就call哪,但是这个验证需要再研究是通信时是如何序列化数据的,我暂时并不想花时间在这上面,emmmm。

如果是远程过程调用,传统的套接字编程一样可以调用服务端的接口函数,rpc的这些特征和传统套接字通信一模一样,只不过通信时你不知道他发送的是啥而已(序列化),实际上调用服务端的函数就是绑定正确的套接字然后发送一块内存。

扩展

hellorpc
https://github.com/muxq/hellorpc

rpc示例
https://github.com/gentilkiwi/basic_rpc

rpc挖洞
https://github.com/houjingyi233/ALPC-fuzz-study

rpc fuzz
https://github.com/sogeti-esec-lab/RPCForge

分析rpc
https://blog.xpnsec.com/analysing-rpc-with-ghidra-neo4j/

上一篇:CRC16


下一篇:824【毕设课设】基于单片机四路红外遥控开关电路设计