安卓端出現https請求失敗的一次問題排查

三國夢迴發表於2023-12-02

背景

某天早上,正在一個會議時,突然好幾個同事被叫出去了;後面才知道,是有業務同事反饋到領導那裡,我們app裡面某個功能異常。

具體是這樣,我們安卓版本的app是禁止截圖的(應該是app裡做了攔截),但部分頁面,支援配置成可以截圖。這個配置是透過後端介面獲取的,意思就是,如果呼叫這個介面失敗,就整個app預設不能截圖;如果呼叫成功,就可以在配置的指定頁面截圖。

業務反饋就是說,之前可以截圖的幾個頁面,現在突然不能截圖了,不知道是不是我們搞了啥變更;後面產品去業務那深入瞭解了下,發現:連線公司wifi後就不能截圖,用4g/5g是可以的。

排查過程

前期排查

安卓開發首先介入,具體方式就是,因為可以復現,找了個安卓裝置,連線電腦就可以debug app(沒搞過安卓,具體不清楚),後面說是獲取截圖配置的介面(https)報錯了:

ret:
java.security.cert.CertPathValidatorException: Trust anchor for certification path not found

image-20231202110550337

丟出這個後,就沒有進一步的動作了,認為不是安卓端的問題,因為用5g就可以,只是wifi不行。然後問題就卡在那了。有人又丟出之前的一個變更通知,那次變更是這樣,之前我們https證照解除安裝都是在業務伺服器的nginx做的,這樣的話,每個業務都會有自己的nginx,每個nginx都要負責https加解密,後來就提出來,要把這個https加解密前置,後面就前置到了負載均衡裝置(比如典型的硬體負載均衡裝置:F5)。

有人就說是不是動了這個導致的,雖然這個極有可能,但是,沒有人去查,去確認。

後端開始介入

因為安卓側認為自己沒問題,產品後面來找我,我才開始介入這個問題。

下午先了解了下整個事情,比較重要的事情是,拿到了復現問題的手機,然後試著連線電腦charles進行抓包,才想起來安卓目前抓包非常困難,在電腦端用charles、fiddler這類代理是沒有用的;那就只能找安卓開發看這個,我本來預期的是,在他那裡,透過debug,要知道這個錯誤到底是什麼導致的,比如是https的哪個階段,是不是https證照的哪個欄位有啥問題,結果,最終和我說的是,這個是底層okhttp的,沒法debug到那一層;我其實是對這塊持懷疑態度,肯定是有辦法的,但可能他不會,從沒深入過https這層,所以就說他沒辦法繼續定位到更多資訊了。

他麼當時火也大,但問題還是得解決(後面我看到貨拉拉那個文章裡,其實是可以debug那部分程式碼,不過確實是不在android.jar原始碼裡,在單獨的模組中)。

安卓端沒法看,電腦端沒法用簡單的方式抓包,我瞭解到的一些抓包的辦法都是很複雜,不搞安卓開發的話,光是搭環境都要搭半天那種;要麼就是在手機上裝抓包軟體,但有些需要root,且能不能抓https這層檢查證照,我也持懷疑態度,我個人又是垃圾iphone,對安卓確實不熟悉。

唯一的辦法,就只有:wifi路由器上抓包,或者是找到目前負責https加解密的負載均衡裝置的同事,來進行抓包。

搜尋引擎查詢可能原因

證照鎖定

拿那個錯誤,查了下原因,查到一篇貨拉拉的文章,感覺比較靠譜。

https://mp.weixin.qq.com/s/Je1Kf0UX9pkwedaL7pTe3A 貨拉拉SSL證照踩坑之旅

裡面提到,app內部可能內建了服務端的證照,而app在訪問https後端建立https連線的過程中,服務端會把自己的證照(一般配置在nginx,我們這邊就是負載均衡裝置,F5)返回給app,app檢查到返回的證照如果和本地內建的不一致,就可能報那個錯;

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found

這個專業術語叫做:證照鎖定. (https://zhuanlan.zhihu.com/p/58204817)

這種就是可以防止中間人攻擊的,如fiddler、charles這類基於代理的,基本就屬於中間人攻擊,因為charles他們會把自己的證照給我們,我們內建了證照的話,就會發現charles證照和內建證照不一致,就可以主動終止連線。

好些安卓的專業抓包方案,就是基於hook,把證照校驗的那些程式碼都給hook掉,這類方案對於非安卓開發人員還是困難了一點,要一整套工具鏈,以後換個遙遙領先的話,可以好好折騰下。

另外,如果真用了證照鎖定,那麼根據貨拉拉文章內容,新證照可能少了某個欄位,導致這個問題:

image-20231202115202773

檢測網站

https://myssl.com/

可以輸入自己的網址,檢查下,不一定準,我們的問題當時就沒查出來。

檢查安卓端配置

可能有如下這個配置檔案,看看裡面的內容,這裡面也涉及一些trust-anchor的內容:

圖片

負載均衡裝置抓包

排除後端嫌疑

次日,我直接找了app端的leader,結果leader反饋說,app沒搞證照鎖定那些高階玩意,其他配置也檢查了,好像沒啥問題,所以無疾而終。

然後去找了負載均衡裝置的同事,同事還是非常支援,所以,那天下午,我們就在一塊,在負載均衡裝置上,抓了一下午的包。

他首先懷疑的是,後端服務返回的內容是不是有問題,因為,用他手機嘗試時,一會可以截圖,一會不可以,就是沒能穩定復現。

於是就抓取負載裝置和後端nginx之前的報文,這塊我們面臨一個問題,負載上流量很大,怎麼區分出他手機的流量呢?尤其是現在好多手機都是優先用Ipv6,而目前在百度這種查ip,基本只顯示了ipv4

那天我看同事用的ip138.com,我今天又搜了一個:https://ipw.cn/

都還不錯。

1701490193819

所以我們就抓負載和nginx之間的包,包裡會有欄位帶了我們的手機的出口ip:

image-20231202121151609

就用這個欄位篩選出我們的流量後,檢查發現,後端返回的內容沒啥問題。

後面和那個能穩定復現的安卓裝置比較,發現是同事手機的app版本低了,艹,升到最新版,就能穩定復現了。

各種場景對比

後面就開始對比,從公網過來,和從wifi過來的包;再就是,安卓裝置端公網出口ip為ipv4和ipv6的,這麼一組合,就有4種組合。

後面發現,公網過來的,不管是ipv4還是ipv6,都沒問題;從wifi過來的,我們這邊測試,好像都是有問題的,但我們也抓包發現了其他人的請求,看著好像是從wifi來的,又沒問題的。

這期間其實探索了很多可能性,比如也檢查了waf裝置(waf裝置比負載均衡裝置還要靠前,且waf工作在7層,也會涉及https的加解密,我是有懷疑過waf,但當時看了waf的日誌啥的,沒發現異常)

另外,這期間,我也在自己的雲伺服器上,嘗試瞭如下方式:

 openssl s_client -debug -connect xxx.com.cn:443
 
 tcpdump -i any host xxx.com.cn and tcp port 443 -w 443.pcap

和負載均衡端側的抓包進行交叉對比。

對比的場景太多,都記不清了,但最終確定的是,wifi網路下,出口ip是ipv4還是ipv6來著的時候,就有問題。

其實我一開始就是懷疑證照那塊可能有問題,但是,也不能在沒找到確切原因的時候,貿然對證照進行操作,所以就和負載均衡裝置的同事搞了一下午。

雖然當時沒確定出根因,但收穫包括:

流量情況下,訪問xxx.com.cn:443是直接到xxx.com.cn:443的防火牆裝置;

wifi下,訪問xxx.com.cn:443也是繞到了公司的網際網路出口,再去訪問xxx.com.cn:443的防火牆裝置;

但是,可以肯定的是,這兩種情況下,xxx.com.cn:443的防火牆那邊,肯定是配置了不同的路由策略,兩者的網路路徑應該是不一樣的,這塊就還得找具體負責防火牆的同事來一起看。

本機模擬發現新端倪

我們不是在負載均衡和nginx那層抓了包嗎,那層是明文的,我們就照著那個明文,錄入到本機的postman裡,呼叫,發現是成功的。

後來,我想是不是postman沒校驗證照,所以才成功的,然後找了找,發現確實有這麼個選項:

image-20231202132420296

預設是false,不校驗,我打卡後,再一請求,果然報錯了,不過報的是服務端返回的證照缺少了中間證照。

所謂的中間證照,可以這麼理解,目前世界上,有一批權威機構(ROOT CA),他們負責給大家頒發https證照,頒發的證照會給到我們,然後我們就放到伺服器上。

瀏覽器、手機等客戶端訪問我們時,我們就把證照返回給瀏覽器等,此時,他們怎麼知道我們的證照是真的假的呢,就是靠證照裡的頒發者欄位,他們找到頒發者,再和自己瀏覽器內建的或者作業系統中內建的ROOT CA白名單做一個匹配,如果在本機內建的ROOT CA白名單中,就可以認為證照確實是這些權威機構頒發的,值得信賴。(當然,這只是其中的一個檢查項,不是全部,比如還要檢查證照是否在有效期內,是否已經被吊銷了)

但是哈,一般我們的證照,不會是這些ROOT CA直接頒發的,而是ROOT CA下屬的某個中間證照頒發的,以下面百度的為例:

image-20231202133132969

此時,百度服務端就必須返回baidu.com這個證照,但是它是由中間證照籤發的,而一般作業系統或者瀏覽器沒內建中間證照那些機構,所以,服務端一般要把baidu.com以及中間證照機構的證照,一併返回,這樣,才能一層層找到中間證照的簽發者,然後發現簽發者是root ca的話,就和本機的白名單做對比。

另外,我也在本機對了對照組,postman在兩種網路下發請求:

  • 本機pc在公司wifi下,此時,走的是公司wifi
  • 本機pc連線手機的熱點,此時,走的是流量網路

對比了下,發現真的有問題:

image-20231202133649730

在這兩種情況下,客戶端首先發請求(client hello)和服務端協商後續用哪個版本的tls協議。客戶端發出去的請求我對比了,除了隨機數部分,基本一致,但是,服務端最終協商出來的結果卻不一樣,一個是tls v1.2 ,一個是tls v.1.3

從這裡也驗證了,這個xxx.com.cn:443的接入這塊(一般接入那裡應該是路由器,但一般好像也具有防火牆的功能),會根據客戶端的網路來源於wifi和流量,走了不同的路線。

這塊也得具體諮詢接入這塊的同事了。

補齊證照鏈解決問題

結果我們後續還沒來得及去找接入的同事,負責負載均衡裝置的同事跟我說,他把證照鏈補充完整了,讓我再試試。

所謂證照鏈補齊了的意思是,他之前就是負責將nginx層的證照挪到了負載均衡裝置,在他完成這次變更後,https建立連線時,每次服務端就只返回兩層證照了:

image-20231202143535618

其實更好的辦法是用openssl工具,因為上面這個方法我發現也不一定準確,我之前確實是發現有返回3層證照(含root ca)的時候,但我寫文章這會,測試了下,發現又只有兩層了。

但是,用openssl進行如下測試,都是能看到三層證照的:

 openssl s_client -debug -connect xxx.com.cn:443
 或
 [root@VM-0-6-centos ~]# openssl s_client -showcerts -verify 5 -verify_return_error -connect xxx.com.cn:443
CONNECTED(00000003)
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert Secure Site CN CA G3
verify return:1
depth=0 C = CN, ST = xxxx, CN = *.xxx.com.cn
verify return:1

對我對上述openssl命令,同時抓包的話,包內顯示依然只有兩層證照,我之前看一本書裡也說,一般也是不推薦返回ROOT CA的證照的,沒必要:

image-20231202152459708

遺留問題

因為問題解決了,也就沒有再去找負責xxx.com.cn網路接入的同事查問題了,大家事情也多,就這樣吧,事情搞定就行了。

但這也算隱形的坑,我猜測的話,可能是一個鏈路走了waf,一個鏈路沒走waf;所以最終一個協商出用tls v1.2,一個協商出用tls v1.3.

補充問題

我翻到一個8月份的抓包檔案:跟隨追蹤.pcap,裡面的話,服務端確實是返回了3層證照的,包括了ROOT CA的,如下:

image-20231202153427056

所以,我現在也有點疑問了,到底他麼該返回幾層呢,只能說,如果大家遇到這類問題,可以往這個方面試一下,這個https水還是比較深的。

curl知識補充

平時經常用curl,但遇到https這種時,一般會失敗;此時,習慣性加個-k,跳過https證照校驗.

-k, --insecure
              (SSL)  This  option  explicitly  allows  curl  to  perform  "insecure"  SSL connections and transfers. All SSL connections are
              attempted to be made secure by using the CA certificate bundle installed by default. This  makes  all  connections  considered
              "insecure" fail unless -k, --insecure is used.

              See this online resource for further details: http://curl.haxx.se/docs/sslcerts.html
[root@VM-0-6-centos ~]# curl https://www.baidu.com
curl: (77) error setting certificate verify locations:  CAfile: /etc/ssl/certs/ca-certificates.crt CApath: none

[root@VM-0-6-centos ~]# curl https://www.baidu.com -k
<!DOCTYPE html>
...

但是,這次是要解決https的問題,肯定不能跳過了,所以研究了下怎麼把root ca裝到機器上,我是centos機器,我發現這樣就可以了:

root ca檔案參考:https://curl.se/docs/caextract.html 
wget https://curl.se/ca/cacert.pem -k  下載到cacert.pem

然後指定下ca檔案: 
[root@VM-0-6-centos ~]# curl --cacert cacert.pem   https://www.baidu.com

參考文件

https://mp.weixin.qq.com/s/Je1Kf0UX9pkwedaL7pTe3A 貨拉拉SSL證照踩坑之旅

https://mp.weixin.qq.com/s/bGc-GScIEn_1cqZ64A7E3Q

https://mp.weixin.qq.com/s/5Cfwli0aC-ueaXTi0Pwfyw

https://mp.weixin.qq.com/s/zAFkcDBTNjfDLAnzbi5j6Q

https://cloud.tencent.com/developer/article/1973401

https://mp.weixin.qq.com/s/eKLNLj7ZqD80kZbsjQzS6Q

https://mp.weixin.qq.com/s/xFj9fjQ7crc2RnR5ckTfpw

https://mp.weixin.qq.com/s/XD8cvqb1ScWMxEwhnwaVtg

https://mp.weixin.qq.com/s/7-iQtXifIvwyXcleO2rpzw

https://mp.weixin.qq.com/s/_bVnCAheO5e71iniSzTLcg

https://mp.weixin.qq.com/s/faExv_-y0MxTFBoum7csHQ

openssl: man openssl

openssl s_client : man s_client

相關文章