文章目录
- 前言
- [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(其中 $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请求与响应信息的完整捕获。
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 内置的拦截器和开发者自定义的拦截器)。
具体流程如下:
- 当调用
Request.Builder().build()构建请求时,请求会先进入第一个拦截器; - 拦截器处理后,将请求传递给下一个拦截器;
- 经过所有拦截器后,请求才会真正发送到服务器;
- 服务器返回响应后,响应会按相反顺序经过所有拦截器,最终回到应用层。
正因为这种"链式传递",我们 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 脚本详解
-
Hook 请求构建:
Request.Builder.build()javascriptvar 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()javascriptvar 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()javascriptvar 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 拦截器机制导致的重复日志问题。
- 在响应体解析时统一输出,确保一次请求-响应对应一条完整日志。