iOS Exploiting
Reading time: 74 minutes
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 Exploit Mitigations
1. Code Signing / 运行时签名验证
Introduced early (iPhone OS → iOS) 这是基础的保护之一:所有可执行代码(apps、dynamic libraries、JIT-ed code、extensions、frameworks、caches)必须由根植于 Apple 信任链的证书链进行加密签名。在运行时,在将二进制加载到内存之前(或在跨某些边界跳转之前),系统会检查其签名。如果代码被修改(位翻转、补丁)或未签名,加载就会失败。
- Thwarts: exploit 链中的“经典 payload drop + execute”阶段;任意代码注入;修改已有二进制以插入恶意逻辑。
- Mechanism detail:
- Mach-O loader(和 dynamic linker)检查代码页、segments、entitlements、team IDs,并确保签名覆盖文件内容。
- 对于像 JIT caches 或动态生成代码这样的内存区域,Apple 强制要求页面被签名或通过特殊 API 验证(例如带有 code-sign 检查的
mprotect
)。 - 签名包含 entitlements 和标识符;OS 强制要求某些 API 或特权能力需要特定的 entitlements,且这些不能被伪造。
Example
假设 exploit 在某个进程中获得代码执行并尝试将 shellcode 写入 heap 并跳转到它。在 iOS 上,那页必须被标记为 executable **并且** 满足 code-signature 约束。由于 shellcode 不是用 Apple 的证书签名,跳转会失败或系统拒绝将该内存区域设为可执行。2. CoreTrust
Introduced around iOS 14+ era (or gradually in newer devices / later iOS) CoreTrust 是在运行时对二进制(包括系统和用户二进制)进行签名验证的子系统,验证时参照的是 Apple 的 root certificate,而不是依赖本地缓存的 userland trust stores。
- Thwarts: 安装后对二进制的篡改、试图替换或打补丁系统库或用户 app 的越狱技术;通过替换受信任二进制来欺骗系统。
- Mechanism detail:
- 不再信任本地的 trust database 或 certificate cache,CoreTrust 直接引用或验证指向 Apple 根的安全链或 intermediate certificates。
- 它确保对现有二进制在 filesystem 上的修改(例如替换)会被检测并拒绝。
- 在加载时将 entitlements、team IDs、code signing flags 等元数据绑定到二进制。
Example
越狱可能尝试替换 `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 强制将标记为可写(data)的页面设为 不可执行,将标记为可执行的页面设为 不可写。你不能简单地把 shellcode 写到 heap 或 stack 上然后执行它。
- Thwarts: 直接的 shellcode 执行;经典的 buffer-overflow → 跳转到注入的 shellcode。
- Mechanism detail:
- MMU / memory protection flags(通过 page tables)强制执行这种分离。
- 任何试图将一个可写页面改为可执行都会触发系统检查(要么被禁止,要么需要 code-sign 批准)。
- 在很多情况下,使页面可执行需要通过受限的 OS API,这些 API 会执行额外约束或检查。
Example
一次 overflow 将 shellcode 写到 heap。攻击者尝试 `mprotect(heap_addr, size, PROT_EXEC)` 使其可执行。但系统拒绝或验证新页面必须通过 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 的地址在不同运行间会变化。
- Thwarts: 为 ROP/JOP 硬编码 gadget 地址;静态 exploit 链;盲目跳转到已知偏移。
- Mechanism detail:
- 每个加载的 library / dynamic module 在随机偏移处重定位。
- stack 和 heap 的基指针被随机化(在一定熵范围内)。
- 有时其他区域(例如 mmap 分配)也会被随机化。
- 与信息泄露缓解结合,强制攻击者先 leak 一个地址或指针以在运行时发现基地址。
Example
一个 ROP 链期望 gadget 在 `0x….lib + offset`。但由于 `lib` 在每次运行时都被重新定位,硬编码的链会失败。exploit 必须先 leak 模块的基地址,然后再计算 gadget 地址。5. Kernel Address Space Layout Randomization (KASLR)
Introduced in iOS ~ (iOS 5 / iOS 6 timeframe) 类似于用户态的 ASLR,KASLR 在启动时随机化 kernel text 和其它内核结构的基地址。
- Thwarts: 依赖于固定内核代码或数据位置的内核级 exploit;静态内核 exploit。
- Mechanism detail:
- 每次启动时,内核的基地址在一个范围内随机化。
- 内核数据结构(如
task_structs
、vm_map
等)也可能被重新定位或偏移。 - 攻击者必须先 leak 内核指针或利用信息泄露漏洞来计算偏移,才能劫持内核结构或代码。
Example
一个本地漏洞试图破坏内核函数指针(例如 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 页的完整性(通过 hash 或 checksum)。如果检测到在允许的窗口外有篡改(补丁、inline hooks、代码修改),它会触发 kernel panic 或重启。
- Thwarts: 持久化的内核补丁(修改内核指令)、inline hooks、静态函数覆盖。
- Mechanism detail:
- 一个硬件或固件模块监控内核 text 区域。
- 它周期性或按需对页面重新哈希并与预期值比较。
- 如果在非正常的更新窗口出现不匹配,它会 panic 设备(以避免持久性恶意补丁)。
- 攻击者必须避开检测窗口或使用合法的补丁路径。
Example
一个 exploit 试图补丁某内核函数的函数序言(例如 `memcmp`)来拦截调用。但 KPP 注意到代码页的哈希不再匹配预期值并触发 kernel panic,设备在补丁稳定前就崩溃。7. Kernel Text Read‐Only Region (KTRR)
Introduced in modern SoCs (post ~A12 / newer hardware) KTRR 是硬件强制的机制:在启动早期一旦锁定 kernel text,它从 EL1(kernel)变为只读,从而阻止对代码页的进一步写入。
- Thwarts: 在启动后对内核代码的任何修改(例如 patching、就地代码注入)在 EL1 权限级别上。
- Mechanism detail:
- 在引导阶段(secure/bootloader 阶段),memory controller(或安全硬件单元)将包含内核 text 的物理页面标记为只读。
- 即使 exploit 获得完整内核权限,也无法写入这些页面来打补丁指令。
- 若要修改它们,攻击者必须先破坏 boot chain,或攻破 KTRR 本身。
Example
一个权限提升 exploit 跳入 EL1 并尝试在内核函数中写入 trampoline(例如在 `syscall` handler 中)。但因为这些页面被 KTRR 锁定为只读,写入失败或触发 fault,所以补丁无法应用。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 会被剥离并得到纯指针;如果不合法,指针会变成“poisoned”(或触发 fault)。
- 用于生成/验证 PAC 的密钥保存在特权寄存器(EL1,kernel)中,user mode 无法直接读取。
- 因为很多系统的指针并未使用全部 64 位(例如 48-bit 地址空间),高位是“空闲”的,可以在不改变有效地址的情况下存放 PAC。
Architectural Basis & Key Types
-
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 等可访问),user mode 无法访问。
-
PAC 是通过一个加密函数(ARM 建议使用 QARMA 作为算法)计算的,输入包括:
- 指针值(规范化部分)
- 一个 modifier(上下文值,像 salt)
- 秘密密钥
- 内部 tweak 逻辑 若计算出的 PAC 与存储在指针高位的值匹配,验证成功。
Instruction Families
命名约定为:PAC / AUT / XPAC,随后是域字母。
PACxx
指令用于 sign 指针并插入 PACAUTxx
指令用于 authenticate + strip(验证并移除 PAC)XPACxx
指令用于 strip(不验证)
Domains / suffixes:
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。这阻止了在不同帧或对象间简单地重用指针。类似于给 hash 添加一个 salt。
因此:
- modifier 是一个上下文值(另一个寄存器),该值会混入 PAC 的计算。典型选择:stack pointer(
SP
)、frame pointer,或某个对象 ID。 - 使用 SP 作为 modifier 常用于返回地址签名:PAC 会被绑定到特定的 stack frame。如果你试图在不同帧中重用 LR,modifier 变化,PAC 验证失败。
- 相同的指针值在不同 modifier 下签名会产生不同 PAC。
- modifier 并不需要是秘密,但理想情况下不应由攻击者控制。
- 对于那些没有有意义 modifier 的签名/验证指令,一些形式使用零或隐式常量。
Apple / iOS / XNU Customizations & Observations
- Apple 的 PAC 实现包括 每次启动的 diversifiers,使得 keys 或 tweaks 在每次启动时变化,防止跨启动重用。
- 他们还包含 跨域缓解,使得 user mode 签名的 PAC 不容易在 kernel mode 中重用等。
- 在 Apple M1 / Apple Silicon 上的逆向工程显示有 九种 modifier 类型 及 Apple 特定的用于 key 控制的系统寄存器。
- Apple 在许多内核子系统中使用 PAC:返回地址签名、内核数据指针完整性、签名的线程上下文等。
- Google Project Zero 展示了在强大的内存读/写 primitive 下,可以在 A12 世代设备上伪造内核 PAC(针对 A keys),但 Apple 修补了很多这些路径。
- 在 Apple 的系统中,有些 keys 在内核范围内是 全局的,而用户进程可能得到每进程的 key 随机性。
PAC Bypasses
- Kernel-mode PAC: theoretical vs real bypasses
- 由于内核 PAC keys 和逻辑被严格控制(特权寄存器、diversifiers、域隔离),伪造任意签名的内核指针非常困难。
- Azad 在 2020 年的 "iOS Kernel PAC, One Year Later" 报告中提到,在 iOS 12-13 中,他发现了一些部分绕过(signing gadgets、重用 signed states、未保护的间接分支),但没有发现通用的完全绕过方法。 bazad.github.io
- Apple 的 “Dark Magic” 自定义进一步缩小了可利用面(域切换、每-key 启用位)。 i.blackhat.com
- 已知存在一个 kernel PAC bypass CVE-2023-32424 在 Apple silicon(M1/M2)上,由 Zecao Cai 等人报告。 i.blackhat.com
- 但这些绕过通常依赖非常特定的 gadgets 或实现错误;它们并非通用绕过方法。
因此内核 PAC 被认为 非常健壮,尽管并非完美。
- User-mode / runtime PAC bypass techniques
这些更常见,利用 PAC 在 dynamic linking / runtime frameworks 中应用或使用的不完善之处。下面列出若干类别并给出示例。
2.1 Shared Cache / A key issues
- dyld shared cache 是一个包含系统 frameworks 和 libraries 的大型预链接 blob。因为它被广泛共享,shared cache 中的函数指针是“预签名”的,并被许多进程使用。攻击者将这些已签名指针视为 “PAC oracles”。
- 一些绕过技术尝试提取或重用存在于 shared cache 的 A-key 签名指针并在 gadgets 中重用它们。
- “No Clicks Required” 的演讲描述了如何在 shared cache 上构建一个 oracle 来推断相对地址,并将其与已签名指针结合以绕过 PAC。 saelo.github.io
- 还有用户态中从 shared libraries 导入的函数指针被发现对 PAC 的保护不足,使攻击者能在不改变签名的情况下获得函数指针。(Project Zero 的漏洞条目) bugs.chromium.org
2.2 dlsym(3) / dynamic symbol resolution
- 一个已知的绕过是调用
dlsym()
来获取一个 已签名的 函数指针(用 A-key 签名,diversifier 为零),然后直接使用它。因为dlsym
返回的是合法签名的指针,使用它可以绕过伪造 PAC 的需求。 - Epsilon 的 blog 详细说明了一些绕过如何利用这点:调用
dlsym("someSym")
会产生一个签名的指针并可用于间接调用。 blog.epsilon-sec.com - Synacktiv 的 “iOS 18.4 --- dlsym considered harmful” 描述了一个 bug:iOS 18.4 中通过
dlsym
解析的某些符号返回了错误签名(或具有 buggy diversifiers)的指针,从而导致非预期的 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 和动态重定位逻辑很复杂,有时会暂时将页面映射为可读写以执行重定位,然后再改回只读。攻击者利用这些时间窗口。Synacktiv 的演讲描述了 “Operation Triangulation”,这是一种基于时序的通过动态重定位绕过 PAC 的方法。 Synacktiv
- DYLD 页面现在用 SPRR / VM_FLAGS_TPRO 等保护标志保护。但早期版本的防护较弱。 Synacktiv
- 在 WebKit exploit 链中,DYLD loader 常常是 PAC 绕过的目标。幻灯片提到许多 PAC 绕过针对 DYLD loader(通过 relocation、interposer hooks)。 Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
- 在 userland exploit 链中,Objective-C runtime 方法如
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
- 在 pointer authentication 部分应用不完整的环境中,方法 / 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 不匹配而产生 fault。链路失败。
Project Zero 对 A12 (iPhone XS) 的分析展示了 Apple 的 PAC 是如何使用的,以及如果攻击者拥有内存读/写原语,伪造 PAC 的方法。
</details>
### 9. **Branch Target Identification (BTI)**
**Introduced with ARMv8.5 (later hardware)**
BTI 是一项硬件特性,用于检查 **indirect branch targets**:在执行 `blr` 或间接 call/jump 时,目标必须以 **BTI landing pad**(`BTI j` 或 `BTI c`)开头。跳转到缺少 landing pad 的 gadget 地址会触发异常。
LLVM 的实现指出了三种 BTI 指令变体以及它们如何映射到各种分支类型。
| BTI 变体 | 它允许的内容(哪些分支类型) | 典型放置位置 / 使用场景 |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | 作为 *call*-style 间接分支的目标(例如 `BLR`,或使用 X16/X17 的 `BR`) | 放在可能被间接调用的函数入口处 |
| **BTI J** | 作为 *jump*-style 分支的目标(例如用于尾调用的 `BR`) | 放在由 jump table 或尾调用可达的基本块开头 |
| **BTI JC** | 同时作为 C 和 J | 可被 call 或 jump 分支任一方式指向 |
- 在启用了 branch target enforcement 的代码中,编译器会在每个合法的间接分支目标(函数开头或可被 jump 到的基本块)插入 BTI 指令(C、J 或 JC),以保证间接分支只能成功跳转到这些位置。
- **Direct branches / calls**(即 固定地址的 `B`、`BL`)不受 BTI 限制。前提是假定代码页是受信任的且攻击者无法修改(因此 direct branches 是安全的)。
- 此外,**RET / return** 指令通常不受 BTI 限制,因为返回地址通常通过 PAC 或 return signing 机制受到保护。
#### Mechanism and enforcement
- 当 CPU 在被标记为“guarded / BTI-enabled”的页面中解码到一个 **indirect branch (BLR / BR)** 时,会检查目标地址的第一条指令是否为允许的有效 BTI(C、J 或 JC)。如果不是,会发生 **Branch Target Exception**。
- BTI 指令的编码设计为重用先前保留给 NOP 的 opcode(在较早的 ARM 版本中)。因此在没有 BTI 支持的硬件上,这些指令表现为 NOP,从而保持向后兼容。
- 添加 BTI 的编译器 pass 只在必要位置插入:可能被间接调用的函数,或被 jump 指向的基本块。
- 一些补丁和 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` 时检查目标并产生 fault,因为目标指令未包含有效的 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** 中引入的一项特性,它阻止 **privileged code**(EL1 或 EL2)对标记为 **user-accessible (EL0)** 的内存进行 **读取或写入**,除非 PAN 被显式禁用。
- 其核心思想:即使内核被诱骗或被攻破,也不能在不先 *清除* PAN 的情况下任意解引用用户空间指针,从而降低诸如 **ret2usr** 风格利用或滥用用户控制缓冲区的风险。
- 当 PAN 启用(PSTATE.PAN = 1)时,任何特权的 load/store 指令访问被标记为“在 EL0 可访问”的虚拟地址都会触发 **permission fault**。
- 当内核需要合法地访问用户空间内存(例如在复制数据到/从用户缓冲区时),必须 **临时禁用 PAN**(或使用“非特权 load/store”指令)以允许该访问。
- 在 ARM64 上的 Linux 中,PAN 支持大约在 2015 年被引入:内核补丁增加了对该特性的检测,并用会在访问用户内存周围清除 PAN 的变体替换了 `get_user` / `put_user` 等。
**关键细节 / 限制 / 漏洞**
- 如 Siguza 等人所指出,ARM 设计中的一个规范漏洞(或行为不明确)导致 **execute-only user mappings**(`--x`)可能**不会触发 PAN**。换言之,如果一个用户页面被标记为可执行但没有读权限,内核的读取尝试在某些实现下可能绕过 PAN,因为架构可能将“在 EL0 可访问”解释为需要可读权限,而不仅仅是可执行。这在某些 ARMv8+ 系统中导致了 PAN 绕过。
- 因此,如果 iOS / XNU 允许 execute-only 的用户页面(例如某些 JIT 或 code-cache 设置),内核在 PAN 启用时可能仍然意外地从这些页面读取数据。这在实际系统中是一个已知且微妙的可利用区域。
#### PXN (Privileged eXecute Never)
- **PXN** 是页表条目(leaf 或 block entries)中的一个页表标志,表示该页面在特权模式下(即 EL1 执行时)**不可执行**。
- PXN 防止内核(或任何特权代码)在控制流被劫持时跳转到或执行来自用户空间页面的指令。实际上,它阻止了内核级别的控制流重定向到用户内存。
- 与 PAN 结合使用,可确保:
1. 内核默认不能读取或写入用户空间数据(PAN)
2. 内核不能执行用户空间代码(PXN)
- 在 ARMv8 页表格式中,leaf 条目具有 `PXN` 位(以及用于非特权的 `UXN`)作为属性位。
因此,即使内核有一个被破坏的函数指针指向用户内存并尝试分支,PXN 位也会导致 fault。
#### Memory-permission model & how PAN and PXN map to page table bits
要理解 PAN / PXN 的工作原理,需要看 ARM 的转换和权限模型(简化):
- 每个 page 或 block 条目都有属性字段,包括用于访问权限的 **AP[2:1]**(读/写,特权与非特权)以及用于执行限制的 **UXN / PXN** 位。
- 当 PSTATE.PAN 为 1(启用)时,硬件会执行修改后的语义:对被标记为“在 EL0 可访问”的页面的特权访问会被禁止(产生 fault)。
- 由于前述漏洞,在某些实现中仅标记为可执行(无读权限)的页面可能不会被视为“在 EL0 可访问”,从而导致绕过 PAN。
- 当某页的 PXN 位被设置时,即使指令取自更高特权级别,也会禁止执行。
#### Kernel usage of PAN / PXN in a hardened OS (e.g. iOS / XNU)
在一个加固的内核设计中(例如 Apple 可能使用的):
- 内核默认启用 PAN(因此特权代码受到约束)。
- 在需要合法读取或写入用户缓冲区的路径(例如 syscall 缓冲区复制、I/O、读取/写入用户指针)中,内核会临时 **禁用 PAN** 或使用特殊指令来覆盖。
- 完成用户数据访问后,必须重新启用 PAN。
- PXN 通过页表强制执行:用户页面设置 PXN = 1(因此内核不能执行它们),内核页面不设置 PXN(因此内核代码可以执行)。
- 内核必须确保没有代码路径会导致控制流执行到用户内存区域(那会绕过 PXN)——因此依赖 “跳转到用户控制的 shellcode” 的利用链被阻断。
鉴于通过 execute-only 页面可能绕过 PAN,实际系统中 Apple 可能会禁用或不允许 execute-only 的用户页面,或对规范弱点进行补丁修补。
#### Attack surfaces, bypasses, and mitigations
- **PAN bypass via execute-only pages**:如上所述,规范存在漏洞:仅可执行(无读权限)的用户页面在某些实现中可能不被视为“在 EL0 可访问”,因此 PAN 不会阻止内核从这些页面读取。这为攻击者提供了通过“execute-only”段传递数据的非同寻常路径。
- **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 的做法,以及如何可能触发 fault。
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs"> </span></div>
// 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
如果内核没有在该用户页上设置 PXN,那么该分支可能会成功 —— 这将是不安全的。
如果内核在访问用户内存后忘记重新启用 PAN,就会出现一个窗口期,进一步的内核逻辑可能会意外地读/写任意用户内存。
如果用户指针指向一个只有执行权限(不可读/写)的执行-only 页面,根据 PAN 规范的缺陷,`ldr W2, [X1]` 即使在启用 PAN 的情况下也可能不会产生故障,这取决于实现,从而可能实现绕过利用。
</details>
<details>
<summary>示例</summary>
一个内核漏洞尝试获取用户提供的函数指针并在内核上下文中调用它(即 `call user_buffer`)。在 PAN/PXN 下,该操作被禁止或导致故障。
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
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`,忽略(掩去)最高字节。最高字节可被软件用于存储**元数据 / 标记位**。
- 这为软件提供了“免费”的同地址空间内用于在每个指针中嵌入一个字节标签的空间,而不改变指针所指向的内存位置。
- 架构保证在进行实际内存访问之前,加载、存储和指令获取会将指针的最高字节掩去(即去除标签)。
因此 TBI 将**逻辑指针**(pointer + tag)与用于内存操作的**物理地址**解耦。
#### Why TBI: Use cases and motivation
- **Pointer tagging / metadata**:你可以在最高字节中存储额外的元数据(例如对象类型、版本、边界、完整性标签)。当你随后使用该指针时,硬件会忽略标签位,所以你不需要在内存访问前手动剥离它。
- **Memory tagging / MTE (Memory Tagging Extension)**:TBI 是 MTE 构建的基础硬件机制。在 ARMv8.5 中,**Memory Tagging Extension** 使用指针的位 59:56 作为**逻辑标签**,并将其与存储在内存中的**分配标签**进行校验。
- **Enhanced security & integrity**:通过将 TBI 与 pointer authentication (PAC) 或运行时检查结合起来,可以不仅强制指针值正确,还要求标签也正确。攻击者在覆盖指针但没有正确标签时会产生标签不匹配。
- **Compatibility**:因为 TBI 是可选的且硬件会忽略标签位,现有未带标签的代码可以正常运行。对于旧代码来说,这些标签位实际上成为“无关紧要”的位。
#### Example
<details>
<summary>示例</summary>
一个函数指针在最高字节中包含了标签(比如 `0xAA`)。一次利用覆盖了指针的低位但忽略了标签,因此当内核验证或清理时,该指针因标签不匹配而失败或被拒绝。
</details>
---
### 12. **Page Protection Layer (PPL)**
**Introduced in late iOS / modern hardware (iOS ~17 / Apple silicon / high-end models)**(有报告显示 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 运作方式的推测:
#### Use of APRR / permission routing (APRR = Access Permission ReRouting)
- Apple 硬件使用一种叫 **APRR (Access Permission ReRouting)** 的机制,允许页表项(PTEs)包含小的索引,而不是完整的权限位。这些索引通过 APRR 寄存器映射为实际权限。这允许按域动态重映射权限。
- PPL 利用 APRR 在内核上下文中隔离特权:只有 PPL 域被允许更新索引与实际权限之间的映射。也就是说,当非 PPL 内核代码写入 PTE 或尝试翻转权限位时,APRR 逻辑会拒绝(或强制只读映射)。
- PPL 代码本身运行在受限区域(例如 `__PPLTEXT`),该区域在入口门开启之前通常是不可执行或不可写的。内核调用 PPL 入口点(“PPL 例程”)以执行敏感操作。
#### Gate / Entry & Exit
- 当内核需要修改受保护页面(例如更改内核代码页的权限或修改页表)时,会调用 PPL 包装例程,该例程进行验证,然后切换到 PPL 域。在该域之外,受保护页面对主内核实际上是只读或不可修改的。
- 在 PPL 入口期间,APRR 映射会被调整,使 PPL 区域内的内存页在 PPL 内部被设置为**可执行且可写**。退出后,它们会被恢复为只读/不可写。这确保只有经过详审的 PPL 例程可以写入受保护页面。
- 在 PPL 之外,内核代码尝试写入这些受保护页面会因为该代码域的 APRR 映射不允许写入而产生 fault(权限被拒绝)。
#### Protected page categories
PPL 通常保护的页面包括:
- 页表结构(转换表项、映射元数据)
- 内核代码页,特别是包含关键逻辑的页面
- 代码签名元数据(trust caches、签名 blob)
- entitlement 表、签名强制表
- 其他高价值内核结构,修改这些页面会允许绕过签名检查或操纵凭证
其理念是,即使内核内存完全被控制,攻击者也不能简单地打补丁或重写这些页面,除非他们也破坏 PPL 例程或绕过 PPL。
#### Known Bypasses & Vulnerabilities
1. **Project Zero’s PPL bypass (stale TLB trick)**
- Project Zero 发布的公开分析描述了一个涉及**陈旧 TLB 条目**的绕过方法。
- 思路如下:
1. 分配两个物理页 A 和 B,并将它们标记为 PPL 页(因此受保护)。
2. 映射两个虚拟地址 P 和 Q,它们的 L3 转换表页分别来自 A 和 B。
3. 启动一个线程不断访问 Q,保持其 TLB 条目活跃。
4. 调用 `pmap_remove_options()` 来移除从 P 开始的映射;由于一个 bug,代码错误地移除了 P 和 Q 的 TTE,但仅失效了 P 的 TLB 条目,留下 Q 的陈旧条目仍然生效。
5. 重新使用 B(Q 的表)来映射任意内存(例如 PPL 受保护页面)。因为陈旧的 TLB 条目仍为该上下文映射 Q 的旧映射,所以该映射在该上下文中仍然有效。
6. 通过这种方式,攻击者可以在不通过 PPL 接口的情况下,放置对 PPL 受保护页面的可写映射。
- 该利用需要对物理映射和 TLB 行为进行精细控制。它表明依赖 TLB / 映射正确性的安全边界必须非常小心 TLB 失效和映射一致性。
- Project Zero 评论说,此类绕过很微妙且罕见,但在复杂系统中是可能的。不过,他们仍然认为 PPL 是一个可靠的缓解措施。
2. **Other potential hazards & constraints**
- 如果内核利用可以直接进入 PPL 例程(通过调用 PPL 包装器),则可能绕过限制。因此参数验证至关重要。
- PPL 代码本身的漏洞(例如算术溢出、边界检查错误)可以允许在 PPL 内部进行越界修改。Project Zero 观察到 `pmap_remove_options_internal()` 中的此类 bug 在他们的绕过中被利用。
- PPL 边界不可逆地绑定到硬件强制(APRR、内存控制器),因此它的强度取决于硬件实现的正确性。
#### Example
<details>
<summary>代码示例</summary>
这里是一个简化的伪代码 / 逻辑,展示内核如何调用 PPL 来修改受保护页面:
</details>
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs">c</span></div>
```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
内核利用(kernel exploit)可能试图覆盖 entitlement table,或通过修改内核签名 blob 来禁用 code-sign enforcement。因为该页受 PPL 保护,除非通过 PPL 接口,否则写入会被阻止。所以即便获得了内核代码执行,也无法任意绕过 code-sign 限制或任意修改凭证数据。 在 iOS 17+ 上,某些设备使用 SPTM 进一步隔离由 PPL 管理的页面。PPL → SPTM / Replacements / Future
- 在 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 架构很可能将更多策略执行移到内核控制之外的、更高特权的监控器中,从而进一步缩小信任边界。
MTE | EMTE | MIE
下面是 EMTE 在 Apple 的 MIE 配置下的一个更高层次的工作描述:
- Tag assignment
- 当内存被分配(例如在内核或用户空间通过安全分配器)时,会给该块分配一个 secret tag。
- 返回给用户或内核的指针在高位包含该 tag(使用 TBI / top byte ignore 机制)。
- Tag checking on access
- 每当使用指针执行 load 或 store 时,硬件会检查指针的 tag 是否与内存块的 tag(分配 tag)匹配。若不匹配,会立即产生 fault(因为是同步的)。
- 因为是同步检测,所以不存在“延迟检测”的窗口。
- Retagging on free / reuse
- 当内存被释放时,分配器会更改该块的 tag(因此旧指针上的旧 tag 将不再匹配)。
- 因此,use-after-free 指针在访问时会拥有过时的 tag 而导致不匹配。
- Neighbor-tag differentiation to catch overflows
- 相邻的分配会被赋予不同的 tag。如果缓冲区溢出越过边界写入到相邻内存,tag 不匹配会导致 fault。
- 这在捕获跨边界的小型 overflow 时尤其有效。
- Tag confidentiality enforcement
- Apple 必须防止 tag 值被 leaked(因为如果攻击者知道该 tag,他们就可以构造带有正确 tag 的指针)。
- 他们加入了保护(微架构 / 规范化控制)以避免通过 side-channel 漏出 tag 位。
- Kernel and user-space integration
- Apple 不仅在用户空间使用 EMTE,也在内核 / 操作系统关键组件中使用(以防止内核内存损坏)。
- 硬件/OS 保证即使在内核代表用户空间执行时,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**: 如果溢出停留在相同的分配内(没有越过边界)且标签保持相同,标签不匹配机制不会捕获它。
- **Tag width limitation**: 可用于标签的位非常有限(例如 4 位或很小的域)——命名空间受限。
- **Side-channel leak**: 如果标签位可以被 leaked(通过 cache / speculative execution),攻击者可能会获取有效标签并绕过保护。Apple 的标签保密性强制旨在缓解此类问题。
- **Performance overhead**: 每次 load/store 的标签检查都会增加成本;Apple 必须在硬件上优化以将开销压低。
- **Compatibility & fallback**: 在不支持 EMTE 的旧硬件或部分区域,必须存在回退机制。Apple 声称只有在硬件支持的设备上才启用 MIE。
- **Complex allocator logic**: 分配器必须管理标签、重新标记(retagging)、对齐边界,并避免错误的标签碰撞。分配器逻辑中的漏洞可能引入新的漏洞。
- **Mixed memory / hybrid areas**: 部分内存可能保持未打标签(legacy),这会使互操作更复杂。
- **Speculative / transient attacks**: 与许多微架构防护一样,推测执行或微操作融合可能暂时绕过检查或泄露标签位。
- **Limited to supported regions**: Apple 可能仅在选择的高风险区域(内核、关键安全子系统)强制执行 EMTE,而非全局强制。
---
## Key enhancements / differences compared to standard 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,避免性能陷阱并关闭侧信道漏洞。
---
## 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**
- 当内存被分配(例如在内核或通过安全分配器在用户空间)时,会为该块分配一个**秘密标签**。
- 返回给用户或内核的指针在高位包含该标签(使用 TBI / top byte ignore mechanisms)。
2. **Tag checking on access**
- 每当使用指针执行 load 或 store 时,硬件会检查指针的标签是否与内存块的标签(分配标签)匹配。如果不匹配,则立即故障(因为是同步的)。
- 由于采用同步模式,不存在“延迟检测”窗口。
3. **Retagging on free / reuse**
- 当内存被释放时,分配器会更改该块的标签(使旧指针上的旧标签不再匹配)。
- 因此,use-after-free 指针在访问时会因标签过时而不匹配。
4. **Neighbor-tag differentiation to catch overflows**
- 相邻的分配会被赋予不同的标签。如果缓冲区溢出溢入邻居的内存,标签不匹配会导致故障。
- 这在捕捉越过边界的小型溢出时尤其有效。
5. **Tag confidentiality enforcement**
- Apple 必须防止标签值被泄露(因为如果攻击者知道标签,就能伪造携带正确标签的指针)。
- 他们包含了保护措施(微架构/推测执行控制)以避免标签位的侧信道泄露。
6. **Kernel and user-space integration**
- Apple 不仅在用户空间使用 EMTE,也在内核/操作系统关键组件中使用(以保护内核免受内存损坏)。
- 硬件/OS 确保标签规则即便在内核代表用户空间执行时也会被应用。
因为 EMTE 集成在 MIE 中,Apple 在关键攻击面上以同步模式启用 EMTE,而非作为可选或调试模式。
---
## Exception handling in XNU
当发生 **exception**(例如,`EXC_BAD_ACCESS`、`EXC_BAD_INSTRUCTION`、`EXC_CRASH`、`EXC_ARM_PAC` 等),XNU 内核的 **Mach 层**负责在将其转换为 UNIX 风格的 **signal**(如 `SIGSEGV`、`SIGBUS`、`SIGILL` 等)之前拦截该异常。
这个过程涉及多个层次的异常传递和处理,才会到达用户空间或被转换为 BSD 信号。
### Exception Flow (High-Level)
1. **CPU triggers a synchronous exception** (e.g., invalid pointer dereference, PAC failure, illegal instruction, etc.).
2. **Low-level trap handler** runs (`trap.c`, `exception.c` in XNU source).
3. The trap handler calls **`exception_triage()`**, the core of the Mach exception handling.
4. `exception_triage()` decides how to route the exception:
- First to the **thread's exception port**.
- Then to the **task's exception port**.
- Then to the **host's exception port** (often `launchd` or `ReportCrash`).
If none of these ports handle the exception, the kernel may:
- **Convert it into a BSD signal** (for user-space processes).
- **Panic** (for kernel-space exceptions).
### Core Function: `exception_triage()`
函数 `exception_triage()` 会将 Mach 异常沿可能的处理链向上路由,直到某个处理器处理它或最终致命为止。它在 `osfmk/kern/exception.c` 中定义。
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs">c</span></div>
```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 (which exceptions it wants to receive)
- A port name (Mach port to receive messages)
- A behavior (how the kernel sends the message)
- A flavor (which thread state to include)
Debuggers and Exception Handling
A debugger (e.g., LLDB) sets an exception port on the target task or thread, usually using task_set_exception_ports()
.
When an exception occurs:
- The Mach message is sent to the debugger process.
- The debugger can decide to handle (resume, modify registers, skip instruction) or not handle the exception.
- If the debugger doesn't handle it, the exception propagates to the next level (task → host).
Flow of EXC_BAD_ACCESS
-
Thread dereferences invalid pointer → CPU raises Data Abort.
-
Kernel trap handler calls
exception_triage(EXC_BAD_ACCESS, ...)
. -
Message sent to:
-
Thread port → (debugger can intercept breakpoint).
-
If debugger ignores → Task port → (process-level handler).
-
If ignored → Host port (usually ReportCrash).
- If no one handles →
bsd_exception()
translates toSIGSEGV
.
PAC Exceptions
When Pointer Authentication (PAC) fails (signature mismatch), a special Mach exception is raised:
EXC_ARM_PAC
(type)- Codes may include details (e.g., key type, pointer type).
If the binary has the flag TFRO_PAC_EXC_FATAL
, the kernel treats PAC failures as fatal, bypassing debugger interception. This is to prevent attackers from using debuggers to bypass PAC checks and it's enabled for platform binaries.
Software Breakpoints
A software breakpoint (int3
on x86, brk
on ARM64) is implemented by causing a deliberate fault.
The debugger catches this via the exception port:
- Modifies instruction pointer or memory.
- Restores original instruction.
- Resumes execution.
This same mechanism is what allows you to "catch" a PAC exception --- unless TFRO_PAC_EXC_FATAL
is set, in which case it never reaches the debugger.
Conversion to BSD Signals
If no handler accepts the exception:
-
Kernel calls
task_exception_notify() → bsd_exception()
. -
This maps Mach exceptions to signals:
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
→ Core ofexception_triage()
,exception_deliver_*()
. -
bsd/kern/kern_sig.c
→ Signal delivery logic. -
osfmk/arm64/trap.c
→ Low-level trap handlers. -
osfmk/mach/exc.h
→ Exception codes and structures. -
osfmk/kern/task.c
→ Task exception port setup.
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
The kernel used a zone allocator (kalloc
) divided into fixed-size "zones."
Each zone only stores allocations of a single size class.
From the screenshot:
Zone Name | Element Size | Example Use |
---|---|---|
default.kalloc.16 | 16 bytes | 非常小的内核结构体、指针。 |
default.kalloc.32 | 32 bytes | 小型结构体、对象头。 |
default.kalloc.64 | 64 bytes | IPC messages、微小的内核缓冲区。 |
default.kalloc.128 | 128 bytes | 中等对象,例如 OSObject 的一部分。 |
… | … | … |
default.kalloc.1280 | 1280 bytes | 大型结构,IOSurface/图形元数据。 |
How it worked:
- Each allocation request gets rounded up to the nearest zone size.
(E.g., a 50-byte request lands in the
kalloc.64
zone). - Memory in each zone was kept in a free list — chunks freed by the kernel went back into that zone.
- If you overflowed a 64-byte buffer, you’d overwrite the next object in the same zone.
This is why heap spraying / feng shui was so effective: you could predict object neighbors by spraying allocations of the same size class.
The freelist
Inside each kalloc zone, freed objects weren’t returned directly to the system — they went into a freelist, a linked list of available chunks.
-
When a chunk was freed, the kernel wrote a pointer at the start of that chunk → the address of the next free chunk in the same zone.
-
The zone kept a HEAD pointer to the first free chunk.
-
Allocation always used the current HEAD:
-
Pop HEAD (return that memory to the caller).
-
Update HEAD = HEAD->next (stored in the freed chunk’s header).
-
Freeing pushed chunks back:
-
freed_chunk->next = HEAD
-
HEAD = freed_chunk
So the freelist was just a linked list built inside the freed memory itself.
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
由于一个 free chunk 的前 8 个字节 = freelist 指针,攻击者可以破坏它:
-
Heap overflow into an adjacent freed chunk → 覆盖其 “next” 指针。
-
Use-after-free write into a freed object → 覆盖其 “next” 指针。
然后,在下一次分配相同大小的对象时:
-
分配器会弹出被破坏的 chunk。
-
跟随攻击者提供的 “next” 指针。
-
返回指向任意内存的指针,从而可以实现 fake object primitives 或有针对性的覆盖。
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 塑造堆布局 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 →
OSData
maps tokalloc_type_osdata
zone (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[oaicite: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
.ipsw
files. - Decompress until you get the bin format of the kernelcache of both
.ipsw
files. 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 BinDiff
and 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 来分享黑客技巧。