原文作者:Alan Mujumdar April 7, 2021
原文链接:https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/armv8-1-m-pointer-authentication-and-branch-target-identification-extension
翻译: Zenon Xiu (修志龙)
我们高兴地宣布Armv8.1-M Pointer Authentication 和 Branch Target Identification (PACBTI) 扩展, 请参看最新的Armv8-M构架参考手册(version B.o及更新版本)。 这个扩展增强了M-profile的安全模式,为软件开发者提供了新的工具。
PACBTI是受到A-profile构架的两个功能的启发, Pointer Authentication 在Armv8.3-A构架中引入,Branch Target Identification在Armv8.5-A构架中引入。这些技术设计的目的是防御Return-Oriented Programming (ROP) 和Jump-Oriented Programming (JOP) 攻击。
这些攻击利用一些已经存在且合法的,称之为gadget的代码片段。 在一个成功的攻击中,攻击者可以通过如stack smashing的方法,获取对调用栈的控制, 然后改写存在stack(栈)里面的指针值,把它指向选定的gadget。通过从一个gadget跳转到另一个gadget, 攻击者可以提升操作权限并完全控制系统。
请参照Learn the architecture: Providing protection for complex software, 这里面介绍了stack smashing, ROP 和 JOP。
Armv8-M通过Trustzone for Armv8-M, Memory Protection Unit (MPU) 和Privileged Execute-never (PXN) 技术提供了安全和内存保护功能。这些功能提供了足够的机制来,
PACBTI是建立在这些现有功能之上的,并提供了检测ROP和JOP软件攻击的新手段。
和许多硬件安全功能一样,PACBTI设计用来捕获通常可能用作攻击的软件错误,但是它不是所有ROP和JOP攻击的终极解决方案。这些扩展依赖于健壮的软件模型,当和良好的软件开发实践结合时,它可以是一个强大的工具。支持PACBTI扩展的编译器应该保证PAC和BTI指令被正确地插入到编译出来的代码中。
Pointer authentication
Pointer authentication(指针验证)技术最开始是为64位构架Armv8-A AArch64设计的。指针经常被存在Stack里,如果stack被攻击者控制,那么指针就可以被改写。当前,全部的64bit地址范围并没有被完全利用,因此空余的指针高bit位可以用来嵌入用作验证指针的安全信息。Armv8.3-A的Pointer authentication技术不能直接移到Armv8.1-M上, 因为M-profile构架是基于32bit的物理内存。指向任何地址的指针都不会超过32bit, 指针没有空余的bit用来嵌入用作验证指针的安全信息。正因为如此,在Armv8.1-M构架中, 指针的authentication信息是保存在独立于指针的一个通用寄存器中(GPR)中, 只要软件正确插入了pointer authentication指令,那么行为就可以比较。 Pointer authentication可以在每个安全状态和特权级状态使能,参见表1
表1: 使能Pointer authentication
生成pointer authentication code(指针验证码)
生成Pointer Authentication Code(PAC)可以认为是对指针签名的过程。 为了生成PAC, 将指针,一个modifier, 和一个密钥输入到加密算法,这会产生一个固定32bit长度的码,我们称这个码为PAC_enter,签名指令将PAC存到一个通用寄存器。 比如,PAC指令用固定的寄存器-Link Register (LR)作为指针,Stack Pointer (SP)寄存器作为modifier, R12用来存生成的PAC, PACG指令可以选择输入,输出通用寄存器。
这个过程由图1来表示:
图1 PAC生成
Modifier的值必须在生成PAC和验证签名指针时是一样的。比如,当在函数调用时对函数返回地址签名,SP在每次函数调用时都不一样,但是SP在进入和退出函数调用时会是一样的值。使用SP作为modifier可以生成只有当前函数调用实体才有效的PAC,因为SP可能在每次函数调用时会在不一样的地址。
验证pointer authentication code
为了验证一个指针,验证指令(authentication instruction)比较PAC_enter(由前面指针签名指令产生)和由验证指令隐式生成新的PAC(我们称之为PAC_return)。 PAC_return的值对软件不可见。如果PAC_enter和PAC_return一致,那么可以确定下面的值没有被修改:
如果PAC_enter和PAC_return不匹配,验证指令会导致INVSTATE UsageFault异常,如图2所示。异常处理会终止这个线程,因为任何的验证失败明显表示存在攻击。任何推测性执行的指令都会被终止,保证不会有因为被破坏指针,modifier, 密钥和PAC导致的副作用被观察到。
图2:PAC验证
验证机制可以在很大程度上检测到任何或所有输入值的修改。加密算法的输出是一个32bit的PAC, 因此有可能不同的输入和加密密钥组合会产生一样的PAC。 这种加密算法冲突来自许多加密机制的固有限制,不能通过PAC验证指令来检测。然而,这种冲突的可能性非常小。
密钥
构架提供4个128bit的密钥,每个安全状态和特权级状态一个,参见表2, 每个密钥存在4个32bit的系统寄存器中
PAC_KEY_U_S
表2: PAC密钥
特权级软件可以通过MSR和MRS指令访问,它也可以管理像密钥升级维护这样的操作。非特权级软件不能直接访问PAC密钥。我们也支持Trustzone for Armv8-M的通用规则,以下访问是允许的:
每个安全状态和特权级状态有唯一的密钥,因此软件不需要切换密钥,硬件自动会根据状态切换密钥。但是,多数用户软件会在非安全非特权级Thread模式运行,因此我们推荐每个线程分配唯一的密钥。如果攻击者试图制造gadget链,那么每个PAC都必须被正确猜出来,否则就会导致异常,阻止进一步攻击。
新指令
提示:如果使用相同的输入来生成和验证PAC的话,PAC, PACBTI,PACG操作可以和任何的AUT, BXAUT, AUTG指令一起相互操作。
使用NOP空间
有些新的指针验证指令使用NOP指令编码空间。使用这些NOP空间指令的应用和库可以运行在硬件上不支持pointer authentication的老处理器上。尽管老处理器不能从pointer authentication获益,但这对异构系统很有用。
以下指令使用NOP空间:
• PAC• PACBTI• AUT
调试
PACBTI扩展可以支持软件和外部调试器调试
何时何地使用PAC?
可能易受到ROP攻击的软件可以使用以下C代码来演示。在这个例子里,我们调用一个函数来获取外部输入。因为没有边界检查,用户可以输入一个任意长度的字符串来overrun分配的内存。这一个非常糟糕代码的好例子,它应该很容易被发现,但是编译器也许不会报任何警告,因此程序员必须有足够的经验来理解它的弱点。
void callee(void){ char username[12]; //保存在栈中 scanf("%s", username); // 如果输入大于分配的数组大小, //那么返回指针可能被改写 } void caller(void){ callee(); }
当这个C代码编译成Cortex-M指令时,取决于编译优化级别,反汇编可能是这样的
scanf: ... ; 接收外部输入 callee: PUSH {LR} ; 压栈link register SUB SP, SP, #12 ; 调整栈指针 MOV R1, SP ; SP通过R1传给scanf LDR R0, .L3 ; .L3 中有指向 “%s” 的指针 BL scanf ADD SP, SP, #12 ; 在返回之前重新调整SP的值 POP {PC} ;如果外部输入越过边界, ;那么加载到PC的值可以用作ROP攻击 caller: PUSH {R3, LR} BL callee POP {R3, PC}
那我们怎么才能避免这个问题呢?显然的答案是修复软件,但是不是所有的情况都可以简单修复的,这就是像PAC这样的额外硬件机制的用武之地。
可以告诉编译器在被调函数里使用PAC功能,反汇编代码结果如下
callee: PAC R12, LR, SP ; 对返回地址签名 PUSH {R12, LR} ; 压栈PAC和返回地址 SUB SP, SP, #16 ; 调整栈指针 MOV R1, SP ; SP通过R1传给scanf LDR R0, .L3 ; .L3 中有指向 “%s”的指针 BL scanf ADD SP, SP, #16 ; 返回之前重新调整SP的值 POP {R12, LR} ; 恢复PAC和返回地址 BXAUT R12, LR, SP ; 验证LR并返回调用者
在函数开始位置加入PAC指令,保证任何对LR和R12的破坏都会在BXAUT指令执行时被发现。 R12必须被压栈,因为scanf函数不保证会保护它。 在这个例子里SP不需要被压栈,但是如果SP原来值被改变,PAC验证会失败。
虽然PAC对捕获指针破坏很有用,但是不是所有的函数都需要保护。当从Stack中读取一个函数返回指针然后跳转到这个地址时最容易受攻击。一个典型的函数例子是放在LR寄存器里的返回地址,函数返回是通过“BX LR”指令来实现的,如果指针被篡改,那么跳转不会回到本来的调用者函数。然而,在叶函数中,LR不会在stack中保存和恢复,因此这种攻击不能实施,因此PAC保护是不必要的。
验证和跳转组合指令,BXAUT,阻止了一些编译器优化,但鲁棒性更好。两个原因使它更有用:代码密度提升和消除一些gadget。但我们不能消除所有的代码密度开销,因为还是场景有需要PAC指令。
BXAUT可以用AUT+BX两条指令来替代,来提供向后兼容的代码。因为验证操作和跳转返回是独立的两条指令,有些编译器指令重排优化可能在AUT和BX之间插入其他的指令。如果被验证过的LR没有被压栈和恢复,这完全是可以的,这也适用于AUTG指令。但是,如果PACBTI保护没有应用到整个软件栈时,任何在指针验证和跳转之间的间隙都可能暴露为ROP和JOP gadget。
PACBTI是一个新功能,我们不能期望所有的软件库都马上重新编译,支持这个功能,因此软件很可能使用混合的方式。PACBTI保护是为了使你的代码更安全,减小被攻击的可能性。但是,系统其他部分的安全性不能保证,有些库和应用代码仍然可能受ROP攻击。
Memory保护
在下面的例子中,我们演示一个典型的易受ROP攻击的代码,怎么通过PAC机制进行保护。注意为了可读性,有些代码的复杂度被隐藏。
原始代码
main: BL memcpy memcpy: PUSH {R0, LR} WLS LR, R2, loopEnd loopStart: LDRSB R3, [R1], #1 STRB R3, [R0], #1 LE LR, loopStart loopEnd: POP {R0, LR} BX LR
PAC保护的代码
main: BL memcpy memcpy: PAC R12, LR, SP ; 对指针签名 PUSH {R0, LR} WLS LR, R2, loopEnd loopStart: LDRSB R3, [R1], #1 STRB R3, [R0], #1 LE LR, loopStart loopEnd: POP {R0, LR} AUT R12, LR, SP ; 对指针验证
这个例子演示了使用3个armv8.1-M技术,Helium,Low Overhead Branches(LOB)和PAC,的简单”memcpy”函数。LOB操作使用LR来计算循环次数,或使用Helium的情况下的向量元素。因此,如果没有scratch寄存器可用时,即使是页函数,还是需要压栈LR。
我们可以替换AUT和BX操作为单条指令BXAUT。这条指令不在NOP空间,所以任何编译出这条指令的代码只能在支持PACBTI的CPU上运行。
向后兼容方案
loopEnd: POP {R0, LR} AUT R12, LR, SP BX LR
紧凑方案
loopEnd: POP {R0, LR} BXAUT R12, LR, SP
M-profile和A-Profile PAC的比较
A-profile
M-profile
PAC和BTI扩展可以独立实现
PACBTI扩展提供PAC和BTI功能,但是提供了两个功能独自的控制
这个功能是Armv8.3必须实现的
PACBTI是Armv8.1-M可选的功能
计算出的PAC放在64bit虚拟地址的高bit
计算出的PAC放在一个32bit的通用寄存器中,PAC和物理地址放在不同的寄存器中
PAC的长度范围为11~13 bit(如果tagged addresses功能不使能的话),或2~23bit(如果tagged address功能使能的话)
PAC的长度不可配,固定为32bit
提供了5个128bit的密钥,两个用在指令地址,两个用在数据访问,一个用作通用验证
PAC密钥在Exception Level间没有bank
每个安全状态和特权级状态组合都有PAC密钥
PAC算法:QARMA或自定义
PAC算法:QARMA或自定义,和A-profile一样
指针验证功能通过SCTLR使能
指针验证通过CONTROL寄存器使能。 PAC可以在每个安全状态和特权级状态组合使能
作为验证过程的一部分,验证指令做以下其中一个事情:
· 如果指针通过验证,将PAC替换为扩展bit
· 替换PAC为扩展bit并且设置2 bit扩展为固定唯一的值。如果这个指针被用作跳转,那么执行这个跳转导致一个translation fault
验证指令做以下其中一个事情:
· 如果指针通过验证,没有任何副作用
· 如果PAC,指针,modifier或密钥不匹配原始值,这条指令产生一个同步INVSTATE UsageFault
当验证失败时,有些验证指令产生同步异常,比如AUTIASP,但是当这个地址被访问时,有些指令产生translation fault,比如RETAA
所有验证指令在验证失败时产生同步fault
验证失败时,产生特殊的PAC异常
验证失败时,产生INVSTATE UsageFault
PAC码嵌在指针中,一次有些特殊指令,如XPACI,可以用来不进行验证而将PAC从指针中移除
PAC放在另一个通用寄存器中,因此这个寄存器可以被单独清除。指针可以不进行验证过程使用,这是软件来决定的。
Branch target identification (跳转目标识别)
Branch Target Identification (BTI)可以防御一些JOP攻击,这是通过创建构架定义的非直接跳转指令和指令跳转目标之间依赖关系实现的。因为指针常被存在stack中,如果stack被攻击那么指针就可以被篡改,因而非直接跳转易受到JOP攻击。通过修改指针,攻击者可以利用现存的非直接跳转跳到想要的gadget.
在AArch64中,CPU可以配置为非直接跳转只能跳到选定内存区域的有效的“landing pad”(着陆点)指令,这个内存区域由translation table的Guarded Page (GP)bit 来指定。 构架可以记录跳到landing pad的跳转类型,直接跳转和非直接跳转都可以追踪。这是通过使用PSTATE的BTYPE来实现的,可以识别3种跳转类型, calls(函数调用), jump (跳转)和所有的branch(分支跳转)。
Armv8.1-M仅支持物理地址,在MPU寄存器中没有空余的bit, 因此我们不能通过GP bit或等同方式来标志内存区域。但是,BTI仍然可以在没有MPU支持下工作。我们引进了EPSR.B bit用来记录非直接跳转。与AArch64不同,我们选择了非直接跳转的子集,而直接跳转不能被记录。 直接跳转使用相对于PC的寻址,如典型的函数调用可以通过PAC来保护,因此在M-Profile中仅jump被BTI追踪。
这些Jump指令被称为 “BTI setting”指令,当他们被执行时,CPU设置EPSR.B为1。
“BTI clearing“或”landing pad” 指令清EPSR.B为0。如果硬件实现正确,BTI setting指令必须只能跳到BTI clearing 指令,否则导致INVSTATE UsageFault 异常。 Armv8.1-M大致的行为模型如图3所示。注意Branch Future (BF)指令仅通知PE(CPU)有一个将要到来的跳转,他们不直接修改EPSR.B,而是更新LO_BRANCH_INFO.BTI来指示有一个将要来的Branch setting指令。
图3 BTI行为
BTI异常会在当EPSR.B被设为1时,但预取非BTI setting指令时同步产生的。当异常产生时,EPSR可以被正常压栈,因此EPSR.B的状态可以被捕获。在进入异常处理代码时,EPSR.B 被清零,因此BTI 在异常处理代码里可能没有使能。因为识别失败明显表示正受攻击,因此异常处理可以终止这个线程。
BTI setting指令
我们在现有的Armv8.1-M非直接跳转指令基础上增加了BTI setting功能。如果BTI在目标安全状态和特权级状态下被使能时,以下指令带BTI setting功能:
“BX LR”和“BFX LR”不是BTI setting 指令,因为他们常用作函数返回,这个指针可以通过PAC验证保护。 BTI setting指令是基于典型代码的编译选择的,因此不是所有的非直接跳转都需要BTI setting功能。
BTI clearing指令
以下指令是BTI clearing:
执行这些有效的landing pad指令时将EPSR.B清零, 这是重要的,因为如函数或case statement等通常软件结构可以从任何地方被调用。这和被BTI保护的软件库特别相关。
除了当作调试使用的BKPT指令,试图执行所有其他非landing pad指令都会导致异常。异常会在指令预取时产生,因此任何试图执行可疑代码的JOP都会被阻止,并不会有任何构架可见的副作用。
新指令BTI和PACBTI是在NOP空间。通过NOP空间指令保护的应用和库可以在不支持BTI的老处理器上执行。尽管这对老处理器来说没有保护的好处,但对异构系统有用。
BTI支持软件和外部调试器
安全状态转换
Armv8-M Trustzone技术描述了安全和非安全软件的转换。请阅读TrustZone technology for Armv8-M architecture 了解更多。
PACBTI扩展引进了每个安全和特权级使能BTI的单独控制,见表3. 比如编译的不支持BTI的用户代码可以调用BTI保护的安全库。
表3: BTI控制
所有用作安全状态转换的指令都支持BTI。
BTI Setting
BTI clearing
当实现了BTI并使能了时,在安全状态转换模型中的行文描述如图4所示
图4: 带BTI的安全状态转换
转换到安全状态
在这个例子里,我们演示了Trustzone如何与BTI一起工作。使能BTI不会影响汇编代码,因为软件不直接控制BTI的构架状态,并且现有的指令隐式地支持这个行为。
non-secure: ... LDR R4, =non-secure-callable ... BLX R4 ; 设置EPSR.B为1,BTI setting 指令 ; 非直接跳转到SG ... ; 这里不需要是BTI clearing指令 ; 这里是调用安全函数后的返回地址 ... non-secure-callable: SG ; 设置EPSR.B为0, BTI clearing 指令 B secure ; 不是BTI setting 指令 ; 直接跳转到安全函数 ... secure: ... ; 不需要 BTI clearing 指令 ; 函数体 BXNS LR ; 不是BTI setting 指令,因为使用了LR ; 返回非安全函数
这个例子显示了当BTI在非安全状态使能的行为,但是代码会与非安全状态BTI没有使能是一样的。与安全状态的BTI设置无关,因为是通过SG指令访问的,SG总是BTI clearing。从安全状态返回到非安全状态不会触发任何BTI行为,因为它是通过”BXNS LR”指令完成的,这个指令不是BTI setting。
调用非安全软件
在这个例子,我们演示了BTI行为如何被加到安全程序调用非安全函数。
非安全状态BTI没使能时
当非安全态的BTI没有使能时,安全软件必须保证当调用非安全函数时BTI没有被设置。安全BTI可以是使能或禁止。因为安全软件可以访问非安全bank的CONTROL寄存器,它总是可以查询非安全态的设置。非安全软件,如库,可以没有编译为支持PACBTI,因此安全软件必须保证对非安全态的典型访问可以正常工作。
安全软件可以通过BLXNS指令调用其他安全函数,在这种情况下,BLXNS指令会查询当前安全 bank的CONTROL寄存器,并决定BTI setting的功能是否必须使能。如果安全BTI是使能的,那么BLXNS会设置EPSR.B为1,否则不修改EPSR.B
secure: ... LDR R0, =non-secure ... BLXNS R0 ; 指令隐式地检查 CONTROL_NS.UBTI_EN ; EPSR.B没变, 非安全BTI没有使能 ... ;这里是调用非安全函数后的返回地址 non-secure: ... ; 不需要BTI setting指令 BX LR ; 不是 BTI setting 指令 ; 返回到安全函数(调用者)
非安全状态BTI使能时
当非安全态的BTI使能,并调用非安全函数时,安全软件会要求BTI setting 指令设置EPSR.B。 非安全函数必须以BTI clearing指令开始, 当需要PAC保护时为PACBTI,或当不需要PAC保护时为BTI。
secure: ... LDR R0, =non-secure ... BLXNS R0 ; 指令隐式地检查 CONTROL_NS.UBTI_EN ; EPSR.B设置为1, 非安全 BTI 是使能的 ... ; 调用后的返回地址 non-secure: BTI ; EPSR.B 清 0, BTI clearing 指令 ; 可以使用PACBTI,但是FNC_RETURN是从 ; 安全状态栈中加载的, PAC可能是多余的 ... ; 函数体 BX LR ; 不是BTI setting 指令 ; 返回到安全函数
PACBTI指令
当BTI使能时,只有一小部分指令是有效的landing pad, 比如PACBTI. 在编译时,如果Link Time Optimizer (LTO)用非BTI setting 指令替换BTI setting指令,那么可能不需要BTI clearing指令,并可以安全地移除这个landing pad. 如果LTO确定所有目标为landing pad的指令都不是BTI setting, 那么这个替换就可以发生。移除landing pad会加强安全,因为有更少的gadget入口。为了覆盖这个场景,和其他不需要PACBTI的地方,PACBTI指令可以被PAC指令代替。
示例
简单函数
在函数开始的地方加入BTI指令可以保证即使攻击者可以操纵一个指针,但跳到一个函数体中间会失败,因为这会违背BTI功能的规则,PE(CPU)会产生异常。 在这个例子里,ADD指令可能被攻击者用作可用的gadget,
main: LDR R4, =func LDR PC, [R4] func: ADD R0, R1, R2 BX LR
BTI保护代码
main: LDR R4, =func LDR PC, [R4] ; BTI setting func: BTI ; BTI clearing ADD R0, R1, R2 BX LR
非页函数
这个例子中,函数返回地址被PAC保护,并且使用PACBTI指令保护函数的入口,这样我们可以保证攻击者不能跳到函数体中间。
func: PUSH {R4-R6, LR} ... ; Function body POP {R4-R6, LR} BX LR
PAC和BTI保护的代码
func: PACBTI R12, LR, SP PUSH {R4-R6, R12, LR} ... ; Function body POP {R4-R6, R12, LR} BXAUT R12, LR, SP
Branch future
Branch future序列指令被设计用来通知处理器有一个将要到来的跳转。 如果BTI使能了,BFLX指令将隐式地设置LO_BRANCH_INFO.BTI为1. 当执行到BF 跳转点,隐式跳转时,如果LO_BRANCH_INFO cache有效,处理器会自动设置EPSR.B。因为LO_BRANCH_INFO cache 可能在异常时被清, BFLX不能直接更新EPSR.B。
main: LDR R4, =func BFLX call, R4 ... call: ; Implicit call to func ; ; Fallback code BLX R4 ; Call func ... func: ... ; Function body BX LR
main: LDR R4, =func BFLX call, R4 ... call: ; BTI setting ; Implicit call to func ; ; Fallback code BLX R4 ; BTI setting ... func: PACBTI R12, LR, SP ... ; Function body BXAUT R12, LR, SP
M-profile和A-Profile BTI的比较
BTI和PAC扩展可以独立实现
这个功能在armv8.5必须实现
PACBTI 是Armv8.1-M的可选功能.
执行非直接跳转时,跳转类型记录在PSTATE.BTYPE中
只有特定的非直接跳转指令,BTI setting指令,才设置EPSR.B
不能直接读写PSTATE.BTYPE
EPSR.B可以通过MSR,MRS指令被特权级软件和特权级调试器访问
构架区分用作函数调用,非函数调用比如case-statement的跳转。 一个通用的”all” BTYPE也是允许的
构架不区分各个BTI setting指令
在translation table里使用GP bit,支持为每页使能landing pad
内存为物理映射,BTI只通过EPSR.B控制
BTI在non-guarded page中当成NOP
BTI clearing 指令总是清EPSR.B, 不管当前安全和特权状态的BTI配置
违反BTI规则对一个guarded page内存访问会导致Branch Target Exception
当一个非BTI clearing指令被预取时,违反BTI规则会导致INVSTATE UsageFault 异常
PAC耗费几个时钟周期?
构架没规定,取决于PAC算法和微构架实现
多谢,是厂家自己实现算法是吧。
有没有考虑过从编译器上做类似windows GS的防御机制,防御ROP攻击?