How Kernel Patches Are Anchored in vphone-cli: String-Based Binary Patching Explained
Kernel patches in vphone-cli are anchored to immutable hard-coded strings within the kernel binary, allowing the tool to reliably locate target instructions across different iOS versions by scanning for string references rather than using fixed memory offsets.
The Lakr233/vphone-cli project implements a robust kernel patching system for iOS firmware by anchoring modifications to unique string literals embedded in the kernel binary. Unlike traditional patchers that rely on brittle static offsets, vphone-cli uses a multi-stage analysis pipeline to locate target code through data references. This approach ensures patches remain functional across kernel rebase operations and minor code changes between iOS releases.
The Six-Stage Anchor Resolution Workflow
vphone-cli applies kernel patches through a systematic six-step process that bridges from immutable data anchors to executable code modifications. Each patch definition in sources/FirmwarePatcher/Kernel/Patches/ implements this workflow.
Step 1: Locating the Anchor String
The patching process begins by locating a unique, hard-coded string within the kernel's binary buffer. This string serves as the immutable anchor that survives rebasing and version changes.
guard let strOff = buffer.findString("rootvp not authenticated after mounting") else {
return false
}
Source: sources/FirmwarePatcher/Kernel/Patches/KernelPatchBsdInit.swift (lines 63-66)
The findString method, implemented in BinaryBuffer.swift, returns the file offset of the literal string within the kernel image. If the anchor string is absent, the patch aborts gracefully.
Step 2: Resolving ADRP/ADD Instruction Pairs
After locating the string offset, the patcher scans the __text section for ADRP (Address of Page) and ADD instruction pairs that reference the anchor string. These instruction pairs load the string's address into a register at runtime.
let refs = findStringRefs(strOff) // returns [(adrpOff, addOff), …]
Source: KernelPatchBsdInit.swift (lines 69-71)
The findStringRefs function, defined in KernelPatcherBase.swift, returns a list of tuples containing the offsets of the ADRP and corresponding ADD instructions that reference the anchor.
Step 3: Forward Scanning for Functional Blocks
From each ADD instruction offset, the patcher performs a forward scan (typically up to 0x40 bytes) to locate functional blocks protected by the anchor. This scan searches for BL (Branch with Link) or BLR (Branch with Link to Register) instructions targeting known functions like _panic.
// forward-scan up to 0x40 bytes for BL _panic
while scan <= fwdLimit {
if let blTarget = decodeBranchTarget(from: scan), blTarget == panicAddr {
// Found the error path entry point
break
}
scan += 4 // Advance by instruction size
}
Source: KernelPatchBsdInit.swift (lines 78-95)
This phase identifies the error-handling code path that the targeted conditional branch guards.
Step 4: Back-Scanning for Conditional Branches
Working backward from the ADRP instruction, the patcher locates the conditional branch that gates access to the error block identified in Step 3. The back-scan uses the Capstone disassembler to decode each instruction and filters for conditional branch mnemonics.
guard let insn = disasm.disassembleOne(in: buffer.original, at: back) else { continue }
guard condBranchMnemonics.contains(insn.mnemonic) else { continue }
Source: KernelPatchBsdInit.swift (lines 100-116)
Supported conditional branch types include b.eq, cbz, tbz, and other ARM64 conditional variants defined in the patcher's mnemonic whitelist.
Step 5: Validating Branch Targets
Before modification, the patcher validates that the discovered conditional branch actually targets the error block. The target address is extracted from Capstone's operand string via decodeBranchTargetFromInsn and checked against the previously identified error block boundaries (errLo and errHi).
guard let branchTarget = decodeBranchTargetFromInsn(insn) else { continue }
guard branchTarget >= errLo, branchTarget < errHi else { continue }
Source: KernelPatchBsdInit.swift (lines 118-125)
This validation prevents false positives where an unrelated conditional branch might be located near the anchor reference.
Step 6: Emitting the Patch
Once validated, the patcher replaces the conditional branch with a NOP (No Operation) instruction, effectively removing the guard check. The emit function records the modification with metadata for verification.
emit(
back,
ARM64.nop,
patchID: "kernel.bsd_init_rootvp",
virtualAddress: va,
description: "NOP conditional branch guarding rootvp panic"
)
Source: KernelPatchBsdInit.swift (lines 126-134)
The emit function, centralized in KernelPatcher.swift, handles deduplication, logging, and patch serialization.
Key Architectural Components
The vphone-cli anchoring system relies on several specialized components that work together to provide version-independent patching:
-
BinaryBuffer.swift – Contains the
findStringmethod that locates anchor strings in the raw kernel binary. This provides the stable entry point that survives kernel rebasing. -
KernelPatcherBase.swift – Implements
findStringRefs, which bridges from data offsets (strings) to code locations by identifying ADRP+ADD instruction pairs in the__textsection. -
ARM64Disassembler.swift – Wraps the Capstone disassembly engine to decode ARM64 instructions and extract branch targets. This enables architecture-agnostic analysis of conditional branches.
-
KernelPatcher.swift – Provides the core
emitfunction that records patches and assigns unique patch IDs for later verification and debugging. -
Patch Definition Files – Individual Swift files in
sources/FirmwarePatcher/Kernel/Patches/encapsulate specific patches. Examples include:KernelPatchBsdInit.swift– Removes the rootvp authentication check using the anchor "rootvp not authenticated after mounting"KernelPatchExcGuard.swift– Disables thread guard violations using the anchor "thread_guard_violation"KernelPatchApfsSnapshot.swift– Modifies APFS snapshot handling using related string anchors
Implementation Example: Thread Guard Patch
The KernelPatchExcGuard.swift file demonstrates the same anchoring pattern applied to a different kernel subsystem. This patch locates the thread_guard_violation string and follows identical resolution steps to disable the guard mechanism.
// Example from KernelPatchExcGuard.swift
guard let strOff = buffer.findString("thread_guard_violation") else { return false }
let refs = findStringRefs(strOff) // locate ADRP/ADD pairs
// ... forward-scan for the guard call, back-scan for the conditional branch,
// then NOP the branch to allow continued execution
Source: KernelPatchExcGuard.swift (lines 30-40)
The decodeBranchTargetFromInsn helper function parses Capstone's operand strings to extract hexadecimal branch targets:
private func decodeBranchTargetFromInsn(_ insn: Instruction) -> Int? {
let tokens = insn.operandString.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
guard let last = tokens.last else { return nil }
let hex = last.hasPrefix("#") ? String(last.dropFirst()) : last
return hex.hasPrefix("0x") ? Int(hex.dropFirst(2), radix: 16) : Int(hex)
}
Source: KernelPatchBsdInit.swift (lines 45-65)
Summary
vphone-cli implements a string-based anchoring system for kernel patches that provides several critical advantages over offset-based patching:
- Version independence – Patches survive kernel rebasing and minor code changes across iOS releases
- Reliability – Hard-coded strings provide unique, immutable anchors that are unlikely to collide
- Maintainability – Each patch is self-contained in its own file with a clear anchor definition
- Safety – Multi-stage validation (forward scanning, back-scanning, and target verification) prevents incorrect modifications
The architecture separates concerns between binary searching (BinaryBuffer), reference resolution (KernelPatcherBase), disassembly (ARM64Disassembler), and patch application (KernelPatcher).
Frequently Asked Questions
How does vphone-cli find the specific instruction to patch without knowing the kernel version?
vphone-cli uses hard-coded strings as immutable anchors. Since these strings remain constant across kernel versions, the tool can always locate them using buffer.findString(). From there, it follows code references (ADRP/ADD pairs) to find the specific instructions that reference the string, rather than relying on fixed memory addresses that change between builds.
What ARM64 instructions does vphone-cli look for when resolving string references?
The patcher specifically searches for ADRP (Address of Page) paired with ADD instructions. On ARM64, this instruction sequence is the standard compiler pattern for loading the address of a string literal into a register. By scanning the __text section for these pairs that reference the anchor string's offset, vphone-cli reliably locates all code paths that use that string.
Why does vphone-cli use both forward and backward scanning from the anchor location?
Forward scanning identifies the functional block protected by the anchor (such as a call to _panic), establishing the boundaries of the error path. Backward scanning locates the conditional branch (like b.eq or cbz) that gates entry to that error path. This bidirectional approach ensures the patcher modifies the correct control-flow instruction—the one that decides whether to enter the error block—not just any nearby branch.
What happens if the anchor string appears multiple times in the kernel?
The findStringRefs function returns all (ADRP, ADD) pairs that reference the anchor string. The patching logic then iterates through each reference, performing the forward/backward scan and validation for every occurrence. This ensures patches are applied consistently across all code paths that check the anchored condition, even if the kernel contains multiple functions referencing the same string literal.
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 →