当前位置:首页>学习笔记>Frida学习笔记(八):SSL Pinning 绕过全攻略

Frida学习笔记(八):SSL Pinning 绕过全攻略

  • 2026-05-14 12:17:34
Frida学习笔记(八):SSL Pinning 绕过全攻略

本篇目标:彻底解决「App 抓不到包」的问题。不只是给你一个能用的绕过脚本——而是让你理解 HTTPS 的信任模型、SSL Pinning 的每一种实现方式、以及为什么特定的绕过方案能对应特定的实现方式。这样当你遇到脚本不管用的情况时,你知道该往哪个方向调整。

一、抓包的基本原理:为什么需要绕过

在进入 SSL Pinning 绕过之前,你需要先理解一个更基础的问题:HTTPS 抓包是怎么工作的,以及为什么它会被 SSL Pinning 阻止。

1.1 HTTPS 抓包的本质:中间人攻击

当你使用 Charles 或 mitmproxy 抓取 HTTPS 流量时,你实际上是在执行一次合法的中间人攻击(Man-in-the-Middle, MITM)。整个过程是这样的:

你在手机的 Wi-Fi 设置中配置了代理,所有 HTTP/HTTPS 流量都被路由到你电脑上运行的代理工具。当 App 向服务器发起 HTTPS 连接时,代理工具拦截了这个连接,并做了两件事。

第一件事是和 App 之间建立一个「假的」HTTPS 连接。代理工具临时生成一张针对目标域名的 SSL 证书(比如 api.example.com 的证书),用自己的 CA 根证书签名,然后把这张假证书发给 App。App 收到证书后会检查它是否可信——如果你已经把代理工具的 CA 根证书安装到手机的信任存储中,App 就会认为这张假证书是可信的,从而与代理建立加密连接。

第二件事是代理工具再以「真实客户端」的身份,与真正的服务器建立一个正常的 HTTPS 连接。

这样,代理工具就坐在 App 和服务器之间,两头都持有解密密钥:一头用假证书和 App 通信,一头用真证书和服务器通信。所有经过代理的流量在代理这里都是明文的——你就能看到完整的请求和响应了。

HTTPS 抓包原理:中间人代理

1.2 SSL Pinning 的防御逻辑

上面整个方案能成功的关键前提是:App 会信任你安装的代理 CA 根证书。SSL Pinning(SSL 证书锁定)就是为了打破这个前提而存在的。

正常情况下,App 通过操作系统的「受信任的根证书列表」来判断一张证书是否可信。只要证书的签名链能追溯到列表中的某个根证书,就认为可信。当你把代理的 CA 证书安装到这个列表后,代理签发的所有假证书都会被信任。

SSL Pinning 的做法是:App 不再依赖操作系统的信任列表,而是在代码中硬编码了它信任的证书或公钥的指纹(hash)。在 TLS 握手阶段,App 收到服务器(或代理)发来的证书后,不只是检查它是否被系统信任的 CA 签发,还要额外检查这张证书的指纹是否与硬编码的指纹匹配。代理签发的假证书指纹自然和真实服务器的不同,校验失败,连接被拒绝。

这就是为什么你安装了 Charles 的 CA 证书,很多 App 仍然抓不到包——不是因为证书安装有问题,而是 App 根本不看你安装的证书,它只认自己硬编码的那一个。

Pin 的两种形式

  • Certificate Pinning(证书锁定):锁定整张证书的指纹。优点是精确;缺点是证书到期后必须更新 App
  • Public Key Pinning(公钥锁定):只锁定证书中公钥的指纹。证书续期时只要公钥不变,Pin 就不需要更新。OkHttp 的 CertificatePinner 实际上锁定的是公钥(尽管名字叫 CertificatePinner)

1.3 Android 7.0+ 的额外变化

Android 7.0(Nougat, API 24)引入了一个重要的安全改进:App 默认不再信任用户安装的 CA 证书。在 Android 7.0 之前,用户安装到「受信任的凭据 → 用户」中的 CA 证书对所有 App 都有效。从 Android 7.0 开始,App 默认只信任系统预装的 CA 证书(存储在 /system/etc/security/cacerts/ 中),除非 App 在 network_security_config.xml 中明确声明信任用户证书。

<!-- AndroidManifest.xml 中引用 -->
<application android:networkSecurityConfig="@xml/network_security_config">

<!-- res/xml/network_security_config.xml -->
<!-- 只有这样配置,App 才会信任用户安装的 CA 证书 -->
<network-security-config>
    <base-config>
        <trust-anchors>
            <!-- 信任系统 CA(默认行为)-->
            <certificates src="system"/>
            <!-- 额外信任用户安装的 CA(需要显式声明)-->
            <certificates src="user"/>
        </trust-anchors>
    </base-config>
</network-security-config>

这意味着即使目标 App 没有实现 SSL Pinning,在 Android 7.0+ 的设备上你也可能抓不到包——因为代理的 CA 证书安装在「用户」分区而不是「系统」分区。

解决这个基础问题有三种方式(选其一):

第一种,将代理 CA 证书安装为系统 CA(需要 Root)。具体做法是将证书文件复制到 /system/etc/security/cacerts/ 目录下,文件名需要用特定的 hash 格式命名。Magisk 有现成的模块可以做这件事——常用的有 AlwaysTrustUserCertsMoveCertificate等,挑一个活跃维护的即可。

Android 14+ 注意:从 Android 14(API 34)开始,/system 分区强制只读,即使 adb remount 也无法写入。新设备上推荐用 Magisk/Zygisk 模块自动把用户证书"提升"为系统证书,而不要再尝试手动 mount -o rw,remount。下面命令行版本(手动 mount + cp)仅适用于 Android 13 及以下,作为原理演示保留。

# 将代理 CA 证书安装为系统证书的步骤(需要 Root)
# 1. 获取证书的 hash 值
# - PEM 格式(.pem / .crt 文本):默认就是 PEM,无需 -inform
# - DER 格式(.der / .cer 二进制):加 -inform DER
openssl x509 -subject_hash_old -in charles-ca.pem -noout
# 输出例如: 9a5ba575

# 2. 重命名证书文件(hash + .0)
cp charles-ca.pem 9a5ba575.0

# 3. 推送到系统分区(Magisk 环境下)
adb push 9a5ba575.0 /data/local/tmp/
adb shell su -c "mount -o rw,remount /system"
adb shell su -c "cp /data/local/tmp/9a5ba575.0 /system/etc/security/cacerts/"
adb shell su -c "chmod 644 /system/etc/security/cacerts/9a5ba575.0"
adb reboot

第二种,使用 Frida Hook 绕过证书校验(本篇的主题)。这种方式不需要修改系统分区,更加灵活。

第三种,修改 APK 的 network_security_config.xml 添加对用户 CA 的信任,然后重打包安装(但这会改变签名,可能触发签名校验失败)。

推荐组合:在实际工作中,建议「系统 CA + Frida 绕过」双管齐下。系统 CA 解决 Android 7.0+ 的基础信任问题,Frida 绕过解决 App 自身的 SSL Pinning。两者配合,覆盖率最高。

二、先试两条命令:80% 的 App 就够了

在开始分析具体实现之前,先试一下社区维护的「通杀方案」。这两个工具内部已经覆盖了下面要讲的第 1-3 层中绝大多数主流实现,对常规 App 几乎不用动脑筋。

2.1 objection — 一条命令绕过

objection 是 SensePost 基于 Frida 开发的运行时分析工具,内置 SSL Pinning 一键绕过。

pip install objection

# 以 spawn 模式启动目标 App 并进入交互终端
objection -g com.example.app explore

# 在 objection 提示符下执行
android sslpinning disable

它内部依次 Hook 了 SSLContext.init / TrustManagerImpl.verifyChain / OkHttp3 CertificatePinner.check 等关键方法——本质上就是后面第四章到第六章要讲的几招的合集,只是被打包成了一句命令。

2.2 frida-multiple-unpinning — 覆盖面最广的脚本

社区脚本里覆盖面最广的是 frida-multiple-unpinning(GitHub 搜同名仓库即可,由 @httptoolkit 等多位作者持续维护)。直接挂上去:

frida -U -f com.example.app -l frida-multiple-unpinning.js

它默认覆盖:TrustManagerImpl、OkHttp 3.x/4.x、TrustKit、Conscrypt、HttpsURLConnection、NetworkSecurityConfig、Apache HTTP Client 等十几种实现。

2.3 什么时候必须往下读

通用方案不是万能。以下三类场景几乎一定失败,需要进入后续章节做"针对性 Hook":

  1. 类名被 ProGuard/R8 混淆 —— 通杀脚本按字面类名 Hook,混淆后命中不到。见第九章「对抗混淆」。
  2. SSL 校验在 Native 层 —— 通杀脚本只覆盖 Java 层。Flutter / 字节系产品 / 自带 libcurl 的应用都属于这一类。见第七章 Native SSL。
  3. App 检测到 Frida 直接闪退 —— 通杀脚本本身能加载,但 App 起来就死。见第 16-17 篇 Root 检测 / Frida 检测对抗。

理解了这层"先粗后细"的工作流,下面的五层模型不是"必读章节",而是"通用方案失败后的诊断地图"——出哪一层的事,你就翻到对应一节。

三、SSL Pinning 的实现层次:知己知彼

不同的 App 使用不同的方式实现 SSL Pinning。理解每一种实现方式的原理,是编写有效绕过脚本的基础。我把它们按照从简单到复杂排列,形成了一个五层模型。

3.1 五层模型

SSL Pinning 五层实现模型

关于"覆盖率":上面五档来自作者多年实测的主观分档,不是统计意义上的占比。不过最近几年随着 Flutter / RN 普及与 Cronet 集成增多,第 4 层占比在快速上升——读者实战时如果觉得"似乎人人都在 Native 做",那是因为你看的样本里高安全应用更多,不代表全行业。

绕过策略的核心思路是:从第 1 层开始尝试,如果不生效再逐层递进。 大多数 App 在第 1-2 层就能解决;如果一上来就走第二章的通杀脚本,则更直接。

3.2 如何判断 App 使用了哪一层

在编写绕过脚本之前,有一些快速判断的方法:

用 jadx 反编译 APK 后搜索关键字:

  • 搜到 CertificatePinner → OkHttp 证书锁定(第 2 层)
  • 搜到 X509TrustManagercheckServerTrusted → 自定义 TrustManager(第 1 层)
  • res/xml/ 下的 network_security_config.xml 包含 <pin-set> 标签 → Android 原生 Pin(第 3 层)
  • lib/ 下有自定义的 SSL 相关 SO 文件 → 可能是第 4 层
  • 搜到 KeyStore.load + KeyManagerFactory → 可能有 mTLS(第 5 层)

更高效的实战做法:直接加载一个覆盖 1-4 层的综合绕过脚本(见第十章),它会自动尝试所有层级的绕过。通过控制台日志中哪些 Hook 被触发(显示 [OK]),你可以反向推断 App 使用的是哪种实现。这比先分析再写脚本快得多。

四、第 1 层绕过:TrustManager 和 HostnameVerifier

4.1 原理:SSLContext 的信任链

Android 的 HTTPS 连接建立过程中,有一个关键类 javax.net.ssl.SSLContext。App 调用 SSLContext.init(KeyManager[], TrustManager[], SecureRandom) 来初始化 SSL 上下文。第二个参数 TrustManager[] 决定了这个 SSL 上下文如何验证服务器的证书。

正常情况下,App 传入 null 作为 TrustManager,表示使用系统默认的信任逻辑(信任系统 CA 列表中的所有证书)。当 App 想实现 SSL Pinning 时,它会创建一个自定义的 X509TrustManager,在 checkServerTrusted 方法中加入额外的校验逻辑——比如检查证书的公钥 SHA-256 hash 是否与预期值匹配。

我们的绕过思路就是:Hook SSLContext.init,将 App 传入的自定义 TrustManager 替换为一个「什么都不检查」的空实现。

为什么 Hook SSLContext.init 而不是 checkServerTrusted? 因为不同 App 的自定义 TrustManager 类名各不相同(而且很可能被混淆),逐一 Hook 不现实。而所有自定义 TrustManager 最终都要通过 SSLContext.init 注册到 SSL 上下文——这是一个统一的「咽喉」位置。

4.2 绕过代码(详细注释版)

Java.perform(function() {
    // === 步骤 1:创建一个信任所有证书的 TrustManager ===

    // Java.registerClass: Frida 的动态类创建 API
    // 在运行时创建一个新的 Java 类,实现 X509TrustManager 接口
    // 所有验证方法都是空实现 = 无条件信任任何证书

    var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
    var SSLContext = Java.use("javax.net.ssl.SSLContext");

    var BypassTrustManager = Java.registerClass({
        // 类名随意,不与已有类冲突即可
        name: "com.frida.bypass.TrustManager",
        // 声明实现 X509TrustManager 接口
        implements: [X509TrustManager],
        methods: {
            // checkClientTrusted: 验证客户端证书
            // 在普通 HTTPS(单向 TLS)中不会被调用
            // 只有 mTLS 场景才会用到
            checkClientTrusted: function(chain, authType) {
                // 空实现:不做任何检查
            },

            // checkServerTrusted: 核心方法 - 验证服务器证书
            // 原始实现会在这里检查证书指纹,不匹配就抛 CertificateException
            // 我们的空实现不抛异常 = 无条件信任
            checkServerTrusted: function(chain, authType) {
                // 空实现:无条件信任服务器证书
                // 这里什么都不做,方法正常返回 = 验证通过
            },

            // getAcceptedIssuers: 返回受信任的证书颁发者列表
            // 返回空数组表示接受任何颁发者签发的证书
            getAcceptedIssuers: function() {
                return [];
            }
        }
    });

    // === 步骤 2:Hook SSLContext.init,替换 TrustManager ===

    // SSLContext.init 的三个参数:
    // KeyManager[] - 客户端密钥管理(mTLS 用),通常为 null
    // TrustManager[] - 信任管理器(我们要替换这个)
    // SecureRandom - 随机数生成器,通常为 null

    SSLContext.init.overload(
        "[Ljavax.net.ssl.KeyManager;",     // JNI 签名:KeyManager[]
        "[Ljavax.net.ssl.TrustManager;",   // JNI 签名:TrustManager[]
        "java.security.SecureRandom"
    ).implementation = function(keyManagers, trustManagers, secureRandom) {

        console.log("[SSL] SSLContext.init 拦截");

        // 创建只包含 BypassTrustManager 的数组
        var bypassTm = BypassTrustManager.$new();
        var tmArray = Java.array("javax.net.ssl.TrustManager", [bypassTm]);

        // 用我们的 TrustManager 替换原始的,其他参数保持不变
        // 重要:保留 keyManagers 参数!如果 App 使用 mTLS,
        // keyManagers 中包含客户端证书,丢掉会导致 mTLS 握手失败
        this.init(keyManagers, tmArray, secureRandom);
    };

    // === 步骤 3:处理 HostnameVerifier ===

    // TLS 握手除了验证证书有效性,还验证证书中的域名(CN/SAN)
    // 是否与请求的域名匹配。HostnameVerifier 执行这个检查。
    // 代理签发的假证书域名通常是对的,但有些 App 会设置
    // 自定义的 HostnameVerifier 来执行更严格的检查。

    try {
        var HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");

        // 拦截设置自定义 HostnameVerifier 的调用
        HttpsURLConnection.setDefaultHostnameVerifier.implementation = function(verifier) {
            console.log("[SSL] 拦截 setDefaultHostnameVerifier(忽略自定义 verifier)");
            // 不调用原始方法 = 不设置自定义 verifier = 使用默认验证
        };

        // 拦截设置自定义 SSLSocketFactory 的调用
        HttpsURLConnection.setSSLSocketFactory.implementation = function(factory) {
            console.log("[SSL] 拦截 setSSLSocketFactory(忽略自定义 factory)");
        };
    } catch(e) {
        // HttpsURLConnection 可能没有被 App 使用(现代 App 多用 OkHttp),忽略
    }

    console.log("[*] 第 1 层 SSL 绕过已加载");
});

4.3 为什么这套方案覆盖率高

因为大多数 App 的 SSL Pinning 实现都是在 Java 层通过自定义 TrustManager 完成的——不管它是直接实现了 X509TrustManager 接口,还是通过 TrustManagerFactory 加载了自定义的 KeyStore,最终都要调用 SSLContext.init 来注册这个 TrustManager。我们 Hook 的是 SSLContext.init 这个「咽喉」位置,所有走标准 Java TLS 栈的实现都绕不过它。

局限性:这套方案对以下情况无效:

(1) App 使用 OkHttp 的 CertificatePinner,因为 OkHttp 在 TLS 握手之后才做额外的 Pin 校验;

(2) App 在 Native 层做 SSL 校验,完全不经过 Java 的 SSLContext;

(3) Flutter/React Native 等框架,它们有自己的 TLS 实现。

五、第 2 层绕过:OkHttp CertificatePinner

5.1 OkHttp 的证书锁定机制

OkHttp 是 Android 生态中最流行的 HTTP 客户端库(Retrofit 底层用它,Volley 也可以配置使用 OkHttp 后端)。OkHttp 提供了一个内置的证书锁定 API——CertificatePinner。开发者可以这样使用它:

// App 中的典型 OkHttp 证书锁定配置
// Builder 模式逐一添加域名与公钥 hash 的映射
CertificatePinner pinner = new CertificatePinner.Builder()
    // 每个 add() 绑定一个域名和对应的公钥 SHA-256 hash
    // 同一个域名可以添加多个 hash(主证书 + 备用证书)
    .add("api.example.com""sha256/AAAAAAAAAA...=")
    .add("api.example.com""sha256/BBBBBBBBBB...=")
    .build();

// 将 CertificatePinner 配置到 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
    .certificatePinner(pinner)
    .build();

每个 .add() 调用将一个域名与一个证书公钥的 SHA-256 hash(Base64 编码)关联。在 TLS 握手完成后,OkHttp 会检查服务器证书链中是否有任何一个证书的公钥 hash 与配置的值匹配。如果没有匹配的,抛出 SSLPeerUnverifiedException,连接失败。

关键时序:OkHttp 的 CertificatePinner 校验发生在 TLS 握手之后。TLS 握手本身可能已经通过了(因为我们第 1 层已经替换了 TrustManager),但 OkHttp 在握手成功后还会做一次额外的 Pin 校验。所以第 1 层绕过对 OkHttp 不够——你需要同时绕过 CertificatePinner.check

TLS 握手过程中的 Pin 校验位置与 Hook 点

5.2 绕过代码

Java.perform(function() {
    // OkHttp3 的 CertificatePinner 绕过
    try {
        var CertificatePinner = Java.use("okhttp3.CertificatePinner");

        // 主要的 check 方法:check(String hostname, List peerCertificates)
        // 如果 Pin 校验失败,会抛出 SSLPeerUnverifiedException
        // 绕过方式:替换为空方法,什么都不做 = 不会抛异常 = 校验通过
        CertificatePinner.check.overload("java.lang.String""java.util.List")
            .implementation = function(hostname, peerCertificates) {
            console.log("[SSL] OkHttp CertificatePinner.check 绕过: " + hostname);
            // 空实现:不抛异常 = 校验通过
        };

        // 兼容性处理:旧版本 OkHttp 的 check 方法可能接收 Certificate[] 而非 List
        try {
            CertificatePinner.check.overload(
                "java.lang.String""[Ljava.security.cert.Certificate;"
            ).implementation = function(hostname, certs) {
                console.log("[SSL] OkHttp CertificatePinner.check(Certificate[]) 绕过: " + hostname);
            };
        } catch(e) {
            // 当前 OkHttp 版本没有这个重载,忽略
        }

        // Kotlin 版本的 OkHttp(4.x+)的内部方法名带有 $okhttp 后缀
        // 这是 Kotlin 编译器生成的 internal 方法的命名规则
        // 第二个参数是 Kotlin Function0<List<Certificate>>(惰性求值的证书列表)
        // 必须显式声明 overload 签名,避免多重载冲突时 Hook 失败
        try {
            CertificatePinner.check$okhttp.overload(
                "java.lang.String",
                "kotlin.jvm.functions.Function0"
            ).implementation = function(hostname, fn) {
                console.log("[SSL] OkHttp check$okhttp 绕过: " + hostname);
            };
        } catch(e) {}

        console.log("[*] OkHttp CertificatePinner 绕过成功");
    } catch(e) {
        // App 没有使用 OkHttp,ClassNotFoundException
        console.log("[*] OkHttp CertificatePinner 未检测到(App 可能不使用 OkHttp)");
    }
});

5.3 为什么需要 try-catch 包裹

整个 OkHttp 绕过代码被 try-catch 包裹。这是因为不是所有 App 都使用 OkHttp——如果 App 使用的是 HttpURLConnection、Volley(不带 OkHttp 后端)、或其他 HTTP 库,Java.use("okhttp3.CertificatePinner") 会抛出 ClassNotFoundException。用 try-catch 捕获这个异常,让脚本跳过 OkHttp 相关的 Hook,继续处理其他层级。

8六、第 3 层绕过:Android 平台特有的机制

6.1 Android 的 TrustManagerImpl

Android 操作系统内部有一个 TrustManagerImpl 类(全名 com.android.org.conscrypt.TrustManagerImpl),它是系统默认的 TrustManager 实现,基于 Google 的 Conscrypt(BoringSSL 的 Java 包装)库。即使 App 没有自定义 TrustManager,系统的 TrustManagerImpl 也会被使用。部分安全框架直接 Hook 或扩展这个系统类来实现 Pinning。

绕过方法是直接 Hook TrustManagerImpl 的关键验证方法:

try {
    // Conscrypt 是 Android 的默认 TLS 提供者
    var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");

    // verifyChain: 执行完整证书链验证的核心方法
    // 参数说明:
    // untrustedChain - App 从服务器收到的原始证书链(未验证)
    // trustAnchorChain - 信任锚点证书链
    // host - 请求的主机名
    // clientAuth - 是否是客户端认证
    // ocspData - OCSP (在线证书状态协议) 响应数据
    // tlsSctData - Certificate Transparency SCT 数据
    //
    // 返回值:经过验证的证书链。验证失败则抛 CertificateException。
    // 绕过:直接返回传入的未验证证书链,跳过所有验证逻辑。
    TrustManagerImpl.verifyChain.implementation = function(untrustedChain,
        trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
        console.log("[SSL] TrustManagerImpl.verifyChain 绕过: " + host);
        return untrustedChain;  // 直接返回原始链 = 视为验证通过
    };

    console.log("[*] TrustManagerImpl 绕过成功");
catch(e) {
    // 某些定制 ROM 或旧版 Android 可能没有 Conscrypt
    console.log("[*] TrustManagerImpl 未找到(可能不是 Conscrypt 实现)");
}

verifyChain 的参数变化:不同 Android 版本的 verifyChain 方法签名可能不同(参数数量可能是 4 个、5 个或 6 个)。如果 Hook 时报 overload 错误,可以通过 TrustManagerImpl.verifyChain.overloads 查看所有版本,然后逐一 Hook。

Android 11+ Hidden API 限制com.android.org.conscrypt.* 是隐藏 API,从 Android 11(API 30)开始对第三方进程加了灰名单限制——Java.use 不一定报错,但 Hook 可能静默失败。如果观察不到日志输出,启动 frida-server 时加上 --enable-hidden-api(v14+ 支持),或在脚本里先调用 setHiddenApiExemptions("L");新版 Frida(≥16)默认已经处理这件事。

6.2 NetworkSecurityConfig(Android 原生 Pin)

NSC 从 XML 配置到运行时校验的两层分发链路

Android 7.0(API 24)起,Google 推荐通过 res/xml/network_security_config.xml 声明式地配置 Pinning——只要在 XML 里写 <pin-set>,框架就会在 TLS 握手后自动校验。这条路径的实际执行者是 android.security.net.config.NetworkSecurityTrustManager(域名级)与 android.security.net.config.RootTrustManager(顶层分发器),它们独立于第四章讲的 SSLContext.init 链路——也就是说,第 1 层 Hook 对它们无效,必须单独处理。

Java.perform(function() {
    // NetworkSecurityTrustManager:实际执行 pin 校验
    try {
        var NSTM = Java.use("android.security.net.config.NetworkSecurityTrustManager");

        // 两参数版本(标准 X509TrustManager 接口)
        NSTM.checkServerTrusted.overload(
            "[Ljava.security.cert.X509Certificate;""java.lang.String"
        ).implementation = function(certs, authType) {
            console.log("[SSL] NetworkSecurityTrustManager.checkServerTrusted 绕过");
            // void 方法:直接 return = 校验通过
        };

        // 三参数版本(X509ExtendedTrustManager 扩展接口,带 hostname)
        // 返回值是"经过验证的证书链",给空 ArrayList 表示通过
        NSTM.checkServerTrusted.overload(
            "[Ljava.security.cert.X509Certificate;""java.lang.String""java.lang.String"
        ).implementation = function(certs, authType, host) {
            console.log("[SSL] NetworkSecurityTrustManager.checkServerTrusted(host) 绕过: " + host);
            return Java.use("java.util.ArrayList").$new();
        };
    } catch(e) {}

    // RootTrustManager:根据域名匹配把请求分派到具体的 NetworkSecurityTrustManager
    // 即使 NSTM 被 Hook,调用方也可能直接走 RootTrustManager.checkServerTrusted,
    // 所以两层都要拦
    try {
        var RTM = Java.use("android.security.net.config.RootTrustManager");
        RTM.checkServerTrusted.overload(
            "[Ljava.security.cert.X509Certificate;""java.lang.String"
        ).implementation = function(certs, authType) {
            console.log("[SSL] RootTrustManager.checkServerTrusted 绕过");
        };
    } catch(e) {}
});

识别方式:反编译 APK 后查看 res/xml/network_security_config.xml,若包含 <pin-set> 节点即说明走的是这条路径。也可以直接搜 AndroidManifest.xml 中的 android:networkSecurityConfig 属性。

替代方案(非 Frida):直接改 network_security_config.xml,删掉 <pin-set> 节点后重打包签名——但会触发签名校验失败(如果 App 自带签名校验,见第 25 篇)。Frida Hook 这种方式更干净。

6.3 嵌入式 KeyStore:自带证书的 TrustManagerFactory

三种 TrustManager 构造路径对比

部分 App(金融类、政企类居多)会把"自己信任的服务器证书"直接打包进 APK,路径通常是 assets/server.bksres/raw/cert.p12assets/cacert.pem 等。运行时通过这样的代码加载:

// App 中的典型代码
KeyStore ks = KeyStore.getInstance("BKS");
ks.load(getAssets().open("server.bks"), "password".toCharArray());

TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(ks);   // 关键:用自定义 KeyStore 构造 TrustManager

SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);

这里产生的 TrustManager 只信任 KeyStore 里那张证书——和第四章讲的「自定义 X509TrustManager」是两种不同实现。第 1 层 Hook SSLContext.init 替换 TrustManager 也能搞定它,但更精准的入口是 TrustManagerFactory.init

Java.perform(function() {
    var TMF = Java.use("javax.net.ssl.TrustManagerFactory");

    // 把传入的自定义 KeyStore 替换为 null
    // null = 使用系统默认信任存储(包含已安装的代理 CA)
    TMF.init.overload("java.security.KeyStore").implementation = function(keyStore) {
        console.log("[SSL] TrustManagerFactory.init(KeyStore) 绕过 — 改用系统默认");
        this.init(null);
    };
});

识别方式:反编译后在 assets/ / res/raw/ 下找 .bks / .p12 / .pfx / .cer / .pem 文件,再 grep TrustManagerFactoryKeyStore.load

和第 1 层 Hook 的关系:本节的 Hook 与第四章的 SSLContext.init 替换是叠加关系——综合脚本(第十章)会两个都装,互不冲突。

6.4 WebView 的 SSL 错误处理

很多 App 包含 WebView 组件来展示网页内容。WebView 有自己独立的 SSL 错误处理机制——当遇到不受信任的证书时,会调用 WebViewClient.onReceivedSslError 回调。默认行为是调用 handler.cancel() 取消连接。

try {
    var WebViewClient = Java.use("android.webkit.WebViewClient");

    // onReceivedSslError: WebView 遇到 SSL 错误时的回调
    // 参数:view - WebView 实例, handler - SslErrorHandler, error - SslError
    // 默认行为是 handler.cancel()(拒绝加载)
    // 绕过:调用 handler.proceed()(继续加载,忽略 SSL 错误)
    WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
        console.log("[SSL] WebView SSL 错误绕过(错误: " + error.toString() + ")");
        // proceed() 指示 WebView 忽略 SSL 错误,继续加载页面
        handler.proceed();
    };

    console.log("[*] WebView SSL 绕过成功");
catch(e) {}

安全提示onReceivedSslError 的绕过只在 WebView 内生效。如果 App 的主要网络请求不通过 WebView 发送(大多数 App 都是这样),这个 Hook 只解决 WebView 内嵌网页的证书问题。

6.5 Cronet (Chromium 网络栈) 的处理

Google 的部分 App(如 YouTube、Chrome)和一些第三方 App 使用 Cronet 作为网络库。Cronet 是 Chromium 网络栈的独立封装,它有自己的证书校验逻辑,不走 Java 标准的 TrustManager。

try {
    var CronetEngineBuilder = Java.use("org.chromium.net.CronetEngine$Builder");

    // enablePublicKeyPinningBypassForLocalTrustAnchors:
    // 当设为 true 时,对于本地信任的 CA(用户安装的证书)不做 Pin 校验
    // 这正是我们需要的行为
    CronetEngineBuilder.enablePublicKeyPinningBypassForLocalTrustAnchors
        .implementation = function(enable) {
        console.log("[SSL] Cronet: 绕过本地信任锚点的 Pin 校验");
        // 强制传入 true,绕过 Pin 校验
        return this.enablePublicKeyPinningBypassForLocalTrustAnchors(true);
    };

    console.log("[*] Cronet 绕过成功");
catch(e) {}

七、第 4 层绕过:Native SSL(BoringSSL / OpenSSL)

7.1 什么时候会遇到 Native 层 Pinning

当你加载了覆盖 1-3 层的绕过脚本,启动 App 后发现控制台没有任何 SSL 相关的日志输出,但 App 确实在联网(你可以通过 adb shell tcpdump -i any -c 10 port 443 确认有 TCP 流量到 443 端口),这说明 SSL 校验不在 Java 层,而在 Native 层。

Android 底层的 TLS 实现使用 BoringSSL(Google 维护的 OpenSSL 分支)。它的代码编译在 libssl.solibcrypto.so 这两个系统库中。以下场景会遇到 Native 层 Pinning:

  • Flutter App:Dart 的 HTTP 库直接调用 BoringSSL,完全不经过 Java 层
  • React Native App:某些配置下使用 Native HTTP 实现
  • 自带 Native HTTP 库的 App:如使用 libcurl 编译的 SO
  • 游戏类 App:Unity/Unreal 引擎的网络模块通常在 Native 层

7.2 关键函数和绕过原理

BoringSSL 证书验证调用链

BoringSSL/OpenSSL 中与证书验证相关的关键函数(按"被命中的概率"排序):

函数作用绕过方式备注
SSL_CTX_set_custom_verifyBoringSSL:设自定义验证回调替换回调为返回 0ssl_verify_okBoringSSL 专有
SSL_CTX_set_verify / SSL_set_verify设置验证模式(连接级 / 上下文级)mode 强制设 0SSL_VERIFY_NONEOpenSSL/BoringSSL 通用
SSL_CTX_set_cert_verify_callback另一套自定义验证入口替换回调为返回 1(通过)OpenSSL/BoringSSL 通用
SSL_get_verify_result读取验证结果返回值强制改 0X509_V_OK兜底,简单粗暴
X509_verify_cert底层 X.509 链验证返回值强制改 1(成功)OpenSSL 历史 API,BoringSSL 也保留
ssl_verify_peer_certBoringSSL 内部入口(多为非导出)返回值强制改 0需要遍历符号表查找

返回值差异提醒SSL_CTX_set_custom_verify 的回调返回 ssl_verify_result_t 枚举(0 = ok, 1 = invalid, 2 = retry);SSL_CTX_set_cert_verify_callback 的回调返回布尔语义 int1 = 成功, 0 = 失败);X509_verify_cert 也是 1 = 成功。一个返回 0 表示通过、一个返回 1 表示通过——抄代码时务必区分清楚,搞反了不会有任何报错但 Hook 完全无效。

7.3 系统 libssl.so 绕过代码(详细注释版)

// native_ssl_bypass.js
// 绕过 Native 层(BoringSSL/OpenSSL)的 SSL 证书校验
// 注意:这段代码不需要 Java.perform 包裹 - 它操作的是 Native 层

(function() {
    console.log("[*] 开始加载 Native SSL 绕过...");
    var hookCount = 0;

    // === Hook 1: SSL_CTX_set_custom_verify ===
    // C 函数签名:
    // void SSL_CTX_set_custom_verify(
    // SSL_CTX *ctx, // SSL 上下文
    // int mode, // 验证模式
    // enum ssl_verify_result_t (*callback)(SSL*, uint8_t*) // 回调
    // );
    // 绕过:将验证回调替换为始终返回 ssl_verify_ok (0) 的函数

    var fn1 = Module.findExportByName("libssl.so""SSL_CTX_set_custom_verify");
    if (fn1) {
        Interceptor.attach(fn1, {
            onEnter: function(args) {
                // args[0] = SSL_CTX* (保持不变)
                // args[1] = mode (保持不变)
                // args[2] = callback (替换为我们的空回调)

                // NativeCallback: Frida API,创建一个 C 函数
                // 签名: int callback(pointer ssl, pointer out_alert)
                // 返回 0 = ssl_verify_ok
                var alwaysOk = new NativeCallback(function(ssl, out_alert) {
                    return 0// ssl_verify_ok
                }, "int", ["pointer""pointer"]);

                args[2= alwaysOk;  // 替换原始回调
                hookCount++;
            }
        });
        console.log("[Native SSL] SSL_CTX_set_custom_verify -> hooked");
    }

    // === Hook 2: SSL_set_verify ===
    // C 函数签名:
    // void SSL_set_verify(
    // SSL *ssl, // SSL 连接对象
    // int mode, // SSL_VERIFY_NONE=0, SSL_VERIFY_PEER=1
    // int (*callback)(int ok, X509_STORE_CTX *ctx) // 回调
    // );
    // 绕过:将 mode 强制设为 SSL_VERIFY_NONE (0),清空回调

    var fn2 = Module.findExportByName("libssl.so""SSL_set_verify");
    if (fn2) {
        Interceptor.attach(fn2, {
            onEnter: function(args) {
                // 强制验证模式为 SSL_VERIFY_NONE
                args[1= ptr(0);  // mode = 0 (不验证)
                args[2= ptr(0);  // callback = null (无回调)
                hookCount++;
            }
        });
        console.log("[Native SSL] SSL_set_verify -> hooked");
    }

    // === Hook 3: X509_verify_cert(终极兜底)===
    // C 函数签名:
    // int X509_verify_cert(X509_STORE_CTX *ctx);
    // 返回值: 1 = 验证成功, 0 或负数 = 验证失败
    // 绕过:如果原始返回值不是 1,强制改为 1

    ["libssl.so""libcrypto.so"].forEach(function(lib) {
        var fn = Module.findExportByName(lib, "X509_verify_cert");
        if (fn) {
            Interceptor.attach(fn, {
                onLeave: function(retval) {
                    if (retval.toInt32() !== 1) {
                        retval.replace(ptr(1));  // 强制返回 1 (成功)
                    }
                }
            });
            console.log("[Native SSL] X509_verify_cert in " + lib + " -> hooked");
        }
    });

    // === Hook 4: SSL_CTX_set_cert_verify_callback ===
    // C 函数签名(OpenSSL/BoringSSL 通用):
    // void SSL_CTX_set_cert_verify_callback(
    // SSL_CTX *ctx,
    // int (*cb)(X509_STORE_CTX *, void *), // 注意:返回 int,1=成功,0=失败
    // void *arg
    // );
    // 这是 set_custom_verify 之外的另一套回调机制,
    // 同时 Hook 两者才能覆盖所有 NDK 应用写法。

    var fn4 = Module.findExportByName("libssl.so""SSL_CTX_set_cert_verify_callback");
    if (fn4) {
        Interceptor.attach(fn4, {
            onEnter: function(args) {
                // 替换为始终返回 1(成功)的回调
                var alwaysPass = new NativeCallback(function(storeCtx, arg) {
                    return 1;
                }, "int", ["pointer""pointer"]);
                args[1= alwaysPass;
                hookCount++;
            }
        });
        console.log("[Native SSL] SSL_CTX_set_cert_verify_callback -> hooked");
    }

    // === Hook 5: SSL_get_verify_result(兜底)===
    // C 函数签名:
    // long SSL_get_verify_result(const SSL *ssl);
    // 返回值: 0 = X509_V_OK(验证通过),其它值都是失败原因码
    // 绕过:直接把返回值改成 0
    // 注意:这只影响"读取结果"的代码路径——如果 App 在底层校验时
    // 已经 abort 了连接,光改这里没用。和前面 1-4 配合使用。

    var fn5 = Module.findExportByName("libssl.so""SSL_get_verify_result");
    if (fn5) {
        Interceptor.attach(fn5, {
            onLeave: function(retval) {
                if (!retval.equals(ptr(0))) {
                    retval.replace(ptr(0));  // X509_V_OK = 0
                }
            }
        });
        console.log("[Native SSL] SSL_get_verify_result -> hooked");
    }

    // === Hook 6: X509_verify_cert(终极兜底)===
    // C 函数签名:
    // int X509_verify_cert(X509_STORE_CTX *ctx);
    // 返回值: 1 = 验证成功, 0 或负数 = 验证失败
    // 绕过:如果原始返回值不是 1,强制改为 1

    ["libssl.so""libcrypto.so"].forEach(function(lib) {
        var fn = Module.findExportByName(lib, "X509_verify_cert");
        if (fn) {
            Interceptor.attach(fn, {
                onLeave: function(retval) {
                    if (retval.toInt32() !== 1) {
                        retval.replace(ptr(1));  // 强制返回 1 (成功)
                    }
                }
            });
            console.log("[Native SSL] X509_verify_cert in " + lib + " -> hooked");
        }
    });

    // === Hook 7: BoringSSL 内部的 ssl_verify_peer_cert ===
    // 这个函数不是公开 API,但它是 BoringSSL 证书验证的核心入口
    // 在某些 Android 版本上,Hook 前面几个函数可能不够
    // 需要通过符号表搜索来定位

    try {
        var sslModule = Process.findModuleByName("libssl.so");
        if (sslModule) {
            // enumerateSymbols(): 遍历 SO 文件的符号表
            // 查找包含 "ssl_verify_peer_cert" 的符号
            sslModule.enumerateSymbols().forEach(function(sym) {
                if (sym.name.indexOf("ssl_verify_peer_cert"!== -1) {
                    Interceptor.attach(sym.address, {
                        onLeave: function(retval) {
                            // BoringSSL 中 ssl_verify_ok = 0
                            if (retval.toInt32() !== 0) {
                                retval.replace(ptr(0));
                            }
                        }
                    });
                    console.log("[Native SSL] ssl_verify_peer_cert -> hooked");
                }
            });
        }
    } catch(e) {}

    console.log("[*] Native SSL 绕过加载完成(" + hookCount + " 个 Hook 点)");
})();

7.4 独立打包的 libboringssl.so

部分 App(尤其是 Chromium 内核的 WebView 和高安全应用)会把 BoringSSL 打成独立的 libboringssl.so 与 APK 一起发布,而不是链接系统 libssl.so。系统库的 Hook 对它完全无效——必须把上一节的 Hook 复制一份指向这个独立库:

// 把第 7.3 节的 7 个 Hook 全部复制一份,把 "libssl.so" / "libcrypto.so" 改成
// "libboringssl.so"。这里给一段简化模板:
["libboringssl.so"].forEach(function(lib) {
    var mod = Process.findModuleByName(lib);
    if (!mod) return;
    console.log("[+] " + lib + " base=" + mod.base);

    // set_custom_verify
    var addr = Module.findExportByName(lib, "SSL_CTX_set_custom_verify");
    if (addr) Interceptor.attach(addr, {
        onEnter: function(args) {
            args[2= new NativeCallback(function() { return 0; },
                "int", ["pointer""pointer"]);
        }
    });

    // SSL_set_verify / SSL_CTX_set_verify
    ["SSL_CTX_set_verify""SSL_set_verify"].forEach(function(fn) {
        var a = Module.findExportByName(lib, fn);
        if (a) Interceptor.attach(a, {
            onEnter: function(args) { args[1= ptr(0); args[2= ptr(0); }
        });
    });

    // SSL_get_verify_result 兜底
    var gv = Module.findExportByName(lib, "SSL_get_verify_result");
    if (gv) Interceptor.attach(gv, {
        onLeave: function(retval) { retval.replace(ptr(0)); }
    });
});

第十章的综合脚本会自动遍历 libssl.solibboringssl.solibttboringssl.so 三个候选名,不用手抄。

7.5 字节跳动系:libttboringssl.so 专项

libttboringssl.so 是字节跳动基于 BoringSSL 深度定制的 TLS 库——抖音、TikTok、今日头条、西瓜视频、飞书 等旗下产品几乎全用它。相比标准 BoringSSL 有三个差异点必须照顾:

  1. 导出符号精简 —— 内部验证函数多为非导出,Module.findExportByName 经常返回 null
  2. 额外 Pin 逻辑 —— 在 TLS 握手多个阶段注入自定义校验,光把 SSL_VERIFY_NONE 设上去不够。
  3. 延迟加载 —— 库不在 App 启动时就 dlopen,spawn 模式下脚本立刻去找 Process.findModuleByName("libttboringssl.so") 会返回 null

下面这张时序图展示了 dlopen 监听如何解决延迟加载问题,配合后面的代码理解:

libttboringssl 延迟加载场景下的 6 步 Hook 安装时序

下面是覆盖这三个差异的完整方案:

// === Step 1:枚举导出符号(命令行先跑一次,看哪些能 Hook)===
// adb shell rabin2 -E /data/app/<pkg>/lib/arm64/libttboringssl.so | grep -i verify

// === Step 2:在 JS 里写 Hook 入口 ===
function hookTTBoringSSL() {
    var lib = "libttboringssl.so";
    var mod = Process.findModuleByName(lib);
    if (!mod) { console.log("[-] " + lib + " 未加载"); return; }
    console.log("[+] " + lib + " base=" + mod.base + " size=" + mod.size);

    // 优先 Hook 导出的标准接口
    var targets = {
        "SSL_CTX_set_custom_verify": function(args) {
            args[2= new NativeCallback(function(ssl, alert) { return 0; },
                "int", ["pointer""pointer"]);
            console.log("[+] ttBSSL set_custom_verify -> ssl_verify_ok");
        },
        "SSL_CTX_set_cert_verify_callback": function(args) {
            args[1= new NativeCallback(function(ctx, arg) { return 1; },
                "int", ["pointer""pointer"]);
            console.log("[+] ttBSSL cert_verify_callback -> 1");
        },
        "SSL_CTX_set_verify": function(args) {
            args[1= ptr(0); args[2= ptr(0);
        },
        "SSL_set_verify": function(args) {
            args[1= ptr(0); args[2= ptr(0);
        }
    };

    Object.keys(targets).forEach(function(name) {
        var addr = Module.findExportByName(lib, name);
        if (addr) {
            Interceptor.attach(addr, { onEnter: targets[name] });
            console.log("[+] hooked " + name);
        } else {
            console.log("[-] " + name + " 未导出");
        }
    });

    // 兜底:SSL_get_verify_result
    var gv = Module.findExportByName(lib, "SSL_get_verify_result");
    if (gv) Interceptor.attach(gv, {
        onLeave: function(rv) { rv.replace(ptr(0)); }
    });
}

// === Step 3:dlopen 监听 — 处理延迟加载 ===
if (Process.findModuleByName("libttboringssl.so")) {
    hookTTBoringSSL();   // 已加载,立即上 Hook
else {
    // 没加载就监听 dlopen / android_dlopen_ext
    var dlopenPtr = Module.findExportByName(null"android_dlopen_ext")
                  || Module.findExportByName(null"dlopen");
    Interceptor.attach(dlopenPtr, {
        onEnter: function(args) { this.libPath = args[0].readCString(); },
        onLeave: function() {
            if (this.libPath && this.libPath.indexOf("ttboringssl"!== -1) {
                console.log("[+] libttboringssl.so 刚加载,安装 Hook");
                hookTTBoringSSL();
            }
        }
    });
}

当导出符号都失效时:意味着字节又升级了。这时只能借助字符串特征定位偏移——在 IDA/Ghidra 里搜 "CERTIFICATE_VERIFY_FAILED" / "ssl_verify_peer_cert" 字符串,做 X-Ref 找到调用它的函数,把那个函数偏移记下来,然后:

var mod = Process.findModuleByName("libttboringssl.so");
Interceptor.attach(mod.base.add(0x3A5678), {  // 偏移按实际逆向结果替换
    onLeave: function(retval) { retval.replace(ptr(0)); }
});

7.6 libcurl:通过选项设置绕过

少数 NDK 应用直接用 libcurl 发请求。它的证书校验是通过 curl_easy_setopt 设置选项控制的——把两个选项强制设 0 即可:

// CURLOPT_SSL_VERIFYPEER = 64  服务器证书校验开关(默认 1 开启)
// CURLOPT_SSL_VERIFYHOST = 81 主机名校验等级(默认 2 严格,0 关闭)
var setopt = Module.findExportByName("libcurl.so""curl_easy_setopt");
if (setopt) {
    Interceptor.attach(setopt, {
        onEnter: function(args) {
            var opt = args[1].toInt32();
            if (opt === 64 || opt === 81) {
                console.log("[curl] setopt " + opt + " -> 0 (disabled)");
                args[2= ptr(0);
            }
        }
    });
}

7.7 Flutter App 的特殊处理

Flutter 使用 Dart 的 dart:io HTTP 库,它内部使用 BoringSSL 但通过 libflutter.so 而非系统的 libssl.so,且符号通常被 strip——只能靠特征字符串定位。社区维护的 reFlutter 和各种 flutter-ssl-bypass 脚本覆盖了常见 Flutter 版本,无需自己写。

// 只有少数 Flutter 版本保留了符号,可以试试:
var fn = Module.findExportByName("libflutter.so",
    "ssl_crypto_x509_session_verify_cert_chain");
// 找不到就需要 reFlutter 修过的 SO 来替换,或按版本号查找社区脚本。

7.8 Native Hook 的重要细节

为什么不需要 Java.perform Interceptor.attachModule.findExportByName 是 Frida 的 Native API,直接操作进程内存中的机器码,不需要 ART VM 的参与。在脚本顶层直接调用即可。

Module.findExportByName 的工作原理:它在指定 SO 文件的导出符号表(ELF 的 .dynsym 段)中按名称查找函数,返回内存中的实际地址。如果函数不存在(如 SO 未加载或符号被 strip),返回 null——所以前面所有 Hook 都用 if (fn) {...} 包了一层,防 NPE。

Native Hook 与 spawn / attach 模式的关系:spawn 模式(-f 参数)下脚本在 App 进程刚启动、库还没全部加载时就开始跑,常导致 findModuleByName 返回 null。两种应对方式:

  • 配合 dlopen 监听器,等待目标库加载后再 Hook(第 7.5 节已示范);
  • 或者用 attach 模式(先让 App 跑起来再 frida -U <pkg>),所有库都已就位,但脚本错过 App 启动阶段的 Hook 时机。

八、第 5 层:双向认证(mTLS)

8.1 mTLS 与普通 TLS 的区别

普通的 HTTPS(单向 TLS)中,只有客户端验证服务器的身份——通过检查服务器发来的证书。服务器不验证客户端的身份。

双向认证(Mutual TLS, mTLS)中,两个方向都要验证:客户端验证服务器,服务器也验证客户端。这意味着 App 内部嵌入了一个客户端证书和对应的私钥。在 TLS 握手过程中,服务器会要求 App 提供客户端证书,并验证这个证书是否有效。

mTLS 给抓包带来的额外困难:即使你绕过了 App 端的证书验证(让 App 信任代理的证书),你的代理工具在与真正的服务器通信时,也需要提供 App 的客户端证书——否则服务器会拒绝连接。所以你需要从 App 中提取客户端证书和私钥,然后配置到代理工具中。

mTLS 双向认证握手时序

8.2 提取客户端证书

// mtls_extract.js
// 从 App 中提取 mTLS 客户端证书的密码和详细信息
Java.perform(function() {
    // === 提取 KeyStore 密码 ===
    // 客户端证书通常存储在 PKCS12 格式的 KeyStore 中(.p12 或 .bks 文件)
    // App 调用 KeyStore.load(inputStream, password) 加载它
    // Hook 这个方法可以拿到密码

    var KeyStore = Java.use("java.security.KeyStore");
    KeyStore.load.overload("java.io.InputStream""[C")
        .implementation = function(stream, password) {

        // password 参数是 char[],转换为 String 打印
        if (password) {
            var pwdStr = Java.use("java.lang.String").$new(password);
            console.log("[mTLS] KeyStore.load 密码: " + pwdStr);
        }

        // 获取 KeyStore 类型(PKCS12, BKS, JKS 等)
        console.log("[mTLS] KeyStore 类型: " + this.getType());

        // 调用原始方法完成加载
        this.load(stream, password);

        // 加载完成后,枚举 KeyStore 中的所有条目
        var aliases = this.aliases();
        while (aliases.hasMoreElements()) {
            var alias = aliases.nextElement().toString();
            console.log("[mTLS] 别名: " + alias);

            try {
                var cert = this.getCertificate(alias);
                if (cert) {
                    // 向下转型为 X509Certificate 以获取详细信息
                    var x509 = Java.cast(cert,
                        Java.use("java.security.cert.X509Certificate"));
                    console.log(" 主题: " + x509.getSubjectDN());
                    console.log(" 颁发者: " + x509.getIssuerDN());
                    console.log(" 有效期: " + x509.getNotBefore() +
                                " 至 " + x509.getNotAfter());
                    // 获取公钥算法(RSA / EC)
                    console.log(" 公钥算法: " + x509.getPublicKey().getAlgorithm());
                }
            } catch(e) {}

            // 检查是否包含私钥(PrivateKeyEntry 表示有私钥)
            try {
                if (this.isKeyEntry(alias)) {
                    console.log(" [包含私钥] 这是客户端证书条目");
                }
            } catch(e) {}
        }
    };

    // === 提取 KeyManagerFactory 密码 ===
    // KeyManagerFactory.init 初始化密钥管理器,同样需要 KeyStore 密码
    var KeyManagerFactory = Java.use("javax.net.ssl.KeyManagerFactory");
    KeyManagerFactory.init.overload("java.security.KeyStore""[C")
        .implementation = function(ks, password) {
        if (password) {
            console.log("[mTLS] KeyManagerFactory 密码: " +
                Java.use("java.lang.String").$new(password));
        }
        this.init(ks, password);
    };

    console.log("[*] mTLS 证书提取 Hook 已加载");
    console.log("[*] 请触发 App 的网络请求,等待证书加载...");
});

提取到密码和 KeyStore 类型后,你需要:

  1. 找到 App 中的证书文件(通常在 APK 的 assets/res/raw/ 目录下,格式为 .p12.bks.pfx
  2. 将它从 APK 中解压出来
  3. 配置到 Charles 或 mitmproxy 中作为客户端证书使用
# Charles 配置 mTLS 客户端证书:
# Proxy → SSL Proxying Settings → Client Certificates → Add
# 导入 .p12 文件并输入密码

# mitmproxy 配置客户端证书:
mitmproxy --set client_certs=/path/to/client.p12

九、对抗混淆:当类名不再叫 CertificatePinner

前面所有 Hook 都假设你能直接 Java.use("okhttp3.CertificatePinner")——但稍微上点防护的 App 都会用 ProGuard/R8 把它混淆成 a.b.c、方法名变成 a(),按字面类名 Hook 直接 ClassNotFoundException。这一节给三个不靠类名的定位策略,按从易到难排列。

9.1 策略 A:接口枚举(最稳,优先尝试)

Java 标准库的接口(X509TrustManagerHostnameVerifierSSLSocketFactory 等)名是 API 契约的一部分,ProGuard 不会重命名。我们用 Java.choose 在堆里枚举所有实现了这些接口的实例,反查出混淆后的类名:

Java.perform(function() {
    // 枚举堆中所有 X509TrustManager 实例
    Java.choose("javax.net.ssl.X509TrustManager", {
        onMatch: function(instance) {
            // 输出运行时实际类名(混淆后的)
            console.log("[Found X509TrustManager] " + instance.getClass().getName());
        },
        onComplete: function() {}
    });

    // 同理枚举 HostnameVerifier、SSLSocketFactory
    Java.choose("javax.net.ssl.HostnameVerifier", {
        onMatch: function(instance) {
            console.log("[Found HostnameVerifier] " + instance.getClass().getName());
        },
        onComplete: function() {}
    });
});

跑完后控制台会列出类似 a.b.c$1com.example.x.y 这样的名字——这就是混淆后的 TrustManager。拿到名字后再 Java.use(那个名字) 针对性 Hook。

注意Java.choose 只能找到已经被实例化的对象。要在 App 触发过一次 HTTPS 请求之后再跑,否则堆里还没有 TrustManager 实例。

9.2 策略 B:异常构造函数追溯(最准,知道是 Pinning 失败时用)

如果 App 的 Pinning 校验失败会闪退或报"网络错误",几乎可以肯定它内部抛了 CertificateExceptionSSLPeerUnverifiedException。Hook 这两个异常的构造函数,能直接拿到完整调用栈——栈里就有混淆后的执行类名:

Java.perform(function() {
    var Log = Java.use("android.util.Log");
    var Exception = Java.use("java.lang.Exception");

    // Hook CertificateException 构造函数
    var CertException = Java.use("java.security.cert.CertificateException");
    CertException.$init.overload("java.lang.String").implementation = function(msg) {
        console.log("\n[!] CertificateException: " + msg);
        // 抛个新异常拿到当前调用栈
        console.log(Log.getStackTraceString(Exception.$new()));
        return this.$init(msg);
    };

    // Hook SSLPeerUnverifiedException(OkHttp Pin 校验失败抛这个)
    var SSLEx = Java.use("javax.net.ssl.SSLPeerUnverifiedException");
    SSLEx.$init.overload("java.lang.String").implementation = function(msg) {
        console.log("\n[!] SSLPeerUnverifiedException: " + msg);
        console.log(Log.getStackTraceString(Exception.$new()));
        return this.$init(msg);
    };
});

输出长这样:

[!] SSLPeerUnverifiedException: Certificate pinning failure!
java.lang.Exception
    at javax.net.ssl.SSLPeerUnverifiedException.<init>(...)
    at a.b.c.d(SourceFile:42) ← 这一行就是混淆后的校验类
    at e.f.g.h(SourceFile:88)
    at okhttp3.internal.connection.RealCall.execute(...)

栈中第一个非系统类(即 a.b.c.d)就是执行校验的位置。用 jadx 打开 APK,搜索这个混淆类名,看反编译代码确认 check / verify 方法的实际签名,再针对性 Hook。

9.3 策略 C:字符串常量反向追踪(兜底,连异常都不抛时)

少数 App 会把"失败"做成无声断网而不抛异常——这时连堆栈都拿不到。借助一个事实:OkHttp 的 Pin hash 总是以 sha256/ 前缀存储,BoringSSL 错误日志里总会出现 CERTIFICATE_VERIFY_FAILEDssl_verify_peer_cert 等固定字符串。这些字符串常量混淆器不会动(它们是编译后的字面量):

# Java 层:在 smali 中搜 sha256/ 字符串
apktool d target.apk -o target_decoded
grep -rn 'const-string.*"sha256/"' target_decoded/smali*/

# Native 层:在 SO 中搜特征字符串
strings lib/arm64-v8a/libttboringssl.so | grep -E "verify|CERT"
# 用 IDA / Ghidra 打开 SO,对找到的字符串做 X-Ref,定位调用它的函数地址

拿到字符串所在类(Java 层)或函数偏移(Native 层)后,直接按地址 Hook:

// Native 示例:假设 IDA 看到 libxxx.so 中 0x3A5678 是校验函数
var mod = Process.findModuleByName("libxxx.so");
Interceptor.attach(mod.base.add(0x3A5678), {
    onLeave: function(retval) { retval.replace(ptr(0)); }
});

9.4 三策略的适用顺序

对抗混淆三策略决策树
现象优先用策略备注
App 加载脚本后部分日志出现,但某些 Hook 未命中A(接口枚举)通常是混淆 TrustManager
App 抓包时闪退或控制台抛异常B(异常追溯)顺着堆栈第一个非系统类直接打
没有任何异常但连接静默失败C(字符串特征)配合 jadx / IDA 静态分析
Native 层完全没有日志C + 配合第七章 dlopen 监听库可能被延迟加载

实战节奏:A 跑一次列出所有混淆类,B 跑一次定位真正抛异常的那个,两步信息合起来基本能锁定目标。C 留给真正"哑巴"的 App。

十、综合绕过脚本:一键覆盖所有层级

在实际工作中,你通常不知道 App 使用了哪一层的 Pinning。最高效的做法是加载一个综合脚本,一次性覆盖所有层级。以下脚本将前面各层的绕过整合在一起:

// ssl_bypass_all.js
// 综合 SSL Pinning 绕过脚本 - 覆盖 Java 层 + Native 层
// 用法: frida -U -f <包名> -l ssl_bypass_all.js

console.log("===========================================");
console.log(" SSL Pinning 综合绕过 v3.0");
console.log(" Java: TM / NSC / KeyStore / OkHttp /");
console.log(" Conscrypt / WebView / Cronet");
console.log(" Native: libssl / libboringssl /");
console.log(" libttboringssl / libcurl");
console.log("===========================================\n");

// 安全 Hook 包装器:每个 Hook 独立 try-catch
// 一个 Hook 失败不影响其他 Hook
function tryHook(description, hookFn) {
    try {
        hookFn();
        console.log("[OK] " + description);
    } catch(e) {
        console.log("[--] " + description + "(未检测到,跳过)");
    }
}

// ==================== Java 层 ====================
Java.perform(function() {

    // 第 1 层:TrustManager + SSLContext.init
    tryHook("TrustManager + SSLContext.init"function() {
        var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
        var SSLContext = Java.use("javax.net.ssl.SSLContext");

        var BypassTM = Java.registerClass({
            name: "com.frida.bypass.TM",
            implements: [X509TrustManager],
            methods: {
                checkClientTrusted: function(chain, authType) {},
                checkServerTrusted: function(chain, authType) {},
                getAcceptedIssuers: function() { return []; }
            }
        });

        SSLContext.init.overload(
            "[Ljavax.net.ssl.KeyManager;",
            "[Ljavax.net.ssl.TrustManager;",
            "java.security.SecureRandom"
        ).implementation = function(km, tm, sr) {
            this.init(km, Java.array("javax.net.ssl.TrustManager",
                [BypassTM.$new()]), sr);
        };
    });

    // 第 1 层补充:Conscrypt TrustManagerImpl(系统 + 独立两套包名)
    ["com.android.org.conscrypt.TrustManagerImpl",
     "org.conscrypt.TrustManagerImpl"].forEach(function(clsName) {
        tryHook("Conscrypt: " + clsName, function() {
            var TMImpl = Java.use(clsName);
            TMImpl.verifyChain.implementation = function(untrustedChain) {
                return untrustedChain;
            };
        });
    });

    // 第 6.2 节 NetworkSecurityConfig: NetworkSecurityTrustManager
    tryHook("NetworkSecurityTrustManager (2-arg)"function() {
        var NSTM = Java.use("android.security.net.config.NetworkSecurityTrustManager");
        NSTM.checkServerTrusted.overload(
            "[Ljava.security.cert.X509Certificate;""java.lang.String"
        ).implementation = function() {};
    });
    tryHook("NetworkSecurityTrustManager (3-arg)"function() {
        var NSTM = Java.use("android.security.net.config.NetworkSecurityTrustManager");
        NSTM.checkServerTrusted.overload(
            "[Ljava.security.cert.X509Certificate;""java.lang.String""java.lang.String"
        ).implementation = function() {
            return Java.use("java.util.ArrayList").$new();
        };
    });
    tryHook("RootTrustManager"function() {
        var RTM = Java.use("android.security.net.config.RootTrustManager");
        RTM.checkServerTrusted.overload(
            "[Ljava.security.cert.X509Certificate;""java.lang.String"
        ).implementation = function() {};
    });

    // 第 6.3 节 嵌入式 KeyStore: TrustManagerFactory.init(KeyStore) -> null
    tryHook("TrustManagerFactory.init(KeyStore)"function() {
        var TMF = Java.use("javax.net.ssl.TrustManagerFactory");
        TMF.init.overload("java.security.KeyStore")
            .implementation = function(ks) { this.init(null); };
    });

    // 第 2 层:OkHttp3 CertificatePinner
    tryHook("OkHttp3 CertificatePinner"function() {
        var CP = Java.use("okhttp3.CertificatePinner");
        CP.check.overload("java.lang.String""java.util.List")
            .implementation = function(h, c) {};
    });

    // OkHttp3 Kotlin 版本(OkHttp 4.x+)
    tryHook("OkHttp3 check$okhttp"function() {
        var CP = Java.use("okhttp3.CertificatePinner");
        CP.check$okhttp.overload(
            "java.lang.String",
            "kotlin.jvm.functions.Function0"
        ).implementation = function(h, f) {};
    });

    // OkHttp 内置 HostnameVerifier
    tryHook("OkHostnameVerifier"function() {
        var V = Java.use("okhttp3.internal.tls.OkHostnameVerifier");
        V.verify.overload("java.lang.String""javax.net.ssl.SSLSession")
            .implementation = function() { return true; };
    });

    // 旧版 OkHttp (com.squareup.okhttp)
    tryHook("OkHttp (旧版)"function() {
        var CP = Java.use("com.squareup.okhttp.CertificatePinner");
        CP.check.overload("java.lang.String""[Ljava.security.cert.Certificate;")
            .implementation = function(h, c) {};
    });

    // HttpsURLConnection
    tryHook("HttpsURLConnection"function() {
        var HC = Java.use("javax.net.ssl.HttpsURLConnection");
        HC.setDefaultHostnameVerifier.implementation = function(v) {};
        HC.setSSLSocketFactory.implementation = function(f) {};
    });

    // TrustKit
    tryHook("TrustKit PinningTrustManager"function() {
        var PTM = Java.use(
            "com.datatheorem.android.trustkit.pinning.PinningTrustManager");
        PTM.checkServerTrusted.implementation = function() {};
    });

    // WebView SSL 错误
    tryHook("WebViewClient SSL Error"function() {
        var WC = Java.use("android.webkit.WebViewClient");
        WC.onReceivedSslError.implementation = function(v, h, e) {
            h.proceed();
        };
    });

    // Cronet
    tryHook("Cronet"function() {
        var CE = Java.use("org.chromium.net.CronetEngine$Builder");
        CE.enablePublicKeyPinningBypassForLocalTrustAnchors
            .implementation = function(e) {
            return this.enablePublicKeyPinningBypassForLocalTrustAnchors(true);
        };
    });

    console.log("");
});

// ==================== Native 层 ====================
// 候选 SO 名列表:常见的 TLS 库都试一遍
var SSL_LIBS = ["libssl.so""libboringssl.so""libttboringssl.so"];

function hookNativeSSL(lib) {
    var mod = Process.findModuleByName(lib);
    if (!mod) { console.log("[--] Native: " + lib + "(未加载,跳过)"); return; }
    console.log("[OK] Native: " + lib + " base=" + mod.base);

    // SSL_CTX_set_custom_verify(BoringSSL 专有,回调返回 0 = ok)
    var a1 = Module.findExportByName(lib, "SSL_CTX_set_custom_verify");
    if (a1) Interceptor.attach(a1, {
        onEnter: function(args) {
            args[2= new NativeCallback(function() { return 0; },
                "int", ["pointer""pointer"]);
        }
    });

    // SSL_CTX_set_cert_verify_callback(回调返回 1 = ok)
    var a2 = Module.findExportByName(lib, "SSL_CTX_set_cert_verify_callback");
    if (a2) Interceptor.attach(a2, {
        onEnter: function(args) {
            args[1= new NativeCallback(function() { return 1; },
                "int", ["pointer""pointer"]);
        }
    });

    // SSL_CTX_set_verify / SSL_set_verify -> SSL_VERIFY_NONE
    ["SSL_CTX_set_verify""SSL_set_verify"].forEach(function(name) {
        var a = Module.findExportByName(lib, name);
        if (a) Interceptor.attach(a, {
            onEnter: function(args) { args[1= ptr(0); args[2= ptr(0); }
        });
    });

    // SSL_get_verify_result -> X509_V_OK
    var a3 = Module.findExportByName(lib, "SSL_get_verify_result");
    if (a3) Interceptor.attach(a3, {
        onLeave: function(rv) { if (!rv.equals(ptr(0))) rv.replace(ptr(0)); }
    });

    // ssl_verify_peer_cert(内部符号,遍历找)
    try {
        mod.enumerateSymbols().forEach(function(sym) {
            if (sym.name.indexOf("ssl_verify_peer_cert"!== -1) {
                Interceptor.attach(sym.address, {
                    onLeave: function(rv) {
                        if (rv.toInt32() !== 0) rv.replace(ptr(0));
                    }
                });
            }
        });
    } catch(e) {}
}

// 立即对已加载的库下手
SSL_LIBS.forEach(hookNativeSSL);

// 对未加载的库挂 dlopen 监听
(function() {
    var dlopenPtr = Module.findExportByName(null"android_dlopen_ext")
                  || Module.findExportByName(null"dlopen");
    if (!dlopenPtr) return;
    Interceptor.attach(dlopenPtr, {
        onEnter: function(args) { this.libPath = args[0].readCString(); },
        onLeave: function() {
            if (!this.libPath) return;
            for (var i = 0; i < SSL_LIBS.length; i++) {
                if (this.libPath.indexOf(SSL_LIBS[i].replace(".so""")) !== -1) {
                    console.log("[+] dlopen 监听到 " + SSL_LIBS[i] + ",安装 Hook");
                    hookNativeSSL(SSL_LIBS[i]);
                }
            }
        }
    });
})();

// X509_verify_cert(OpenSSL 历史 API,独立处理)
tryHook("Native: X509_verify_cert"function() {
    var hooked = false;
    ["libssl.so""libcrypto.so"].forEach(function(lib) {
        var fn = Module.findExportByName(lib, "X509_verify_cert");
        if (fn) {
            Interceptor.attach(fn, {
                onLeave: function(retval) {
                    if (retval.toInt32() !== 1) retval.replace(ptr(1));
                }
            });
            hooked = true;
        }
    });
    if (!hooked) throw new Error("not found");
});

// libcurl: curl_easy_setopt 关掉 VERIFYPEER / VERIFYHOST
tryHook("Native: libcurl curl_easy_setopt"function() {
    var fn = Module.findExportByName("libcurl.so""curl_easy_setopt");
    if (!fn) throw new Error("not found");
    Interceptor.attach(fn, {
        onEnter: function(args) {
            var opt = args[1].toInt32();
            if (opt === 64 || opt === 81) args[2= ptr(0);
        }
    });
});

console.log("\n[*] SSL Pinning 绕过全部加载完成");
console.log("[*] 请配合 Charles/mitmproxy 使用\n");

十一、绕过失败的排查流程

当你加载了综合绕过脚本但仍然抓不到包时,按照以下流程逐步排查。

绕过失败排查流程

第一步,确认代理工具本身工作正常。 在手机浏览器中访问 http://mitm.it(mitmproxy)或 http://chls.pro/ssl(Charles),看是否能正常显示证书下载页面。如果不能,说明代理配置有问题(Wi-Fi 代理设置、电脑防火墙、端口冲突等),和 SSL Pinning 无关。

第二步,确认 Frida 脚本已加载且没有报错。 检查终端输出,应该看到多个 [OK] 行。如果某个关键的 Hook 显示 [--](比如 TrustManager 或 CertificatePinner),说明对应的类没有被 App 使用——这是正常的。但如果所有 Hook 都显示 [--],说明 App 可能使用了非标准的实现。

第三步,确认 App 确实在发网络请求。 在 ADB shell 中运行:

# 监控 443 端口的 TCP 流量(需要 root)
adb shell su -c "tcpdump -i any -c 20 port 443"

# 如果没有 tcpdump,可以用 netstat 查看活跃连接
adb shell netstat -tunp | grep 443

如果有 TCP 流量输出,说明 App 在尝试建立 HTTPS 连接;如果没有流量,说明 App 可能检测到了异常环境(Root/Frida/代理)而拒绝联网。

第四步,检查是否有反 Frida 检测。 尝试加载一个完全空的脚本:

frida -U -f <包名> -e "" --no-pause

看 App 是否正常运行。如果空脚本就导致 App 闪退或行为异常,说明 App 有反 Frida 检测,需要先解决检测问题(见第16篇 Root 检测绕过和第17篇 Frida 检测对抗)。

第五步,考虑特殊情况:

症状可能原因解决方案
代理有流量但全是加密的App 不走系统代理见下方「App 不走系统代理」一节
代理完全没有流量App 使用 QUIC (HTTP/3)Hook 禁用 QUIC,让 App 降级到 HTTP/2
Java Hook 全部 [--]App 在 Native 层做 SSL确保 Native 层 Hook 已生效(第七章)
类名 ClassNotFoundExceptionApp 做了混淆见第九章对抗混淆三策略
连接建立但立刻断开服务器验证客户端证书 (mTLS)提取客户端证书配置到代理(第八章)
App 闪退无任何日志反 Frida 检测见第 16-17 篇

11.1 App 不走系统代理时的两条出路

部分 App(OkHttp 的 proxy(Proxy.NO_PROXY) 配置、字节系产品、自带 Native 网络库的应用)会显式绕开系统 HTTP 代理设置,Wi-Fi 代理设了也没用。两种应对思路:

A. iptables 透明代理(Root 设备首选) —— 在 Linux 网络层把 443 流量强制 DNAT 到代理机:

# 把出站 443 流量重定向到 PC 上的 Burp/mitmproxy:8080
adb shell "su -c 'iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination <PC_IP>:8080'"

# 清掉规则
adb shell "su -c 'iptables -t nat -F OUTPUT'"

代理工具需要开启「透明代理」模式:mitmproxy 用 mitmproxy --mode transparent,Burp 在 Proxy → Options → Request handling 勾选 "Support invisible proxying"。

B. 设备端 VPN 抓包(无 Root 备选) —— 用 HttpCanary / PCAPdroid 在设备本机起 VPN 服务,所有出网流量统一走它,不依赖系统代理设置。代价是它无法做主动 MITM,只能看明文应用层数据。

iptables 透明代理 vs 设备端 VPN 抓包路径对比

11.2 QUIC 禁用

QUIC 基于 UDP 而不是 TCP,传统 HTTPS 代理无法拦截。如果 App 使用 Cronet 的 QUIC 支持,可以 Hook 强制关闭:

Java.perform(function() {
    try {
        var CE = Java.use("org.chromium.net.CronetEngine$Builder");
        CE.enableQuic.implementation = function(enable) {
            return this.enableQuic(false);   // 强制关 QUIC,降级到 HTTP/2
        };
    } catch(e) {}
});

十二、抓包成功后:直接在 Frida 中查看流量

绕过 SSL Pinning 后,配合 Charles/mitmproxy 抓包是标准做法。但有时候你可能只是想快速看一下 App 发了什么请求,不想启动完整的抓包工具。这时可以用 Frida 直接在 OkHttp 的请求层面捕获 HTTP 流量:

// okhttp_traffic.js - 在 SSL 绕过脚本的基础上叠加使用
// 直接在控制台打印所有 OkHttp 的 HTTP 请求和响应

Java.perform(function() {
    try {
        var RealCall = Java.use("okhttp3.RealCall");

        // Hook RealCall.execute: 同步 HTTP 请求
        RealCall.execute.implementation = function() {
            // 获取 Request 对象
            var request = this.request();
            console.log("\n[HTTP] " + request.method() + " " + request.url());

            // 打印请求头
            var headers = request.headers();
            for (var i = 0; i < headers.size(); i++) {
                console.log(" > " + headers.name(i) + ": " + headers.value(i));
            }

            // 执行原始请求
            var response = this.execute();

            // 打印响应状态
            console.log(" < " + response.code() + " " + response.message());

            // 打印响应体(使用 peekBody 避免消耗流)
            try {
                var body = response.peekBody(1024 * 512);  // 最多 512KB
                // body.string() 返回的 Java String 会被 Frida 自动 marshal 为 JS string
                // 因此用 .length 属性访问、用 .substring() 方法切片(JS 风格)
                var bodyStr = body.string();
                if (bodyStr.length > 1000) {
                    console.log(" Body: " + bodyStr.substring(01000+ "...(截断)");
                } else {
                    console.log(" Body: " + bodyStr);
                }
            } catch(e) {
                console.log(" Body: (无法读取)");
            }

            return response;
        };

        console.log("[*] OkHttp 流量捕获已启动");
    } catch(e) {
        console.log("[*] OkHttp 流量捕获未启用(App 可能不使用 OkHttp)");
    }
});

peekBody vs bodyresponse.body().string()消耗响应体流——调用后 App 自己再读取响应体时就会得到空数据,导致功能异常。response.peekBody(maxBytes) 是 OkHttp 提供的安全读取方法,它会缓冲指定字节数的响应体内容供你读取,但不影响 App 对原始响应体的读取。永远使用 peekBody 而非 body。

十三、工具生态速查

把本篇用到、以及实战中可能更省事的工具集中在一张表里。优先级从上到下:能解决问题的最简方案放前面。

工具用途一行命令 / 用法适用场景
objection一键绕过objection -g <pkg> exploreandroid sslpinning disable常规 App,先试这条
frida-multiple-unpinning通杀脚本frida -U -f <pkg> -l frida-multiple-unpinning.js覆盖面最广的 JS 脚本
Frida CodeShare社区脚本frida --codeshare <作者>/<脚本> -f <pkg>抖音/TikTok 等特定 App 的现成脚本
HTTP Toolkit一键注入抓包图形化「Android Device via ADB」不想配 Frida 时的快速方案
apk-mitm重打包绕过npx apk-mitm app.apk无 Root 设备,且 App 不做签名校验时
PCAPdroid无 Root 流量捕获Play Store 安装仅看明文应用层,不做 MITM
HttpCanary / ProxyDroid设备端 VPN/SOCKS 代理设置目标 App 走它的隧道App 不走系统代理
AlwaysTrustUserCerts (Magisk)系统级 CA 安装Magisk Manager 安装模块Android 7.0+ 用户 CA 默认不信任
MoveCertificate (Zygisk)同上Zygisk 模块Android 14+ /system 只读
reFlutterFlutter SO 替换重打包 APK 时注入修过的 libflutter.soFlutter App 抓包
mitmproxy + transparent透明代理mitmproxy --mode transparent 配合 iptablesRoot 设备强制路由 443

顺序心法:先 objection 一句;不行换 frida-multiple-unpinning;再不行翻 CodeShare 找针对性脚本;最后才是按本篇的五层模型逐层手写 Hook。从「找现成的」到「自己改」到「自己写」是性价比最高的三档。

总结

这一篇系统地覆盖了 SSL Pinning 绕过的所有层级。

层级核心 Hook 点绕过原理
第1层 TrustManagerSSLContext.init替换 TrustManager 为空实现
第2层 OkHttpCertificatePinner.check空实现不抛异常 = 校验通过
第3层 平台TrustManagerImpl.verifyChain直接返回未验证证书链
第4层 NativeSSL_CTX_set_custom_verify替换回调 / 强制返回成功
第5层 mTLSKeyStore.load提取证书密码配置到代理

实战要诀

  1. 先试第二章通用方案,不行再走五层模型分析
  2. 「系统 CA + Frida 绕过」双管齐下覆盖率最高
  3. 绕过失败时按第十一章排查流程逐步定位(代理 → 脚本 → 流量 → 反检测 → 特殊协议)
  4. 类名混淆走第九章三策略,Native 延迟加载走第七章 dlopen 监听
  5. Flutter App 需要额外处理 libflutter.so / 抖音系走 libttboringssl.so 专项(第 7.5 节)

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-14 12:37:12 HTTP/2.0 GET : https://67808.cn/a/488664.html
  2. 运行时间 : 0.192928s [ 吞吐率:5.18req/s ] 内存消耗:4,838.35kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=ecf223cd8da878f029a26e774d9d3c8b
  1. /yingpanguazai/ssd/ssd1/www/no.67808.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/no.67808.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/no.67808.cn/runtime/temp/6df755f970a38e704c5414acbc6e8bcd.php ( 12.06 KB )
  140. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001184s ] mysql:host=127.0.0.1;port=3306;dbname=no_67808;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001838s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000807s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000671s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001584s ]
  6. SELECT * FROM `set` [ RunTime:0.000605s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001764s ]
  8. SELECT * FROM `article` WHERE `id` = 488664 LIMIT 1 [ RunTime:0.001917s ]
  9. UPDATE `article` SET `lasttime` = 1778733432 WHERE `id` = 488664 [ RunTime:0.010790s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 65 LIMIT 1 [ RunTime:0.000718s ]
  11. SELECT * FROM `article` WHERE `id` < 488664 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001440s ]
  12. SELECT * FROM `article` WHERE `id` > 488664 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001141s ]
  13. SELECT * FROM `article` WHERE `id` < 488664 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.002879s ]
  14. SELECT * FROM `article` WHERE `id` < 488664 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.008439s ]
  15. SELECT * FROM `article` WHERE `id` < 488664 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.020608s ]
0.196674s