一場開源 RSA 庫引發的“血案”

語言: CN / TW / HK

01

 導 讀

RSA 加密演算法是一種非對稱加密演算法,該演算法極為可靠,在現有技術條件下,很難破解,因此在軟體開發中被廣泛使用。你不必擔心,本文不會介紹深奧的 RSA 加密演算法,也沒有複雜的數學公式。本文將結合 58 iOS App 專案實踐,分享一次我們奇異的 Bug 排查經歷,談談 GitHub 上一個知名的 RSA 演算法庫 Objective-C-RSA 使用過程中遇到的一些暗坑。我們調研發現許多 SDK(如:高德地圖、快手激勵影片、電信一鍵登入、 極驗等) 和 App(如:百度、京東、快手、美團、淘寶、微信等) 都使用了該庫,經反覆測試在併發的情況下同時利用該庫生成不同的RSA公鑰會有30%甚至更高的概率出現衝突,導致加密結果異常。希望本文對讀者有所啟示,避免出現類似問題


02

 問題背景

在 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 加密結果為空

  • 加密資料無法解密


欲知是何原因,且看下文分解!


03

 都是併發惹的禍

在分析伺服器端提供的錯誤日誌後,客戶端研發同學第一反應可能是併發引起的問題。仔細閱讀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錯誤的原因。


04

 命名的藝術

搞明白問題出現的原因,處理完併發問題,你以為問題就解決了!曾以為這段程式碼的坑在第一層,誰也料不到坑在地下十八層。軟體開發中命名確實是讓很多人頭疼的事情,即使我們不追求優雅的藝術,但也要留神避免衝突,那麼問題與命名又有何關係?

上文提到伺服器端反饋過兩種錯誤型別,解決完併發問題,RSA 加密結果為空的資料得到解決。但另外一個問題,客戶端 RSA 加密資料,伺服器端解密異常,在我們上百萬併發測試的過程中,始終沒能復現(當時使用了測試 Demo 驗證,與工程環境有所差異)。曾一度提出各種猜想:雙端 RSA Padding 不一致、ObjC 與 Java 端 RSA 演算法相容性有問題、後端的解密程式碼有問題、亦或資料傳輸過程中有丟失。通過仔細分析,這些假定都被我們一一排除。在始終找不到頭緒的情況下,又讓後端提供了數萬條錯誤日誌,一位細心的同事在其中發現了端倪,撥開層層迷霧,看到一縷曙光。

在後端提供的錯誤日誌中,少量解密失敗的資料,長度恰好是正常資料的一半,加密結果莫明其妙短了一半,這僅僅是巧合嗎?我們知道 RSA 加密後的密文位長跟金鑰的位長度是相同的,2048 位的金鑰長度,為何會出現 1024 位的加密結果?再次細讀 Objective-C-RSA 加密實現程式碼,還是在 +[RSA addPublicKey:] 方法中,讀寫鑰匙串資料相關程式碼,使用了一個 TAG 標記(kSecAttrApplicationTag),其預設值為 RSAUtil_PubKey,靈光乍現,拍案而起,莫非與其他 SDK 有衝突!

為了驗證我們的猜測,需要確認其他 SDK 中是否使用了同樣的 TAG。結合以往的逆向經驗,我們首先想到的方法是反編譯 App,搜尋字串,查詢資料引用,結果如下圖所示:


其中一個方法,由集團內部 SDK 實現,與相應研發人員溝通,使用對方私鑰,測試驗證可以解密部分錯誤日誌,在實踐層面進一步證實我們的猜測:各 SDK 之間使用相同 TAG 讀寫 RSA 公鑰,併發環境下會出現髒讀,導致加密資料混亂
為解決工程中其他 SDK 相互影響出現問題,我們進一步排查哪些 SDK 使用過預設的 TAG 值。在 Hopper 中反編譯,可以查詢引用函式,但不易判斷函數出自哪個 SDK。想到 strings 命令可以在二進位制檔案中查詢字串,我們編寫了一個指令碼,輸入檔案目錄,可以批量查詢 .framework.a 庫中是否存在指定的字串。
指令碼核心實現如下,完整指令碼可移步 WBBlades(https://github.com/wuba/WBBlades/) 下載。
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內部的不同公鑰出現衝突。

這裡提供一種最簡單的解決方案,修改 +[RSA addPublicKey:] 方法的 TAG為隨機值,例如使用 UUID,保證每次讀寫 Keychain 資料時的唯一性,不僅可以避免自己程式碼併發產生的髒資料,還可以避免與其他 SDK 發生衝突。另外 RSA 加密是一個相對耗時的操作,該方法在每次加密時呼叫,同時執行鑰匙串的刪除、新增、讀取操作,設計是否合理也有待商榷。


05

 總結

本文分享了 58 iOS App 專案使用 Objective-C-RSA 三方庫,遇到的一些坑,詳細剖析了問題出 現的原因。 在分析過程中我們發現,此現象並非個例,許多 SDK 都存在同 樣的漏洞,或許由於使用場景限制,暫時沒有發現潛在的問題,希望能夠引起諸位同行重視。
  • 客戶端研發過程中,併發問題容易被人忽略,QA 測試環節也很難發現。程式碼中涉及操作共享資料(記憶體資料、磁碟資料)時,一定要多考慮執行緒安全、資源競爭問題,建議開發者自己編寫併發測試 case 驗證。

  • 在分析定位問題時,我們使用了逆向技術、指令碼等多種手段,還分析了上萬條錯誤日誌,通過分析資料發現問題,靈活運用工具解決問題,有助於快速定位疑難問題,提升工作效率。

  • 開源專案讓我們普通開發者受益匪淺,在此感謝開源作者的無私奉獻。與此同時,開源專案也深深影響著每一位使用者,想起幾年前 Ant Design 聖誕彩蛋事件和最近的 log4j 漏洞,誠可畏也!

  • 在軟體開發中,我們不提倡重複造輪子,但在使用輪子的過程中,一定要了解其中構造,知其然知其所以然。死搬硬套,一不小心就會被帶到溝裡,勿謂言之不預也。

參考資料:

1.Objective-C-RSA(https://github.com/ideawu/Objective-C-RSA)

2.is keychain in ios threadsafe?(https://stackoverflow.com/questions/30169879/is-keychain-in-ios-threadsafe

3.Working with Concurrency(https://developer.apple.com/documentation/security/certificate_key_and_trust_services/working_with_concurrency?language=objc) 4.WBBlades - search symbol script(https://github.com/wuba/WBBlades/blob/master/Scripts/search_symbol.sh


作者簡介:
  • 賈學文,58 同城 – 使用者價值增長部 – iOS 技術部
  • 王曉暉,58 同城 – 使用者價值增長部 - iOS 技術部
  • 蔣演,58 同城 – 使用者價值增長部 – iOS 技術部
  • 彭飛,58 同城 – 使用者價值增長部 – iOS 技術部


本文分享自微信公眾號 - 58技術(architects_58)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。