Android HTTPS认证的N种方式和对抗方法总结

0x0 前言

本文将通过一个Demo APP实现Android中各种HTTPS证书认证,并尝试通过各种方式绕过证书校验和证书绑定。目的是自我总结与知识点回顾,如果你正在学习这方面的知识,那这篇文章能帮助你从实践中体会Android APP中对HTTPS的防抓包技术及其对抗技术。

a. 前置知识

阅读本文之前需要的前置知识:

  • HTTPS
    • 加密原理
    • 证书交换和验证原理
  • Android 开发技能(一点点)
  • Frida Hook
  • Android 代理抓包技能

b. 设备环境:

  • RedmiK305G Android10(root) with Magisk
  • 代理端BurpSuite Pro v2.0.11 & Fiddler v5.0
  • PC Windows 10
  • 虚拟机 Kali Linux 2021
  • frida 14.2.16

c. 简单配置

简单介绍一下我的证书配置方法,并不适合所有人,根据自己的设备配置,总而言之目的就是将代理软件的证书信任为系统证书

  • 将Fiddler 证书导入Android 手机,安装为用户证书

  • 将BurpSuite 证书导入Android 手机,安装为用户证书

  • Magisk 开启Always Trust User Certificates 模块

  • 手机和PC在同一局域网下

  • PC开启代理软件,配置好监听地址和端口

  • 手机在WIFI网络中设置代理

d. Demo APP

建议阅读下文代码片段时参考APP源码

APP 源码:Ch3nYe/httpstest: Android APP for https test (github.com)

APP Demo 截图:

image-20210430205919949

0x1 HTTP 直连

本文中使用的https访问组件是okhttp3,直接访问http协议的url就可以了。不需要进行任何多余的配置,只需要配置好代理,http流量会被代理软件抓到。

/*
* http协议
*/
button_http_connect.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread(new Runnable() {
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void run() {
                OkHttpClient mClient = client.newBuilder().build();
                Request request = new Request.Builder()
                        .url("http://www.vulnweb.com/")
                        .build();
                Message message = new Message();
                message.what = 1;
                try (Response response = mClient.newCall(request).execute()) {
                    message.obj = "http connect access vulnweb.com success";
                    Log.d(TAG, "http connect access vulnweb.com success return code:"+response.code());
                } catch (IOException e) {
                    message.obj = "http connect access vulnweb.com failed";
                    Log.d(TAG, "http connect access vulnweb.com failed");
                    e.printStackTrace();
                }
                mHandler.sendMessage(message);
            }
        }).start();
    }
});

成功访问时,APP界面上会显示http connect access vulnweb.com success

AS(AndroidStudio)中查看日志会看到打印类似 2021-04-30 21:25:38.888 8351-8516/com.example.httpstest D/[+]MainActivity: http connect access vulnweb.com success return code:200

上述代码中的message对象的目的是:使用Binder服务从子线程,将HTTP/HTTPS请求结果发送到主线程以改变UI。处理函数Handler的实现如下:

// 注册Handler处理从thread中返回的url请求结果
@SuppressLint("HandlerLeak") final Handler mHandler = new Handler(){
    public void handleMessage(Message msg) {
        // 处理消息
        super.handleMessage(msg);
        switch (msg.what) {
            case 1:
                textView.setText((CharSequence) msg.obj); break;
        }
    }
};

0x2 HTTPS 忽略证书验证

正常人不会忽略证书验证,写这个功能纯粹是为了实验而实验。

/*
* https协议
* 忽略证书验证
*/
button_https_connect_without_ca.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread(new Runnable(){
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void run() {
                OkHttpClient mClient = client.newBuilder().sslSocketFactory(HttpsTrustAllCerts.createSSLSocketFactory(),new HttpsTrustAllCerts()).hostnameVerifier(new HttpsTrustAllCerts.TrustAllHostnameVerifier()).build();
                Request request = new Request.Builder()
                        .url("https://www.baidu.com/?q=trustAllCerts")
                        .build();
                Message message = new Message();
                message.what = 1;
                try (Response response = mClient.newCall(request).execute()) {
                    message.obj = "https connect without ca success";
                    Log.d(TAG, "https connect without ca success return code:"+response.code());
                } catch (IOException e) {
                    message.obj = "https connect without ca failed";
                    Log.d(TAG, "https connect without ca failed");
                    e.printStackTrace();
                }
                mHandler.sendMessage(message);
            }
        }).start();
    }
});

HttpsTrustAllCerts对象实现如下,重写checkClientTrusted、checkServerTrusted方法,使其验证逻辑为空,重写域名验证器TrustAllHostnameVerifier 的verify方法,总是返回true,达到信任所有证书的效果:

public class HttpsTrustAllCerts implements X509TrustManager {


    @SuppressLint("TrustAllX509TrustManager")
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @SuppressLint("TrustAllX509TrustManager")
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 验证服务端证书需要重写该函数

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0]; //返回长度为0的数组,相当于return null
    }


    public static SSLSocketFactory createSSLSocketFactory() { // SSLSocketFactory 创建器
        SSLSocketFactory sSLSocketFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, new TrustManager[]{new HttpsTrustAllCerts()},new SecureRandom());
            sSLSocketFactory = sc.getSocketFactory();
        } catch (Exception e) {
        }
        return sSLSocketFactory;
    }


    public static class TrustAllHostnameVerifier implements HostnameVerifier { // 域名验证器
        @Override
        public boolean verify(String s, SSLSession sslSession) {
            return true;
        }
    }
}

忽略证书校验的https请求和使用http协议的请求报文一样,可以通过直接设置网络代理抓包解密明文。

0x3 HTTPS 系统证书校验

OkHttp3框架发起HTTPS请求时,默认就是使用的系统信任库证书链对服务端返回的证书进行验证,实现如下:

/*
* https协议
* 默认证书链校验,只信任系统CA(根证书)
*
* tips: OKHTTP默认的https请求使用系统CA验证服务端证书(Android7.0以下还信任用户证书,Android7.0开始默认只信任系统证书)
*/
button_https_connect_with_system_ca.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread(new Runnable(){
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void run() {
                Request request = new Request.Builder()
                        .url("https://www.baidu.com/?q=defaultCerts")
                        .build();
                Message message = new Message();
                message.what = 1;
                try (Response response = client.newCall(request).execute()) {
                    message.obj = "https connect with system ca success";
                    Log.d(TAG, "https connect with system ca success return code:"+response.code());
                } catch (IOException e) {
                    message.obj = "https connect with system ca failed";
                    Log.d(TAG, "https connect with system ca failed");
                    e.printStackTrace();
                }
                mHandler.sendMessage(message);
            }
        }).start();
    }
});

此时需要安装代理软件的证书到系统证书目录,前面说过我本机已经配置两个抓包软件的证书到系统证书列表了,所以仍然可以抓包解密明文。

0x4 SSL PINNING(代码校验)

SSL PINNING 就是说通过客户端检查服务端证书是否真的是服务端证书来判断是否被中间人攻击。由于客户端需要验证服务端证书的正确性,那大原则上就是说客户端必须要有能判断正确性的依据。对于Android APP来说通常有两种方式:

  • 客户端持有证书公钥hash
  • 客户端持有证书文件

下面的代码段也是通过这两种方式进行了证书验证。这样代理软件充当服务端的时候发给APP的证书就不能通过SSL PINNING验证。证书的获取方式可以参考代码中的注释。

/*
* https协议 SSL Pinning
* 证书公钥绑定:验证证书公钥 baidu.com 使用CertificatePinner
* 证书文件绑定:验证证书文件 bing.com  使用SSLSocketFactory
*/
button_SSL_PINNING_with_key.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread(new Runnable(){
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void run() {
                final String CA_DOMAIN = "www.baidu.com";
                //获取目标公钥: openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
                final String CA_PUBLIC_KEY = "sha256//558pd1Y5Vercv1ZoSqOrJWDsh9sTMEolM6T8csLucQ=";
                //只校验公钥
                CertificatePinner pinner = new CertificatePinner.Builder()
                        .add(CA_DOMAIN, CA_PUBLIC_KEY)
                        .build();
                OkHttpClient pClient = client.newBuilder().certificatePinner(pinner).build();
                Request request = new Request.Builder()
                        .url("https://www.baidu.com/?q=SSLPinningCode")
                        .build();
                Message message = new Message();
                message.what = 1;
                try (Response response = pClient.newCall(request).execute()) {
                    message.obj = "https SSL_PINNING_with_key access baidu.com success";
                    Log.d(TAG, "https SSL_PINNING_with_key access baidu.com success return code:"+response.code());
                } catch (IOException e) {
                    message.obj = "https SSL_PINNING_with_key access baidu.com failed";
                    Log.d(TAG, "https SSL_PINNING_with_key access baidu.com failed");
                    e.printStackTrace();
                }


                try {
                    // 获取证书输入流
                    // 获取证书 openssl s_client -connect bing.com:443 -servername bing.com | openssl x509 -out bing.pem
                    InputStream openRawResource = getApplicationContext().getResources().openRawResource(R.raw.bing); //R.raw.bing是bing.com的正确证书,R.raw.bing2_so是hostname=bing.com的so.com的证书,可视为用作测试的虚假bing.com证书
                    Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(openRawResource);
                    // 创建 Keystore 包含我们的证书
                    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                    keyStore.load(null, null);
                    keyStore.setCertificateEntry("ca", ca);
                    // 创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点
                    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // 建议不要使用自己实现的X509TrustManager,而是使用默认的X509TrustManager
                    trustManagerFactory.init(keyStore);
                    // 用 TrustManager 初始化一个 SSLContext
                    sslContext = SSLContext.getInstance("TLS");  //定义:public static SSLContext sslContext = null;
                    sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

                    OkHttpClient pClient2 = client.newBuilder().sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0]).build();
                    Request request2 = new Request.Builder()
                            .url("https://www.bing.com/?q=SSLPinningCAfile")
                            .build();
                    try (Response response2 = pClient2.newCall(request2).execute()) {
                        message.obj += "\nhttps SSL_PINNING_with_CA_file access bing.com success";
                        Log.d(TAG, "https SSL_PINNING_with_CA_file access bing.com success return code:"+response2.code());
                    } catch (IOException e) {
                        message.obj += "\nhttps SSL_PINNING_with_CA_file access bing.com failed";
                        Log.d(TAG, "https SSL_PINNING_with_CA_file access bing.com failed");
                        e.printStackTrace();
                    }

                } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | KeyManagementException e) {
                    e.printStackTrace();
                }
                mHandler.sendMessage(message);
            }
        }).start();
    }
});

此时可以使用Frida对上述证书绑定进行动作Hook,阻断绑定,hook代码在0x-1附录。

0x5 SSL PINNING(配置文件)

通过res/xml/network_security_config.xml配置文件对证书进行校验是官方推荐使用的方法,配置方式还是可以为两种(同上)

  • 公钥校验

  • 证书校验

xml 配置如下所示:

<network-security-config xmlns:tools="http://schemas.android.com/tools">
    <!--允许http访问-->
    <base-config cleartextTrafficPermitted="true"
        tools:ignore="InsecureBaseConfiguration" />
    <!--证书校验-->
    <domain-config>
        <domain includeSubdomains="true">sogou.com</domain>
        <trust-anchors>
            <!--获取证书: openssl s_client -connect sogou.com:443 -servername sogou.com | openssl x509 -out sogou.pem-->
            <certificates src="@raw/sogou"/>
        </trust-anchors>
    </domain-config>

    <!--公钥校验-->
    <domain-config>
        <domain includeSubdomains="true">zhihu.com</domain>
        <!--zhihu.com公钥校验
        获取公钥: openssl s_client -connect zhihu.com:443 -servername zhihu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
        -->
        <pin-set expiration="2099-01-01"
            tools:ignore="MissingBackupPin">
            <pin digest="SHA-256">vzXV96/gpZMyyNNhyTdjtX0/NUVYTtmYqWcVVaUtTdQ=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

这样配置好以后就可以对上述指定的域名进行https访问,将会自动对证书进行校验,请求代码:

/*
* https协议 SSL PINNING
* 证书绑定验证 配置在 @xml/network_security_config 中
* sogou.com 使用 sogou.pem 验证证书
* so.com 使用 sha256 key 验证
*/
button_SSL_PINNING_with_CA.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread(new Runnable(){
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void run() {
                OkHttpClient pClient = client.newBuilder().build();
                Request request = new Request.Builder()
                        .url("https://www.sogou.com/web?query=SSLPinningXML")
                        .build();
                Request request2 = new Request.Builder()
                        .url("https://www.zhihu.com/")
                        .build();
                Message message = new Message();
                message.what = 1;
                try (Response response = pClient.newCall(request).execute()) {
                    message.obj = "https SSL_PINNING_with_CA, config in xml with CA.pem file access sogou.com success";
                    Log.d(TAG, "https SSL_PINNING_with_CA, config in xml with CA.pem file access sogou.com success return code:"+response.code());
                } catch (IOException e) {
                    message.obj = "https SSL_PINNING_with_CA, config in xml with CA.pem file access sogou.com failed";
                    Log.d(TAG, "https SSL_PINNING_with_CA, config in xml with CA.pem file access sogou.com failed");
                    e.printStackTrace();
                }
                try (Response response = pClient.newCall(request2).execute()) {
                    message.obj += "\nhttps SSL_PINNING_with_CA, config in xml with key access zhihu.com success";
                    Log.d(TAG, "https SSL_PINNING_with_CA, config in xml with key access zhihu.com success return code:"+response.code());
                } catch (IOException e) {
                    message.obj += "\nhttps SSL_PINNING_with_CA, config in xml with key access zhihu.com failed";
                    Log.d(TAG, "https SSL_PINNING_with_CA, config in xml with key access zhihu.com failed");
                    e.printStackTrace();
                }
                mHandler.sendMessage(message);
            }
        }).start();
    }
});

该证书绑定实现,绕过方式同0x4 SSL PINNING(代码校验)可以使用frida hook unpinning。

0x6 HTTPS 双向验证

双向验证,顾名思义就是客户端验证服务器端证书的正确性,服务器端也验证客户端的证书正确性,所以大原则上客户端要持有服务器端证书,服务端也要持有客户端的证书,对于Android APP来说打包发布的时候既要内置一个服务端证书也要生成一个客户端证书给服务器存储起来。这种双向认证非常可以做到非常高的安全性,但是同时服务端要想保持所有服务端的证书比较困难,因此这种方式只适用于某些点对点的高安全性需求的通信场合,对于Android APP来说可能是某类高机密性的内网业务才会使用这种双向HTTPS认证。

接下来我们需要实现一个server模拟HTTPS双向认证。首先通过以下命令生成一对证书。

服务端证书:

openssl genrsa -out server-key.key 2048
openssl req -new -out server-req.csr -key server-key.key
openssl x509 -req -in server-req.csr -out server-cert.cer -signkey server-key.key  -CAcreateserial -days 3650

客户端证书:

openssl genrsa -out client-key.key 2048
openssl req -new -out client-req.csr -key client-key.key
openssl x509 -req -in client-req.csr -out client-cert.cer -signkey client-key.key -CAcreateserial -days 3650

生成客户端带密码的p12证书(这步很重要,双向认证的话,浏览器访问时候要导入该证书才行;可能某些Android系统版本请求的时候需要把它转成bks来请求双向认证,我的设备用p12格式是可行的):

openssl pkcs12 -export -clcerts -in client-cert.cer -inkey client-key.key -out client.p12

这里你也可以选择使用我生成好的证书,需要使用的证书都在源码certs目录,我的client.p12密码是clientpassword,server证书的密码是serverpassword。

服务端代码:

import os
import sys
import ssl
from http.server import HTTPServer, BaseHTTPRequestHandler

#服务端证书和私钥
serverCerts = "%s\\certs\\server-cert.cer" % os.getcwd()
serverKey = "%s\\certs\\server-key.key" % os.getcwd()
#客户端证书
clientCerts = "%s\\certs\\client-cert.cer" % os.getcwd()

class RequestHandler(BaseHTTPRequestHandler):
    def _writeheaders(self):
        self.send_response(200)
        self.send_header('Content-type','text/plain')
        self.end_headers()
    def do_GET(self):
        self._writeheaders()
        self.wfile.write("OK".encode("utf-8"))

def main():
    if (len(sys.argv) != 2):
        port = 443
    else:
        port = sys.argv[1]
    server_address = ("0.0.0.0", int(port))
    server = HTTPServer(server_address, RequestHandler)
    #双向校验
    server.socket = ssl.wrap_socket(server.socket, certfile = serverCerts, server_side = True,  
                               keyfile = serverKey,
                               cert_reqs = ssl.CERT_REQUIRED,
                               ca_certs = clientCerts,
                               do_handshake_on_connect = False
                               )
    print("Starting server, listen at: %s:%s" % server_address)
    server.serve_forever()

if __name__ == "__main__":
    main()

使用python启动server,在windows上运行可能报错,大概率是由于防火墙禁止启用443端口。

OSError: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。

所以我的方案是在VMware虚拟机中启动这个server,然后再宿主机配置hosts,添加主机域名

<虚拟机IP> www.test.com

宿主机安装client.p12证书(windows双击安装好方便),然后使用浏览器访问 www.test.com

image-20210430222039858

然后你就选这个,就可以访问了,访问成功会返回OK字样的空白页。服务端日志:

image-20210430222259900

尽管输出ERROR但是还是访问成功了,错误原因应该是证书为自签名证书,客户端并不想认可这个没娘的证书。

如果你能做到这里说明你已经正确实现了HTTPS双向认证,接下来我们要在Android APP上复现。

/*
* 双向校验
* 因该测试是自建服务器并自签名,所以需要先在res/xml/network_security_config中配置信任服务端证书
*/
button_https_twoway.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread(new Runnable(){
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void run() {
                X509TrustManager trustManager = null;
                try {
                    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

                    trustManagerFactory.init((KeyStore) null);
                    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
                    if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                        throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
                    }
                    trustManager = (X509TrustManager) trustManagers[0];
                } catch (Exception e) {
                    e.printStackTrace();
                }
                OkHttpClient mClient = client.newBuilder().sslSocketFactory(Objects.requireNonNull(ClientSSLSocketFactory.getSocketFactory(getApplicationContext())), Objects.requireNonNull(trustManager)).hostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
                        return hv.verify("www.test.com", session);
                    }
                }).build();

                Request request = new Request.Builder()
                        .url("https://www.test.com/?q=TwoWayVerify")
                        .build();
                Message message = new Message();
                message.what = 1;
                try (Response response = mClient.newCall(request).execute()) {
                    Log.d("TestReq", response.peekBody(2048).string());
                    message.obj = "请求成功: " + response.peekBody(2048).string();
                    mHandler.sendMessage(message);
                } catch (IOException e) {
                    message.obj = e.getLocalizedMessage();
                    mHandler.sendMessage(message);
                    e.printStackTrace();
                }
            }
        }).start();
    }
});

network_security_config.xml 中添加配置:

<!--证书校验-->
<domain-config>
    <domain includeSubdomains="true">www.test.com</domain>
    <trust-anchors>
        <certificates src="@raw/server_cert"
            tools:ignore="NetworkSecurityConfig" />
    </trust-anchors>
</domain-config>

server_cert就是server_cert.cer。到这里完成了客户端PIN服务端证书,然后还要在客户端这边写把客户端证书给服务端验证的代码逻辑,即ClientSSLSocketFactory的实现:

public class ClientSSLSocketFactory  {
    private static final String KEY_STORE_PASSWORD = "clientpassword"; // 证书密码
    private static InputStream client_input;

    public static SSLSocketFactory getSocketFactory(Context context) {
        try {
            //客户端证书
            client_input = context.getResources().getAssets().open("client.p12");
            SSLContext sslContext = SSLContext.getInstance("TLS");
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(client_input, KEY_STORE_PASSWORD.toCharArray());
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, KEY_STORE_PASSWORD.toCharArray());
            sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                client_input.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

代码写完了,现在我们来缕一缕:

client app包含server-cert.cer 对服务端证书PIN,这个过程需要验证服务端证书的签发单位是否为合法CA Issuer,但是我们的证书是自签名的,所以就算是不使用代理还是不能通过SSL PINNING,大概率是报错:

Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

所以,一开始我们就需要用frida hook启动app,这样跳过app端的证书绑定,才可以继续后面的步骤。

接下来需要配置代理,给代理软件加客户端证书,这里我们使用BurpSuite,Fidller在这个场景不能用:

image-20210430223211340

选择client.p12证书文件,如果你使用我生成的证书文件密码是clientpassword。

此时确认以下条件:

  • [x] 服务端能通过PC浏览器访问
  • [x] APP 中 client.p12 证书装载成功
  • [x] APP 通过frida hook spawn模式启动
  • [x] BurpSuite 加载 client.p12 证书成功
  • [x] PC 代理软件未开启
  • [x] PC hosts 中将 www.test.com 域名定向到正确IP

确定没问题以后,点击 HTTPS 双向验证 按钮访问。

最终效果:

image-20210430232813386

APP 页面上显示:请求成功: 200

碎碎念:

可以把server-cert.cer 连带 server-key.key 导出一份可以装载的证书,这样就可以把这个证书加到系统证书列表中,应该可以通过SSL PINNING。

如果能把android 系统中hosts在/system/etc/hosts 是只读文件系统,我remount以后修改这个文件系统就会崩溃,随意没有尝试在android手机上直接将域名定向到server ip地址。

如果上述两点能正确实现,那就可以在局域网下,完成一个正经的HTTPS双向认证了。

0x7 WebView 忽略证书验证

WebView 正常应该做成一个跳转页的,但是我为了图省事,就做了个小框在最下面,只是为了看是否访问成功。

/*
* https协议
* WebView 不进行证书校验
*/
button_webview_ssl_without_ca.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        MyWebViewClient mWebViewClient = new MyWebViewClient();
        mWebViewClient.setCheckflag("trustAllCerts");
        mWebview.setWebViewClient(mWebViewClient);
        mWebview.loadUrl("https://www.baidu.com/?q=WebView_without_CAcheck");
    }
});

上述代码片段中MyWebViewClient的实现如下:

private class MyWebViewClient extends WebViewClient {
    private String checkflag="checkCerts"; // 是否忽略证书校验

    public void setCheckflag(String checkflag) {
        this.checkflag = checkflag;
    }

    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        if("trustAllCerts".equals(checkflag)){
            handler.proceed();
        }else {
            handler.cancel();
            Toast.makeText(MainActivity.this, "证书异常,停止访问", Toast.LENGTH_SHORT).show();
        }
    }
}

这样就做到了在遇到SSL错误时继续访问而中断访问,也不发出任何提醒。这样只需要配置好代理,甚至不需要安装代理证书就可以抓包了。

0x8 WebView 系统证书验证

很遗憾,WebView没有自定义SSL PINNING的实现方法,只能通过network_security_config.xml配置证书绑定。但这确实是一个很好的现象,在我看来把安全相关的业务逻辑交给普通开发者简直就是白给,这么做反而是提升安全性的最佳实践。

/*
 * https协议
 * WebView 使用系统证书校验
 */
button_webview_ssl_with_system_ca.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        MyWebViewClient mWebViewClient = new MyWebViewClient();
        mWebViewClient.setCheckflag("checkCerts");
        mWebview.setWebViewClient(mWebViewClient);
        mWebview.loadUrl("https://www.baidu.com/?q=WebView_with_SystemCAcheck");
    }
});

baidu.com未在network_security_config.xml中配置证书绑定,所以访问 https://www.baidu.com/ 时只使用系统证书进行校验。

所以只需要将代理软件的证书加入系统证书列表就可以抓包解密明文了。

0x9 WebView SSL PINNING

/*
* https协议 SSL PINNING WebView
* 通过network_security_config.xml中定义的证书和密钥进行绑定
*/
button_webview_ssl_pinning.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        MyWebViewClient mWebViewClient = new MyWebViewClient();
        mWebViewClient.setCheckflag("checkCerts");
        mWebview.setWebViewClient(mWebViewClient);
        mWebview.loadUrl("https://www.sogou.com/web?query=WebView_SSLPinningXML"); // 证书文件校验
        // mWebview.loadUrl("https://www.zhihu.com/"); // 证书公钥校验
    }
});

sogou.com和zhihu.com都通过network_security_config.xml做了证书绑定,所以直接访问的过程中就会触发SSL PINNING的验证。

WebView SSL PINNING还不能通过0x-1附录的hook 代码unpinning,如果你有能unpinning的方法请务必告诉我。

0x-1 附录

Frida Hook Code:

usage: frida -U -f com.example.httpstest -l ./frida_multiple_unpinning.js --no-pause

/*  Android ssl certificate pinning bypass script for various methods
	by Maurizio Siddu modify by Ch3nYe

	Run with:
	frida -U -f [APP_ID] -l frida_multiple_unpinning.js --no-pause
*/
setTimeout(function() {
	Java.perform(function () {
		console.log('');
		console.log('======');
		console.log('[#] Android Bypass for various Certificate Pinning methods [#]');
		console.log('======');


		var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
		var SSLContext = Java.use('javax.net.ssl.SSLContext');


		// TrustManager (Android < 7) //
		////////////////////////////////
		var TrustManager = Java.registerClass({
			// Implement a custom TrustManager
			name: 'dev.asd.test.TrustManager',
			implements: [X509TrustManager],
			methods: {
				checkClientTrusted: function (chain, authType) {},
				checkServerTrusted: function (chain, authType) {},
				getAcceptedIssuers: function () {return []; }
			}
		});
		// Prepare the TrustManager array to pass to SSLContext.init()
		var TrustManagers = [TrustManager.$new()];
		// 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');
		try {
			// Override the init method, specifying the custom TrustManager
			SSLContext_init.implementation = function(keyManager, trustManager, secureRandom) {
				console.log('[+] Bypassing Trustmanager (Android < 7) request');
				SSLContext_init.call(this, keyManager, TrustManagers, secureRandom);
			};
		} catch (err) {
			console.log('[-] TrustManager (Android < 7) pinner not found');
			//console.log(err);
		}



		// OkHTTPv3 (quadruple bypass) //
		/////////////////////////////////
		try {
			// Bypass OkHTTPv3 {1}
			var okhttp3_Activity_1 = Java.use('okhttp3.CertificatePinner');
			okhttp3_Activity_1.check.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
				console.log('[+] Bypassing OkHTTPv3 {1}: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] OkHTTPv3 {1} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass OkHTTPv3 {2}
			// This method of CertificatePinner.check could be found in some old Android app
			var okhttp3_Activity_2 = Java.use('okhttp3.CertificatePinner');
			okhttp3_Activity_2.check.overload('java.lang.String', 'java.security.cert.Certificate').implementation = function (a, b) {
				console.log('[+] Bypassing OkHTTPv3 {2}: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] OkHTTPv3 {2} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass OkHTTPv3 {3}
			var okhttp3_Activity_3 = Java.use('okhttp3.CertificatePinner');
			okhttp3_Activity_3.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function (a, b) {
				console.log('[+] Bypassing OkHTTPv3 {3}: ' + a);
				return true;
			};
		} catch(err) {
			console.log('[-] OkHTTPv3 {3} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass OkHTTPv3 {4}
			var okhttp3_Activity_4 = Java.use('okhttp3.CertificatePinner');
			okhttp3_Activity_4['check$okhttp'].implementation = function (a, b) {
				console.log('[+] Bypassing OkHTTPv3 {4}: ' + a);
			};
		} catch(err) {
			console.log('[-] OkHTTPv3 {4} pinner not found');
			//console.log(err);
		}



		// Trustkit (triple bypass) //
		//////////////////////////////
		try {
			// Bypass Trustkit {1}
			var trustkit_Activity_1 = Java.use('com.datatheorem.android.trustkit.pinning.OkHostnameVerifier');
			trustkit_Activity_1.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (a, b) {
				console.log('[+] Bypassing Trustkit {1}: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] Trustkit {1} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass Trustkit {2}
			var trustkit_Activity_2 = Java.use('com.datatheorem.android.trustkit.pinning.OkHostnameVerifier');
			trustkit_Activity_2.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (a, b) {
				console.log('[+] Bypassing Trustkit {2}: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] Trustkit {2} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass Trustkit {3}
			var trustkit_PinningTrustManager = Java.use('com.datatheorem.android.trustkit.pinning.PinningTrustManager');
			trustkit_PinningTrustManager.checkServerTrusted.implementation = function () {
				console.log('[+] Bypassing Trustkit {3}');
			};
		} catch (err) {
			console.log('[-] Trustkit {3} pinner not found');
			//console.log(err);
		}




		// TrustManagerImpl (Android > 7) //
		////////////////////////////////////
		try {
			var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
			TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
				console.log('[+] Bypassing TrustManagerImpl (Android > 7): ' + host);
				return untrustedChain;
			};
		} catch (err) {
			console.log('[-] TrustManagerImpl (Android > 7) pinner not found');
			//console.log(err);
		}



		// Appcelerator Titanium //
		///////////////////////////
		try {
			var appcelerator_PinningTrustManager = Java.use('appcelerator.https.PinningTrustManager');
			appcelerator_PinningTrustManager.checkServerTrusted.implementation = function () {
				console.log('[+] Bypassing Appcelerator PinningTrustManager');
			};
		} catch (err) {
			console.log('[-] Appcelerator PinningTrustManager pinner not found');
			//console.log(err);
		}



		// OpenSSLSocketImpl Conscrypt //
		/////////////////////////////////
		try {
			var OpenSSLSocketImpl = Java.use('com.android.org.conscrypt.OpenSSLSocketImpl');
			OpenSSLSocketImpl.verifyCertificateChain.implementation = function (certRefs, JavaObject, authMethod) {
				console.log('[+] Bypassing OpenSSLSocketImpl Conscrypt');
			};
		} catch (err) {
			console.log('[-] OpenSSLSocketImpl Conscrypt pinner not found');
			//console.log(err);
		}



		// OpenSSLEngineSocketImpl Conscrypt //
		///////////////////////////////////////
		try {
			var OpenSSLEngineSocketImpl_Activity = Java.use('com.android.org.conscrypt.OpenSSLEngineSocketImpl');
			OpenSSLSocketImpl_Activity.verifyCertificateChain.overload('[Ljava.lang.Long;', 'java.lang.String').implementation = function (a, b) {
				console.log('[+] Bypassing OpenSSLEngineSocketImpl Conscrypt: ' + b);
			};
		} catch (err) {
			console.log('[-] OpenSSLEngineSocketImpl Conscrypt pinner not found');
			//console.log(err);
		}



		// OpenSSLSocketImpl Apache Harmony //
		//////////////////////////////////////
		try {
			var OpenSSLSocketImpl_Harmony = Java.use('org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl');
			OpenSSLSocketImpl_Harmony.verifyCertificateChain.implementation = function (asn1DerEncodedCertificateChain, authMethod) {
				console.log('[+] Bypassing OpenSSLSocketImpl Apache Harmony');
			};
		} catch (err) {
			console.log('[-] OpenSSLSocketImpl Apache Harmony pinner not found');
			//console.log(err);
		}



		// PhoneGap sslCertificateChecker (https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin) //
		//////////////////////////////////////////////////////////////////////////////////////////////////////////////
		try {
			var phonegap_Activity = Java.use('nl.xservices.plugins.sslCertificateChecker');
			phonegap_Activity.execute.overload('java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext').implementation = function (a, b, c) {
				console.log('[+] Bypassing PhoneGap sslCertificateChecker: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] PhoneGap sslCertificateChecker pinner not found');
			//console.log(err);
		}



		// IBM MobileFirst pinTrustedCertificatePublicKey (double bypass) //
		////////////////////////////////////////////////////////////////////
		try {
			// Bypass IBM MobileFirst {1}
			var WLClient_Activity_1 = Java.use('com.worklight.wlclient.api.WLClient');
			WLClient_Activity_1.getInstance().pinTrustedCertificatePublicKey.overload('java.lang.String').implementation = function (cert) {
				console.log('[+] Bypassing IBM MobileFirst pinTrustedCertificatePublicKey {1}: ' + cert);
				return;
			};
			} catch (err) {
			console.log('[-] IBM MobileFirst pinTrustedCertificatePublicKey {1} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass IBM MobileFirst {2}
			var WLClient_Activity_2 = Java.use('com.worklight.wlclient.api.WLClient');
			WLClient_Activity_2.getInstance().pinTrustedCertificatePublicKey.overload('[Ljava.lang.String;').implementation = function (cert) {
				console.log('[+] Bypassing IBM MobileFirst pinTrustedCertificatePublicKey {2}: ' + cert);
				return;
			};
		} catch (err) {
			console.log('[-] IBM MobileFirst pinTrustedCertificatePublicKey {2} pinner not found');
			//console.log(err);
		}



		// IBM WorkLight (ancestor of MobileFirst) HostNameVerifierWithCertificatePinning (quadruple bypass) //
		///////////////////////////////////////////////////////////////////////////////////////////////////////
		try {
			// Bypass IBM WorkLight {1}
			var worklight_Activity_1 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
			worklight_Activity_1.verify.overload('java.lang.String', 'javax.net.ssl.SSLSocket').implementation = function (a, b) {
				console.log('[+] Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {1}: ' + a);
				return;
			};
		} catch (err) {
			console.log('[-] IBM WorkLight HostNameVerifierWithCertificatePinning {1} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass IBM WorkLight {2}
			var worklight_Activity_2 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
			worklight_Activity_2.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (a, b) {
				console.log('[+] Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {2}: ' + a);
				return;
			};
		} catch (err) {
			console.log('[-] IBM WorkLight HostNameVerifierWithCertificatePinning {2} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass IBM WorkLight {3}
			var worklight_Activity_3 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
			worklight_Activity_3.verify.overload('java.lang.String', '[Ljava.lang.String;', '[Ljava.lang.String;').implementation = function (a, b) {
				console.log('[+] Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {3}: ' + a);
				return;
			};
		} catch (err) {
			console.log('[-] IBM WorkLight HostNameVerifierWithCertificatePinning {3} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass IBM WorkLight {4}
			var worklight_Activity_4 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
			worklight_Activity_4.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (a, b) {
				console.log('[+] Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning {4}: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] IBM WorkLight HostNameVerifierWithCertificatePinning {4} pinner not found');
			//console.log(err);
		}



		// Conscrypt CertPinManager //
		//////////////////////////////
		try {
			var conscrypt_CertPinManager_Activity = Java.use('com.android.org.conscrypt.CertPinManager');
			conscrypt_CertPinManager_Activity.isChainValid.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
				console.log('[+] Bypassing Conscrypt CertPinManager: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] Conscrypt CertPinManager pinner not found');
			//console.log(err);
		}



		// CWAC-Netsecurity (unofficial back-port pinner for Android<4.2) CertPinManager //
		///////////////////////////////////////////////////////////////////////////////////
		try {
			var cwac_CertPinManager_Activity = Java.use('com.commonsware.cwac.netsecurity.conscrypt.CertPinManager');
			cwac_CertPinManager_Activity.isChainValid.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
				console.log('[+] Bypassing CWAC-Netsecurity CertPinManager: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] CWAC-Netsecurity CertPinManager pinner not found');
			//console.log(err);
		}



		// Worklight Androidgap WLCertificatePinningPlugin //
		/////////////////////////////////////////////////////
		try {
			var androidgap_WLCertificatePinningPlugin_Activity = Java.use('com.worklight.androidgap.plugin.WLCertificatePinningPlugin');
			androidgap_WLCertificatePinningPlugin_Activity.execute.overload('java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext').implementation = function (a, b, c) {
				console.log('[+] Bypassing Worklight Androidgap WLCertificatePinningPlugin: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] Worklight Androidgap WLCertificatePinningPlugin pinner not found');
			//console.log(err);
		}



		// Netty FingerprintTrustManagerFactory //
		//////////////////////////////////////////
		try {
			var netty_FingerprintTrustManagerFactory = Java.use('io.netty.handler.ssl.util.FingerprintTrustManagerFactory');
			//NOTE: sometimes this below implementation could be useful
			//var netty_FingerprintTrustManagerFactory = Java.use('org.jboss.netty.handler.ssl.util.FingerprintTrustManagerFactory');
			netty_FingerprintTrustManagerFactory.checkTrusted.implementation = function (type, chain) {
				console.log('[+] Bypassing Netty FingerprintTrustManagerFactory');
			};
		} catch (err) {
			console.log('[-] Netty FingerprintTrustManagerFactory pinner not found');
			//console.log(err);
		}



		// Squareup CertificatePinner [OkHTTP<v3] (double bypass) //
		////////////////////////////////////////////////////////////
		try {
			// Bypass Squareup CertificatePinner  {1}
			var Squareup_CertificatePinner_Activity_1 = Java.use('com.squareup.okhttp.CertificatePinner');
			Squareup_CertificatePinner_Activity_1.check.overload('java.lang.String', 'java.security.cert.Certificate').implementation = function (a, b) {
				console.log('[+] Bypassing Squareup CertificatePinner {1}: ' + a);
				return;
			};
		} catch (err) {
			console.log('[-] Squareup CertificatePinner {1} pinner not found');
			//console.log(err);
		}
		try {
			// Bypass Squareup CertificatePinner {2}
			var Squareup_CertificatePinner_Activity_2 = Java.use('com.squareup.okhttp.CertificatePinner');
			Squareup_CertificatePinner_Activity_2.check.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
				console.log('[+] Bypassing Squareup CertificatePinner {2}: ' + a);
				return;
			};
		} catch (err) {
			console.log('[-] Squareup CertificatePinner {2} pinner not found');
			//console.log(err);
		}



		// Squareup OkHostnameVerifier [OkHTTP v3] (double bypass) //
		/////////////////////////////////////////////////////////////
		try {
			// Bypass Squareup OkHostnameVerifier {1}
			var Squareup_OkHostnameVerifier_Activity_1 = Java.use('com.squareup.okhttp.internal.tls.OkHostnameVerifier');
			Squareup_OkHostnameVerifier_Activity_1.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (a, b) {
				console.log('[+] Bypassing Squareup OkHostnameVerifier {1}: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] Squareup OkHostnameVerifier pinner not found');
			//console.log(err);
		}
		try {
			// Bypass Squareup OkHostnameVerifier {2}
			var Squareup_OkHostnameVerifier_Activity_2 = Java.use('com.squareup.okhttp.internal.tls.OkHostnameVerifier');
			Squareup_OkHostnameVerifier_Activity_2.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (a, b) {
				console.log('[+] Bypassing Squareup OkHostnameVerifier {2}: ' + a);
				return true;
			};
		} catch (err) {
			console.log('[-] Squareup OkHostnameVerifier pinner not found');
			//console.log(err);
		}



		// Android WebViewClient (double bypass) //
		///////////////////////////////////////////
		try {
			// Bypass WebViewClient {1} (deprecated from Android 6)
			var AndroidWebViewClient_Activity_1 = Java.use('android.webkit.WebViewClient');
			AndroidWebViewClient_Activity_1.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.SslErrorHandler', 'android.net.http.SslError').implementation = function (obj1, obj2, obj3) {
				console.log('[+] Bypassing Android WebViewClient {1}');
			};
		} catch (err) {
			console.log('[-] Android WebViewClient {1} pinner not found');
			//console.log(err)
		}
		try {
			// Bypass WebViewClient {2}
			var AndroidWebViewClient_Activity_2 = Java.use('android.webkit.WebViewClient');
			AndroidWebViewClient_Activity_2.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest', 'android.webkit.WebResourceError').implementation = function (obj1, obj2, obj3) {
				console.log('[+] Bypassing Android WebViewClient {2}');
			};
		} catch (err) {
			console.log('[-] Android WebViewClient {2} pinner not found');
			//console.log(err)
		}



		// Apache Cordova WebViewClient //
		//////////////////////////////////
		try {
			var CordovaWebViewClient_Activity = Java.use('org.apache.cordova.CordovaWebViewClient');
			CordovaWebViewClient_Activity.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.SslErrorHandler', 'android.net.http.SslError').implementation = function (obj1, obj2, obj3) {
				console.log('[+] Bypassing Apache Cordova WebViewClient');
				obj3.proceed();
			};
		} catch (err) {
			console.log('[-] Apache Cordova WebViewClient pinner not found');
			//console.log(err);
		}



		// Boye AbstractVerifier //
		///////////////////////////
		try {
			var boye_AbstractVerifier = Java.use('ch.boye.httpclientandroidlib.conn.ssl.AbstractVerifier');
			boye_AbstractVerifier.verify.implementation = function (host, ssl) {
				console.log('[+] Bypassing Boye AbstractVerifier: ' + host);
			};
		} catch (err) {
			console.log('[-] Boye AbstractVerifier pinner not found');
			//console.log(err);
		}


	});

}, 0);

参考文章:

Android HTTPS防抓包策略与对抗方法总结

Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法 - sheldon_blogs - 博客园 (cnblogs.com)

(19条消息) Android+Nginx一步步配置https单向/双向认证请求_Dream Fly的专栏-CSDN博客