AArch64 Linux 的受保护控制栈支持

本文档简要概述了 Linux 为支持使用 ARM 受保护控制栈 (GCS) 功能而提供给用户空间的接口。

这只是对最重要特性和问题的概述,并非旨在详尽无遗。

1. 概述

  • GCS 是一种架构功能,旨在提供更强的保护以抵御返回导向编程 (ROP) 攻击,并简化需要收集堆栈跟踪的功能(例如分析)的实现。

  • 启用 GCS 后,PE 会维护一个单独的受保护控制栈,该栈只能通过特定的 GCS 操作写入。它仅存储调用堆栈,执行过程调用指令时,当前 PC 会被推送到 GCS 上,并且在 RET 时,LR 中的地址会根据 GCS 顶部的地址进行验证。

  • 激活后,当前 GCS 指针存储在系统寄存器 GCSPR_EL0 中。 用户空间可以读取此寄存器,但只能通过特定的 GCS 指令更新。

  • 该架构提供了用于在受保护控制栈之间切换的指令,并检查以确保新堆栈是切换的有效目标。

  • GCS 的功能类似于 x86 影子堆栈功能提供的功能。 由于共享用户空间接口,ABI 指的是影子堆栈而不是 GCS。

  • 通过辅助向量 AT_HWCAP 条目中的 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 功能。

  • 设置 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 的 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 个字节;如果指定了 SHADOW_STACK_SET_MARKER,则上限将是接下来的 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,这是一个具有令牌类型(位 0..11)全部清除的架构 GCS 上限。 信号帧中报告的 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。

  • 可以使用 ptrace 配置 GCS 模式(包括启用和禁用)。 如果通过 ptrace 启用 GCS,则不会为线程分配新的 GCS。

  • 通过 ptrace 进行的配置会忽略 GCS 模式位的锁定。

7. ELF 核心转储扩展

  • NT_ARM_GCS 注释将添加到为每个转储进程的每个线程的每个核心转储。 这些内容将等同于如果为生成核心转储时为每个线程执行了相应类型的 PTRACE_GETREGSET 而读取的数据。

8. /proc 扩展

  • 受保护的控制栈页面将在 /proc/<pid>/smaps 中的 VmFlags 中包含 “ss”。