How to Use MachOKit for Mach-O Patching in iOS VMs: A Complete Guide
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) 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) 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). 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:
- Load the binary into a mutable
Databuffer. - Parse segments using
MachOParser.parseSegmentsto build a mapping of virtual addresses to file ranges. - Locate target symbols via
MachOParser.findSymbol(containing:in:)to identify patch targets like kernel functions. - 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. - Inject patch bytes at the calculated offset using Swift's
replaceSubrange. - 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.
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 extractingLC_SEGMENT_64data, returning an array ofMachOSegmentInfocontainingvmAddr,vmSize, andfileOffset.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
MachOParserenum insources/FirmwarePatcher/Binary/MachOHelpers.swiftprovides 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
Databuffers, locates symbols, calculates physical offsets, and writes modifications using standard SwiftDatamanipulation. KernelPatcherBase.swiftorchestrates 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, 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 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →