0x01 客户端段SSL证书绑定
SSL Pinning 也称为证书锁定,是Google官方推荐的检验方式,意思是将服务器提供的SSL/TLS证书内置到移动客户端,当客户端发起请求的时候,通过对比内置的证书与服务器的证书是否一致,来确认这个连接的合法性。
SSL Pinning 一般实现方式有两种
- 一种是在代码层进行校验
- 一种是通过配置文件network_security_config.xml进行配置实现校验。
代码层实现
以常见的OKHttp网络框架举例,在代码层面校验证书的代码如下:
详细的代码实现可以参考:
https-capture/NetCaptureTest/app/src/main/java/com/actorn/netcapture/MainActivity.java at main · act0rn/https-capture (github.com)
配置文件实现
通过 res/xml/network_security_config.xml 进行配置,实现SSL Pinning,配置文件可以分为两种,使用证书校验和公钥校验。
下面我们先看看证书锁定和公钥锁定的具体配置实现:
单向认证校验流程
具体的SSLPinning的交互过程,可以使用如下流程图来表示:
通过SSL PINNING绑定证书以后,设置系统代理,即使将抓包工具的证书导入到系统目录,抓包时依然会发现App报错:javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure! 或者 javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.,报错的原因是APP预置的证书信息与服务端返回的证书信息校验不一致,导致握手失败
0x02 SSLPinning原理
代码解读
将代码分为以下几个部分:
- 调用x509的标准,创建了一个证书(包含公钥、签发信息等等);
- 生成证书仓库keyStore并初始化,将第一步创建的证书加载到keyStore中;
- 首先拿到默认的信任管理器工厂,将keyStore加载进去,也就是说,只要有存在于KeyStore中的证书,是可以校验通过的;
- 新建了SSL的对象,用来做SSL握手。进行了init初始化,里面有三个参数:
- km:null==>做服务端校验(服务端校验客户端证书)
- trustManagers==>做客户端校验(客户端校验服务端证书)
- new SecureRandom()==>SSL握手过程中创建随机数,安全考虑
- 上面的四个步骤其实都是为下面做铺垫的,这块调用了OKHttp.builder框架,实现了信任证书工厂中的证书,不信任系统证书。【第一层校验】
- 证书绑定,也是SSLPinning的一种校验方式。【第二次校验】
- HostnameVerifier,也是一种校验方式【第三层校验】
下面的代码可以看做是一些常见的SSLPinning
实现的代码:
private void sendRequestWithOkHttp1(){ new Thread(new Runnable() { @Override public void run() { OkHttpClient mClient = new OkHttpClient().newBuilder().sslSocketFactory(HttpsTrustAllCerts.createSSLSocketFactory(),new HttpsTrustAllCerts()).hostnameVerifier(new HttpsTrustAllCerts.TrustAllHostnameVerifier()).build(); Request request = new Request.Builder() .url("https://www.baidu.com") .build(); try { Response response = mClient.newCall(request).execute(); String responseData = response.body().string(); showResponse(responseData); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
private void sendRequestWithOkHttp2(){ new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://www.baidu.com") .build(); try { Response response = client.newCall(request).execute(); String responseData = response.body().string(); showResponse(responseData); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
private void sendRequestWithOkHttp3(){ new Thread(new Runnable() { @Override public void run() { final String CA_DOMAIN = "www.baidu.com"; final String CA_PUBLIC_KEY = "sha256/Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU="; CertificatePinner pinner = new CertificatePinner.Builder() .add(CA_DOMAIN,CA_PUBLIC_KEY) .build(); OkHttpClient client = new OkHttpClient().newBuilder().certificatePinner(pinner).build(); Request request = new Request.Builder() .url("https://www.baidu.com") .build(); try { Response response = client.newCall(request).execute(); String responseData = response.body().string(); showResponse(responseData); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
|
【举例】X509TrustManager校验实现
X509TrustManager类介绍
在JSSE中,证书信任管理器类就是实现了接口X509TrustManager的类。我们可以自己实现该接口,让它信任我们指定的证书。
接口X509TrustManager有下述三个公有的方法需要我们实现:
⑴ void checkClientTrusted(X509Certificate[] chain, String authType)throws CertificateException
该方法检查客户端的证书,若不信任该证书则抛出异常。由于我们不需要对客户端进行认证,因此我们只需要执行默认的信任管理器的这个方法。JSSE中,默认的信任管理器类为TrustManager。
⑵ void checkServerTrusted(X509Certificate[] chain, String authType)throws CertificateException
该方法检查服务器的证书,若不信任该证书同样抛出异常。通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
⑶ X509Certificate[] getAcceptedIssuers()
返回受信任的X509证书数组。
自己实现了信任管理器类,如何使用呢?
类HttpsURLConnection似乎并没有提供方法设置信任管理器。其实,HttpsURLConnection通过SSLSocket来建立与HTTPS的安全连接,SSLSocket对象是由SSLSocketFactory生成的。HttpsURLConnection提供了方法setSSLSocketFactory(SSLSocketFactory)设置它使用的SSLSocketFactory对象。SSLSocketFactory通过SSLContext对象来获得,在初始化SSLContext对象时,可指定信任管理器对象。下面用一个图简单表示这几个JSSE类的关系:
SSLPinning原理&代码实现
原理是—>(不信任系统库证书,只信任自己的证书)
1、正常情况,要是不对X509TrustManager
接口中的方法(checkServerTrusted)进行重写的话,客户端默认是信任所有证书的。
2、但如果有做了校验的话,客户端会根据设置校验服务端的证书。
下图中的代码,是自己设置了一个证书(CA证书申请是要钱的),但如果是自己只做的证书,手机是不认可的,所有用代码实现了校验。。。
3、代码中引入了trustManagers,trustManagers
是一个数组。在trustManagers里面包含了一个keyStore,keyStore里面就是我们定义好的证书
所以,就是在trustManagers中存放了我们自己定义好的证书,我们才可以校验通过。
所以,这块针对与客户端来说,做了SSL证书绑定之后,默认是不信任系统证书列表中的证书的。简单来说就是,即时将Charles证书放到了系统证书列表中,相对于设置了trustManagers的客户端而言,服务端给到Charle
证书不是在定义好的证书商店中,所以这块会校验不通过。
4、 “TrustManager[] trustManagers“ 信任管理器。
trustManager是一个接口,在里面定义了实现类,这块用的是X509TrustManager类,里面定义了一个 checkServerTrusted方法,如下:
平常针对APP的校验逻辑一般都是写在这个方法里面的
这块如果将 checkServerTrusted方法体置空,默认是信任所有证书的。也就是说,如果遮掩个设置了,即使Charles的证书不在系统证书列表中也是可以正常抓包的。
同理,如果要实现SSLPinning校验,那就可以对 checkServerTrusted方法进行重写,如下:
public static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain, String authType, OpenSSLSocketImpl socket) throws CertificateException { if (!checkTrusted("checkServerTrusted", tm, chain, authType, Socket.class, socket) && !checkTrusted("checkServerTrusted", tm, chain, authType, String.class, socket.getHandshakeSession().getPeerHost())) { tm.checkServerTrusted(chain, authType); } }
|
5、所以,站在渗透角度而言,我们只需要将不信任的代码给他hook掉就可以了。
在 checkServerTrusted方法处打断点,可以看一下它服务端证书校验的信任过程:
有一个变量 "chain"
,这个里面就是它的证书校验链。
如果校验不通过,就会抛出一个异常。这块将它直接给hook掉。。。
0x03 Frida Hook实现sslunpinning
checkServerTrusted 方法
根据前面的代码,可以得到checkServerTrusted
校验的整个逻辑如下:
checkServerTrusted方法 ————> 归属于X509TrustManager类 X509TrustManager类————>是TrustManager接口的一个实现类 TrustManager[]数组————>里面定义了所有需要校验的内容
|
X509TrustManager类有三个方法:
- checkServerTrusted:校验服务端
- checkClientTrusted:校验客户端
- getAcceptIssuers:证书信任链
所以,Hook的原理就是:将所有实现了 TrustManager 接口实现类中的 checkServerTrusted 方法给hook掉
1、首先,枚举进行匹配,打印出所有实现了TrustManager 接口
的类
function main(){ Java.preform(function(){ function checkIsImplementInterface(className,interfaceName){ try{ var cls = Java.use(className); if(cls.class.isInterface()){ return false; } if(cls.class != undefined){ if (interfaceName.class.isAssignableFrom(cls.class)){ return true; } return false; } }catch(e){ return false; } } var trustManagerInterface =Java.use("javax.net.ssl.TrustManager"); Java.enumerateLoadedClasses({ onMatch:function(className){ if(checkIsImplementInterface(className,trustManagerInterface)){ console.log(className); } }, onComplete:function(){ consoole.log('done!'); } }) }) }
|
调用后,成功打印出了实现了TrustManager接口的类。
2、将实现接口类中的checkServerTrusted方法hook掉
这块使用for循环打印来做,是为了防止有重载的情况。。。
function main() { Java.perform(function() {
function checkIsImplementInterface(className, interfaceName) { try { var cls = Java.use(className); if (cls.class.isInterface()){ return false; } if (cls.class != undefined){ if (interfaceName.class.isAssignableFrom(cls.class)){ return true } return false; } } catch (e) { return false; }
}
var trustManagerInterface = Java.use('javax.net.ssl.TrustManager');
Java.enumerateLoadedClasses({ onMatch: function(className) { if(checkIsImplementInterface(className, trustManagerInterface)){ console.log(className); var targetClass = Java.use(className); var len = targetClass["checkServerTrusted"].overloads.length; for (var i = 0; i < len; i++) { targetClass["checkServerTrusted"].overloads[i].implementation = function() { console.log("checkServerTrusted called"); } } } }, onComplete: function() { console.log('done'); } }); }) };
setImmediate(main);
|
可以看到这块使用Charle是可以成功抓取到数据包的,内容也成功加载出来了
3、但是Frida脚本报错了,报错原因是说 “hook的方法没有设定返回值”
这块猜测原因为:hook的一些类的checkServerTrusted方法,有的是有返回值的,有的没有。这块修改代码如下:
certificatePinner证书绑定
certificatePinner的作用就是提前做一个证书校验,将证书的哈希值提前预存进去,每次建立连接的时候,会先校验服务端证书的哈希是否和本地匹配。
这块以OkHttpClient初始化部分改为下面代码锁定证书:
CertificatePinner certificatePinner = new CertificatePinner.Builder() .add("*.taobao.com", "sha256/IfXz1a0gWBA5oH+zasmRutUiyoZN3I8wLxHNQxk3NVo=") .add("*.taobao.com", "sha256/IQBnNBEiFuhj+8x6X8XLgh01V9Ic5/V3IRQLNFFc7v4=") .add("*.taobao.com", "sha256/K87oWBWM9UZfyddvDfoxL+8lpNyoUB2ptGtn0fv6G2Q=") .build();
OkHttpClient okHttpClient = new OkHttpClient().newBuilder().certificatePinner(certificatePinner).build();
|
这块的hash值可以使用 CertificatePinner.pin(certificate)来获取。
这样通过代理证书肯定是无法请求到数据和抓包了,使用下面代码Hook:
function hookCertificatePinner(){ Java.perform(function () { let Builer = Java.use("okhttp3.CertificatePinner$Builder"); Builer.add.implementation = function () { console.log("Frida Hook hookCertificatePinner()", "Success!!!"); return this; } }); }
|
Hook之后可以正常取得数据并抓到了:
HostnameVerifier校验
HostnameVerifier
代码实现如下:
builder.hostnameVerifier(new HostnameVerify){ @Override public boolean verify(String hostname, SSLSession session){ if (hostname.equals('www.baidu.com')){ try{ Certificate[] peerCertificates = session.getPeerCertificates(); if(peerCertificates[0].getPublicKey().equals(myCertificate.getPublicKey())){ return true; }else{ return false; } }catch(SSLPeerUnverifiedException e){ e.printStackTrace(); return false; } } return false; } }
|
【注】这块的”hostnameVerify” 是一个接口,如果我们要hook它的verify方法,就要先找到实现这个接口的所有类。然后hook这些类的verify方法
判断当前类是否是实现hostnameVerify接口的类
checkIsImplementInterface(className, interfaceName)
Java.perform(function () { function checkIsImplementInterface(className, interfaceName) { try { var cls = Java.use(className); if (cls.class.isInterface()){ return false; } if (cls.class != undefined){ if (interfaceName.class.isAssignableFrom(cls.class)){ return true } return false; } } catch (e) { return false; } } });
|
打印出所有实现了hostnameVerify接口的类
Java.perform(function () { var hostnameVerifierInterface = Java.use("javax.net.ssl.HostnameVerifier"); Java.enumerateLoadedClasses({ onMatch: function (className) { if(checkIsImplementInterface(className, hostnameVerifierInterface)){ onsole.log(className); } }, onComplete: function () { console.log("Enumeration completed."); } }); });
|
对实现类中的verify方法进行hook
Java.perform(function () {
var hostnameVerifierInterface = Java.use("javax.net.ssl.HostnameVerifier"); Java.enumerateLoadedClasses({ onMatch: function (className) { if(checkIsImplementInterface(className, hostnameVerifierInterface)){ onsole.log(className);
var targetClass = Java.use(className); var len = targetClass['verify'].overload('java.lang.String', 'javax.net.ssl.SSLSession').length;
for(var i = 0; i < len; i++){ targetClass['verify'].overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function(hostname, session){ console.log("verify: " + hostname); return true; } } } }, onComplete: function () { console.log("Enumeration completed."); } }); });
|
完整hook代码如下
Java.perform(function () { function checkIsImplementInterface(className, interfaceName) { try { var cls = Java.use(className); if (cls.class.isInterface()){ return false; } if (cls.class != undefined){ if (interfaceName.class.isAssignableFrom(cls.class)){ return true } return false; } } catch (e) { return false; } }
var hostnameVerifierInterface = Java.use("javax.net.ssl.HostnameVerifier"); Java.enumerateLoadedClasses({ onMatch: function (className) { if(checkIsImplementInterface(className, hostnameVerifierInterface)){ onsole.log(className);
var targetClass = Java.use(className); var len = targetClass['verify'].overload('java.lang.String', 'javax.net.ssl.SSLSession').length;
for(var i = 0; i < len; i++){ targetClass['verify'].overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function(hostname, session){ console.log("verify: " + hostname); return true; } } } }, onComplete: function () { console.log("Enumeration completed."); } }); });
|
0x04 objection的SSLunpinning
使用 objection 启动explore模式:objection -g com.actorn.netcapture explore
然后再输入android sslpinning disable绕过证书绑定
然后再点击app的后三个按钮,就可以抓到https的请求包了
0x05 混淆框架之关键点寻找
如果有对代码进行混淆处理,那么直接hook对应的类和方法,这样在内存中是直接找不到的。那么也就会导致hook失败。
这种情况怎么处理呢???
1、实战样本:滴答清单.apk
2、使用charles开启抓包,但抓包失败
报错 “Client closed the connection before a request was made. Possibly the SSL certificate was rejected.”
3、通过报错可以看出是存在客户端校验的,这块直接使用常见 SSLUnpinning,没啥用
4、猜测可能是APP本身有做了一些混淆。
(1)首先未加壳,直接使用jadx打开
(2)打开后,发现使用了OKhttp3框架
(3)可以看到做了一定的混淆处理,直接和之前一样hook肯定是不行了。。
(4)这块可以尝试搜索关键的类和函数,看能不能定位到
①TrustManager校验:OkhttpClinet—没找到
②certificatePinner证书绑定:certificatePinner
(5)定位到证书绑定代码位置 hq.f
在f类的a方法上,刚好有定义certificatePinner绑定校验
(6)Frida编写代码 hook hq.f.a
function main() { Java.perform(function() { var className = Java.use("hq.f"); className.a.implementation = function() { console.log("certificatePinner called") } }); }
setImmediate(main)
|
(7)成功抓包!