一篇文章帶你全面瞭解 Rust 與 安全

最近又是 xz 後門事件,又是 Rust 標準庫發現 Windows 平臺漏洞,也發現一些朋友可能對 Rust 的安全承諾有所誤解,所以就打算寫一篇文章,再談一談 Rust 與安全。

目錄

安全的分類  :Safety 與 Security

在技術和工程領域中,"Safety"(功能安全性)和 "Security"(信息安全保障)是兩個關鍵概念,它們雖然聽起來相似,但代表着不同的關注點。尤其是中文翻譯,這兩個英文單詞都被翻譯爲「安全」一詞,所以會讓一些人造成一些困惑。

其實這兩個術語有着不同的內涵。

  1. Safety(功能安全性):
  1. 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 不保證 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 問題就是:

本公衆號歷史文章裏也介紹過多個安全問題。包括也介紹了用 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調用批處理文件(帶有batcmd擴展名)時,沒有正確轉義參數。

能夠控制傳遞給生成的進程的參數的攻擊者可以通過繞過轉義來執行任意的 shell 命令。

對於在 Windows 上使用不受信任的參數調用批處理文件的人來說,這個漏洞的嚴重程度是關鍵的。其他平臺或用途不受影響。

Command::argCommand::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 後門事件所起到的作用。

這種策略對於防禦類似 xz 後門這樣的人爲攻擊具有潛在幫助。通過公開和及時更新組件的維護狀態,可以提高社區的警覺性,從而減少因使用不安全或棄用的依賴而導致的安全風險。這有助於及時識別和替換那些可能被植入惡意代碼的組件。

然而,對於 xz 後門這類屬於社會工程學層面的攻擊,安全策略作用也及其有限。

Rust 供應鏈安全解決方案:cargo vet

Rust 在提供供應鏈安全方面也有另外一種解決方案。

其實爲應對此類供應鏈問題,Mozilla 兩年前就開發了 cargo vet 工具,用於幫助開發者審覈其項目的依賴。這個工具檢查依賴項的安全性記錄,幫助識別和防範可能的安全威脅。雖然這不能保證完全防止所有供應鏈攻擊,但它提供了一種機制,通過增加代碼和依賴的透明度來降低風險。這個工具也被 Google 內部採用,Google 也發佈了經過他們團隊審計過的 Rust crate 列表 [6]。

Cargo vet 動機

cargo-vet 的主旨是確保項目的第三方依賴已經由可信的實體審計,力求輕巧和易於集成。

運行時,cargo-vet 會將一個項目的所有第三方依賴關係與項目作者或他們信任的實體進行的一系列審計進行匹配。如果有任何差距,該工具在執行和記錄審計方面提供輔助。

人們通常不審計開源依賴關係的主要原因是,它的工作量太大。cargo-vet 的目的是將開發者的工作降低到一個可管理的水平,有幾個關鍵的方法。

在 Rust 代碼中減少第三方代碼安全風險相比於其他語言較爲容易,Rust 有以下兩個獨有的特點創造了系統分析的條件:

Cargo-vet 工作機制

大多數開發人員都是忙碌的人,他們致力於供應鏈完整性的精力有限。因此,cargo-vet 背後的驅動原理是儘量減少摩擦並儘可能輕鬆地做正確的事情。它旨在簡化設置,不顯眼地融入現有工作流程,引導人們完成每一步,並允許整個生態系統共享審計廣泛使用的軟件包的工作。

具體工作流爲:

  1. 初始設置 :Cargo-vet 可以通過將工具添加爲 linter 並運行來啓用cargo vet init,這會在存儲庫中創建一些元數據。這大約需要五分鐘,而且至關重要的是,不需要審覈現有的依賴項。這些會自動添加到豁免列表中。

  2. 添加新的第三方 crate。一段時間後,開發人員嘗試將新的第三方代碼拉入項目。這可能是一個新的依賴,或者是對現有依賴的更新。作爲持續集成的一部分,cargo-vet 分析更新的構建圖,以驗證新代碼是否已由受信任的組織審覈。如果沒有,補丁將被拒絕。

  3. 如果被拒絕,cargo-vet 會幫助開發者解決問題:

  4. 首先,它會掃描註冊表以查看是否有任何知名組織之前審覈過該包。

  5. 如果匹配,cargo-vet 會通知開發人員並提供將該組織添加到項目受信任導入的選項。

  6. 導入和審計提交的批准自動落入supply-chain/目錄的代碼所有者手中,該所有者應由項目領導或專門的安全團隊組成。

  7. 如果沒有匹配,開發人員可以自己審計。 cargo-vet 簡化了這個過程。通常有人已經審覈過同一個 crate 的不同版本,在這種情況下,cargo-vet 會計算相關的差異並確定最小的差異 1[7]。在引導開發人員完成確定要審計什麼的過程之後,它會在本地或在 Sourcegraph[8] 上呈現相關工件以供檢查。

  8. 共享審計結果。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/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pp-PYm1udqZoc1naHkQKcg