[安卓网络安全]安卓SSL中间人攻击防范

前言

目前,我们在开发App的时候对Https的校验方面都比较忽略,当我们使用一些抓包工具测试的时候,即使手机不安装抓包工具的证书,也可以抓取我们Https进行劫持看见我们的明文内容会泄露用户的隐私数据,下面我就写下我的防护措施。

概念

中间人攻击原理

针对SSL的中间人攻击方式主要有两类,分别是SSL劫持攻击和SSL剥离攻击

SSL劫持攻击

SSL劫持攻击即SSL证书欺骗攻击,攻击者为了获得HTTPS传输的明文数据,需要先将自己接入到客户端和目标网站之间;在传输过程中伪造服务器的证书,将服务器的公钥替换成自己的公钥,这样,中间人就可以得到明文传输带Key1、Key2和Pre-Master-Key,从而窃取客户端和服务端的通信数据;

但是对于客户端来说,如果中间人伪造了证书,在校验证书过程中会提示证书错误,由用户选择继续操作还是返回,由于大多数用户的安全意识不强,会选择继续操作,此时,中间人就可以获取浏览器和服务器之间的通信数据

SSL剥离攻击

这种攻击方式也需要将攻击者设置为中间人,之后见HTTPS范文替换为HTTP返回给浏览器,而中间人和服务器之间仍然保持HTTPS服务器。由于HTTP是明文传输的,所以中间人可以获取客户端和服务器传输数据。

[安卓网络安全]安卓SSL中间人攻击防范_第1张图片

上面的内容来自《Https协议简析及中间人攻击原理》如果还需要对Https的知识理解和补充可以自行Google,这里就不多做解释了。

抓取不校验的App信息

通常我们看似做了Https,但是我们没有做一些安全措施的话,我们的数据就会暴露,下面我们来做一些测试。

安装抓包工具Charles

Charles是一个很好用的抓包工具,有很多强大的功能,不过是付费的,有30天的试用期。
这里有一个很简单的教程看了配置下就可以了《十分钟学会Charles抓包(iOS的http/https请求)
》,虽然是文章是关于IOS但是Android的配置方式是一样的。

抓取信息

[安卓网络安全]安卓SSL中间人攻击防范_第2张图片
我屏蔽了一些敏感信息,可以看见我们成功的抓取了Https链接里面的明文信息,并且我没有安装抓包工具的证书,下面我们来进行分析。

为什么用了Https还能抓取明文信息?

我来展示下我在App中编写的代码:

OkHttpClient okHttpClient = new OkHttpClient();
        try {
            final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                @Override
                public void checkClientTrusted(
                        java.security.cert.X509Certificate[] chain,
                        String authType) throws CertificateException {
                }

                @Override
                public void checkServerTrusted(
                        java.security.cert.X509Certificate[] chain,
                        String authType) throws CertificateException {
                }

                @Override
                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            }};
            final SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCerts,
                    new java.security.SecureRandom());
            // Create an ssl socket factory with our all-trusting manager
            final SSLSocketFactory sslSocketFactory = sslContext
                    .getSocketFactory();

            okHttpClient.setSslSocketFactory(sslSocketFactory);
            okHttpClient.setHostnameVerifier(new HostnameVerifier() {

                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;

                }
            });

        } catch (Exception e) {
        }

在代码中造成ssl中间人攻击有三个可能性:

  • 自定义的X509TrustManager不校验证书;
  • 自定义HostnameVerifier不校验域名接受任意域名;
  • 使用setHostnameVerifier (ALLOW_ALL_HOSTNAME_VERIFIER)允许所有域名。
    以上三点这样应用容易遭受中间人攻击,存在用户敏感数据被盗取的风险,其实网上有很多这样的代码,教你简单的配置一个Https链接,是非常不可取的,我们来看看通过 HTTPS 和 SSL 确保安全怎么说:
    [安卓网络安全]安卓SSL中间人攻击防范_第3张图片
    这里官网已经说明,当我们编写一个没用的TrustManager的时候就不会对链接进行验证了。那么如何编写一个安全的呢。

PS:这里如果理解有问题的话,需要看一些HTTPS的相关知识,这里我补一张图。方便阅读便于记忆
SSL/TLS层负责客户端和服务器之间的加解密算法协商、密钥交换、通信连接的建立,安全连接的建立过程如下所示:
[安卓网络安全]安卓SSL中间人攻击防范_第4张图片

两种签名证书

目前的Https认证中,分为两种证书:

  • CA机构颁发的证书
    HTTPS网站所用的证书可向可信CA机构申请,不过这一类基本上都是商业机构,申请证书需要缴费,一般是按年缴费,费用因为CA机构的不同而不同。

  • 自签署服务证书
    如果只是APP与后台服务器进行HTTPS通信,可以使用openssl工具生成自签发的数字证书,可以节约费用,不过得妥善保护好证书私钥,不能泄露或者丢失。HTTPS通信所用的数字证书格式为X.509。

自签名证书认证

我们先来看一段代码:

        OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
        try {
            client = builder.hostnameVerifier(RELEASE)
                            .sslSocketFactory(getSslSocketFactory1())
                            .build();

        } catch (Exception e) {
        }

ps:代码这里我使用了OKhttp,在这里就默认大家会使用了。网上教程很多,可以自行google。

验证证书有两个重要的步骤:

  • 验证证书是否来自值得信任的来源
  • 主机名验证

第一步,是通过我们的数字证书生成一个新的SSLSocketFactory替换掉默认的,也就是我们的自定义TrustManager,使系统信任我们的证书,和前面的抓包工具安装证书类似。
第二步, 是对我们证书的校验,当第一步成功以后,就会将我们Https的域名变为合法的,当我们访问自己的服务器的时候就不会出现SSLHandshakeException。

验证证书是否来自值得信任的来源

如果申请CA认证的话,需要我们对CA机构缴费,许多公司不愿意缴费,在这种情况下,由于您具有系统不信任的 CA,将发生 SSLHandshakeException。原因可能是您有一个来自 Android 还未信任的新 CA 的证书,或您的应用在没有 CA 的较旧版本上运行。

在这里,您可以指示 HttpsURLConnection 信任特定的 CA 集。此过程可能有点复杂,下面的示例展示了这个过程,从 InputStream 获取一个特定的 CA,用该 CA 创建 KeyStore,然后用后者创建和初始化 TrustManager。TrustManager 是系统用于从服务器验证证书的工具,可以使用一个或多个 CA 从 KeyStore 创建,而创建的 TrustManager 将仅信任这些 CA。

如果是新的 TrustManager,此示例将初始化一个新的 SSLContext,后者可以提供一个 SSLSocketFactory,您可以通过 HttpsURLConnection 用它来替换默认的 SSLSocketFactory。这样一来,连接将使用您的 CA 验证证书。

  • 方法1
public static SSLSocketFactory getSslSocketFactory1() throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException {
        //以X.509格式获取证书
        CertificateFactory cf = CertificateFactory.getInstance(X_509);
        InputStream caInput = null;
        try {
             caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));//一般是存放在Assert下
        } catch (IOException e) {
            e.printStackTrace();
        }
        Certificate ca;
        try {
            ca = cf.generateCertificate(caInput);
            System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
        } finally {
            caInput.close();
        }

        // 创建包含受信任CA的KeyStore
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);
        keyStore.setCertificateEntry(CA, ca);

        // 创建一个信任我们的KeyStore中的CA的TrustManager
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);

        // 创建一个使用我们的TrustManager的SSLContext
        SSLContext context = SSLContext.getInstance(TSL);
        context.init(null, tmf.getTrustManagers(), null);

        return context.getSocketFactory();
    }

上面的代码就是将自签署的服务证书放在Assert包下,然后通过带代码生成一个SSLSocketFactory替换默认的。上面的代码中我们还可以使用另外一种形式来进行验证。

  • 方法2
    public static SSLSocketFactory getSslSocketFactory2() throws NoSuchAlgorithmException, KeyManagementException {
        final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {

        private X509Certificate serverCert;
        private final String CRT_NAME = "martin.crt";

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {

        }

        @Override
        public void checkServerTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException{
            if (x509Certificates == null) {
                throw new IllegalArgumentException("check Server X509Certificates is null");
            }

            if (x509Certificates.length < 0) {
                throw new IllegalArgumentException("check Server X509Certificates is empty");
            }
            InputStream caInput = null;
            try {
                caInput = new BufferedInputStream(Utils.getApp()
                                                       .getAssets()
                                                       .open(CRT_NAME));
            } catch (IOException e) {
                e.printStackTrace();
            }
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            serverCert = (X509Certificate) cf.generateCertificate(caInput);


            for (X509Certificate cert : x509Certificates) {
                try {
                    cert.checkValidity();
                    cert.verify(serverCert.getPublicKey());//和App预埋证书做对比
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchProviderException e) {
                    e.printStackTrace();
                } catch (SignatureException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    };};
        final SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        return sslContext.getSocketFactory();
    }

上面的两种方法,选一种实现就行。

主机名验证

正如本文开头所述,验证 SSL 连接有两个关键环节。首先是验证证书是否来自值得信任的来源,这是前面部分重点讲述的内容。而此部分侧重于第二个环节:确保您正在通信的服务器提供正确的证书。

public static HostnameVerifier RELEASE = new HostnameVerifier() {

        @Override
        public boolean verify(String hostname, SSLSession session) {
            return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
        }
};

当我们加入了自签署证书以后,验证就会为true。

CA机构颁发证书验证

因为在Android手机刚出厂的时候,系统都会默认装一些CA机构的根证书以作为认证。所以不需要公钥进行自定义TrustManager,那怎么得知我们的链接是进行过CA机构认证的呢?
[安卓网络安全]安卓SSL中间人攻击防范_第5张图片
当我们再google输入百度的网址时候,会出现一个锁的图片,点击后出现下面的证书细节。
[安卓网络安全]安卓SSL中间人攻击防范_第6张图片
上面已经看见百度的证书是CA机构认证的,如果你们公司的也是的话,我们只需要进行主机域名认证就没问题了。

public static HostnameVerifier RELEASE = new HostnameVerifier() {

        @Override
        public boolean verify(String hostname, SSLSession session) {
            return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
        }
};

小结

今天讲解的内容还是比较简单,总结一下:

  1. 中间人攻击攻击分为
    – SSL剥离攻击
    – SSL劫持攻击

  2. 安装抓包工具Charles,对不进行证书校验的App进行明文抓包。

  3. 两种签名证书
    – 自签名服务证书校验的两个步骤: 自定义SSLContextFactory 2.进行主机名验证。
    – CA认证证书的校验方法。

参考文章

  • Android安全开发之安全使用HTTPS
  • HTTPS IP直连问题小结
  • Android中http转https时,有时使用本地证书,有时使用服务器自签名,两种方式有什么区别?
  • 通过 HTTPS 和 SSL 确保安全

你可能感兴趣的:(安全与逆向)