AArch64 Linux 的受保护控制栈支持¶
本文档简要概述了 Linux 为支持使用 ARM 受保护控制栈(GCS)功能而向用户空间提供的接口。
这仅是关于最重要的功能和问题的概述,并非详尽无遗。
1. 概述¶
GCS 是一项架构特性,旨在提供对面向返回的编程(ROP)攻击的更高保护,并简化需要收集栈回溯(如性能分析)的功能的实现。
启用 GCS 后,处理单元(PE)会维护一个单独的受保护控制栈,该栈只能通过特定的 GCS 操作进行写入。这仅存储调用栈;当执行过程调用指令时,当前的 PC 会被压入 GCS,而在执行 RET 时,链接寄存器(LR)中的地址将与 GCS 顶部的地址进行验证。
处于活动状态时,当前的 GCS 指针存储在系统寄存器 GCSPR_EL0 中。这可由用户空间读取,但只能通过特定的 GCS 指令进行更新。
该架构提供了用于在受保护控制栈之间进行切换的指令,并附带检查以确保新栈是切换的有效目标。
GCS 的功能类似于 x86 阴影栈(Shadow Stack)特性提供的功能。由于共享用户空间接口,ABI 中使用的是 shadow stack 而不是 GCS。
对 GCS 的支持通过辅助向量(aux vector)`AT_HWCAP` 条目中的 `HWCAP_GCS` 报告给用户空间。
GCS 是按线程启用的。虽然支持在运行时禁用 GCS,但这应当非常谨慎地进行。
GCS 内存访问错误作为普通内存访问错误进行报告。
GCS 特定的错误(以 EC 0x2d 报告的错误)将作为 `SIGSEGV` 报告,其 `si_code` 为 `SEGV_CPERR`(控制保护错误)。
GCS 仅在 AArch64 上受支持。
在支持 GCS 的系统上,无论线程的 GCS 配置如何,EL0 总是可读取 GCSPR_EL0。
该架构支持启用 GCS 但不验证 LR 中的返回值是否与 GCS 中的返回值匹配(LR 将被忽略)。Linux 不支持此功能。
2. 启用和禁用受保护控制栈¶
线程的 GCS 可以通过 `PR_SET_SHADOW_STACK_STATUS`
prctl()来启用和禁用,它接受一个单独的标志参数,指定应使用哪些 GCS 功能。设置 `PR_SHADOW_STACK_ENABLE` 标志时,会为线程分配一个受保护控制栈并启用 GCS,从而启用由 `GCSCRE0_EL1.{nTR, RVCHKEN, PCRSEL}` 控制的功能。
设置 `PR_SHADOW_STACK_PUSH` 标志时,会启用由 `GCSCRE0_EL1.PUSHMEn` 控制的功能,允许显式压入 GCS。
设置 `PR_SHADOW_STACK_WRITE` 标志时,会启用由 `GCSCRE0_EL1.STREn` 控制的功能,允许对受保护控制栈进行显式写入。
任何未知的标志都会导致 `PR_SET_SHADOW_STACK_STATUS` 返回 `-EINVAL`。
`PR_LOCK_SHADOW_STACK_STATUS` 传递的是一个特征位掩码,其值与用于 `PR_SET_SHADOW_STACK_STATUS` 的值相同。对指定 GCS 模式位的状态的任何未来更改都将被拒绝。
`PR_LOCK_SHADOW_STACK_STATUS` 允许锁定任何位,这使用户空间能够防止对任何未来功能的更改。
不支持进程移除为其设置的锁。
`PR_SET_SHADOW_STACK_STATUS` 和 `PR_LOCK_SHADOW_STACK_STATUS` 仅影响调用它们的线程,任何其他运行中的线程都不会受到影响。
新线程继承创建它们的线程的 GCS 配置。
在
exec()时 GCS 会被禁用。可以使用 `PR_GET_SHADOW_STACK_STATUS`
prctl()读取线程当前的 GCS 配置,这会返回传递给 `PR_SET_SHADOW_STACK_STATUS` 的相同标志。如果线程在先前启用后又禁用了 GCS,则该栈将在线程的整个生命周期内保持分配状态。目前,任何重新为该线程启用 GCS 的尝试都将被拒绝,这在未来可能会重新评估。
应当注意,由于启用 GCS 会导致 GCS 立即生效,因此通常无法从调用启用 GCS 的
prctl()的函数中返回。预计通常的用法是在程序执行的极早期阶段启用 GCS。
3. 受保护控制栈的分配¶
当为线程启用 GCS 时,将为其分配一个新的受保护控制栈,其大小为标准栈大小的一半或 2 GB,取两者中的较小者。
当由已启用 GCS 的线程创建一个新线程时,将为新线程分配一个新的受保护控制栈,大小为标准栈的一半。
当通过启用 GCS 或在线程创建期间分配栈时,栈顶部的 8 个字节将被初始化为 0,并且 `GCSPR_EL0` 将被设置为指向该 0 值的地址,这可用于检测栈顶。
可以使用
map_shadow_stack()系统调用分配额外的受保护控制栈。使用
map_shadow_stack()分配的栈可以可选地在栈顶放置栈底标记和上限标记(cap)。如果指定了 `SHADOW_STACK_SET_TOKEN` 标志,则会在栈上放置一个 cap;如果未指定 `SHADOW_STACK_SET_MARKER`,则 cap 将是栈的顶部 8 个字节;如果指定了,则 cap 将是接下来的 8 个字节。虽然仅单独指定 `SHADOW_STACK_SET_MARKER` 是有效的,但由于标记的所有位都是 0,因此它没有可观测的效果。使用
map_shadow_stack()分配的栈,其大小必须是大于 8 字节的 8 的倍数,并且必须按 8 字节对齐。可以向
map_shadow_stack()指定一个地址,如果提供了该地址,则它必须按页边界对齐。当释放线程时,最初为该线程分配的受保护控制栈将被释放。请仔细注意,如果栈已被切换,这可能不是该线程当前正在使用的栈。
4. 信号处理¶
在信号传递时,一个新的信号帧记录 `gcs_context` 会对被中断上下文当前的 GCS 模式和指针进行编码。在支持 GCS 的系统上,这始终会存在。
该记录包含一个标志字段,用于报告被中断上下文当前的 GCS 配置,就像 `PR_GET_SHADOW_STACK_STATUS` 所做的那样。
信号处理程序在运行时的 GCS 配置与被中断上下文相同。
当为被中断的线程启用 GCS 时,一个信号处理专用的 GCS cap 令牌将被写入 GCS,这是一个架构级别的 GCS cap,其令牌类型(第 0..11 位)全清零。信号帧中报告的 `GCSPR_EL0` 将指向此 cap 令牌。
信号处理程序将使用与被中断上下文相同的 GCS。
在进入信号时如果启用了 GCS,则带有信号返回处理程序地址的帧将被压入 GCS,从而允许通过 RET 从信号处理程序正常返回。这不会在信号帧的 `gcs_context` 中报告。
5. 信号返回¶
从信号处理函数返回时:
如果信号帧中存在 `gcs_context` 记录,则在进行进一步验证之前,GCS 标志和 `GCSPR_EL0` 将从该上下文中恢复。
如果信号帧中没有 `gcs_context` 记录,则 GCS 配置将保持不变。
如果从信号处理程序返回时启用了 GCS,则 `GCSPR_EL0` 必须指向一个有效的 GCS 信号 cap 记录,该记录将在信号返回之前从 GCS 中弹出。
如果从信号返回时 GCS 配置已被锁定,则任何更改 GCS 配置的尝试都将被视为错误。即使在进入信号之前未启用 GCS,也是如此。
可以通过信号返回禁用 GCS,但任何通过信号返回启用 GCS 的尝试都将被拒绝。
6. ptrace 扩展¶
定义了一个新的寄存器集(regset)`NT_ARM_GCS`,用于 `PTRACE_GETREGSET` 和 `PTRACE_SETREGSET`。
GCS 模式(包括启用和禁用)可以通过 ptrace 进行配置。如果通过 ptrace 启用 GCS,则不会为该线程分配新的 GCS。
通过 ptrace 进行配置会忽略 GCS 模式位的锁定。
7. ELF 核心转储(coredump)扩展¶
`NT_ARM_GCS` notes 将被添加到被转储进程的每个线程的每个核心转储中。其内容将等效于在生成核心转储时为每个线程执行相应类型的 `PTRACE_GETREGSET` 所读取的数据。
8. /proc 扩展¶
受保护控制栈页面在其 `/proc/<pid>/smaps` 的 `VmFlags` 中将包含“ss”。