HTTPS 抓包原理以及 Android 端如何防止抓包

抓包原理

抓包的基本原理就是中间人攻击 HTTPS 的握手过程。Mac 上可使用 Charles 进行抓包。本质上就是两段 HTTPS 连接,Client <--> Man-In-The-Middle 和 Man-In-The-Middle <--> Server。使用 Charles 进行抓包,需要 Client 端提前将 Charles 的根证书添加在 Client 的信任列表中。

HTTPS中间人攻击

Android 端防止抓包 —— Certificate Pinning

回顾之前的 HTTPS 的握手过程,可以知道 SSL 的核心过程就是客户端验证证书链合法性——客户端检查证书链中是否有一个证书或者公钥存在于客户端的可信任列表中。
手机系统中内置了上百份不同的根证书。Certificate Pinning 的原理其实就是 app 中内置需要被信任的特定证书,app 在验证服务器传过来的证书链时,使用这些特定证书来验证的。

  • 叶子证书。如果 app 选择 pinning 叶子证书,那么就可以 100% 保证 ssl 证书链的合法性。但是叶子证书有效期短,服务器换证书(因为私钥泄露、证书到期等原因)的话就客户端 app 就需要用新的叶子证书验证。
  • 中间证书。如果 app 选择 pinning 中间证书,那么客户端 app 也就选择了相信签发中间证书的机构所签发的其他证书。这样的话,只要服务端使用同一个中间机构签发的叶子证书,客户端 app 就不需要做任何改变。同时,中间证书有效期也非常长。
  • 根证书。从证书链的验证过程来看,它的效果与中间证书相同,但是信任了更多的中间机构及其签发的证书。根证书的有效期比中间证书更长。
    客户端 app 可以同时 pinning 多个证书,以灵活地适应各种证书验证策略。

pinning 证书还是公钥?

证书的主要作用是公钥的载体,但在实践中我们更多是去 pinning 公钥,SubjectPublicKeyInfo(SPKI)。这是因为很多服务器会去定期旋转证书,但是证书旋转后,证书中的公钥还是相同的公钥。

私钥泄露怎么办?

如果私钥泄露了,那么服务器端就不得不使用新的私钥做出新的证书。客户端为了预防这种情况,可以提前 pinning 这些新的证书。这样,当服务器替换新的证书时,客户端 app 就可以不做任何改动。

如何存放证书或者公钥?

  • 内置。客户端 app 可以将证书或公钥 hardcode 到代码中,或者作为资源文件放进 asset 中。这样做的坏处就是,老版本的 app 难以替换其中的证书或公钥。
  • 第一次使用时保存。客户端 app 第一次访问服务器进行 ssl 证书链验证时,将其中的公钥保存下来。这种方法适用于客户端 app 不能提前知道它将要访问的服务器地址的时候。
  • 网络下发。最灵活的方法是,服务端提供一个证书服务器专门用于向客户端 app 下发需要 pinning 的证书或公钥,客户端 app 只需要 pinning 这个证书服务器即可。

Pinning Certificate

Android N

从 SDK 24 开始,Android 支持通过 xml 来配置 certificate pinning,见 Network Security Configuration

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <pin-set expiration="2018-01-01">
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
            <!-- backup pin -->
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

其中 <pin> 节点接受 SubjectPublicKeyInfo 的 hash 值。

OkHttp

OkHttp 从 2.1 开始直接支持 Certificate Pinning

HttpsURLConnection

1. Add Certificate To TrustManager

我在项目实践中发现有的服务器并不会在 ssl 握手阶段将完整的证书链传输过来——只会传证书链中的根证书和叶子证书。如果安卓系统中使用 HttpUrlConnection 访问服务器,抛出如下类似异常:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

但是浏览器对于这种缺失中间证书的服务器却能验证通过,主要原因是浏览器访问有完整证书链的网站时,如果发现证书链中有浏览器没有内置的中间证书,那么浏览器会将该证书缓存下来,这样浏览器访问其他没有该中间证书的服务器时,就可以使用这个缓存的中间证书来验证证书链。
解决安卓上出现这个问题的方法是将这个中间证书通过 app 添加到信任证书列表中。我们需要将该中间证书加入到 App 运行时所用的 TrustManager 中。

  • 将需要添加的 CA 证书加载到 InputStream
  • 使用这个 InputStream 创建一个 KeyStore
  • 使用这个 KeyStore 初始化一个 TrustManager
  • 使用这个 TrustManager 去初始化一个 SSLContext
  • SSLContext 提供一个 SSLSocketFactor
  • 使用这个 SSLSocketFactory 覆盖 HttpUrlConnectionSSLSocketFactory
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
val cf: CertificateFactory = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt"))
val ca: X509Certificate = caInput.use {
    ca.generateCertificate(it) as X509Certificate
}
System.out.println("ca=" + ca.subjectDN)

// Create a KeyStore containing our trusted CAs
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType).apply {
    load(null, null)
    setCertificateEntry("ca", ca)
}

// Create a TrustManager that trusts the CAs inputStream our KeyStore
val tmfAlgorithm: String = TrustManagerFoctory.getDefaultAlgorithm()
val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
    init(keyStore)
}

// Create an SSLContext that uses our TrustManager
val context: SSLContext = SSLContext.getInstance("TLS").apply {
    init(null, tmf.trustManagers, null)
}

// Tell the URLConnection to use a SocketFactory from our SSLContext
val url = URL("https://certs.cac.washington.edu/CAtest/")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.sslSocketFactory = context.socketFactory
val inputStream: InputStream = urlConnection.inputStream
copyInputStreamToOutputStream(inputStream, System.out)
2. Certificate Pinning With HttpsUrlConnection

使用 X509TrustManagerExtensions 可以将证书 pinning 到 app 中。X509TrustManagerExtensions.checkServerTrusted() 允许开发者在系统对证书链验证通过后,再次使用自己的方法验证证书链。

private void validatePinning(X509TrustManagerExtensions trustManagerExt, HttpsURLConnection conn, Set<String> validPins) throws SSLException {
    String certChainMsg = "";
    try {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        List<X509Certificate> trustedChain = trustedChain(trustManagerExt, conn);
        for (X509Certificate cert : trustedChain) {
            byte[] publicKey = cert.getPublicKey().getEncoded();
            md.update(publicKey, 0, publicKey.length);
            String pin = Base64.encodeToString(md.digest(), Base64.NO_WRAP);
            certChainMsg = " sha256/" + pin + " : " + cert.getSubjectDN().toString() + "\n";
            if (validPins.contains(pin)) {
                return;
            }
        }
    } catch(NoSuchAlgorithmException e) {
        thrown new SSLException(e);
    }
    throw new SSLPeerUnverifiedException("Certificate pinning failure\n" + "Peer certificate  chain:\n" + certChainMsg);
}

private List<X509Certificate> trustedChain(X509TrustManagerExtensions trustManagerExt, HttpsURLConnection conn) throws SSLException {
    Certificate[] serverCerts = conn.getServerCertificates();
    X509Certificate[] untrustedCerts = Arrays.copyOf(serverCerts, serverCerts.length, X509Certificate[].class);
    String host = conn.getURL().getHost();
    try {
        return trustManagerExt.checkServerTrusted(untrustedCerts, "RSA", host);
    } catch(CertificateException e) {
        throw new SSLException(e);
    }
}

使用方法如下:

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
// Find first X509TrustManagerFactory in the TrustManagerFactory
X509TrustManager x509TrustManager = null;
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
    if (trustManager instanceof X509TrustManager) {
        x509TrustManager = (X509TrustManager) trustManager;
        break;
    }
}
X509TrustManagerExtensions trustManagerExt = new X509TrustManagerExtensions(x509TrustManager);
...
URL url = new URL("https://www.xxx.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();

Set<String> validPins = Collections.singleton("4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=");
validatePinning(trustManagerExt, urlConnection, validPins);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容