淺析HTTPS中間人攻擊與證書校驗

0x00 引言

隨着安全的普及,https通信應用越發廣泛,但是由於對https不熟悉導致開發人員頻繁錯誤的使用https,例如最常見的是未校驗https證書從而導致“中間人攻擊”,並且由於修復方案也一直是個坑,導致修復這個問題時踩各種坑,故謹以此文簡單的介紹相關問題。

本文第一節主要講述https的握手過程,第二節主要講述常見的“https中間人攻擊”場景,第三節主要介紹證書校驗修復方案,各位看官可根據自己口味瀏覽。

0x01 Https原理

 

淺析HTTPS中間人攻擊與證書校驗

 

首先來看下https的工作原理,上圖大致介紹了https的握手流程,後續我們通過抓包看下每個握手包到底幹了些什麼神奇的事。

注:本文所有內容以TLS_RSA_WITH_AES_128_CBC_SHA加密組件作爲基礎進行說明,其他加密組件以及TLS版本會存在一定差異,例如TLS1.3針對移動客戶端有了很大的改動,現在的ECDHE等密鑰交換算法與RSA作爲密鑰交換算法也完全不一樣,所以有些地方和大家實際操作會存在一定出入。

1.1 TCP

TCP的三次握手,這是必須的。

 

淺析HTTPS中間人攻擊與證書校驗

 

1.2 Client Hello

 

淺析HTTPS中間人攻擊與證書校驗

 

TLS的版本號 隨機數random_c:這個是用來生成最後加密密鑰的因子之一,它包含兩部分,時間戳和隨機數 session-id:用來標識會話,第一次握手時爲空,如果以前建立過,可以直接帶過去從而避免完全握手 Cipher Suites加密組件列表:瀏覽器所支持的加密算法的清單客戶端支持的加密簽名算法的列表,讓服務器進行選擇 擴展字段:比如密碼交換算法的參數、請求主機的名字,用於單ip多域名的情況指定域名。

 

淺析HTTPS中間人攻擊與證書校驗

 

1.3 Sever Hello

 

淺析HTTPS中間人攻擊與證書校驗

 

隨機數rando_s,這個是用來生成最後加密密鑰的因子之一,包含兩部分,時間戳和隨機數 32字節的SID,在我們想要重新連接到該站點的時候可以避免一整套握手過程。 在客戶端提供的加密組件中,服務器選擇了TLS_RSA_WITH_AES_128_CBC_SHA組件。1.4 Certificate

 

淺析HTTPS中間人攻擊與證書校驗

 

證書是https裏非常重要的主體,可用來識別對方是否可信,以及用其公鑰做密鑰交換。可以看見證書裏面包含證書的頒發者,證書的使用者,證書的公鑰,頒發者的簽名等信息。其中Issuer Name是簽發此證書的CA名稱,用來指定簽發證書的CA的可識別的唯一名稱(DN, Distinguished Name),用於證書鏈的認證,這樣通過各級實體證書的驗證,逐漸上溯到鏈的終止點,即可信任的根CA,如果到達終點在自己的信任列表內未發現可信任的CA則認爲此證書不可信。

驗證證書鏈的時候,用上一級的公鑰對證書裏的簽名進行解密,還原對應的摘要值,再使用證書信息計算證書的摘要值,最後通過對比兩個摘要值是否相等,如果不相等則認爲該證書不可信,如果相等則認爲該級證書鏈正確,以此類推對整個證書鏈進行校驗,引用高性能網絡中的證書鏈校驗圖。

 

淺析HTTPS中間人攻擊與證書校驗

 

 

淺析HTTPS中間人攻擊與證書校驗

 

二級機構的公鑰

 

淺析HTTPS中間人攻擊與證書校驗

 

網站證書的簽名

不僅僅進行證書鏈的校驗,此時還會進行另一個協議即Online Certificate Status Protocol, 該協議爲證書狀態在線查詢協議,一個實時查詢證書是否吊銷的方式,客戶端發送證書的信息並請求查詢,服務器返回正常、吊銷或未知中的任何一個狀態,這個查詢地址會附在證書中供客戶端使用。

1.5 Server Hello Done

 

淺析HTTPS中間人攻擊與證書校驗

 

這是一個零字節信息,用於告訴客戶端整個server hello過程已經結束。

1.6 ClientKeyExchange

 

淺析HTTPS中間人攻擊與證書校驗

 

客戶端在驗證證書有效之後發送ClientKeyExchange消息,ClientKeyExchange消息中,會設置48字節的premaster secret,通過密鑰交換算法加密發送premaster secret的值,例如通過 RSA公鑰加密premaster secret的得到Encrypted PreMaster傳給服務端。PreMaster前兩個字節是TLS的版本號,該版本號字段是用來防止版本回退攻擊的。

從握手包到目前爲止,已經出現了三個隨機數(客戶端的random_c,服務端的random_s,premaster secret),使用這三個隨機數以及一定的算法即可獲得對稱加密AES的加密主密鑰Master-key,主密鑰的生成非常的精妙,通過閱讀RFC2246文檔以及openssl的函數int master_secret(unsigned char *dest,int len,unsigned char *pre_master_secret,int pms_len,unsigned char *label,unsigned char *seed,int seed_len)可得到主密鑰的生成過程如下:

1 2 PRF(secret, label, seed) = P_MD5(S1, label + seed) XOR P_SHA-1(S2, label + seed);

函數中的參數定義如下:

secret即爲pre secret ,label是一個字符串,seed爲random_c+random_s,S1爲前半部分的pre secret,S2爲後半部分的pre secret。分別使用P_MD5()和P_SHA-1進行hash計算得到兩個hash,再使用這兩個hash進行異或得到最終的主密鑰master key,這兩個hash由下面的運算得來:

1 2 3 P_hash(1/2secret, label,seed) = HMAC_hash(1/2secret, A(1) + seed) + HMAC_hash(1/2secret, A(2) + seed) + HMAC_hash(1/2secret, A(3) + seed) + ...

P_hash()是P_MD5()和P_SHA-1()的統稱。

A()的賦值相對比較複雜,變形後如下:

1 2 A(0)=HMAC(md5,1/2secret,strlen(1/2secret), actual_seed(0), strlen(label)+strlen(seed),temp_md5,NULL); //temp_md5數組用於接收最後生成的hash,attual_seed爲label和seed結合 A(0) = HMAC(sha,1/2secret,strlen(1/2secret) , actual_seed(0), strlen(label)+strlen(seed),temp_sha,NULL);// temp_sha數組用於接收最後生成的hash,attual_seed爲label和seed結合

簡化表達如下:

1 A(i)=HMAC_hash(1/2secret,A(i-1)+seed)

由於只進行一次SHA-1或者MD5產生的hash字節數是不夠的,所以使用迭代的方式產生足夠多的hash,多出來的hash則直接拋棄。例如,使用P_SHA-1()產生64字節的數據,就需要迭代4次(通過A(4)),產生80字節的輸出數據,最後迭代產生的16字節將會被拋棄,只留下64字節的數據,如果使用P_MD5()則需要迭代5次。

1.7 Change Cipher Spec

 

淺析HTTPS中間人攻擊與證書校驗

 

發送一個不加密的信息,瀏覽器使用該信息通知服務器後續的通信都採用協商的通信密鑰和加密算法進行加密通信。

1.8 Encrypted Handshake Message

 

淺析HTTPS中間人攻擊與證書校驗

 

驗證加密算法的有效性,結合之前所有通信參數的 hash 值與其它相關信息生成一段數據,採用協商密鑰 session secret 與算法進行加密,然後發送給服務器用於數據與握手驗證,通過驗證說明加密算法有效。

1.9 Change_cipher_spec

 

淺析HTTPS中間人攻擊與證書校驗

 

Encrypted Handshake Message通過驗證之後,服務器同樣發送 change_cipher_spec 以通知客戶端後續的通信都採用協商的密鑰與算法進行加密通信。這裏還有一個New Session Ticket並不是必須的,這是服務器做的優化,後續我們再講解該協議的作用。

1.10 Encrypted Handshake Message

 

淺析HTTPS中間人攻擊與證書校驗

 

同樣的,服務端也會發送一個Encrypted Handshake Message供客戶端驗證加密算法有效性。

1.11 Application Data

 

淺析HTTPS中間人攻擊與證書校驗

 

經過一大串的的計算之後,終於一切就緒,後續傳輸的數據可通過主密鑰master key進行加密傳輸,加密數據查看圖中的Encrypted Apploication Data字段數據,至此https的一次完整握手以及數據加密傳輸終於完成。

https裏還有很多可優化並且很多精妙的設計,例如爲了防止經常進行完整的https握手影響性能,於是通過sessionid來避免同一個客戶端重複完成握手,但是又由於sessionid消耗的內存性能比較大,於是又出現了new session ticket,如果客戶端表明它支持Session Ticket並且服務端也支持,那麼在TLS握手的最後一步服務器將包含一個“New Session Ticket”信息,其中包含了一個加密通信所需要的信息,這些數據採用一個只有服務器知道的密鑰進行加密。這個Session Ticket由客戶端進行存儲,並可以在隨後的一次會話中添加到 ClientHello消息的SessionTicket擴展中。雖然所有的會話信息都只存儲在客戶端上,但是由於密鑰只有服務器知道,所以Session Ticket仍然是安全的,因此這不僅避免了性能消耗還保證了會話的安全性。

最後我們可以使用openssl命令來直觀的看下https握手的流程:

 

淺析HTTPS中間人攻擊與證書校驗

 

0x02 中間人攻擊

https握手過程的證書校驗環節就是爲了識別證書的有效性唯一性等等,所以嚴格意義上來說https下不存在中間人攻擊,存在中間人攻擊的前提條件是沒有嚴格的對證書進行校驗,或者人爲的信任僞造證書,下面一起看下幾種常見的https“中間人攻擊”場景。

2.1 證書未校驗

由於客戶端沒有做任何的證書校驗,所以此時隨意一張證書都可以進行中間人攻擊,可以使用burp裏的這個模塊進行中間人攻擊。

 

淺析HTTPS中間人攻擊與證書校驗

 

通過瀏覽器查看實際的https證書,是一個自簽名的僞造證書。

 

淺析HTTPS中間人攻擊與證書校驗

 

2.2 部分校驗

做了部分校驗,例如在證書校驗過程中只做了證書域名是否匹配的校驗,可以使用burp的如下模塊生成任意域名的僞造證書進行中間人攻擊。

 

淺析HTTPS中間人攻擊與證書校驗

 

實際生成的證書效果,如果只做了域名、證書是否過期等校驗可輕鬆進行中間人攻擊(由於chrome是做了證書校驗的所以會提示證書不可信任)。

 

淺析HTTPS中間人攻擊與證書校驗

 

2.3 證書鏈校驗

如果客戶端對證書鏈做了校驗,那麼攻擊難度就會上升一個層次,此時需要人爲的信任僞造的證書或者安裝僞造的CA公鑰證書從而間接信任僞造的證書,可以使用burp的如下模塊進行中間人攻擊。

 

淺析HTTPS中間人攻擊與證書校驗

 

 

淺析HTTPS中間人攻擊與證書校驗

 

可以看見瀏覽器是會報警告的,因爲burp的根證書PortSwigger CA並不在瀏覽器可信任列表內,所以由它作爲根證書籤發的證書都是不能通過瀏覽器的證書校驗的,如果將PortSwigger CA導入系統設置爲可信任證書,那麼瀏覽器將不會有任何警告。

2.4 手機客戶端Https數據包抓取

上述第一、二種情況不多加贅述,第三種情況就是我們經常使用的抓手機應用https數據包的方法,即導入代理工具的公鑰證書到手機裏,再進行https數據包的抓取。導入手機的公鑰證書在android平臺上稱之爲受信任的憑據,在ios平臺上稱之爲描述文件,可以通過openssl的命令直接查看我們導入到手機客戶端裏的這個PortSwiggerCA.crt

 

淺析HTTPS中間人攻擊與證書校驗

 

可以看見是Issuer和Subject一樣的自簽名CA公鑰證書,另外我們也可以通過證書類型就可以知道此爲公鑰證書,crt、der格式的證書不支持存儲私鑰或證書路徑(有興趣的同學可查找證書相關信息)。導入CA公鑰證書之後,參考上文的證書校驗過程不難發現通過此方式能通過證書鏈校驗,從而形成中間人攻擊,客戶端使用代理工具的公鑰證書加密隨機數,代理工具使用私鑰解密並計算得到對稱加密密鑰,再對數據包進行解密即可抓取明文數據包。

2.5 中間人攻擊原理

一直在說中間人攻擊,那麼中間人攻擊到底是怎麼進行的呢,下面我們通過一個流行的MITM開源庫mitmproxy來分析中間人攻擊的原理。中間人攻擊的關鍵在於https握手過程的ClientKeyExchange,由於pre key交換的時候是使用服務器證書裏的公鑰進行加密,如果用的僞造證書的公鑰,那麼中間人就可以解開該密文得到pre_master_secret計算出用於對稱加密算法的master_key,從而獲取到客戶端發送的數據;然後中間人代理工具再使用其和服務端的master_key加密傳輸給服務端;同樣的服務器返回給客戶端的數據也是經過中間人解密再加密,於是完整的https中間人攻擊過程就形成了,一圖勝千言,來吧。

 

淺析HTTPS中間人攻擊與證書校驗

 

通過讀Mitmproxy的源碼發現mitmproxy生成僞造證書的函數如下:

 

淺析HTTPS中間人攻擊與證書校驗

 

通過上述函數一張完美僞造的證書就出現了,使用瀏覽器通過mitmproxy做代理看下實際僞造出來的證書。

 

淺析HTTPS中間人攻擊與證書校驗

 

可以看到實際的證書是由mimtproxy頒發的,其中的公鑰就是mimtproxy自己的公鑰,後續的加密數據就可以使用mimtproxy的私鑰進行解密了。如果導入了mitmproxy的公鑰證書到客戶端,那麼該僞造的證書就可以完美的通過客戶端的證書校驗了。這就是平時爲什麼導入代理的CA證書到手機客戶端能抓取https的原因。

0x03 App證書校驗

通過上文第一和第二部分的說明,相信大家已經對https有個大概的瞭解了,那麼問題來了,怎樣才能防止這些“中間人攻擊”呢?

app證書校驗已經是一個老生常談的問題了,但是市場上還是有很多的app未做好證書校驗,有些只做了部分校驗,例如檢查證書域名是否匹配證書是否過期,更多數的是根本就不做校驗,於是就造成了中間人攻擊。做證書校驗需要做完全,只做一部分都會導致中間人攻擊,對於安全要求並不是特別高的app可使用如下校驗方式:

查看證書是否過期 服務器證書上的域名是否和服務器的實際域名相匹配 校驗證書鏈

可參考http://drops.wooyun.org/tips/3296,此類校驗方式雖然在導入CA公鑰證書到客戶端之後會造成中間人攻擊,但是攻擊門檻已相對較高,所以對於安全要求不是特別高的app可採用此方法進行防禦。對於安全有較高要求一些app(例如金融)上述方法或許還未達到要求,那麼此時可以使用如下更安全的校驗方式,將服務端證書打包放到app裏,再建立https鏈接時使用本地證書和網絡下發證書進行一致性校驗,可參考安卓官方提供的https連接demo:https://developer.android.com/training/articles/security-ssl.html

#!java
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import android.content.Context;
import android.util.Log;

public class TestHttpsConnect {
    public static void test(Context mcontext, String name, String weburl) throws Exception {

        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        InputStream caInput = new BufferedInputStream(new FileInputStream("baidu.cer"));
        Certificate ca;
        try {
            ca = cf.generateCertificate(caInput);
            System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
        } finally {
            caInput.close();
        }

        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca", ca);

        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);

        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, tmf.getTrustManagers(), null);

        URL url = new URL(weburl);
        HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
        urlConnection.setSSLSocketFactory(context.getSocketFactory());
        InputStream in = urlConnection.getInputStream();
        ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();

        byte[] buffer = new byte[1024];
        int len = -1;
        while ((len = in.read(buffer)) >= 0) {
            arrayOutputStream.write(buffer, 0, len);
        }

        Log.e("", arrayOutputStream.toString());

    }

}

此類校驗即便導入CA公鑰證書也無法進行中間人攻擊,但是相應的維護成本會相對升高,例如服務器證書過期,證書更換時如果app不升級就無法使用,那麼可以改一下:

生成一對RSA的公私鑰,公鑰可硬編碼在app,私鑰放服務器。 https握手前可通過服務器下發證書信息,例如公鑰、辦法機構、簽名等,該下發的信息使用服務器裏的私鑰進行簽名; 通過app裏預置的公鑰驗簽得到證書信息並存在內容中供後續使用; 發起https連接獲取服務器的證書,通過對比兩個證書信息是否一致進行證書校驗。

這樣即可避免強升的問題,但是問題又來了,這樣效率是不是低太多了?答案是肯定的,所以對於安全要求一般的應用使用第一種方法即可,對於一些安全要求較高的例如金融企業可選擇第二種方法。

說了挺多,但是該來的問題還是會來啊!現在的app一般採用混合開發,會使用很多webveiw直接加載html5頁面,上面的方法只解決了java層證書校驗的問題,並沒有涉及到webview裏面的證書校驗,對於這種情況怎麼辦呢?既然問題來了那麼就一起說說解決方案,對於webview加載html5進行證書校驗的方法如下:

webview創建實例加載網頁時通過onPageStart方法返回url地址; 將返回的地址轉發到java層使用上述的證書校驗代碼進行進行校驗; 如果證書校驗出錯則使用stoploading()方法停止網頁加載,證書校驗通過則正常加載。

提供參考代碼如下:
 

#!java
package com.example.testhttps;

import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

public class TestWebViewActivity extends Activity {

    static final String TAB = "MainActivity";
    WebView mWebView;

    SSLContext mSslContext;
    KeyStore keyStore;
    TrustManagerFactory tmf;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);

        setContentView(R.layout.test_webview_acitity);

        iniData();

        mWebView = (WebView) findViewById(R.id.webView1);
        mWebView.setWebViewClient(new MyWebViewClient());
        mWebView.getSettings().setJavaScriptEnabled(true);

        Intent i = getIntent();
        String url = i.getStringExtra("url");
        mWebView.loadUrl(url);
    }

    /**
     * 初始化證書
     */
    void iniData() {
        try {

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = getAssets().open("baidu.cer");
            Certificate ca;
            try {
                ca = cf.generateCertificate(caInput);
                System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
            } finally {
                caInput.close();
            }

            String keyStoreType = KeyStore.getDefaultType();
            keyStore = KeyStore.getInstance(keyStoreType);
            keyStore.load(null, null);
            keyStore.setCertificateEntry("ca", ca);

            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(keyStore);

            mSslContext = SSLContext.getInstance("TLS");
            mSslContext.init(null, tmf.getTrustManagers(), null);
        } catch (Exception e) {
            Log.e("", "iniData error");
            e.printStackTrace();
        }
    }

    class MyWebViewClient extends WebViewClient {

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            // TODO Auto-generated method stub
            super.onPageStarted(view, url, favicon);

            // 如果不是https,不用校驗
            if (!url.startsWith("https://")) {
                return;
            }

            final WebView tempView = view;
            final String tempurl = url;

            /**
             * 測試url校驗,如果不通過,就不加載
             */
            new AsyncTask() {

                @Override
                protected Boolean doInBackground(String... params) {
                    // TODO Auto-generated method stub
                    try {
                        // 檢驗證書是否正確
                        URL url = new URL(tempurl);
                        HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
                        urlConnection.setSSLSocketFactory(mSslContext.getSocketFactory());
                        InputStream in = urlConnection.getInputStream();
                        in.close();
                        // TestHttpsConnect.test(TestWebViewActivity.this,
                        // "baidu.cer", tempurl);
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                        return false;
                    }
                    return true;
                }

                protected void onPostExecute(Boolean result) {
                    if (!result) {
                        Toast.makeText(TestWebViewActivity.this, "證書校驗錯誤", Toast.LENGTH_SHORT).show();
                        tempView.stopLoading();
                    }
                };

            }.execute(url);
        }

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

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章