抓包:Android对抗证书校验

Android客户端单向验证

客户端单向验证手段很多,可以参考JustTrustMe,SSLKiller 之类的Hook框架源码去探索,这里选取几个常用的API讲解。
这里我们依旧拿 https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=XXX 接口做测试,首先代码里嵌入证书(下面代码中的 TAOBAO_CERTIFICATE 变量)用来做校验:

public final static String TAOBAO_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" +
        "MIIeCzCCHPOgAwIBAgIMUsU3P5Y0P8vdHcZnMA0GCSqGSIb3DQEBCwUAMGYxCzAJ\n" +
        "BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTwwOgYDVQQDEzNH\n" +
        ...此处省略百来行...
        "GwQ/XhBRqDw9PZIlGokmQEjKrHTA1/F3a7ZI4penyDeIVP5Qcum0IBFbZl1vaxSf\n" +
        "zSg424KuPxlXi6ivheAC\n" +
        "-----END CERTIFICATE-----\n";

X509TrustManager

从下面代码中不难看出证书验证逻辑都在X509TrustManager的方法中:

//HttpsURLConnection请求https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13999999999
public void getHtmlByHttpsUrlconnection(String path) throws Exception {
    URL url = new URL(path);
    HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
    conn.setDoOutput(true);
    conn.setDoInput(true);
    conn.setConnectTimeout(3000);
    conn.setSSLSocketFactory(getSSLContext().getSocketFactory()); //关键在这里

    if (conn.getResponseCode() == 200) {
        InputStream inStream = conn.getInputStream();
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, len);
        }
        inStream.close();
        String res = new String(outStream.toByteArray(), "GBK");
        Log.d("GRAB", res);
    }
}

private static SSLContext getSSLContext() {
    X509TrustManager x509TrustManager = new X509TrustManager() {

        /**
         * 校验服务端证书
         * @param chain 证书链(不包含根证书)
         * @param authType 算法类型
         * @throws CertificateException
         */
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            if (chain == null || chain.length <= 0) {
                throw new CertificateException("没证书");
            }
            if (TextUtils.isEmpty(authType) || authType.toUpperCase().contains("RSA")) {
                throw new CertificateException("算法类型不对劲");
            }

            //请求获取的证书
            X509Certificate x509Certificate0 = chain[0]; //证书从子往父
            PublicKey publicKey = x509Certificate0.getPublicKey();
            String strPublicKey = Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT);
            Log.d("公钥是:", strPublicKey);

            //用服务端证书生成对象
            CertificateFactory x509Certificate = CertificateFactory.getInstance("X.509");
            Certificate taobaoCertificate = x509Certificate.generateCertificate(new ByteArrayInputStream(TAOBAO_CERTIFICATE.getBytes()));
            String strTaobaoPublicKey = Base64.encodeToString(taobaoCertificate.getPublicKey().getEncoded(), Base64.DEFAULT);
            Log.d("服务器公钥是:", strTaobaoPublicKey);

            if (!strPublicKey.equalsIgnoreCase(strTaobaoPublicKey)) {
                throw new CertificateException("公钥不对劲");
            }

            x509Certificate0.checkValidity();//验证证书到期时间

            //可以用来验证的东西很多
            String subjectDN = x509Certificate0.getSubjectDN().toString();
            if (subjectDN.equalsIgnoreCase("taobao.com")) {
                throw new CertificateException("subjectDN不对劲");
            }
        }

        @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[]{x509TrustManager}, new SecureRandom());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return sslContext;
}

如果我们直接通过Fiddler抓包,那肯定是得不到数据的,于是使用Frida Hook上面的校验方法:

function hookX509TrustManager() {
    Java.perform(function () {
        let MainActivity = Java.use("com.zyc.grabdata.MainActivity");
        MainActivity.getSSLContext.implementation = function () {
            //构造自己的X509TrustManager实现类
            let X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
            let MyX509TrustManager = Java.registerClass({
                name: 'com.zyc.grabdata.MyX509TrustManager',
                implements: [X509TrustManager],
                methods: {
                    checkClientTrusted: function (chain, authType) {
                        console.log("Frida Hook checkClientTrusted()", "Success!!!");
                    },
                    checkServerTrusted: function (chain, authType) {
                        console.log("Frida Hook checkServerTrusted()", "Success!!!");
                    },
                    getAcceptedIssuers: function (chain, authType) {
                        console.log("Frida Hook getAcceptedIssuers()", "Success!!!");
                        return [];
                    }
                }
            });

            //实例化一个SSLContext
            let SSLContext = Java.use("javax.net.ssl.SSLContext");
            let sslContext = SSLContext.getInstance("TLS");
            let TrustManagers = [MyX509TrustManager.$new()];
            let SecureRandom = Java.use("java.security.SecureRandom");
            let secureRandom = SecureRandom.$new();
            sslContext.init(null, TrustManagers, secureRandom);
            return sslContext;
        }
    });
}

运行:
在这里插入图片描述

HostnameVerifier

注释掉checkServerTrusted()中的验证,在 HttpsURLConnection 加入下面代码,这里让verify()强制返回false,即无论如何都不让通过。

conn.setHostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) { // 这里也可以通过session验证证书
        Log.d("GRAB", "hostname就不给你过");
        return false;
    }
});

此时发送HttpsURLConnection肯定是不通过的,同样使用Frida Hook上面的校验方法:

function hookHostnameVerifier() {
    Java.perform(function () {
        //getHostnameVerifierInterfaces();

        var HostnameVerifier = Java.use("com.zyc.grabdata.MainActivity$5"); 
        HostnameVerifier.verify.implementation = function(){
            return true;
        }
    });
}

/**
 * 遍历查找到HostnameVerifier实例是com.zyc.grabdata.MainActivity$5
 */
function getHostnameVerifierInterfaces(){
    Java.enumerateLoadedClasses({
        onMatch: function (name){
            if (name.indexOf("com.zyc.grabdata") != -1 ) {
                var clazz = Java.use(name)
                var interfaces = clazz.class.getInterfaces()
                if (interfaces.length > 0) {
                    console.log(name + ": ")
                    for (var i in interfaces) {
                        console.log("\t", interfaces[i].toString())
                    }
                }
            }
        },
        onComplete: function () {
            console.log("end")
        }
    })
}

运行,可以正常请求了。
在这里插入图片描述

CertificatePinner

用例OkHttpClient初始化部分改为下面代码锁定证书:

CertificatePinner certificatePinner = new CertificatePinner.Builder()
        .add("*.taobao.com", "sha256/IfXz1a0gWBA5oH+zasmRutUiyoZN3I8wLxHNQxk3NVo=")
        .add("*.taobao.com", "sha256/IQBnNBEiFuhj+8x6X8XLgh01V9Ic5/V3IRQLNFFc7v4=")
        .add("*.taobao.com", "sha256/K87oWBWM9UZfyddvDfoxL+8lpNyoUB2ptGtn0fv6G2Q=")
        .build();

OkHttpClient okHttpClient = new OkHttpClient().newBuilder().certificatePinner(certificatePinner).build();

通过代理证书肯定是无法请求到数据和抓包了,使用下面代码Hook:

function hookCertificatePinner(){
    Java.perform(function () {
        let Builer = Java.use("okhttp3.CertificatePinner$Builder");
        Builer.add.implementation = function () {
            console.log("Frida Hook hookCertificatePinner()", "Success!!!");
            return this;
        }
    });
}

Hook之后可以正常取得数据并抓到了:
在这里插入图片描述
 

服务端单向验证

上面介绍了客户端单向验证的通用方案,接下来介绍一个服务端单向验证的案例。打开App试图抓包,会发现提示证书相关错误:
在这里插入图片描述
这里其实是服务端对客户端的证书进行了校验,Charles没有其需要的证书自然是通过不了。那么只要将客户端证书导入Charles问题就解决了,从apk的assets目录可以找到p12文件,这个便是服务端要校验的证书。拖出来发现该文件没有加固,可以直接导入。
在这里插入图片描述
不过导入p12是需要密码的:
在这里插入图片描述
密码常在下面API中使用:

// 参看java.security.KeyStore

/**
 * Loads this KeyStore from the given input stream.
 *
 * <p>A password may be given to unlock the keystore
 * (e.g. the keystore resides on a hardware token device),
 * or to check the integrity of the keystore data.
 * If a password is not given for integrity checking,
 * then integrity checking is not performed.
 *
 * <p>In order to create an empty keystore, or if the keystore cannot
 * be initialized from a stream, pass {@code null}
 * as the {@code stream} argument.
 *
 * <p> Note that if this keystore has already been loaded, it is
 * reinitialized and loaded again from the given input stream.
 *
 * @param stream the input stream from which the keystore is loaded,
 * or {@code null}
 * @param password the password used to check the integrity of
 * the keystore, the password used to unlock the keystore,
 * or {@code null}
 *
 * @exception IOException if there is an I/O or format problem with the
 * keystore data, if a password is required but not given,
 * or if the given password was incorrect. If the error is due to a
 * wrong password, the {@link Throwable#getCause cause} of the
 * {@code IOException} should be an
 * {@code UnrecoverableKeyException}
 * @exception NoSuchAlgorithmException if the algorithm used to check
 * the integrity of the keystore cannot be found
 * @exception CertificateException if any of the certificates in the
 * keystore could not be loaded
 */
public final void load(InputStream stream, char[] password)
    throws IOException, NoSuchAlgorithmException, CertificateException
{
    keyStoreSpi.engineLoad(stream, password);
    initialized = true;
}

/**
 * Loads this keystore using the given {@code LoadStoreParameter}.
 *
 * <p> Note that if this KeyStore has already been loaded, it is
 * reinitialized and loaded again from the given parameter.
 *
 * @param param the {@code LoadStoreParameter}
 *          that specifies how to load the keystore,
 *          which may be {@code null}
 *
 * @exception IllegalArgumentException if the given
 *          {@code LoadStoreParameter}
 *          input is not recognized
 * @exception IOException if there is an I/O or format problem with the
 *          keystore data. If the error is due to an incorrect
 *         {@code ProtectionParameter} (e.g. wrong password)
 *         the {@link Throwable#getCause cause} of the
 *         {@code IOException} should be an
 *         {@code UnrecoverableKeyException}
 * @exception NoSuchAlgorithmException if the algorithm used to check
 *          the integrity of the keystore cannot be found
 * @exception CertificateException if any of the certificates in the
 *          keystore could not be loaded
 *
 * @since 1.5
 */
public final void load(LoadStoreParameter param)
            throws IOException, NoSuchAlgorithmException,
            CertificateException {

    keyStoreSpi.engineLoad(param);
    initialized = true;
}

根据这个方法写出Hook代码:

function hookKeyStore() {
    Java.perform(function () {
        const String = Java.use("java.lang.String");
        const KeyStore = Java.use("java.security.KeyStore");
        KeyStore.load.overload("java.security.KeyStore$LoadStoreParameter").implementation = function (param) {
            console.log("进入了param重载");
            if(param){
                console.log("param:", param);
            }
            this.load(param);
        }
        KeyStore.load.overload("java.io.InputStream", "[C").implementation = function (stream, password) {
            console.log("进入了stream, password重载");
            if(stream){
                console.log("stream:", stream);
            }
            if(password){
                console.log("password:", String.$new(password));
            }
            this.load(stream, password);
        }
    });
}

运行拿到密码:
在这里插入图片描述
填入Charles可以通过。
在这里插入图片描述
能正常抓包了:
在这里插入图片描述
 

相关资料

Https单向认证和双向认证

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值