15. 控制流強制技術 (CET) 影子堆疊¶
15.1. CET 背景¶
控制流強制技術 (CET) 涵蓋了幾個相關的 x86 處理器特性,這些特性提供了針對控制流劫持攻擊的保護。 CET 可以保護應用程式和核心。
CET 引入了影子堆疊和間接分支跟蹤 (IBT)。影子堆疊是從記憶體中分配的二級堆疊,應用程式無法直接修改。執行 CALL 指令時,處理器會將返回地址推送到普通堆疊和影子堆疊。函式返回時,處理器會彈出影子堆疊副本並將其與普通堆疊副本進行比較。如果兩者不同,處理器會引發控制保護故障。 IBT 驗證間接 CALL/JMP 目標是否按照編譯器使用 'ENDBR' 操作碼標記的那樣。並非所有 CPU 都同時具有影子堆疊和間接分支跟蹤。目前,在 64 位核心中,僅支援使用者空間影子堆疊和核心 IBT。
15.2. 使用影子堆疊的要求¶
要使用使用者空間影子堆疊,您需要支援它的硬體、配置了它的核心以及使用它編譯的使用者空間庫。
核心 Kconfig 選項為 X86_USER_SHADOW_STACK。編譯後,可以使用核心引數在執行時停用影子堆疊:nousershstk。
要構建啟用使用者影子堆疊的核心,需要 Binutils v2.29 或 LLVM v6 或更高版本。
在執行時,如果處理器支援 CET,則 /proc/cpuinfo 會顯示 CET 特性。 "user_shstk" 表示當前核心和硬體支援使用者空間影子堆疊。
15.3. 應用程式啟用¶
應用程式的 CET 功能在其 ELF 註釋中標記,並且可以從 readelf/llvm-readelf 輸出中驗證
readelf -n <application> | grep -a SHSTK
properties: x86 feature: SHSTK
核心不直接處理這些應用程式標記。應用程式或載入程式必須使用第 4 節中描述的介面啟用 CET 特性。通常,這將在動態載入程式或靜態執行時物件中完成,就像 GLIBC 中那樣。
15.4. 啟用 arch_prctl() 的方法¶
Elf 特性應由載入程式使用以下 arch_prctl 啟用。它們僅在 64 位使用者應用程式中受支援。這些操作以每個執行緒為基礎。啟用狀態在克隆時繼承,因此如果在第一個執行緒上啟用了該特性,它將傳播到應用程式中的所有執行緒。
- arch_prctl(ARCH_SHSTK_ENABLE, unsigned long feature)
啟用 'feature' 中指定的單個特性。一次只能操作一個特性。
- arch_prctl(ARCH_SHSTK_DISABLE, unsigned long feature)
停用 'feature' 中指定的單個特性。一次只能操作一個特性。
- arch_prctl(ARCH_SHSTK_LOCK, unsigned long features)
鎖定其當前啟用或停用狀態的特性。 'features' 是要鎖定的所有特性的掩碼。所有設定的位都將被處理,未設定的位將被忽略。掩碼與現有值進行 OR 運算。因此,此處設定的任何特性位之後都無法啟用或停用。
- arch_prctl(ARCH_SHSTK_UNLOCK, unsigned long features)
解鎖特性。 'features' 是要解鎖的所有特性的掩碼。所有設定的位都將被處理,未設定的位將被忽略。僅透過 ptrace 工作。
- arch_prctl(ARCH_SHSTK_STATUS, unsigned long addr)
將當前啟用的特性複製到 addr 中傳遞的地址。這些特性使用傳遞到 'features' 中其他特性的位來描述。
返回值如下。成功時,返回 0。出錯時,errno 可以是
-EPERM if any of the passed feature are locked.
-ENOTSUPP if the feature is not supported by the hardware or
kernel.
-EINVAL arguments (non existing feature, etc)
-EFAULT if could not copy information back to userspace
支援的特性的位是
ARCH_SHSTK_SHSTK - Shadow stack
ARCH_SHSTK_WRSS - WRSS
目前,影子堆疊和 WRSS 透過此介面支援。 WRSS 只能與影子堆疊一起啟用,並且如果停用影子堆疊,則會自動停用。
15.5. Proc 狀態¶
要檢查應用程式是否實際使用影子堆疊執行,使用者可以讀取 /proc/$PID/status。它將報告 "wrss" 或 "shstk",具體取決於啟用了什麼。這些行看起來像這樣
x86_Thread_features: shstk wrss
x86_Thread_features_locked: shstk wrss
15.6. 影子堆疊的實現¶
15.6.1. 影子堆疊大小¶
任務的影子堆疊從記憶體中分配到 MIN(RLIMIT_STACK, 4 GB) 的固定大小。換句話說,影子堆疊分配到正常堆疊的最大大小,但上限為 4 GB。在 clone3 系統呼叫的情況下,會傳入堆疊大小,影子堆疊會使用它來代替 rlimit。
15.6.2. 訊號¶
主程式及其訊號處理程式使用相同的影子堆疊。由於影子堆疊僅儲存返回地址,因此大的影子堆疊涵蓋了程式堆疊和訊號交替堆疊都耗盡的情況。
發生訊號時,舊的訊號前狀態會被推送到堆疊上。啟用影子堆疊後,影子堆疊的特定狀態會被推送到影子堆疊上。今天,這只是舊的 SSP(影子堆疊指標),以設定了位 63 的特殊格式推送。在 sigreturn 時,核心會驗證並恢復此舊的 SSP 令牌。核心還將把正常的恢復器地址推送到影子堆疊,以幫助使用者空間避免在透過恢復器的 sigreturn 路徑上發生影子堆疊違規。
因此,影子堆疊訊號幀格式如下
|1...old SSP| - Pointer to old pre-signal ssp in sigframe token format
(bit 63 set to 1)
| ...| - Other state may be added in the future
在影子堆疊程序中不支援 32 位 ABI 訊號。 Linux 透過在 32 位地址空間之外分配影子堆疊來防止在啟用影子堆疊時執行 32 位程式。當執行進入 32 位模式時,無論是透過遠呼叫還是返回到使用者空間,硬體都會生成 #GP,這將作為段錯誤傳遞給程序。當轉換到使用者空間時,暫存器的狀態就好像返回到的使用者空間 ip 導致了段錯誤。
15.6.3. Fork¶
影子堆疊的 vma 設定了 VM_SHADOW_STACK 標誌;其 PTE 必須是隻讀且已修改。當影子堆疊 PTE 不是 RO 且已修改時,影子訪問會觸發頁面錯誤,並在頁面錯誤錯誤程式碼中設定影子堆疊訪問位。
當任務 fork 子程序時,其影子堆疊 PTE 會被複制,並且父程序和子程序的影子堆疊 PTE 都會清除修改位。在下次影子堆疊訪問時,生成的影子堆疊頁面錯誤由頁面複製/重用處理。
建立 pthread 子程序時,核心會為新執行緒分配一個新的影子堆疊。新的影子堆疊建立在 ASLR 行為方面類似於 mmap()。類似地,線上程退出時,該執行緒的影子堆疊會被停用。
15.6.4. Exec¶
在 exec 上,核心會停用影子堆疊特性。此時,使用者空間可以選擇重新啟用或鎖定它們。