跳转到内容
123xiao | 无名键客

《安卓逆向实战:从 Frida 动态插桩到 OkHttp HTTPS 抓包与证书校验绕过》

字数: 0 阅读时长: 1 分钟

安卓逆向实战:从 Frida 动态插桩到 OkHttp HTTPS 抓包与证书校验绕过

这篇文章我会按“能跑起来、能看到效果、能定位问题”的思路,带你走一遍完整链路:定位 App 是否使用 OkHttp、用 Frida 动态插桩观察请求、配置 HTTPS 抓包、最后在证书校验拦截点完成绕过

很多同学一开始卡住,不是因为不会写脚本,而是因为链路没串起来:
到底是代理没生效、证书没装对、App 做了 pinning,还是自己 hook 的位置不对?
这篇就专门解决这个问题。

说明:以下内容用于授权测试、学习研究与企业安全自检。不要用于未授权目标。


背景与问题

在 Android 安全测试里,最常见的目标之一,就是分析 App 的网络行为。
如果目标使用明文 HTTP,事情很简单;但现实里大多是:

  • 使用 HTTPS
  • 网络库是 OkHttp
  • 还启用了 证书校验 / 证书锁定(Certificate Pinning)
  • 有些 App 甚至会加上代理检测双向校验混淆

结果就是:你把手机代理指向 Burp 或 Charles,App 直接报错,抓不到包。

一个典型现象是:

  • 浏览器能抓包
  • 其他 App 能抓包
  • 就这个目标 App 不行
  • 日志里常见:
    • SSLHandshakeException
    • Certificate pinning failure!
    • Trust anchor for certification path not found
    • CLEARTEXT communication not permitted(这不是证书问题,但经常混在一起)

所以,我们需要一个更稳定的思路:

  1. 先确认 App 的网络栈
  2. 动态观察请求路径
  3. 对准证书校验位置做最小化 hook
  4. 验证抓包链路是否恢复

前置知识

建议你至少了解这些概念:

  • Android 代理与用户证书
  • TLS 握手基础
  • Java 层与 Native 层 hook 的区别
  • OkHttp 常见类:
    • OkHttpClient
    • Request
    • Interceptor
    • CertificatePinner

如果你对 Frida 还不熟,也没关系,本文会尽量按“实战路径”讲,而不是一上来堆 API。


环境准备

测试环境

建议准备:

  • 一台 Android 真机或模拟器
  • 已安装目标 App
  • 电脑上安装:
    • adb
    • frida-tools
    • Burp SuiteCharles
  • 手机端:
    • 安装 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 服务时,抓包工具会作为一个中间人:

  1. App 发起 TLS 握手
  2. 抓包工具伪造目标站点证书
  3. App 校验这个证书
  4. 如果 App 信任抓包工具的 CA,则连接继续
  5. 否则握手失败

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

实战代码(可运行)

下面的代码分成三步:

  1. 判断 App 是否使用 OkHttp
  2. 观察请求信息
  3. 绕过常见证书校验

第一步:识别 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 了还是不行,才去看:

  • libssl
  • cronet
  • conscrypt
  • 自研 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

排查方法:

  1. 先枚举类名
  2. 观察报错位置
  3. 从请求构建点向连接建立点逐步逼近

坑 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:只处理 pinning
  • universal_ssl_bypass.js:最后兜底

这样你不会因为一个“大而全脚本”把问题复杂化。

3. 日志别打太猛

Frida 打日志过多会影响性能,尤其是:

  • 高频请求
  • 大量 Header / Body
  • UI 线程调用

建议:

  • 先只打印 URL、Method
  • 确认路径后再加详细字段
  • 避免对每个字节流都做 console 输出

4. 对生产环境保持克制

哪怕是企业内授权测试,也建议:

  • 使用测试账号
  • 不抓取无关用户数据
  • 对敏感数据做脱敏保存
  • 测试后撤销证书、代理、脚本

5. 遇到 Native 再升级手段

如果 Java 层已经确认无效,再考虑:

  • Hook libssl
  • Hook SSL_read / SSL_write
  • 观察 JNI_OnLoad
  • 配合 objectionr2fridajadx

不要一开始就把难度拉到最高,那样很容易陷在细节里。


一套推荐的实战路径

如果你现在要真的上手,我建议按下面节奏走:

  1. 配好代理和证书
  2. 浏览器验证抓包链路
  3. Frida 枚举类,确认是否使用 OkHttp
  4. Hook Request/RealCall,确认请求确实发起
  5. 尝试只绕过 CertificatePinner
  6. 如果还失败,再上 TrustManager
  7. 若主机名错误,再补 HostnameVerifier
  8. 仍不行,再检查代理检测、WebView、Cronet、Native

这条路径最大的好处是:
你每一步都知道自己在验证什么,不会陷入“脚本贴了一堆,但不知道哪行起作用”的混乱状态。


总结

这篇文章的核心不是“背几个通用 Frida 脚本”,而是建立一条清晰的 HTTPS 逆向分析链路:

  • 先确认网络栈
  • 再观察请求行为
  • 最后针对证书校验点做绕过

针对 OkHttp,最常见也最值得优先尝试的点是:

  • okhttp3.CertificatePinner.check(...)
  • SSLContext.init(...)
  • HostnameVerifier.verify(...)

如果你只想先拿到结果,可以先跑组合脚本;
但如果你想真正提升实战能力,我更建议你按“观察 → 定位 → 最小化绕过”的方式来做。

最后给几个可执行建议:

  • 能最小 hook,就别全局信任
  • 先看请求有没有发,再看为什么握手失败
  • Java 层不通,再考虑 Native
  • 排查时保留日志,但别过量
  • 始终限定在授权测试边界内

只要把这套方法论走顺,后面无论是 OkHttp、WebView 还是更复杂的网络栈,你都会更有把握。


分享到:

上一篇
《Spring Boot 中基于 Redis 与 Caffeine 的多级缓存实战:一致性、穿透防护与性能优化》
下一篇
《微服务架构中基于服务网格的灰度发布与流量治理实战》