關(guān)于 Charles 出現(xiàn) unknown 的一點探索

用 Charles 截取一些 Android 和 iOS應(yīng)用的請求數(shù)據(jù),發(fā)現(xiàn)好多 https 的請求無線顯示內(nèi)容了,只是顯示個紅色的 unknown

這是因為 Android 從 6.0 之后加強了系統(tǒng)安全性,ssl 證書的驗證由系統(tǒng)級別精確到了應(yīng)用級別,所以即使安裝了 Charles 的根證書依然無法看到 https 的內(nèi)容

但 iOS 還沒有像 Android 這么做,但也有一些應(yīng)用無法看到 https 了,一番搜索后了解到原來有些應(yīng)用采用了 SSL Pinning 的技術(shù),簡單來說就是在建立 ssl 連接的時候?qū)ψC書做了驗證,如果發(fā)現(xiàn)證書和請求的域名不匹配的時候就拒絕連接了,所以看到的 unkown 其實是說明這次請求根本沒有發(fā)送成功,這就牽涉到了 ssl 三次握手的一些知識,可以理解為在建立 ssl 連接的時候服務(wù)器和客戶端之間會進行幾次身份驗證,采用的是 RSA 加密, 其中私鑰在服務(wù)器容器中配置好了,證書信息(包含公鑰)會在客戶端請求建立 https 連接的時候拿到,至于具體的握手建立連接的過程這里就不詳談了,有興趣的可以去看其他同學的文章

既然了解了問題的原因,我們可以在本地進行一個簡單的測試來驗證一下,用 java寫一個http客戶端和線上的 https 服務(wù)建立連接,然后驗證服務(wù)器公鑰,就拿https://www.baidu.com來測試吧,首先我們可以先看一下百度的 https 證書,通過 chrome 訪問https://www.baidu.com,點擊地址欄左上角的小鎖,然后點擊證書,就能看到證書信息了

image

接下來我們把證書拖到一個文件夾里,這樣就獲取到一個 .cer 格式的證書文件,這個證書包含了很多信息,我們主要是要取到證書里的公鑰信息,通過 openssl 工具來獲取,命令如下

openssl x509 -inform der -in /xxx/xxx.cer -pubkey -noout > public_key.pem

這樣就把證書里的公鑰導出來了,文件打開后就是下面這個樣子:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtMa/2lMgD+pA87hSF2Y7
NgGNErSZDdObbBhTsRkIsPpzRz4NOnlieGEuVDxJfFbawL5hVdVCcGoQvvW9jWSW
IQCTYwmHtxm6DiA+SchT7QKPRgHroQeTc7vt8bPJ4vvd8Dkqg630QZi8huq6dKim
49DlxY6zC7LSrJF0Dv+AECM2YmUItIf1VwwlxwDY9ahduDNBpypf2/pwniG7rkIW
Zgdp/hwmKoEPq3Pj1lIgpG2obNRmSKRv8mgKxWWhTr8EekBDHNN1+3WsGdZKNQVu
z9Vl0UTKawxYBMSFTx++LDLR8cYo+/kmNrVt+suWoqDQvPhR3wdEvY9vZ8DUr9nN
wwIDAQAB
-----END PUBLIC KEY-----

這個就是百度證書的公鑰了,然后我們接下來寫一個 http 客戶端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.bedrock.util.encrypt.Base64;

/**
 * HttpKit
 */
public class HttpsTest {

    private static final SSLSocketFactory sslSocketFactory = initSSLSocketFactory();

    private static SSLSocketFactory initSSLSocketFactory() {
        try {
            TrustManager[] tm = { new HttpsTest().new OptionTrustManager() };
            SSLContext sslContext = SSLContext.getInstance("TLS", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * https 證書管理
     */
    private class OptionTrustManager implements X509TrustManager {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            for (X509Certificate x509Certificate : chain) {
                String principal = x509Certificate.getSubjectX500Principal().getName();
                String pubkey = new String(Base64.encode(x509Certificate.getPublicKey().getEncoded(), Base64.DEFAULT));
                if (principal.contains("baidu.com")) {
                    if (!pubkey.contains("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtMa/2lMgD+p")) {
                        throw new CertificateException("error public key");
                    }
                }
                System.out.println("domain:" + principal + "    public key:\n" + pubkey);
            }
        }
    }

    public static void main(String[] args) {
        String responseStr = get("https://www.baidu.com");
        System.out.println(responseStr.length());
    }

    public static HttpURLConnection getHttpConnection(String url)
            throws IOException, NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException {
        URL _url = new URL(url);
        HttpURLConnection conn = (HttpURLConnection) _url.openConnection();
        if (conn instanceof HttpsURLConnection) {
            ((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory);
        }

        conn.setRequestMethod("GET");
        conn.setDoOutput(true);
        conn.setDoInput(true);

        conn.setConnectTimeout(30000);
        conn.setReadTimeout(30000);

        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        conn.setRequestProperty("User-Agent",
                "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36");

        return conn;
    }

    /**
     * Send GET request
     */
    public static String get(String url) {
        HttpURLConnection conn = null;
        try {
            conn = getHttpConnection(url);
            conn.connect();
            return readResponseString(conn);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    private static String readResponseString(HttpURLConnection conn) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        try {
            inputStream = conn.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            String line = null;
            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

主要關(guān)注下 * OptionTrustManager 的 checkServerTrusted * 這個方法,我們在自定義的 X509TrustManager 中對證書進行了簡單的驗證,取了證書的一部分內(nèi)容做校驗(這個只是例子,線上使用的話最好做全部字符的校驗)

好了,客戶端也有了,接下來我們用 Charles 來驗證下,看加上這個驗證后 charles 還能不能截取到我們的通信內(nèi)容了(這里 java 代碼相當于一個客戶端,baidu 相當于服務(wù)器)

首先,我們開啟 Charles 的 macOS Proxy來獲取系統(tǒng)的 http 通信內(nèi)容.如下圖(開啟 macOS Proxy 需要安裝根證書,安裝方法看下面第二張圖)


image.png

image.png

首先開啟 macOS Proxy,接下來我們運行下 main 方法,發(fā)現(xiàn)現(xiàn)在訪問 baidu.com 顯示的是 unknown 了,再看 console 輸出了異常信息,公鑰驗證不通過


image.png

這是因為 Charles 是作為中間人來劫持我們的請求的,我們訪問 baidu.com 實際是先訪問了 Charles 服務(wù),然后 Charles 服務(wù)再去跟 baidu.com 進行交互,完了再把請求數(shù)據(jù)返回給我們,所以我們拿到的公鑰信息是 Charles 的根證書的,自然是跟 baidu 的證書不匹配的,所以就不會再發(fā)起請求了

然后關(guān)閉 macOS Proxy,再次運行 main 方法,可以看到這次沒有拋異常,請求到了 baidu.com 的頁面內(nèi)容,而且打印了兩個證書信息,其中第一個證書是 baidu 的,第二個是 CA 的證書

有一點需要注意,這里驗證的是證書的公鑰信息,正式情況下,還需要驗證證書的過期時間等信息,驗證公鑰是為了再證書過期后不至于客戶端留的證書和新的證書不一致,只要我們的服務(wù)器私鑰不變,生成的證書的公鑰信息也就不會變

到此,我們已經(jīng)對 unknown 有了比較清晰的認識,以上只是自己個人淺顯的理解,如有紕漏還望指正

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 互聯(lián)網(wǎng)的通信安全,建立在SSL/TLS協(xié)議之上。 本文簡要介紹SSL/TLS協(xié)議的運行機制。文章的重點是設(shè)計思想和...
    拉肚閱讀 3,002評論 0 6
  • HTTPS介紹 超文本傳輸安全協(xié)議(英語:Hypertext Transfer Protocol Secure,縮...
    齊滇大圣閱讀 9,232評論 8 96
  • 前言 在說HTTPS之前先說說什么是HTTP,HTTP就是我們平時瀏覽網(wǎng)頁時候使用的一種協(xié)議。HTTP協(xié)議傳輸?shù)臄?shù)...
    布丁大人閱讀 2,845評論 2 14
  • 本文分為以下五節(jié): 中間人攻擊:介紹中間人攻擊常見方法,并模擬了一個簡單的中間人攻擊; 校驗證書的正確姿勢:介紹校...
    半島夏天閱讀 2,438評論 0 1
  • 原文地址 http://blog.csdn.net/u012409247/article/details/4985...
    0fbf551ff6fb閱讀 3,696評論 0 13

友情鏈接更多精彩內(nèi)容