Protobuf脱壳二进制
Protocol Buffers(简称 Protobuf)是由 Google 开发的一种语言中立、平台中立的可扩展序列化结构数据的方法。Protobuf 是一种高效的协议,常用于网络通信、数据存储和使用数据的应用程序之间的传输,最近几年的国赛和其他大赛都相继会在原pwn的基础上套一层protobuf的壳 故写下此文章总结:
需要安装 Protobuf运行时 和 协议编译器(用于编译.proto文件)。
1 | sudo apt-get update |
1 | protoc --c_out=. user.proto |
这会在当前目录下生成两个文件:user.pb-c.h 和 user.pb-c.c
user.pb-c.h:头文件,定义了消息类型(如User结构体)及其相关的函数。
user.pb-c.c:源文件,实现了这些函数,包含序列化和反序列化等操作。
在你的C程序中,你需要包含生成的user.pb-c.h头文件,这样你就可以使用User消息类型及其相关的protobuf操作函数。
如果想要编译为Python代码,用如下命令(在CTF中通常编译为Python代码以在脚本中与程序交互):
1 | protoc --python_out=. demo.proto1 |
会生成 demo_pb2.py。(pb2后缀只是为了和protobuf1区分)
Protobuf 的基本语法:
1 | syntax = "proto3"; // 指定使用 Protocol Buffers version 3 (proto3) 语法 |
逆向分析关键
在生成的demo-pb-c.c文件中,可以发现存在unpack函数:
1 | /** |
这个反序列化函数传入描述消息结构体数据的descriptor。我们可以在IDA中分析descriptor还原消息结构体。
Descriptor结构体
1 | struct ProtobufCMessageDescriptor { |
- fields是ProtobufCFieldDescriptor类型。
ProtobufCFieldDescriptor结构体
1 | struct ProtobufCFieldDescriptor { |
type和label是枚举类型:
1 | typedef enum { |
C 结构体变量类型内存数值
| 内存数值 (Dec) | 内存数值 (Hex) | 对应 Proto 类型 | 底层 Wire Type | 实战内存占用特征 |
|---|---|---|---|---|
| 0 | 0x00 |
int32 |
0 (Varint) | 占 4 字节(有符号) |
| 1 | 0x01 |
sint32 |
0 (Varint) | 占 4 字节(ZigZag 压缩编码) |
| 2 | 0x02 |
sfixed32 |
5 (32-bit) | 占 4 字节(固定长度) |
| 3 | 0x03 |
int64 |
0 (Varint) | 占 8 字节 |
| 4 | 0x04 |
sint64 |
0 (Varint) | 占 8 字节(高频考点,解析用 ZigZag) |
| 5 | 0x05 |
sfixed64 |
1 (64-bit) | 占 8 字节(固定长度) |
| 6 | 0x06 |
uint32 |
0 (Varint) | 占 4 字节(无符号) |
| 7 | 0x07 |
fixed32 |
5 (32-bit) | 占 4 字节 |
| 8 | 0x08 |
uint64 |
0 (Varint) | 占 8 字节 |
| 9 | 0x09 |
fixed64 |
1 (64-bit) | 占 8 字节 |
| 10 | 0x0A |
float |
5 (32-bit) | 占 4 字节 |
| 11 | 0x0B |
double |
1 (64-bit) | 占 8 字节 |
| 12 | 0x0C |
bool |
0 (Varint) | 占 4 字节(实际存 0 或 1) |
| 13 | 0x0D |
enum |
0 (Varint) | 占 4 字节 |
| 14 | 0x0E |
string |
2 (Length-del) | *占 8 字节(仅存指向字符串的指针 char*)* |
| 15 | 0x0F |
bytes |
2 (Length-del) | 占 16 字节(复合结构体 ProtobufCBinaryData) |
| 16 | 0x10 |
message |
2 (Length-del) | 占 8 字节(指向子 Message 实例的指针) |
C++ 结构体变量类型内存数值
| 数值 (Hex) | 数值 (Dec) | 对应 Proto 类型 | 传输编码 (Wire Type) | 说明 |
|---|---|---|---|---|
| 0x01 | 1 | double |
1 (64-bit) | 固定的 8 字节浮点数 |
| 0x02 | 2 | float |
5 (32-bit) | 固定的 4 字节浮点数 |
| 0x03 | 3 | int64 |
0 (Varint) | 变长编码,处理负数效率低 |
| 0x04 | 4 | uint64 |
0 (Varint) | 无符号变长 64 位整型 |
| 0x05 | 5 | int32 |
0 (Varint) | 变长编码,常用 |
| 0x06 | 6 | fixed64 |
1 (64-bit) | 固定 8 字节,常用于存储大整数地址 |
| 0x07 | 7 | fixed32 |
5 (32-bit) | 固定 4 字节 |
| 0x08 | 8 | bool |
0 (Varint) | 实际上是 0 或 1 的 Varint |
| 0x09 | 9 | string |
2 (Length-delimited) | 必须是 UTF-8 文本 |
| 0x0A | 10 | group |
3 (Start) | 已废弃的 Proto2 语法 |
| 0x0B | 11 | message |
2 (Length-delimited) | 嵌套 Message,会调用子解析函数 |
| 0x0C | 12 | bytes |
2 (Length-delimited) | 原始字节流,Pwn 题最爱 |
| 0x0D | 13 | uint32 |
0 (Varint) | 无符号变长 32 位整型 |
| 0x0E | 14 | enum |
0 (Varint) | 对应的 C++ 里的枚举值 |
| 0x0F | 15 | sfixed32 |
5 (32-bit) | 有符号固定 4 字节 |
| 0x10 | 16 | sfixed64 |
1 (64-bit) | 有符号固定 8 字节 |
| 0x11 | 17 | sint32 |
0 (Varint) | ZigZag 编码,高效处理负数 |
| 0x12 | 18 | sint64 |
0 (Varint) | ZigZag 编码,高效处理负数 |
protobuf 脱壳
protpbuf 脱壳既可以手动分析也可以使用工具分析
工具分析:
1 | pip install PySide6 -i https://pypi.tuna.tsinghua.edu.cn/simple |
然后就会出现图形化软件:

手动分析:
分析的关键就是定位,根据magic的值定位到description结构体的位置 然后按照字节去逆向还原结构体,并根据还原的结构体去定位到ProtobufCFieldDescriptor结构体同样是按照字节去逆向还原结构体,还原之后再根据参数个数去判断是proto2还是proto3,因为proto3删除了预留的值
接下来我们通过两道国赛 protobuf 的堆题进行讲解
参考资料:https://xz.aliyun.com/news/16091
更新: 2026-05-12 19:53:54
原文: https://www.yuque.com/idcm/wnemg9/fk0ue9icar5bgq26