为什么选拦截器层而不是 hook URL
逆向 App 抓接口,最笨的办法是逐个 hook 业务方法。OkHttp3 的正确打开方式是 hook 拦截器链的 proceed 方法——所有请求(无论业务层怎么封装)都必须经过 RealInterceptorChain.proceed,在这里能同时拿到完整的 request 和 response,一处 hook 全网打尽。
选 proceed 而不是 okhttp3.Request 的构造方法,原因有两个:一是构造方法拿不到响应;二是很多 App 在拦截器里加签名头(比如 token、sign),hook proceed 能看到签名之后的最终请求,这才是协议模拟要的东西。
完整脚本
Java.perform(function() {
var RealInterceptorChain = Java.use('okhttp3.internal.http.RealInterceptorChain');
RealInterceptorChain.proceed.overload('okhttp3.Request').implementation = function(request) {
// ===== 请求侧 =====
var url = request.url().toString();
var method = request.method();
var headers = request.headers().toString();
var bodyStr = '';
try {
var body = request.body();
if (body) {
var Buffer = Java.use('okio.Buffer');
var buf = Buffer.$new();
body.writeTo(buf); // 把请求体读进 buffer
bodyStr = buf.readUtf8(); // 不影响原 body 的读取
}
} catch(e) {}
console.log('\n[REQ] ' + method + ' ' + url);
console.log('[HEADERS]\n' + headers.trim());
if (bodyStr) console.log('[BODY] ' + bodyStr);
// ===== 放行,拿响应 =====
var response = this.proceed(request);
// ===== 响应侧:body 只能读一次,要用 peekBody =====
try {
var peek = response.peekBody(1024 * 1024); // 窥视1MB,不消耗原流
console.log('[RESP ' + response.code() + '] ' + peek.string().substring(0, 2000));
} catch(e) {
console.log('[RESP] body read error: ' + e);
}
return response;
};
});精髓:五个实战坑
- 响应体必须用
peekBody,不能用response.body().string()。响应流只能消费一次,你读了 App 就读不到,直接崩。peekBody 是 OkHttp 提供的"窥视"机制,复制一份给你。 - 请求体用
okio.Buffer中转。request.body()是抽象类,没有直接 toString。writeTo 到 Buffer 再 readUtf8 是标准姿势,且不影响原 body(body 本身可重复写)。 - 混淆 App 的类名会变。
RealInterceptorChain被混淆成a.b.c时,用特征搜类:
Java.enumerateLoadedClasses().forEach(function(name) {
// 同步枚举可能漏 dex 动态加载的类,必要时用 enumerateLoadedClassesSync
});
// 更稳的办法:hook 已知方法名 proceed,反查类- WebSocket 请求不走这里。OkHttp 的 WebSocket 走
newWebSocket分支,要单独 hookokhttp3.OkHttpClient.newWebSocket。 - 大响应截断打印。peekBody(1MB) 对图片/文件下载会拖慢速度,实战里加 url 白名单过滤,只打印 API 域名。
进阶:定位签名生成点
这个脚本还有个隐藏用法——签名参数溯源。看到请求头里有 x-sign 之类的签名后,在脚本里加一行:
if (headers.includes('x-sign')) {
console.log(Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new()));
}打印调用栈,往上翻几帧就是签名头的添加位置——签名算法的入口自己就送上门了。
总结
- hook
RealInterceptorChain.proceed一处通杀所有 OkHttp 请求 - 请求体用 okio.Buffer 中转,响应体用 peekBody 窥视
- 混淆 App 按方法特征反查类名
- 配合调用栈打印,签名入口自动现身
交流微信:run1255