针对某多多使用的 Titan 长连接协议导致无法通过常规代理抓包的问题,本文提供了一种基于 Hook 的应用层拦截方案
在对某多多进行协议分析时,发现其采用了名为 Titan 的私有长连接(LongLink)技术。这导致常规的 HTTP/HTTPS 抓包工具(如 Charles, Fiddler)无法捕获核心业务流量(如商品详情)。
本文主要探讨如何绕过网络层加密,直接在 Java 应用层截获数据:
- Titan 协议拦截: Hook 网络库响应处理函数,获取解密后的原始响应
- 商品详情抓取: Hook 业务层数据模型,将内存对象序列化为 JSON
技术架构与核心机制
关键注入点与探针部署
| 探针类型 | 注入类/方法 | 功能说明 |
|---|
| Frida | TitanApiResponse.getBodyBytes | 拦截 Titan 网络层原始响应数据 |
| Xposed | GoodsResponse.setRenderResponse | 拦截商品详情页面渲染数据 |
| Frida/Xposed | Gson.toJson | 辅助将内存对象序列化为可读 JSON |
数据流向与拦截逻辑
Titan 协议层拦截
针对 Titan 协议,直接在网络库的回调处进行拦截是最有效的方式。此时数据已经完成了协议层的解包,但尚未分发给业务逻辑。
Frida 注入脚本示例
Java.perform(function () {
// 1. 定位目标类 (需根据混淆调整)
var TitanApiResponse = Java.use("xxx.xxxx.xxxxtitan.api.TitanApiResponse");
var StringCls = Java.use("java.lang.String");
// 2. Hook getBodyBytes 方法
if (TitanApiResponse.getBodyBytes) {
TitanApiResponse.getBodyBytes.implementation = function () {
var hookPoint = "TitanApiResponse.getBodyBytes()";
var data = this.getBodyBytes();
// 3. 拦截并转码
if (data) {
try {
var resp = StringCls.$new(data, "UTF-8");
if (resp) {
console.log("\n=== 📥 " + hookPoint + " 响应捕获 ===");
// 截断输出避免日志爆炸
console.log(resp.length > 2000 ? resp.substring(0, 2000) + "..." : resp);
console.log("====================================\n");
}
} catch (e) {
console.log("[!] " + hookPoint + " - 响应解析失败:", e);
}
}
// 4. 必须返回原始数据,否则 App 会崩溃
return data;
};
}
});
概述:
- 定位类: 寻找实现 Titan 协议响应接口的关键类
- 拦截点:
getBodyBytes 通常是获取原始响应数据的必经之路
- 无感注入: 仅读取数据进行打印或保存,不修改原始数据流,保证 App 正常运行
业务层数据抓取
对于结构复杂的业务数据(如商品详情),直接分析网络层 JSON 可能较为繁琐。直接 Hook 业务逻辑层的数据对象(Model)并进行序列化,可以获得结构化的清晰数据。
伪代码实现逻辑
伪代码,需配合 Xposed/YukiHookAPI 实现
// 目标:获取商品详情的结构化数据
// Hook 点:GoodsResponse.setRenderResponse(IntegrationRenderResponse response)
public void beforeHookedMethod(MethodHookParam param) {
// 1. 获取入参对象 (IntegrationRenderResponse)
Object renderResponse = param.args[0];
if (renderResponse == null) return;
// 2. 序列化为 JSON
// 优先使用 App 内部混淆后的 Gson,避免类加载器冲突
// 假设 App 内部 Gson 实例可通过静态方法获取
String json = AppGson.toJson(renderResponse);
// 3. 数据持久化或上报
if (json != null && !json.isEmpty()) {
Logger.i("GoodsDetail", json);
FileUtil.write("/sdcard/pdd_goods_" + System.currentTimeMillis() + ".json", json);
}
}
实现要点:
- Gson 实例: 务必使用 App 自身的
Gson 实例进行序列化,否则可能会因为 ClassLoader 不同或缺少 TypeAdapter 导致序列化失败
- 线程安全: IO 操作(如写文件、网络上报)应放入子线程执行,避免阻塞 UI 线程导致 ANR
- 时机选择:
setRenderResponse 通常在网络请求回调后、UI 渲染前调用,是截获数据的最佳时机
常见问题与排查
在实践过程中,可能会遇到以下问题:
- Hook 不生效: 检查注入时机,建议在
Application.attachBaseContext 之后或 Activity 启动时进行注入
- 混淆导致类找不到: 结合 Jadx 反编译对比版本差异,Titan 库的包名和类名经常变动
- 数据乱码: 检查响应头是否为 Gzip 压缩,如果是,需先进行 Gzip 解压再转 String
- Longlink 无法降级: 新版本 App 即使检测到系统代理也可能强制走 Longlink,因此直接 Hook 应用层比尝试降级协议更稳定
本文所提供的脚本和代码逻辑仅用于安全研究与技术交流。目标应用的协议与混淆策略会频繁更新,文中涉及的类名与方法名可能已失效,请结合实际环境进行分析。