阅读提示:抓包分析 Android 应用的方式远不止绕过 SSL Pinning 一种——HTTP Toolkit 一键注入、VPN 路由劫持、 R0capture、ecapture等手段层出不穷,很多场景下根本不需要与 SSL Pinning 正面硬刚。本文不做工具横向对比,仅聚焦于"SSL Pinning 这一对抗技术的原理与 Frida 绕过实现",适合想深入理解这一机制、或在通用方案失效时需要精准定位问题的读者继续阅读。
摘要:在 Android 逆向与安全测试中,SSL Pinning 是阻碍 HTTPS 流量抓包的最大障碍。本文从逆向工程师视角出发,系统梳理 9 种主流 SSL Pinning 实现方式,并给出对应的 Frida 绕过方案——从一键通杀脚本到 Native 层深度 Hook,覆盖从入门到高级对抗的绝大部分场景。
前言
当你用 Charles / Burp Suite / mitmproxy 等工具抓包一个 Android 应用时,如果遇到以下症状:
- Logcat 中出现
javax.net.ssl.SSLHandshakeException 或 Certificate pinning failure
大概率就是遇到了 SSL Pinning(证书锁定)。
SSL Pinning 的本质是:应用在代码中硬编码了服务器证书或公钥的指纹信息,即使你的中间人代理证书已被系统信任,也会因为指纹不匹配而拒绝建立连接。这意味着仅仅安装 CA 证书是不够的——我们需要 Frida 这样的动态插桩工具,在运行时修改校验逻辑,才能顺利抓包。
本文将详细介绍如何使用 Frida 逐一攻破各种 SSL Pinning 实现。
一、SSL Pinning 原理回顾
1.1 标准 TLS 握手 vs SSL Pinning
在标准 TLS 握手中,客户端只验证服务器证书是否由系统信任的 CA(Certificate Authority,证书颁发机构)签发。这意味着只要我们将 Burp/Charles 的 CA 证书安装到设备的系统信任存储中,就能作为中间人(Man-in-the-Middle, MITM)解密全部 HTTPS 流量。
SSL Pinning 在此基础上增加了额外校验:客户端在代码中硬编码了服务器证书或公钥的指纹(Pin),即使中间人的证书被系统信任,也会因指纹不匹配而拒绝连接。
下图展示了两种场景的对比——左侧为标准 TLS 握手(可抓包),右侧为启用 SSL Pinning 后(抓包失败):
SSL Pinning 原理:标准 TLS 握手 vs 证书锁定核心区别:标准 TLS 只关心"证书链是否可信",而 SSL Pinning 还要求"证书/公钥是否是我预期的那个"。这个"预期"就是硬编码在应用中的 Pin 值。
1.2 Pin 的对象
SSL Pinning 可以锁定证书链中不同层级的信息:
| | | |
|---|
| 整个证书 | | | |
| 公钥 | 比对证书中的 SubjectPublicKeyInfo (SPKI) | | |
| 公钥哈希 | | | |
绝大多数现代实现采用公钥哈希 Pinning,因为它在安全性和可维护性之间取得了最佳平衡——服务器续签证书时只要保持同一密钥对,客户端无需更新。
二、抓包环境准备
2.1 基础环境
# 安装 Frida 工具链
pip install frida-tools
# 确认设备连接(USB 调试已开启)
adb devices
frida-ls-devices
# 下载与设备架构匹配的 frida-server
# 从 https://github.com/frida/frida/releases 下载
# arm64 设备选择 frida-server-<version>-android-arm64
adb push frida-server-<version>-android-arm64 /data/local/tmp/frida-server
adb shell "chmod 755 /data/local/tmp/frida-server"
# 以 root 权限启动 frida-server(需要 Root 设备)
adb shell "su -c '/data/local/tmp/frida-server -D &'"
# 验证 frida-server 运行状态
frida-ps -U
版本匹配:frida-tools 和 frida-server 的主版本号必须一致(如都是 16.x.x),否则会出现连接错误。使用 frida --version 查看本地版本。
2.2 代理设置
# 方案 A:设置 Wi-Fi 代理(常规方式)
# 大部分应用会走系统代理
adb shell settings put global http_proxy <PC_IP>:8080
# 方案 B:iptables 透明代理(适用于不走系统代理的应用)
# 将所有 443 端口流量重定向到 Burp
adb shell "su -c 'iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination <PC_IP>:8080'"
# 清除代理设置
adb shell settings put global http_proxy :0
# 清除 iptables 规则
adb shell "su -c 'iptables -t nat -F OUTPUT'"
为什么有些应用不走系统代理:部分应用使用 OkHttp 或 Native 网络库时,可以显式设置 NO_PROXY,或者直接通过 IP + 端口连接服务器而不查询系统代理设置。此时必须使用 iptables 透明代理或 VPN 方式(如 ProxyDroid)强制转发流量。
2.3 安装 CA 证书
Android 7.0(API 24)开始,应用默认不信任用户安装的 CA 证书,因此需要将抓包工具的 CA 证书安装到系统信任存储:
# 步骤 1:导出 Burp/Charles 的 CA 证书(DER 格式)
# Burp: Proxy -> Options -> Import/Export CA Certificate -> Export DER
# Charles: Help -> SSL Proxying -> Save Charles Root Certificate
# 步骤 2:转换为 PEM 格式,并用旧算法计算哈希(Android 使用 OpenSSL 旧哈希)
openssl x509 -inform DER -in cacert.der -out cacert.pem
HASH=$(openssl x509 -inform PEM -subject_hash_old -in cacert.pem | head -1)
cp cacert.pem "${HASH}.0"
# 步骤 3:推送到系统证书目录(需要 root + 可写系统分区)
adb root
adb remount
adb push "${HASH}.0" /system/etc/security/cacerts/
adb shell "chmod 644 /system/etc/security/cacerts/${HASH}.0"
adb reboot
Android 14+ 注意:从 Android 14 开始,/system 分区采用只读挂载(read-only),即使 adb remount 也无法写入。推荐使用 Magisk 模块 MagiskTrustUserCerts 或者 MoveCertificate 自动将用户证书提升为系统证书,无需修改系统分区。
三、通用 SSL Pinning 绕过方案
在逐一分析各种 SSL Pinning 实现之前,先介绍几种万能方案,它们能覆盖大部分常见场景,建议优先尝试。
下图展示了 Frida 绕过 SSL Pinning 的整体架构——从 PC 端注入脚本,到替换应用进程内的 SSL 校验逻辑:
Frida 绕过 SSL Pinning 架构图3.1 方案一:使用现成的通杀脚本
社区维护了多个成熟的 SSL Pinning 绕过脚本,覆盖了绝大多数 Java 层实现。对于大部分应用,直接使用这些脚本即可成功抓包,无需编写任何代码。
objection
objection 是基于 Frida 的自动化安全测试工具,内置了 SSL Pinning 绕过功能:
# 安装 objection
pip install objection
# 以 spawn 方式启动目标应用并进入交互模式
objection -g <package_name> explore
# 在 objection 交互终端中执行一键绕过
android sslpinning disable
原理:objection 内部会 Hook SSLContext.init()、TrustManagerImpl.verifyChain()、OkHttp CertificatePinner.check() 等关键方法,将校验逻辑替换为空实现。
frida-multiple-unpinning
这是目前覆盖面最广的通杀脚本之一,由社区持续维护:
# 直接使用(-f 表示 spawn 模式启动应用)
frida -U -f <package_name> -l frida-multiple-unpinning.js
它内部覆盖了以下实现的绕过:
TrustManagerImpl (Android 默认)OkHttp CertificatePinner (v3/v4)
3.2 方案二:全局 TrustManager 替换
核心思路:替换所有 SSLContext.init() 调用中的 TrustManager 参数为一个接受任意证书的空实现。这是所有 Java 层绕过的基础原理。
Java.perform(function () {
// 第一步:注册一个信任所有证书的 TrustManager 实现类
// Java.registerClass 会在 Dalvik/ART 虚拟机中动态创建一个新类
var TrustManager = Java.registerClass({
name: "com.frida.TrustAllManager",
implements: [Java.use("javax.net.ssl.X509TrustManager")],
methods: {
// 不校验客户端证书(服务器侧调用,这里不关心)
checkClientTrusted: function (chain, authType) { },
// 不校验服务器证书 —— 这是绕过 SSL Pinning 的关键
// 原始实现会在这里校验证书链/公钥哈希,抛出 CertificateException
// 我们直接 return,相当于"无条件信任"
checkServerTrusted: function (chain, authType) { },
// 返回空数组,表示不限制可接受的 CA
getAcceptedIssuers: function () {
return [];
}
}
});
// 实例化并包装为数组(SSLContext.init 接受 TrustManager[])
var trustAllManager = TrustManager.$new();
var trustManagers = Java.array("javax.net.ssl.TrustManager", [trustAllManager]);
// 第二步:Hook SSLContext.init(),替换第二个参数(TrustManager 数组)
// 所有 Java 层 HTTPS 实现最终都会调用这个方法来设置 SSL 参数
var SSLContext = Java.use("javax.net.ssl.SSLContext");
SSLContext.init.overload(
"[Ljavax.net.ssl.KeyManager;",
"[Ljavax.net.ssl.TrustManager;",
"java.security.SecureRandom"
).implementation = function (keyManager, tm, secureRandom) {
console.log("[*] SSLContext.init() intercepted - replacing TrustManager");
// 保留原始 keyManager(mTLS 场景需要),只替换 TrustManager
this.init(keyManager, trustManagers, secureRandom);
};
// 第三步:同时绕过 HostnameVerifier(主机名校验)
// 某些实现会在 TrustManager 之外额外校验主机名
var HostnameVerifier = Java.registerClass({
name: "com.frida.TrustAllHostnameVerifier",
implements: [Java.use("javax.net.ssl.HostnameVerifier")],
methods: {
verify: function (hostname, session) {
returntrue; // 任意主机名都通过
}
}
});
// 替换全局默认的 HostnameVerifier
var HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");
HttpsURLConnection.setDefaultHostnameVerifier(HostnameVerifier.$new());
// Hook 实例级别的 setHostnameVerifier,防止应用在连接时重新设置
HttpsURLConnection.setHostnameVerifier.implementation = function (verifier) {
console.log("[*] HostnameVerifier replaced");
this.setHostnameVerifier(HostnameVerifier.$new());
};
console.log("[+] Global TrustManager & HostnameVerifier bypass installed");
});
这个方案的本质:SSL Pinning 无论怎么实现,最终都要经过 SSLContext.init() 设置 TrustManager。替换这个入口就能拦截绝大多数 Java 层 pinning。但它对 Native 层(如 OpenSSL/BoringSSL)的 pinning 无效——那些实现根本不经过 Java API。
四、针对各种 SSL Pinning 实现的绕过方案
Android 应用实现 SSL Pinning 的方式多种多样,下图是 9 种主流实现的全景概览,按 Java 层和 Native 层分类,并标注了各自的 Frida Hook 点和绕过难度:
Android SSL Pinning 实现方式全景图4.1 Network Security Configuration
实现特征:
res/xml/network_security_config.xml 中配置 <pin-set> 节点AndroidManifest.xml 中声明 android:networkSecurityConfig="@xml/network_security_config"- Android 7.0 (API 24) 及以上系统原生支持,Google 官方推荐的 pinning 方式
识别方式:
# 反编译 APK 后搜索配置文件
# 使用 apktool 或 jadx 反编译
grep -r "pin-set" res/xml/
grep -r "networkSecurityConfig" AndroidManifest.xml
典型的配置文件示例:
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config>
<domainincludeSubdomains="true">api.example.com</domain>
<pin-setexpiration="2025-12-31">
<!-- 主 pin:服务器当前公钥的 SHA-256 哈希 -->
<pindigest="SHA-256">A7b...mP2=</pin>
<!-- 备份 pin:备用密钥的哈希,防止主密钥泄露时无法更新 -->
<pindigest="SHA-256">X3f...9kQ=</pin>
</pin-set>
</domain-config>
</network-security-config>
绕过方案:Hook NetworkSecurityTrustManager 的证书校验方法,使其直接返回而不执行 Pin 校验。
Java.perform(function () {
try {
// Android 7.0+ 的 NetworkSecurityConfig 框架使用 NetworkSecurityTrustManager
// 作为实际执行 pin 校验的 TrustManager 实现
var NetworkSecurityTrustManager = Java.use(
"android.security.net.config.NetworkSecurityTrustManager"
);
// 两参数版本的 checkServerTrusted(基础校验)
NetworkSecurityTrustManager.checkServerTrusted.overload(
"[Ljava.security.cert.X509Certificate;",
"java.lang.String"
).implementation = function (certs, authType) {
console.log("[*] NetworkSecurityTrustManager.checkServerTrusted() bypassed");
// 直接 return,不执行任何校验
return;
};
// 三参数版本的 checkServerTrusted(扩展校验,带 hostname)
// 返回值是受信任的证书列表
NetworkSecurityTrustManager.checkServerTrusted.overload(
"[Ljava.security.cert.X509Certificate;",
"java.lang.String",
"java.lang.String"
).implementation = function (certs, authType, host) {
console.log("[*] NetworkSecurityTrustManager.checkServerTrusted(host) bypassed for: " + host);
// 返回空列表表示校验通过
return Java.use("java.util.ArrayList").$new();
};
} catch (e) {
console.log("[-] NetworkSecurityTrustManager not found: " + e);
}
try {
// RootTrustManager 是 NetworkSecurityConfig 框架的顶层分发器
// 它根据域名匹配将请求路由到不同的 NetworkSecurityTrustManager
var RootTrustManager = Java.use(
"android.security.net.config.RootTrustManager"
);
RootTrustManager.checkServerTrusted.overload(
"[Ljava.security.cert.X509Certificate;",
"java.lang.String"
).implementation = function (certs, authType) {
console.log("[*] RootTrustManager.checkServerTrusted() bypassed");
return;
};
} catch (e) {
console.log("[-] RootTrustManager not found: " + e);
}
});
替代方案(非 Frida):直接修改反编译后的 network_security_config.xml,移除 <pin-set> 节点后重打包签名。这种方式更简单,但会触发应用的签名校验(如果有的话)。
4.2 OkHttp CertificatePinner
实现特征:
- 使用
okhttp3.CertificatePinner 或旧版 com.squareup.okhttp.CertificatePinner - 调用
.certificatePinner(pinner) 构建 OkHttpClient - 使用极其广泛:Retrofit(目前 Android 最主流的网络框架)底层就是 OkHttp
识别方式:
# 在反编译代码中搜索特征字符串
grep -r "CertificatePinner" --include="*.java" --include="*.smali"
# 搜索 pin 哈希格式(sha256/ 前缀是 OkHttp 特有的)
grep -r "sha256/" --include="*.java" --include="*.smali"
绕过方案:Hook CertificatePinner.check() 方法使其直接返回。该方法校验失败时会抛出 SSLPeerUnverifiedException,我们让它直接 return 就跳过了校验。
Java.perform(function () {
// ========== OkHttp 3.x ==========
try {
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
// check(String hostname, List<Certificate> peerCertificates)
// 这是 OkHttp 3.x 的标准签名
CertificatePinner.check.overload(
"java.lang.String",
"java.util.List"
).implementation = function (hostname, peerCertificates) {
console.log("[*] OkHttp3 CertificatePinner.check() bypassed for: " + hostname);
// 原方法校验失败会抛 SSLPeerUnverifiedException
// 直接 return 表示"校验通过"(void 方法)
return;
};
// ========== OkHttp 4.x (Kotlin) ==========
// OkHttp 4.x 使用 Kotlin 重写,方法签名变为 check$okhttp
// 参数类型也不同:第二个参数是 Function0(惰性求值的证书列表)
try {
CertificatePinner.check$okhttp.overload(
"java.lang.String",
"kotlin.jvm.functions.Function0"
).implementation = function (hostname, fn) {
console.log("[*] OkHttp4 CertificatePinner.check$okhttp() bypassed for: " + hostname);
return;
};
} catch (e) {
// OkHttp 3.x 没有这个方法,忽略
}
} catch (e) {
console.log("[-] okhttp3.CertificatePinner not found: " + e);
}
// OkHttp 2.x 包名为 com.squareup.okhttp(极老版本,现代应用已基本不再使用)
// 若确需兼容,将上方 "okhttp3" 替换为 "com.squareup.okhttp" 即可
});
处理混淆场景:很多应用使用 ProGuard/R8 混淆了 OkHttp 的类名(如 okhttp3.CertificatePinner 变成了 a.b.c)。此时直接 Hook 类名会失败,需要通过特征码定位混淆后的类名:
Java.perform(function () {
// 枚举所有已加载的类,通过结构特征匹配 CertificatePinner
// CertificatePinner 的特征:
// 1. 有 check 方法(参数为 String + List)
// 2. 内部持有 Map 或 Set 类型字段(存储 hostname -> pins 映射)
// 3. 代码中包含 "sha256/" 字符串常量
Java.enumerateLoadedClasses({
onMatch: function (className) {
// 混淆后类名通常很短(如 a.b, x.y.z)
if (className.match(/^[a-z]\.[a-z]$/)) {
try {
var cls = Java.use(className);
var methods = cls.class.getDeclaredMethods();
for (var i = 0; i < methods.length; i++) {
if (methods[i].toString().indexOf("check") !== -1) {
var fields = cls.class.getDeclaredFields();
for (var j = 0; j < fields.length; j++) {
if (fields[j].getType().getName() === "java.util.Map" ||
fields[j].getType().getName() === "java.util.Set") {
console.log("[?] Possible CertificatePinner: " + className);
}
}
}
}
} catch (e) { }
}
},
onComplete: function () { }
});
});
更高效的定位方式:直接搜索 smali 代码中的 "sha256/" 字符串常量,然后反向追踪使用该常量的类——这个类极大概率就是混淆后的 CertificatePinner。
4.3 自定义 TrustManager
实现特征:
- 应用自己实现了
javax.net.ssl.X509TrustManager 接口 - 在
checkServerTrusted() 方法中编写自定义校验逻辑(比对证书指纹、公钥哈希等) - 校验失败时抛出
CertificateException
识别方式:
# 搜索自定义 TrustManager 实现
grep -r "checkServerTrusted" --include="*.java"
grep -r "X509TrustManager" --include="*.java"
绕过方案:同时 Hook 系统默认的 TrustManagerImpl 和所有自定义 X509TrustManager 实现类。
Java.perform(function () {
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
// 1. Hook 系统默认的 TrustManagerImpl(Android 内置的 Conscrypt 实现)
// verifyChain 是实际执行证书链验证的核心方法
TrustManagerImpl.verifyChain.implementation = function (
untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData
) {
console.log("[*] TrustManagerImpl.verifyChain() bypassed for: " + host);
// 直接返回传入的证书链,表示"全部信任"
return untrustedChain;
};
// 2. 枚举并 Hook 所有自定义 X509TrustManager 实现类
// 这样即使应用使用了自定义实现也能被覆盖
Java.enumerateLoadedClasses({
onMatch: function (className) {
try {
var cls = Java.use(className);
// 检查是否实现了 X509TrustManager 接口
if (cls.class && X509TrustManager.class.isAssignableFrom(cls.class)) {
// 排除系统类和我们自己注入的类
if (className.indexOf("com.android.") === -1 &&
className.indexOf("com.frida.") === -1) {
console.log("[*] Found custom TrustManager: " + className);
// Hook 两参数版本
try {
cls.checkServerTrusted.overload(
"[Ljava.security.cert.X509Certificate;",
"java.lang.String"
).implementation = function () {
console.log("[*] " + className + ".checkServerTrusted() bypassed");
return;
};
} catch (e) { }
// Hook 三参数版本(Android 扩展接口 X509ExtendedTrustManager)
try {
cls.checkServerTrusted.overload(
"[Ljava.security.cert.X509Certificate;",
"java.lang.String",
"java.lang.String"
).implementation = function () {
console.log("[*] " + className + ".checkServerTrusted(host) bypassed");
return Java.use("java.util.ArrayList").$new();
};
} catch (e) { }
}
}
} catch (e) { }
},
onComplete: function () {
console.log("[+] Custom TrustManager scan complete");
}
});
});
4.4 自定义 HostnameVerifier
实现特征:
- 实现
javax.net.ssl.HostnameVerifier 接口 - 在
verify(hostname, session) 方法中比对证书中的主机名或公钥指纹
绕过方案:枚举所有 HostnameVerifier 实现类,强制 verify() 返回 true。
Java.perform(function () {
var HostnameVerifier = Java.use("javax.net.ssl.HostnameVerifier");
// 枚举所有 HostnameVerifier 实现类并 Hook
Java.enumerateLoadedClasses({
onMatch: function (className) {
try {
var cls = Java.use(className);
if (cls.class && HostnameVerifier.class.isAssignableFrom(cls.class)) {
// 排除我们自己注入的类
if (className.indexOf("com.frida.") === -1) {
cls.verify.overload(
"java.lang.String",
"javax.net.ssl.SSLSession"
).implementation = function (hostname, session) {
console.log("[*] " + className + ".verify() bypassed for: " + hostname);
returntrue; // 主机名校验始终通过
};
}
}
} catch (e) { }
},
onComplete: function () { }
});
// 单独处理 OkHttp 的内置 HostnameVerifier
// OkHttp 使用自己的 OkHostnameVerifier,不一定被上面的枚举覆盖
try {
var OkHostnameVerifier = Java.use("okhttp3.internal.tls.OkHostnameVerifier");
OkHostnameVerifier.verify.overload(
"java.lang.String",
"javax.net.ssl.SSLSession"
).implementation = function (hostname, session) {
console.log("[*] OkHostnameVerifier.verify() bypassed for: " + hostname);
returntrue;
};
// OkHostnameVerifier 还有一个直接接受证书的重载
OkHostnameVerifier.verify.overload(
"java.lang.String",
"java.security.cert.X509Certificate"
).implementation = function (hostname, cert) {
console.log("[*] OkHostnameVerifier.verify(cert) bypassed for: " + hostname);
returntrue;
};
} catch (e) { }
});
4.5 嵌入证书文件 (KeyStore 方式)
实现特征:
assets/ 或 res/raw/ 目录下存在证书文件:.cer、.pem、.bks(BouncyCastle KeyStore)、.p12(PKCS12)- 代码中通过
KeyStore.load() 加载这些证书 - 通过
TrustManagerFactory.init(keyStore) 构建只信任特定证书的 TrustManager
识别方式:
# 反编译 APK 后搜索证书文件
find . -name "*.cer" -o -name "*.pem" -o -name "*.bks" -o -name "*.p12" -o -name "*.crt"
# 搜索代码中的 KeyStore 使用
grep -r "TrustManagerFactory" --include="*.java"
grep -r "KeyStore" --include="*.java"
绕过方案:Hook TrustManagerFactory.init(),忽略应用自定义的 KeyStore,改用系统默认信任存储。
Java.perform(function () {
// Hook TrustManagerFactory.init(),将参数替换为 null
// 传入 null 表示使用系统默认信任存储(包含我们安装的 Burp CA 证书)
var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
TrustManagerFactory.init.overload("java.security.KeyStore").implementation = function (keyStore) {
console.log("[*] TrustManagerFactory.init() intercepted - using default system trust store");
this.init(null); // null = 系统默认 KeyStore
};
// 可选:监控 KeyStore 加载过程,获取额外信息
var KeyStore = Java.use("java.security.KeyStore");
KeyStore.getCertificate.implementation = function (alias) {
console.log("[*] KeyStore.getCertificate() called for alias: " + alias);
returnthis.getCertificate(alias);
};
// 建议同时配合全局 TrustManager 替换(见 3.2)效果更好
console.log("[+] KeyStore-based pinning bypass installed");
});
4.6 第三方库 (TrustKit / Conscrypt)
TrustKit
TrustKit 是一个跨平台的 SSL Pinning 库,在 Android 上它封装了标准的 TrustManager,额外提供了 pinning 失败上报功能(会向服务器报告绕过尝试)。
识别方式:
grep -r "TrustKit" --include="*.java" --include="*.smali"
grep -r "com.datatheorem.android.trustkit" --include="*.java"
绕过方案:
Java.perform(function () {
try {
// Hook TrustKit 的核心校验类
var PinningTrustManager = Java.use(
"com.datatheorem.android.trustkit.pinning.PinningTrustManager"
);
PinningTrustManager.checkServerTrusted.implementation = function (chain, authType) {
console.log("[*] TrustKit PinningTrustManager.checkServerTrusted() bypassed");
return; // 跳过 pin 校验
};
} catch (e) {
console.log("[-] TrustKit not found: " + e);
}
try {
// 同时禁用 TrustKit 的失败上报功能
// 防止服务器端收到"pinning 被绕过"的告警
var BackgroundReporter = Java.use(
"com.datatheorem.android.trustkit.reporting.BackgroundReporter"
);
BackgroundReporter.pinValidationFailed.implementation = function () {
console.log("[*] TrustKit pin failure report suppressed");
return; // 不发送上报请求
};
} catch (e) { }
});
Conscrypt
Conscrypt 是 Google 维护的 Java 安全库(Java Security Provider),底层使用 BoringSSL。Android 系统内置了 Conscrypt 作为默认的 TLS 实现,部分应用也会额外引入独立版本。
识别方式:
grep -r "org.conscrypt" --include="*.java" --include="*.smali"
grep -r "Conscrypt.newTrustManager" --include="*.java"
绕过方案:
Java.perform(function () {
try {
// 独立引入的 Conscrypt(第三方库版本)
var ConscryptTrustManager = Java.use("org.conscrypt.TrustManagerImpl");
ConscryptTrustManager.verifyChain.implementation = function (
untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData
) {
console.log("[*] Conscrypt TrustManagerImpl.verifyChain() bypassed for: " + host);
return untrustedChain; // 返回原证书链,表示信任
};
} catch (e) {
console.log("[-] Conscrypt (external) TrustManagerImpl not found: " + e);
}
try {
// Android 系统内置的 Conscrypt(包名前缀为 com.android.org.conscrypt)
var PlatformConscrypt = Java.use("com.android.org.conscrypt.TrustManagerImpl");
PlatformConscrypt.verifyChain.implementation = function (
untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData
) {
console.log("[*] Platform Conscrypt verifyChain() bypassed for: " + host);
return untrustedChain;
};
} catch (e) { }
});
4.7 WebView SSL Pinning
实现特征:
- 在
WebViewClient.onReceivedSslError() 回调中进行证书校验 - 或在
WebViewClient.shouldInterceptRequest() 中拦截请求后使用 OkHttp + CertificatePinner 发起
绕过方案:Hook onReceivedSslError,强制调用 handler.proceed() 继续加载。
Java.perform(function () {
var WebViewClient = Java.use("android.webkit.WebViewClient");
// Hook 基类的 onReceivedSslError,强制 proceed
WebViewClient.onReceivedSslError.implementation = function (view, handler, error) {
console.log("[*] WebView SSL error bypassed: " + error.toString());
handler.proceed(); // 忽略 SSL 错误,继续加载页面
};
// 枚举所有自定义 WebViewClient 子类
// 因为子类可能覆盖了 onReceivedSslError 并在内部做额外校验
Java.enumerateLoadedClasses({
onMatch: function (className) {
try {
var cls = Java.use(className);
if (cls.class.getSuperclass() &&
cls.class.getSuperclass().getName() === "android.webkit.WebViewClient") {
console.log("[*] Found WebViewClient subclass: " + className);
try {
cls.onReceivedSslError.implementation = function (view, handler, error) {
console.log("[*] " + className + ".onReceivedSslError() bypassed");
handler.proceed();
};
} catch (e) { }
}
} catch (e) { }
},
onComplete: function () { }
});
});
注意:如果 WebView 的 SSL Pinning 是通过 shouldInterceptRequest 拦截后用 OkHttp 发起请求来实现的,那么需要同时绕过 OkHttp 的 CertificatePinner(见 4.2 节)。
4.8 Native 层 SSL Pinning(重点难点)
实现特征:
- 应用使用 NDK 编写的网络库(如 libcurl、BoringSSL、OpenSSL)
- 证书校验逻辑在
.so 文件中,Java 层完全看不到 pinning 相关代码 - 使用
SSL_CTX_set_verify() 或 SSL_CTX_set_custom_verify() 注册 C/C++ 层的验证回调 - 常见于金融类、支付类、游戏类、短视频类应用——这些应用追求更高的逆向门槛
识别方式:
# 查看应用使用的 native 库
ls lib/arm64-v8a/
# 关注: libssl.so, libboringssl.so, libttboringssl.so, libcurl.so, libnative-lib.so 等
# 使用 radare2 分析 SO 文件中的 SSL 相关导入/导出
rabin2 -E libboringssl.so | grep -iE "SSL_CTX_set|SSL_get_verify|custom_verify"
rabin2 -i libnative-lib.so | grep -i ssl
4.8.1 系统 OpenSSL / BoringSSL(libssl.so)
Android 系统内置的 TLS 库(底层是 BoringSSL)以 libssl.so 对外暴露,大多数 NDK 应用直接链接系统库。
绕过方案 A:将验证模式强制改为 SSL_VERIFY_NONE。
// Hook SSL_CTX_set_verify —— 设置 SSL 上下文的验证模式
// mode = 0 (SSL_VERIFY_NONE) 表示不校验对端证书
Interceptor.attach(Module.findExportByName("libssl.so", "SSL_CTX_set_verify"), {
onEnter: function (args) {
console.log("[*] SSL_CTX_set_verify() called, mode=" + args[1].toInt32());
args[1] = ptr(0x0); // SSL_VERIFY_NONE
args[2] = ptr(0x0); // 清除自定义验证回调
}
});
// 同样 Hook SSL_set_verify(per-connection 级别,覆盖更细粒度的设置)
Interceptor.attach(Module.findExportByName("libssl.so", "SSL_set_verify"), {
onEnter: function (args) {
args[1] = ptr(0x0);
args[2] = ptr(0x0);
console.log("[*] SSL_set_verify() -> NONE");
}
});
绕过方案 B:强制验证结果为成功。
// SSL_get_verify_result 返回 X509_V_OK (0) 表示验证通过
Interceptor.attach(Module.findExportByName("libssl.so", "SSL_get_verify_result"), {
onLeave: function (retval) {
retval.replace(ptr(0x0)); // 强制返回 X509_V_OK = 0
console.log("[*] SSL_get_verify_result -> X509_V_OK");
}
});
// X509_verify_cert 返回 1 表示验证通过
Interceptor.attach(Module.findExportByName("libcrypto.so", "X509_verify_cert"), {
onLeave: function (retval) {
retval.replace(ptr(0x1));
console.log("[*] X509_verify_cert -> 1 (success)");
}
});
绕过方案 C:Hook 自定义验证回调(需配合逆向分析定位回调地址)。
// 先通过 Hook SSL_CTX_set_verify 捕获回调地址,再替换其返回值
Interceptor.attach(Module.findExportByName("libssl.so", "SSL_CTX_set_verify"), {
onEnter: function (args) {
if (!args[2].isNull()) {
var callbackAddr = args[2];
console.log("[*] Custom verify callback @ " + callbackAddr +
" (" + Process.findModuleByAddress(callbackAddr).name + ")");
Interceptor.attach(callbackAddr, {
onLeave: function (retval) {
retval.replace(ptr(0x1)); // 强制返回 1(通过)
console.log("[*] Custom verify callback -> 1");
}
});
}
}
});
静态编译场景:如果应用将 BoringSSL 静态编译进了自己的 SO 文件(如 libnative-lib.so),函数不会出现在 libssl.so 导出表中。需要用 Ghidra/IDA 通过字符串引用("ssl_verify_peer_cert" 等错误日志)或函数签名特征定位后,直接 Hook 绝对地址。
4.8.2 独立打包的 BoringSSL(libboringssl.so)
部分应用(尤其是 Google 系、Chromium 内核的 WebView、以及一些高安全性应用)会在 APK 中打包独立的 libboringssl.so,而不是链接系统 libssl.so。相比 OpenSSL,BoringSSL 引入了更灵活的 SSL_CTX_set_custom_verify API,其回调的返回值类型与 OpenSSL 不同:
| |
|---|
ssl_verify_ok | |
ssl_verify_invalid | |
ssl_verify_retry | |
var libName = "libboringssl.so";
// --- 1. Hook SSL_CTX_set_custom_verify(BoringSSL 专有 API)---
// 原型: void SSL_CTX_set_custom_verify(SSL_CTX *ctx, int mode,
// enum ssl_verify_result_t (*callback)(SSL *ssl, uint8_t *out_alert))
var customVerifyPtr = Module.findExportByName(libName, "SSL_CTX_set_custom_verify");
if (customVerifyPtr) {
Interceptor.attach(customVerifyPtr, {
onEnter: function (args) {
console.log("[*] SSL_CTX_set_custom_verify mode=" + args[1].toInt32());
if (!args[2].isNull()) {
// 注入一个始终返回 ssl_verify_ok (0) 的假回调
// 'int' 对应枚举类型 ssl_verify_result_t
var fakeVerify = new NativeCallback(function (ssl, out_alert) {
console.log("[*] BoringSSL custom verify callback -> ssl_verify_ok");
return0; // ssl_verify_ok
}, "int", ["pointer", "pointer"]);
args[2] = fakeVerify;
}
}
});
} else {
console.log("[-] SSL_CTX_set_custom_verify not exported in " + libName);
}
// --- 2. Hook SSL_CTX_set_cert_verify_callback(另一套回调机制)---
// 原型: void SSL_CTX_set_cert_verify_callback(SSL_CTX *ctx,
// int (*cb)(X509_STORE_CTX *, void *), void *arg)
// 返回 1 = 成功, 0 = 失败
var certVerifyCbPtr = Module.findExportByName(libName, "SSL_CTX_set_cert_verify_callback");
if (certVerifyCbPtr) {
Interceptor.attach(certVerifyCbPtr, {
onEnter: function (args) {
if (!args[1].isNull()) {
var fakeCallback = new NativeCallback(function (storeCtx, arg) {
console.log("[*] cert_verify_callback -> 1 (always pass)");
return1;
}, "int", ["pointer", "pointer"]);
args[1] = fakeCallback;
}
}
});
}
// --- 3. 兜底:强制验证模式和验证结果 ---
["SSL_CTX_set_verify", "SSL_set_verify"].forEach(function (fnName) {
var addr = Module.findExportByName(libName, fnName);
if (addr) {
Interceptor.attach(addr, {
onEnter: function (args) {
args[1] = ptr(0x0); // SSL_VERIFY_NONE
args[2] = ptr(0x0);
console.log("[*] " + libName + " " + fnName + " -> NONE");
}
});
}
});
var getVerifyResultPtr = Module.findExportByName(libName, "SSL_get_verify_result");
if (getVerifyResultPtr) {
Interceptor.attach(getVerifyResultPtr, {
onLeave: function (retval) {
retval.replace(ptr(0x0)); // X509_V_OK = 0
}
});
}
4.8.3 字节跳动系应用:libttboringssl.so
libttboringssl.so 是字节跳动(ByteDance)基于 BoringSSL 深度定制的 TLS 库,抖音、TikTok、今日头条、西瓜视频、飞书等旗下主流应用均使用此库。相比标准 BoringSSL,它有以下特点:
- 导出符号精简:只暴露必要的 SSL API,内部验证函数多数为非导出符号
- 额外 Pinning 逻辑:在 TLS 握手握手多阶段注入自定义证书锁定检查
Step 1:枚举可用的导出符号
# 先确认库是否存在及其导出情况
rabin2 -E lib/arm64-v8a/libttboringssl.so | grep -iE "SSL|verify|cert" | head -30
Step 2:Hook 导出的标准验证函数
var ttLib = "libttboringssl.so";
// 等待库加载(部分应用延迟加载 so)
functionhookTTBoringSSL() {
var ttMod = Process.findModuleByName(ttLib);
if (!ttMod) {
console.log("[-] " + ttLib + " not loaded yet");
return;
}
console.log("[+] " + ttLib + " base: " + ttMod.base + " size: " + ttMod.size);
// 尝试 Hook 所有可能的验证入口
var targets = {
// BoringSSL 标准接口(通常仍然导出)
"SSL_CTX_set_custom_verify": function (args) {
if (!args[2].isNull()) {
args[2] = new NativeCallback(function (ssl, out_alert) {
console.log("[*] ttBoringSSL custom_verify -> ssl_verify_ok");
return0;
}, "int", ["pointer", "pointer"]);
}
},
"SSL_CTX_set_cert_verify_callback": function (args) {
if (!args[1].isNull()) {
args[1] = new NativeCallback(function (storeCtx, arg) {
console.log("[*] ttBoringSSL cert_verify_callback -> 1");
return1;
}, "int", ["pointer", "pointer"]);
}
},
"SSL_CTX_set_verify": function (args) {
args[1] = ptr(0x0);
args[2] = ptr(0x0);
console.log("[*] ttBoringSSL SSL_CTX_set_verify -> NONE");
},
"SSL_set_verify": function (args) {
args[1] = ptr(0x0);
args[2] = ptr(0x0);
console.log("[*] ttBoringSSL SSL_set_verify -> NONE");
}
};
Object.keys(targets).forEach(function (fnName) {
var addr = Module.findExportByName(ttLib, fnName);
if (addr) {
console.log("[+] Hooking " + fnName + " @ " + addr);
Interceptor.attach(addr, { onEnter: targets[fnName] });
} else {
console.log("[-] " + fnName + " not exported in " + ttLib);
}
});
// 强制 SSL_get_verify_result 返回 X509_V_OK
var getVerifyAddr = Module.findExportByName(ttLib, "SSL_get_verify_result");
if (getVerifyAddr) {
Interceptor.attach(getVerifyAddr, {
onLeave: function (retval) { retval.replace(ptr(0x0)); }
});
}
}
// 如果库已加载则立即执行,否则监听模块加载事件
if (Process.findModuleByName(ttLib)) {
hookTTBoringSSL();
} else {
// 监听 dlopen,等待 libttboringssl.so 被加载
var dlopenPtr = Module.findExportByName(null, "android_dlopen_ext") ||
Module.findExportByName(null, "dlopen");
if (dlopenPtr) {
Interceptor.attach(dlopenPtr, {
onEnter: function (args) {
this.libPath = args[0].readCString();
},
onLeave: function (retval) {
if (this.libPath && this.libPath.indexOf("ttboringssl") !== -1) {
console.log("[+] " + ttLib + " loaded, installing hooks...");
hookTTBoringSSL();
}
}
});
}
}
Step 3:符号被剥离时——通过特征字节定位验证函数
当 SSL_CTX_set_custom_verify 等函数未导出时,可以借助库内特征字符串定位相关函数,再配合 IDA/Ghidra 进行静态分析后 Hook 偏移地址:
var ttMod = Process.findModuleByName("libttboringssl.so");
if (ttMod) {
// 在库的内存区域扫描特征字符串,辅助定位验证相关代码
var signatures = [
// "ssl_verify" 系列错误日志字符串(BoringSSL 源码中固定出现)
{ name: "CERTIFICATE_VERIFY_FAILED", hex: "43455254494649434154455f564552494659_4641494c4544" },
{ name: "ssl_verify_peer_cert", hex: "73736c5f766572696679_706565725f63657274" }
];
signatures.forEach(function (sig) {
// 找到字符串后,用 IDA/Ghidra 交叉引用到调用函数,再 Hook 该偏移
console.log("[*] Search for '" + sig.name + "' to locate verify function via xref");
});
// 示例:若已通过逆向确认某版本的 ssl_verify_peer_cert 偏移为 0x3A5678
// var verifyOffset = 0x3A5678;
// Interceptor.attach(ttMod.base.add(verifyOffset), {
// onLeave: function(retval) { retval.replace(ptr(0x0)); }
// });
}
实战建议:抖音/TikTok 每次版本更新可能调整偏移,建议优先依赖导出符号 Hook(Step 2)。若导出符号均失效,可配合 Frida CodeShare 搜索社区针对该版本的现成脚本,或用 Ghidra 分析新版 so 后更新偏移。
4.8.4 libcurl(C/C++ 通用网络框架)
// libcurl 通过 curl_easy_setopt 设置 SSL 验证选项
// CURLOPT_SSL_VERIFYPEER = 64 —— 是否校验服务器证书
// CURLOPT_SSL_VERIFYHOST = 81 —— 是否校验主机名(2 = 开启,0 = 关闭)
Interceptor.attach(Module.findExportByName("libcurl.so", "curl_easy_setopt"), {
onEnter: function (args) {
var option = args[1].toInt32();
if (option === 64 || option === 81) {
console.log("[*] curl_easy_setopt option=" + option + " -> 0 (disabled)");
args[2] = ptr(0x0);
}
}
});
4.9 双向认证 (mTLS) 绕过
实现特征:
- 应用在
assets/ 中嵌入了客户端证书(.p12、.pfx、.bks) - 代码中同时配置了
KeyManager(客户端证书)和 TrustManager(服务器验证) - 服务器端会校验客户端证书,拒绝没有有效客户端证书的连接
核心难点:mTLS 不仅需要绕过客户端对服务器的验证(SSL Pinning),还需要提取客户端证书和私钥交给抓包工具(Burp Suite),否则服务器会拒绝 Burp 的连接。
下图展示了完整的 mTLS 绕过三步流程:
mTLS 双向认证绕过流程Step 1:Hook 提取客户端证书密码和别名信息。
Java.perform(function () {
// Hook KeyStore.load() —— 拦截证书加载过程
// 应用在这里加载 .p12/.bks 文件,我们可以获取密码
var KeyStore = Java.use("java.security.KeyStore");
KeyStore.load.overload("java.io.InputStream", "[C").implementation = function (stream, password) {
console.log("[*] KeyStore.load() called");
// 打印密码(char[] 转 String)
if (password) {
console.log(" Password: " + Java.use("java.lang.String").$new(password));
}
// 先执行原始加载
this.load(stream, password);
// 枚举 KeyStore 中的所有条目
var aliases = this.aliases();
while (aliases.hasMoreElements()) {
var alias = aliases.nextElement().toString();
console.log(" Alias: " + alias);
// 检查是否包含私钥(客户端证书必须包含私钥)
if (this.isKeyEntry(alias)) {
console.log(" -> Contains private key!");
}
}
};
// Hook KeyManagerFactory.init() 获取密码确认
var KeyManagerFactory = Java.use("javax.net.ssl.KeyManagerFactory");
KeyManagerFactory.init.overload(
"java.security.KeyStore", "[C"
).implementation = function (keyStore, password) {
console.log("[*] KeyManagerFactory.init()");
if (password) {
console.log(" Password: " + Java.use("java.lang.String").$new(password));
}
this.init(keyStore, password);
};
});
Step 2:导出客户端证书为 PKCS12 格式文件。
Java.perform(function () {
var KeyStore = Java.use("java.security.KeyStore");
KeyStore.load.overload("java.io.InputStream", "[C").implementation = function (stream, password) {
// 先执行原始加载
this.load(stream, password);
// 将完整的 KeyStore(包含私钥 + 证书)导出为 PKCS12 文件
var FileOutputStream = Java.use("java.io.FileOutputStream");
var fos = FileOutputStream.$new("/data/local/tmp/client_cert.p12");
this.store(fos, password); // 使用原密码保护导出的文件
fos.close();
console.log("[+] Client certificate exported to /data/local/tmp/client_cert.p12");
if (password) {
console.log("[+] Password: " + Java.use("java.lang.String").$new(password));
}
};
});
Step 3:将导出的证书从设备拉取到 PC,导入 Burp Suite。
# 从设备拉取导出的客户端证书
adb pull /data/local/tmp/client_cert.p12
# 在 Burp Suite 中导入客户端证书:
# Settings -> Network -> TLS -> Client TLS Certificates -> Add
# Destination host: *.example.com(或具体域名)
# Certificate file: 选择导出的 client_cert.p12
# Password: 输入 Step 1 中获取的密码
重要:导入客户端证书后,还需要同时运行 SSL Pinning 绕过脚本(绕过客户端对服务器的校验),双管齐下才能实现 mTLS 场景下的完整抓包。
五、高级对抗场景
5.1 证书校验代码被混淆
当应用使用 ProGuard/R8 混淆后,类名变成了 a.b.c、方法名变成了 a(),直接按类名 Hook 会失败。
应对策略:
Java.perform(function () {
// 策略 1:通过接口枚举 —— 接口名不会被混淆
// Java 接口是 API 的一部分,ProGuard 不会重命名标准库接口
var X509TM = Java.use("javax.net.ssl.X509TrustManager");
Java.choose(X509TM.class.getName(), {
onMatch: function (instance) {
console.log("[*] X509TrustManager instance: " + instance.getClass().getName());
// 输出混淆后的实际类名,然后可以针对性 Hook
},
onComplete: function () { }
});
// 策略 2:Hook 异常构造函数,通过堆栈追溯校验位置
// SSL Pinning 失败时必定会抛出 CertificateException
// 我们 Hook 这个异常的构造函数,打印调用栈
var CertificateException = Java.use("java.security.cert.CertificateException");
CertificateException.$init.overload("java.lang.String").implementation = function (msg) {
console.log("[!] CertificateException: " + msg);
// 打印完整的 Java 调用栈 —— 栈中会包含混淆后的类名和方法名
console.log(Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Exception").$new()
));
this.$init(msg);
};
// 策略 3:Hook SSLPeerUnverifiedException(OkHttp 抛出的异常)
var SSLPeerUnverifiedException = Java.use("javax.net.ssl.SSLPeerUnverifiedException");
SSLPeerUnverifiedException.$init.overload("java.lang.String").implementation = function (msg) {
console.log("[!] SSLPeerUnverifiedException: " + msg);
console.log(Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Exception").$new()
));
this.$init(msg);
};
});
实战技巧:拿到异常堆栈后,找到栈中应用自己的类(非系统类),那就是执行校验的位置。接着用 jadx 查看该类的反编译代码,定位 check 或 verify 方法,编写针对性的 Hook。
5.2 多重 Pinning 组合
某些高安全应用(银行、支付、证券)同时使用多种 pinning 机制,构成"多层防御"。例如:Java 层用 OkHttp CertificatePinner + Network Security Config,Native 层再用 BoringSSL 做一次校验。
完整通杀脚本模板(覆盖 Java 层 + Native 层):
// universal_ssl_bypass.js
// 一站式覆盖 Java 层 + Native 层的完整绕过方案
Java.perform(function () {
console.log("[*] === Universal SSL Pinning Bypass ===");
// ========== 1. 全局 TrustManager 替换 ==========
var TrustAllManager = Java.registerClass({
name: "com.bypass.TrustAllManager",
implements: [Java.use("javax.net.ssl.X509TrustManager")],
methods: {
checkClientTrusted: function (chain, authType) { },
checkServerTrusted: function (chain, authType) { },
getAcceptedIssuers: function () { return []; }
}
});
var trustAllManagers = Java.array(
"javax.net.ssl.TrustManager", [TrustAllManager.$new()]
);
var SSLContext = Java.use("javax.net.ssl.SSLContext");
SSLContext.init.overload(
"[Ljavax.net.ssl.KeyManager;",
"[Ljavax.net.ssl.TrustManager;",
"java.security.SecureRandom"
).implementation = function (km, tm, sr) {
console.log("[+] SSLContext.init() -> TrustAll");
this.init(km, trustAllManagers, sr);
};
// ========== 2. HostnameVerifier ==========
var TrustAllVerifier = Java.registerClass({
name: "com.bypass.TrustAllVerifier",
implements: [Java.use("javax.net.ssl.HostnameVerifier")],
methods: {
verify: function () { returntrue; }
}
});
var HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");
HttpsURLConnection.setDefaultHostnameVerifier(TrustAllVerifier.$new());
// ========== 3. OkHttp ==========
try {
var CertPinner = Java.use("okhttp3.CertificatePinner");
CertPinner.check.overload("java.lang.String", "java.util.List").implementation =
function () { console.log("[+] OkHttp3 CertificatePinner bypassed"); };
} catch (e) { }
// ========== 4. TrustManagerImpl (Android / Conscrypt) ==========
["com.android.org.conscrypt.TrustManagerImpl",
"org.conscrypt.TrustManagerImpl"].forEach(function (clsName) {
try {
var TrustManagerImpl = Java.use(clsName);
TrustManagerImpl.verifyChain.implementation = function (
untrusted, trustAnchor, host, clientAuth, ocspData, tlsSctData
) {
console.log("[+] " + clsName + ".verifyChain() bypassed for: " + host);
return untrusted;
};
} catch (e) { }
});
// ========== 5. NetworkSecurityConfig ==========
try {
var NSTM = Java.use("android.security.net.config.NetworkSecurityTrustManager");
NSTM.checkServerTrusted.overload(
"[Ljava.security.cert.X509Certificate;", "java.lang.String"
).implementation = function () {
console.log("[+] NetworkSecurityTrustManager bypassed");
};
} catch (e) { }
// ========== 6. WebView ==========
var WebViewClient = Java.use("android.webkit.WebViewClient");
WebViewClient.onReceivedSslError.implementation = function (view, handler, error) {
console.log("[+] WebView SSL error bypassed");
handler.proceed();
};
// ========== 7. TrustKit ==========
try {
var PTM = Java.use(
"com.datatheorem.android.trustkit.pinning.PinningTrustManager"
);
PTM.checkServerTrusted.implementation = function () {
console.log("[+] TrustKit bypassed");
};
} catch (e) { }
console.log("[*] === Java Layer Bypass Complete ===");
});
// ========== 8. Native 层 (libssl.so / libboringssl.so / libttboringssl.so) ==========
var sslLibNames = ["libssl.so", "libboringssl.so", "libttboringssl.so"];
sslLibNames.forEach(function (libName) {
var mod = Process.findModuleByName(libName);
if (!mod) return;
// 禁用验证模式(OpenSSL / BoringSSL 通用)
var addr = Module.findExportByName(libName, "SSL_CTX_set_verify");
if (addr) {
Interceptor.attach(addr, {
onEnter: function (args) {
args[1] = ptr(0x0); // SSL_VERIFY_NONE
args[2] = ptr(0x0);
console.log("[+] " + libName + " SSL_CTX_set_verify -> NONE");
}
});
}
// BoringSSL 特有:SSL_CTX_set_custom_verify
var customVerifyAddr = Module.findExportByName(libName, "SSL_CTX_set_custom_verify");
if (customVerifyAddr) {
Interceptor.attach(customVerifyAddr, {
onEnter: function (args) {
if (!args[2].isNull()) {
args[2] = new NativeCallback(function (ssl, out_alert) {
return0; // ssl_verify_ok
}, "int", ["pointer", "pointer"]);
console.log("[+] " + libName + " SSL_CTX_set_custom_verify -> ssl_verify_ok");
}
}
});
}
// BoringSSL 特有:SSL_CTX_set_cert_verify_callback
var certVerifyAddr = Module.findExportByName(libName, "SSL_CTX_set_cert_verify_callback");
if (certVerifyAddr) {
Interceptor.attach(certVerifyAddr, {
onEnter: function (args) {
if (!args[1].isNull()) {
args[1] = new NativeCallback(function (storeCtx, arg) {
return1;
}, "int", ["pointer", "pointer"]);
console.log("[+] " + libName + " SSL_CTX_set_cert_verify_callback -> 1");
}
}
});
}
// 强制验证结果为通过
var addr2 = Module.findExportByName(libName, "SSL_get_verify_result");
if (addr2) {
Interceptor.attach(addr2, {
onLeave: function (retval) {
retval.replace(ptr(0x0)); // X509_V_OK = 0
}
});
}
});
console.log("[*] === Native Layer Bypass Complete ===");
5.3 应用检测 Frida 并崩溃
部分高安全应用会主动检测 Frida 的存在并强制退出。常见的检测手段包括:
| | |
|---|
| 端口扫描 | | connect() |
/proc/self/maps 扫描 | | |
| 进程名检测 | | |
| D-Bus 协议探测 | | |
| 内联 Hook 检测 | | |
pthread_create 监控 | | |
快速应对措施:
# 方法 1:使用非默认端口启动 frida-server(躲避端口扫描)
./frida-server -l 0.0.0.0:31337 &
# 客户端连接时指定端口
frida -H 127.0.0.1:31337 -f <package_name> -l bypass.js
# 方法 2:重命名 frida-server 二进制(躲避进程名检测)
cp frida-server fs-16.x
./fs-16.x &
# 方法 3:使用 Frida Gadget 注入(不需要 frida-server 进程)
# 将 frida-gadget.so 重命名后嵌入到 APK 的 lib 目录
# 这种方式没有 frida-server 进程,可以躲避大部分进程检测
深入对抗:对于高级检测(如 maps 扫描、inline hook 检测),需要编写专门的反检测脚本。这是一个独立的议题,涉及 Hook open()、read()、strstr() 等系统调用来隐藏 Frida 痕迹。
六、实战排错流程
当 SSL Pinning 绕过脚本不生效时,不要盲目换方案——按以下决策树逐步排查,定位真正的问题:
SSL Pinning 绕过排错决策树排查要点速查:
| | |
|---|
| | frida-ps -U |
| | frida --version vs frida-server --version |
| | |
| | |
| | |
| | |
| | |
| | 服务器日志 / 抓包看 ClientCertificate |
| | |
| | |
| | |
七、工具生态速查
| | | |
|---|
| objection | | objection -g pkg explore -> android sslpinning disable | |
| frida-multiple-unpinning | | frida -U -f pkg -l unpinning.js | |
| apk-mitm | | npx apk-mitm app.apk | |
| httptoolkit | | | |
| Magisk + MagiskTrustUserCerts | | | |
| PCAPdroid | | | |
| ProxyDroid | | | |
八、总结
| | |
|---|
| | |
| | |
| | |
| Native 层 pinning(libssl.so) | Hook SSL_CTX_set_verify / SSL_get_verify_result | |
| Hook SSL_CTX_set_custom_verify + SSL_CTX_set_cert_verify_callback | |
| 枚举导出符号 Hook + dlopen 监听 + 必要时逆向偏移 | |
| | |
| | |
核心原则:无论 SSL Pinning 实现多复杂,最终都要经过 TLS 握手。找到验证入口点,让它返回"通过",就是所有绕过方案的本质。
推荐的实战顺序:
- 分析失败原因——通过异常堆栈、Logcat、Frida 日志定位
- 针对性 Hook——根据具体实现编写精确的绕过脚本
- 处理反调试——如果应用检测 Frida,先绕过检测再绕过 Pinning