參考原文@ 我們不生產(chǎn)代碼, 只是Bug 的搬運(yùn)工
摘要:?漏洞描述 對(duì)于數(shù)字證書(shū)相關(guān)概念、Android 里 https 通信代碼就不再?gòu)?fù)述了,直接講問(wèn)題。缺少相應(yīng)的安全校驗(yàn)很容易導(dǎo)致中間人攻擊,而漏洞的形式主要有以下3種: 自定義X509TrustManager。
漏洞描述
對(duì)于數(shù)字證書(shū)相關(guān)概念、Android 里 https 通信代碼就不再?gòu)?fù)述了,直接講問(wèn)題。缺少相應(yīng)的安全校驗(yàn)很容易導(dǎo)致中間人攻擊,而漏洞的形式主要有以下3種:
自定義X509TrustManager。在使用HttpsURLConnection發(fā)起 HTTPS 請(qǐng)求的時(shí)候,提供了一個(gè)自定義的X509TrustManager,未實(shí)現(xiàn)安全校驗(yàn)邏輯,下面片段就是常見(jiàn)的容易犯錯(cuò)的代碼片段。如果不提供自定義的X509TrustManager,代碼運(yùn)行起來(lái)可能會(huì)報(bào)異常(原因下文解釋?zhuān)鯇W(xué)者就很容易在不明真相的情況下提供了一個(gè)自定義的X509TrustManager,卻忘記正確地實(shí)現(xiàn)相應(yīng)的方法。本文重點(diǎn)介紹這種場(chǎng)景的處理方式。
TrustManager tm=newX509TrustManager(){public void checkClientTrusted(X509Certificate[] chain, String authType)? ? ? ? ? ? throws CertificateException {//do nothing,接受任意客戶端證書(shū)}public void checkServerTrusted(X509Certificate[] chain, String authType)? ? ? ? ? ? throws CertificateException {//do nothing,接受任意服務(wù)端證書(shū)}publicX509Certificate[]getAcceptedIssuers(){returnnull;}};sslContext.init(null,newTrustManager[]{tm},null);
自定義了HostnameVerifier。在握手期間,如果 URL 的主機(jī)名和服務(wù)器的標(biāo)識(shí)主機(jī)名不匹配,則驗(yàn)證機(jī)制可以回調(diào)此接口的實(shí)現(xiàn)程序來(lái)確定是否應(yīng)該允許此連接。如果回調(diào)內(nèi)實(shí)現(xiàn)不恰當(dāng),默認(rèn)接受所有域名,則有安全風(fēng)險(xiǎn)。代碼示例。
HostnameVerifier hnv=newHostnameVerifier(){@Overridepublic boolean verify(String hostname, SSLSession session) {// Always return true,接受任意域名服務(wù)器returntrue;}};HttpsURLConnection.setDefaultHostnameVerifier(hnv);
信任所有主機(jī)名。
SSLSocketFactory sf=newMySSLSocketFactory(trustStore);sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
修復(fù)方案
分而治之,針對(duì)不同的漏洞點(diǎn)分別描述,這里就講的修復(fù)方案主要是針對(duì)非瀏覽器App,非瀏覽器 App 的服務(wù)端通信對(duì)象比較固定,一般都是自家服務(wù)器,可以做很多特定場(chǎng)景的定制化校驗(yàn)。如果是瀏覽器 App,校驗(yàn)策略就有更通用一些。
自定義X509TrustManager。前面說(shuō)到,當(dāng)發(fā)起 HTTPS 請(qǐng)求時(shí),可能拋起一個(gè)異常,以下面這段代碼為例(來(lái)自官方文檔):
try{URL url=newURL("https://certs.cac.washington.edu/CAtest/");URLConnection urlConnection=url.openConnection();InputStream in=urlConnection.getInputStream();copyInputStreamToOutputStream(in,System.out);}catch(MalformedURLExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}
private void copyInputStreamToOutputStream(InputStream in, PrintStream out) throws IOException {byte[]buffer=newbyte[1024];intc=0;while((c=in.read(buffer))!=-1){out.write(buffer,0,c);}}
它會(huì)拋出一個(gè)SSLHandshakeException的異常。
javax.net.ssl.SSLHandshakeException:java.security.cert.CertPathValidatorException:Trust anchorforcertification path not found.at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:322)at com.android.okhttp.Connection.upgradeToTls(Connection.java:201)at com.android.okhttp.Connection.connect(Connection.java:155)at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:276)at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:211)at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:382)at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:332)at com.android.okhttp.internal.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:199)at com.android.okhttp.internal.http.DelegatingHttpsURLConnection.getInputStream(DelegatingHttpsURLConnection.java:210)at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:25)at me.longerian.abcandroid.datetimepicker.TestDateTimePickerActivity$1.run(TestDateTimePickerActivity.java:236)Caused by:java.security.cert.CertificateException:java.security.cert.CertPathValidatorException:Trust anchorforcertification path not found.at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:318)at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:219)at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:114)at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:550)at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:318)...10moreCaused by:java.security.cert.CertPathValidatorException:Trust anchorforcertification path not found....16more
Android 手機(jī)有一套共享證書(shū)的機(jī)制,如果目標(biāo) URL 服務(wù)器下發(fā)的證書(shū)不在已信任的證書(shū)列表里,或者該證書(shū)是自簽名的,不是由權(quán)威機(jī)構(gòu)頒發(fā),那么會(huì)出異常。對(duì)于我們這種非瀏覽器 app 來(lái)說(shuō),如果提示用戶去下載安裝證書(shū),可能會(huì)顯得比較詭異。幸好還可以通過(guò)自定義的驗(yàn)證機(jī)制讓證書(shū)通過(guò)驗(yàn)證。驗(yàn)證的思路有兩種:
方案1
不論是權(quán)威機(jī)構(gòu)頒發(fā)的證書(shū)還是自簽名的,打包一份到 app 內(nèi)部,比如存放在 asset 里。通過(guò)這份內(nèi)置的證書(shū)初始化一個(gè)KeyStore,然后用這個(gè)KeyStore去引導(dǎo)生成的TrustManager來(lái)提供驗(yàn)證,具體代碼如下:
try{CertificateFactory cf=CertificateFactory.getInstance("X.509");// uwca.crt 打包在 asset 中,該證書(shū)可以從https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下載InputStream caInput=newBufferedInputStream(getAssets().open("uwca.crt"));Certificate ca;try{ca=cf.generateCertificate(caInput);Log.i("Longer","ca="+((X509Certificate)ca).getSubjectDN());Log.i("Longer","key="+((X509Certificate)ca).getPublicKey();}finally{caInput.close();}// Create a KeyStore containing our trusted CAsString 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 KeyStoreString tmfAlgorithm=TrustManagerFactory.getDefaultAlgorithm();TrustManagerFactory tmf=TrustManagerFactory.getInstance(tmfAlgorithm);tmf.init(keyStore);// Create an SSLContext that uses our TrustManagerSSLContext context=SSLContext.getInstance("TLSv1","AndroidOpenSSL");context.init(null,tmf.getTrustManagers(),null);URL url=newURL("https://certs.cac.washington.edu/CAtest/");HttpsURLConnection urlConnection=(HttpsURLConnection)url.openConnection();urlConnection.setSSLSocketFactory(context.getSocketFactory());InputStream in=urlConnection.getInputStream();copyInputStreamToOutputStream(in,System.out);}catch(CertificateExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}catch(NoSuchAlgorithmExceptione){e.printStackTrace();}catch(KeyStoreExceptione){e.printStackTrace();}catch(KeyManagementExceptione){e.printStackTrace();}catch(NoSuchProviderExceptione){e.printStackTrace();}
這樣就可以得到正確的輸出內(nèi)容:
<html><head><title>UW Services CA Test Page</title></head><body><h2>UW Services CA test page</h2><p><b>QUESTION</b>:Did you arrive here without any security alerts or warnings?</p><ul><p><li><b>YES</b>- This test page uses a certificate issued by theUW Services Certificate Authority. If you reached this pagewithout any alerts or warnings from your browser, youhave successfully installed the UW Services CA Certificateinto your browser.<p><li><b>NO</b>- If your browser warned you about the validity of thistest page's security certificate, or the certificateauthority is unrecognized, you may not have successfullyinstalled the UW Services CA Certificate.<p></ul><form action="https://www.washington.edu/computing/ca/" method=get><input type=submit value="Return to the Install Page"></form></body></html>
如果你用上述同樣的代碼訪問(wèn) https://www.taobao.com/ 或者 https://www.baidu.com/ ,則會(huì)拋出那個(gè)SSLHandshakeException異常,也就是說(shuō)對(duì)于特定證書(shū)生成的TrustManager,只能驗(yàn)證與特定服務(wù)器建立安全鏈接,這樣就提高了安全性。如之前提到的,對(duì)于非瀏覽器 app 來(lái)說(shuō),這是可以接受的。
方案2
同方案1,打包一份到證書(shū)到 app 內(nèi)部,但不通過(guò)KeyStore去引導(dǎo)生成的TrustManager,而是干脆直接自定義一個(gè)TrustManager,自己實(shí)現(xiàn)校驗(yàn)邏輯;校驗(yàn)邏輯主要包括:
服務(wù)器證書(shū)是否過(guò)期
證書(shū)簽名是否合法
try{CertificateFactory cf=CertificateFactory.getInstance("X.509");// uwca.crt 打包在 asset 中,該證書(shū)可以從https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下載InputStream caInput=newBufferedInputStream(getAssets().open("uwca.crt"));finalCertificate ca;try{ca=cf.generateCertificate(caInput);Log.i("Longer","ca="+((X509Certificate)ca).getSubjectDN());Log.i("Longer","key="+((X509Certificate)ca).getPublicKey());}finally{caInput.close();}// Create an SSLContext that uses our TrustManagerSSLContext context=SSLContext.getInstance("TLSv1","AndroidOpenSSL");context.init(null,newTrustManager[]{newX509TrustManager(){@Overridepublic void checkClientTrusted(X509Certificate[] chain,? ? ? ? ? ? ? ? ? ? ? String authType)? ? ? ? ? ? ? ? ? ? ? throws CertificateException {}@Overridepublic void checkServerTrusted(X509Certificate[] chain,? ? ? ? ? ? ? ? ? ? ? String authType)? ? ? ? ? ? ? ? ? ? ? throws CertificateException {for(X509Certificate cert:chain){// Make sure that it hasn't expired.cert.checkValidity();// Verify the certificate's public key chain.try{cert.verify(((X509Certificate)ca).getPublicKey());}catch(NoSuchAlgorithmExceptione){e.printStackTrace();}catch(InvalidKeyExceptione){e.printStackTrace();}catch(NoSuchProviderExceptione){e.printStackTrace();}catch(SignatureExceptione){e.printStackTrace();}}}@OverridepublicX509Certificate[]getAcceptedIssuers(){returnnewX509Certificate[0];}}},null);URL url=newURL("https://certs.cac.washington.edu/CAtest/");HttpsURLConnection urlConnection=(HttpsURLConnection)url.openConnection();urlConnection.setSSLSocketFactory(context.getSocketFactory());InputStream in=urlConnection.getInputStream();copyInputStreamToOutputStream(in,System.out);}catch(CertificateExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}catch(NoSuchAlgorithmExceptione){e.printStackTrace();}catch(KeyManagementExceptione){e.printStackTrace();}catch(NoSuchProviderExceptione){e.printStackTrace();}
同樣上述代碼只能訪問(wèn) certs.cac.washington.edu 相關(guān)域名地址,如果訪問(wèn) https://www.taobao.com/ 或者 https://www.baidu.com/ ,則會(huì)在cert.verify(((X509Certificate) ca).getPublicKey());處拋異常,導(dǎo)致連接失敗。
自定義HostnameVerifier,簡(jiǎn)單的話就是根據(jù)域名進(jìn)行字符串匹配校驗(yàn);業(yè)務(wù)復(fù)雜的話,還可以結(jié)合配置中心、白名單、黑名單、正則匹配等多級(jí)別動(dòng)態(tài)校驗(yàn);總體來(lái)說(shuō)邏輯還是比較簡(jiǎn)單的,反正只要正確地實(shí)現(xiàn)那個(gè)方法。
HostnameVerifier hnv=newHostnameVerifier(){@Overridepublic boolean verify(String hostname, SSLSession session) {//示例if("yourhostname".equals(hostname)){returntrue;}else{HostnameVerifier hv=HttpsURLConnection.getDefaultHostnameVerifier();returnhv.verify(hostname,session);}}};
主機(jī)名驗(yàn)證策略改成嚴(yán)格模式
SSLSocketFactory sf=newMySSLSocketFactory(trustStore);sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
參考資料
竊聽(tīng)風(fēng)暴: Android平臺(tái)https嗅探劫持漏洞
數(shù)字證書(shū)及其在安全測(cè)試中的應(yīng)用
為你的安卓應(yīng)用實(shí)現(xiàn)自簽名的 SSL 證書(shū)
DRD19. Properly verify server certificate on SSL/TLS
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),版權(quán)歸作者所有,本社區(qū)不擁有所有權(quán),也不承擔(dān)相關(guān)法律責(zé)任。如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,歡迎發(fā)送郵件至:yqgroup@service.aliyun.com?進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。