mseal 简介¶
- 作者:
Jeff Xu <jeffxu@chromium.org>
现代 CPU 支持内存权限,例如 RW 和 NX 位。内存权限功能提高了内存损坏错误的安全性,即攻击者不能只是写入任意内存并将代码指向它,内存必须标记为 X 位,否则会发生异常。
内存密封额外地保护映射本身免受修改。这对于缓解内存损坏问题很有用,在这种问题中,损坏的指针被传递到内存管理系统。例如,这种攻击者原语可能会破坏控制流完整性保证,因为本应受信任的只读内存可能变为可写,或者 .text 页面可能会被重新映射。运行时加载器可以自动应用内存密封来密封 .text 和 .rodata 页面,应用程序还可以在运行时密封安全关键数据。
XNU 内核中已经存在类似的功能,具有 VM_FLAGS_PERMANENT 标志 [1],OpenBSD 中具有 mimmutable 系统调用 [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 位 CPU。
- 关于错误返回的说明:
对于上述错误情况,用户可以预期给定的内存范围未被修改,即没有部分更新。
可能还存在此处未列出的其他内部错误/情况,例如合并/拆分 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 浏览器:保护一些安全敏感的数据结构。
何时不使用 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 的第一个用户。