要分析抓包的技术首先要介绍数字证书是什么,一切的抓包手段都是围绕数字证书做文章
数字证书
TLS 握手的作用之一是身份认证,被验证的一方需要提供一个身份证明,在 HTTPS 的世界里,这个身份证明就是 TLS 证书,或者称为 数字证书
JDK 中用 java.security.cert.X509Certificate 来表示一个证书,它继承自抽象类 java.scurity.cert.Certificate,通过 X509Certificate 我们可以获取证书的信息,例如x509Certificate.getSubjectDN().getName()
x509Certificate.getSubjectDN().getName()获取到的是证书的Subject's Name的信息即证书拥有者,内容是一组符合 X.500 规范的 DN(Distinguished Name)
CN=*,OU=IT Department,O=China Merchants Bank Co., Ltd,L=Shenzhen,C=CN
这是从我们证书中获取到的信息,DN的属性含义如图所示
通过命令openssl x509 -in base64.cer –text 查看证书信息,输出如图所示
可以看出,一个 Certificate 由 Data 和 Signature 两部分组成。其中 Data 包含的内容有:
证书版本号:X.509v3
序列号:一个 CA 机构内是唯一的,但不是全局唯一
签名算法:签名的计算公式为RSA(sha256(Data), IssuerPrivateKey)
签发者:DN(Distinguished Name)
有效期:证书的有效期间 [Not Before, Not After]
证书拥有者:也是一个 DN公钥长度一般是 2048bit,1024bit已经被证明不安全
扩展字段:证书所携带的域名信息会配置在 SAN 中(X509v3 Subject Alternative Name)
Signature 位于证书最末尾,签名算法 sha256WithRSAEncryption 在 Data 域内已经指明 ,而 RSA 进行非对称加密所需的私钥(Private Key)则是由 Issuer 提供,Issuer 是一个可以签发证书的证书,由证书权威 CA 提供,CA 需要保证证书的有效性。
因为 Signature 是 RSA 算法生成的,那么客户端拿到 TLS 证书之后,需要 Issuer 的公钥(Public Key)才能解码出 Data 的摘要。
然而证书只携带了 Issuer 的 DN,并没有公钥,为了弄清楚客户端如何获取公钥,我们需要先搞明白 Certificate Chain(证书链)。
证书链
一个完整的证书链一般由三种类型的证书组成
- Root Certificate (DigiCert Global Root CA) :
根证书
Root Certificate是由Root CAs 发布,一个Root CAs 下包含多个Intermediate CAs ,同理一个Root Certificate下可以包含多个Intermediate Certificate。根证书是CA自己的证书,是证书验证链的开头 - Intermediate Certificate (DigiCert SHA2 Secure Server CA):
中间证书或者叫做中介证书
Intermediate Certificate是由Intermediate CAs 发布,一个Intermediate CAs 下可以包含多个Intermediate CAs ,同理一个Intermediate Certificate下可以包含多个Intermediate Certificate也可以包含End-entity Certificate。CA会使用Intermediate Certificate 替代Root Certificate去做服务器端的证书签名,确保根证书密钥绝对不可访问 -
End-entity Certificate (user.cmbchina.com):
终端证书即用户证书
包含用来加密传输数据的公钥的证书,是HTTPS中使用的证书。 End-entity Certificate上面几级证书都是为了保证End-entity Certificate未被篡改,保证是CA签发的合法证书,进而保证End-entity Certificate中的公钥未被篡改
证书链验证
1.客户端得到服务端返回的证书,通过读取得到 服务端证书的发布机构(Issuer)
2.客户端去操作系统查找这个发布机构的的证书,如果是不是根证书就继续递归下去直到拿到根证书。
3.匹配客户端保存的可信任CA与根证书的CA,确保根证书的合法性
4.用 根证书的公钥 去 解密验证 上一层证书的合法性,再拿上一层证书的公钥去验证更上层证书的合法性;递归回溯。
5.最后验证服务器端的证书是 可信任 的
关于证书链信任认证过程可以参考我的另一篇文档
抓包原理-中间人攻击
前面基础性内容已经铺垫完毕,现在正式分析抓包技术
根据流程可以得出需要让中间人攻击生效,必须让客户端的证书连验证能够通过,并且现在几乎所有攻击手段都是从这入手,实现原理上可以分为两大类:
1.攻击客户端CA信任库,让系统在误认为伪证书是可以信任的,从而向中间人发送信息。手段包括
手机系统中安装伪证书
root手段导入伪证书至证书库
修改rom中系统证书库并烧录系统
例如Charles,Burp,Magisk,Packet Capture(VPN)都是使用这种方法
SSL PINING正是对抗这种技术的方法
2.通过脚本或者框架hook验证证书的方法,主要手段有:
修改系统checkServerTrusted或者TrustManager等涉及验证或者整理证书的相关代码,返回他们需要的结果
针对有自己的证书验证逻辑的第三方框架例如OKHttp,则hook修改CertificationPinner等方法
APP使用自己封装的通信方法,则通过逆向代码hook相关方法
Xposed+JustTrustMe或者Frida使用的是这种方法,针对这种技术则要避免将涉及通信的代码暴露,可以使用混淆,加壳等多种手段
SSL PINNING与双向认证
绿色表示ssl pinning在正常HTTPS通信过程中增加的操作
红色表示双向认证在正常HTTPS通信过程中增加的操作
SSL PINNING本地证书管理
SSL PINNING唯一缺点即是如何对本地绑定的证书进行更新,针对该问题研究了一种解决方案方案:
- 证书服务器:搭建一个CA服务器专门用于更新证书或者在验证证书时通过访问CA服务器验证
- 与证书服务器通信内容及证书间比对都采用不可逆加密信息,防止中间人拿到通信内容反推出明文
- 建立客户端及证书服务器端黑名单,尽可能减少与通信次数并增加中间人攻击成本
-
证书服务器与客户端采用相同加密算法,保证加密后信息一致
Hook对抗方案
在攻击手段中,hook代码是一个较难防范的点,针对常用的Xposed框架可以做一下防范措施:
-
检测进程中使用so名中包含关键"hack|inject|hook|call" 的信息
-
检测手机是否被root:含有su程序和ro.secure是否为1
防止被Hook的方式就是可以查看XposedBridge这个类,有一个全局的hook开关,所有有的应用在启动的时候就用反射把这个值设置成true,这样Xposed的hook功能就是失效了
-
Xposed的hook原理的就是在程序启动都注入jar功能,所以安装hook模块之后,每个应用内部都包含了这个Xposed功能jar,就相当于你的应用中有了Xposed的所有功能类,所以在应用中反射Xposed的类是可以成功的
-
如果应用被Xposed进行hook操作之后,抛出的异常堆栈信息中就会包含Xposed字样,所以可以通过应用自身内部抛出异常来检测是否包含Xposed字段来进行防护
如何在代码中执行shell命令并返回结果
public static ArrayList<String> executeCommand(String[] shellCmd){
String line = null;
ArrayList<String> fullResponse = new ArrayList<String>();
Process localProcess = null;
try {
Log.d("gscgsc","excute cmd");
localProcess = Runtime.getRuntime().exec(shellCmd);
} catch (Exception e) {
return null;
}
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(localProcess.getOutputStream()));
BufferedReader in = new BufferedReader(new InputStreamReader(localProcess.getInputStream()));
try {
while ((line = in.readLine()) != null) {
Log.d("gscgsc","Line received: " + line);
fullResponse.add(line);
}
} catch (Exception e) {
e.printStackTrace();
}
Log.d("gscgsc","–> response was: " + fullResponse);
return fullResponse
}
SSL PINING实现
String hostname = "xxx.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
.add(hostname, "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
.add(hostname, "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();
Request request = new Request.Builder()
.url("https://" + hostname)
.build();
client.newCall(request).execute();
SSL PINING实现原理
证书校验时机
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
//包装socket
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
//配置socket参数
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
//建立握手
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
//证书校验
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
//检验无异常则保存连接
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
}
//校验证书与本地证书
public void check (String hostname, List < Certificate > peerCertificates)
throws SSLPeerUnverifiedException {
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
//加密后本地证书的存储
Pin pin = pins.get(p);
//sha256加密
if (pin.hashAlgorithm.equals("sha256/")) {
if (sha256 == null) sha256 = sha256(x509Certificate);
if (pin.hash.equals(sha256)) return; // Success!
}
//sha1加密
else if (pin.hashAlgorithm.equals("sha1/")) {
if (sha1 == null) sha1 = sha1(x509Certificate);
if (pin.hash.equals(sha1)) return; // Success!
}
}
//校验失败抛出异常
throw new SSLPeerUnverifiedException(message.toString());
}
结果如下