欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Android】HTTPS“中间人攻击”分析与防御

程序员文章站 2022-05-01 09:47:59
...

一、HTTPS的作用

1、 认证服务端与服务器,确保相互之间的信任关系。

2、 加密数据,防止泄露。

3、 验证数据完整性,防止篡改。

二、 HTTPS工作原理

我们这里所关注的Https工作原理,主要指的是SSL协议的握手流程;

HTTP +加密+认证+完整性保护=HTTPS;

HTTPS是身披SSL外壳的HTTP;HTTPS并非是应用层的一种新协议,而是HTTP通信接口部分用SSL协议代替,通信时,HTTP先和SSL通信,再由SSL与TCP通信。

HTTPS采用对称加密和非对称加密两种加密机制,首先使用非对称加密方式(数字证书)完成对称加***的交换,然后再使用对称**加密报文并发送。

非对称加密:一个私钥一个公钥,公钥可以随意发布,公钥加密的密文,可以使用私钥解密;公钥本身几乎无法解密密文。所以安全性高,但是加解密效率低。

对称加密:只有一把**,可以加密和解密,在传输时被截获的话,就没有安全性可言了;所以安全性低,但是加解密效率高。

下面是HTTPS通信流程:

【Android】HTTPS“中间人攻击”分析与防御

三、 中间人攻击

从HTTPS的工作原理,可以看出HTTPS极好的安全性;但是这种安全性不是绝对的,“中间人攻击”可以很轻易的攻破HTTPS的安全防护,如在用户设备上安装证书(将中间人服务器的证书放到设备的信任列表中)进行中间人攻击;

下面我们来分析“中间人攻击”一般都做了什么,如下图所示,“中间人攻击”的攻击者在网络中拦截客户端的请求,窃取篡改后发送给服务端,并将服务端返回的数据拦截窃取篡改后返回给客户端,即对客户端伪装成服务端,对服务端伪装成客户端。

实现“中间人攻击”的关键点是,攻击者如何绕过客户端的数字证书验证;分析Charles和其他抓包工具的抓包情况,可以分析一种实现方式,在设备上安装并信任攻击者证书,攻击者拦截到客户端的请求后,将自己的数字证书发送给客户端,客户端收到数字证书后,会用内置的数字证书认证机构的公开**和设备安装并信任的证书列表验证收到的数字证书,这里是关键,如果攻击者发送过来的证书已安装在设备信任列表中,那么客户端验证就会通过,客户端会认为对方就是正确的服务端,于是发送对称**给服务端,这样攻击者就成功实现了“中间人攻击”。

【Android】HTTPS“中间人攻击”分析与防御

四、 防御“中间人攻击”

通过上面对“中间人攻击”的分析,要防御“中间人攻击”,重点是不能让攻击者骗过客户端的数字证书验证,常见措施:

1、单向验证

单向验证也是使用自签名证书常用的证书验证方式,即在APP中预埋公钥证书A_public(对应服务端使用私钥证书A_privite),并直接用预埋的证书来生成TrustManger进行证书验证。在进行验证数字证书时,使用内置的数字证书对其进行验证,这样就算设备中安装并信任了攻击者的数字证书,也无法通过验证,进而确保只有真正的服务端返回的数字证书才能验证通过;

Android下实现代码:

// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));
Certificate ca;
try {
    ca = cf.generateCertificate(caInput);
    System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} finally {
    caInput.close();
}

// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);

// Tell the URLConnection to use a SocketFactory from our SSLContext
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =
    (HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

另外我们也可以使用预埋证书自定义TrustManager,但是切记在重写TrustManager的checkServerTrusted()方法时真正进行证书验证(网上很多资料都是在此处什么也没做,即信任所有证书,那就失去了HTTPS的意义),其中要验证那些属性可以根据实际情况自选。

再就是,我们还可实现自定义HostnameVerifer进行服务器证书域名验证,同样重写时要进行真正的验证(网上很多资料都是在此处也是什么没做)。

2、双向验证

双向验证顾名思义,在单向验证的基础上,再增加一对证书B_private与B_public,且APP持有私钥证书B_private,服务端持有公钥证书B_public,并且服务端使用公钥证书B_public对客户端进行证书验证。

Android下的实现代码:

public void setCertificates(InputStream... certificates) {
   try {
      CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
      KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
      keyStore.load(null);
      int index = 0;
      for (InputStream certificate : certificates) {
         String certificateAlias = Integer.toString(index++);
         keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));

         try {
            if (certificate != null)
               certificate.close();
         } catch (IOException e) {
         }
      }

      SSLContext sslContext = SSLContext.getInstance("TLS");
      TrustManagerFactory trustManagerFactory = TrustManagerFactory.
            getInstance(TrustManagerFactory.getDefaultAlgorithm());
      trustManagerFactory.init(keyStore);

  KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
      clientKeyStore.load(mContext.getAssets().open("私钥证书文件名"), "私钥证书密码".toCharArray());

      KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
      keyManagerFactory.init(clientKeyStore, "私钥证书密码".toCharArray());

      sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
      mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
   } catch (Exception e) {
      e.printStackTrace();
   }
}

核心的代码:

1、使用私钥证书文件初始化KeyStore

KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
clientKeyStore.load(mContext.getAssets().open("私钥证书文件名"), "私钥证书密码".toCharArray());

2、使用1的KeyStore初始化KeyManagerFactory

KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "私钥证书密码".toCharArray());

3、通过2的KeyManagerFactory得到KeyManager数组,并使用其初始化SSLContext。

sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());

针对一般应用,使用单向验证即可满足安全需要,当然对于金融等对安全要求特别高的应用,一般会使用双向验证。

相关标签: 笔记