RFC-0159:只执行内存

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

支持映射只执行内存。

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

摘要

本文档建议对内核 API 进行更改,以支持具有仅执行段的二进制文件,具体方法是在 zx_system_get_features 中添加新的功能检查,并更改 launchpad 和 process_builder 加载器以及 Fuchsia 的树内 libc 中的动态链接器,以支持“--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 需要解决此问题。一种简单的方法是将代码写入不可执行的页面,然后通过 mprotect 或 zx_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 如何针对不同的目标 clang-example 将此 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,或使用 ldtr 和 sttr 指令来访问这些页面。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 10 linux-revert 中添加了该功能并将其设为所有 aarch64 二进制文件的默认功能,但 Android 11 中无限期地取消了对该功能的支持。他们计划在解决该问题的硬件变得更加普及后重新启用该功能,但目前尚无重新添加该功能的具体时间表。

此后,ARM 提出了“增强型”PAN (ePAN) 解决方案,该方案不仅会检查页面是否可由用户读取,还会检查页面是否不可由用户执行。 遗憾的是,具有该功能的硬件可能多年都不会出现在任何 Fuchsia 目标设备上。在 ePAN linux-re-land 之后,Linux 重新添加了其 XOM 实现。我们无法控制设备对 ePAN 的支持,并且与 PAN 和 XOM 的不兼容不应妨碍内核实现 PAN。了解详情。

从图 2 中可以看出,没有任何可能的配置可以从内核中剥离读取权限。唯一的例外情况是 PAN,当内核尝试访问用户可读页面时,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 的页面始终会失败,因为硬件和操作系统不支持映射不带读取权限的页面。为了顺利过渡到支持具有仅执行段的二进制文件和分配仅执行内存的用户空间程序,需要一种方法来检查操作系统是否可以映射仅执行页面,然后再请求这些页面。

Readable Code Security

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

代码重用攻击(例如“return-to-libc”rtl-attack)用于将函数的控制权返回到已知地址。libc 是返回或跳转到的逻辑选择,因为它包含对攻击者有用的丰富功能,并且该进程极有可能链接到 libc。事实证明,典型程序中的可用小工具是图灵完备的,这使攻击者能够执行任意代码。

在许多情况下,攻击者的目标是获取 shell。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 读作“读异或执行”。

"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 添加新的 kind 值 ZX_FEATURE_KIND_VM,这将产生类似于 ZX_FEATURE_KIND_CPU 的位集。此外,我们还将推出一项新功能:ZX_VM_FEATURE_CAN_MAP_XOM。当前的实现将始终使此位保持为 false,因为 XOM 稍后才会启用。加载程序不会使用此功能,因为“r-x”内存权限对“--x”段有效,但对于用户空间来说,能够查询此功能仍然很重要。

系统加载器 ABI 变更

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

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

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

编译器工具链变更

clang 驱动程序也将更改为在定位到 aarch64-*-fuchsia 时始终将 --execute-only 传递给链接器。我们还需要一种方法来停用此行为,最可能的方法是向链接器添加新的“--no-execute-only”标志,以便程序可以轻松停用新的默认行为。

内核 XOM 实现

一旦支持 ePAN 的硬件推出,内核就可以处理仅具有 ZX_VM_PERM_EXECUTE 的内存页请求。arm64 用户复制实现可能需要更新,以确保其与用户内存访问受限的方式保持一致。应更新 user_copy 以使用 ldtr 和 sttr 指令。这样可以确保用户无法诱骗内核为其读取不可读的页面。此外,内核会假设映射在几个位置可读,这些位置需要根据需要进行更改。这项工作稍后会完成。

不必要的更改

zx_process_read_memory 无需更改,调试程序在调试仅执行二进制文件时应能正常工作。zx_process_read_memory 会忽略其读取的页面的权限,仅检查进程句柄是否具有 ZX_RIGHT_READ 和 ZX_RIGHT_WRITE。

zx_vmar_protect 将继续按当前方式运行。最值得注意的是,这意味着进程可以在必要时使用读取权限保护其代码页。

性能

预计不会对效果产生任何影响。

安全

在内核中实现 XOM 之前,具有“--x”段的二进制文件与使用“r-x”段的等效二进制文件一样安全。一旦硬件和操作系统都支持 XOM,选择使用只执行内存的程序将变得更加安全。请参阅代码页的权限、XOM 和 PAN 以及可读代码安全性部分。

隐私权

除了安全性中提到的注意事项外,没有其他注意事项。

测试

当我们在内核中强制支持 XOM 时,zx_system_get_features 将进行简单测试,在构建时我们就可以知道系统调用应该返回什么。

当 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