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 / Runtime Signature Verification
Introduced early (iPhone OS → iOS) 这是基本的保护之一:all executable code(apps、dynamic libraries、JIT-ed code、extensions、frameworks、caches)必须由一个以 Apple 信任为根的证书链进行加密签名。运行时,在将二进制加载到内存之前(或在跨某些边界执行跳转之前),系统会检查其签名。如果代码被修改(bit-flipped、patched)或未签名,加载会失败。
- 阻止:“经典的 payload drop + execute” 阶段在 exploit chains 中;arbitrary code injection;修改现有二进制以插入恶意逻辑。
- 机制细节:
- Mach-O loader(以及 dynamic linker)检查代码页、segments、entitlements、team IDs,并确保证书覆盖文件内容。
- 对于像 JIT caches 或动态生成的代码这样的内存区域,Apple 强制要求页面被签名或通过特殊 API 验证(例如使用带有 code-sign 检查的
mprotect)。 - 签名包含 entitlements 和标识符;OS 强制要求某些 API 或特权能力需要特定的 entitlements,这些是无法伪造的。
示例
假设一个 exploit 在某个进程中获得了代码执行并尝试将 shellcode 写入 heap 并跳转到那里。在 iOS 上,该页面需要被标记为 executable **并且** 满足 code-signature 约束。由于该 shellcode 没有用 Apple 的证书签名,跳转会失败或系统会拒绝将该内存区域设置为 executable。2. CoreTrust
Introduced around iOS 14+ era (or gradually in newer devices / later iOS) CoreTrust 是在运行时对二进制(包括系统和用户二进制)进行 签名验证 的子系统,它针对的是 Apple 的根证书,而不是依赖于本地缓存的 userland trust stores。
- 阻止:post-install tampering of binaries,尝试替换或 patch 系统库或用户应用以进行 jailbreaking 的技术;通过替换受信任二进制来欺骗系统。
- 机制细节:
- 它不是信任本地的 trust database 或证书缓存,CoreTrust 会直接参考或验证到 Apple root 的安全证书链或验证中间证书链。
- 它确保对已存在二进制(例如在文件系统中)的修改能被检测并拒绝。
- 它在加载时将 entitlements、team IDs、code signing flags 与二进制绑定。
示例
一个 jailbreak 可能尝试用补丁版替换 `SpringBoard` 或 `libsystem` 以获得持久性。但是当 OS 的 loader 或 CoreTrust 检查时,会发现签名不匹配(或 entitlements 被修改),并拒绝执行。3. Data Execution Prevention (DEP / NX / W^X)
Introduced in many OSes earlier; iOS had NX-bit / w^x for a long time DEP 强制要求被标记为 writable(用于数据)的页面为 non-executable,被标记为 executable 的页面为 non-writable。你不能简单地把 shellcode 写入 heap 或 stack 区域然后执行它。
- 阻止:直接执行 shellcode;经典的 buffer-overflow → 跳转到注入的 shellcode。
- 机制细节:
- MMU / memory protection flags(通过 page tables)强制实施该隔离。
- 任何试图将 writable 页面标记为 executable 都会触发系统检查(要么被禁止,要么需要 code-sign 批准)。
- 在许多情况下,使页面可执行需要通过执行会强制附加约束或检查的 OS API。
示例
一个 overflow 将 shellcode 写到 heap。攻击者尝试 `mprotect(heap_addr, size, PROT_EXEC)` 将其标记为 executable。但系统会拒绝或验证新页面必须通过 code-sign 约束(shellcode 无法满足)。4. Address Space Layout Randomization (ASLR)
Introduced in iOS ~4–5 era (roughly iOS 4–5 timeframe) ASLR 在每次进程启动时对关键内存区域进行随机化:libraries、heap、stack 等。gadgets 的地址在不同运行间会移动。
- 阻止:为 ROP/JOP 硬编码 gadget 地址;静态 exploit chains;盲目跳转到已知偏移。
- 机制细节:
- 每个加载的 library / dynamic module 都会被重定位到一个随机偏移。
- stack 和 heap 的基地址会被随机化(在一定的熵限制内)。
- 有时其他区域(例如 mmap 分配)也会被随机化。
- 结合信息泄露缓解,它迫使攻击者先泄露一个地址或指针以在运行时发现基地址。
示例
一个 ROP chain 期望 gadget 在 `0x….lib + offset`。但由于 `lib` 在每次运行时被不同地重定位,硬编码的 chain 会失败。exploit 必须先 leak 模块的基地址,才能计算 gadget 地址。5. Kernel Address Space Layout Randomization (KASLR)
Introduced in iOS ~ (iOS 5 / iOS 6 timeframe) 类似于用户态 ASLR,KASLR 在每次引导时随机化 kernel text 和其他内核结构的基址。
- 阻止:依赖固定内核代码或数据位置的 kernel-level exploits;静态内核 exploit。
- 机制细节:
- 每次启动时,内核基址会在一个范围内随机化。
- 内核数据结构(如
task_structs、vm_map等)也可能被重新定位或偏移。 - 攻击者必须先泄露内核指针或利用信息泄露漏洞来计算偏移,然后才能劫持内核结构或代码。
示例
一个本地漏洞试图损坏一个内核函数指针(例如在 `vtable` 中)位于 `KERN_BASE + offset`。但由于 `KERN_BASE` 是未知的,攻击者必须先 leak 它(例如通过 read primitive),然后再计算正确的地址进行破坏。6. Kernel Patch Protection (KPP / AMCC)
Introduced in newer iOS / A-series hardware (post around iOS 15–16 era or newer chips) KPP(也称 AMCC)持续监控内核 text 页的完整性(通过哈希或校验和)。如果检测到 tampering(补丁、inline hooks、代码修改)在不允许的时间窗口发生,它会触发 kernel panic 或重启。
- 阻止:对内核进行持久 patch(修改内核指令)、inline hooks、静态函数覆盖。
- 机制细节:
- 一个硬件或固件模块监控内核 text 区域。
- 它周期性或按需对页面重新哈希并与预期值比较。
- 如果在非正常更新窗口发现不匹配,会触发 panic(以避免持久的恶意 patch)。
- 攻击者必须避免检测窗口或使用合法的 patch 路径。
示例
一个 exploit 试图 patch 一个内核函数的前导(例如 `memcmp`)以拦截调用。但 KPP 注意到代码页的哈希不再匹配预期值并触发 kernel panic,设备在 patch 稳定前崩溃。7. Kernel Text Read‐Only Region (KTRR)
Introduced in modern SoCs (post ~A12 / newer hardware) KTRR 是一个硬件强制的机制:在引导早期一旦锁定了 kernel text,它从 EL1(内核)变为只读,从而防止后续对代码页的写入。
- 阻止:在引导后对内核代码的任何修改(例如 patch、就地注入代码)在 EL1 权限下。
- 机制细节:
- 在引导期间(secure/bootloader 阶段),memory controller(或一个安全硬件单元)将包含内核 text 的物理页面标记为只读。
- 即便 exploit 获得了完整的内核权限,也无法写入这些页面来 patch 指令。
- 要修改它们,攻击者必须首先破坏引导链,或篡改 KTRR 本身。
示例
一个提权 exploit 跳入 EL1 并试图在内核函数(例如 syscall handler)中写入 trampoline。但因为这些页面被 KTRR 锁定为只读,写入失败(或触发 fault),因此无法应用 patch。8. Pointer Authentication Codes (PAC)
Introduced with ARMv8.3 (hardware), Apple beginning with A12 / iOS ~12+
- PAC 是在 ARMv8.3-A 中引入的硬件特性,用来检测指针值(返回地址、函数指针、某些数据指针)的篡改,它把一个小的加密签名(一个“MAC”)嵌入到指针的高位空闲位中。
- 该签名(“PAC”)是对指针值加上一个 modifier(上下文值,例如 stack pointer 或某个区分数据)来计算的。因此同一指针值在不同上下文下会有不同的 PAC。
- 在使用时,在解引用或通过该指针分支之前,使用 authenticate 指令检查 PAC。如果有效,PAC 会被剥离并得到纯指针;如果无效,指针会被“poison”或触发 fault。
- 用于生成/验证 PAC 的密钥驻留在特权寄存器(EL1、kernel)中,用户态无法直接读取。
- 由于许多系统中并非全部 64 位都用于地址(例如 48-bit 地址空间),上位比特是“备用”的,可以在不改变有效地址的情况下存放 PAC。
Architectural Basis & Key Types
-
ARMv8.3 引入了 五个 128-bit 密钥(每个通过两个 64-bit 系统寄存器实现)用于指针认证。
-
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 与存储在指针高位的值匹配,认证成功。
Instruction Families
命名约定为:PAC / AUT / XPAC,然后跟域字母。
PACxx指令用于 sign 指针并插入 PACAUTxx指令用于 authenticate + strip(验证并移除 PAC)XPACxx指令用于 strip(不验证)
Domains / 后缀:
| Mnemonic | Meaning / Domain | Key / Domain | Example Usage in Assembly |
|---|---|---|---|
| PACIA | Sign instruction pointer with APIAKey | “I, A” | PACIA X0, X1 — sign pointer in X0 using APIAKey with modifier X1 |
| PACIB | Sign instruction pointer with APIBKey | “I, B” | PACIB X2, X3 |
| PACDA | Sign data pointer with APDAKey | “D, A” | PACDA X4, X5 |
| PACDB | Sign data pointer with APDBKey | “D, B” | PACDB X6, X7 |
| PACG / PACGA | Generic (non-pointer) signing with APGAKey | “G” | PACGA X8, X9, X10 (sign X9 with modifier X10 into X8) |
| AUTIA | Authenticate APIA-signed instruction pointer & strip PAC | “I, A” | AUTIA X0, X1 — check PAC on X0 using modifier X1, then strip |
| AUTIB | Authenticate APIB domain | “I, B” | AUTIB X2, X3 |
| AUTDA | Authenticate APDA-signed data pointer | “D, A” | AUTDA X4, X5 |
| AUTDB | Authenticate APDB-signed data pointer | “D, B” | AUTDB X6, X7 |
| AUTGA | Authenticate generic / blob (APGA) | “G” | AUTGA X8, X9, X10 (validate generic) |
| XPACI | Strip PAC (instruction pointer, no validation) | “I” | XPACI X0 — remove PAC from X0 (instruction domain) |
| XPACD | Strip PAC (data pointer, no validation) | “D” | XPACD X4 — remove PAC from data pointer in X4 |
有一些专用 / 别名形式:
PACIASP是PACIA X30, SP的简写(使用 SP 作为 modifier 签名 link register)AUTIASP是AUTIA X30, SP(使用 SP 认证 link register)- 还有组合形式如
RETAA、RETAB(authenticate-and-return)或BLRAA(authenticate & branch),存在于 ARM 扩展 / 编译器支持中。 - 也有零 modifier 变体:
PACIZA/PACIZB,其中 modifier 隐式为零,等等。
Modifiers
modifier 的主要目的是将 PAC 绑定到特定上下文,这样同一地址在不同上下文中签名会产生不同的 PAC。这阻止了在不同栈帧或对象间简单地重用指针。类似于给哈希加入 salt。
因此:
- modifier 是一个上下文值(另一个寄存器),在 PAC 计算中被混入。典型选择:stack pointer(
SP)、frame pointer,或某个 object ID。 - 使用 SP 作为 modifier 在返回地址签名中很常见:PAC 会绑定到特定的栈帧。如果你试图在不同帧中重用 LR,modifier 会变化,PAC 验证失败。
- 同一指针值在不同 modifier 下签名会得到不同的 PAC。
- modifier 不需要是秘密,但理想情况下不应为攻击者可控。
- 对于那些在没有有意义 modifier 的情形下对指针进行签名或验证的指令,有些形式使用零或隐式常量。
Apple / iOS / XNU Customizations & Observations
- Apple 的 PAC 实现包含 每次启动的 diversifiers,使得密钥或 tweak 在每次启动时变化,防止跨启动的重用。
- 他们还包含 跨域缓解,使得在 user mode 签名的 PAC 不容易在 kernel mode 中重用等。
- 在 Apple M1 / Apple Silicon 上,逆向显示存在 九种 modifier 类型 和 Apple 特定的用于密钥控制的系统寄存器。
- Apple 在许多内核子系统中使用 PAC:返回地址签名、内核数据中的指针完整性、签名线程上下文等。
- Google Project Zero 表明,在 kernel 中存在强大的内存读/写 primitive 时,可以伪造 kernel PAC(用于 A keys)在 A12-era 设备上,但 Apple 修补了许多这些路径。
- 在 Apple 的系统中,某些密钥是 全局跨内核 的,而 user 进程可能会得到每进程的密钥随机性。
PAC Bypasses
- Kernel-mode PAC: theoretical vs real bypasses
- 由于 kernel PAC 密钥和逻辑受到严格控制(特权寄存器、diversifiers、域隔离),伪造任意签名的 kernel 指针非常困难。
- Azad 在 2020 年的 “iOS Kernel PAC, One Year Later” 报告中指出,在 iOS 12-13 中,他发现了一些部分绕过(signing gadgets、reuse of signed states、unprotected indirect branches),但没有完整的通用绕过。 bazad.github.io
- Apple 的 “Dark Magic” 定制进一步缩小了可被利用的表面(域切换、per-key enabling bits)。i.blackhat.com
- 已知存在一个 kernel PAC bypass CVE-2023-32424,影响 Apple silicon(M1/M2),由 Zecao Cai 等人报告。 i.blackhat.com
- 但这些绕过通常依赖非常具体的 gadgets 或实现漏洞;它们并非通用的绕过方法。
因此 kernel PAC 被认为是 高度稳健 的,尽管并非完美。
- User-mode / runtime PAC bypass techniques
这些更为常见,并利用 PAC 在动态链接 / 运行时框架中应用或使用的不完美之处。下面列出几类并附示例。
2.1 Shared Cache / A key issues
- dyld shared cache 是一个大型的预链接系统框架和库的 blob。由于它被广泛共享,shared cache 内的函数指针是“预签名”的,随后被许多进程使用。攻击者将这些已签名的指针作为 “PAC oracle”。
- 一些绕过技术尝试提取或重用 shared cache 中已用 A-key 签名的指针并在 gadgets 中重用它们。
- “No Clicks Required” 的演讲描述了如何在 shared cache 上构建一个 oracle 来推断相对地址,并将其与已签名指针结合以绕过 PAC。 saelo.github.io
- 另外,userspace 中从 shared libraries 导入的函数指针曾被发现对 PAC 的保护不足,使攻击者可以在不更改其签名的情况下获取函数指针。(Project Zero bug 记录) bugs.chromium.org
2.2 dlsym(3) / dynamic symbol resolution
- 一个已知的绕过方法是调用
dlsym()来获取一个 already signed 的函数指针(用 A-key 签名,diversifier 为零),然后使用它。因为dlsym返回的是合法签名的指针,使用它就绕过了伪造 PAC 的需要。 - Epsilon 的博客详细说明了一些绕过如何利用这一点:调用
dlsym("someSym")返回一个签名指针,可用于间接调用。 blog.epsilon-sec.com - Synacktiv 的 “iOS 18.4 — dlsym considered harmful” 描述了一个 bug:在 iOS 18.4 上,通过
dlsym解析的一些符号返回的指针被错误地签名(或具有有问题的 diversifier),从而导致意外的 PAC 绕过。 Synacktiv - dyld 中的逻辑包括:当
result->isCode时,他们用__builtin_ptrauth_sign_unauthenticated(..., key_asia, 0)来对返回的指针进行签名,即上下文为零。 blog.epsilon-sec.com
因此,dlsym 是 user-mode PAC 绕过中经常被利用的向量。
2.3 Other DYLD / runtime relocations
- DYLD loader 和动态重定位逻辑很复杂,有时会临时将页面映射为 read/write 来执行重定位,然后再切回只读。攻击者会利用这些窗口。Synacktiv 的演讲描述了 “Operation Triangulation”,一种基于时序的通过动态重定位绕过 PAC 的方法。 Synacktiv
- DYLD 页面现在被 SPRR / VM_FLAGS_TPRO 等保护标志保护,但早期版本的防护较弱。 Synacktiv
- 在 WebKit exploit chains 中,DYLD loader 常常是 PAC 绕过的目标。幻灯片提到,许多 PAC 绕过针对 DYLD loader(通过重定位、interposer hooks)。 Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
- 在用户态的 exploit chains 中,Objective-C 运行时方法如
NSPredicate、NSExpression或NSInvocation被用来在不明显伪造指针的情况下走私控制调用。 - 在较早的 iOS(PAC 之前),一个 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
示例: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>
A buffer overflow 会覆写栈上的返回地址。攻击者写入目标 gadget 的地址但无法计算出正确的 PAC。当函数返回时,CPU 的 `AUTIA` 指令因 PAC 不匹配而发生异常。链路失败。
Project Zero 对 A12 (iPhone XS) 的分析展示了 Apple 如何使用 PAC,以及当攻击者拥有内存读/写原语时伪造 PAC 的方法。
</details>
### 9. **分支目标识别 (BTI)**
**引入于 ARMv8.5(后期硬件)**
BTI 是一种硬件特性,用于检查间接分支目标:在执行 `blr` 或间接调用/跳转时,目标必须以一个 BTI landing pad(`BTI j` 或 `BTI c`)开头。跳到不含落脚指令的 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 或返回签名机制保护。
#### 机制与强制执行
- 当 CPU 在一个被标记为“guarded / BTI-enabled”的页面中解码到一个 **间接分支(BLR / BR)** 时,它会检查目标地址的第一条指令是否是允许的 BTI(C、J 或 JC)。如果不是,则发生 **Branch Target Exception**。
- BTI 指令的编码设计为重用在早期 ARM 版本中保留给 NOP 的操作码。所以在不支持 BTI 的硬件上,这些指令会作为 NOP 起作用,从而保持向后兼容。
- 插入 BTI 的编译器 pass 只在必要处插入:可能被间接调用的函数,或可能被跳转到的基本块。
- 一些补丁和 LLVM 代码显示,BTI 并不会插入到所有基本块——只在那些有可能成为分支目标的基本块(例如来自 switch / jump 表的目标)插入。
#### BTI 与 PAC 的协同
PAC 保护指针值(源)——确保间接调用/返回链没有被篡改。
BTI 确保即使指针合法,也只能指向被正确标记的入口点。
合起来,攻击者既需要一个带有正确 PAC 的有效指针,也需要目标处有 BTI 放置。这增加了构造可利用 gadget 链的难度。
#### Example
<details>
<summary>Example</summary>
某个 exploit 试图 pivot 到一个在 `0xABCDEF` 的 gadget,但该地址处并未以 `BTI c` 开头。CPU 在执行 `blr x0` 时会检查目标并因没有有效落脚点而发生故障。因此许多 gadget 除非前缀有 BTI,否则变得不可用。
</details>
### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**引入于较新 ARMv8 扩展 / iOS 支持(用于加固内核)**
#### PAN (Privileged Access Never)
- **PAN** 是在 **ARMv8.1-A** 中引入的一项特性,它阻止 **特权代码**(EL1 或 EL2)在 PAN 未被显式禁用的情况下**读取或写入**被标记为 **用户可访问(EL0)** 的内存。
- 其思想是:即使内核被欺骗或被攻破,也不能在不先*清除* PAN 的情况下任意解引用用户空间指针,从而降低 `ret2usr` 风格利用或滥用用户控制缓冲区的风险。
- 当 PAN 启用(PSTATE.PAN = 1)时,任何特权的 load/store 指令访问被视为“accessible at EL0”的虚拟地址都会触发**权限错误**。
- 内核在需要合法访问用户空间内存时(例如复制数据到/从用户缓冲区),必须**临时禁用 PAN**(或使用“非特权 load/store”指令)来允许该访问。
- 在 ARM64 的 Linux 中,大约从 2015 年起引入了对 PAN 的支持:内核补丁检测该特性,并用在访问用户内存时清除 PAN 的变体替换了 `get_user` / `put_user` 等。
**关键细节 / 限制 / 漏洞**
- 正如 Siguza 等人指出的,ARM 规范中的一个设计缺陷(或歧义)导致 **execute-only user mappings**(`--x`)可能**不触发 PAN**。换句话说,如果用户页被标记为可执行但没有读权限,内核的读取尝试在某些实现下可能绕过 PAN,因为架构认为“accessible at EL0”需要可读权限而不仅仅是可执行。这在某些 ARMv8+ 实现中导致了 PAN 绕过。
- 因此,如果 iOS / XNU 允许 execute-only 用户页(例如某些 JIT 或代码缓存设置),内核在 PAN 启用时可能仍然意外地从这些页读取数据。这在一些系统中是已知的可利用微妙点。
#### PXN (Privileged eXecute Never)
- **PXN** 是一个页表标志(出现在页表条目的 leaf 或 block 条目中),表示该页面在特权模式下(即 EL1 执行时)**不可执行**。
- PXN 阻止内核(或任何特权代码)跳转到或从用户空间页面执行指令。实际上,它阻止了内核级别的控制流重定向进入用户内存。
- 与 PAN 结合使用,可以确保:
1. 内核默认不能读取或写入用户空间数据(PAN)
2. 内核不能执行用户空间代码(PXN)
- 在 ARMv8 页表格式中,leaf 条目有一个 `PXN` 位(以及用于非特权的 `UXN`)在其属性位中。
因此即便内核有一个被破坏的函数指针指向用户内存并尝试分支过去,PXN 位也会导致异常。
#### 内存权限模型 & PAN / PXN 如何映射到页表位
要理解 PAN / PXN 的工作,需要看 ARM 的地址转换与权限模型(简化):
- 每个页面或 block 条目都有属性字段,包括用于访问权限的 **AP[2:1]**(读/写、特权 vs 非特权)以及用于执行禁止的 **UXN / PXN** 位。
- 当 PSTATE.PAN = 1(启用)时,硬件会强制执行修改后的语义:对被标记为“可被 EL0 访问”的页面(即用户可访问)的特权访问将被禁止(触发故障)。
- 由于前述漏洞的存在,那些仅被标记为可执行而不可读的页面在某些实现中可能不会被视为“accessible by EL0”,从而绕过 PAN。
- 当页面的 PXN 位被设置,即便指令抓取来自更高特权级,执行仍被禁止。
#### 内核在加固 OS(例如 iOS / XNU)中对 PAN / PXN 的使用
在加固内核设计中(如 Apple 可能采用的做法):
- 内核默认启用 PAN(因此特权代码受限)。
- 在确实需要读取或写入用户缓冲区的路径中(例如 syscall 的缓冲区复制、I/O、读/写用户指针),内核会临时**禁用 PAN**或使用特定指令来覆盖。
- 在完成用户数据访问后,必须重新启用 PAN。
- PXN 通过页表强制执行:用户页设置 PXN = 1(因此内核不能执行它们),内核页不设置 PXN(因此内核代码可执行)。
- 内核必须确保没有代码路径导致执行流进入用户内存区域(那会绕过 PXN)——因此依赖“跳入用户控制的 shellcode”的利用链被阻断。
由于存在可执行但不可读页面造成的 PAN 绕过,实际系统中 Apple 可能会禁用或不允许可执行-仅可执行的用户页面,或对规范弱点进行补丁处理。
#### 攻击面、绕过方式与缓解措施
- **通过 execute-only 页面绕过 PAN**:如前所述,规范允许出现缝隙:如果用户页面被标记为 execute-only(无读权限),在某些实现下 PAN 不会阻止内核从这些页面读取。这给攻击者一种通过“execute-only”段传递数据的非典型路径。
- **时间窗口利用**:如果内核为某个较长时间窗口禁用了 PAN,则可能被竞态或恶意路径利用在该窗口内进行未预期的用户内存访问。
- **忘记重新启用**:如果在某些代码路径中未能重新启用 PAN,后续的内核操作可能错误地访问用户内存。
- **PXN 配置错误**:如果页表未在用户页上设置 PXN 或错误映射用户代码页,内核可能被诱导去执行用户空间代码。
- **推测 / 侧信道**:类似于推测执行绕过,可能存在微架构副作用导致 PAN / PXN 检查的瞬态违反(但此类攻击高度依赖于具体 CPU 设计)。
- **复杂交互**:在更高级的特性(例如 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
If the kernel had **not** set PXN on that user page, then the branch might succeed — which would be insecure.
If the kernel forgets to re-enable PAN after user memory access, it opens a window where further kernel logic might accidentally read/write arbitrary user memory.
If the user pointer is into an execute-only page (user page with only execute permission, no read/write), under the PAN spec bug, `ldr W2, [X1]` might **not** fault even with PAN enabled, enabling a bypass exploit, depending on implementation.
</details>
<details>
<summary>Example</summary>
A kernel vulnerability tries to take a user-provided function pointer and call it in kernel context (i.e. `call user_buffer`). Under PAN/PXN, that operation is disallowed or faults.
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
TBI means the top byte (most-significant byte) of a 64-bit pointer is ignored by address translation. This lets OS or hardware embed **tag bits** in the pointer’s top byte without affecting the actual address.
- TBI stands for **Top Byte Ignore** (sometimes called *Address Tagging*). It is a hardware feature (available in many ARMv8+ implementations) that **ignores the top 8 bits** (bits 63:56) of a 64-bit pointer when performing **address translation / load/store / instruction fetch**.
- In effect, the CPU treats a pointer `0xTTxxxx_xxxx_xxxx` (where `TT` = top byte) as `0x00xxxx_xxxx_xxxx` for the purposes of address translation, ignoring (masking off) the top byte. The top byte can be used by software to store **metadata / tag bits**.
- This gives software “free” in-band space to embed a byte of tag in each pointer without altering which memory location it refers to.
- The architecture ensures that loads, stores, and instruction fetch treat the pointer with its top byte masked (i.e. tag stripped off) before performing the actual memory access.
Thus TBI decouples the **logical pointer** (pointer + tag) from the **physical address** used for memory operations.
#### Why TBI: Use cases and motivation
- **Pointer tagging / metadata**: You can store extra metadata (e.g. object type, version, bounds, integrity tags) in that top byte. When you later use the pointer, the tag is ignored at hardware level, so you don’t need to strip manually for the memory access.
- **Memory tagging / MTE (Memory Tagging Extension)**: TBI is the base hardware mechanism that MTE builds on. In ARMv8.5, the **Memory Tagging Extension** uses bits 59:56 of the pointer as a **logical tag** and checks it against an **allocation tag** stored in memory.
- **Enhanced security & integrity**: By combining TBI with pointer authentication (PAC) or runtime checks, you can force not just the pointer value but also the tag to be correct. An attacker overwriting a pointer without the correct tag will produce a mismatched tag.
- **Compatibility**: Because TBI is optional and tag bits are ignored by hardware, existing untagged code continues to operate normally. The tag bits effectively become “don’t care” bits for legacy code.
#### Example
<details>
<summary>Example</summary>
A function pointer included a tag in its top byte (say `0xAA`). An exploit overwrites the pointer low bits but neglects the tag, so when the kernel verifies or sanitizes, the pointer fails or is rejected.
</details>
---
### 12. **Page Protection Layer (PPL)**
**Introduced in late iOS / modern hardware (iOS ~17 / Apple silicon / high-end models)** (some reports show PPL circa macOS / Apple silicon, but Apple is bringing analogous protections to iOS)
- PPL is designed as an **intra-kernel protection boundary**: even if the kernel (EL1) is compromised and has read/write capabilities, **it should not be able to freely modify** certain **sensitive pages** (especially page tables, code-signing metadata, kernel code pages, entitlements, trust caches, etc.).
- It effectively creates a **“kernel within the kernel”** — a smaller trusted component (PPL) with **elevated privileges** that alone can modify protected pages. Other kernel code must call into PPL routines to effect changes.
- This reduces the attack surface for kernel exploits: even with full arbitrary R/W/execute in kernel mode, exploit code must also somehow get into the PPL domain (or bypass PPL) to modify critical structures.
- On newer Apple silicon (A15+ / M2+), Apple is transitioning to **SPTM (Secure Page Table Monitor)**, which in many cases replaces PPL for page-table protection on those platforms.
Here’s how PPL is believed to operate, based on public analysis:
#### Use of APRR / permission routing (APRR = Access Permission ReRouting)
- Apple hardware uses a mechanism called **APRR (Access Permission ReRouting)**, which allows page table entries (PTEs) to contain small indices, rather than full permission bits. Those indices are mapped via APRR registers to actual permissions. This allows dynamic remapping of permissions per domain.
- PPL leverages APRR to segregate privilege within kernel context: only the PPL domain is permitted to update the mapping between indices and effective permissions. That is, when non-PPL kernel code writes a PTE or tries to flip permission bits, the APRR logic disallows it (or enforces read-only mapping).
- PPL code itself runs in a restricted region (e.g. `__PPLTEXT`) which is normally non-executable or non-writable until entry gates temporarily allow it. The kernel calls PPL entry points (“PPL routines”) to perform sensitive operations.
#### Gate / Entry & Exit
- When the kernel needs to modify a protected page (e.g. change permissions of a kernel code page, or modify page tables), it calls into a **PPL wrapper** routine, which does validation and then transitions into the PPL domain. Outside that domain, the protected pages are effectively read-only or non-modifiable by the main kernel.
- During PPL entry, the APRR mappings are adjusted so that memory pages in the PPL region are set to **executable & writable** within PPL. Upon exit, they are returned to read-only / non-writable. This ensures that only well-audited PPL routines can write to protected pages.
- Outside PPL, attempts by kernel code to write to those protected pages will fault (permission denied) because the APRR mapping for that code domain doesn’t permit writing.
#### Protected page categories
The pages that PPL typically protects include:
- Page table structures (translation table entries, mapping metadata)
- Kernel code pages, especially those containing critical logic
- Code-sign metadata (trust caches, signature blobs)
- Entitlement tables, signature enforcement tables
- Other high-value kernel structures where a patch would allow bypassing signature checks or credentials manipulation
The idea is that even if the kernel memory is fully controlled, the attacker cannot simply patch or rewrite these pages, unless they also compromise PPL routines or bypass PPL.
#### Known Bypasses & Vulnerabilities
1. **Project Zero’s PPL bypass (stale TLB trick)**
- A public writeup by Project Zero describes a bypass involving **stale TLB entries**.
- The idea:
1. Allocate two physical pages A and B, mark them as PPL pages (so they are protected).
2. Map two virtual addresses P and Q whose L3 translation table pages come from A and B.
3. Spin a thread to continuously access Q, keeping its TLB entry alive.
4. Call `pmap_remove_options()` to remove mappings starting at P; due to a bug, the code mistakenly removes the TTEs for both P and Q, but only invalidates the TLB entry for P, leaving Q’s stale entry live.
5. Reuse B (page Q’s table) to map arbitrary memory (e.g. PPL-protected pages). Because the stale TLB entry still maps Q’s old mapping, that mapping remains valid for that context.
6. Through this, the attacker can put writable mapping of PPL-protected pages in place without going through PPL interface.
- This exploit required fine control of physical mapping and TLB behavior. It demonstrates that a security boundary relying on TLB / mapping correctness must be extremely careful about TLB invalidations and mapping consistency.
- Project Zero commented that bypasses like this are subtle and rare, but possible in complex systems. Still, they regard PPL as a solid mitigation.
2. **Other potential hazards & constraints**
- If a kernel exploit can directly enter PPL routines (via calling the PPL wrappers), it might bypass restrictions. Thus argument validation is critical.
- Bugs in the PPL code itself (e.g. arithmetic overflow, boundary checks) can allow out-of-bounds modifications inside PPL. Project Zero observed that such a bug in `pmap_remove_options_internal()` was exploited in their bypass.
- The PPL boundary is irrevocably tied to hardware enforcement (APRR, memory controller), so it's only as strong as the hardware implementation.
#### Example
<details>
<summary>Code Example</summary>
Here’s a simplified pseudocode / logic showing how a kernel might call into PPL to modify protected pages:
```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
内核可以执行许多正常操作,但只有通过 ppl_call_* 例程才能更改受保护的映射或修补代码。
示例
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) 和 Secure Page Table Monitor (SPTM) 强制执行已签名且受信任代码的执行 … PPL 管理页表权限覆盖 … Secure Page Table Monitor 在受支持的平台上取代 PPL。”
- SPTM 架构可能将更多策略执行移入内核控制之外、更高特权的监控器中,进一步缩小可信边界。
MTE | EMTE | MIE
下面是 EMTE 在 Apple 的 MIE 设置下如何工作的高层描述:
- 标记分配
- 当内存被分配(例如在内核或用户空间通过安全分配器)时,会为该内存块分配一个 secret tag。
- 返回给用户或内核的指针在高位包含该 tag(使用 TBI / top byte ignore mechanisms)。
- 访问时的标记检查
- 每当使用指针执行 load 或 store 时,硬件会检查指针的 tag 是否与内存块的 tag(分配 tag)匹配。如果不匹配,会立即产生 fault(因为是 synchronous)。
- 由于这是同步的,因此不存在“delayed detection”窗口。
- 释放/重用时的重标记
- 当内存被释放时,分配器会更改该块的 tag(因此带有旧 tag 的指针将不再匹配)。
- 因此 use-after-free 指针在访问时会有过时的 tag 并导致不匹配。
- 邻近标记区分以捕获溢出
- 相邻的分配会被赋予不同的 tag。如果缓冲区溢出泄入邻近内存,tag 不匹配会导致 fault。
- 这在捕获跨边界的小型溢出方面尤其有效。
- 标记机密性强制
- Apple 必须防止 tag 值被 leaked(因为如果攻击者知道了 tag,他们可以构造带有正确 tag 的指针)。
- 他们包含保护措施(微架构/推测控制)以避免通过侧信道泄露 tag 位。
- 内核与用户空间集成
- Apple 不仅在用户空间使用 EMTE,也在内核/操作系统关键组件中使用(以防止内核内存损坏)。
- 硬件/OS 确保即使在内核代表用户空间执行时,tag 规则仍然适用。
示例
``` 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**: 如果溢出保持在相同的分配内(未跨越边界)且 tag 保持相同,tag mismatch 无法检测到它。
- **Tag width limitation**: 可用于 tag 的位数很少(例如 4 位,或小的命名空间),命名空间受限。
- **Side-channel leaks**: 如果 tag 位可以通过(cache / speculative execution)被 leaked,攻击者可能会学到有效的 tags 并绕过防护。Apple 的 tag confidentiality enforcement 旨在缓解此类问题。
- **Performance overhead**: 每次 load/store 的 tag 检查都会增加开销;Apple 必须在硬件上做优化以将开销压低。
- **Compatibility & fallback**: 在不支持 EMTE 的旧硬件或不支持的部分,必须存在回退机制。Apple 声称 MIE 只在有硬件支持的设备上启用。
- **Complex allocator logic**: 分配器必须管理 tags、retagging、对齐边界并避免 tag 冲突。分配器逻辑中的 bug 可能引入漏洞。
- **Mixed memory / hybrid areas**: 某些内存可能仍保持 untagged(遗留),使互操作变得更棘手。
- **Speculative / transient attacks**: 如同许多微架构防护,推测执行或微操作融合可能暂时绕过检查或泄露 tag 位(leak)。
- **Limited to supported regions**: Apple 可能只在选择的高风险区域(kernel、关键安全子系统)强制执行 EMTE,而不是全局强制。
---
## Key enhancements / differences compared to standard MTE
Here are the improvements and changes Apple emphasizes:
| 特性 | Original MTE | EMTE (Apple’s enhanced) / MIE |
|---|---|---|
| **Check mode** | 支持同步和异步模式。在异步模式下,tag mismatches 会被延后报告(有延迟) | Apple 默认坚持 **synchronous mode**——tag mismatches 立即被捕获,不允许延迟/竞态窗口。|
| **Coverage of non-tagged memory** | 对非-tagged 内存(例如 globals)的访问在某些实现中可能绕过检查 | EMTE 要求从 tagged 区域访问非-tagged 内存时也验证 tag 知识,使通过混合分配绕过更难。|
| **Tag confidentiality / secrecy** | Tags 可能通过侧信道被观察或 leaked | Apple 添加了 **Tag Confidentiality Enforcement**,试图防止 tag 值通过(speculative side-channels 等)被泄露。|
| **Allocator integration & retagging** | MTE 在很大程度上将分配器逻辑留给软件实现 | Apple 的 secure typed allocators(kalloc_type、xzone malloc 等)与 EMTE 集成:在内存分配或释放时,会以细粒度管理 tags。|
| **Always-on by default** | 在许多平台上,MTE 是可选或默认关闭的 | Apple 在支持的硬件(例如 iPhone 17 / A19)上对 kernel 及许多 user 进程默认启用 EMTE / MIE。|
因为 Apple 同时控制硬件和软件栈,它可以严格执行 EMTE、避免性能陷阱并修补侧信道漏洞。
---
## How EMTE works in practice (Apple / MIE)
Here’s a higher-level description of how EMTE operates under Apple’s MIE setup:
1. **Tag assignment**
- 当内存被分配(例如在 kernel 或用户空间通过 secure allocators)时,会为该块分配一个 **secret tag**。
- 返回给用户或 kernel 的指针在高位中包含该 tag(使用 TBI / top byte ignore mechanisms)。
2. **Tag checking on access**
- 无论何时使用指针执行 load 或 store,硬件都会检查指针的 tag 是否与内存块的 tag(allocation tag)匹配。如果不匹配,会立即发生 fault(因为是同步的)。
- 由于是同步模式,因此不存在“延迟检测”的窗口。
3. **Retagging on free / reuse**
- 当内存被释放时,分配器会改变该块的 tag(因此带有旧 tag 的指针将不再匹配)。
- 因此 use-after-free 指针在访问时会因为 stale tag 而 mismatch。
4. **Neighbor-tag differentiation to catch overflows**
- 相邻的分配会赋予不同的 tags。如果缓冲区溢出写入到邻居内存,tag mismatch 会导致 fault。
- 这对检测跨越边界的小型溢出尤其有效。
5. **Tag confidentiality enforcement**
- Apple 必须防止 tag 值被 leaked(因为如果攻击者知道了 tag,就可以构造带有正确 tag 的指针)。
- 他们包含了保护措施(微架构/推测相关控制)以避免 tag 位的侧信道泄露。
6. **Kernel and user-space integration**
- Apple 在不仅在用户态,而且在 kernel/OS 关键组件中使用 EMTE(以防护内核免受内存破坏)。
- 硬件/OS 确保即使在内核为用户态执行时,tag 规则依然适用。
因为 EMTE 集成于 MIE,Apple 在重要攻击面上以同步模式使用 EMTE,而不是作为可选或调试模式。
---
## Exception handling in XNU
当发生一个 **exception**(例如 `EXC_BAD_ACCESS`、`EXC_BAD_INSTRUCTION`、`EXC_CRASH`、`EXC_ARM_PAC` 等)时,XNU 内核的 **Mach layer** 负责在将其转换为 UNIX 风格的 **signal**(如 `SIGSEGV`、`SIGBUS`、`SIGILL` 等)之前拦截该异常。
这个过程涉及多层异常传播与处理,然后才到达用户空间或被转换为 BSD 信号。
### Exception Flow (High-Level)
1. **CPU 触发一个同步异常**(例如无效指针解引用、PAC failure、非法指令等)。
2. **低级 trap handler** 运行(在 XNU 源码中的 `trap.c`、`exception.c`)。
3. trap handler 调用 **`exception_triage()`**,这是 Mach 异常处理的核心。
4. `exception_triage()` 决定如何路由该异常:
- 首先送到 **thread 的 exception port**。
- 然后送到 **task 的 exception port**。
- 然后送到 **host 的 exception port**(通常是 `launchd` 或 `ReportCrash`)。
如果这些 port 都不处理该异常,内核可能会:
- **将其转换为 BSD 信号**(针对用户态进程)。
- **Panic**(针对内核态异常)。
### Core Function: `exception_triage()`
函数 `exception_triage()` 会将 Mach 异常沿着可能的处理链向上路由,直到某个处理者处理它,或直到异常最终致命。该函数定义在 `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) 可以注册 异常端口,异常消息会被发送到这些端口。
它们由 API 定义:
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
Each exception port has:
- A 掩码 (哪些异常希望接收)
- A 端口名 (接收消息的 Mach port)
- A 行为 (内核如何发送消息)
- A flavor (包含哪个线程状态)
Debuggers and Exception Handling
一个 debugger(例如 LLDB)会在目标 task 或 thread 上设置一个 exception port,通常使用 task_set_exception_ports()。
当发生异常时:
- Mach 消息会被发送到 debugger 进程。
- debugger 可以决定 处理(resume、修改寄存器、跳过指令)或 不处理 该异常。
- 如果 debugger 不处理,异常会传播到下一级(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 type、pointer type)。
如果二进制具有标志 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 | 信号 |
|---|---|
| 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 的设置。
旧内核堆(Pre-iOS 15 / Pre-A12 era)
内核使用一个 zone allocator(kalloc),被划分为固定大小的 “zones”。
每个 zone 只存储单一大小类的分配。
从截图:
| Zone Name | 元素大小 | 示例用途 |
|---|---|---|
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/图形元数据。 |
工作方式:
- 每个分配请求会被向上取整到最近的 zone 大小。
(例如,一个 50 字节的请求会落在
kalloc.64zone)。 - 每个 zone 的内存保存在一个 freelist 中 — 内核释放的块会回到该 zone。
- 如果你溢出了一个 64 字节的缓冲区,你会覆盖 同一 zone 中的下一个对象。
这就是为什么 heap spraying / feng shui 如此有效:你可以通过喷同一大小类的分配来预测对象邻居。
The freelist
在每个 kalloc zone 内,被释放的对象不会直接返回给系统 —— 它们进入 freelist,一个可用块的链表。
-
当一个块被释放时,内核会在该块的起始处写入一个指针 → 指向同一 zone 中下一个可用块的地址。
-
zone 保持一个 HEAD 指针,指向第一个空闲块。
-
分配总是使用当前的 HEAD:
-
Pop HEAD(将该内存返回给调用者)。
-
更新 HEAD = HEAD->next(存储在已释放块的头部)。
-
释放时将块推回:
-
freed_chunk->next = HEAD -
HEAD = freed_chunk
所以 freelist 只是一个构建在已释放内存内部的链表。
正常状态:
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
因为 the first 8 bytes of a free chunk = freelist pointer,攻击者可以破坏它:
-
Heap overflow 写入相邻的已释放 chunk → 覆盖其 “next” 指针。
-
Use-after-free 在已释放对象上写入 → 覆盖其 “next” 指针。
然后,在下一次分配相同大小的内存时:
-
分配器会弹出被篡改的 chunk。
-
跟随攻击者提供的 “next” 指针。
-
返回指向任意内存的指针,从而实现伪造对象原语或定向覆盖。
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.
现代内核堆 (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.
JSKit-Based Safari Chains and PREYHUNTER Stagers
Renderer RCE abstraction with JSKit
- Reusable entry: Recent in-the-wild chains abused a WebKit JIT bug (patched as CVE-2023-41993) purely to gain JavaScript-level arbitrary read/write. The exploit immediately pivots into a purchased framework called JSKit, so any future Safari bug only needs to deliver the same primitive.
- Version abstraction & PAC bypasses: JSKit bundles support for a wide range of iOS releases together with multiple, selectable Pointer Authentication Code bypass modules. The framework fingerprints the target build, selects the appropriate PAC bypass logic, and verifies every step (primitive validation, shellcode launch) before progressing.
- Manual Mach-O mapping: JSKit parses Mach-O headers directly from memory, resolves the symbols it needs inside dyld-cached images, and can manually map additional Mach-O payloads without writing them to disk. This keeps the renderer process in-memory only and evades code-signature checks tied to filesystem artifacts.
- Portfolio model: Debug strings such as “exploit number 7” show that the suppliers maintain multiple interchangeable WebKit exploits. Once the JS primitive matches JSKit’s interface, the rest of the chain is unchanged across campaigns.
Kernel bridge: IPC UAF -> code-sign bypass pattern
- Kernel IPC UAF (CVE-2023-41992): The second stage, still running inside the Safari context, triggers a kernel use-after-free in IPC code, re-allocates the freed object from userland, and abuses the dangling pointers to pivot into arbitrary kernel read/write. The stage also reuses PAC bypass material previously computed by JSKit instead of re-deriving it.
- Code-signing bypass (CVE-2023-41991): With kernel R/W available, the exploit patches the trust cache / code-signing structures so unsigned payloads execute as
system. The stage then exposes a lightweight kernel R/W service to later payloads. - Composed pattern: This chain demonstrates a reusable recipe that defenders should expect going forward:
WebKit renderer RCE -> kernel IPC UAF -> kernel arbitrary R/W -> code-sign bypass -> unsigned system stager
PREYHUNTER helper & watcher 模块
- Watcher anti-analysis: 一个专用的 watcher 二进制持续对设备进行分析并在检测到研究环境时中止 kill-chain。它检查
security.mac.amfi.developer_mode_status、是否存在diagnosticd控制台、地区设置是否为US或IL、jailbreak 痕迹(如 Cydia)、进程如bash、tcpdump、frida、sshd或checkrain、mobile AV apps(McAfee、AvastMobileSecurity、NortonMobileSecurity)、自定义 HTTP 代理设置以及自定义根 CA。任一检查失败都会阻止进一步的 payload 交付。 - Helper surveillance hooks: Helper 组件通过
/tmp/helper.sock与其他阶段通信,然后加载名为 DMHooker 和 UMHooker 的 hook 集。这些 hooks 钩取 VOIP 音频路径(录音存放于/private/var/tmp/l/voip_%lu_%u_PART.m4a),实现 system-wide keylogger、在无 UI 的情况下拍照,并 hook SpringBoard 以抑制这些操作通常会触发的通知。因此 helper 在部署诸如 Predator 之类的更重型 implant 之前,充当一个隐蔽的验证 + 轻度监视层。
WebKit DFG Store-Barrier UAF + ANGLE PBO OOB (iOS 26.1)
Webkit Dfg Store Barrier Uaf Angle Oob
iMessage/Media Parser Zero-Click Chains
Imessage Media Parser Zero Click Coreaudio Pac Bypass
References
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 来分享黑客技巧。


