Protobuf Pwn
Protobuf Pwn
protobuf
Protobuf(Protocol Buffers)是谷歌开发的一款跟平台和语言无关、可扩展、轻量级高效的序列化的结构类型。是一种高效的数据压缩编码方式,可用于通信协议,数据存储等。
能够将数据结构体序列化为bytes字节流,也能将bytes字节流反序列化成数据结构体。
安装protobuf
1 | 在ubuntu22.04下 |
Protobuf的使用以及逆向分析
首先创建一个简单的proto例子:
1 | syntax = "proto3"; |
分析C语言中的内容
1 | protoc --c_out=./ z0yuan.proto |
--c_out=./
将.proto文件以c语言格式输出在当前目录下,之后会生成z0yuan.pb-c.c 、z0yuan.pb-c.h两个文件。
查看.c文件内容
关键函数——序列化函数和反序列化函数
1 | size_t devicemsg__pack(const Devicemsg *message, uint8_t *out) {//序列化 |
pack
用来序列化
得到bytes字节流
,unpack
用来反序列化
得到结构数据
。
注意看unpack函数,第一个allocator一般为0,第二个为数据包长度,第三个是数据字节流。通过执行内置函数protobuf_c_message_unpack
来实现反序列化将字节流数据转化为结构数据。其中devicemsg_descriptor
就是一个描述先前定义的message结构的数据。返回地址就是之前定义的message结构体。
描述符结构体
devicemsg__descriptor为ProtobufCMessageDescriptor类型的一个结构。
1 | /** |
关键点:
- magic,一般为0x28AAEEF9
- n_fields,关系到原始的message结构内有几条记录(本篇例子中有4条记录)
- fields,这个指向message内所有记录类型组成的一个数组,可以借助此部分内容逆向分析message结构。
fields结构体
重点要看fields,这个是ProtobufCFieldDescriptor类型
1 | struct ProtobufCFieldDescriptor { |
关键点:
- name,名字,变量名
- id,序号(在message结构体中的顺序)
- label(在proto2语法中对应的是required、optional)
- type,数据类型,sting还是int64等,label和type都是枚举类型,占四个字节。
类型表
从0开始
1 | typedef enum { |
结构体信息
1 | static const ProtobufCFieldDescriptor devicemsg__field_descriptors[4] = |
可以分析与ProtobufCFieldDescriptor类型对应的结构来还原message结构体
利用Python配合protobuf打包
执行命令
1 | protoc --python_out=./ z0yuan.proto |
生成z0yuan_pb2.py
打包模板
1 | import z0yuan_pb2.py |
Pbtk提取proto结构
pbtk工具可以直接提取程序中的proto结构,这样就方便生成python包然后进行解题。
1 | sudo apt install python3-pip git openjdk-9-jre libqt5x11extras5 python3-pyqt5.qtwebengine python3-pyqt5 |
命令
1 | cd pbtk |
CISCN 2024 ezbuf例题
恢复proto结构体
这道题无法利用工具pbtk直接提取,所以需要手动逆向分析提取。
关键的结构体部分在这,根据这段连续的data来恢复结构体。1代表的是序号,3代表的是label,0xf代表的是Type类型,查表即可
最终恢复proto结构体如下:
1 | syntax="proto3"; |
还原C语言的结构体
1 | //ProtobufCMessage结构体 |
结构体大小对齐后是24字节。
bytes类型,转化为c语言结构时会变成一个结构体,里面存放长度和内容指针。
1 | struct ProtobufCBinaryData { |
1 |
|
代码分析
由于ida会把bytes转化的结构体内容当作8字节数组解析,所以前两个实际上是代表同一个记录。
分别对应的是五个记录,而不是ida显示的6个。
其中v4+40对应的是whattodo也就是a3
- 函数0只是解析数据包,但是会根据数据包的长度申请堆块,比如func0(str) –> malloc(len(str)),实际上就是反序列化数据包时,需要申请一个数据包大小的chunk来保存数据
- 函数1创建0x30大小的chunk并且将数据(此处的数据就是unpack函数中通过创建堆块来保存的content数据)复制过去
- 函数2是释放chunk的功能,存在UAF,但是最多释放10次
- 函数3是打印功能,超过两次会关闭标准输入流和标准输出流
利用思路
先申请0x30大小的chunk来切割unsortedbin泄露libc,然后再释放一个chunk来泄露heap_key和heap_base。利用fastbin中的double_free触发malloc_consolidate使得fastbin中的chunk进入到tcache中,此时可以任意写一次。
观察此时的bin,由于glibc-2.35没有hook,所以可以利用申请到栈中内容打ret2libc。所以此时需要的条件就是泄露出environ的内容,并且能申请到栈内存。
此时就要利用好函数0的功能了,因为函数0虽然只解析数据包,但是会有一步根据数据包长度进行堆块申请的操作。
注意到此时bin中有0xf0,所以可以先修改tcache的fd指针指向tcache_perthread_struct+0xf0,修改内容为tcache_perthread_struct。
可以看到此时0xf0处保存的就是tcache_perthread_struct+0x10。
此时如果触发函数0,并且数据包的长度为0xe0的话,那么数据包的内容就会写入到tcache_perthread_struct+0x10处。那么就可以控制tcachebin中的chunk数量以及chunk指针。我们控制其指向IO_stdout,然后申请出IO_stdout通过修改write_base和write_prt来触发IO_leak泄露出environ的内容也就是栈地址,但是需要注意的是还需要控制0xb0处的chunk指针为tcache_perthread_struct+0x10,这样就能再次利用申请出栈内存。
泄露完environ之后的bins:
再利用0xb0这个chunk控制tcache_perthread_struct的内容来申请出栈内存
劫持的是这个函数的返回地址
构造ROP即可触发system(‘/bin/sh\x00’)
EXP
1 | from pwn import * |
参考
1 | https://www.cnblogs.com/JmpCliff/articles/17595397.html |