~/posts/mobile/xnu-arm64-pac-context-state-authentication.md

XNU PAC:上下文切换与线程状态认证

追踪启动常量、共享区域、调度和异常往返中的 PAC 状态,区分硬件键、软件值与 discriminator,核对 jophash 字段、carry 掩码及辅助接口真正接受的键类型。

date[31:24]
read[23:16]
7 分钟
cat[15:8]
移动端

AI 翻译,尚未人工审核

目录
  1. 0x00三类值分别回答三个问题
  2. 0x01启动常量要按写入顺序解释
  3. 0x02共享区域先复用,再决定新值
  4. 0x03jophash 只覆盖明确送入链条的字段
  5. 0x04调度与异常往返不是同一次换键
  6. 0x05辅助函数中的指令不等于入口接受范围
  7. 0x06ABI 别名不保证所有进程永远共享值
  8. 0x07离线验证只覆盖算术和依赖关系
  9. 0x08官方参考

一条 PACIA 指令只能说明一次认证码计算,解释不了进程切换后为何仍能验证指针。XNU 中的指针认证(Pointer Authentication)还依赖启动配置、共享区域状态、线程调度、异常往返以及受控辅助接口,共同维护签名时与验证时的上下文。

这里围绕 2021 年 iPhone 12 内核分析中的状态流展开,以 Apple XNU 7195.60.75 的实现和设计文档交叉核对。样本的完整构建标识缺失,因此偏移、私有控制位和启动常量只属于所示路径;公开源码中的快慢切换分支也应与样本分开,而不是拼成所有设备共有的一条指令流。

三类值分别回答三个问题

寄存器、软件状态与 discriminator3 行
对象 回答的问题 需要避免的混淆
APIAKey / APIBKey / APDAKey / APDBKey 等寄存器 本次操作选择哪组硬件认证材料? 高低各部分的写入不等于完整硬件派生说明
jop_pid / rop_pid / srk_jop_key 等软件字段 当前任务或共享区域需要什么上下文? CPU 缓存副本不是额外一种 PAC 指令键
modifier / discriminator 这次签名允许在哪个用途或存储位置使用? 它不是完整认证密钥

固定软件常量不等于所有实际签名材料已知。Apple 的固定版本设计文档描述了可选实现中的每次启动 diversifier,以及区分内核和用户态的 KERNKey;即使架构寄存器写入常量,也还存在其他参与认证的上下文。具体芯片行为仍以适用实现为准。

启动常量要按写入顺序解释

启动寄存器顺序 / K = 0xFEEDFACEFEEDFACF6 行
寄存器组 低部分 高部分
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 一致;这与新建条目的值选择是两个不同分支。

样本的新建条目选择4 行
条件 保存值
未启用 diversify_user_jop 默认 0xFEEDFACEFEEDFAD5
启用且要求继承 inherited_key
启用、不继承、标识非空 反复取随机值直到非零
其余所示默认路径 保留默认值

条目步长 0x28 包含队列链接、标识指针、64 位软件值与引用计数相关存储,但并非跨版本 ABI。共享区域的作用是允许符合条件的进程共享相容的签名上下文,不是向普通用户进程公开内核寄存器。

样本中的固定 modifier 0x2ABE、0x9BF6 可作为追踪间接调用认证上下文的线索。仅发现重复常量,不足以证明认证碰撞或随机源可预测;还应核对签名方案、目标指针和可达调用路径。

jophash 只覆盖明确送入链条的字段

Saved-state authentication — operand dependenciestext
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 单独变化触发失配。

arm_kernel_saved_state — observed fields onlyarm64 · LE
偏移名称类型大小
0x00填充0x68
0x68lr8
0x70填充8
0x78pc8
0x80cpsr4
0x84填充4
0x88jophash8
大小 = 0x90(144 字节) · 填充 0x74

上面只列出样本已识别字段;空隙不补成完整结构定义。载入线程时,ml_check_kernel_signed_state 用相同字段顺序与状态地址重算并比较。machine_stack_attach 对新建内核栈的初始状态进行签名,之后的切换才有合法值可检查。

修改已签名状态也不是“写字段再重签”这么简单。公开设计要求先关中断,再加载和验证敏感字段,在寄存器内完成修改、重签及回存,最后恢复中断状态;中途意外落栈会重新引入内存修改窗口。

调度与异常往返不是同一次换键

两类切换的观察重点3 行
路径 样本中的主要操作 对照源码的边界
调度到新线程 比较软件值与 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 别名4 行
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,也不证明密码学上的无碰撞。模型中的地址和随机输出均为示例值;条目选择只模拟新建分支,不包含队列复用或并发。

pac-state-model.pypython
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 用途,不用于替换样本的具体偏移。

NORMAL~/posts/mobile/xnu-arm64-pac-context-state-authentication.md§--
0%zh-CN