一条 PACIA 指令只能说明一次认证码计算,解释不了进程切换后为何仍能验证指针。XNU 中的指针认证(Pointer Authentication)还依赖启动配置、共享区域状态、线程调度、异常往返以及受控辅助接口,共同维护签名时与验证时的上下文。
这里围绕 2021 年 iPhone 12 内核分析中的状态流展开,以 Apple XNU 7195.60.75 的实现和设计文档交叉核对。样本的完整构建标识缺失,因此偏移、私有控制位和启动常量只属于所示路径;公开源码中的快慢切换分支也应与样本分开,而不是拼成所有设备共有的一条指令流。
三类值分别回答三个问题
| 对象 | 回答的问题 | 需要避免的混淆 |
|---|---|---|
APIAKey / APIBKey / APDAKey / APDBKey 等寄存器 |
本次操作选择哪组硬件认证材料? | 高低各部分的写入不等于完整硬件派生说明 |
jop_pid / rop_pid / srk_jop_key 等软件字段 |
当前任务或共享区域需要什么上下文? | CPU 缓存副本不是额外一种 PAC 指令键 |
| modifier / discriminator | 这次签名允许在哪个用途或存储位置使用? | 它不是完整认证密钥 |
固定软件常量不等于所有实际签名材料已知。Apple 的固定版本设计文档描述了可选实现中的每次启动 diversifier,以及区分内核和用户态的 KERNKey;即使架构寄存器写入常量,也还存在其他参与认证的上下文。具体芯片行为仍以适用实现为准。
启动常量要按写入顺序解释
| 寄存器组 | 低部分 | 高部分 |
|---|---|---|
| APIBKey | K |
K+1 |
| APDBKey | K+2 |
K+3 |
| 私有寄存器组 | K+4 |
K+5 |
| APIAKey | K+6 |
K+7 |
| APDAKey | K+8 |
K+9 |
| APGAKey | K+10 |
K+11 |
中间两个私有寄存器的完整语义未确认,因此不根据连续数值给它们强行命名。K+6 等于 0xFEEDFACEFEEDFAD5,这也是后续路径中出现的默认软件状态值。
对 S3_4_C15_C0_4 的 AND #0x2 测试 bit 1,而 ORR #0x1 与 ORR #0x4 设置 bit 0、bit 2。位编号从零开始;“第三个位”与“bit 3”混用,会改变整个控制条件。
0x3454593d | 0x40000000 = 0x7454593d,其中增设的 bit 30 对应 SCTLR_EL1.EnIB。这只能说明该写入的效果,其他认证类型还可能由后续路径或平台扩展控制,不应据此宣称它们永久关闭。
共享区域先复用,再决定新值
shared_region_key_alloc 按 shared_region_id 查找软件条目,命中时复用并增加引用计数。要求继承的调用还需要核对保存值与 inherited_key 一致;这与新建条目的值选择是两个不同分支。
| 条件 | 保存值 |
|---|---|
未启用 diversify_user_jop |
默认 0xFEEDFACEFEEDFAD5 |
| 启用且要求继承 | inherited_key |
| 启用、不继承、标识非空 | 反复取随机值直到非零 |
| 其余所示默认路径 | 保留默认值 |
条目步长 0x28 包含队列链接、标识指针、64 位软件值与引用计数相关存储,但并非跨版本 ABI。共享区域的作用是允许符合条件的进程共享相容的签名上下文,不是向普通用户进程公开内核寄存器。
样本中的固定 modifier 0x2ABE、0x9BF6 可作为追踪间接调用认证上下文的线索。仅发现重复常量,不足以证明认证碰撞或随机源可预测;还应核对签名方案、目标指针和可达调用路径。
jophash 只覆盖明确送入链条的字段
h = PACGA(pc, state_address)
h = PACGA(cpsr & 0xFFFFFFFFDFFFFFFF, h)
h = PACGA(lr, h)
h = PACGA(x16, h)
h = PACGA(x17, h)
state.jophash = h这是 PACGA 的操作数依赖,不是它的密码学实现。PC 与状态结构地址先进入链条,随后加入 CPSR、LR、X16 和 X17;其他字段没有因为位于同一块结构内就自动获得这个摘要的保护。
CPSR 掩码清除 bit 29,也就是 carry flag。固定版本源码的签名与验证宏都执行同一处理,以便系统调用返回路径调整该标志时不必重新签名。忽略这个掩码,会错误地期待 carry 单独变化触发失配。
| 偏移 | 名称 | 类型 | 大小 |
|---|---|---|---|
| 0x00 | 填充 | 0x68 | |
| 0x68 | lr | 8 | |
| 0x70 | 填充 | 8 | |
| 0x78 | pc | 8 | |
| 0x80 | cpsr | 4 | |
| 0x84 | 填充 | 4 | |
| 0x88 | jophash | 8 |
上面只列出样本已识别字段;空隙不补成完整结构定义。载入线程时,ml_check_kernel_signed_state 用相同字段顺序与状态地址重算并比较。machine_stack_attach 对新建内核栈的初始状态进行签名,之后的切换才有合法值可检查。
修改已签名状态也不是“写字段再重签”这么简单。公开设计要求先关中断,再加载和验证敏感字段,在寄存器内完成修改、重签及回存,最后恢复中断状态;中途意外落栈会重新引入内存修改窗口。
调度与异常往返不是同一次换键
| 路径 | 样本中的主要操作 | 对照源码的边界 |
|---|---|---|
| 调度到新线程 | 比较软件值与 CPU 缓存,按需更新 B 组 | rop_pid 对应状态与缓存一起跟踪 |
| 用户态进入内核 | 准备内核侧 A 组上下文并签名保存状态 | 具体寄存器取决于快慢 A-key 切换路径 |
| 返回用户态 | 先验证保存状态,再恢复用户侧上下文 | 不与内核内部异常返回混用 |
B 组写入按 value、value+1、value+2、value+3 分别填入 APIB 与 APDB 的高低部分;缓存相同时跳过冗余写入。样本异常路径展示直接写 A 组寄存器,但固定版本的 REPROGRAM_JOP_KEYS 还包含快路径,经 KERNKey 寄存器切换。两种实现有配置条件,并非每次异常都要连续重写所有架构键。
签名保存状态与验证保存状态必须使用配对的上下文。把调度、用户异常和内核异常压成一个“更新 PAC key”的步骤,会掩盖签名究竟在何种执行状态下产生。
辅助函数中的指令不等于入口接受范围
pmap_sign_user_ptr 在受控窗口内保存中断和认证上下文,选择用户侧状态,执行操作后恢复。样本的控制位操作是清除或设置私有寄存器 bit 2,不是 bit 1;该位与用户/内核签名上下文的关系应结合实现理解,而非仅凭函数名称作“锁定位”。
签名分派接受编号 0 的 PACIA 与编号 2 的 PACDA。认证汇编辅助函数中还可能出现 AUTIB、AUTDB,但出现指令不等于上层接口接受对应键。核对的 XNU 7195.60.75 pmap_auth_user_ptr_internal 在进入辅助函数前,仅接受 ptrauth_key_asia 与 ptrauth_key_asda。
因此,判断接口能力要从参数检查一路跟到调用目标。对其他历史构建中 B 组分支的可达性,需单独核对入口约束;也不要因为看到 B 组认证,就推断该调用刚刚重写了 B 组寄存器。
ABI 别名不保证所有进程永远共享值
| ABI 名称 | 基础键 | 编号 |
|---|---|---|
ptrauth_key_process_independent_code |
ptrauth_key_asia |
0 |
ptrauth_key_process_dependent_code |
ptrauth_key_asib |
1 |
ptrauth_key_process_independent_data |
ptrauth_key_asda |
2 |
ptrauth_key_process_dependent_data |
ptrauth_key_asdb |
3 |
A 对应 process-independent 别名,B 对应 process-dependent 别名;某个辅助函数只更新 A 组并不会交换这些定义。C 函数指针与 C++ vtable 指针分别使用相应 A 组代码键和数据键;返回地址则属于 B 组代码键用途。
这些名称表达 ABI 用途,不是“任意进程间相同数值”的无条件承诺。共享区域、进程继承和平台实现仍影响实际值;Clang 的当前说明也明确提示跨进程性质存在平台边界。签名方案除了键名,还要包含 discriminator 及地址参与规则。
离线验证只覆盖算术和依赖关系
下列模型把 PACGA 表示成嵌套元组,只追踪输入依赖,既不生成真实 PAC,也不证明密码学上的无碰撞。模型中的地址和随机输出均为示例值;条目选择只模拟新建分支,不包含队列复用或并发。
MASK64 = (1 << 64) - 1
K = 0xFEEDFACEFEEDFACF
values = [(K + i) & MASK64 for i in range(12)]
assert values[6] == 0xFEEDFACEFEEDFAD5
assert values[11] == 0xFEEDFACEFEEDFADA
assert (0x3454593D | 0x40000000) == 0x7454593D
assert [i for i in (0, 1, 2) if ((1 | 4) >> i) & 1] == [0, 2]
CARRY = 1 << 29
assert MASK64 & ~CARRY == 0xFFFFFFFFDFFFFFFF
def transcript(address, pc, cpsr, lr, x16, x17):
h = ("PACGA", pc, address)
for value in (cpsr & ~CARRY, lr, x16, x17):
h = ("PACGA", value, h)
return h
state = [0x1000, 0x2000, 0x40000000, 0x3000, 4, 5]
baseline = transcript(*state)
for i in range(6):
changed = state.copy()
changed[i] ^= 1
assert transcript(*changed) != baseline
changed = state.copy()
changed[2] ^= CARRY
assert transcript(*changed) == baseline
def fresh_value(diversify, inherit, identifier, inherited, random_values):
if not diversify: return values[6]
if inherit: return inherited
if not identifier: return values[6]
return next(x for x in random_values if x != 0)
assert fresh_value(False, True, "example", 7, iter(())) == values[6]
assert fresh_value(True, True, "example", 7, iter(())) == 7
assert fresh_value(True, False, "", 7, iter(())) == values[6]
assert fresh_value(True, False, "example", 7, iter((0, 0, 9))) == 9
print("PASS: constants; 6 transcript dependencies; carry exclusion; 4 new-entry choices")检查通过:启动常量递增、6 个依赖输入、carry 排除和 4 类新建条目选择。它帮助核对“哪些值参与”和“何时选择什么状态”,不替代 arm64e 硬件执行。
审阅 PAC 状态流时,始终把架构寄存器、软件保存值、认证方案和调用可达性分开。只有证明签名与验证使用相容的字段、地址与执行上下文,才算解释了一条完整的认证路径。
官方参考
固定版本源码说明当时公开的设计与实现;Clang 和 Apple 开发文档用于核对术语与 ABI 用途,不用于替换样本的具体偏移。