平时的安全研究经常会有抓包的需求,随着互联网厂商安全意识的不断提高,多数Android APP采用了https协议和服务端进行通信,增加了中间人攻击的难度。
在使用burpsuilt、Fiddler等工具抓https数据包时,通常需要在手机上安装一个证书,即便这样仍然还会出现抓不到包等各种各样的错误和问题,为了解决这些问题,有必要了解下https的原理和实现,方可在遇到问题时根据不同的情况来解决。
文章有点长,搞个图便于以后采坑定位:
什么是证书
这里的证书是指https数字证书,为了防止被中间人挟持,整个互联网都在由http向https切换,证书是https协议中的一个重要角色,本质上证书被用来验证“一个站点是不是真正的站点”。下图是https的协议过程,其中第4阶段就是证书的校验阶段
那么如何通过证书来验证“一个站点是不是真正的站点”呢?首先从证书的内容说起。
使用openssl可以查看证书的内容,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40openssl x509 -noout -text -in kubernetes.pem
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
06:6c:9f:d7:c1:bb:10:4c:29:43:e5:71:7b:7b:2c:c8:1a:c1:0e
Signature Algorithm: ecdsa-with-SHA384
Issuer: C=US, O=Amazon, CN=Amazon Root CA 4
Validity
Not Before: May 26 00:00:00 2015 GMT
Not After : May 26 00:00:00 2040 GMT
Subject: C=US, O=Amazon, CN=Amazon Root CA 4
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (384 bit)
pub:
04:d2:ab:8a:37:4f:a3:53:0d:fe:c1:8a:7b:4b:a8:
7b:46:4b:63:b0:62:f6:2d:1b:db:08:71:21:d2:00:
e8:63:bd:9a:27:fb:f0:39:6e:5d:ea:3d:a5:c9:81:
aa:a3:5b:20:98:45:5d:16:db:fd:e8:10:6d:e3:9c:
e0:e3:bd:5f:84:62:f3:70:64:33:a0:cb:24:2f:70:
ba:88:a1:2a:a0:75:f8:81:ae:62:06:c4:81:db:39:
6e:29:b0:1e:fa:2e:5c
ASN1 OID: secp384r1
NIST CURVE: P-384
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Digital Signature, Certificate Sign, CRL Sign
X509v3 Subject Key Identifier:
D3:EC:C7:3A:65:6E:CC:E1:DA:76:9A:56:FB:9C:F3:86:6D:57:E5:81
Signature Algorithm: ecdsa-with-SHA384
30:65:02:30:3a:8b:21:f1:bd:7e:11:ad:d0:ef:58:96:2f:d6:
eb:9d:7e:90:8d:2b:cf:66:55:c3:2c:e3:28:a9:70:0a:47:0e:
f0:37:59:12:ff:2d:99:94:28:4e:2a:4f:35:4d:33:5a:02:31:
00:ea:75:00:4e:3b:c4:3a:94:12:91:c9:58:46:9d:21:13:72:
a7:88:9c:8a:e4:4c:4a:db:96:d4:ac:8b:6b:6b:49:12:53:33:
ad:d7:e4:be:24:fc:b5:0a:76:d4:a5:bc:10
客户端通过证书来验证服务端的合法性,包括:
1. 验证证书的颁发机构是否受浏览器信任
2. 验证证书的有效期
3. 验证部署SSL证书的网站域名与证书颁发的域名是否一致
等等。
到这里可能有些晕了,证书本来就在客户端放着,怎么自己校验自己?其实https协议中的证书分服务端证书和客户端证书,部署https环境的时候会要求生成这两个证书,证书校验是指用“客户端的证书来校验服务端的证书”,他们两个是PKI体系,客户端证书里有一个字段是CA的公钥,用于解开服务端证书的内容以进行下一步的校验。
通常,在浏览器中会内置一些根证书,如果网站的证书是这些信任根发的或者信任根的二级证书机构颁发的,那么浏览器才会同服务器发起ssl连接。如果验证失败,则拒绝连接。证书的校验包括证书是否过期、域名和服务器域名是否一致等等,由于用户只是作为浏览网页的角色,这些校验功能由浏览器或操作系统的内置代码来完成。
不同于浏览器,APP除了靠系统或浏览器,还可以自己实现代码来校验证书,因此在APP中证书校验通常存在三种情况:
APP通过系统和浏览器校验证书;
APP自己校验证书,也叫证书绑定;
APP除了校验服务端的证书,服务端还可以校验APP的证书,即https双向校验;
本文主要基于Android平台对以上三种情况下的中间人攻击(抓包)进行分析。
Android平台下https抓包实践
对于中间人来说,能成功攻击的核心在于“如何让客户端认为我是服务端?”。在现有https协议不变的情况下,中间人既要当真实客户端的“服务者”,又要当真实服务端的“客户”,作为https协议下的“服务者”,自然是需要给客户端提供证书的,因此,在有证书校验的前提下,问题就变成了“如何让客户端信任中间人的证书?”
我们将根据上述三种情况分别分析:
1. APP通过系统和浏览器校验证书
由于证书校验是由系统或浏览器完成,那么只需要系统或浏览器信任中间人的证书即可,现有的操作系统或浏览器都有导入可信证书的功能,因此通过这些功能直接导入中间人的证书即可,这也解释了为什么burpsuilt、Fiddler等工具在抓包时需要往手机里导入一个证书,信任了证书后的攻击过程就由这些工具来搞定了。
这里以burpsuit为例,贴出在Android上导入burp证书的方法:
(1)从设置中手动导入证书
- 1.代理都设置开启好之后,在浏览器访问http://burp ,点击”CA Certificate”下载证书-“cacert.der”
- 2.重命名cacert.der 为cacert.cer或cacert.crt,并导入到手机
由于android 只支持导入以.crt 或.cer 后缀的文件形式保存的PEM格式或DER格式的证书(这里注意区分后缀和格式),如下图所示。
而burp导出的证书是DER后缀的DER格式,因此需要重命名为.crt或.cer后缀。很多文章说要用firfox或openssl等工具转换一下格式,其实只要换一个后缀就行了,详情可以参考burp官方教程https://support.portswigger.net/customer/portal/articles/1841102-installing-burp-s-ca-certificate-in-an-android-device1
2//使用openssl转换证书格式,这里不需要了但还是贴一下吧
openssl x509 -inform PEM -outform DER -in ca_cert.crt -out ca_cert_der.crt
- 3.安装证书
设置-> 安全-> 用户凭据 -> 从存储设备安装(证书),选择刚导入到手机里的证书即可。需要注意的是必须要设置锁屏密码才可以。
完成后在设置-> 安全-> 用户凭据 ->信任的凭据 中可查看刚刚安装的证书。
之后,配置好客户端的代理ip和端口就可以抓包了。
然而,安卓7.0之后有了network-security-config选项参考Android开发文档,当app支持的最小API为24(Android 7.0)或以上时(并非7.0以上的手机),默认情况下app只信任系统级别的CA,这样从sdcard安装Burp证书将无法拦截应用流量,于是就需要把burp证书变为系统证书。
这里有两种方法可以帮我们绕过:
1.将Burp CA作为系统级CA安装在设备上。这是最简单的解决方法,但需要root
2.修改manifest文件并重打包,比较麻烦,但无需root。
这里介绍一下第1种方法。
(2)root下导入证书为系统证书
(本文由于时间过长,可以在最新版本系统上遇到其他问题,Android 10/11可以参考https://pswalia2u.medium.com/install-burpsuites-or-any-ca-certificate-to-system-store-in-android-10-and-11-38e508a5541a)
有root的前提下,可以把我们自己的证书安装成系统证书,主要步骤如下:
1.转为PEM格式
1
openssl x509 -inform der -in burp.der -out burp.pem
2.计算证书哈希值:
1
openssl x509 -inform PEM -subject_hash_old -in burp.pem|head -1
假设结果是80326040,则我们把证书重命名为80326040.0。
3.把/system mount成为可读可写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16mount -o rw,remount /system
/*如果失败,尝试关闭dm_verity
$ adb remount
dm_verity is enabled on the system partition.
Use "adb disable-verity" to disable verity.
If you do not, remount may succeed, however, you will still not be able to write to these volumes.
remount succeeded
$ adb disable-verity
Verity disabled on /system
Now reboot your device for settings to take effect
$ adb reboot
$ adb root
restarting adbd as root
$ adb remount
remount succeeded
*/4.把80326040.0拷贝到Android的路径/system/etc/security/cacerts/下,添加644权限:
1
2
3chmod 644 80326040.0
chown root:root 80326040.0
reboot5.重启手机后生效(切记)
Android的系统证书路径为“/system/etc/security/cacerts/”,用户证书路径为“/data/misc/user/0/cacerts-added”,所以把用户证书move到系统证书路径也是可以的。1
2
3
4
5mount -o rw,remount /system
mv /data/misc/user/0/cacerts-added/xxxx.0 /system/etc/security/cacerts/
chmod 644 xxxx.0
chown root:root xxxx.0
reboot
Magisk有一个插件叫movecert(https://github.com/Magisk-Modules-Repo/movecert),其原理就是如此。所以不想那么复杂,直接安装Magisk的这个模块后,导入用户证书后重启手机即可变为系统证书。
(另外一个插件https://github.com/NVISO-BE/MagiskTrustUserCerts)
(3)证书有效期过长的问题
然而,我在oneplus 5T 和HTC的手机(均为8.0以上系统)通过导入burp证书为系统证书,仍然无法抓到https数据,浏览器报“NET::ERR_CERT_VALIDITY_TOO_LONG”错误,这是为什么呢?
原因是chrome从2018年开始只信任有效期少于825天(27个月)的证书(参考https://www.ssl.com/blogs/ssl-certificate-maximum-duration-825-days/),而burp发行的root证书有效期远大于27个月,在Android 7以上,即使把burp的自定义证书添加进系统证书里(但用户证书并不会检测这个有效期是否太长),这个证书也是不工作的,所以chrome会判定这个证书无效,报ERR_CERT_VALIDITY_TOO_LONG的错误。
解决的办法是自己做一个低于27个月的root证书导入burp,再通过burp重新导出证书放到系统证书路径下。这也是在(1)和(2)都无法抓包的条件下的终极解决方案。
下面来介绍如何生成root CA证书:
生成密钥
1
openssl genrsa -out key.pem 3072 -nodes
下载openssl.cnf (OpenSSL example configuration file,下载链接:https://gist.github.com/nevermoe/f1a4bbcd9cf76143e9520c717caff306) 并使用如下命令生成证书:
1
openssl req -new -x509 -key key.pem -sha256 -config openssl.cnf -out cert.pem -days 730 -subj "/C=JP/ST=/L=/O=m4bln/CN=MY CA"
把密钥和证书导出成pfx格式
1
openssl pkcs12 -export -inkey key.pem -in cert.pem -out cert_and_key.pfx
把cert_and_key.pfx导入burp
注意:需要输入创建时的密码- 导出证书,并按照(2)中的方法把导出证书变为系统证书。
需要注意的是自签名的root证书一定要有x509v3 extesion,其中包含了CA: True这个属性,不然我们生成的证书是无法导入成系统证书的。
至此,依靠系统或默认浏览器校验证书的抓包方法就到此为止了,为了更清楚的搞清楚各种方法的使用场景,这里简单总结一下:
- 在依靠系统或默认浏览器校验证书的情况下,导入burp证书为用户证书是可以抓https包的
- 当app支持的最小API为24(Android 7.0)或以上时,默认情况下app只信任系统级别的证书,需要把burp变为系统证书
- 自签名证书作为系统证书时,有效期最长不超过825天,用户证书则没有限制
总之,如果不想那么麻烦的话,不妨直接采用方案(3)作为终极方案。
2. APP自己校验证书(SSL Pinning)
如果APP自己实现证书校验时,证书验证的逻辑在app内部,与系统和浏览器无关,因此导入为用户证书或系统证书都解决不了问题。
APP自己校验证书,通常会把自己的证书或证书的hash值内置在APK安装包内,在发请求时app自身通过代码来校验证书和服务器的关系,即证书绑定(也叫Certificate Pinning或SSL Pinning)。
这种情况下如果想抓https数据包,我们有两种解决办法:
- 替换证书或证书的hash
- 通过hook绕过证书的校验逻辑
由于方案一需要找到证书的位置或hash值所在的代码片段,并进行重打包和签名,操作起来比较复杂且不通用,这里我们主要采用方案2。
通过hook绕过ssl pinning(即ssl unpinning)比较成熟的工具主要有JustTrustMe(https://github.com/Fuzion24/JustTrustMe)和DroidSSLUnpinning(https://github.com/WooyunDota/DroidSSLUnpinning)。二者的原理均为hook常用证书校验的API,不同的是前者基于Xposed,后者基于Frida。
下边我们基于Android中常见的证书绑定的的实现,来讨论绕过方法。
证书绑定实例及绕过方法
(1)使用TrustManager
1 | TrustManager[] trustAllCerts = new TrustManager[] { |
TrustManager是android用来处理证书的一个类,其子类X509TrustManager实现了各种校验。它会被SSLcontext的init函数调用并传给上层处理。1
2
3SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
因此,通过hookSSLcontext的init函数,传递一个没有内容的TrustManager即可绕过,DroidSSLUnpinning的绕过代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48// Prepare the TrustManagers array to pass to SSLContext.init()
var X509Certificate = Java.use("java.security.cert.X509Certificate");
var TrustManager;
try {
TrustManager = Java.registerClass({
name: 'org.wooyun.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {
},
checkServerTrusted: function (chain, authType) {
},
getAcceptedIssuers: function () {
// var certs = [X509Certificate.$new()];
// return certs;
return [];
}
}
});
} catch (e) {
quiet_send("registerClass from X509TrustManager >>>>>>>> " + e.message);
}
// Prepare the TrustManagers array to pass to SSLContext.init()
var TrustManagers = [TrustManager.$new()];
try {
// Prepare a Empty SSLFactory
var TLS_SSLContext = SSLContext.getInstance("TLS");
TLS_SSLContext.init(null,TrustManagers,null);
var EmptySSLFactory = TLS_SSLContext.getSocketFactory();
} catch (e) {
quiet_send(e.message);
}
// hook init() of SSLContext
var SSLContext = Java.use('javax.net.ssl.SSLContext');
var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom');
// Override the init method, specifying our new TrustManager
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {
quiet_send('Overriding SSLContext.init() with the custom TrustManager');
SSLContext_init.call(this, null, TrustManagers, null);
};
(2)使用Okhttp的CertificatePinner
绑定域名和证书的hash1
2
3
4
5
6
7
8
9val certificatePinner = CertificatePinner.Builder()
.add(
"www.example.com",
"sha256/ZC3lTYTDBJQVf1P2V7+fibTqbIsWNR/X7CWNVW+CEEA="
).build()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
DroidSSLUnpinning的绕过代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/*** okhttp3.x unpinning ***/
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
quiet_send('OkHTTP 3.x Found');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function () {
quiet_send('OkHTTP 3.x check() called. Not throwing an exception.');
}
/*** okhttp unpinning ***/
var OkHttpClient = Java.use("com.squareup.okhttp.OkHttpClient");
OkHttpClient.setCertificatePinner.implementation = function(certificatePinner){
// do nothing
quiet_send("OkHttpClient.setCertificatePinner Called!");
return this;
};
// Invalidate the certificate pinnet checks (if "setCertificatePinner" was called before the previous invalidation)
var CertificatePinner = Java.use("com.squareup.okhttp.CertificatePinner");
CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function(p0, p1){
// do nothing
quiet_send("okhttp Called! [Certificate]");
return;
};
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(p0, p1){
// do nothing
quiet_send("okhttp Called! [List]");
return;
};
(3)Manifest中配置networkSecurityConfig(Android 7.0以上)
创建res/xml/network_security_config.xml,并在
1
2
3
4
5
6
7
8
9
10<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set>
<pin digest="SHA-256">ZC3lTYTDBJQVf1P2V7+fibTqbIsWNR/X7CWNVW+CEEA=</pin>
<pin digest="SHA-256">GUAL5bejH7czkXcAeJ0vCiRxwMnVBsDlBMBsFtfLF8A=</pin>
</pin-set>
</domain-config>
</network-security-config>在manifest.xml文件中添加配置
1
2
3
4
5
6
7
8
9<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="co.netguru.demoapp">
<application
android:networkSecurityConfig="@xml/network_security_config">
...
</application>
</manifest..
DroidSSLUnpinning的绕过代码如下:1
2
3
4
5
6
7
8
9
10var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
quiet_send("TrustManagerImpl verifyChain called");
return untrustedChain;
}
var OpenSSLSocketImpl = Java.use('com.android.org.conscrypt.OpenSSLSocketImpl');
OpenSSLSocketImpl.verifyCertificateChain.implementation = function (certRefs, authMethod) {
quiet_send('OpenSSLSocketImpl.verifyCertificateChain');
}
(4)WebView绑定证书
在WebViewClient类中的onReceivedSslError回调中实现校验逻辑
代码来自https://blog.csdn.net/lsyz0021/article/details/546699141
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
381.实现证书校验函数
public static boolean isSSLCertOk(SslCertificate cert, String sha256Str) {
byte[] SSLSHA256 = hexToBytes(sha256Str);
Bundle bundle = SslCertificate.saveState(cert);
if (bundle != null) {
byte[] bytes = bundle.getByteArray("x509-certificate");
if (bytes != null) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate ca = cf.generateCertificate(new ByteArrayInputStream(bytes));
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] key = sha256.digest(((X509Certificate) ca).getEncoded());
return Arrays.equals(key, SSLSHA256);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return false;
2.onReceivedSslError回调中调用校验函数
webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslErroronReceivedSslError回调中实现校验逻辑(WebView view, SslErrorHandler handler, SslError error) {
if (error.getPrimaryError() == SslError.SSL_INVALID) {
// 如果手动校验sha256成功就允许加载页面
if (SSLCertUtil.isSSLCertOk(error.getCertificate(), "6683c9584b8287ec3a50e312f4a540c79938aaeb76bd02e40a9ca037ee5d24f4")) {
handler.proceed();//验证通过
} else {
//证书验证失败
}
} else {
handler.cancel();
}
}
});
DroidSSLUnpinning的绕过代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var WebViewClient = Java.use("android.webkit.WebViewClient");
WebViewClient.onReceivedSslError.implementation = function (webView,sslErrorHandler,sslError){
quiet_send("WebViewClient onReceivedSslError invoke");
//执行proceed方法
sslErrorHandler.proceed();
return ;
};
WebViewClient.onReceivedError.overload('android.webkit.WebView', 'int', 'java.lang.String', 'java.lang.String').implementation = function (a,b,c,d){
quiet_send("WebViewClient onReceivedError invoked");
return ;
};
WebViewClient.onReceivedError.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest', 'android.webkit.WebResourceError').implementation = function (){
quiet_send("WebViewClient onReceivedError invoked");
return ;
};
还有其他几种用法这里不再一一介绍,详情可以参考DroidSSLUnpinning的代码注释部分。
3. https双向证书校验
https双向校验是指APP除了校验服务端的证书,服务端还会校验APP的证书。对于双向校验。https双向证书校验在实际中几乎很少用到,因为服务器端需要维护所有客户端的证书,这无疑增加了很多消耗,因此大部分厂商选择使用单向证书绑定。
双向认证需要完成两个环节:
(1)让客户端认为burp是服务端
这一步其实就是破解Certificate Pinning,方法和上述过程完全相同。
(2)让服务端认为burp是客户端
这一步需要导入客户端的证书到burp,客户端的证书一定会存在本地代码中,而且还可能会有密码,这种情况下需要逆向客户端app,找到证书和密码,并转为pkcs12格式导入到burp。
User options -> SSL -> Client SSL Certificate
这样下来就可以双向抓包了。
使用ssl_logger
至此,我们可以绕过证书绑定,抓APP发出的https包了,然而上述的证书解绑hook工具仅仅是通过hook了几种绑定证书的API,不适用于新出现或者非主流的证书绑定技术。当折腾半天都搞不定证书绑定时,这时候就需要神器 —— ssl_logger。
ssl_logger(https://github.com/google/ssl_logger)是用来解密SSL流量的工具,它也是一款基于frida的hook工具,通过hook libssl库中的SSL_read、SSL_write等函数来实现流量解密。由于底层的实现会调用这几个函数来封装,所以可以直接解出流量数据。
全球知名黑客5alt大佬对ssl_logger进行了修改,使之在Android设备上的使用更加友好,参考 https://github.com/5alt/ssl_logger。
使用ssl_logger无需安装burp的中间人攻具的证书就可以抓https流量,但和burp相比,其重放、修改等功能会受限。
其他
特定的工具、特定的app等特殊情况的案例会不定期粘贴在下边:
1.How to bypass Instagram SSL Pinning on Android
2.apk-mitm 通过重打包使APP信任用户证书
参考资料:
- https://github.com/WooyunDota/DroidSSLUnpinning
- https://xz.aliyun.com/t/2336 Frida.Android.Practice (ssl unpinning)
- https://xz.aliyun.com/t/2098 安卓证书锁定解除的工具
- https://www.nevermoe.com/2018/11/20/openssl%E5%91%BD%E4%BB%A4%E6%95%B4%E7%90%86/ 安装Android自定义证书
- https://blog.freessl.cn/ssl-cert-format-introduce/ SSL证书格式普及PEM、CER、JKS、PKCS12
- https://blog.ropnop.com/configuring-burp-suite-with-android-nougat/ 在Android Nougat中正确配置Burp Suite
- https://blog.nviso.be/2017/12/22/intercepting-https-traffic-from-apps-on-android-7-using-magisk-burp/
- https://support.portswigger.net/customer/portal/questions/17281202-intercepting-android-version-8-1-https-traffic
- https://developer.android.com/training/articles/security-ssl#java
- https://xz.aliyun.com/t/2440 SSL Pinning Practice
- http://www.wisedream.net/2017/03/17/cryption/crack-mutual-auth/