24、证书绑定与SSL Pinning:证书固定原理、绕过方案
说到移动安全,SSL Pinning 是个绕不开的话题。我刚开始做逆向那会儿,遇到一个银行App,抓包死活抓不到流量,全是乱码。后来才知道,人家做了证书绑定。说白了,就是App只认自己家的证书,别人给的证书一概不信。
今天我们就来聊聊这个机制,以及怎么绕过它。
为什么要有SSL Pinning?
先说说背景。正常情况下,App通过HTTPS通信,客户端会验证服务器返回的证书是否由可信CA签发。但问题是,攻击者可以在中间人位置(比如公共WiFi)部署一个伪造的证书,如果用户手机里安装了攻击者的根证书,那中间人攻击就成功了。
SSL Pinning 就是为了解决这个问题。它让App只信任特定的证书或公钥,而不是信任所有CA签发的证书。这样一来,就算攻击者伪造了证书,App也不认。
核心思想: 将服务器证书或公钥硬编码到App中,运行时只接受这个固定的证书。
证书绑定的两种方式
我见过两种主流做法,一种是绑定证书文件,另一种是绑定公钥哈希。
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 证书绑定 | 将服务器证书的二进制数据硬编码到App中 | 实现简单,直接比较证书 | 证书过期后需要更新App |
| 公钥绑定 | 提取证书中的公钥,计算哈希后硬编码 | 证书更新但公钥不变时无需更新App | 实现稍复杂 |
我个人习惯用公钥绑定,因为证书可以轮换,但公钥一般不变。你想想看,如果证书过期了,你还得发版更新,多麻烦。
Android端的实现方式
Android 7.0之后,Google推出了Network Security Config,可以在XML里配置证书绑定。但很多App还是用代码实现,因为更灵活。
举个例子,OkHttp库的写法:
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();
嗯,这里要注意,那个SHA256哈希值是从服务器证书的公钥算出来的。如果算错了,App就连不上服务器了。
iOS端的实现方式
iOS上一般用AFNetworking或Alamofire来实现。我记得有个项目,用的是AFNetworking的AFSecurityPolicy:
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
policy.allowInvalidCertificates = NO;
policy.validatesDomainName = YES;
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = policy;
这里AFSSLPinningModeCertificate就是证书绑定模式。还有AFSSLPinningModePublicKey,对应公钥绑定。
绕过方案:Frida
好了,重点来了。怎么绕过?
Frida是我最常用的工具。它的原理是Hook掉证书验证相关的函数,让验证永远返回成功。
一个典型的Frida脚本:
Java.perform(function() {
var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl.checkTrustedRecursive.implementation = function(certs, host, clientAuth, untrustedChain, trustAnchorChain, used) {
return; // 直接返回,不验证
};
});
这段代码Hook了checkTrustedRecursive方法,让它什么都不做。这样一来,任何证书都能通过验证。
小技巧: 如果App用了OkHttp,可以Hook CertificatePinner.check 方法。如果用了WebView,可以Hook onReceivedSslError 并调用 proceed()。
绕过方案:JustTrustMe
JustTrustMe是一个Xposed模块,专门用来绕过SSL Pinning。它的原理和Frida类似,但不需要写脚本,装上去就能用。
它的工作流程是这样的:
1. 拦截所有SSL相关的类加载
2. Hook X509TrustManager的checkServerTrusted方法
3. 返回空实现,让验证通过
我曾经在一个App上试过,JustTrustMe直接搞定,连代码都不用写。但要注意,有些App会检测Xposed框架,这时候JustTrustMe就失效了。
绕过方案:SSLUnpinning
SSLUnpinning是另一个Xposed模块,它比JustTrustMe更激进一些。它会Hook更多的类,包括SSLSocketFactory、HostnameVerifier等。
它的核心代码大概是这样:
XposedHelpers.findAndHookMethod("javax.net.ssl.SSLSocketFactory",
lpparam.classLoader, "createSocket", ...);
XposedHelpers.findAndHookMethod("org.apache.http.conn.ssl.SSLSocketFactory",
lpparam.classLoader, "createSocket", ...);
说白了,它把能想到的SSL相关类都Hook了一遍。覆盖面更广,但稳定性稍差。
避坑指南: 我曾经遇到一个App,它用了自定义的SSL实现,没有用系统默认的X509TrustManager。这时候JustTrustMe和SSLUnpinning都失效了。最后我是通过Frida动态跟踪,找到了它自定义的验证方法,才Hook成功的。
三种方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Frida | 动态Hook,自定义脚本 | 灵活,可定制 | 需要写脚本,学习成本高 |
| JustTrustMe | Xposed模块,自动Hook | 开箱即用,简单 | 依赖Xposed,容易被检测 |
| SSLUnpinning | Xposed模块,覆盖更广 | Hook点多,成功率高 | 稳定性差,可能影响其他App |
知识体系图
下面这张图展示了SSL Pinning的核心逻辑和绕过路径:
实战中的注意事项
最后说几点实战经验:
- 先判断是否做了Pinning: 抓包时如果全是乱码或连接失败,大概率做了Pinning。可以先用JustTrustMe试试,不行再上Frida。
- 注意App的检测机制: 有些App会检测是否运行在模拟器或是否安装了Xposed。这时候Frida更隐蔽,因为它不需要重启App。
- 自定义SSL实现: 我遇到过一些金融App,它们自己实现了SSL层,完全绕过了系统API。这时候只能通过动态跟踪找到关键函数。
- 证书过期问题: 如果你在分析一个老App,它的证书可能已经过期了。这时候Pinning反而会阻止正常通信,绕过它才能继续分析。
我的建议: 先学会Frida,因为它最灵活。JustTrustMe和SSLUnpinning可以作为辅助工具,但不要依赖它们。毕竟,真正的逆向工程师,手里得有把趁手的刀。
好了,SSL Pinning 的内容就讲到这里。记住,绕过只是手段,理解原理才是目的。下次遇到抓不到包的App,你知道该怎么做了。
公众号:蓝海资料掘金营,微信deep3321