From 08eb9d260f6494549220c3109eafd18da9fa75f4 Mon Sep 17 00:00:00 2001 From: Lakr Date: Wed, 11 Mar 2026 02:57:28 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=F0=9F=90=A6=20iBSS=20iBEC=20LLB=20TXM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update update --- .gitmodules | 15 + Package.swift | 22 +- scripts/export_patch_reference.py | 240 +++++++ scripts/export_patch_reference_all.py | 150 +++++ .../ARM64/ARM64Constants.swift | 164 +++++ .../ARM64/ARM64Disassembler.swift | 57 ++ .../FirmwarePatcher/ARM64/ARM64Encoder.swift | 98 +++ .../AVPBooter/AVPBooterPatcher.swift | 148 +++++ .../FirmwarePatcher/Binary/BinaryBuffer.swift | 124 ++++ .../FirmwarePatcher/Binary/IM4PHandler.swift | 52 ++ .../FirmwarePatcher/Binary/MachOHelpers.swift | 183 ++++++ .../FirmwarePatcher/Core/PatchRecord.swift | 72 +++ .../FirmwarePatcher/Core/PatcherError.swift | 29 + .../Core/PatcherProtocol.swift | 28 + .../DeviceTree/DeviceTreePatcher.swift | 337 ++++++++++ .../IBoot/IBootJBPatcher.swift | 187 ++++++ .../FirmwarePatcher/IBoot/IBootPatcher.swift | 585 ++++++++++++++++++ .../JBPatches/KernelJBPatchAmfiExecve.swift | 110 ++++ .../KernelJBPatchAmfiTrustcache.swift | 139 +++++ .../JBPatches/KernelJBPatchBsdInitAuth.swift | 188 ++++++ .../JBPatches/KernelJBPatchCredLabel.swift | 348 +++++++++++ .../JBPatches/KernelJBPatchDounmount.swift | 118 ++++ .../KernelJBPatchHookCredLabel.swift | 449 ++++++++++++++ .../JBPatches/KernelJBPatchIoucMacf.swift | 146 +++++ .../JBPatches/KernelJBPatchKcall10.swift | 324 ++++++++++ .../JBPatches/KernelJBPatchLoadDylinker.swift | 81 +++ .../JBPatches/KernelJBPatchMacMount.swift | 187 ++++++ .../Kernel/JBPatches/KernelJBPatchNvram.swift | 65 ++ .../JBPatches/KernelJBPatchPortToMap.swift | 64 ++ .../KernelJBPatchPostValidation.swift | 93 +++ .../JBPatches/KernelJBPatchProcPidinfo.swift | 67 ++ .../JBPatches/KernelJBPatchProcSecurity.swift | 86 +++ .../KernelJBPatchSandboxExtended.swift | 208 +++++++ .../JBPatches/KernelJBPatchSecureRoot.swift | 217 +++++++ .../JBPatches/KernelJBPatchSharedRegion.swift | 98 +++ .../JBPatches/KernelJBPatchSpawnPersona.swift | 200 ++++++ .../JBPatches/KernelJBPatchSyscallmask.swift | 363 +++++++++++ .../KernelJBPatchTaskConversion.swift | 160 +++++ .../JBPatches/KernelJBPatchTaskForPid.swift | 156 +++++ .../JBPatches/KernelJBPatchThidCrash.swift | 60 ++ .../JBPatches/KernelJBPatchVmFault.swift | 135 ++++ .../JBPatches/KernelJBPatchVmProtect.swift | 141 +++++ .../Kernel/KernelJBPatcher.swift | 62 ++ .../Kernel/KernelJBPatcherBase.swift | 440 +++++++++++++ .../Kernel/KernelPatcher.swift | 53 ++ .../Kernel/KernelPatcherBase.swift | 495 +++++++++++++++ .../Kernel/Patches/KernelPatchApfsGraft.swift | 121 ++++ .../Kernel/Patches/KernelPatchApfsMount.swift | 415 +++++++++++++ .../Kernel/Patches/KernelPatchApfsSeal.swift | 82 +++ .../Patches/KernelPatchApfsSnapshot.swift | 69 +++ .../Kernel/Patches/KernelPatchBsdInit.swift | 166 +++++ .../Kernel/Patches/KernelPatchDebugger.swift | 172 +++++ .../Patches/KernelPatchDyldPolicy.swift | 129 ++++ .../KernelPatchLaunchConstraints.swift | 72 +++ .../Patches/KernelPatchPostValidation.swift | 183 ++++++ .../Kernel/Patches/KernelPatchSandbox.swift | 346 +++++++++++ .../Manifest/FirmwareManifest.swift | 403 ++++++++++++ .../Pipeline/FirmwarePipeline.swift | 334 ++++++++++ .../FirmwarePatcher/TXM/TXMDevPatcher.swift | 522 ++++++++++++++++ sources/FirmwarePatcher/TXM/TXMPatcher.swift | 184 ++++++ sources/vphone-cli/VPhoneAppBrowserView.swift | 2 +- .../FirmwarePatcherTests.swift | 174 ++++++ .../PatchComparisonTests.swift | 201 ++++++ .../FirmwarePatcherTests/VerboseJBDebug.swift | 37 ++ vendor/Dynamic | 1 + vendor/MachOKit | 1 + vendor/libcapstone-spm | 1 + vendor/libimg4-spm | 1 + vendor/swift-argument-parser | 1 + 69 files changed, 11358 insertions(+), 3 deletions(-) create mode 100644 scripts/export_patch_reference.py create mode 100644 scripts/export_patch_reference_all.py create mode 100644 sources/FirmwarePatcher/ARM64/ARM64Constants.swift create mode 100644 sources/FirmwarePatcher/ARM64/ARM64Disassembler.swift create mode 100644 sources/FirmwarePatcher/ARM64/ARM64Encoder.swift create mode 100644 sources/FirmwarePatcher/AVPBooter/AVPBooterPatcher.swift create mode 100644 sources/FirmwarePatcher/Binary/BinaryBuffer.swift create mode 100644 sources/FirmwarePatcher/Binary/IM4PHandler.swift create mode 100644 sources/FirmwarePatcher/Binary/MachOHelpers.swift create mode 100644 sources/FirmwarePatcher/Core/PatchRecord.swift create mode 100644 sources/FirmwarePatcher/Core/PatcherError.swift create mode 100644 sources/FirmwarePatcher/Core/PatcherProtocol.swift create mode 100644 sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift create mode 100644 sources/FirmwarePatcher/IBoot/IBootJBPatcher.swift create mode 100644 sources/FirmwarePatcher/IBoot/IBootPatcher.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiExecve.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiTrustcache.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchBsdInitAuth.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchCredLabel.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchDounmount.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchHookCredLabel.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchIoucMacf.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchKcall10.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchLoadDylinker.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchMacMount.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchNvram.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchPortToMap.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchPostValidation.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchProcPidinfo.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchProcSecurity.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchSandboxExtended.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchSecureRoot.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchSharedRegion.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchSpawnPersona.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchSyscallmask.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchTaskConversion.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchTaskForPid.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchThidCrash.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchVmFault.swift create mode 100644 sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchVmProtect.swift create mode 100644 sources/FirmwarePatcher/Kernel/KernelJBPatcher.swift create mode 100644 sources/FirmwarePatcher/Kernel/KernelJBPatcherBase.swift create mode 100644 sources/FirmwarePatcher/Kernel/KernelPatcher.swift create mode 100644 sources/FirmwarePatcher/Kernel/KernelPatcherBase.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchApfsGraft.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchApfsMount.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchApfsSeal.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchApfsSnapshot.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchBsdInit.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchDebugger.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchDyldPolicy.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchLaunchConstraints.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchPostValidation.swift create mode 100644 sources/FirmwarePatcher/Kernel/Patches/KernelPatchSandbox.swift create mode 100644 sources/FirmwarePatcher/Manifest/FirmwareManifest.swift create mode 100644 sources/FirmwarePatcher/Pipeline/FirmwarePipeline.swift create mode 100644 sources/FirmwarePatcher/TXM/TXMDevPatcher.swift create mode 100644 sources/FirmwarePatcher/TXM/TXMPatcher.swift create mode 100644 tests/FirmwarePatcherTests/FirmwarePatcherTests.swift create mode 100644 tests/FirmwarePatcherTests/PatchComparisonTests.swift create mode 100644 tests/FirmwarePatcherTests/VerboseJBDebug.swift create mode 160000 vendor/Dynamic create mode 160000 vendor/MachOKit create mode 160000 vendor/libcapstone-spm create mode 160000 vendor/libimg4-spm create mode 160000 vendor/swift-argument-parser diff --git a/.gitmodules b/.gitmodules index fb6cd9d..e5d69d5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,18 @@ [submodule "scripts/resources"] path = scripts/resources url = https://github.com/Lakr233/vphone-cli-storage.git +[submodule "vendor/libcapstone-spm"] + path = vendor/libcapstone-spm + url = https://github.com/Lakr233/libcapstone-spm.git +[submodule "vendor/libimg4-spm"] + path = vendor/libimg4-spm + url = https://github.com/Lakr233/libimg4-spm.git +[submodule "vendor/MachOKit"] + path = vendor/MachOKit + url = https://github.com/p-x9/MachOKit.git +[submodule "vendor/Dynamic"] + path = vendor/Dynamic + url = https://github.com/mhdhejazi/Dynamic.git +[submodule "vendor/swift-argument-parser"] + path = vendor/swift-argument-parser + url = https://github.com/apple/swift-argument-parser.git diff --git a/Package.swift b/Package.swift index 80d3c11..6590161 100644 --- a/Package.swift +++ b/Package.swift @@ -9,15 +9,28 @@ let package = Package( ], products: [], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"), - .package(url: "https://github.com/mhdhejazi/Dynamic", from: "1.2.0"), + .package(path: "vendor/swift-argument-parser"), + .package(path: "vendor/Dynamic"), + .package(path: "vendor/libcapstone-spm"), + .package(path: "vendor/libimg4-spm"), + .package(path: "vendor/MachOKit"), ], targets: [ + .target( + name: "FirmwarePatcher", + dependencies: [ + .product(name: "Capstone", package: "libcapstone-spm"), + .product(name: "Img4tool", package: "libimg4-spm"), + .product(name: "MachOKit", package: "MachOKit"), + ], + path: "sources/FirmwarePatcher" + ), .executableTarget( name: "vphone-cli", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Dynamic", package: "Dynamic"), + "FirmwarePatcher", ], path: "sources/vphone-cli", linkerSettings: [ @@ -28,5 +41,10 @@ let package = Package( .linkedFramework("AVFoundation"), ] ), + .testTarget( + name: "FirmwarePatcherTests", + dependencies: ["FirmwarePatcher"], + path: "tests/FirmwarePatcherTests" + ), ] ) diff --git a/scripts/export_patch_reference.py b/scripts/export_patch_reference.py new file mode 100644 index 0000000..e4279b2 --- /dev/null +++ b/scripts/export_patch_reference.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Generate patch reference JSON for each firmware component. + +Runs each Python patcher in dry-run mode (find patches but don't apply) +and exports the patch sites with offsets and bytes as JSON. + +Usage: + source .venv/bin/activate + python3 scripts/export_patch_reference.py ipsws/patch_refactor_input +""" + +import json +import os +import struct +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN + +_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) +_cs.detail = True + + +def disasm_one(data, off): + insns = list(_cs.disasm(bytes(data[off:off + 4]), off)) + return insns[0] if insns else None + + +def disasm_bytes(b, addr=0): + insns = list(_cs.disasm(bytes(b), addr)) + if insns: + return f"{insns[0].mnemonic} {insns[0].op_str}" + return "???" + + +def patches_to_json(patches, component): + """Convert list of (offset, patch_bytes, description) to JSON-serializable records.""" + records = [] + for off, pb, desc in patches: + records.append({ + "file_offset": off, + "patch_bytes": pb.hex(), + "patch_size": len(pb), + "description": desc, + "component": component, + }) + return records + + +def load_firmware(path): + """Load firmware file, decompress IM4P if needed.""" + with open(path, "rb") as f: + raw = f.read() + try: + from pyimg4 import IM4P + im4p = IM4P(raw) + if im4p.payload.compression: + im4p.payload.decompress() + return bytearray(im4p.payload.data) + except Exception: + return bytearray(raw) + + +def export_avpbooter(base_dir, out_dir): + """Export AVPBooter patch reference.""" + import glob + paths = glob.glob(os.path.join(base_dir, "AVPBooter*.bin")) + if not paths: + print(" [!] AVPBooter not found, skipping") + return + + path = paths[0] + data = bytearray(open(path, "rb").read()) + print(f" AVPBooter: {path} ({len(data)} bytes)") + + # Inline the AVPBooter patcher logic (from fw_patch.py) + from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN + _ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) + + def asm(s): + enc, _ = _ks.asm(s) + return bytes(enc) + + patches = [] + DGST = struct.pack(" 0 else None + if prev and prev.mnemonic == "mov" and "x0" in prev.op_str: + patches.append((prev.address, asm("mov x0, #0"), + "AVPBooter DGST bypass: mov x0, #0")) + break + + records = patches_to_json(patches, "avpbooter") + out_path = os.path.join(out_dir, "avpbooter.json") + with open(out_path, "w") as f: + json.dump(records, f, indent=2) + print(f" → {out_path} ({len(records)} patches)") + + +def export_iboot(base_dir, out_dir): + """Export iBSS/iBEC/LLB patch references.""" + from patchers.iboot import IBootPatcher + + components = [ + ("ibss", "Firmware/dfu/iBSS.vresearch101.RELEASE.im4p"), + ("ibec", "Firmware/dfu/iBEC.vresearch101.RELEASE.im4p"), + ("llb", "Firmware/all_flash/LLB.vresearch101.RELEASE.im4p"), + ] + + for mode, rel_path in components: + path = os.path.join(base_dir, rel_path) + if not os.path.exists(path): + print(f" [!] {mode}: {rel_path} not found, skipping") + continue + + data = load_firmware(path) + print(f" {mode}: {rel_path} ({len(data)} bytes)") + + patcher = IBootPatcher(data, mode=mode, verbose=True) + patcher.find_all() + records = patches_to_json(patcher.patches, mode) + + out_path = os.path.join(out_dir, f"{mode}.json") + with open(out_path, "w") as f: + json.dump(records, f, indent=2) + print(f" → {out_path} ({len(records)} patches)") + + +def export_txm(base_dir, out_dir): + """Export TXM patch reference.""" + from patchers.txm import TXMPatcher as TXMBasePatcher + + path = os.path.join(base_dir, "Firmware/txm.iphoneos.research.im4p") + if not os.path.exists(path): + print(" [!] TXM not found, skipping") + return + + data = load_firmware(path) + print(f" TXM: ({len(data)} bytes)") + + patcher = TXMBasePatcher(data, verbose=True) + patcher.find_all() + records = patches_to_json(patcher.patches, "txm") + + out_path = os.path.join(out_dir, "txm.json") + with open(out_path, "w") as f: + json.dump(records, f, indent=2) + print(f" → {out_path} ({len(records)} patches)") + + +def export_kernel(base_dir, out_dir): + """Export kernel patch reference.""" + from patchers.kernel import KernelPatcher + + path = os.path.join(base_dir, "kernelcache.research.vphone600") + if not os.path.exists(path): + print(" [!] kernelcache not found, skipping") + return + + data = load_firmware(path) + print(f" kernelcache: ({len(data)} bytes)") + + patcher = KernelPatcher(data, verbose=True) + patcher.find_all() + records = patches_to_json(patcher.patches, "kernelcache") + + out_path = os.path.join(out_dir, "kernelcache.json") + with open(out_path, "w") as f: + json.dump(records, f, indent=2) + print(f" → {out_path} ({len(records)} patches)") + + +def export_dtree(base_dir, out_dir): + """Export DeviceTree patch reference.""" + import dtree + + path = os.path.join(base_dir, "Firmware/all_flash/DeviceTree.vphone600ap.im4p") + if not os.path.exists(path): + print(" [!] DeviceTree not found, skipping") + return + + data = load_firmware(path) + print(f" DeviceTree: ({len(data)} bytes)") + + # dtree.patch_device_tree_payload returns list of patches + patches = dtree.find_patches(data) + records = [] + for off, old_bytes, new_bytes, desc in patches: + records.append({ + "file_offset": off, + "original_bytes": old_bytes.hex() if isinstance(old_bytes, (bytes, bytearray)) else old_bytes, + "patch_bytes": new_bytes.hex() if isinstance(new_bytes, (bytes, bytearray)) else new_bytes, + "patch_size": len(new_bytes) if isinstance(new_bytes, (bytes, bytearray)) else 0, + "description": desc, + "component": "devicetree", + }) + + out_path = os.path.join(out_dir, "devicetree.json") + with open(out_path, "w") as f: + json.dump(records, f, indent=2) + print(f" → {out_path} ({len(records)} patches)") + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + base_dir = os.path.abspath(sys.argv[1]) + out_dir = os.path.join(base_dir, "reference_patches") + os.makedirs(out_dir, exist_ok=True) + + print(f"=== Exporting patch references from {base_dir} ===\n") + + # Change to scripts dir so imports work + os.chdir(os.path.join(os.path.dirname(__file__))) + + export_avpbooter(base_dir, out_dir) + print() + export_iboot(base_dir, out_dir) + print() + export_txm(base_dir, out_dir) + print() + export_kernel(base_dir, out_dir) + print() + # DeviceTree needs special handling - the dtree.py may not have find_patches + # We'll handle it separately + print(f"\n=== Done. References saved to {out_dir}/ ===") + + +if __name__ == "__main__": + main() diff --git a/scripts/export_patch_reference_all.py b/scripts/export_patch_reference_all.py new file mode 100644 index 0000000..af7225a --- /dev/null +++ b/scripts/export_patch_reference_all.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Generate patch reference JSON for ALL variants (regular + dev + jb). + +Usage: + source .venv/bin/activate + python3 scripts/export_patch_reference_all.py ipsws/patch_refactor_input +""" + +import json +import os +import struct +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN + +_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) +_cs.detail = True + + +def disasm_one(data, off): + insns = list(_cs.disasm(bytes(data[off:off + 4]), off)) + return insns[0] if insns else None + + +def patches_to_json(patches, component): + records = [] + for off, pb, desc in patches: + records.append({ + "file_offset": off, + "patch_bytes": pb.hex(), + "patch_size": len(pb), + "description": desc, + "component": component, + }) + return records + + +def load_firmware(path): + with open(path, "rb") as f: + raw = f.read() + try: + from pyimg4 import IM4P + im4p = IM4P(raw) + if im4p.payload.compression: + im4p.payload.decompress() + return bytearray(im4p.payload.data) + except Exception: + return bytearray(raw) + + +def export_txm_dev(base_dir, out_dir): + """Export TXM dev patch reference (base + dev patches).""" + from patchers.txm import TXMPatcher as TXMBasePatcher + from patchers.txm_dev import TXMPatcher as TXMDevPatcher + + path = os.path.join(base_dir, "Firmware/txm.iphoneos.research.im4p") + if not os.path.exists(path): + print(" [!] TXM not found, skipping txm_dev") + return + + data = load_firmware(path) + print(f" TXM dev: ({len(data)} bytes)") + + # Base TXM patches + base = TXMBasePatcher(data, verbose=True) + base.find_all() + base_records = patches_to_json(base.patches, "txm_dev_base") + + # Dev TXM patches (on same data, without applying base) + dev = TXMDevPatcher(bytearray(data), verbose=True) + dev.find_all() + dev_records = patches_to_json(dev.patches, "txm_dev") + + out_path = os.path.join(out_dir, "txm_dev.json") + with open(out_path, "w") as f: + json.dump({"base": base_records, "dev": dev_records}, f, indent=2) + print(f" → {out_path} ({len(base_records)} base + {len(dev_records)} dev patches)") + + +def export_iboot_jb(base_dir, out_dir): + """Export iBSS JB patch reference.""" + from patchers.iboot_jb import IBootJBPatcher + + path = os.path.join(base_dir, "Firmware/dfu/iBSS.vresearch101.RELEASE.im4p") + if not os.path.exists(path): + print(" [!] iBSS not found, skipping iboot_jb") + return + + data = load_firmware(path) + print(f" iBSS JB: ({len(data)} bytes)") + + patcher = IBootJBPatcher(data, mode="ibss", verbose=True) + # Only find JB patches (not base) + patcher.patches = [] + patcher.patch_skip_generate_nonce() + records = patches_to_json(patcher.patches, "ibss_jb") + + out_path = os.path.join(out_dir, "ibss_jb.json") + with open(out_path, "w") as f: + json.dump(records, f, indent=2) + print(f" → {out_path} ({len(records)} patches)") + + +def export_kernel_jb(base_dir, out_dir): + """Export kernel JB patch reference.""" + from patchers.kernel_jb import KernelJBPatcher + + path = os.path.join(base_dir, "kernelcache.research.vphone600") + if not os.path.exists(path): + print(" [!] kernelcache not found, skipping kernel_jb") + return + + data = load_firmware(path) + print(f" kernelcache JB: ({len(data)} bytes)") + + patcher = KernelJBPatcher(data, verbose=True) + patches = patcher.find_all() + records = patches_to_json(patches, "kernelcache_jb") + + out_path = os.path.join(out_dir, "kernelcache_jb.json") + with open(out_path, "w") as f: + json.dump(records, f, indent=2) + print(f" → {out_path} ({len(records)} patches)") + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + base_dir = os.path.abspath(sys.argv[1]) + out_dir = os.path.join(base_dir, "reference_patches") + os.makedirs(out_dir, exist_ok=True) + + print(f"=== Exporting dev/jb patch references from {base_dir} ===\n") + os.chdir(os.path.join(os.path.dirname(__file__))) + + export_txm_dev(base_dir, out_dir) + print() + export_iboot_jb(base_dir, out_dir) + print() + export_kernel_jb(base_dir, out_dir) + + print(f"\n=== Done. References saved to {out_dir}/ ===") + + +if __name__ == "__main__": + main() diff --git a/sources/FirmwarePatcher/ARM64/ARM64Constants.swift b/sources/FirmwarePatcher/ARM64/ARM64Constants.swift new file mode 100644 index 0000000..45c135e --- /dev/null +++ b/sources/FirmwarePatcher/ARM64/ARM64Constants.swift @@ -0,0 +1,164 @@ +// ARM64Constants.swift — Pre-encoded ARM64 instruction constants. +// +// Every constant was generated by keystone-engine and verified +// via capstone round-trip disassembly. Do NOT edit raw bytes +// without updating the corresponding test case. + +import Foundation + +// MARK: - ARM64 Instruction Constants + +public enum ARM64 { + // MARK: Fixed Instructions + + /// NOP — no operation + public static let nop = encodeU32(0xD503_201F) + + /// RET — return from subroutine + public static let ret = encodeU32(0xD65F_03C0) + + /// RETAA — return with pointer authentication (A key) + public static let retaa = encodeU32(0xD65F_0BFF) + + /// RETAB — return with pointer authentication (B key) + public static let retab = encodeU32(0xD65F_0FFF) + + /// PACIBSP — sign LR with B key using SP (hint #27) + public static let pacibsp = encodeU32(0xD503_237F) + + // MARK: MOV Variants + + /// MOV X0, #0 (MOVZ X0, #0) + public static let movX0_0 = encodeU32(0xD280_0000) + + /// MOV X0, #1 (MOVZ X0, #1) + public static let movX0_1 = encodeU32(0xD280_0020) + + /// MOV W0, #0 (MOVZ W0, #0) + public static let movW0_0 = encodeU32(0x5280_0000) + + /// MOV W0, #1 (MOVZ W0, #1) + public static let movW0_1 = encodeU32(0x5280_0020) + + /// MOV X0, X20 (ORR X0, XZR, X20) + public static let movX0X20 = encodeU32(0xAA14_03E0) + + /// MOV W0, #0xA1 (MOVZ W0, #0xA1) + public static let movW0_0xA1 = encodeU32(0x5280_1420) + + // MARK: Compare + + /// CMP W0, W0 (SUBS WZR, W0, W0) + public static let cmpW0W0 = encodeU32(0x6B00_001F) + + /// CMP X0, X0 (SUBS XZR, X0, X0) + public static let cmpX0X0 = encodeU32(0xEB00_001F) + + // MARK: Memory + + /// STRB W0, [X20, #0x30] + public static let strbW0X20_30 = encodeU32(0x3900_C280) + + // MARK: JB Constants + + /// CBZ X2, #8 (branch if X2 == 0, skip 2 instructions) + public static let cbzX2_8 = encodeU32(0xB400_0042) + + /// STR X0, [X2] + public static let strX0X2 = encodeU32(0xF900_0040) + + /// CMP XZR, XZR (SUBS XZR, XZR, XZR — always sets Z flag) + public static let cmpXzrXzr = encodeU32(0xEB1F_03FF) + + /// MOV X8, XZR (ORR X8, XZR, XZR) + public static let movX8Xzr = encodeU32(0xAA1F_03E8) + + // MARK: Common Prologue/Epilogue Patterns (verified via keystone) + + /// STP X29, X30, [SP, #-0x10]! (push frame) + static let stpFP_LR_pre: UInt32 = 0xA9BF_7BFD + /// MOV X29, SP (set frame pointer) + static let movFP_SP: UInt32 = 0x9100_03FD + + // MARK: IOUC MACF Aggregator Shape + + /// LDR X10, [X10, #0x9E8] (mac_policy_list slot load) + static let ldr_x10_x10_0x9e8: UInt32 = 0xF944_F54A + /// BLRAA X10, SP (authenticated indirect call) + static let blraa_x10: UInt32 = 0xD73F_0940 + /// BLRAB X10, SP + static let blrab_x10: UInt32 = 0xD73F_0D40 + /// BLR X10 + static let blr_x10: UInt32 = 0xD63F_0140 + + // MARK: C23 Cave Instructions (verified via keystone) + + // + // These are the fixed instructions used in the faithful upstream C23 + // shellcode cave (vnode getattr → uid/gid/P_SUGID fixup). + // Position-dependent BL/B instructions are NOT included here — they are + // encoded at build time by ARM64Encoder. + + static let c23_cbzX3_0xA8: UInt32 = 0xB400_0543 // cbz x3, #+0xa8 + static let c23_subSP_0x400: UInt32 = 0xD110_03FF // sub sp, sp, #0x400 + static let c23_stpFP_LR: UInt32 = 0xA900_7BFD // stp x29, x30, [sp] + static let c23_stpX0X1_0x10: UInt32 = 0xA901_07E0 // stp x0, x1, [sp, #0x10] + static let c23_stpX2X3_0x20: UInt32 = 0xA902_0FE2 // stp x2, x3, [sp, #0x20] + static let c23_stpX4X5_0x30: UInt32 = 0xA903_17E4 // stp x4, x5, [sp, #0x30] + static let c23_stpX6X7_0x40: UInt32 = 0xA904_1FE6 // stp x6, x7, [sp, #0x40] + static let c23_movX2_X0: UInt32 = 0xAA00_03E2 // mov x2, x0 + static let c23_ldrX0_sp_0x28: UInt32 = 0xF940_17E0 // ldr x0, [sp, #0x28] + static let c23_addX1_sp_0x80: UInt32 = 0x9102_03E1 // add x1, sp, #0x80 + static let c23_movzW8_0x380: UInt32 = 0x5280_7008 // movz w8, #0x380 + static let c23_stpXZR_X8: UInt32 = 0xA900_203F // stp xzr, x8, [x1] + static let c23_stpXZR_XZR_0x10: UInt32 = 0xA901_7C3F // stp xzr, xzr, [x1, #0x10] + static let c23_cbnzX0_0x4c: UInt32 = 0xB500_0260 // cbnz x0, #+0x4c + static let c23_movW2_0: UInt32 = 0x5280_0002 // mov w2, #0 + static let c23_ldrW8_sp_0xcc: UInt32 = 0xB940_CFE8 // ldr w8, [sp, #0xcc] + static let c23_tbzW8_11_0x14: UInt32 = 0x3658_00A8 // tbz w8, #0xb, #+0x14 + static let c23_ldrW8_sp_0xc4: UInt32 = 0xB940_C7E8 // ldr w8, [sp, #0xc4] + static let c23_ldrX0_sp_0x18: UInt32 = 0xF940_0FE0 // ldr x0, [sp, #0x18] + static let c23_strW8_x0_0x18: UInt32 = 0xB900_1808 // str w8, [x0, #0x18] + static let c23_movW2_1: UInt32 = 0x5280_0022 // mov w2, #1 + static let c23_tbzW8_10_0x14: UInt32 = 0x3650_00A8 // tbz w8, #0xa, #+0x14 + static let c23_ldrW8_sp_0xc8: UInt32 = 0xB940_CBE8 // ldr w8, [sp, #0xc8] + static let c23_strW8_x0_0x28: UInt32 = 0xB900_2808 // str w8, [x0, #0x28] + static let c23_cbzW2_0x14: UInt32 = 0x3400_00A2 // cbz w2, #+0x14 + static let c23_ldrX0_sp_0x20: UInt32 = 0xF940_13E0 // ldr x0, [sp, #0x20] + static let c23_ldrW8_x0_0x454: UInt32 = 0xB944_5408 // ldr w8, [x0, #0x454] + static let c23_orrW8_0x100: UInt32 = 0x3218_0108 // orr w8, w8, #0x100 + static let c23_strW8_x0_0x454: UInt32 = 0xB904_5408 // str w8, [x0, #0x454] + static let c23_ldpX0X1_0x10: UInt32 = 0xA941_07E0 // ldp x0, x1, [sp, #0x10] + static let c23_ldpX2X3_0x20: UInt32 = 0xA942_0FE2 // ldp x2, x3, [sp, #0x20] + static let c23_ldpX4X5_0x30: UInt32 = 0xA943_17E4 // ldp x4, x5, [sp, #0x30] + static let c23_ldpX6X7_0x40: UInt32 = 0xA944_1FE6 // ldp x6, x7, [sp, #0x40] + static let c23_ldpFP_LR: UInt32 = 0xA940_7BFD // ldp x29, x30, [sp] + static let c23_addSP_0x400: UInt32 = 0x9110_03FF // add sp, sp, #0x400 + + // MARK: vfs_context_current Shape (verified via keystone) + + /// mrs x0, tpidr_el1 + static let mrs_x0_tpidr_el1: UInt32 = 0xD538_D080 + /// ldr x1, [x0, #0x3e0] + static let ldr_x1_x0_0x3e0: UInt32 = 0xF941_F001 + + // MARK: UInt32 Values (for pattern matching) + + public static let nopU32: UInt32 = 0xD503_201F + public static let retU32: UInt32 = 0xD65F_03C0 + public static let retaaU32: UInt32 = 0xD65F_0BFF + public static let retabU32: UInt32 = 0xD65F_0FFF + public static let pacibspU32: UInt32 = 0xD503_237F + + /// Set of instruction uint32 values that indicate function boundaries. + public static let funcBoundaryU32s: Set = [ + retU32, retaaU32, retabU32, pacibspU32, + ] + + // MARK: - Helpers + + @inlinable + static func encodeU32(_ value: UInt32) -> Data { + withUnsafeBytes(of: value.littleEndian) { Data($0) } + } +} diff --git a/sources/FirmwarePatcher/ARM64/ARM64Disassembler.swift b/sources/FirmwarePatcher/ARM64/ARM64Disassembler.swift new file mode 100644 index 0000000..9dccc2b --- /dev/null +++ b/sources/FirmwarePatcher/ARM64/ARM64Disassembler.swift @@ -0,0 +1,57 @@ +// ARM64Disassembler.swift — Capstone wrapper for ARM64 disassembly. + +import Capstone +import Foundation + +public final class ARM64Disassembler: Sendable { + /// Shared singleton instance with detail mode enabled. + public static let shared: ARM64Disassembler = .init() + + private let cs: Disassembler + + public init() { + // CS_ARCH_AARCH64 and CS_MODE_LITTLE_ENDIAN are the correct constants + cs = try! Disassembler(arch: CS_ARCH_AARCH64, mode: CS_MODE_LITTLE_ENDIAN) + cs.detail = true + cs.skipData = true + } + + /// Disassemble instructions from data starting at the given virtual address. + /// + /// - Parameters: + /// - data: Raw instruction bytes. + /// - address: Virtual address of the first byte. + /// - count: Maximum number of instructions to disassemble (0 = all). + /// - Returns: Array of disassembled instructions. + public func disassemble(_ data: Data, at address: UInt64 = 0, count: Int = 0) -> [Instruction] { + cs.disassemble(code: data, address: address, count: count) + } + + /// Disassemble a single 4-byte instruction at the given address. + public func disassembleOne(_ data: Data, at address: UInt64 = 0) -> Instruction? { + let insns = cs.disassemble(code: data, address: address, count: 1) + return insns.first + } + + /// Disassemble a single instruction from a buffer at a file offset. + public func disassembleOne(in buffer: Data, at offset: Int, address: UInt64? = nil) -> Instruction? { + guard offset >= 0, offset + 4 <= buffer.count else { return nil } + let slice = buffer[offset ..< offset + 4] + let addr = address ?? UInt64(offset) + return disassembleOne(Data(slice), at: addr) + } + + /// Disassemble `count` instructions starting at file offset. + public func disassemble(in buffer: Data, at offset: Int, count: Int, address: UInt64? = nil) -> [Instruction] { + let byteCount = count * 4 + guard offset >= 0, offset + byteCount <= buffer.count else { return [] } + let slice = buffer[offset ..< offset + byteCount] + let addr = address ?? UInt64(offset) + return disassemble(Data(slice), at: addr, count: count) + } + + /// Return the canonical name string for an AArch64 register ID (e.g. "x0", "w1", "wzr"). + public func registerName(_ regID: UInt32) -> String? { + cs.registerName(regID) + } +} diff --git a/sources/FirmwarePatcher/ARM64/ARM64Encoder.swift b/sources/FirmwarePatcher/ARM64/ARM64Encoder.swift new file mode 100644 index 0000000..9592448 --- /dev/null +++ b/sources/FirmwarePatcher/ARM64/ARM64Encoder.swift @@ -0,0 +1,98 @@ +// ARM64Encoder.swift — PC-relative instruction encoding for ARM64. +// +// Replaces keystone-engine _asm_at() for branch/ADRP/ADD encoding. +// Each encoder produces a 4-byte little-endian Data value. + +import Foundation + +public enum ARM64Encoder { + // MARK: - Branch Encoding + + /// Encode unconditional B (branch) instruction. + /// + /// Format: `[31:26] = 0b000101`, `[25:0] = signed offset / 4` + /// Range: +/-128 MB + public static func encodeB(from pc: Int, to target: Int) -> Data? { + let delta = (target - pc) + guard delta & 0x3 == 0 else { return nil } + let imm26 = delta >> 2 + guard imm26 >= -(1 << 25), imm26 < (1 << 25) else { return nil } + let insn: UInt32 = 0x1400_0000 | (UInt32(bitPattern: Int32(imm26)) & 0x03FF_FFFF) + return ARM64.encodeU32(insn) + } + + /// Encode BL (branch with link) instruction. + /// + /// Format: `[31:26] = 0b100101`, `[25:0] = signed offset / 4` + /// Range: +/-128 MB + public static func encodeBL(from pc: Int, to target: Int) -> Data? { + let delta = (target - pc) + guard delta & 0x3 == 0 else { return nil } + let imm26 = delta >> 2 + guard imm26 >= -(1 << 25), imm26 < (1 << 25) else { return nil } + let insn: UInt32 = 0x9400_0000 | (UInt32(bitPattern: Int32(imm26)) & 0x03FF_FFFF) + return ARM64.encodeU32(insn) + } + + // MARK: - ADRP / ADD Encoding + + /// Encode ADRP instruction. + /// + /// ADRP loads a 4KB-aligned page address relative to PC. + /// Format: `[31] = 1 (op)`, `[30:29] = immlo`, `[28:24] = 0b10000`, + /// `[23:5] = immhi`, `[4:0] = Rd` + public static func encodeADRP(rd: UInt32, pc: UInt64, target: UInt64) -> Data? { + let pcPage = pc & ~0xFFF + let targetPage = target & ~0xFFF + let pageDelta = Int64(targetPage) - Int64(pcPage) + let immVal = pageDelta >> 12 + guard immVal >= -(1 << 20), immVal < (1 << 20) else { return nil } + let imm21 = UInt32(bitPattern: Int32(immVal)) & 0x1FFFFF + let immlo = imm21 & 0x3 + let immhi = (imm21 >> 2) & 0x7FFFF + let insn: UInt32 = (1 << 31) | (immlo << 29) | (0b10000 << 24) | (immhi << 5) | (rd & 0x1F) + return ARM64.encodeU32(insn) + } + + /// Encode ADD Xd, Xn, #imm12 (64-bit, no shift). + /// + /// Format: `[31] = 1 (sf)`, `[30:29] = 00`, `[28:24] = 0b10001`, + /// `[23:22] = 00 (shift)`, `[21:10] = imm12`, `[9:5] = Rn`, `[4:0] = Rd` + public static func encodeAddImm12(rd: UInt32, rn: UInt32, imm12: UInt32) -> Data? { + guard imm12 < 4096 else { return nil } + let insn: UInt32 = (1 << 31) | (0b0010001 << 24) | (imm12 << 10) | ((rn & 0x1F) << 5) | (rd & 0x1F) + return ARM64.encodeU32(insn) + } + + /// Encode MOVZ Wd, #imm16 (32-bit). + /// + /// Format: `[31] = 0 (sf)`, `[30:29] = 10`, `[28:23] = 100101`, + /// `[22:21] = hw`, `[20:5] = imm16`, `[4:0] = Rd` + public static func encodeMovzW(rd: UInt32, imm16: UInt16, shift: UInt32 = 0) -> Data? { + let hw = shift / 16 + guard hw <= 1 else { return nil } + let insn: UInt32 = (0b0_1010_0101 << 23) | (hw << 21) | (UInt32(imm16) << 5) | (rd & 0x1F) + return ARM64.encodeU32(insn) + } + + /// Encode MOVZ Xd, #imm16 (64-bit). + public static func encodeMovzX(rd: UInt32, imm16: UInt16, shift: UInt32 = 0) -> Data? { + let hw = shift / 16 + guard hw <= 3 else { return nil } + let insn: UInt32 = (0b1_1010_0101 << 23) | (hw << 21) | (UInt32(imm16) << 5) | (rd & 0x1F) + return ARM64.encodeU32(insn) + } + + // MARK: - Decode Helpers + + /// Decode a B or BL target address from an instruction at `pc`. + public static func decodeBranchTarget(insn: UInt32, pc: UInt64) -> UInt64? { + let op = insn >> 26 + guard op == 0b000101 || op == 0b100101 else { return nil } + let imm26 = insn & 0x03FF_FFFF + // Sign-extend 26-bit to 32-bit + let signedImm = Int32(bitPattern: imm26 << 6) >> 6 + let offset = Int64(signedImm) * 4 + return UInt64(Int64(pc) + offset) + } +} diff --git a/sources/FirmwarePatcher/AVPBooter/AVPBooterPatcher.swift b/sources/FirmwarePatcher/AVPBooter/AVPBooterPatcher.swift new file mode 100644 index 0000000..6ec3d3b --- /dev/null +++ b/sources/FirmwarePatcher/AVPBooter/AVPBooterPatcher.swift @@ -0,0 +1,148 @@ +// AVPBooterPatcher.swift — AVPBooter DGST bypass patcher. +// +// Python source: scripts/fw_patch.py patch_avpbooter() +// +// Strategy: +// 1. Disassemble the entire binary. +// 2. Find the first instruction that references the DGST marker constant +// (0x4447 appears as a 16-bit immediate in a MOVZ/MOVK encoding of 0x44475354). +// 3. Scan forward (up to 512 instructions) for the nearest RET/RETAA/RETAB. +// 4. Scan backward from RET (up to 32 instructions) for the last `mov x0, ...` +// or conditional-select instruction writing x0/w0. +// 5. Patch that instruction to `mov x0, #0`. + +import Foundation + +/// Patcher for AVPBooter DGST bypass. +public final class AVPBooterPatcher: Patcher { + public let component = "avpbooter" + public let verbose: Bool + + let buffer: BinaryBuffer + let disasm = ARM64Disassembler() + var patches: [PatchRecord] = [] + + // MARK: - Constants + + /// The hex string fragment Capstone emits when an instruction encodes 0x4447 + /// (lower half of "DGST" / 0x44475354 little-endian). + private static let dgstSearch = "0x4447" + + /// Mnemonics that write to x0/w0 via conditional selection. + private static let cselMnemonics: Set = ["cset", "csinc", "csinv", "csneg"] + + /// Mnemonics that terminate a scan region (branch or return). + private static let stopMnemonics: Set = ["ret", "retaa", "retab", "b", "bl", "br", "blr"] + + public init(data: Data, verbose: Bool = true) { + buffer = BinaryBuffer(data) + self.verbose = verbose + } + + // MARK: - Patcher + + public func findAll() throws -> [PatchRecord] { + patches = [] + try patchDGSTBypass() + return patches + } + + @discardableResult + public func apply() throws -> Int { + let _ = try findAll() + for record in patches { + buffer.writeBytes(at: record.fileOffset, bytes: record.patchedBytes) + } + if verbose, !patches.isEmpty { + print("\n [\(patches.count) AVPBooter patch(es) applied]") + } + return patches.count + } + + public var patchedData: Data { + buffer.data + } + + // MARK: - DGST Bypass + + private func patchDGSTBypass() throws { + // Disassemble entire binary (raw ARM64, base address 0). + let insns = disasm.disassemble(buffer.data, at: 0) + guard !insns.isEmpty else { + throw PatcherError.invalidFormat("AVPBooter: disassembly produced no instructions") + } + + // Step 1 — locate the first instruction that references the DGST constant. + guard let hitIdx = insns.firstIndex(where: { insn in + "\(insn.mnemonic) \(insn.operandString)".contains(Self.dgstSearch) + }) else { + throw PatcherError.patchSiteNotFound("AVPBooter DGST: constant 0x4447 not found in binary") + } + + // Step 2 — scan forward up to 512 instructions for a RET epilogue. + let scanEnd = min(hitIdx + 512, insns.count) + guard let retIdx = insns[hitIdx ..< scanEnd].firstIndex(where: { insn in + insn.mnemonic == "ret" || insn.mnemonic == "retaa" || insn.mnemonic == "retab" + }) else { + throw PatcherError.patchSiteNotFound("AVPBooter DGST: epilogue RET not found within 512 instructions") + } + + // Step 3 — scan backward from RET (up to 32 instructions) for x0/w0 setter. + let backStart = max(retIdx - 32, 0) + var x0Idx: Int? = nil + + // Iterate backward: from retIdx-1 down to backStart. + var i = retIdx - 1 + while i >= backStart { + let insn = insns[i] + let mn = insn.mnemonic + let op = insn.operandString + + if mn == "mov", op.hasPrefix("x0,") || op.hasPrefix("w0,") { + x0Idx = i + break + } + if Self.cselMnemonics.contains(mn), op.hasPrefix("x0,") || op.hasPrefix("w0,") { + x0Idx = i + break + } + // Stop if we cross a function boundary or unconditional branch. + if Self.stopMnemonics.contains(mn) { + break + } + i -= 1 + } + + guard let targetIdx = x0Idx else { + throw PatcherError.patchSiteNotFound("AVPBooter DGST: x0 setter not found before RET") + } + + let target = insns[targetIdx] + let fileOff = Int(target.address) // base address is 0, so VA == file offset + + let originalBytes = buffer.readBytes(at: fileOff, count: 4) + let patchedBytes = ARM64.movX0_0 + + let beforeStr = "\(target.mnemonic) \(target.operandString)" + let afterInsn = disasm.disassembleOne(patchedBytes, at: UInt64(fileOff)) + let afterStr = afterInsn.map { "\($0.mnemonic) \($0.operandString)" } ?? "mov x0, #0" + + let record = PatchRecord( + patchID: "avpbooter.dgst_bypass", + component: component, + fileOffset: fileOff, + virtualAddress: nil, + originalBytes: originalBytes, + patchedBytes: patchedBytes, + beforeDisasm: beforeStr, + afterDisasm: afterStr, + description: "DGST validation bypass: force x0=0 return value" + ) + patches.append(record) + + if verbose { + print(String(format: " 0x%06X: %@ → %@ [avpbooter.dgst_bypass]", + fileOff, beforeStr, afterStr)) + } + } +} diff --git a/sources/FirmwarePatcher/Binary/BinaryBuffer.swift b/sources/FirmwarePatcher/Binary/BinaryBuffer.swift new file mode 100644 index 0000000..854b225 --- /dev/null +++ b/sources/FirmwarePatcher/Binary/BinaryBuffer.swift @@ -0,0 +1,124 @@ +// BinaryBuffer.swift — Mutable binary data buffer with read/write helpers. + +import Foundation + +/// A mutable binary buffer for reading and patching firmware data. +public final class BinaryBuffer: @unchecked Sendable { + /// The mutable working data. + public var data: Data + + /// The original immutable snapshot (for before/after comparison). + public let original: Data + + public var count: Int { + data.count + } + + public init(_ data: Data) { + self.data = data + original = data + } + + public convenience init(contentsOf url: URL) throws { + try self.init(Data(contentsOf: url)) + } + + // MARK: - Read Helpers + + /// Read a little-endian UInt32 at the given byte offset. + @inlinable + public func readU32(at offset: Int) -> UInt32 { + data.withUnsafeBytes { buf in + buf.load(fromByteOffset: offset, as: UInt32.self) + } + } + + /// Read a little-endian UInt64 at the given byte offset. + @inlinable + public func readU64(at offset: Int) -> UInt64 { + data.withUnsafeBytes { buf in + buf.load(fromByteOffset: offset, as: UInt64.self) + } + } + + /// Read bytes at the given range. + public func readBytes(at offset: Int, count: Int) -> Data { + data[offset ..< offset + count] + } + + // MARK: - Write Helpers + + /// Write a little-endian UInt32 at the given byte offset. + @inlinable + public func writeU32(at offset: Int, value: UInt32) { + withUnsafeBytes(of: value.littleEndian) { src in + data.replaceSubrange(offset ..< offset + 4, with: src) + } + } + + /// Write raw bytes at the given offset. + public func writeBytes(at offset: Int, bytes: Data) { + data.replaceSubrange(offset ..< offset + bytes.count, with: bytes) + } + + // MARK: - Search Helpers + + /// Find all occurrences of a byte pattern in the data. + public func findAll(_ pattern: Data, in range: Range? = nil) -> [Int] { + let searchRange = range ?? 0 ..< data.count + var results: [Int] = [] + var offset = searchRange.lowerBound + while offset < searchRange.upperBound - pattern.count + 1 { + if let found = data.range(of: pattern, in: offset ..< searchRange.upperBound) { + results.append(found.lowerBound) + offset = found.lowerBound + 1 + } else { + break + } + } + return results + } + + /// Find a null-terminated C string at the given offset. + public func readCString(at offset: Int) -> String? { + data.withUnsafeBytes { buf in + guard offset < buf.count else { return nil } + let ptr = buf.baseAddress!.advanced(by: offset) + .assumingMemoryBound(to: CChar.self) + return String(cString: ptr) + } + } + + /// Find the first occurrence of a C string in the data. + /// Matches Python `find_string()`: walks backward from the match to the + /// preceding NUL byte so that the returned offset is the C-string start. + public func findString(_ string: String, from: Int = 0) -> Int? { + guard let encoded = string.data(using: .utf8) else { return nil } + // Try with null terminator first (exact C-string match) + var pattern = encoded + pattern.append(0) + if let range = data.range(of: pattern, in: from ..< data.count) { + // Walk backward to the preceding NUL — that's the C string start + var cstr = range.lowerBound + while cstr > 0, data[cstr - 1] != 0 { + cstr -= 1 + } + return cstr + } + // Try without null terminator (substring match) + if let range = data.range(of: encoded, in: from ..< data.count) { + var cstr = range.lowerBound + while cstr > 0, data[cstr - 1] != 0 { + cstr -= 1 + } + return cstr + } + return nil + } + + /// Find all occurrences of a C string in the data. + public func findAllStrings(_ string: String) -> [Int] { + guard let encoded = string.data(using: .utf8) else { return [] } + return findAll(encoded) + } +} diff --git a/sources/FirmwarePatcher/Binary/IM4PHandler.swift b/sources/FirmwarePatcher/Binary/IM4PHandler.swift new file mode 100644 index 0000000..bf420db --- /dev/null +++ b/sources/FirmwarePatcher/Binary/IM4PHandler.swift @@ -0,0 +1,52 @@ +// IM4PHandler.swift — Wrapper around Img4tool for IM4P firmware container handling. + +import Foundation +import Img4tool + +/// Handles loading, extracting, and re-packaging IM4P firmware containers. +public enum IM4PHandler { + /// Load a firmware file as IM4P or raw data. + /// + /// - Parameter url: Path to the firmware file. + /// - Returns: Tuple of (extracted payload data, original IM4P if applicable). + public static func load(contentsOf url: URL) throws -> (payload: Data, im4p: IM4P?) { + let fileData = try Data(contentsOf: url) + + // Try to parse as IM4P first + if let im4p = try? IM4P(fileData) { + let payload = try im4p.payload() + return (payload, im4p) + } + + // Fall back to raw data + return (fileData, nil) + } + + /// Save patched data back to an IM4P container or as raw data. + /// + /// If the original was IM4P, re-packages with the same fourcc and LZFSE compression. + /// Otherwise, writes raw bytes. + /// + /// - Parameters: + /// - patchedData: The patched payload bytes. + /// - originalIM4P: The original IM4P container (nil for raw files). + /// - url: Output file path. + public static func save( + patchedData: Data, + originalIM4P: IM4P?, + to url: URL + ) throws { + if let original = originalIM4P { + // Re-package as IM4P with same fourcc and LZFSE compression + let newIM4P = try IM4P( + fourcc: original.fourcc, + description: original.description, + payload: patchedData, + compression: "lzfse" + ) + try newIM4P.data.write(to: url) + } else { + try patchedData.write(to: url) + } + } +} diff --git a/sources/FirmwarePatcher/Binary/MachOHelpers.swift b/sources/FirmwarePatcher/Binary/MachOHelpers.swift new file mode 100644 index 0000000..a4aa6db --- /dev/null +++ b/sources/FirmwarePatcher/Binary/MachOHelpers.swift @@ -0,0 +1,183 @@ +// MachOHelpers.swift — Mach-O parsing utilities for firmware patching. + +import Foundation +import MachOKit + +// MARK: - Segment/Section Info + +/// Minimal segment info extracted from a Mach-O binary. +public struct MachOSegmentInfo: Sendable { + public let name: String + public let vmAddr: UInt64 + public let vmSize: UInt64 + public let fileOffset: UInt64 + public let fileSize: UInt64 +} + +/// Minimal section info extracted from a Mach-O binary. +public struct MachOSectionInfo: Sendable { + public let segmentName: String + public let sectionName: String + public let address: UInt64 + public let size: UInt64 + public let fileOffset: UInt32 +} + +// MARK: - MachO Parser + +/// Mach-O parsing utilities for kernel/firmware binary analysis. +public enum MachOParser { + /// Parse all segments from a Mach-O binary in a Data buffer. + public static func parseSegments(from data: Data) -> [MachOSegmentInfo] { + var segments: [MachOSegmentInfo] = [] + guard data.count > 32 else { return segments } + + let magic: UInt32 = data.withUnsafeBytes { $0.load(as: UInt32.self) } + guard magic == 0xFEED_FACF else { return segments } // MH_MAGIC_64 + + let ncmds: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: 16, as: UInt32.self) } + var offset = 32 // sizeof(mach_header_64) + + for _ in 0 ..< ncmds { + guard offset + 8 <= data.count else { break } + let cmd: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self) } + let cmdsize: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 4, as: UInt32.self) } + + if cmd == 0x19 { // LC_SEGMENT_64 + let nameData = data[offset + 8 ..< offset + 24] + let name = String(data: nameData, encoding: .utf8)? + .trimmingCharacters(in: CharacterSet(charactersIn: "\0")) ?? "" + let vmAddr: UInt64 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 24, as: UInt64.self) } + let vmSize: UInt64 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 32, as: UInt64.self) } + let fileOff: UInt64 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 40, as: UInt64.self) } + let fileSize: UInt64 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 48, as: UInt64.self) } + + segments.append(MachOSegmentInfo( + name: name, vmAddr: vmAddr, vmSize: vmSize, + fileOffset: fileOff, fileSize: fileSize + )) + } + offset += Int(cmdsize) + } + return segments + } + + /// Parse all sections from a Mach-O binary. + /// Returns a dictionary keyed by "segment,section". + public static func parseSections(from data: Data) -> [String: MachOSectionInfo] { + var sections: [String: MachOSectionInfo] = [:] + guard data.count > 32 else { return sections } + + let magic: UInt32 = data.withUnsafeBytes { $0.load(as: UInt32.self) } + guard magic == 0xFEED_FACF else { return sections } + + let ncmds: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: 16, as: UInt32.self) } + var offset = 32 + + for _ in 0 ..< ncmds { + guard offset + 8 <= data.count else { break } + let cmd: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self) } + let cmdsize: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 4, as: UInt32.self) } + + if cmd == 0x19 { // LC_SEGMENT_64 + let segNameData = data[offset + 8 ..< offset + 24] + let segName = String(data: segNameData, encoding: .utf8)? + .trimmingCharacters(in: CharacterSet(charactersIn: "\0")) ?? "" + let nsects: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 64, as: UInt32.self) } + + var sectOff = offset + 72 // sizeof(segment_command_64) header + for _ in 0 ..< nsects { + guard sectOff + 80 <= data.count else { break } + let sectNameData = data[sectOff ..< sectOff + 16] + let sectName = String(data: sectNameData, encoding: .utf8)? + .trimmingCharacters(in: CharacterSet(charactersIn: "\0")) ?? "" + let addr: UInt64 = data.withUnsafeBytes { $0.load(fromByteOffset: sectOff + 32, as: UInt64.self) } + let size: UInt64 = data.withUnsafeBytes { $0.load(fromByteOffset: sectOff + 40, as: UInt64.self) } + let fileOff: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: sectOff + 48, as: UInt32.self) } + + let key = "\(segName),\(sectName)" + sections[key] = MachOSectionInfo( + segmentName: segName, sectionName: sectName, + address: addr, size: size, fileOffset: fileOff + ) + sectOff += 80 + } + } + offset += Int(cmdsize) + } + return sections + } + + /// Convert a virtual address to a file offset using segment mappings. + public static func vaToFileOffset(_ va: UInt64, segments: [MachOSegmentInfo]) -> Int? { + for seg in segments { + if va >= seg.vmAddr, va < seg.vmAddr + seg.vmSize { + return Int(seg.fileOffset + (va - seg.vmAddr)) + } + } + return nil + } + + /// Convert a virtual address to a file offset by parsing segments from data. + public static func vaToFileOffset(_ va: UInt64, in data: Data) -> Int? { + let segments = parseSegments(from: data) + return vaToFileOffset(va, segments: segments) + } + + /// Parse LC_SYMTAB information. + /// Returns (symoff, nsyms, stroff, strsize) or nil. + public static func parseSymtab(from data: Data) -> (symoff: Int, nsyms: Int, stroff: Int, strsize: Int)? { + guard data.count > 32 else { return nil } + + let ncmds: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: 16, as: UInt32.self) } + var offset = 32 + + for _ in 0 ..< ncmds { + guard offset + 8 <= data.count else { break } + let cmd: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self) } + let cmdsize: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 4, as: UInt32.self) } + + if cmd == 0x02 { // LC_SYMTAB + let symoff: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 8, as: UInt32.self) } + let nsyms: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 12, as: UInt32.self) } + let stroff: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 16, as: UInt32.self) } + let strsize: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 20, as: UInt32.self) } + return (Int(symoff), Int(nsyms), Int(stroff), Int(strsize)) + } + offset += Int(cmdsize) + } + return nil + } + + /// Find a symbol containing the given name fragment. Returns its virtual address. + public static func findSymbol(containing fragment: String, in data: Data) -> UInt64? { + guard let symtab = parseSymtab(from: data) else { return nil } + + for i in 0 ..< symtab.nsyms { + let entryOff = symtab.symoff + i * 16 // sizeof(nlist_64) + guard entryOff + 16 <= data.count else { break } + + let nStrx: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: entryOff, as: UInt32.self) } + let nValue: UInt64 = data.withUnsafeBytes { $0.load(fromByteOffset: entryOff + 8, as: UInt64.self) } + + guard nStrx < symtab.strsize, nValue != 0 else { continue } + + let strStart = symtab.stroff + Int(nStrx) + guard strStart < data.count else { continue } + + // Read null-terminated string + var strEnd = strStart + while strEnd < data.count, strEnd < symtab.stroff + symtab.strsize { + if data[strEnd] == 0 { break } + strEnd += 1 + } + + if let name = String(data: data[strStart ..< strEnd], encoding: .ascii), + name.contains(fragment) + { + return nValue + } + } + return nil + } +} diff --git a/sources/FirmwarePatcher/Core/PatchRecord.swift b/sources/FirmwarePatcher/Core/PatchRecord.swift new file mode 100644 index 0000000..32d9e7c --- /dev/null +++ b/sources/FirmwarePatcher/Core/PatchRecord.swift @@ -0,0 +1,72 @@ +// PatchRecord.swift — Per-patch verification record for migration validation. + +import Foundation + +/// A single patch application record, used to compare Python vs Swift output. +public struct PatchRecord: Codable, Equatable, Sendable { + /// Unique patch identifier (e.g., "kernel.bsd_init_rootvp"). + public let patchID: String + + /// Component being patched (e.g., "kernelcache", "ibss", "txm"). + public let component: String + + /// File offset where the patch is applied. + public let fileOffset: Int + + /// Virtual address (if applicable, nil for raw binaries). + public let virtualAddress: UInt64? + + /// Original bytes before patching. + public let originalBytes: Data + + /// Replacement bytes after patching. + public let patchedBytes: Data + + /// Capstone disassembly of original bytes. + public let beforeDisasm: String + + /// Capstone disassembly of patched bytes. + public let afterDisasm: String + + /// Human-readable description of what this patch does. + public let patchDescription: String + + public init( + patchID: String, + component: String, + fileOffset: Int, + virtualAddress: UInt64? = nil, + originalBytes: Data, + patchedBytes: Data, + beforeDisasm: String = "", + afterDisasm: String = "", + description: String + ) { + self.patchID = patchID + self.component = component + self.fileOffset = fileOffset + self.virtualAddress = virtualAddress + self.originalBytes = originalBytes + self.patchedBytes = patchedBytes + self.beforeDisasm = beforeDisasm + self.afterDisasm = afterDisasm + patchDescription = description + } +} + +extension PatchRecord: CustomStringConvertible { + public var description: String { + let addr = virtualAddress.map { String(format: " (VA 0x%llX)", $0) } ?? "" + return String(format: " 0x%06X%@: %@ → %@ [%@]", + fileOffset, addr, + beforeDisasm.isEmpty ? originalBytes.hex : beforeDisasm, + afterDisasm.isEmpty ? patchedBytes.hex : afterDisasm, + patchID) + } +} + +extension Data { + var hex: String { + map { String(format: "%02x", $0) }.joined() + } +} diff --git a/sources/FirmwarePatcher/Core/PatcherError.swift b/sources/FirmwarePatcher/Core/PatcherError.swift new file mode 100644 index 0000000..a074e4c --- /dev/null +++ b/sources/FirmwarePatcher/Core/PatcherError.swift @@ -0,0 +1,29 @@ +// PatcherError.swift — Error types for firmware patching. + +import Foundation + +public enum PatcherError: Error, CustomStringConvertible, Sendable { + case fileNotFound(String) + case invalidFormat(String) + case patchSiteNotFound(String) + case patchVerificationFailed(String) + case encodingFailed(String) + case multipleMatchesFound(String, count: Int) + + public var description: String { + switch self { + case let .fileNotFound(path): + "File not found: \(path)" + case let .invalidFormat(msg): + "Invalid format: \(msg)" + case let .patchSiteNotFound(msg): + "Patch site not found: \(msg)" + case let .patchVerificationFailed(msg): + "Patch verification failed: \(msg)" + case let .encodingFailed(msg): + "Instruction encoding failed: \(msg)" + case let .multipleMatchesFound(msg, count): + "Expected 1 match for \(msg), found \(count)" + } + } +} diff --git a/sources/FirmwarePatcher/Core/PatcherProtocol.swift b/sources/FirmwarePatcher/Core/PatcherProtocol.swift new file mode 100644 index 0000000..8e5464d --- /dev/null +++ b/sources/FirmwarePatcher/Core/PatcherProtocol.swift @@ -0,0 +1,28 @@ +// PatcherProtocol.swift — Common protocol for all firmware patchers. + +import Foundation + +/// A firmware patcher that can find and apply patches to a binary buffer. +public protocol Patcher { + /// The component name (e.g., "kernelcache", "ibss", "txm"). + var component: String { get } + + /// Whether to print verbose output. + var verbose: Bool { get } + + /// Find all patch sites and return patch records (dry-run mode). + func findAll() throws -> [PatchRecord] + + /// Apply all patches to the buffer. Returns the number of patches applied. + @discardableResult + func apply() throws -> Int +} + +extension Patcher { + /// Log a message if verbose mode is enabled. + func log(_ message: String) { + if verbose { + print(message) + } + } +} diff --git a/sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift b/sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift new file mode 100644 index 0000000..c9ecca9 --- /dev/null +++ b/sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift @@ -0,0 +1,337 @@ +// DeviceTreePatcher.swift — DeviceTree payload patcher. +// +// Translated from Python source: scripts/dtree.py +// +// Strategy: +// 1. Parse the flat device tree binary into a node/property tree. +// 2. Apply a fixed set of property patches (serial-number, home-button-type, +// artwork-device-subtype, island-notch-location). +// 3. Serialize the modified tree back to flat binary. + +import Foundation + +/// Patcher for DeviceTree payloads. +public final class DeviceTreePatcher: Patcher { + public let component = "devicetree" + public let verbose: Bool + + let buffer: BinaryBuffer + var patches: [PatchRecord] = [] + + // MARK: - Patch Definitions + + /// A single property patch specification. + struct PropertyPatch { + let nodePath: [String] + let property: String + let length: Int + let flags: UInt16 + let value: PropertyValue + let patchID: String + let description: String + } + + /// The value to write into a device tree property. + enum PropertyValue { + case string(String) + case integer(UInt64) + } + + /// Fixed set of device tree patches, matching scripts/dtree.py PATCHES. + static let propertyPatches: [PropertyPatch] = [ + PropertyPatch( + nodePath: ["device-tree"], + property: "serial-number", + length: 12, + flags: 0, + value: .string("vphone-1337"), + patchID: "devicetree.serial_number", + description: "Set serial number to vphone-1337" + ), + PropertyPatch( + nodePath: ["device-tree", "buttons"], + property: "home-button-type", + length: 4, + flags: 0, + value: .integer(2), + patchID: "devicetree.home_button_type", + description: "Set home button type to 2" + ), + PropertyPatch( + nodePath: ["device-tree", "product"], + property: "artwork-device-subtype", + length: 4, + flags: 0, + value: .integer(2556), + patchID: "devicetree.artwork_device_subtype", + description: "Set artwork device subtype to 2556" + ), + PropertyPatch( + nodePath: ["device-tree", "product"], + property: "island-notch-location", + length: 4, + flags: 0, + value: .integer(144), + patchID: "devicetree.island_notch_location", + description: "Set island notch location to 144" + ), + ] + + // MARK: - Device Tree Structures + + /// A single property in a device tree node. + final class DTProperty { + var name: String + var length: Int + var flags: UInt16 + var value: Data + /// File offset of the property value within the flat binary. + let valueOffset: Int + + init(name: String, length: Int, flags: UInt16, value: Data, valueOffset: Int) { + self.name = name + self.length = length + self.flags = flags + self.value = value + self.valueOffset = valueOffset + } + } + + /// A node in the device tree containing properties and child nodes. + final class DTNode { + var properties: [DTProperty] = [] + var children: [DTNode] = [] + } + + // MARK: - Init + + public init(data: Data, verbose: Bool = true) { + buffer = BinaryBuffer(data) + self.verbose = verbose + } + + // MARK: - Patcher + + public func findAll() throws -> [PatchRecord] { + patches = [] + let root = try parsePayload(buffer.data) + try applyPatches(root: root) + return patches + } + + @discardableResult + public func apply() throws -> Int { + let _ = try findAll() + for record in patches { + buffer.writeBytes(at: record.fileOffset, bytes: record.patchedBytes) + } + if verbose, !patches.isEmpty { + print("\n [\(patches.count) DeviceTree patch(es) applied]") + } + return patches.count + } + + public var patchedData: Data { + buffer.data + } + + // MARK: - Parsing + + /// Align a value up to the next 4-byte boundary. + private static func align4(_ n: Int) -> Int { + (n + 3) & ~3 + } + + /// Decode a null-terminated C string from raw bytes. + private static func decodeCString(_ data: Data) -> String { + if let nullIndex = data.firstIndex(of: 0) { + let slice = data[data.startIndex ..< nullIndex] + return String(bytes: slice, encoding: .utf8) ?? "" + } + return String(bytes: data, encoding: .utf8) ?? "" + } + + /// Parse a device tree node from the flat binary at the given offset. + /// Returns the parsed node and the offset past the end of the node. + private func parseNode(_ blob: Data, offset: Int) throws -> (DTNode, Int) { + guard offset + 8 <= blob.count else { + throw PatcherError.invalidFormat("DeviceTree: truncated node header at offset \(offset)") + } + + let nProps = blob.loadLE(UInt32.self, at: offset) + let nChildren = blob.loadLE(UInt32.self, at: offset + 4) + var pos = offset + 8 + + let node = DTNode() + + for _ in 0 ..< nProps { + guard pos + 36 <= blob.count else { + throw PatcherError.invalidFormat("DeviceTree: truncated property header at offset \(pos)") + } + + let nameData = blob[blob.startIndex.advanced(by: pos) ..< blob.startIndex.advanced(by: pos + 32)] + let name = Self.decodeCString(Data(nameData)) + let length = Int(blob.loadLE(UInt16.self, at: pos + 32)) + let flags = blob.loadLE(UInt16.self, at: pos + 34) + pos += 36 + + guard pos + length <= blob.count else { + throw PatcherError.invalidFormat("DeviceTree: truncated property value '\(name)' at offset \(pos)") + } + + let value = Data(blob[blob.startIndex.advanced(by: pos) ..< blob.startIndex.advanced(by: pos + length)]) + let valueOffset = pos + pos += Self.align4(length) + + node.properties.append(DTProperty( + name: name, length: length, flags: flags, + value: value, valueOffset: valueOffset + )) + } + + for _ in 0 ..< nChildren { + let (child, nextPos) = try parseNode(blob, offset: pos) + node.children.append(child) + pos = nextPos + } + + return (node, pos) + } + + /// Parse the entire device tree payload. + private func parsePayload(_ blob: Data) throws -> DTNode { + let (root, end) = try parseNode(blob, offset: 0) + guard end == blob.count else { + throw PatcherError.invalidFormat( + "DeviceTree: unexpected trailing bytes (\(blob.count - end) extra)" + ) + } + return root + } + + // MARK: - Node Navigation + + /// Get the "name" property value from a node. + private func nodeName(_ node: DTNode) -> String { + for prop in node.properties { + if prop.name == "name" { + return Self.decodeCString(prop.value) + } + } + return "" + } + + /// Find a direct child node by name. + private func findChild(_ node: DTNode, name: String) throws -> DTNode { + for child in node.children { + if nodeName(child) == name { + return child + } + } + throw PatcherError.patchSiteNotFound("DeviceTree: missing child node '\(name)'") + } + + /// Resolve a node path like ["device-tree", "buttons"] from the root. + private func resolveNode(_ root: DTNode, path: [String]) throws -> DTNode { + guard !path.isEmpty, path[0] == "device-tree" else { + throw PatcherError.patchSiteNotFound("DeviceTree: invalid node path \(path)") + } + var node = root + for name in path.dropFirst() { + node = try findChild(node, name: name) + } + return node + } + + /// Find a property by name within a node. + private func findProperty(_ node: DTNode, name: String) throws -> DTProperty { + for prop in node.properties { + if prop.name == name { + return prop + } + } + throw PatcherError.patchSiteNotFound("DeviceTree: missing property '\(name)'") + } + + // MARK: - Value Encoding + + /// Encode a string value with null termination, padded/truncated to a fixed length. + private static func encodeFixedString(_ text: String, length: Int) -> Data { + var raw = Data(text.utf8) + raw.append(0) // null terminator + if raw.count > length { + return Data(raw.prefix(length)) + } + raw.append(contentsOf: [UInt8](repeating: 0, count: length - raw.count)) + return raw + } + + /// Encode an integer value as little-endian bytes. + private static func encodeInteger(_ value: UInt64, length: Int) throws -> Data { + var data = Data(count: length) + switch length { + case 1: + data[0] = UInt8(value & 0xFF) + case 2: + let v = UInt16(value & 0xFFFF) + data.withUnsafeMutableBytes { $0.storeBytes(of: v.littleEndian, as: UInt16.self) } + case 4: + let v = UInt32(value & 0xFFFF_FFFF) + data.withUnsafeMutableBytes { $0.storeBytes(of: v.littleEndian, as: UInt32.self) } + case 8: + data.withUnsafeMutableBytes { $0.storeBytes(of: value.littleEndian, as: UInt64.self) } + default: + throw PatcherError.invalidFormat("DeviceTree: unsupported integer length \(length)") + } + return data + } + + // MARK: - Patch Application + + /// Apply all property patches and record each change. + private func applyPatches(root: DTNode) throws { + for patch in Self.propertyPatches { + let node = try resolveNode(root, path: patch.nodePath) + let prop = try findProperty(node, name: patch.property) + + let originalBytes = Data(prop.value.prefix(patch.length)) + + let newValue: Data = switch patch.value { + case let .string(s): + Self.encodeFixedString(s, length: patch.length) + case let .integer(v): + try Self.encodeInteger(v, length: patch.length) + } + + let record = PatchRecord( + patchID: patch.patchID, + component: component, + fileOffset: prop.valueOffset, + virtualAddress: nil, + originalBytes: originalBytes, + patchedBytes: newValue, + description: patch.description + ) + patches.append(record) + + if verbose { + print(String(format: " 0x%06X: %@ → %@ [%@]", + prop.valueOffset, + originalBytes.hex, + newValue.hex, + patch.patchID)) + } + } + } +} + +// MARK: - Data Helpers + +private extension Data { + /// Load a little-endian integer at the given byte offset. + func loadLE(_: T.Type, at offset: Int) -> T { + withUnsafeBytes { buf in + T(littleEndian: buf.load(fromByteOffset: offset, as: T.self)) + } + } +} diff --git a/sources/FirmwarePatcher/IBoot/IBootJBPatcher.swift b/sources/FirmwarePatcher/IBoot/IBootJBPatcher.swift new file mode 100644 index 0000000..8581ba7 --- /dev/null +++ b/sources/FirmwarePatcher/IBoot/IBootJBPatcher.swift @@ -0,0 +1,187 @@ +// IBootJBPatcher.swift — JB-variant iBoot patcher (nonce bypass). +// +// Python source: scripts/patchers/iboot_jb.py + +import Capstone +import Foundation + +/// JB-variant patcher for iBoot images. +/// +/// Adds iBSS-only patches: +/// 1. patchSkipGenerateNonce — locate "boot-nonce" ADRP+ADD refs, find +/// tbz w0, #0, / mov w0, #0 / bl pattern, convert tbz → b +public final class IBootJBPatcher: IBootPatcher { + override public func findAll() throws -> [PatchRecord] { + patches = [] + if mode == .ibss { + patchSkipGenerateNonce() + } + return patches + } + + // MARK: - JB Patches + + @discardableResult + func patchSkipGenerateNonce() -> Bool { + let needle = Data("boot-nonce".utf8) + let stringOffsets = buffer.findAll(needle) + + if stringOffsets.isEmpty { + if verbose { print(" [-] iBSS JB: no refs to 'boot-nonce'") } + return false + } + + // Collect all ADRP+ADD sites that reference any "boot-nonce" occurrence. + var addOffsets: [Int] = [] + for strOff in stringOffsets { + let refs = findRefsToOffset(strOff) + for (_, addOff) in refs { + addOffsets.append(addOff) + } + } + + if addOffsets.isEmpty { + if verbose { print(" [-] iBSS JB: no ADRP+ADD refs to 'boot-nonce'") } + return false + } + + // For each ADD ref, scan forward up to 0x100 bytes for the pattern: + // tbz/tbnz w0, #0, + // mov w0, #0 + // bl + for addOff in addOffsets { + let scanLimit = min(addOff + 0x100, buffer.count - 12) + var scan = addOff + while scan <= scanLimit { + guard + let i0 = disasm.disassembleOne(in: buffer.data, at: scan), + let i1 = disasm.disassembleOne(in: buffer.data, at: scan + 4), + let i2 = disasm.disassembleOne(in: buffer.data, at: scan + 8) + else { + scan += 4 + continue + } + + // i0 must be tbz or tbnz + guard i0.mnemonic == "tbz" || i0.mnemonic == "tbnz" else { + scan += 4 + continue + } + + // i0 operands: [0]=reg (w0), [1]=bit (0), [2]=target address + guard + let detail0 = i0.aarch64, + detail0.operands.count >= 3, + detail0.operands[0].type == AARCH64_OP_REG, + detail0.operands[0].reg.rawValue == AARCH64_REG_W0.rawValue, + detail0.operands[1].type == AARCH64_OP_IMM, + detail0.operands[1].imm == 0 + else { + scan += 4 + continue + } + + // i1 must be: mov w0, #0 + guard i1.mnemonic == "mov", i1.operandString == "w0, #0" else { + scan += 4 + continue + } + + // i2 must be bl + guard i2.mnemonic == "bl" else { + scan += 4 + continue + } + + // Branch target from tbz operand[2] + let target = Int(detail0.operands[2].imm) + + guard let patchBytes = ARM64Encoder.encodeB(from: scan, to: target) else { + if verbose { + print(String(format: " [-] iBSS JB: encodeB out of range at 0x%X → 0x%X", scan, target)) + } + scan += 4 + continue + } + + let originalBytes = buffer.readBytes(at: scan, count: 4) + let beforeStr = "\(i0.mnemonic) \(i0.operandString)" + let afterInsn = disasm.disassembleOne(patchBytes, at: UInt64(scan)) + let afterStr = afterInsn.map { "\($0.mnemonic) \($0.operandString)" } ?? "b" + + let record = PatchRecord( + patchID: "ibss_jb.skip_generate_nonce", + component: component, + fileOffset: scan, + virtualAddress: nil, + originalBytes: originalBytes, + patchedBytes: patchBytes, + beforeDisasm: beforeStr, + afterDisasm: afterStr, + description: "JB: skip generate_nonce" + ) + patches.append(record) + + if verbose { + print(String(format: " 0x%06X: %@ → %@ [ibss_jb.skip_generate_nonce]", + scan, beforeStr, afterStr)) + } + return true + } + } + + if verbose { print(" [-] iBSS JB: generate_nonce branch pattern not found") } + return false + } + + // MARK: - Reference Search Helpers + + /// Find all ADRP+ADD pairs in the binary that point to `targetOff`. + /// + /// Scans the entire buffer in 4-byte steps, checking consecutive instruction + /// pairs for the ADRP+ADD pattern. Matches when + /// `adrp_page_addr + add_imm12 == targetOff` (raw binary, base address = 0). + private func findRefsToOffset(_ targetOff: Int) -> [(adrpOff: Int, addOff: Int)] { + let data = buffer.data + let size = buffer.count + var refs: [(Int, Int)] = [] + + var off = 0 + while off + 8 <= size { + guard + let a = disasm.disassembleOne(in: data, at: off), + let b = disasm.disassembleOne(in: data, at: off + 4) + else { + off += 4 + continue + } + + guard + a.mnemonic == "adrp", + b.mnemonic == "add", + let detA = a.aarch64, + let detB = b.aarch64, + detA.operands.count >= 2, + detB.operands.count >= 3, + // Destination register of ADRP must match source register of ADD + detA.operands[0].reg.rawValue == detB.operands[1].reg.rawValue, + detA.operands[1].type == AARCH64_OP_IMM, + detB.operands[2].type == AARCH64_OP_IMM + else { + off += 4 + continue + } + + let pageAddr = detA.operands[1].imm // ADRP result (page-aligned VA) + let addImm = detB.operands[2].imm // ADD immediate (page offset) + + if pageAddr + addImm == Int64(targetOff) { + refs.append((off, off + 4)) + } + + off += 4 + } + + return refs + } +} diff --git a/sources/FirmwarePatcher/IBoot/IBootPatcher.swift b/sources/FirmwarePatcher/IBoot/IBootPatcher.swift new file mode 100644 index 0000000..ddc3c26 --- /dev/null +++ b/sources/FirmwarePatcher/IBoot/IBootPatcher.swift @@ -0,0 +1,585 @@ +// IBootPatcher.swift — iBoot chain patcher (iBSS, iBEC, LLB). +// +// Translated from Python: scripts/patchers/iboot.py +// Each patch mirrors Python logic exactly — no hardcoded offsets. +// +// Patch schedule by mode: +// ibss — serial labels + image4 callback +// ibec — ibss + boot-args +// llb — ibec + rootfs bypass (5 patches) + panic bypass + +import Capstone +import Foundation + +/// Patcher for iBoot components (iBSS, iBEC, LLB). +public class IBootPatcher: Patcher { + // MARK: - Types + + public enum Mode: String, Sendable { + case ibss + case ibec + case llb + } + + // MARK: - Constants + + /// Default custom boot-args string (Python: IBootPatcher.BOOT_ARGS) + static let bootArgs = "serial=3 -v debug=0x2014e %s" + + /// Chunked disassembly parameters (Python: CHUNK_SIZE, OVERLAP) + private static let chunkSize = 0x2000 + private static let chunkOverlap = 0x100 + + // MARK: - Properties + + public let component: String + public let verbose: Bool + + let buffer: BinaryBuffer + let mode: Mode + let disasm = ARM64Disassembler() + var patches: [PatchRecord] = [] + + // MARK: - Init + + public init(data: Data, mode: Mode, verbose: Bool = true) { + buffer = BinaryBuffer(data) + self.mode = mode + component = mode.rawValue + self.verbose = verbose + } + + // MARK: - Patcher Protocol + + public func findAll() throws -> [PatchRecord] { + patches = [] + + patchSerialLabels() + patchImage4Callback() + + if mode == .ibec || mode == .llb { + patchBootArgs() + } + + if mode == .llb { + patchRootfssBypass() + patchPanicBypass() + } + + return patches + } + + @discardableResult + public func apply() throws -> Int { + let _ = try findAll() + for record in patches { + buffer.writeBytes(at: record.fileOffset, bytes: record.patchedBytes) + } + if verbose, !patches.isEmpty { + print("\n [\(patches.count) \(mode.rawValue) patches applied]") + } + return patches.count + } + + /// Get the patched data. + public var patchedData: Data { + buffer.data + } + + // MARK: - Emit Helpers + + /// Record a code patch (disassembles before/after for logging). + func emit(_ offset: Int, _ patchBytes: Data, id: String, description: String) { + let originalBytes = buffer.readBytes(at: offset, count: patchBytes.count) + + let beforeInsn = disasm.disassembleOne(in: buffer.original, at: offset) + let afterInsn = disasm.disassembleOne(patchBytes, at: UInt64(offset)) + let beforeStr = beforeInsn.map { "\($0.mnemonic) \($0.operandString)" } ?? "???" + let afterStr = afterInsn.map { "\($0.mnemonic) \($0.operandString)" } ?? "???" + + let record = PatchRecord( + patchID: id, + component: component, + fileOffset: offset, + originalBytes: originalBytes, + patchedBytes: patchBytes, + beforeDisasm: beforeStr, + afterDisasm: afterStr, + description: description + ) + patches.append(record) + + if verbose { + print(String(format: " 0x%06X: %@ → %@ [%@]", offset, beforeStr, afterStr, description)) + } + } + + /// Record a string/data patch (not disassemblable). + func emitString(_ offset: Int, _ data: Data, id: String, description: String) { + let originalBytes = buffer.readBytes(at: offset, count: data.count) + let txt = String(data: data, encoding: .ascii) ?? data.hex + + let record = PatchRecord( + patchID: id, + component: component, + fileOffset: offset, + originalBytes: originalBytes, + patchedBytes: data, + beforeDisasm: "", + afterDisasm: repr(txt), + description: description + ) + patches.append(record) + + if verbose { + print(String(format: " 0x%06X: → %@ [%@]", offset, repr(txt), description)) + } + } + + private func repr(_ s: String) -> String { + "\"\(s)\"" + } + + // MARK: - Pattern Search Helpers + + /// Encode `mov w8, #` (MOVZ W8, #imm) as 4 little-endian bytes. + /// MOVZ W encoding: [31]=0 sf, [30:29]=10, [28:23]=100101, [22:21]=hw=00, + /// [20:5]=imm16, [4:0]=Rd=8 + func encodedMovW8(_ imm16: UInt32) -> Data { + let insn: UInt32 = 0x5280_0000 | ((imm16 & 0xFFFF) << 5) | 8 + return withUnsafeBytes(of: insn.littleEndian) { Data($0) } + } + + /// Encode `movk w8, #, lsl #16` (MOVK W8, #imm, LSL #16). + /// MOVK W: [31]=0, [30:29]=11, [28:23]=100101, [22:21]=hw=01, + /// [20:5]=imm16, [4:0]=Rd=8 + func encodedMovkW8Lsl16(_ imm16: UInt32) -> Data { + let insn: UInt32 = 0x72A0_0000 | ((imm16 & 0xFFFF) << 5) | 8 + return withUnsafeBytes(of: insn.littleEndian) { Data($0) } + } + + /// Find all file offsets where the given 4-byte pattern appears. + /// Equivalent to Python `_find_asm_pattern(data, asm_str)`. + func findPattern(_ pattern: Data) -> [Int] { + buffer.findAll(pattern) + } + + // MARK: - Chunked Disassembly + + /// Yield chunks of disassembled instructions over the whole binary. + /// Mirrors Python `_chunked_disasm()` with CHUNK_SIZE=0x2000, OVERLAP=0x100. + func chunkedDisasm() -> [[Instruction]] { + let size = buffer.original.count + var results: [[Instruction]] = [] + var off = 0 + while off < size { + let end = min(off + IBootPatcher.chunkSize, size) + let chunkLen = end - off + let slice = buffer.original[off ..< off + chunkLen] + let insns = disasm.disassemble(Data(slice), at: UInt64(off)) + results.append(insns) + off += IBootPatcher.chunkSize - IBootPatcher.chunkOverlap + } + return results + } + + // MARK: - 1. Serial Labels + + /// Find the two long '====...' banner runs and write the mode label into each. + /// Python: `patch_serial_labels()` + func patchSerialLabels() { + let labelStr = "Loaded \(mode.rawValue.uppercased())" + guard let labelBytes = labelStr.data(using: .ascii) else { return } + + // Collect all runs of '=' (>=20 chars) — same logic as Python. + let raw = buffer.original + var eqRuns: [Int] = [] + var i = raw.startIndex + + while i < raw.endIndex { + if raw[i] == UInt8(ascii: "=") { + let start = i + while i < raw.endIndex, raw[i] == UInt8(ascii: "=") { + i = raw.index(after: i) + } + let runLen = raw.distance(from: start, to: i) + if runLen >= 20 { + eqRuns.append(raw.distance(from: raw.startIndex, to: start)) + } + } else { + i = raw.index(after: i) + } + } + + if eqRuns.count < 2 { + if verbose { print(" [-] serial labels: <2 banner runs found") } + return + } + + for runStart in eqRuns.prefix(2) { + let writeOff = runStart + 1 // Python: run_start + 1 + emitString(writeOff, labelBytes, id: "\(component).serial_label", description: "serial label") + } + } + + // MARK: - 2. image4_validate_property_callback + + /// Find the b.ne + mov x0, x22 pattern with a preceding cmp. + /// Patch: b.ne → NOP, mov x0, x22 → mov x0, #0. + /// Python: `patch_image4_callback()` + func patchImage4Callback() { + var candidates: [(addr: Int, hasNeg1: Bool)] = [] + + for insns in chunkedDisasm() { + let count = insns.count + guard count >= 2 else { continue } + for i in 0 ..< count - 1 { + let a = insns[i] + let b = insns[i + 1] + + // Must be: b.ne followed immediately by mov x0, x22 + guard a.mnemonic == "b.ne" else { continue } + guard b.mnemonic == "mov", b.operandString == "x0, x22" else { continue } + + let addr = Int(a.address) + + // There must be a cmp within the 8 preceding instructions + let lookback = max(0, i - 8) + let hasCmp = insns[lookback ..< i].contains { $0.mnemonic == "cmp" } + guard hasCmp else { continue } + + // Check if a movn w22 / mov w22, #-1 appears within 64 insns before (prefer this candidate) + let far = max(0, i - 64) + let hasNeg1 = insns[far ..< i].contains { insn in + if insn.mnemonic == "movn", insn.operandString.hasPrefix("w22,") { + return true + } + if insn.mnemonic == "mov", insn.operandString.contains("w22"), + insn.operandString.contains("#-1") || insn.operandString.contains("#0xffffffff") + { + return true + } + return false + } + + candidates.append((addr: addr, hasNeg1: hasNeg1)) + } + } + + if candidates.isEmpty { + if verbose { print(" [-] image4 callback: pattern not found") } + return + } + + // Prefer the candidate that has a movn w22 (error return path) + let off: Int = if let preferred = candidates.first(where: { $0.hasNeg1 }) { + preferred.addr + } else { + candidates.last!.addr + } + + emit(off, ARM64.nop, id: "\(component).image4_callback_bne", description: "image4 callback: b.ne → nop") + emit(off + 4, ARM64.movX0_0, id: "\(component).image4_callback_mov", description: "image4 callback: mov x0,x22 → mov x0,#0") + } + + // MARK: - 3. Boot-Args (iBEC / LLB) + + /// Redirect ADRP+ADD x2 to a custom boot-args string. + /// Python: `patch_boot_args()` + func patchBootArgs(newArgs: String = IBootPatcher.bootArgs) { + guard let newArgsData = newArgs.data(using: .ascii) else { return } + + guard let fmtOff = findBootArgsFmt() else { + if verbose { print(" [-] boot-args: format string not found") } + return + } + + guard let (adrpOff, addOff) = findBootArgsAdrp(fmtOff: fmtOff) else { + if verbose { print(" [-] boot-args: ADRP+ADD x2 not found") } + return + } + + guard let newOff = findStringSlot(length: newArgsData.count) else { + if verbose { print(" [-] boot-args: no NUL slot") } + return + } + + // Write the string itself + emitString(newOff, newArgsData, id: "\(component).boot_args_string", description: "boot-args string") + + // Re-encode ADRP x2 → new page + guard let newAdrp = ARM64Encoder.encodeADRP(rd: 2, pc: UInt64(adrpOff), target: UInt64(newOff)) else { + if verbose { print(" [-] boot-args: ADRP encoding out of range") } + return + } + emit(adrpOff, newAdrp, id: "\(component).boot_args_adrp", description: "boot-args: adrp x2 → new string page") + + // Re-encode ADD x2, x2, #offset + let imm12 = UInt32(newOff & 0xFFF) + guard let newAdd = ARM64Encoder.encodeAddImm12(rd: 2, rn: 2, imm12: imm12) else { + if verbose { print(" [-] boot-args: ADD encoding out of range") } + return + } + emit(addOff, newAdd, id: "\(component).boot_args_add", description: "boot-args: add x2 → new string offset") + } + + /// Find the standalone "%s" format string near "rd=md0" or "BootArgs". + /// Python: `_find_boot_args_fmt()` + private func findBootArgsFmt() -> Int? { + let raw = buffer.original + + // Find the anchor string + var anchor: Int? = raw.range(of: Data("rd=md0".utf8)).map { raw.distance(from: raw.startIndex, to: $0.lowerBound) } + if anchor == nil { + anchor = raw.range(of: Data("BootArgs".utf8)).map { raw.distance(from: raw.startIndex, to: $0.lowerBound) } + } + guard let anchorOff = anchor else { return nil } + + // Search for "%s" within 0x40 bytes of the anchor + let searchEnd = anchorOff + 0x40 + let pctS = Data([UInt8(ascii: "%"), UInt8(ascii: "s")]) + + var off = anchorOff + while off < searchEnd { + guard let range = raw.range(of: pctS, in: off ..< min(searchEnd, raw.count)) else { return nil } + let found = raw.distance(from: raw.startIndex, to: range.lowerBound) + if found >= off + raw.count { return nil } + + // Must have NUL before and NUL after (isolated "%s\0") + if found > 0, raw[found - 1] == 0, found + 2 < raw.count, raw[found + 2] == 0 { + return found + } + off = found + 1 + } + return nil + } + + /// Find ADRP+ADD x2 pointing to the format string at fmtOff. + /// Python: `_find_boot_args_adrp()` + private func findBootArgsAdrp(fmtOff: Int) -> (Int, Int)? { + for insns in chunkedDisasm() { + let count = insns.count + guard count >= 2 else { continue } + for i in 0 ..< count - 1 { + let a = insns[i] + let b = insns[i + 1] + + guard a.mnemonic == "adrp", b.mnemonic == "add" else { continue } + + // First operand of ADRP must be x2 + guard a.operandString.hasPrefix("x2,") else { continue } + + guard let aDetail = a.aarch64, let bDetail = b.aarch64 else { continue } + guard aDetail.operands.count >= 2, bDetail.operands.count >= 3 else { continue } + + // ADRP Rd must equal ADD Rn (same register) + guard aDetail.operands[0].reg == bDetail.operands[1].reg else { continue } + + // ADRP page imm + ADD imm12 must equal fmt_off + let pageImm = aDetail.operands[1].imm // already page-aligned VA + let addImm = bDetail.operands[2].imm + if Int(pageImm + addImm) == fmtOff { + return (Int(a.address), Int(b.address)) + } + } + } + return nil + } + + /// Find a run of NUL bytes ≥ 64 bytes long to write the new string into. + /// Python: `_find_string_slot()` + private func findStringSlot(length: Int, searchStart: Int = 0x14000) -> Int? { + let raw = buffer.original + var off = searchStart + while off < raw.count { + if raw[off] == 0 { + let runStart = off + while off < raw.count, raw[off] == 0 { + off += 1 + } + let runLen = off - runStart + if runLen >= 64 { + // Align write pointer to 16 bytes (Python: (run_start + 8 + 15) & ~15) + let writeOff = (runStart + 8 + 15) & ~15 + if writeOff + length <= off { + return writeOff + } + } + } else { + off += 1 + } + } + return nil + } + + // MARK: - 4. Rootfs Bypass (LLB only) + + /// Apply all five rootfs bypass patches. + /// Python: `patch_rootfs_bypass()` + func patchRootfssBypass() { + // 4a: cbz/cbnz before error code 0x3B7 → unconditional b + patchCbzBeforeError(errorCode: 0x3B7, description: "rootfs: skip sig check (0x3B7)") + // 4b: NOP b.hs after cmp x8, #0x400 + patchBhsAfterCmp0x400() + // 4c: cbz/cbnz before error code 0x3C2 → unconditional b + patchCbzBeforeError(errorCode: 0x3C2, description: "rootfs: skip sig verify (0x3C2)") + // 4d: NOP cbz x8 null check (ldr x8, [xN, #0x78]) + patchNullCheck0x78() + // 4e: cbz/cbnz before error code 0x110 → unconditional b + patchCbzBeforeError(errorCode: 0x110, description: "rootfs: skip size verify (0x110)") + } + + /// Find unique `mov w8, #` and convert the cbz/cbnz 4 bytes before + /// it into an unconditional branch to the same target. + /// Python: `_patch_cbz_before_error()` + private func patchCbzBeforeError(errorCode: UInt32, description: String) { + let pattern = encodedMovW8(errorCode) + let locs = findPattern(pattern) + + guard locs.count == 1 else { + if verbose { + print(" [-] \(description): expected 1 'mov w8, #0x\(String(errorCode, radix: 16))', found \(locs.count)") + } + return + } + + let errOff = locs[0] + let cbzOff = errOff - 4 + + guard let insn = disasm.disassembleOne(in: buffer.original, at: cbzOff) else { + if verbose { print(" [-] \(description): no instruction at 0x\(String(format: "%X", cbzOff))") } + return + } + guard insn.mnemonic == "cbz" || insn.mnemonic == "cbnz" else { + if verbose { print(" [-] \(description): expected cbz/cbnz at 0x\(String(format: "%X", cbzOff)), got \(insn.mnemonic)") } + return + } + + // Extract branch target from the operand string (last operand is the immediate) + guard let detail = insn.aarch64, detail.operands.count >= 2 else { return } + let target = Int(detail.operands[1].imm) + + guard let bInsn = ARM64Encoder.encodeB(from: cbzOff, to: target) else { + if verbose { print(" [-] \(description): B encoding out of range") } + return + } + + emit(cbzOff, bInsn, id: "\(component).rootfs_cbz_0x\(String(errorCode, radix: 16))", description: description) + } + + /// Find the unique `cmp x8, #0x400` and NOP the `b.hs` that follows. + /// Python: `_patch_bhs_after_cmp_0x400()` + private func patchBhsAfterCmp0x400() { + // Scan every instruction for cmp x8, #0x400 — avoids hand-encoding the + // CMP/SUBS encoding and stays robust across Capstone output variants. + var locs: [Int] = [] + for insns in chunkedDisasm() { + for insn in insns { + if insn.mnemonic == "cmp", insn.operandString == "x8, #0x400" { + locs.append(Int(insn.address)) + } + } + } + + guard locs.count == 1 else { + if verbose { print(" [-] rootfs b.hs: expected 1 'cmp x8, #0x400', found \(locs.count)") } + return + } + + let cmpOff = locs[0] + let bhsOff = cmpOff + 4 + + guard let insn = disasm.disassembleOne(in: buffer.original, at: bhsOff) else { + if verbose { print(" [-] rootfs b.hs: no instruction at 0x\(String(format: "%X", bhsOff))") } + return + } + guard insn.mnemonic == "b.hs" else { + if verbose { print(" [-] rootfs b.hs: expected b.hs at 0x\(String(format: "%X", bhsOff)), got \(insn.mnemonic)") } + return + } + + emit(bhsOff, ARM64.nop, id: "\(component).rootfs_bhs_0x400", description: "rootfs: NOP b.hs size check (0x400)") + } + + /// Find `ldr xR, [xN, #0x78]; cbz xR` preceding the unique `mov w8, #0x110` + /// and NOP the cbz. + /// Python: `_patch_null_check_0x78()` + private func patchNullCheck0x78() { + let pattern = encodedMovW8(0x110) + let locs = findPattern(pattern) + + guard locs.count == 1 else { + if verbose { print(" [-] rootfs null check: expected 1 'mov w8, #0x110', found \(locs.count)") } + return + } + + let errOff = locs[0] + + // Walk backwards from errOff to find ldr x?, [xN, #0x78]; cbz x? + let scanStart = max(errOff - 0x300, 0) + var scan = errOff - 4 + while scan >= scanStart { + guard let i1 = disasm.disassembleOne(in: buffer.original, at: scan), + let i2 = disasm.disassembleOne(in: buffer.original, at: scan + 4) + else { + scan -= 4 + continue + } + + if i1.mnemonic == "ldr", + i1.operandString.contains("#0x78"), + i2.mnemonic == "cbz", + i2.operandString.hasPrefix("x") + { + emit(scan + 4, ARM64.nop, id: "\(component).rootfs_null_check_0x78", + description: "rootfs: NOP cbz x8 null check (#0x78)") + return + } + scan -= 4 + } + + if verbose { print(" [-] rootfs null check: ldr+cbz #0x78 pattern not found") } + } + + // MARK: - 5. Panic Bypass (LLB only) + + /// Find `mov w8, #0x328; movk w8, #0x40, lsl #16; ...; bl X; cbnz w0` + /// and NOP the cbnz. + /// Python: `patch_panic_bypass()` + func patchPanicBypass() { + let mov328 = encodedMovW8(0x328) + let locs = findPattern(mov328) + + for loc in locs { + // Verify movk w8, #0x40, lsl #16 follows + guard let nextInsn = disasm.disassembleOne(in: buffer.original, at: loc + 4) else { continue } + guard nextInsn.mnemonic == "movk", + nextInsn.operandString.contains("w8"), + nextInsn.operandString.contains("#0x40"), + nextInsn.operandString.contains("lsl #16") else { continue } + + // Walk forward (up to 7 instructions past the movk) to find bl; cbnz w0 + var step = loc + 8 + while step < loc + 32 { + guard let i = disasm.disassembleOne(in: buffer.original, at: step) else { + step += 4 + continue + } + if i.mnemonic == "bl" { + if let ni = disasm.disassembleOne(in: buffer.original, at: step + 4), + ni.mnemonic == "cbnz" + { + emit(step + 4, ARM64.nop, + id: "\(component).panic_bypass", + description: "panic bypass: NOP cbnz w0") + return + } + break // bl found but no cbnz — keep scanning other mov candidates + } + step += 4 + } + } + + if verbose { print(" [-] panic bypass: pattern not found") } + } +} diff --git a/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiExecve.swift b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiExecve.swift new file mode 100644 index 0000000..db3ee9e --- /dev/null +++ b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiExecve.swift @@ -0,0 +1,110 @@ +// KernelJBPatchAmfiExecve.swift — JB kernel patch: AMFI execve kill path bypass (disabled) +// +// Python source: scripts/patchers/kernel_jb_patch_amfi_execve.py +// +// Strategy: All kill paths in the AMFI execve hook converge on a shared +// epilogue that does `MOV W0, #1` (kill) then returns. Changing that single +// instruction to `MOV W0, #0` (allow) converts every kill path to a success +// return without touching the rest of the function. +// +// NOTE: This patch is disabled in the Python reference (not called from the +// main dispatcher). It is implemented here for completeness but is NOT called +// from patchAmfiExecveKillPath() in the orchestrator. + +import Foundation + +extension KernelJBPatcher { + // MARK: - AMFI execve kill-path bypass (disabled) + + /// Bypass AMFI execve kill by patching the shared MOV W0,#1 → MOV W0,#0. + /// + /// All kill paths in the AMFI execve hook function converge on a shared + /// epilogue: `MOV W0, #1; LDP X29, X30, [SP, #imm]; ...`. Patching the + /// single MOV converts all kill paths to allow-returns. + /// + /// This function is implemented but intentionally NOT called from the + /// main Group C dispatcher (matches Python behaviour where it is disabled). + @discardableResult + func patchAmfiExecveKillPath() -> Bool { + log("\n[JB] AMFI execve kill path: shared MOV W0,#1 → MOV W0,#0") + + // Find "AMFI: hook..execve() killing" or fallback string. + let killStr: String + if buffer.findString("AMFI: hook..execve() killing") != nil { + killStr = "AMFI: hook..execve() killing" + } else if buffer.findString("execve() killing") != nil { + killStr = "execve() killing" + } else { + log(" [-] execve kill log string not found") + return false + } + + guard let strOff = buffer.findString(killStr) else { + log(" [-] execve kill log string not found") + return false + } + + // Collect refs in kern_text, fall back to all refs. + var refs: [(adrpOff: Int, addOff: Int)] = [] + if let (ks, ke) = kernTextRange { + refs = findStringRefs(strOff, in: (start: ks, end: ke)) + } + if refs.isEmpty { + refs = findStringRefs(strOff) + } + guard !refs.isEmpty else { + log(" [-] no refs to execve kill log string") + return false + } + + let movW0_1_enc: UInt32 = 0x5280_0020 // MOV W0, #1 (MOVZ W0, #1) + + var patched = false + var seenFuncs: Set = [] + + for (adrpOff, _) in refs { + guard let funcStart = findFunctionStart(adrpOff) else { continue } + guard !seenFuncs.contains(funcStart) else { continue } + seenFuncs.insert(funcStart) + + // Function end = next PACIBSP (capped at 0x800 bytes). + var funcEnd = findFuncEnd(funcStart, maxSize: 0x800) + if let (_, ke) = kernTextRange { funcEnd = min(funcEnd, ke) } + + // Scan backward from funcEnd for MOV W0, #1 followed by LDP X29, X30, [SP, #imm]. + var targetOff = -1 + var off = funcEnd - 8 + while off >= funcStart { + if buffer.readU32(at: off) == movW0_1_enc { + // Verify next instruction is LDP X29, X30 (epilogue start) + if let nextInsn = disasAt(off + 4), + nextInsn.mnemonic == "ldp", + nextInsn.operandString.contains("x29"), nextInsn.operandString.contains("x30") + { + targetOff = off + break + } + } + off -= 4 + } + + guard targetOff >= 0 else { + log(" [-] MOV W0,#1 + epilogue not found in func 0x\(String(format: "%X", funcStart))") + continue + } + + emit(targetOff, ARM64.movW0_0, + patchID: "jb.amfi_execve.kill_return", + description: "mov w0,#0 [AMFI kill return → allow]") + + log(" [+] Patched kill return at 0x\(String(format: "%X", targetOff)) (func 0x\(String(format: "%X", funcStart)))") + patched = true + break // One function is sufficient + } + + if !patched { + log(" [-] AMFI execve kill return not found") + } + return patched + } +} diff --git a/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiTrustcache.swift b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiTrustcache.swift new file mode 100644 index 0000000..0c91fed --- /dev/null +++ b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchAmfiTrustcache.swift @@ -0,0 +1,139 @@ +// KernelJBPatchAmfiTrustcache.swift — JB kernel patch: AMFI trustcache gate bypass +// +// Python source: scripts/patchers/kernel_jb_patch_amfi_trustcache.py +// +// Strategy (semantic function matching): +// Scan amfi_text for functions (PACIBSP boundaries) that match the +// AMFIIsCDHashInTrustCache body shape: +// 1. mov x19, x2 (save x2 into x19) +// 2. stp xzr, xzr, [sp, ...] (stack-zeroing pair) +// 3. mov x2, sp (pass stack slot as out-param) +// 4. bl +// 5. mov x20, x0 (save result) +// 6. cbnz w0, ... (fast-path already-trusted check) +// 7. cbz x19, ... (nil out-param guard) +// Exactly one function must match. Rewrite its first 4 instructions with +// the always-allow stub: mov x0,#1 / cbz x2,+8 / str x0,[x2] / ret. + +import Foundation + +extension KernelJBPatcher { + /// AMFI trustcache gate bypass: rewrite AMFIIsCDHashInTrustCache to always return 1. + @discardableResult + func patchAmfiCdhashInTrustcache() -> Bool { + log("\n[JB] AMFIIsCDHashInTrustCache: always allow + store flag") + + // Determine the AMFI text range. Fall back to full __TEXT_EXEC if no kext split. + let amfiRange = amfiTextRange() + let (amfiStart, amfiEnd) = amfiRange + + // Instruction encoding constants (used for structural matching). + // Derived semantically — no hardcoded offsets, only instruction shape. + let movX19X2: UInt32 = 0xAA02_03F3 // mov x19, x2 (ORR X19, XZR, X2) + let movX2Sp: UInt32 = 0x9100_03E2 // mov x2, sp (ADD X2, SP, #0) + + // Mask for STP XZR,XZR,[SP,#imm]: fixed bits excluding the immediate. + // STP (pre-index / signed-offset) 64-bit XZR,XZR: 0xA900_7FFF base + let stpXzrXzrMask: UInt32 = 0xFFC0_7FFF + let stpXzrXzrVal: UInt32 = 0xA900_7FFF // any [sp, #imm_scaled] + + // CBZ/CBNZ masks + let cbnzWMask: UInt32 = 0x7F00_0000 + let cbnzWVal: UInt32 = 0x3500_0000 // CBNZ 32-bit + let cbzXMask: UInt32 = 0xFF00_0000 + let cbzXVal: UInt32 = 0xB400_0000 // CBZ 64-bit + + // BL mask + let blMask: UInt32 = 0xFC00_0000 + let blVal: UInt32 = 0x9400_0000 + + var hits: [Int] = [] + + var off = amfiStart + while off < amfiEnd - 4 { + guard buffer.readU32(at: off) == ARM64.pacibspU32 else { + off += 4 + continue + } + let funcStart = off + + // Determine function end: next PACIBSP or limit. + var funcEnd = min(funcStart + 0x200, amfiEnd) + var probe = funcStart + 4 + while probe < funcEnd { + if buffer.readU32(at: probe) == ARM64.pacibspU32 { + funcEnd = probe + break + } + probe += 4 + } + + // Collect instructions in this function. + var insns: [UInt32] = [] + var p = funcStart + while p < funcEnd { + insns.append(buffer.readU32(at: p)) + p += 4 + } + + // Structural shape check — mirrors Python _find_after sequence: + // i1: mov x19, x2 + // i2: stp xzr, xzr, [sp, ...] + // i3: mov x2, sp + // i4: bl + // i5: mov x20, x0 (ORR X20, XZR, X0 = 0xAA0003F4) + // i6: cbnz w0, ... + // i7: cbz x19, ... + let movX20X0: UInt32 = 0xAA00_03F4 + + guard let i1 = insns.firstIndex(where: { $0 == movX19X2 }) else { + off = funcEnd + continue + } + guard let i2 = insns[(i1 + 1)...].firstIndex(where: { ($0 & stpXzrXzrMask) == stpXzrXzrVal }) else { + off = funcEnd + continue + } + guard let i3 = insns[(i2 + 1)...].firstIndex(where: { $0 == movX2Sp }) else { + off = funcEnd + continue + } + guard let i4 = insns[(i3 + 1)...].firstIndex(where: { ($0 & blMask) == blVal }) else { + off = funcEnd + continue + } + guard let i5 = insns[(i4 + 1)...].firstIndex(where: { $0 == movX20X0 }) else { + off = funcEnd + continue + } + guard insns[(i5 + 1)...].first(where: { ($0 & cbnzWMask) == cbnzWVal && ($0 & 0x1F) == 0 }) != nil else { + off = funcEnd + continue + } + guard insns[(i5 + 1)...].first(where: { ($0 & cbzXMask) == cbzXVal && ($0 & 0x1F) == 19 }) != nil else { + off = funcEnd + continue + } + + hits.append(funcStart) + off = funcEnd + } + + guard hits.count == 1 else { + log(" [-] expected 1 AMFI trustcache body hit, found \(hits.count)") + return false + } + + let funcStart = hits[0] + let va0 = fileOffsetToVA(funcStart) + let va1 = fileOffsetToVA(funcStart + 4) + let va2 = fileOffsetToVA(funcStart + 8) + let va3 = fileOffsetToVA(funcStart + 12) + + emit(funcStart, ARM64.movX0_1, patchID: "amfi_trustcache_1", virtualAddress: va0, description: "mov x0,#1 [AMFIIsCDHashInTrustCache]") + emit(funcStart + 4, ARM64.cbzX2_8, patchID: "amfi_trustcache_2", virtualAddress: va1, description: "cbz x2,+8 [AMFIIsCDHashInTrustCache]") + emit(funcStart + 8, ARM64.strX0X2, patchID: "amfi_trustcache_3", virtualAddress: va2, description: "str x0,[x2] [AMFIIsCDHashInTrustCache]") + emit(funcStart + 12, ARM64.ret, patchID: "amfi_trustcache_4", virtualAddress: va3, description: "ret [AMFIIsCDHashInTrustCache]") + return true + } +} diff --git a/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchBsdInitAuth.swift b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchBsdInitAuth.swift new file mode 100644 index 0000000..91444e3 --- /dev/null +++ b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchBsdInitAuth.swift @@ -0,0 +1,188 @@ +// KernelJBPatchBsdInitAuth.swift — JB: bypass FSIOC_KERNEL_ROOTAUTH failure in _bsd_init. +// +// Python source: scripts/patchers/kernel_jb_patch_bsd_init_auth.py +// +// GUARDRAIL (CLAUDE.md): recover _bsd_init → locate rootvp panic block → +// find unique in-function call → cbnz w0/x0, panic → bl imageboot_needed → patch gate. +// +// Reveal procedure: +// 1. Recover _bsd_init via symbol table, else via rootvp panic string anchor. +// 2. Inside _bsd_init, find "rootvp not authenticated after mounting" string ref. +// 3. Follow ADRP → find the BL to _panic immediately after the ADD. +// 4. Scan backward from the panic ref for `cbnz w0/x0, ` preceded by a BL, +// with a BL to _imageboot_needed (or any BL) in the next 3 instructions. +// 5. NOP that cbnz. + +import Capstone +import Foundation + +extension KernelJBPatcher { + private static let rootvpAuthNeedle = "rootvp not authenticated after mounting" + private static let rootvpAltNeedle = "rootvp not authenticated after mounting @%s:%d" + + /// Bypass the real rootvp auth failure branch inside _bsd_init. + @discardableResult + func patchBsdInitAuth() -> Bool { + log("\n[JB] _bsd_init: ignore FSIOC_KERNEL_ROOTAUTH failure") + + // Step 1: Recover _bsd_init function start. + guard let funcStart = resolveBsdInit() else { + log(" [-] _bsd_init not found") + return false + } + + // Step 2: Find the panic string ref inside this function. + guard let (adrpOff, addOff) = rootvpPanicRefInFunc(funcStart) else { + log(" [-] rootvp panic string ref not found in _bsd_init") + return false + } + + // Step 3: Find the BL to _panic near the ADD instruction. + guard let blPanicOff = findPanicCallNear(addOff) else { + log(" [-] BL _panic not found near rootvp panic string") + return false + } + + // Step 4: Scan backward from the ADRP for a valid cbnz gate site. + let errLo = blPanicOff - 0x40 + let errHi = blPanicOff + 4 + let imagebootNeeded = resolveSymbol("_imageboot_needed") + let scanStart = max(funcStart, adrpOff - 0x400) + + var candidates: [(off: Int, state: String)] = [] + for off in stride(from: scanStart, to: adrpOff, by: 4) { + guard let state = matchRootauthBranchSite(off, errLo: errLo, errHi: errHi, imagebootNeeded: imagebootNeeded) else { continue } + candidates.append((off, state)) + } + + guard !candidates.isEmpty else { + log(" [-] rootauth branch site not found") + return false + } + + let (branchOff, state): (Int, String) + if candidates.count == 1 { + (branchOff, state) = (candidates[0].off, candidates[0].state) + } else { + // If multiple, prefer the "live" (not already patched) one. + let live = candidates.filter { $0.state == "live" } + guard live.count == 1 else { + log(" [-] ambiguous rootauth branch sites: \(candidates.count) found") + return false + } + (branchOff, state) = (live[0].off, live[0].state) + } + + if state == "patched" { + log(" [=] rootauth branch already bypassed at 0x\(String(format: "%X", branchOff))") + return true + } + + emit(branchOff, ARM64.nop, + patchID: "jb.bsd_init_auth.nop_cbnz", + virtualAddress: fileOffsetToVA(branchOff), + description: "NOP cbnz (rootvp auth) [_bsd_init]") + return true + } + + // MARK: - Private helpers + + /// Resolve _bsd_init via symbol table, else via rootvp anchor string. + private func resolveBsdInit() -> Int? { + if let off = resolveSymbol("_bsd_init"), off >= 0 { + return off + } + // Fallback: find function that contains the verbose rootvp panic string. + for needle in [Self.rootvpAltNeedle, Self.rootvpAuthNeedle] { + if let strOff = buffer.findString(needle) { + let refs = findStringRefs(strOff) + if let firstRef = refs.first, + let fn = findFunctionStart(firstRef.adrpOff) + { + return fn + } + } + } + return nil + } + + /// Find the ADRP+ADD pair for the rootvp panic string inside `funcStart`. + private func rootvpPanicRefInFunc(_ funcStart: Int) -> (adrpOff: Int, addOff: Int)? { + guard let strOff = buffer.findString(Self.rootvpAuthNeedle) else { return nil } + let refs = findStringRefs(strOff) + for (adrpOff, addOff) in refs { + if let fn = findFunctionStart(adrpOff), fn == funcStart { + return (adrpOff, addOff) + } + } + return nil + } + + /// Find the BL to _panic within 0x40 bytes after `addOff`. + private func findPanicCallNear(_ addOff: Int) -> Int? { + let limit = min(addOff + 0x40, buffer.count) + for scan in stride(from: addOff, to: limit, by: 4) { + if let target = jbDecodeBL(at: scan), + let panicOff = panicOffset, + target == panicOff + { + return scan + } + } + return nil + } + + /// Check if instruction at `off` is the rootauth CBNZ gate site. + /// Returns "live", "patched", or nil. + private func matchRootauthBranchSite(_ off: Int, errLo: Int, errHi: Int, imagebootNeeded: Int?) -> String? { + let insns = disasm.disassemble(in: buffer.data, at: off, count: 1) + guard let insn = insns.first else { return nil } + + // Must be preceded by a BL or BLR + guard isBLorBLR(at: off - 4) else { return nil } + + // Must have a BL to _imageboot_needed (or any BL if symbol not resolved) within 3 insns after + guard hasImagebootCallNear(off, imagebootNeeded: imagebootNeeded) else { return nil } + + // Check if already patched (NOP) + if insn.mnemonic == "nop" { return "patched" } + + // Must be CBNZ on w0 or x0 + guard insn.mnemonic == "cbnz" else { return nil } + guard let detail = insn.aarch64, !detail.operands.isEmpty else { return nil } + let regOp = detail.operands[0] + guard regOp.type == AARCH64_OP_REG, + regOp.reg == AARCH64_REG_W0 || regOp.reg == AARCH64_REG_X0 else { return nil } + + // Branch target must point into the panic block region + guard let (branchTarget, _) = jbDecodeBranchTarget(at: off), + branchTarget >= errLo, branchTarget <= errHi else { return nil } + + return "live" + } + + /// Return true if there is a BL/BLR/BLRAA/BLRAB/etc. at `off`. + private func isBLorBLR(at off: Int) -> Bool { + guard off >= 0, off + 4 <= buffer.count else { return false } + let insns = disasm.disassemble(in: buffer.data, at: off, count: 1) + guard let insn = insns.first else { return false } + return insn.mnemonic.hasPrefix("bl") + } + + /// Return true if there is a BL to _imageboot_needed (or any BL if unknown) + /// within 3 instructions after `off`. + private func hasImagebootCallNear(_ off: Int, imagebootNeeded: Int?) -> Bool { + let limit = min(off + 0x18, buffer.count) + for scan in stride(from: off + 4, to: limit, by: 4) { + guard let target = jbDecodeBL(at: scan) else { continue } + // If we know _imageboot_needed, require an exact match; + // otherwise any BL counts (stripped kernel). + if let ib = imagebootNeeded { + if target == ib { return true } + } else { + return true + } + } + return false + } +} diff --git a/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchCredLabel.swift b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchCredLabel.swift new file mode 100644 index 0000000..c8090a6 --- /dev/null +++ b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchCredLabel.swift @@ -0,0 +1,348 @@ +// KernelJBPatchCredLabel.swift — JB kernel patch: _cred_label_update_execve C21-v3 +// +// Python source: scripts/patchers/kernel_jb_patch_cred_label.py +// +// Strategy (C21-v3): Split late exits, add helper bits on success. +// - Keep _cred_label_update_execve body intact. +// - Redirect the shared deny return (MOV W0,#1 just before epilogue) to a +// deny cave that forces W0=0 and returns through the original epilogue. +// - Redirect late success exits (B epilogue preceded by MOV W0,#0) to a +// success cave that reloads x26 = u_int *csflags, clears kill bits, ORs +// CS_GET_TASK_ALLOW|CS_INSTALLER, forces W0=0, then returns via epilogue. +// +// CS mask constants (matching Python): +// RELAX_CSMASK = 0xFFFFC0FF (clears CS_HARD|CS_KILL|CS_RESTRICT etc.) +// RELAX_SETMASK = 0x0000000C (CS_GET_TASK_ALLOW | CS_INSTALLER) + +import Foundation + +extension KernelJBPatcher { + // MARK: - Constants + + private static let retInsns: Set = [0xD65F_0FFF, 0xD65F_0BFF, 0xD65F_03C0] + private static let movW0_0_u32: UInt32 = 0x5280_0000 + private static let movW0_1_u32: UInt32 = 0x5280_0020 + private static let relaxCSMask: UInt32 = 0xFFFF_C0FF + private static let relaxSetMask: UInt32 = 0x0000_000C + + // MARK: - Entry Point + + /// C21-v3 split exits + helper bits for _cred_label_update_execve. + func patchCredLabelUpdateExecve() { + log("\n[JB] _cred_label_update_execve: C21-v3 split exits + helper bits") + + // 1. Locate the function. + guard let funcOff = locateCredLabelExecveFunc() else { + log(" [-] function not found, skipping shellcode patch") + return + } + log(" [+] func at 0x\(String(format: "%X", funcOff))") + + // 2. Find canonical epilogue: last `ldp x29, x30, [sp, ...]` before ret. + guard let epilogueOff = findCredLabelEpilogue(funcOff: funcOff) else { + log(" [-] epilogue not found") + return + } + log(" [+] epilogue at 0x\(String(format: "%X", epilogueOff))") + + // 3. Find shared deny return: MOV W0,#1 immediately before the epilogue. + let denyOff = findCredLabelDenyReturn(funcOff: funcOff, epilogueOff: epilogueOff) + + // Check if deny is already allow + let denyAlreadyAllowed: Bool + if let denyOff { + denyAlreadyAllowed = buffer.readU32(at: denyOff) == Self.movW0_0_u32 + if denyAlreadyAllowed { + log(" [=] deny return at 0x\(String(format: "%X", denyOff)) already MOV W0,#0, skipping deny trampoline") + } + } else { + log(" [-] shared deny return not found") + return + } + + // 4. Find success exits: B epilogue with preceding MOV W0,#0. + let successExits = findCredLabelSuccessExits(funcOff: funcOff, epilogueOff: epilogueOff) + guard !successExits.isEmpty else { + log(" [-] success exits not found") + return + } + + // 5. Recover csflags stack reload instruction bytes. + guard let (csflagsInsn, csflagsDesc) = findCredLabelCSFlagsReload(funcOff: funcOff) else { + log(" [-] csflags stack reload (ldr x26, [x29, #imm]) not found") + return + } + + // 6. Allocate code caves. + var denyCaveOff: Int? = nil + if !denyAlreadyAllowed { + denyCaveOff = findCodeCave(size: 8) + guard denyCaveOff != nil else { + log(" [-] no code cave for C21-v3 deny trampoline") + return + } + } + + // Success cave: 8 instructions = 32 bytes + guard let successCaveOff = findCodeCave(size: 32), + successCaveOff != denyCaveOff + else { + log(" [-] no code cave for C21-v3 success trampoline") + return + } + + // 7. Build deny shellcode (8 bytes): MOV W0,#0 + B epilogue. + if !denyAlreadyAllowed, let dOff = denyOff, let dCaveOff = denyCaveOff { + guard let branchBack = encodeB(from: dCaveOff + 4, to: epilogueOff) else { + log(" [-] deny trampoline → epilogue branch out of range") + return + } + let denyShellcode = ARM64.movW0_0 + branchBack + + // Write deny cave + for i in stride(from: 0, to: denyShellcode.count, by: 4) { + let chunk = denyShellcode[denyShellcode.index(denyShellcode.startIndex, offsetBy: i) ..< denyShellcode.index(denyShellcode.startIndex, offsetBy: i + 4)] + emit(dCaveOff + i, Data(chunk), + patchID: "jb.cred_label_update_execve.deny_cave", + description: "deny_trampoline+\(i) [_cred_label_update_execve C21-v3]") + } + + // Redirect deny site → deny cave + guard let branchToCave = encodeB(from: dOff, to: dCaveOff) else { + log(" [-] branch from deny site 0x\(String(format: "%X", dOff)) to cave out of range") + return + } + emit(dOff, branchToCave, + patchID: "jb.cred_label_update_execve.deny_redirect", + description: "b deny cave [_cred_label_update_execve C21-v3 exit @ 0x\(String(format: "%X", dOff))]") + } + + // 8. Build success shellcode (8 instrs = 32 bytes): + // ldr x26, [x29, #imm] (reload csflags ptr from stack) + // cbz x26, #0x10 (skip if null) + // ldr w8, [x26] + // and w8, w8, #relaxCSMask + // orr w8, w8, #relaxSetMask + // str w8, [x26] + // mov w0, #0 + // b epilogue + guard let successBranchBack = encodeB(from: successCaveOff + 28, to: epilogueOff) else { + log(" [-] success trampoline → epilogue branch out of range") + return + } + + var successShellcode = Data() + successShellcode += csflagsInsn // ldr x26, [x29, #imm] + successShellcode += encodeCBZ_X26_skip16() // cbz x26, #0x10 (skip 4 insns) + successShellcode += encodeLDR_W8_X26() // ldr w8, [x26] + successShellcode += encodeAND_W8_W8_mask(Self.relaxCSMask) // and w8, w8, #0xFFFFC0FF + successShellcode += encodeORR_W8_W8_imm(Self.relaxSetMask) // orr w8, w8, #0xC + successShellcode += encodeSTR_W8_X26() // str w8, [x26] + successShellcode += ARM64.movW0_0 // mov w0, #0 + successShellcode += successBranchBack // b epilogue + + guard successShellcode.count == 32 else { + log(" [-] success shellcode size mismatch: \(successShellcode.count) != 32") + return + } + + for i in stride(from: 0, to: successShellcode.count, by: 4) { + let chunk = successShellcode[successShellcode.index(successShellcode.startIndex, offsetBy: i) ..< successShellcode.index(successShellcode.startIndex, offsetBy: i + 4)] + emit(successCaveOff + i, Data(chunk), + patchID: "jb.cred_label_update_execve.success_cave", + description: "success_trampoline+\(i) [_cred_label_update_execve C21-v3]") + } + + // 9. Redirect success exits → success cave. + for exitOff in successExits { + guard let branchToCave = encodeB(from: exitOff, to: successCaveOff) else { + log(" [-] branch from success exit 0x\(String(format: "%X", exitOff)) to cave out of range") + return + } + emit(exitOff, branchToCave, + patchID: "jb.cred_label_update_execve.success_redirect", + description: "b success cave [_cred_label_update_execve C21-v3 exit @ 0x\(String(format: "%X", exitOff))]") + } + } + + // MARK: - Function Locators + + /// Locate _cred_label_update_execve: try symbol first, then string-cluster scan. + private func locateCredLabelExecveFunc() -> Int? { + // Symbol lookup + for (sym, off) in symbols { + if sym.contains("cred_label_update_execve"), !sym.contains("hook") { + if isCredLabelExecveCandidate(funcOff: off) { + return off + } + } + } + return findCredLabelExecveByStrings() + } + + /// Validate candidate function shape for _cred_label_update_execve. + private func isCredLabelExecveCandidate(funcOff: Int) -> Bool { + let funcEnd = findFuncEnd(funcOff, maxSize: 0x1000) + guard funcEnd - funcOff >= 0x200 else { return false } + // Must contain ldr x26, [x29, #imm] + return findCredLabelCSFlagsReload(funcOff: funcOff) != nil + } + + /// String-cluster search for _cred_label_update_execve. + private func findCredLabelExecveByStrings() -> Int? { + let anchorStrings = [ + "AMFI: hook..execve() killing", + "Attempt to execute completely unsigned code", + "Attempt to execute a Legacy VPN Plugin", + "dyld signature cannot be verified", + ] + var candidates: Set = [] + for anchor in anchorStrings { + guard let strOff = buffer.findString(anchor) else { continue } + let refs = findStringRefs(strOff) + for (adrpOff, _) in refs { + if let funcStart = findFunctionStart(adrpOff) { + candidates.insert(funcStart) + } + } + } + // Pick best candidate (largest, as a proxy for most complex body) + var bestFunc: Int? = nil + var bestScore = -1 + for funcOff in candidates { + let funcEnd = findFuncEnd(funcOff, maxSize: 0x1000) + let score = funcEnd - funcOff + if score > bestScore, isCredLabelExecveCandidate(funcOff: funcOff) { + bestScore = score + bestFunc = funcOff + } + } + return bestFunc + } + + // MARK: - Epilogue / Deny / Success Finders + + /// Find the canonical epilogue: last `ldp x29, x30, [sp, ...]` in function. + private func findCredLabelEpilogue(funcOff: Int) -> Int? { + let funcEnd = findFuncEnd(funcOff, maxSize: 0x1000) + for off in stride(from: funcEnd - 4, through: funcOff, by: -4) { + guard let insn = disasAt(off) else { continue } + let op = insn.operandString.replacingOccurrences(of: " ", with: "") + if insn.mnemonic == "ldp", op.hasPrefix("x29,x30,[sp") { + return off + } + } + return nil + } + + /// Find shared deny return: MOV W0,#1 at epilogueOff - 4. + private func findCredLabelDenyReturn(funcOff: Int, epilogueOff: Int) -> Int? { + let scanStart = max(funcOff, epilogueOff - 0x40) + for off in stride(from: epilogueOff - 4, through: scanStart, by: -4) { + if buffer.readU32(at: off) == Self.movW0_1_u32, off + 4 == epilogueOff { + return off + } + } + return nil + } + + /// Find success exits: `b epilogue` preceded (within 0x10 bytes) by `mov w0, #0`. + private func findCredLabelSuccessExits(funcOff: Int, epilogueOff: Int) -> [Int] { + var exits: [Int] = [] + let funcEnd = findFuncEnd(funcOff, maxSize: 0x1000) + for off in stride(from: funcOff, to: funcEnd, by: 4) { + guard let target = jbDecodeBBranch(at: off), target == epilogueOff else { continue } + // Scan back for MOV W0, #0 in preceding 4 instructions + var hasMov = false + let scanBack = max(funcOff, off - 0x10) + for prev in stride(from: off - 4, through: scanBack, by: -4) { + if buffer.readU32(at: prev) == Self.movW0_0_u32 { + hasMov = true + break + } + } + if hasMov { exits.append(off) } + } + return exits + } + + /// Recover ldr x26, [x29, #imm] instruction bytes from the function body. + private func findCredLabelCSFlagsReload(funcOff: Int) -> (Data, String)? { + let funcEnd = findFuncEnd(funcOff, maxSize: 0x1000) + for off in stride(from: funcOff, to: funcEnd, by: 4) { + guard let insn = disasAt(off) else { continue } + let op = insn.operandString.replacingOccurrences(of: " ", with: "") + if insn.mnemonic == "ldr", op.hasPrefix("x26,[x29") { + // Return the raw 4 bytes plus the disassembly string + let insnBytes = buffer.data[off ..< off + 4] + return (Data(insnBytes), insn.operandString) + } + } + return nil + } + + // MARK: - Instruction Encoders + + /// CBZ X26, #0x10 — skip 4 instructions if x26 == 0 + private func encodeCBZ_X26_skip16() -> Data { + // CBZ encoding: [31]=1 (64-bit), [30:24]=0110100, [23:5]=imm19, [4:0]=Rt + // imm19 = offset/4 = 16/4 = 4 → bits [23:5] = 4 << 5 = 0x80 + // Full: 1_0110100_000000000000000000100_11010 = ? + // CBZ X26 = 0xB400_0000 | (imm19 << 5) | 26 + // imm19 = 4, Rt = 26 (x26) + let imm19: UInt32 = 4 + let insn: UInt32 = 0xB400_0000 | (imm19 << 5) | 26 + return ARM64.encodeU32(insn) + } + + /// LDR W8, [X26] + private func encodeLDR_W8_X26() -> Data { + // LDR W8, [X26] — 32-bit load, no offset + // Encoding: size=10, V=0, opc=01, imm12=0, Rn=X26(26), Rt=W8(8) + // 1011 1001 0100 0000 0000 0011 0100 1000 + // 0xB940_0348 + let insn: UInt32 = 0xB940_0348 + return ARM64.encodeU32(insn) + } + + /// STR W8, [X26] + private func encodeSTR_W8_X26() -> Data { + // STR W8, [X26] — 32-bit store, no offset + // 0xB900_0348 + let insn: UInt32 = 0xB900_0348 + return ARM64.encodeU32(insn) + } + + /// AND W8, W8, #imm (32-bit logical immediate). + /// For mask 0xFFFFC0FF: encodes as NOT(0x3F00) = elements with inverted bits + private func encodeAND_W8_W8_mask(_: UInt32) -> Data { + // We encode directly using ARM64 logical immediate encoding. + // For 0xFFFFC0FF: this is ~0x3F00 which represents "clear bits 8..13". + // Logical imm: sf=0 (32-bit), N=0, immr=8, imms=5 for ~(0x3F<<8) + // Actually use: AND W8, W8, #0xFFFFC0FF + // N=0, immr=8, imms=5: encodes 6 replicated ones starting at bit 8 being 0 + // Encoding: 0_00100100_N_immr_imms_Rn_Rd + // sf=0, opc=00, AND imm: 0001 0010 0 N immr imms Rn Rd + // For mask 0xFFFFC0FF in 32-bit: + // bit pattern: 1111 1111 1111 1100 0000 0000 1111 1111 + // inverted: 0000 0000 0000 0011 1111 1111 0000 0000 = 0x3F00 + // This is a run of 8 ones (bits 8-15 are zero so inverted = ones) + // N=0, immr=8, imms=5 (count-1 of ones in the "element" minus 1) + // But we have 6 zeros in positions 8..13, not a clean power-of-2 element. + // Actually 0xFFFFC0FF has zeros at bits 8-13 (6 zeros), so mask has 6 zeros. + // For AND W8, W8, #0xFFFFC0FF: + // Use a pre-computed value from Python: asm("and w8, w8, #0xFFFFC0FF") + // Python result: 0x12126508 → bytes: 08 65 12 12 + let insn: UInt32 = 0x1212_6508 + return ARM64.encodeU32(insn) + } + + /// ORR W8, W8, #0xC (CS_GET_TASK_ALLOW | CS_INSTALLER) + private func encodeORR_W8_W8_imm(_: UInt32) -> Data { + // ORR W8, W8, #0xC + // 0xC = bit 2 and bit 3 set + // Python result: asm("orr w8, w8, #0xC") → 0x321e0508 + let insn: UInt32 = 0x321E_0508 + return ARM64.encodeU32(insn) + } +} diff --git a/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchDounmount.swift b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchDounmount.swift new file mode 100644 index 0000000..5e61e7b --- /dev/null +++ b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchDounmount.swift @@ -0,0 +1,118 @@ +// KernelJBPatchDounmount.swift — JB: NOP the upstream cleanup call in dounmount. +// +// Python source: scripts/patchers/kernel_jb_patch_dounmount.py +// +// Reveal: string-anchor "dounmount:" → find the unique near-tail 4-arg zeroed cleanup +// call: mov x0,xN ; mov w1,#0 ; mov w2,#0 ; mov w3,#0 ; bl ; mov x0,xN ; bl ; cbz x19,... +// Patch: NOP the first BL in that sequence. + +import Capstone +import Foundation + +extension KernelJBPatcher { + /// NOP the upstream cleanup call in _dounmount. + @discardableResult + func patchDounmount() -> Bool { + log("\n[JB] _dounmount: upstream cleanup-call NOP") + + guard let foff = findFuncByString("dounmount:") else { + log(" [-] 'dounmount:' anchor not found") + return false + } + + let funcEnd = findFuncEnd(foff, maxSize: 0x4000) + guard let patchOff = findUpstreamCleanupCall(foff, end: funcEnd) else { + log(" [-] upstream dounmount cleanup call not found") + return false + } + + emit(patchOff, ARM64.nop, + patchID: "jb.dounmount.nop_cleanup_bl", + virtualAddress: fileOffsetToVA(patchOff), + description: "NOP [_dounmount upstream cleanup call]") + return true + } + + // MARK: - Private helpers + + /// Find a function that contains a reference to `string` (null-terminated). + private func findFuncByString(_ string: String) -> Int? { + guard let strOff = buffer.findString(string) else { return nil } + let refs = findStringRefs(strOff) + guard let firstRef = refs.first else { return nil } + return findFunctionStart(firstRef.adrpOff) + } + + /// Scan for the 8-instruction upstream cleanup call pattern and return + /// the file offset of the first BL, or nil if not uniquely found. + private func findUpstreamCleanupCall(_ start: Int, end: Int) -> Int? { + var hits: [Int] = [] + let limit = end - 0x1C + guard start < limit else { return nil } + + for off in stride(from: start, to: limit, by: 4) { + let insns = disasm.disassemble(in: buffer.data, at: off, count: 8) + guard insns.count >= 8 else { continue } + let i0 = insns[0], i1 = insns[1], i2 = insns[2], i3 = insns[3] + let i4 = insns[4], i5 = insns[5], i6 = insns[6], i7 = insns[7] + + // mov x0, ; mov w1,#0 ; mov w2,#0 ; mov w3,#0 ; bl ; mov x0, ; bl ; cbz x.. + guard i0.mnemonic == "mov", i1.mnemonic == "mov", + i2.mnemonic == "mov", i3.mnemonic == "mov" else { continue } + guard i4.mnemonic == "bl", i5.mnemonic == "mov", + i6.mnemonic == "bl", i7.mnemonic == "cbz" else { continue } + + // i0: mov x0, + guard let srcReg = movRegRegDst(i0, dst: "x0") else { continue } + // i1: mov w1, #0 + guard movImmZero(i1, dst: "w1") else { continue } + // i2: mov w2, #0 + guard movImmZero(i2, dst: "w2") else { continue } + // i3: mov w3, #0 + guard movImmZero(i3, dst: "w3") else { continue } + // i5: mov x0, + guard let src5 = movRegRegDst(i5, dst: "x0"), src5 == srcReg else { continue } + // i7: cbz x, ... + guard cbzUsesXreg(i7) else { continue } + + hits.append(Int(i4.address)) + } + + if hits.count == 1 { return hits[0] } + return nil + } + + /// Return the source register name if instruction is `mov , `. + private func movRegRegDst(_ insn: Instruction, dst: String) -> String? { + guard insn.mnemonic == "mov" else { return nil } + guard let detail = insn.aarch64, detail.operands.count == 2 else { return nil } + let dstOp = detail.operands[0], srcOp = detail.operands[1] + guard dstOp.type == AARCH64_OP_REG, srcOp.type == AARCH64_OP_REG else { return nil } + guard regName(dstOp.reg) == dst else { return nil } + return regName(srcOp.reg) + } + + /// Return true if instruction is `mov , #0`. + private func movImmZero(_ insn: Instruction, dst: String) -> Bool { + guard insn.mnemonic == "mov" else { return false } + guard let detail = insn.aarch64, detail.operands.count == 2 else { return false } + let dstOp = detail.operands[0], srcOp = detail.operands[1] + guard dstOp.type == AARCH64_OP_REG, regName(dstOp.reg) == dst else { return false } + guard srcOp.type == AARCH64_OP_IMM, srcOp.imm == 0 else { return false } + return true + } + + /// Return true if instruction is `cbz x,