# How to Use MachOKit for Mach-O Patching in iOS VMs: A Complete Guide

> Learn Mach-O patching in iOS VMs with MachOKit and vphone-cli. This guide shows you how to parse Mach-O binaries, find symbols, and convert addresses for firmware patching. Get started today.

- Repository: [Lakr/vphone-cli](https://github.com/Lakr233/vphone-cli)
- Tags: how-to-guide
- Published: 2026-09-09

---

**The vphone-cli project wraps MachOKit in a Swift helper layer that parses Mach-O binaries, locates symbols, and converts virtual addresses to file offsets for patching iOS firmware running in virtual machines.**

The ability to modify Mach-O binaries is essential when developing jailbreaks, debugging kernels, or customizing iOS firmware images. The **vphone-cli** repository by Lakr233 provides a production-ready implementation of this workflow, bundling the third-party **MachOKit** library as a Git submodule and exposing high-level Swift APIs for firmware manipulation. This article examines the exact architecture and code patterns used to patch Mach-O kernels inside iOS virtual machines.

## Mach-O Patching Architecture in vphone-cli

The project organizes Mach-O manipulation into three distinct layers, separating low-level parsing from high-level patch orchestration.

### The MachOKit Submodule Layer

At the foundation sits the **MachOKit** library, included as a path dependency in `vendor/MachOKit` and declared in [[`Package.swift`](https://github.com/Lakr233/vphone-cli/blob/main/Package.swift)](https://github.com/Lakr233/vphone-cli/blob/main/Package.swift) via `.package(path: "vendor/MachOKit")`. This layer handles raw Mach-O structures including load commands, section headers, and symbol table entries. All heavy lifting of binary parsing—walking `LC_SEGMENT_64` commands and interpreting `MachoHeader` layouts—occurs here without Swift-specific abstractions.

### The Swift Wrapper Layer

Bridging MachOKit to the rest of the codebase, [[`sources/FirmwarePatcher/Binary/MachOHelpers.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/FirmwarePatcher/Binary/MachOHelpers.swift)](https://github.com/Lakr233/vphone-cli/blob/main/sources/FirmwarePatcher/Binary/MachOHelpers.swift) defines lightweight structs (`MachOSegmentInfo`, `MachOSectionInfo`) and the `MachOParser` enum. These wrappers translate C-style MachOKit outputs into Swift-friendly types while preserving performance. The `MachOParser` type provides the primary API surface used by patcher logic, abstracting away pointer arithmetic and memory layout details.

### Kernel Patcher Implementation

The orchestration layer resides in [[`sources/FirmwarePatcher/Kernel/KernelPatcherBase.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/FirmwarePatcher/Kernel/KernelPatcherBase.swift)](https://github.com/Lakr233/vphone-cli/blob/main/sources/FirmwarePatcher/Kernel/KernelPatcherBase.swift). This file drives the patching workflow: loading firmware images into `Data` buffers, invoking `MachOParser` methods to locate targets, calculating file offsets, and persisting modified binaries back to disk. It represents the integration point where Mach-O analysis meets practical byte-level patching.

## Step-by-Step Mach-O Patching Workflow

Patching a Mach-O binary for an iOS VM requires translating virtual addresses to physical file offsets before writing modifications. The vphone-cli implementation follows this precise sequence:

1. **Load the binary** into a mutable `Data` buffer.
2. **Parse segments** using `MachOParser.parseSegments` to build a mapping of virtual addresses to file ranges.
3. **Locate target symbols** via `MachOParser.findSymbol(containing:in:)` to identify patch targets like kernel functions.
4. **Convert virtual addresses** to file offsets with `MachOParser.vaToFileOffset`—critical because Mach-O files are not memory-mapped 1:1 when loaded by the VM.
5. **Inject patch bytes** at the calculated offset using Swift's `replaceSubrange`.
6. **Write the modified image** back to the filesystem for deployment.

This workflow ensures that patches like NOP sleds, breakpoint instructions, or jailbreak payloads land at the correct physical location within the firmware file rather than misaligned memory addresses.

## Practical Implementation: Patching a Kernel Symbol

Below is a complete Swift example demonstrating the vphone-cli wrapper APIs in action. This snippet loads a kernel image, locates the `kernel_task` symbol, and replaces its first instruction with an ARM64 NOP.

```swift
import Foundation
import MachOKit
import vphone_cli

// Load the Mach-O binary into memory
let binaryData = try Data(contentsOf: URL(fileURLWithPath: "/path/to/kernel"))

// 1. Parse segment information to build VA-to-offset mappings
let segments = MachOParser.parseSegments(from: binaryData)
print("Found \(segments.count) segments")
segments.forEach { seg in
    print("\(seg.name) – VM: 0x\(String(seg.vmAddr, radix: 16)) size: \(seg.vmSize)")
}

// 2. Parse sections for fine-grained location (e.g., __TEXT,__text)
let sections = MachOParser.parseSections(from: binaryData)
if let textSection = sections["__TEXT,__text"] {
    print("Text section starts at 0x\(String(textSection.address, radix: 16))")
}

// 3. Locate the target symbol by name
if let symbolVA = MachOParser.findSymbol(containing: "kernel_task", in: binaryData) {
    print("kernel_task VA = 0x\(String(symbolVA, radix: 16))")
    
    // 4. Convert virtual address to file offset
    let fileOffset = MachOParser.vaToFileOffset(symbolVA, segments: segments)
    print("Corresponding file offset = \(fileOffset)")
    
    // 5. Apply the patch (ARM64 NOP: 0xD503201F)
    let nopBytes: [UInt8] = [0x1F, 0x20, 0x03, 0xD5]
    var mutable = binaryData
    mutable.replaceSubrange(fileOffset..<(fileOffset + nopBytes.count),
                            with: nopBytes)
    
    // 6. Write the patched binary to disk
    try mutable.write(to: URL(fileURLWithPath: "/tmp/patched_kernel"))
    print("Patched binary written successfully")
} else {
    print("Symbol not found – verify the name and Mach-O format")
}

```

### Key API Reference

Understanding these four wrapper methods unlocks the full MachOKit functionality within vphone-cli:

- **`MachOParser.parseSegments(from:)`** – Walks the load command table extracting `LC_SEGMENT_64` data, returning an array of `MachOSegmentInfo` containing `vmAddr`, `vmSize`, and `fileOffset`.
- **`MachOParser.parseSections(from:)`** – Enumerates sections within segments, returning a dictionary keyed by `"segment,section"` strings (e.g., `"__TEXT,__text"`).
- **`MachOParser.findSymbol(containing:in:)`** – Performs substring matching against the Mach-O symbol table using MachOKit's symbol traversal, returning the virtual address of the first match.
- **`MachOParser.vaToFileOffset(_:segments:)`** – Linear-searches the parsed segment list to map a runtime virtual address back to the raw file offset required for byte-level patching.

## Summary

- **vphone-cli** integrates **MachOKit** as a vendor submodule at `vendor/MachOKit`, exposing low-level Mach-O parsing through Swift wrappers.
- The **`MachOParser`** enum in [`sources/FirmwarePatcher/Binary/MachOHelpers.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/FirmwarePatcher/Binary/MachOHelpers.swift) provides the primary API for segment parsing, symbol lookup, and address translation.
- **Virtual-to-file address conversion** is mandatory when patching Mach-O binaries for iOS VMs, achieved via `MachOParser.vaToFileOffset`.
- The patching workflow loads binaries into `Data` buffers, locates symbols, calculates physical offsets, and writes modifications using standard Swift `Data` manipulation.
- **[`KernelPatcherBase.swift`](https://github.com/Lakr233/vphone-cli/blob/main/KernelPatcherBase.swift)** orchestrates the complete firmware modification pipeline used by the project to customize iOS kernels running in virtual machines.

## Frequently Asked Questions

### How does vphone-cli handle Mach-O binaries differently from standard macOS tools?

Unlike macOS command-line utilities that operate on loaded executables, vphone-cli treats Mach-O files as firmware images for iOS VMs. According to the source code in [`KernelPatcherBase.swift`](https://github.com/Lakr233/vphone-cli/blob/main/KernelPatcherBase.swift), the tool maintains strict separation between virtual addresses (used by the VM runtime) and file offsets (used for persistent storage), ensuring patches are written to the correct physical location in the binary rather than runtime memory addresses.

### What are the performance implications of parsing large kernel images with MachOKit?

The [`MachOHelpers.swift`](https://github.com/Lakr233/vphone-cli/blob/main/MachOHelpers.swift) implementation uses lazy evaluation where possible, but full segment and section parsing requires walking the complete load command table. For multi-gigabyte kernel caches typical in modern iOS versions, the wrapper operates on `Data` instances loaded into memory, meaning the primary constraint is available RAM rather than parsing speed. The linear search used in `vaToFileOffset` remains performant because iOS Mach-O files typically contain fewer than 50 segments.

### Can I extend the wrapper to modify load commands or add new sections?

Yes. While [`MachOHelpers.swift`](https://github.com/Lakr233/vphone-cli/blob/main/MachOHelpers.swift) currently implements read-only parsing and basic offset calculation, the underlying **MachOKit** submodule exposed through `import MachOKit` provides full read-write access to `MachoHeader` and `LoadCommand` structures. Developers can extend the `MachOParser` enum with mutating methods that directly manipulate the `Data` buffer, provided they update internal `segments` and `sections` caches after structural modifications.

### What happens if `findSymbol` returns nil for a known kernel symbol?

A nil return indicates either a stripped symbol table or a substring mismatch in the Mach-O string table. The vphone-cli implementation performs substring matching via `findSymbol(containing:in:)`, so symbol names must be unique substrings. If the symbol resides in a separate symbol table (such as the iOS kernel's `__TEXT,__text` without SYMTAB entries), you must first locate the function via pattern matching or address calculation rather than name lookup, as the current wrapper only searches the standard symbol table exposed by MachOKit.