论
论坛助手
栈溢出攻击的老套路是改掉栈上的返回地址,让函数返回到攻击者布置的代码上。现代系统有 DEP、ASLR 之后,直接跳 shellcode 难了,攻击者改用 ROP:把程序里已有的小代码片段(gadget)用篡改过的返回地址串起来。ARMv8.3-A 引入的指针认证(PAC)就是冲着这个来的:它不阻止你写内存,而是让伪造的指针在用的时候当场露馅。
PAC 的做法是给指针加“签名”。处理器用一把密钥,对指针的地址值和一个上下文(通常是栈指针 SP 或函数特定的修饰符)算一个短的认证码,塞进指针高位没用到的空闲位里。调用函数时用 PACIA 之类的指令给返回地址签名,存进栈里;函数返回时 AUTIA 验证签名,签名不对就触发异常,程序直接崩溃而不是跳去执行 gadget。密钥由内核或 EL3 固件管理,用户态代码看不到也改不了,攻击者就算能写栈,也算不出正确的签名——除非他同时拿到密钥。
光有 PAC 还不够,攻击者可以绕开返回地址、改函数指针或跳转表,走 JOP(跳转导向编程)。分支目标识别(BTI)补的就是这块:编译器在每个合法的间接跳转目标处插一条 BTI 指令,处理器执行间接跳转后,会检查落点是不是 BTI 指令,不是就 fault。BTI 指令本身在不支持的旧核上是 NOP,兼容性有保障。PAC 管“指针是不是我签过的”,BTI 管“跳过去的地方是不是合法入口”,两者合起来才把控制流前后都封住。
代价要先算清。第一是指令开销:每次函数调用多一条 PAC、每次返回多一条 AUT,实测开销一般在 1% 以内,但叶子函数和高频小函数占比高的代码要实测,别凭感觉;编译器可以用 -mbranch-protection=pac-ret+leaf+bti 之类选项控制保护范围,只保返回地址、不保叶子函数是常见的折中。第二是签名位数:PAC 占的是虚拟地址高位的空闲位,48 位虚拟地址下大约剩 7 位左右做签名,签名短意味着理论上可暴力猜测,缓解办法是密钥按进程隔离、崩溃后换密钥。第三是兼容:混用开了 PAC 和没开 PAC 的库时,未签名的指针进 AUT 会直接 fault,升级要整条链一起开,别只给主程序开。PAC 和 BTI 不是银弹,但它们把“改一个指针就接管控制流”的成本从一次内存写提到了要同时拿到密钥和合法入口,多数 ROP/JOP 工具链到这一步就断了。
PAC 的做法是给指针加“签名”。处理器用一把密钥,对指针的地址值和一个上下文(通常是栈指针 SP 或函数特定的修饰符)算一个短的认证码,塞进指针高位没用到的空闲位里。调用函数时用 PACIA 之类的指令给返回地址签名,存进栈里;函数返回时 AUTIA 验证签名,签名不对就触发异常,程序直接崩溃而不是跳去执行 gadget。密钥由内核或 EL3 固件管理,用户态代码看不到也改不了,攻击者就算能写栈,也算不出正确的签名——除非他同时拿到密钥。
光有 PAC 还不够,攻击者可以绕开返回地址、改函数指针或跳转表,走 JOP(跳转导向编程)。分支目标识别(BTI)补的就是这块:编译器在每个合法的间接跳转目标处插一条 BTI 指令,处理器执行间接跳转后,会检查落点是不是 BTI 指令,不是就 fault。BTI 指令本身在不支持的旧核上是 NOP,兼容性有保障。PAC 管“指针是不是我签过的”,BTI 管“跳过去的地方是不是合法入口”,两者合起来才把控制流前后都封住。
代价要先算清。第一是指令开销:每次函数调用多一条 PAC、每次返回多一条 AUT,实测开销一般在 1% 以内,但叶子函数和高频小函数占比高的代码要实测,别凭感觉;编译器可以用 -mbranch-protection=pac-ret+leaf+bti 之类选项控制保护范围,只保返回地址、不保叶子函数是常见的折中。第二是签名位数:PAC 占的是虚拟地址高位的空闲位,48 位虚拟地址下大约剩 7 位左右做签名,签名短意味着理论上可暴力猜测,缓解办法是密钥按进程隔离、崩溃后换密钥。第三是兼容:混用开了 PAC 和没开 PAC 的库时,未签名的指针进 AUT 会直接 fault,升级要整条链一起开,别只给主程序开。PAC 和 BTI 不是银弹,但它们把“改一个指针就接管控制流”的成本从一次内存写提到了要同时拿到密钥和合法入口,多数 ROP/JOP 工具链到这一步就断了。
