1、前提
我们首先对基本抓包环境、HTTP协议、抓包原理以及没有检测情况下的抓包方法有大致的了解
1.1、图解浏览器访问HTTPS网页的密钥交换过程
服务端会下发证书,结构如下,同时带有RSA公钥
首先用RSA把随机产生的对称密钥进行加密,然后用对称加密的密钥去加密大量的数据,同时需要把加密后的对称加密密钥发送给服务端,由于服务端拥有RSA的私钥,就可以对加密后的对称加密密钥进行解密,再用对称加密密钥去解密发送过来的数据。
1.2、思考
1、如果图上的服务端,变成了代理抓包工具内置的代理服务端,能正常通信么?
代理抓包工具必须伪造一个假证书进行替换,这样子就可以发送一个自己的公钥,客户端拿抓包工具的公钥去加密对称密钥,而抓包工具可以用自己的私钥去解密。相当于把浏览器的对称密钥骗了出来。而如果不进行证书替换,使用百度的证书,抓包工具是没有百度的私钥的,这样子就无法进行解密。
2、浏览器能否识别出是网页真实服务端,还是抓包工具的代理服务端?
不能,因为网站太多了,每个网站对应的服务端都不一样,浏览器不可能知道所有的网站对应的服务端是什么证书。
3、那么浏览器检验证书的过程是怎样的?证书链又是什么?
其实就是校验证书链
baidu.com是GlobalSign RSA OV SSL CA 2018签发的,然后再校验GlobalSign RSA OV SSL CA 2018的签发者
是由GlobalSign 签发的,而GlobalSign 是由自己签发的,这一张根证书,根证书必须要在windows的系统证书库里才是受信任的。
4、APP端和服务端都是同一团队开发的,因此APP端可以识别真实服务端
2、常见的抓包检测
2.1、代理检测
通过System.getProperty获取host和port,可以知道是否进行了代理
防护
1、HttpsURLConnection设置不走代理
2、okhttp3设置不走代理
对抗
Hook设置代理、在模拟器中可以使用httpv7来抓包、使用VPN抓包
2.2、VPN检测
主要检测方法
java.net.NetworkInterface.getName
android.net.ConnectivityManager.getNetworkCapabilities
java.net.NetworkInterface.getName
防护
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
int count = 0;
while (networkInterfaces.hasMoreElements()) {
NetworkInterface next = networkInterfaces.nextElement();
logOutPut("getName获得网络设备名称=" + next.getName());
logOutPut("getDisplayName获得网络设备显示名称=" + next.getDisplayName());
logOutPut("getIndex获得网络接口的索引=" + next.getIndex());
logOutPut("isUp是否已经开启并运行=" + next.isUp());
logOutPut("isBoopback是否为回调接口=" + next.isLoopback());
logOutPut("**********************" + count++);
}
} catch (SocketException e) {
e.printStackTrace();
}
没开启VPN前
开启VPN后
多了一个tun0,这个就是开启了VPN
其实也就是ip addr
通过获取网关名是否为tun0来判断是否开启了VPN
对抗
function hook_vpn(){
Java.perform(function() {
var NetworkInterface = Java.use("java.net.NetworkInterface");
NetworkInterface.getName.implementation = function() {
var name = this.getName();
console.log("name: " + name);
if(name == "tun0" || name == "ppp0"){
return "rmnet_data0";
}else {
return name;
}
}
})
}
通过hook getName方法,判断如果得到的name是tun0或者ppp0,就返回rmnet_data0。不过对于VPN检测肯定不止getName方法,需要绕过的点应该会很多,可以尝试使用Objection把java.net下的类全hook,来查看app使用了哪些方法做检测。
android.net.ConnectivityManager.getNetworkCapabilities
防护
@RequiresApi(api = Build.VERSION_CODES.M)
public void networkCheck() {
try {
ConnectivityManager connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
Network network = connectivityManager.getActiveNetwork();
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
Log.i("TAG", "networkCapabilities -> " + networkCapabilities.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
查看logcat,分别为未开启VPN和开启VPN
2023-10-01 00:22:29.866 7467-7467 TAG com.xiaojianbang.app I networkCapabilities -> [ Transports: WIFI Capabilities: NOT_METERED&INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED&PARTIAL_CONNECTIVITY LinkUpBandwidth>=1048576Kbps LinkDnBandwidth>=1048576Kbps SignalStrength: -48]
2023-10-01 00:22:37.780 7467-7467 TAG com.xiaojianbang.app I networkCapabilities -> [ Transports: WIFI|VPN Capabilities: NOT_METERED&INTERNET&NOT_RESTRICTED&TRUSTED&VALIDATED&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED LinkUpBandwidth>=1048576Kbps LinkDnBandwidth>=1048576Kbps]
下面的开启VPN后会有VPN字段,而上面的会有NOT_VPN字段
对抗
对比上面的内容,可以发现在执行完
appendStringRepresentationOfBitMaskToStringBuilder(sb, mTransportTypes,NetworkCapabilities::transportNameOf, "|");
就已经产生vpn字符了,所以我们需要进入到transportNameOf,NetworkCapabilities::表示调用构造函数
TRANSPORT_NAMES就是包含WIFI和VPN字段的数组
所以我们在这里直接hook 返回WIFI字符串就行了
通常还有种检测
Log.i("TAG", "networkCapabilities -> " + networkCapabilities.hasTransport(4));
首先对传入的值进行判断是否合法,也就是参数在0-7之间
我们可以看到 public static final int TRANSPORT_VPN = 4;,也就说明传入4就是检测是否开启VPN,所以我们需要过掉这两个检测
对于hasTransport直接返回true就行了
Java.perform( function () {
var NetworkCapabilities = Java.use("android.net.NetworkCapabilities");
NetworkCapabilities.hasTransport.implementation = function (a) {
console.log("argc:"+a);
return false;
}
})
已经返回false了,我们继续解决上面一个检测
VPN字段是在appendStringRepresentationOfBitMaskToStringBuilder产生的
NetworkCapabilities.appendStringRepresentationOfBitMaskToStringBuilder.implementation = function (sb, bitMask, nameFetcher, separator) {
console.log("StringBuilder sb:"+sb+"\n");
console.log("long bitMask:"+bitMask+"\n");
console.log("NameOf nameFetcher:"+nameFetcher+"\n");
console.log("String separator:"+separator+"\n");
if (separator=='|') {
sb.append("WIFI");
}else {
this.appendStringRepresentationOfBitMaskToStringBuilder(sb, bitMask, nameFetcher, separator);
}
}
也就是在当分隔符是|时,我们自动加入了WIFI字段
2.3、单向验证与双向验证
单向验证
客户端校验服务端的证书或者服务器校验客户端证书
通常利用系统相关函数来校验证书,这时可以通过Hook相关系统函数来绕过
双向验证
客户端与服务端一起校验对方
2.4、Hook抓包
不需要理会各种抓包检测,抓到的可能没有抓包工具全,会被Hook检测,检测hook框架
3、常用的网络框架-HttpsURLConnection
3.1、HttpsURLConnection的GET和POST请求
package com.xiaojianbang.test;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
public class HttpsUtils {
public static void doRequest(){
new Thread(){
public void run(){
String result = HttpsRequest("POST", "https://www.baidu.com/", "user");
Log.d("xiaojianbang","" + result);
}
}.start();
}
private static SSLContext getSSLContext() {
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,null, null);
} catch (Exception e) {
e.printStackTrace();
}
return sslContext;
}
public static String HttpsRequest(String method, String url, String outputStr) {
try {
SSLContext sslContext = getSSLContext();
if (sslContext != null) {
URL u = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) u.openConnection(Proxy.NO_PROXY);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setConnectTimeout(30000);
if(method.equals("POST")){
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
}
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
conn.connect();
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
conn.disconnect();
return buffer.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
3.2、HttpsURLConnection的自吐
objection -g com.xiaojianbang.app explore
先开启objection 的hook
android hooking search classes HttpsURLConnection
android hooking search classes URLConnection
android hooking watch class com.android.okhttp.internal.huc.HttpURLConnectionImpl
点击button后
确实调用了getOutputStream
我们直接用 android hooking watch class java.net.URLConnection,虽然也有java.net.URLConnection.getOutputStream(),而且也符合Android Studio里面的类路径
但是我们点击button后并不会调用getOutputStream
3.3、HttpsURLConnection的证书检测
setSSLSocketFactory
主要用到了下面两个方法
com.android.okhttp.internal.huc.HttpsURLConnectionImpl.setSSLSocketFactory
com.android.okhttp.internal.huc.HttpsURLConnectionImpl.setHostnameVerifier
conn.setSSLSocketFactory(sslContext.getSocketFactory());
这个就是设置证书检测
getSocketFactory就是获取sslContext,而我们的证书检测是加在init中,第二个参数设置为TrustManager
trustManager需要我们自己定义,里面加入检测的代码
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
导出百度官方的证书
-----BEGIN CERTIFICATE-----
MIIJ6DCCCNCgAwIBAgIMVeasrtH4pDD5qTjFMA0GCSqGSIb3DQEBCwUAMFAxCzAJ
BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSYwJAYDVQQDEx1H
bG9iYWxTaWduIFJTQSBPViBTU0wgQ0EgMjAxODAeFw0yMzA3MDYwMTUxMDZaFw0y
NDA4MDYwMTUxMDVaMIGAMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHYmVpamluZzEQ
MA4GA1UEBxMHYmVpamluZzE5MDcGA1UEChMwQmVpamluZyBCYWlkdSBOZXRjb20g
U2NpZW5jZSBUZWNobm9sb2d5IENvLiwgTHRkMRIwEAYDVQQDEwliYWlkdS5jb20w
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7BLuEdlgHtFqIVOBqVrzl
1I0+Hrko4NcBjzgrQbJZffCsJ7QmJBQ4/kzqO0lR9+lbQPc/psjaDwJuJYtHkbgu
ngAhGR0YAPzeBP0meTld8pC8gJ2ofLKRiYnYQC/l0qfzXm1IK8UfCrHgjox2/7zR
ZwrSSdYJ7iYDAvPMzeqK1TGoLY8D/V785DrGiWeZTM6YbfqEDQ5Ti+ZjUsWbSqmr
oyI1mQ3uGf+bLfWkd/LsEID0q4K50X42Hw6fmxmg9cNX3Yi7zuGQnD9Lut06qUGz
3YZNwsK36P83E8AEiUNEOBHmo5b3CSIhLyxODn7l2Fy7AERbr97ks7DwPLY4RUld
AgMBAAGjggaPMIIGizAOBgNVHQ8BAf8EBAMCBaAwgY4GCCsGAQUFBwEBBIGBMH8w
RAYIKwYBBQUHMAKGOGh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0
L2dzcnNhb3Zzc2xjYTIwMTguY3J0MDcGCCsGAQUFBzABhitodHRwOi8vb2NzcC5n
bG9iYWxzaWduLmNvbS9nc3JzYW92c3NsY2EyMDE4MFYGA1UdIARPME0wQQYJKwYB
BAGgMgEUMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29t
L3JlcG9zaXRvcnkvMAgGBmeBDAECAjAJBgNVHRMEAjAAMD8GA1UdHwQ4MDYwNKAy
oDCGLmh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vZ3Nyc2FvdnNzbGNhMjAxOC5j
cmwwggNhBgNVHREEggNYMIIDVIIJYmFpZHUuY29tggxiYWlmdWJhby5jb22CDHd3
dy5iYWlkdS5jboIQd3d3LmJhaWR1LmNvbS5jboIPbWN0LnkubnVvbWkuY29tggth
cG9sbG8uYXV0b4IGZHd6LmNuggsqLmJhaWR1LmNvbYIOKi5iYWlmdWJhby5jb22C
ESouYmFpZHVzdGF0aWMuY29tgg4qLmJkc3RhdGljLmNvbYILKi5iZGltZy5jb22C
DCouaGFvMTIzLmNvbYILKi5udW9taS5jb22CDSouY2h1YW5rZS5jb22CDSoudHJ1
c3Rnby5jb22CDyouYmNlLmJhaWR1LmNvbYIQKi5leXVuLmJhaWR1LmNvbYIPKi5t
YXAuYmFpZHUuY29tgg8qLm1iZC5iYWlkdS5jb22CESouZmFueWkuYmFpZHUuY29t
gg4qLmJhaWR1YmNlLmNvbYIMKi5taXBjZG4uY29tghAqLm5ld3MuYmFpZHUuY29t
gg4qLmJhaWR1cGNzLmNvbYIMKi5haXBhZ2UuY29tggsqLmFpcGFnZS5jboINKi5i
Y2Vob3N0LmNvbYIQKi5zYWZlLmJhaWR1LmNvbYIOKi5pbS5iYWlkdS5jb22CEiou
YmFpZHVjb250ZW50LmNvbYILKi5kbG5lbC5jb22CCyouZGxuZWwub3JnghIqLmR1
ZXJvcy5iYWlkdS5jb22CDiouc3UuYmFpZHUuY29tgggqLjkxLmNvbYISKi5oYW8x
MjMuYmFpZHUuY29tgg0qLmFwb2xsby5hdXRvghIqLnh1ZXNodS5iYWlkdS5jb22C
ESouYmouYmFpZHViY2UuY29tghEqLmd6LmJhaWR1YmNlLmNvbYIOKi5zbWFydGFw
cHMuY26CDSouYmR0anJjdi5jb22CDCouaGFvMjIyLmNvbYIMKi5oYW9rYW4uY29t
gg8qLnBhZS5iYWlkdS5jb22CESoudmQuYmRzdGF0aWMuY29tghEqLmNsb3VkLmJh
aWR1LmNvbYISY2xpY2suaG0uYmFpZHUuY29tghBsb2cuaG0uYmFpZHUuY29tghBj
bS5wb3MuYmFpZHUuY29tghB3bi5wb3MuYmFpZHUuY29tghR1cGRhdGUucGFuLmJh
aWR1LmNvbTAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgw
FoAU+O9/8s14Z6jeb48kjYjxhwMCs+swHQYDVR0OBBYEFO1zq/kgvnoZn1kfsp/y
Py8/kYQSMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgBIsONr2qZHNA/lagL6
nTDrHFIBy1bdLIHZu7+rOdiEcwAAAYko5XABAAAEAwBHMEUCIQDtGvRfSswr/1ff
5bjL+SRct34Ue6PaRsDYvGhpiYejgwIgX/aCg9Og5EZbVLo+ZsrU9s3IJusYzZYj
ASJszEzwZ1oAdwDuzdBk1dsazsVct520zROiModGfLzs3sNRSFlGcR+1mwAAAYko
5XAdAAAEAwBIMEYCIQC9HcMYKn54HivSbhH0wuWtwTaHYtuIvJD8IhPF+zJ9/gIh
AICMnoiGocc6FGIMIYmMd7p7JJSXMZCpFXSibCwzg1ItAHUA2ra/az+1tiKfm8K7
XGvocJFxbLtRhIU0vaQ9MEjX+6sAAAGJKOVtVwAABAMARjBEAiBUbWpp6uCjWPkX
1a3kdzajezONw5Uwdn7l+xypjE6bdwIgG2GK8pH+5UqZTTKxNyqCRoiJDX7rAXzx
O22aIRkkBcAwDQYJKoZIhvcNAQELBQADggEBABlaZ1BDsax6k6hoGHKLQH6mdd6s
IfzJQRYgS/OMC7lHRa74XXn2QzUmAZjwuYY+KQHx37Byta540t9htnhnisl3mt7g
5EEvnB7lO3yXP0IvreNJf50rAoiQaSUDARS5tcsPWT0tlz0C1VGQaQyBECLaxlHv
SAzST95h8mqHFaVtcY43AqKFDx4ZdaOALmoaogKML+y9PYEDP4rAoOa0DghXywAc
ircbjzhxmo3AcQw/vNS+Vp33GMGqvuTfGobiYm8jhjBUeC1HH7StBSlzJJgUoBnA
Av2QkE5iXOhNMYnD6Iuec1k7mJHKR6UFW8Uej4U5Ds61JgqATp8IShFJE2M=
-----END CERTIFICATE-----
校验代码
public static String certificate = "-----BEGIN CERTIFICATE-----\n" +
"MIIJ6DCCCNCgAwIBAgIMVeasrtH4pDD5qTjFMA0GCSqGSIb3DQEBCwUAMFAxCzAJ\n" +
"BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSYwJAYDVQQDEx1H\n" +
"bG9iYWxTaWduIFJTQSBPViBTU0wgQ0EgMjAxODAeFw0yMzA3MDYwMTUxMDZaFw0y\n" +
"NDA4MDYwMTUxMDVaMIGAMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHYmVpamluZzEQ\n" +
"MA4GA1UEBxMHYmVpamluZzE5MDcGA1UEChMwQmVpamluZyBCYWlkdSBOZXRjb20g\n" +
"U2NpZW5jZSBUZWNobm9sb2d5IENvLiwgTHRkMRIwEAYDVQQDEwliYWlkdS5jb20w\n" +
"ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7BLuEdlgHtFqIVOBqVrzl\n" +
"1I0+Hrko4NcBjzgrQbJZffCsJ7QmJBQ4/kzqO0lR9+lbQPc/psjaDwJuJYtHkbgu\n" +
"ngAhGR0YAPzeBP0meTld8pC8gJ2ofLKRiYnYQC/l0qfzXm1IK8UfCrHgjox2/7zR\n" +
"ZwrSSdYJ7iYDAvPMzeqK1TGoLY8D/V785DrGiWeZTM6YbfqEDQ5Ti+ZjUsWbSqmr\n" +
"oyI1mQ3uGf+bLfWkd/LsEID0q4K50X42Hw6fmxmg9cNX3Yi7zuGQnD9Lut06qUGz\n" +
"3YZNwsK36P83E8AEiUNEOBHmo5b3CSIhLyxODn7l2Fy7AERbr97ks7DwPLY4RUld\n" +
"AgMBAAGjggaPMIIGizAOBgNVHQ8BAf8EBAMCBaAwgY4GCCsGAQUFBwEBBIGBMH8w\n" +
"RAYIKwYBBQUHMAKGOGh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0\n" +
"L2dzcnNhb3Zzc2xjYTIwMTguY3J0MDcGCCsGAQUFBzABhitodHRwOi8vb2NzcC5n\n" +
"bG9iYWxzaWduLmNvbS9nc3JzYW92c3NsY2EyMDE4MFYGA1UdIARPME0wQQYJKwYB\n" +
"BAGgMgEUMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29t\n" +
"L3JlcG9zaXRvcnkvMAgGBmeBDAECAjAJBgNVHRMEAjAAMD8GA1UdHwQ4MDYwNKAy\n" +
"oDCGLmh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vZ3Nyc2FvdnNzbGNhMjAxOC5j\n" +
"cmwwggNhBgNVHREEggNYMIIDVIIJYmFpZHUuY29tggxiYWlmdWJhby5jb22CDHd3\n" +
"dy5iYWlkdS5jboIQd3d3LmJhaWR1LmNvbS5jboIPbWN0LnkubnVvbWkuY29tggth\n" +
"cG9sbG8uYXV0b4IGZHd6LmNuggsqLmJhaWR1LmNvbYIOKi5iYWlmdWJhby5jb22C\n" +
"ESouYmFpZHVzdGF0aWMuY29tgg4qLmJkc3RhdGljLmNvbYILKi5iZGltZy5jb22C\n" +
"DCouaGFvMTIzLmNvbYILKi5udW9taS5jb22CDSouY2h1YW5rZS5jb22CDSoudHJ1\n" +
"c3Rnby5jb22CDyouYmNlLmJhaWR1LmNvbYIQKi5leXVuLmJhaWR1LmNvbYIPKi5t\n" +
"YXAuYmFpZHUuY29tgg8qLm1iZC5iYWlkdS5jb22CESouZmFueWkuYmFpZHUuY29t\n" +
"gg4qLmJhaWR1YmNlLmNvbYIMKi5taXBjZG4uY29tghAqLm5ld3MuYmFpZHUuY29t\n" +
"gg4qLmJhaWR1cGNzLmNvbYIMKi5haXBhZ2UuY29tggsqLmFpcGFnZS5jboINKi5i\n" +
"Y2Vob3N0LmNvbYIQKi5zYWZlLmJhaWR1LmNvbYIOKi5pbS5iYWlkdS5jb22CEiou\n" +
"YmFpZHVjb250ZW50LmNvbYILKi5kbG5lbC5jb22CCyouZGxuZWwub3JnghIqLmR1\n" +
"ZXJvcy5iYWlkdS5jb22CDiouc3UuYmFpZHUuY29tgggqLjkxLmNvbYISKi5oYW8x\n" +
"MjMuYmFpZHUuY29tgg0qLmFwb2xsby5hdXRvghIqLnh1ZXNodS5iYWlkdS5jb22C\n" +
"ESouYmouYmFpZHViY2UuY29tghEqLmd6LmJhaWR1YmNlLmNvbYIOKi5zbWFydGFw\n" +
"cHMuY26CDSouYmR0anJjdi5jb22CDCouaGFvMjIyLmNvbYIMKi5oYW9rYW4uY29t\n" +
"gg8qLnBhZS5iYWlkdS5jb22CESoudmQuYmRzdGF0aWMuY29tghEqLmNsb3VkLmJh\n" +
"aWR1LmNvbYISY2xpY2suaG0uYmFpZHUuY29tghBsb2cuaG0uYmFpZHUuY29tghBj\n" +
"bS5wb3MuYmFpZHUuY29tghB3bi5wb3MuYmFpZHUuY29tghR1cGRhdGUucGFuLmJh\n" +
"aWR1LmNvbTAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgw\n" +
"FoAU+O9/8s14Z6jeb48kjYjxhwMCs+swHQYDVR0OBBYEFO1zq/kgvnoZn1kfsp/y\n" +
"Py8/kYQSMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgBIsONr2qZHNA/lagL6\n" +
"nTDrHFIBy1bdLIHZu7+rOdiEcwAAAYko5XABAAAEAwBHMEUCIQDtGvRfSswr/1ff\n" +
"5bjL+SRct34Ue6PaRsDYvGhpiYejgwIgX/aCg9Og5EZbVLo+ZsrU9s3IJusYzZYj\n" +
"ASJszEzwZ1oAdwDuzdBk1dsazsVct520zROiModGfLzs3sNRSFlGcR+1mwAAAYko\n" +
"5XAdAAAEAwBIMEYCIQC9HcMYKn54HivSbhH0wuWtwTaHYtuIvJD8IhPF+zJ9/gIh\n" +
"AICMnoiGocc6FGIMIYmMd7p7JJSXMZCpFXSibCwzg1ItAHUA2ra/az+1tiKfm8K7\n" +
"XGvocJFxbLtRhIU0vaQ9MEjX+6sAAAGJKOVtVwAABAMARjBEAiBUbWpp6uCjWPkX\n" +
"1a3kdzajezONw5Uwdn7l+xypjE6bdwIgG2GK8pH+5UqZTTKxNyqCRoiJDX7rAXzx\n" +
"O22aIRkkBcAwDQYJKoZIhvcNAQELBQADggEBABlaZ1BDsax6k6hoGHKLQH6mdd6s\n" +
"IfzJQRYgS/OMC7lHRa74XXn2QzUmAZjwuYY+KQHx37Byta540t9htnhnisl3mt7g\n" +
"5EEvnB7lO3yXP0IvreNJf50rAoiQaSUDARS5tcsPWT0tlz0C1VGQaQyBECLaxlHv\n" +
"SAzST95h8mqHFaVtcY43AqKFDx4ZdaOALmoaogKML+y9PYEDP4rAoOa0DghXywAc\n" +
"ircbjzhxmo3AcQw/vNS+Vp33GMGqvuTfGobiYm8jhjBUeC1HH7StBSlzJJgUoBnA\n" +
"Av2QkE5iXOhNMYnD6Iuec1k7mJHKR6UFW8Uej4U5Ds61JgqATp8IShFJE2M=\n" +
"-----END CERTIFICATE-----\n";
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
//判断证书链是否为空
if (x509Certificates == null){
throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null");
}
//判断证书链长度是否不为零
if (!(x509Certificates.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
//判断加密模式是否是RSA
if (!(!TextUtils.isEmpty(s) && s.toUpperCase().contains("RSA"))) {
throw new CertificateException("checkServerTrusted: AuthType is not RSA");
}
Log.d("whitebird","authType: " + s);
//获取第一张证书
X509Certificate cf = x509Certificates[0];
//获取证书公钥
RSAPublicKey pubkey = (RSAPublicKey)cf.getPublicKey();
//进行base64编码
String encoded = Base64.encodeToString(pubkey.getEncoded(),0);
//生成一张证书,里面的公钥是我们从百度官方下载的证书中导出的,也就是上面的certificate
CertificateFactory finalcf = CertificateFactory.getInstance("X.509");
X509Certificate realCertificate = (X509Certificate)finalcf.generateCertificate(new ByteArrayInputStream(certificate.getBytes()));
//对生成的证书的公钥进行base64编码
String realPubKey = Base64.encodeToString(realCertificate.getPublicKey().getEncoded(),0);
cf.checkValidity();
Log.d("whitebird", "IssuerDN: " + cf.getIssuerDN().toString());
Log.d("whitebird", "SubjectDN: " + cf.getSubjectDN().toString());
Log.d("whitebird", "证书版本: "+ cf.getVersion());
//拿我们提前获取的百度导出的证书公约与获取的证书公钥进行比对
final boolean expected = realPubKey.equalsIgnoreCase(encoded);
if (!expected) {
throw new CertificateException("checkServerTrusted: got error public key: " + encoded);
}
Log.d("whitebird","证书公钥验证正确");
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
看看检验的效果
如果我们开启代理抓包,再看看效果
发生了报错,抛出了checkServerTrusted: got error public key的异常
可以看到获取的证书信息也不是百度的
HttpCanary作为根路径签发了一张www.baidu.com的伪证书
setHostnameVerifier
public static HostnameVerifier VERIFY = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return false;
}
};
conn.setHostnameVerifier(VERIFY);
如果返回false就表明域名检测失败
通过log打印获取了访问的域名,但是由于我们返回false,所以就有报错
改成true正常访问
setHostnameVerifier不仅可以检测访问的域名,还可以检测证书,和上面一样,只不过通过sslSession获取证书链
public static HostnameVerifier VERIFY = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
Log.d("whitebird","HostnameVerifier s:"+s);
try {
X509Certificate[] x509Certificates = (X509Certificate[])sslSession.getPeerCertificates();
//判断证书链是否为空
if (x509Certificates == null){
throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null");
}
//判断证书链长度是否不为零
if (!(x509Certificates.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
//获取第一张证书
X509Certificate cf = x509Certificates[0];
//获取证书公钥
RSAPublicKey pubkey = (RSAPublicKey)cf.getPublicKey();
//进行base64编码
String encoded = Base64.encodeToString(pubkey.getEncoded(),0);
//生成一张证书,里面的公钥是我们从百度官方下载的证书中导出的
CertificateFactory finalcf = CertificateFactory.getInstance("X.509");
X509Certificate realCertificate = (X509Certificate)finalcf.generateCertificate(new ByteArrayInputStream(certificate.getBytes()));
//对生成的证书的公钥进行base64编码
String realPubKey = Base64.encodeToString(realCertificate.getPublicKey().getEncoded(),0);
cf.checkValidity();
Log.d("whitebird", "HostnameVerifier IssuerDN: " + cf.getIssuerDN().toString());
Log.d("whitebird", "HostnameVerifier SubjectDN: " + cf.getSubjectDN().toString());
Log.d("whitebird", "HostnameVerifier 证书版本: "+ cf.getVersion());
//拿我们提前获取的百度导出的证书公约与获取的证书公钥进行比对
final boolean expected = realPubKey.equalsIgnoreCase(encoded);
if (!expected) {
throw new CertificateException("checkServerTrusted: got error public key: " + encoded);
}
Log.d("whitebird","HostnameVerifier 证书公钥验证正确");
} catch (Exception e) {
throw new RuntimeException(e);
}
return true;
}
};
可以实现检测
3.4、HttpsURLConnection的绕过证书检测
其实绕过证书校验很简单,因为setSSLSocketFactory和setHostnameVerifier都是设置检验代码的,我们直接给这两个函数hook掉就行了。
注意setSSLSocketFactory和setHostnameVerifier是属于com.android.okhttp.internal.huc.HttpsURLConnectionImpl类下面的
Java.perform( function () {
var httpUrlConnection = Java.use("com.android.okhttp.internal.huc.HttpsURLConnectionImpl");
httpUrlConnection.setSSLSocketFactory.implementation= function (a) {
console.log("setSSLSocketFactory invoke");
return ;
}
httpUrlConnection.setHostnameVerifier.implementation= function (a) {
console.log("setHostnameVerifier invoke");
return;
}
})
可以正常访问
4、常用的网络框架-okhttp3
4.1、okhttp3的GET和POST请求
build.gradle的dependencies中需要引入以下代码api ‘com.squareup.okhttp3:okhttp:3.14.2’
package com.xiaojianbang.test;
import android.util.Log;
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class ohHttp3Utils {
public static OkHttpClient client = new OkHttpClient.Builder().build();
public static void doRequest(){
new Thread(){
public void run() {
Request request = new Request.Builder()
.url("https://www.baidu.com")
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36"
).build();
try {
Response response =client.newCall(request).execute();
Log.d("whitebird", "response: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
如果需要提交post数据,需要用到formBody和.post
package com.xiaojianbang.test;
import android.util.Log;
import java.io.IOException;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class ohHttp3Utils {
public static OkHttpClient client = new OkHttpClient.Builder().build();
public static void doRequest(){
new Thread(){
public void run() {
FormBody formBody = new FormBody.Builder().add("user", "whitebird").add("password", "123456").build();
Request request = new Request.Builder()
.url("https://www.baidu.com")
.post(formBody)
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36"
).build();
try {
Response response =client.newCall(request).execute();
Log.d("whitebird", "response: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
由于提交的数据有误,所以返回错误的页面,但是说明了post是有效果的
4.2、okhttp3的拦截器
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
Log.d("whitebird", String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
Log.d("whitebird", String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
通过实现Interceptor接口,添加拦截器的代码如下
public static OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new LoggingInterceptor()).build();
通过addInterceptor添加拦截器
可以成功打印出一些信息,其实就类似自己写代码实现hook功能
下面的拦截器打印的日志更详细
package com.xiaojianbang.test;
import android.util.Log;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import okhttp3.Connection;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpHeaders;
import okio.Buffer;
import okio.BufferedSource;
import okio.GzipSource;
public final class okhttp3Logging implements Interceptor {
private static final String TAG = "okhttpGET";
private static final Charset UTF8 = Charset.forName("UTF-8");
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
String requestStartMessage = "--> "
+ request.method()
+ ' ' + request.url();
Log.e(TAG, requestStartMessage);
if (hasRequestBody) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody.contentType() != null) {
Log.e(TAG, "Content-Type: " + requestBody.contentType());
}
if (requestBody.contentLength() != -1) {
Log.e(TAG, "Content-Length: " + requestBody.contentLength());
}
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
Log.e(TAG, name + ": " + headers.value(i));
}
}
if (!hasRequestBody) {
Log.e(TAG, "--> END " + request.method());
} else if (bodyHasUnknownEncoding(request.headers())) {
Log.e(TAG, "--> END " + request.method() + " (encoded body omitted)");
} else {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
Log.e(TAG, "");
if (isPlaintext(buffer)) {
Log.e(TAG, buffer.readString(charset));
Log.e(TAG, "--> END " + request.method()
+ " (" + requestBody.contentLength() + "-byte body)");
} else {
Log.e(TAG, "--> END " + request.method() + " (binary "
+ requestBody.contentLength() + "-byte body omitted)");
}
}
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
Log.e(TAG, "<-- HTTP FAILED: " + e);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
Log.e(TAG, "<-- "
+ response.code()
+ (response.message().isEmpty() ? "" : ' ' + response.message())
+ ' ' + response.request().url()
+ " (" + tookMs + "ms" + (", " + bodySize + " body:" + "") + ')');
Headers myheaders = response.headers();
for (int i = 0, count = myheaders.size(); i < count; i++) {
Log.e(TAG, myheaders.name(i) + ": " + myheaders.value(i));
}
if (!HttpHeaders.hasBody(response)) {
Log.e(TAG, "<-- END HTTP");
} else if (bodyHasUnknownEncoding(response.headers())) {
Log.e(TAG, "<-- END HTTP (encoded body omitted)");
} else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Long gzippedLength = null;
if ("gzip".equalsIgnoreCase(myheaders.get("Content-Encoding"))) {
gzippedLength = buffer.size();
GzipSource gzippedResponseBody = null;
try {
gzippedResponseBody = new GzipSource(buffer.clone());
buffer = new Buffer();
buffer.writeAll(gzippedResponseBody);
} finally {
if (gzippedResponseBody != null) {
gzippedResponseBody.close();
}
}
}
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
Log.e(TAG, "");
Log.e(TAG, "<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
return response;
}
if (contentLength != 0) {
Log.e(TAG, "");
Log.e(TAG, buffer.clone().readString(charset));
}
if (gzippedLength != null) {
Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte, "
+ gzippedLength + "-gzipped-byte body)");
} else {
Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte body)");
}
}
return response;
}
/**
* Returns true if the body in question probably contains human readable text. Uses a small sample
* of code points to detect unicode control characters commonly used in binary file signatures.
*/
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private boolean bodyHasUnknownEncoding(Headers myheaders) {
String contentEncoding = myheaders.get("Content-Encoding");
return contentEncoding != null
&& !contentEncoding.equalsIgnoreCase("identity")
&& !contentEncoding.equalsIgnoreCase("gzip");
}
}
4.3、okhttp3的自吐及快速定位
public static OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new okhttp3Logging()).build();
我们可以通过hook Builder()的addInterceptor去添加一个我们自己实现的拦截器。
拦截器之前我们是直接写在demo里的,现在假如我们直接拿到了一个打包好的app,想向里面加入自定义的拦截器,可以先将拦截器编译成dex文件,然后在frida脚本中加载进来
创建一个demo,导入okhttp3Logging代码,注意要在build.gradle中导入okhttp3
api 'com.squareup.okhttp3:okhttp:3.14.2'
classes3.dex中有我们的关键代码
现在需要去写一个frida的hook脚本
Java.perform(function () {
//加载dex,这个需要手动推送到手机中,并给上可执行权限
Java.openClassFile("/data/local/tmp/interceptor.dex").load();
//找到自定义的拦截器类
var okhttp3Logging = Java.use("com.example.interceptor.okhttp3Logging");
var Builder = Java.use("okhttp3.OkHttpClient$Builder");
Builder.build.implementation=function () {
console.log("okhttp3.OkHttpClient$Builder is called!");
return this.addInterceptor(okhttp3Logging.$new()).build();
}
})
先加载了带有自定义拦截器类的dex,然后找到dex中的拦截器类com.example.interceptor.okhttp3Logging,我们的hook点是Builder的build时机,对build之前的对象进行addInterceptor,然后返回时build
源码中我取消了addInterceptor,但是logcat中可以看到hook代码已经生效了。
4.4、okhttp3的证书检测
sslSocketFactory和hostnameVerifier
和HttpsURLConnection的操作其实差不多,只不过这里的sslSocketFactory需要传入两个参数sslSocketFactory和trustManager,我们把剩下的代码直接拷贝过来
okhttp3的certificatePinner检测
public static OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(getSSLContext().getSocketFactory(),trustManager)
.hostnameVerifier(VERIFY)
.certificatePinner(new CertificatePinner.Builder().add("www.baidu.com","sha256//sssssssssssssssssssss").build())
.build();
和之前一样,我们直接在OkHttpClient的地方使用.certificatePinner,参数是一个CertificatePinner对象,add里放要访问的网址和对应的证书公钥的sha256结果,用来做比对
出现报错,网上搜索了解如果trustmanager不接受自签名证书,则不能使用certificatepiner来固定该证书
public static SSLSocketFactory getSSLSocketFactory(InputStream in) {
SSLSocketFactory sSLSocketFactory = null;
try {
trustManager = trustManagerForCertificates(in);
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[]{trustManager}, new SecureRandom());
sSLSocketFactory = sc.getSocketFactory();
} catch (Exception e) {
}
return sSLSocketFactory;
}
private static X509TrustManager trustManagerForCertificates(InputStream in) throws GeneralSecurityException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
// Put the certificates a key store.
char[] password = "password".toCharArray(); // Any password will work.
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}
// Use it to build an X509 trust manager.
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
}
private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, password);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}
这段代码的功能:读取特定证书文件,将其作为信任的证书,并基于这些证书配置一个自定义的 SSLSocketFactory
,用于建立安全的 SSL 连接
现在我们重新访问
发现还是有报错,说我们的sha256错误,校验失败,并把正确的sha256给了我们
我们替换了sha256,重新访问看看
访问成功
我们自己加密一次试试
因为有不可见字符,我们直接复制过来
是一样的
我们看看certificatepinner是怎么校验的
先取出对应网站的pin,这个pin是我们自己传入的,然后获取证书,判断我们的pin开头是sha256或者sha1,然后对证书的公钥进行sha256或者sha1加密,将加密完的结果进行比较。
可以看到加密的是证书的公钥
现在我们尝试用sha1加密校验,先随便输入一个假的密文
并没有帮我们给出sha1的正确密文,我们自己加密一次
我们替换完试试,可以正常访问
4.5、okhttp3证书检测的绕过
其实和HttpURLConnection一样,我们需要hook sslSocketFactory、hostnameVerifier和certificatePinner
Java.perform(function() {
/*
hook list:
1.SSLcontext
2.okhttp
3.webview
4.XUtils
5.httpclientandroidlib
6.JSSE
7.network\_security\_config (android 7.0+)
8.Apache Http client (support partly)
9.OpenSSLSocketImpl
10.TrustKit
11.Cronet
*/
// Attempts to bypass SSL pinning implementations in a number of
// ways. These include implementing a new TrustManager that will
// accept any SSL certificate, overriding OkHTTP v3 check()
// method etc.
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var HostnameVerifier = Java.use('javax.net.ssl.HostnameVerifier');
var SSLContext = Java.use('javax.net.ssl.SSLContext');
var quiet_output = false;
// Helper method to honor the quiet flag.
function quiet_send(data) {
if (quiet_output) {
return;
}
send(data)
}
// Implement a new TrustManager
// ref: https://gist.github.com/oleavr/3ca67a173ff7d207c6b8c3b0ca65a9d8
// Java.registerClass() is only supported on ART for now(201803). 所以android 4.4以下不兼容,4.4要切换成ART使用.
/*
06-07 16:15:38.541 27021-27073/mi.sslpinningdemo W/System.err: java.lang.IllegalArgumentException: Required method checkServerTrusted(X509Certificate[], String, String, String) missing
06-07 16:15:38.542 27021-27073/mi.sslpinningdemo W/System.err: at android.net.http.X509TrustManagerExtensions.<init>(X509TrustManagerExtensions.java:73)
at mi.ssl.MiPinningTrustManger.<init>(MiPinningTrustManger.java:61)
06-07 16:15:38.543 27021-27073/mi.sslpinningdemo W/System.err: at mi.sslpinningdemo.OkHttpUtil.getSecPinningClient(OkHttpUtil.java:112)
at mi.sslpinningdemo.OkHttpUtil.get(OkHttpUtil.java:62)
at mi.sslpinningdemo.MainActivity$1$1.run(MainActivity.java:36)
*/
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);
}
send('Custom, Empty TrustManager ready');
// Get a handle on the init() on the SSLContext class
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);
};
/*** okhttp3.x unpinning ***/
// Wrap the logic in a try/catch as not all applications will have
// okhttp as part of the app.
try {
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.');
}
var OkHttpClient$Builder = Java.use('okhttp3.OkHttpClient$Builder');
quiet_send('OkHttpClient$Builder Found');
console.log("hostnameVerifier", OkHttpClient$Builder.hostnameVerifier);
OkHttpClient$Builder.hostnameVerifier.implementation = function () {
quiet_send('OkHttpClient$Builder hostnameVerifier() called. Not throwing an exception.');
return this;
}
var myHostnameVerifier = Java.registerClass({
name: 'com.whitebird.MyHostnameVerifier',
implements: [HostnameVerifier],
methods: {
verify: function (hostname, session) {
return true;
}
}
});
var OkHttpClient = Java.use('okhttp3.OkHttpClient');
OkHttpClient.hostnameVerifier.implementation = function () {
quiet_send('OkHttpClient hostnameVerifier() called. Not throwing an exception.');
return myHostnameVerifier.$new();
}
} catch (err) {
// If we dont have a ClassNotFoundException exception, raise the
// problem encountered.
if (err.message.indexOf('ClassNotFoundException') === 0) {
throw new Error(err);
}
}
// Appcelerator Titanium PinningTrustManager
// Wrap the logic in a try/catch as not all applications will have
// appcelerator as part of the app.
try {
var PinningTrustManager = Java.use('appcelerator.https.PinningTrustManager');
send('Appcelerator Titanium Found');
PinningTrustManager.checkServerTrusted.implementation = function() {
quiet_send('Appcelerator checkServerTrusted() called. Not throwing an exception.');
}
} catch (err) {
// If we dont have a ClassNotFoundException exception, raise the
// problem encountered.
if (err.message.indexOf('ClassNotFoundException') === 0) {
throw new Error(err);
}
}
/*** okhttp unpinning ***/
try {
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;
};
} catch (e) {
quiet_send("com.squareup.okhttp not found");
}
/*** WebView Hooks ***/
/* frameworks/base/core/java/android/webkit/WebViewClient.java */
/* public void onReceivedSslError(Webview, SslErrorHandler, SslError) */
var 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;
};
/*** JSSE Hooks ***/
/* libcore/luni/src/main/java/javax/net/ssl/TrustManagerFactory.java */
/* public final TrustManager[] getTrustManager() */
/* TrustManagerFactory.getTrustManagers maybe cause X509TrustManagerExtensions error */
// var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
// TrustManagerFactory.getTrustManagers.implementation = function(){
// quiet_send("TrustManagerFactory getTrustManagers invoked");
// return TrustManagers;
// }
var HttpsURLConnection = Java.use("com.android.okhttp.internal.huc.HttpsURLConnectionImpl");
HttpsURLConnection.setSSLSocketFactory.implementation = function(SSLSocketFactory) {
quiet_send("HttpsURLConnection.setSSLSocketFactory invoked");
};
HttpsURLConnection.setHostnameVerifier.implementation = function(hostnameVerifier) {
quiet_send("HttpsURLConnection.setHostnameVerifier invoked");
};
/*** Xutils3.x hooks ***/
//Implement a new HostnameVerifier
var TrustHostnameVerifier;
try {
TrustHostnameVerifier = Java.registerClass({
name: 'org.wooyun.TrustHostnameVerifier',
implements: [HostnameVerifier],
method: {
verify: function(hostname, session) {
return true;
}
}
});
} catch (e) {
//java.lang.ClassNotFoundException: Didn't find class "org.wooyun.TrustHostnameVerifier"
quiet_send("registerClass from hostnameVerifier >>>>>>>> " + e.message);
}
try {
var RequestParams = Java.use('org.xutils.http.RequestParams');
RequestParams.setSslSocketFactory.implementation = function(sslSocketFactory) {
sslSocketFactory = EmptySSLFactory;
return null;
}
RequestParams.setHostnameVerifier.implementation = function(hostnameVerifier) {
hostnameVerifier = TrustHostnameVerifier.$new();
return null;
}
} catch (e) {
quiet_send("Xutils hooks not Found");
}
/*** httpclientandroidlib Hooks ***/
try {
var AbstractVerifier = Java.use("ch.boye.httpclientandroidlib.conn.ssl.AbstractVerifier");
AbstractVerifier.verify.overload('java.lang.String', '[Ljava.lang.String', '[Ljava.lang.String', 'boolean').implementation = function() {
quiet_send("httpclientandroidlib Hooks");
return null;
}
} catch (e) {
quiet_send("httpclientandroidlib Hooks not found");
}
/***
android 7.0+ network_security_config TrustManagerImpl hook
apache httpclient partly
***/
var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
// try {
// var Arrays = Java.use("java.util.Arrays");
// //apache http client pinning maybe baypass
// //https://github.com/google/conscrypt/blob/c88f9f55a523f128f0e4dace76a34724bfa1e88c/platform/src/main/java/org/conscrypt/TrustManagerImpl.java#471
// TrustManagerImpl.checkTrusted.implementation = function (chain, authType, session, parameters, authType) {
// quiet_send("TrustManagerImpl checkTrusted called");
// //Generics currently result in java.lang.Object
// return Arrays.asList(chain);
// }
//
// } catch (e) {
// quiet_send("TrustManagerImpl checkTrusted nout found");
// }
try {
// Android 7+ TrustManagerImpl
TrustManagerImpl.verifyChain.implementation = function(untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
quiet_send("TrustManagerImpl verifyChain called");
// Skip all the logic and just return the chain again :P
//https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2017/november/bypassing-androids-network-security-configuration/
// https://github.com/google/conscrypt/blob/c88f9f55a523f128f0e4dace76a34724bfa1e88c/platform/src/main/java/org/conscrypt/TrustManagerImpl.java#L650
return untrustedChain;
}
} catch (e) {
quiet_send("TrustManagerImpl verifyChain nout found below 7.0");
}
// OpenSSLSocketImpl
try {
var OpenSSLSocketImpl = Java.use('com.android.org.conscrypt.OpenSSLSocketImpl');
OpenSSLSocketImpl.verifyCertificateChain.implementation = function(certRefs, authMethod) {
quiet_send('OpenSSLSocketImpl.verifyCertificateChain');
}
quiet_send('OpenSSLSocketImpl pinning')
} catch (err) {
quiet_send('OpenSSLSocketImpl pinner not found');
}
// Trustkit
try {
var Activity = Java.use("com.datatheorem.android.trustkit.pinning.OkHostnameVerifier");
Activity.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function(str) {
quiet_send('Trustkit.verify1: ' + str);
return true;
};
Activity.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function(str) {
quiet_send('Trustkit.verify2: ' + str);
return true;
};
quiet_send('Trustkit pinning')
} catch (err) {
quiet_send('Trustkit pinner not found')
}
try {
//cronet pinner hook
//weibo don't invoke
var netBuilder = Java.use("org.chromium.net.CronetEngine$Builder");
//https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/CronetEngine.Builder.html#enablePublicKeyPinningBypassForLocalTrustAnchors(boolean)
netBuilder.enablePublicKeyPinningBypassForLocalTrustAnchors.implementation = function(arg) {
//weibo not invoke
console.log("Enables or disables public key pinning bypass for local trust anchors = " + arg);
//true to enable the bypass, false to disable.
var ret = netBuilder.enablePublicKeyPinningBypassForLocalTrustAnchors.call(this, true);
return ret;
};
netBuilder.addPublicKeyPins.implementation = function(hostName, pinsSha256, includeSubdomains, expirationDate) {
console.log("cronet addPublicKeyPins hostName = " + hostName);
//var ret = netBuilder.addPublicKeyPins.call(this,hostName, pinsSha256,includeSubdomains, expirationDate);
//this 是调用 addPublicKeyPins 前的对象吗? Yes,CronetEngine.Builder
return this;
};
} catch (err) {
console.log('[-] Cronet pinner not found')
}
});
重点看这
就是对 sslSocketFactory、hostnameVerifier和certificatePinner进行了hook,自己注册了一个新的TrustManagers,这个TrustManagers不包含检测代码,然后通过init初始化,hostnameVerifier直接hook就行了,certificatePinner是hook的底层的check函数
hook到了关键函数
正常访问
4.6、okhttp3混淆后的定位方法
okhttp3是一个第三方库,编译的时候会打包到app中
okhttp3可以被混淆,如果混淆了函数名,我们可以通过搜索函数的参数来定位
直接搜索
或者也可以通过hook一些使用的系统函数进行定位
result是List通过add添加了一个pin,可以hook这里
Java.perform(function () {
function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(a, b) {
console.log('OkHTTP 3.x check() called. Not throwing an exception.');
console.log("=======================================================");
return this.check(a, b);
}
var arrayList = Java.use("java.util.ArrayList");
arrayList.add.overload('java.lang.Object').implementation = function (a) {
try{
if (a.toString().startsWith("sha1/")) {
console.log("realStr: ", a.toString());
showStacks();
}else if (a.toString().startsWith("sha256/")) {
console.log("realStr: ", a.toString());
showStacks();
}
}catch (e) {
}
return this.add(a);
}
})
4.7、通过hook获取网络请求数据
java层
对于okhttp3的源码分析我在 沙盒定制文章就写过了,这里不在重复
我们对于网络请求的发送和接受都会走底层的函数
NativeCrypto.SSL_write(ssl, this, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
NativeCrypto.SSL_read(ssl, this, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
Java.perform(function () {
function byteToString(arr) {
if (typeof arr === 'string') {
return arr;
}
var str = '',
_arr = arr;
for (var i = 0; i < _arr.length; i++) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for (var st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
return str;
}
var NativeCrypto =Java.use("com.android.org.conscrypt.NativeCrypto");
NativeCrypto.SSL_write.implementation=function (ssl, NativeCryptoObj, fd, handshakeCallbacks, buf, offset, len, timeoutMillis) {
console.log("hook SSL_write buf:"+byteToString(buf));
console.log("--------------------------------------------------------------------------------------\n")
return this.SSL_write(ssl, NativeCryptoObj, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}
NativeCrypto.SSL_read.implementation=function (ssl, NativeCryptoObj, fd, handshakeCallbacks, buf, offset, len, timeoutMillis) {
console.log("hook SSL_read buf:"+byteToString(buf));
console.log("--------------------------------------------------------------------------------------\n")
return this.SSL_read(ssl, NativeCryptoObj, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}
})
同理SSL_read也可以hook得到buf数据
上面的测试使用的是okhttp3框架,我们再测试一下HttpsURLConnection框架,也是可以hook成功的
注意通过hook的方法获取网络请求是只适用于https,因为走的是SSL_write和SSL_read,而http是不走这两个函数的,不过http可以直接通过抓包拦截,也不需要这么麻烦。
c层
c层之前我们也分析过,会走到libssl.so的下面两个方法
int SSL_write(SSL *ssl, const void *buf, int num)
int SSL_read(SSL *ssl, void *buf, int num)
这里的buf是还没有加密的,所以可以打印获取网络请求。
Java.perform(function () {
var SSL_write = Module.findExportByName("libssl.so","SSL_write");
var SSL_read = Module.findExportByName("libssl.so","SSL_read");
console.log("SSL_write Addr:"+SSL_write);
console.log("SSL_read Addr:"+SSL_read);
Interceptor.attach(SSL_write, {
onEnter: function (args) {
console.log("SSL_write buf:"+hexdump(args[1],{length:args[2].toInt32()}));
}, onLeave: function (retval) {
}
});
Interceptor.attach(SSL_read, {
onEnter: function (args) {
this.args1=args[1];
this.args2=args[2];
}, onLeave: function (retval) {
if (retval.toInt32()>0){
console.log("SSL_read buf:"+hexdump(this.args1,{length:retval.toInt32()}));
}
}
});
})
我们继续分析,SSL_write和SSL_read底层走的libc.so的write和read,虽然已经加密了,但是这个是最底层的函数了,我们可以通过hook这里,打印堆栈来定位发包位置,因为有的app会魔改libssl.so
Java.perform(function () {
var write = Module.findExportByName("libc.so","write");
var read = Module.findExportByName("libc.so","read");
Interceptor.attach(write, {
onEnter: function (args) {
console.log("write :"+Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log("write buf:"+hexdump(args[1],{length:args[2].toInt32()}));
}, onLeave: function (retval) {
}
});
Interceptor.attach(read, {
onEnter: function (args) {
this.args1=args[1];
this.args2=args[2];
}, onLeave: function (retval) {
if (retval.toInt32()>0){
console.log("read :"+Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log("read buf:"+hexdump(this.args1,{length:retval.toInt32()}));
}
}
});
})
这里就是libcrypto.so直接调用了libc.so的read
5、r0capture的使用
r0capture其实原理还是hook了libssl.so的SSL_write和SSL_read
GitHub地址:https://github.com/r0ysue/r0capture
使用方式
1、先安装依赖 pip install hexdump
2、命令
attach模式,抓包内容可以保存成pcap文件供后续分析
python/python3 r0capture.py -U 进程名 -v -p a.pcap
spawn模式
python/python3 r0capture.py -U -f 包名 -v
用wireshark打开a.pcap
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com