簡介
LLVM 的安全堆疊功能是一種編譯器模式,旨在強化產生的程式碼,防範堆疊破壞攻擊,例如緩衝區溢位錯誤的攻擊。
上方連結的 Clang/LLVM 說明文件頁面說明瞭一般架構。簡而言之,每個執行緒都有兩個堆疊,而不是通常的一個:「安全堆疊」和「不安全堆疊」。凡是可能使用堆疊記憶體指標的用途,一律使用不安全堆疊;安全堆疊則僅用於不應有任何程式碼看到堆疊記憶體指標的用途。因此,不安全堆疊會用於以參照形式傳遞至其他函式,或位址儲存在堆積中的陣列或變數,這類記憶體可能會受到緩衝區溢位或釋放後使用錯誤及其利用的影響。安全堆疊用於編譯器的暫存器溢出,以及函式呼叫的傳回位址。因此,舉例來說,簡單的緩衝區溢位錯誤無法用於覆寫所含函式的回傳位址,而這正是利用所謂 ROP (Return-Oriented Programming,回傳導向程式設計) 技術進行攻擊的基礎。
該頁面的「相容性」部分不適用於 Zircon (或 Fuchsia)。在 Zircon 使用者模式程式碼 (包括所有 Fuchsia 程式碼) 中,SafeStack 的執行階段支援直接納入標準 C 執行階段程式庫,且共用程式庫 (DSO) 中的一切運作正常。
safe-stack 和 shadow-call-stack 插樁機制和 ABI 相關且相似,但也是正交的。您可以為任何函式個別啟用或停用這些功能。無論是否使用任何一種插樁,Fuchsia 的編譯器 ABI 和 libc 一律會與建構的程式碼互通,與特定 libc 建構中是否使用插樁無關。
互通性和 ABI 影響
一般來說,安全堆疊不會影響 ABI。機器專屬的呼叫慣例維持不變。在以 Safe-Stack 建構的程式中,部分函式使用安全函式,部分則否,這樣也沒問題。無論是直接編譯的 .o
檔案、封存程式庫 (.a
檔案) 或共用程式庫 (.so
檔案),都可以任意組合。
雖然每個執行緒都有一些額外的狀態 (不安全的堆疊指標,請參閱下方的「實作詳細資料」),但如果程式碼未使用安全堆疊,就不必處理這個狀態,即可在呼叫或被呼叫使用安全堆疊的程式碼時,保持狀態正確。唯一可能的例外狀況是實作自有非本機結束或內容切換 (例如協同程式) 的程式碼。Zircon C 程式庫的 setjmp
/longjmp
程式碼會自動儲存及還原這個額外狀態,因此即使呼叫 setjmp
和 longjmp
的程式碼不知道安全堆疊,以 longjmp
為基礎的任何項目都已正確處理所有事項。
在 Zircon 和 Fuchsia 中使用
Clang 編譯器會透過 -fsanitize=safe-stack
指令列選項啟用這項功能。這是 x86_64-fuchsia
目標的預設編譯器模式。如要為特定編譯作業停用這項功能,請使用 -fno-sanitize=safe-stack
選項。aarch64-fuchsia
和 riscv64-fuchsia
目標預設會啟用 shadow-call-stack。
Zircon 支援使用者模式和核心程式碼的 safe-stack。在 x86 Zircon 版本中,使用 Clang 建構時一律會啟用 safe-stack (將 variants = [ "clang" ]
傳遞至 GN
)。
導入作業詳細資料
支援安全堆疊程式碼的必要新增項目是不安全的堆疊指標。抽象來說,這可以視為額外的暫存器,就像機器的正常堆疊指標暫存器一樣。機器堆疊指標暫存器會用於安全堆疊,一如往常。不安全的堆疊指標會當做 ABI 中具有固定用途的另一個暫存器使用,但當然機器實際上沒有新的暫存器,而且為了相容性,safe-stack 不會變更指派用途給所有機器暫存器的基本機器專屬呼叫慣例。
Zircon 和 Fuchsia 的 C 和 C++ ABI 會將不安全的堆疊指標儲存在記憶體中,該記憶體與執行緒指標的偏移量固定。<zircon/tls.h>
標頭會定義每部機器的偏移量。
在 x86 使用者模式中,執行緒指標為 fsbase
,也就是說,組語程式碼中的存取權看起來像 %fs:ZX_TLS_UNSAFE_SP_OFFSET
。對於 x86 核心,執行緒指標是 gsbase
,也就是說,組語中的存取權看起來像 %gs:ZX_TLS_UNSAFE_SP_OFFSET
。
如果是 Aarch64 (ARM64),在 C 或 C++ 程式碼中,__builtin_thread_pointer()
會傳回執行緒指標。在使用者模式中,執行緒指標位於 TPIDR_EL0
特殊暫存器中,且必須擷取至一般暫存器 (使用 mrs *reg*, TPIDR_EL0
) 才能存取記憶體,因此並非組合語言程式碼中的單一指令。在核心中,這與上述情況相同,但使用的是 TPIDR_EL1
特殊暫存器。
低階和組合語言程式碼的注意事項
即使是組語,大多數程式碼也不必考慮安全堆疊問題。呼叫慣例沒有變更。無論有無安全堆疊,使用堆疊儲存暫存器、尋找回傳位址等作業都相同。主要例外狀況是實作非本機結束或內容切換等項目的程式碼。這類程式碼可能需要儲存或還原不安全的堆疊指標。longjmp
函式和 C++ throw
都已直接處理這項作業,因此使用這些建構體的 C 或 C++ 程式碼不需要執行任何新作業。
核心中的內容切換程式碼會處理不安全堆疊指標的切換作業。在 x86 上,程式碼中會明確指出:%gs
指向 struct x86_percpu
,後者在 ZX_TLS_UNSAFE_SP_OFFSET
有 kernel_unsafe_sp
成員;arch_context_switch
會將此成員複製到舊執行緒的 struct arch_thread
的 unsafe_sp
欄位,然後將新執行緒的 unsafe_sp
複製到 kernel_unsafe_sp
。在 ARM64 上,這項作業會由 set_current_thread
隱含完成,因為這會變更 TPIDR_EL1
特殊暫存器,該暫存器會直接指向每個執行緒的 struct thread
,而不是像 x86 一樣指向每個 CPU 的結構。
實作某種新類型非本機結束或內容切換的新程式碼,需要以類似於處理傳統機器堆疊指標暫存器的方式,處理不安全的堆疊指標。這類程式碼應使用 #if __has_feature(safe_stack)
,在編譯時測試特定建構作業是否使用安全堆疊。該前置處理器建構體可用於 C、C++ 或組語 (.S
) 來源檔案。