轉自:(https://blog.csdn.net/liuwenjie517333813/article/details/78042819)
難得的一篇好文,把HTTPS禁荒,HTTPS下的MITM勘纯,證書校驗講清楚了帮辟。
0x00 引言
隨著安全的普及,https通信應用越發(fā)廣泛,但是由于對https不熟悉導致開發(fā)人員頻繁錯誤的使用https,例如最常見的是未校驗https證書從而導致“中間人攻擊”,并且由于修復方案也一直是個坑剿另,導致修復這個問題時踩各種坑,故謹以此文簡單的介紹相關問題帅腌。
本文第一節(jié)主要講述https的握手過程驰弄,第二節(jié)主要講述常見的“https中間人攻擊”場景麻汰,第三節(jié)主要介紹證書校驗修復方案速客,各位看官可根據(jù)自己口味瀏覽。
0x01 Https原理
首先來看下https的工作原理五鲫,上圖大致介紹了https的握手流程溺职,后續(xù)我們通過抓包看下每個握手包到底干了些什么神奇的事。
注:本文所有內(nèi)容以TLS_RSA_WITH_AES_128_CBC_SHA加密組件作為基礎進行說明位喂,其他加密組件以及TLS版本會存在一定差異浪耘,例如TLS1.3針對移動客戶端有了很大的改動,現(xiàn)在的ECDHE等密鑰交換算法與RSA作為密鑰交換算法也完全不一樣塑崖,所以有些地方和大家實際操作會存在一定出入七冲。
1.1 TCP
TCP的三次握手,這是必須的规婆。
1.2 Client Hello
TLS的版本號 隨機數(shù)random_c:這個是用來生成最后加密密鑰的因子之一澜躺,它包含兩部分,時間戳和隨機數(shù) session-id:用來標識會話抒蚜,第一次握手時為空掘鄙,如果以前建立過,可以直接帶過去從而避免完全握手 Cipher Suites加密組件列表:瀏覽器所支持的加密算法的清單客戶端支持的加密簽名算法的列表嗡髓,讓服務器進行選擇 擴展字段:比如密碼交換算法的參數(shù)操漠、請求主機的名字,用于單ip多域名的情況指定域名饿这。
1.3 Sever Hello
隨機數(shù)rando_s浊伙,這個是用來生成最后加密密鑰的因子之一撞秋,包含兩部分,時間戳和隨機數(shù) 32字節(jié)的SID吧黄,在我們想要重新連接到該站點的時候可以避免一整套握手過程部服。 在客戶端提供的加密組件中,服務器選擇了TLS_RSA_WITH_AES_128_CBC_SHA組件拗慨。
1.4 Certificate
證書是https里非常重要的主體廓八,可用來識別對方是否可信,以及用其公鑰做密鑰交換赵抢【珲澹可以看見證書里面包含證書的頒發(fā)者,證書的使用者烦却,證書的公鑰宠叼,頒發(fā)者的簽名等信息。其中Issuer Name是簽發(fā)此證書的CA名稱,用來指定簽發(fā)證書的CA的可識別的唯一名稱(DN, Distinguished Name)其爵,用于證書鏈的認證冒冬,這樣通過各級實體證書的驗證,逐漸上溯到鏈的終止點摩渺,即可信任的根CA简烤,如果到達終點在自己的信任列表內(nèi)未發(fā)現(xiàn)可信任的CA則認為此證書不可信。
驗證證書鏈的時候摇幻,用上一級的公鑰對證書里的簽名進行解密横侦,還原對應的摘要值,再使用證書信息計算證書的摘要值绰姻,最后通過對比兩個摘要值是否相等枉侧,如果不相等則認為該證書不可信,如果相等則認為該級證書鏈正確狂芋,以此類推對整個證書鏈進行校驗榨馁,引用高性能網(wǎng)絡中的證書鏈校驗圖。
二級機構的公鑰
網(wǎng)站證書的簽名
不僅僅進行證書鏈的校驗帜矾,此時還會進行另一個協(xié)議即Online Certificate Status Protocol, 該協(xié)議為證書狀態(tài)在線查詢協(xié)議翼虫,一個實時查詢證書是否吊銷的方式,客戶端發(fā)送證書的信息并請求查詢黍特,服務器返回正常蛙讥、吊銷或未知中的任何一個狀態(tài),這個查詢地址會附在證書中供客戶端使用灭衷。
1.5 Server Hello Done
這是一個零字節(jié)信息次慢,用于告訴客戶端整個server hello過程已經(jīng)結束。
1.6 ClientKeyExchange
客戶端在驗證證書有效之后發(fā)送ClientKeyExchange消息,ClientKeyExchange消息中迫像,會設置48字節(jié)的premaster secret劈愚,通過密鑰交換算法加密發(fā)送premaster secret的值,例如通過 RSA公鑰加密premaster secret的得到Encrypted PreMaster傳給服務端闻妓。PreMaster前兩個字節(jié)是TLS的版本號菌羽,該版本號字段是用來防止版本回退攻擊的。
從握手包到目前為止由缆,已經(jīng)出現(xiàn)了三個隨機數(shù)(客戶端的random_c注祖,服務端的random_s,premaster secret)均唉,使用這三個隨機數(shù)以及一定的算法即可獲得對稱加密AES的加密主密鑰Master-key是晨,主密鑰的生成非常的精妙,通過閱讀RFC2246文檔以及openssl的函數(shù)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);
函數(shù)中的參數(shù)定義如下:
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()的統(tǒng)稱檬寂。
A()的賦值相對比較復雜,變形后如下:
1 2 A(0)=HMAC(md5,1/2secret,strlen(1/2secret), actual_seed(0), strlen(label)+strlen(seed),temp_md5,NULL); //temp_md5數(shù)組用于接收最后生成的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數(shù)組用于接收最后生成的hash焰薄,attual_seed為label和seed結合
簡化表達如下:
1 A(i)=HMAC_hash(1/2secret,A(i-1)+seed)
由于只進行一次SHA-1或者MD5產(chǎn)生的hash字節(jié)數(shù)是不夠的拿诸,所以使用迭代的方式產(chǎn)生足夠多的hash扒袖,多出來的hash則直接拋棄。例如亩码,使用P_SHA-1()產(chǎn)生64字節(jié)的數(shù)據(jù)季率,就需要迭代4次(通過A(4)),產(chǎn)生80字節(jié)的輸出數(shù)據(jù),最后迭代產(chǎn)生的16字節(jié)將會被拋棄描沟,只留下64字節(jié)的數(shù)據(jù)飒泻,如果使用P_MD5()則需要迭代5次。
1.7 Change Cipher Spec
發(fā)送一個不加密的信息吏廉,瀏覽器使用該信息通知服務器后續(xù)的通信都采用協(xié)商的通信密鑰和加密算法進行加密通信泞遗。
1.8 Encrypted Handshake Message
驗證加密算法的有效性,結合之前所有通信參數(shù)的 hash 值與其它相關信息生成一段數(shù)據(jù)席覆,采用協(xié)商密鑰 session secret 與算法進行加密史辙,然后發(fā)送給服務器用于數(shù)據(jù)與握手驗證,通過驗證說明加密算法有效。
1.9 Change_cipher_spec
Encrypted Handshake Message通過驗證之后聊倔,服務器同樣發(fā)送 change_cipher_spec 以通知客戶端后續(xù)的通信都采用協(xié)商的密鑰與算法進行加密通信晦毙。這里還有一個New Session Ticket并不是必須的,這是服務器做的優(yōu)化耙蔑,后續(xù)我們再講解該協(xié)議的作用见妒。
1.10 Encrypted Handshake Message
同樣的,服務端也會發(fā)送一個Encrypted Handshake Message供客戶端驗證加密算法有效性甸陌。
1.11 Application Data
經(jīng)過一大串的的計算之后须揣,終于一切就緒,后續(xù)傳輸?shù)臄?shù)據(jù)可通過主密鑰master key進行加密傳輸钱豁,加密數(shù)據(jù)查看圖中的Encrypted Apploication Data字段數(shù)據(jù)返敬,至此https的一次完整握手以及數(shù)據(jù)加密傳輸終于完成。
https里還有很多可優(yōu)化并且很多精妙的設計寥院,例如為了防止經(jīng)常進行完整的https握手影響性能劲赠,于是通過sessionid來避免同一個客戶端重復完成握手,但是又由于sessionid消耗的內(nèi)存性能比較大秸谢,于是又出現(xiàn)了new session ticket凛澎,如果客戶端表明它支持Session Ticket并且服務端也支持,那么在TLS握手的最后一步服務器將包含一個“New Session Ticket”信息估蹄,其中包含了一個加密通信所需要的信息塑煎,這些數(shù)據(jù)采用一個只有服務器知道的密鑰進行加密。這個Session Ticket由客戶端進行存儲臭蚁,并可以在隨后的一次會話中添加到 ClientHello消息的SessionTicket擴展中最铁。雖然所有的會話信息都只存儲在客戶端上,但是由于密鑰只有服務器知道垮兑,所以Session Ticket仍然是安全的冷尉,因此這不僅避免了性能消耗還保證了會話的安全性。
最后我們可以使用openssl命令來直觀的看下https握手的流程:
0x02 中間人攻擊
https握手過程的證書校驗環(huán)節(jié)就是為了識別證書的有效性唯一性等等系枪,所以嚴格意義上來說https下不存在中間人攻擊雀哨,存在中間人攻擊的前提條件是沒有嚴格的對證書進行校驗,或者人為的信任偽造證書私爷,下面一起看下幾種常見的https“中間人攻擊”場景雾棺。
2.1 證書未校驗
由于客戶端沒有做任何的證書校驗,所以此時隨意一張證書都可以進行中間人攻擊衬浑,可以使用burp里的這個模塊進行中間人攻擊捌浩。
通過瀏覽器查看實際的https證書,是一個自簽名的偽造證書工秩。
2.2 部分校驗
做了部分校驗尸饺,例如在證書校驗過程中只做了證書域名是否匹配的校驗宏榕,可以使用burp的如下模塊生成任意域名的偽造證書進行中間人攻擊。
實際生成的證書效果侵佃,如果只做了域名麻昼、證書是否過期等校驗可輕松進行中間人攻擊(由于chrome是做了證書校驗的所以會提示證書不可信任)。
2.3 證書鏈校驗
如果客戶端對證書鏈做了校驗馋辈,那么攻擊難度就會上升一個層次抚芦,此時需要人為的信任偽造的證書或者安裝偽造的CA公鑰證書從而間接信任偽造的證書,可以使用burp的如下模塊進行中間人攻擊迈螟。
可以看見瀏覽器是會報警告的叉抡,因為burp的根證書PortSwigger CA并不在瀏覽器可信任列表內(nèi),所以由它作為根證書簽發(fā)的證書都是不能通過瀏覽器的證書校驗的答毫,如果將PortSwigger CA導入系統(tǒng)設置為可信任證書褥民,那么瀏覽器將不會有任何警告。
2.4 手機客戶端Https數(shù)據(jù)包抓取
上述第一洗搂、二種情況不多加贅述消返,第三種情況就是我們經(jīng)常使用的抓手機應用https數(shù)據(jù)包的方法,即導入代理工具的公鑰證書到手機里耘拇,再進行https數(shù)據(jù)包的抓取撵颊。導入手機的公鑰證書在android平臺上稱之為受信任的憑據(jù),在ios平臺上稱之為描述文件惫叛,可以通過openssl的命令直接查看我們導入到手機客戶端里的這個PortSwiggerCA.crt
可以看見是Issuer和Subject一樣的自簽名CA公鑰證書倡勇,另外我們也可以通過證書類型就可以知道此為公鑰證書,crt嘉涌、der格式的證書不支持存儲私鑰或證書路徑(有興趣的同學可查找證書相關信息)妻熊。導入CA公鑰證書之后,參考上文的證書校驗過程不難發(fā)現(xiàn)通過此方式能通過證書鏈校驗仑最,從而形成中間人攻擊扔役,客戶端使用代理工具的公鑰證書加密隨機數(shù),代理工具使用私鑰解密并計算得到對稱加密密鑰词身,再對數(shù)據(jù)包進行解密即可抓取明文數(shù)據(jù)包厅目。
2.5 中間人攻擊原理
一直在說中間人攻擊番枚,那么中間人攻擊到底是怎么進行的呢法严,下面我們通過一個流行的MITM開源庫mitmproxy來分析中間人攻擊的原理。中間人攻擊的關鍵在于https握手過程的ClientKeyExchange葫笼,由于pre key交換的時候是使用服務器證書里的公鑰進行加密深啤,如果用的偽造證書的公鑰,那么中間人就可以解開該密文得到pre_master_secret計算出用于對稱加密算法的master_key路星,從而獲取到客戶端發(fā)送的數(shù)據(jù);然后中間人代理工具再使用其和服務端的master_key加密傳輸給服務端;同樣的服務器返回給客戶端的數(shù)據(jù)也是經(jīng)過中間人解密再加密溯街,于是完整的https中間人攻擊過程就形成了诱桂,一圖勝千言,來吧呈昔。
通過讀Mitmproxy的源碼發(fā)現(xiàn)mitmproxy生成偽造證書的函數(shù)如下:
通過上述函數(shù)一張完美偽造的證書就出現(xiàn)了挥等,使用瀏覽器通過mitmproxy做代理看下實際偽造出來的證書。
可以看到實際的證書是由mimtproxy頒發(fā)的堤尾,其中的公鑰就是mimtproxy自己的公鑰肝劲,后續(xù)的加密數(shù)據(jù)就可以使用mimtproxy的私鑰進行解密了。如果導入了mitmproxy的公鑰證書到客戶端郭宝,那么該偽造的證書就可以完美的通過客戶端的證書校驗了辞槐。這就是平時為什么導入代理的CA證書到手機客戶端能抓取https的原因。
0x03 App證書校驗
通過上文第一和第二部分的說明粘室,相信大家已經(jīng)對https有個大概的了解了榄檬,那么問題來了,怎樣才能防止這些“中間人攻擊”呢?
app證書校驗已經(jīng)是一個老生常談的問題了衔统,但是市場上還是有很多的app未做好證書校驗鹿榜,有些只做了部分校驗,例如檢查證書域名是否匹配證書是否過期锦爵,更多數(shù)的是根本就不做校驗犬缨,于是就造成了中間人攻擊。做證書校驗需要做完全棉浸,只做一部分都會導致中間人攻擊怀薛,對于安全要求并不是特別高的app可使用如下校驗方式:
查看證書是否過期 服務器證書上的域名是否和服務器的實際域名相匹配 校驗證書鏈
可參考http://drops.wooyun.org/tips/3296,此類校驗方式雖然在導入CA公鑰證書到客戶端之后會造成中間人攻擊迷郑,但是攻擊門檻已相對較高枝恋,所以對于安全要求不是特別高的app可采用此方法進行防御。對于安全有較高要求一些app(例如金融)上述方法或許還未達到要求嗡害,那么此時可以使用如下更安全的校驗方式焚碌,將服務端證書打包放到app里,再建立https鏈接時使用本地證書和網(wǎng)絡下發(fā)證書進行一致性校驗霸妹,可參考安卓官方提供的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握手前可通過服務器下發(fā)證書信息,例如公鑰良价、辦法機構寝殴、簽名等蒿叠,該下發(fā)的信息使用服務器里的私鑰進行簽名; 通過app里預置的公鑰驗簽得到證書信息并存在內(nèi)容中供后續(xù)使用; 發(fā)起https連接獲取服務器的證書,通過對比兩個證書信息是否一致進行證書校驗蚣常。
這樣即可避免強升的問題市咽,但是問題又來了,這樣效率是不是低太多了?答案是肯定的抵蚊,所以對于安全要求一般的應用使用第一種方法即可魂务,對于一些安全要求較高的例如金融企業(yè)可選擇第二種方法。
說了挺多泌射,但是該來的問題還是會來啊!現(xiàn)在的app一般采用混合開發(fā)粘姜,會使用很多webveiw直接加載html5頁面,上面的方法只解決了java層證書校驗的問題熔酷,并沒有涉及到webview里面的證書校驗孤紧,對于這種情況怎么辦呢?既然問題來了那么就一起說說解決方案,對于webview加載html5進行證書校驗的方法如下:
webview創(chuàng)建實例加載網(wǎng)頁時通過onPageStart方法返回url地址; 將返回的地址轉發(fā)到java層使用上述的證書校驗代碼進行進行校驗; 如果證書校驗出錯則使用stoploading()方法停止網(wǎng)頁加載拒秘,證書校驗通過則正常加載号显。
提供參考代碼如下:
#!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");
}
}
}