ARMv8.3-A PA在GCC里的相关实现
@(GCC)[GNU System-Security] –zet
00 导引
很多的安全问题通过攻击者人为制造的恶意指针,然后处理器解释恶意指针为代码地址,然 后执行恶意指针所指的代码,这里的代码恰恰就是攻击者预先准备的恶意代码。所以对于指 针的合法性问题一直是安全防御的重点,有很多的防御手段以保证指针没有被污染为重点: SSP(software statck protection),CFI(control flow integrity)。这些防御手段的核心 思想就是检查指针有没有被更改过,这些防御手段几乎都需要多条软件指令来实现。
在2016年的时候ARMv8架构里增加了ARMv8.3-A,这个版本里增加了Pointer Authentication 指令:强化指针安全的一种机制。硬件级别的安全防御可以预见将会是趋势,因为不论是从 代码执行效率代码的清晰度还是加固的彻底程度来说都明显优于软件级别的实现。所以这也 是一个非常让人兴奋的ARM处理器特性。
本文的描述基于GCC最新的一个release: GCC-7.1.0,由于PA是一个非常新的特性,即便是 最新的GCC版本(GCC-7.1.0)目前也只是做了一个大概雏型,可以使用GCC参数 -msign-return-address=[none | non-leaf | all]来使用。计划的让GCC选择key(A/B key) 用作返回地址签名的选项:-msign-return-address-key=key_name(key_name可选的值为: a_key/b_key分别对应A/B key)并没有在GCC-7.1.0里完成。目前相关key的代码还在GNU binutils里进行。所以本文只是在GCC-7.1.0现有代码的基础上做一个分析。后面随着相关 代码在GCC里的merge再进行补充完善。
01 细节
ARMv8.3-A能够支持PA的根本原因是因为当前的64位的linux下,地址空间也就是指针的实际 长度并不是64位,比如三级页表的情况下实际有效值只有低40位,这样高位的未使用的bit 就可以用来存储特定于某个指针的签名:Pointer Authentication Code(PAC)。
总得来说很简单,就是处理器内部在硬件级别以指针为一个参数来计算其特别的PAC然后存 储在指针的未使用的高位,计算的算法采用了一种叫做QARMA的可调分组密码(tweakable block ciphers)。计算/验证PAC的时候有(数据/指令 x A-key/B-key)四种组合指令: PAC*/AUT*,还有另外一种使用普通key计算PAC的指令PACGA。ARMv8.3-A还提供了检测PAC 的跳转指令族BRA*。
key存放在处理器内部的不可见寄存器里。所以攻击者很难不知道key的情况下得到PAC。
02 示例
在SSP(software statck protection)下如果不使用PA指令和使用PA指令的对比示例如下:
不使用PA指令
+--------------------------------------------------------------------+
| | No SSP | SSP |
+-----------|---------------------------|----------------------------+
| | SUB sp, sp, #0x40 | SUB sp, sp, #0x50 |
| | STP x29, x30, [sp,#0x30] | STP x29, x30, [sp, #0x40] |
| Prologue | ADD x29, sp, #0x30 | ADD x29, sp, #0x40 |
| | ... | ` ADRP x3, {pc} ` |
| | | ` LDR x4, [x3, #SSP] ` |
| | | ` STR x4, [sp, #0x38] ` |
| | | ... |
+-----------|---------------------------|----------------------------+
| | ... | ... |
| | LDP x29,x30,[sp,#0x30] | ` LDR x1, [x3, #SSP] ` |
| Epilogue | ADD sp,sp,#0x40 | ` LDR x2, [sp, #0x38] ` |
| | RET | ` CMP x1, x2 ` |
| | | ` B.NE __stack_chk_fail ` |
| | | LDP x29, x30, [sp, #0x40] |
| | | ADD sp, sp, #0x50 |
| | | RET |
+--------------------------------------------------------------------+
使用PA指令
+--------------------------------------------------------------------+
| | No SSP | SSP |
+-----------|---------------------------|----------------------------+
| | SUB sp, sp, #0x40 | ` PACIASP ` |
| | STP x29, x30, [sp,#0x30] | SUB sp, sp, #0x40 |
| Prologue | ADD x29, sp, #0x30 | STP x29, x30, [sp,#0x30] |
| | ... | ADD x29, sp, #0x30 |
| | | ... |
+-----------|---------------------------|----------------------------+
| | ... | ... |
| | LDP x29,x30,[sp,#0x30] | LDP x29,x30,[sp,#0x30] |
| Epilogue | ADD sp,sp,#0x40 | ADD sp,sp,#0x40 |
| | RET | ` AUTIASP ` |
| | | RET |
+--------------------------------------------------------------------+
注意反引号里指令的对应。可以看出PA的优势。
03 实现
下面来查看要输出上面示例代码在GCC里的实现。
因为GCC在后端和处理调用参数的时候使用了大量的文本处理程序来自动生成C++源代码,所 以在GCC后端特性的实现和参数的实现上基本就是一些配置文件加回调的特化C++代码。
规范GCC参数及其可用值的代码,这是一个脚本文件,编译GCC binary时由另外的脚本生成 参数C++文件。
/// gcc-7.1.0/gcc/config/aarch64.opt
msign-return-address=
Target RejectNegative Report Joined Enum(aarch64_ra_sign_scope_t)
Var(aarch64_ra_sign_scope) Init(AARCH64_FUNCTION_NONE) Save
Select return address signing scope.
Enum
Name(aarch64_ra_sign_scope_t) Type(enum aarch64_function_type)
Supported AArch64 return address signing scope
(for use with -msign-return-address= option):
/// gcc-7.1.0/gcc/config/aarch64-opts.h
/* Function types -msign-return-address should sign. */
enum aarch64_function_type {
/* Don't sign any function. */
AARCH64_FUNCTION_NONE,
/* Non-leaf functions. */
AARCH64_FUNCTION_NON_LEAF,
/* All functions. */
AARCH64_FUNCTION_ALL
};
输出PA指令的代码。
/// gcc-7.1.0/gcc/config/aarch64.c
void
aarch64_expand_prologue (void)
{
... // 删掉不重要代码
/* Sign return address for functions. */
if (aarch64_return_address_signing_enabled ())
{
// 指导输出PACIASP,这里的insn再经过一个简单匹配就是输出汇编代码了。
insn = emit_insn (gen_pacisp ());
add_reg_note (insn, REG_CFA_TOGGLE_RA_MANGLE, const0_rtx);
RTX_FRAME_RELATED_P (insn) = 1;
}
... // 删掉不重要代码
}
void
aarch64_expand_epilogue (bool for_sibcall)
{
... // 删掉不重要代码
if (aarch64_return_address_signing_enabled ()
&& (for_sibcall || !TARGET_ARMV8_3 || crtl->calls_eh_return))
{
// 指导输出AUTIASP
insn = emit_insn (gen_autisp ());
add_reg_note (insn, REG_CFA_TOGGLE_RA_MANGLE, const0_rtx);
RTX_FRAME_RELATED_P (insn) = 1;
}
... // 删掉不重要代码
}
上面输出的insn都是GCC rtl的一种表示。
04 总结
虽然硬件级别的加固特性目前很少,PA这种指针级别的加密验证更是属于开创阶段,不过由 于硬件加固的优势:代码清晰,效率高,易于实现。预计在安全问题越来越重要的今天将会 是未来的发展方向。
live long and prosper.