19、协议逆向基础:Protocol Buffers解析、FlatBuffers、自定义二进制协议分析
协议逆向,说白了就是跟App的“黑话”打交道。你抓到了加密数据,但不知道它长什么样,怎么拆解?这时候就得靠协议逆向。
我个人习惯把协议分成三类:结构化序列化协议(比如Protobuf)、高效内存布局协议(比如FlatBuffers)、以及纯自定义二进制协议(各家自己定的规则)。今天咱们一个一个聊。
一、Protocol Buffers 解析
Protobuf 是 Google 搞的,现在移动端用得特别多。它的核心思路是:定义一份 .proto 文件,然后生成各语言的序列化代码。数据在传输时是紧凑的二进制,不是 JSON 那种明文。
1.1 怎么识别 Protobuf
你在抓包时看到一串二进制,怎么判断是不是 Protobuf?我总结几个特征:
- 没有明显的分隔符:不像 JSON 有花括号、逗号,Protobuf 就是纯字节流。
- 字段编号规律:每个字段前有一个 varint 编码的 tag,包含 field_number 和 wire_type。
- 常见 wire_type:0(varint)、1(64位固定)、2(长度分隔)、5(32位固定)。
实战技巧:用 protoc 自带的 --decode_raw 选项,可以尝试解析未知的 Protobuf 数据。我在项目中遇到过好几次,服务端改了 proto 文件但没通知客户端,全靠这个命令临时救场。
1.2 手动解析示例
假设你抓到一段二进制:08 96 01 12 07 74 65 73 74 69 6e 67。我们来拆解:
08 → tag: field_number=1, wire_type=0 (varint)
96 01 → varint 值 = 150
12 → tag: field_number=2, wire_type=2 (length-delimited)
07 → 长度 = 7
74 65 73 74 69 6e 67 → UTF-8 字符串 "testing"
嗯,这里要注意:varint 编码用的是小端序,每个字节的最高位是 continuation bit。如果字节最高位是 1,说明后面还有字节。
1.3 逆向工具链
| 工具 | 用途 | 我的评价 |
|---|---|---|
| protoc | 官方编译器,支持 --decode_raw | 必备,但需要自己写脚本 |
| Protobuf Inspector | Frida 插件,动态解析 | 调试时很好用 |
| Blackbox Protobuf | Burp Suite 插件 | 抓包时直接看字段 |
我的建议:别一上来就想着还原完整的 .proto 文件。先抓几个包,用 --decode_raw 看看字段结构,再结合业务逻辑猜字段含义。我曾经花了两天去猜一个 int64 字段,最后发现是时间戳——嗯,白费功夫。
二、FlatBuffers 分析
FlatBuffers 是 Google 的另一个作品,跟 Protobuf 思路完全不同。它的特点是:不需要解析,直接通过偏移量访问数据。说白了,数据在内存里怎么布局,传输时就是什么样。
2.1 为什么用 FlatBuffers
移动端游戏、实时性要求高的场景,FlatBuffers 很常见。原因很简单:
- 零拷贝:收到数据后直接读取,不需要反序列化。
- 内存效率高:没有额外的对象开销。
- 访问速度快:O(1) 时间复杂度访问任意字段。
你想想看,一个游戏每帧要同步几百个对象的位置,如果用 JSON 或 Protobuf,光解析就能把 CPU 吃满。FlatBuffers 就没有这个问题。
2.2 识别 FlatBuffers
FlatBuffers 的二进制特征比较明显:
- 文件头:通常以 4 字节的 magic number 开头,比如 "FLAT" 或自定义标识。
- vtable 机制:每个对象前有一个 vtable,记录各字段的偏移量。
- 对齐填充:字段按 4 字节或 8 字节对齐,中间可能有 padding。
避坑指南:我曾经在逆向一个手游时,发现它的 FlatBuffers 魔数被改成了 "GAME"。当时没注意,用标准解析器一直报错。后来才发现是魔数被魔改了。所以,别迷信标准魔数,多看看二进制头部的特征。
2.3 手动解析 FlatBuffers
解析 FlatBuffers 的核心是理解 vtable 的结构。假设你有一个 table 对象:
// 二进制布局(简化)
[SOF] [vtable偏移] [数据区] [vtable]
4字节 4字节 变长 变长
// vtable 结构
[table大小] [vtable大小] [字段1偏移] [字段2偏移] ...
2字节 2字节 2字节 2字节
解析步骤:
- 读取 SOF(起始偏移),通常是 4 字节。
- 从 SOF 位置读取 vtable 偏移。
- 跳转到 vtable,读取字段偏移表。
- 根据字段偏移,从数据区读取实际值。
个人经验:我建议用 Python 写一个简单的 FlatBuffers 解析器,配合 Frida 动态调用。这样既能静态分析二进制,又能动态验证字段含义。我自己的工具链里,这个脚本已经迭代了三个版本了。
三、自定义二进制协议分析
这是最头疼的一类。没有标准,没有文档,全靠逆向工程师自己猜。我遇到过很多 App,自己定义了一套二进制协议,字段含义全靠服务端和客户端约定。
3.1 分析思路
面对一个陌生的二进制协议,我的套路是这样的:
- 找固定模式:看有没有魔数、长度字段、校验和。
- 对比多个包:同一个接口发两次,看哪些字节变了,哪些没变。
- 边界测试:改一个输入参数,看哪些字节跟着变。
- 找长度字段:通常变长字段前会有一个长度指示。
为什么会这样?因为自定义协议再奇葩,也得遵循基本的通信规则。服务端和客户端得能互相理解,所以一定有某种约定。
3.2 一个真实案例
我曾经逆向一个社交 App 的登录协议。抓到的包长这样:
AA BB 00 1E 01 00 00 00 0A 00 00 00 74 65 73 74
00 00 00 00 08 00 00 00 31 32 33 34 35 36 00 00
我的分析过程:
- AA BB:魔数,固定不变。
- 00 1E:总长度 30 字节(小端)。
- 01:命令号,登录请求。
- 0A 00 00 00:用户名长度 10。
- 74 65 73 74:用户名 "test"(但只有4字节,说明有 padding)。
- 08 00 00 00:密码长度 8。
- 31 32 33 34 35 36:密码 "123456"。
避坑指南:注意 padding!很多自定义协议为了对齐,会在字符串后面补 0x00。我刚开始分析时,经常把 padding 当成数据的一部分,导致解析出来的字符串多了一堆空字符。后来我养成了习惯:先看长度字段,再按长度截取,忽略后面的 padding。
3.3 工具推荐
| 工具 | 适用场景 | 备注 |
|---|---|---|
| 010 Editor | 静态分析二进制 | 支持模板,可以写解析脚本 |
| Binwalk | 识别协议特征 | 适合找魔数、签名 |
| Frida + 自定义脚本 | 动态 Hook 解析 | 我最常用的方式 |
四、知识体系总览
下面这张图,是我对协议逆向知识体系的总结。你可以把它当作一个思维导图来用:
协议逆向这件事,说白了就是跟二进制数据较劲。Protobuf 有标准可循,FlatBuffers 有规律可抓,自定义协议就得靠经验积累。我个人觉得,最重要的不是记住每种协议的细节,而是培养一种“二进制直觉”——看到一串字节,能快速判断它大概是什么结构。
嗯,今天就聊到这里。记住:多动手,多对比,多总结。工具只是辅助,真正的功力在于你分析问题的思路。