安卓逆向实战:从 Frida 动态插桩到 OkHttp HTTPS 抓包与证书校验绕过
这篇文章我会按“能跑起来、能看到效果、能定位问题”的思路,带你走一遍完整链路:定位 App 是否使用 OkHttp、用 Frida 动态插桩观察请求、配置 HTTPS 抓包、最后在证书校验拦截点完成绕过。
很多同学一开始卡住,不是因为不会写脚本,而是因为链路没串起来:
到底是代理没生效、证书没装对、App 做了 pinning,还是自己 hook 的位置不对?
这篇就专门解决这个问题。
说明:以下内容用于授权测试、学习研究与企业安全自检。不要用于未授权目标。
背景与问题
在 Android 安全测试里,最常见的目标之一,就是分析 App 的网络行为。
如果目标使用明文 HTTP,事情很简单;但现实里大多是:
- 使用 HTTPS
- 网络库是 OkHttp
- 还启用了 证书校验 / 证书锁定(Certificate Pinning)
- 有些 App 甚至会加上代理检测、双向校验、混淆
结果就是:你把手机代理指向 Burp 或 Charles,App 直接报错,抓不到包。
一个典型现象是:
- 浏览器能抓包
- 其他 App 能抓包
- 就这个目标 App 不行
- 日志里常见:
SSLHandshakeExceptionCertificate pinning failure!Trust anchor for certification path not foundCLEARTEXT communication not permitted(这不是证书问题,但经常混在一起)
所以,我们需要一个更稳定的思路:
- 先确认 App 的网络栈
- 动态观察请求路径
- 对准证书校验位置做最小化 hook
- 验证抓包链路是否恢复
前置知识
建议你至少了解这些概念:
- Android 代理与用户证书
- TLS 握手基础
- Java 层与 Native 层 hook 的区别
- OkHttp 常见类:
OkHttpClientRequestInterceptorCertificatePinner
如果你对 Frida 还不熟,也没关系,本文会尽量按“实战路径”讲,而不是一上来堆 API。
环境准备
测试环境
建议准备:
- 一台 Android 真机或模拟器
- 已安装目标 App
- 电脑上安装:
adbfrida-toolsBurp Suite或Charles
- 手机端:
- 安装
frida-server(需与设备架构、Frida 版本匹配) - 安装抓包工具根证书
- 安装
基础连通性检查
先确认 Frida 能看到进程:
frida-ps -U
如果能列出设备进程,说明连接没问题。
如果你想附加到目标 App:
frida -U -f com.example.app -l hook.js --no-pause
或者附加到已启动进程:
frida -U -n com.example.app -l hook.js
核心原理
这个问题的关键,不在“抓包工具怎么配”,而在于你要理解 HTTPS 抓包为什么会失败。
1. 正常 HTTPS 抓包链路
当 App 通过代理访问 HTTPS 服务时,抓包工具会作为一个中间人:
- App 发起 TLS 握手
- 抓包工具伪造目标站点证书
- App 校验这个证书
- 如果 App 信任抓包工具的 CA,则连接继续
- 否则握手失败
2. 为什么装了证书还失败
因为 App 不一定只依赖系统信任链。它可能还做了:
- 自定义
TrustManager - 自定义
HostnameVerifier - OkHttp 的
CertificatePinner - Network Security Config 限制
- Native 层校验
其中,OkHttp 的 CertificatePinner 是非常高频的一类。
3. OkHttp 的典型校验点
常见的绕过位置有:
okhttp3.CertificatePinner.check(...)- 自定义
X509TrustManager.checkServerTrusted(...) javax.net.ssl.SSLContext.init(...)javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(...)okhttp3.OkHttpClient$Builder.sslSocketFactory(...)
实战里我一般不主张一上来就“全家桶式通杀 hook”,因为副作用大,也容易把问题掩盖掉。
更好的方法是:先定位,再最小化绕过。
整体流程图
flowchart TD
A[启动抓包代理 Burp/Charles] --> B[设备配置 WiFi 代理]
B --> C[安装代理根证书]
C --> D[尝试抓包]
D --> E{是否成功}
E -- 是 --> F[进入请求分析]
E -- 否 --> G[Frida 识别网络栈与校验点]
G --> H[Hook OkHttp/TrustManager/HostnameVerifier]
H --> I[再次验证 HTTPS 抓包]
I --> J[定位残余问题: Pinning/代理检测/Native]
核心调用关系
sequenceDiagram
participant App
participant OkHttp
participant TLS
participant Proxy as Burp/Charles
participant Server
App->>OkHttp: 发起 HTTPS 请求
OkHttp->>TLS: 建立 SSL/TLS 连接
TLS->>Proxy: CONNECT / TLS 握手
Proxy->>TLS: 返回伪造站点证书
TLS->>OkHttp: 证书链校验
alt 启用 Pinning 且不匹配
OkHttp-->>App: SSLHandshakeException / Pinning failure
else 信任链通过
Proxy->>Server: 代表客户端访问真实服务
Server-->>Proxy: 响应数据
Proxy-->>App: 返回解密后的响应
end
实战代码(可运行)
下面的代码分成三步:
- 判断 App 是否使用 OkHttp
- 观察请求信息
- 绕过常见证书校验
第一步:识别 OkHttp 并打印请求
这个脚本优先做“观察”,先别急着绕过。
因为你得先知道目标是不是 OkHttp,以及请求发没发出去。
Java.perform(function () {
function safeHook(className, methodName, overloadsHandler) {
try {
var clazz = Java.use(className);
overloadsHandler(clazz);
console.log("[+] Hooked " + className + "." + methodName);
} catch (e) {
console.log("[-] Failed to hook " + className + "." + methodName + ": " + e);
}
}
safeHook("okhttp3.RealCall", "execute/enqueue", function (RealCall) {
RealCall.execute.implementation = function () {
var req = this.request();
console.log("\n[RealCall.execute]");
console.log("URL : " + req.url().toString());
console.log("Method : " + req.method());
console.log("Headers: " + req.headers().toString());
return this.execute();
};
RealCall.enqueue.overload("okhttp3.Callback").implementation = function (cb) {
var req = this.request();
console.log("\n[RealCall.enqueue]");
console.log("URL : " + req.url().toString());
console.log("Method : " + req.method());
console.log("Headers: " + req.headers().toString());
return this.enqueue(cb);
};
});
safeHook("okhttp3.Request$Builder", "build", function (Builder) {
Builder.build.implementation = function () {
var req = this.build();
console.log("\n[Request.Builder.build]");
console.log("URL : " + req.url().toString());
console.log("Method : " + req.method());
return req;
};
});
});
运行方式:
frida -U -f com.example.app -l okhttp_observe.js --no-pause
如果控制台里能看到 URL、Method、Headers,说明:
- App 大概率在 Java 层使用了 OkHttp
- 请求路径可观测
- 后续可以更精准地 hook 校验点
第二步:绕过 OkHttp CertificatePinner
如果你在日志里见到类似 Certificate pinning failure!,优先看这个点。
Java.perform(function () {
try {
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
CertificatePinner.check.overload("java.lang.String", "java.util.List").implementation = function (hostname, peerCertificates) {
console.log("[+] Bypass CertificatePinner.check(String, List): " + hostname);
return;
};
console.log("[+] CertificatePinner bypass installed");
} catch (e) {
console.log("[-] CertificatePinner hook failed: " + e);
}
});
有些版本/混淆场景下,check 重载可能不止一种,可以把所有重载都打出来:
Java.perform(function () {
try {
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
var overloads = CertificatePinner.check.overloads;
console.log("[*] CertificatePinner.check overload count: " + overloads.length);
overloads.forEach(function (ov) {
console.log("[*] overload: " + ov.argumentTypes.map(function (t) {
return t.className;
}).join(", "));
ov.implementation = function () {
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
console.log("[+] Bypass CertificatePinner.check with args: " + args);
return;
};
});
} catch (e) {
console.log("[-] Failed to bypass CertificatePinner: " + e);
}
});
第三步:绕过自定义 TrustManager
如果目标没有走 CertificatePinner,但仍然报 TLS 校验错误,那往往是 TrustManager。
下面这个脚本通过替换 SSLContext.init() 里使用的信任管理器,实现“信任所有证书”的效果。
这是测试里很常见的一种通用打法。
Java.perform(function () {
try {
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var SSLContext = Java.use("javax.net.ssl.SSLContext");
var TrustManager = Java.registerClass({
name: "com.frida.TrustAllManager",
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {},
checkServerTrusted: function (chain, authType) {},
getAcceptedIssuers: function () {
return [];
}
}
});
var TrustManagers = [TrustManager.$new()];
var SSLContext_init = SSLContext.init.overload(
"[Ljavax.net.ssl.KeyManager;",
"[Ljavax.net.ssl.TrustManager;",
"java.security.SecureRandom"
);
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {
console.log("[+] SSLContext.init() intercepted, replacing TrustManagers");
SSLContext_init.call(this, keyManager, TrustManagers, secureRandom);
};
console.log("[+] TrustManager bypass installed");
} catch (e) {
console.log("[-] TrustManager bypass failed: " + e);
}
});
第四步:绕过 HostnameVerifier
有些应用证书链虽然过了,但会在主机名校验时失败。
这时可以补一个 HostnameVerifier 的 hook。
Java.perform(function () {
try {
var HostnameVerifier = Java.use("javax.net.ssl.HostnameVerifier");
var HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");
var TrustHostnameVerifier = Java.registerClass({
name: "com.frida.TrustHostnameVerifier",
implements: [HostnameVerifier],
methods: {
verify: function (hostname, session) {
console.log("[+] HostnameVerifier bypass for: " + hostname);
return true;
}
}
});
HttpsURLConnection.setDefaultHostnameVerifier(TrustHostnameVerifier.$new());
console.log("[+] HostnameVerifier bypass installed");
} catch (e) {
console.log("[-] HostnameVerifier bypass failed: " + e);
}
});
第五步:组合脚本
实际测试时,我更推荐用一个“可直接落地”的组合版。
下面这个脚本同时覆盖:
- OkHttp CertificatePinner
- SSLContext TrustManager
- HostnameVerifier
Java.perform(function () {
console.log("[*] Frida SSL bypass script starting...");
try {
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
CertificatePinner.check.overloads.forEach(function (ov) {
ov.implementation = function () {
var host = arguments.length > 0 ? arguments[0] : "<unknown>";
console.log("[+] Bypass CertificatePinner for host: " + host);
return;
};
});
console.log("[+] OkHttp CertificatePinner hooked");
} catch (e) {
console.log("[-] OkHttp CertificatePinner not hooked: " + e);
}
try {
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var SSLContext = Java.use("javax.net.ssl.SSLContext");
var TrustAllManager = Java.registerClass({
name: "com.frida.TrustAllManager",
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {},
checkServerTrusted: function (chain, authType) {},
getAcceptedIssuers: function () {
return [];
}
}
});
var trustManagers = [TrustAllManager.$new()];
var init = SSLContext.init.overload(
"[Ljavax.net.ssl.KeyManager;",
"[Ljavax.net.ssl.TrustManager;",
"java.security.SecureRandom"
);
init.implementation = function (km, tm, sr) {
console.log("[+] Replacing SSLContext TrustManagers");
init.call(this, km, trustManagers, sr);
};
console.log("[+] SSLContext hooked");
} catch (e) {
console.log("[-] SSLContext hook failed: " + e);
}
try {
var HostnameVerifier = Java.use("javax.net.ssl.HostnameVerifier");
var HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");
var MyHostnameVerifier = Java.registerClass({
name: "com.frida.MyHostnameVerifier",
implements: [HostnameVerifier],
methods: {
verify: function (hostname, session) {
console.log("[+] Hostname verified: " + hostname);
return true;
}
}
});
HttpsURLConnection.setDefaultHostnameVerifier(MyHostnameVerifier.$new());
console.log("[+] HostnameVerifier hooked");
} catch (e) {
console.log("[-] HostnameVerifier hook failed: " + e);
}
console.log("[*] Frida SSL bypass script loaded");
});
运行:
frida -U -f com.example.app -l ssl_bypass.js --no-pause
逐步验证清单
我建议你每次都按这个顺序验证,效率会高很多:
1. 先验证设备代理
浏览器访问一个 HTTPS 网站,看看 Burp/Charles 能否看到流量。
2. 再验证目标 App 是否真的走代理
有些 App 会忽略系统代理,或者自己实现 socket 连接。
这时你需要观察:
- 抓包工具里完全无 CONNECT 请求
- Frida 中能看到请求,但代理侧没流量
3. 再判断是不是证书问题
如果 Burp 有连接痕迹,但 App 报 SSL 错,多半是:
- TrustManager
- HostnameVerifier
- CertificatePinner
4. 最后再考虑 Native 或混淆
如果 Java 层全 hook 了还是不行,才去看:
libsslcronetconscrypt- 自研 Native 校验逻辑
常见坑与排查
这一节很重要,基本都是我自己或者同事实战里踩过的坑。
坑 1:Frida 能连设备,但 hook 不生效
常见原因:
- 附加时机太晚
- 目标类还没加载
- 混淆后类名变了
- 多进程 App,你 hook 错了进程
建议:
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch: function (name) {
if (name.indexOf("okhttp") >= 0) {
console.log(name);
}
},
onComplete: function () {
console.log("[*] done");
}
});
});
这个办法可以先确认类名是否存在。
坑 2:脚本里递归调用自己,App 直接卡死
比如你这样写:
RealCall.execute.implementation = function () {
return this.execute();
};
这会无限递归。
正确方式一般是调用原始重载对象,例如:
var execute = RealCall.execute.overload();
execute.implementation = function () {
console.log("[*] execute called");
return execute.call(this);
};
我当时第一次写 Frida 时,就在这地方卡了半天,现象像“App 莫名无响应”,实际是自己把自己绕进去了。
坑 3:Burp 装了证书,Android 7+ 还是抓不到
因为 Android 7.0 以后,很多 App 默认不信任用户安装的 CA。
这时即便系统里已安装 Burp 证书,也不代表 App 会信。
要么:
- 修改 App 的 Network Security Config(重打包路线)
- 要么用 Frida 动态绕过校验(本文路线)
坑 4:OkHttp 版本差异导致 hook 失败
不同版本里类和方法有差异,比如:
okhttp3.CertificatePinner- 某些内部类命名变化
- 混淆后包名不一定还是
okhttp3
排查方法:
- 先枚举类名
- 观察报错位置
- 从请求构建点向连接建立点逐步逼近
坑 5:不是 OkHttp,而是 WebView / Cronet / Native
有时你很确定“这是个 HTTP 请求”,但就是 hook 不到 OkHttp。
那就别死盯着 OkHttp 了,可能是:
android.webkit.WebView- Google
Cronet - Native 层 TLS
- Flutter / React Native 的网络桥接实现
这也是为什么我前面一直强调:
先观察,后绕过;先确认栈,后下手。
坑 6:代理检测导致请求根本不发
部分 App 会检测:
- 是否设置了系统代理
- 是否安装了特定抓包证书
- 本地端口是否像 Burp
- VPN/调试器/Root/Frida 痕迹
这时的现象看起来很像“证书失败”,但本质不是一回事。
如果请求压根没发出,先别急着搞 SSL bypass。
排查流程图
flowchart TD
A[抓不到 HTTPS 包] --> B{浏览器能否抓包}
B -- 否 --> C[检查代理/证书/网络连通性]
B -- 是 --> D{目标 App 是否有 CONNECT 痕迹}
D -- 否 --> E[可能未走系统代理或有代理检测]
D -- 是 --> F{App 是否报 SSL/Pinning 错误}
F -- 是 --> G[Hook CertificatePinner/TrustManager/HostnameVerifier]
F -- 否 --> H[检查是否为 WebView/Cronet/Native 实现]
安全/性能最佳实践
这部分不只是“安全姿势正确”,也关系到你的测试是否稳定。
1. 优先做最小化 hook
能只 hook CertificatePinner.check(),就别直接全局信任所有证书。
原因很现实:
- 更接近真实行为
- 副作用小
- 便于定位到底是谁在拦
2. 把观察脚本和绕过脚本分开
我自己的习惯是分三类脚本:
observe.js:打印请求、类加载、堆栈pinning_bypass.js:只处理 pinninguniversal_ssl_bypass.js:最后兜底
这样你不会因为一个“大而全脚本”把问题复杂化。
3. 日志别打太猛
Frida 打日志过多会影响性能,尤其是:
- 高频请求
- 大量 Header / Body
- UI 线程调用
建议:
- 先只打印 URL、Method
- 确认路径后再加详细字段
- 避免对每个字节流都做 console 输出
4. 对生产环境保持克制
哪怕是企业内授权测试,也建议:
- 使用测试账号
- 不抓取无关用户数据
- 对敏感数据做脱敏保存
- 测试后撤销证书、代理、脚本
5. 遇到 Native 再升级手段
如果 Java 层已经确认无效,再考虑:
- Hook
libssl - Hook
SSL_read/SSL_write - 观察
JNI_OnLoad - 配合
objection、r2frida、jadx
不要一开始就把难度拉到最高,那样很容易陷在细节里。
一套推荐的实战路径
如果你现在要真的上手,我建议按下面节奏走:
- 配好代理和证书
- 浏览器验证抓包链路
- Frida 枚举类,确认是否使用 OkHttp
- Hook Request/RealCall,确认请求确实发起
- 尝试只绕过 CertificatePinner
- 如果还失败,再上 TrustManager
- 若主机名错误,再补 HostnameVerifier
- 仍不行,再检查代理检测、WebView、Cronet、Native
这条路径最大的好处是:
你每一步都知道自己在验证什么,不会陷入“脚本贴了一堆,但不知道哪行起作用”的混乱状态。
总结
这篇文章的核心不是“背几个通用 Frida 脚本”,而是建立一条清晰的 HTTPS 逆向分析链路:
- 先确认网络栈
- 再观察请求行为
- 最后针对证书校验点做绕过
针对 OkHttp,最常见也最值得优先尝试的点是:
okhttp3.CertificatePinner.check(...)SSLContext.init(...)HostnameVerifier.verify(...)
如果你只想先拿到结果,可以先跑组合脚本;
但如果你想真正提升实战能力,我更建议你按“观察 → 定位 → 最小化绕过”的方式来做。
最后给几个可执行建议:
- 能最小 hook,就别全局信任
- 先看请求有没有发,再看为什么握手失败
- Java 层不通,再考虑 Native
- 排查时保留日志,但别过量
- 始终限定在授权测试边界内
只要把这套方法论走顺,后面无论是 OkHttp、WebView 还是更复杂的网络栈,你都会更有把握。