[譯] Android 核心控制流完整性

淚已無痕發表於2018-12-17

Android 核心控制流完整性

由 Android 安全研究工程師 Sami Tolvanen 釋出

Android 的安全模型由 Linux 核心強制執行,這將誘使攻擊者將其視為攻擊目標。我們在已釋出的 Android 版本和 Android 9 上為加強核心投入了大量精力,我們將繼續這項工作,通過將關注點放在基於編譯器的安全緩解措施上以防止程式碼重用攻擊。

Google 的 Pixel 3 將是第一款在核心中實施 LLVM 前端控制流完整性(CFI)的裝置,我們已經實現了 Android 核心版本 4.9 和 4.14 中對 CFI 的支援。這篇文章描述了核心 CFI 的工作原理,併為開發人員在啟用該功能時可能遇到的常見問題提供瞭解決方案。

防止程式碼重用攻擊

利用核心的常用方法是使用錯誤來覆蓋儲存在記憶體中的函式指標,例如儲存了回撥函式的指標,或已被推送到堆疊的返回地址。這允許攻擊者執行任意核心程式碼來完成利用,即使他們不能注入自己的可執行程式碼。這種獲取程式碼執行能力的方法在核心中特別受歡迎,因為它使用了大量的函式指標,以及使程式碼注入更具挑戰性的現有記憶體保護機制。

CFI 嘗試通過新增額外的檢查來確認核心控制流停留在預先設計的版圖中,以便緩解這類攻擊。儘管這無法阻止攻擊者利用一個已存在的 bug 獲取寫入許可權,從而更改函式指標,但它會嚴格限制可被其有效呼叫的目標,這使得攻擊者在實踐中利用漏洞的過程變得更加困難。

[譯] Android 核心控制流完整性

圖 1. 在 Android 裝置核心中,LLVM 的 CFI 將 55% 的間接呼叫限制為最多 5 個可能的目標,80% 限制為最多 20 個目標。

通過連結時優化(LTO)獲得完整的程式可見性

為了確定每個間接分支的所有有效呼叫目標,編譯器需要立即檢視所有核心程式碼。傳統上,編譯器一次處理單個編譯單元(源代檔案),並將目標檔案合併到連結器。LLVM 的 CFI 要求使用 LTO,其編譯器為所有 C 編譯單元生成特定於 LLVM 的 bitcode,並且 LTO 感知連結器使用 LLVM 後端來組合 bitcode,並將其編譯為本機程式碼。

[譯] Android 核心控制流完整性

圖 2. LTO 在核心中的工作原理的簡單概述。所有 LLVM bitcode 在連結時被組合,優化並生成本機程式碼。

幾十年來,Linux 一直使用 GNU 工具鏈來彙編,編譯和連結核心。雖然我們繼續將 GNU 彙編程式用於獨立的彙編程式碼,但 LTO 要求我們切換到 LLVM 的整合彙編程式以進行內聯彙編,並將 GNU gold 或 LLVM 自己的 lld 作為連結器。在巨大的軟體專案上切換到未經測試的工具鏈會導致相容性問題,我們已經在核心版本 4.94.14 的 arm64 LTO 補丁集中解決了這些問題。

除了使 CFI 成為可能,由於全域性優化,LTO 還可以生成更快的程式碼。但額外的優化通常會導致更大的二進位制尺寸,這在資源受限的裝置上可能是不需要的。禁用 LTO 特定的優化(比如全域性內聯和迴圈展開)可以通過犧牲一些效能收益來減少二進位制尺寸。使用 GNU gold 時,可以通過以下方式設定 LDFLAGS 來禁用上述優化:

LDFLAGS += -plugin-opt=-inline-threshold=0 \
           -plugin-opt=-unroll-threshold=0
複製程式碼

注意,禁用單個優化的標誌不是穩定 LLVM 介面的一部分,在將來的編譯器版本中可能會更改。

在 Linux 核心中實現 CFI

LLVM 的 CFI 實現在每個間接分支之前新增一個檢查,以確認目標地址指向一個擁有有效簽名的函式。這可以防止一個間接分支跳轉到任意程式碼位置,甚至限制可以呼叫的函式。由於 C 編譯器沒有對間接分支強制執行類似限制,函式型別宣告不匹配導致了幾個 CFI 違規,即使在我們在核心的 CFI 補丁集中解決的核心 4.94.14 中也是如此。

核心模組為 CFI 新增了另一個複雜功能,因為它們在執行時載入,並且可以獨立於核心的其它部分進行編譯。為了支援可載入模組,我們在核心中實現了 LLVM 的 cross-DSO CFI 支援,包括用來加速跨模組查詢的 CFI 影子。在使用 cross-DSO 支援進行編譯時,每個核心模組都會包含有關有效本地分支目標的資訊,核心根據目標地址和模組的記憶體佈局從正確的模組中查詢資訊。

[譯] Android 核心控制流完整性

圖 3. 注入 arm64 核心的 cross-DSO CFI 檢查示例。型別資訊在 X0 中傳遞,目標地址在 X1 中驗證。

CFI 檢查會給間接分支增加一些開銷,但由於更積極的優化,我們的測試表明影響很小,在很多情況下整體系統效能甚至提高了 1-2%。

為 Android 裝置啟用核心 CFI

arm64 中的 CFI 需要 clang 版本 >= 5.0 並且 binutils >= 2.27。核心構建系統還假定 LLVMgold.so 外掛在 LD_LIBRARY_PATH 中可用。clangbinutils 預構建工具鏈二進位制檔案可在 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 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章