0x01 双向证书校验

双向认证,顾名思义,客户端和服务器端都需要验证对方的身份,在建立Https连接的过程中,握手的流程比单向认证多了几步。单向认证的过程,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。双向通信流程,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。而常规的https只是客户端校验服务端的证书验证服务器的身份。因此双向证书认证在一定程度上能增大中间人攻击的难度和我们抓包分析的难度。

下图为双向认证的流程图,如下所示:

img

0x02客户端校验 VS 服务端校验

客户端校验

适用场景:如果服务器数量非常多、app版本众多,app在实现Https的策略上通常会采取客户端校验服务器证书的策略。

常见的校验方式:

  • X509TrustManager校验
  • certificatePinner证书绑定
  • HostnameVerifier验证

服务端校验

适用场景:比如银行、公共交通、游戏等行业,这种服务器高度集中、数量比较少,全国就那么几台、且app版本较少、对应用的版本控制的非常严格的时候,服务器就会对app的证书进行校验。

如果使用了服务端校验,也就是说app客户端一定是夹带了一张证书的。最常用的就是p12证书了。

获取到客户端携带的证书和证书密码,这样将证书导入进抓包软件,如Charles或burp中即可。

0x03 密钥证书自吐实践

服务端的校验:是需要服务端来校验客户端携带的证书摘要是否合法。。。

如果要绕过服务端校验,那么就需要获取客户端的证书和密钥。。。

这块以网上最常分析的soul 为例。

1. 绕过SSLPinning

直接使用xpose插件,安装JustTrustMe等这些插件,绕过SSLPinning。

image.png

发现我们使用burpsuite、charles等抓包工具也只能抓到上行包

image.png

2. 服务端校验

解决了服务器证书校验和ssl pining后,可以抓到上行包,但观察响应时,会发现服务器返回了400异常信息:

image-20240707180621261

这个信息告诉我们,我们用了抓包工具以后发出去的包没有带上客户端证书,所以服务器端拒绝处理。

想要正常抓包,就必须导入客户端证书,同时需要输入证书密码。

3. 直接获取客户端证书

这就说明app使用了双向证书校验,也就是说app客户端一定是夹带了一张证书的。最常用的就是p12证书了。所以我们可以直接用apktool等工具反编译apk或者直接解压缩。证书这类文件通常放在assets目录下面,我们在里面找p12后缀名的文件即可:

image-20240707180652715

把这个文件复制出来保存到任意位置,第一步就完成了。但光有证书,不知道证书密码也没用。
所以第二部:找证书密码。
直接dex2jar或者jadx等工具逆向出java代码,搜索关键词“client.p12”,或者搜索java.security.KeyStore.open()方法,基本能直接找到使用证书的地方。此处我们搜索“client.p12”:

image-20240707180842889

如果是jadx可以右键查找使用的地方,跟进去:

image-20240707180905450

上下文中就能找到加载证书的地方,找到load方法,这个load方法第一个参数是证书文件流,第二个参数即为证书的密码,所以我们只要知道这第二个参数传进来的值是什么就可以了。

从代码可以看到第二个参数c2来源是SoulNetworkSDK.c方法,c方法返回值即为我们需要的证书密码,这时候办法就多了,直接使用Frida尝试hook即可:

image-20240707181053744

这样就获得了客户端存储的证书和对应的密码了。。。

4. R0Capture获取证书

这块直接参考R0Capture中的代码,来实现证书获取

function main(){
Java.perform(function(){
function storeP12(pri, p7, p12Path, p12Password) {
var X509Certificate = Java.use("java.security.cert.X509Certificate") //导入X509Certificate类
var p7X509 = Java.cast(p7, X509Certificate); //转换为X509Certificate类型
var chain = Java.array("java.security.cert.X509Certificate", [p7X509]) //创建证书链数组
var ks = Java.use("java.security.KeyStore").getInstance("PKCS12", "BC"); //创建KeyStore对象
ks.load(null, null); //加载KeyStore对象
ks.setKeyEntry("client", pri, Java.use('java.lang.String').$new(p12Password).toCharArray(), chain); //设置私钥和证书链到KeyStore对象
try {
var out = Java.use("java.io.FileOutputStream").$new(p12Path); //创建文件输出流
ks.store(out, Java.use('java.lang.String').$new(p12Password).toCharArray()) //保存KeyStore对象到p12文件
} catch (exp) {
console.log(exp) //输出异常信息
}
}
//在服务器校验客户端的情形下,帮助dump客户端证书,并保存为p12的格式,证书密码为r0ysue
Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation = function () {
var result = this.getPrivateKey()
var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName();
storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + '.p12', 'r0ysue');
console.log("dumpClinetCertificate=>" + '/sdcard/Download/' + packageName + '.p12' + ' pwd: r0ysue');
return result;
}
Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain.implementation = function () {
var result = this.getCertificateChain()
var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName();
storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + '.p12', 'r0ysue');
console.log("dumpClinetCertificate=>" + '/sdcard/Download/' + packageName + '.p12' + ' pwd: r0ysue');
return result;
}
});
}


setImmediate(main);

Frida加载上面的代码,来实现密钥证书自吐:

【注意】这块最好使用spawn的方式,因为APP已经初始化了,使用attach附加的方式可能dump不下来

frida -U -f com.soulapp.android -l saveCertificate.js   --no-pause

运行代码后,发现没有任何反应,也没有打印console.log

img

这是因为还在开着抓包软件,导致我们不能正常访问APP,关闭抓包后,成功dump下来了证书:

image.png

image.png

证书文件详情,可以使用keystore查看:

image.png

5. 抓包软件配置客户端证书

这块以Charles为例,其他的类似。。。

在charles中添加APP客户端证书,可以过服务端校验。。。

这块将刚刚使用Frida dump下来的证书,加载到charles中。(charles是支持客户端证书导入的)

image.png

image.png

块的密码可以随便设置,就先设置为 “12345”

image.png

image.png

点击【Add】进行证书导入:

image.png

重新抓包,发现可以成功抓包了:

image.png