名词释义:
- PAC: Pointer Authentication Code
- PA: Pointer Authentication
- SCTLR: System ConTroL Register
该特性为指针值提供【签名】和【校验】的能力。ARM64 硬件上提供 5 个秘钥,分别用于为三种地址生成 PAC:
- 代码地址:API{A,B}Key
- 数据地址:APD{A,B}Key
- 通用地址:APGAKey
PAC 的值是关于以下三者的函数:1、指针本身;2、上下文相关值(也称 Modifier)1;3、秘钥。
生成后,PAC 会被附在指针值上(即“签在上面”),其所处的位置和长度与当前处理器:1)设置的虚拟地址长度以及;2)是否使能了 Address Tagging(即 TBI 特性)有关:
- 使能了 Address Tagging:
- 未使能 Address Tagging:
详见《Arm® Architecture Reference Manual for A-profile architecture》 (发布号 I.a)中的 D8.8.1 PAC field 一节。
值得一提的是,在不支持该特性的低版本处理器上,相关指令会被当作 nop
来执行。这一点在高通的文档
上有提及。
利用方式
目前最常见的利用方式如下:
- 压栈保存时用
paciasp
,基于 APIAKey 和 SP 对 LR 进行签名; - 出栈使用时用
autiasp
,基于 APIAKey 和 SP 对 LR 进行校验。
该方式实现了一种后向 CFI(Backward-edge CFI),即针对返回地址的防护。除此之外,有的厂商(如苹果的 Mac 系统)还利用 PAC 实现前向 CFI(Forward-edge CFI),针对函数间接调用的防护;以及 DFI,针对重要数据指针的防护。
若要为某个程序开启这类保护,需要使用合适版本的编译器,并在编译构建时添加编译参数 -mbranch-protection=
(可选值请参考编译器手册,如这里
)。如此,pac*
/aut*
系列指令就会由编译器安插在程序里适当的位置。可参考 Linux 内核的 PAC 编译支持:commit 74afda4016a7 (“arm64: compile the kernel with ptrauth return address signing”)
。
Linux 内核利用现状
Linux 内核引入 ARMv8.3 Pointer Authentication 的初始补丁集:(v7) arm64: return address signing 。
当前 Linux 内核仅实现了上述基于 PAC 的后向 CFI 方案,配有两个 configs:
ARM64_PTR_AUTH
:支持用户态 PA,在各进程执行exec()
时初始化秘钥ARM64_PTR_AUTH_KERNEL
2:(在编译器支持的情况下)使能内核态 PA,内核本身会使能对返回地址的保护
具体使用方式如内核文档 所述:
When
CONFIG_ARM64_PTR_AUTH
is selected, and relevant HW support is present, the kernel will assign random key values to each process atexec*()
time. The keys are shared by all threads within the process, and are preserved acrossfork()
.
对上述的 5 个秘钥,内核态会使用 IA(若内核态 PA 开启的话);另外 update_sctlr_el1()
的注释也有提到:在内核态中 EnIA3 不能被关闭,仅当从内核态退出至用户态内核退出(即类似关机这样的场景)4时才可能按需关闭。
1void update_sctlr_el1(u64 sctlr)
2{
3 /*
4 * EnIA must not be cleared while in the kernel as this is necessary for
5 * in-kernel PAC. It will be cleared on kernel exit if needed.
6 */
7 sysreg_clear_set(sctlr_el1, SCTLR_USER_MASK & ~SCTLR_ELx_ENIA, sctlr);
8 // ...
9}
一个陷阱
在 Linux 内核中,每个进程都有各自的一套 PA 秘钥,每次进程切换都涉及秘钥切换。而秘钥切换存在这样一个问题:若在一个使能了 PA 的函数执行过程中切换新的秘钥,
1paciasp
2stp fp, lr, [sp, #-FRAME_SIZE]!
3mov fp, sp
4
5/* ... function body ... */
6pa_switch_keys
7/* ... function body ... */
8
9ldp fp, lr, [sp], #FRAME_SIZE
10autiasp
11ret
那么在函数准备返回时,由于加密 LR 用的秘钥与切换后的秘钥不一致,autiasp
一定会发生校验错误。
关于这个情况,Linux 内核的应对有两点:
-
在进程切换(context switch)的最后阶段才切换秘钥:
1__switch_to(prev, next) 2 ptrauth_thread_switch_user(next) 3 last = cpu_switch_to(prev, next) // really the last moment 4 ptrauth_keys_install_kernel
其中
cpu_switch_to()
是一个汇编函数,切换了包括 SP 和 LR 在内的 callee-saved registers;ptrauth_keys_install_kernel
是一段汇编宏,不涉及函数上下文切换,仅切换 APIAKey。从内核线程的视角来看,进入和退出cpu_switch_to()
时的 SP、LR 与 APIAKey 三者是匹配的。 -
内核初始化秘钥的过程不涉及函数调用,如 commit 33e45234987e 所述:
The keys for idle threads need to be set before calling any C functions, because it is not possible to enter and exit a function with different keys.
1start_kernel // never returns so it's fine 2 boot_init_stack_canary // always_inline 3 ptrauth_thread_init_kernel(current) // macro 4 ptrauth_thread_switch_kernel(current) // macro
其他厂商利用情况
目前已知苹果已在 A12 及后续 A 系列处理器上支持了 PAC,并在 iOS、macOS 等操作系统的层面上基于 PAC 实现了前向 CFI、后向 CFI 及 DFI 防护。摘自苹果开发者文档 :
- Return addresses are signed with a key that’s unique per process, using a salt derived from the stack pointer.
- Function pointers are signed with a key that’s fixed across all processes, allowing sharing of library code between processes.
- Virtual method table entries are signed with a key that’s shared across all apps, using a salt derived from the method signature.
参考资料
- Arm® Architecture Reference Manual for A-profile architecture, issue I.a
- Pointer Authentication on ARMv8.3: Design and Analysis of the New Software Security Instructions from Qualcomm
- ARMv8.3 Pointer Authentication for LSS 2017 (local copy )
- (v7) arm64: return address signing ,Linux 内核引入该特性的初始补丁集
- Examining Pointer Authentication on the iPhone XS from Google Project Zero
-
应该认识到,指针的正确性与其所处的上下文有关。以返回地址为例,一个地址即便对某个函数来说是合法的返回地址,它也不应由其他函数使用。 ↩︎
-
SCTLR 中的 EnIA、EnIB、EnDA、EnDB(即 31、30、27、13)位控制 AP{I,D}{A,B}Key 的使能。详见《Arm® Architecture Reference Manual for A-profile architecture》 (发布号 I.a)中的 D17.2.118 SCTLR_EL1, System Control Register (EL1) 一节。 ↩︎
-
~~关于这里"kernel exit"的意思我有两种猜测,更倾向后者:若开启了内核态 PAC,理论上不需要也不应该在运行时关闭它。话说我在内核代码里没找到哪里有关闭 EnIA 的。~~通过阅读这个补丁 和 arch/arm64/kernel/entry.S,发现之前这里理解错了:kernel entry/exit 指的就是从用户态进入内核态/从内核态退出至用户态。 ↩︎