mseal 簡介

作者:

Jeff Xu <jeffxu@chromium.org>

現代 CPU 支援記憶體許可權,例如 RW 和 NX 位。記憶體許可權特性提高了記憶體損壞漏洞的安全性,即攻擊者不能僅僅寫入任意記憶體並將程式碼指向它,記憶體必須標記為 X 位,否則會發生異常。

記憶體密封額外保護對映本身免受修改。這對於緩解將損壞的指標傳遞給記憶體管理系統的記憶體損壞問題非常有用。 例如,這樣的攻擊者原語可能會破壞控制流完整性保證,因為應該信任的只讀記憶體可能變為可寫,或者 .text 頁面可能被重新對映。 執行時載入器可以自動應用記憶體密封來密封 .text 和 .rodata 頁面,並且應用程式還可以在執行時密封安全關鍵資料。

XNU 核心中已存在類似的功能,帶有 VM_FLAGS_PERMANENT 標誌 [1],OpenBSD 中存在 mimmutable syscall [2]。

系統呼叫

mseal 系統呼叫簽名

int mseal(void *addr, size_t len, unsigned long flags)

addr/len:虛擬記憶體地址範圍。
addr/len 設定的地址範圍必須滿足
  • 起始地址必須位於已分配的 VMA 中。

  • 起始地址必須頁對齊。

  • 結束地址 (addr + len) 必須位於已分配的 VMA 中。

  • 起始地址和結束地址之間沒有間隙(未分配的記憶體)。

核心將隱式地將 len 進行頁對齊。

flags:保留供將來使用。

返回值:
  • 0:成功。

  • -EINVAL:
    • 輸入 flags 無效。

    • 起始地址 (addr) 未頁對齊。

    • 地址範圍 (addr + len) 溢位。

  • -ENOMEM:
    • 起始地址 (addr) 未分配。

    • 結束地址 (addr + len) 未分配。

    • 起始地址和結束地址之間存在間隙(未分配的記憶體)。

  • -EPERM:
    • 密封僅在 64 位 CPU 上受支援,不支援 32 位。

關於錯誤返回的說明:
  • 對於上述錯誤情況,使用者可以期望給定的記憶體範圍未被修改,即沒有部分更新。

  • 可能存在此處未列出的其他內部錯誤/情況,例如,合併/拆分 VMA 期間的錯誤,或者程序達到支援的 VMA 的最大數量。 在這些情況下,可能會對給定的記憶體範圍進行部分更新。 但是,這些情況應該很少見。

架構支援:

mseal 僅適用於 64 位 CPU,不適用於 32 位 CPU。

冪等性:

使用者可以多次呼叫 mseal。 在已密封的記憶體上呼叫 mseal 不會執行任何操作(不是錯誤)。

沒有 munseal

一旦對映被密封,它就不能被取消密封。 核心永遠不應該有 munseal,這與其他密封特性一致,例如檔案的 F_SEAL_SEAL。

阻止用於密封對映的 mm 系統呼叫

可能需要注意的是:**一旦對映被密封,它將保留在程序的記憶體中,直到程序終止**。

示例

*ptr = mmap(0, 4096, PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
rc = mseal(ptr, 4096, 0);
/* munmap will fail */
rc = munmap(ptr, 4096);
assert(rc < 0);
阻止 mm 系統呼叫
  • munmap

  • mmap

  • mremap

  • mprotect 和 pkey_mprotect

  • 一些破壞性的 madvise 行為:MADV_DONTNEED、MADV_FREE、MADV_DONTNEED_LOCKED、MADV_FREE、MADV_DONTFORK、MADV_WIPEONFORK

要阻止的第一組系統呼叫是 munmap、mremap、mmap。 它們可能會在地址空間中留下一個空的空間,從而允許用一組新的屬性替換為新的對映,或者可以用另一個對映覆蓋現有的對映。

mprotect 和 pkey_mprotect 被阻止,因為它們會更改對映的保護位 (RWX)。

某些破壞性的 madvise 行為,特別是 MADV_DONTNEED、MADV_FREE、MADV_DONTNEED_LOCKED 和 MADV_WIPEONFORK,當由缺乏寫入許可權的執行緒應用於匿名記憶體時,可能會引入風險。 因此,在這種情況下禁止這些操作。 上述行為有可能透過丟棄頁面來修改區域內容,從而有效地對匿名記憶體執行 memset(0) 操作。

核心將為阻止的系統呼叫返回 -EPERM。

當被阻止的系統呼叫由於密封而返回 -EPERM 時,記憶體區域可能會或可能不會被更改,這取決於被阻止的系統呼叫

  • munmap:munmap 是原子的。 如果給定範圍內的某個 VMA 被密封,則不會更新任何 VMA。

  • mprotect、pkey_mprotect、madvise:可能會發生部分更新,例如,當 mprotect 跨多個 VMA 時,mprotect 可能會在到達密封的 VMA 之前更新開始的 VMA 並返回 -EPERM。

  • mmap 和 mremap:未定義的行為。

用例

  • glibc:動態連結器在載入 ELF 可執行檔案期間,可以將密封應用於對映段。

  • Chrome 瀏覽器:保護一些安全敏感的資料結構。

  • 系統對映:系統對映由核心建立,包括 vdso、vvar、vvar_vclock、vectors (arm 相容模式)、sigpage (arm 相容模式)、uprobes。

    那些系統對映是隻讀或只執行的,記憶體密封可以保護它們免受永遠更改為可寫或以不同屬性 unmmap/remapped 的影響。 這對於緩解將損壞的指標傳遞給記憶體管理系統的記憶體損壞問題非常有用。

    如果某個架構支援 (CONFIG_ARCH_SUPPORTS_MSEAL_SYSTEM_MAPPINGS),CONFIG_MSEAL_SYSTEM_MAPPINGS 將密封該架構的所有系統對映。

    以下架構當前支援此功能:x86-64、arm64、loongarch 和 s390。

    警告:此功能會破壞依賴於重新定位或取消對映系統對映的程式。 在編寫本文時,已知的損壞軟體包括 CHECKPOINT_RESTORE、UML、gVisor、rr。 因此,此配置不能普遍啟用。

何時不使用 mseal

應用程式可以將密封應用於來自使用者空間的任何虛擬記憶體區域,但在應用密封之前,*徹底分析對映的生命週期*至關重要。 這是因為密封的對映在程序終止或呼叫 exec 系統呼叫之前 *不會被取消對映*。

例如
  • aio/shm aio/shm 可以代表使用者空間呼叫 mmap 和 munmap,例如 shm.c 中的 ksys_shmdt()。 這些對映的生命週期與程序的生命週期無關。 如果這些記憶體從使用者空間密封,則 munmap 將失敗,從而導致程序生命週期中 VMA 地址空間中的洩漏。

  • malloc 分配的 ptr(堆)不要在 malloc() 返回的記憶體 ptr 上使用 mseal()。 malloc() 由分配器實現,例如 glibc。 堆管理器可能會從 brk 或由 mmap 建立的對映中分配一個 ptr。 如果應用程式在 malloc() 返回的 ptr 上呼叫 mseal(),這會影響堆管理器管理對映的能力; 結果是不確定的。

    示例

    ptr = malloc(size);
    /* don't call mseal on ptr return from malloc. */
    mseal(ptr, size);
    /* free will success, allocator can't shrink heap lower than ptr */
    free(ptr);
    

mseal 不會阻止

簡而言之,mseal 會阻止某些 mm 系統呼叫修改某些 VMA 的屬性,例如保護位 (RWX)。 密封的對映並不意味著記憶體是不可變的。

正如 Jann Horn 在 [3] 中指出的那樣,仍然有一些方法可以寫入 RO 記憶體,這在某種程度上是故意的。 這些可以透過不同的安全措施來阻止。

這些情況是

  • 透過 /proc/self/mem 介面寫入只讀記憶體 (FOLL_FORCE)。

  • 透過 ptrace 寫入只讀記憶體(例如 PTRACE_POKETEXT)。

  • userfaultfd。

啟發此補丁的想法來自 Stephen Röttger 在 V8 CFI [4] 中的工作。 ChromeOS 中的 Chrome 瀏覽器將成為此 API 的第一個使用者。

參考