Windows RPC Study

之前MSRC微软放出了CVE-2022-26809这个漏洞,当时的评分有9.8那么高,我十分好奇这么高评分的漏洞到底是个怎么样子的洞,所以对其进行了简单的分析,不过一通分析过后,在个人有限的水平下,分析出来的结果是感觉这个漏洞好像挺理论洞的。。

为了能够更好的描述这个漏洞,首先要了解Windows下的RPC调用模式,所以这里可能要分成两个部分来讲漏洞。前面可能更加倾向于介绍RPC本身,摆出一堆术语,之后才能比较好的介绍漏洞本身。

RPC 基础知识

RPC为远程过程调用,分为Server端和Client端。其调用模式如图

由于RPC代码在编写过程中存在很多基本模式以及很多需要遵守的规则,所以一般开发平台提供一个叫做MIDL( Microsoft Interface Definition Language )的定义语言来生成Server和Client的对应接口,其后缀为.idl。当定义语言写好之后,使用midl.exe即可生成对应的桩(Stub)文件。桩(Stub)不做真正的工作,在RPC中它负责将调用的数据重新组织好,并且将数据传输到指定的远程主机侧完成系统调用。

在这里有很多微软提供的参考代码,可以通过这边学习一下整个RPC的调用过程。文章后面的内容也会从里面选取其中的Hello项目进行介绍。

实例:MIDL,与Stub的关系

假设interface文件如下

1
2
3
4
5
6
7
8
9
10
11
[ uuid (f691b703-f681-47dc-afcd-034b2faab911), // You must change this when you change the interface
version(1.0),
pointer_default(unique),
]
interface hello
{
void HelloProc([in] handle_t h1,
[in, string] unsigned char * pszString);

void Shutdown([in] handle_t h1);
}

我们通过输入

1
midl -oldnames -cpp_opt "-E"  hello.idl

可以生成如下的文件

1
2
3
hello.h   // 通用接口文件
hello_c.c // client端stub
hello_s.c // server端stub

可以简单看一下内容

hello.h

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
29
30
31
32
#ifdef __cplusplus
extern "C"{
#endif


#ifndef __hello_INTERFACE_DEFINED__
#define __hello_INTERFACE_DEFINED__

/* interface hello */
/* [implicit_handle][unique][version][uuid] */

void HelloProc(
/* [in] */ handle_t h1,
/* [string][in] */ unsigned char *pszString);

void Shutdown(
/* [in] */ handle_t h1);


extern handle_t hello_IfHandle;


extern RPC_IF_HANDLE hello_ClientIfHandle;
extern RPC_IF_HANDLE hello_ServerIfHandle;
#endif /* __hello_INTERFACE_DEFINED__ */

/* Additional Prototypes for ALL interfaces */

/* end of Additional Prototypes */

#ifdef __cplusplus
}

基本上是定义了一些基本的变量和对应的接口函数类型。其中

1
2
extern RPC_IF_HANDLE hello_ClientIfHandle;
extern RPC_IF_HANDLE hello_ServerIfHandle;

为RPC调用中会使用到的接口句柄,其本质为RpcInterfaceInformation,也就是RPC接口信息句柄,在之后注册RPC调用的时候会用到。

client侧

Client侧生成的文件信息如下(节选)

1
2
3
4
5
6
7
8
9
10
11
12
13
static const RPC_CLIENT_INTERFACE hello___RpcClientInterface =
{
sizeof(RPC_CLIENT_INTERFACE),
{{0xf691b703,0xf681,0x47dc,{0xaf,0xcd,0x03,0x4b,0x2f,0xaa,0xb9,0x11}},{1,0}}, // InterfaceId
{{0x8A885D04,0x1CEB,0x11C9,{0x9F,0xE8,0x08,0x00,0x2B,0x10,0x48,0x60}},{2,0}}, // TransferSyntax
0, //DispatchTable
0,
0,
0,
0,
0x00000000
};
RPC_IF_HANDLE hello_ClientIfHandle = (RPC_IF_HANDLE)& hello___RpcClientInterface;

可以看到,hello_ClientIfHandle在这边被hello___RpcClientInterface赋值。这个变量中记录的了一些在定义过程中能够知道的值:当前接口的接口IDInterface ID以及当前NDR(在RPC调用过程中用于描述传输单位的数据)使用的传输语法 TransferSyntax。传输语法可以定义当前NDR使用的语法。在MIDL中可以使用/protocol对其进行指定。结构体后方的变量会在运行时逐渐填充。

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
extern const MIDL_STUB_DESC hello_StubDesc;

static RPC_BINDING_HANDLE hello__MIDL_AutoBindHandle;

static const MIDL_STUB_DESC hello_StubDesc =
{
(void *)& hello___RpcClientInterface,
MIDL_user_allocate,
MIDL_user_free,
&hello_IfHandle,
0,
0,
0,
0,
hello__MIDL_TypeFormatString.Format,
1, /* -error bounds_check flag */
0x50002, /* Ndr library version */
0,
0x801026e, /* MIDL Version 8.1.622 */
0,
0,
0, /* notify & notify_flag routine table */
0x1, /* MIDL flag */
0, /* cs routines */
0, /* proxy/server info */
0
};

xxx_StubDesc变量中的xxxx为当前IDL文件中定义的接口的名字。这个变量存放了针对每个Stub的一些基本定义的。包括用于分配对象和释放对象的MIDL_user_allocateMIDL_user_free。这两个程序需要在主程序中声明,用于对对象进行内存管理。通过暴露这个接口,也方便后期进行数据的追踪。同时将前文的hello___RpcClientInterface绑定在Stub中,表明hello_StubDesc描述的是hello___RpcClientInterface接口句柄指向的Stubhello_IfHandle则为前文提到的,用于表示当前Stub的原始句柄。在通常情况下与hello___RpcClientInterface是等价的。但是当在接口中指明使用当权句柄的时候,一般会使用_IfHandle进行接口绑定。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

static const hello_MIDL_PROC_FORMAT_STRING hello__MIDL_ProcFormatString =
{
0,
{

/* Procedure HelloProc */

0x0, /* 0 */
0x48, /* Old Flags: */
/* 2 */ NdrFcLong( 0x0 ), /* 0 */
/* 6 */ NdrFcShort( 0x0 ), /* 0 */
/* 8 */ NdrFcShort( 0x10 ), /* X64 Stack size/offset = 16 */
/* 10 */ 0x32, /* FC_BIND_PRIMITIVE */
0x0, /* 0 */
/* 12 */ NdrFcShort( 0x0 ), /* X64 Stack size/offset = 0 */
/* 14 */ NdrFcShort( 0x0 ), /* 0 */
/* 16 */ NdrFcShort( 0x0 ), /* 0 */
/* 18 */ 0x42, /* Oi2 Flags: clt must size, has ext, */
0x1, /* 1 */
/* 20 */ 0xa, /* 10 */
0x1, /* Ext Flags: new corr desc, */
/* 22 */ NdrFcShort( 0x0 ), /* 0 */
/* 24 */ NdrFcShort( 0x0 ), /* 0 */
/* 26 */ NdrFcShort( 0x0 ), /* 0 */
/* 28 */ NdrFcShort( 0x0 ), /* 0 */

/* Parameter pszString */

/* 30 */ NdrFcShort( 0x10b ), /* Flags: must size, must free, in, simple ref, */
/* 32 */ NdrFcShort( 0x8 ), /* X64 Stack size/offset = 8 */
/* 34 */ NdrFcShort( 0x4 ), /* Type Offset=4 */

/* Procedure Shutdown */

/* 36 */ 0x0, /* 0 */
0x48, /* Old Flags: */
/* 38 */ NdrFcLong( 0x0 ), /* 0 */
/* 42 */ NdrFcShort( 0x1 ), /* 1 */
/* 44 */ NdrFcShort( 0x8 ), /* X64 Stack size/offset = 8 */
/* 46 */ 0x32, /* FC_BIND_PRIMITIVE */
0x0, /* 0 */
/* 48 */ NdrFcShort( 0x0 ), /* X64 Stack size/offset = 0 */
/* 50 */ NdrFcShort( 0x0 ), /* 0 */
/* 52 */ NdrFcShort( 0x0 ), /* 0 */
/* 54 */ 0x40, /* Oi2 Flags: has ext, */
0x0, /* 0 */
/* 56 */ 0xa, /* 10 */
0x1, /* Ext Flags: new corr desc, */
/* 58 */ NdrFcShort( 0x0 ), /* 0 */
/* 60 */ NdrFcShort( 0x0 ), /* 0 */
/* 62 */ NdrFcShort( 0x0 ), /* 0 */
/* 64 */ NdrFcShort( 0x0 ), /* 0 */

0x0
}
};

static const hello_MIDL_TYPE_FORMAT_STRING hello__MIDL_TypeFormatString =
{
0,
{
NdrFcShort( 0x0 ), /* 0 */
/* 2 */
0x11, 0x8, /* FC_RP [simple_pointer] */
/* 4 */
0x22, /* FC_C_CSTRING */
0x5c, /* FC_PAD */

0x0
}
};

static const unsigned short hello_FormatStringOffsetTable[] =
{
0,
36
};

hello__MIDL_ProcFormatString被称为格式化字符串(类似与printf用的的那个字符串),使用特定的数值来描述当前调用函数中接口的各种属性。包括当前接口类型(用flag表示)参数数量等。如果存在参数的画,则会在描述完接口之后跟着描述对应的参数类型,会描述参数的大小,种类等等。
hello__MIDL_TypeFormatString用于描述当前使用的一些函数的参数种类等
hello_FormatStringOffsetTable则用于描述hello__MIDL_ProcFormatString中每个接口的起始地址。

完成这些定义之后,最终就能声明接口函数

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

void HelloProc(
/* [in] */ handle_t h1,
/* [string][in] */ unsigned char *pszString)
{

NdrClientCall2(
( PMIDL_STUB_DESC )&hello_StubDesc,
(PFORMAT_STRING) &hello__MIDL_ProcFormatString.Format[0],
h1,
pszString);

}


void Shutdown(
/* [in] */ handle_t h1)
{

NdrClientCall2(
( PMIDL_STUB_DESC )&hello_StubDesc,
(PFORMAT_STRING) &hello__MIDL_ProcFormatString.Format[36],
h1);

}

可以看到Client端的HelloProcShutdown函数定义本质上只是调用了一个叫做NdrClientCall2的API,这个API由RPCRT4.dll提供,根据生成的hello_StubDeschello__MIDL_ProcFormatString.Format[0]以及hello__MIDL_ProcFormatString.Format[36]进行函数调用接口和参数的一些定义。之后个根据这种特殊的格式化字符串形式,根据需要传入参数。

Server侧

server侧大部分关于接口的定义等同Client侧,但是接口的实现需要由自己完成,同时会多出如下的几个变量:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42

static const RPC_SERVER_INTERFACE hello___RpcServerInterface =
{
sizeof(RPC_SERVER_INTERFACE),
{{0xf691b703,0xf681,0x47dc,{0xaf,0xcd,0x03,0x4b,0x2f,0xaa,0xb9,0x11}},{1,0}},
{{0x8A885D04,0x1CEB,0x11C9,{0x9F,0xE8,0x08,0x00,0x2B,0x10,0x48,0x60}},{2,0}},
(RPC_DISPATCH_TABLE*)&hello_DispatchTable,
0,
0,
0,
&hello_ServerInfo,
0x04000000
};

static const RPC_DISPATCH_FUNCTION hello_table[] =
{
NdrServerCall2,
NdrServerCall2,
0
};
static const RPC_DISPATCH_TABLE hello_DispatchTable =
{
2,
(RPC_DISPATCH_FUNCTION*)hello_table
};

static const SERVER_ROUTINE hello_ServerRoutineTable[] =
{
(SERVER_ROUTINE)HelloProc,
(SERVER_ROUTINE)Shutdown
};

static const MIDL_SERVER_INFO hello_ServerInfo =
{
&hello_StubDesc,
hello_ServerRoutineTable,
hello__MIDL_ProcFormatString.Format,
hello_FormatStringOffsetTable,
0,
0,
0,
0};

首先,server侧的hello___RpcServerInterface定义了DispatchTable。这个变量会在之后提到的PDU中由procnum指定的操作数指定调对应分发函数。然后对应的NdrServerCall2则会去寻找hello___RpcServerInterface中注册的hello_ServerInfo指定的hello_ServerRoutineTable,最终形成一种对应关系,找到需要调用的相关函数。在NdrServerCall2调用过程中,中途会根据之前注册的接口信息,再合适的时候进行内存管理(之后漏洞会详细分析部分),从而保证传入Server的API中的变量为我们需要的形式。
同时,Server侧需要实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void HelloProc(IN RPC_BINDING_HANDLE hBinding,unsigned char * pszString)
{
printf_s("%s\n", pszString);
}

void Shutdown(IN RPC_BINDING_HANDLE hBinding)
{
RPC_STATUS status;

printf_s("Calling RpcMgmtStopServerListening\n");
status = RpcMgmtStopServerListening(NULL);
printf_s("RpcMgmtStopServerListening returned: 0x%x\n", status);
if (status) {
exit(status);
}

printf_s("Calling RpcServerUnregisterIf\n");
status = RpcServerUnregisterIf(NULL, NULL, FALSE);
printf_s("RpcServerUnregisterIf returned 0x%x\n", status);
if (status) {
exit(status);
}
}

这边就没什么特别的了,就按照正常的API编写即可。

关于RPC的注册机制

[TODO]

特殊类型:Pipe

RPC接口中,支持很多常见的数据类型,例如int, long,char等等,同时也支持类似结构体的格式。详情可以看官方文档,介绍了所有可以用的类型。
这里我们要额外介绍一种特殊的数据类型:PipePipe这种数据类型能够实现如下的能力

The pipe type constructor is a highly efficient mechanism for passing large amounts of data, or any quantity of data that is not all available in memory at one time. By using a pipe, RPC run time handles the actual data transfer, eliminating the overhead associated with repeated remote procedure calls.

概括来讲就是:Pipe中的数据可以想在管道中流通一下,无数次的从某个特定的API中读入or输出。当发生读入or输出的动作的时候,传输的数据无需马上准备好,程序可以根据需要同步or异步的进行数据的输入。

这种类型在MIDL中的声明如下:

1
2
3
4
5
6

typedef pipe long LONG_PIPE;
void InPipe( [in] LONG_PIPE pipe_data );
void OutPipe( [out] LONG_PIPE *pipe_data );

void InOutPipe( [in, out] LONG_PIPE pipe_data);

首先我们需要使用typedef pipe long将pipe类型指定为一个我们新的变量类型上,表明当前管道中的pipe中,传输的元素全部都是long类型的变量。变量前的[in]表示被调用者(Server)将会用这个接口,从调用者(Client)**拖拽(Pull)数据。而[out](后跟指针类型为自行需要),表示被调用者将会用这个接口往调用者处推送(Push)**数据。如果[in,out]都用,则表示这个接口中的数据可能极有可能发生pull也可能发生push

头文件中的新增特征

观察生成头文件:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

static const hello_MIDL_PROC_FORMAT_STRING hello__MIDL_ProcFormatString =
{
0,
{

/* Procedure InPipe */

/* 66 */ 0x32, /* FC_BIND_PRIMITIVE */
0x48, /* Old Flags: */
/* 68 */ NdrFcLong( 0x0 ), /* 0 */
/* 72 */ NdrFcShort( 0x2 ), /* 2 */
/* 74 */ NdrFcShort( 0x8 ), /* X64 Stack size/offset = 8 */
/* 76 */ NdrFcShort( 0x0 ), /* 0 */
/* 78 */ NdrFcShort( 0x0 ), /* 0 */
/* 80 */ 0x48, /* Oi2 Flags: has pipes, has ext, */
0x1, /* 1 */
/* 82 */ 0xa, /* 10 */
0x1, /* Ext Flags: new corr desc, */
/* 84 */ NdrFcShort( 0x0 ), /* 0 */
/* 86 */ NdrFcShort( 0x0 ), /* 0 */
/* 88 */ NdrFcShort( 0x0 ), /* 0 */
/* 90 */ NdrFcShort( 0x0 ), /* 0 */

/* Parameter pipe_data */

/* 92 */ NdrFcShort( 0xc ), /* Flags: pipe, in, */
/* 94 */ NdrFcShort( 0x0 ), /* X64 Stack size/offset = 0 */
/* 96 */ NdrFcShort( 0x8 ), /* Type Offset=8 */

/* Procedure OutPipe */

/* 98 */ 0x32, /* FC_BIND_PRIMITIVE */
0x48, /* Old Flags: */
/* 100 */ NdrFcLong( 0x0 ), /* 0 */
/* 104 */ NdrFcShort( 0x3 ), /* 3 */
/* 106 */ NdrFcShort( 0x8 ), /* X64 Stack size/offset = 8 */
/* 108 */ NdrFcShort( 0x0 ), /* 0 */
/* 110 */ NdrFcShort( 0x0 ), /* 0 */
/* 112 */ 0x48, /* Oi2 Flags: has pipes, has ext, */
0x1, /* 1 */
/* 114 */ 0xa, /* 10 */
0x1, /* Ext Flags: new corr desc, */
/* 116 */ NdrFcShort( 0x0 ), /* 0 */
/* 118 */ NdrFcShort( 0x0 ), /* 0 */
/* 120 */ NdrFcShort( 0x0 ), /* 0 */
/* 122 */ NdrFcShort( 0x0 ), /* 0 */

/* Parameter pipe_data */

/* 124 */ NdrFcShort( 0x4114 ), /* Flags: pipe, out, simple ref, srv alloc size=16 */
/* 126 */ NdrFcShort( 0x0 ), /* X64 Stack size/offset = 0 */
/* 128 */ NdrFcShort( 0x16 ), /* Type Offset=22 */

/* Procedure InOutPipe */

/* 130 */ 0x32, /* FC_BIND_PRIMITIVE */
0x48, /* Old Flags: */
/* 132 */ NdrFcLong( 0x0 ), /* 0 */
/* 136 */ NdrFcShort( 0x4 ), /* 4 */
/* 138 */ NdrFcShort( 0x8 ), /* X64 Stack size/offset = 8 */
/* 140 */ NdrFcShort( 0x0 ), /* 0 */
/* 142 */ NdrFcShort( 0x0 ), /* 0 */
/* 144 */ 0x48, /* Oi2 Flags: has pipes, has ext, */
0x1, /* 1 */
/* 146 */ 0xa, /* 10 */
0x1, /* Ext Flags: new corr desc, */
/* 148 */ NdrFcShort( 0x0 ), /* 0 */
/* 150 */ NdrFcShort( 0x0 ), /* 0 */
/* 152 */ NdrFcShort( 0x0 ), /* 0 */
/* 154 */ NdrFcShort( 0x0 ), /* 0 */

/* Parameter pipe_data */

/* 156 */ NdrFcShort( 0x1c ), /* Flags: pipe, in, out, */
/* 158 */ NdrFcShort( 0x0 ), /* X64 Stack size/offset = 0 */
/* 160 */ NdrFcShort( 0x20 ), /* Type Offset=32 */

0x0
}
};

static const hello_MIDL_TYPE_FORMAT_STRING hello__MIDL_TypeFormatString =
{
0,
{
NdrFcShort( 0x0 ), /* 0 */
/* 2 */
0x11, 0x8, /* FC_RP [simple_pointer] */
/* 4 */
0x22, /* FC_C_CSTRING */
0x5c, /* FC_PAD */
/* 6 */ 0x8, /* FC_LONG */
0x5c, /* FC_PAD */
// 从这里开始才是pip的定义
/* 8 */ 0xb5, /* FC_PIPE 也就是pipe的魔数*/
0x3, /* 3 非常重要的符号位,后面要考*/
/* 10 */ NdrFcShort( 0xfffc ), /* Offset= -4 (6) */
/* 12 */ NdrFcShort( 0x4 ), /* 4 */
/* 14 */ NdrFcShort( 0x4 ), /* 4 */
/* 16 */
0x11, 0x4, /* FC_RP [alloced_on_stack] */
/* 18 */ NdrFcShort( 0x4 ), /* Offset= 4 (22) */
/* 20 */ 0x8, /* FC_LONG */
0x5c, /* FC_PAD */
/* 22 */ 0xb5, /* FC_PIPE */
0x3, /* 3 */
/* 24 */ NdrFcShort( 0xfffc ), /* Offset= -4 (20) */
/* 26 */ NdrFcShort( 0x4 ), /* 4 */
/* 28 */ NdrFcShort( 0x4 ), /* 4 */
/* 30 */ 0x8, /* FC_LONG */
0x5c, /* FC_PAD */
/* 32 */ 0xb5, /* FC_PIPE */
0x3, /* 3 */
/* 34 */ NdrFcShort( 0xfffc ), /* Offset= -4 (30) */
/* 36 */ NdrFcShort( 0x4 ), /* 4 */
/* 38 */ NdrFcShort( 0x4 ), /* 4 */

0x0
}
};

删除部分无用变量

可以看到,生成的Server Stub文件中,多出来一些针对Pipe的特殊声明。我们能够使用这里的特征,对所有使用了RPC调用的binary进行搜索,检查其中是否包含pipe类型。其中这里可以稍微关注一下pipe的属性
[TODO:考虑删掉,改成逆向结果]

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

00000000 PipeInit struc ; (sizeof=0x14, align=0x4, mappedto_440)
00000000 gap0 db ?
00000001 PipeFlag db ?
00000002 db ? ; undefined
00000003 db ? ; undefined
00000004 AnotherSize dw ?
00000006 unsigned___int166 dw ?
00000008 targetSize dd ?
0000000C dwordC dd ?
00000010 dword10 dd ?
00000014 PipeInit ends
00000014


/* 8 */ 0xb5, /* FC_PIPE 对应FC 也就是pipe的魔数*/
0x3, /* 3 对应【pipe符号位】,后面要考*/
/* 10 */ NdrFcShort( 0xfffc ), /* Offset= -4 (6) 对应field_2 */
/* 12 */ NdrFcShort( 0x4 ), /* 4 对应AnotherSize,根据pipe,其占用空间大小可变*/
/* 14 */ NdrFcShort( 0x4 ), /* 4 对应targetSize,根据pipe,其占用空间大小可变*/
/* 16 */
0x11, 0x4, /* FC_RP [alloced_on_stack] */
/* 18 */ NdrFcShort( 0x4 ), /* Offset= 4 (22) */
/* 20 */ 0x8, /* FC_LONG */
0x5c, /* FC_PAD */

Pipe会在InitPipeStateWithType函数中被初始化。其中flag最高位会表示当前大小AnotherSize和targetSize两个字节还是四个字节。默认情况下的targetSize会在NdrReadPipeElements被抬高为64字节(这也就是GetCoalescedBuffer在后期攻击的时候,为什么不会被重复调用的理由)。但是当设置flag最高位为1的场合,这个值最大可以设置为0x7fffff00。不过此时AnotherSize也需要保持一样的大小。

Server侧代码编写

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

#define PIPE_TRANSFER_SIZE 0x100 /* Transfer 100 pipe elements at one time */
#pragma comment(lib, "rpcrt4.lib")
#define BUF_SIZE 0x100
#define PIPE_SIZE 0x100
#define IN_VALUE 0x40

typedef unsigned long ulong;
typedef void* rpc_ss_pipe_state_t;
// extern RPC_IF_HANDLE PIPEDemo_ServerIfHandle;


void InPipe(LONG_PIPE long_pipe)
{
long local_pipe_buf[PIPE_TRANSFER_SIZE];
ulong actual_transfer_count = PIPE_TRANSFER_SIZE;

while (actual_transfer_count > 0) /* Loop to get all
the pipe data elements */
{
int count = PIPE_TRANSFER_SIZE / 2;
printf("Each count size is %d\n", count);
long_pipe.pull(long_pipe.state,
local_pipe_buf,
count,
&actual_transfer_count);
/* process the elements */
printf("Server has receive %d item!\n",actual_transfer_count);
} // end while
printf("And the first ten item is :\n");
for(int i = 0; i < 10; i++)
{
printf("%d,", local_pipe_buf[i]);
}
puts("\n");
} //end InPipe

void OutPipe(LONG_PIPE* outputPipe)
{
long* outputPipeData;
ulong index = 0;
ulong elementsToSend = PIPE_TRANSFER_SIZE;

/* Allocate memory for the data to be passed back in the pipe */
if (outputPipe == NULL)
{
return;
}

outputPipeData = (long*)malloc(sizeof(long) * PIPE_SIZE);

for(int i = 0; i < PIPE_SIZE; i++)
{
outputPipeData[i] = i;
}
while (elementsToSend > 0) /* Loop to send pipe data elements */
{
if (index >= PIPE_SIZE)
elementsToSend = 0;
else
{
if ((index + PIPE_TRANSFER_SIZE) > PIPE_SIZE)
elementsToSend = PIPE_SIZE - index;
else
elementsToSend = PIPE_TRANSFER_SIZE;
}

outputPipe->push(outputPipe->state,
&(outputPipeData[index]),
elementsToSend);
index += elementsToSend;

} //end while

free((void*)outputPipeData);

}

void InOutPipe( LONG_PIPE pipe_data)
{
printf("First enter InPipe\n");
InPipe(pipe_data);
printf("Next enter OutPipe\n");
OutPipe(&pipe_data);
}

再编写Server侧代码的时候,首先注意,当前传入的LONG_PIPE对象无需我们初始化,因为实际上pipe对象具体要怎么做是交给用户态来定义的
InPipe接口中,我们调用了pull接口

1
2
3
4
5
6
7
8
9
10
11
12
while (actual_transfer_count > 0) /* Loop to get all
the pipe data elements */
{
int count = PIPE_TRANSFER_SIZE / 2;
printf("Each count size is %d\n", count);
long_pipe.pull(long_pipe.state,
local_pipe_buf,
count,
&actual_transfer_count);
/* process the elements */
printf("Server has receive %d item!\n",actual_transfer_count);
} // end while
  • state用于描述当前管道中的状态值,这里我们用它代表了下标(client侧体现)
  • local_pipe_buf用于存放当前用于存放收入数据的缓冲区
  • count表示pipe单次接受的pipe中元素大小,这里也就是能接受count个long类型
  • actual_transfer_count表示实际接受了多少个元素

当我们发现接收到的数据大小为0的时候,此时停止循环,完成pipe数据读取。
OutPipe接口也类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (elementsToSend > 0) /* Loop to send pipe data elements */
{
if (index >= PIPE_SIZE)
elementsToSend = 0;
else
{
if ((index + PIPE_TRANSFER_SIZE) > PIPE_SIZE)
elementsToSend = PIPE_SIZE - index;
else
elementsToSend = PIPE_TRANSFER_SIZE;
}

outputPipe->push(outputPipe->state,
&(outputPipeData[index]),
elementsToSend);
index += elementsToSend;

} //end while

只不过这次我们会按照一定的比率进行数据的输送。

Client侧

Client侧的逻辑稍微复杂,且需要和Server侧颠倒

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
void PipeAlloc(rpc_ss_pipe_state_t stateInfo,
ulong requestedSize,
long** allocatedBuffer,
ulong* allocatedSize)
{
printf("request size is %d\n", requestedSize);
ulong* state = (ulong*)stateInfo;
if (requestedSize > (BUF_SIZE * sizeof(long)))
{
*allocatedSize = BUF_SIZE * sizeof(long);
}
else
{
*allocatedSize = requestedSize;
}
*allocatedBuffer = globalBuffer;
} //end PipeAlloc

void PipePull(rpc_ss_pipe_state_t stateInfo,
long* inputBuffer,
ulong maxBufSize,
ulong* sizeToSend)
{
ulong currentIndex;
ulong i;
ulong elementsToRead;
ulong* state = (ulong*)stateInfo;

currentIndex = *state;
printf("Max Buffer size is %d\n",maxBufSize);
printf("currentIndex is %d\n",currentIndex);
if (*state >= PIPE_SIZE)
{
*sizeToSend = 0; /* end of pipe data */
*state = 0; /* Reset the state = global index */
}
else
{
if (currentIndex + maxBufSize > PIPE_SIZE)
elementsToRead = PIPE_SIZE - currentIndex;
else
elementsToRead = maxBufSize;

for (i = 0; i < elementsToRead; i++)
{
/*client sends data */
inputBuffer[i] = globalSendPipeData[i + currentIndex];
}

printf("Now send %d element to server\n", elementsToRead);
*state += elementsToRead;
*sizeToSend = elementsToRead;
}
}//end PipePull
void PipePush(rpc_ss_pipe_state_t stateInfo,
long* buffer,
ulong numberOfElements)
{
ulong elementsToCopy, i;
ulong* state = (ulong*)stateInfo;

if (numberOfElements == 0)/* end of data */
{
printf("Receive the final one\n");
*state = 0; /* Reset the state = global index */
}
else
{
// state is the like the index of the push offset
if (*state + numberOfElements > PIPE_SIZE)
elementsToCopy = PIPE_SIZE - *state;
else
elementsToCopy = numberOfElements;

for (i = 0; i < elementsToCopy; i++)
{
/*client receives data */
globalRecvPipeData[*state] = buffer[i];
(*state)++;
}
printf("Receive from server, the first 10 is \n");
for(int i = 0; i< 10; i++)
{
printf("%d,", globalRecvPipeData[i]);
}
puts("\n");
}
}//end PipePush

首先可以看到,这里首先定义了三个和pipe相关的函数。之后这三个函数会分别被赋值给传入Client侧的InPipeOutPipe中的LONG_PIPE对象中。这里先解释其作用

  • PipeAlloc会在每次pipe_pullpipe_push被调用的时候,给allocatedBuffer申请变量,而且必须在allocatedSize给出反馈。注意这个allocatedBuffer只需要在用户态可用即可,未规定一定要是malloc出来的内容。例子中就简单的使用全局变量进行了分配
  • PipePull中传入的inputBufferPipeAlloc中申请的Buffer,然后我们往Buffer中写入我们需要传入的内容。这里的stateInfo其实是一个整数变量,用于维护Pipe的状态,我们在这里用于表示当前Pipe的下标,记录传输的状态。完成传输之后,需要将要发送的数据数量存入sizeToSend
  • PipePushbuffer同样为PipeAlloc申请来的数据大小。这边而是表示此时有numberOfElements个对象需要接受,此时只需要将数据传入对应state表示的下标即可

完成上述准备,此时可以编写用于发送数据和接受数据的函数

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
void SendLongs()
{
LONG_PIPE inPipe;
int i;
globalSendPipeData =
(long*)malloc(sizeof(long) * PIPE_SIZE);

for (i = 0; i < PIPE_SIZE; i++)
globalSendPipeData[i] = 0x42424242;

pipeDataIndex = 0;
inPipe.state = (rpc_ss_pipe_state_t)&pipeDataIndex;
inPipe.pull = PipePull;
inPipe.alloc = PipeAlloc;

printf("Using inpipe seding....\n");
InPipe(inPipe); /* Make the rpc */

free((void*)globalSendPipeData);

}//end SendLongs
void ReceiveLongs()
{
LONG_PIPE outputPipe;
//idl_long_int i;

globalRecvPipeData =
(long*)malloc(sizeof(long) * PIPE_SIZE);

pipeDataIndex = 0;
outputPipe.state = (rpc_ss_pipe_state_t)&pipeDataIndex;
outputPipe.push = PipePush;
outputPipe.alloc = PipeAlloc;

OutPipe(&outputPipe); /* Make the rpc */

free((void*)globalRecvPipeData);

}//end ReceiveLongs()

// in pipedemoc_c.c

void InPipe(
/* [in] */ LONG_PIPE pipe_data)
{

NdrClientCall2(
( PMIDL_STUB_DESC )&pipedemo_StubDesc,
(PFORMAT_STRING) &hello__MIDL_ProcFormatString.Format[66],
pipe_data);

}


void OutPipe(
/* [out] */ LONG_PIPE *pipe_data)
{

NdrClientCall2(
( PMIDL_STUB_DESC )&pipedemo_StubDesc,
(PFORMAT_STRING) &hello__MIDL_ProcFormatString.Format[98],
pipe_data);

}

可以看到,这两个函数调用过程中,都有一个针对当前的LONG_PIPE对象赋值的过程。此过程其实是为了在调用InPipeOutPipe中,此时的pipe类型将会怎么使用。这个函数在自动生成的Stub文件中作为普通的参数传入,不过其在RPC调用过程中存在特殊的处理过程(和之后漏洞相关)。此时根据Server的实现情况,其会反向调用来自用户态的函数,也就是最后会反过来找用户态的pushpull函数。在用户态完成调用之后,其会调用NdrClientCall2重新使用socket协议将数据发送出去,从而实现RPC。

总结

本文主要介绍了RPC的一些基础知识(主要也是MSDN的翻译),下一篇文章中将会详细介绍漏洞的相关详情

参考链接

https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-start-page
https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/netds