Jul 02 2026 Off 摘要 ARM64 的指针认证通过 PACIA/PACIB 指令为返回地址和函数指针附加密码学签名,阻断了传统 ROP/JOP 攻击。然而,PAC 的密钥分域——指令地址用 A-key,数据地址用 B-key——留下了裂隙。攻击者可以在 JavaScript JIT 编译过程中喷射精心构造的 shellcode,利用 JIT 引擎生成混合了返回指令序列的代码,在指令域签名却在数据域校验,从而绕过 PAC 验证。 ARM64 PAC密钥分域与签名 PAC 指令与密钥ARMv8.3-A 引入了指针认证(PAC),使用基于 QARMA 的轻量级 MAC 算法为 64 位指针生成认证码,嵌入指针的高位。核心指令:PACIA Xd, Xn:使用指令 A-key 对 Xn 生成签名,存储到 Xd。PACIB Xd, Xn:使用指令 B-key 对 Xn 签名。AUTIA Xd, Xn:验证 A-key 签名,若失败则将 Xd 设为无效指针。AUTIB Xd, Xn:验证 B-key 签名。A-key 用于指令地址(如返回地址、函数指针),B-key 用于数据地址(如虚函数表指针)。这构成了密钥分域:一个域下生成的签名无法在另一个域下通过验证。密钥分域安全分域设计假设攻击者无法同时控制指令域和数据域的签名生成。但在 JIT 环境下,这一假设被打破。JIT 编译器动态生成代码,其中包含大量的函数调用和返回指令序列。如果攻击者能够操控 JIT 生成的代码,在指令域中嵌入特定的返回指令序列,并在数据域中喷射与之对应的指针,就可能制造跨域 PAC 伪造。 JavaScript JIT 喷射的跨域攻击窗口 V8 JIT 的代码生成与返回地址签名V8 的 TurboFan 编译器在生成代码时,对每个函数入口会使用 PACIBSP 对返回地址签名(存储在栈上),在函数返回前使用 AUTIBSP 验证。默认情况下,JIT 生成的代码运行在指令域内,受 A-key 保护。JIT 喷射中的返回指令序列混淆JIT 喷射的核心是构造一个巨大的常量数组(如浮点数组),其中包含精心挑选的指令字节,通过跳转到这些常量的不同偏移位置来执行不同的指令序列。在 ARM64 上,攻击者可以喷射以下混合:一段模拟 AUTIB 失败后不会崩溃的合法代码路径(即不会产生异常,因为 AUTIB 失败通常会将指针置零,导致空指针访问崩溃,但攻击者可以提前在零地址映射内存,使得零地址解引用有效)。使用 B-key 伪造的返回地址。由于 JIT 数据区(常量池)内的数据地址通常使用 B-key 签名,攻击者可以预先用 PACIB 对目标返回地址签名,然后将签名后的指针嵌入数据区。当 JIT 代码加载这个伪造的返回地址并执行 RET 时,由于硬件自动用 B-key 验证(AUTIB),验证通过,CPU 即跳转到攻击者指定的代码。这一跨域过程本质上是:在指令域(A-key)生成代码,将 B-key 签名的伪造返回地址带入控制流,完成从 A-key 到 B-key 的信任转移。 从JavaScript到跨域PAC伪造 构造恶意 JIT 函数攻击者需要构造一个特殊的 JavaScript 函数,使其 JIT 编译后生成如下模式的代码:读取常量池中的一个 64 位值(即伪造的返回地址)。将该值写入 Link Register (X30) 或直接作为返回地址压栈。执行返回指令 RET。这可以通过控制 JIT 优化来实现,例如使用 try/catch、生成特定形状的对象等,诱导 TurboFan 发射特定的加载/返回序列。跨域签名伪造的数据准备在 JavaScript 中,利用 ArrayBuffer 和 Float64Array 可以精确控制二进制数据。攻击者通过预先计算 B-key 签名的有效载荷地址(例如目标 shellcode 的地址,该地址已经用 B-key 签名),并将其作为常量写入数组。随后诱导 JIT 将这组常量编译为只读数据段,并将对应的机器码生成到指令段。利用零地址映射应对验证失败由于 ARM64 上 AUTIB 失败会导致 X30 被置为零(在某些实现中为 -1),如果 CPU 跳转到零地址,通常会导致崩溃。攻击者可以通过在 JavaScript 中分配大型 ArrayBuffer 并利用内存碎片,在地址 0 附近映射一页,写入 NOP sled 和最终的 shellcode,从而安全接管执行流。 核心利用代码 ARM64 汇编级别的跨域签名伪造演示 // 假设 X1 指向数据段,其中包含使用 B-key 签名的目标地址 // 攻击者预先用 PACIB 计算好的值:target_b_signed LDR X30, [X1, #0] // 将伪造的返回地址加载到链接寄存器 AUTIB X30, SP // 使用 B-key 验证(与 SP 作为上下文) CBNZ X30, skip_trap // 如果验证成功(X30 != 0),跳转 // 如果失败,X30 为零,执行零地址 shellcode(攻击者已映射) MOV X0, #0 BR X0 // 跳转到零地址(已部署 payload) skip_trap: RET // 正常返回,实际跳转到攻击者期望的目标 在实际攻击中,攻击者会确保 target_b_signed 是用 B-key 正确签名的一个合法代码地址,这样 AUTIB 成功,直接执行 RET 跳转到 shellcode。JavaScript 侧构造 JIT 常量喷射 // 构造一个包含 B-key 签名地址的 Float64Array // 假设 target_addr 是 shellcode 的地址,事先用 PACIB 签名(通过原生函数计算) // 这里仅展示概念,实际签名需要通过外部计算(例如预先在原生代码中计算好) functiongenBKeySignedAddress(targetAddr, modifier) { // 模拟 PACIB 计算(实际硬件相关,这里用伪码) // 实际攻击中,攻击者在编译时计算好签名值 return targetAddr ^ (modifier & 0xFFFF000000000000); // 简化 } const TARGET_ADDR = 0x1234567800n; // shellcode 地址 const MODIFIER = 0xabcdef0000000000n; const signed = genBKeySignedAddress(TARGET_ADDR, MODIFIER); // 将 64 位值表示为两个 32 位值放入 ArrayBuffer const buf = newArrayBuffer(8); const view = new BigInt64Array(buf); view[0] = signed; // 创建一个函数,强制 JIT 优化并包含该常量 functionjitSpray(arr) { let x = arr[0]; // 诱导 JIT 生成 "LDR X0, [常量]; RET" 的代码 // ... 经过一系列优化后,JIT 可能内联常量并执行 RET } V8 引擎 Patch 以模拟零地址 shellcode在真实环境中,需在 V8 启动时预先分配 0 地址内存(通过 mmap 或 VirtualAlloc),并写入 shellcode。攻击者在获得代码执行后,可通过漏洞(如 JIT 内存保护绕过)将 shellcode 写入零页面,然后触发上述跨域 PAC 绕过。 防御 细粒度 PAC 密钥管理:限制 B-key 在用户态的使用,对 JIT 生成的数据段使用不同于标准 B-key 的派生密钥,使攻击者难以预计算签名。JIT 常量隔离:将 JIT 生成的常量数据放置于不可执行页,并禁止在这些页中使用 B-key 签名。控制流完整性(CFI)补充:配合 Clang CFI,检查间接分支目标是否合法。硬件更新:未来的 ARMv9 可能引入更细粒度的 PAC 密钥(如每个 EL 级别独立密钥),缩减跨域攻击面。 结语 PAC 并非无懈可击。JavaScript JIT 引擎的动态代码生成能力,为攻击者提供了跨越密钥域的桥梁。通过巧妙地喷射 B-key 签名的返回地址,并配合零地址映射,攻击者能够打破指令与数据之间的密码学壁垒。这一攻击手法再次印证了:在复杂的系统软件栈中,安全特性的设计假设需覆盖从高级语言到微架构的所有层级,任何未预见的交互都可能滋生新的攻击面。对于防御者,除了依赖硬件特性,还需要在编译器、运行时和操作系统层面构建纵深防御,方能封堵这不断涌现的 PAC 绕过新维度。 Post navigation Previous PostPrevious Mojo IPC 的序列化陷阱Next PostNext 单向链表缺陷与 AuthzBasep SecurityDescriptor 传播攻击