RFC-0159:僅執行記憶體

RFC-0159:執行記憶體
狀態已接受
區域
  • 核心
  • 工具鏈
說明

支援對應僅執行記憶體。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2022-03-29
審查日期 (年-月-日)2022-05-10

摘要

本文建議對核心 API 進行變更,以便支援含有僅執行片段的二進位檔,方法是在 zx_system_get_features 中新增功能檢查,並變更 Fuchsia 樹狀結構中的 launchpadprocess_builder 載入器,以及動態連結器,以支援 '--x' 片段。這項計畫將為最終核心支援提供藍圖,以便在支援的硬體上對映僅供執行的頁面。

我們通常不需要在可執行記憶體載入後讀取該記憶體。預設啟用僅執行程式碼可提升 Fuchsia 使用者空間程序的安全性,並進一步實踐最少權限的工程最佳做法。

提振精神

ARMv7m 中的 ARM MMU 已新增僅供執行的頁面支援功能,可將記憶體頁面對應至僅供執行,而無法讀取或寫入。雖然可寫的程式碼頁面長期以來一直被視為安全威脅,但允許程式碼保持可讀性,已證實會讓應用程式公開不必要的風險。具體來說,讀取程式碼頁面通常是攻擊鏈中的第一步,而防範程式碼讀取功能會妨礙攻擊者。請參閱「可讀程式碼安全性」。此外,支援僅執行頁面非常適合 Fuchsia 的權限模型,且更符合最低權限原則:程式碼通常不需要讀取,只需要執行。

相關人員

協助人員:

  • cpu@google.com

審查者:

  • phosek@google.com
  • mvanotti@google.com
  • maniscalco@google.com
  • travisg@google.com

背景

僅執行記憶體

唯執行記憶體 (XOM) 是指記憶體頁面既沒有讀取權限也沒有寫入權限,只能執行的記憶體頁面。ARMv7m 以上版本原生支援 XOM,但較舊的 ISA 需要考量一些事項。詳情請參閱 XOM 和 PAN

本文幾乎只著重於 AArch64,但實作方式不受架構限制。當硬體和工具鍊支援其他架構的成熟度提升時,這些架構就能輕鬆利用 Fuchsia 的執行專屬支援功能。

程式碼頁面的權限

一開始,電腦支援對實體記憶體的直接記憶體存取,但沒有任何檢查或保護措施。MMU 的引入,透過將程式對記憶體的觀點與基礎實體資源分離,以虛擬記憶體的形式提供重要抽象概念。這項功能可讓作業系統實作者透過程序抽象化,在程式之間提供強大的隔離機制,進而促成更靈活、安全的程式設計模式。現今的 MMU 提供多項重要設施,例如分頁式記憶體、快速位址轉譯和權限檢查。使用者也可以透過頁面權限,大幅控管記憶體區域的存取和使用方式,這些權限通常會控制記憶體頁面是否可供讀取、寫入或執行。這是程式安全性、錯誤隔離和安全性的關鍵屬性,因為它會透過硬體強制執行權限檢查,限制程式濫用系統資源的能力。

可寫入及可執行的記憶體特別危險,因為這會讓對手輕易透過常見的安全漏洞 (例如緩衝區溢位) 執行任意程式碼。因此,許多作業系統設定都明確禁止頁面同時具備可寫入和可執行的特性 (W^X)。這項標準已沿用超過十年,OpenBSD 在 2003 年推出 OpenBSD 3.3 openbsd-wxorx 時,也加入了對 W^X 的支援。另請參閱 SELinux W^X 政策 selinux-wxorx。可寫入的程式碼可用於 Just-In-Time (JIT) 編譯,這類編譯會在執行階段將可執行的指令寫入記憶體。系統可能會禁止使用 W|X 頁面,而 JIT 需要繞過這個問題。您可以輕鬆地將程式碼寫入非可執行的頁面,然後變更頁面防護措施 (例如透過 mprotectzx_vmar_protect),讓頁面可執行但無法寫入 example-fuchsia-test。在幾乎所有情況下,W|X 網頁都過於寬鬆。同樣地,執行頁面幾乎不需要讀取查看例外狀況。一般來說,在可執行頁面上允許讀取作業是不必要的,也不應設為預設值。

可讀程式碼

由於 ARM 的固定指令寬度,立即值具有大小限制。因此,載入作業會使用 PC 相對於位址。為解決這個問題,虛擬指令 ldr Rd, =imm 會在字面值池中,靠近載入該字面值的程式碼處,發出 imm。這與 XOM 不相容,因為它會將資料放入必須可讀取的文字部分。在搜尋程式碼庫中使用字面池的用途,以確保我們不會讀取可執行的區段時,我們在 Zircon 中發現了一些 ldr Rd, =imm 用途,但這些用途現已全部移除。Clang 不會為 aarch64 使用文字常值集區,而是會發出多個指令來建立大型立即值。Clang 有 -mexecute-only 標記和別名 -mpure-code,但這些標記只有在 arm32 上才有意義,因為在以 aarch64 為目標時,這些標記是固有的。

範例:大型中介

這個範例說明 Clang 如何在指定不同目標 clang-example 時,將此 C 程式碼編譯為彙整。頂端列顯示 aarch64,底部列則顯示 arm32:

uint32_t a() {
    return 0x12345678u;
}
# -target aarch64
a:
    mov w0, #22136
    movk w0, #4660, lsl #16
    ret
# -target arm
a:
    ldr r0, .LCPI0_0
    bx lr
.LCPI0_0:
    .long 305419896

XOM 和 PAN

從未使用特權存取 (PAN) 是 ARM 晶片上的安全性功能,可防止使用者頁面從核心模式進行一般記憶體存取。由於核心無法透過一般載入或儲存指令存取使用者記憶體,因此這有助於防範潛在的核心漏洞。相反地,作業系統必須關閉 PAN 或使用 ldtrsttr 指令存取這些頁面。目前未為 Fuchsia 啟用 PAN,但我們已計劃在 zircon pan-fxb 中支援這項功能。

Aarch64 頁面表格項目有 4 個相關位元,可用於控制頁面權限。2 位元用於使用者和特權執行「永不執行」。其餘兩個則用於說明兩個存取層級的讀取和寫入網頁權限。唯執行對應項目已移除讀取和寫入權限,但允許使用者執行。

這份來自 ARMv8 參考手冊的表格顯示了使用僅有的 4 個可用位元時,可能的記憶體保護措施。EL0 是使用者空間的例外狀況層級。列 0 和 2 說明如何建立使用者空間的執行專用頁面。請參閱 ARMv8 參考手冊中的表格 D5-34 階段 1。

UXN PXN AP[2:1] 從較高例外層級存取 從 EL0 存取
0 1 00 R、W X
0 1 01 R、W R、W、X
0 1 10 R X
0 1 11 R R、X

很遺憾,PAN 的演算法會判斷是否應禁止使用者存取某個網頁,而這項判斷會檢查該網頁是否可供使用者閱讀。從 PAN 的角度來看,只有使用者可執行的頁面看起來就像是特權對應項目。這會讓核心在不適當的情況下存取使用者記憶體,進而繞過 PAN 的預期用途,並讓 PAN 和 XOM 不相容,產生pan-issue。這會導致日後使用 PAN 時,無法對付試圖利用觸及使用者記憶體的核心,但仍可用於偵測核心錯誤。

這個問題導致 Linux 和 Android 都停止支援 XOM。這項異動對 Android 來說特別明顯,因為 Android 在新增後就不再支援 Android 11,並將 Android 10 的所有 aarch64 二進位檔設為預設值。linux-revert他們打算重新啟用這項功能,因為解決問題的硬體會越來越普遍,但目前還沒有具體的時間表。

自此之後,ARM 便提出了採用「強化」PAN 或 ePAN 的解決方案,除了檢查 PAN 是否可供使用者閱讀,還可檢查是否可供使用者執行。很抱歉,這項功能的硬體可能在未來幾年內不會出現在任何 Fuchsia 指定裝置上。自此之後,Linux 在 ePAN 成為 linux-re-land 後,便重新加入了 XOM 的實作項目。裝置上是否支援 ePAN 不在我們的控制範圍內,且與 PAN 和 XOM 的不相容性不應阻斷核心的 PAN 實作方式。瞭解詳情

從圖 2 可知,沒有任何設定可從核心中移除讀取權限。唯一的例外狀況是 PAN,因為當核心嘗試觸碰使用者可讀取的頁面時,可能會導致例外狀況。因此,無法為核心建立僅執行對應,因為核心無法將頁面標示為在 EL1 執行,但無法讀取。因此,您只能為使用者空間程序建立僅執行對應項目。

指定 XOM 硬體

ELF 中的區段權限會指出程式碼需要哪些權限才能正確執行。換句話說,軟體在建構時,不需要知道所執行的硬體是否支援 XOM。相反地,如果不需要讀取程式碼頁面,則應無條件使用 XOM。系統允許的 elf-segment-perm 權限,取決於作業系統和載入器的執行方式。

虛擬記憶體權限

POSIX 指定 mmap 可允許讀取未明確設定 posix-mmap 的頁面。PROT_READ在 x86 上的 Linux 和 macOS,以及 M1 晶片上的 macOS 中,如果只使用 PROT_EXEC 從 mmap 要求頁面,並將頁面設為 PROT_READ | PROT_EXEC,則不會發生失敗。這些實作項目的系統呼叫具有「盡力」的功能,可滿足使用者的要求。另一方面,Fuchsia 系統呼叫一律會明確指出可執行和無法執行的操作。zx_vmar_* 系統呼叫不會在未經許可的情況下提升頁面權限,這與 POSIX 系統呼叫的標準不同。目前,如果沒有 ZX_VM_PERM_READ,要求頁面一律會失敗,因為硬體和作業系統不支援沒有讀取權限的對應頁面。如要順利轉換為支援僅執行區段的二進位檔,以及分配僅執行記憶體的使用者空間程式,就必須先檢查作業系統是否能夠在要求時對應僅執行頁面。

可讀的程式碼安全防護機制

許多攻擊都會透過讀取程式碼頁面,找出「小工具」或可執行的程式碼,以便取得相關資訊。位址空間配置隨機化 (ASLR) 是作業系統用來在程序的位址空間中隨機載入二進位區段的技術。Fuchsia 和許多其他作業系統都會使用這項功能,以防範需要知道程式碼或其他資料在記憶體中位置的攻擊。讓程式碼無法讀取,可進一步減少攻擊面。

程式碼重複使用攻擊 (例如「return-to-libc」rtl-attack) 會將函式的控制權傳回至已知位址。libc 是返回或跳轉的合理選擇,因為它包含攻擊者可用的豐富功能,且程序極有可能會連結至 libc。研究人員已證明,一般程式中的可用小工具是圖靈完備的,因此攻擊者可以執行任意程式碼。

在許多情況下,攻擊者的目標是取得殼層。由於函式在程式呼叫之間的位址不同,因此 ASLR 會讓這類攻擊更加困難。不過,ASLR 並非全面的緩解措施,因為攻擊者可以讀取程式碼頁面,找出他們無法透過檢視二進位程式中的位址而得知的函式位址。XOM 可防止以這種方式破壞 ASLR,攻擊者必須使用其他方式,才能找出特定程式碼頁面的位置資訊。

常見符號

‘rwx/r-x/–x’

這些代表 ELF 區段的權限,這些區段會以相應的權限對應至程序位址空間。這類符號通常用於描述檔案權限,以及 readelf 等工具的 ELF 區段。r、w 和 x 分別代表讀取、寫入和執行,而「-」則表示未授予權限。僅執行的區段會有「--x」權限。

R^X、W|X 等

如上所述,R、W 和 X 分別代表讀取、寫入和執行。「^」和「|」是類似 C 的運算子,用於執行或運算。R^X 會解讀為「read xor execute」。

「ax」

這是組譯器語法,用於將某個區段標示為已配置且可執行。目前連結器會將「ax」區段放入「r-x」區段。lld 中的 --execute-only 標記會將這些區段標示為「--x」。

設計

為了透過支援 XOM 提升使用者空間程式的安全性,我們必須更新工具鍊和載入器。Clang 驅動程式庫需要將「--execute-only」標記傳遞至連結器,確保「ax」區段會對應至「r-x」區段,否則會對應至「--x」區段。載入器也需要變更健全性檢查,確保所有要求的權限至少包含 read,因為這不再是事實。

由於 XOM 只能用於具備 ePAN 的硬體,因此我們需要妥善支援轉換作業。我們提供兩種選項:

  1. vmar_* 函式變更為最佳努力,就像許多 mmap 實作一樣
  2. 建立查詢核心的方法,如果核心支援僅執行對應,則在 XOM 無法使用時,讓載入器將「--x」區段的權限提升至「r-x」。
  3. 新增 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 旗標,供載入器搭配「--x」區段使用。

在所有情況下,權限都可能會在無聲音的情況下提升。第一個選項是最簡單的做法,除了移除健全性檢查之外,載入器不需要進行任何變更。第二個選項並沒有比第一個複雜太多,只是在決定要向作業系統要求哪些記憶體權限之前,會在載入器中新增簡單的檢查程序。第三個選項很實用,因為使用者程式碼不太容易出錯。

第一個選項最終會違反 Fuchsia 目前與使用者空間的嚴格合約,因為使用者空間一向明確指出系統呼叫可以和不可以執行的操作。第二和第三個選項在載入 ELF 檔案時,也會導致對記憶體權限的處理方式不夠明確。不過,這符合 ELF 規格。區段權限並不會 1:1 指定為區段分配的記憶體將具備哪些權限,而是指定記憶體至少必須具備哪些權限,才能讓程式正常運作。ELF 載入器有權將「--x」區段對應至「r-x」記憶體 elf-segment-perm

第一個選項是違反 Fuchsia 目前明確的 syscall 處理合約,這不是理想做法。選項 2 和 3 都很有價值,而本 RFC 中提出的實作方式將以這兩個選項為基礎。

實作

系統呼叫新增功能

我們會新增新的標記 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED,讓各種 zx_vmar_* 系統呼叫在 options 中使用權限標記,如果不支援 XOM,系統會隱含新增讀取權限。ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 在邏輯上只適用於 ZX_VM_PERM_EXEC,而非 ZX_VM_PERM_READ,不過接受此旗標的各種系統呼叫不會將其視為不變量。ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 搭配任何其他標記組合都很安全,在系統無法對應僅執行頁面的情況下,系統會將其視為 ZX_VM_PERM_READ

系統會為 zx_system_get_features 新增新的 kindZX_FEATURE_KIND_VM,並產生類似 ZX_FEATURE_KIND_CPU 的位元組合。另外,我們也將推出新功能 ZX_VM_FEATURE_CAN_MAP_XOM。由於 XOM 會在稍後才啟用,因此目前的實作方式會一律將此位元設為 false。由於「r-x」記憶體權限適用於「--x」區段,因此載入器不會使用這項權限,但這項權限對於使用者空間仍相當重要,因為使用者空間必須能夠查詢這項功能。

系統載入器 ABI 變更

目前和未來的載入器會確保即使目標不支援 XOM,也能將「--x」區段載入記憶體。載入器會在對僅執行區段進行對應時新增 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED

已發布的動態連結器 ABI 變更

同樣地,當使用 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 為「--x」區段分配記憶體時,Fuchsia 的 libc 中的動態連結器 (隨 SDK 提供) 也會在必要時提升權限。

編譯器工具鍊變更

在指定 aarch64-*-fuchsia 時,clang 驅動程式庫也會變更為一律將 --execute-only 傳遞至連結器。我們也需要一種方法來選擇不採用這種行為,最有可能的方式是為連結器新增「--no-execute-only」標記,讓程式可以輕鬆選擇不採用新的預設行為。

核心 XOM 實作

一旦硬體支援 ePAN,核心就能處理記憶體頁面只含 ZX_VM_PERM_EXECUTE 的要求。arm64 使用者複製版本實作項目可能需要更新,以確保與使用者記憶體存取權限的限制一致。user_copy 應更新為使用 ldtrsttr 指令。這可確保使用者無法欺騙核心,讓核心為他們讀取無法讀取的頁面。此外,核心會假設對應項目可在幾個位置讀取,因此需要視情況變更這些項目。這項工作會在稍後完成。

不必要的變更

zx_process_read_memory 不需要變更,而且在偵錯僅執行二進位檔時,偵錯工具應可正常運作。zx_process_read_memory 會忽略所讀取頁面的權限,只會檢查程序句柄是否有 ZX_RIGHT_READZX_RIGHT_WRITE

zx_vmar_protect 會繼續照常運作。最值得注意的是,這表示程序可在必要時使用讀取權限保護其程式碼頁面。

成效

預期不會對成效造成任何影響。

安全性

在核心中實作 XOM 之前,含有「--x」區段的二進位檔與使用「r-x」區段的二進位檔一樣安全。一旦硬體和作業系統都支援 XOM,選擇使用僅執行記憶體的程式就會變得更安全。請參閱「程式碼頁面的權限」、「XOM 和 PAN」和「可讀程式碼安全性」一節。

隱私權

除了「安全性」一節中提到的事項以外,不需考量其他因素。

測試

當我們在核心中強制支援 XOM 時,zx_system_get_features 將進行簡單的測試,以便我們在建構期間瞭解預期的系統呼叫回傳內容。

zx_system_get_features 回報作業系統無法建立僅執行頁面時,系統會測試 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED,確認其可讓頁面可讀。

同樣地,除了不會測試預期功能的模糊測試外,elfload 程式庫不會進行任何實際測試。相反地,其功能會由依賴它的其他元件進行測試。請在此新增測試,確保「--x」區段已正確對應。process_builder 程式庫確實有測試,這些測試可確保在無法使用 XOM 時,能夠正確要求可讀取和可執行的記憶體。

系統不會直接測試對目前動態連結器所做的變更。我們預計推出新的動態連結器,並進行全面測試,包括測試「--x」區段。

對 Clang 驅動程式庫所做的變更,會在 LLVM 上游進行測試。

我們也會設定測試設定,以便在測試機器人上啟用 XOM,即使該硬體沒有 ePAN 且我們不會啟用 XOM 也一樣。這有助於我們在讀取程式碼頁面的樹狀結構程式中,找出需要停用「僅執行」的程式。

說明文件

我們會記錄 zx_system_get_features 的變更,以及使用者空間為何想使用 ZX_VM_FEATURE_CAN_MAP_XOM 類型進行查詢的動機。同樣地,新ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED標記也會記錄在文件中。我們不會在本 RFC 以外的文件中記錄對各種載入器和 Clang 驅動程式庫預設值的變更。

缺點、替代方案、未知

目前和未來的樹狀結構外程式碼,究竟有多少依賴可執行程式碼的可讀性,目前尚無法得知。這可能是因為使用手寫彙整的文字中資料常數、從其他工具鍊編譯的程式碼,或程式內省。無論如何,需要可讀取程式碼頁面的程式仍可受惠,因為其共用程式庫依附元件 (包括 libc) 會標示為僅執行。如果將 clang 工具鍊預設為僅執行區段,會導致依賴可讀程式碼的程式中斷。在建構期間檢查程式是否依賴這項行為,並沒有簡單的方法。不過,一旦發現程式需要「r-x」區段,就可以輕鬆選擇停用預設的「--x」。

對於需要讀取部分程式碼 (而非全部) 的程式,目前的工具無法輕易支援這項需求。--execute-only linker 標記會從任何可執行區段中移除讀取權限,而且無法將單一區段標示為需要讀取的區段。如要採用這種行為,程式就必須完全停用「僅執行」模式。

風險

在硬體和核心支援 XOM 之前,Clang 驅動程式庫預設會使用 --execute-only,且從「--x」區段讀取的程式碼不會中斷。這會導致未變更的軟體出現潛在的向前相容性問題。樹狀結構軟體會進行測試,但樹狀結構外程式碼大多不會進行測試。

既有技術與參考資料

由於許多 POSIX 實作方式在處理 mmap 權限旗標時會產生模糊處理情形,因此不需要 zx_system_get_features(ZX_FEATURE_KIND_CAN_MAP_XOM, &feature) 的類似項目。

Darwin 支援新版 Apple 晶片的 XOM,但使用專屬硬體功能的實作方式更為穩健。他們的晶片具備硬體支援功能,可從核心和使用者記憶體中移除個別權限位元。在 macOS 中,此功能未針對使用者空間啟用。apple-xom