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 的第一个用户。

参考