P.S 最近報名參加了開放原子基金的一個比賽,是以公衆号一直沒有更新,等 15 号決賽以後,開始繼續更新「Rust 研學 :Rust 與 LLM 系列」,正好這次比賽也用到了 Rust 實作了一個 AI Agent 來解決特定問題,到時候可以分享一下我的心得。
最近又是 xz 後門事件,又是 Rust 标準庫發現 Windows 平台漏洞,也發現一些朋友可能對 Rust 的安全承諾有所誤解,是以就打算寫一篇文章,再談一談 Rust 與安全。
目錄
- 安全的分類 :Safety 與 Security
- Rust 的安全承諾
- 但 Rust 不保證 100% 安全
- Rust 基金會和 Rust 安全工作組的努力
- Memory Safety
- 典型的 Rust Security : "BatBadBut" 關鍵安全漏洞
- xz 後門啟示錄
- Rust 安全政策防範
- Rust 供應鍊安全解決方案:cargo vet
- 安全開發政策:減少依賴庫
- 後記
安全的分類 :Safety 與 Security
在技術和工程領域中,"Safety"(功能安全性)和 "Security"(資訊安全保障)是兩個關鍵概念,它們雖然聽起來相似,但代表着不同的關注點。尤其是中文翻譯,這兩個英文單詞都被翻譯為「安全」一詞,是以會讓一些人造成一些困惑。
其實這兩個術語有着不同的内涵。
- Safety(功能安全性):
- Safety 特指功能安全性。通常指的是保護系統、裝置或程式免受意外的、非故意的錯誤和故障的能力。這包括防止由系統故障、操作失誤或外部事件引起的傷害或損害。例如,在程式設計中,功能安全性關注的是如何避免程式崩潰、資料損壞或意外行為,如緩沖區溢出、空指針通路、資料競争、越界寫入等。因為這些漏洞會直接影響系統功能本身。
- Security(資訊安全保障):
- Security 特指資訊安全,它屬于一種安全保障。側重于保護系統免受惡意攻擊和威脅。這涉及到預防未授權通路、資料洩露、系統入侵、注入、DDOS 等其他形式的惡意行為。在同樣的程式設計場景中,安全保障措施可能包括使用加密技術、實作複雜的使用者認證機制、防禦網絡攻擊等。相對于 Safety 來說, Security 更注重系統外側的安全防護和保障。
總之,Safety 功能安全性的提升通常是通過增強系統的魯棒性和錯誤處理能力來實作的,而Security 資訊安全保障則需要考慮潛在的惡意行為,采取主動防禦措施。
在實際應用中,Safety 和 Security 是相輔相成的。例如,記憶體安全漏洞(Safety 問題)可能被利用來執行惡意代碼(Security 問題)。是以,編寫安全的代碼不僅需要關注代碼本身的穩定性和防錯性(提升 Safety),也必須考慮到潛在的安全威脅和防護措施(增強 Security)。
Rust 的安全承諾
很多人隻聽說 Rust 安全,但不知道 Rust 的安全承諾是什麼,也不明白 Rust 的安全保障邊界在哪裡。以至于看到 Rust 語言曝出 CVE 就會說,「号稱安全的 Rust 語言又不安全了」之類的“胡話”。
應用 Rust 語言,了解 Rust 語言的安全承諾很重要。Rust 程式設計語言的設計核心在于提供一種安全且高效的方式來編寫系統級軟體。其安全承諾主要圍繞以下幾個方面:
Memory Safety
Rust 最為人稱道的特性之一是其記憶體安全性。Rust 通過所有權(ownership)、借用(borrowing)和生命周期(lifetime)的概念,防止了空指針異常和資料競争等常見的記憶體錯誤。這種機制確定了在編譯時就能捕捉到潛在的記憶體錯誤,極大地提高了軟體的可靠性和安全性。
- 所有權系統:在 Rust 中,每個值都有一個稱為其“所有者”的變量。值在任何時候隻能有一個所有者。這個機制也同時阻止了記憶體洩漏的風險,因為當所有者變量離開作用域時,值和它占用的記憶體就會自動被清理。
- 借用規則:Rust 允許值的借用,但是有嚴格的規則:要麼隻能有一個可變借用(可以改變資料),要麼有多個不可變借用(隻讀通路),這兩者不能同時存在。這避免了資料競争,保證了線程安全。
- 生命周期标注:Rust 要求開發者在某些情況下标明記憶體資料的使用期限(生命周期),這有助于編譯器了解引用何時仍然有效,何時則可能導緻懸挂引用。
以上記憶體安全規則,都是借由 Rust 語言精心設計的類型系統來保障的,由編譯器在編譯期根據類型系統來進行檢查,進而達到記憶體安全目标。
但 Rust 不保證 100% 安全
了解 Rust 的安全承諾也意味着要認識到它的界限。
盡管 Rust 提供了強大的安全保障,但它并不聲稱能100%保證軟體安全。安全性依舊依賴于開發者正确使用語言提供的功能。例如,Rust 不能自動防範邏輯錯誤或算法缺陷,開發者需要對其代碼邏輯進行徹底的測試和審查。
另外,如果開發者使用 Unsafe Rust ,則安全保障的義務和責任也将落到每個開發者身上,不僅僅是 Unsafe Rust 代碼編寫者,還有 Unsafe Rust 代碼調用者。通過 unsafe 代碼塊,開發者可以選擇繞過 Rust 的安全檢查,直接操作記憶體。這為進階優化提供了可能,但同時也帶來了風險。
雖然官方沒有給出一個統一的 Unsafe Rust 編碼規範,但是業内還是有一套約定俗成的 Unsafe Rust 安全抽象規範的。這方面可以參考 Google Android/ Rust for Linux/ Rust std 這些内部實作。
關于這一點,我在 《Rust 編碼規範》的 Unsafe Rust 部分[1]也有總結,供大家參考。
另外,雖然 Rust 努力提供記憶體安全,但它不直接處理其他類型的安全問題,如網絡安全或使用者認證等。因為這屬于 Security 資訊安全保障範圍。對于 Security 問題,是很多語言都會面臨的問題。
幾個比較典型的 Security 問題就是:
- DDoS 問題。比如你采用了錯誤的 Hash 算法,是有可能導緻這種問題。
- 字元類問題。比如 2021 年 Rust 編譯器也發過安全公告 (CVE-2021-42574),公開了一個利用 Unicode 漏洞攻擊方法「特洛伊之源」。本公衆号也專門介紹過這個漏洞 : 特洛伊之源 | 在 Rust 代碼中隐藏的無形漏洞
本公衆号曆史文章裡也介紹過多個安全問題。包括也介紹了用 Safe Rust 如何構造安全問題的 cve-rs 相關代碼解讀。
你要明白,Rust 語言不是萬能的,也不是安全銀彈,它隻是軟體安全發展路上的一個比較進步的解決方案而已。尤其是,Security 問題,不能僅僅依賴語言。
Rust 基金會和 Rust 安全工作組的努力
Rust 官方安全工作組正緻力于擴充 Rust 的安全特性,通過推廣更安全的程式設計實踐和改進現有的工具支援來提升 Rust 程式的安全性。例如,他們推出了如 cargo-audit 這樣的工具,幫助開發者檢測已知的依賴庫漏洞,及時進行修補。并且維護一個 https://rustsec.org/[2] 來跟蹤 Rust 及其生态庫中被發現的 CVE 和 軟體缺陷等問題。
Rust 基金會在進一步擴大 Rust security 安全保障也在努力行動中。包括雇傭了安全專家,對 Rust 生态庫做威脅情報分析等等。
通過這些機制和社群的持續努力,Rust 希望能夠在系統程式設計領域提供一個既安全又高效的選擇,減少常見的安全漏洞,同時提升開發效率和程式性能。
典型的 Rust Security : "BatBadBut" 關鍵安全漏洞
這兩天在 Rust 标準庫中發現了一個名為 "BatBadBut" 的關鍵安全漏洞,影響所有在 Windows 上 1.77.2 版本之前的版本。該漏洞被辨別為 CVE-2024-24576,CVSS 分數為 10.0,允許攻擊者通過繞過調用批處理檔案時的轉義機制來執行任意的 shell 指令。
近期 Rust 安全響應工作組收到通知,Rust 标準庫在 1.77.2 版本之前,在 Windows 上使用Command調用批處理檔案(帶有bat和cmd擴充名)時,沒有正确轉義參數。
能夠控制傳遞給生成的程序的參數的攻擊者可以通過繞過轉義來執行任意的 shell 指令。
對于在 Windows 上使用不受信任的參數調用批處理檔案的人來說,這個漏洞的嚴重程度是關鍵的。其他平台或用途不受影響。
Command::arg和Command::args 的API在文檔中聲明,無論參數的内容如何,參數都将原樣傳遞給生成的程序,并且不會被 shell 評估。這意味着可以安全地将不受信任的輸入作為參數傳遞。
這個函數不屬于 Rust 記憶體安全承諾範疇,是以将函數命名為 unsafe 也無濟于事。
在 Windows 上,這個實作比其他平台更複雜,因為 Windows API 隻提供一個包含所有參數的字元串,并且由生成的程序來拆分它們。大多數程式使用标準的 C 運作時 argv,實際上導緻參數被拆分的方式基本一緻。有一個例外,即 cmd.exe(用于執行批處理檔案等其他任務),它具有自己的參數拆分邏輯。這迫使标準庫為傳遞給批處理檔案的參數實作自定義轉義。
是以,有人報告說 Rust 的轉義邏輯不夠嚴謹,可能會傳遞惡意參數導緻任意的 shell 執行。由于 cmd.exe 的複雜性,Rust 團隊也沒有找到一個能夠正确轉義所有情況下參數的解決方案。為了保持标準庫的 API 保證,官方團隊改進了轉義代碼的魯棒性,并将 Command API 更改為在無法安全轉義參數時傳回 InvalidInput 錯誤。在生成程序時将發出此錯誤。
其實這種問題,很多語言都有,但就是因為 Rust 語言主打安全,是以,它就當了出頭鳥。不明是以之人,就開始攻擊 Rust 的安全性了。"BatBadBut" 漏洞是由安全研究員 RyotaK 發現,并負責向 Rust 安全團隊進行了負責任的披露。
雖然最初的關注點是 Rust 程式設計語言,但現在已經發現 "BatBadBut" 漏洞不僅僅局限于一個 CVE 辨別符。該漏洞影響多種程式設計語言和工具,每種都配置設定了不同的 CVE ID,具體取決于實作和影響。
除了與 Rust 标準庫相關的 CVE-2024-24576 外,"BatBadBut" 還包括 CVE-2024-1874、CVE-2024-22423(影響 yt-dlp,風險評分為 8.3)和 CVE-2024-3566(影響 Haskell、Node.js、Rust、PHP 和 yt-dlp)。這凸顯了該漏洞的廣泛性質,以及開發人員需要評估各種程式設計語言和工具中的應用程式和依賴關系的需求。
本着負責任的态度,Rust 官方團隊還是在 Rust 1.77.2 中修複了這個問題(其他語言不一定給你修複)。請注意,批處理檔案的新轉義邏輯偏向保守一些,可能會拒絕有效的參數。那些自己實作轉義或僅處理受信任的 Windows 輸入的人也可以使用 CommandExt::raw_arg 方法繞過标準庫的轉義邏輯。
xz 後門啟示錄
xz 後門事件[3],官方标記為 CVE-2024-3094,揭露了在廣泛使用的開源庫中植入惡意代碼的潛在危害。攻擊者通過精心計劃和執行,逐漸獲得了項目的維護權,最終在xz/liblzma庫中引入後門。
這種攻擊不僅影響了Linux的多個發行版,還可能對使用這些庫的應用程式造成間接影響。特别是在此事件中,後門被設計為難以檢測,它不直接修改代碼庫的檔案,而是在釋出的壓縮包中植入,這使得它能夠在不引起立即懷疑的情況下傳播。
Rust 安全政策防範
Rust 社群的 Guillaume Endignoux[4] 從他自己的 lzma-rs 項目(一個純 Rust 實作的 XZ 壓縮格式庫)的視角出發,分析了 Rust 社群安全政策防範 xz 後門事件所起到的作用。
- RustSec advisory[5] 安全政策對于标記 Rust crates 為未維護狀态有嚴格定義,強調了在開源項目中關于軟體元件維護狀态的透明性。這一過程涉及送出包含特定細節的 pull request,有助于庫的使用者做出知情決策,避免使用可能存在安全漏洞的過時或未維護的元件。
- crates.io 也嚴格限制了 crate 所有者的身份,是由 token 和 個人郵箱綁定的。
這種政策對于防禦類似 xz 後門這樣的人為攻擊具有潛在幫助。通過公開和及時更新元件的維護狀态,可以提高社群的警覺性,進而減少因使用不安全或棄用的依賴而導緻的安全風險。這有助于及時識别和替換那些可能被植入惡意代碼的元件。
然而,對于 xz 後門這類屬于社會工程學層面的攻擊,安全政策作用也及其有限。
Rust 供應鍊安全解決方案:cargo vet
Rust 在提供供應鍊安全方面也有另外一種解決方案。
其實為應對此類供應鍊問題,Mozilla 兩年前就開發了 cargo vet 工具,用于幫助開發者稽核其項目的依賴。這個工具檢查依賴項的安全性記錄,幫助識别和防範可能的安全威脅。雖然這不能保證完全防止所有供應鍊攻擊,但它提供了一種機制,通過增加代碼和依賴的透明度來降低風險。這個工具也被 Google 内部采用,Google 也釋出了經過他們團隊審計過的 Rust crate 清單[6]。
Cargo vet 動機
cargo-vet 的主旨是確定項目的第三方依賴已經由可信的實體審計,力求輕巧和易于內建。
運作時,cargo-vet 會将一個項目的所有第三方依賴關系與項目作者或他們信任的實體進行的一系列審計進行比對。如果有任何差距,該工具在執行和記錄審計方面提供輔助。
人們通常不審計開源依賴關系的主要原因是,它的工作量太大。cargo-vet的目的是将開發者的工作降低到一個可管理的水準,有幾個關鍵的方法。
- 共享。公開的 crate 經常會被很多項目使用,這些項目可以共享他們的發現,以避免重複工作。
- 相對審計。同一crate 的不同版本往往是非常相似的。開發人員可以檢查兩個版本之間的差異,并記錄如果第一個版本被稽核過,第二個版本也可以被認為是被稽核過的。
- 延遲審計。要實作全覆寫并不總是實際的。依賴關系可以被添加到一個例外清單中,這個清單可以随着時間的推移而逐漸縮小。這使得在一個新的項目中引入貨真價實的稽核,并防範未來的漏洞,同時在時間允許的情況下逐漸稽核預先存在的代碼,這是很瑣碎的。
在 Rust 代碼中減少第三方代碼安全風險相比于其他語言較為容易,Rust有以下兩個獨有的特點創造了系統分析的條件:
- 首先,審計 Rust 代碼相對容易。與 C/C++ 不同,Rust 代碼預設是記憶體安全的,與 JavaScript 不同的是,沒有高度動态的共享全局環境。這意味着開發者通常可以在高層次上推斷子產品潛在行為的範圍,而無需仔細研究其所有内部不變量。例如,一個複雜的字元串解析器,具有适當的接口、沒有Unsafe 代碼,也沒有強大的導入,其損害程式其他部分的手段是有限的。這也使得我們更容易根據與之前可信版本的差異來斷定新版本是安全的。
- 其次,Rust生态系統中幾乎每個人都依賴同一套基本工具--Cargo和crates.io--來導入和管理第三方元件,而且依賴集的重疊度很高。
Cargo-vet 工作機制
大多數開發人員都是忙碌的人,他們緻力于供應鍊完整性的精力有限。是以,cargo-vet 背後的驅動原理是盡量減少摩擦并盡可能輕松地做正确的事情。它旨在簡化設定,不顯眼地融入現有工作流程,引導人們完成每一步,并允許整個生态系統共享審計廣泛使用的軟體包的工作。
具體工作流為:
- 初始設定 :Cargo-vet 可以通過将工具添加為 linter 并運作來啟用cargo vet init,這會在存儲庫中建立一些中繼資料。這大約需要五分鐘,而且至關重要的是,不需要稽核現有的依賴項。這些會自動添加到豁免清單中。
- 添加新的第三方crate。一段時間後,開發人員嘗試将新的第三方代碼拉入項目。這可能是一個新的依賴,或者是對現有依賴的更新。作為持續內建的一部分,cargo-vet 分析更新的建構圖,以驗證新代碼是否已由受信任的組織稽核。如果沒有,更新檔将被拒絕。
- 如果被拒絕,cargo-vet會幫助開發者解決問題:
a. 首先,它會掃描系統資料庫以檢視是否有任何知名組織之前稽核過該包。
b. 如果比對,cargo-vet 會通知開發人員并提供将該組織添加到項目受信任導入的選項。
c. 導入和審計送出的準許自動落入supply-chain/目錄的代碼所有者手中,該所有者應由項目上司或專門的安全團隊組成。
d. 如果沒有比對,開發人員可以自己審計。cargo-vet 簡化了這個過程。通常有人已經稽核過同一個 crate 的不同版本,在這種情況下,cargo-vet 會計算相關的差異并确定最小的差異1[7]。在引導開發人員完成确定要審計什麼的過程之後,它會在本地或在 Sourcegraph[8]上呈現相關工件以供檢查。
e. 共享審計結果。Cargo-vet 的共享和發現機制建立在這種去中心化存儲之上。導入是通過直接指向外部存儲庫中的審計檔案來實作的,而系統資料庫隻是來自知名組織的此類檔案的索引。這也意味着攻擊者沒有中央基礎設施可以攻破。
看得出來,Cargo-vet 将複雜的審計工作通過統計分析和巧妙的流程設計給簡化了,讓開發者可以「懶洋洋」地一步步完成這份工作。
Cargo-vet 具有許多進階功能——它支援自定義審計标準、建構圖中不同子樹的可配置政策以及過濾特定于平台的代碼。
安全開發政策:減少依賴庫
在實際操作中,減少不必要的外部依賴是降低被攻擊風險的有效政策之一。例如,sudo-rs 項目通過精簡其依賴庫來減少潛在的安全威脅[9]。這種方法有助于控制項目的攻擊面,進而提高整體的安全性。這種政策強調了在軟體開發中,依賴管理的重要性,特别是在供應鍊攻擊越來越多的當下,通過控制和審計依賴項可以顯著提升項目的安全性。
然而,無論 Rust 社群采用多麼嚴格的安全政策,也無法避免底層 Linux 庫被攻擊的風險。我在 Rust 接棒 C 語言 :Rust for Linux 中正在發生的技術變革 一文中寫到過:
上文說到,在網絡方面,Rust 開發人員不得不要求網絡維護者減慢合并 Rust 代碼的速度[3]。具體情況是,目前 Tomonori Fujita 正在為實體層(PHY)驅動程式添加一些 Rust 抽象。已經進行了大量的審查,并且根據這些審查意見頻繁地重新制定了更新檔集。不幸的是,Rust-for-Linux 開發人員在跟上這個速度方面遇到了困難。兩個社群的開發實踐似乎存在一些脫節。Andrew Lunn(該更新檔的審查者)指出,網絡更新檔不需要經過稽核就可以合并;“如果在三天内沒有回報,并且通過了CI(持續內建)測試,那麼很可能會被合并。”但 Ojeda (Rust for Linux 核心開發者)表示,CI 測試無法确定抽象是否經過良好和合理的設計,這是 Rust 抽象(重點是安全抽象)所需的關鍵屬性;他希望有人參與其中。Lunn 回答說,最終是人決定是否合并代碼,但 API 問題隻是像其他 bug 一樣,如果發現問題,可以稍後修複。
我是認同 Ojeda 的觀點。因為Rust 抽象,尤其是 Unsafe Rust 安全抽象是需要專門設計的;但是 Linux 中某些子產品開發和合并速度太快,人手嚴重不足,以及 C 那邊的人認為 API 後面修改也可以,這是有誤解的。其實 Rust 安全抽象在後面修改就已經來不及了,必須在前期就為其做好安全抽象。
然而,網絡維護者Jakub Kicinski 表示, "更長的審查周期将使跟蹤更新檔和讨論變得難以管理。" 他想知道在初始階段之後,Rust-for-Linux 項目是否會減少對更新檔審查的參與。Ojeda 同意這是目标,但初始的抽象集将需要更多的審查時間。
是以,我覺得 Linux 過快的釋出節奏,對于供應鍊安全的保障是脆弱的。是以,供應鍊安全還是任重道遠啊。作為開發者,不可能把你代碼裡所有的依賴庫都審查一遍,也許未來 AI 在供應鍊安全上能做出一些突破。
後記
希望本文能讓讀者朋友們對 Rust 與 安全建立一個「系統且健康」的認知。
感謝閱讀。
參考資料
[1] 《Rust 編碼規範》的 Unsafe Rust 部分: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/
[2] https://rustsec.org/: https://rustsec.org/
[3] xz 後門事件: https://www.wired.com/story/xz-backdoor-everything-you-need-to-know/
[4] Guillaume Endignoux: https://gendignoux.com/blog/2024/04/08/xz-backdoor.html
[5] RustSec advisory: https://rustsec.org/
[6] Google 也釋出了經過他們團隊審計過的 Rust crate 清單: https://opensource.googleblog.com/2023/05/open-sourcing-our-rust-crate-audits.html
[7] 1: https://mozilla.github.io/cargo-vet/how-it-works.html#1
[8] Sourcegraph: https://sourcegraph.com/
[9] sudo-rs 項目通過精簡其依賴庫來減少潛在的安全威脅: https://www.memorysafety.org/blog/reducing-dependencies-in-sudo/