指挥 AI 逆向牛马畅听 App:一次完整的实战复盘
这篇文章记录了我如何指挥 Claude Code(AI)完成牛马畅听 Android App 的协议逆向,包括整个过程中 AI 走的弯路、遇到的死胡同、以及最终是怎么绕出来的。
一、任务目标
目标:逆向牛马畅听(com.xs.fm)v6.3.6.32 的网络协议,抓取业务 API 的请求和响应数据,最终实现 Python 脚本直接调用接口。
难点预判:
- 大厂系 App,使用自研 TTNet 网络栈(基于 Chromium Cronet)
- 支持 QUIC/HTTP3 + TLS 1.3/HTTP2 双栈通信
- 有 Anti-Frida 检测(libmetasec_ml.so)
- 有 SSL Pinning
- 业务接口可能需要 x-argus / x-gorgon 等签名
二、AI 的解题路线图(以及它是怎么一步步走偏又纠正回来的)
整个过程可以用一张图概括:
复制代码 隐藏代码第一阶段:常规 SSL Hook(碰壁) hook_fanqie.js → hook_cronet_minimal.js → hook_quic.js ↓ 发现:SSL_write/SSL_read 抓不到业务数据 第二阶段:深入 Cronet Native 层(继续碰壁) hook_native_v2.js → hook_v4.js → hook_v5.js ↓ 发现:Cronet C API 只能拿到 URL,拿不到 Body 第三阶段:自己解析 HTTP/2(技术上成功,实际无用) hook_v6.js → hook_v7.js + hpack_module.js ↓ 发现:能解析辅助接口,但核心业务接口走的是 TTNet 内部通道 第四阶段:尝试 TLS Keylog(理论可行,实践受阻) hook_v8_keylog.js → hook_v9_keylog.js → hook_v10.js + tcpdump/Wireshark ↓ 发现:Keylog 注入成功但只覆盖辅助连接,核心连接的 SSL_CTX 不走公开 API 第五阶段:降维打击——直接 Hook Java 层(一击命中) hook_java_callback.js → hook_java_intercept.js → hook_v11_full.js ↓ 发现:业务 API 根本不需要签名!直接 Python requests 就能调
三、详细过程:每一步 AI 做了什么、卡在哪、怎么绕的
3.1 第一阶段:常规思路——Hook SSL 函数(hook_fanqie.js)
AI 的思路:既然要抓 HTTPS 数据,那就 Hook SSL_write 和 SSL_read,这是最标准的做法。
AI 做了什么:
- 编写了完整的 Anti-Frida bypass(hook strstr/strcmp/ptrace/connect)
- 定位到
libttboringssl.so(大厂自定义的 BoringSSL) - Hook 了
SSL_write、SSL_read、SSL_do_handshake - 同时 Hook 了
libsscronet.so的 Cronet C API
碰壁:
复制代码 隐藏代码SSL_write/SSL_read 确实有数据流过,但全是辅助接口的流量(DNS解析、日志上报、配置拉取)。 核心业务 API(novelfm.snssdk.com)的数据完全不经过标准 SSL 函数。
关键发现:AI 通过分析 SNI 和数据流向,发现牛马畅听的网络架构是:
- 辅助接口 → 标准 TLS →
SSL_write/SSL_read✅ 能抓到 - 业务接口 → TTNet 内部 TLS 通道 → 完全绕过标准 SSL API ❌ 抓不到
AI 走的弯路:花了大量精力在 Cronet 的 C API 上,写了
hook_cronet_minimal.js和hook_quic.js两个脚本,Hook 了Cronet_UrlRequest_Start、Cronet_UrlResponseInfo_url_get、Cronet_Buffer_GetData等十几个函数。结果只能拿到 URL 和状态码,请求体和响应体都是空的。
3.2 第二阶段:加码——Hook Cronet 内部函数(hook_v4.js, hook_v5.js)
AI 的思路:既然标准 API 不行,那就深入 Cronet 内部,Hook 更底层的函数。
AI 做了什么:
- V4:Hook
_set系列函数(Cronet_UrlResponseInfo_url_set等),试图在数据写入时截获 - V5:Hook 大厂自定义的
Cronet_LogMonitorListener_OnInnerRequestFinished,试图从日志回调中拿数据 - 还尝试了 Hook
sendto/recvfrom(因为 QUIC 走 UDP)
碰壁:
_set函数确实被调用了,但只设置了 URL 和状态码,Body 数据不经过这些函数LogMonitorListener只有统计信息,没有实际数据- UDP 层的数据是 QUIC 加密后的密文,看不出任何有用信息
这里 AI 犯了一个典型错误:试图用"加量"的方式解决"方向错误"的问题。Hook 了越来越多的 Native 函数,但本质上都是在错误的层级上找数据。
3.3 第三阶段:硬核方案——自己实现 HTTP/2 帧解析(hook_v6.js, hook_v7.js)
AI 的思路:SSL_write/SSL_read 能拿到一些数据,但它们是 HTTP/2 二进制帧,需要自己解析。
AI 做了什么:
这是整个过程中技术含量最高的一步。AI 在 Frida 脚本里从零实现了:
- HTTP/2 帧头解析器(9 大厂帧头 → Length/Type/Flags/StreamID)
- HPACK 头部压缩解码器(包含 RFC 7541 完整静态表 + 动态表)
- Huffman 解码树(RFC 7541 Appendix B 完整 256+EOS 码表)
- HTTP/2 流状态管理(多路复用的 DATA 帧拼接)
- 配套的 Python 端(hook_realtime.py):Brotli 解压 + JSON 格式化
V7 脚本直接膨胀到了 700+ 行,其中光 Huffman 码表就占了 82 行。
部分成功:
复制代码 隐藏代码
辅助接口的 HTTP/2 数据被完美解析了:
- DNS 解析请求/响应 ✅
- 配置拉取 ✅
- 日志上报 ✅
- AB 测试 ✅但核心业务接口依然是空白。
AI 走的最大弯路就在这里:花费了巨大精力实现了一整套 HTTP/2 + HPACK + Huffman 的解码系统,技术上确实很硬核,但解决的是错误的问题。核心业务数据根本不经过标准
SSL_write/SSL_read,所以不管你怎么解析,都解析不到想要的数据。
3.4 第四阶段:最后挣扎——TLS Keylog + Wireshark(hook_v8, v9, v10)
AI 的思路:既然 Hook 不到内存中的明文,那就导出 TLS 密钥,配合抓包在 Wireshark 里离线解密。
AI 做了什么:
- V8:注入
SSL_CTX_set_keylog_callback,导出 Master Secret - V9:在
SSL_CTX_new时立即注入 keylog callback,确保不遗漏任何连接 - V10:深入到 BIO 层,Hook
SSL_set0_wbio/SSL_set0_rbio,试图拦截 BIO 函数指针 - 配合
tcpdump抓 pcap 文件
部分成功:
复制代码 隐藏代码导出了 sslkeylog.txt,确实解密了部分 TLS 连接。 但核心业务接口使用了 TTNet 内部的 SSL_CTX,不经过公开的 SSL_CTX_new, 所以 keylog callback 注入不进去。
5 个 pcap 文件 + 5 个 sslkeylog 文件 就是这一阶段的产物,但核心数据仍然解密不了。
README 中记录的原话:
"牛马畅听的核心业务API使用
libsscronet.so内部的自定义TLS实现:SSL握手不经过SSL_do_handshake,数据传输不经过SSL_write/SSL_read,使用自定义BIO通过SSL_set0_rbio/SSL_set0_wbio,Keylog callback无法注入到内部SSL_CTX"
3.5 第五阶段:降维打击——回到 Java 层(hookjava*.js, hook_v11)
转折点:经历了 4 个阶段的 Native 层死磕后,AI 终于意识到——既然 Native 层被大厂做了深度定制,那就不要在 Native 层纠缠,直接回到 Java 层。
数据在到达 Native 网络栈之前,必然会经过 Java 层的序列化。如果在 Java 层截获,就完全绕过了 TTNet 的自定义 TLS。
AI 做了什么:
hook_java_callback.js:先试了
UrlResponseInfoImpl构造函数和CronetUrlRequest.getCurrentUrl- 拿到了 URL 和响应头 ✅
- 通过
CronetHttpURLConnection$CronetUrlRequestCallback.onReadCompleted拿到了部分响应体 ✅
hook_java_intercept.js:找到了关键类
com.bytedance.ttnet.retrofit.SsInterceptor- 这是大厂 TTNet Retrofit 框架的拦截器,所有请求必经之路
- Hook
SsInterceptor.intercept()方法 - 通过
request.getBody().writeTo(ByteArrayOutputStream)拿到请求体 ✅ - 通过
SsResponse.body()尝试拿响应体(遇到序列化问题)
hook_v11_full.js:最终版,精简高效
- 只 Hook
SsInterceptor.intercept一个方法 - 同时禁用 QUIC(强制走 HTTP/2,更稳定)
- Anti-Frida 也精简为最小集
- 整个脚本只有 88 行,对比 V7 的 700+ 行
- 只 Hook
最关键的发现:
拿到请求数据后,AI 做了一个大胆的测试——直接用 Python requests 发相同的请求,去掉所有签名头:
复制代码 隐藏代码
# 不带 x-argus, x-gorgon, x-helios 等任何签名
r = requests.post("https://novelfm.snssdk.com/novelfm/bookmall/search/page/v1/",
params=BASE_PARAMS, headers=HEADERS, json=body, verify=False)
# 结果:200 OK,返回完整数据结论:牛马畅听的业务 API 根本不校验签名参数。
这意味着前面 4 个阶段的所有工作——SSL Hook、Cronet 内部函数、HTTP/2 帧解析、TLS Keylog——虽然技术上没有错,但如果一开始就从 Java 层入手,可能 30 分钟就能搞定的事情,AI 绕了一大圈。
四、最终成果
4.1 可用的接口
| 接口 | 方法 | 用途 | 需要签名 |
|---|---|---|---|
/novelfm/bookmall/search/page/v1/ | POST | 搜索书籍/音频 | 否 |
/novelfm/bookapi/page_extra_info/v1/ | POST | 书籍章节列表 | 否 |
/novelfm/playerapi/video_model/mget/v1/ | POST | 音频播放详情(含 CDN 地址) | 否 |
/novelfm/bookapi/directory/all_infos/v1/ | POST | 书籍目录 | 否 |
/novelfm/bookmall/recommend/book/v1/ | POST | 推荐书籍 | 否 |
/novelfm/userapi/user_info/v1/ | GET | 用户信息 | 否 |
4.2 交付的文件
复制代码 隐藏代码
project/
├── crawler.py # 直接可用的采集脚本
├── search_video.py # 搜索 + 通用请求重放
├── test_search.py # 搜索接口测试
├── realtime_hook.py # Frida 实时抓包 + Python 重放(管道模式)
├── run.sh # 一键启动脚本(macOS)
├── start_hook.sh # Frida 启动脚本
└── hooks/
├── hook_v11_full.js # 最终版 Hook(88行,精简高效)
├── hook_java_intercept.js # Java 层调试脚本
└── ...五、复盘:AI 走了哪些弯路,为什么
5.1 弯路清单
| # | 弯路 | 花费的精力 | 产出 |
|---|---|---|---|
| 1 | Hook SSL_write/SSL_read | 2 个脚本(hook_fanqie.js, hook_cronet_minimal.js) | 只抓到辅助接口 |
| 2 | Hook Cronet C API(_set/_get 函数) | 3 个脚本(hook_quic.js, hook_native_v2.js, hook_v4.js) | 只拿到 URL |
| 3 | 在 Frida 里实现 HTTP/2 + HPACK + Huffman | 2 个脚本 700+ 行(hook_v6.js, hook_v7.js) | 技术上成功,但解析的不是目标数据 |
| 4 | TLS Keylog + tcpdump + Wireshark | 3 个脚本 + 5 个 pcap + 5 个 keylog(hook_v8~v10) | 部分解密成功,核心接口仍然失败 |
| 5 | Java 层 Hook(正确方向) | 3 个脚本(hookjava*.js, hook_v11) | 一击命中 |
总计产出了 15+ 个脚本版本,但最终只有最后 1 个(88 行的 V11)是真正需要的。
5.2 根本原因分析
AI 犯的核心错误:自底向上 vs 自顶向下
AI 选择了"自底向上"的策略——从最底层的 SSL/TLS 开始,逐层往上走。这在大多数 App 上是对的,因为大多数 App 用的是标准网络库。
但牛马畅听用的是大厂自研的 TTNet(基于 Cronet 深度定制),它的网络栈从 TLS 到 HTTP/2 都是自己实现的,完全绕过了操作系统和标准库的 SSL 接口。
正确策略应该是"自顶向下"——先从 Java 层看数据在哪里产生,再决定要不要深入 Native 层。
5.3 但弯路并非毫无价值
这些"弯路"产生了重要的副产品:
- 完整的安全保护分析:摸清了牛马畅听的 Anti-Frida、SSL Pinning、VMP 保护、反调试等全套防护
- TTNet 架构理解:搞清楚了 libsscronet.so + libttboringssl.so 的协作关系
- HTTP/2 解析工具:hook_v7.js + hpack_module.js 可以复用到其他需要解析 HTTP/2 的场景
- 关键结论:业务 API 不需要签名——这个发现是通过 Java 层 Hook 拿到完整请求后,对比测试得出的
六、指挥 AI 的经验总结
6.1 什么时候该介入
| 信号 | 应该做什么 |
|---|---|
| AI 连续写了 3 个脚本还在同一层级打转 | 让它换一个层级试试 |
| Hook 了大量函数但数据全是辅助接口的 | 提示它区分"辅助接口"和"业务接口"的流量路径 |
| 脚本越写越复杂(V7 膨胀到 700 行) | 问它"有没有更简单的方案" |
| AI 说"部分成功"但核心目标未达成 | 直接要求它放弃当前方案,换思路 |
6.2 给 AI 的有效指令模式
低效指令:
"帮我抓牛马畅听的数据"
高效指令:
"牛马畅听用的是大厂 TTNet,标准 SSL Hook 可能不行。先从 Java 层的 Retrofit 拦截器入手,抓到请求后直接用 Python 重放测试是否需要签名"
关键是给 AI 足够的上下文和约束,避免它自己选择一条看起来合理但实际上是死路的路径。
6.3 什么时候该放手让 AI 自己试
- 编写具体的 Hook 脚本代码
- 调试报错和崩溃问题
- 参数格式的试错(请求体字段、URL 参数等)
- 编写最终的 Python 采集脚本
七、如果重来一次,最优路径是什么
复制代码 隐藏代码
1. 反编译 APK,搜索 "retrofit" / "Interceptor" 关键字 → 5 分钟
2. 找到 SsInterceptor 类,写 Java 层 Hook → 10 分钟
3. 抓到几个业务请求的 URL + Body + Headers → 5 分钟
4. 用 Python requests 重放,测试是否需要签名 → 5 分钟
5. 发现不需要签名,直接写采集脚本 → 15 分钟
总计:40 分钟对比实际花费的路径(经历了 V1~V11,产出 15+ 个脚本),效率差距是巨大的。
但这就是逆向的本质——你不知道最短路径是什么,直到你把所有弯路都走过一遍。AI 的价值在于:它走弯路的速度比人快得多,而且每条弯路都会留下有价值的信息。