一場開源 RSA 庫引發的“血案”
導 讀
問題背景
在 GitHub 上搜索 RSA,選擇 Objective-C 語言過濾,結果如下:
在匹配度、點贊、復刻等多維度搜索結果中,Objective-C-RSA 都名列第一,可以稱得上是 ObjC 中質量最好的 RSA 演算法開源庫。在我們事後的調研中,發現快手激勵影片 、高德地圖、電信免密登入、極驗等許多三方 SDK 都使用了該演算法庫,另外百度、京東、快手、美團、淘寶、微信等大型 App 都直接或間接的依賴了該庫。
-
使用 strings 查詢字串
使用反編譯工具查詢 RSAUtil_PubKey 字串引用
注:1. 上述分析只查找了各 App 主二進位制中是否存在 Objective-C-RSA 庫中的 RSAUtil_PubKey 字串,結果僅供參考,如有出入,實屬正常。2. 在併發環境下,RSA 加密會有一定概率出現異常,非併發環境一般不會有問題。各 App 應根據業務場景,具體問題具體分析。
在最近的專案中,我們也有用到 RSA 演算法,Objective-C-RSA 作為 iOS 端質量最佳的 RSA 開源庫,開發同學優先選擇了該庫。由於當時專案特殊,開發週期比較緊張,整個開發、測試過程中,都沒能發現潛在的問題。待專案上線後,後端同學監控發現,RSA 解密過程出現大量異常資料,主要包括以下兩種錯誤型別:
RSA 加密結果為空
加密資料無法解密
欲知是何原因,且看下文分解!
都是併發惹的禍
在分析伺服器端提供的錯誤日誌後,客戶端研發同學第一反應可能是併發引起的問題。仔細閱讀Objective-C-RSA 說明文件後,發現作者在 README 文件中簡單提到執行緒不安全,但具體原因未寫。而後又翻閱了相關 issue,曾有人提到過該問題(詳見 issue#50),作者回復給出的方案是“避免在業務層多執行緒呼叫加密方法”。作為一千多 Star 的開源庫,無論在說明文件,還是 issue 解決中,如此草率,真是坑人,開發同學看完直欲噴人。要是 Linus Torvalds 看到這樣的程式碼,畫面簡直不敢想象。
問題至此,只怪自己學藝不精,噴亦無用,還是要優先解決問題。開發同學通過編寫併發測試用例,發現在 addPublicKey: 方法中,呼叫SecItemCopyMatching 方法時,會出現查詢資料失敗的情況,錯誤碼為-25300,具體錯誤原因為The specified item could not be found in the keychain,即『在 Keychain 中未找到指定的資料項』。
執行緒不安全,難道 Keychain 中的 API 執行緒不安全嗎?這鍋應該蘋果來背,開始我們也是這樣認為的。之後又查閱的了更多資料,發現許多開發者都有 Keychain API 是否執行緒安全的困惑。蘋果在官方文件 Certificate, Key, and Trust Services 併發章節中提到:
In macOS, some of the functions of this API block while waiting for input from the user (for example, when the user is asked to unlock a keychain or give permission to change trust settings). In general, it is safe to use this API in threads other than your main thread, but avoid calling the functions from multiple operations, work queues, or threads concurrently. Instead, serialize function calls or confine them to a single thread.In iOS, all the functions in this API are thread-safe and reentrant.
大意翻譯一下:
在 macOS 中,證書、金鑰、信任服務 API 中的一些函式在等待使用者輸入時會被阻塞(例如,當用戶被要求解鎖鑰匙串或允許改變信任設定時)。一般來說,線上程中使用該 API 是安全的,不僅僅是主執行緒,但要避免在多個操作、工作佇列或執行緒中同時呼叫這些函式。相反,要將函式呼叫序列化,或將其限制在一個執行緒中。在 iOS 中,該 API 中的所有函式都是執行緒安全和可重入的。
從蘋果官方文件看來,Keychain 中相關的 API 都是執行緒安全的,那麼問題又出自哪裡?我們來看下 +[RSA addPublicKey:] 方法的具體實現:
上述方法使用鑰匙串 API 將 RSA 公鑰字串,新增到鑰匙串中,最後從鑰匙串中讀取返回 SecKeyRef 結構。我們知道 Keychain 中的資料儲存於系統提供的共享資料庫中,是位於磁碟上的資料。在越獄裝置中使用Keychain-Dumper工具可以檢視其中資料。
作者在生成 RSA 公鑰資料結構 SecKeyRef 過程中,同時使用了鑰匙串的刪除(SecItemDelete)、新增(SecItemAdd)、查詢(SecItemCopyMatching)介面,其中每個 API 都是執行緒安全的。真正的問題在於,多執行緒環境中組合使用上述 API,同時讀寫鑰匙串中的資料,並不能保證資料正確性。當一個執行緒在讀取鑰匙串中資料時,另外一個執行緒碰巧將資料刪除,這時會出現讀取鑰匙串資料為空的情況,此正是上文提到的-25300錯誤的原因。
命名的藝術
上文提到伺服器端反饋過兩種錯誤型別,解決完併發問題,RSA 加密結果為空的資料得到解決。但另外一個問題,客戶端 RSA 加密資料,伺服器端解密異常,在我們上百萬併發測試的過程中,始終沒能復現(當時使用了測試 Demo 驗證,與工程環境有所差異)。曾一度提出各種猜想:雙端 RSA Padding 不一致、ObjC 與 Java 端 RSA 演算法相容性有問題、後端的解密程式碼有問題、亦或資料傳輸過程中有丟失。通過仔細分析,這些假定都被我們一一排除。在始終找不到頭緒的情況下,又讓後端提供了數萬條錯誤日誌,一位細心的同事在其中發現了端倪,撥開層層迷霧,看到一縷曙光。
在後端提供的錯誤日誌中,少量解密失敗的資料,長度恰好是正常資料的一半,加密結果莫明其妙短了一半,這僅僅是巧合嗎?我們知道 RSA 加密後的密文位長跟金鑰的位長度是相同的,2048 位的金鑰長度,為何會出現 1024 位的加密結果?再次細讀 Objective-C-RSA 加密實現程式碼,還是在 +[RSA addPublicKey:] 方法中,讀寫鑰匙串資料相關程式碼,使用了一個 TAG 標記(kSecAttrApplicationTag),其預設值為 RSAUtil_PubKey,靈光乍現,拍案而起,莫非與其他 SDK 有衝突!
為了驗證我們的猜測,需要確認其他 SDK 中是否使用了同樣的 TAG。結合以往的逆向經驗,我們首先想到的方法是反編譯 App,搜尋字串,查詢資料引用,結果如下圖所示:
search_liba() {
dir=$1
for lib in $(find $dir -type f -name "*.a");
do
cnt=$(strings $lib | grep $keyword -wc)
if [[ $cnt -gt 0 ]]; then
echo "$lib -> $keyword($cnt)"
fi
done
}
search_framework() {
dir=$1
for lib in $(find $dir -type d -name "*.framework");
do
lib_name=${lib##*/}
lib_name_without_ext=${lib_name%.framework}
lib_full_name=$lib/$lib_name_without_ext
if [[ -e "$lib_full_name" ]]; then
cnt=$(strings $lib_full_name | grep $keyword -wc)
if [[ $cnt -gt 0 ]]; then
echo "$lib_full_name -> $keyword($cnt)"
fi
fi
done
}
使用指令碼搜尋字串 RSAUtil_PubKey,部分查詢結果如下:
the keyword you input: RSAUtil_PubKey
/AMapFoundationKit.framework/AMapFoundationKit -> RSAUtil_PubKey(4)
/AnyThinkSDK.framework/AnyThinkSDK -> RSAUtil_PubKey(5)
/EAccountApiSDK.framework/EAccountApiSDK -> RSAUtil_PubKey(4)
/GT3Captcha.framework/GT3Captcha -> RSAUtil_PubKey(4)
/KSAdSDK.xcframework/ios-arm64_armv7/KSAdSDK.framework/KSAdSDK -> RSAUtil_PubKey(2)
......
搜尋結果著實讓人吃驚,本以為是我們一時疏忽導致的問題,沒想到是高德地圖、快手激勵影片、電信一鍵登入、極驗等諸多 SDK 都使用了預設的 TAG 值(注:上述結果只表明 SDK 中存在 RSAUtil_PubKey 字串,是否會引起衝突,需要進一步除錯驗證。限於 SDK 版本,結果僅供參考),很容易與呼叫方出現衝突,這也正是本文寫作初衷,希望能夠引起大家重視。
至此問題出現的根源我們已剖析完成,機智的同學可能已經發現,這兩個問題本質上可以歸結為同一個問題,都是由於併發導致的資料髒讀。多個 SDK 在構造 RSA 公鑰資料結構,讀取 Keychain 共享資源時都使用了同一個 TAG,相互之間產生影響,不僅自己程式碼併發會導致錯亂,還會與其他 SDK 發生衝突。再進一步分析這個問題,即便每個SDK的TAG值不同,但每個SDK內只要存在同一時間獲取不同公鑰的邏輯,依然有可能使SDK內部的不同公鑰出現衝突。
總結
客戶端研發過程中,併發問題容易被人忽略,QA 測試環節也很難發現。程式碼中涉及操作共享資料(記憶體資料、磁碟資料)時,一定要多考慮執行緒安全、資源競爭問題,建議開發者自己編寫併發測試 case 驗證。
在分析定位問題時,我們使用了逆向技術、指令碼等多種手段,還分析了上萬條錯誤日誌,通過分析資料發現問題,靈活運用工具解決問題,有助於快速定位疑難問題,提升工作效率。
開源專案讓我們普通開發者受益匪淺,在此感謝開源作者的無私奉獻。與此同時,開源專案也深深影響著每一位使用者,想起幾年前 Ant Design 聖誕彩蛋事件和最近的 log4j 漏洞,誠可畏也!
在軟體開發中,我們不提倡重複造輪子,但在使用輪子的過程中,一定要了解其中構造,知其然知其所以然。死搬硬套,一不小心就會被帶到溝裡,勿謂言之不預也。
參考資料:
1.Objective-C-RSA(http://github.com/ideawu/Objective-C-RSA)
2.is keychain in ios threadsafe?(http://stackoverflow.com/questions/30169879/is-keychain-in-ios-threadsafe)
3.Working with Concurrency(http://developer.apple.com/documentation/security/certificate_key_and_trust_services/working_with_concurrency?language=objc) 4.WBBlades - search symbol script(http://github.com/wuba/WBBlades/blob/master/Scripts/search_symbol.sh)
-
賈學文,58 同城 – 使用者價值增長部 – iOS 技術部 -
王曉暉,58 同城 – 使用者價值增長部 - iOS 技術部 -
蔣演,58 同城 – 使用者價值增長部 – iOS 技術部 -
彭飛,58 同城 – 使用者價值增長部 – iOS 技術部
本文分享自微信公眾號 - 58技術(architects_58)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。
- Flutter動態化 | Fair 2.4.0 新版本特性
- 乾貨|短影片創意設計,爆款影片手到擒來!
- 一種支援泛型解析的PHPScf無痕化技術方案
- 乾貨|短影片創意設計,爆款影片手到擒來!
- 分散式鎖實現原理解析(Redis & WLock)
- 一種支援泛型解析的PHPScf無痕化技術方案
- 低程式碼實時數倉構建系統的設計與實踐
- 分散式鎖實現原理解析(Redis & WLock)
- 低程式碼實時數倉構建系統的設計與實踐
- 開源 | Fair 在 58 同城拍客 App 中的實踐
- 58神奇管家——基於零信任終端安全管理系統的設計與實現
- 安全業務全鏈路資料倉庫在58的實踐與應用
- 安全業務全鏈路資料倉庫在58的實踐與應用
- 開源|Fair 在 58 同城拍客 App 中的實踐
- 開源|Fair 在 58 同城拍客 App 中的實踐
- 58集團處罰資料中心的設計與實踐
- 58集團處罰資料中心的設計與實踐
- 58同城Swift版小遊戲研發之路
- 58同城Swift版小遊戲研發之路
- 售價 11.98 萬元~15.58 萬元,比亞迪驅逐艦 05 正式上市:搭載 DM-i 超混技術,超 1200 公里綜合續...