[pwn] protobuf学习与逆向
介绍
一个轻量的跨平台数据结构协议。
说人话就是json-pro,或者说zip-pro。zip不也是不论在手机、电脑、macos都能解压吗,,
当然运转起来和zip不同,毕竟这个是为了程序间收发信息而发明的,而zip只是为了打包数据,只是一个不太恰当的类比。
快速安装
1 | sudo apt install protobuf-compiler libprotobuf-dev |
protobuf支持c++、java、python、go等多种语言,但是不支持c语言。
https://github.com/protobuf-c/protobuf-c
不过这里有一个社区成员自发维护的proto-c实现,要支持c语言额外安装这个即可
(注意make之前先checkout到最新的release tag)。
基本语法
定义消息类型
1 | syntax = "proto2"; |
文件的第一行指定您正在使用 protobuf 语言规范的 2023 版。
edition(或 proto2/proto3 的syntax)必须是文件的第一个非空、非注释行。- 如果未指定
edition或syntax,协议缓冲区编译器将假定您正在使用 proto2。
SearchRequest消息定义指定了三个字段(名称/值对),每种你想包含在此类消息中的数据都对应一个字段。每个字段都有一个名称和一个类型。
定义字段
定义字段类型

这里有一些类型,总之就是整数,浮点数,布尔和字符串。
除了前面那个例子里定义的标量类型,也可以是枚举或者复合类型。
在proto3里面这个有些许扩展,具体可以上官网查看。
分配字段编号
1 | syntax = "proto2"; |
依然是这个例子,其中的=1和=2就是字段编号。每个字段编号必须唯一,范围是从0到2^29
指定字段基数
翻译成基数很奇怪,感觉应该类似字段属性的意思。
- 单一 (Singular):
optional:(推荐)optional字段处于两种可能状态之一- 你可以用接口查询发送方有没有显式设置值,比如说接收到了默认值,但你不清楚发送方是没设置,还是设置了,刚好就是这个值。
- 建议使用
optional而不是隐式字段,以实现与protobuf版本和proto2的最大兼容性。 - 隐式:(不推荐)隐式字段没有明确的基数标签,其行为如下
- 如果字段是消息类型,则其行为与
optional字段完全相同。 - 如果字段不是消息,它有两种状态
- 字段设置为非默认(非零)值,该值已显式设置或从数据线解析。它将被序列化到数据线。字段设置为默认(零)值。它不会被序列化到数据线。事实上,您无法确定默认(零)值是已设置还是从数据线解析,还是未提供。有关此主题的更多信息,请参阅字段存在。
- (翻译够怪的,总之参考上面
option的意思,这里和option相反)
repeated:此字段类型在一个格式良好的消息中可以重复零次或多次。重复值的顺序将被保留。map:这是一个键值对字段类型。有关此字段类型的更多信息,请参阅映射。
其实就是普通的optional,数组repeated,哈希表map
还有一个require被废弃了,官网上写不建议使用
字段默认值
- 对于字符串,默认值是空字符串。
- 对于字节,默认值是空字节。
- 对于布尔值,默认值是 false。
- 对于数值类型,默认值是零。
- 对于消息字段,该字段未设置。其确切值取决于语言。
- 对于枚举,默认值是第一个定义的枚举值,该值必须为 0。
当然你可以自己指定默认值。
1 | syntax = "proto2"; |
如果发送方未指定 result_per_page,接收方将观察到以下状态
result_per_page字段不存在。也就是说,has_result_per_page()(存在器方法)方法将返回false。result_per_page的值(从“getter”返回)为10。
这里有点特别,虽然有值,但是查询这个值存不存在返回false,直接获取值又能获取到,感觉容易出小错误。
枚举类型
1 | enum Corpus { |
文档建议枚举类型的第一个值必须为0且不包含任何语义,因为兼容性问题。
像 SearchRequest.corpus 这样的枚举字段的默认值可以像这样显式覆盖
1 | Corpus corpus = 4 [default = CORPUS_UNIVERSAL]; |
枚举也可以有别名,就是一个值有两个名字,像下面这样
1 | enum EnumAllowingAlias { |
不过如果这么用的话,protocol buffer 编译器会生成一条警告消息(可设置allow_alias 为 true来关闭)。以及虽然所有别名值在序列化时都有效,但在反序列化时只使用第一个值。
删除枚举条目
1 | enum Foo { |
要用reserved。
比如说某项目里,字段3是AAA,一次更新以后字段3被删掉,但是没有reversed。过了一段时间另一个人要添加一个新字段,发现3空着的,就把3用给BBB。但是有其他模块没有及时更新到这个信息,结果就是本来接收BBB的接口会收到AAA,造成严重的问题。
用reversed把被删除的flag标记,这样如果后来的维护者重用了字段,编译就会报错。
proto序列化的不规范性
意思是对于同样的数据,proto不能保证其被序列化为相同的结果。
比如说哈希密钥验证等流程,不应该直接使用proto序列化出来的数据,因为它们可能随着版本更新发生变化。正确的姿势是反序列化提取其中的信息,然后再拿去用。
使用教程
参考了下这个
https://github.com/protobuf-c/protobuf-c/wiki/Examples
这里是c的使用教材,如果需要python或者C++可以区看官方文档
1 | syntax = "proto2"; |
写一个这个,然后输入protoc-c --c_out=. amessage.proto
接下来在自己的代码中引用生成的文件
1 |
|
pack和unpack,然后unpack可以传allocator,可以传自定义堆管理器,传null就是用malloc。
编译指令
1 | gcc ./main.c ./message.pb-c.c -o ./main -lprotobuf-c |
如果你用syntax = "proto3"会发现生成不了,因为神秘的“咕勾”在3.12版本把optional扬了,引发了设区的强烈反对,后来又加了回来。
而proto-c是社区人员维护的,最新的release版本还没对齐这个需求。
我们可以看看生成的文件里面有什么,其实也很简单。
1 | /* Generated by the protocol buffer compiler. DO NOT EDIT! */ |
程序输出结果:

rpc协议支持
1 | service SearchService { |
rpc(Remote Produce Call)是一种通信协议,让你可以调用远程服务器上的函数,就如同调用本地的函数一样。protobuf支持这个,但是你需要自己提供rpc框架。
后面可以跟gRPC结合,详细放到新的文档写吧。
逆向
定位结构体,逆向通信协议,然后根据结构体把.proto文件重新写出来,然后用proto生成.py接口就行了。
这里有魔数可以定位。

然后就是把类型一个一个套上去,根据宏的名字把.proto反推就行了ovo
叽里咕噜说什么呢跟我结构体板子说去吧
右键parse statements全部导入,然后想要恢复哪个结构体自己填
1 | typedef enum { |
叽里咕噜说什么呢跟我自动化脚本说去吧
有pbtk,实测识别不太出来
也有auto-re,实测太久没人维护(6years ago)
// todo