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 案例
先使用如下命令进行初始化:
“go-test” 为 module 名称,程序会生成一个 go.mod
更新依赖至最新版本,程序会更新 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 { 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
初始化完成后会生成 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) } type GreeterClient interface { SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) }
定义了服务端和客户端关于模块函数的接口
该接口对应的函数是定义在服务端上的,但客户端可以通过该接口来调用该函数
服务端 API 的实现:
1 2 3 4 5 6 7 8 9 10 11 12 var Greeter_ServiceDesc = grpc.ServiceDesc{ ServiceName: "greeter.srv.Greeter" , HandlerType: (*GreeterServer)(nil ), Methods: []grpc.MethodDesc{ { MethodName: "SayHello" , Handler: _Greeter_SayHello_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "proto/greeter.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 } func NewGreeterClient (cc grpc.ClientConnInterface) GreeterClient { return &greeterClient{cc} } func (c *greeterClient) SayHello (ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { out := new (HelloReply) err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, opts...) if err != nil { return nil , err } return out, nil }
1 2 3 4 5 6 7 type ClientConnInterface interface { Invoke(ctx context.Context, method string , args any, reply any, opts ...CallOption) error 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 mainimport ( "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 mainimport ( "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; int v4; runtime__type_0 *v5; runtime_tmpBuf *v6; string v7; string v8; uint8 *str; void *retaddr; retval_7E3C40 result; 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:00000000007E3 CF2 jmp main__ptr_server_SayHello
有一个方法就是找到注册函数 s.RegisterService
(在无符号的情况下也适用):
1 2 3 4 5 6 7 8 .text:00000000007E3 D80 E8 DB 59 FE FF call google_golang_org_grpc_NewServer .text:00000000007E3 D80 .text:00000000007E3 D85 48 89 44 24 28 mov [rsp+68 h+var_40], rax .text:00000000007E3 D8A 90 nop .text:00000000007E3 D8B 48 8B 0 D 36 AD 18 00 mov rcx, cs:off_96EAC8 .text:00000000007E3 D92 48 8 D 1 D 47 D6 46 00 lea rbx, go_test_proto_Greeter_ServiceDesc .text:00000000007E3 D99 48 8 D 3 D 50 7 A 4 A 00 lea rdi, runtime_zerobase .text:00000000007E3 DA0 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 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:0000000000 C513E0 public go_test_proto_Greeter_ServiceDesc .data:0000000000 C513E0 ; google_golang_org_grpc_ServiceDesc_0 go_test_proto_Greeter_ServiceDesc .data:0000000000 C513E0 9B EB 8 C 00 00 00 00 00 13 00 +go_test_proto_Greeter_ServiceDesc google_golang_org_grpc_ServiceDesc_0 <<offset aGreeterSrvGree, 13 h>, <offset unk_80B520, 0 >, <\ .data:0000000000 C513E0 00 00 00 00 00 00 20 B5 80 00 + ; DATA XREF: main_main+92 ↑o .data:0000000000 C513E0 00 00 00 00 00 00 00 00 00 00 + offset off_C49860, 1 , 1 >, <offset regexp_arrayNoInts, 0 , 0 >, <\ ; "greeter.srv.Greeter" ... .data:0000000000 C513E0 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.' ) 01 :0008 │ 0xc513e8 ◂— 0x13 02 :0010 │ 0xc513f0 —▸ 0x80b540 ◂— 0x8 03 :0018 │ 0xc513f8 ◂— 0x0 04 :0020 │ 0xc51400 —▸ 0xc49860 —▸ 0x8c81dc ◂— 0x6f6c6c6548796153 ('SayHello' ) 05 :0028 │ 0xc51408 ◂— 0x1 06 :0030 │ 0xc51410 ◂— 0x1 07 :0038 │ 0xc51418 —▸ 0xc8b4e0 ◂— 0x1010101010101 08 :0040 │ 0xc51420 ◂— 0x0 09 :0048 │ 0xc51428 ◂— 0x0 0 a:0050 │ 0xc51430 —▸ 0x822200 ◂— 0x10 0b :0058 │ 0xc51438 —▸ 0xc481a0 —▸ 0x8cedd6 ◂— 0x72672f6f746f7270 ('proto/gr' ) 0 c:0060 │ 0xc51440 —▸ 0x8c4889 ◂— 0x6956696156435455 ('UTCVaiVi' )0 d:0068 │ 0xc51448 ◂— 0x3
1 2 3 4 5 6 pwndbg> telescope 0xc49860 00 :0000 │ 0xc49860 —▸ 0x8c81dc ◂— 0x6f6c6c6548796153 ('SayHello' ) 01 :0008 │ 0xc49868 ◂— 0x8 02 :0010 │ 0xc49870 —▸ 0x900b28 —▸ 0x7e3860 ◂— cmp rsp, qword ptr [r14 + 10 h] 03 :0018 │ 0xc49878 ◂— 0x0 ... ↓ 4 skipped
之前我们已经分析了存根文件中的 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); retval_7E3A60 (__golang *v5)(context_Context_0, interface__0); __int64 (**v6)(void ); void (*v7)(context_Context_0, interface__0, google_golang_org_grpc_UnaryServerInfo_0 *, google_golang_org_grpc_UnaryHandler, interface__0 *, error_0 *); int v8; retval_7E3A60 (__golang **v9)(context_Context_0, interface__0); retval_7E3A60 (__golang **v10)(context_Context_0, interface__0); int v11; runtime__type_0 *v12; runtime_interfacetype_0 *v13; runtime__type_0 *v14; runtime__type_0 *v15; retval_7E3A60 (__golang *v16)(context_Context_0, interface__0); void *retaddr; google_golang_org_grpc_UnaryServerInterceptor interceptora; retval_7E3860 result; 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)(); } else { runtime_assertE2I(v13, v15); (*(void (**)(void ))(v11 + 24LL ))(); } } return result; }
1 ► 0x7e39ce call rdx <0x7e3c40 >