上一篇《iOS安全系列之一:HTTPS》被CocoaChina轉載,還順便上了下頭條: 打造安全的App!iOS安全系列之 HTTPS,但那篇文章只是介紹了比較偏應用的初級知識,對于想要深入了解HTTPS的同學來說是遠遠不夠的,剛好本人最近工作上也遇到并解決了一些HTTPS相關的問題,以此為契機,決定寫這篇更深入介紹HTTPS的文章。
本文分為以下五節:
- 中間人攻擊:介紹中間人攻擊常見方法,并模擬了一個簡單的中間人攻擊;
- 校驗證書的正確姿勢:介紹校驗證書的一些誤區,并討論了正確校驗方式;
-
ATS:討論下 iOS 9.0 新發布的的特性
App Transport Security
; - 調試SSL/TLS:討論使用Wireshark進行SSL/TLS調試的方法;
- 后記
其中第1節“中間人”是比較常見基礎的知識,網上也可以找到相關的資料,如果對中間人攻擊已經有了足夠的了解,可以跳過。后面幾節則是個人在iOS方面的實踐總結,除了一些與系統相關的特性外,大部分都是系統無關的通用知識,并且每一章節都比較獨立,所以可以直接跳到感興趣的地方閱讀。
1. 中間人攻擊
關于HTTPS,我經常會提到的就是中間人攻擊,那究竟什么是中間人攻擊呢?中間人攻擊,即所謂的Man-in-the-middle attack(MITM),顧名思義,就是攻擊者插入到原本直接通信的雙方,讓雙方以為還在直接跟對方通訊,但實際上雙方的通信對方已變成了中間人,信息已經是被中間人獲取或篡改。
當然,本文并不是科普性文章,本節就針對HTTPS攻擊,特別是HTTPS在App這一應用場景下的常見的攻擊手段進行分析討論。
由前文我們知道,HTTPS在建立了TCP連接之后,會進行SSL握手(SSL Handshake)來校驗證書,協商加密協議和對稱加密的密鑰,之后就會使用協商好的密鑰來進行傳輸。所以HTTPS攻擊一般分為SSL連接建立前的攻擊,以及HTTPS傳輸過程中的攻擊;
常見的HTTPS中間人攻擊,首先需要結合ARP、DNS欺騙等技術,來對會話進行攔截,
1.1 SSL證書欺騙攻擊
此類攻擊較為簡單常見。首先通過ARP欺騙、DNS劫持甚至網關劫持等等,將客戶端的訪問重定向到攻擊者的機器,讓客戶端機器與攻擊者機器建立HTTPS連接(使用偽造證書),而攻擊者機器再跟服務端連接。這樣用戶在客戶端看到的是相同域名的網站,但瀏覽器會提示證書不可信,用戶不點擊繼續瀏覽就能避免被劫持的。所以這是最簡單的攻擊方式,也是最容易識別的攻擊方式。
此類攻擊有個經典的工具:SSLSniff。SSLSniff是大神Moxie Marlinspike開發的工具,該工具一開始是設計用于上一篇文章中提到的Basic Constaints 漏洞的,這類系統級別的漏洞,基本上可以讓你不知不覺;現在的操作系統和瀏覽器基本修復了這一漏洞。但也可以使用SSLSniff來偽造證書實現釣魚攻擊。
防范措施:
釣魚類攻擊,App直接調用系統API創建的HTTPS連接(NSURLConnection
)一般不會受到影響,只使用默認的系統校驗,只要系統之前沒有信任相關的偽造證書,校驗就直接失敗,不會SSL握手成功;但如果是使用WebView瀏覽網頁,需要在UIWebView中加入較強的授權校驗,禁止用戶在校驗失敗的情況下繼續訪問。
1.2 SSL剝離攻擊(SSLStrip)
SSL剝離,即將HTTPS連接降級到HTTP連接。假如客戶端直接訪問HTTPS的URL,攻擊者是沒辦法直接進行降級的,因為HTTPS與HTTP雖然都是TCP連接,但HTTPS在傳輸HTTP數據之前,需要在進行了SSL握手,并協商傳輸密鑰用來后續的加密傳輸;假如客戶端與攻擊者進行SSL握手,而攻擊者無法提供可信任的證書來讓客戶端驗證通過進行連接,所以客戶端的系統會判斷為SSL握手失敗,斷開連接。
該攻擊方式主要是利用用戶并不會每次都直接在瀏覽器上輸入https://xxx.xxx.com來訪問網站,或者有些網站并非全網HTTPS,而是只在需要進行敏感數據傳輸時才使用HTTPS的漏洞。中間人攻擊者在劫持了客戶端與服務端的HTTP會話后,將HTTP頁面里面所有的https://
超鏈接都換成http://
,用戶在點擊相應的鏈接時,是使用HTTP協議來進行訪問;這樣,就算服務器對相應的URL只支持HTTPS鏈接,但中間人一樣可以和服務建立HTTPS連接之后,將數據使用HTTP協議轉發給客戶端,實現會話劫持。
這種攻擊手段更讓人難以提防,因為它使用HTTP,不會讓瀏覽器出現HTTPS證書不可信的警告,而且用戶很少會去看瀏覽器上的URL是https://
還是http://
。特別是App的WebView中,應用一般會把URL隱藏掉,用戶根本無法直接查看到URL出現異常。
防范措施:
該種攻擊方式同樣無法劫持App內的HTTPS連接會話,因為App中傳入請求的URL參數是固定帶有https://
的;但在WebView中打開網頁同樣需要注意,在非全網HTTPS的網站,建議對WebView中打開的URL做檢查,檢查應該使用https://
的URL是否被篡改為http://
;也建議服務端在配置HTTPS服務時,加上“HTTP Strict Transport Security”配置項。
1.3 針對SSL算法進行攻擊
上述兩種方式,技術含量較低,而且一般只能影響 WebApp,而很難攻擊到 Native App , 所以高階的 Hacker,會直接針對SSL算法相關漏洞進行攻擊,期間會使用很多的密碼學相關手段。由于本人非專業安全相關人員,沒有多少相關實踐經驗,所以本節不會深入講解相關的攻擊原理和手段,有興趣的同學可以查看以下拓展閱讀:
防范措施:
這類攻擊手段是利用SSL算法的相關漏洞,所以最好的防范措施就是對服務端 SSL/TLS 的配置進行升級:
- 只支持盡量高版本的TLS(最低TLSv1);
- 禁用一些已爆出安全隱患的加密方法;
- 使用2048位的數字證書;
1.4 模擬最簡單的攻擊
經過上述幾種攻擊方式的說明之后,我們來模擬下最簡單的中間人攻擊。
中間人攻擊步驟方式的上文已經說過了,流量劫持相關操作不是本文重點,可以參考流量劫持是如何產生的?, 本例直接使用Charles來做代理,對流量進行劫持。并使用SSL代理來模擬下對iPhone設備HTTPS請求的中間人攻擊,讓大家在思考理解中間人攻擊方式的同時,了解在開發中如何防范類似的攻擊。
1) Charles設置代理
在Charles中開啟并設置HTTP代理和SSL代理,Menu -> Proxy -> Proxy Setting,設置如圖:
HTTP代理設置,注意記住端口號為:8888
SSL代理設置,在Locations上可以設置想要進行SSL代理的域名,這里以百度的百付寶*.baifubao.com
為模擬對象。
2) 在iPhone端設置HTTP代理
在Mac上獲取當前機器的IP地址:
ifconfig en0
:
還有一個簡單的方法,按住option+點擊頂部菜單欄的WiFi網絡圖標:
可以看到當前電腦的IP地址為:192.168.199.249
。
將iPhone連接到與電腦相同的WiFi,在iPhone設置中:無線局域網 -> 已連接WiFi右邊的Info詳情圖標 -> HTTP代理 -> 手動 -> 設置HTTP代理:
設置完成之后,打開Safari隨便訪問一個網頁,初次設置代理的話,Charles會彈出一個iPhone請求代理的確認框,點擊Allow即可。然后在Charles上就可以看到iPhone上的HTTP請求了。為了避免Mac上的請求過多影響對被代理iPhone上HTTP請求的查看和調試,可以在Charles取消Mac的代理:Menu -> Proxy -> 取消勾選Mac OS X Proxy 即可。
假如你訪問的是被代理的目標 URL http://www.baifubao.com 則打不開網頁。因為iPhone的HTTPS請求已經被Charles攔截,但iPhone無法信任Charles的證書,所以SSL Handshake失敗,無法建立HTTPS連接。
3) 偽造證書欺騙
在被代理的iPhone上打開Safari,訪問http://www.charlesproxy.com/getssl,會彈出安裝描述符文件的界面,該描述文件包含了Charles根證書:
注意:這個Charles證書是內置在Charles中的,可以在菜單Help -> SSL Proxying可以直接保存和安裝證書。安裝后的描述文件可以在iPhone設備的設置 -> 通用 -> 描述文件進行查看和管理。
“安裝”完成之后,就會將Charles根證書加入系統可信任證書列表中,使用該證書簽發的子證書也會被系統信任。Charles會為之前SSL代理設置中配置的域名生成對應的SSL證書,這樣偽造證書的證書就實現了欺騙。可以使用Mac SSL代理查看下:
4) 結果驗證
下載百度App,然后登錄賬號,在我 -> 我的錢包,就會訪問百付寶:
看到已成功獲取到HTTPS請求包的內容。從這里,我們可以猜測出該App是使用系統默認的校驗方式:系統信任了這個中間人服務器返回的SSL證書,App就信任了這一校驗,SSL握手成功;而沒有對服務器證書進行本地對比校驗。這是當下非常多App存在的安全隱患。
這個簡單的SSL代理模擬了簡單釣魚式的中間人攻擊,大家應該都基本明白了這種攻擊方式的所針對的漏洞,以及防范這種攻擊方法的措施:
- 不要隨意連入公共場合內的WiFi,或者使用未知代理服務器
- 不要安裝不可信或突然出現的描述文件,信任偽造的證書;
- App內部需對服務器證書進行單獨的對比校驗,確認證書不是偽造的;
2. 校驗證書的正確姿勢
上一節對中間人攻擊進行了簡單介紹,本節就上一節我們遇到的安全隱患問題,來討論下在App中,應該怎么校驗服務器返回的SSL證書,來保證HTTPS通信的安全。上一篇文章《iOS安全系列之一:HTTPS》有對基本校驗過程相關代碼進行講解,本文不會贅述這些細節,而是主要討論校驗證書中幾個重要的點:
2.1 域名驗證
前不久,iOS上最知名的網絡開源庫AFNetworking爆出HTTPS校驗漏洞,該漏洞是因為其校驗策略模塊 AFSecurityPolicy
內的參數 validatesDomainName
默認為NO,這會導致校驗證書的時候不會校驗這個證書對應的域名。即請求返回的服務器證書,只要是可信任CA機構簽發的,都會校驗通過,這是非常嚴重的漏洞。該漏洞已在v2.5.2版本中修復,對應Git版本號3e631b203dd95bb82dfbcc2c47a2d84b59d1eeb4。
這個漏洞以及AFNetworking的相關源碼會讓很多人以為系統的默認校驗是不校驗證書對應域名的,實際上并非如此。這里AFNetworking確有畫蛇添足之嫌。首先我們查看下系統的默認校驗策略:
<figure class="highlight" style="box-sizing: border-box; color: rgb(0, 0, 0); font-family: "PingFang SC", "Source Han Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 1px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px;">
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)獲取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
//獲取默認的校驗策略
CFArrayRef defaultPolicies = NULL;
SecTrustCopyPolicies(serverTrust, &defaultPolicies);
NSLog(@"Default Trust Policies: %@", (__bridge id)defaultPolicies);
//...
}
</figure>
打印默認校驗策略信息:
5 : <CFString 0x197814dc0 [0x196ea5fa0]>{contents = "ValidRoot"} = <CFBoolean 0x196ea6340 [0x196ea5fa0]>{value = true}
6 : <CFString 0x197814b20 [0x196ea5fa0]>{contents = "SSLHostname"} = <CFString 0x170226b60 [0x196ea5fa0]>{contents = "xxx.xxx.com"}
8 : <CFString 0x197814da0 [0x196ea5fa0]>{contents = "ValidLeaf"} = <CFBoolean 0x196ea6340 [0x196ea5fa0]>{value = true}
從打印信息來看,系統的默認校驗策略中已包含了域名校驗。然后再看AFSecurityPolicy
中相關源碼:
<figure class="highlight" style="box-sizing: border-box; color: rgb(0, 0, 0); font-family: "PingFang SC", "Source Han Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 1px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px;">
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
//...
}
</figure>
這其實也是很多開發者在處理異常與默認邏輯分支時會犯的錯誤,這段邏輯推薦實現方式是:
<figure class="highlight" style="box-sizing: border-box; color: rgb(0, 0, 0); font-family: "PingFang SC", "Source Han Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 1px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px;">
//取代validatesDomainName,默認為NO,就是系統默認行為
@property (nonatomic, assign) BOOL skipDomainNameValidation;
//校驗
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
if (self.skipDomainNameValidation) {
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
}
//...
}
</figure>
從代碼上看,邏輯是否變得更清晰了?而且也表明系統默認的校驗方式是會驗證域名的。實際上調用SecTrustSetPolicies
來重新設置校驗策略,主要是用于使用IP進行HTTPS請求,或者一個證書用于多個域名的場景;在這些場景下,服務器證書上的域名和請求域名(可能是IP,也可能是其他域名)就會出現不一致,導致校驗不通過;這就需要重新設置下校驗策略,把這個證書對應的域名設置下。詳細說明請查看官方文檔:《Overriding TLS Chain Validation Correctly》
2.2 校驗證書鏈?
上一篇文章介紹系統驗證SSL證書的方法和流程時,不是已經說明了會對證書鏈進行層層校驗,以保證證書的可信么?為什么還需要討論這一問題?其實本節要討論的是AFNetworking
中validatesCertificateChain
的問題。
先說明下結果:在AFNetworking
最新發布的V2.6.0,已經將該特性去掉了。相關的討論:SSL Pinning: What Should Be Certificate Chain Validation Expected Behavior?#2744
AFNetworking
中實現的驗證證書鏈,是將App本地打包好的證書與服務器返回的證書鏈進行數據上的一一對比,只有打包到App的證書中包含了服務器返回的證書鏈上的所有證書,校驗才會通過。如google的SSL證書:
開啟validatesCertificateChain
后請求https://google.com,需要將GeoTrust Global CA、Google Internet Authority G2和google.com的證書都導入App中才能驗證通過。請回憶下上一篇文章關于證書鏈的可信任機制,會發現這是完全沒有必要的;證書鏈的驗證,主要由三部分來保證證書的可信:葉子證書是對應HTTPS請求域名的證書,根證書是被系統信任的證書,以及這個證書鏈之間都是層層簽發可信任鏈;證書之所以能成立,本質是基于信任鏈,這樣任何一個節點證書加上域名校驗(CA機構不會為不同的對不同的用戶簽發相同域名的證書),就確定一條唯一可信證書鏈,所以不需要每個節點都驗證。
2.3打包證書校驗
那是否就不需要在App中打包證書進行驗證了呢?
這時需要想想為什么偽造證書是可以實現中間人攻擊的?答案就在于用戶讓系統信任了不應該信任的證書。用戶設置系統信任的證書,會作為錨點證書(Anchor Certificate)來驗證其他證書,當返回的服務器證書是錨點證書或者是基于該證書簽發的證書(可以是多個層級)都會被信任。這就是基于信任鏈校驗方式的最大弱點。我們不能完全相信系統的校驗,因為系統的校驗依賴的證書的源很可能被污染了。這就需要選取一個節點證書,打包到App中,作為Anchor Certificate來保證證書鏈的唯一性和可信性。
所以還是需要App本地打包證書,使用SecTrustSetAnchorCertificates(SecTrustRef trust, CFArrayRef anchorCertificates)
來設置Anchor Certificate進行校驗。需要注意的是,官方文檔《Certificate, Key, and Trust Services Reference》針對傳入的 Anchor Certificates 有說明:
IMPORTANT
Calling this function without also calling SecTrustSetAnchorCertificatesOnly disables the trusting of any anchors other than the ones specified by this function call.
也就是說,單純調用SecTrustSetAnchorCertificates
方法后不調用SecTrustSetAnchorCertificatesOnly
來驗證證書,則只會相信SecTrustSetAnchorCertificates
傳入的證書,而不會信任其他錨點證書。關于這一點,SecTrustSetAnchorCertificatesOnly
方法參數講解中也有說明:
anchorCertificatesOnly:
If true, disables trusting any anchors other than the ones passed in with the SecTrustSetAnchorCertificates function. If false, the built-in anchor certificates are also trusted. If SecTrustSetAnchorCertificates is called and SecTrustSetAnchorCertificatesOnly is not called, only the anchors explicitly passed in are trusted.
只相信傳入的錨點證書,也就只會驗證通過由這些錨點證書簽發的證書。這樣就算被驗證的證書是由系統其他信任的錨點證書簽發的,也無法驗證通過。
最后一個問題:選擇證書鏈的哪一節點作為錨點證書打包到App中?很多開發者會直接選擇葉子證書。其實對于自建證書來說,選擇哪一節點都是可行的。而對于由CA頒發的證書,則建議導入頒發該證書的CA機構證書或者是更上一級CA機構的證書,甚至可以是根證書。這是因為:
一般葉子證書的有效期都比較短,Google和Baidu官網證書的有效期也就幾個月;而App由于是客戶端,需要一定的向后兼容,稍疏于檢查,今天發布,過兩天證書就過期了。
越往證書鏈的末端,證書越有可能變動;比如葉子證書由特定域名(aaa.bbb.com)改為通配域名(*.bbb.com)等等。短期內的變動,重新部署后,有可能舊版本App更新不及時而出現無法訪問的問題。
因此使用CA機構證書是比較合適的,至于哪一級CA機構證書,并沒有完全的定論,你可以自己評估選擇。
3. ATS
在本文發表的時間(2015-09-03),大部分的iOS開發同學應該升級到iOS9了,在iOS9下進行HTTP/HTTPS請求時會遇到如下錯誤:
Request failed: Error Domain=NSURLErrorDomain Code=-1022 “The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.” UserInfo=0x7fbb4a158f00 {NSUnderlyingError=0x7fbb4a1141c0 “The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.”, NSErrorFailingURLStringKey=http://api.xxx.com/mobile, NSErrorFailingURLKey=http://api.xxx.com/mobile, NSLocalizedDescription=The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.}
這是iOS9中一個重大的更新:App Transport Security,簡稱ATS。ATS對使用NSURLConnection, CFURL, 或NSURLSession 等 APIs 進行網絡請求的行為作了一系列的強制要求,反逼服務器配置,以提高網絡數據傳輸的安全性:
These are the App Transport Security requirements:
- The server must support at least Transport Layer Security (TLS) protocol version 1.2.
- Connection ciphers are limited to those that provide forward secrecy (see the list of ciphers below.)
- Certificates must be signed using a SHA256 or better signature hash algorithm, with either a 2048 bit or greater RSA key or a 256 bit or greater Elliptic-Curve (ECC) key. Invalid certificates result in a hard failure and no connection.
ATS要求運行在iOS9的App,需將HTTP連接升級到HTTPS,并且TLS版本不得低于v1.2;而且規定了支持的加密套件(Cipher Suite)和證書簽名的哈希算法;如果想要向前兼容的話,可以通過設置Info.plist來降低校驗強度,具體可以看這篇文章:Configuring App Transport Security Exceptions in iOS 9 and OSX 10.11。
本人升級到iOS9 GM版,從App Store上下載了一些并沒有完全支持ATS的應用,使用起來也完全沒有問題,估計iOS系統對使用低于SDK9編譯的App做了兼容,這方面也是符合預期的,畢竟ATS的影響實在太大,基本上沒有任何的App能夠幸免,比如圖片下載一般使用HTTP,而不會使用HTTPS。所以建議可以暫時使用NSAllowsArbitraryLoads
來取消ATS的限制,后續慢慢完善對ATS的支持。
日益復雜脆弱的網絡難以保證用戶的數據安全,因此Apple才在iOS9上強推ATS,反向逼迫服務端升級,以提供更安全的網絡環境。建議開發者不要簡單地將ATS禁用,而應該升級服務器的配置支持ATS,為用戶提供更安全的服務。
4. 調試SSL/TLS
開發一個新的App,通常終端和后端先協商好了具體業務邏輯的通信協議,后端和終端按照協議實現邏輯之后,就進入聯調階段,第一次聯調往往會回到很多問題,包括數據格式不對,缺少基礎字段等;假如是基于HTTPS的網絡請求,則很可能由于后臺配置問題,導致遇到如CFNetwork SSLHandshake failed (-9824)
這類握手失敗的錯誤。面對這類SSL錯誤,該如何來解決呢?根據本人經驗,主要是分兩步:
4.1 錯誤碼
這會不會太簡單了?其實最簡單的往往是最有效的。SSL相關錯誤碼可以在<Security/SecureTransport.h>
中找到。上面-9824
的錯誤,對應的是errSSLPeerHandshakeFail = -9824, /* handshake failure */
,其他常見的錯誤碼還有:
<figure class="highlight" style="box-sizing: border-box; color: rgb(0, 0, 0); font-family: "PingFang SC", "Source Han Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 1px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px;">
//...
/* fatal errors detected by peer */
errSSLPeerUnexpectedMsg = -9819, /* unexpected message received */
errSSLPeerBadRecordMac = -9820, /* bad MAC */
errSSLPeerDecryptionFail = -9821, /* decryption failed */
errSSLPeerRecordOverflow = -9822, /* record overflow */
errSSLPeerDecompressFail = -9823, /* decompression failure */
errSSLPeerHandshakeFail = -9824, /* handshake failure */
errSSLPeerBadCert = -9825, /* misc. bad certificate */
errSSLPeerUnsupportedCert = -9826, /* bad unsupported cert format */
errSSLPeerCertRevoked = -9827, /* certificate revoked */
errSSLPeerCertExpired = -9828, /* certificate expired */
errSSLPeerCertUnknown = -9829, /* unknown certificate */
errSSLIllegalParam = -9830, /* illegal parameter */
errSSLPeerUnknownCA = -9831, /* unknown Cert Authority */
errSSLPeerAccessDenied = -9832, /* access denied */
/* more errors detected by us */
errSSLHostNameMismatch = -9843, /* peer host name mismatch */
errSSLConnectionRefused = -9844, /* peer dropped connection before responding */
errSSLDecryptionFail = -9845, /* decryption failure */
errSSLBadRecordMac = -9846, /* bad MAC */
errSSLRecordOverflow = -9847, /* record overflow */
errSSLBadConfiguration = -9848, /* configuration error */
//...
</figure>
但靠錯誤碼只能判斷大概的情況,很多時候并不能明確知道到底是什么原因導致的,所以最直觀的,還是需要抓包分析。
4.2 抓包分析
在這一階段,使用Charles來抓包是沒有用的,因為Charles是作為HTTP代理工作的,它會抓取代理的網絡報文,然后將報文組合成HTTP/HTTPS協議包,對于HTTP調試非常方便,但由于細節的缺失,沒辦法使用它來分析SSL相關錯誤。所以我們需要使用上古神器Wireshark。
關于Wireshark就不再多介紹了,網上已經有很多相關介紹和抓包教程,如《Mac OS X上使用Wireshark抓包》等,基本上可以很快上手。下面我們就以適配iOS9的ATS為例,來說下如何進行抓包分析,找出因為不支持ATS導致SSL握手失敗問題。
還記得SSL握手過程么?不記得可以重溫下這篇文章:圖解SSL/TLS協議。我們也來看看Wireshark上抓取到的包來直觀學習正常的SSL握手流程:
上圖是一個標準的HTTPS請求抓取的包:
- 在TCP三次握手成功之后,客戶端發起SSL的
Client Hello
(No.68幀),傳遞隨機數(Random),和客戶端支持的加密套件(Cipher Suites)、壓縮方法、簽名算法等信息; 如下圖所示,這是Client Hello
所攜帶的信息,可以展開來看相關的詳情:
- 服務器從
Client Hello
中匹配支持的加密套件(Cipher Suites)、壓縮算法和簽名算法,和服務器新生成的一個隨機數返回給客戶端,這就是Server Hello
(No.70幀)。 下圖就是對1)中Client Hello
的回應,由圖可以看出,服務端匹配的Cipher Suite是TLS_DHE_RSA_WITH_AES_256_CBC_SHA256:
- 服務器同時會將證書發給客戶端(No.73幀);有時候抓取的包只有
Client Hello
和Server Hello
,而沒有再發送證書的,這是SSL/TLS的Session重用了:由于新建立一個SSL/TLS Session的成本太高,所以之前有建立SSL/TLS連接Session的話,客戶端會保存Session ID,在下一次請求時在Client Hello
中帶上,服務端驗證有效之后,就會成功重用Sesssion。
注:關于重用TLS Session,在特定場景下會引發嚴重的問題:當App只針對了代碼中發起的HTTPS請求做了本地證書校驗,而WebView中發起的HTTPS請求并沒有做本地證書校驗,可能會出現App內代碼發起的請求直接重用WebView中建立的HTTPS鏈接,導致中間人可以實現短暫的繞過攻擊。
拓展閱讀:
- RFC5246#Handshake Protocol Overview查看Handshake的流程和相關信息。
- Apple官方開發文檔:TLS Session Cache
客戶端確認證書有效,則會生產最后一個隨機數(Premaster secret),并使用證書的公鑰RSA加密這個隨機數,發回給服務端。為了更高的安全性,會改為Diffie-Hellman算法(簡稱DH算法);采用DH算法,最后一個隨機數(Premaster secret)是不需要傳遞的,客戶端和服務端交換參數之后就可以算出。
Client Key Exchange
(No. 75幀);接下來雙方都會發送
Change Cipher Spec
通知對方,接下來的所有消息都會使用簽名約定好的密鑰進行加密通信。最后是雙方的
Finished Message
(即Encrypted Handshake Message
, No. 77、79幀),這個消息是最終的校驗,里面包含了握手過程中的Session Key等信息,如果對方能夠解密這個消息則表示握手成功,結束整個SSL Handshake流程。
掌握了SSL/TLS握手流程之后,調試SSL/TLS就會變得非常簡單,只需要看在哪個環節報錯(Alert),就可以基本推斷出相關的錯誤。
相關SSL/TLS接口信息,請查看:RFC5246以及SSL/TLS in Detail
下面就列舉下調試適配ATS過程中遇到的主要問題:
- 加密套件(Cipher Suite)等參數無法匹配:加密套件不匹配是最常見的握手失敗的例子。
在ATS中,可接受的加密套件有包括:
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
但往往很多服務器的HTTPS配置很久沒有升級,沒辦法支持這些Cipher Suite;客戶端發送Client Hello
給服務端,帶上支持加密套件參數;服務端查看這些參數,發現一個都不支持,則直接返回Handshake Failure
的信息。如下圖:
一般在接受到客戶端發送的Client Hello
后返回Handshake Failure
,都是因為服務端無法匹配客戶端SSL握手參數。至于是不是加密套件這個參數匹配的問題,建議抓取取消ATS了的正常HTTPS請求包進行對比,找出具體不匹配的參數。
SSL/TLS版本過低,這個也非常常見,但一般會被上一個參數不匹配的錯誤所掩蓋。因為大多數SSL/TLS版本低的服務器HTTPS配置支持的加密套件等參數版本也比較低,而SSL/TLS版本是客戶端收到
Server Hello
之后才驗證的,但前面握手失敗就走不到這一步了。所以加密套件(Cipher Suite)等參數無法匹配支持,一般也就意味著服務端SSL/TLS版本過低。證書鏈配置錯誤:在開發過程中,本人遇到過證書鏈沒有按照順序進行配置的問題,也遇到過只配置了葉子證書的問題。對于這些問題,可以直接查看SSL握手過程中,服務端返回的
Certificate
包:
上圖可以看到證書鏈Certificates
只有一個,這是典型的配置錯誤。
PS:使用Wireshark進行抓包的時候,有時候由于一些HTTPS請求的SSL/TLS版本號太低,Wireshark沒辦法辨認其是SSL包,而是顯示為TCP;此時可以手動來Decode:選擇對應的TCP數據幀,右鍵 -》Decode As -》Transport 選擇SSL -》Apply既可。
5. 后記
這個時代,安全重要么?這是我曾常疑惑的。90%以上的大眾對安全沒有切實的概念,即使安全上了春晚,過了熱潮一切又重歸原樣。特別最近換工作到保險金融類公司,安全問題更是觸目驚心。一直相信,人如同一個圓,你知道的越多,學的越深,接觸的越廣,圓就越大,越知道自己的渺小,越懂得敬畏。
這世界永遠不會缺少矛和盾,沒有“Mission Impossible”,不是么?