0x01 客户端段SSL证书绑定

SSL Pinning 也称为证书锁定,是Google官方推荐的检验方式,意思是将服务器提供的SSL/TLS证书内置到移动客户端,当客户端发起请求的时候,通过对比内置的证书与服务器的证书是否一致,来确认这个连接的合法性。

SSL Pinning 一般实现方式有两种

  • 一种是在代码层进行校验
  • 一种是通过配置文件network_security_config.xml进行配置实现校验。

代码层实现

以常见的OKHttp网络框架举例,在代码层面校验证书的代码如下:

image.png

详细的代码实现可以参考:

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,配置文件可以分为两种,使用证书校验和公钥校验。

  • 使用证书锁定,由于CA签发证书存在有效期的问题,所以在证书续期后,需要将证书重新内置到App中(不方便,应用较少)。

  • 使用公钥锁定,需要提取证书中的公钥并内置到app中,公钥在证书续期后可以保持不变(即密钥对可以不变),可以避免有效期问题。

下面我们先看看证书锁定和公钥锁定的具体配置实现:

image.png

单向认证校验流程

具体的SSLPinning的交互过程,可以使用如下流程图来表示:

img

通过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预置的证书信息与服务端返回的证书信息校验不一致,导致握手失败

img

0x02 SSLPinning原理

代码解读

image.png

将代码分为以下几个部分:

  1. 调用x509的标准,创建了一个证书(包含公钥、签发信息等等);
  2. 生成证书仓库keyStore并初始化,将第一步创建的证书加载到keyStore中;
  3. 首先拿到默认的信任管理器工厂,将keyStore加载进去,也就是说,只要有存在于KeyStore中的证书,是可以校验通过的;
  4. 新建了SSL的对象,用来做SSL握手。进行了init初始化,里面有三个参数:
  5. km:null==>做服务端校验(服务端校验客户端证书)
  6. trustManagers==>做客户端校验(客户端校验服务端证书)
  7. new SecureRandom()==>SSL握手过程中创建随机数,安全考虑
  8. 上面的四个步骤其实都是为下面做铺垫的,这块调用了OKHttp.builder框架,实现了信任证书工厂中的证书,不信任系统证书。【第一层校验】
  9. 证书绑定,也是SSLPinning的一种校验方式。【第二次校验】
  10. HostnameVerifier,也是一种校验方式【第三层校验】

下面的代码可以看做是一些常见的SSLPinning实现的代码:

//第一种校验方式:HostnameVerifier
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();
}

//第二种校验方式:OKHttp.builder校验
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();
}


//第三种校验:CertificatePinner证书绑定
private void sendRequestWithOkHttp3(){
new Thread(new Runnable() {
@Override
public void run() {
final String CA_DOMAIN = "www.baidu.com";
//获取公钥:
//openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
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类的关系:

image-20240706200808341

SSLPinning原理&代码实现

原理是—>(不信任系统库证书,只信任自己的证书)

1、正常情况,要是不对X509TrustManager接口中的方法(checkServerTrusted)进行重写的话,客户端默认是信任所有证书的。

image.png

2、但如果有做了校验的话,客户端会根据设置校验服务端的证书。

下图中的代码,是自己设置了一个证书(CA证书申请是要钱的),但如果是自己只做的证书,手机是不认可的,所有用代码实现了校验。。。

image.png

3、代码中引入了trustManagers,trustManagers是一个数组。在trustManagers里面包含了一个keyStore,keyStore里面就是我们定义好的证书

所以,就是在trustManagers中存放了我们自己定义好的证书,我们才可以校验通过。

image.png

所以,这块针对与客户端来说,做了SSL证书绑定之后,默认是不信任系统证书列表中的证书的。简单来说就是,即时将Charles证书放到了系统证书列表中,相对于设置了trustManagers的客户端而言,服务端给到Charle证书不是在定义好的证书商店中,所以这块会校验不通过。

4、 “TrustManager[] trustManagers“ 信任管理器。

trustManager是一个接口,在里面定义了实现类,这块用的是X509TrustManager类,里面定义了一个 checkServerTrusted方法,如下:

平常针对APP的校验逻辑一般都是写在这个方法里面的

image.png

这块如果将 checkServerTrusted方法体置空,默认是信任所有证书的。也就是说,如果遮掩个设置了,即使Charles的证书不在系统证书列表中也是可以正常抓包的。

image-20240706195000810

同理,如果要实现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方法处打断点,可以看一下它服务端证书校验的信任过程:

image.png

有一个变量 "chain",这个里面就是它的证书校验链。

image.png

如果校验不通过,就会抛出一个异常。这块将它直接给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;
}
}


//首先声明TrustManager接口
var trustManagerInterface =Java.use("javax.net.ssl.TrustManager");

//调用Frida的API:Java.enumerateLoadedClasses枚举内存中的所有类
//再调用前面定义好的函数checkIsImplementInterface来判断当前类是否实现了TrustManager接口
Java.enumerateLoadedClasses({
onMatch:function(className){
if(checkIsImplementInterface(className,trustManagerInterface)){
console.log(className);
}
},
onComplete:function(){
consoole.log('done!');
}
})
})
}

调用后,成功打印出了实现了TrustManager接口的类。

image.png

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); //打印出实现了TrustManager接口的类名
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);

image-20240706202348324

可以看到这块使用Charle是可以成功抓取到数据包的,内容也成功加载出来了

image.png

3、但是Frida脚本报错了,报错原因是说 “hook的方法没有设定返回值”

这块猜测原因为:hook的一些类的checkServerTrusted方法,有的是有返回值的,有的没有。这块修改代码如下:

image.png

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)来获取。

image.png

这样通过代理证书肯定是无法请求到数据和抓包了,使用下面代码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之后可以正常取得数据并抓到了:

img

HostnameVerifier校验

HostnameVerifier代码实现如下:

builder.hostnameVerifier(new HostnameVerify){
@Override
public boolean verify(String hostname, SSLSession session){ //传入两个参数,hostname:服务器域名 session:会话信息,里面会包含证书相关内容
if (hostname.equals('www.baidu.com')){ //首先判断hostname是否为www.baidu.com(这块随便写的)
try{
Certificate[] peerCertificates = session.getPeerCertificates(); //从session获取到服务器端证书相关信息
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()){ //判断这个传入的class是不是一个接口
return false;
}
if (cls.class != undefined){
if (interfaceName.class.isAssignableFrom(cls.class)){
//java中,isAssignableFrom 方法用于确定一个类是否可以被另一个类赋值
//frida中,检查 cls 是否实现了 interfaceName 接口,或者 cls 是否是 interfaceName 接口的子类型
return true
}
return false;
}
} catch (e) {
return false;
}
}
});

打印出所有实现了hostnameVerify接口的类

Java.perform(function () {
var hostnameVerifierInterface = Java.use("javax.net.ssl.HostnameVerifier");
Java.enumerateLoadedClasses({ //调用Java.enumerateLoadedClasses来遍历所有加载的类
onMatch: function (className) {
if(checkIsImplementInterface(className, hostnameVerifierInterface)){
onsole.log(className); //打印出实现了TrustManager接口的类名
}
},
onComplete: function () {
console.log("Enumeration completed.");
}
});

});

对实现类中的verify方法进行hook

Java.perform(function () {

var hostnameVerifierInterface = Java.use("javax.net.ssl.HostnameVerifier");
Java.enumerateLoadedClasses({ //调用Java.enumerateLoadedClasses来遍历所有加载的类
onMatch: function (className) {
if(checkIsImplementInterface(className, hostnameVerifierInterface)){
onsole.log(className); //打印出实现了TrustManager接口的类名

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; //返回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()){ //判断这个传入的class是不是一个接口
return false;
}
if (cls.class != undefined){
if (interfaceName.class.isAssignableFrom(cls.class)){
//java中,isAssignableFrom 方法用于确定一个类是否可以被另一个类赋值
//frida中,检查 cls 是否实现了 interfaceName 接口,或者 cls 是否是 interfaceName 接口的子类型
return true
}
return false;
}
} catch (e) {
return false;
}
}

var hostnameVerifierInterface = Java.use("javax.net.ssl.HostnameVerifier");
Java.enumerateLoadedClasses({ //调用Java.enumerateLoadedClasses来遍历所有加载的类
onMatch: function (className) {
if(checkIsImplementInterface(className, hostnameVerifierInterface)){
onsole.log(className); //打印出实现了TrustManager接口的类名

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; //返回true表示验证通过
}
}
}
},
onComplete: function () {
console.log("Enumeration completed.");
}
});

});

0x04 objection的SSLunpinning

使用 objection 启动explore模式:objection -g com.actorn.netcapture explore

然后再输入android sslpinning disable绕过证书绑定

img

然后再点击app的后三个按钮,就可以抓到https的请求包了

img

0x05 混淆框架之关键点寻找

如果有对代码进行混淆处理,那么直接hook对应的类和方法,这样在内存中是直接找不到的。那么也就会导致hook失败。

这种情况怎么处理呢???

1、实战样本:滴答清单.apk

image.png

image.png

2、使用charles开启抓包,但抓包失败

image.png

报错 “Client closed the connection before a request was made. Possibly the SSL certificate was rejected.”

image.png

3、通过报错可以看出是存在客户端校验的,这块直接使用常见 SSLUnpinning,没啥用

image.png

4、猜测可能是APP本身有做了一些混淆。

(1)首先未加壳,直接使用jadx打开

image.png

(2)打开后,发现使用了OKhttp3框架

image.png

(3)可以看到做了一定的混淆处理,直接和之前一样hook肯定是不行了。。

image.png

(4)这块可以尝试搜索关键的类和函数,看能不能定位到

①TrustManager校验:OkhttpClinet—没找到

image.png

②certificatePinner证书绑定:certificatePinner

image.png

(5)定位到证书绑定代码位置 hq.f

在f类的a方法上,刚好有定义certificatePinner绑定校验

image.png

(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)成功抓包!

image.png