RFC-0159:只执行内存

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

支持映射只执行内存。

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

摘要

本文档提出了对内核 API 的更改,以支持包含仅限执行段的二进制文件,具体方法是在 zx_system_get_features 中添加新的功能检查,并更改 Fuchsia 树内 libc 中的 launchpadprocess_builder 加载器以及动态链接器,以支持“--x”段。该文档制定了一项计划,以便最终在支持该功能的硬件上映射仅限执行页面,并提供内核支持。

通常,我们不需要在可执行内存加载后读取该内存。默认启用仅限执行代码可提高 Fuchsia 用户空间进程的安全性,并进一步遵循“最少权限”的工程最佳实践。

设计初衷

ARMv7m 中的 ARM MMU 添加了对仅限执行页面的支持,并允许映射内存页面,使其仅可执行,而不可读取或写入。虽然可写代码页长期以来一直被视为安全威胁,但事实证明,允许代码保持可读性会使应用面临不必要的风险。具体而言,读取代码页通常是攻击链中的第一个步骤,而阻止读取代码会妨碍攻击者。请参阅可读代码安全。此外,支持仅限执行的页面非常适合 Fuchsia 的权限模型,并且更符合最小权限原则:通常不需要读取代码,只需执行即可。

利益相关方

教员

  • cpu@google.com

Reviewers:

  • 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-example 的情况下,Clang 如何将此 C 代码编译为汇编语言。顶行显示 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

特许访问永不 (PAN) 是 ARM 芯片上的一项安全功能,可防止从内核模式对用户页面进行正常内存访问。它有助于防范潜在的内核漏洞,因为内核无法使用正常的加载或存储指令触及用户内存。相反,操作系统需要关闭 PAN 或使用 ldtrsttr 指令来访问这些页面。目前,Fuchsia 尚不支持 PAN,但我们已经计划在 zircon pan-fxb 中支持 PAN。

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 在添加此功能后,在 Android 11 中永久性地弃用了此功能,并在 Android 10 linux-revert 中将其设为所有 aarch64 二进制文件的默认值。他们计划在解决此问题的硬件变得更加普及后重新启用此功能,但尚未确定具体重新添加的时间。

自此,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 可以允许对未明确设置 posix-mmapPROT_READ 的页面进行读取访问。在 x86 上的 Linux 和 macOS 以及 M1 芯片上的 macOS 中,仅使用 PROT_EXEC 从 mmap 请求页面时不会失败,而是会将页面设为 PROT_READ | PROT_EXEC。这些实现具有系统调用,这些系统调用在执行用户请求时会尽最大努力。另一方面,Fuchsia 系统调用始终明确说明它们可以和不可以执行哪些操作。zx_vmar_* 系统调用不会像 POSIX 系统调用那样根据标准静默提升页面权限。目前,如果不使用 ZX_VM_PERM_READ 请求页面,请求始终会失败,因为硬件和操作系统不支持映射没有读取权限的页面。为了顺利过渡到支持包含只执行段的二进制文件和分配只执行内存的用户空间程序,需要一种方法来检查操作系统是否可以在请求只执行页面之前映射这些页面。

可读代码安全

许多攻击都依赖于通过读取代码页来了解进程的相关信息,以便找到“小工具”或感兴趣的可执行代码。地址空间布局随机化 (ASLR) 是一种技术,操作系统使用该技术在进程地址空间中的半随机位置加载二进制段。Fuchsia 和许多其他操作系统都使用它来防范依赖于知道代码或其他数据在内存中的位置的攻击。使代码不可读可进一步缩小攻击面。

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

在许多情况下,攻击者的目标是获取 shell。ASLR 会使这类攻击更难以成功,因为函数的地址在程序调用之间会有所不同。不过,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 读作“read xor execute”。

“ax”

这是汇编器语法,用于将某个部分标记为已分配且可执行。目前,链接器会将“ax”段放入“r-x”分段。lld 中的 --execute-only 标志将改为将这些分段标记为“--x”。

设计

为了通过支持 XOM 来提高用户空间程序的安全性,我们需要更新工具链和加载器。clang 驱动程序需要将“--execute-only”标志传递给链接器,以确保原本会映射到“r-x”段的“ax”段改为映射到“--x”段。加载器还需要更改所有请求的权限至少包含读取权限的完整性检查,因为这将不再成立。

由于 XOM 只能在具有 ePAN 的硬件上使用,因此我们需要妥善支持过渡。我们有两种选择:

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

在任何情况下,系统都可能会静默提升权限。第一种方法是最简单的,除了移除其完整性检查之外,加载器无需进行任何更改。第二种方法并不会显著增加复杂性,只需在决定从操作系统请求哪些内存权限之前,在加载器中添加一个简单的检查即可。第三种方法很有用,因为它在用户代码中不易出错。

第 1 个选项最终会违反 Fuchsia 与用户空间之间的当前严格协定,即始终明确说明系统调用可以和不可以执行哪些操作。第 2 个和第 3 个选项在加载 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。由于 XOM 将在稍后才会启用,因此当前实现将始终将此位保持为 false。加载器不会使用此功能,因为“r-x”内存权限对“--x”段有效,但对于用户空间能够查询此功能而言,此功能仍然很重要。

系统加载器 ABI 变更

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

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

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

编译器工具链变更

clang 驱动程序也将更改为在定位到 aarch64-*-fuchsia 时始终将 --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 将进行简单的测试,以便我们在构建时知道系统预期 syscall 会返回什么。

系统会测试 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED,以确保当 zx_system_get_features 报告操作系统无法创建仅限执行的页面时,它会使页面可读。

同样,除了不测试预期功能的模糊测试之外,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,并且在硬件和内核支持 XOM 之前,从“--x”段读取的代码不会中断。这可能会导致未更改的软件出现向前兼容性问题。系统会对树内软件进行测试,但对树外代码进行测试的可能性不大。

在先技术和参考文档

由于许多 POSIX 实现对 mmap 权限标志的处理不明确,因此它们不需要 zx_system_get_features(ZX_FEATURE_KIND_CAN_MAP_XOM, &feature) 的类似项。

Darwin 支持在较新的 Apple 芯片上使用 XOM,但使用专有硬件功能实现 XOM 会更为稳健。他们的芯片具有硬件支持,可从内核和用户内存中剥离各个权限位。在 macOS 中,未为用户空间启用此功能。apple-xom