【Frida Android】实战篇3:基于 OkHttp 库的 Hook 抓包

文章目录

  • 前言
  • [1. OkHttp 介绍](#1. OkHttp 介绍)
    • [1.1 作用](#1.1 作用)
    • [1.2 常用类与方法](#1.2 常用类与方法)
  • [2. 前置操作](#2. 前置操作)
    • [2.1 启动 frida-server](#2.1 启动 frida-server)
    • [2.2 启动脚本](#2.2 启动脚本)
  • [3. Hook 思路](#3. Hook 思路)
    • [3.1 断点调试](#3.1 断点调试)
    • [3.2 Hook 脚本](#3.2 Hook 脚本)
    • [3.3 脚本详解](#3.3 脚本详解)
  • [4. 技术总结](#4. 技术总结)

⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。

前言

上一章我们已掌握 HTTPS 中间人抓包与原生 HTTP 请求的 Hook 抓包技巧,而在实际企业级 Android 开发中,OkHttp 库因高效、稳定的特性被广泛应用于网络请求场景。

针对这类基于 OkHttp 实现的网络通信,传统抓包方式可能面临适配难题,因此本章将聚焦企业常用的 OkHttp 请求 Hook 抓包方案,通过拆解 OkHttp 核心类与方法、梳理 Hook 思路、编写实战脚本,帮助大家实现对 OkHttp 请求与响应信息的完整捕获,为逆向分析、接口调试等工作提供技术支撑。

本章节使用的示例 APK 和 APK 源码如下:

通过网盘分享的文件:

链接: https://pan.baidu.com/s/1y5rnZKsjKtwZkMP6K2zriA?pwd=m2qj

提取码: m2qj

1. OkHttp 介绍

1.1 作用

OkHttp 是一款由 Square 公司开发的高效 HTTP 客户端库,广泛应用于 Android 平台和 Java 项目中。它的核心作用是简化 HTTP 通信流程,支持 HTTP/1.1、HTTP/2 以及WebSocket 协议,提供了连接池管理、请求重试、缓存机制、拦截器等功能,能够显著提升网络请求的性能和稳定性。在移动应用开发中,OkHttp 常被用于与后端服务器进行数据交互,处理 GET、POST 等各类 HTTP 请求。

1.2 常用类与方法

在实际开发和逆向分析中,以下类和方法是核心关注点:

  • okhttp3.Request$Builder:请求构建器内部类,用于组装 HTTP 请求的各项参数(URL、请求方法、请求头、请求体等),核心方法 build() 用于生成最终的 Request 对象。
  • okhttp3.Request:表示一个 HTTP 请求,通过 url()method()headers()body() 等方法可获取请求的 URL、方法、头信息和体内容。
  • okhttp3.Response$Builder:响应构建器内部类,用于组装 HTTP 响应的各项参数(状态码、响应头、响应体等),核心方法 build() 用于生成最终的 Response 对象。
  • okhttp3.Response:表示一个 HTTP 响应,通过 code()message()headers()request() 等方法可获取响应状态码、消息、头信息和对应的请求对象。
  • okhttp3.ResponseBody:表示响应体内容,核心方法 string() 用于将响应体转换为字符串形式,是获取响应数据的关键。
  • okhttp3.OkHttpClient:HTTP 客户端实例,通过 newCall(Request) 方法创建一个 Call 对象,用于执行请求(同步 execute() 或异步 enqueue(Callback))。

2. 前置操作

与上一章节相同,只是替换了示例 APK 应用。

2.1 启动 frida-server

进入模拟器设备中启动 frida 服务

2.2 启动脚本

与和上一章相同:

python 复制代码
import frida
import sys
import time
from datetime import datetime

# 创建日志文件
log_file = open("frida_http_monitor.log", "w", encoding="utf-8")

def on_message(message, data):
    if message['type'] == 'send':
        payload = message['payload']
        if isinstance(payload, dict) and payload.get('type') == 'http_log':
            # 写入日志文件
            log_msg = payload['message']
            log_file.write(log_msg + "\n")
            log_file.flush()  # 确保立即写入磁盘
            print(f"[Hook 日志] {log_msg}")
        else:
            print(f"[Hook 消息] {payload}")
    elif message['type'] == 'error':
        error_msg = f"[错误] {str(message)}"
        log_file.write(error_msg + "\n")
        log_file.flush()
        print(error_msg)

# 目标应用包名
PACKAGE_NAME = "com.example.fridaapk"

def main():
    try:
        device = frida.get_usb_device(timeout=10)
        print(f"已连接设备:{device.name}")

        print(f"启动进程 {PACKAGE_NAME}...")
        pid = device.spawn([PACKAGE_NAME])
        device.resume(pid)
        time.sleep(2)

        process = device.attach(pid)
        print(f"已附加到进程 PID: {pid}")

        with open("./js/compiled_hook.js", "r", encoding="utf-8") as f:
            js_code = f.read()

        script = process.create_script(js_code)
        script.on('message', on_message)
        script.load()
        time.sleep(2)
        print("JS 脚本注入成功,开始监控...(按 Ctrl+C 退出)")

        # 等待用户输入
        try:
            sys.stdin.read()
        except KeyboardInterrupt:
            print("\n正在退出...")

    except frida.TimedOutError:
        print("未找到USB设备")
    except frida.ProcessNotFoundError:
        print(f"应用 {PACKAGE_NAME} 未安装")
    except FileNotFoundError:
        print("未找到 js 脚本,请检查路径")
    except Exception as e:
        print(f"异常:{str(e)}")
    finally:
        # 关闭日志文件
        if 'log_file' in locals():
            log_file.close()
        if 'process' in locals():
            process.detach()
        print("程序退出")

if __name__ == "__main__":
    main()

3. Hook 思路

3.1 断点调试

为捕获完整的请求信息,我们在请求构建的最终方法处设置断点。在下面 APK 源代码中,在 build() 方法处下断点:

java 复制代码
val request = Request.Builder()
                .url(url)
                .post(requestBody)
                .addHeader("Content-Type", "application/json")
                .build()    // 在此处设置断点

启动应用并进入调试模式,当触发 POST 请求时,程序会暂停在该断点处。此时观察调用栈,首先会执行 okhttp3.RequestBody$Companion$toRequestBody$3@83519bf 相关逻辑,我们继续通过单步调试深入调用链。

按 F7 键步入内部调用后,可定位到 okhttp3.Request$Builder@f5109a2(其中 $BuilderRequest 类的内部类,@f5109a2 为实例内存地址,无需关注具体值)。查看该内部类的结构可知,Request.Builder 的核心作用是组装请求的各项参数,包括 URL、请求头(headers)、请求方法(method)等。

为获取响应信息,我们在 client.newCall(request).execute() 方法处补充断点。

经过多步调试后,可确定响应信息由 Response$Builder 实例构建。展开该实例可观察到其包含响应体(ResponseBody)、响应状态码(status code)、响应头(headers)等关键信息,这些均为我们需要捕获的核心内容。

进一步分析发现,Response$Builder 实例中,ResponseBody 字段关联了具体的响应体实现类,因此通过 Hook okhttp3.ResponseBody 类可更加简洁、直接获取响应体的详细内容。

3.2 Hook 脚本

在上一章节的断点调试结果,我们明确了OkHttp库的核心类结构与方法调用逻辑------这为后续设计Hook脚本提供了关键依据。

具体而言,调试过程让我们掌握了诸如okhttp3.Request$Builder这类内部类的命名规则,理清了Request.Builder().build()的执行链路,同时观察到okhttp3.RequestBodyokhttp3.Request$Builder之间的调用关系。

在Frida Hook技术中,针对此类底层内部类进行拦截是一种高效且常用的手段。基于上述调试所得的类结构与调用链信息,我们可以精准定位需要Hook的目标方法,从而实现对HTTP请求与响应信息的完整捕获。

javascript 复制代码
import Java from "frida-java-bridge";

// 统一日志函数
function log(message) {
    const timestamp = new Date().toISOString();
    const logStr = `[${timestamp}] ${message}`;
    console.log(logStr);
    send({ type: "http_log", message: logStr });
}

// hook OkHttp请求/响应信息
Java.perform(function () {
    // Hook Request.Builder.build() 方法来捕获完整的请求信息
    var RequestBuilder = Java.use('okhttp3.Request$Builder');
    RequestBuilder.build.implementation = function () {
        var request = this.build();
        log('=== OkHttp Request Info ===');
        log('URL: ' + request.url().toString());
        log('Method: ' + request.method());

        // 读取请求头
        var headers = request.headers();
        log('Headers:');
        for (var i = 0; i < headers.size(); i++) {
            log('  ' + headers.name(i) + ': ' + headers.value(i));
        }

        // 读取请求体
        var requestBody = request.body();
        if (requestBody) {
            try {
                var buffer = Java.use('okio.Buffer').$new();
                requestBody.writeTo(buffer);
                var bodyContent = buffer.readUtf8();
                log('Request Body: ' + bodyContent);
            } catch (e) {
                log('Failed to read request body: ' + e);
            }
        }
        return request;
    };

    // Hook Response.Builder.build() 方法,在响应构建完成时记录完整信息
    var ResponseBuilder = Java.use('okhttp3.Response$Builder');
    ResponseBuilder.build.implementation = function () {
        var response = this.build();

        log('=== OkHttp Response Info ===');
        log('Status Code: ' + response.code());
        log('Message: ' + response.message());
        log('URL: ' + response.request().url().toString());

        // 记录响应头
        var headers = response.headers();
        log('Headers:');
        for (var i = 0; i < headers.size(); i++) {
            log('  ' + headers.name(i) + ': ' + headers.value(i));
        }

        return response;
    };

    // 记录响应体
    var ResponseBody = Java.use('okhttp3.ResponseBody');
    ResponseBody.string.implementation = function () {
        var result = this.string();
        log('Response Content: ' + result);

        return result;
    };
});

输出日志如下:

shell 复制代码
[2025-11-11T02:35:16.031Z] === OkHttp Request Info ===
[2025-11-11T02:35:16.032Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.032Z] Method: GET
[2025-11-11T02:35:16.033Z] Headers:
[2025-11-11T02:35:16.033Z]   Content-Type: application/json
[2025-11-11T02:35:16.033Z]   Cache-Control: max-age=3600
[2025-11-11T02:35:16.036Z] === OkHttp Request Info ===
[2025-11-11T02:35:16.036Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.036Z] Method: GET
[2025-11-11T02:35:16.037Z] Headers:
[2025-11-11T02:35:16.037Z]   Content-Type: application/json
[2025-11-11T02:35:16.037Z]   Cache-Control: max-age=3600
[2025-11-11T02:35:16.037Z]   Host: 192.168.10.6:3000
[2025-11-11T02:35:16.037Z]   Connection: Keep-Alive
[2025-11-11T02:35:16.037Z]   Accept-Encoding: gzip
[2025-11-11T02:35:16.038Z]   User-Agent: okhttp/5.3.0
[2025-11-11T02:35:16.069Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.070Z] Status Code: 200
[2025-11-11T02:35:16.070Z] Message: OK
[2025-11-11T02:35:16.070Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.071Z] Headers:
[2025-11-11T02:35:16.071Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.071Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.071Z]   Content-Type: application/json
[2025-11-11T02:35:16.072Z]   Content-Length: 172
[2025-11-11T02:35:16.072Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.072Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.072Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.073Z]   Connection: close
[2025-11-11T02:35:16.074Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.074Z] Status Code: 200
[2025-11-11T02:35:16.074Z] Message: OK
[2025-11-11T02:35:16.074Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.074Z] Headers:
[2025-11-11T02:35:16.074Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.074Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.074Z]   Content-Type: application/json
[2025-11-11T02:35:16.074Z]   Content-Length: 172
[2025-11-11T02:35:16.075Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.075Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.075Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.075Z]   Connection: close
[2025-11-11T02:35:16.076Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.076Z] Status Code: 200
[2025-11-11T02:35:16.076Z] Message: OK
[2025-11-11T02:35:16.077Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.077Z] Headers:
[2025-11-11T02:35:16.077Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.077Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.077Z]   Content-Type: application/json
[2025-11-11T02:35:16.077Z]   Content-Length: 172
[2025-11-11T02:35:16.078Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.078Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.078Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.078Z]   Connection: close
[2025-11-11T02:35:16.079Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.079Z] Status Code: 200
[2025-11-11T02:35:16.079Z] Message: OK
[2025-11-11T02:35:16.079Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.079Z] Headers:
[2025-11-11T02:35:16.079Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.080Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.080Z]   Content-Type: application/json
[2025-11-11T02:35:16.080Z]   Content-Length: 172
[2025-11-11T02:35:16.080Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.080Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.080Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.080Z]   Connection: close
[2025-11-11T02:35:16.081Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.081Z] Status Code: 200
[2025-11-11T02:35:16.081Z] Message: OK
[2025-11-11T02:35:16.081Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.082Z] Headers:
[2025-11-11T02:35:16.082Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.082Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.082Z]   Content-Type: application/json
[2025-11-11T02:35:16.082Z]   Content-Length: 172
[2025-11-11T02:35:16.082Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.082Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.082Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.082Z]   Connection: close
[2025-11-11T02:35:16.083Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.083Z] Status Code: 200
[2025-11-11T02:35:16.083Z] Message: OK
[2025-11-11T02:35:16.083Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.083Z] Headers:
[2025-11-11T02:35:16.083Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.084Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.084Z]   Content-Type: application/json
[2025-11-11T02:35:16.084Z]   Content-Length: 172
[2025-11-11T02:35:16.084Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.084Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.084Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.084Z]   Connection: close
[2025-11-11T02:35:16.086Z] Response Content: {
  "client_ip": "192.168.10.7",
  "code": 200,
  "msg": "GET Request Success",
  "request_method": "GET",
  "request_params": {},
  "server_time": "2025-11-11 10:35:17"
}

[2025-11-11T02:35:16.517Z] === OkHttp Request Info ===
[2025-11-11T02:35:16.517Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.517Z] Method: POST
[2025-11-11T02:35:16.517Z] Headers:
[2025-11-11T02:35:16.517Z]   Content-Type: application/json
[2025-11-11T02:35:16.519Z] Request Body: {"body":"frida body","title":"frida title"}
[2025-11-11T02:35:16.520Z] === OkHttp Request Info ===
[2025-11-11T02:35:16.520Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.520Z] Method: POST
[2025-11-11T02:35:16.520Z] Headers:
[2025-11-11T02:35:16.520Z]   Content-Type: application/json; charset=utf-8
[2025-11-11T02:35:16.520Z]   Content-Length: 43
[2025-11-11T02:35:16.520Z]   Host: 192.168.10.6:3000
[2025-11-11T02:35:16.521Z]   Connection: Keep-Alive
[2025-11-11T02:35:16.521Z]   Accept-Encoding: gzip
[2025-11-11T02:35:16.521Z]   User-Agent: okhttp/5.3.0
[2025-11-11T02:35:16.521Z] Request Body: {"body":"frida body","title":"frida title"}
[2025-11-11T02:35:16.567Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.567Z] Status Code: 200
[2025-11-11T02:35:16.567Z] Message: OK
[2025-11-11T02:35:16.568Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.568Z] Headers:
[2025-11-11T02:35:16.568Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.568Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.569Z]   Content-Type: application/json
[2025-11-11T02:35:16.569Z]   Content-Length: 231
[2025-11-11T02:35:16.569Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.570Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.570Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.570Z]   Connection: close
[2025-11-11T02:35:16.570Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.570Z] Status Code: 200
[2025-11-11T02:35:16.570Z] Message: OK
[2025-11-11T02:35:16.571Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.571Z] Headers:
[2025-11-11T02:35:16.572Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.572Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.572Z]   Content-Type: application/json
[2025-11-11T02:35:16.572Z]   Content-Length: 231
[2025-11-11T02:35:16.573Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.573Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.573Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.574Z]   Connection: close
[2025-11-11T02:35:16.575Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.575Z] Status Code: 200
[2025-11-11T02:35:16.575Z] Message: OK
[2025-11-11T02:35:16.575Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.575Z] Headers:
[2025-11-11T02:35:16.575Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.575Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.575Z]   Content-Type: application/json
[2025-11-11T02:35:16.576Z]   Content-Length: 231
[2025-11-11T02:35:16.576Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.576Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.576Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.576Z]   Connection: close
[2025-11-11T02:35:16.577Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.577Z] Status Code: 200
[2025-11-11T02:35:16.577Z] Message: OK
[2025-11-11T02:35:16.577Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.578Z] Headers:
[2025-11-11T02:35:16.578Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.579Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.579Z]   Content-Type: application/json
[2025-11-11T02:35:16.579Z]   Content-Length: 231
[2025-11-11T02:35:16.579Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.580Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.580Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.580Z]   Connection: close
[2025-11-11T02:35:16.580Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.581Z] Status Code: 200
[2025-11-11T02:35:16.581Z] Message: OK
[2025-11-11T02:35:16.581Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.581Z] Headers:
[2025-11-11T02:35:16.581Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.582Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.582Z]   Content-Type: application/json
[2025-11-11T02:35:16.582Z]   Content-Length: 231
[2025-11-11T02:35:16.582Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.582Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.582Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.583Z]   Connection: close
[2025-11-11T02:35:16.583Z] === OkHttp Response Info ===
[2025-11-11T02:35:16.583Z] Status Code: 200
[2025-11-11T02:35:16.583Z] Message: OK
[2025-11-11T02:35:16.584Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T02:35:16.584Z] Headers:
[2025-11-11T02:35:16.585Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T02:35:16.585Z]   Date: Tue, 11 Nov 2025 02:35:17 GMT
[2025-11-11T02:35:16.585Z]   Content-Type: application/json
[2025-11-11T02:35:16.585Z]   Content-Length: 231
[2025-11-11T02:35:16.586Z]   Access-Control-Allow-Origin: *
[2025-11-11T02:35:16.586Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T02:35:16.586Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T02:35:16.586Z]   Connection: close
[2025-11-11T02:35:16.588Z] Response Content: {
  "client_ip": "192.168.10.7",
  "code": 200,
  "msg": "POST Request Success",
  "received_params": {
    "body": "frida body",
    "title": "frida title"
  },
  "request_method": "POST",
  "server_time": "2025-11-11 10:35:17"
}

包含 HTTP 请求的 URL、请求方法、请求头、请求体(body)、响应体,可以看到日志文件中的记录有很多重复内容。

这并非脚本设计问题,而是由 OkHttp 框架的核心机制------Interceptor(拦截器) 导致的。它是 OkHttp 中用于拦截、处理 HTTP 请求和响应的组件。你可以把它理解成请求/响应在发送和接收过程中经过的"检查站"------每个拦截器都可以对请求进行修改(比如添加头信息、加密参数),或对响应进行处理(比如解密数据、记录日志),然后将处理后的内容传递给下一个拦截器。

OkHttp 的拦截器分为两类:

  • 应用拦截器(Application Interceptors):面向开发者,通常用于添加全局头信息、打印日志等业务逻辑。
  • 网络拦截器(Network Interceptors):更底层,会处理实际网络请求(如 DNS 解析、连接复用等),甚至包括重试、重定向等场景。

拦截器的"链式调用"导致重复日志

OkHttp 的拦截器采用链式结构(Interceptor Chain)工作:一个请求从发起到收到响应,会依次经过所有拦截器(包括 OkHttp 内置的拦截器和开发者自定义的拦截器)。

具体流程如下:

  1. 当调用 Request.Builder().build() 构建请求时,请求会先进入第一个拦截器;
  2. 拦截器处理后,将请求传递给下一个拦截器;
  3. 经过所有拦截器后,请求才会真正发送到服务器;
  4. 服务器返回响应后,响应会按相反顺序经过所有拦截器,最终回到应用层。

正因为这种"链式传递",我们 Hook 的 Request.Builder.build()Response.Builder.build() 方法会被每个拦截器调用一次。例如:

  • 如果 OkHttp 内部有 3 个内置拦截器,加上 1 个自定义应用拦截器,那么一个请求会触发 4 次 build() 调用;
  • 对应的响应也会经过同样的拦截器链,导致响应相关的 Hook 方法被多次触发。

这就是日志中出现重复内容的核心原因------并非请求被发送了多次,而是同一请求/响应在拦截器链中被多次处理,每次处理都会触发我们的 Hook 逻辑。

了解这一机制后,我们就能理解如何优化脚本:通过全局变量记录最近一次的请求/响应信息,只在最终获取响应体时统一输出,从而避免拦截器链式调用导致的重复日志。

最终脚本 优化后如下:请求和响应内容进行了去重,你也可以根据自己的需求继续改造,以获得适合自己业务需要的数据,该脚本也可以作为 OkHttp 库的通用 Hook 脚本。

javascript 复制代码
import Java from "frida-java-bridge";

// 统一日志函数
function log(message) {
    const timestamp = new Date().toISOString();
    const logStr = `[${timestamp}] ${message}`;
    console.log(logStr);
    send({ type: "http_log", message: logStr });
}

// 添加全局变量来存储响应信息
var lastResponseInfo = {
    headers: [],
    body: "",
    statusCode: 0,
    url: ""
};

// 添加全局变量来存储请求信息
var lastRequestInfo = {
    url: "",
    method: "",
    headers: [],
    body: ""
};

Java.perform(function () {
    // Hook Request.Builder.build() 方法来捕获完整的请求信息
    var RequestBuilder = Java.use('okhttp3.Request$Builder');
    RequestBuilder.build.implementation = function () {
        var request = this.build();
        
        // 更新最后一次请求信息
        lastRequestInfo.url = request.url().toString();
        lastRequestInfo.method = request.method();
        lastRequestInfo.headers = [];
        
        // 读取请求头
        var headers = request.headers();
        for (var i = 0; i < headers.size(); i++) {
            lastRequestInfo.headers.push({
                name: headers.name(i),
                value: headers.value(i)
            });
        }

        // 读取请求体
        var requestBody = request.body();
        lastRequestInfo.body = "";
        if (requestBody) {
            try {
                var buffer = Java.use('okio.Buffer').$new();
                requestBody.writeTo(buffer);
                lastRequestInfo.body = buffer.readUtf8();
            } catch (e) {
                // 忽略错误
            }
        }
        
        return request;
    };

    // Hook Response.Builder.build() 方法,在响应构建完成时记录完整信息
    var ResponseBuilder = Java.use('okhttp3.Response$Builder');
    ResponseBuilder.build.implementation = function () {
        var response = this.build();

        // 更新最后一次响应头信息
        lastResponseInfo.statusCode = response.code();
        lastResponseInfo.url = response.request().url().toString();
        lastResponseInfo.headers = [];
        
        var headers = response.headers();
        for (var i = 0; i < headers.size(); i++) {
            lastResponseInfo.headers.push({
                name: headers.name(i),
                value: headers.value(i)
            });
        }

        return response;
    };

    // 记录响应体,并输出完整响应信息
    var ResponseBody = Java.use('okhttp3.ResponseBody');
    ResponseBody.string.implementation = function () {
        var result = this.string();
        
        // 更新响应体并记录完整响应信息
        lastResponseInfo.body = result;
        
        // 输出完整的请求信息
        log('=== Complete OkHttp Request ===');
        log('URL: ' + lastRequestInfo.url);
        log('Method: ' + lastRequestInfo.method);
        log('Headers:');
        for (var i = 0; i < lastRequestInfo.headers.length; i++) {
            log('  ' + lastRequestInfo.headers[i].name + ': ' + lastRequestInfo.headers[i].value);
        }
        if (lastRequestInfo.body) {
            log('Request Body: ' + lastRequestInfo.body);
        }
        
        // 输出完整的响应信息
        log('=== Complete OkHttp Response ===');
        log('URL: ' + lastResponseInfo.url);
        log('Status Code: ' + lastResponseInfo.statusCode);
        log('Headers:');
        for (var i = 0; i < lastResponseInfo.headers.length; i++) {
            log('  ' + lastResponseInfo.headers[i].name + ': ' + lastResponseInfo.headers[i].value);
        }
        log('Response Body: ' + lastResponseInfo.body);
        log('================================');
        
        return result;
    };
});

优化后日志文件frida_http_monitor.log记录如下:可以看到内容更加简洁清晰。

shell 复制代码
[2025-11-11T04:40:10.270Z] === Complete OkHttp Request ===
[2025-11-11T04:40:10.270Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T04:40:10.271Z] Method: GET
[2025-11-11T04:40:10.271Z] Headers:
[2025-11-11T04:40:10.271Z]   Content-Type: application/json
[2025-11-11T04:40:10.271Z]   Cache-Control: max-age=3600
[2025-11-11T04:40:10.271Z]   Host: 192.168.10.6:3000
[2025-11-11T04:40:10.271Z]   Connection: Keep-Alive
[2025-11-11T04:40:10.271Z]   Accept-Encoding: gzip
[2025-11-11T04:40:10.271Z]   User-Agent: okhttp/5.3.0
[2025-11-11T04:40:10.271Z] === Complete OkHttp Response ===
[2025-11-11T04:40:10.271Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T04:40:10.271Z] Status Code: 200
[2025-11-11T04:40:10.271Z] Headers:
[2025-11-11T04:40:10.271Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T04:40:10.271Z]   Date: Tue, 11 Nov 2025 04:40:11 GMT
[2025-11-11T04:40:10.271Z]   Content-Type: application/json
[2025-11-11T04:40:10.271Z]   Content-Length: 172
[2025-11-11T04:40:10.272Z]   Access-Control-Allow-Origin: *
[2025-11-11T04:40:10.272Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T04:40:10.272Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T04:40:10.272Z]   Connection: close
[2025-11-11T04:40:10.272Z] Response Body: {
  "client_ip": "192.168.10.7",
  "code": 200,
  "msg": "GET Request Success",
  "request_method": "GET",
  "request_params": {},
  "server_time": "2025-11-11 12:40:11"
}

[2025-11-11T04:40:10.272Z] ================================
[2025-11-11T04:40:10.886Z] === Complete OkHttp Request ===
[2025-11-11T04:40:10.886Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T04:40:10.886Z] Method: POST
[2025-11-11T04:40:10.886Z] Headers:
[2025-11-11T04:40:10.886Z]   Content-Type: application/json; charset=utf-8
[2025-11-11T04:40:10.886Z]   Content-Length: 43
[2025-11-11T04:40:10.886Z]   Host: 192.168.10.6:3000
[2025-11-11T04:40:10.886Z]   Connection: Keep-Alive
[2025-11-11T04:40:10.886Z]   Accept-Encoding: gzip
[2025-11-11T04:40:10.886Z]   User-Agent: okhttp/5.3.0
[2025-11-11T04:40:10.886Z] Request Body: {"body":"frida body","title":"frida title"}
[2025-11-11T04:40:10.886Z] === Complete OkHttp Response ===
[2025-11-11T04:40:10.886Z] URL: https://192.168.10.6:3000/api/frida/test
[2025-11-11T04:40:10.886Z] Status Code: 200
[2025-11-11T04:40:10.886Z] Headers:
[2025-11-11T04:40:10.886Z]   Server: Werkzeug/3.1.3 Python/3.13.2
[2025-11-11T04:40:10.886Z]   Date: Tue, 11 Nov 2025 04:40:12 GMT
[2025-11-11T04:40:10.886Z]   Content-Type: application/json
[2025-11-11T04:40:10.886Z]   Content-Length: 231
[2025-11-11T04:40:10.886Z]   Access-Control-Allow-Origin: *
[2025-11-11T04:40:10.886Z]   Access-Control-Allow-Methods: GET, POST, OPTIONS
[2025-11-11T04:40:10.886Z]   Access-Control-Allow-Headers: Content-Type
[2025-11-11T04:40:10.886Z]   Connection: close
[2025-11-11T04:40:10.886Z] Response Body: {
  "client_ip": "192.168.10.7",
  "code": 200,
  "msg": "POST Request Success",
  "received_params": {
    "body": "frida body",
    "title": "frida title"
  },
  "request_method": "POST",
  "server_time": "2025-11-11 12:40:12"
}

[2025-11-11T04:40:10.886Z] ================================

3.3 脚本详解

  1. Hook 请求构建:Request.Builder.build()

    javascript 复制代码
    var RequestBuilder = Java.use('okhttp3.Request$Builder');
    RequestBuilder.build.implementation = function () {
        var request = this.build(); // 调用原方法获取请求对象
        
        // 存储请求信息到全局变量
        lastRequestInfo.url = request.url().toString();
        lastRequestInfo.method = request.method();
        lastRequestInfo.headers = [];
        
        // 遍历请求头并存储
        var headers = request.headers();
        for (var i = 0; i < headers.size(); i++) {
            lastRequestInfo.headers.push({
                name: headers.name(i),
                value: headers.value(i)
            });
        }
        
        // 读取并存储请求体(通过 okio.Buffer 转换)
        var requestBody = request.body();
        lastRequestInfo.body = "";
        if (requestBody) {
            try {
                var buffer = Java.use('okio.Buffer').$new(); // 创建缓冲区
                requestBody.writeTo(buffer); // 将请求体写入缓冲区
                lastRequestInfo.body = buffer.readUtf8(); // 从缓冲区读取字符串
            } catch (e) {}
        }
        
        return request; // 返回原请求对象,不影响正常流程
    };
    • 作用:捕获请求的 URL、方法、头和体,并存储到 lastRequestInfo 全局变量。
    • 关键:通过 Java.use('类名') 获取内部类 Request$Builder,用 implementation 替换 build() 方法,既保留原功能(this.build()),又新增信息收集逻辑。
  2. Hook 响应构建:Response.Builder.build()

    javascript 复制代码
    var ResponseBuilder = Java.use('okhttp3.Response$Builder');
    ResponseBuilder.build.implementation = function () {
        var response = this.build(); // 调用原方法获取响应对象
        
        // 存储响应元信息到全局变量
        lastResponseInfo.statusCode = response.code();
        lastResponseInfo.url = response.request().url().toString();
        lastResponseInfo.headers = [];
        
        // 遍历响应头并存储
        var headers = response.headers();
        for (var i = 0; i < headers.size(); i++) {
            lastResponseInfo.headers.push({
                name: headers.name(i),
                value: headers.value(i)
            });
        }
        
        return response; // 返回原响应对象,不影响正常流程
    };
    • 作用:捕获响应的状态码、URL、响应头,并存储到 lastResponseInfo 全局变量。
    • 关键:响应与请求通过 response.request().url() 关联,确保后续输出时请求与响应对应。
  3. Hook 响应体:ResponseBody.string()

    javascript 复制代码
    var ResponseBody = Java.use('okhttp3.ResponseBody');
    ResponseBody.string.implementation = function () {
        var result = this.string(); // 调用原方法获取响应体字符串
        
        lastResponseInfo.body = result; // 存储响应体到全局变量
        
        // 输出完整的请求和响应信息
        log('=== Complete OkHttp Request ===');
        // ... 输出请求的 URL、方法、头、体
        log('=== Complete OkHttp Response ===');
        // ... 输出响应的 URL、状态码、头、体
        
        return result; // 返回原响应体,不影响应用解析
    };
    • 作用:在响应体被应用读取时,将之前存储的请求和响应信息一次性输出,避免重复日志。
    • 关键:string() 是响应体解析的最后一步,此时 lastRequestInfolastResponseInfo 已包含完整数据,适合作为输出触发点。

4. 技术总结

  1. OkHttp 核心逻辑与 hook 点选择
    OkHttp 通过 Request.BuilderResponse.Builder 分别构建请求和响应,其 build() 方法是参数组装的终点,适合捕获完整的元信息;ResponseBody.string() 是响应体解析的关键方法,适合捕获响应内容。
  2. 断点调试的价值
    通过调试可明确数据在哪个方法中最终确定(如 build() 方法),避免 hook 中间过程导致的信息不完整或重复。
  3. Frida 脚本设计思路
    • 用全局变量暂存请求和响应信息,解决 OkHttp 拦截器机制导致的重复日志问题。
    • 在响应体解析时统一输出,确保一次请求-响应对应一条完整日志。
相关推荐
sTone873752 小时前
Android Room部件协同使用
android·前端
我命由我123452 小时前
Android 开发 - Android JNI 开发关键要点
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
千码君20162 小时前
Android Emulator hypervisor driver is not installed on this machine
android
lichong9512 小时前
Android studio release 包打包配置 build.gradle
android·前端·ide·flutter·android studio·大前端·大前端++
傲世(C/C++,Linux)2 小时前
Linux系统编程——进程通信之有名管道
android·linux·运维
Hy行者勇哥4 小时前
物联网工控一体机操作系统选型:安卓、Ubuntu、Debian 场景化决策指南
android·物联网·ubuntu
llxxyy卢4 小时前
polar网站被黑
android
信创天地6 小时前
RISC-V 2025年在国内的发展趋势
python·网络安全·系统架构·系统安全·运维开发