iOS 利用
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
iOS 漏洞缓解措施
1. Code Signing / 运行时签名验证
早期引入(iPhone OS → iOS) 这是基本的保护之一:所有可执行代码(apps、dynamic libraries、JIT-ed code、extensions、frameworks、caches)必须由以 Apple 信任根为根的证书链进行加密签名。在运行时,在将二进制加载到内存(或在跨某些边界执行跳转)之前,系统会检查其签名。如果代码被修改(位翻转、打补丁)或未签名,加载会失败。
- 阻止:利用链中经典的 “payload drop + execute” 阶段;任意代码注入;修改现有二进制以插入恶意逻辑。
- 机制细节:
- Mach-O loader(和 dynamic linker)会检查代码页、段、entitlements、team IDs,并验证签名覆盖文件内容。
- 对于像 JIT 缓存或动态生成的代码这样的内存区域,Apple 强制要求页面被签名或通过特殊 API 验证(例如带有 code-sign 检查的
mprotect)。 - 签名包含 entitlements 和标识符;OS 强制要求某些 API 或特权能力需要特定的 entitlements,且这些不能伪造。
Example
假设某个 exploit 在进程中获得代码执行并尝试把 shellcode 写到 heap 并跳转到它。在 iOS 上,该页面需要被标记为可执行 **并且** 满足 code-signature 约束。由于 shellcode 没有用 Apple 的证书签名,跳转会失败或系统会拒绝将该内存区域设为可执行。2. CoreTrust
在 iOS 14+ 时代左右引入(或在更新设备 / 更高 iOS 版本逐步启用) CoreTrust 是执行二进制(包括系统和用户二进制)运行时签名验证的子系统,它针对的是 Apple 的根证书,而不是依赖于本地的用户态信任存储副本。
- 阻止:安装后篡改二进制、试图交换或打补丁 system libraries 或用户应用以实现越狱的技术;通过替换受信任二进制来欺骗系统。
- 机制细节:
- 它不是信任本地的 trust database 或证书缓存,而是直接引用或验证与 Apple 根相关的证书链。
- 它确保检测并拒绝对现有二进制(例如文件系统中的)进行的修改。
- 它在加载时将 entitlements、team IDs、code signing 标志和其他元数据绑定到二进制上。
Example
越狱可能尝试用已打补丁的版本替换 `SpringBoard` 或 `libsystem` 以获取持久性。但当 OS 的 loader 或 CoreTrust 检查时,会发现签名不匹配(或 entitlements 被修改),并拒绝执行。3. Data Execution Prevention (DEP / NX / W^X)
在许多 OS 较早引入;iOS 长期支持 NX-bit / w^x DEP 强制将标记为可写(用于数据)的页面设为 不可执行,而标记为可执行的页面为 不可写。你不能简单地把 shellcode 写入 heap 或 stack 区域然后执行它。
- 阻止:直接执行 shellcode;经典的 buffer-overflow → 跳转到注入 shellcode。
- 机制细节:
- MMU / memory protection flags(通过页表)强制实施这种分离。
- 任何尝试将可写页面标记为可执行都会触发系统检查(要么被禁止,要么需要 code-sign 批准)。
- 在许多情况下,使页面可执行需要通过强制额外约束或检查的 OS API。
Example
一次溢出把 shellcode 写到 heap。攻击者尝试 `mprotect(heap_addr, size, PROT_EXEC)` 使其可执行。但系统会拒绝或验证新页面必须通过 code-sign 约束(shellcode 无法满足)。4. Address Space Layout Randomization (ASLR)
大约在 iOS 4–5 时代引入 ASLR 在每次进程启动时随机化关键内存区域的基地址:libraries、heap、stack 等。gadgets 的地址在不同运行间会变化。
- 阻止:为 ROP/JOP 硬编码 gadget 地址;静态 exploit 链;盲目跳转到已知偏移。
- 机制细节:
- 每个加载的 library / dynamic module 都会以随机偏移重定位。
- stack 和 heap 的基指针被随机化(在一定熵范围内)。
- 有时其它区域(例如 mmap 分配)也会被随机化。
- 与 information-leak 缓解措施结合,迫使攻击者首先 leak 一个地址或指针以在运行时发现基地址。
Example
一个 ROP 链期望 gadget 在 `0x….lib + offset`。但由于 `lib` 每次运行都被重新定位,硬编码的链会失败。exploit 必须先 leak 模块的基地址,然后再计算 gadget 地址。5. Kernel Address Space Layout Randomization (KASLR)
在 iOS 大约(iOS 5 / iOS 6 时期)引入 类似于用户态 ASLR,KASLR 在启动时随机化 kernel text 和其它内核结构的基址。
- 阻止:依赖固定内核代码或数据位置的内核级 exploit;静态内核 exploit。
- 机制细节:
- 每次启动时,内核的基址在一定范围内随机化。
- 内核数据结构(如
task_structs、vm_map等)也可能被重定位或偏移。 - 攻击者必须先 leak 内核指针或利用信息披露漏洞来计算偏移,才能劫持内核结构或代码。
Example
一个本地漏洞旨在破坏某个内核函数指针(例如在 `vtable` 中)位于 `KERN_BASE + offset`。但由于 `KERN_BASE` 未知,攻击者必须先 leak 它(例如通过读原语),然后再计算正确的破坏地址。6. Kernel Patch Protection (KPP / AMCC)
在更新的 iOS / A 系列硬件(大约 iOS 15–16 之后或更新芯片)引入 KPP(又名 AMCC)持续监控内核文本页的完整性(通过 hash 或 checksum)。如果检测到在允许窗口外的篡改(补丁、inline hooks、代码修改),它会触发内核 panic 或重启。
- 阻止:持久内核打补丁(修改内核指令)、inline hooks、静态函数覆盖。
- 机制细节:
- 一个硬件或固件模块监控内核文本区域。
- 它周期性或按需重新哈希这些页面并与期望值比较。
- 如果在非良性更新窗口出现不匹配,它会使设备 panic(以避免持久恶意补丁)。
- 攻击者必须避免检测窗口或使用合法的补丁路径。
Example
一个 exploit 试图补丁内核函数的 prologue(例如 `memcmp`)以截获调用。但 KPP 会注意到代码页的 hash 不再匹配预期值并触发 kernel panic,在补丁稳定前就让设备崩溃。7. Kernel Text Read‐Only Region (KTRR)
在现代 SoC(大约 A12 / 更新硬件之后)引入 KTRR 是硬件强制机制:在引导早期将内核文本锁定后,它从 EL1(内核)变为只读,阻止对代码页的进一步写入。
- 阻止:启动后对内核代码的任何修改(例如打补丁、就地注入代码)在 EL1 特权级别上。
- 机制细节:
- 在引导期间(secure/bootloader 阶段),内存控制器(或安全硬件单元)将包含内核文本的物理页面标记为只读。
- 即使 exploit 获得了完整的内核权限,也无法写入这些页面以打补丁指令。
- 要修改它们,攻击者必须先破坏引导链,或破坏 KTRR 本身。
Example
一个权限提升 exploit 跳入 EL1 并尝试把 trampoline 写入内核函数(例如在 `syscall` handler 中)。但因为这些页面已被 KTRR 锁定为只读,写入会失败(或触发 fault),所以补丁无法应用。8. Pointer Authentication Codes (PAC)
随 ARMv8.3(硬件)引入,Apple 自 A12 / iOS ~12+ 开始采用
- PAC 是 ARMv8.3-A 引入的硬件特性,用于检测指针值(返回地址、函数指针、某些数据指针)被篡改,通过在指针的未使用高位中嵌入一个小型加密签名(“MAC”)来实现。
- 该签名(“PAC”)是对指针值加上一个 modifier(上下文值,例如 stack pointer 或某些区分数据)计算得出。这样在不同上下文中相同的指针值会产生不同的 PAC。
- 在使用时,在解引用或通过该指针分支之前,会执行一个 authenticate 指令检查 PAC。如果有效,PAC 被剥离并得到纯指针;如果无效,指针会被“poison”或触发 fault。
- 用于生成/验证 PAC 的密钥保存在特权寄存器(EL1、kernel)中,用户态无法直接读取。
- 由于许多系统的指针并未使用全部 64 位(例如 48 位地址空间),上位比特是“空闲”的,可以存放 PAC 而不改变实际地址。
架构基础 & 密钥类型
-
ARMv8.3 引入了 五个 128-bit 密钥(每个通过两个 64-bit 系统寄存器实现)用于 pointer authentication。
-
APIAKey — 用于 instruction pointers(域 “I”,key A)
-
APIBKey — 第二个 instruction pointer key(域 “I”,key B)
-
APDAKey — 用于 data pointers(域 “D”,key A)
-
APDBKey — 用于 data pointers(域 “D”,key B)
-
APGAKey — “generic” key,用于对非指针数据或其它通用用途进行签名
-
这些密钥存储在特权系统寄存器中(仅在 EL1/EL2 等可访问),用户态不可访问。
-
PAC 是通过一个加密函数计算的(ARM 建议使用 QARMA 作为算法),使用:
- 指针值(规范化部分)
- 一个 modifier(上下文值,如 salt)
- 秘密密钥
- 一些内部 tweak 逻辑 如果生成的 PAC 与存储在指针高位的值匹配,则认证成功。
指令族
命名约定为:PAC / AUT / XPAC,然后跟域字母。
PACxx指令对指针进行 签名 并插入 PACAUTxx指令 认证并剥离(验证并移除 PAC)XPACxx指令 剥离 且不验证
域 / 后缀:
| Mnemonic | 含义 / 域 | Key / 域 | 汇编中的示例用法 |
|---|---|---|---|
| PACIA | 使用 APIAKey 对指令指针进行签名 | “I, A” | PACIA X0, X1 — 使用 APIAKey 和 modifier X1 对 X0 中的指针签名 |
| PACIB | 使用 APIBKey 对指令指针进行签名 | “I, B” | PACIB X2, X3 |
| PACDA | 使用 APDAKey 对数据指针进行签名 | “D, A” | PACDA X4, X5 |
| PACDB | 使用 APDBKey 对数据指针进行签名 | “D, B” | PACDB X6, X7 |
| PACG / PACGA | 使用 APGAKey 对通用(非指针)数据签名 | “G” | PACGA X8, X9, X10(用 X10 作为 modifier 对 X9 签名,结果放入 X8) |
| AUTIA | 认证并剥离 APIA 签名的指令指针 | “I, A” | AUTIA X0, X1 — 使用 modifier X1 检查 X0 上的 PAC,然后剥离 |
| AUTIB | 认证 APIB 域 | “I, B” | AUTIB X2, X3 |
| AUTDA | 认证 APDA 签名的数据指针 | “D, A” | AUTDA X4, X5 |
| AUTDB | 认证 APDB 签名的数据指针 | “D, B” | AUTDB X6, X7 |
| AUTGA | 认证通用 / blob(APGA) | “G” | AUTGA X8, X9, X10(验证通用数据) |
| XPACI | 剥离 PAC(指令指针,不验证) | “I” | XPACI X0 — 从 X0 中移除指令域的 PAC |
| XPACD | 剥离 PAC(数据指针,不验证) | “D” | XPACD X4 — 从 X4 中移除数据指针的 PAC |
存在一些专用 / 别名形式:
PACIASP是PACIA X30, SP的简写(使用 SP 作为 modifier 对 link register 签名)AUTIASP是AUTIA X30, SP(使用 SP 认证 link register)- 还有组合形式如
RETAA、RETAB(认证后返回)或BLRAA(认证并分支),这些存在于 ARM 扩展 / 编译器支持中。 - 还有零 modifier 变体:
PACIZA/PACIZB,其中 modifier 隐式为零等。
Modifiers
modifier 的主要目标是将 PAC 绑定到特定上下文,因此在不同上下文中对相同地址的签名会产生不同的 PAC。这防止了跨帧或跨对象简单地重用指针。类似于对 hash 加盐。
因此:
- modifier 是一个上下文值(另一个寄存器),在 PAC 计算时被混入。典型选择:stack pointer(SP)、frame pointer 或某个对象 ID。
- 使用 SP 作为 modifier 常见于返回地址签名:PAC 被绑定到具体的栈帧。如果你尝试在不同的帧中重用 LR,modifier 会变化,PAC 验证失败。
- 相同的指针值在不同 modifier 下签名会产生不同的 PAC。
- modifier 不需要是保密的,但理想情况下不由攻击者控制。
- 对于那些没有有意义 modifier 的签名/验证指令,有些形式使用零或隐式常量。
Apple / iOS / XNU 的自定义与观察
- Apple 的 PAC 实现包含 每次启动的 diversifiers,使得密钥或 tweak 在每次启动时改变,防止跨启动重用。
- 他们还包含 跨域缓解,使得在 user mode 签名的 PAC 不容易被在 kernel mode 重用等。
- 在 Apple M1 / Apple Silicon 上的逆向显示,有 九种 modifier 类型 和 Apple 特定的系统寄存器用于密钥控制。
- Apple 在许多内核子系统中使用 PAC:返回地址签名、内核数据指针完整性、签名的线程上下文等。
- Google Project Zero 显示,在强大的内核内存读/写 原语下,可以在 A12 时代的设备上伪造内核 PAC(A keys),但 Apple 修补了许多这些路径。
- 在 Apple 的系统中,某些密钥是 全局内核共用,而用户进程可能获得每进程的密钥随机性。
PAC 绕过
- 内核态 PAC:理论 vs 真实绕过
- 由于内核 PAC 密钥和逻辑受到严格控制(特权寄存器、多样化、域隔离),伪造任意签名的内核指针非常困难。
- Azad 在 2020 年的 “iOS Kernel PAC, One Year Later” 报告称在 iOS 12-13 中他发现了一些部分绕过(签名 gadget、重用已签名状态、未受保护的间接分支),但没有发现完全通用的绕过方法。 bazad.github.io
- Apple 的 “Dark Magic” 自定义进一步收窄了可利用面(域切换、每密钥启用位)。 i.blackhat.com
- 已知存在针对 Apple silicon(M1/M2)的 kernel PAC 绕过 CVE-2023-32424,由 Zecao Cai 等人报告。 i.blackhat.com
- 但这些绕过通常依赖非常具体的 gadget 或实现 bug;它们并非通用绕过。
因此内核 PAC 被认为是 高度可靠 的,尽管并非完美。
- 用户态 / 运行时 PAC 绕过技术
这些更为常见,利用 PAC 在动态链接 / 运行时框架中的应用或使用上的不完善。下面是几类及示例。
2.1 Shared Cache / A key 问题
-
dyld shared cache 是一个大型预链接的 system frameworks 和 libraries 的 blob。由于它被广泛共享,shared cache 中的函数指针是“预签名”的,并被许多进程使用。攻击者将这些已签名指针当作 “PAC oracles” 来利用。
-
一些绕过技术试图提取或重用 shared cache 中使用 A-key 签名的指针并在 gadget 中重用它们。
-
“No Clicks Required” 的演讲描述了如何在 shared cache 上构建一个 oracle 来推断相对地址,并将其与已签名指针结合以绕过 PAC。 saelo.github.io
-
此外,userspace 中从 shared libraries 导入的函数指针被发现没有受到充分的 PAC 保护,允许攻击者在不更改签名的情况下获取函数指针。 (Project Zero bug entry) bugs.chromium.org
2.2 dlsym(3) / 动态符号解析
-
一个已知的绕过是调用
dlsym()来获取一个 已签名 的函数指针(用 A-key 签名,diversifier 为零),然后使用它。由于dlsym返回的是合法签名的指针,使用它就绕过了伪造 PAC 的需求。 -
Epsilon 的博客详细说明了一些绕过:调用
dlsym("someSym")会返回一个已签名指针并可用于间接调用。 blog.epsilon-sec.com -
Synacktiv 的 “iOS 18.4 — dlsym considered harmful” 描述了一个 bug:iOS 18.4 中通过
dlsym解析的一些符号返回了错误签名(或有 bug 的 diversifiers),使得意外的 PAC 绕过成为可能。 Synacktiv -
dyld 中与 dlsym 相关的逻辑包括:当
result->isCode时,它会用__builtin_ptrauth_sign_unauthenticated(..., key_asia, 0)对返回的指针进行签名,即上下文为零。 blog.epsilon-sec.com
因此,dlsym 是用户态 PAC 绕过的常见向量。
2.3 其它 DYLD / 运行时重定位
-
DYLD loader 和动态重定位逻辑复杂,有时会临时将页面映射为读/写以执行重定位,然后再切换回只读。攻击者利用这些时间窗口。Synacktiv 的演讲描述了基于时序的通过动态重定位绕过 PAC 的 “Operation Triangulation”。 Synacktiv
-
DYLD 页面现在受 SPRR / VM_FLAGS_TPRO 等保护标志保护。但早期版本的保护较弱。 Synacktiv
-
在 WebKit exploit 链中,DYLD loader 往往是 PAC 绕过的目标。幻灯片提到许多 PAC 绕过针对 DYLD loader(通过重定位、interposer hooks)。 Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
-
在用户态 exploit 链中,Objective-C 运行时方法如
NSPredicate、NSExpression或NSInvocation被用来在不明显伪造指针的情况下走私控制调用。 -
在 PAC 出现之前(更早的 iOS),一个 exploit 使用了 fake NSInvocation 对象来调用受控内存上的任意 selector。引入 PAC 后需要对该技术进行修改。但 SLOP(SeLector Oriented Programming)在 PAC 下也被扩展使用。 Project Zero
-
原始的 SLOP 技术允许通过构造假 invocation 链接 ObjC 调用;绕过依赖于 ISA 或 selector 指针有时并未被完全用 PAC 保护。 Project Zero
-
在 PAC 部分应用的环境中,方法 / selectors / target pointers 可能并不总是受到 PAC 保护,从而提供了绕过的空间。
Example Flow
Example Signing & Authenticating
``` ; Example: function prologue / return address protection my_func: stp x29, x30, [sp, #-0x20]! ; push frame pointer + LR mov x29, sp PACIASP ; sign LR (x30) using SP as modifier ; … body … mov sp, x29 ldp x29, x30, [sp], #0x20 ; restore AUTIASP ; authenticate & strip PAC ret; Example: indirect function pointer stored in a struct ; suppose X1 contains a function pointer PACDA X1, X2 ; sign data pointer X1 with context X2 STR X1, [X0] ; store signed pointer
; later retrieval: LDR X1, [X0] AUTDA X1, X2 ; authenticate & strip BLR X1 ; branch to valid target
; Example: stripping for comparison (unsafe) LDR X1, [X0] XPACI X1 ; strip PAC (instruction domain) CMP X1, #some_label_address BEQ matched_label
</details>
<details>
<summary>Example</summary>
缓冲区溢出会覆盖栈上的返回地址。攻击者写入目标 gadget 地址但无法计算正确的 PAC。当函数返回时,CPU 的 `AUTIA` 指令因 PAC 不匹配而发生故障。链路失败。
Project Zero 对 A12 (iPhone XS) 的分析展示了 Apple 如何使用 PAC,以及如果攻击者拥有内存读/写原语时伪造 PAC 的方法。
</details>
### 9. **Branch Target Identification (BTI)**
**Introduced with ARMv8.5 (later hardware)**
BTI 是一项硬件特性,用于检查**间接分支目标**:在执行 `blr` 或间接调用/跳转时,目标必须以 **BTI landing pad**(`BTI j` 或 `BTI c`)开头。跳转到缺少 landing pad 的 gadget 地址会触发异常。
LLVM 的实现说明了三种 BTI 指令变体以及它们如何映射到不同的分支类型。
| BTI Variant | What it permits (which branch types) | Typical placement / use case |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | Targets of *call*-style indirect branches (e.g. `BLR`, or `BR` using X16/X17) | 放在可能被间接调用的函数入口处 |
| **BTI J** | Targets of *jump*-style branches (e.g. `BR` used for tail calls) | 放在由跳表或尾调用可达的基本块开头 |
| **BTI JC** | Acts as both C and J | 可被 call 或 jump 分支所定位 |
- 在启用了 branch target enforcement 的代码中,编译器会在每个有效的间接分支目标(函数开头或可被跳转到的基本块)插入 BTI 指令(C、J 或 JC),以确保间接分支只能成功跳转到这些位置。
- **直接分支 / 调用**(即固定地址的 `B`, `BL`)**不受 BTI 限制**。假定代码页是受信任的且攻击者无法修改它们(因此直接分支是安全的)。
- 此外,**RET / return** 指令通常不受 BTI 限制,因为返回地址通过 PAC 或返回签名机制受到保护。
#### Mechanism and enforcement
- 当 CPU 解码位于被标记为“guarded / BTI-enabled”的页中的**间接分支(BLR / BR)**时,会检查目标地址的第一条指令是否为允许的 BTI(C、J 或 JC)。如果不是,则发生**Branch Target Exception**。
- BTI 指令编码设计为重用先前留作 NOP 的操作码(在早期 ARM 版本中)。因此在没有 BTI 支持的硬件上,这些指令作为 NOP 工作,从而保证向后兼容性。
- 插入 BTI 的编译器 pass 仅在需要处插入:可能被间接调用的函数,或被跳转目标指向的基本块。
- 一些补丁和 LLVM 代码表明,BTI 并非插入到*所有*基本块——只插入到那些可能成为分支目标的基本块(例如来自 switch / jump table 的目标)。
#### BTI + PAC synergy
PAC 保护指针值(源)——确保间接调用 / 返回链未被篡改。
BTI 确保即使指针有效,也只能跳转到正确标记的入口点。
两者结合起来,攻击者既需要一个带有正确 PAC 的有效指针,又需要目标位置包含 BTI 前缀。这增加了构造利用 gadget 的难度。
#### Example
<details>
<summary>Example</summary>
一次利用尝试 pivot 到不以 `BTI c` 开头的 `0xABCDEF` gadget。CPU 在执行 `blr x0` 时检查目标并因指令不包含有效 landing pad 而发生故障。因此许多 gadget 在未包含 BTI 前缀时变得不可用。
</details>
### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**Introduced in more recent ARMv8 extensions / iOS support (for hardened kernel)**
#### PAN (Privileged Access Never)
- **PAN** 是在 **ARMv8.1-A** 中引入的一项特性,阻止**特权代码**(EL1 或 EL2)读取或写入被标记为**用户可访问(EL0)**的内存,除非显式禁用 PAN。
- 其思想是:即便内核被欺骗或被破坏,也不能在不先*清除* PAN 的情况下任意取消引用用户空间指针,从而降低发生 **`ret2usr`** 风格利用或滥用用户可控缓冲区的风险。
- 当 PAN 启用(PSTATE.PAN = 1)时,任何特权的 load/store 指令访问被标记为“在 EL0 可访问”的虚拟地址都会触发**权限故障**。
- 当内核必须合法访问用户空间内存(例如将数据拷贝到/从用户缓冲区)时,必须**暂时禁用 PAN**(或使用“非特权 load/store” 指令)以允许访问。
- 在 ARM64 的 Linux 中,PAN 支持大约在 2015 年引入:内核补丁添加了对该特性的检测,并用在访问用户内存时会清除 PAN 的变体替换了 `get_user` / `put_user` 等。
**Key nuance / limitation / bug**
- 如 Siguza 等人所述,ARM 规范中的一个 bug(或歧义行为)意味着**仅可执行的用户映射**(`--x`)可能**不会触发 PAN**。换言之,如果一个用户页被标记为可执行但没有读权限,内核的读取尝试可能会绕过 PAN,因为架构把“在 EL0 可访问”视为需要可读权限,而非仅可执行。这在某些实现中导致 PAN 绕过。
- 因此,如果 iOS / XNU 允许仅可执行的用户页(例如某些 JIT 或代码缓存设置),在 PAN 启用时内核可能仍然能够意外地从这些页读取数据。这是在某些 ARMv8+ 系统中已知的微妙可利用区域。
#### PXN (Privileged eXecute Never)
- **PXN** 是页表标志(出现在页表项,leaf 或 block 条目中),指示该页在特权模式下(例如 EL1)**不可执行**。
- PXN 阻止内核(或任何特权代码)跳转到或执行用户空间页中的指令,即使控制被转移。实际上,它阻止了内核级的控制流重定向到用户内存。
- 与 PAN 结合,可以确保:
1. 内核默认不能读取或写入用户空间数据(PAN)
2. 内核不能执行用户空间代码(PXN)
- 在 ARMv8 的页表格式中,leaf 条目有一个 `PXN` 位(以及用于非特权执行禁止的 `UXN`)在其属性位中。
因此即使内核有一个损坏的函数指针指向用户内存并尝试分支过去,PXN 位也会导致故障。
#### Memory-permission model & how PAN and PXN map to page table bits
要理解 PAN / PXN 的工作,需要看 ARM 的地址转换与权限模型(简化):
- 每个页或 block 条目都有包括 **AP[2:1]**(访问权限:读/写、特权与非特权)和 **UXN / PXN**(执行禁止)位的属性字段。
- 当 PSTATE.PAN 为 1(启用)时,硬件执行修改后的语义:对被标记为“EL0 可访问”的页的特权访问会被禁止(触发故障)。
- 由于前述 bug,只有可执行(不可读)的页在某些实现上可能不被视为“EL0 可访问”,从而绕过 PAN。
- 当某页的 PXN 位被设置时,即使指令取自更高特权级别,执行也被禁止。
#### Kernel usage of PAN / PXN in a hardened OS (e.g. iOS / XNU)
在加固内核设计中(例如 Apple 可能采用的):
- 内核默认启用 PAN(因此特权代码受到约束)。
- 在那些合法需要读取或写入用户缓冲区的路径(例如 syscall 缓冲区拷贝、I/O、read/write user pointer),内核会暂时**禁用 PAN**或使用特殊指令来覆盖。
- 完成用户数据访问后,必须重新启用 PAN。
- PXN 通过页表强制:用户页面设置 PXN = 1(因此内核无法执行它们),内核页面不设置 PXN(因此内核代码可执行)。
- 内核必须确保没有代码路径导致控制流进入用户内存区域(那会绕过 PXN)——因此依赖于“跳转到用户控制的 shellcode”的利用链被阻断。
由于通过仅可执行页面可绕过 PAN,实际系统中 Apple 可能会禁用或不允许仅可执行的用户页,或修补该规范弱点。
#### Attack surfaces, bypasses, and mitigations
- **PAN bypass via execute-only pages**:如前所述,规范存在漏洞:对于仅可执行(无读权限)的用户页,某些实现可能不将其视为“在 EL0 可访问”,因此 PAN 不会阻止内核从这些页读取。这为攻击者提供了通过“仅可执行”段传递数据的非典型路径。
- **Temporal window exploit**:若内核在比必要更长的时间内禁用 PAN,竞态或恶意路径可能利用该窗口执行未授权的用户内存访问。
- **Forgotten re-enable**:若代码路径未能重新启用 PAN,后续内核操作可能会错误地访问用户内存。
- **Misconfiguration of PXN**:如果页表没有对用户页设置 PXN 或错误映射用户代码页,内核可能被诱导执行用户空间代码。
- **Speculation / side-channels**:类似于推测执行绕过,可能存在微架构副作用导致 PAN / PXN 检查的短暂规避(尽管此类攻击高度依赖 CPU 设计)。
- **Complex interactions**:在更复杂的特性(例如 JIT、共享内存、即时生成代码区域)中,内核可能需要精细控制以允许对某些用户映射区域的访问或执行;在 PAN/PXN 约束下安全设计这些路径并非易事。
#### Example
<details>
<summary>Code Example</summary>
下面是说明性的伪汇编序列,展示在用户内存访问周围启用/禁用 PAN 的方式,以及可能如何触发故障。
</details>
// Suppose kernel entry point, PAN is enabled (privileged code cannot access user memory by default)
; Kernel receives a syscall with user pointer in X0 ; wants to read an integer from user space mov X1, X0 ; X1 = user pointer
; disable PAN to allow privileged access to user memory MSR PSTATE.PAN, #0 ; clear PAN bit, disabling the restriction
ldr W2, [X1] ; now allowed load from user address
; re-enable PAN before doing other kernel logic MSR PSTATE.PAN, #1 ; set PAN
; … further kernel work …
; Later, suppose an exploit corrupts a pointer to a user-space code page and jumps there BR X3 ; branch to X3 (which points into user memory)
; Because the target page is marked PXN = 1 for privileged execution, ; the CPU throws an exception (fault) and rejects execution
<details>
<summary>Example</summary>
如果内核没有在该用户页上设置 PXN,那么该分支可能会成功——这是不安全的。
如果内核在访问用户内存后忘记重新启用 PAN,就会打开一个窗口,使后续的内核逻辑可能意外地读/写任意用户内存。
如果用户指针指向一个仅可执行页面(只有 execute 权限、没有读/写),在 PAN 规范的 bug 下,`ldr W2, [X1]` 可能即便在启用 PAN 时也不会触发 fault,从而视实现情况可能允许绕过利用。
</details>
<details>
<summary>Example</summary>
内核漏洞尝试将用户提供的函数指针在内核上下文中调用(例如 `call user_buffer`)。在 PAN/PXN 下,该操作会被禁止或触发 fault。
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**引入于 ARMv8.5 及更高版本(或作为可选扩展)**
TBI 意味着 64 位指针的最高字节(最重要的字节)在地址转换时被忽略。这允许操作系统或硬件在指针的最高字节中嵌入 **tag bits** 而不影响实际地址。
- TBI 表示 **Top Byte Ignore**(有时称为 *Address Tagging*)。它是一个硬件特性(在许多 ARMv8+ 实现中可用),在执行**地址转换 / load/store / instruction fetch** 时**忽略最高 8 位**(位 63:56)。
- 实际上,CPU 会将指针 `0xTTxxxx_xxxx_xxxx`(其中 `TT` = 顶字节)在地址转换时当作 `0x00xxxx_xxxx_xxxx` 来处理,忽略(掩去)顶字节。顶字节可以被软件用于存储 **metadata / tag bits**。
- 这为软件提供了“免费”的带内空间,可以在每个指针中嵌入一个字节的 tag 而不改变它所引用的内存位置。
- 架构保证在执行 load、store 和 instruction fetch 时,会在进行实际内存访问前将指针的顶字节掩去(即剥离 tag)。
因此 TBI 将**逻辑指针**(指针 + tag)与用于内存操作的**物理地址**解耦。
#### 为什么要用 TBI:用例与动机
- **Pointer tagging / metadata**:你可以在顶字节中存储额外的元数据(例如对象类型、版本、边界、完整性 tag)。当稍后使用该指针时,硬件会忽略 tag,因此在进行内存访问时不需要手动剥离。
- **Memory tagging / MTE (Memory Tagging Extension)**:TBI 是 MTE 构建的基础硬件机制。在 ARMv8.5 中,**Memory Tagging Extension** 使用指针的位 59:56 作为**逻辑 tag**,并将其与存储在内存中的**allocation tag** 进行比对。
- **增强的安全性与完整性**:通过将 TBI 与 pointer authentication(PAC)或运行时检查结合使用,可以不仅强制检查指针值,还强制检查 tag 是否正确。攻击者覆盖指针但没有正确的 tag 将导致 tag 不匹配。
- **兼容性**:由于 TBI 是可选的且硬件会忽略 tag 位,现有未打 tag 的代码可以照常运行。对于旧代码,tag 位实际上成为“无关紧要”的位。
#### Example
<details>
<summary>Example</summary>
一个函数指针在其顶字节中包含了一个 tag(例如 `0xAA`)。利用者覆盖了指针的低位但忘记更新 tag,因此当内核验证或清理时,该指针因 tag 不匹配而失败或被拒绝。
</details>
---
### 12. **Page Protection Layer (PPL)**
**在较新的 iOS / 现代硬件中引入(iOS ~17 / Apple silicon / 高端机型)**(有些报告显示 PPL 在 macOS / Apple silicon 中出现,但 Apple 正将类似的保护移植到 iOS)
- PPL 设计为一种**内核内的保护边界**:即使内核(EL1)被攻破并拥有读/写能力,**也不应能自由修改**某些**敏感页面**(尤其是页表、代码签名元数据、内核代码页、entitlements、trust caches 等)。
- 它在内核内部实质上创建了一个“内核内的内核”——一个较小的受信任组件(PPL),只有该组件具有修改受保护页面的**提升权限**。其他内核代码必须通过调用 PPL 例程来进行修改。
- 这减少了内核漏洞的攻击面:即使在内核模式下拥有任意 R/W/execute,利用代码仍必须以某种方式进入 PPL 域(或绕过 PPL)来修改关键结构。
- 在较新的 Apple silicon(A15+ / M2+)上,Apple 正在向 **SPTM (Secure Page Table Monitor)** 过渡,在许多情况下它取代了 PPL 对页表保护的作用。
基于公开分析,PPL 的运作大致如下:
#### APRR / permission routing 的使用(APRR = Access Permission ReRouting)
- Apple 硬件使用一种叫 **APRR (Access Permission ReRouting)** 的机制,允许页表项(PTEs)包含小的索引,而不是完整的权限位。这些索引通过 APRR 寄存器映射到实际权限。这使得可以按域动态重映射权限。
- PPL 利用 APRR 在内核上下文中分隔特权:只有 PPL 域被允许更新索引与实际生效权限之间的映射。也就是说,当非 PPL 内核代码写入 PTE 或尝试翻转权限位时,APRR 逻辑会拒绝(或强制只读映射)。
- PPL 代码本身运行在受限区域(例如 `__PPLTEXT`),在进入门被临时允许之前通常是不可执行或不可写的。内核通过调用 PPL 入口点(“PPL 例程”)来执行敏感操作。
#### 进入 / 退出 门控
- 当内核需要修改受保护页面(例如更改内核代码页的权限或修改页表)时,它会调用一个 **PPL wrapper** 例程,该例程进行校验然后切换到 PPL 域。在该域之外,受保护页面对于主内核代码实际上是只读或不可修改的。
- 在 PPL 入口期间,APRR 映射会被调整,使得 PPL 区域内的内存页面在 PPL 内部被设置为**可执行 & 可写**。退出时这些映射会被恢复为只读/不可写。这确保只有经过审计的 PPL 例程可以写入受保护页面。
- 在 PPL 之外,内核代码尝试写入这些受保护页面将会触发 fault(权限被拒绝),因为该代码域的 APRR 映射不允许写入。
#### 受保护页面类别
PPL 通常保护的页面包括:
- 页表结构(翻译表条目、映射元数据)
- 内核代码页,特别是包含关键逻辑的那些
- 代码签名元数据(trust caches、签名 blob)
- 权限表、签名强制表
- 其他高价值的内核结构,篡改这些结构可以绕过签名检查或操纵凭证
核心思想是即使攻击者完全控制了内核内存,也不能简单地修补或重写这些页面,除非他们也妥协 PPL 例程或绕过 PPL。
#### 已知绕过与漏洞
1. **Project Zero 的 PPL 绕过(陈旧 TLB 技巧)**
- Project Zero 的公开文章描述了一种涉及 **stale TLB entries** 的绕过方法。
- 思路如下:
1. 分配两页物理页面 A 和 B,并将它们标记为 PPL 页面(因此受保护)。
2. 映射两个虚拟地址 P 和 Q,它们的 L3 translation table 页面分别来自 A 和 B。
3. 启动一个线程持续访问 Q,使得其 TLB 条目保持活跃(stale)。
4. 调用 `pmap_remove_options()` 来移除从 P 开始的映射;由于一个 bug,代码错误地移除了 P 和 Q 的 TTE,但只对 P 的 TLB 条目进行了失效,使得 Q 的陈旧条目仍然保留。
5. 重新使用 B(即 Q 的表页)来映射任意内存(例如 PPL 受保护页)。因为陈旧的 TLB 条目仍然映射着 Q 的旧映射,在该上下文中该映射仍然有效。
6. 通过此方式,攻击者可以在不经过 PPL 接口的情况下放置可写的 PPL 受保护页面映射。
- 该利用需要对物理映射和 TLB 行为的精细控制。它展示了依赖 TLB / 映射一致性的安全边界在遇到失效处理不当时可能出现隐蔽的漏洞。
- Project Zero 评论称此类绕过微妙且罕见,但在复杂系统中是可能的。尽管如此,他们仍将 PPL 视为一个有效的缓解措施。
2. **其他潜在风险与限制**
- 如果内核漏洞能直接进入 PPL 例程(通过调用 PPL wrapper),则可能绕过限制。因此参数验证极其关键。
- PPL 代码本身的缺陷(例如算术溢出、边界检查错误)可能允许在 PPL 内部进行越界修改。Project Zero 观察到 `pmap_remove_options_internal()` 中的这类 bug 被用于绕过。
- PPL 边界不可撤销地依赖硬件执行(APRR、memory controller),因此其强度取决于硬件实现的可靠性。
#### Example
<details>
<summary>Code Example</summary>
这里是一个简化的伪代码 / 逻辑,展示内核如何调用 PPL 来修改受保护页面:
</details>
```c
// In kernel (outside PPL domain)
function kernel_modify_pptable(pt_addr, new_entry) {
// validate arguments, etc.
return ppl_call_modify(pt_addr, new_entry) // call PPL wrapper
}
// In PPL (trusted domain)
function ppl_call_modify(pt_addr, new_entry) {
// temporarily enable write access to protected pages (via APRR adjustments)
aprr_set_index_for_write(PPL_INDEX)
// perform the modification
*pt_addr = new_entry
// restore permissions (make pages read-only again)
aprr_restore_default()
return success
}
// If kernel code outside PPL does:
*pt_addr = new_entry // a direct write
// It will fault because APRR mapping for non-PPL domain disallows write to that page
The kernel can do many normal operations, but only through ppl_call_* routines can it change protected mappings or patch code.
Example
A kernel exploit tries to overwrite the entitlement table, or disable code-sign enforcement by modifying a kernel signature blob. Because that page is PPL-protected, the write is blocked unless going through the PPL interface. So even with kernel code execution, you cannot bypass code-sign constraints or modify credential data arbitrarily. On iOS 17+ certain devices use SPTM to further isolate PPL-managed pages.PPL → SPTM / 替代 / 未来
- 在 Apple 的现代 SoC(A15 或更高,M2 或更高)上,Apple 支持 SPTM (Secure Page Table Monitor),它在页表保护方面取代了 PPL。
- Apple 在文档中指出:“Page Protection Layer (PPL) and Secure Page Table Monitor (SPTM) enforce execution of signed and trusted code … PPL manages the page table permission overrides … Secure Page Table Monitor replaces PPL on supported platforms.”
- SPTM 架构可能将更多策略执行移到 kernel 控制之外的更高特权监控器中,进一步缩小信任边界。
MTE | EMTE | MIE
Here’s a higher-level description of how EMTE operates under Apple’s MIE setup:
- Tag assignment
- 当内存被分配时(例如在 kernel 或通过安全分配器在用户空间),会为该内存块分配一个secret tag。
- 返回给用户或 kernel 的指针在高位包含该 tag(使用 TBI / top byte ignore 机制)。
- Tag checking on access
- 无论何时使用指针执行 load 或 store,硬件都会检查指针的 tag 是否与内存块的 tag(allocation tag)匹配。如果不匹配,会立即产生异常(因为是同步的)。
- 由于是同步的,所以不存在“延迟检测”窗口。
- Retagging on free / reuse
- 当内存被释放时,分配器会更改该块的 tag(因此具有旧 tag 的老指针将不再匹配)。
- 因此,use-after-free 指针在访问时会有过期的 tag 而产生不匹配。
- Neighbor-tag differentiation to catch overflows
- 相邻分配会被赋予不同的 tag。如果缓冲区溢出溢入相邻内存,tag 不匹配会导致异常。
- 对于跨界的小型溢出,这一点尤其有效。
- Tag confidentiality enforcement
- Apple 必须防止 tag values being leaked(因为如果攻击者获得了 tag,就可以构造具有正确 tag 的指针)。
- 他们加入了保护(microarchitectural / speculative controls)来避免通过侧信道泄露 tag 位。
- Kernel and user-space integration
- Apple 在不仅用户空间也在 kernel / OS 关键组件中使用 EMTE(以防护 kernel 免受内存破坏)。
- 硬件/OS 确保即使在 kernel 代表用户空间执行时,tag 规则也仍然适用。
Example
``` Allocate A = 0x1000, assign tag T1 Allocate B = 0x2000, assign tag T2// pointer P points into A with tag T1 P = (T1 << 56) | 0x1000
// Valid store *(P + offset) = value // tag T1 matches allocation → allowed
// Overflow attempt: P’ = P + size_of_A (into B region) *(P’ + delta) = value → pointer includes tag T1 but memory block has tag T2 → mismatch → fault
// Free A, allocator retags it to T3 free(A)
// Use-after-free: *(P) = value → pointer still has old tag T1, memory region is now T3 → mismatch → fault
</details>
#### 限制与挑战
- **Intrablock overflows**:如果 overflow 保留在同一分配内(未越过边界)且 tag 保持不变,tag mismatch 无法检测到。
- **Tag width limitation**:可用于 tag 的位数很少(例如 4 位或小域)——命名空间受限。
- **Side-channel leaks**:如果 tag 位可以被 leaked(通过 cache / speculative execution),攻击者可能会学到有效的 tags 并绕过防护。Apple 的 tag confidentiality enforcement 旨在缓解此问题。
- **性能开销**:每次 load/store 的 tag 检查会增加成本;Apple 必须优化硬件以将开销降到最低。
- **兼容性与回退**:在不支持 EMTE 的旧硬件或部分组件上,必须存在回退机制。Apple 声称 MIE 仅在硬件支持的设备上启用。
- **复杂的分配器逻辑**:分配器必须管理 tags、retagging、对齐边界并避免错误 tag 碰撞。分配器逻辑中的 bug 可能引入漏洞。
- **混合内存 / 混合区域**:部分内存可能仍未标记(legacy),使互操作性更为棘手。
- **Speculative / transient 攻击**:与许多微体系结构防护一样,speculative execution 或 micro-op 融合可能在短暂时序上绕过检查或 leak tag 位。
- **限制于受支持区域**:Apple 可能只在选择的高风险区域(kernel、security-critical subsystems)强制 EMTE,而非全局覆盖。
---
## 与标准 MTE 的主要增强/差异
Here are the improvements and changes Apple emphasizes:
| Feature | Original MTE | EMTE (Apple’s enhanced) / MIE |
|---|---|---|
| **Check mode** | Supports synchronous and asynchronous modes. In async, tag mismatches are reported later (delayed)| Apple insists on **synchronous mode** by default—tag mismatches are caught immediately, no delay/race windows allowed.|
| **Coverage of non-tagged memory** | Accesses to non-tagged memory (e.g. globals) may bypass checks in some implementations | EMTE requires that accesses from a tagged region to non-tagged memory also validate tag knowledge, making it harder to bypass by mixing allocations.|
| **Tag confidentiality / secrecy** | Tags might be observable or leaked via side channels | Apple adds **Tag Confidentiality Enforcement**, which attempts to prevent leakage of tag values (via speculative side-channels etc.).|
| **Allocator integration & retagging** | MTE leaves much of allocator logic to software | Apple’s secure typed allocators (kalloc_type, xzone malloc, etc.) integrate with EMTE: when memory is allocated or freed, tags are managed at fine granularity.|
| **Always-on by default** | In many platforms, MTE is optional or off by default | Apple enables EMTE / MIE by default on supported hardware (e.g. iPhone 17 / A19) for kernel and many user processes.|
因为 Apple 控制了硬件与软件栈,它可以紧密地强制实施 EMTE,避免性能陷阱,并修补侧信道弱点。
---
## EMTE 在实践中的工作原理(Apple / MIE)
下面是 EMTE 在 Apple 的 MIE 配置下的高层描述:
1. **Tag assignment**
- 当内存被分配(例如在 kernel 或通过 secure allocators 在 user space 分配)时,会给该块分配一个 **secret tag**。
- 返回给用户或 kernel 的指针在高位中包含该 tag(使用 TBI / top byte ignore 机制)。
2. **Tag checking on access**
- 每当使用指针执行 load 或 store 时,硬件会检查指针的 tag 是否与内存块的 tag(allocation tag)匹配。如果不匹配,会立即 fault(因为是 synchronous)。
- 由于是 synchronous,所以不存在“延迟检测”的窗口。
3. **Retagging on free / reuse**
- 当内存被释放时,分配器会更改该块的 tag(因此具有旧 tag 的指针将不再匹配)。
- 因此,use-after-free 指针在被访问时会因为过期的 tag 而产生 mismatch。
4. **Neighbor-tag differentiation to catch overflows**
- 相邻的分配会被赋予不同的 tags。如果缓冲区 overflow 溢出到邻近内存,tag mismatch 会导致 fault。
- 这在捕捉跨界的小规模 overflow 时尤为有效。
5. **Tag confidentiality enforcement**
- Apple 必须防止 tag 值被泄露(因为如果攻击者知道了 tag,他们可以构造带有正确 tag 的指针)。
- 他们包含了保护措施(microarchitectural / speculative 控制)以避免 tag 值的 side-channel 泄露。
6. **Kernel and user-space integration**
- Apple 不仅在 user-space 使用 EMTE,也在 kernel / OS 关键组件中使用(以保护 kernel 免受内存破坏)。
- 硬件/OS 确保即使在 kernel 为 user space 执行操作时,tag 规则仍然适用。
因为 EMTE 被内置在 MIE 中,Apple 在关键攻击面采用 synchronous 模式,而不是作为可选或调试模式来使用。
---
## Exception handling in XNU
当发生一个 **exception**(例如 `EXC_BAD_ACCESS`、`EXC_BAD_INSTRUCTION`、`EXC_CRASH`、`EXC_ARM_PAC` 等),XNU 的 **Mach layer** 负责在它转换为 UNIX-style **signal**(像 `SIGSEGV`、`SIGBUS`、`SIGILL` ...)之前拦截该异常。
这个过程涉及多个层次的异常传播和处理,先在内核中处理,最后才到达用户空间或被转换为 BSD signal。
### Exception Flow (High-Level)
1. **CPU triggers a synchronous exception**(例如,invalid pointer dereference、PAC failure、illegal instruction 等)。
2. **Low-level trap handler** 运行(`trap.c`、`exception.c` 在 XNU 源码中)。
3. trap handler 调用 **`exception_triage()`**,这是 Mach 异常处理的核心。
4. `exception_triage()` 决定如何路由该异常:
- 首先发送到 **thread 的 exception port**。
- 然后到 **task 的 exception port**。
- 然后到 **host 的 exception port**(通常是 `launchd` 或 `ReportCrash`)。
如果这些端口都没有处理该异常,内核可能会:
- **将其转换为 BSD signal**(针对 user-space 进程)。
- **panic(内核崩溃)**(针对 kernel-space 异常)。
### Core Function: `exception_triage()`
函数 `exception_triage()` 将 Mach exception 沿可能的处理链向上路由,直到某个处理器处理它或最终判定为致命。它在 osfmk/kern/exception.c 中定义。
```c
void exception_triage(exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt);
典型调用流程:
exception_triage() └── exception_deliver() ├── exception_deliver_thread() ├── exception_deliver_task() └── exception_deliver_host()
如果全部失败 → 由 bsd_exception() 处理 → 转换为类似 SIGSEGV 的信号。
异常端口
每个 Mach 对象(thread、task、host)可以注册 exception ports,异常消息会发送到这些端口。
它们由以下 API 定义:
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
Each exception port has:
- A mask(想要接收哪些异常)
- A port name(用于接收消息的 Mach port)
- A behavior(内核发送消息的方式)
- A flavor(包含哪种线程状态)
Debuggers and Exception Handling
一个 debugger(例如 LLDB)会在目标 task 或 thread 上设置一个 exception port,通常使用 task_set_exception_ports()。
当异常发生时:
- Mach 消息会被发送到 debugger 进程。
- debugger 可以决定处理(恢复、修改寄存器、跳过指令)或不处理该异常。
- 如果 debugger 不处理,异常会向下一级传播(thread → task → host)。
Flow of EXC_BAD_ACCESS
-
线程解引用无效指针 → CPU 触发 Data Abort。
-
内核 trap 处理器调用
exception_triage(EXC_BAD_ACCESS, ...)。 -
消息发送到:
-
Thread port →(debugger 可以拦截断点)。
-
如果 debugger 忽略 → Task port →(进程级处理程序)。
-
如果仍被忽略 → Host port(通常是 ReportCrash)。
- 如果无人处理 →
bsd_exception()将其转换为SIGSEGV。
PAC Exceptions
当 Pointer Authentication (PAC) 失败(签名不匹配)时,会触发一个 特殊的 Mach 异常:
EXC_ARM_PAC(类型)- Codes 可能包含详细信息(例如,key 类型、指针类型)。
如果二进制具有标志 TFRO_PAC_EXC_FATAL,内核会将 PAC 失败视为 致命,绕过 debugger 拦截。这样做是为了防止攻击者使用 debugger 绕过 PAC 检查,该标志在 platform binaries 上启用。
Software Breakpoints
软件断点(x86 上的 int3,ARM64 上的 brk)是通过故意引发故障来实现的。
debugger 通过 exception port 捕获该故障:
- 修改指令指针或内存。
- 恢复原始指令。
- 恢复执行。
同样的机制允许你“捕获”PAC 异常——除非 TFRO_PAC_EXC_FATAL 被设置,在那种情况下异常永远到达不了 debugger。
Conversion to BSD Signals
如果没有处理程序接受该异常:
-
内核调用
task_exception_notify() → bsd_exception()。 -
这将 Mach 异常映射为信号:
| Mach Exception | Signal |
|---|---|
| EXC_BAD_ACCESS | SIGSEGV or SIGBUS |
| EXC_BAD_INSTRUCTION | SIGILL |
| EXC_ARITHMETIC | SIGFPE |
| EXC_SOFTWARE | SIGTRAP |
| EXC_BREAKPOINT | SIGTRAP |
| EXC_CRASH | SIGKILL |
| EXC_ARM_PAC | SIGILL (on non-fatal) |
### Key Files in XNU Source
-
osfmk/kern/exception.c→exception_triage()、exception_deliver_*()的核心实现。 -
bsd/kern/kern_sig.c→ 信号投递逻辑。 -
osfmk/arm64/trap.c→ 低级 trap 处理。 -
osfmk/mach/exc.h→ 异常代码和结构体。 -
osfmk/kern/task.c→ Task exception port 的设置。
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
内核使用一个 zone allocator(kalloc),按固定大小划分为多个“zone”。每个 zone 只存放单一大小类的分配。
从截图:
| Zone Name | Element Size | Example Use |
|---|---|---|
default.kalloc.16 | 16 bytes | 非常小的内核结构体,指针。 |
default.kalloc.32 | 32 bytes | 小型结构体,对象头。 |
default.kalloc.64 | 64 bytes | IPC 消息,微小的内核缓冲区。 |
default.kalloc.128 | 128 bytes | 中等对象,比如 OSObject 的部分。 |
| … | … | … |
default.kalloc.1280 | 1280 bytes | 大型结构体,IOSurface/graphics 元数据。 |
工作原理:
- 每次分配请求会被向上取整到最近的 zone 大小。(例如,50 字节的请求会落在
kalloc.64zone) - 每个 zone 的内存通过 freelist 管理 — 内核释放的块回到该 zone。
- 如果你溢出一个 64 字节的缓冲区,会覆盖同一 zone 中的下一个对象。
这就是为什么 heap spraying / feng shui 如此有效:通过喷射相同大小类的分配,你可以预测对象的邻居。
The freelist
在每个 kalloc zone 内,被释放的对象不会直接返还给系统——它们进入一个 freelist,即可用块的链表。
-
当一个块被释放时,内核在该块起始处写入一个指针 → 指向同一 zone 中下一个空闲块的地址。
-
zone 保持一个 HEAD 指针,指向第一个空闲块。
-
分配总是使用当前的 HEAD:
-
弹出 HEAD(将该内存返回给调用者)。
-
更新 HEAD = HEAD->next(存储在已释放块的头部)。
-
释放操作将块压回链表:
-
freed_chunk->next = HEAD -
HEAD = freed_chunk
所以 freelist 只是构建在已释放内存内部的一个链表。
Normal state:
Zone page (64-byte chunks for example):
[ A ] [ F ] [ F ] [ A ] [ F ] [ A ] [ F ]
Freelist view:
HEAD ──► [ F ] ──► [ F ] ──► [ F ] ──► [ F ] ──► NULL
(next ptrs stored at start of freed chunks)
利用 freelist
Because the first 8 bytes of a free chunk = freelist pointer, an attacker could corrupt it:
-
Heap overflow 写入相邻的已释放 chunk → 覆盖其 “next” pointer。
-
Use-after-free 写入已释放的对象 → 覆盖其 “next” pointer。
Then, on the next allocation of that size:
-
allocator 从中弹出被破坏的 chunk。
-
跟随攻击者提供的 “next” pointer。
-
返回一个指向任意内存的 pointer,从而实现 fake object primitives 或 targeted overwrite。
Visual example of freelist poisoning:
Before corruption:
HEAD ──► [ F1 ] ──► [ F2 ] ──► [ F3 ] ──► NULL
After attacker overwrite of F1->next:
HEAD ──► [ F1 ]
(next) ──► 0xDEAD_BEEF_CAFE_BABE (attacker-chosen)
Next alloc of this zone → kernel hands out memory at attacker-controlled address.
This freelist design made exploitation highly effective pre-hardening: predictable neighbors from heap sprays, raw pointer freelist links, and no type separation allowed attackers to escalate UAF/overflow bugs into arbitrary kernel memory control.
Heap Grooming / Feng Shui
The goal of heap grooming is to shape the heap layout so that when an attacker triggers an overflow or use-after-free, the target (victim) object sits right next to an attacker-controlled object.
That way, when memory corruption happens, the attacker can reliably overwrite the victim object with controlled data.
Steps:
- Spray allocations (fill the holes)
- Over time, the kernel heap gets fragmented: some zones have holes where old objects were freed.
- The attacker first makes lots of dummy allocations to fill these gaps, so the heap becomes “packed” and predictable.
- Force new pages
- Once the holes are filled, the next allocations must come from new pages added to the zone.
- Fresh pages mean objects will be clustered together, not scattered across old fragmented memory.
- This gives the attacker much better control of neighbors.
- Place attacker objects
- The attacker now sprays again, creating lots of attacker-controlled objects in those new pages.
- These objects are predictable in size and placement (since they all belong to the same zone).
- Free a controlled object (make a gap)
- The attacker deliberately frees one of their own objects.
- This creates a “hole” in the heap, which the allocator will later reuse for the next allocation of that size.
- Victim object lands in the hole
- The attacker triggers the kernel to allocate the victim object (the one they want to corrupt).
- Since the hole is the first available slot in the freelist, the victim is placed exactly where the attacker freed their object.
- Overflow / UAF into victim
- Now the attacker has attacker-controlled objects around the victim.
- By overflowing from one of their own objects (or reusing a freed one), they can reliably overwrite the victim’s memory fields with chosen values.
Why it works:
- Zone allocator predictability: allocations of the same size always come from the same zone.
- Freelist behavior: new allocations reuse the most recently freed chunk first.
- Heap sprays: attacker fills memory with predictable content and controls layout.
- End result: attacker controls where the victim object lands and what data sits next to it.
Modern Kernel Heap (iOS 15+/A12+ SoCs)
Apple hardened the allocator and made heap grooming much harder:
1. From Classic kalloc to kalloc_type
- Before: a single
kalloc.<size>zone existed for each size class (16, 32, 64, … 1280, etc.). Any object of that size was placed there → attacker objects could sit next to privileged kernel objects. - Now:
- Kernel objects are allocated from typed zones (
kalloc_type). - Each type of object (e.g.,
ipc_port_t,task_t,OSString,OSData) has its own dedicated zone, even if they’re the same size. - The mapping between object type ↔ zone is generated from the kalloc_type system at compile time.
An attacker can no longer guarantee that controlled data (OSData) ends up adjacent to sensitive kernel objects (task_t) of the same size.
2. Slabs and Per-CPU Caches
- The heap is divided into slabs (pages of memory carved into fixed-size chunks for that zone).
- Each zone has a per-CPU cache to reduce contention.
- Allocation path:
- Try per-CPU cache.
- If empty, pull from the global freelist.
- If freelist is empty, allocate a new slab (one or more pages).
- Benefit: This decentralization makes heap sprays less deterministic, since allocations may be satisfied from different CPUs’ caches.
3. Randomization inside zones
- Within a zone, freed elements are not handed back in simple FIFO/LIFO order.
- Modern XNU uses encoded freelist pointers (safe-linking like Linux, introduced ~iOS 14).
- Each freelist pointer is XOR-encoded with a per-zone secret cookie.
- This prevents attackers from forging a fake freelist pointer if they gain a write primitive.
- Some allocations are randomized in their placement within a slab, so spraying doesn’t guarantee adjacency.
4. Guarded Allocations
- Certain critical kernel objects (e.g., credentials, task structures) are allocated in guarded zones.
- These zones insert guard pages (unmapped memory) between slabs or use redzones around objects.
- Any overflow into the guard page triggers a fault → immediate panic instead of silent corruption.
5. Page Protection Layer (PPL) and SPTM
- Even if you control a freed object, you can’t modify all of kernel memory:
- PPL (Page Protection Layer) enforces that certain regions (e.g., code signing data, entitlements) are read-only even to the kernel itself.
- On A15/M2+ devices, this role is replaced/enhanced by SPTM (Secure Page Table Monitor) + TXM (Trusted Execution Monitor).
- These hardware-enforced layers mean attackers can’t escalate from a single heap corruption to arbitrary patching of critical security structures.
- (Added / Enhanced): also, PAC (Pointer Authentication Codes) is used in the kernel to protect pointers (especially function pointers, vtables) so that forging or corrupting them becomes harder.
- (Added / Enhanced): zones may enforce zone_require / zone enforcement, i.e. that an object freed can only be returned through its correct typed zone; invalid cross-zone frees may panic or be rejected. (Apple alludes to this in their memory safety posts)
6. Large Allocations
- Not all allocations go through
kalloc_type. - Very large requests (above ~16 KB) bypass typed zones and are served directly from kernel VM (kmem) via page allocations.
- These are less predictable, but also less exploitable, since they don’t share slabs with other objects.
7. Allocation Patterns Attackers Target
Even with these protections, attackers still look for:
- Reference count objects: if you can tamper with retain/release counters, you may cause use-after-free.
- Objects with function pointers (vtables): corrupting one still yields control flow.
- Shared memory objects (IOSurface, Mach ports): these are still attack targets because they bridge user ↔ kernel.
But — unlike before — you can’t just spray OSData and expect it to neighbor a task_t. You need type-specific bugs or info leaks to succeed.
Example: Allocation Flow in Modern Heap
Suppose userspace calls into IOKit to allocate an OSData object:
- Type lookup →
OSDatamaps tokalloc_type_osdatazone (size 64 bytes). - Check per-CPU cache for free elements.
- If found → return one.
- If empty → go to global freelist.
- If freelist empty → allocate a new slab (page of 4KB → 64 chunks of 64 bytes).
- Return chunk to caller.
Freelist pointer protection:
- Each freed chunk stores the address of the next free chunk, but encoded with a secret key.
- Overwriting that field with attacker data won’t work unless you know the key.
Comparison Table
| Feature | Old Heap (Pre-iOS 15) | Modern Heap (iOS 15+ / A12+) |
|---|---|---|
| Allocation granularity | Fixed size buckets (kalloc.16, kalloc.32, etc.) | Size + type-based buckets (kalloc_type) |
| Placement predictability | High (same-size objects side by side) | Low (same-type grouping + randomness) |
| Freelist management | Raw pointers in freed chunks (easy to corrupt) | Encoded pointers (safe-linking style) |
| Adjacent object control | Easy via sprays/frees (feng shui predictable) | Hard — typed zones separate attacker objects |
| Kernel data/code protections | Few hardware protections | PPL / SPTM protect page tables & code pages, and PAC protects pointers |
| Allocation reuse validation | None (freelist pointers raw) | zone_require / zone enforcement |
| Exploit reliability | High with heap sprays | Much lower, requires logic bugs or info leaks |
| Large allocations handling | All small allocations managed equally | Large ones bypass zones → handled via VM |
Modern Userland Heap (iOS, macOS — type-aware / xzone malloc)
In recent Apple OS versions (especially iOS 17+), Apple introduced a more secure userland allocator, xzone malloc (XZM). This is the user-space analog to the kernel’s kalloc_type, applying type awareness, metadata isolation, and memory tagging safeguards.
Goals & Design Principles
- Type segregation / type awareness: group allocations by type or usage (pointer vs data) to prevent type confusion and cross-type reuse.
- Metadata isolation: separate heap metadata (e.g. free lists, size/state bits) from object payloads so that out-of-bounds writes are less likely to corrupt metadata.
- Guard pages / redzones: insert unmapped pages or padding around allocations to catch overflows.
- Memory tagging (EMTE / MIE): work in conjunction with hardware tagging to detect use-after-free, out-of-bounds, and invalid accesses.
- Scalable performance: maintain low overhead, avoid excessive fragmentation, and support many allocations per second with low latency.
Architecture & Components
Below are the main elements in the xzone allocator:
Segment Groups & Zones
- Segment groups partition the address space by usage categories: e.g.
data,pointer_xzones,data_large,pointer_large. - Each segment group contains segments (VM ranges) that host allocations for that category.
- Associated with each segment is a metadata slab (separate VM area) that stores metadata (e.g. free/used bits, size classes) for that segment. This out-of-line (OOL) metadata ensures that metadata is not intermingled with object payloads, mitigating corruption from overflows.
- Segments are carved into chunks (slices) which in turn are subdivided into blocks (allocation units). A chunk is tied to a specific size class and segment group (i.e. all blocks in a chunk share the same size & category).
- For small / medium allocations, it will use fixed-size chunks; for large/huges, it may map separately.
Chunks & Blocks
- A chunk is a region (often several pages) dedicated to allocations of one size class within a group.
- Inside a chunk, blocks are slots available for allocations. Freed blocks are tracked via the metadata slab — e.g. via bitmaps or free lists stored out-of-line.
- Between chunks (or within), guard slices / guard pages may be inserted (e.g. unmapped slices) to catch out-of-bounds writes.
Type / Type ID
- Every allocation site (or call to malloc, calloc, etc.) is associated with a type identifier (a
malloc_type_id_t) which encodes what kind of object is being allocated. That type ID is passed to the allocator, which uses it to select which zone / segment to serve the allocation. - Because of this, even if two allocations have the same size, they may go into entirely different zones if their types differ.
- In early iOS 17 versions, not all APIs (e.g. CFAllocator) were fully type-aware; Apple addressed some of those weaknesses in iOS 18.
Allocation & Freeing Workflow
Here is a high-level flow of how allocation and deallocation operate in xzone:
- malloc / calloc / realloc / typed alloc is invoked with a size and type ID.
- The allocator uses the type ID to pick the correct segment group / zone.
- Within that zone/segment, it seeks a chunk that has free blocks of the requested size.
- It may consult local caches / per-thread pools or free block lists from metadata.
- If no free block is available, it may allocate a new chunk in that zone.
- The metadata slab is updated (free bit cleared, bookkeeping).
- If memory tagging (EMTE) is in play, the returned block gets a tag assigned, and metadata is updated to reflect its “live” state.
- When
free()is called:
- The block is marked as freed in metadata (via OOL slab).
- The block may be placed into a free list or pooled for reuse.
- Optionally, block contents may be cleared or poisoned to reduce data leaks or use-after-free exploitation.
- The hardware tag associated with the block may be invalidated or re-tagged.
- If an entire chunk becomes free (all blocks freed), the allocator may reclaim that chunk (unmap it or return to OS) under memory pressure.
Security Features & Hardening
These are the defenses built into modern userland xzone:
| Feature | Purpose | Notes |
|---|---|---|
| Metadata decoupling | Prevent overflow from corrupting metadata | Metadata lives in separate VM region (metadata slab) |
| Guard pages / unmapped slices | Catch out-of-bounds writes | Helps detect buffer overflows rather than silently corrupting adjacent blocks |
| Type-based segregation | Prevent cross-type reuse & type confusion | Even same-size allocations from different types go to different zones |
| Memory Tagging (EMTE / MIE) | Detect invalid access, stale references, OOB, UAF | xzone works in concert with hardware EMTE in synchronous mode (“Memory Integrity Enforcement”) |
| Delayed reuse / poisoning / zap | Reduce chance of use-after-free exploitation | Freed blocks may be poisoned, zeroed, or quarantined before reuse |
| Chunk reclamation / dynamic unmapping | Reduce memory waste and fragmentation | Entire chunks may be unmapped when unused |
| Randomization / placement variation | Prevent deterministic adjacency | Blocks in a chunk and chunk selection may have randomized aspects |
| Segregation of “data-only” allocations | Separate allocations that don’t store pointers | Reduces attacker control over metadata or control fields |
Interaction with Memory Integrity Enforcement (MIE / EMTE)
- Apple’s MIE (Memory Integrity Enforcement) is the hardware + OS framework that brings Enhanced Memory Tagging Extension (EMTE) into always-on, synchronous mode across major attack surfaces.
- xzone allocator is a fundamental foundation of MIE in user space: allocations done via xzone get tags, and accesses are checked by hardware.
- In MIE, the allocator, tag assignment, metadata management, and tag confidentiality enforcement are integrated to ensure that memory errors (e.g. stale reads, OOB, UAF) are caught immediately, not exploited later.
- If you like, I can also generate a cheat-sheet or diagram of xzone internals for your book. Do you want me to do that next?
- :contentReference[oai:20]{index=20}
(Old) Physical Use-After-Free via IOSurface
Ghidra Install BinDiff
Download BinDiff DMG from https://www.zynamics.com/bindiff/manual and install it.
Open Ghidra with ghidraRun and go to File –> Install Extensions, press the add button and select the path /Applications/BinDiff/Extra/Ghidra/BinExport and click OK and isntall it even if there is a version mismatch.
Using BinDiff with Kernel versions
- Go to the page https://ipsw.me/ and download the iOS versions you want to diff. These will be
.ipswfiles. - Decompress until you get the bin format of the kernelcache of both
.ipswfiles. You have information on how to do this on:
macOS Kernel Extensions & Kernelcache
- Open Ghidra with
ghidraRun, create a new project and load the kernelcaches. - Open each kernelcache so they are automatically analyzed by Ghidra.
- Then, on the project Window of Ghidra, right click each kernelcache, select
Export, select formatBinary BinExport (v2) for BinDiffand export them. - Open BinDiff, create a new workspace and add a new diff indicating as primary file the kernelcache that contains the vulnerability and as secondary file the patched kernelcache.
Finding the right XNU version
If you want to check for vulnerabilities in a specific version of iOS, you can check which XNU release version the iOS version uses at [https://www.theiphonewiki.com/wiki/kernel]https://www.theiphonewiki.com/wiki/kernel).
For example, the versions 15.1 RC, 15.1 and 15.1.1 use the version Darwin Kernel Version 21.1.0: Wed Oct 13 19:14:48 PDT 2021; root:xnu-8019.43.1~1/RELEASE_ARM64_T8006.
iMessage/Media Parser Zero-Click Chains
Imessage Media Parser Zero Click Coreaudio Pac Bypass
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
HackTricks

