0%

gRPC-go基础+逆向

RPC & gRPC

RPC 全称 Remote Procedure Call,中文译为远程过程调用

  • 使用 RPC 进行通信,调用远程函数就像调用本地函数一样
  • RPC 底层会做好数据的序列化与传输,从而能使我们更轻松地创建分布式应用和服务

gRPC 是RPC的一种,典型特征就是使用 protobuf 作为其 IDL 接口定义语言

  • 使用 gRPC,我们只需要定义好每个 API 的 Request 和 Response,剩下的 gRPC 这个框架会帮我们自动搞定

gRPC 的通信流程:

  • 定义IDL,即我们的接口文档(后缀为.proto)
  • 编译 proto 文件,得到存根(stub)文件
  • 服务端(gRPC Server)实现第一步定义的接口并启动,这些接口的定义在存根文件里面
  • 客户端借助存根文件调用服务端的函数,虽然客户端调用的函数是由服务端实现的,但是调用起来就像是本地函数一样

gRPC-go 基础知识

接下来用 go 语言写一个 gRPC 案例

先使用如下命令进行初始化:

1
go mod init go-test
  • “go-test” 为 module 名称,程序会生成一个 go.mod
1
go mod tidy
  • 更新依赖至最新版本,程序会更新 go.mod 条目并生成一个 go.sum

定义 protobuf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";

package greeter.srv;

option go_package = "proto/greeter";

service Greeter { // 设置Greeter服务
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest { // 定义传参格式
string name = 1;
}

message HelloReply { // 定义返回格式
string message = 1;
}

将 proto 文件编译为存根文件:

1
protoc --proto_path=proto  --go_out=proto  --go_opt=paths=source_relative proto/greeter.proto
  • —proto_path:指定 import 路径
  • —go_out:指定输出文件路径
  • —go_opt:指定参数(paths=source_relative 表示生成文件输出使用相对路径)
  • 被编译的 .proto 文件放在最后面

也可以使用集成化工具 powerproto 来进行编译,安装方法如下:

1
go install github.com/storyicon/powerproto/cmd/powerproto@latest 
1
2
3
4
git clone https://github.com/storyicon/powerproto.git
cd powerproto
make
cp dist/powerproto-linux-amd64 /usr/local/bin/powerproto

powerproto 常用命令如下:

1
2
3
powerproto init /* 初始化 */
powerproto tidy /* 整理配置文件 */
powerproto build proto/greeter.proto /* 编译proto */
  • 初始化完成后会生成 powerproto.yaml 文件,在其中我们可以设置 proto 的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scopes:
- ./
protoc: v3.7.1 # 修改protoc版本
protocWorkDir: ""
plugins:
protoc-gen-go: google.golang.org/protobuf/cmd/protoc-gen-go@v1.26.0 # 修改protoc-gen-go版本
protoc-gen-go-grpc: google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 # 修改protoc-gen-go-grpc版本
repositories:
GOOGLE_APIS: https://github.com/googleapis/googleapis@75e9812478607db997376ccea247dd6928f70f45
options:
- --go_out=.
- --go_opt=paths=source_relative
- --go-grpc_out=.
- --go-grpc_opt=paths=source_relative
importPaths:
- .
- $GOPATH
- $POWERPROTO_INCLUDE
- $SOURCE_RELATIVE
- $GOOGLE_APIS/github.com/googleapis/googleapis
postActions: []
postShell: ""

编译完成后的存根文件部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
type GreeterServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
/* context.Context:上下文
HelloRequest:请求入参 */

type GreeterClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
/* context.Context:上下文
HelloRequest:请求入参
grpc.CallOption:定义了before方法和after方法的接口 */
  • 定义了服务端和客户端关于模块函数的接口
  • 该接口对应的函数是定义在服务端上的,但客户端可以通过该接口来调用该函数

服务端 API 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
var Greeter_ServiceDesc = grpc.ServiceDesc{ /* 创建一个ServiceDesc对象 */
ServiceName: "greeter.srv.Greeter", /* 服务名称 */
HandlerType: (*GreeterServer)(nil), /* 处理的结构体 */
Methods: []grpc.MethodDesc{ /* 一次响应的方法集 */
{
MethodName: "SayHello", /* 模块名称 */
Handler: _Greeter_SayHello_Handler, /* 对应的handler函数 */
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/greeter.proto", /* 元数据,也就是proto文件 */
}
  • grpc.ServiceDesc 对象将会作为 RegisterService 的参数
1
2
3
func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
s.RegisterService(&Greeter_ServiceDesc, srv)
}
  • RegisterService 为注册函数,将一个 grpc.ServiceDesc 对象注册到系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest) /* 开辟空间,存储客户端的请求数据 */
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil { /* 如果没有注册拦截器,则直接调用目标函数 */
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Greeter_SayHello_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
} /* 否则调用拦截器(目标函数也会注册到拦截器中) */
return interceptor(ctx, in, info, handler)
}
  • 核心点就是接收客户端的流对象,调用服务端上的函数并响应

客户端 API 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type greeterClient struct { 
cc grpc.ClientConnInterface /* 定义了执行RPC方法对象需要实现的函数 */
}

func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
/* 创建一个客户端:
入参值:客户端和服务器端建立的连接
返回值:greeterClient结构体 */
return &greeterClient{cc} /* 相当于创建了一个greeterClient结构体(用传参cc初始化),然后将该结构体返回 */
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { /* 客户端接口的实现 */
out := new(HelloReply) /* 创建HelloReply结构体,用于返回数据 */
err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, opts...) /* 调用客户端连接的Inoke方法 */
if err != nil {
return nil, err
}
return out, nil
}
  • 客户端接口的核心步骤就是调用 Inoke 方法
1
2
3
4
5
6
7
type ClientConnInterface interface {
// Invoke performs a unary RPC and returns after the response is received
// into reply.
Invoke(ctx context.Context, method string, args any, reply any, opts ...CallOption) error
// NewStream begins a streaming RPC.
NewStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error)
}
1
2
3
4
5
6
7
8
9
10
func (cc *ClientConn) Invoke(ctx context.Context, method string, args, 
reply interface{}, opts ...CallOption) error {

opts = combine(cc.dopts.callOptions, opts) /* 把客户端的拦截器和和调用方法入参的拦截器合并 */
if cc.dopts.unaryInt != nil {
return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
}

return invoke(ctx, method, args, reply, cc, opts...)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func invoke(ctx context.Context, method string, req, reply interface{}, 
cc *ClientConn, opts ...CallOption) error {

cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...) /* 创建客户端流对象 */
if err != nil {
return err
}

if err := cs.SendMsg(req); err != nil { /* 发送请求 */
return err
}

return cs.RecvMsg(reply) /* 接受响应 */
}
  • 核心点就是创建客户端流对象,发送请求并且接受响应

PS:和 mustEmbedUnimplementedGreeterServer 相关的内容都是 protoc-gen-go-grpc 为 gRPC 设置的保护,可以直接删去

服务端代码如下:

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
package main

import (
"context"
"fmt"
greeter "go-test/proto"
"log"
"net"

"google.golang.org/grpc"
)

type server struct {
}

func (s *server) SayHello(ctx context.Context, req *greeter.HelloRequest) (rsp *greeter.HelloReply, err error) {
rsp = &greeter.HelloReply{Message: "Hello " + req.Name}
return rsp, nil
}

func main() {
listener, err := net.Listen("tcp", ":52001")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
greeter.RegisterGreeterServer(s, &server{})

fmt.Println("gRPC server listen in 52001...")
err = s.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

客户端代码如下:

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
package main

import (
"context"
"fmt"
greeter "go-test/proto"
"log"
"time"

"google.golang.org/grpc"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:52001", grpc.WithInsecure())
if err != nil {
log.Fatalf("connect failed: %v", err)
}

defer conn.Close()

c := greeter.NewGreeterClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()

r, err := c.SayHello(ctx, &greeter.HelloRequest{Name: "World"})
if err != nil {
log.Fatalf("call service failed: %v", err)
}
fmt.Println("call service success: ", r.Message)
}

在客户端上调用 SayHello 函数(客户端上没有实现),客户端通过 gRPC 将请求数据发送到服务端,服务端执行完成以后将响应数据发回客户端

gRPC-go 逆向分析

对于无符号的 go 语言逆向,可以先使用 IDAGolangHelper 初步恢复符号,然后编译一个有符号的 go 语言程序并用 bindiff 再次恢复符号

由于 IDA7.7 对于 IDAGolangHelper 的兼容性不好,因此这里选择使用 AlphaGolang

  • PS:这里强烈推荐 AlphaGolang,分析出来的伪代码比带符号的都好看,另外它还有其他功能

接着就可以使用 pbtk 来从二进制文件中提取 proto 文件:

1
/pbtk/extractors/from_binary.py server
  • /pbtk/extractors/from_binary.py 会生成一个 .proto 文件(可能会报错,但还是生成了文件)
  • 生成的文件和源文件几乎没有区别

接下来我们需要利用 IDA 快速找到服务端为客户端定义的函数代码

在有符号的二进制文件中搜索函数名称,很容易就能找到该函数:

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
retval_7E3C40 __golang main__ptr_server_SayHello(
main_server_0 *s,
context_Context_0 ctx,
go_test_proto_HelloRequest_0 *req)
{
int v3; // r14
int v4; // rax
runtime__type_0 *v5; // [rsp-28h] [rbp-38h]
runtime_tmpBuf *v6; // [rsp-28h] [rbp-38h]
string v7; // [rsp-20h] [rbp-30h]
string v8; // [rsp-10h] [rbp-20h]
uint8 *str; // [rsp+0h] [rbp-10h]
void *retaddr; // [rsp+10h] [rbp+0h] BYREF
retval_7E3C40 result; // [rsp+38h] [rbp+28h]

if ( (unsigned int)&retaddr <= *(_QWORD *)(v3 + 16LL) )
runtime_morestack_noctxt();
v7.str = (uint8 *)runtime_newobject(v5);
str = runtime_concatstring2(v6, v7, v8).str;
*((_QWORD *)str + 6LL) = "Hello ";
if ( *(_DWORD *)&runtime_writeBarrier.enabled )
runtime_gcWriteBarrier();
else
*((_QWORD *)str + 5LL) = v4;
return result;
}

但该函数并没有直接被其他函数调用,我们必须通过其他方法找到其调用链:

1
Down	j	.text:00000000007E3CF2	jmp     main__ptr_server_SayHello

有一个方法就是找到注册函数 s.RegisterService(在无符号的情况下也适用):

1
2
3
4
5
6
7
8
.text:00000000007E3D80 E8 DB 59 FE FF                call    google_golang_org_grpc_NewServer
.text:00000000007E3D80
.text:00000000007E3D85 48 89 44 24 28 mov [rsp+68h+var_40], rax
.text:00000000007E3D8A 90 nop
.text:00000000007E3D8B 48 8B 0D 36 AD 18 00 mov rcx, cs:off_96EAC8
.text:00000000007E3D92 48 8D 1D 47 D6 46 00 lea rbx, go_test_proto_Greeter_ServiceDesc
.text:00000000007E3D99 48 8D 3D 50 7A 4A 00 lea rdi, runtime_zerobase
.text:00000000007E3DA0 E8 7B 5F FE FF call google_golang_org_grpc__ptr_Server_RegisterService
  • rax 为 NewServer 的返回值
  • rbx 为 ServiceDesc 对象
  • rcx 为定义的 interface 接口

我们需要的信息就在第二个参数 ServiceDesc 对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type ServiceDesc struct {
ServiceName string
// The pointer to the service interface. Used to check whether the user
// provided implementation satisfies the interface requirements.
HandlerType any
Methods []MethodDesc
Streams []StreamDesc
Metadata any
}

type MethodDesc struct {
MethodName string
Handler methodHandler
}

type StreamDesc struct {
StreamName string
Handler StreamHandler

ServerStreams bool
ClientStreams bool
}
1
2
3
4
5
6
.data:0000000000C513E0                               public go_test_proto_Greeter_ServiceDesc
.data:0000000000C513E0 ; google_golang_org_grpc_ServiceDesc_0 go_test_proto_Greeter_ServiceDesc
.data:0000000000C513E0 9B EB 8C 00 00 00 00 00 13 00+go_test_proto_Greeter_ServiceDesc google_golang_org_grpc_ServiceDesc_0 <<offset aGreeterSrvGree, 13h>, <offset unk_80B520, 0>, <\
.data:0000000000C513E0 00 00 00 00 00 00 20 B5 80 00+ ; DATA XREF: main_main+92↑o
.data:0000000000C513E0 00 00 00 00 00 00 00 00 00 00+ offset off_C49860, 1, 1>, <offset regexp_arrayNoInts, 0, 0>, <\ ; "greeter.srv.Greeter" ...
.data:0000000000C513E0 00 00 60 98 C4 00 00 00 00 00+ offset unk_8221E0, offset off_C481A0>>
  • IDA 信息可能有点难看,这里直接看 GDB 调试信息(长度大小为 0x60)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> telescope 0xc513e0
00:0000│ rbx 0xc513e0 —▸ 0x8cebfb ◂— 0x2e72657465657267 ('greeter.') /* ServiceName */
01:00080xc513e8 ◂— 0x13
02:00100xc513f0 —▸ 0x80b540 ◂— 0x8 /* HandlerType */
03:00180xc513f8 ◂— 0x0
04:00200xc51400 —▸ 0xc49860 —▸ 0x8c81dc ◂— 0x6f6c6c6548796153 ('SayHello') /* MethodDesc */
05:00280xc51408 ◂— 0x1
06:00300xc51410 ◂— 0x1
07:00380xc51418 —▸ 0xc8b4e0 ◂— 0x1010101010101 /* StreamDesc */
08:00400xc51420 ◂— 0x0
09:00480xc51428 ◂— 0x0
0a:00500xc51430 —▸ 0x822200 ◂— 0x10 /* Metadata */
0b:00580xc51438 —▸ 0xc481a0 —▸ 0x8cedd6 ◂— 0x72672f6f746f7270 ('proto/gr')
0c:00600xc51440 —▸ 0x8c4889 ◂— 0x6956696156435455 ('UTCVaiVi')
0d:00680xc51448 ◂— 0x3
1
2
3
4
5
6
pwndbg> telescope 0xc49860 /* MethodDesc */
00:00000xc49860 —▸ 0x8c81dc ◂— 0x6f6c6c6548796153 ('SayHello') /* MethodName */
01:00080xc49868 ◂— 0x8
02:00100xc49870 —▸ 0x900b28 —▸ 0x7e3860 ◂— cmp rsp, qword ptr [r14 + 10h] /* Handler */
03:00180xc49878 ◂— 0x0
... ↓ 4 skipped
  • 在 Handler 中可以找到目标函数

之前我们已经分析了存根文件中的 Handler 函数,但是在 IDA 逆向时发现其有很大不同:

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
retval_7E3860 __golang go_test_proto__Greeter_SayHello_Handler(
interface__0 srv,
context_Context_0 ctx,
funcinterface__error dec,
google_golang_org_grpc_UnaryServerInterceptor interceptor)
{
retval_7E3A60 (__golang *v4)(context_Context_0, interface__0); // rax
retval_7E3A60 (__golang *v5)(context_Context_0, interface__0); // rbx
__int64 (**v6)(void); // rsi
void (*v7)(context_Context_0, interface__0, google_golang_org_grpc_UnaryServerInfo_0 *, google_golang_org_grpc_UnaryHandler, interface__0 *, error_0 *); // r8
int v8; // r14
retval_7E3A60 (__golang **v9)(context_Context_0, interface__0); // rax
retval_7E3A60 (__golang **v10)(context_Context_0, interface__0); // rax
int v11; // rax
runtime__type_0 *v12; // [rsp-30h] [rbp-50h]
runtime_interfacetype_0 *v13; // [rsp-30h] [rbp-50h]
runtime__type_0 *v14; // [rsp-30h] [rbp-50h]
runtime__type_0 *v15; // [rsp-28h] [rbp-48h]
retval_7E3A60 (__golang *v16)(context_Context_0, interface__0); // [rsp+10h] [rbp-10h]
void *retaddr; // [rsp+20h] [rbp+0h] BYREF
google_golang_org_grpc_UnaryServerInterceptor interceptora; // [rsp+50h] [rbp+30h]
retval_7E3860 result; // [rsp+58h] [rbp+38h]

if ( (unsigned int)&retaddr <= *(_QWORD *)(v8 + 16LL) )
runtime_morestack_noctxt();
interceptora = v7;
v16 = v4;
v15 = (runtime__type_0 *)runtime_newobject(v12);
if ( !(*v6)() )
{
if ( interceptora )
{
runtime_newobject(&v13->typ);
*v9 = v16;
if ( *(_DWORD *)&runtime_writeBarrier.enabled )
runtime_gcWriteBarrierDX();
else
v9[1LL] = v5;
v9[3LL] = (retval_7E3A60 (__golang *)(context_Context_0, interface__0))29LL;
v9[2LL] = (retval_7E3A60 (__golang *)(context_Context_0, interface__0))"/greeter.srv.Greeter/SayHello";
runtime_newobject(v14);
*v10 = go_test_proto__Greeter_SayHello_Handler_func1;
v10[1LL] = v16;
if ( *(_DWORD *)&runtime_writeBarrier.enabled )
runtime_gcWriteBarrierR9();
else
v10[2LL] = v5;
(*(void (**)(void))interceptora)(); /* 调用interceptor(注册的拦截器) */
}
else
{
runtime_assertE2I(v13, v15);
(*(void (**)(void))(v11 + 24LL))(); /* 调用目标函数 */
}
}
return result;
}
  • 通过调试即可找到目标函数的具体地址:
1
0x7e39ce    call   rdx                           <0x7e3c40> /* main__ptr_server_SayHello */