https通信过程
1 2 3 4 5 6 7
| 服务器返回证书公钥
客户端拿着公钥去加密一段随机的字符串(对称加密的密钥)
服务器拿着私钥去解密,得到这段字符串,也就是对称加密的密钥
服务器和客户端通过对称密钥加密的密文进行通信
|
抓包的时候,公钥肯定和真实的服务端证书不一样
所谓hook抓包,就是把app发送数据包时调用的函数给hook下来,然后获取发送的数据包,而不是去替换证书
常见的抓包检测
不走代理
开发的时候可以在源码里指定不走系统代理
1 2 3 4 5 6 7
| HttpsURLConnection设置不走代理 okhttp3设置不走代理
解决方案: Hook设置代理的函数,手动给设置代理 在模拟器中可以使用httpv7来抓包 使用VPN抓包(httpcanary或者charles+postman就属于这种,使用这两种方法的时候不用管设置走不走代理)
|
代理检测与VPN检测
代理检测,通过System.getProperty
来获取当前代理设置,C代码也可以实现,但是httpcanary或者charles+postman可以无视代理检测
VPN检测,见后面
单向验证与双向验证
属于证书检测的范围,单项检测就是客户端校验服务器的证书
1 2 3 4 5 6 7 8 9 10
| 2.1 抓包解密SSL流量,需要伪造证书 2.2 通常使用抓包工具的根证书,来签发一个服务器实体证书,这时可以证书检 2.3 客户端校验服务器证书 通常利用系统相关函数来校验证书,这时可以通过Hook相关系统函数来绕过 X509TrustManager HostnameVerifier okhttp3证书锁定 okhttp3证书校验 2.4 服务器校验客户端证书 有的客户端请求的时候会带上自己的内置证书一起发给服务端,就需要把内置证书找出来,放到抓包工具里
|
Hook抓包
Hook方式抓包,不走代理、没有VPN、不改证书,上面的检测一般不用管,但是hook也可以被检测到,也就是检测hook框架
抓到的可能没有抓包工具全
代理检测与VPN检测的实现
代理检测,通过System.getProperty
来获取当前代理设置
检测VPN的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static void getNetworkName() { 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(); } }
|
其实就是Hook NetworkInterface这个类的各种方法,也就是java.net.NetworkInterface.getName
可以使用objection把java.net下的类全部hook,来查看app使用了哪些方法
demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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; } } }) }
|
或者通过connectivityManager.getNetworkCapabilities()
来检测VPN
1 2 3 4 5 6 7 8 9 10 11
| 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(); } }
|
hook也就是hook这些函数,底层还调用了一些,也没细看了,差不多就这个思路,返回正常的字符串就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Java.perform(function () { var NetworkCapabilities = Java.use("android.net.NetworkCapabilities"); NetworkCapabilities.hasTransport.implementation = function () { return false; }
NetworkCapabilities.appendStringRepresentationOfBitMaskToStringBuilder.implementation = function (sb, bitMask, nameFetcher, separator) { if (bitMask == 18) { console.log("bitMask", bitMask); sb.append("WIFI"); }else { console.log(sb, bitMask); this.appendStringRepresentationOfBitMaskToStringBuilder(sb, bitMask, nameFetcher, separator); } }
})
|
HttpsURLConnection的使用
Android开发里一般把网络请求放到子线程里,因为比较耗时
1 2 3 4 5 6
| new Thread(){ public void run(){ String str = SSLHelper.getHttpsURLConnection("POST","https://www.baidu.com/","user"); Log.d("DemoJohn","DemoJohn:" + str); } }.start();
|
SSLHelper
类的实现
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
| package com.DemoJohn.https;
import ...;
public class SSLHelper { public static String certificate = "-----BEGIN CERTIFICATE-----\n" + "MIIKLjCCCRagAwIBAgIMclh4Nm6fVugdQYhIMA0GCSqGSIb3DQEBCwUAMGYxCzAJ\n" + "rlylLTTYmlW3WETOATi70HYsZN6NACuZ4t1hEO3AsF7lqjdA2HwTN10FX2HuaUvf\n" + "5OzP+PKupV9VKw8x8mQKU6vr\n" + "-----END CERTIFICATE-----";
private static SSLContext getSSLContext() { X509TrustManager trustManager = new X509TrustManager() {
@ Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (chain == null) { throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null"); } if (!(chain.length > 0)) { throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty"); } if (!(!TextUtils.isEmpty(authType) && authType.toUpperCase().contains("RSA"))) { throw new CertificateException("checkServerTrusted: AuthType is not RSA"); } Log.d("DemoJohn","authType: " + authType); X509Certificate cf = chain[0];
RSAPublicKey pubkey = (RSAPublicKey)cf.getPublicKey(); String encoded = Base64.encodeToString(pubkey.getEncoded(),0);
CertificateFactory finalcf = CertificateFactory.getInstance("X.509"); X509Certificate PUB_KEY = (X509Certificate)finalcf.generateCertificate(new ByteArrayInputStream(certificate.getBytes())); String realPubKey = Base64.encodeToString(PUB_KEY.getPublicKey().getEncoded(),0);
cf.checkValidity(); Log.d("DemoJohn", "IssuerDN: " + cf.getIssuerDN().toString()); Log.d("DemoJohn", "SubjectDN: " + cf.getSubjectDN().toString()); Log.d("DemoJohn", "证书版本: "+ cf.getVersion());
final boolean expected = realPubKey.equalsIgnoreCase(encoded); if (!expected) { throw new CertificateException("checkServerTrusted: got error public key: " + encoded); } Log.d("DemoJohn","证书公钥验证正确");
}
@Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@ Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } };
SSLContext sslContext = null; try { sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom()); } catch (Exception e) { e.printStackTrace(); } return sslContext; }
public static String getHttpsURLConnection(String method, String url, String outputStr) { HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() { @ Override public boolean verify(String hostname, SSLSession session) { Log.d("DemoJohn", hostname); return true; } };
Log.d("DemoJohn", "https: " + System.getProperty("https.proxyHost")); Log.d("DemoJohn", "https: " + System.getProperty("https.proxyPort")); Log.d("DemoJohn", "http: " + System.getProperty("http.proxyHost")); Log.d("DemoJohn", "http: " + System.getProperty("http.proxyPort"));
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); conn.setSSLSocketFactory(sslContext.getSocketFactory()); conn.setHostnameVerifier(DO_NOT_VERIFY);
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; } }
|
hook的话,就hook相关的类就行,注意HttpsURLConnection
的类比较杂,涉及到javax.net.ssl.HttpsURLConnection
、java.net.URLConnection
和com.android.okhttp.internal.huc.HttpsURLConnectionImpl