- 原文地址:Control Flow Integrity in the Android kernel
- 原文做者:Android Developers Blog
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:nanjingboy
- 校對者:gs666
由 Android 安全研究工程師 Sami Tolvanen 發佈html
Android 的安全模型由 Linux 內核強制執行,這將誘使攻擊者將其視爲攻擊目標。咱們在已發佈的 Android 版本和 Android 9 上爲增強內核投入了大量精力,咱們將繼續這項工做,經過將關注點放在基於編譯器的安全緩解措施上以防止代碼重用攻擊。前端
Google 的 Pixel 3 將是第一款在內核中實施 LLVM 前端控制流完整性(CFI)的設備,咱們已經實現了 Android 內核版本 4.9 和 4.14 中對 CFI 的支持。這篇文章描述了內核 CFI 的工做原理,併爲開發人員在啓用該功能時可能遇到的常見問題提供瞭解決方案。linux
利用內核的經常使用方法是使用錯誤來覆蓋存儲在內存中的函數指針,例如存儲了回調函數的指針,或已被推送到堆棧的返回地址。這容許攻擊者執行任意內核代碼來完成利用,即便他們不能注入本身的可執行代碼。這種獲取代碼執行能力的方法在內核中特別受歡迎,由於它使用了大量的函數指針,以及使代碼注入更具挑戰性的現有內存保護機制。android
CFI 嘗試經過添加額外的檢查來確認內核控制流停留在預先設計的版圖中,以便緩解這類攻擊。儘管這沒法阻止攻擊者利用一個已存在的 bug 獲取寫入權限,從而更改函數指針,但它會嚴格限制可被其有效調用的目標,這使得攻擊者在實踐中利用漏洞的過程變得更加困難。ios
圖 1. 在 Android 設備內核中,LLVM 的 CFI 將 55% 的間接調用限制爲最多 5 個可能的目標,80% 限制爲最多 20 個目標。git
爲了肯定每一個間接分支的全部有效調用目標,編譯器須要當即查看全部內核代碼。傳統上,編譯器一次處理單個編譯單元(源代文件),並將目標文件合併到連接器。LLVM 的 CFI 要求使用 LTO,其編譯器爲全部 C 編譯單元生成特定於 LLVM 的 bitcode,而且 LTO 感知連接器使用 LLVM 後端來組合 bitcode,並將其編譯爲本機代碼。github
圖 2. LTO 在內核中的工做原理的簡單概述。全部 LLVM bitcode 在連接時被組合,優化並生成本機代碼。編程
幾十年來,Linux 一直使用 GNU 工具鏈來彙編,編譯和連接內核。雖然咱們繼續將 GNU 彙編程序用於獨立的彙編代碼,但 LTO 要求咱們切換到 LLVM 的集成彙編程序以進行內聯彙編,並將 GNU gold 或 LLVM 本身的 lld 做爲連接器。在巨大的軟件項目上切換到未經測試的工具鏈會致使兼容性問題,咱們已經在內核版本 4.9 和 4.14 的 arm64 LTO 補丁集中解決了這些問題。後端
除了使 CFI 成爲可能,因爲全局優化,LTO 還能夠生成更快的代碼。但額外的優化一般會致使更大的二進制尺寸,這在資源受限的設備上多是不須要的。禁用 LTO 特定的優化(好比全局內聯和循環展開)能夠經過犧牲一些性能收益來減小二進制尺寸。使用 GNU gold 時,能夠經過如下方式設置 LDFLAGS 來禁用上述優化:安全
LDFLAGS += -plugin-opt=-inline-threshold=0 \
-plugin-opt=-unroll-threshold=0
複製代碼
注意,禁用單個優化的標誌不是穩定 LLVM 接口的一部分,在未來的編譯器版本中可能會更改。
LLVM 的 CFI 實如今每一個間接分支以前添加一個檢查,以確認目標地址指向一個擁有有效簽名的函數。這能夠防止一個間接分支跳轉到任意代碼位置,甚至限制能夠調用的函數。因爲 C 編譯器沒有對間接分支強制執行相似限制,函數類型聲明不匹配致使了幾個 CFI 違規,即便在咱們在內核的 CFI 補丁集中解決的內核 4.9 和 4.14 中也是如此。
內核模塊爲 CFI 添加了另外一個複雜功能,由於它們在運行時加載,而且能夠獨立於內核的其它部分進行編譯。爲了支持可加載模塊,咱們在內核中實現了 LLVM 的 cross-DSO CFI 支持,包括用來加速跨模塊查找的 CFI 影子。在使用 cross-DSO 支持進行編譯時,每一個內核模塊都會包含有關有效本地分支目標的信息,內核根據目標地址和模塊的內存佈局從正確的模塊中查找信息。
圖 3. 注入 arm64 內核的 cross-DSO CFI 檢查示例。類型信息在 X0 中傳遞,目標地址在 X1 中驗證。
CFI 檢查會給間接分支增長一些開銷,但因爲更積極的優化,咱們的測試代表影響很小,在不少狀況下總體系統性能甚至提升了 1-2%。
arm64 中的 CFI 須要 clang 版本 >= 5.0 而且 binutils >= 2.27。內核構建系統還假定 LLVMgold.so 插件在 LD_LIBRARY_PATH 中可用。clang 和 binutils 預構建工具鏈二進制文件可在 AOSP 得到,也可以使用上游二進制文件。
啓用內核 CFI 須要開啓如下內核配置選項:
CONFIG_LTO_CLANG=y
CONFIG_CFI_CLANG=y
複製代碼
在調試 CFI 違規或設備啓動期間,使用 CONFIG_CFI_PERMISSIVE=y 可能會有所幫助。此選項將違規轉換爲警告而不是內核恐慌。
如前一節所述,咱們在 Pixel 3 上啓用 CFI 時遇到的最多見問題是由函數指針類型不匹配引發的良性違規。當內核遇到這種違規時,它會打印出一個運行時警告,其中包含失敗時的調用堆棧,以及未經過 CFI 檢查的目標調用。更改代碼以使用正確的函數指針類型能夠解決問題。雖然咱們已經修復了 Android 內核中全部已知的間接分支類型不匹配的問題,但在設備特定的驅動程序中仍然可能發現相似的問題,例如。
CFI failure (target: [<fffffff3e83d4d80>] my_target_function+0x0/0xd80):
------------[ cut here ]------------
kernel BUG at kernel/cfi.c:32!
Internal error: Oops - BUG: 0 [#1] PREEMPT SMP
…
調用堆棧:
…
[<ffffff8752d00084>] handle_cfi_failure+0x20/0x28
[<ffffff8752d00268>] my_buggy_function+0x0/0x10
…
複製代碼
圖 4. CFI 故障引發的內核恐慌示例
另外一個潛在的缺陷是地址空間衝突,但這在驅動程序代碼中應該不太常見。LLVM 的 CFI 檢查僅清楚內核虛擬地址和在另外一個異常級別運行或間接調用物理地址的任何代碼都將致使 CFI 違規。可經過使用 __nocfi
屬性禁用單個函數的 CFI 來解決這些類型的故障,甚至可使用 Makefile 中的 $(DISABLE_CFI) 編譯器標誌來禁用整個文件的 CFI。
static int __nocfi address_space_conflict()
{
void (*fn)(void);
…
/* 切換分支到物理地址將使 CFI 沒有 __nocfi */
fn = (void *)__pa_symbol(function_name);
cpu_install_idmap();
fn();
cpu_uninstall_idmap();
…
}
複製代碼
圖 5. 修復由地址空間衝突引發 CFI 故障的示例。
最後,和許多加強功能同樣,CFI 也可能因內存損壞錯誤而被觸發,不然可能致使隨後的內核崩潰。這些可能更難以調試,但內存調試工具,如 KASAN 在這種狀況下能夠提供幫助。
咱們已經在 Android 內核 4.9 和 4.14 中實現了對 LLVM 的 CFI 的支持。Google 的 Pixel 3 將是第一款提供這些保護功能的 Android 設備,咱們已經過 Android 通用內核向全部設備供應商提供了該功能。若是你要發佈運行 Android 9 的新 arm64 設備,咱們強烈建議啓用內核 CFI 以幫助防止內核漏洞。
LLVM 的 CFI 保護間接分支免受攻擊者的攻擊,這些攻擊者設法訪問存儲在內核中的函數指針。這使得利用內核的經常使用方法更加困難。咱們將來的工做還涉及到 LLVM 的 影子調用堆棧來保護函數返回地址免受相似攻擊,這將在即將發佈的編譯器版本中提供。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。