ai-reverse-fm


/thread-2099021-1-1.html

指挥 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 做了什么

  1. 编写了完整的 Anti-Frida bypass(hook strstr/strcmp/ptrace/connect)
  2. 定位到 libttboringssl.so(大厂自定义的 BoringSSL)
  3. Hook 了 SSL_writeSSL_readSSL_do_handshake
  4. 同时 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_StartCronet_UrlResponseInfo_url_getCronet_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 脚本里从零实现了:

  1. HTTP/2 帧头解析器(9 大厂帧头 → Length/Type/Flags/StreamID)
  2. HPACK 头部压缩解码器(包含 RFC 7541 完整静态表 + 动态表)
  3. Huffman 解码树(RFC 7541 Appendix B 完整 256+EOS 码表)
  4. HTTP/2 流状态管理(多路复用的 DATA 帧拼接)
  5. 配套的 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 做了什么

  1. hook_java_callback.js:先试了 UrlResponseInfoImpl 构造函数和 CronetUrlRequest.getCurrentUrl

    • 拿到了 URL 和响应头 ✅
    • 通过 CronetHttpURLConnection$CronetUrlRequestCallback.onReadCompleted 拿到了部分响应体 ✅
  2. hook_java_intercept.js:找到了关键类 com.bytedance.ttnet.retrofit.SsInterceptor

    • 这是大厂 TTNet Retrofit 框架的拦截器,所有请求必经之路
    • Hook SsInterceptor.intercept() 方法
    • 通过 request.getBody().writeTo(ByteArrayOutputStream) 拿到请求体 ✅
    • 通过 SsResponse.body() 尝试拿响应体(遇到序列化问题)
  3. hook_v11_full.js:最终版,精简高效

    • 只 Hook SsInterceptor.intercept 一个方法
    • 同时禁用 QUIC(强制走 HTTP/2,更稳定)
    • Anti-Frida 也精简为最小集
    • 整个脚本只有 88 行,对比 V7 的 700+ 行

最关键的发现

拿到请求数据后,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 弯路清单

#弯路花费的精力产出
1Hook SSL_write/SSL_read2 个脚本(hook_fanqie.js, hook_cronet_minimal.js)只抓到辅助接口
2Hook Cronet C API(_set/_get 函数)3 个脚本(hook_quic.js, hook_native_v2.js, hook_v4.js)只拿到 URL
3在 Frida 里实现 HTTP/2 + HPACK + Huffman2 个脚本 700+ 行(hook_v6.js, hook_v7.js)技术上成功,但解析的不是目标数据
4TLS Keylog + tcpdump + Wireshark3 个脚本 + 5 个 pcap + 5 个 keylog(hook_v8~v10)部分解密成功,核心接口仍然失败
5Java 层 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 但弯路并非毫无价值

这些"弯路"产生了重要的副产品:

  1. 完整的安全保护分析:摸清了牛马畅听的 Anti-Frida、SSL Pinning、VMP 保护、反调试等全套防护
  2. TTNet 架构理解:搞清楚了 libsscronet.so + libttboringssl.so 的协作关系
  3. HTTP/2 解析工具:hook_v7.js + hpack_module.js 可以复用到其他需要解析 HTTP/2 的场景
  4. 关键结论:业务 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 的价值在于:它走弯路的速度比人快得多,而且每条弯路都会留下有价值的信息。