Android證書信任問題與大表哥

wyzsk發表於2020-08-19
作者: 瘦蛟舞 · 2014/10/23 12:14

0x00 起因


1、近期icloud.com、yahoo.com、apple.com遭遇到大規模劫持

WooYun: Yahoo雅虎在國內訪問遭遇SSL中間人攻擊(被替換為自簽名證照)

2、烏雲平臺、CVE都收到大量有關Android APP信任所有證照的漏洞

WooYun: 國內絕大部分Android APP存在信任所有證照漏洞

3、老外寫有關大表哥的文章中提到MITM時360瀏覽器不提示證照錯誤

http://www.computerworld.com/article/2836084/chinese-big-brother-launches-nationwide-attack-on-icloud.html

之前信任證照問題一直都有被提到,但是普遍不受大家重視,因為這個漏洞是利用是需要場景的:MITM(中間人攻擊 Man-in-the-middle attack)。一般情況下MITM相對其他攻擊是比較少見的,如果有良好的上網習慣如不接入不受信任的網路,那就更少可能受此類攻擊了。但是近期發生的MITM據傳是在核心骨幹網BGP上做了改動所以劫持範圍非常之廣,真是防不勝防呀,你被劫持了麼?

0x01 科普


https&&ssl

為了提高網站的安全性,一般會在比較敏感的部分頁面採用https傳輸,比如註冊、登入、控制檯等。像Gmail、網銀、icloud等則全部採用https傳輸。https/ssl主要起到兩個作用:網站認證、內容加密傳輸和資料一致性。經CA簽發的證照才起到認證可信的作用,所有有效證照均可以起到加密傳輸的作用。

數字證照

主要在網際網路上的用於身份驗證的用途。 安全站點在獲得CA(Certificate Authority數字證照認證機構)認證後,獲得一個數字證照,以此來標識其合法身份的真實性。數字證照主要分為伺服器證照和客戶端證照。伺服器證照(SSL證照)用來進行身份驗證和通訊的加密,客戶端證照主要用於身份驗證和電子簽名。找CA申請證照是要收費的。

自簽名證照

非CA頒發的證照,透過自簽名的方式得到的證照。通常Web瀏覽器會顯示一個對話方塊,詢問您是否希望信任一個自簽名證照。這個是不用花錢的。

中間人攻擊

是指攻擊者與通訊的兩端分別建立獨立的聯絡,並交換其所收到的資料,使通訊的兩端認為他們正在透過一個私密的連線與對方直接對話,但事實上整個會話都被攻擊者完全控制。在中間人攻擊中,攻擊者可以攔截通訊雙方的通話並插入新的內容。在許多情況下這是很簡單的。

0x02 分析


如果自己簡單的實現android webview載入網頁,如果直接訪問可信證照的站點是可以正常顯示,但是如果訪問自簽名的證照的站點就會顯示notfound的頁面。(寫本文時apple.com以及apple.com.cn處於劫持狀態)

logcat會輸出網頁顯示不安全的內容

Web Console:The page displayed insecure content!

功能健全的手機瀏覽器訪問自簽名證照的站點會如下提醒

在PC端如果訪問自簽名證照的站點則會出現如下圖左側的提醒

為解決javax.net.ssl.SSLPeerUnverifiedException: No peer certificate的異常,開發者往往會採用以下的錯誤解決方案。如此是瀏覽器應用採用此類解決方案,那麼風險就更大了。

覆蓋google預設的證照檢查機制

#!java
class bv
  implements X509TrustManager
{
  bv(bu parambu) {}

  public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) {// Do nothing -> accept any certificates}

  public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) {// Do nothing -> accept any certificates}

  public X509Certificate[] getAcceptedIssuers()
  {
    return null;
  }
}

信任所有主機名

#!java
public static HttpClient getNewHttpClient() {  
    try {  
        //獲得密匙庫
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());  
        trustStore.load(null, null);  

        SSLSocketFactory sf = new SSLSocketFactoryEx(trustStore); 
        //信任所有主機名 
        sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);  

        HttpParams params = new BasicHttpParams();  
        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);  
        HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);  

        SchemeRegistry registry = new SchemeRegistry();  
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));  
        registry.register(new Scheme("https", sf, 443));  

        ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);  

        return new DefaultHttpClient(ccm, params);  
    } catch (Exception e) {  
        return new DefaultHttpClient();  
    }  
}  

empty HostnameVerifier

#!java
HostnameVerifier hv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// Always return true -> Accespt any host names
return true;
}
};

忽略WebView證照錯誤繼續載入

#!java
    myWebView.setWebViewClient(new WebViewClient(){

        @Override
        public void onReceivedError(WebView view, int errorCode,
                String description, String failingUrl) {
            // TODO Auto-generated method stub
            super.onReceivedError(view, errorCode, description, failingUrl);
        }

        @Override
        public void onReceivedSslError(WebView view,
                SslErrorHandler handler, SslError error) {
            // TODO Auto-generated method stub
            handler.proceed();
        }});

其實早在14年2月竊聽風暴: Android平臺https嗅探劫持漏洞文中就有提到android平臺的app因為覆蓋google預設的證照檢查機制(X509TrustManager)之後沒有對證照進行應有的安全性檢查,直接接受了所有異常的https證照,不提醒使用者存在安全風險,也不終止這次危險的連線。文中對證照域名檢查(HostnameVerifier)部分沒有細說。

上文有提到PC版的360瀏覽器訪問被劫持網站居然沒有證照錯誤提示,讓人很不敢相信。加上最近android app 證照問題頻發,猜想是否有可能一些手機瀏覽器也會有此類漏洞了。測試過程中發現360手機瀏覽器、和搜狗瀏覽器存在此風險。

百度和遨遊輕鬆檢測出證照異常

而360和搜狗直接載入進入了被劫持的網站。

反編譯檢視遨遊瀏覽器的程式碼,針對證照異常做了處理

而搜狗瀏覽器則是做了證照信任所有主機名不當處理

關鍵字:checkServerTrusted、setHostnameVerifier、ALLOW_ALL_HOSTNAME_VERIFIER、X509TrustManager、onReceivedSslError

0x03 對比


對主流手機瀏覽器進行了橫向對比,測試物件包括:firefox、chrome、UC瀏覽器、搜狗瀏覽器、百度瀏覽器、360安全瀏覽器、歐鵬瀏覽器、遨遊雲瀏覽器、獵豹瀏覽器。

測試方法:手機訪問https://example.com/,觀察是否有安全提醒。(update:此方法已經無效.)

未做提醒直接載入網頁:360安全瀏覽器、獵豹瀏覽器、搜狗瀏覽器

正常做出安全提醒:firefox、chrome、UC瀏覽器、百度瀏覽器、歐鵬瀏覽器、遨遊雲瀏覽器

0x04 建議


開發者:

1、非瀏覽器app,有錢申請ca證照沒錢在客戶端中新增證照,切勿信任所有證照。

2、瀏覽器app,嚴格按照客戶端校驗伺服器證照流程處理:

  • 檢視證照是否過期
  • CA是否可靠
  • CA的公鑰能否正確解開伺服器證照的CA數字簽名,即證照的簽名值
  • 伺服器證照上的域名是否和伺服器的實際域名相匹配

3、建議使用setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER)

一個SSLSocketFactory的example

#!java
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 [email protected] 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) {
        }
    }


} 

使用者:使用安全性較好的app

0x05 參考


/tips/?id=2775

/papers/?id=959

http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html

http://developer.android.com/reference/javax/net/ssl/X509TrustManager.html

http://developer.android.com/training/articles/security-ssl.html

http://developer.android.com/reference/org/apache/http/conn/ssl/SSLSocketFactory.html

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章