【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 拦截器机制导致的重复日志问题。
    • 在响应体解析时统一输出,确保一次请求-响应对应一条完整日志。
相关推荐
半路_出家ren11 分钟前
1.古典密码概述
python·网络安全·密码学·古典密码·加密方式
阿杰 AJie1 小时前
MySQL 聚合函数
android·数据库·mysql
cws2004011 小时前
MFA双因素用户使用手册
运维·windows·网络安全·github·邮件·邮箱
孟秋与你2 小时前
【安卓】开发一个读取文件信息的简易apk
android
42nf2 小时前
Android Launcher3添加负一屏
android·launcher3·android负一屏
LcVong3 小时前
老版本Android源码在新版本IDE打开的常规报错及解决方案
android·ide
天荒地老笑话么3 小时前
为什么访问 http://example.com 和 https://example.com 安全性不同(明文 vs 加密)
网络安全
别退3 小时前
flutter_gradle_android
android·flutter
2501_944424123 小时前
Flutter for OpenHarmony游戏集合App实战之黑白棋落子翻转
android·开发语言·windows·flutter·游戏·harmonyos
zhangphil3 小时前
Android adb shell抓取trace(二)
android