安卓逆向系列 07:SSL Pinning 绕过与 HTTPS 抓包全攻略
本系列文章仅供安全研究与学习使用,请在合法授权环境下操作,切勿用于非法用途。
一、什么是 SSL Pinning,为什么需要绕过
1.1 问题的起点
你架好了 Burp Suite,手机上配好了代理,满心期待地打开 App——结果发现:
Network error: SSL handshake failed
或者
javax.net.ssl.SSLHandshakeException: Certificate pinning failure
这就是遇上了 SSL Pinning(证书锁定/证书绑定)。
1.2 SSL Pinning 的作用
普通 HTTPS 只验证证书链是否由受信任 CA 签发。而 SSL Pinning 更进一步——App 内置了服务端证书的指纹(Hash)或公钥,只有与内置值完全匹配才允许连接:
普通 HTTPS:系统信任的任意 CA 签发的证书 → 通过
SSL Pinning:必须是我指定的那张证书/公钥 → 其他全拒绝
这样即使攻击者(或安全研究员)插入了中间人证书,也无法通过 Pinning 验证,抓包失败。
1.3 为什么逆向分析需要绕过 Pinning
| 目的 | 说明 |
|---|---|
| 接口分析 | 了解 App 与服务端的通信格式和字段含义 |
| 参数还原 | 定位签名参数的生成位置 |
| 协议复现 | 用 Python/脚本复现 API 调用 |
| 安全测试 | 检测接口是否存在越权、注入等漏洞 |
二、SSL Pinning 原理图解
正常流程(无 Pinning):
App → HTTPS 请求 → [Burp 中间人] → 用 Burp CA 签名 → 服务端
App ← HTTPS 响应 ← [Burp 解密/重加密] ← 服务端
但中间人的证书是 Burp 自签的,若 App 开启了 Pinning,会在握手阶段检测到证书不匹配并拒绝连接。
绕过后:
App → Frida/objection Hook Pinning 检查函数 → 始终返回"通过"
App → HTTPS 请求 → [Burp 中间人] 正常工作 → 服务端
三、三种 Pinning 实现方式
3.1 OkHttp CertificatePinner(最常见)
// 典型的 OkHttp Pinning 代码
OkHttpClientclient=newOkHttpClient.Builder()
.certificatePinner(newCertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build())
.build();
检测特征:jadx 中搜索 CertificatePinner 或 sha256/ 字符串即可定位。
3.2 自定义 TrustManager(中等难度)
// 手动实现证书验证
classCustomTrustManagerimplementsX509TrustManager {
@Override
publicvoidcheckServerTrusted(X509Certificate[] chain, StringauthType)
throwsCertificateException {
// 比对证书指纹
Stringfingerprint=getFingerprint(chain[0]);
if (!EXPECTED_FINGERPRINT.equals(fingerprint)) {
thrownewCertificateException("Certificate mismatch!");
}
}
}
3.3 Native 层 Pinning(最难绕过)
部分 App 在 .so 文件中实现证书校验,通过 OpenSSL/BoringSSL 的 SSL_CTX_set_verify 等函数完成。这种方式无法用 Java 层 Hook 绕过,需要 Hook Native 函数。
| 实现方式 | 所在层 | Objection 能否绕过 | Frida 难度 |
|---|---|---|---|
| OkHttp CertificatePinner | Java | ✅ 可以 | 简单 |
| 自定义 TrustManager | Java | ✅ 大多可以 | 简单-中等 |
| Conscrypt/BouncyCastle | Java | ✅ 可以 | 中等 |
| OpenSSL Native | Native | ❌ 不能 | 较难 |
| BoringSSL Native | Native | ❌ 不能 | 较难 |
四、环境准备:Burp + 手机代理配置
4.1 Burp Suite 配置
1. Burp → Proxy → Options → Proxy Listeners
2. 添加监听器:0.0.0.0:8888(监听所有网卡)
3. Burp → Proxy → Options → Export CA Certificate
→ 格式选 DER → 保存为 burp_ca.der
4.2 手机安装 Burp CA 证书
# 将证书推送到手机
adb push burp_ca.der /sdcard/
# 手机操作路径:
# 设置 → 安全 → 加密与凭据 → 安装证书 → CA 证书
# 选择 /sdcard/burp_ca.der
4.3 手机配置代理
设置 → WLAN → 长按当前 WiFi → 修改网络
→ 代理:手动
→ 代理主机名:电脑 IP(如 192.168.1.100)
→ 代理端口:8888
→ 保存
4.4 Android 7+ 用户证书信任问题
Android 7.0 起,App 默认不信任用户安装的 CA 证书(只信任系统 CA),需要额外处理(见第九节)。
五、方案一:objection 一键绕过
objection 是基于 Frida 的移动端安全测试框架,内置了主流 Pinning 的绕过脚本,使用最简单。
5.1 安装 objection
pip install objection
# 验证安装
objection version
5.2 连接 App
# 方式一:附加到运行中的进程
objection -g com.target.app explore
# 方式二:spawn 模式(App 启动前注入)
objection -g com.target.app explore --startup-command'android sslpinning disable'
5.3 执行绕过
# 进入 objection shell 后
[com.target.app]# android sslpinning disable
# 成功输出:
# (agent) Custom TrustManager registered, overriding...
# (agent) OkHTTP 3.x TrustManager overridden
# (agent) OkHTTP 4.x CertificatePinner overridden
# (agent) Bypassing TrustManagerImpl (Android)
5.4 objection 绕过覆盖范围
| 检测类 | 是否覆盖 |
|---|---|
OkHttpClient.CertificatePinner |
✅ |
javax.net.ssl.TrustManager |
✅ |
SSLContext.init |
✅ |
HttpsURLConnection |
✅ |
WebViewClient.onReceivedSslError |
✅ |
| Native OpenSSL | ❌ |
六、方案二:Frida 脚本绕过(完整代码)
当 objection 无效时,需要手写 Frida 脚本精准 Hook。
6.1 通用绕过脚本(覆盖主流 Java 实现)
// ssl_bypass.js - 通用 SSL Pinning 绕过脚本
Java.perform(function() {
// ========== 1. OkHttp CertificatePinner ==========
try {
varCertificatePinner=Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List')
.implementation=function(hostname, certs) {
console.log('[*] OkHttp CertificatePinner.check() bypassed for: '+hostname);
return; // 直接返回,不抛出异常
};
CertificatePinner.check.overload('java.lang.String', 'java.security.cert.Certificate')
.implementation=function(hostname, cert) {
console.log('[*] OkHttp CertificatePinner.check(cert) bypassed');
return;
};
} catch(e) {
console.log('[-] OkHttp CertificatePinner not found: '+e);
}
// ========== 2. 自定义 TrustManager ==========
try {
varTrustManagerImpl=Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl.verifyChain.implementation=function(
untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
console.log('[*] TrustManagerImpl.verifyChain() bypassed for: '+host);
returnuntrustedChain;
};
} catch(e) {
console.log('[-] TrustManagerImpl not found: '+e);
}
// ========== 3. HttpsURLConnection ==========
try {
varHttpsURLConnection=Java.use('javax.net.ssl.HttpsURLConnection');
HttpsURLConnection.setDefaultHostnameVerifier.implementation=function(verifier) {
console.log('[*] HttpsURLConnection.setDefaultHostnameVerifier bypassed');
// 不调用原始方法,相当于使用宽松的 verifier
};
} catch(e) {}
// ========== 4. 动态创建绕过 TrustManager ==========
try {
varSSLContext=Java.use('javax.net.ssl.SSLContext');
varTrustManager=Java.use('javax.net.ssl.TrustManager');
varX509TrustManager=Java.use('javax.net.ssl.X509TrustManager');
// 创建一个信任一切的 TrustManager
varTrustEverything=Java.registerClass({
name: 'com.bypass.TrustEverything',
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(keyManagers, trustManagers, secureRandom) {
console.log('[*] SSLContext.init() intercepted, injecting TrustAll');
vartrustAllManagers= [TrustEverything.$new()];
this.init(keyManagers, trustAllManagers, secureRandom);
};
} catch(e) {
console.log('[-] SSLContext override failed: '+e);
}
// ========== 5. WebView SSL 错误绕过 ==========
try {
varWebViewClient=Java.use('android.webkit.WebViewClient');
WebViewClient.onReceivedSslError.implementation=function(view, handler, error) {
console.log('[*] WebViewClient.onReceivedSslError bypassed');
handler.proceed(); // 忽略 SSL 错误
};
} catch(e) {}
console.log('[+] SSL Pinning bypass loaded successfully!');
});
6.2 运行脚本
# 附加到进程
frida -U-f com.target.app -l ssl_bypass.js --no-pause
# 或者附加到已运行的进程
frida -U com.target.app -l ssl_bypass.js
6.3 OkHttp 4.x 专用绕过
OkHttp 4.x 使用 Kotlin 重写,类名略有不同:
// OkHttp 4.x (kotlin)
try {
varRealX509TrustManager=Java.use('okhttp3.internal.tls.RealTrustRootIndex');
// ...
} catch(e) {}
// 更通用的方式:Hook CertificatePinner$check 方法
varCertificatePinner=Java.use('okhttp3.CertificatePinner');
// 获取所有重载版本
CertificatePinner['check$okhttp'].implementation=function() {
console.log('[*] OkHttp4 check$ bypassed');
};
七、方案三:r0capture 直接抓明文
r0capture 在 SSL 握手完成后、应用数据加密前的位置 Hook,可以直接抓到明文 HTTP 数据,无需证书配置。
7.1 原理
r0capture Hook 的是 javax.net.ssl.SSLOutputStream.write() 和 javax.net.ssl.SSLInputStream.read(),在数据传输层面拦截,完全绕过证书验证机制。
7.2 安装与使用
# 下载 r0capture
git clone https://github.com/r0ysue/r0capture
# 运行
python3 r0capture.py -U-f com.target.app
# 同时保存到 pcap 文件(可用 Wireshark 打开)
python3 r0capture.py -U-f com.target.app -p /tmp/output.pcap
7.3 r0capture 输出示例
[r0capture] Attached to com.target.app
[r0capture] SSL Write: POST /api/v2/user/login HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
{"username":"test@example.com","password":"xxx","sign":"a1b2c3d4"}
[r0capture] SSL Read: HTTP/1.1 200 OK
Content-Type: application/json
{"code":0,"data":{"token":"xxxxxx","userId":12345}}
八、方案四:VPN 层抓包(mitmproxy)
不修改系统证书,通过 VPN 层面拦截流量,适合处理复杂的证书信任问题。
8.1 mitmproxy 配置
# 安装 mitmproxy
pip install mitmproxy
# 启动(透明代理模式)
mitmproxy --mode transparent --ssl-insecure
# 或
mitmdump --mode transparent -w /tmp/capture.mitm
8.2 配合 Android VPN App
推荐工具:
- VPNHotspot + 路由规则
- tun2socks(将流量路由到 mitmproxy)
- ProxyDroid(Root 模式,强制代理)
8.3 使用 mitmproxy 脚本过滤
# intercept_filter.py - 只记录目标域名的请求
frommitmproxyimporthttp
TARGET_DOMAINS = ['api.example.com', 'auth.example.com']
defrequest(flow: http.HTTPFlow):
ifany(domaininflow.request.hostfordomaininTARGET_DOMAINS):
print(f"→ {flow.request.method} {flow.request.url}")
print(f" Body: {flow.request.get_text()}")
defresponse(flow: http.HTTPFlow):
ifany(domaininflow.request.hostfordomaininTARGET_DOMAINS):
print(f"← {flow.response.status_code}")
print(f" Body: {flow.response.get_text()[:500]}")
# 运行过滤脚本
mitmproxy -s intercept_filter.py
九、Android 7+ 证书信任问题解决
9.1 问题原因
Android 7.0+ 引入了 Network Security Config,默认规则:
-
系统 CA:App 完全信任
-
用户安装的 CA:Debug 版 App 信任,Release 版不信任
9.2 方案一:修改 AndroidManifest.xml(推荐)
# 1. 反编译 APK
apktool d target.apk -o target_decoded/
# 2. 在 AndroidManifest.xml 的 <application> 标签中添加
# android:networkSecurityConfig="@xml/network_security_config"
# 3. 创建 res/xml/network_security_config.xml
<!-- res/xml/network_security_config.xml -->
<?xmlversion="1.0" encoding="utf-8"?>
<network-security-config>
<base-configcleartextTrafficPermitted="true">
<trust-anchors>
<!-- 信任系统 CA -->
<certificatessrc="system"/>
<!-- 信任用户 CA(Burp 证书) -->
<certificatessrc="user"/>
</trust-anchors>
</base-config>
</network-security-config>
# 4. 重新打包 + 签名
apktool b target_decoded/ -o target_patched.apk
# 使用 uber-apk-signer 签名
java -jar uber-apk-signer.jar --apks target_patched.apk
9.3 方案二:将 Burp CA 安装到系统证书目录(需 Root)
# 转换证书格式
openssl x509 -inform DER -in burp_ca.der -out burp_ca.pem
# 计算证书 hash(Android 需要特定命名格式)
HASH=$(openssl x509 -inform PEM -subject_hash_old -in burp_ca.pem | head -1)
cp burp_ca.pem ${HASH}.0
# 推送到系统证书目录
adb root
adb remount
adb push ${HASH}.0 /system/etc/security/cacerts/
adb shell chmod644 /system/etc/security/cacerts/${HASH}.0
adb reboot
9.4 方案三:Magisk 模块(最优雅)
Magisk Manager → 搜索并安装 "Move Certificates" 模块
→ 重启后,用户安装的 CA 自动移动到系统证书目录
→ 所有 App 均信任 Burp CA
十、双向认证(mTLS)绕过
部分金融类 App 不仅验证服务端证书,还要求客户端出示证书(双向认证)。
10.1 提取客户端证书
// Frida 脚本:拦截 KeyManager,提取客户端证书和私钥
Java.perform(function() {
varKeyManagerFactory=Java.use('javax.net.ssl.KeyManagerFactory');
KeyManagerFactory.init.overload(
'java.security.KeyStore', '[C'
).implementation=function(ks, password) {
console.log('[*] KeyManagerFactory.init() called');
// 枚举 KeyStore 中的证书
varaliases=ks.aliases();
while(aliases.hasMoreElements()) {
varalias=aliases.nextElement();
console.log(' Alias: '+alias);
varcert=ks.getCertificate(alias);
console.log(' Cert: '+cert.toString());
}
this.init(ks, password);
};
});
10.2 导入到 Burp
# 将提取的 PKCS12 格式证书导入 Burp
# Burp → Settings → Network → TLS → Client TLS Certificates
# → Add → 选择证书文件和密码
十一、常见抓包工具对比
| 工具 | 平台 | 支持协议 | 免 Root | SSL Pinning 绕过 | 优势 | 劣势 |
|---|---|---|---|---|---|---|
| Burp Suite | PC | HTTP/HTTPS/WebSocket | 需配代理 | 需额外处理 | 功能强大,插件丰富 | 配置复杂 |
| Charles | PC/Mac | HTTP/HTTPS | 需配代理 | 需额外处理 | 界面友好 | 付费 |
| mitmproxy | PC | HTTP/HTTPS | 透明代理 | 需额外处理 | 可编程,脚本强大 | 命令行 |
| r0capture | PC+Frida | HTTP/HTTPS | 需 Root | ✅ 内置绕过 | 直接抓明文 | 依赖 Frida |
| objection | PC+Frida | 所有协议 | 需 Root | ✅ 内置绕过 | 操作简便 | 复杂场景需手写 |
| HttpCanary | 手机 | HTTP/HTTPS | ❌ 不需要 | 部分支持 | 手机端直接使用 | 功能有限 |
| Stream | 手机 | HTTP/HTTPS | ❌ 不需要 | 不支持 | 操作简单 | SSL Pinning 无解 |
| Packet Capture | 手机 | HTTP/HTTPS | ❌ 不需要 | 不支持 | 免费无 Root | 功能基础 |
小结
SSL Pinning 绕过按难度从低到高:
-
objection 一键绕过:覆盖 90% 的 Java 层 Pinning
-
Frida 自定义脚本:覆盖特殊实现,精准 Hook
-
r0capture:绕过所有 Java 层证书验证,直接抓明文
-
修改 AndroidManifest:解决 Android 7+ 系统级信任问题
-
Native Hook:处理 .so 中实现的 Pinning(高难度)
拿到了明文流量之后,下一步就是分析接口中的加密字段和签名,这正是下一篇的主题。
文章持续更新,欢迎关注。转载请注明出处。
夜雨聆风