【Android】HTTPS“中间人攻击”分析与防御
一、HTTPS的作用
1、 认证服务端与服务器,确保相互之间的信任关系。
2、 加密数据,防止泄露。
3、 验证数据完整性,防止篡改。
二、 HTTPS工作原理
我们这里所关注的Https工作原理,主要指的是SSL协议的握手流程;
HTTP +加密+认证+完整性保护=HTTPS;
HTTPS是身披SSL外壳的HTTP;HTTPS并非是应用层的一种新协议,而是HTTP通信接口部分用SSL协议代替,通信时,HTTP先和SSL通信,再由SSL与TCP通信。
HTTPS采用对称加密和非对称加密两种加密机制,首先使用非对称加密方式(数字证书)完成对称加***的交换,然后再使用对称**加密报文并发送。
非对称加密:一个私钥一个公钥,公钥可以随意发布,公钥加密的密文,可以使用私钥解密;公钥本身几乎无法解密密文。所以安全性高,但是加解密效率低。
对称加密:只有一把**,可以加密和解密,在传输时被截获的话,就没有安全性可言了;所以安全性低,但是加解密效率高。
下面是HTTPS通信流程:
三、 中间人攻击
从HTTPS的工作原理,可以看出HTTPS极好的安全性;但是这种安全性不是绝对的,“中间人攻击”可以很轻易的攻破HTTPS的安全防护,如在用户设备上安装证书(将中间人服务器的证书放到设备的信任列表中)进行中间人攻击;
下面我们来分析“中间人攻击”一般都做了什么,如下图所示,“中间人攻击”的攻击者在网络中拦截客户端的请求,窃取篡改后发送给服务端,并将服务端返回的数据拦截窃取篡改后返回给客户端,即对客户端伪装成服务端,对服务端伪装成客户端。
实现“中间人攻击”的关键点是,攻击者如何绕过客户端的数字证书验证;分析Charles和其他抓包工具的抓包情况,可以分析一种实现方式,在设备上安装并信任攻击者证书,攻击者拦截到客户端的请求后,将自己的数字证书发送给客户端,客户端收到数字证书后,会用内置的数字证书认证机构的公开**和设备安装并信任的证书列表验证收到的数字证书,这里是关键,如果攻击者发送过来的证书已安装在设备信任列表中,那么客户端验证就会通过,客户端会认为对方就是正确的服务端,于是发送对称**给服务端,这样攻击者就成功实现了“中间人攻击”。
四、 防御“中间人攻击”
通过上面对“中间人攻击”的分析,要防御“中间人攻击”,重点是不能让攻击者骗过客户端的数字证书验证,常见措施:
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());
针对一般应用,使用单向验证即可满足安全需要,当然对于金融等对安全要求特别高的应用,一般会使用双向验证。
上一篇: SSL Tomcat 双向认证
下一篇: 椰青怎么打开?看这里让你轻轻松松打开