1. 项目概述:为什么我们需要一个更强大的OkHttp日志工具
在移动应用安全分析、逆向工程或者日常的客户端开发调试中,网络请求的监控是一个绕不开的核心环节。OkHttp作为Android平台上事实标准的HTTP客户端库,承载了绝大多数应用与服务器通信的重任。无论是想分析某个App的API接口、调试自己应用的网络问题,还是进行安全测试,能够清晰地看到OkHttp发出的每一个请求和收到的每一个响应,其价值不言而喻。
市面上有很多工具可以做到这一点,比如配置代理抓包。但代理抓包有其局限性:对于使用了证书绑定(SSL Pinning)的应用,你需要额外绕过;对于非HTTP/HTTPS的流量(比如一些自定义的二进制协议),代理可能无能为力;更重要的是,你看到的只是最终发出的网络包,而看不到在应用层,OkHttp库在构建这个请求时完整的“心路历程”——比如,请求体是如何被构建和加密的?头部信息是在哪个环节被添加的?一个请求在被真正发出前,经历了哪些拦截器的处理?
这就是 OkHttpLogger-Frida 这类脚本的价值所在。它不依赖于网络层,而是直接“附身”于目标应用的OkHttp库内部,从源头进行Hook。它能打印出最原始、最清晰的请求和响应信息,包括完整的URL、方法、头部、请求体(甚至是加密前的明文)和响应体。而本次教程要做的,不仅仅是教会你使用这个强大的脚本,更是要带你进行一次“升级改造”,为它加上打印调用堆栈的功能。这意味着,你不仅能知道“发生了什么”,还能精确地定位到“是谁、在代码的哪个位置、调用了哪段逻辑”发起了这个请求。这对于逆向分析复杂业务逻辑、定位特定功能的代码入口点,具有决定性的意义。
2. 环境准备与工具链搭建
工欲善其事,必先利其器。在开始Hook之前,我们需要一个稳定、可用的工作环境。整个过程主要涉及三个部分:目标设备(或模拟器)、Frida运行环境以及我们的核心脚本。
2.1 Frida环境部署
Frida是一个动态代码插桩工具包,是我们的“手术刀”。它分为两部分:运行在目标设备上的服务端( frida-server )和运行在你电脑上的客户端( frida-tools )。
第一步:安装Python与Frida客户端 在你的电脑(分析机)上,确保已安装Python 3.7或更高版本。通过pip安装Frida客户端工具包是最简单的方式:
pip install frida-tools
安装完成后,在命令行输入 frida --version ,如果能正确显示版本号(如 16.1.11 ),说明客户端安装成功。
第二步:准备目标设备与Frida服务端 目标设备可以是Root后的安卓真机,也可以是x86/x86_64架构的安卓模拟器(如Android Studio自带的AVD)。 注意: ARM架构的模拟器(如蓝叠、雷电的部分版本)需要安装对应ARM架构的frida-server,过程会更复杂,建议初学者优先使用x86模拟器。
- 获取frida-server :访问Frida的GitHub发布页,根据你目标设备的CPU架构(可通过
adb shell getprop ro.product.cpu.abi查看)下载对应的frida-server-版本号-平台-android-架构.xz文件。例如,对于x86_64模拟器,就下载frida-server-*-android-x86_64.xz。 - 推送并启动 :解压下载的.xz文件,得到一个名为
frida-server的二进制文件。
执行adb push frida-server /data/local/tmp/ adb shell cd /data/local/tmp chmod 755 frida-server ./frida-server &./frida-server &后,进程会在后台运行。你可以新开一个命令行窗口,执行frida-ps -U,如果能看到设备上的进程列表,则证明frida-server运行正常,且与客户端连接成功。
注意 :每次重启目标设备后,都需要重新执行
./frida-server &来启动服务。你可以考虑将启动命令写入设备的启动脚本,但对于调试而言,手动启动更为可控。
2.2 获取与理解OkHttpLogger-Frida脚本
原始的OkHttpLogger-Frida脚本可以在GitHub等开源平台找到。其核心原理是使用Frida的JavaScript API,对OkHttp库中的关键类和方法进行拦截(Hook)。
核心Hook点通常包括:
-
okhttp3.OkHttpClient的newCall方法:这是发起请求的入口。 -
okhttp3.Call的execute(同步)和enqueue(异步)方法:这是请求被执行的地方。 -
okhttp3.Request和okhttp3.Response的构造体:从这里可以提取请求和响应的详细信息。
脚本的基本工作流程是:当上述方法被调用时,Frida会接管控制权,脚本则从中提取出 Request 对象,打印出其方法、URL、头部和请求体;对于响应,则拦截 Response 对象,打印状态码、头部和响应体。
脚本的潜在不足: 原始脚本可能只打印了请求/响应的基本信息,但缺少了至关重要的 调用堆栈(Stack Trace) 。没有堆栈信息,你就像在监控一个繁忙的路口,能看到所有车辆(请求)经过,却不知道它们从哪个小区(类)、哪栋楼(方法)开出来的。这在分析一个大型、复杂的应用时,会让人迷失方向。
3. 核心原理:Frida如何Hook与打印堆栈
在动手升级脚本前,我们需要从原理上搞清楚两件事:Frida Hook的基本模式,以及在JavaScript中如何获取Java的调用堆栈。
3.1 Frida Hook的两种常见模式
Frida提供了多种方式来拦截函数,最常用的是 Interceptor.attach 。
模式一:Hook已知地址的函数(Native/C++) 这对于系统库或原生库的函数非常有效,但不太适用于我们当前纯Java的OkHttp场景。
模式二:Hook Java类的方法(我们使用的方式) 这是Hook Android Java应用最直接的方式。通过Frida的 Java.use API,我们可以获取到Java类的包装对象,然后替换其方法的实现。
var OkHttpClient = Java.use('okhttp3.OkHttpClient');
OkHttpClient.newCall.overload('okhttp3.Request').implementation = function(request) {
console.log("[*] newCall hooked! URL: " + request.url().toString());
// 执行原始逻辑
return this.newCall(request);
};
上面的代码片段展示了基本的Hook模式: Java.use 获取类引用, .overload 指定要Hook的方法签名(因为可能有重载), .implementation 替换为我们自己的函数。在我们的函数里,我们可以打印信息、修改参数,最后再调用原始方法( this.newCall(request) )以保证程序正常运行。
3.2 在Frida中捕获Java调用堆栈
这是本次升级的 核心技巧 。在Java中,我们通常通过 Thread.currentThread().getStackTrace() 来获取堆栈。在Frida的JavaScript环境里,我们需要通过Java的反射机制来调用这个方法。
一个可靠且信息丰富的堆栈打印函数可以这样实现:
function printStackTrace() {
var thread = Java.use('java.lang.Thread');
var currentThread = thread.currentThread();
var stackTraceElements = currentThread.getStackTrace();
console.log("\n=== Java Call Stack ===");
// 从第4个元素开始打印,跳过getStackTrace、currentThread、printStackTrace自身
for (var i = 3; i < stackTraceElements.length; i++) {
var element = stackTraceElements[i];
console.log(" at " + element.getClassName() + "." + element.getMethodName() +
"(" + element.getFileName() + ":" + element.getLineNumber() + ")");
}
console.log("=====================\n");
}
为什么从 i=3 开始? 数组 stackTraceElements 的第0个元素通常是 getStackTrace 方法本身,第1个是 currentThread ,第2个是我们自己定义的 printStackTrace 函数。从第3个开始,才是真正有分析价值的、导致本次OkHttp调用的业务代码堆栈。
实操心得 :堆栈信息可能会非常长,尤其是在大型框架中。你可能会看到很多系统类库(如
android.os.Handler、java.util.concurrent)和中间件(如RxJava、Retrofit)的调用。关键是要学会快速识别与你关心的业务逻辑相关的包名和类名。例如,如果你的目标是分析com.example.app这个包下的代码,那么在堆栈中寻找包含这个包名的行就是你的重点。
4. 脚本升级实战:集成堆栈打印功能
现在,我们将堆栈打印功能集成到OkHttpLogger-Frida脚本中。我们的策略是:在Hook到关键方法(如 newCall )时,不仅打印请求信息,同时打印出此刻的调用堆栈。
4.1 定位并修改核心Hook函数
首先,找到原始脚本中Hook okhttp3.OkHttpClient.newCall 方法的部分。它可能看起来像这样:
var okhttp_client = Java.use('okhttp3.OkHttpClient');
okhttp_client.newCall.overload('okhttp3.Request').implementation = function(request){
var result = this.newCall(request);
// 原始脚本可能在这里打印request信息
console.log("[OkHttp] Request: " + request.method() + " " + request.url().toString());
// ... 可能还有打印headers和body的代码
return result;
};
我们需要对其进行改造,在打印请求信息前或后,调用我们的 printStackTrace 函数。
4.2 升级后的Hook代码示例
以下是集成堆栈打印后的一个完整示例片段:
// 定义堆栈打印函数
function printJavaStackTrace() {
try {
var Thread = Java.use('java.lang.Thread');
var stackTrace = Thread.currentThread().getStackTrace();
console.log("\n\x1b[36m[Call Stack Trace]\x1b[0m"); // 使用颜色高亮
// 跳过无关的堆栈帧
for (var i = 4; i < stackTrace.length && i < 15; i++) { // 限制打印深度,避免刷屏
var ste = stackTrace[i];
var className = ste.getClassName();
var methodName = ste.getMethodName();
var fileName = ste.getFileName();
var lineNumber = ste.getLineNumber();
// 过滤掉一些过于底层的系统堆栈,让输出更清晰
if (className.startsWith('okhttp3.') || className.startsWith('com.android.') || className.startsWith('java.')) {
continue;
}
console.log(" \x1b[33mat\x1b[0m " + className + "." + methodName +
"(" + (fileName ? fileName : "Unknown Source") + ":" + (lineNumber >= 0 ? lineNumber : "?") + ")");
}
console.log("");
} catch (e) {
console.log("[!] Failed to print stack trace: " + e);
}
}
// Hook OkHttpClient.newCall
var OkHttpClient = Java.use('okhttp3.OkHttpClient');
OkHttpClient.newCall.overload('okhttp3.Request').implementation = function(request) {
// 1. 打印堆栈,追溯调用来源
printJavaStackTrace();
// 2. 打印请求基本信息
console.log("\x1b[32m[OkHttp Request Intercepted]\x1b[0m");
console.log("Method: " + request.method());
console.log("URL: " + request.url().toString());
// 3. 打印请求头
var headers = request.headers();
var headerSize = headers.size();
if (headerSize > 0) {
console.log("Headers:");
for (var i = 0; i < headerSize; i++) {
console.log(" " + headers.name(i) + ": " + headers.value(i));
}
}
// 4. 尝试打印请求体(对于非GET请求)
var requestBody = request.body();
if (requestBody) {
try {
var buffer = Java.use('okio.Buffer').$new();
requestBody.writeTo(buffer);
var bodyString = buffer.readUtf8();
// 简单判断是否为可打印文本,避免二进制乱码
if (bodyString && bodyString.length > 0 && bodyString.length < 1024) {
console.log("Body (Preview): " + bodyString.substring(0, Math.min(200, bodyString.length)));
} else if (bodyString) {
console.log("Body Length: " + bodyString.length + " chars (likely binary or large text)");
}
} catch (e) {
console.log("Body: <无法读取或非文本格式>");
}
}
console.log("-".repeat(50));
// 继续执行原方法
return this.newCall(request);
};
代码解读与升级点:
- 独立的堆栈函数 :我们将
printJavaStackTrace定义为一个独立函数,便于在多个Hook点复用。 - 堆栈过滤与美化 :
- 通过
for (var i = 4; ...)跳过了更多Frida和反射相关的底层堆栈,让输出起点更接近业务代码。 - 增加了
i < 15的限制,防止某个递归或深度循环调用产生海量堆栈输出刷屏。 - 添加了简单的过滤逻辑,跳过了
okhttp3.、com.android.和java.开头的包,让输出聚焦于应用自身的业务代码。你可以根据目标应用修改这个过滤条件。 - 使用了ANSI颜色代码(
\x1b[36m等)在支持颜色的终端(如iTerm2, Windows Terminal)中高亮显示不同部分,提升可读性。
- 通过
- 健壮性增强 :在打印请求体时,用
try-catch包裹,并增加了对内容长度和类型的简单判断,避免因处理二进制体(如图片、加密数据)而抛出异常导致脚本崩溃。 - 结构化输出 :用分隔线
-和明确的标题([OkHttp Request Intercepted])让日志输出层次更清晰。
4.3 同时Hook响应(Response)
一个完整的日志工具也需要监控响应。Hook响应的地方通常在 Call.execute() 的返回值,或者异步回调 Callback.onResponse 中。这里以Hook Response 的构造为例(这是一种更底层的拦截方式):
var Response = Java.use('okhttp3.Response');
var ResponseBuilder = Java.use('okhttp3.Response$Builder');
// Hook Response.Builder.build() 方法,这是Response对象最终创建的地方
ResponseBuilder.build.implementation = function() {
var response = this.build(); // 先调用原方法获取Response对象
// 只有在特定的请求上下文下才打印,避免日志过多
// 可以通过关联之前的request来实现,这里简化为直接打印
console.log("\x1b[35m[OkHttp Response Received]\x1b[0m");
console.log("URL: " + response.request().url().toString());
console.log("Code: " + response.code());
console.log("Message: " + response.message());
var responseBody = response.body();
if (responseBody) {
try {
var source = responseBody.source();
source.request(Java.use('java.lang.Long').MAX_VALUE); // 缓冲整个响应体
var buffer = source.buffer().clone();
var bodyString = buffer.readUtf8();
if (bodyString) {
console.log("Response Body Preview:\n" + bodyString.substring(0, Math.min(500, bodyString.length)));
if (bodyString.length > 500) {
console.log("... (truncated, total " + bodyString.length + " chars)");
}
}
} catch (e) {
console.log("Response Body: <无法读取> " + e);
}
}
console.log("-".repeat(50));
return response;
};
重要提示 :直接Hook
Response.Builder.build会拦截 所有 OkHttp响应,包括图片、视频等资源请求,可能会产生巨量日志并严重影响性能。在生产调试中,你需要添加更精细的过滤逻辑,例如只关注特定域名、特定路径的请求。
5. 实战操作:运行与调试脚本
脚本编写完成后,下一步就是将其注入到目标进程中。
5.1 启动Frida并附加进程
首先,确保你的目标App已经安装在设备上并处于运行状态(或可启动状态)。通过以下命令找到目标App的进程名(包名):
frida-ps -U | grep -i 应用名关键词
或者,如果你知道包名(例如 com.example.targetapp ),可以直接使用。
然后,使用Frida CLI将我们的脚本注入到目标进程:
frida -U -f com.example.targetapp -l upgraded_okhttp_logger.js --no-pause
-
-U: 连接到USB设备。 -
-f com.example.targetapp: 启动(spawn)这个应用。如果应用已在运行,你可以用-F(附加到最前台的应用程序)或直接使用进程ID。 -
-l upgraded_okhttp_logger.js: 加载我们刚刚写好的JavaScript脚本文件。 -
--no-pause: 立即启动应用,不要暂停。对于需要自动启动的应用很有用。
如果注入成功,你会在终端看到Frida的连接提示,随后当目标应用发起OkHttp网络请求时,你精心设计的日志(包含堆栈!)就会源源不断地打印出来。
5.2 日志分析与问题排查技巧
当脚本开始工作后,你可能会遇到各种情况。如何从海量日志中快速找到有用信息?
- 信息过载 :如果日志刷屏太快,首先考虑添加过滤条件。可以在Hook函数的最开始添加判断:
var url = request.url().toString(); if (!url.includes("api.example.com")) { // 只关心特定域名 return this.newCall(request); // 直接返回,不打印 } - 脚本崩溃或无输出 :
- 检查Frida-server :确保
frida-server仍在设备后台运行,并且版本与客户端frida-tools兼容。 - 检查Hook的类名和方法签名 :不同版本的OkHttp库,类名和方法签名可能有细微差别。使用
frida的-D参数开启调试,或者使用Java.available和Java.enumerateLoadedClasses()来确认目标类是否已加载。 - 查看JavaScript错误 :Frida可能会在终端输出JavaScript执行错误。仔细阅读错误信息,通常是语法错误或访问了不存在的属性/方法。
- 检查Frida-server :确保
- 堆栈信息不完整或全是系统类 :这说明触发网络请求的代码可能位于Native层(C/C++)、使用了其他网络库、或者请求是在工作线程中由系统框架发起的。此时,你需要调整堆栈打印的起始索引(
i的值)或放宽过滤条件。也可以尝试Hook更底层的点,如java.net.Socket相关类,但这需要更深入的分析。 - 请求体/响应体为乱码或空 :这很可能是因为数据被压缩(如gzip)或加密了。OkHttp的拦截器链可能会在请求发出前对Body进行压缩,在响应收到后进行解压。我们的Hook点如果在压缩/加密之后,解压/解密之前,看到的就是乱码。解决这个问题需要更精确地定位Hook点,或者Hook负责加解密/压缩的特定拦截器或方法,这属于更高级的逆向工程范畴。
6. 高级技巧与扩展思路
掌握了基础Hook和堆栈打印后,你可以根据实际需求,将这个脚本变得更加强大和定制化。
6.1 动态配置与过滤
将需要监控的域名、关键词等配置提取到脚本开头,方便修改:
var config = {
targetDomains: ["api.target.com", "auth.target.com"],
enableStacktrace: true,
maxStackDepth: 12,
logLevel: "DEBUG" // DEBUG, INFO, ERROR
};
// 在Hook函数中使用
if (config.enableStacktrace) {
printJavaStackTrace(config.maxStackDepth);
}
var url = request.url().toString();
var shouldLog = config.targetDomains.some(domain => url.includes(domain));
if (!shouldLog) {
return this.newCall(request);
}
6.2 关联请求与响应
在复杂的异步场景下,一个请求和它的响应可能在日志中相隔很远。为了关联它们,可以为每个拦截的请求生成一个唯一ID(如UUID),并同时打印在请求和响应日志中。
function generateRequestId() {
return Java.use('java.util.UUID').randomUUID().toString().substring(0, 8);
}
OkHttpClient.newCall.implementation = function(request) {
var requestId = generateRequestId();
console.log(`[ReqId: ${requestId}] ` + request.url().toString());
// ... 存储requestId到某个全局映射,或附加到call对象(如果可能)
var call = this.newCall(request);
// 假设我们可以通过某种方式将requestId与call关联(这里需要更巧妙的技巧,例如Hook Call对象本身)
return call;
};
6.3 文件输出与持久化
终端输出不利于长期分析。可以将日志写入到设备的文件中,或者通过Frida的 send 函数发回给PC端Python脚本进行处理和存储。
// 在设备上写文件(需要应用有写权限,或脚本在root环境下运行)
var FileWriter = Java.use('java.io.FileWriter');
var file = Java.use('java.io.File').$new("/sdcard/okhttp_log.txt");
var fw = FileWriter.$new(file, true); // true表示追加
fw.write(JSON.stringify(logData) + "\n");
fw.flush();
fw.close();
更优雅的方式是使用Frida的RPC(Remote Procedure Call),在Python端接收和处理数据。
6.4 应对反调试与检测
一些安全意识较强的应用会检测Frida的存在。常见的检测手段包括检查特定端口( 27042 默认端口)、检查加载的库、检查进程名等。对抗这些检测是一个猫鼠游戏,可以尝试:
- 使用非默认端口启动frida-server:
./frida-server -l 0.0.0.0:8080 - 重命名frida-server二进制文件。
- 使用定制编译的、去除了特征字的Frida。
- 在脚本中主动Hook应用自身的检测函数,使其返回假值。
7. 常见问题与排查实录
在这一部分,我汇总了在实际使用和教学过程中,学员们最常踩到的“坑”及其解决方案。
问题1:执行 frida-ps -U 提示 Failed to enumerate processes: unable to connect to remote frida-server
- 排查步骤 :
- 检查设备连接 :
adb devices确认设备已连接。 - 检查frida-server进程 :
adb shell ps | grep frida或adb shell "ps -A | grep frida",查看进程是否存在。如果不存在,重新执行启动命令。 - 检查端口 :
adb shell netstat -tulpn | grep 27042,查看27042端口是否被监听。如果frida-server使用了非默认端口,需要在使用客户端时指定,如frida -H 设备IP:端口 ...。 - 检查版本兼容性 :确保PC上的
frida-tools与设备上的frida-server主版本号一致。最好都使用相同的最新稳定版。
- 检查设备连接 :
问题2:脚本注入成功,但没有任何日志输出
- 可能原因与解决 :
- 目标应用未使用OkHttp :它可能使用了
HttpURLConnection、Volley或其他网络库。你需要先确认这一点,可以通过检查APK的依赖库,或者用Frida枚举已加载的类Java.enumerateLoadedClasses({onMatch: function(c){console.log(c)}, onComplete: function(){}}),搜索okhttp3。 - Hook的类/方法签名错误 :OkHttp版本不同,类路径或方法可能变化。使用
Java.use尝试捕获异常,或者用Java.choose在堆上查找已存在的实例来推导类名。 - 请求发生在脚本注入之前 :有些请求在App启动初始化时就发出了。确保在App启动早期就注入脚本(使用
-f参数spawn应用),或者Hook更早的生命周期点。
- 目标应用未使用OkHttp :它可能使用了
问题3:打印堆栈时脚本崩溃,报错 TypeError: cannot read property 'getStackTrace' of undefined
- 原因 :在Frida的JavaScript线程中直接调用
Thread.currentThread()可能无法正确获取到Java线程上下文。特别是在异步回调或特定Hook点。 - 解决 :将堆栈打印的代码包裹在
Java.perform()函数内部,确保它在正确的Java线程上下文中执行。
在所有Hook函数的实现中,调用function printStackTraceSafe() { Java.perform(function() { // 原来的printStackTrace代码放在这里 var thread = Java.use('java.lang.Thread'); // ... }); }printStackTraceSafe()而不是printStackTrace()。
问题4:日志太多太快,看不清也存不下
- 解决 :
- 强化过滤 :如前所述,在Hook点入口处根据URL、方法(GET/POST)、甚至特定的Header进行过滤。
- 采样输出 :可以设置一个计数器,每N个请求只打印一次。
- 输出到文件并轮转 :实现一个简单的日志模块,当文件达到一定大小时,自动备份并创建新文件。
- 使用图形化工具 :考虑将数据通过Frida RPC发送到PC端,用Wireshark、Charles的远程接口,或自己编写一个简单的Python图形界面来接收和展示日志,支持搜索和过滤。
问题5:遇到“Detected suspicious behavior”或应用闪退
- 原因 :应用可能集成了反调试或反Frida机制。
- 初步应对 :
- 尝试使用
frida -U -f com.example.app --no-pause中的--no-pause有时能绕过简单的检测。 - 尝试在非Root环境下使用
frida-gadget(将so库嵌入APK)的方式进行注入,这种方式更隐蔽。 - 搜索并学习常见的Frida反反调试技巧,例如Hook
android.os.Debug.isDebuggerConnected()、java.lang.System.getProperty等检测函数。
- 尝试使用
这个升级版的OkHttpLogger-Frida脚本,通过引入调用堆栈打印功能,将网络请求监控从“黑盒观察”提升到了“白盒追踪”的层面。它不仅仅是一个调试工具,更是一个强大的逆向分析助手。掌握它,意味着你能更清晰地洞察移动应用的内在网络行为逻辑。当然,真实环境永远更复杂,你可能需要根据面对的具体应用和挑战,灵活调整Hook策略、过滤条件和对抗检测的方法。记住,关键不在于记住所有代码,而在于理解其原理,并能根据原理去解决新的问题。

被折叠的 条评论
为什么被折叠?



