Lazy loaded image
Frida学习笔记(八):SSL Pinning 绕过全攻略
Words 14732Read Time 37 min
2026-7-9
2026-7-9
type
Post
status
Published
date
Jul 9, 2026
slug
summary
本篇目标:彻底解决「App 抓不到包」的问题。不只是给你一个能用的绕过脚本——而是让你理解 HTTPS 的信任模型、SSL Pinning 的每一种实现方式、以及为什么特定的绕过方案能对应特定的实现方式。这样当你遇到脚本不管用的情况时,你知道该往哪个方向调整。
tags
frida
Android
逆向工程
category
公众号
icon
password
网址
上次编辑时间
Jul 9, 2026 07:49 AM
comment
AI 总结
本篇目标:彻底解决「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 通信,一头用真证书和服务器通信。所有经过代理的流量在代理这里都是明文的——你就能看到完整的请求和响应了。
notion image
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 中明确声明信任用户证书。
这意味着即使目标 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 及以下,作为原理演示保留。
第二种,使用 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 一键绕过。
它内部依次 Hook 了 SSLContext.init / TrustManagerImpl.verifyChain / OkHttp3 CertificatePinner.check 等关键方法——本质上就是后面第四章到第六章要讲的几招的合集,只是被打包成了一句命令。

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

社区脚本里覆盖面最广的是 frida-multiple-unpinning(GitHub 搜同名仓库即可,由 @httptoolkit 等多位作者持续维护)。直接挂上去:
它默认覆盖:TrustManagerImpl、OkHttp 3.x/4.x、TrustKit、Conscrypt、HttpsURLConnection、NetworkSecurityConfig、Apache HTTP Client 等十几种实现。

2.3 什么时候必须往下读

通用方案不是万能。以下三类场景几乎一定失败,需要进入后续章节做"针对性 Hook":
  1. 类名被 ProGuard/R8 混淆 —— 通杀脚本按字面类名 Hook,混淆后命中不到。见第九章「对抗混淆」。
  1. SSL 校验在 Native 层 —— 通杀脚本只覆盖 Java 层。Flutter / 字节系产品 / 自带 libcurl 的应用都属于这一类。见第七章 Native SSL。
  1. App 检测到 Frida 直接闪退 —— 通杀脚本本身能加载,但 App 起来就死。见第 16-17 篇 Root 检测 / Frida 检测对抗。
理解了这层"先粗后细"的工作流,下面的五层模型不是"必读章节",而是"通用方案失败后的诊断地图"——出哪一层的事,你就翻到对应一节。

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

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

3.1 五层模型

notion image
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 绕过代码(详细注释版)

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。开发者可以这样使用它:
每个 .add() 调用将一个域名与一个证书公钥的 SHA-256 hash(Base64 编码)关联。在 TLS 握手完成后,OkHttp 会检查服务器证书链中是否有任何一个证书的公钥 hash 与配置的值匹配。如果没有匹配的,抛出 SSLPeerUnverifiedException,连接失败。
关键时序:OkHttp 的 CertificatePinner 校验发生在 TLS 握手之后。TLS 握手本身可能已经通过了(因为我们第 1 层已经替换了 TrustManager),但 OkHttp 在握手成功后还会做一次额外的 Pin 校验。所以第 1 层绕过对 OkHttp 不够——你需要同时绕过 CertificatePinner.check
notion image
TLS 握手过程中的 Pin 校验位置与 Hook 点

5.2 绕过代码

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 的关键验证方法:
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)

notion image
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 对它们无效,必须单独处理。
识别方式:反编译 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

notion image
三种 TrustManager 构造路径对比
部分 App(金融类、政企类居多)会把"自己信任的服务器证书"直接打包进 APK,路径通常是 assets/server.bksres/raw/cert.p12assets/cacert.pem 等。运行时通过这样的代码加载:
这里产生的 TrustManager 只信任 KeyStore 里那张证书——和第四章讲的「自定义 X509TrustManager」是两种不同实现。第 1 层 Hook SSLContext.init 替换 TrustManager 也能搞定它,但更精准的入口是 TrustManagerFactory.init
识别方式:反编译后在 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() 取消连接。
安全提示onReceivedSslError 的绕过只在 WebView 内生效。如果 App 的主要网络请求不通过 WebView 发送(大多数 App 都是这样),这个 Hook 只解决 WebView 内嵌网页的证书问题。

6.5 Cronet (Chromium 网络栈) 的处理

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

七、第 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 关键函数和绕过原理

notion image
BoringSSL 证书验证调用链
BoringSSL/OpenSSL 中与证书验证相关的关键函数(按"被命中的概率"排序):
| 函数 | 作用 | 绕过方式 | 备注 | | :-- | :-- | :-- | :-- | | SSL_CTX_set_custom_verify | BoringSSL:设自定义验证回调 | 替换回调为返回 0ssl_verify_ok) | BoringSSL 专有 | | SSL_CTX_set_verify / SSL_set_verify | 设置验证模式(连接级 / 上下文级) | mode 强制设 0SSL_VERIFY_NONE) | OpenSSL/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_cert | BoringSSL 内部入口(多为非导出) | 返回值强制改 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 绕过代码(详细注释版)

7.4 独立打包的 libboringssl.so

部分 App(尤其是 Chromium 内核的 WebView 和高安全应用)会把 BoringSSL 打成独立的 libboringssl.so 与 APK 一起发布,**而不是链接系统 libssl.so**。系统库的 Hook 对它完全无效——必须把上一节的 Hook 复制一份指向这个独立库:
第十章的综合脚本会自动遍历 libssl.solibboringssl.solibttboringssl.so 三个候选名,不用手抄。

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

libttboringssl.so 是字节跳动基于 BoringSSL 深度定制的 TLS 库——抖音、TikTok、今日头条、西瓜视频、飞书 等旗下产品几乎全用它。相比标准 BoringSSL 有三个差异点必须照顾:
  1. 导出符号精简 —— 内部验证函数多为非导出,Module.findExportByName 经常返回 null
  1. 额外 Pin 逻辑 —— 在 TLS 握手多个阶段注入自定义校验,光把 SSL_VERIFY_NONE 设上去不够。
  1. 延迟加载 —— 库不在 App 启动时就 dlopen,spawn 模式下脚本立刻去找 Process.findModuleByName("libttboringssl.so") 会返回 null
下面这张时序图展示了 dlopen 监听如何解决延迟加载问题,配合后面的代码理解:
notion image
libttboringssl 延迟加载场景下的 6 步 Hook 安装时序
下面是覆盖这三个差异的完整方案:
当导出符号都失效时:意味着字节又升级了。这时只能借助字符串特征定位偏移——在 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 即可:

7.7 Flutter App 的特殊处理

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

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 中提取客户端证书和私钥,然后配置到代理工具中。
notion image
mTLS 双向认证握手时序

8.2 提取客户端证书

提取到密码和 KeyStore 类型后,你需要:
  1. 找到 App 中的证书文件(通常在 APK 的 assets/res/raw/ 目录下,格式为 .p12.bks.pfx
  1. 将它从 APK 中解压出来
  1. 配置到 Charles 或 mitmproxy 中作为客户端证书使用

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

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

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

Java 标准库的接口(X509TrustManagerHostnameVerifierSSLSocketFactory 等)名是 API 契约的一部分,ProGuard 不会重命名。我们用 Java.choose 在堆里枚举所有实现了这些接口的实例,反查出混淆后的类名:
跑完后控制台会列出类似 a.b.c$1com.example.x.y 这样的名字——这就是混淆后的 TrustManager。拿到名字后再 Java.use(那个名字) 针对性 Hook。
注意Java.choose 只能找到已经被实例化的对象。要在 App 触发过一次 HTTPS 请求之后再跑,否则堆里还没有 TrustManager 实例。

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

如果 App 的 Pinning 校验失败会闪退或报"网络错误",几乎可以肯定它内部抛了 CertificateExceptionSSLPeerUnverifiedException。Hook 这两个异常的构造函数,能直接拿到完整调用栈——栈里就有混淆后的执行类名:
输出长这样:
栈中第一个非系统类(即 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 层)或函数偏移(Native 层)后,直接按地址 Hook:

9.4 三策略的适用顺序

notion image
对抗混淆三策略决策树
| 现象 | 优先用策略 | 备注 | | :-- | :-- | :-- | | App 加载脚本后部分日志出现,但某些 Hook 未命中 | A(接口枚举) | 通常是混淆 TrustManager | | App 抓包时闪退或控制台抛异常 | B(异常追溯) | 顺着堆栈第一个非系统类直接打 | | 没有任何异常但连接静默失败 | C(字符串特征) | 配合 jadx / IDA 静态分析 | | Native 层完全没有日志 | C + 配合第七章 dlopen 监听 | 库可能被延迟加载 |
实战节奏:A 跑一次列出所有混淆类,B 跑一次定位真正抛异常的那个,两步信息合起来基本能锁定目标。C 留给真正"哑巴"的 App。

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

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

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

当你加载了综合绕过脚本但仍然抓不到包时,按照以下流程逐步排查。
notion image
绕过失败排查流程
第一步,确认代理工具本身工作正常。 在手机浏览器中访问 http://mitm.it(mitmproxy)或 http://chls.pro/ssl(Charles),看是否能正常显示证书下载页面。如果不能,说明代理配置有问题(Wi-Fi 代理设置、电脑防火墙、端口冲突等),和 SSL Pinning 无关。
第二步,确认 Frida 脚本已加载且没有报错。 检查终端输出,应该看到多个 [OK] 行。如果某个关键的 Hook 显示 [--](比如 TrustManager 或 CertificatePinner),说明对应的类没有被 App 使用——这是正常的。但如果所有 Hook 都显示 [--],说明 App 可能使用了非标准的实现。
第三步,确认 App 确实在发网络请求。 在 ADB shell 中运行:
如果有 TCP 流量输出,说明 App 在尝试建立 HTTPS 连接;如果没有流量,说明 App 可能检测到了异常环境(Root/Frida/代理)而拒绝联网。
第四步,检查是否有反 Frida 检测。 尝试加载一个完全空的脚本:
看 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 已生效(第七章) | | 类名 ClassNotFoundException | App 做了混淆 | 见第九章对抗混淆三策略 | | 连接建立但立刻断开 | 服务器验证客户端证书 (mTLS) | 提取客户端证书配置到代理(第八章) | | App 闪退无任何日志 | 反 Frida 检测 | 见第 16-17 篇 |

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

部分 App(OkHttp 的 proxy(Proxy.NO_PROXY) 配置、字节系产品、自带 Native 网络库的应用)会显式绕开系统 HTTP 代理设置,Wi-Fi 代理设了也没用。两种应对思路:
A. iptables 透明代理(Root 设备首选) —— 在 Linux 网络层把 443 流量强制 DNAT 到代理机:
代理工具需要开启「透明代理」模式:mitmproxy 用 mitmproxy --mode transparent,Burp 在 Proxy → Options → Request handling 勾选 "Support invisible proxying"。
B. 设备端 VPN 抓包(无 Root 备选) —— 用 HttpCanary / PCAPdroid 在设备本机起 VPN 服务,所有出网流量统一走它,不依赖系统代理设置。代价是它无法做主动 MITM,只能看明文应用层数据。
notion image
iptables 透明代理 vs 设备端 VPN 抓包路径对比

11.2 QUIC 禁用

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

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

绕过 SSL Pinning 后,配合 Charles/mitmproxy 抓包是标准做法。但有时候你可能只是想快速看一下 App 发了什么请求,不想启动完整的抓包工具。这时可以用 Frida 直接在 OkHttp 的请求层面捕获 HTTP 流量:
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 只读 | | reFlutter | Flutter SO 替换 | 重打包 APK 时注入修过的 libflutter.so | Flutter App 抓包 | | mitmproxy + transparent | 透明代理 | mitmproxy --mode transparent 配合 iptables | Root 设备强制路由 443 |
顺序心法:先 objection 一句;不行换 frida-multiple-unpinning;再不行翻 CodeShare 找针对性脚本;最后才是按本篇的五层模型逐层手写 Hook。从「找现成的」到「自己改」到「自己写」是性价比最高的三档。

总结

这一篇系统地覆盖了 SSL Pinning 绕过的所有层级。
| 层级 | 核心 Hook 点 | 绕过原理 | | :-- | :-- | :-- | | 第1层 TrustManager | SSLContext.init | 替换 TrustManager 为空实现 | | 第2层 OkHttp | CertificatePinner.check | 空实现不抛异常 = 校验通过 | | 第3层 平台 | TrustManagerImpl.verifyChain | 直接返回未验证证书链 | | 第4层 Native | SSL_CTX_set_custom_verify 等 | 替换回调 / 强制返回成功 | | 第5层 mTLS | KeyStore.load | 提取证书密码配置到代理 |
实战要诀
  1. 先试第二章通用方案,不行再走五层模型分析
  1. 「系统 CA + Frida 绕过」双管齐下覆盖率最高
  1. 绕过失败时按第十一章排查流程逐步定位(代理 → 脚本 → 流量 → 反检测 → 特殊协议)
  1. 类名混淆走第九章三策略,Native 延迟加载走第七章 dlopen 监听
  1. Flutter App 需要额外处理 libflutter.so / 抖音系走 libttboringssl.so 专项(第 7.5 节)
上一篇
Frida学习笔记(九):r0capture 源码精读 · 不靠代理的抓包是怎么实现的
下一篇
Frida学习笔记(七):调用栈、日志与 Hook 排错

Comments
Loading...
Catalog