Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 關於Android的https通訊安全

關於Android的https通訊安全

編輯:關於Android編程

起因

前段時間,同事拿著一個代碼安全掃描出來的 bug 過來咨詢,我一看原來是個 https通信時數字證書校驗的漏洞,一想就明白了大概;其實這種問題早兩年就有大規模的暴露,各大廠商App也紛紛中招,想不到過了這麼久天貓客戶端裡還留有這種坑;然後仔細研究了漏洞所在的代碼片段,原來所屬的是新浪微博分享 sdk 內部的,因為這個 sdk是源碼引用的,一直沒有更新,年久失修,所以也就被掃描出來了。因此給出的解決方案是:

先獲取最新的 sdk,看其內部是否已解決,已解決的話升級 sdk 版本即可;

第1步行不通,那就自己寫校驗邏輯,貓客全局通信基本已經使用 https 通信,參考著再寫一遍校驗邏輯也不是問題;

後來查了一下網上信息,早在2014年10月份,烏雲
平台裡就已經暴露過天貓這個漏洞,想必當時一定是忙於雙十一忽略了這個問題。

雖然這個問題通過升級 sdk
解決了,但是這個問題純粹是由於開發者本身疏忽造成的;特別是對於初級開發人員來說,可能為了解決異常,屏蔽了校驗邏輯;所以我還是抽空再 review
了一下這個漏洞,整理相關信息。

漏洞描述

對於數字證書相關概念、Android 裡 https 通信代碼就不再復述了,直接講問題。缺少相應的安全校驗很容易導致中間人攻擊,而漏洞的形式主要有以下3種:

自定義X509TrustManager

在使用HttpsURLConnection發起 HTTPS 請求的時候,提供了一個自定義的X509TrustManager,未實現安全校驗邏輯,下面片段就是當時新浪微博 sdk 內部的代碼片段。如果不提供自定義的X509TrustManager,代碼運行起來可能會報異常(原因下文解釋),初學者就很容易在不明真相的情況下提供了一個自定義的X509TrustManager,卻忘記正確地實現相應的方法。本文重點介紹這種場景的處理方式。

TrustManager tm = new X509TrustManager() {
    public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
              //do nothing,接受任意客戶端證書
    }

    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
              //do nothing,接受任意服務端證書
    }

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

sslContext.init(null, new TrustManager[] { tm }, null);

自定義了HostnameVerifier

在握手期間,如果 URL 的主機名和服務器的標識主機名不匹配,則驗證機制可以回調此接口的實現程序來確定是否應該允許此連接。如果回調內實現不恰當,默認接受所有域名,則有安全風險。代碼示例。

HostnameVerifier hnv = new HostnameVerifier() {
  @Override
  public boolean verify(String hostname, SSLSession session) {
    // Always return true,接受任意域名服務器
    return true;
  }
};
HttpsURLConnection.setDefaultHostnameVerifier(hnv);

信任所有主機名

SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

修復方案

分而治之,針對不同的漏洞點分別描述,這裡就講的修復方案主要是針對非浏覽器App,非浏覽器 App 的服務端通信對象比較固定,一般都是自家服務器,可以做很多特定場景的定制化校驗。如果是浏覽器 App,校驗策略就有更通用一些。

自定義X509TrustManager。前面說到,當發起 HTTPS 請求時,可能拋起一個異常,以下面這段代碼為例(來自官方文檔):

try {
    URL url = new URL("https://certs.cac.washington.edu/CAtest/");
    URLConnection urlConnection = url.openConnection();
    InputStream in = urlConnection.getInputStream();
    copyInputStreamToOutputStream(in, System.out);
} catch (MalformedURLException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
private void copyInputStreamToOutputStream(InputStream in, PrintStream out) throws IOException {
    byte[] buffer = new byte[1024];
    int c = 0;
    while ((c = in.read(buffer)) != -1) {
        out.write(buffer, 0, c);
    }
}

它會拋出一個SSLHandshakeException的異常。

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification 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 anchor for certification 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)
 ... 10 more
Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
 ... 16 more

Android 手機有一套共享證書的機制,如果目標 URL 服務器下發的證書不在已信任的證書列表裡,或者該證書是自簽名的,不是由權威機構頒發,那麼會出異常。對於我們這種非浏覽器 app 來說,如果提示用戶去下載安裝證書,可能會顯得比較詭異。幸好還可以通過自定義的驗證機制讓證書通過驗證。驗證的思路有兩種:

方案1

不論是權威機構頒發的證書還是自簽名的,打包一份到 app 內部,比如存放在 asset 裡。通過這份內置的證書初始化一個KeyStore,然後用這個KeyStore去引導生成的TrustManager來提供驗證,具體代碼如下:

try {
  CertificateFactory cf = CertificateFactory.getInstance("X.509");
  // uwca.crt 打包在 asset 中,該證書可以從https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下載
  InputStream caInput = new BufferedInputStream(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 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("TLSv1","AndroidOpenSSL");
  context.init(null, tmf.getTrustManagers(), null);

  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);
} catch (CertificateException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
  e.printStackTrace();
} catch (KeyStoreException e) {
  e.printStackTrace();
} catch (KeyManagementException e) {
  e.printStackTrace();
} catch (NoSuchProviderException e) {
  e.printStackTrace();
}

這樣就可以得到正確的輸出內容:



UW Services CA test page

QUESTION: Did you arrive here without any security alerts or warnings?

 

  • YES - This test page uses a certificate issued by the UW Services Certificate Authority. If you reached this page without any alerts or warnings from your browser, you have successfully installed the UW Services CA Certificate into your browser.

     

  • NO - If your browser warned you about the validity of this test page's security certificate, or the certificate authority is unrecognized, you may not have successfully installed the UW Services CA Certificate.

     

如果你用上述同樣的代碼訪問 https://www.taobao.com/ 或者 https://www.baidu.com/ ,則會拋出那個SSLHandshakeException異常,也就是說對於特定證書生成的TrustManager,只能驗證與特定服務器建立安全鏈接,這樣就提高了安全性。如之前提到的,對於非浏覽器 app 來說,這是可以接受的。

方案2

同方案1,打包一份到證書到 app 內部,但不通過KeyStore去引導生成的TrustManager,而是干脆直接自定義一個TrustManager,自己實現校驗邏輯;校驗邏輯主要包括:

?服務器證書是否過期
?證書簽名是否合法

try {
  CertificateFactory cf = CertificateFactory.getInstance("X.509");
  // uwca.crt 打包在 asset 中,該證書可以從https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下載
  InputStream caInput = new BufferedInputStream(getAssets().open("uwca.crt"));
  final 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 an SSLContext that uses our TrustManager
  SSLContext context = SSLContext.getInstance("TLSv1","AndroidOpenSSL");
  context.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 {
                  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 (NoSuchAlgorithmException e) {
                          e.printStackTrace();
                      } catch (InvalidKeyException e) {
                          e.printStackTrace();
                      } catch (NoSuchProviderException e) {
                          e.printStackTrace();
                      } catch (SignatureException e) {
                          e.printStackTrace();
                      }
                  }
              }

              @Override
              public X509Certificate[] getAcceptedIssuers() {
                  return new X509Certificate[0];
              }
          }
  }, null);

  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);
} catch (CertificateException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
  e.printStackTrace();
} catch (KeyManagementException e) {
  e.printStackTrace();
} catch (NoSuchProviderException e) {
  e.printStackTrace();
}

同樣上述代碼只能訪問 certs.cac.washington.edu 相關域名地址,如果訪問 https://www.taobao.com/ 或者 https://www.baidu.com/ ,則會在cert.verify(((X509Certificate) ca).getPublicKey());處拋異常,導致連接失敗。

?自定義HostnameVerifier,簡單的話就是根據域名進行字符串匹配校驗;業務復雜的話,還可以結合配置中心、白名單、黑名單、正則匹配等多級別動態校驗;總體來說邏輯還是比較簡單的,反正只要正確地實現那個方法。

HostnameVerifier hnv = new HostnameVerifier() {
  @Override
  public boolean verify(String hostname, SSLSession session) {
    //示例
    if("yourhostname".equals(hostname)){  
      return true;  
    } else {  
      HostnameVerifier hv =
            HttpsURLConnection.getDefaultHostnameVerifier();
      return hv.verify(hostname, session);
    }
  }
};

?主機名驗證策略改成嚴格模式

SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved