Android逆向中抓包检测与hook抓包

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