控制流程完整性

截至 2016 年,Android 上約有 86% 的漏洞與記憶體安全性有關。攻擊者會利用大多數漏洞,變更應用程式的正常控制流程,以便利用遭到攻擊的應用程式,執行任意惡意活動。控制流程完整性 (CFI) 是一種安全機制,可禁止編譯二進位檔的原始控制流程圖變更,因此攻擊者要執行這類攻擊的難度會大幅提高。

在 Android 8.1 中,我們在媒體堆疊中啟用了 LLVM 的 CFI 實作。在 Android 9 中,我們在更多元件和核心中啟用 CFI。系統 CFI 預設為開啟,但您需要啟用核心 CFI。

LLVM 的 CFI 需要使用連結時間最佳化 (LTO) 進行編譯。LTO 會保留物件檔案的 LLVM 位元碼表示法,直到連結時間為止,讓編譯器能更妥善地判斷可執行哪些最佳化作業。啟用 LTO 可縮減最終二進位檔的大小並提升效能,但會增加編譯時間。在 Android 上進行測試時,LTO 和 CFI 的組合會對程式碼大小和效能造成微不足道的額外負擔;在少數情況下,兩者都會有所改善。

如要進一步瞭解 CFI 的技術細節,以及如何處理其他前向控制檢查,請參閱 LLVM 設計文件

範例和來源

CFI 是由編譯器提供,並在編譯期間將檢測功能加入二進位檔。我們在 Clang 工具鍊和 AOSP 中的 Android 建構系統中支援 CFI。

根據預設,Arm64 裝置會為 /platform/build/target/product/cfi-common.mk 中的一組元件啟用 CFI。您也可以直接在一系列媒體元件的 Makefile/藍圖檔案中啟用此功能,例如 /platform/frameworks/av/media/libmedia/Android.bp/platform/frameworks/av/cmds/stagefright/Android.mk

實作系統 CFI

如果您使用 Clang 和 Android 建構系統,系統會預設啟用 CFI。CFI 可協助保障 Android 使用者的安全,因此不應停用。

事實上,我們強烈建議您為其他元件啟用 CFI。理想的候選項目是具有特殊權限的原生程式碼,或是處理不受信任的使用者輸入內容的原生程式碼。如果您使用的是 clang 和 Android 建構系統,可以在 Makefile 或藍圖檔案中加入幾行程式碼,在新的元件中啟用 CFI。

在 Makefile 中支援 CFI

如要在 /platform/frameworks/av/cmds/stagefright/Android.mk 等建構檔案中啟用 CFI,請新增以下內容:

LOCAL_SANITIZE := cfi
# Optional features
LOCAL_SANITIZE_DIAG := cfi
LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
  • LOCAL_SANITIZE 會在建構期間將 CFI 指定為消毒劑。
  • LOCAL_SANITIZE_DIAG 會為 CFI 開啟診斷模式。診斷模式會在應用程式發生當機時,在 Logcat 中列印其他偵錯資訊,這在開發及測試版本時非常實用。不過,請務必在正式版版本中移除診斷模式。
  • LOCAL_SANITIZE_BLACKLIST 可讓元件針對個別函式或來源檔案,有選擇地停用 CFI 檢測功能。您可以使用黑名單做為最後手段,解決可能存在的任何使用者面向問題。詳情請參閱「停用 CFI」。

在藍圖檔案中支援 CFI

如要在藍圖檔案 (例如 /platform/frameworks/av/media/libmedia/Android.bp) 中啟用 CFI,請新增以下內容:

   sanitize: {
        cfi: true,
        diag: {
            cfi: true,
        },
        blacklist: "cfi_blacklist.txt",
    },

疑難排解

如果您要在新元件中啟用 CFI,可能會遇到幾個問題,包括函式類型不相符錯誤彙整程式碼類型不相符錯誤

由於 CFI 會限制間接呼叫,只跳至與呼叫中使用的靜態類型相同的動態類型函式,因此會發生函式類型不相符的錯誤。CFI 會限制虛擬和非虛擬成員函式呼叫,只跳至用於呼叫的物件靜態類型的衍生類別物件。也就是說,如果程式碼違反上述任一假設,CFI 所新增的檢測功能就會中止。舉例來說,堆疊追蹤記錄會顯示 SIGABRT,而 Logcat 會包含一行關於控制流程完整性發現不相符的訊息。

如要修正這個問題,請確認呼叫的函式與靜態宣告的類型相同。以下是兩個 CL 範例:

另一個可能的問題是,嘗試在包含對彙編作業的間接呼叫的程式碼中啟用 CFI。由於組合語程式碼未經過型別化,因此會導致型別不相符。

如要修正這個問題,請為每個組合呼叫建立原生程式碼包裝函式,並為包裝函式提供與呼叫指標相同的函式簽章。接著,包裝函式就能直接呼叫彙整程式碼。由於直接分支並未由 CFI 檢測 (無法在執行階段重新指派,因此不會造成安全性風險),因此這麼做就能解決問題。

如果組合函式過多,且無法全部修正,您也可以將所有包含對組合間接呼叫的函式加入黑名單。這麼做並不建議,因為這會停用這些函式的 CFI 檢查,進而開放攻擊面。

停用 CFI

我們並未觀察到任何效能額外負擔,因此您不必停用 CFI。不過,如果有使用者面臨的影響,您可以在編譯時提供清理器黑名單檔案,針對個別函式或來源檔案選擇性停用 CFI。黑名單會指示編譯器在指定位置停用 CFI 檢測。

Android 建構系統支援 Make 和 Soong 的個別元件黑名單 (可讓您選擇不會接收 CFI 檢測的來源檔案或個別函式)。如要進一步瞭解黑名單檔案的格式,請參閱 上游 Clang 說明文件

驗證

目前沒有專門針對 CFI 進行的 CTS 測試。請改為確認啟用或停用 CFI 時,CTS 測試是否都能通過,以驗證 CFI 不會影響裝置。