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更多的类,包括SSLSocketFactoryHostnameVerifier等。

它的核心代码大概是这样:

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的核心逻辑和绕过路径:

SSL Pinning 知识体系 SSL Pinning 证书绑定 公钥绑定 Frida 动态Hook JustTrustMe SSLUnpinning 核心原理:Hook证书验证函数,使其永远返回成功 X509TrustManager.checkServerTrusted → 空实现

实战中的注意事项

最后说几点实战经验:

  • 先判断是否做了Pinning: 抓包时如果全是乱码或连接失败,大概率做了Pinning。可以先用JustTrustMe试试,不行再上Frida。
  • 注意App的检测机制: 有些App会检测是否运行在模拟器或是否安装了Xposed。这时候Frida更隐蔽,因为它不需要重启App。
  • 自定义SSL实现: 我遇到过一些金融App,它们自己实现了SSL层,完全绕过了系统API。这时候只能通过动态跟踪找到关键函数。
  • 证书过期问题: 如果你在分析一个老App,它的证书可能已经过期了。这时候Pinning反而会阻止正常通信,绕过它才能继续分析。

我的建议: 先学会Frida,因为它最灵活。JustTrustMe和SSLUnpinning可以作为辅助工具,但不要依赖它们。毕竟,真正的逆向工程师,手里得有把趁手的刀。

好了,SSL Pinning 的内容就讲到这里。记住,绕过只是手段,理解原理才是目的。下次遇到抓不到包的App,你知道该怎么做了。


公众号:蓝海资料掘金营,微信deep3321