核心自保護

核心自保護是指在 Linux 核心中設計和實現系統與結構,以抵禦核心自身存在的安全漏洞。這涵蓋了廣泛的問題,包括消除整類錯誤、阻止安全漏洞利用方法以及主動檢測攻擊嘗試。本文件並未探討所有主題,但它應該是一個合理的起點,並能解答常見的疑問。(當然,歡迎提交補丁!)

在最壞的情況下,我們假設一個非特權本地攻擊者可以對核心記憶體進行任意讀寫。在許多情況下,被利用的漏洞並不會提供這種級別的訪問許可權,但透過部署能夠防禦最壞情況的系統,我們也能覆蓋更有限的情況。一個更高的標準(也應牢記在心)是保護核心免受_特權_本地攻擊者的侵害,因為 root 使用者擁有大幅增加的攻擊面。(特別是當他們能夠載入任意核心模組時。)

成功的自保護系統的目標是:有效、預設開啟、無需開發者選擇啟用、無效能影響、不阻礙核心除錯,並有相應的測試。很少能完全滿足所有這些目標,但明確提及它們是值得的,因為這些方面需要被探索、處理和/或接受。

減小攻擊面

抵禦安全漏洞最基本的防禦措施是減少核心中可用於重定向執行的區域。這包括限制暴露給使用者空間的 API、使核心內 API 難以被錯誤使用、最小化可寫核心記憶體區域等。

嚴格的核心記憶體許可權

當所有核心記憶體都可寫時,攻擊者重定向執行流就變得輕而易舉。為了減少這些攻擊目標的可用性,核心需要用一套嚴格的許可權來保護其記憶體。

可執行程式碼和只讀資料必須不可寫

核心中任何具有可執行記憶體的區域都必須不可寫。顯然,這包括核心文字本身,但我們也必須考慮所有其他地方:核心模組、JIT 記憶體等。(此規則存在臨時例外,以支援指令替代、斷點、kprobes 等。如果這些必須存在於核心中,則其實現方式是:在更新期間記憶體被臨時設定為可寫,然後恢復為原始許可權。)

為此,有 CONFIG_STRICT_KERNEL_RWXCONFIG_STRICT_MODULE_RWX,它們旨在確保程式碼不可寫,資料不可執行,只讀資料既不可寫也不可執行。

大多數架構預設啟用這些選項,並且使用者不可選擇。對於一些希望這些選項可選的架構(例如 ARM),架構的 Kconfig 可以選擇 ARCH_OPTIONAL_KERNEL_RWX 來啟用 Kconfig 提示。當 ARCH_OPTIONAL_KERNEL_RWX 啟用時,CONFIG_ARCH_OPTIONAL_KERNEL_RWX_DEFAULT 決定了預設設定。

函式指標和敏感變數必須不可寫

大量核心記憶體區域包含函式指標,這些指標由核心查詢並用於繼續執行(例如,描述符/向量表、檔案/網路/等操作結構等)。必須將這些變數的數量減少到絕對最少。

許多此類變數可以透過將其設定為“const”而變為只讀,從而使其儲存在核心的 .rodata 段而不是 .data 段中,從而獲得上述核心嚴格記憶體許可權的保護。

對於在 __init 時一次性初始化的變數,可以使用 __ro_after_init 屬性標記它們。

剩下的是很少更新的變數(例如 GDT)。這些將需要另一種基礎設施(類似於上面提到的對核心程式碼所做的臨時例外),使其在其餘生命週期中保持只讀。(例如,在更新時,只有執行更新的 CPU 執行緒才會被授予對記憶體的不可中斷寫入訪問許可權。)

核心記憶體與使用者空間記憶體隔離

核心絕不能執行使用者空間記憶體。核心也絕不能在沒有明確預期的情況下訪問使用者空間記憶體。這些規則可以透過硬體限制(x86 的 SMEP/SMAP、ARM 的 PXN/PAN)或透過模擬(ARM 的記憶體域)來強制執行。透過這種方式阻止使用者空間記憶體,執行和資料解析就不能傳遞到輕易受控的使用者空間記憶體,從而迫使攻擊完全在核心記憶體中進行。

減少對系統呼叫的訪問

對於 64 位系統,消除許多系統呼叫的一種簡單方法是構建時停用 CONFIG_COMPAT。然而,這很少是可行的方案。

“seccomp”系統提供了一個可供使用者空間選擇啟用的功能,它提供了一種方法來減少執行程序可用的核心入口點數量。這限制了可觸及的核心程式碼範圍,從而可能減少給定漏洞可被攻擊利用的程度。

一個可以改進的領域是建立可行的方法,將對相容性(compat)、使用者名稱空間、BPF 建立和 perf 等功能的訪問僅限於受信任的程序。這將使核心入口點的範圍限制在通常提供給非特權使用者空間的更常規的集合。

限制對核心模組的訪問

核心絕不應允許非特權使用者載入特定的核心模組,因為這將提供一個意想不到地擴充套件可用攻擊面的途徑。(透過預定義子系統按需載入模組,例如 MODULE_ALIAS_*,在此被認為是“預期”行為,但即使對這些情況也應給予額外考慮。) 例如,透過非特權套接字 API 載入檔案系統模組是無稽之談:只有 root 使用者或物理本地使用者才應觸發檔案系統模組載入。(即使這一點在某些情況下也可能存在爭議。)

為了保護甚至特權使用者,系統可能需要完全停用模組載入(例如,單片核心構建或 modules_disabled sysctl),或提供簽名模組(例如,CONFIG_MODULE_SIG_FORCE,或帶有 LoadPin 的 dm-crypt),以防止 root 透過模組載入器介面載入任意核心程式碼。

記憶體完整性

核心中有許多記憶體結構在攻擊期間經常被濫用以獲取執行控制。到目前為止,最普遍理解的是棧緩衝區溢位,其中棧上儲存的返回地址被覆蓋。這種攻擊還有許多其他例子,並且存在防禦措施來抵禦它們。

棧緩衝區溢位

經典的棧緩衝區溢位涉及寫入超出棧上儲存變數的預期末端,最終向棧幀儲存的返回地址寫入一個受控值。最廣泛使用的防禦措施是在棧變數和返回地址之間存在棧金絲雀(CONFIG_STACKPROTECTOR),該金絲雀在函式返回前進行驗證。其他防禦措施包括影子棧等。

棧深度溢位

一種較少被理解的攻擊是利用一個錯誤,該錯誤觸發核心透過深度函式呼叫或大量棧分配來消耗棧記憶體。透過這種攻擊,有可能寫入超出核心預分配棧空間的末端並進入敏感結構。為了更好的保護,需要進行兩個重要的改變:將敏感的 thread_info 結構移到別處,並在棧底部新增一個故障記憶體空洞以捕獲這些溢位。

堆記憶體完整性

用於跟蹤堆空閒列表的結構可以在分配和釋放期間進行健全性檢查,以確保它們不會被用於操作其他記憶體區域。

計數器完整性

核心中許多地方使用原子計數器來跟蹤物件引用或執行類似的生命週期管理。當這些計數器可以被強制迴繞(向上或向下溢位)時,傳統上會暴露一個使用後釋放(use-after-free)漏洞。透過捕獲原子迴繞,這類錯誤就會消失。

大小計算溢位檢測

類似於計數器溢位,整數溢位(通常是大小計算)需要在執行時被檢測到,以消除這類錯誤,這類錯誤傳統上會導致能夠寫入超出核心緩衝區末端。

機率防禦

雖然許多保護措施可以被認為是確定性的(例如,只讀記憶體不可寫入),但有些保護措施僅提供統計性防禦,因為攻擊者必須收集足夠的關於執行系統的資訊才能克服防禦。儘管不完美,但這些防禦確實提供了有意義的保護。

金絲雀、致盲及其他秘密

值得注意的是,前面討論的棧金絲雀等技術在技術上屬於統計性防禦,因為它們依賴於一個秘密值,而這些值可能會透過資訊洩露漏洞被發現。

對於像 JIT 這樣的情況,可執行內容可能部分受使用者空間控制,此時對字面值進行致盲處理也需要類似的秘密值。

至關重要的是,所使用的秘密值必須是獨立的(例如,每個棧有不同的金絲雀)並且具有高熵值(例如,RNG 實際工作了嗎?),以最大限度地提高其成功率。

核心地址空間佈局隨機化 (KASLR)

由於核心記憶體的位置幾乎總是發起成功攻擊的關鍵,因此使位置非確定性會增加漏洞利用的難度。(請注意,這反過來又會提高資訊洩露的價值,因為它們可能被用於發現所需的記憶體位置。)

文字和模組基址

透過在啟動時重新定位核心的物理和虛擬基址(CONFIG_RANDOMIZE_BASE),需要核心程式碼的攻擊將受挫。此外,偏移模組載入基址意味著即使在每次啟動時以相同順序載入相同模組集的系統,也不會與其餘核心文字共享公共基址。

棧基址

如果核心棧的基址在不同程序之間不同,甚至在不同系統呼叫之間也不同,那麼位於棧上或棧以外的目標將變得更難定位。

動態記憶體基址

核心的大部分動態記憶體(例如 kmalloc、vmalloc 等)由於早期啟動初始化的順序,其佈局最終相對確定。如果這些區域的基址在不同啟動之間不同,那麼針對它們的攻擊將受挫,需要針對該區域的特定資訊洩露。

結構佈局

透過對敏感結構進行每次構建的佈局隨機化,攻擊必須要麼針對已知的核心構建進行調整,要麼暴露足夠的核心記憶體以在操作它們之前確定結構佈局。

防止資訊洩露

由於敏感結構的位置是攻擊的主要目標,因此防禦核心記憶體地址和核心記憶體內容的洩露都非常重要(因為它們可能包含核心地址或其他敏感資訊,如金絲雀值)。

核心地址

將核心地址列印到使用者空間會洩露有關核心記憶體佈局的敏感資訊。在使用任何列印原始地址的 printk 格式說明符時應謹慎,目前包括 %px、%p[ad] 以及在某些情況下(*)的 %p[sSb]。使用這些格式說明符寫入的任何檔案都應僅對特權程序可讀。

4.14 及更早的核心使用 %p 列印原始地址。自 4.15-rc1 起,使用 %p 格式說明符列印的地址在列印前會進行雜湊處理。

[*] 如果 KALLSYMS 啟用且符號查詢失敗,則列印原始地址。如果 KALLSYMS 未啟用,則列印原始地址。

唯一識別符號

核心記憶體地址絕不能用作暴露給使用者空間的識別符號。相反,應使用原子計數器、idr 或類似的唯一識別符號。

記憶體初始化

複製到使用者空間的記憶體必須始終完全初始化。如果沒有顯式地 memset(),這將需要修改編譯器以確保結構中的空洞被清除。

記憶體毒化

釋放記憶體時,最好對內容進行“毒化”處理,以避免依賴記憶體舊內容的重用攻擊。例如,在系統呼叫返回時清除棧(CONFIG_GCC_PLUGIN_STACKLEAK),在釋放時擦除堆記憶體。這會挫敗許多未初始化變數攻擊、棧內容洩露、堆內容洩露以及使用後釋放攻擊。

目標跟蹤

為了幫助消除導致核心地址寫入使用者空間的錯誤類別,需要跟蹤寫入的目的地。如果緩衝區是為使用者空間準備的(例如 seq_file 支援的 /proc 檔案),它應該自動審查敏感值。