#SSL Pinning绕过技术 - 数据采集&安全测试必备
做App数据采集、功能安全测试时,明明装了Charles/Fiddler的系统根证书,Wi‑Fi代理也配了,还是抓不到明文HTTPS包?别慌——90% 以上是遇到了SSL Pinning(SSL证书锁定) 这道安全防护墙。
本文将带你快速理解原理,用Frida实现覆盖主流场景的Java层通用绕过,附带一键Python注入脚本,上手难度极低。
#一、快速理解SSL Pinning原理
#正常HTTPS流程回顾
- 客户端(App/浏览器)发起HTTPS连接请求
- 服务端返回自己的公钥证书 + 证书链
- 客户端用操作系统/浏览器预装的「信任根证书库」 验证证书链的有效性(真实性、有效期等)
#SSL Pinning做了什么额外动作?
客户端硬编码/内置一套只信任该App服务端的证书/公钥哈希/证书指纹,完全跳过/增强系统根证书库的验证:要么只认内置的;要么内置+系统根都过,但内置更优先。
这么一来,就算你把代理证书装到系统里,App也会因「代理证书不在内置信任列表」直接断开连接,流量自然抓不到明文。
#二、Frida通用Java层绕过方案
Frida是一款跨平台动态插桩工具,能在App运行时直接Hook Java层的关键验证函数,强制Pinning逻辑失效——无需反编译改包签名,无需重启设备多次调试。
下面是经过精简、适配了OkHttp全系列/系统SSLContext/WebView三大主流场景的Frida JS脚本:
#核心Frida绕过脚本
Java.perform(function() {
console.log("[+] 🌐 通用SSL Pinning Bypass已激活");
// --------------------------
// 1. 覆盖OkHttp全系列(国内90%+App用它做网络)
// --------------------------
const okHttpCheckClasses = [
'okhttp3.CertificatePinner',
'okhttp3.internal.tls.CertificatePinnerChainCleaner',
'com.squareup.okhttp.CertificatePinner' // 老版本OkHttp兼容
];
okHttpCheckClasses.forEach(className => {
try {
const clazz = Java.use(className);
// 自动获取类的所有check/checkChain重载,避免遗漏
const methods = clazz.class.getDeclaredMethods();
methods.forEach(method => {
const methodName = method.getName();
if (methodName.startsWith('check') && clazz[methodName]) {
clazz[methodName].overload.apply(
clazz[methodName],
Array.from({length: method.getParameterCount()}, () => '*')
).implementation = function() {
console.log(`[+] ✅ OkHttp ${className}.${methodName} 绕过成功`);
return; // 直接返回,不做任何验证
};
}
});
} catch(e) {
// 类不存在直接跳过,不影响其他模块
}
});
// --------------------------
// 2. 替换系统X509TrustManager(所有Java原生网络的通用兜底)
// --------------------------
try {
const X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
const SSLContext = Java.use('javax.net.ssl.SSLContext');
// 注册「全信任」的自定义TrustManager
const TrustAllManager = Java.registerClass({
name: 'com.universal.trustall',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function() {}, // 客户端证书验证直接跳过
checkServerTrusted: function() {}, // 服务端证书验证直接跳过
getAcceptedIssuers: function() { return []; } // 返回空数组就行
}
});
const trustAllArray = [TrustAllManager.$new()];
// Hook SSLContext.init的所有重载,强制替换TrustManager
SSLContext.init.overloads.forEach(overload => {
overload.implementation = function(keyManagers, trustManagers, secureRandom) {
this.init(keyManagers, trustAllArray, secureRandom);
};
});
console.log("[+] ✅ 系统X509TrustManager已替换为全信任");
} catch(e) {
// 兜底方案异常不影响其他绕过
}
// --------------------------
// 3. 绕过WebView SSL错误(内嵌H5场景的HTTPS抓包)
// --------------------------
try {
const WebViewClient = Java.use('android.webkit.WebViewClient');
WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
console.log(`[+] ✅ WebView SSL错误已忽略,错误类型: ${error.getPrimaryError()}`);
handler.proceed(); // 强制继续加载
};
} catch(e) {
// 无WebView直接跳过
}
});#一键Python注入工具
上面的脚本单独用frida/frida-tools命令行也能用,但为了减少输入,我封装了自动连接USB设备、支持spawn启动/attach附加两种模式、实时输出脚本日志的Python工具:
import frida
import sys
import time
from typing import Tuple, Optional
# 直接把核心绕过脚本嵌入,无需单独创建JS文件
BYPASS_SCRIPT = """
Java.perform(function() {
console.log("[+] 🌐 通用SSL Pinning Bypass已激活");
const okHttpCheckClasses = [
'okhttp3.CertificatePinner',
'okhttp3.internal.tls.CertificatePinnerChainCleaner',
'com.squareup.okhttp.CertificatePinner'
];
okHttpCheckClasses.forEach(className => {
try {
const clazz = Java.use(className);
const methods = clazz.class.getDeclaredMethods();
methods.forEach(method => {
const methodName = method.getName();
if (methodName.startsWith('check') && clazz[methodName]) {
clazz[methodName].overload.apply(
clazz[methodName],
Array.from({length: method.getParameterCount()}, () => '*')
).implementation = function() {
console.log(`[+] ✅ OkHttp ${className}.${methodName} 绕过成功`);
return;
};
}
});
} catch(e) {}
});
try {
const X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
const SSLContext = Java.use('javax.net.ssl.SSLContext');
const TrustAllManager = Java.registerClass({
name: 'com.universal.trustall',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function() {},
checkServerTrusted: function() {},
getAcceptedIssuers: function() { return []; }
}
});
const trustAllArray = [TrustAllManager.$new()];
SSLContext.init.overloads.forEach(overload => {
overload.implementation = function(keyManagers, trustManagers, secureRandom) {
this.init(keyManagers, trustAllArray, secureRandom);
};
});
console.log("[+] ✅ 系统X509TrustManager已替换为全信任");
} catch(e) {}
try {
const WebViewClient = Java.use('android.webkit.WebViewClient');
WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
console.log(`[+] ✅ WebView SSL错误已忽略,错误类型: ${error.getPrimaryError()}`);
handler.proceed();
};
} catch(e) {}
});
"""
def bypass_ssl_pinning(app_package: str) -> Tuple[Optional[frida.core.Session], Optional[frida.core.Script]]:
"""
一键绕过Android App的SSL Pinning
:param app_package: 目标应用的完整包名(如 com.tencent.mm)
:return: (session, script) 成功返回Frida对象对,失败返回(None, None)
"""
# --------------------------
# 1. 获取USB设备
# --------------------------
try:
device = frida.get_usb_device(timeout=5)
print(f"✅ 连接到设备: {device.name}")
except Exception as e:
print(f"❌ 未找到USB设备,请检查ADB/Frida Server是否运行: {str(e).strip()}")
return None, None
# --------------------------
# 2. 连接App(优先spawn启动更稳定)
# --------------------------
session: Optional[frida.core.Session] = None
spawned_pid: Optional[int] = None
try:
print(f"⚠️ 正在尝试spawn启动App: {app_package}...")
spawned_pid = device.spawn([app_package])
session = device.attach(spawned_pid)
except frida.ProcessNotFoundError:
print(f"❌ 找不到包名对应的App,请检查是否正确: {app_package}")
return None, None
except frida.NotSupportedError:
print(f"⚠️ 设备不支持spawn(部分Android 14+定制系统会禁用),尝试附加到已运行的App...")
try:
session = device.attach(app_package)
except Exception as e:
print(f"❌ 附加失败,请确认App已在前台运行: {str(e).strip()}")
return None, None
except Exception as e:
print(f"❌ 连接App时发生未知错误: {str(e).strip()}")
return None, None
# --------------------------
# 3. 注入绕过脚本
# --------------------------
script: Optional[frida.core.Script] = None
try:
script = session.create_script(BYPASS_SCRIPT)
# 实时接收并输出Frida脚本的日志
def on_message(message, data):
if message['type'] == 'send':
print(f"📜 [Frida] {message['payload']}")
script.on('message', on_message)
script.load()
print("✅ SSL Pinning绕过脚本注入成功")
# 如果是spawn启动的,必须resume才能让App继续运行
if spawned_pid is not None:
device.resume(spawned_pid)
print("✅ App已恢复运行")
except Exception as e:
print(f"❌ 脚本注入失败,请检查Frida Server与电脑端Frida版本是否严格匹配: {str(e).strip()}")
if session:
session.detach()
return None, None
return session, script
if __name__ == "__main__":
# 检查命令行参数
if len(sys.argv) != 2:
print("⚠️ 使用方法: python ssl_bypass.py <目标应用完整包名>")
print("💡 示例: python ssl_bypass.py com.tencent.mm")
sys.exit(1)
target_pkg = sys.argv[1]
session_obj, script_obj = bypass_ssl_pinning(target_pkg)
if session_obj and script_obj:
print("\n🎉 绕过成功!现在可以用Fiddler/Charles/Burp Suite抓明文HTTPS包了")
print("⏳ 按 Ctrl+C 退出并清理Frida资源...")
try:
# 保持脚本运行,直到用户手动中断
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n🛑 正在清理资源...")
script_obj.unload()
session_obj.detach()
print("✅ 清理完成,已退出")
else:
sys.exit(1)#三、使用前的必要准备(必看)
#硬件/软件要求
-
Android设备/模拟器
✅ 推荐:MuMu Player 12(已ROOT、自带ADB、适配Windows/Mac、内存占用小)
✅ 真机:必须已ROOT + 开启「USB调试」 + 允许「USB安装」(可选)
❌ 注意:Android 7.0+ 系统不再默认信任「用户证书库」,需把抓包工具的根证书手动移动到「系统证书库」(需要ROOT或借助Magisk+TrustMeAlready插件) -
Frida工具链
-
电脑端:
pip install frida frida-tools -
设备端:
- 打开 Frida GitHub Releases
- 下载与电脑端Frida版本严格一致、与设备/模拟器架构匹配的
frida-server-xxx-android-xxx(如MuMu Player 12 64位下载frida-server-16.5.4-android-x86_64) - 重命名为
frida-server,用ADB推送到/data/local/tmp/并运行:
adb push frida-server /data/local/tmp/ adb shell su chmod 755 /data/local/tmp/frida-server /data/local/tmp/frida-server &
-
-
抓包工具
- 已配置好本地代理(如Charles默认8888端口)
- 抓包工具根证书已安装到Android设备的系统证书库
#四、常见问题快速排查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 脚本提示成功,但抓不到明文HTTPS包 | 1. 代理未配置 2. 证书在用户库 3. App用了非HTTP(S)协议 | 1. 检查Wi‑Fi/模拟器代理 2. 把证书移到系统库 3. 确认抓包协议 |
| 脚本注入失败,提示「Frida版本不匹配」 | Frida Server与电脑端版本不一致 | 下载并安装/运行完全相同版本的工具链 |
| spawn启动App后,App立即闪退 | App有反Frida检测 | 可尝试「隐藏Frida特征」的插件(如frida-gadget+frida-unpin预编译),或先附加到已启动的后台App |
| 附加已运行的App时,提示「未找到进程」 | App未在前台运行,或被系统优化挂起 | 把App切到前台,再重新运行脚本 |
#五、免责声明
本文仅用于合法的App功能测试、自身应用的安全审计、明确授权的数据采集场景。请勿用于非法入侵、窃取他人隐私数据、破坏正常网络服务等用途,否则一切后果由使用者自行承担,与本文作者无关。

