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”。