RFC-0159:只执行内存

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

支持映射只执行内存。

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

总结

本文档提议对内核 API 进行更改,以支持具有只执行段的二进制文件,具体方法是在 zx_system_get_features 中添加一项新功能检查,并更改 Fuchsia 树内 libc 中的 launchpadprocess_builder 加载器以及动态链接器,以支持“--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 通过了 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 如何在给定不同目标 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,或使用 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 X
0 1 11 R、X

遗憾的是,用于决定某个页面是否不应具有访问权限的 PAN 算法会检查该页面是否可供用户读取。从 PAN 的角度来看,用户只执行页面看起来像是特权映射。这样一来,内核就可以访问用户内存,而之前不应访问用户内存,从而绕过 PAN 的预期用途并使 PAN 和 XOM 不兼容。“平移问题”这样一来,将来使用 PAN 对那些试图利用触及用户内存的内核的攻击都毫无用处,不过对于检测内核 bug,它仍然有用。

此问题导致 Linux 和 Android 不再支持 XOM。对于 Android 而言,这一点尤其明显。在 Android 11 中添加该库后,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 可以允许对 PROT_READ 未明确设置 posix-mmap 的页面进行读取访问。如果仅使用 PROT_EXEC 从 mmap 请求页面,而是将页面设为 PROT_READ | PROT_EXEC,则 x86 上的 Linux 和 macOS 以及 M1 芯片上的 macOS 不会失败。这些实现具有“尽最大努力”的系统调用,以遵循用户的请求。另一方面,Fuchsia 系统调用在可以和不能支持的方面始终是显式的。zx_vmar_* 系统调用不会像标准允许的 POSIX 对应项一样,静默地升级页面的权限。目前,在没有 ZX_VM_PERM_READ 的情况下请求页面总是会失败,因为硬件和操作系统不支持在没有读取权限的情况下映射页面。如需顺利过渡到支持具有只执行区段和用户空间程序(分配只执行内存)的二进制文件,则需要在请求之前检查操作系统是否可以映射只执行页面。

可读代码安全性

许多攻击都依赖于通过读取代码页来查找“小工具”或相关可执行代码,从而获取该过程的相关信息。地址空间布局随机化 (ASLR) 是操作系统用于在进程地址空间中的半随机位置加载二进制段的技术。Fuchsia 和许多其他操作系统使用它来阻止攻击,这些攻击依靠了解代码或其他数据在内存中的位置。将代码设为不可读可进一步减少受攻击面。

代码重用攻击(例如“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 分别指读取、写入和执行。“^”和“|”是 xor 和 or 的类似 C 运算符。R^X 被读取为“读取 xor 执行”。

“ax”

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

设计

为了通过支持 XOM 来提高用户空间程序的安全性,需要更新工具链和加载器。Clang 驱动程序需要将“--execute-only”标志传递给链接器,以确保本来会映射到“r-x”段的“ax”部分会映射到“--x”段。这些加载器还需要更改健全性检查,以确保所有请求的权限都至少包含 read,因为此状态将不再为 true。

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

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

在所有情况下,都可能会静默升级权限。第一个选项是最简单的选项,除了移除其健全性检查之外,加载器无需进行任何更改。第二个选项并不复杂,只是在确定从操作系统请求哪些内存权限之前,在加载器中添加一项简单的检查。第三个选项非常有用,因为它不容易出现用户代码错误。

第一个选项最终会破坏 Fuchsia 当前与用户空间的严格协定,即始终明确说明系统调用可以支持和不能支持的功能。第二个和第三个选项最终会导致在加载 ELF 文件时对内存权限的处理方式不明确。不过,这符合 ELF 规范。分段权限并未以 1:1 的形式指定为分段分配的内存将具有哪些权限,而是指定内存必须至少具备哪些权限才能使程序正常运行。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 更改

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

随附的动态链接器 ABI 更改

同样,SDK 随附的 Fuchsia libc 中的动态链接器也会在通过 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED 为“--x”段分配内存时在必要时升级权限。

编译器工具链变更

此外,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 将进行一些无关紧要的测试,这样我们就可以在构建时知道系统调用会返回什么。

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