AArch64 Linux 的受保护控制栈支持¶
本文档简要概述了 Linux 为支持使用 ARM 受保护控制栈 (GCS) 功能而向用户空间提供的接口。
这只是最重要特性和问题的概述,并非详尽无遗。
1. 概述¶
GCS 是一种架构特性,旨在提供更强大的保护以防止返回导向编程 (ROP) 攻击,并简化需要收集堆栈跟踪(如性能分析)的功能的实现。
启用 GCS 后,PE 维护一个单独的受保护控制栈,该栈只能通过特定的 GCS 操作进行写入。这只存储调用堆栈,当执行过程调用指令时,当前 PC 被推入 GCS,而在 RET 上,LR 中的地址会根据 GCS 顶部的地址进行验证。
活动时,当前 GCS 指针存储在系统寄存器 GCSPR_EL0 中。用户空间可以读取它,但只能通过特定的 GCS 指令更新它。
该架构提供了在受保护控制栈之间切换的指令,并带有检查以确保新栈是切换的有效目标。
GCS 的功能类似于 x86 影子栈功能提供的功能,由于共享用户空间接口,ABI 引用的是影子栈而不是 GCS。
通过 aux 向量 AT_HWCAP2 条目中的 HWCAP_GCS 向用户空间报告对 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. 启用和禁用受保护控制栈¶
通过 PR_SET_SHADOW_STACK_STATUS prctl() 启用和禁用线程的 GCS,这需要一个指定应使用哪些 GCS 功能的 flags 参数。
设置 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 配置。
GCS 在 exec() 上禁用。
可以使用 PR_GET_SHADOW_STACK_STATUS prctl() 读取线程的当前 GCS 配置,这会返回传递给 PR_SET_SHADOW_STACK_STATUS 的相同标志。
如果线程之前启用了 GCS,但之后禁用了 GCS,则堆栈将在线程的生命周期内保持分配状态。目前,任何尝试为线程重新启用 GCS 的操作都将被拒绝,将来可能会重新考虑此问题。
应该注意的是,由于启用 GCS 会导致 GCS 立即变为活动状态,因此通常不可能从调用启用 GCS 的 prctl() 的函数返回。预计正常用法是,GCS 在程序执行的早期启用。
3. 受保护控制栈的分配¶
当为线程启用 GCS 时,将为其分配一个新的受保护控制栈,其大小为标准堆栈大小的一半或 2 千兆字节,以较小者为准。
当通过启用 GCS 的线程创建新线程时,将为新线程分配一个新的受保护控制栈,其大小为标准堆栈的一半。
当通过启用 GCS 或在线程创建期间分配堆栈时,堆栈的前 8 个字节将被初始化为 0,GCSPR_EL0 将设置为指向此 0 值的地址,这可用于检测堆栈的顶部。
可以使用 map_shadow_stack() 系统调用分配其他受保护控制栈。
使用 map_shadow_stack() 分配的堆栈可以选择在堆栈顶部放置一个堆栈末尾标记和上限。如果指定了标志 SHADOW_STACK_SET_TOKEN,则上限将放置在堆栈上,如果未指定 SHADOW_STACK_SET_MARKER,则上限将是堆栈的前 8 个字节,如果指定了,则上限将是接下来的 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 上限令牌将被写入 GCS,这是一个架构 GCS 上限,令牌类型(位 0..11)全部清除。信号帧中报告的 GCSPR_EL0 将指向此上限令牌。
信号处理程序将使用与中断上下文相同的 GCS。
在信号进入时启用 GCS 时,包含信号返回处理程序地址的帧将被推入 GCS,允许通过 RET 从信号处理程序正常返回。这不会在信号帧的 gcs_context 中报告。
5. 信号返回¶
从信号处理程序返回时
如果信号帧中存在 gcs_context 记录,则将在进一步验证之前从该上下文中恢复 GCS 标志和 GCSPR_EL0。
如果信号帧中没有 gcs_context 记录,则 GCS 配置将保持不变。
如果在从信号处理程序返回时启用了 GCS,则 GCSPR_EL0 必须指向有效的 GCS 信号上限记录,这将在信号返回之前从 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 核心转储扩展¶
NT_ARM_GCS 注释将添加到转储进程的每个线程的每个核心转储中。其内容将等效于当生成核心转储时,为每个线程执行相应类型的 PTRACE_GETREGSET 时读取的数据。
8. /proc 扩展¶
受保护控制栈页将在 /proc/<pid>/smaps 的 VmFlags 中包含 “ss”。