Android SSL通信安全
一、漏洞介绍
1、描述
中间人攻击时指攻击者与通信的两端建立连接,使得通信双方以为自己在使用安全加密连接进行通信,其实,攻击者已经拦截获取了整个会话的内容,同时,攻击者还可以修改插入新的内容。
Android HTTPS中间人攻击漏洞源于:1没有对SSL证书进行校验;2没有对域名进行校验;3. 证书颁发机构(Certification Authority)被攻击导致私钥泄露等。攻击者可通过中间人攻击,盗取账户密码明文、聊天内容、通讯地址、电话号码以及信用卡支付信息等敏感信息,甚至通过中间人劫持将原有信息替换成恶意链接或恶意代码程序,以达到远程控制、恶意扣费等攻击意图。
2、影响范围
所有的Android系统。
3、漏洞原理
由于客户端没有校验服务端的证书,因此攻击者就能与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。
4、漏洞攻击位置
X509TrustManager、HostnameVerifer、setHostnameVerifier(X509HostnameVeriier hostnameVerifier)
5、漏洞促发条件
l 自定义的X509TrustManager不校验证书;
l 实现的自定义HostnameVerifier不校验域名接受任意域名;
l 使用setHostnameVerifier (ALLOW_ALL_HOSTNAME_VERIFIER);
l 忽略webview证书继续加载
二、样例解析
1、自定义X509TrustManager没有校验证书

2、实现的自定义HostnameVerifier不校验域名接受任意域名

3、使用setHostnameVerifier (ALLOW_ALL_HOSTNAME_VERIFIER)

4、忽略webview证书继续加载

三、漏洞利用
中间人攻击
四、修复建议
1. 建议使用setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER)
2. 对浏览器app,对SSL证书进行强校验(签名CA是否合法、证书是否是自签名、主机域名是否匹配、证书是否过期等),参考谷歌官方关于SSL的安全建议[4]
3.对于非浏览器的app来说,可以申请CA证书,或者在客户端中添加证书,参考[5]
4.调用WebViewClient类的onReceivedSslError方法,不应该调用proceed()方法忽略证书错误,应该采用默认的处理方法handler.cancel(),停止加载问题页面[6]、[7]
4.一个SSLSocketFactory的例子:
java public class SecureSocketFactory extends SSLSocketFactory {
private static final String LOG_TAG = "SecureSocketFactory";
private final SSLContext sslCtx;
private final X509Certificate[] acceptedIssuers;
/**
* Instantiate a new secured factory pertaining to the passed store. Be sure to initialize the
* store with the password using {@link java.security.KeyStore#load(java.io.InputStream,
* char[])} method.
*
* @param store The key store holding the certificate details
* @param alias The alias of the certificate to use
*/
public SecureSocketFactory(KeyStore store, String alias)
throws
CertificateException,
NoSuchAlgorithmException,
KeyManagementException,
KeyStoreException,
UnrecoverableKeyException {
super(store);
// Loading the CA certificate from store.
final Certificate rootca = store.getCertificate(alias);
// Turn it to X509 format.
InputStream is = new ByteArrayInputStream(rootca.getEncoded());
X509Certificate x509ca = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
AsyncHttpClient.silentCloseInputStream(is);
if (null == x509ca) {
throw new CertificateException("Embedded SSL certificate has expired.");
}
// Check the CA's validity.
x509ca.checkValidity();
// Accepted CA is only the one installed in the store.
acceptedIssuers = new X509Certificate[]{x509ca};
sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(
null,
new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
Exception error = null;
if (null == chain || 0 == chain.length) {
error = new CertificateException("Certificate chain is invalid.");
} else if (null == authType || 0 == authType.length()) {
error = new CertificateException("Authentication type is invalid.");
} else {
Log.i(LOG_TAG, "Chain includes " + chain.length + " certificates.");
try {
for (X509Certificate cert : chain) {
Log.i(LOG_TAG, "Server Certificate Details:");
Log.i(LOG_TAG, "---------------------------");
Log.i(LOG_TAG, "IssuerDN: " + cert.getIssuerDN().toString());
Log.i(LOG_TAG, "SubjectDN: " + cert.getSubjectDN().toString());
Log.i(LOG_TAG, "Serial Number: " + cert.getSerialNumber());
Log.i(LOG_TAG, "Version: " + cert.getVersion());
Log.i(LOG_TAG, "Not before: " + cert.getNotBefore().toString());
Log.i(LOG_TAG, "Not after: " + cert.getNotAfter().toString());
Log.i(LOG_TAG, "---------------------------");
// Make sure that it hasn't expired.
cert.checkValidity();
// Verify the certificate's public key chain.
cert.verify(rootca.getPublicKey());
}
} catch (InvalidKeyException e) {
error = e;
} catch (NoSuchAlgorithmException e) {
error = e;
} catch (NoSuchProviderException e) {
error = e;
} catch (SignatureException e) {
error = e;
}
}
if (null != error) {
Log.e(LOG_TAG, "Certificate error", error);
throw new CertificateException(error);
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return acceptedIssuers;
}
}
},
null
);
setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
throws IOException {
injectHostname(socket, host);
Socket sslSocket = sslCtx.getSocketFactory().createSocket(socket, host, port, autoClose);
// throw an exception if the hostname does not match the certificate
getHostnameVerifier().verify(host, (SSLSocket) sslSocket);
return sslSocket;
}
@Override
public Socket createSocket() throws IOException {
return sslCtx.getSocketFactory().createSocket();
}
/**
* Pre-ICS Android had a bug resolving HTTPS addresses. This workaround fixes that bug.
*
* @param socket The socket to alter
* @param host Hostname to connect to
* @see <a href="https://code.google.com/p/android/issues/detail?id=13117#c14">https://code.google.com/p/android/issues/detail?id=13117#c14</a>
*/
private void injectHostname(Socket socket, String host) {
try {
if (Integer.valueOf(Build.VERSION.SDK) >= 4) {
Field field = InetAddress.class.getDeclaredField("hostName");
field.setAccessible(true);
field.set(socket.getInetAddress(), host);
}
} catch (Exception ignored) {
}
}
}
五、参考资料
[1] http://drops.wooyun.org/tips/3296
[2] http://jaq.alibaba.com/blog.htm?id=60
[3] http://www.wooyun.org/bugs/wooyun-2014-079358
[4] https://developer.android.com/training/articles/security-ssl.html
[5] http://pingguohe.net/2016/02/26/Android-App-secure-ssl.html
[6] http://wolfeye.baidu.com/blog/webview-ignore-ssl-error/
[7] http://developer.android.com/reference/android/webkit/SslErrorHandler.html

浙公网安备 33010602011771号