AArch64 Linux 中的指针认证

作者: Mark Rutland <mark.rutland@arm.com>

日期: 2017-07-19

本文档简要描述了 AArch64 Linux 中指针认证功能的提供情况。

架构概述

ARMv8.3 指针认证扩展添加了一些原语,可用于缓解攻击者能够破坏某些内存内容(例如栈)的特定类别的攻击。

该扩展使用指针认证码(PAC)来确定指针是否被意外修改。PAC 是由指针、另一个值(如栈指针)以及保存在系统寄存器中的密钥派生而来的。

该扩展添加了将有效 PAC 插入指针,以及验证/从指针中移除 PAC 的指令。PAC 占据了指针的一定数量的高位比特,具体数量取决于配置的虚拟地址大小以及是否正在使用指针标记。

这些指令的子集是从 HINT 编码空间中分配的。在缺少该扩展(或禁用该扩展)的情况下,这些指令表现为 NOP。使用这些指令的应用程序和库无论是否存在该扩展都能正常运行。

该扩展提供了五个独立的密钥来生成 PAC —— 两个用于指令地址(APIAKey、APIBKey),两个用于数据地址(APDAKey、APDBKey),以及一个用于通用认证(APGAKey)。

基本支持

当选中 CONFIG_ARM64_PTR_AUTH 且存在相关的硬件支持时,内核将在 exec*() 时为每个进程分配随机密钥值。这些密钥由进程内的所有线程共享,并在 fork() 过程中得以保留。

地址认证功能的存在通过 HWCAP_PACA 宣告,通用认证功能则通过 HWCAP_PACG 宣告。

PAC 在指针中占用的位数是 55 减去内核配置的虚拟地址大小。例如,当虚拟地址大小为 48 时,PAC 的宽度为 7 位。

当选中 CONFIG_ARM64_PTR_AUTH_KERNEL 时,内核在编译时会包含使用 HINT 空间指针认证指令来保护函数返回的代码。使用此选项构建的内核既可以在支持指针认证的硬件上运行,也可以在不支持该功能的硬件上运行。

除了 exec() 之外,还可以使用 PR_PAC_RESET_KEYS prctl 将密钥重新初始化为随机值。由 PR_PAC_APIAKEY、PR_PAC_APIBKEY、PR_PAC_APDAKEY、PR_PAC_APDBKEY 和 PR_PAC_APGAKEY 组成的位掩码指定了要重新初始化的密钥;指定 0 意味着“所有密钥”。

调试

当选中 CONFIG_ARM64_PTR_AUTH 且存在地址认证的硬件支持时,内核将把 TTBR0 PAC 位的位置暴露在 NT_ARM_PAC_MASK 寄存器集(struct user_pac_mask)中,用户空间可以通过 PTRACE_GETREGSET 获取该寄存器集。

该寄存器集仅在设置了 HWCAP_PACA 时才会暴露。由于数据指针和指令指针的 PAC 位集合可能有所不同,因此会分别暴露数据指针和指令指针的掩码。请注意,这些掩码适用于 TTBR0 地址,不适用于 TTBR1 地址(例如内核指针)。

此外,当同时设置了 CONFIG_CHECKPOINT_RESTORE 时,内核将暴露 NT_ARM_PACA_KEYS 和 NT_ARM_PACG_KEYS 寄存器集(struct user_pac_address_keysstruct user_pac_generic_keys)。这些寄存器集可用于获取和设置线程的密钥。

虚拟化

在初始化每个虚拟 CPU 时,通过传递标志 KVM_ARM_VCPU_PTRAUTH_[ADDRESS/GENERIC] 并请求启用这两个独立的 CPU 特性,指针认证在 KVM 客户机中被启用。当前的 KVM 客户机实现是通过同时启用这两个特性来工作的,因此在启用指针认证之前会检查这两个用户空间标志. 独立的用户空间标志确保了如果将来添加支持以允许这两个特性彼此独立启用时,无需更改用户空间 ABI。

由于 Arm 架构规定指针认证特性与 VHE 特性一起实现,因此 KVM arm64 ptrauth 代码依赖于 VHE 模式的存在。

此外,当未设置这些 vcpu 特性标志时,KVM 将从 KVM_GET/SET_REG_* ioctl 中过滤掉指针认证系统密钥寄存器,并从 cpufeature ID 寄存器中屏蔽这些特性。任何使用指针认证指令的尝试都将导致向客户机注入一个 UNDEFINED 异常。

启用和禁用密钥

prctl PR_PAC_SET_ENABLED_KEYS 允许用户程序控制在特定任务中启用哪些 PAC 密钥。它接受两个参数,第一个是由 PR_PAC_APIAKEY、PR_PAC_APIBKEY、PR_PAC_APDAKEY 和 PR_PAC_APDBKEY 组成的位掩码,用于指定受此 prctl 影响的密钥;第二个是相同位的位掩码,用于指定应启用还是禁用该密钥。例如

prctl(PR_PAC_SET_ENABLED_KEYS,
      PR_PAC_APIAKEY | PR_PAC_APIBKEY | PR_PAC_APDAKEY | PR_PAC_APDBKEY,
      PR_PAC_APIBKEY, 0, 0);

禁用了除 IB 密钥之外的所有密钥。

这之所以有用,其主要原因是为了支持这样一种用户空间 ABI:它使用 PAC 指令来对函数指针以及在函数外部暴露的其他指针进行签名和认证,同时仍允许符合该 ABI 的二进制文件与不签名或认证指针的旧版二进制文件进行互操作。

其构想是,动态加载程序或早期启动代码在确定进程可能会加载旧版二进制文件之后、但在执行任何 PAC 指令之前,会尽早发出这个 prctl。

为了与以前的内核版本保持兼容,进程启动时默认会启用 IA、IB、DA 和 DB,并且在调用 exec() 时会重置为该状态。通过 fork() 和 clone() 创建的进程会从调用进程继承密钥的启用状态。

建议避免禁用 IA 密钥,因为与禁用任何其他密钥相比,这会带来更高的性能开销。