RFC-0159:只执行内存

RFC-0159:只执行内存
状态已接受
区域
  • 内核
  • 工具链
说明

支持映射只执行内存。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-03-29
审核日期(年-月-日)2022-05-10

摘要

本文档建议对内核 API 进行更改,以支持具有仅执行段的二进制文件,具体方法是在 zx_system_get_features 中添加新的功能检查,并更改 launchpadprocess_builder 加载器以及 Fuchsia 的树内 libc 中的动态链接器,以支持“--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。可写入代码对于实时 (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 如何将此 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

Privileged Access Never (PAN) 是 ARM 芯片上的一项安全功能,可防止从内核模式对用户页面进行正常内存访问。它有助于防范潜在的内核漏洞,因为内核无法通过正常的加载或存储指令触及用户内存。相反,操作系统需要关闭 PAN 或使用 ldtrsttr 指令来访问这些页面。PAN 目前未针对 Fuchsia 启用,但我们已计划在 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 的使用都无法有效防范试图利用触及用户内存的内核的攻击,但仍可用于检测内核 bug。

此问题导致 Linux 和 Android 均放弃了对 XOM 的支持。对于 Android 而言,这一点尤为明显,因为 Android 在 linux-revert 中添加了对它的支持并将其设为所有 aarch64 二进制文件的默认设置后,又在 Android 11 中无限期地放弃了对它的支持。他们计划在修复该问题的硬件变得更加普及时重新启用该功能,但目前尚无具体的时间表。

ARM 随后提出了一种采用“增强型”PAN 或 ePAN 的解决方案,该方案会更改 PAN,不仅检查页面是否可供用户读取,还会检查页面是否可供用户执行。遗憾的是,具有该功能的硬件可能在未来几年内不会出现在任何以 Fuchsia 为目标的设备上。自 ePAN linux-re-land 以来,Linux 已重新添加了其 XOM 实现。设备上对 ePAN 的支持不在我们的控制范围内,PAN 和 XOM 的不兼容不应阻碍内核对 PAN 的实现了解详情

从图 2 可以看出,没有任何配置可以从内核中剥离读取权限。唯一的例外是 PAN,当内核尝试触及用户可读的页面时,可能会导致异常。因此,无法为内核创建仅执行映射,因为内核无法在 EL1 标记可执行但不可读的页面。因此,只能为用户空间进程创建仅执行映射。

以 XOM 硬件为目标

ELF 中的段权限用于指明代码正常运行所需的权限。换句话说,软件在构建时无需知道其将运行的硬件是否支持 XOM。相反,如果不需要读取代码页,则应无条件使用 XOM。操作系统和加载器应在系统允许的最大程度上强制执行这些权限 elf-segment-perm

虚拟内存权限

POSIX 规定,mmap 可以允许读取未明确设置 PROT_READ 的页面 posix-mmap。在 x86 上的 Linux 和 macOS,以及在 M1 芯片上的 macOS 上,当仅使用 PROT_EXEC 从 mmap 请求页面时,不会失败,而是使页面 PROT_READ | PROT_EXEC。这些实现具有系统调用,但它们在满足用户请求方面的能力是“尽力而为”。另一方面,Fuchsia 系统调用始终明确说明它们可以和不可以实现哪些功能。zx_vmar_* 系统调用不会像标准允许的 POSIX 对应项那样,在不通知的情况下升级网页的权限。目前,请求不含 ZX_VM_PERM_READ 的页面始终会失败,因为硬件和操作系统不支持映射不含读取权限的页面。为了顺利过渡到支持具有只执行段的二进制文件和分配只执行内存的用户空间程序,需要一种方法来检查操作系统是否可以在请求只执行页面之前映射这些页面。

易读的代码安全

许多攻击都依赖于通过读取代码页来查找“gadget”(即感兴趣的可执行代码)来获取有关进程的信息。地址空间布局随机化 (ASLR) 是一种操作系统用来将二进制段加载到进程地址空间中半随机位置的技术。Fuchsia 和许多其他操作系统都使用它来阻止依赖于了解代码或其他数据在内存中的位置的攻击。使代码难以读取可进一步减小攻击面。

代码重用攻击(例如“返回到 libc”rtl 攻击)用于将函数的控制权返回到已知地址。libc 是返回或跳转到的理想选择,因为它包含对攻击者有用的丰富功能,并且进程极有可能与 libc 关联。事实证明,典型程序中的可用 gadget 是图灵完备的,这使得攻击者能够执行任意代码。

在许多情况下,攻击者的目标是获取 shell。ASLR 使此类攻击更加困难,因为在程序的不同调用之间,函数的地址是不同的。不过,ASLR 并非全面的缓解措施,因为攻击者可以读取代码页,找到他们原本无法通过查看二进制文件中的函数地址来获知的函数地址。XOM 使 ASLR 无法以这种方式被破解,攻击者需要使用其他方式来了解特定代码页的位置信息。

通用表示法

“rwx/r-x/--x”

这些表示 ELF 段的权限,这些段会以相应的权限映射到进程的地址空间中。这种表示法通常用于描述文件权限以及 ELF 段(通过 readelf 等工具)。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,因此我们需要妥善支持过渡。我们有两种选择:

  1. vmar_* 函数更改为尽力而为,就像许多 mmap 实现一样
  2. 创建一种方法来查询内核是否支持仅执行映射,并在 XOM 不可用时让加载器将“--x”段的权限提升为“r-x”。
  3. 添加了一个新的 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 标志,供加载器与“--x”段一起使用。

在所有情况下,都可能会出现权限的静默升级。第一种方案最简单,加载程序除了移除其健全性检查之外,无需进行任何更改。第二种方案的复杂性并没有显著增加,只是在加载程序中添加了一个简单的检查,然后再决定向操作系统请求哪些内存权限。第三种方法很有用,因为它可以减少用户代码中的错误。

第一个选项最终会打破 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 添加新的 kindZX_FEATURE_KIND_VM,从而生成类似于 ZX_FEATURE_KIND_CPU 的位集。此外,我们还将推出一项新功能 ZX_VM_FEATURE_CAN_MAP_XOM。当前实现将始终使此位保持为 false,因为 XOM 将在稍后启用。加载器不会使用此功能,因为“r-x”内存权限对“--x”段有效,但用户空间能够查询此功能仍然很重要。

系统加载器 ABI 变更

当前和未来的加载器将确保即使目标无法支持 XOM,也能将“--x”段加载到内存中。加载器在映射仅执行段时会添加 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED

已发布的动态链接器 ABI 变更

同样,随 SDK 提供的 Fuchsia libc 中的动态链接器也会在必要时提升权限,以便为具有 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 的“--x”段分配内存。

编译器工具链变更

当以 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 将进行简单的测试,因为我们可以在 build 时知道我们期望系统调用返回什么。

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 标志也会记录在文档中。对各种加载器和 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