指针认证¶
简介¶
指针认证是一种技术,它提供了强大的概率保护,可以防止利用各种内存错误来控制程序执行。当在语言 ABI 中一致地采用时,它提供了一种相对细粒度的控制流完整性 (CFI) 检查形式,可以抵抗基于返回的编程 (ROP) 和基于跳转的编程 (JOP) 攻击。
虽然指针认证可以在纯软件中实现,但直接硬件支持(例如 Armv8.3 PAuth 提供的)可以显着提高性能和代码大小。同样,虽然指针认证可以在任何架构上实现,但利用 64 位指针的目标的(通常)过多的寻址范围可以最大程度地减少对内存性能的影响,并允许与现有代码互操作(通过动态禁用指针认证)。本文档通常会尝试以与任何硬件实现或 ABI 无关的方式呈现指针认证功能。在整个过程中都会清楚地标识与实现相关的注意事项。
请注意,有多个不同的术语在使用中
指针认证是一种与目标无关的语言技术。
PAuth(有时称为PAC,表示指针认证码)是 AArch64 架构扩展,它为指针认证提供硬件支持。其他扩展要么修改 PAuth 指令行为的某些方面(特别是 FPAC),要么提供新的指令变体(PAuth_LR)。
Armv8.3 是一个 AArch64 架构修订版,它使 PAuth 成为强制性的。
arm64e 是一个特定的 ABI(尚未完全稳定),用于在某些 Apple 操作系统上使用 PAuth 实现指针认证。
本文档有四个目的
它描述了指针认证的基本原理。
它记录了几个在使用指针认证的目标上很有用的语言扩展。
它最终将介绍安全缓解措施的操作理论,描述正确性的基本要求、机制中的各种弱点以及程序员可以加强其保护的方法(包括对语言实现者的建议)。
它最终将记录当前在 arm64e 上用于 C、C++、Objective-C 和 Swift 的语言 ABI,尽管这些 ABI 在任何目标上都尚未稳定。
基本概念¶
对象的简单地址或函数是一个原始指针。原始指针可以被签名以产生一个签名指针。签名指针可以随后被认证以验证它是否被有效签名并提取原始原始指针。这些术语反映了最可能的实现技术:计算和存储一个加密签名以及指针。
一个抽象签名密钥是一个名称,它指的是用于签名和认证指针的密钥。特定名称的具体密钥值在整个进程中保持一致。
一个鉴别器是用于区分签名指针的任意值,以便一个有效签名的指针不能简单地复制到另一个指针。鉴别器只是某种实现定义大小的不透明数据,它作为盐包含在签名中(有关详细信息,请参阅鉴别器)。
几乎所有指针认证的方面都只使用这两个主要操作
sign(raw_pointer, key, discriminator)
在给定原始指针、抽象签名密钥和鉴别器的情况下产生一个签名指针。auth(signed_pointer, key, discriminator)
在给定签名指针、抽象签名密钥和鉴别器的情况下产生一个原始指针。
auth(sign(raw_pointer, key, discriminator), key, discriminator)
必须成功并生成raw_pointer
。应用于以任何其他方式最终产生的值的auth
预计会失败,这将使程序停止运行,或者
立即在强制执行
auth
成功的实现中(例如,当使用编译器生成的auth
失败检查时,或者 Armv8.3 与 FPAC 扩展一起使用时),或者当使用生成的指针值时,在没有强制执行的实现中。
但是,无论实现如何处理auth
失败,auth
允许无法检测到签名指针不是以这种方式生成的,在这种情况下,它可能返回任何内容;这就是使指针认证成为概率缓解而不是完美的缓解的原因。
有两个次要操作,它们仅用于实现<ptrauth.h>
中的某些内联函数
strip(signed_pointer, key)
在给定签名指针和密钥的情况下产生一个原始指针,而不验证其有效性,这与auth
不同。这对于某些类型的工具(例如崩溃回溯)很有用;它通常不应在基本语言 ABI 中使用,除非以非常谨慎的方式使用。sign_generic(value)
为任意数据(不一定是指针)生成加密签名。这对于有效地验证非指针数据是否已被篡改很有用。
只要调用任何这些操作,密钥值都必须在静态地已知。这是因为签名指针的布局可能会根据签名密钥而有所不同。(例如,在 Armv8.3 中,签名指针的布局取决于 TBI(顶部字节忽略)是否已启用,这可以针对 I 和 D 密钥独立设置。)
API 设计人员和语言实现者的注意事项
这些是指针认证的基本操作,为了描述清楚而提供。它们既不适合作为高级接口,也不适合作为编译器 IR 中的基本元素,因为它们会公开原始指针。原始指针需要在语言实现中特别注意,以避免意外创建可利用的代码序列。
以下细节都是实现定义的
签名指针的性质
鉴别器的大小
签名密钥的数量和性质
sign
、auth
、strip
和sign_generic
操作的实现
虽然使用“签名”和“签名指针”这两个术语暗示了加密签名的使用,但其他实现可能是可能的。有关实现选项的探讨,请参阅替代实现。
实现示例:Armv8.3
读者可能会发现了解这些术语如何映射到 Armv8.3 PAuth 会很有帮助
签名指针是指针,其签名存储在否则未使用的最高位中。内核根据系统的寻址需求配置地址宽度,并在需要时为 I 或 D 密钥启用 TBI。地址位以上和 TBI 位以下(如果启用)的位未使用。然后,签名宽度取决于此寻址配置。
鉴别器是一个 64 位整数。常量鉴别器是 16 位整数。将常量鉴别器混合到地址中包括用常量替换包含地址的指针的最高 16 位。用于混合目的的指针应该只有地址位,因为更高的位将至少部分被常量鉴别器覆盖。
有五个 128 位签名密钥寄存器,每个寄存器只能由特权代码直接读取或设置。其中四个用于签名指针,第五个仅用于
sign_generic
。密钥数据只是一个添加到哈希中的胡椒粉,而不是一个加密密钥,因此可以使用随机数据对其进行初始化。sign
计算指针、鉴别器和签名密钥的加密哈希,并将结果存储在最高位作为签名。auth
删除签名,计算相同的哈希,并将结果与存储的签名进行比较。strip
删除签名,而不进行认证。虽然aut*
指令本身不会在 Armv8.3 PAuth 中失败时捕获,但它们会在后面的可选 FPAC 扩展中捕获。实现也可以选择通过在aut*
周围发出额外的指令来模拟这种捕获行为。sign_generic
对应于pacga
指令,该指令采用两个 64 位值并生成一个 64 位加密哈希。此指令的实现不需要在结果的所有位中生成有意义的数据。
鉴别器¶
鉴别器是任意额外的用于更改指针的计算签名的数据。当两个指针被不同地签名时——无论是使用不同的密钥还是使用不同的鉴别器——攻击者不能简单地用一个指针替换另一个指针。
为了使用标准的加密术语,鉴别器充当指针签名的盐,密钥数据充当胡椒粉。也就是说,鉴别器和密钥数据最终只是作为输入添加到签名算法中以及指针,但它们起到截然不同的作用。密钥数据是一个添加到每个签名的公共秘密,而鉴别器是一个可以从签名特定指针的上下文中推断出来的值。但是,与密码盐不同,重要的是鉴别器必须从签名的环境独立推断出来;它们永远不应该简单地存储在指针旁边。然后在认证操作中重新推断鉴别器。
在<ptrauth.h>
中的内联函数接口允许提供任意鉴别器值,但只能在运行正常代码时使用。语言 ABI 使用的鉴别器必须受到限制,以使加载程序能够在没有大量元数据的情况下签名存储在全局内存中的指针。在这些限制下,鉴别器可能包含以下一项或两项内容
指针在内存中存储的地址。用包含其存储地址的鉴别符签名的指针被称为具有**地址多样性**。通常,使用地址多样性意味着攻击者无法可靠地将指针复制到或从不同的内存位置复制。但是,攻击者仍然可以通过更改访问指针的地址来攻击更大的调用序列。此外,由于语言或其他限制,某些情况无法使用地址多样性。
一个常量整数,称为**常量鉴别符**。用非零常量鉴别符签名的指针被称为具有**常量多样性**。如果鉴别符特定于单个声明,则它被称为具有**声明多样性**;如果鉴别符特定于一种值类型,则它被称为具有**类型多样性**。例如,arm64e 上的 C++ v-table 使用其方法名称和签名的哈希值对它们的组件函数进行签名,从而提供声明多样性;类似地,C++ 成员函数指针使用成员指针类型的哈希值对它们的调用函数进行签名,从而提供类型多样性。
实现可能需要将常量鉴别符限制为明显小于鉴别符完整大小的值。例如,在 arm64e 上,常量鉴别符只有 16 位值。据信这不会显著削弱缓解措施,因为碰撞仍然很少见。
将常量鉴别符与存储地址混合的算法是实现定义的。
签名方案¶
正确使用指针认证要求签名代码和认证代码就指针的**签名方案**达成一致
用于签署指针的抽象签名密钥,以及
计算鉴别符的算法。
如上文关于鉴别符一节所述,在大多数情况下,鉴别符是通过获取常量鉴别符并将其可选地与指针的存储地址混合来生成的。在这些情况下,签名方案变得更加简单
抽象签名密钥,
常量鉴别符,以及
是否使用地址多样性。
重要的是,签名方案应在所有签名和认证站点独立推导出来。最好在所有需要的地方对方案进行硬编码,但至少不能通过检查与指针一起存储的信息来推导出方案。
语言特性¶
目前主要有一个指针认证语言特性
该语言提供了
<ptrauth.h>
本征接口,用于在代码中手动签名和认证指针。这些可以在需要非常具体行为的情况下使用。
语言扩展¶
特性测试¶
可以使用多种不同的测试来测试当前目标是否使用指针认证。
__has_feature(ptrauth_intrinsics)
如果<ptrauth.h>
提供其正常接口,则为真。即使在默认情况下未启用指针认证的目标上,这也可能是真的。
<ptrauth.h>
¶
此头文件定义了以下类型和操作
ptrauth_key
¶
此enum
是抽象签名密钥的类型。除了定义一组特定于实现的签名密钥(例如,Armv8.3 定义了ptrauth_key_asia
)之外,它还为这些密钥定义了一些可移植的别名。例如,ptrauth_key_function_pointer
通常用于 C 函数指针的密钥,这通常适用于其他函数签名方案。
在下面所有操作描述中,密钥值必须是与来自此 enum
的特定于实现的抽象签名密钥之一相对应的常量值。
ptrauth_extra_data_t
¶
这是一个 typedef
,它表示标准整数类型,其大小足以容纳鉴别符值。
在下面的签名和认证操作描述中,鉴别符值必须具有指针类型或整数类型。如果鉴别符是整数,它将被强制转换为ptrauth_extra_data_t
。
ptrauth_blend_discriminator
¶
ptrauth_blend_discriminator(pointer, integer)
生成一个鉴别符值,该值混合了来自给定指针和给定整数的信息。
实现可能忽略每个值中的某些位,也就是说,混合算法可以选择速度和方便,而不是作为哈希组合算法的理论强度。例如,arm64e 只用整数的低 16 位覆盖指针的高 16 位,这可以通过使用立即整数的单个指令来完成。
pointer
必须具有指针类型,而 integer
必须具有整数类型。结果的类型为ptrauth_extra_data_t
。
ptrauth_string_discriminator
¶
ptrauth_string_discriminator(string)
从给定字符串计算一个常量鉴别符。
string
必须是char
字符类型的字符串文字。结果的类型为ptrauth_extra_data_t
。
结果值永远不为零,并且始终在__ptrauth
限定符和ptrauth_blend_discriminator
的范围内。
这可以在常量表达式中使用。
ptrauth_strip
¶
ptrauth_strip(signedPointer, key)
鉴于signedPointer
与用给定密钥签名的已签名指针的布局匹配,从中提取原始指针。此操作不会引发陷阱,也不会失败,即使指针未被有效签名。
ptrauth_sign_constant
¶
ptrauth_sign_constant(pointer, key, discriminator)
以一种确保不可攻击的序列的方式返回常量地址的已签名指针。
pointer
必须是评估为非空指针的指针类型的常量表达式。 key
必须是ptrauth_key
类型的常量表达式。 discriminator
必须是指针或整数类型的常量表达式;如果是整数,它将被强制转换为ptrauth_extra_data_t
。结果将与pointer
的类型相同。
这可以在常量表达式中使用。
ptrauth_sign_unauthenticated
¶
ptrauth_sign_unauthenticated(pointer, key, discriminator)
为给定的原始指针生成一个已签名指针,而不会应用任何认证或额外处理。此操作不需要在空指针上具有与语言实现相同的行为。
这是一个危险的操作,很容易导致签名预言机。程序应该很少使用它,并且要谨慎使用。
ptrauth_auth_and_resign
¶
ptrauth_auth_and_resign(pointer, oldKey, oldDiscriminator, newKey, newDiscriminator)
认证pointer
是否是用oldKey
和oldDiscriminator
签名的,然后用newKey
和newDiscriminator
重新签名该认证的原始指针结果。
pointer
必须具有指针类型。结果将与pointer
的类型相同。此操作不需要在空指针上具有与语言实现相同的行为。
为该操作生成的代码序列必须不可直接攻击。但是,如果鉴别符值不是常量整数,则它们的计算可能仍然会受到攻击。将来,Clang 应该得到增强,以保证如果这些表达式是安全推导的,则不可攻击。
ptrauth_auth_data
¶
ptrauth_auth_data(pointer, key, discriminator)
认证pointer
是否是用key
和discriminator
签名的,并删除签名。
pointer
必须具有对象指针类型。结果将与pointer
的类型相同。此操作不需要在空指针上具有与语言实现相同的行为。
将来当 Clang 提供安全推导保证时,此操作的结果应被视为安全推导的。
ptrauth_sign_generic_data
¶
ptrauth_sign_generic_data(value1, value2)
为给定的值对计算一个签名,并结合一个秘密签名密钥。
此操作可用于通过计算数据的签名、存储该签名,然后重复此过程并验证是否产生相同的结果来验证任意数据是否被篡改。 这可以用多种方式合理地完成; 例如,库可以计算数据的普通校验和,并仅对结果进行签名以获得秘密签名密钥的防篡改优势(否则攻击者可以可靠地覆盖数据和校验和)。
value1
和 value2
必须是指针或整数。 如果整数大于 uintptr_t
,则 uintptr_t
中无法表示的数据可能会被丢弃。
结果的类型将为 ptrauth_generic_signature_t
,它是一种整数类型。 实现不需要使结果的所有位都具有同等的重要性; 特别是,一些实现已知不会在低位中留下有意义的数据。
替代实现¶
签名存储¶
指针身份验证的安全性并不取决于签名是否与指针“一起”存储,就像在 Armv8.3 中一样。 实现也可以将签名存储在单独的字中,以便签名指针的 sizeof
大于原始指针的 sizeof
。
将签名存储在高位(如 Armv8.3 所做)有多种权衡
缺点:可用于签名的位数明显更少,削弱了缓解措施,因为攻击者更容易猜测正确的签名。
缺点:地址空间的未来增长必然会进一步削弱缓解措施。
优点:内存布局不会改变,因此支持指针身份验证的代码(例如,在系统库中)可以有效地与现有代码互操作,只要可以动态禁用指针身份验证。
优点:签名指针的大小不会增加,这可能会显着增加内存需求、代码大小和寄存器压力。
优点:签名指针的大小与原始指针相同,因此使用诸如 void *(例如 dlsym)之类的类型的通用 API 仍然可以返回签名指针。 这意味着这些 API 的客户端不需要不安全的代码来正确接收函数指针。
哈希与指针加密¶
Armv8.3 通过计算密码哈希并将该哈希存储在指针的备用位中来实现 sign
。 这意味着有效签名指针的可能值相对较少,因为对应于原始指针的位是已知的。 结合 auth
预言机,这使得通过暴力破解发现正确签名在计算上可行。(实现当然应该努力避免引入 auth
预言机,但这可能很困难,而且攻击者可能很狡猾。)
如果实现可以在 sign
期间加密指针并在 auth
期间解密指针,即使使用 auth
预言机,这种暴力破解攻击也会变得不太可行。 但是,这个想法存在一些问题
目前尚不清楚这种加密是否可能在不增加签名指针的存储大小的情况下实现。 如果存储大小可以增加,则可以通过简单地存储更大的签名来同样有效地缓解暴力破解攻击。
可能无法实现
strip
操作,这可能会使调试器和其他进程外工具更难编写,并且通常会使基本调试更具挑战性。实现可以从能够立即从签名指针中提取原始指针中获益。 执行
auth
和加载指令的 Armv8.3 处理器可以并行执行加载和auth
; 相反,加密指针的处理器将被迫串行执行这些操作。