【Frida Android】实战篇3:基于 OkHttp 库的 Hook 抓包
文章目录
- 前言
- 1. OkHttp 介绍
- 1.1 作用
- 1.2 常用类与方法
- 2. 前置操作
- 2.1 启动 frida-server
- 2.2 启动脚本
- 3. Hook 思路
- 3.1 断点调试
- 3.2 Hook 脚本
- 3.3 脚本详解
- 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 启动脚本
与和上一章相同:
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() 方法处下断点:
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(其中 $Builder 为 Request 类的内部类,@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.RequestBody与okhttp3.Request$Builder之间的调用关系。
在Frida Hook技术中,针对此类底层内部类进行拦截是一种高效且常用的手段。基于上述调试所得的类结构与调用链信息,我们可以精准定位需要Hook的目标方法,从而实现对HTTP请求与响应信息的完整捕获。
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;};
});
输出日志如下:
[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 内置的拦截器和开发者自定义的拦截器)。
具体流程如下:
- 当调用
Request.Builder().build()构建请求时,请求会先进入第一个拦截器; - 拦截器处理后,将请求传递给下一个拦截器;
- 经过所有拦截器后,请求才会真正发送到服务器;
- 服务器返回响应后,响应会按相反顺序经过所有拦截器,最终回到应用层。
正因为这种“链式传递”,我们 Hook 的 Request.Builder.build() 和 Response.Builder.build() 方法会被每个拦截器调用一次。例如:
- 如果 OkHttp 内部有 3 个内置拦截器,加上 1 个自定义应用拦截器,那么一个请求会触发 4 次
build()调用; - 对应的响应也会经过同样的拦截器链,导致响应相关的 Hook 方法被多次触发。
这就是日志中出现重复内容的核心原因——并非请求被发送了多次,而是同一请求/响应在拦截器链中被多次处理,每次处理都会触发我们的 Hook 逻辑。
了解这一机制后,我们就能理解如何优化脚本:通过全局变量记录最近一次的请求/响应信息,只在最终获取响应体时统一输出,从而避免拦截器链式调用导致的重复日志。
最终脚本 优化后如下:请求和响应内容进行了去重,你也可以根据自己的需求继续改造,以获得适合自己业务需要的数据,该脚本也可以作为 OkHttp 库的通用 Hook 脚本。
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记录如下:可以看到内容更加简洁清晰。
[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 脚本详解
-
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)});}// 读取并存储请求体(通过 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()),又新增信息收集逻辑。
- 作用:捕获请求的 URL、方法、头和体,并存储到
-
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; // 返回原响应对象,不影响正常流程 };- 作用:捕获响应的状态码、URL、响应头,并存储到
lastResponseInfo全局变量。 - 关键:响应与请求通过
response.request().url()关联,确保后续输出时请求与响应对应。
- 作用:捕获响应的状态码、URL、响应头,并存储到
-
Hook 响应体:
ResponseBody.string()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()是响应体解析的最后一步,此时lastRequestInfo和lastResponseInfo已包含完整数据,适合作为输出触发点。
4. 技术总结
- OkHttp 核心逻辑与 hook 点选择
OkHttp 通过Request.Builder和Response.Builder分别构建请求和响应,其build()方法是参数组装的终点,适合捕获完整的元信息;ResponseBody.string()是响应体解析的关键方法,适合捕获响应内容。 - 断点调试的价值
通过调试可明确数据在哪个方法中最终确定(如build()方法),避免 hook 中间过程导致的信息不完整或重复。 - Frida 脚本设计思路
- 用全局变量暂存请求和响应信息,解决 OkHttp 拦截器机制导致的重复日志问题。
- 在响应体解析时统一输出,确保一次请求-响应对应一条完整日志。
