| RFC-0159:僅執行記憶體 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 支援對應僅供執行的記憶體。 |
| 問題 | |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2022-03-29 |
| 審查日期 (年-月-日) | 2022-05-10 |
摘要
本文提議變更核心 API,以支援具有僅執行區段的二進位檔,方法是在 zx_system_get_features 中新增功能檢查,並變更 launchpad 和 process_builder 載入器,以及 Fuchsia 樹狀結構內 libc 中的動態連結器,以支援「--x」區段。這項提案列出最終的計畫,在支援這類頁面的硬體上,為僅供執行的頁面提供核心支援。
載入可執行檔記憶體後,我們通常不需要讀取該記憶體。 預設啟用僅可執行的程式碼,可提升 Fuchsia 使用者空間程序的安全性,並進一步落實最低權限的工程最佳做法。
提振精神
ARMv7m 的 ARM MMU 新增了僅供執行的頁面支援功能,可對應記憶體頁面,使其僅供執行,無法讀取或寫入。雖然可寫入的程式碼頁面長期以來都被視為安全威脅,但允許程式碼保持可讀取狀態,會讓應用程式公開在不必要的風險中。具體來說,讀取程式碼頁面通常是攻擊鏈的第一步,因此防止讀取程式碼可阻礙攻擊者。請參閱「Readable Code Security」。此外,支援僅可執行的頁面與 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 導入虛擬記憶體的形式,提供重要的抽象化功能,將程式的記憶體檢視畫面與基礎實體資源分離。這項功能可讓 OS 實作者透過程序抽象化,在程式之間提供強大的隔離功能,進而促進更靈活、安全且可靠的程式設計模型。現今的 MMU 提供多項重要功能,例如分頁記憶體、快速位址轉譯和權限檢查。此外,使用者也能透過頁面權限,大幅控管記憶體區域的存取和使用方式,通常是控管記憶體頁面是否可讀取、寫入或執行。這項屬性對於程式安全、故障隔離和安全性至關重要,因為它會透過硬體強制執行的權限檢查,限制程式濫用系統資源的能力。
可寫入及執行的記憶體特別危險,因為這類記憶體可讓攻擊者透過常見的安全漏洞 (例如緩衝區溢位),輕鬆執行任意程式碼。因此,許多 OS 設定會明確禁止網頁同時可寫入和可執行 (W^X)。這項標準已存在超過十年,OpenBSD 在 2003 年的 OpenBSD 3.3 openbsd-wxorx 中新增了 W^X 支援功能。另請參閱 SELinux W^X 政策
selinux-wxorx。 可寫入的程式碼可用於即時 (JIT) 編譯等作業,在執行階段將可執行的指令寫入記憶體。W|X 頁面可能會遭到禁止,JIT 必須解決這個問題。簡單的方法是將程式碼寫入不可執行的頁面,然後將頁面保護措施 (即透過 mprotect 或 zx_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 如何將這段 C 程式碼編譯為組語,並提供不同的目標 clang-example。頂端資料列顯示 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
ARM 晶片的「永不具備特殊權限存取權」(PAN) 安全功能,可防止核心模式正常存取使用者頁面記憶體。這有助於防範潛在的核心安全漏洞,因為核心無法透過一般載入或儲存指令觸及使用者記憶體。作業系統必須關閉 PAN,或使用 ldtr 和 sttr 指令存取這些頁面。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 10 linux-revert 新增並預設為所有 aarch64 二進位檔後,Android 11 卻無限期停止支援,這點尤其明顯。他們計畫在解決問題的硬體普及後重新啟用這項功能,但目前尚未確定何時會重新加入。
ARM 隨後提出「強化」PAN 或 ePAN 解決方案,變更 PAN,不僅檢查網頁是否可供使用者閱讀,也檢查網頁是否可供使用者執行。很遺憾,搭載這項功能的硬體可能多年後才會出現在任何以 Fuchsia 為目標的裝置上。Linux 之後已在 ePAN linux-re-land 後重新加入 XOM 實作。裝置上的 ePAN 支援不在我們的掌控範圍內,且與 PAN 和 XOM 不相容,不應阻礙核心實作 PAN 瞭解詳情。
如圖 2 所示,沒有任何可能的設定可從核心中移除讀取權限。唯一的例外是 PAN,當核心嘗試觸及使用者可讀取的頁面時,可能會導致例外狀況。因此,無法為核心建立僅供執行的對應,因為核心無法在 EL1 將頁面標示為可執行,但不可讀取。因此,只能為使用者空間程序建立僅供執行的對應。
指定 XOM 硬體
ELF 中的區段權限會指出程式碼正確執行所需的權限。換句話說,軟體不需要在建構時瞭解所執行的硬體是否支援 XOM。如果不需要讀取程式碼頁面,則應無條件使用 XOM。OS 和載入器會盡可能強制執行這些權限,以符合系統允許的 elf-segment-perm。
虛擬記憶體權限
POSIX 規定 mmap 可允許讀取存取權,存取未明確設定 PROT_READ posix-mmap 的頁面。在 x86 上的 Linux 和 macOS,以及 M1 晶片上的 macOS,從 mmap 要求網頁時不會失敗,只會使用 PROT_EXEC,並改為建立 PROT_READ | PROT_EXEC 網頁。這些實作項目具有系統呼叫,可「盡力」配合使用者的要求。另一方面,Fuchsia 系統呼叫一律會明確指出可接受和不可接受的內容。與標準允許的 POSIX 對應項目不同,zx_vmar_* 系統呼叫不會無聲無息地提升網頁權限。目前,如果要求存取沒有 ZX_VM_PERM_READ 的頁面,一律會失敗,因為硬體和 OS 不支援對沒有讀取權限的頁面進行對應。如要順利過渡到支援僅含執行區段的二進位檔,以及分配僅含執行記憶體的使用者空間程式,就必須先檢查 OS 是否能對應僅含執行的頁面,再提出要求。
可讀的程式碼安全防護機制
許多攻擊事件都仰賴於透過讀取程式碼頁面來尋找「小工具」或感興趣的可執行程式碼,藉此取得程序相關資訊。位址空間配置隨機載入 (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 讀為「讀取互斥或執行」。
「ax」
這是組語語法,可將區段標示為已分配且可執行。
目前連結器會將「ax」區段放入「r-x」區段。lld 中的 --execute-only 標記會改為將這些區段標示為「--x」。
設計
為支援 XOM,提升使用者空間程式的安全性,我們的工具鍊和載入器都需要更新。clang 驅動程式庫必須將「--execute-only」標記傳遞至連結器,確保原本會對應至「r-x」區段的「ax」區段,改為對應至「--x」區段。載入器也必須變更健全性檢查,因為所有要求的權限不再包含至少讀取權限。
由於只能在具有 ePAN 的硬體上使用 XOM,因此我們需要順利支援這項轉換。我們提供兩種做法:
- 將
vmar_*函式變更為盡量提供,如同許多mmap實作項目 - 建立查詢核心的方式,確認核心是否支援僅限執行的對應,並讓載入器在 XOM 無法使用時,將「--x」區段的權限提升為「r-x」。
- 新增
ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED旗標,供載入器搭配「--x」區隔使用。
在所有情況下,權限都可能在使用者不知情的情況下遭到提升。第一個選項最簡單,載入器除了移除健全性檢查外,不需要進行任何變更。第二個選項不會複雜太多,只是會在載入器中新增簡單的檢查,再決定要向 OS 要求哪些記憶體權限。第三個選項有助於減少使用者程式碼中的錯誤。
第一個選項最終會破壞 Fuchsia 目前與使用者空間的嚴格合約,也就是一律明確指出系統呼叫可接受和不可接受的內容。第二和第三個選項也會在載入 ELF 檔案時,導致記憶體權限的處理方式模稜兩可。不過,這符合 ELF 規格。區段權限不會指定分配給區段的記憶體將具備哪些權限,而是指定記憶體至少必須具備哪些權限,程式才能正常運作。ELF 載入器有權將「--x」區段對應至「r-x」記憶體 elf-segment-perm。
第一個選項是打破 Fuchsia 目前的明確系統呼叫處理合約,但這並非理想做法。選項 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 新增 kind 值 ZX_FEATURE_KIND_VM,產生類似 ZX_FEATURE_KIND_CPU 的位元集。此外,我們也將推出新功能
ZX_VM_FEATURE_CAN_MAP_XOM。目前的實作方式一律會將這個位元設為 false,因為 XOM 要到稍後才會啟用。載入器不會使用這項功能,因為「r-x」記憶體權限適用於「--x」區段,但使用者空間仍需查詢這項功能。
系統載入器 ABI 變更
目前和未來的載入器都會確保「--x」區隔可載入記憶體,即使目標無法支援 XOM 也是如此。載入器會在對應僅供執行的區隔時新增 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED。
已發布的動態連結器 ABI 變更
同樣地,SDK 隨附的 Fuchsia libc 中的動態連結器,也會在為含有 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 的「--x」區段分配記憶體時,視需要提高權限。
編譯器工具鍊變更
此外,clang 驅動程式庫也會變更,在指定 aarch64-*-fuchsia 時,一律將 --execute-only 傳遞至連結器。我們也需要提供停用這項行為的方法,最有可能是在連結器中新增 '--no-execute-only' 標記,讓程式輕鬆停用新的預設行為。
核心 XOM 實作
支援 ePAN 的硬體到貨後,核心即可處理記憶體頁面的要求,只保留 ZX_VM_PERM_EXECUTE。arm64 使用者副本實作可能需要更新,確保與使用者記憶體存取限制一致。請更新 user_copy,使用 ldtr 和 sttr 指令。這樣一來,使用者就無法欺騙核心,讓核心為他們讀取無法讀取的頁面。此外,核心會假設對應關係可在幾個位置讀取,因此必須在適當位置進行變更。這項工作會在稍後完成。
不必要的變更
zx_process_read_memory 不必變更,偵錯工具在偵錯僅供執行的二進位檔時,應可正常運作。zx_process_read_memory 會忽略所讀取頁面的權限,只檢查程序控制代碼是否具有 ZX_RIGHT_READ 和 ZX_RIGHT_WRITE。
zx_vmar_protect 仍會照常運作。最值得注意的是,這表示程序可以在必要時,透過讀取權限保護程式碼頁面。
效能
預計不會影響成效。
安全性
在核心中實作 XOM 之前,使用「--x」區段的二進位檔,與使用「r-x」區段的對等二進位檔一樣安全。一旦硬體和 OS 都支援 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 中進行測試。
即使該硬體沒有 ePAN,我們也會設定測試設定,在測試機器人上啟用 XOM,否則我們不會啟用 XOM。這有助於我們在樹狀結構程式中,找出讀取程式碼頁面且需要停用僅執行模式的程式。
說明文件
我們會記錄 zx_system_get_features 的變更,以及使用者空間想以 ZX_VM_FEATURE_CAN_MAP_XOM 類型查詢的原因。同樣地,新的 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 旗標也會記錄在文件中。各種載入器和 clang 驅動程式庫預設值的變更,不會記錄在這份 RFC 以外的文件中。
缺點、替代方案、不明
目前和未來的樹外程式碼有多少依賴可讀取的執行檔程式碼,目前不得而知。這可能是因為在手寫組語的文字中使用資料常數、從其他工具鍊編譯的程式碼,或是程式內省。無論如何,需要可讀取程式碼頁面的程式仍會受益,因為包括 libc 在內的共用程式庫依附元件都會標示為僅供執行。將 clang 工具鍊變更為預設僅執行區隔,會導致依賴可讀取程式碼的程式中斷。在建構期間,無法輕易檢查程式是否依賴這項行為。不過,一旦確認程式需要「r-x」區隔,就能輕鬆選擇停用預設的「--x」。
如果程式需要讀取部分程式碼,但不是全部,目前的工具無法輕鬆支援這項需求。--execute-only linker 標記會從任何可執行區段中移除讀取權限,且無法將單一區段標示為需要讀取。如要採用這種做法,程式必須完全停用僅執行模式。
風險
clang 驅動程式庫預設可能會使用 --execute-only,而從「--x」區段讀取的程式碼在 XOM 的硬體和核心支援到位前,不會中斷。這可能會導致未變更的軟體出現潛在的向前相容性問題。樹狀結構內軟體會有測試,但樹狀結構外的程式碼很可能不會有測試。
先前技術和參考資料
由於許多 POSIX 實作項目對 mmap 權限旗標的處理方式模稜兩可,因此不需要 zx_system_get_features(ZX_FEATURE_KIND_CAN_MAP_XOM, &feature) 的類似項目。
Darwin 支援較新的 Apple 晶片上的 XOM,但使用專屬硬體功能時,實作方式會更穩健。這些晶片提供硬體支援,可從核心和使用者記憶體中剝除個別權限位元。macOS 的使用者空間未啟用此功能。apple-xom