前言
其实在以前已经多次遇见过protobuf
了,但是都不太在意因为总感觉会像musl库一样,存活一段时间后就消失了。所以也导致我一直没有去真正做过这样的题,这次国赛第一天恰好出现了这样一道题,不出意外没能解出来,如果不看wp我可能还会怀疑自己的逆向能力,因为我蠢到看了几个小时的1200多行代码。最可恶的是当初不想玩web的一大原因就是我比较粗心大意,面对信息收集时往往会忽略掉重要信息,但是现在的pwn也越来越往这个方向靠了。不可否认的是,这提升了选手的综合实力(恶心选手),只是我不太能接受从一个坑又跳到了另外一个坑里面去了。不过,需要认清现实的是我的逆向水平确实很差,我也准备开始刷逆向题了。
protobuf
什么是protobuf
Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。
同XML相比,Protocol buffers在序列化结构化数据方面有许多优点:
- 更简单
- 数据描述文件只需原来的1/10至1/3
- 解析速度是原来的20倍至100倍
- 减少了二义性
- 生成了更容易在编程中使用的数据访问
- 支持多种编程语言
(转自百度百科)
这里就不多提了,安装的话自己搜一下就有的。
使用protobuf
首先编写一个测试文件test.proto
1 | syntax = "proto2"; |
使用protoc --c_out=. ./test.proto
命令生成对应代码
1 | /* Generated by the protocol buffer compiler. DO NOT EDIT! */ |
这是test.pb-c.h
文件,可以看到其中定义了许多的函数,并且定义了结构体。
1 | /* Generated by the protocol buffer compiler. DO NOT EDIT! */ |
这个文件就是test.pb-c.c
文件,内部对test__field_descriptors
数组进行了复制,这里使用的结构体为ProtobufCFieldDescriptor
1 | struct ProtobufCFieldDescriptor { |
而这个结构体中的type就是数据的类型,需要注意的是,我们在proto
文件中,分别定义了int64
和sint64
虽然结构体中都被翻译成了int64_t
类型,但是可以在test.pb-c.c
文件中看到,他们在上述结构体中的type值是不一样的。
1 | typedef enum { |
而这里type值的定义是这样的,知道这个很重要,在后续的做题环节中需要。
然而,这种题目一般来说都是用户态的题目,而面对用户态题目我们写的脚本更多的是使用python
去写,这里同样可以使用protoc
工具生成python
文件可以引入的文件。命令为:protoc --python_out=. ./test.proto
StrangeTalkBot
这道题是ciscn2023的第二道pwn题。
题目分析
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
可以看到,题目主干比较清晰。
1 | char *__fastcall sub_155D( |
而在这个函数内部其实就是很经典的菜单类堆题,并且题目中的漏洞也很简单,就是一个很单纯的UAF。麻烦的是sub_192D
函数内部的sub_5090
有很长的代码。虽然我也不知道怎么猜的,但是他就是protobuf
对应的unpack
函数。
理解题目中的protobuf
需要理解的话,首先就是需要确定题目中各个函数的含义以及部分可能需要知道的结构体。
1 | __int64 __fastcall sub_192D(__int64 a1, __int64 a2, __int64 a3) |
可以看到这个函数内部只是调用了另外一个函数,并且返回出另外函数的返回值。
1 | char *__fastcall sub_5090( |
然而这个函数的返回值是一个指针(参数的类型都是已经经过了我的修改了)。其实通过对比可以发现结构很类似上述中的unpack
函数。
1 | Test *test__unpack(ProtobufCAllocator *allocator, size_t len, const uint8_t *data) |
当然,可以通过搜索其内部的字符串
1 | actionid_str = desc->fields_sorted_by_name; |
比如函数开头的assert即可搜索到其源码位置。所以这个1200多行的函数其实就是protobuf_c_message_unpack
函数。
那么就可以通过这个函数所使用的参数,直接在ida中添加结构体进行进一步分析。
1 | 00000000 ProtobufCMessageDescriptor struc ; (sizeof=0x78, mappedto_19) |
在前面我们提到了ProtobufCFieldDescriptor
结构体,这个结构体中存储着结构体中所有成员的数据类型,并且第一个成员是指向其名字的地址,那么我们可以根据字符串找到结构体相印的位置。
1 | .data.rel.ro:0000000000009B60 80 70 00 00 00 00 00 00 01 00+stru_9B60 ProtobufCFieldDescriptor <7080h, 1, 0, 4, 0, 18h, 0, 0, 0, 0, 0, 0> |
最终可以得出,前面三个成员的数据类型为sint64
,最后一个成员的数据类型为bytes
,所以可以自己写出proto
文件了。
1 | syntax = "proto2"; |
然后使用上述代码生成python
对应的文件。
1 | # Generated by the protocol buffer compiler. DO NOT EDIT! |
现在就只需要拿着这个文件去使用即可,后续的漏洞利用部分比较简单,这里就不详细说了。
综上可得,exp
1 | from pwn import * |
参考链接:
https://github1s.com/protobuf-c/protobuf-c/blob/HEAD/protobuf-c/protobuf-c.c#L3028