apk-reverse协议逆向实战:从零解码无schema的protobuf流量
【免费下载链接】apk-reverseSuitable for Android APK reverse engineering analysis项目地址: https://gitcode.com/gh_mirrors/ap/apk-reverse
📦apk-reverse是一个面向 Android APK 逆向分析的工具库,覆盖流量抓取、协议解码、数据编辑等全流程。今天这篇实战指南,教你不依赖任何 .proto 文件,把 App 抓下来的 protobuf 二进制流量从零解码成可读的字段树,并进一步还原出可用的 schema。全程只需要 Python 标准库,新手也能跑通。
一、先搞懂:protobuf 为什么"抓下来就看不懂"
普通 HTTP 抓包看到的往往是 JSON,而 protobuf 流量就是一串看不懂的十六进制字节。原因很简单:
| 特性 | 说明 |
|---|---|
| 无字段名 | 线上只有"字段号 + 值",名字全部来自 schema |
| 无头部 | 没有魔数、没有声明,无法自描述 |
| 四种含义共用一种编码 | 字符串、嵌套消息、packed 数组、原始字节都长一个样 |
换句话说:schema 赋予含义,字节本身只负责运输。没有 schema 的解码就是"猜"——但这不是玄学,protobuf 有严格的线格式规则(varint 编码、tag =字段号<<3 | 线类型),可以逐字节走一遍。
二、核心武器:无 schema 解码器的原理
protobuf_decode_raw.py 是这个仓库自带的解码脚本,纯标准库实现,零依赖。它的核心思想很诚实:一个字段如果存在多种合法解读,就全部列出来,而不是替你"编造"一个结论。
python skills/apk-reverse/scripts/protobuf_decode_raw.py --hex "08 96 01 12 07 74 65 73 74 69 6e 67"输出类似这样一棵树:
f1 varint 150 f2 len-delimited(7) view=utf8_string → string: 'testing'关键机制:
- 候选列表 + 置信度:对每个长度字段,同时给出
nested_message / utf8_string / packed_varint / bytes等候选及启发式置信度。当两种解读真的难分伯仲时(这在格式上是合法的),会明确标注tie,提醒你"这一步无法靠字节定论"。 - proto3 零值陷阱标注:proto3 中"值为 0"和"字段根本没设置"在字节上完全相同(什么都不写)。解码器遇到显式 0 会打上
proto3_explicit_zero标记——这是新手最容易踩的坑。 --reencode往返验证:解码成 JSON 树(可手工编辑)再编码回字节,与原始文件逐字节比对。这是"无 schema 条件下最强的证据":字节级往返一致,说明你的解读没丢信息。
工具自身通过26 项已知答案测试(--selftest),并与官方 protobuf 运行时序列化结果逐字段对齐,相关测试见 test_protobuf_decode_raw.py。
三、实战:一条流量的完整解码流程
假设你从代理工具中导出了一段二进制 body(或 hex 文本),四步走:
- 喂入解码:hex、二进制文件、stdin 三种输入都支持,
|可分隔多个帧; - 看候选树:优先信任置信度最高的结构解读,对
tie字段先标记、不下结论; - 多样本对照:解码多条同类消息——哪些字段每次都出现(必有)、哪些时隐时现(可选)、哪个字段的值域很小(可能是枚举);
- 往返验证:
--reencode编回去对比,字节一致即解读成立。
💡没有网络流量也有样本:App 本地存的 OkHttp 磁盘缓存、DataStore 的
.preferences_pb文件、Room 数据库里的 blob 列,都是天然的样本源。runtime-data.md 讲了怎么读取这些文件;仓库自带的 datastore_inject.py 甚至能反过来编辑DataStore 文件并让另一个工具读回新值——完整的闭环验证。
四、进阶:从 APK 中恢复出真正的 schema
解码器能读字节,但字段名叫f1、f2终究不够用。下一步是回到 APK 里挖 schema,参考 protocol-reverse.md 的第二节,有两条路线:
- 路线 A(默认):反编译后搜索 protobuf 生成代码的特征标记,例如
extends GeneratedMessageLite、switch (tag >>> 3)的 case 分派、readInt32()/readStringRequireUtf8()等调用。每个case就是一个字段号,紧跟的read*调用给出线类型——R8 混淆会抹掉类名字段名,但抹不掉字段号和线类型。字段名丢了就叫field_<n>,合法的 .proto 并不要求原名。 - 路线 B(加速):protobuf-java-lite 3.x 把一份紧凑的 schema 字符串内嵌在消息类里,找到它就是字段号、类型、标签三合一。版本敏感,作为路线 A 的加速项使用。
验证是关键一步:用protoc --decode=<消息名> <你的schema.proto>对抓到的真实字节做解码——每一条样本都解出连贯结构,你的字段表才是对的。解出乱码优先怀疑线类型(read*调用看错了),而不是字段号。
五、gRPC 与两个高频坑
如果流量是 gRPC(HTTP/2 +application/grpccontent-type,:path形如/<包名>.<服务>/<方法>),注意帧格式:每条消息前面是1 字节压缩标志 + 4 字节大端长度,这与 protobuf 的 varint 长度分帧不是一回事。
⚠️ 实测记录的失败案例:把 gRPC 帧直接喂给 varint 分帧模式,工具不会报错,而是"成功"解出 5 个帧——错误的分帧依然能产出解码结果,这正是它昂贵的原因。正确做法是手动剥掉 5 字节 gRPC 前缀,并对帧数做双向检查。
另一个坑:解码干净 ≠ 这是真的消息。随机字节常常也能解出一棵像模像样的字段树。防御手段就是第三节第 3、4 步:多样本对照 + 往返验证。
六、写在最后
无 schema 解码的边界同样清晰:它无法判定长度字段"就是"字符串、无法给出 packed 数组的元素边界、也无法证明字段号在两条流量里含义相同。这些判断要靠多样本统计 + schema 恢复 + 往返验证三者合力。
本仓库把整条链路做成了可复现的证据链:解码器、注入工具、官方运行时交叉校验、失败案例记录,全部有相对路径可查——从 docs/tool-verification/EXTENSION-protobuf-raw.md 可以逐条看到每条命令和输出。按这套流程走一遍,"无 schema 的 protobuf 流量"就不再是一堵死墙,而是一棵可以逐层展开的树 🌳
【免费下载链接】apk-reverseSuitable for Android APK reverse engineering analysis项目地址: https://gitcode.com/gh_mirrors/ap/apk-reverse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考