本文由 资源共享网 – ziyuan 发布,转载请注明出处,如有问题请联系我们![免费]某大厂加密的ai全流程分析和逆向
收藏指挥 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 SecretV9:在
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+ 行
最关键的发现:
拿到请求数据后,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 的价值在于:它走弯路的速度比人快得多,而且每条弯路都会留下有价值的信息。

