百趣云 百趣云的博客

Frida hook OkHttp3:一个脚本通杀所有请求的打印方案

为什么选拦截器层而不是 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;
    };
});

精髓:五个实战坑

  1. 响应体必须用 peekBody,不能用 response.body().string()。响应流只能消费一次,你读了 App 就读不到,直接崩。peekBody 是 OkHttp 提供的"窥视"机制,复制一份给你。
  2. 请求体用 okio.Buffer 中转request.body() 是抽象类,没有直接 toString。writeTo 到 Buffer 再 readUtf8 是标准姿势,且不影响原 body(body 本身可重复写)。
  3. 混淆 App 的类名会变RealInterceptorChain 被混淆成 a.b.c 时,用特征搜类:
Java.enumerateLoadedClasses().forEach(function(name) {
    // 同步枚举可能漏 dex 动态加载的类,必要时用 enumerateLoadedClassesSync
});
// 更稳的办法:hook 已知方法名 proceed,反查类
  1. WebSocket 请求不走这里。OkHttp 的 WebSocket 走 newWebSocket 分支,要单独 hook okhttp3.OkHttpClient.newWebSocket
  2. 大响应截断打印。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()));
}

打印调用栈,往上翻几帧就是签名头的添加位置——签名算法的入口自己就送上门了。

总结

  1. hook RealInterceptorChain.proceed 一处通杀所有 OkHttp 请求
  2. 请求体用 okio.Buffer 中转,响应体用 peekBody 窥视
  3. 混淆 App 按方法特征反查类名
  4. 配合调用栈打印,签名入口自动现身

交流微信:run1255

By 百趣云 阅读量:2 On