一、为什么要做 Protobuf 逆向
Protobuf(Protocol Buffers)是 Google 推出的高性能二进制序列化协议,相比 JSON 体积可缩小 60%~80%,编解码速度快 3~5 倍,目前已广泛应用于微服务(gRPC)、移动端 APP、网络游戏、物联网设备等场景。但二进制序列化后,抓包工具中只会呈现一串无规律的 "乱码",无法像明文 JSON 一样直接读取字段含义,给接口调试、业务分析、安全测试带来了天然障碍。
本文从抓包拿到原始二进制数据开始,完整讲解从协议识别、手动解码推导到自动化还原完整 proto 定义的全流程,覆盖入门到高级的实操方法。
二、先搞懂:Protobuf 二进制的编码特征
做逆向的前提是先识别目标是不是 Protobuf,而识别的核心是理解其编码规则。
2.1 核心编码结构:Tag - Length - Value
Protobuf 消息本质是一系列字段的有序集合,每个字段遵循 Tag + [Length] + Value 的结构:
- Tag:每个字段的开头,采用 varint 编码,由「字段号 << 3 | wire type」计算得到,同时标识了字段编号和值的编码类型。
- Wire Type:共定义 6 种类型,常用的有 4 种:
表格
| wire type | 类型名称 | 对应 Protobuf 数据类型 |
|---|---|---|
| 0 | VARINT | int32、int64、uint32、uint64、sint32、sint64、bool、enum |
| 1 | I64(64 位固定长度) | fixed64、sfixed64、double |
| 2 | LEN(长度分隔) | string、bytes、嵌套 message、packed repeated 数组 |
| 5 | I32(32 位固定长度) | fixed32、sfixed32、float |
2.2 快速识别 Protobuf 的典型特征
拿到一段二进制乱码,满足以下特征时,大概率是 Protobuf 编码:
- 字节分布有规律,高频出现
0x08、0x10、0x18、0x20等数值,对应字段号 1、2、3、4 的 VARINT 类型 Tag; - 字符串类数据采用「单字节 / 多字节长度前缀 + 原始内容」结构,没有引号、逗号、大括号等明文分隔符;
- 字段号通常从 1 开始连续递增,很少出现跳号过大的情况;
- 没有固定的文件头 Magic Number,消息整体长度不固定。
常见误区:很多业务接口会将 Protobuf 二进制先做 Base64 编码或 Gzip 压缩,抓包看到的可能是字符串或压缩包。遇到这种情况需要先解码、解压,得到纯二进制后再继续分析。
三、第一步:抓包提取原始二进制数据
不同业务场景的抓包方式和数据提取方法不同,核心目标是拿到纯净的 Protobuf 载荷。
3.1 HTTP/HTTPS 场景(Web / 移动端接口)
- 常用工具:Charles、Fiddler、Burp Suite
- 操作方法 :找到对应接口的请求或响应,选择「Save Body」「导出原始数据」功能,保存为
.bin格式的二进制文件。 - 判断依据 :如果响应头
Content-Type为application/x-protobuf、application/grpc-web,基本可以确定是 Protobuf;如果是application/octet-stream,则需要结合编码特征进一步识别。
3.2 TCP/UDP 原生场景(游戏 / 私有协议)
- 常用工具:Wireshark、tcpdump
- 操作方法:通过端口、IP 筛选目标流量,右键选择「Follow → TCP Stream」,将展示格式切换为「Raw」,导出原始字节流。
- 注意事项:私有协议通常会在 Protobuf 消息外加自定义包头(比如 2 字节消息长度、4 字节序列号、1 字节命令号),需要先根据协议格式剥离包头,取出真正的 Protobuf 载荷。
3.3 gRPC 协议特殊处理
gRPC 默认基于 HTTP/2 传输,单条消息的格式为:1 字节压缩标志 + 4 字节大端消息长度 + Protobuf 本体。 逆向时需要跳过前 5 字节的 gRPC 头部,取出后续的二进制数据,才是真正的 Protobuf 消息体。
四、入门实操:手动解码推导数据结构
手动解码是 Protobuf 逆向的核心能力,工具只是提升效率的辅助。我们用一段真实的二进制数据举例,完整走一遍解码流程: 示例二进制:08 96 01 12 07 74 65 73 74 69 6e 67
步骤 1:解析第一个 Tag
第一个字节为0x08,转换为二进制是00001000:
- 低 3 位为 wire type:
000= 0 → VARINT 类型 - 右移 3 位得到字段号:
00001= 1 → 字段编号为 1
步骤 2:解析 VARINT 值
Tag 后紧跟的0x96 0x01是一个 varint 数值:
- varint 每个字节的最高位是延续位:
0x96最高位为 1,表示后续还有字节;0x01最高位为 0,表示该数值结束。 - 去掉每个字节的最高位,按倒序拼接有效位:
0010110 0000001→ 十进制结果为150。 - 结论:字段 1 的值为 150,类型属于 VARINT 族。
步骤 3:解析第二个 Tag
接下来的字节是0x12,二进制为00010010:
- 低 3 位:
010= 2 → LEN(长度分隔)类型 - 右移 3 位:
00010= 2 → 字段编号为 2
步骤 4:解析长度与内容
Tag 后紧跟的0x07表示内容长度为 7 字节。 后续 7 个字节74 65 73 74 69 6e 67为标准 ASCII 编码,对应字符串"testing"。
- 结论:字段 2 的值为 "testing",类型为 string。
步骤 5:推导初步消息结构
根据以上分析,可以写出对应的 proto 骨架:
message SampleMessage {
optional int32 field_1 = 1; // 数值150,初步推测为int32
optional string field_2 = 2; // 字符串类型
}
进阶判断技巧:如果同一个字段号在消息中重复出现,说明该字段是
repeated数组;如果 LEN 类型的内容本身又符合 Protobuf 编码规则,则说明这是一个嵌套消息。
五、自动化逆向:工具链批量还原
手动解码适合短消息验证,面对多层嵌套、几十上百个字段的复杂消息,需要借助工具提升效率。
5.1 官方基准工具:protoc --decode_raw
Google 官方的 protoc 编译器自带原始解码功能,不需要任何 proto 文件,即可解析二进制并输出字段号与对应的值。
-
使用命令:
cat message.bin | protoc --decode_raw
-
输出示例:
1: 150
2: "testing"
3 {
1: 12345
2: "hello world"
} -
优缺点:解析结果 100% 准确,是验证其他工具的基准;但只能输出匿名字段,无法区分具体子类型(比如 int32 还是 uint32),也没有语义化字段名。
5.2 结构化还原工具
- PBTK(Protobuf Toolkit):专门用于 Protobuf 逆向的工具集,支持输入二进制自动生成 proto 骨架,可智能推断字段类型、识别嵌套结构与 repeated 数组,支持批量文件处理。
- Protobuf Inspector :可视化桌面工具,导入二进制后以树形结构展开嵌套消息,支持直接编辑字段名和类型,最终导出完整的
.proto文件。 - Protoscope:命令行增强版解码工具,输出比 decode_raw 更易读的格式,支持高亮 Tag、长度、值,适合快速查看复杂消息。
5.3 客户端提取:从二进制文件中挖 Proto
如果目标有客户端程序(APP、PC 客户端),通常编译时会将 proto 描述符编译进二进制文件(so/dll/exe),提取的信息远多于纯黑盒逆向。
- protodump:可从 ELF、PE、Mach-O 格式的二进制文件中扫描 Protobuf 描述符,直接提取完整的 proto 定义,包含消息名、字段名、嵌套结构、枚举值等全部信息。
- Frida 运行时 Hook :Hook Protobuf 库的序列化 / 反序列化函数(如
SerializeToString、ParseFromString),在运行时直接打印消息明文,甚至导出内存中的完整 proto 描述符。
5.4 gRPC 专项:利用反射直接拿定义
如果目标 gRPC 服务开启了服务器反射,可以无需逆向直接获取全部接口定义:
# 列出所有服务
grpcurl -plaintext example.com:50051 list
# 查看指定服务的详细消息定义
grpcurl -plaintext example.com:50051 describe package.ServiceName
六、高级优化:从骨架到完整可用的 Proto
工具输出的 proto 骨架通常只有字段号和匿名类型,需要进一步优化补全,才能投入实际使用。
6.1 字段类型精准推断
- VARINT 类型族 :
- 数值均为正整数且范围不大 → int32 /uint32
- 出现极大的无意义数值 → 可能是 sint32/sint64 的 ZigZag 编码负数
- 数值为 10 位左右整数 → 大概率是 Unix 时间戳(int64)
- 只有 0、1、2 等连续小整数 → enum 枚举类型
- 固定长度类型族 :
- 数值符合浮点数分布 → float(I32)/double(I64)
- 整数且字节序固定 → fixed32 /fixed64
- LEN 类型族 :
- 内容为可读文本 → string
- 内容符合 Protobuf 编码规则 → 嵌套 message
- 内容是连续的同类型 VARINT → packed repeated 数组
6.2 字段名的语义还原
字段名不会编码进二进制,只能通过语义和旁证推断:
- 根据字段值反推语义:比如存手机号的字段可命名为
phone,存用户 ID 的命名为user_id; - 参考同业务其他明文接口、客户端日志、错误提示中出现的字段名;
- 从客户端二进制的调试符号、字符串常量中提取匹配的字段名称。
6.3 验证还原正确性
还原后的 proto 是否准确,可以通过编码回测验证:
- 使用还原的 proto 文件,将解码后的数据重新编码为二进制;
- 对比新生成的二进制与原始抓包二进制是否完全一致;
- 如果完全一致,说明结构 100% 正确;如果存在差异,重点检查字段类型判断是否错误、是否遗漏了可选字段。
七、常见坑与应对方案
- ZigZag 编码负数:普通 int32 编码负数会占 10 字节,sint32 采用 ZigZag 编码将负数映射为正整数,体积更小。如果 VARINT 值很大且无实际意义,优先考虑是 ZigZag 编码的负数。
- Packed Repeated 字段:repeated 基础类型字段默认采用 packed 模式,wire type 同样为 2,内容是连续的同类型值,很容易被误判为嵌套消息。
- 未知字段保留:Protobuf 解析时会自动保留未知字段,逆向时可能看到部分无法对应结构的字节,属于正常现象。
- 厂商自定义变种:部分厂商会修改 Protobuf 编码规则(比如调整 Tag 位宽、加密部分敏感字段),纯黑盒逆向难度大,建议结合客户端代码分析。
八、总结
Protobuf 逆向的完整流程可以总结为: 抓包提取原始流量 → 解码 / 解压 / 剥离包头得到纯 Protobuf → 工具初步解析出结构骨架 → 手动推断字段类型与语义命名 → 编码回测验证正确性
本质上 Protobuf 是一种结构化极强的二进制协议,只要掌握核心编码规则,配合合适的工具链,绝大多数场景下都可以还原出可用的数据结构。对于有客户端的场景,优先选择从二进制文件中提取 proto,效率和准确度都远高于纯黑盒逆向。