From e65e78d090140cf6c56c55d7f85c6856bb72b82a Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 28 Feb 2026 03:38:14 +0900 Subject: [PATCH] Update README.md Update README.md Add Simplified Chinese README and link Add README_zh-Hans.md containing a full Simplified Chinese translation of the project README and update README.md to include a link/badge to the new Chinese README. This makes the documentation accessible to zh-Hans readers. --- .gitignore | 5 +- AGENTS.md | 281 ++++ CLAUDE.md | 16 + Makefile | 175 ++ Package.swift | 3 +- README.md | 976 +----------- Scripts/patch_firmware.py | 614 ------- Scripts/prepare_firmware.sh | 240 --- boot.sh | 19 - boot_dfu.sh | 20 - boot_sweet.sh | 33 - build_and_sign.sh | 45 - requirements.txt | 3 + researchs/binary_patches_kernelcache.md | 121 ++ researchs/build_manifest.md | 178 +++ researchs/erase_install_component_origins.md | 179 +++ .../install_cfw.sh => scripts/cfw_install.sh | 52 +- scripts/fw_manifest.py | 222 +++ scripts/fw_patch.py | 314 ++++ scripts/fw_prepare.sh | 100 ++ scripts/patchers/__init__.py | 5 + .../patch_cfw.py => scripts/patchers/cfw.py | 0 scripts/patchers/iboot.py | 470 ++++++ scripts/patchers/kernel.py | 1415 +++++++++++++++++ scripts/patchers/txm.py | 185 +++ scripts/patches/libirecovery-pcc-vm.patch | 13 + .../ramdisk_build.py | 58 +- {Scripts => scripts}/ramdisk_send.sh | 37 +- scripts/setup_libimobiledevice.sh | 153 ++ scripts/setup_venv.sh | 80 + scripts/vm_create.sh | 137 ++ .../vphone-cli/VPhoneCLI.swift | 0 .../vphone-cli/VPhoneHardwareModel.swift | 0 .../vphone-cli/VPhoneVM.swift | 0 .../vphone-cli/VPhoneVMWindow.swift | 0 .../vphone-objc}/VPhoneObjC.m | 0 .../vphone-objc}/include/VPhoneObjC.h | 0 .../vphone.entitlements | 0 38 files changed, 4216 insertions(+), 1933 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 Makefile delete mode 100644 Scripts/patch_firmware.py delete mode 100755 Scripts/prepare_firmware.sh delete mode 100755 boot.sh delete mode 100755 boot_dfu.sh delete mode 100755 boot_sweet.sh delete mode 100755 build_and_sign.sh create mode 100644 requirements.txt create mode 100644 researchs/binary_patches_kernelcache.md create mode 100644 researchs/build_manifest.md create mode 100644 researchs/erase_install_component_origins.md rename Scripts/install_cfw.sh => scripts/cfw_install.sh (86%) create mode 100755 scripts/fw_manifest.py create mode 100755 scripts/fw_patch.py create mode 100755 scripts/fw_prepare.sh create mode 100644 scripts/patchers/__init__.py rename Scripts/patch_cfw.py => scripts/patchers/cfw.py (100%) mode change 100644 => 100755 create mode 100755 scripts/patchers/iboot.py create mode 100755 scripts/patchers/kernel.py create mode 100755 scripts/patchers/txm.py create mode 100644 scripts/patches/libirecovery-pcc-vm.patch rename Scripts/build_ramdisk.py => scripts/ramdisk_build.py (93%) mode change 100644 => 100755 rename {Scripts => scripts}/ramdisk_send.sh (57%) create mode 100755 scripts/setup_libimobiledevice.sh create mode 100755 scripts/setup_venv.sh create mode 100755 scripts/vm_create.sh rename {Sources => sources}/vphone-cli/VPhoneCLI.swift (100%) rename {Sources => sources}/vphone-cli/VPhoneHardwareModel.swift (100%) rename {Sources => sources}/vphone-cli/VPhoneVM.swift (100%) rename {Sources => sources}/vphone-cli/VPhoneVMWindow.swift (100%) rename {Sources/VPhoneObjC => sources/vphone-objc}/VPhoneObjC.m (100%) rename {Sources/VPhoneObjC => sources/vphone-objc}/include/VPhoneObjC.h (100%) rename vphone.entitlements => sources/vphone.entitlements (100%) diff --git a/.gitignore b/.gitignore index bac9cd7..e6c04f2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ __MACOSX/ .AppleDouble .LSOverride -Icon[ ] +Icon[ +] # Thumbnails ._* @@ -307,3 +308,5 @@ __marimo__/ # Streamlit .streamlit/secrets.toml *.resolved +/VM +.limd/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fbd4d66 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,281 @@ +# AGENTS — vphone-cli + +## Project Overview + +CLI tool that boots virtual iPhones (PV=3) via Apple's Virtualization.framework, targeting Private Cloud Compute (PCC) research VMs. Used for iOS security research — firmware patching, boot chain modification, and runtime instrumentation. + +## Architecture + +``` +Makefile # Single entry point — run `make help` + +sources/ +├── vphone-objc/ # ObjC bridge for private Virtualization.framework APIs +│ ├── include/VPhoneObjC.h +│ └── VPhoneObjC.m +└── vphone-cli/ # Swift executable + ├── VPhoneCLI.swift + ├── VPhoneVM.swift + ├── VPhoneHardwareModel.swift + └── VPhoneVMWindow.swift + +scripts/ +├── patchers/ # Python patcher package +│ ├── iboot.py # Dynamic iBoot patcher (iBSS/iBEC/LLB) +│ ├── kernel.py # Dynamic kernel patcher (25 patches) +│ ├── txm.py # Dynamic TXM patcher +│ └── cfw.py # CFW binary patcher +├── resources/ # Resource archives +│ ├── cfw_input.tar.zst +│ └── ramdisk_input.tar.zst +├── fw_prepare.sh # Downloads IPSWs, merges cloudOS into iPhone +├── fw_manifest.py # Generates hybrid BuildManifest.plist & Restore.plist +├── fw_patch.py # Patches 6 boot-chain components (41+ modifications) +├── ramdisk_build.py # Builds SSH ramdisk with trustcache +├── ramdisk_send.sh # Sends ramdisk to device via irecovery +├── cfw_install.sh # Installs custom firmware to VM disk +├── vm_create.sh # Creates VM directory (disk, SEP storage, ROMs) +├── setup_venv.sh # Creates Python venv with native keystone dylib +└── setup_libimobiledevice.sh # Builds libimobiledevice toolchain from source + +Research/ # Research notes and verification reports +researchs/ # Component analysis and architecture docs +``` + +### Key Patterns + +- **Private API access:** All private Virtualization.framework calls go through the ObjC bridge (`VPhoneObjC`). Swift code never calls private APIs directly. +- **Function naming:** ObjC bridge functions use the `VPhone` prefix (e.g., `VPhoneCreateHardwareModel`, `VPhoneConfigureSEP`). +- **Configuration:** CLI options parsed via `ArgumentParser`, converted to `VPhoneVM.Options` struct, then used to build `VZVirtualMachineConfiguration`. +- **Error handling:** `VPhoneError` enum with `CustomStringConvertible` for user-facing messages. +- **Window management:** `VPhoneWindowController` wraps `NSWindow` + `VZVirtualMachineView`. Touch input translated from mouse events to multi-touch via `VPhoneVMView`. + +--- + +## Firmware Assembly Pipeline + +The firmware is a **PCC/iPhone hybrid** — PCC boot infrastructure wrapping iPhone iOS userland. + +### Pipeline Stages + +``` +1. make fw_prepare Download iPhone + cloudOS IPSWs, merge, generate hybrid plists + ↓ +2. make fw_patch Patch 6 boot-chain components for signature bypass + debug + ↓ +3. make ramdisk_build Build SSH ramdisk from SHSH blob, inject tools, sign with IM4M + ↓ +4. make vm_new Create VM directory (sparse disk, SEP storage, copy ROMs) + ↓ +5. make boot_dfu Boot VM in DFU mode + ↓ +6. make ramdisk_send Load boot chain + ramdisk via irecovery + ↓ +7. make cfw_install Mount Cryptex, patch userland, install jailbreak tools +``` + +### Component Origins + +The firmware merges two Apple IPSWs: +- **iPhone IPSW:** `iPhone17,3_26.1_23B85_Restore.ipsw` (d47ap) +- **cloudOS IPSW:** PCC vresearch101ap IPSW (CDN hash URL) + +`fw_prepare.sh` extracts both, then copies cloudOS boot chain into the +iPhone restore directory (`kernelcache.*`, `Firmware/{agx,all_flash,ane,dfu,pmp}/*`, +`Firmware/*.im4p`). The cloudOS extract is deleted after merge. + +#### Boot Chain — from PCC (cloudOS / vresearch101ap) + +| Component | File | Patched | Patch Purpose | +|-----------|------|---------|---------------| +| AVPBooter | `AVPBooter.vresearch1.bin` | Yes (1) | DGST signature validation bypass | +| LLB | `Firmware/all_flash/LLB.vresearch101.RELEASE.im4p` | Yes (6) | Serial + image4 bypass + boot-args + rootfs + panic | +| iBSS | `Firmware/dfu/iBSS.vresearch101.RELEASE.im4p` | Yes (2) | Serial labels + image4 callback bypass | +| iBEC | `Firmware/dfu/iBEC.vresearch101.RELEASE.im4p` | Yes (3) | Serial + image4 bypass + boot-args | +| SPTM | `Firmware/all_flash/sptm.vresearch1.release.im4p` | No | — | +| TXM | `Firmware/txm.iphoneos.research.im4p` | Yes (1) | Trustcache validation bypass | +| SEP Firmware | `Firmware/all_flash/sep-firmware.vresearch101.RELEASE.im4p` | No | — | +| DeviceTree | `Firmware/all_flash/DeviceTree.vphone600ap.im4p` | No | — | +| KernelCache | `kernelcache.release.vphone600` | Yes (25) | APFS, MAC, debugger, launch constraints, etc. | +| GPU/ANE/PMP | `Firmware/{agx,ane,pmp}/*` | No | — | + +> TXM filename says "iphoneos" but is copied from cloudOS IPSW (`fw_prepare.sh` line 81). + +#### OS / Filesystem — from iPhone (iPhone17,3) + +| Component | Notes | +|-----------|-------| +| OS | iPhone OS image | +| SystemVolume | System partition | +| StaticTrustCache | Static trust cache | +| Ap,SystemVolumeCanonicalMetadata | System volume metadata | + +> Cryptex1 components (SystemOS/AppOS DMGs) are **not** included in the BuildManifest. +> They are only needed by `cfw_install.sh` which reads paths from the original iPhone manifest separately. + +### Build Identity + +`fw_manifest.py` generates a **single** DFU erase-install identity (20 components). +The VM always boots via DFU restore, so only one identity is needed. + +| Variant | Boot Chain | Ramdisk | +|---------|-----------|---------| +| `Darwin Cloud Customer Erase Install (IPSW)` | PCC RELEASE (LLB/iBSS/iBEC) + RESEARCH (iBoot/TXM) | PCC erase | + +idevicerestore selects this identity by partial-matching `Info.Variant` against +`"Erase Install (IPSW)"` while excluding `"Research"`. + +### Patched Components Summary + +**Boot chain patches** (`fw_patch.py`) — all 6 targets from **PCC**: + +| Component | Patches | Technique | +|-----------|---------|-----------| +| AVPBooter | 1 | `mov x0, #0` (DGST bypass) | +| iBSS | 2 | Dynamic via `patchers/iboot.py` (string anchors, instruction patterns) | +| iBEC | 3 | Dynamic via `patchers/iboot.py` (string anchors, instruction patterns) | +| LLB | 6 | Dynamic via `patchers/iboot.py` (string anchors, instruction patterns) | +| TXM | 1 | Dynamic via `patchers/txm.py` (trustcache hash lookup bypass) | +| KernelCache | 25 | Dynamic via `patchers/kernel.py` (string anchors, ADRP+ADD xrefs, BL frequency) | + +**CFW patches** (`patchers/cfw.py` / `cfw_install.sh`) — all 4 targets from **iPhone** Cryptex SystemOS: + +| Binary | Technique | Purpose | +|--------|-----------|---------| +| seputil | String patch (`/%s.gl` → `/AA.gl`) | Gigalocker UUID fix | +| launchd_cache_loader | NOP (disassembly-anchored) | Bypass cache validation | +| mobileactivationd | Return true (disassembly-anchored) | Skip activation check | +| launchd.plist | Plist injection | Add bash/dropbear/trollvnc daemons | + +### Boot Flow + +``` +AVPBooter (ROM, PCC) + → LLB (PCC, patched) + → iBSS (PCC, patched, DFU) + → iBEC (PCC, patched, DFU) + → SPTM + TXM (PCC, TXM patched) + → KernelCache (PCC, 25 patches) + → Ramdisk (PCC or iPhone, SSH-injected) + → iOS userland (iPhone, CFW-patched) +``` + +### Ramdisk Build (`ramdisk_build.py`) + +1. Extract IM4M from SHSH blob +2. Process 8 components: iBSS, iBEC, SPTM, DeviceTree, SEP, TXM, KernelCache, Ramdisk+Trustcache +3. For ramdisk: extract base DMG → create 254 MB APFS volume → mount → inject SSH tools from `resources/ramdisk_input.tar.zst` → re-sign Mach-Os with ldid + signcert.p12 → build trustcache +4. Sign all components with IM4M manifest → output to `Ramdisk/` directory as IMG4 files + +### CFW Installation (`cfw_install.sh`) + +7 phases, safe to re-run (idempotent): +1. Decrypt/mount Cryptex SystemOS and AppOS DMGs (`ipsw` + `aea`) +2. Patch seputil (gigalocker UUID) +3. Install GPU driver (AppleParavirtGPUMetalIOGPUFamily) +4. Install iosbinpack64 (jailbreak tools) +5. Patch launchd_cache_loader (NOP cache validation) +6. Patch mobileactivationd (activation bypass) +7. Install LaunchDaemons (bash, dropbear SSH, trollvnc) + +--- + +## Coding Conventions + +### Swift + +- **Style:** Pragmatic, minimal. No unnecessary abstractions. +- **Sections:** Use `// MARK: -` to organize code within files. +- **Access control:** Default (internal). Only mark `private` when needed for clarity. +- **Async:** Use `async/await` for VM lifecycle. `@MainActor` for UI and VM start operations. +- **Naming:** Types are `VPhone`-prefixed (`VPhoneVM`, `VPhoneWindowController`). Match Apple framework conventions. + +### ObjC Bridge + +- All functions are C-style (no ObjC classes exposed to Swift). +- Return `nil`/`NULL` on failure — caller handles gracefully. +- Header documents the private API being wrapped in each function's doc comment. + +### Shell Scripts + +- Use `zsh` with `set -euo pipefail`. +- Scripts resolve their own directory via `${0:a:h}` or `$(cd "$(dirname "$0")" && pwd)`. +- Build uses `make build` which handles compilation and entitlement signing. + +### Python Scripts + +- Firmware patching uses `capstone` (disassembly), `keystone-engine` (assembly), and `pyimg4` (IM4P handling). +- `patchers/kernel.py` uses dynamic pattern finding (string anchors, ADRP+ADD xrefs, BL frequency analysis) — nothing is hardcoded to specific offsets. +- Each patch is logged with offset and before/after state. +- Scripts operate on a VM directory and auto-discover the `*Restore*` subdirectory. +- **Environment:** Use the project venv (`source .venv/bin/activate`). Create with `make setup_venv`. All deps in `requirements.txt`: `capstone`, `keystone-engine`, `pyimg4`. + +## Build & Sign + +The binary requires private entitlements to use PV=3 virtualization: + +- `com.apple.private.virtualization` +- `com.apple.private.virtualization.security-research` +- `com.apple.security.virtualization` +- `com.apple.vm.networking` +- `com.apple.security.get-task-allow` + +Always use `make build` — never `swift build` alone, as the unsigned binary will fail at runtime. + +## VM Creation (`make vm_new`) + +Creates a VM directory with: +- Sparse disk image (default 64 GB) +- SEP storage (512 KB flat file) +- AVPBooter + AVPSEPBooter ROMs (copied from `/System/Library/Frameworks/Virtualization.framework/`) +- NVRAM and machineIdentifier auto-created on first boot + +Override defaults: `make vm_new VM_DIR=myvm DISK_SIZE=32`. + +## Design System + +### Intent + +**Who:** Security researchers working with Apple firmware and virtual devices. Technical, patient, comfortable in terminals. Likely running alongside GDB, serial consoles, and SSH sessions. + +**Task:** Boot, configure, and interact with virtual iPhones for firmware research. Monitor boot state, capture serial output, debug at the firmware level. + +**Feel:** Like a research instrument. Precise, informative, honest about internal state. No decoration — every pixel earns its place. + +### Palette + +- **Background:** Dark neutral (`#1a1a1a` — near-black, low blue to reduce eye strain during long sessions) +- **Surface:** `#242424` (elevated panels), `#2e2e2e` (interactive elements) +- **Text primary:** `#e0e0e0` (high contrast without being pure white) +- **Text secondary:** `#888888` (labels, metadata) +- **Accent — status green:** `#4ade80` (VM running, boot success) +- **Accent — amber:** `#fbbf24` (DFU mode, warnings, in-progress states) +- **Accent — red:** `#f87171` (errors, VM stopped with error) +- **Accent — blue:** `#60a5fa` (informational, links, interactive highlights) + +Rationale: Dark surfaces match the terminal-adjacent workflow. Status colors borrow from oscilloscope/JTAG tooling — green for good, amber for attention, red for fault. No brand colors — this is a tool, not a product. + +### Typography + +- **UI font:** System monospace (SF Mono / Menlo). Everything in this tool is technical — monospace respects the content. +- **Headings:** System sans (SF Pro) semibold, used sparingly for section labels only. +- **Serial/log output:** Monospace, `#e0e0e0` on dark background. No syntax highlighting — raw output, exactly as received. + +### Depth + +- **Approach:** Flat with subtle 1px borders (`#333333`). No shadows, no blur. Depth through color difference only. +- **Rationale:** Shadows suggest consumer software. Borders suggest instrument panels. This is an instrument. + +### Spacing + +- **Base unit:** 8px +- **Component padding:** 12px (1.5 units) +- **Section gaps:** 16px (2 units) +- **Window margins:** 16px + +### Components + +- **Status indicator:** Small circle (8px) with color fill + label. No animation — state changes are instantaneous. +- **VM display:** Full-bleed within its container. No rounded corners on the display itself. +- **Log output:** Scrolling monospace region, bottom-anchored (newest at bottom). No line numbers unless requested. +- **Toolbar (if present):** Icon-only, 32px touch targets, subtle hover state (`#2e2e2e` -> `#3a3a3a`). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d112d49 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,16 @@ +# vphone-cli + +Virtual iPhone boot tool using Apple's Virtualization.framework with PCC research VMs. + +See [AGENTS.md](./AGENTS.md) for project conventions, architecture, and design system. + +## Quick Reference + +- **Build:** `make build` +- **Boot (headless):** `make boot` +- **Boot (DFU):** `make boot_dfu` +- **All targets:** `make help` +- **Python venv:** `make setup_venv` (installs to `.venv/`, activate with `source .venv/bin/activate`) +- **Platform:** macOS 14+ (Sequoia), SIP/AMFI disabled +- **Language:** Swift 5.10 (SwiftPM), ObjC bridge for private APIs +- **Python deps:** `capstone`, `keystone-engine`, `pyimg4` (see `requirements.txt`) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b3f0d80 --- /dev/null +++ b/Makefile @@ -0,0 +1,175 @@ +# ═══════════════════════════════════════════════════════════════════ +# vphone-cli — Virtual iPhone boot tool +# ═══════════════════════════════════════════════════════════════════ + +# ─── Configuration (override with make VAR=value) ───────────────── +VM_DIR ?= vm +CPU ?= 4 +MEMORY ?= 4096 +DISK_SIZE ?= 64 + +# ─── Paths ──────────────────────────────────────────────────────── +SCRIPTS := scripts +BINARY := .build/release/vphone-cli +ENTITLEMENTS := sources/vphone.entitlements +VENV := .venv +LIMD_PREFIX := .limd +IRECOVERY := $(LIMD_PREFIX)/bin/irecovery +IDEVICERESTORE := $(LIMD_PREFIX)/bin/idevicerestore +PYTHON := $(CURDIR)/$(VENV)/bin/python3 + +SWIFT_SOURCES := $(shell find sources -name '*.swift' -o -name '*.m' -o -name '*.h') + +# ─── Environment — prefer project-local binaries ──────────────── +export PATH := $(CURDIR)/$(LIMD_PREFIX)/bin:$(CURDIR)/$(VENV)/bin:$(CURDIR)/.build/release:$(PATH) + +# ─── Default ────────────────────────────────────────────────────── +.PHONY: help +help: + @echo "vphone-cli — Virtual iPhone boot tool" + @echo "" + @echo "Setup (one-time):" + @echo " make setup_venv Create Python .venv" + @echo " make setup_libimobiledevice Build libimobiledevice toolchain" + @echo "" + @echo "Build:" + @echo " make build Build + sign vphone-cli" + @echo " make install Build + copy to ./bin/" + @echo " make clean Remove .build/" + @echo "" + @echo "VM management:" + @echo " make vm_new Create VM directory" + @echo " make boot Boot VM (headless)" + @echo " make boot_dfu Boot VM in DFU mode" + @echo "" + @echo "Firmware pipeline:" + @echo " make fw_prepare Download IPSWs, extract, merge" + @echo " make fw_patch Patch boot chain (6 components)" + @echo "" + @echo "Restore:" + @echo " make restore_get_shsh Fetch SHSH blob from device" + @echo " make restore idevicerestore to device" + @echo "" + @echo "Ramdisk:" + @echo " make ramdisk_build Build signed SSH ramdisk" + @echo " make ramdisk_send Send ramdisk to device" + @echo "" + @echo "CFW:" + @echo " make cfw_install Install CFW mods via SSH" + @echo "" + @echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)" + +# ═══════════════════════════════════════════════════════════════════ +# Setup +# ═══════════════════════════════════════════════════════════════════ + +.PHONY: setup_venv setup_libimobiledevice + +setup_venv: + zsh $(SCRIPTS)/setup_venv.sh + +setup_libimobiledevice: + bash $(SCRIPTS)/setup_libimobiledevice.sh + +# ═══════════════════════════════════════════════════════════════════ +# Build +# ═══════════════════════════════════════════════════════════════════ + +.PHONY: build install clean + +build: $(BINARY) + +$(BINARY): $(SWIFT_SOURCES) Package.swift $(ENTITLEMENTS) + @echo "=== Building vphone-cli ===" + swift build -c release 2>&1 | tail -5 + @echo "" + @echo "=== Signing with entitlements ===" + codesign --force --sign - --entitlements $(ENTITLEMENTS) $@ + @echo " signed OK" + +install: build + mkdir -p ./bin + cp -f $(BINARY) ./bin/vphone-cli + @echo "Installed to ./bin/vphone-cli" + +clean: + swift package clean + rm -rf .build + +# ═══════════════════════════════════════════════════════════════════ +# VM management +# ═══════════════════════════════════════════════════════════════════ + +.PHONY: vm_new boot boot_dfu + +vm_new: + zsh $(SCRIPTS)/vm_create.sh --dir $(VM_DIR) --disk-size $(DISK_SIZE) + +boot: build + cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \ + --rom ./AVPBooter.vresearch1.bin \ + --disk ./Disk.img \ + --nvram ./nvram.bin \ + --cpu $(CPU) --memory $(MEMORY) \ + --serial-log ./serial.log \ + --stop-on-panic --stop-on-fatal-error \ + --sep-rom ./AVPSEPBooter.vresearch1.bin \ + --sep-storage ./SEPStorage \ + --no-graphics + +boot_dfu: build + cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \ + --rom ./AVPBooter.vresearch1.bin \ + --disk ./Disk.img \ + --nvram ./nvram.bin \ + --cpu $(CPU) --memory $(MEMORY) \ + --serial-log ./serial.log \ + --stop-on-panic --stop-on-fatal-error \ + --sep-rom ./AVPSEPBooter.vresearch1.bin \ + --sep-storage ./SEPStorage \ + --no-graphics --dfu + +# ═══════════════════════════════════════════════════════════════════ +# Firmware pipeline +# ═══════════════════════════════════════════════════════════════════ + +.PHONY: fw_prepare fw_patch + +fw_prepare: + cd $(VM_DIR) && bash "$(CURDIR)/$(SCRIPTS)/fw_prepare.sh" + +fw_patch: + cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch.py" . + +# ═══════════════════════════════════════════════════════════════════ +# Restore +# ═══════════════════════════════════════════════════════════════════ + +.PHONY: restore_get_shsh restore + +restore_get_shsh: + cd $(VM_DIR) && "$(CURDIR)/$(IDEVICERESTORE)" -e -y ./iPhone*_Restore -t + +restore: + cd $(VM_DIR) && "$(CURDIR)/$(IDEVICERESTORE)" -e -y ./iPhone*_Restore + +# ═══════════════════════════════════════════════════════════════════ +# Ramdisk +# ═══════════════════════════════════════════════════════════════════ + +.PHONY: ramdisk_build ramdisk_send + +ramdisk_build: + cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/ramdisk_build.py" . + +ramdisk_send: + cd $(VM_DIR) && IRECOVERY="$(CURDIR)/$(IRECOVERY)" zsh "$(CURDIR)/$(SCRIPTS)/ramdisk_send.sh" + +# ═══════════════════════════════════════════════════════════════════ +# CFW +# ═══════════════════════════════════════════════════════════════════ + +.PHONY: cfw_install + +cfw_install: + cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" . diff --git a/Package.swift b/Package.swift index b04e062..120bae1 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( // ObjC module: wraps private Virtualization.framework APIs .target( name: "VPhoneObjC", - path: "Sources/VPhoneObjC", + path: "sources/vphone-objc", publicHeadersPath: "include", linkerSettings: [ .linkedFramework("Virtualization"), @@ -27,6 +27,7 @@ let package = Package( "VPhoneObjC", .product(name: "ArgumentParser", package: "swift-argument-parser"), ], + path: "sources/vphone-cli", swiftSettings: [ .unsafeFlags(["-parse-as-library"]), ], diff --git a/README.md b/README.md index 87f0c72..bed021a 100644 --- a/README.md +++ b/README.md @@ -1,960 +1,140 @@ -# pcc-vmapple +# vphone-cli -Long story short, Apple's Private Cloud Compute provides a series of virtual machines for security research, which includes VM configurations capable of booting an iOS/iPhone environment. - -The VM system used for recovery is a dedicated pcc image, responsible for LLM inference and providing services. After modifying the boot firmware and LLB/iBSS/Kernel, it can be used to load an iOS 26 virtual machine. +Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC research VM infrastructure. ![poc](./demo.png) -## Prepare Development Environment +## Tested Environments -> **Note:** Disabling SIP is not for modifying the system. We can use a custom boot ROM via private APIs, but `Virtualization.framework` checks our binary's entitlements before allowing the launch of a specially configured VM. Therefore, we need to disable SIP to modify boot arguments and disable AMFI checks. +| Record | Host macOS | Host Hardware | iPhone IPSW | CloudOS IPSW | +|--------|------------|---------------|-------------|--------------| +| 1 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.1_23B85_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` | +| 2 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.3_23D127_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` | -### Reboot into Recovery Mode +## Prerequisites -`csrutil disable` +**Disable SIP and AMFI** — required for private Virtualization.framework entitlements. -`csrutil allow-research-guests enable` - -### Reboot into System - -`sudo nvram boot-args="amfi_get_out_of_my_way=1 -v"` - -## Prepare Resource Files - -### Enable Research Environment VM Resource Control - -- `sudo /System/Library/SecurityResearch/usr/bin/pccvre` -- `cd /System/Library/SecurityResearch/usr/bin/` -- `./pccvre release list` -- `./pccvre release download --release 35622` -- `./pccvre instance create -N pcc-research -R 35622 --variant research` - -### Obtain Resource Files - -Please prepare the pcc vm environment. We will need to use this virtual machine as a template, overwrite the boot firmware (removing signature checks) to load the customized LLB/iBoot for recovery. - -- `~/Library/Application\ Support/com.apple.security-research.vrevm/VM-Library/pcc-research.vm` - -### Download Firmware - -We will prepare the hybrid firmware and modify it later. - -- [https://updates.cdn-apple.com/2025FallFCS/fullrestores/089-13864/668EFC0E-5911-454C-96C6-E1063CB80042/iPhone17,3_26.1_23B85_Restore.ipsw](https://updates.cdn-apple.com/2025FallFCS/fullrestores/089-13864/668EFC0E-5911-454C-96C6-E1063CB80042/iPhone17,3_26.1_23B85_Restore.ipsw) -- [https://updates.cdn-apple.com/private-cloud-compute/399b664dd623358c3de118ffc114e42dcd51c9309e751d43bc949b98f4e31349](https://updates.cdn-apple.com/private-cloud-compute/399b664dd623358c3de118ffc114e42dcd51c9309e751d43bc949b98f4e31349) - -## First Boot of the Virtual Machine - -### Build the Binaries Required to Boot the VM - -We can use the `vrevm` binary to boot the pcc virtual machine prepared by Apple, but since we need to boot customized firmware, we need to replicate the relevant configuration builder of `vrevm` and boot it manually. +Boot into Recovery (long press power button), open Terminal: ```bash -➜ vphone-cli ./build_and_sign.sh -=== Building vphone-cli === -[2/2] Compiling plugin GenerateDoccReference -Building for production... -[2/5] Write swift-version--3CB7CFEC50E0D141.txt -[3/4] Linking vphone-cli -Build complete! (1.66s) - -=== Signing with entitlements === - entitlements: /Users/qaq/Desktop/vphone-cli/vphone.entitlements -/Users/qaq/Desktop/vphone-cli/.build/release/vphone-cli: replacing existing signature - signed OK - -=== Entitlement verification === -[Dict] - [Key] com.apple.private.virtualization - [Value] - [Bool] true - [Key] com.apple.private.virtualization.security-research - [Value] - [Bool] true - [Key] com.apple.security.get-task-allow - [Value] - [Bool] true - [Key] com.apple.security.virtualization - [Value] - [Bool] true - [Key] com.apple.vm.networking - [Value] - [Bool] true - -=== Binary === --rwxr-xr-x 1 qaq staff 1.6M Feb 26 15:54 /Users/qaq/Desktop/vphone-cli/.build/release/vphone-cli - -Done. Run with: - /Users/qaq/Desktop/vphone-cli/.build/release/vphone-cli --rom --disk --serial -➜ vphone-cli +csrutil disable +csrutil allow-research-guests enable ``` +After restarting into macOS: + ```bash -➜ vphone-cli ./vphone-cli --help -OVERVIEW: Boot a virtual iPhone (PV=3) in DFU mode - -Creates a Virtualization.framework VM with platform version 3 (vphone) -and boots it into DFU mode for firmware loading via irecovery. - -Requires: - - macOS 15+ (Sequoia or later) - - SIP/AMFI disabled - - Signed with vphone entitlements (done automatically by wrapper script) - -Example: - vphone-cli --rom firmware/rom.bin --disk firmware/disk.img --serial - -USAGE: vphone-cli [] --rom --disk - -OPTIONS: - --rom Path to the AVPBooter / ROM binary - --disk Path to the disk image - --nvram Path to NVRAM storage (created/overwritten) (default: nvram.bin) - --cpu Number of CPU cores (default: 4) - --memory Memory size in MB (default: 4096) - --serial Allocate a PTY for serial console - --serial-path - Path to an existing serial device - --gdb-port GDB debug stub port (default: 8000) - --stop-on-panic Stop VM on guest panic - --stop-on-fatal-error Stop VM on fatal error - --skip-sep Skip SEP coprocessor setup - --sep-storage - Path to SEP storage file (created if missing) - --sep-rom Path to SEP ROM binary - --no-graphics Run without GUI (headless) - -h, --help Show help information. +sudo nvram boot-args="amfi_get_out_of_my_way=1 -v" ``` -### Prepare VM Boot Firmware +Restart once more. -Create a folder to store these files. +**Install dependencies:** ```bash -➜ vphone-cli tree VM - -├── AVPBooter.vresearch1.bin -├── AVPSEPBooter.vresearch1.bin - -├── AuxiliaryStorage -├── Disk.img -├── SEPStorage -└── config.plist - -1 directory, 6 files +make setup_libimobiledevice # build libimobiledevice toolchain +make setup_venv # create Python venv +source .venv/bin/activate ``` -- AVPBooter.vresearch1.bin - - /System/Library/Frameworks/Virtualization.framework/Versions/A/Resources/AVPBooter.vresearch1.bin -- AVPSEPBooter.vresearch1.bin - - /System/Library/Frameworks/Virtualization.framework/Versions/A/Resources/AVPSEPBooter.vresearch1.bin -- Please copy the remaining files from `pcc-research.vm` - -### Boot the VM into Recovery Mode +## Quick Start ```bash -➜ vphone-cli ./boot_dfu.sh -=== vphone-cli === -ROM : ./VM/AVPBooter.vresearch1.bin -Disk : ./VM/Disk.img -NVRAM : ./VM/nvram.bin -CPU : 4 -Memory: 4096 MB -GDB : localhost:8000 -SEP : enabled - storage: ./VM/SEPStorage - rom : ./VM/AVPSEPBooter.vresearch1.bin - -[vphone] PV=3 hardware model: isSupported = true -[vphone] PTY: /dev/ttys001 -2026-02-26 16:03:06.271 vphone-cli[85197:1074455] [vphone] SEP coprocessor configured (storage: /Users/qaq/Desktop/vphone-cli/VM/SEPStorage) -[vphone] SEP coprocessor enabled (storage: /Users/qaq/Desktop/vphone-cli/VM/SEPStorage) -[vphone] Configuration validated -[vphone] Starting DFU... -[vphone] VM +make build # build + sign vphone-cli +make vm_new # create vm/ directory (ROMs, disk, SEP storage) +make fw_prepare # download IPSWs, extract, merge, generate manifest +make fw_patch # patch boot chain (6 components, 41+ modifications) +make boot_dfu # boot VM in DFU mode +make restore_get_shsh # fetch SHSH blob +make restore # flash firmware via idevicerestore ``` -Please confirm the Chip ID in the System Information. +## Ramdisk and CFW + +After restoring, boot into DFU again and load the SSH ramdisk: ```bash -Apple Mobile Device (DFU Mode): - - 位置ID: 0x80100000 - 连接类型: Removable - 生产企业: Apple Inc. - 序列号: SDOM:01 CPID:FE01 CPRV:00 CPFM:00 SCEP:01 BDID:90 ECID:55E4D88BB1F30E6E IBFL:24 SRTG:[iBoot-13822.81.10] - 链接速度: 480 Mb/s - USB供应商ID: 0x05ac - USB产品ID: 0x1227 - USB产品版本: 0x0000 +make boot_dfu # terminal 1 +make ramdisk_build # build signed SSH ramdisk +make ramdisk_send # terminal 2 — send to device ``` -If `CPFM` does not match, it can probably be ignored. The smaller the value, the greater the modification permissions of the system. (Unverified) - -- 00 should be an engineering sample -- 03 should be an end product - ---- - -### Obtain Restore Firmware Signature - -**It may be re-obtained later; this step is only to ensure your environment is working properly.** You need to add device adaptation information to `irecovery` for it to work correctly. - -`{ "iPhone99,11", "vresearch101ap", 0x90, 0xFE01, "iPhone 99,11" }, ` +Install CFW (Cryptexes, patched binaries, jailbreak tools, LaunchDaemons): ```bash -git clone --recursive https://github.com/wh1te4ever/libirecovery -cd libirecovery -./autogen.sh -make -j8 - -# Must be installed to the system, idevicerestore used later depends on this framework -sudo make install +iproxy 2222 22 +make cfw_install ``` -At this point, you can query the virtual machine for device hardware information. +## Boot ```bash -➜ CFW git:(main) ✗ irecovery -q -CPID: 0xfe01 -CPRV: 0x00 -BDID: 0x90 -ECID: 0x02dea93bbf44524c -CPFM: 0x00 -SCEP: 0x01 -IBFL: 0x24 -SRTG: iBoot-13822.81.10 -SRNM: N/A -IMEI: N/A -NONC: e3a3267a539aa88454ec66edc7f8d1f3fade17ad44bb1e962a15f816203bb9b2 -SNON: efbeaddeefbeaddeefbeaddeefbeaddeefbeadde -MODE: DFU -PRODUCT: iPhone99,11 -MODEL: vresearch101ap -NAME: iPhone 99,11 +make boot ``` -Now, request the firmware signature. If the following error occurs, it might be because `autogen.sh` found a `libirecovery` in the system. The fastest way is to replace it directly. 🤣 - -```bash -➜ CFW git:(main) ✗ idevicerestore -e -y ./iPhone17,3_26.1_23B85_Restore -t -idevicerestore 1.0.0-270-g405fcd1 (libirecovery 1.3.1, libtatsu 1.0.5) -Found device in DFU mode -Unable to discover device type -``` - -```bash -# Replace /opt/homebrew/opt/libirecovery/lib/libirecovery-1.0.5.dylib with the following file -./src/.libs/libirecovery-1.0.dylib -./src/.libs/libirecovery-1.0.5.dylib -``` - -Make sure you see shsh in the output. - -```bash -➜ CFW git:(main) ✗ idevicerestore -e -y ./iPhone17,3_26.1_23B85_Restore -t -idevicerestore 1.0.0-270-g405fcd1 (libirecovery 1.3.1, libtatsu 1.0.5) -Found device in DFU mode -ECID: 206788706982711884 -Identified device as vresearch101ap, iPhone99,11 -Device Product Version: N/A -Device Product Build: N/A -Extracting BuildManifest from IPSW -IPSW Product Version: 26.1 -IPSW Product Build: 23B85 Major: 23 -Device supports Image4: true -Variant: Darwin Cloud Customer Erase Install (IPSW) -This restore will erase all device data. -Checking IPSW for required components... -All required components found in IPSW -Getting ApNonce in DFU mode... e3 a3 26 7a 53 9a a8 84 54 ec 66 ed c7 f8 d1 f3 fa de 17 ad 44 bb 1e 96 2a 15 f8 16 20 3b b9 b2 -Trying to fetch new SHSH blob -Getting SepNonce in dfu mode... ef be ad de ef be ad de ef be ad de ef be ad de ef be ad de -Received SHSH blobs -SHSH saved to 'shsh/206788706982711884-iPhone99,11-26.1.shsh' -➜ CFW git:(main) ✗ -``` - -> **Note:** If fetching SHSH keeps failing here, you can skip this step and proceed. This might be caused by a mismatched BuildManifest or similar issues. The firmware preparation scripts in the subsequent steps will build the correct manifest. If you don't encounter any issues later, this error can be safely ignored. - -## Unlock VM Firmware - -`AVPBooter.vresearch1.bin` needs to be unlocked to accept custom hybrid firmware. - -### Find all "DGST" (Optional) - -`if ( (_DWORD)v8 != 'DGST' )` is the logic for judgment. Taking the ROM on the author's system as an example. - -```bash -__int64 __fastcall sub_102400(__int64 a1, __int64 a2, int a3, __int64 a4) - ->> if ( (_DWORD)v8 != 'DGST' ) ->> v20 = sub_1021EC(0, 'DGST', v82); -``` - -### Execute Replacement Script - -```bash -export AVPBOOTER_BIN=/Users/qaq/Desktop/vphone-cli/VM/AVPBooter.vresearch1.bin -python3 patch_AVPBooter.vresearch1.bin.py - -➜ super-tart-vphone-private git:(main) ✗ python3 /Users/qaq/Desktop/vphone-cli/VM/patch_AVPBooter.vresearch1.bin.py -[*] Loaded /Users/qaq/Desktop/vphone-cli/VM/AVPBooter.vresearch1.bin (251856 bytes) -[*] Processor: ARM Little-endian, 64-bit (AArch64) -[*] Base address: 0x100000 -[*] Disassembling full binary ... -[*] Disassembled 62964 instructions -[*] Text-search (slow!) for "0x4447" ... -[*] Found 2 match(es): - [+] 0x1026B0: movk w8, #0x4447, lsl #16 - [+] 0x102860: movk w1, #0x4447, lsl #16 - -[*] Found epilogue `retab` at 0x102C40 -[*] Return value set at 0x102C20: mov x0, x20 - -============================================================ - BEFORE patch (around 0x102C20): -============================================================ - 0x102BF8: bl #0x102d5c - 0x102BFC: add w0, w8, #0x2f - 0x102C00: bl #0x119f7c - 0x102C04: mov w20, #-1 - 0x102C08: ldur x8, [x29, #-0x58] - 0x102C0C: adrp x9, #0x70028000 - 0x102C10: add x9, x9, #0x170 - 0x102C14: ldr x9, [x9] - 0x102C18: cmp x9, x8 - 0x102C1C: b.ne #0x102cd8 - >>> 0x102C20: mov x0, x20 - 0x102C24: ldp x29, x30, [sp, #0xd0] - 0x102C28: ldp x20, x19, [sp, #0xc0] - 0x102C2C: ldp x22, x21, [sp, #0xb0] - 0x102C30: ldp x24, x23, [sp, #0xa0] - 0x102C34: ldp x26, x25, [sp, #0x90] - 0x102C38: ldp x28, x27, [sp, #0x80] - 0x102C3C: add sp, sp, #0xe0 - 0x102C40: retab - 0x102C44: mov w19, #0x11 - 0x102C48: movk w19, #0x4004, lsl #16 - 0x102C4C: stp xzr, xzr, [sp, #0x30] - 0x102C50: add x9, sp, #0x30 - 0x102C54: add x1, x8, #6 - 0x102C58: add x2, sp, #0x30 - 0x102C5C: add x3, x9, #8 - 0x102C60: mov x0, x23 - 0x102C64: bl #0x11bd64 - 0x102C68: cbz w0, #0x102c78 - -[+] Patched 0x102C20 (file offset 0x2C20): e00314aa -> 000080d2 (mov x0, x20 -> mov x0, #0) - -============================================================ - AFTER patch (around 0x102C20): -============================================================ - 0x102BF8: bl #0x102d5c - 0x102BFC: add w0, w8, #0x2f - 0x102C00: bl #0x119f7c - 0x102C04: mov w20, #-1 - 0x102C08: ldur x8, [x29, #-0x58] - 0x102C0C: adrp x9, #0x70028000 - 0x102C10: add x9, x9, #0x170 - 0x102C14: ldr x9, [x9] - 0x102C18: cmp x9, x8 - 0x102C1C: b.ne #0x102cd8 - >>> 0x102C20: mov x0, #0 - 0x102C24: ldp x29, x30, [sp, #0xd0] - 0x102C28: ldp x20, x19, [sp, #0xc0] - 0x102C2C: ldp x22, x21, [sp, #0xb0] - 0x102C30: ldp x24, x23, [sp, #0xa0] - 0x102C34: ldp x26, x25, [sp, #0x90] - 0x102C38: ldp x28, x27, [sp, #0x80] - 0x102C3C: add sp, sp, #0xe0 - 0x102C40: retab - 0x102C44: mov w19, #0x11 - 0x102C48: movk w19, #0x4004, lsl #16 - 0x102C4C: stp xzr, xzr, [sp, #0x30] - 0x102C50: add x9, sp, #0x30 - 0x102C54: add x1, x8, #6 - 0x102C58: add x2, sp, #0x30 - 0x102C5C: add x3, x9, #8 - 0x102C60: mov x0, x23 - 0x102C64: bl #0x11bd64 - 0x102C68: cbz w0, #0x102c78 - -[+] Patched binary written to /Users/qaq/Desktop/vphone-cli/VM/AVPBooter.vresearch1.patched.bin -``` - -### Confirm Correct Boot - -Just execute `./boot_dfu.sh` above once again. - -## Build CFW - -This part is very tedious, be prepared with patience. - -### Obtain Firmware Content - -Run it and confirm that the folder `iPhone17,3_26.1_23B85_Restore` **exists.** - -### Patch Firmware - -The patch system of the entire repository involves **41+ modifications**, covering 7 major categories of components. - -```bash - 1. AVPBooter — DGST validation bypass via text-search + epilogue walk - 2. iBSS — serial labels + image4 callback bypass - 3. iBEC — serial labels + image4 callback + boot-args relocation - 4. LLB — serial labels + image4 callback + boot-args + 6 fixed patches (rootfs/panic) - 5. TXM — trustcache bypass - 6. kernelcache — 25 fixed patches (APFS, MAC hooks, debugger, launch constraints) -``` - -First you need to install some components - -```bash -pip3 install keystone-engine capstone pyimg4 -``` - -Then - -```bash -➜ vphone git:(main) ✗ python3 patch_scripts/patch_firmware.py ~/Desktop/vphone-cli/VM - -[*] VM directory: /Users/qaq/Desktop/vphone-cli/VM -[*] Restore directory: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore -[*] Patching 6 boot-chain components ... - -============================================================ - AVPBooter: /Users/qaq/Desktop/vphone-cli/VM/AVPBooter.vresearch1.bin -============================================================ - format: raw, 251856 bytes - 0x2C20: mov x0, #0 -> mov x0, #0 - [+] saved (raw) - -============================================================ - iBSS: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore/Firmware/dfu/iBSS.d47.RELEASE.im4p -============================================================ - format: IM4P, fourcc=ibss, 3755424 bytes - serial labels -> "Loaded iBSS" - 0x1F7BE0: b.ne -> nop, mov x0,x22 -> mov x0,#0 - [+] saved (IM4P) - -============================================================ - iBEC: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore/Firmware/dfu/iBEC.d47.RELEASE.im4p -============================================================ - format: IM4P, fourcc=ibec, 3755424 bytes - serial labels -> "Loaded iBEC" - 0x1F7BE0: b.ne -> nop, mov x0,x22 -> mov x0,#0 - boot-args -> "serial=3 -v debug=0x2014e %s" at 0x1B2970 - [+] saved (IM4P) - -============================================================ - LLB: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore/Firmware/all_flash/LLB.d47.RELEASE.im4p -============================================================ - format: IM4P, fourcc=illb, 3755424 bytes - serial labels -> "Loaded LLB" - 0x1F7BE0: b.ne -> nop, mov x0,x22 -> mov x0,#0 - boot-args -> "serial=3 -v debug=0x2014e %s" at 0x1B2970 - 0x0002AFE8: b +0x2c: skip sig check - 0x0002ACA0: NOP sig verify - 0x0002B03C: b -0x258 - 0x0002ECEC: NOP verify - 0x0002EEE8: b +0x24 - 0x0001A64C: NOP: bypass panic - [+] saved (IM4P) - -============================================================ - TXM: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore/Firmware/txm.iphoneos.release.im4p -============================================================ - format: IM4P, fourcc=trxm, 458784 bytes - 0x0002C1F8: trustcache bypass - [+] saved (IM4P) - -============================================================ - kernelcache: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore/kernelcache.release.iphone17 -============================================================ - format: IM4P, fourcc=krnl, 74104832 bytes - 0x02476964: _apfs_vfsop_mount (root snapshot) - 0x023CFDE4: _authapfs_seal_is_broken - 0x00F6D960: _bsd_init (rootvp auth) - 0x0163863C: _proc_check_launch_constraints - 0x01638640: ret - 0x012C8138: _PE_i_can_has_debugger - 0x012C813C: ret - 0x00FFAB98: post-validation NOP - 0x016405AC: postValidation (cmp w0, w0) - 0x016410BC: _check_dyld_policy_internal - 0x016410C8: _check_dyld_policy_internal - 0x0242011C: _apfs_graft - 0x02475044: _apfs_vfsop_mount (cmp x0, x0) - 0x02476C00: _apfs_mount_upgrade_checks - 0x0248C800: _handle_fsioc_graft - 0x023AC528: _hook_file_check_mmap - 0x023AC52C: ret - 0x023AAB58: _hook_mount_check_mount - 0x023AAB5C: ret - 0x023AA9A0: _hook_mount_check_remount - 0x023AA9A4: ret - 0x023AA80C: _hook_mount_check_umount - 0x023AA810: ret - 0x023A5514: _hook_vnode_check_rename - 0x023A5518: ret - [+] saved (IM4P) - -============================================================ - All 6 components patched successfully! -============================================================ -➜ vphone git:(main) ✗ -``` - -\ - -## Fix Boot - -After flashing the firmware, a series of modifications are still required to boot vphone. - -### Boot to Ramdisk - -Copy the following files from the software repository into the VM. - -- build_ramdisk.py -- ramdisk_send.sh -- ramdisk_input.tar.zst - -Boot into dfu mode, use `idevicerestore` to fetch `shsh`. - -```bash -idevicerestore -e -y ./iPhone17,3_26.1_23B85_Restore -t - -# Generate and save the shsh compressed as gz to ./shsh -➜ VM file shsh/18302609918026364278-iPhone99,11-26.1.shsh -gzip compressed data, original size modulo 2^32 5897 -``` - -Build Ramdisk - -```bash -➜ VM python3 ./build_ramdisk.py -[*] Setting up ramdisk_input/... -[*] VM directory: /Users/qaq/Desktop/vphone-cli/VM -[*] Restore directory: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore -[*] SHSH blob: /Users/qaq/Desktop/vphone-cli/VM/shsh/18302609918026364278-iPhone99,11-26.1.shsh - -[*] Extracting IM4M from SHSH... - -============================================================ - 1. iBSS (already patched — extract & sign) -============================================================ - [+] iBSS.vresearch101.RELEASE.img4 - -============================================================ - 2. iBEC (patch boot-args for ramdisk) -============================================================ - boot-args -> "serial=3 rd=md0 debug=0x2014e -v wdt=-1 %s" at 0x24070 - [+] iBEC.vresearch101.RELEASE.img4 - -============================================================ - 3. SPTM (sign only) -============================================================ - [+] sptm.vresearch1.release.img4 - -============================================================ - 4. DeviceTree (sign only) -============================================================ - [+] DeviceTree.vphone600ap.img4 - -============================================================ - 5. SEP (sign only) -============================================================ - [+] sep-firmware.vresearch101.RELEASE.img4 - -============================================================ - 6. TXM (patch release variant) -============================================================ - 0x0002C1F8: trustcache bypass - [+] preserved PAYP (264 bytes) - [+] txm.img4 - -============================================================ - 7. Kernelcache (already patched — repack as rkrn) -============================================================ - format: IM4P, 43991040 bytes - [+] preserved PAYP (315 bytes) - [+] krnl.img4 - -============================================================ - 8. Ramdisk + Trustcache -============================================================ - Extracting base ramdisk... - Mounting base ramdisk... -/dev/disk22 -/dev/disk23 EF57347C-0000-11AA-AA11-0030654 -/dev/disk23s1 41504653-0000-11AA-AA11-0030654 /Users/qaq/Desktop/vphone-cli/VM/SSHRD - Creating expanded ramdisk (254 MB)... -............................................................................................................ -created: /Users/qaq/Desktop/vphone-cli/VM/ramdisk_builder_temp/ramdisk1.dmg -"disk22" ejected. - Mounting expanded ramdisk... -/dev/disk22 -/dev/disk23 EF57347C-0000-11AA-AA11-0030654 -/dev/disk23s1 41504653-0000-11AA-AA11-0030654 /Users/qaq/Desktop/vphone-cli/VM/SSHRD - Injecting SSH tools... - Re-signing Mach-O binaries... - Building trustcache... - [+] trustcache.img4 - Signing ramdisk... - [+] ramdisk.img4 - -[*] Cleaning up ramdisk_builder_temp/... - -============================================================ - Ramdisk build complete! - Output: /Users/qaq/Desktop/vphone-cli/VM/Ramdisk/ -============================================================ - DeviceTree.vphone600ap.img4 13,808 bytes - iBEC.vresearch101.RELEASE.img4 611,171 bytes - iBSS.vresearch101.RELEASE.img4 611,171 bytes - krnl.img4 14,373,497 bytes - ramdisk.img4 266,344,150 bytes - sep-firmware.vresearch101.RELEASE.img4 3,315,465 bytes - sptm.vresearch1.release.img4 108,385 bytes - trustcache.img4 16,776 bytes - txm.img4 166,876 bytes -``` - -Send Ramdisk and Boot - -```bash -➜ VM ./ramdisk_send.sh -[*] Sending ramdisk from Ramdisk ... - [1/8] Loading iBSS... -[==================================================] 100.0% - [2/8] Loading iBEC... -[==================================================] 100.0% - [3/8] Loading SPTM... -[==================================================] 100.0% - [4/8] Loading TXM... -[==================================================] 100.0% - [5/8] Loading trustcache... -[==================================================] 100.0% - [6/8] Loading ramdisk... -[==================================================] 100.0% - [7/8] Loading device tree... -[==================================================] 100.0% - [8/8] Loading SEP... -[==================================================] 100.0% - [*] Booting kernel... -[==================================================] 100.0% -[+] Boot sequence complete. Device should be booting into ramdisk. -``` - -Check `vphone-cli` output - -```bash -private> -2026-02-26 12:26:55.359221+0000 Error driverkitd[4:14b][com.apple.km:DriverBinManager] contentsOfFile failed to read plist: -IOReturn AppleUSBDeviceMux::setPropertiesGated(OSObject *) setting debug level to 7 -USB init done -llllllllllllllllllllllllllllllllllllllllllllllllll -llllllllllllllllllllllllllllllllllllllllllllllllll -lllllc:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:clllll -lllll,. .,lllll -lllll, ,lllll -lllll, ,lllll -lllll, '::::, .,::::. ,lllll -lllll, ,llll; .:llll' ,lllll -lllll, ,llll; .:llll' ,lllll -lllll, ,llll; .:llll' ,lllll -lllll, ,llll; .:llll' ,lllll -lllll, ,cccc, .;cccc' ,lllll -lllll, .... ..... ,lllll -lllll, ,lllll -lllll, ,lllll -lllll, .''''''''''''. ,lllll -lllll, ,llllllllllll, ,lllll -lllll, ,llllllllllll, ,lllll -lllll, .............. ,lllll -lllll, ,lllll -lllll, ,lllll -lllll:'....................................':lllll -llllllllllllllllllllllllllllllllllllllllllllllllll -llllllllllllllllllllllllllllllllllllllllllllllllll -llllllllllllllllllllllllllllllllllllllllllllllllll -SSHRD_Script by Nathan (verygenericname) -Running server -``` - -Connect to ssh service - -```bash -➜ VM iproxy 2222 22 - -Creating listening port 2222 for device port 22 -waiting for connection - -# Map port 22 of the machine across the usb to 2222 of the current computer -``` - -```bash -➜ VM ssh root@127.0.0.1 -p2222 - -root@127.0.0.1's password: # Password is alpine -localhost:~ root# uname -a -Darwin localhost 25.1.0 Darwin Kernel Version 25.1.0: Thu Oct 23 11:11:48 PDT 2025; root:xnu-12377.42.6~55/RELEASE_ARM64_VRESEARCH1 iPhone99,11 -localhost:~ root# -``` - -### Patch Boot Disk - -First, you need to mount the disk - -```bash -ocalhost:~ root# mount_apfs -o rw /dev/disk1s1 /mnt1 -localhost:~ root# snaputil -l /mnt1 -com.apple.os.update-8AAB8DBA5C8F1F756928411675F4A892087B04559CFB084B9E400E661ABAD119 -localhost:~ root# snaputil -n $(snaputil -l /mnt1) orig-fs /mnt1 -localhost:~ root# umount /mnt1 - --- -localhost:~ root# snaputil --help -Usage: - snaputil -l (List all snapshots) - snaputil -c (Create snapshot) - snaputil -n (Rename snapshot) - snaputil -d (Delete snapshot) - snaputil -r (Revert to snapshot) - snaputil -s (Mount snapshot) - snaputil -o (Print original snapshot name) -# This is a routine operation for older jailbreaks () -``` - -Then some binary updates are required - -```bash -➜ VM ./install_cfw.sh -[*] install_cfw.sh — Installing CFW on vphone... -[+] Restore directory: /Users/qaq/Desktop/vphone-cli/VM/iPhone17,3_26.1_23B85_Restore -[+] Input resources: /Users/qaq/Desktop/vphone-cli/VM/cfw_input - -[*] Parsing BuildManifest for Cryptex paths... - SystemOS: 043-54303-126.dmg.aea - AppOS: 043-54062-129.dmg - -[1/7] Installing Cryptex (SystemOS + AppOS)... - Using cached SystemOS DMG - Using cached AppOS DMG - Mounting SystemOS... -/dev/disk22 -/dev/disk23 EF57347C-0000-11AA-AA11-0030654 -/dev/disk23s1 41504653-0000-11AA-AA11-0030654 /Users/qaq/Desktop/vphone-cli/VM/.cfw_temp/mnt_sysos - Mounting AppOS... -/dev/disk24 -/dev/disk25 EF57347C-0000-11AA-AA11-0030654 -/dev/disk25s1 41504653-0000-11AA-AA11-0030654 /Users/qaq/Desktop/vphone-cli/VM/.cfw_temp/mnt_appos - Mounting device rootfs rw... - Copying Cryptexes to device (this takes ~3 minutes)... - Creating dyld symlinks... - Unmounting Cryptex DMGs... -"disk22" ejected. -"disk24" ejected. - [+] Cryptex installed - -[2/7] Patching seputil... - Found format string at 0x1B3F0: b'/%s.gl\x00' - [+] Patched at 0x1B3F1: %s -> AA - /%s.gl -> /AA.gl - Renaming gigalocker... - [+] seputil patched - -[3/7] Installing AppleParavirtGPUMetalIOGPUFamily... - [+] GPU driver installed - -[4/7] Installing iosbinpack64... -/usr/bin/tar: Ignoring unknown extended header keyword `SCHILY.xattr.com.apple.quarantine' -/usr/bin/tar: Ignoring unknown extended header keyword `LIBARCHIVE.xattr.com.apple.quarantine' -/usr/bin/tar: Ignoring unknown extended header keyword `SCHILY.xattr.com.apple.quarantine' - [+] iosbinpack64 installed - -[5/7] Patching launchd_cache_loader... - Found anchor 'unsecure_cache' inside "launchd_unsecure_cache=" - String start: va:0x10000238E (match at va:0x100002396) - Found string ref at 0xB48 - Patching: cbz x0, #0xbfc -> nop - [+] NOPped at 0xB58 - [+] launchd_cache_loader patched - -[6/7] Patching mobileactivationd... - Found via symtab: va:0x1002F5F84 -> foff:0x2F5F84 - Original: ldrb w0, [x0, #0x14] - [+] Patched at 0x2F5F84: mov x0, #1; ret - [+] mobileactivationd patched - -[7/7] Installing LaunchDaemons... - Patching launchd.plist... - [+] Injected bash - [+] Injected dropbear - [+] Injected trollvnc - [+] LaunchDaemons installed - -[*] Unmounting device filesystems... -[*] Cleaning up temp binaries... - -[+] CFW installation complete! - Reboot the device for changes to take effect. - After boot, SSH will be available on port 22222 (password: alpine) -➜ VM -``` - -Then ssh into it and enter `halt` - -```bash -launchd quiesce complete -AppleSEPManager: Received Paging off notification -AppleUSBDeviceMux::message - kMessageInterfaceWasDeActivated -AppleUSBDeviceMux::reportStats: USB mux statistics: -USB mux: 4117556 reads / 0 errors, 2628065 writes / 0 errors -USB mux: 0 short packets, 0 dups -asyncReadComplete:1829 USB read status = 0xe00002eb -asyncReadComplete:1829 USB read status = 0xe00002eb -apfs_log_op_with_proc:3297: md0s1 unmounting volume ramdisk, requested by: launchd (pid 1); parent: kernel_task (pid 0) -apfs_vfsop_unmount:3209: md0s1 apfs_fx_defrag_stop_defrag failed w/22 -apfs_vfsop_unmount:3583: md0 nx_num_vols_mounted is 0 -is_system_shutting_down:961: System is shutting down - stop any apfs bg work. -apfs: total mem allocated: 720 (0 mb); -apfs_vfsop_unmount:3596: all done. going home. (numMountedAPFSVolumes 0) -virtual void AppleSEPManager::systemWillShutdown(IOOptionBits): Received system will shut down notification - -ApplePSCI - system off -[vphone] Guest stopped -``` - -## First Boot - -Congratulations, things are done. - -```bash -➜ vphone-cli ./boot.sh -=== Building vphone-cli === -[2/2] Compiling plugin GenerateDoccReference - - - -Using default cache paths -Code: /System/Library/xpc/launchd.plist Sig: /System/Library/xpc/launchd.plist.sig -Using unsecure cache: /System/Library/xpc/launchd.plist -Trying to send bytes to launchd: 2563 16384 -Sending validated cache to launchd -Cache sent to launchd successfully -com.apple.xpc.launchd|2026-02-26 05:34:50.946410 (finish-restore) : Doing boot task -com.apple.xpc.launchd|2026-02-26 05:34:50.948556 (finish-demo-restore) : Doing boot task -com.apple.xpc.launchd|2026-02-26 05:34:50.951290 (sysstatuscheck) : Doing boot task -com.apple.xpc.launchd|2026-02-26 05:34:50.953692 (prng_seedctl) : Doing boot task -com.apple.xpc.launchd|2026-02-26 05:34:50.956821 (launchd_cache_loader) : Doing boot task -com.apple.xpc.launchd|2026-02-26 05:34:50.968980 (workload-properties-init) : Doing boot task -com.apple.xpc.launchd|2026-02-26 05:34:50.968988 (init-exclavekit) : Doing boot task -com.apple.xpc.launchd|2026-02-26 05:34:51.015964 (boot) : Early boot complete. Continuing system boot. -com.apple.xpc.launchd|2026-02-26 05:34:51.048686 : Got first unlock unregistering for AKS events -bash-4.4# -``` - -After entering bash, you need to initialize the shell environment. +On first boot, initialize the shell environment: ```bash +# binaries are looking for each others via PATH so do not ignore this one export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11:/usr/games:/iosbinpack64/usr/local/sbin:/iosbinpack64/usr/local/bin:/iosbinpack64/usr/sbin:/iosbinpack64/usr/bin:/iosbinpack64/sbin:/iosbinpack64/bin' +# call with fullpath /iosbinpack64/bin/mkdir -p /var/dropbear /iosbinpack64/bin/cp /iosbinpack64/etc/profile /var/profile /iosbinpack64/bin/cp /iosbinpack64/etc/motd /var/motd - shutdown -h now - -<...> -"AppleSEPKeyStore":pid:0,:4007: Ready for System Shutdown -virtual void AppleSEPManager::systemWillShutdown(IOOptionBits): Received system will shut down notification - -ApplePSCI - system off -[vphone] Guest stopped -<...> ``` -To connect to the virtual machine, please use `iproxy` to forward 22222 and 5901. +After subsequent boots, connect via: ```bash -iproxy 5901 5901 -iproxy 22222 22222 +iproxy 22222 22222 # SSH +iproxy 5901 5901 # VNC ``` -## Appendix +## All Make Targets -### Boot pcc vm +Run `make help` for the full list. Key targets: + +| Target | Description | +|--------|-------------| +| `build` | Build + sign vphone-cli | +| `vm_new` | Create VM directory | +| `fw_prepare` | Download/merge IPSWs | +| `fw_patch` | Patch boot chain | +| `boot` / `boot_dfu` | Boot VM (normal / DFU) | +| `restore_get_shsh` | Fetch SHSH blob | +| `restore` | Flash firmware | +| `ramdisk_build` | Build SSH ramdisk | +| `ramdisk_send` | Send ramdisk to device | +| `cfw_install` | Install CFW mods | +| `clean` | Remove build artifacts | + +## FAQ + +> **Before anything else — run `git pull` to make sure you have the latest version.** + +**Q: I get `zsh: killed ./vphone-cli` when trying to run it.** + +AMFI is not disabled. Set the boot-arg and restart: ```bash -pccvre release download --release 35622 -pccvre instance create -N pcc-research -R 35622 --variant research +sudo nvram boot-args="amfi_get_out_of_my_way=1 -v" ``` -- -- +**Q: Can I update to a newer iOS version?** + +Yes. Override `fw_prepare` with the IPSW URL for the version you want: ```bash -vrevm restore -d -f --name pcc-research \ - -K ~/Desktop/kernelcache.research.vresearch101 \ - -S ~/Desktop/Firmware/sptm.vresearch1.release.im4p \ - -M ~/Desktop/Firmware/txm.iphoneos.research.im4p \ - --variant-name "Research Darwin Cloud Customer Erase Install (IPSW)" \ - ~/Desktop/PCC-CloudOS-26.1-23B85.ipsw +export IPHONE_SOURCE=/path/to/some_os.ipsw +export CLOUDOS_SOURCE=/path/to/some_os.ipsw +make fw_prepare +make fw_patch ``` -```bash -vrevm run --name pcc-research --debug -``` - -```bash -Starting VM: pcc-research (ecid: 8737a35e085fc3a7) -GDB stub available at localhost:50693 -SEP GDB stub available at localhost:50694 -Console log available at: /Users/qaq/Library/Application Support/com.apple.security-research.vrevm/VM-Library/pcc-research.vm/logs/console.2026-02-26T15:51:26/device -Started VM: pcc-research -======== Start of iBoot serial output. ======== -89994699affdef:138 -503b7933ad51055:716 -image <>: bdev <> type illb offset 0x20000 len 0x4cbe4 -78faf5021313e82:74 -78faf5021313e82:85 -ae71af5ee32b84:129 - - -======================================= -:: -:: ������� Supervisor iBoot for vresearch101, Copyright 2007-2025, Apple Inc. -:: -:: Local boot, Board 0x90 (vresearch101ap)/Rev 0x0 -:: -:: BUILD_TAG: iBoot-13822.42.2 -:: -:: UUID: AD1D9BE7-3400-3E52-856C-D32D1A03C0A7 -:: -:: BUILD_STYLE: RESEARCH_RELEASE -:: -:: USB_SERIAL_NUMBER: SDOM:01 CPID:FE01 CPRV:00 CPFM:03 SCEP:01 BDID:90 ECID:8737A35E085FC3A7 IBFL:3D -:: -======================================= - -a3fae6c53b7baa2:107 -3974bfd3d441da3:1609 -3974bfd3d441da3:1685 -503b7933ad51055:716 -503b7933ad51055:716 -3b9107561aef41e:187 -3b9107561aef41e:254 -2dc92642a4f3ce5:39 -2dc92642a4f3ce5:39 -a60aa294185a059:983 -a60aa294185a059:986 -3bdace14b1a9a68:3646 -3bdace14b1a9a68:3975 -7ab90c923dae682:1384 -======== End of iBoot serial output. ======== -``` +Our patches are applied via binary analysis, not static offsets, so newer versions should work. If something breaks, ask AI for help. ## Acknowledgements diff --git a/Scripts/patch_firmware.py b/Scripts/patch_firmware.py deleted file mode 100644 index b565e72..0000000 --- a/Scripts/patch_firmware.py +++ /dev/null @@ -1,614 +0,0 @@ -#!/usr/bin/env python3 -""" -patch_firmware.py — Patch all boot-chain components for vphone600. - -Run this AFTER prepare_firmware_v2.sh from the VM directory. - -Usage: - python3 patch_firmware.py [vm_directory] - - vm_directory defaults to the current working directory. - The script auto-discovers the iPhone*_Restore directory and all - firmware files by searching for known patterns. - -Components patched: - 1. AVPBooter — DGST validation bypass (mov x0, #0) - 2. iBSS — serial labels + image4 callback bypass - 3. iBEC — serial labels + image4 callback + boot-args - 4. LLB — serial labels + image4 callback + boot-args + rootfs + panic - 5. TXM — trustcache bypass (mov x0, #0) - 6. kernelcache — 25 patches (APFS, MAC, debugger, launch constraints, etc.) - -Dependencies: - pip install keystone-engine capstone pyimg4 - - If keystone fails to import, you may need the native library: - brew install cmake && pip install keystone-engine -""" - -import struct, sys, os, glob, subprocess, tempfile - -from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN -from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE -from pyimg4 import IM4P - -# ══════════════════════════════════════════════════════════════════ -# Assembler / disassembler helpers -# ══════════════════════════════════════════════════════════════════ - -_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE) - - -def asm(s): - enc, _ = _ks.asm(s) - if not enc: - raise RuntimeError(f"asm failed: {s}") - return bytes(enc) - - -def u32(val): - return struct.pack("= 0: - payp_data = original_raw[payp_offset - 10:] - output.extend(payp_data) - # Fix outer DER SEQUENCE length at bytes[2:5] - old_len = int.from_bytes(output[2:5], "big") - output[2:5] = (old_len + len(payp_data)).to_bytes(3, "big") - print(f" [+] preserved PAYP ({len(payp_data)} bytes)") - - with open(path, "wb") as f: - f.write(output) - - -# ══════════════════════════════════════════════════════════════════ -# Shared patch primitives -# ══════════════════════════════════════════════════════════════════ - -# ── image4_validate_property_callback ───────────────────────────── - -def find_image4_callback(buf, base): - candidates = [] - for insns in chunked_disasm(buf, base): - for i in range(len(insns) - 1): - if insns[i].mnemonic != "b.ne": - continue - if not (insns[i + 1].mnemonic == "mov" and insns[i + 1].op_str == "x0, x22"): - continue - addr = insns[i].address - if not any(insns[j].mnemonic == "cmp" for j in range(max(0, i - 8), i)): - continue - neg1 = any( - (insns[j].mnemonic == "movn" and insns[j].op_str.startswith("w22,")) - or ( - insns[j].mnemonic == "mov" - and "w22" in insns[j].op_str - and ("#-1" in insns[j].op_str or "#0xffffffff" in insns[j].op_str) - ) - for j in range(max(0, i - 64), i) - ) - candidates.append((addr, neg1)) - if not candidates: - return -1 - for a, n in candidates: - if n: - return a - base - return candidates[-1][0] - base - - -def patch_image4_callback(data, base): - off = find_image4_callback(bytes(data), base) - if off < 0: - print(" [-] image4 callback not found!") - return False - data[off:off + 4] = NOP - data[off + 4:off + 8] = MOV_X0_0 - print(f" 0x{off:X}: b.ne -> nop, mov x0,x22 -> mov x0,#0") - return True - - -# ── serial labels ───────────────────────────────────────────────── - -SERIAL_OFFSETS = [0x84349, 0x843F4] - - -def patch_serial_labels(data, label): - for off in SERIAL_OFFSETS: - data[off:off + len(label)] = label - print(f' serial labels -> "{label.decode()}"') - - -# ── boot-args ───────────────────────────────────────────────────── - -def encode_adrp(rd, pc, target): - imm = ((target & ~0xFFF) - (pc & ~0xFFF)) >> 12 - imm &= (1 << 21) - 1 - return 0x90000000 | ((imm & 3) << 29) | ((imm >> 2) << 5) | (rd & 0x1F) - - -def encode_add(rd, rn, imm12): - return 0x91000000 | ((imm12 & 0xFFF) << 10) | ((rn & 0x1F) << 5) | (rd & 0x1F) - - -def find_boot_args_fmt(buf): - """Find the standalone '%s' format string near boot-args data.""" - anchor = buf.find(b"rd=md0") - if anchor < 0: - anchor = buf.find(b"BootArgs") - if anchor < 0: - return -1 - off = anchor - while off < anchor + 0x40: - off = buf.find(b"%s", off) - if off < 0 or off >= anchor + 0x40: - return -1 - if buf[off - 1] == 0 and buf[off + 2] == 0: - return off - off += 1 - return -1 - - -def find_boot_args_adrp(buf, fmt_off, base): - """Find ADRP+ADD x2 that loads the boot-args format string.""" - target_va = base + fmt_off - for insns in chunked_disasm(buf, base): - for i in range(len(insns) - 1): - a, b = insns[i], insns[i + 1] - if a.mnemonic != "adrp" or b.mnemonic != "add": - continue - if a.op_str.split(",")[0].strip() != "x2": - continue - if a.operands[0].reg != b.operands[1].reg: - continue - if len(b.operands) < 3: - continue - if a.operands[1].imm + b.operands[2].imm == target_va: - return a.address - base, b.address - base - return -1, -1 - - -def find_string_slot(buf, string_len, search_start=0x14000): - """Find a NUL-filled slot for the new boot-args string. - - Scans for zero regions >= 64 bytes, returns the first 16-byte-aligned - offset with at least 8 bytes of zero padding before it. - """ - off = search_start - while off < len(buf): - if buf[off] == 0: - run_start = off - while off < len(buf) and buf[off] == 0: - off += 1 - if off - run_start >= 64: - write_off = (run_start + 8 + 15) & ~15 - if write_off + string_len <= off: - return write_off - else: - off += 1 - return -1 - - -BOOT_ARGS = b"serial=3 -v debug=0x2014e %s" - - -def patch_boot_args(data, base, new_args=BOOT_ARGS): - fmt_off = find_boot_args_fmt(data) - if fmt_off < 0: - print(" [-] boot-args fmt not found") - return False - adrp_off, add_off = find_boot_args_adrp(bytes(data), fmt_off, base) - if adrp_off < 0: - print(" [-] ADRP+ADD x2 not found") - return False - new_off = find_string_slot(data, len(new_args)) - if new_off < 0: - print(" [-] no NUL slot") - return False - new_va = base + new_off - data[new_off:new_off + len(new_args)] = new_args - wr32(data, adrp_off, encode_adrp(2, base + adrp_off, new_va)) - wr32(data, add_off, encode_add(2, 2, new_va & 0xFFF)) - print(f' boot-args -> "{new_args.decode()}" at 0x{new_off:X}') - return True - - -# ── fixed-offset patches ───────────────────────────────────────── - -def apply_fixed_patches(data, patches): - for off, val, desc in patches: - if off + 4 > len(data): - print(f" SKIP 0x{off:X}: out of range") - continue - new = asm(val) if isinstance(val, str) else u32(val) - data[off:off + 4] = new - print(f" 0x{off:08X}: {desc}") - - -# ══════════════════════════════════════════════════════════════════ -# Per-component patch functions -# ══════════════════════════════════════════════════════════════════ - -# ── 1. AVPBooter ────────────────────────────────────────────────── - -AVP_BASE = 0x100000 -AVP_SEARCH = "0x4447" -RET_MNEMONICS = {"ret", "retaa", "retab"} - - -def patch_avpbooter(data): - md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) - md.skipdata = True - insns = list(md.disasm(bytes(data), AVP_BASE)) - - hits = [i for i in insns if AVP_SEARCH in f"{i.mnemonic} {i.op_str}"] - if not hits: - print(" [-] DGST constant not found") - return False - - addr2idx = {insn.address: i for i, insn in enumerate(insns)} - idx = addr2idx[hits[0].address] - - ret_idx = None - for i in range(idx, min(idx + 512, len(insns))): - if insns[i].mnemonic in RET_MNEMONICS: - ret_idx = i - break - if ret_idx is None: - print(" [-] epilogue not found") - return False - - x0_idx = None - for i in range(ret_idx - 1, max(ret_idx - 32, -1), -1): - op, mn = insns[i].op_str, insns[i].mnemonic - if mn == "mov" and op.startswith(("x0,", "w0,")): - x0_idx = i - break - if mn in ("cset", "csinc", "csinv", "csneg") and op.startswith(("x0,", "w0,")): - x0_idx = i - break - if mn in RET_MNEMONICS or mn in ("b", "bl", "br", "blr"): - break - if x0_idx is None: - print(" [-] x0 setter not found") - return False - - target = insns[x0_idx] - file_off = target.address - AVP_BASE - data[file_off:file_off + 4] = MOV_X0_0 - print(f" 0x{file_off:X}: {target.mnemonic} {target.op_str} -> mov x0, #0") - return True - - -# ── 2. iBSS ────────────────────────────────────────────────────── - -IBOOT_BASE = 0x7006C000 - - -def patch_ibss(data): - patch_serial_labels(data, b"Loaded iBSS") - return patch_image4_callback(data, IBOOT_BASE) - - -# ── 3. iBEC ────────────────────────────────────────────────────── - -def patch_ibec(data): - patch_serial_labels(data, b"Loaded iBEC") - if not patch_image4_callback(data, IBOOT_BASE): - return False - return patch_boot_args(data, IBOOT_BASE) - - -# ── 4. LLB ─────────────────────────────────────────────────────── - -LLB_FIXED_PATCHES = [ - (0x2AFE8, 0x1400000B, "b +0x2c: skip sig check"), - (0x2ACA0, "nop", "NOP sig verify"), - (0x2B03C, 0x17FFFF6A, "b -0x258"), - (0x2ECEC, "nop", "NOP verify"), - (0x2EEE8, 0x14000009, "b +0x24"), - (0x1A64C, "nop", "NOP: bypass panic"), -] - - -def patch_llb(data): - patch_serial_labels(data, b"Loaded LLB") - if not patch_image4_callback(data, IBOOT_BASE): - return False - if not patch_boot_args(data, IBOOT_BASE): - return False - apply_fixed_patches(data, LLB_FIXED_PATCHES) - return True - - -# ── 5. TXM ─────────────────────────────────────────────────────── - -TXM_PATCHES = [ - (0x2C1F8, "mov x0, #0", "trustcache bypass"), -] - - -def patch_txm(data): - apply_fixed_patches(data, TXM_PATCHES) - return True - - -# ── 6. Kernelcache ─────────────────────────────────────────────── - -KERNEL_PATCHES = [ - (0x2476964, "nop", "_apfs_vfsop_mount (root snapshot)"), - (0x23CFDE4, "nop", "_authapfs_seal_is_broken"), - (0x0F6D960, "nop", "_bsd_init (rootvp auth)"), - (0x163863C, "mov w0, #0", "_proc_check_launch_constraints"), - (0x1638640, "ret", " ret"), - (0x12C8138, "mov x0, #1", "_PE_i_can_has_debugger"), - (0x12C813C, "ret", " ret"), - (0xFFAB98, "nop", "post-validation NOP"), - (0x16405AC, 0x6B00001F, "postValidation (cmp w0, w0)"), - (0x16410BC, "mov w0, #1", "_check_dyld_policy_internal"), - (0x16410C8, "mov w0, #1", "_check_dyld_policy_internal"), - (0x242011C, "mov w0, #0", "_apfs_graft"), - (0x2475044, 0xEB00001F, "_apfs_vfsop_mount (cmp x0, x0)"), - (0x2476C00, "mov w0, #0", "_apfs_mount_upgrade_checks"), - (0x248C800, "mov w0, #0", "_handle_fsioc_graft"), - (0x23AC528, "mov x0, #0", "_hook_file_check_mmap"), - (0x23AC52C, "ret", " ret"), - (0x23AAB58, "mov x0, #0", "_hook_mount_check_mount"), - (0x23AAB5C, "ret", " ret"), - (0x23AA9A0, "mov x0, #0", "_hook_mount_check_remount"), - (0x23AA9A4, "ret", " ret"), - (0x23AA80C, "mov x0, #0", "_hook_mount_check_umount"), - (0x23AA810, "ret", " ret"), - (0x23A5514, "mov x0, #0", "_hook_vnode_check_rename"), - (0x23A5518, "ret", " ret"), -] - - -def patch_kernelcache(data): - apply_fixed_patches(data, KERNEL_PATCHES) - return True - - -# ══════════════════════════════════════════════════════════════════ -# File discovery -# ══════════════════════════════════════════════════════════════════ - -def find_restore_dir(base_dir): - """Auto-detect the iPhone restore directory.""" - for entry in sorted(os.listdir(base_dir)): - full = os.path.join(base_dir, entry) - if os.path.isdir(full) and "Restore" in entry: - return full - return None - - -def find_file(base_dir, patterns, label): - """Search for a file matching any of the given glob patterns. - - Returns the first match, or exits with error if none found. - """ - for pattern in patterns: - matches = sorted(glob.glob(os.path.join(base_dir, pattern))) - if matches: - return matches[0] - print(f"[-] {label} not found. Searched patterns:") - for p in patterns: - print(f" {os.path.join(base_dir, p)}") - sys.exit(1) - - -# ══════════════════════════════════════════════════════════════════ -# Main -# ══════════════════════════════════════════════════════════════════ - -COMPONENTS = [ - # (name, search_base_is_restore, search_patterns, patch_function, preserve_payp) - # search_base_is_restore: False = search in vm_dir, True = search in restore_dir - # preserve_payp: True only for TXM/kernelcache (key constraints). - # iBSS/iBEC/LLB PAYP is compression metadata — appending it to an - # uncompressed IM4P causes "Memory image not valid". - # Patterns are tried in order; first match wins. Most-specific first to avoid - # picking d47/release/iphone17 variants that sort alphabetically before the - # vresearch101/research.vphone600 variants we actually need. - ("AVPBooter", False, ["AVPBooter*.bin"], patch_avpbooter, False), - ("iBSS", True, [ - "Firmware/dfu/iBSS.vresearch101.RELEASE.im4p", - "Firmware/dfu/iBSS.vresearch101*.im4p", - "Firmware/dfu/iBSS*.im4p", - "Firmware/dfu/iBSS*.raw", - ], patch_ibss, False), - ("iBEC", True, [ - "Firmware/dfu/iBEC.vresearch101.RELEASE.im4p", - "Firmware/dfu/iBEC.vresearch101*.im4p", - "Firmware/dfu/iBEC*.im4p", - "Firmware/dfu/iBEC*.raw", - ], patch_ibec, False), - ("LLB", True, [ - "Firmware/all_flash/LLB.vresearch101.RELEASE.im4p", - "Firmware/all_flash/LLB.vresearch101*.im4p", - "Firmware/all_flash/LLB*.im4p", - "Firmware/all_flash/LLB*.raw", - ], patch_llb, False), - ("TXM", True, [ - "Firmware/txm.iphoneos.research.im4p", - "Firmware/txm*research*.im4p", - "Firmware/txm*.im4p", - "Firmware/txm*.raw", - ], patch_txm, True), - ("kernelcache", True, [ - "kernelcache.research.vphone600", - "kernelcache.research.vphone600*", - "kernelcache.research.*", - "kernelcache*", - ], patch_kernelcache, True), -] - - -def patch_component(path, patch_fn, name, preserve_payp): - """Load firmware (auto-detect IM4P vs raw), patch, save.""" - print(f"\n{'=' * 60}") - print(f" {name}: {path}") - print(f"{'=' * 60}") - - im4p, data, was_im4p, original_raw = load_firmware(path) - fmt = "IM4P" if was_im4p else "raw" - extra = "" - if was_im4p and im4p: - extra = f", fourcc={im4p.fourcc}" - print(f" format: {fmt}{extra}, {len(data)} bytes") - - if not patch_fn(data): - print(f" [-] FAILED: {name}") - sys.exit(1) - - save_firmware(path, im4p, data, was_im4p, - original_raw if preserve_payp else None) - print(f" [+] saved ({fmt})") - - -def main(): - vm_dir = sys.argv[1] if len(sys.argv) > 1 else os.getcwd() - vm_dir = os.path.abspath(vm_dir) - - if not os.path.isdir(vm_dir): - print(f"[-] Not a directory: {vm_dir}") - sys.exit(1) - - restore_dir = find_restore_dir(vm_dir) - if not restore_dir: - print(f"[-] No *Restore* directory found in {vm_dir}") - print(" Run prepare_firmware_v2.sh first.") - sys.exit(1) - - print(f"[*] VM directory: {vm_dir}") - print(f"[*] Restore directory: {restore_dir}") - print(f"[*] Patching {len(COMPONENTS)} boot-chain components ...") - - for name, in_restore, patterns, patch_fn, preserve_payp in COMPONENTS: - search_base = restore_dir if in_restore else vm_dir - path = find_file(search_base, patterns, name) - patch_component(path, patch_fn, name, preserve_payp) - - print(f"\n{'=' * 60}") - print(f" All {len(COMPONENTS)} components patched successfully!") - print(f"{'=' * 60}") - - -if __name__ == "__main__": - main() diff --git a/Scripts/prepare_firmware.sh b/Scripts/prepare_firmware.sh deleted file mode 100755 index 0f3de8a..0000000 --- a/Scripts/prepare_firmware.sh +++ /dev/null @@ -1,240 +0,0 @@ -#!/bin/bash -# prepare_firmware.sh — Download, merge, and generate hybrid restore firmware. -# Combines cloudOS boot chain with iPhone OS images for vresearch101. -# -# Usage: ./prepare_firmware.sh [iphone_ipsw_url] [cloudos_url] -set -euo pipefail -cd "$(dirname "$0")" - -IPHONE_URL="${1:-https://updates.cdn-apple.com/2025FallFCS/fullrestores/089-13864/668EFC0E-5911-454C-96C6-E1063CB80042/iPhone17,3_26.1_23B85_Restore.ipsw}" -CLOUDOS_URL="${2:-https://updates.cdn-apple.com/private-cloud-compute/399b664dd623358c3de118ffc114e42dcd51c9309e751d43bc949b98f4e31349}" - -IPHONE_IPSW="$(basename "$IPHONE_URL")" -IPHONE_DIR="${IPHONE_IPSW%.ipsw}" -CLOUDOS_IPSW="pcc-base.ipsw" -CLOUDOS_DIR="pcc-base" - -# ── Download ────────────────────────────────────────────────────────── -download() { - local url="$1" out="$2" - if [[ -f "$out" ]]; then - echo "==> Skipping download: '$out' already exists." - else - echo "==> Downloading $out ..." - wget -q --show-progress -O "$out" "$url" --no-check-certificate - fi -} - -download "$IPHONE_URL" "$IPHONE_IPSW" -download "$CLOUDOS_URL" "$CLOUDOS_IPSW" - -# ── Extract ─────────────────────────────────────────────────────────── -extract() { - local zip="$1" dir="$2" - if [[ -d "$dir" ]]; then - echo "==> Skipping extract: '$dir' already exists." - return - fi - echo "==> Extracting $zip ..." - mkdir -p "$dir" - unzip -oq "$zip" -d "$dir" - chmod -R u+w "$dir" -} - -extract "$IPHONE_IPSW" "$IPHONE_DIR" -extract "$CLOUDOS_IPSW" "$CLOUDOS_DIR" - -# ── Merge cloudOS firmware into iPhone restore directory ────────────── -echo "==> Importing cloudOS firmware components ..." - -cp ${CLOUDOS_DIR}/kernelcache.* "$IPHONE_DIR"/ - -for sub in agx all_flash ane dfu pmp; do - cp ${CLOUDOS_DIR}/Firmware/${sub}/* "$IPHONE_DIR/Firmware/${sub}"/ -done - -cp ${CLOUDOS_DIR}/Firmware/*.im4p "$IPHONE_DIR/Firmware"/ - -# ── Generate hybrid BuildManifest.plist & Restore.plist ─────────────── -echo "==> Generating hybrid plists ..." - -python3 - "$IPHONE_DIR" "$CLOUDOS_DIR" <<'PYEOF' -import copy, os, plistlib, sys - -iphone_dir, cloudos_dir = sys.argv[1], sys.argv[2] - -def load(path): - with open(path, "rb") as f: - return plistlib.load(f) - -cloudos_bm = load(os.path.join(cloudos_dir, "BuildManifest.plist")) -iphone_bm = load(os.path.join(iphone_dir, "BuildManifest.plist")) -cloudos_rp = load(os.path.join(cloudos_dir, "Restore.plist")) -iphone_rp = load(os.path.join(iphone_dir, "Restore.plist")) - -# Source identities -# C: [0]j236c [1]j475d [2]vphone600-prod [3]vresearch101-prod [4]vphone600-research [5]vresearch101-research -# I: [0]Erase [1]Upgrade [2]ResearchErase [3]ResearchUpgrade [4]Recovery -C = cloudos_bm["BuildIdentities"] -I = iphone_bm["BuildIdentities"] - -def entry(src, idx, key): - return copy.deepcopy(src[idx]["Manifest"][key]) - -# ── Base identity template (vresearch101) ───────────────────────────── -def make_base(): - b = copy.deepcopy(C[3]) - b["Manifest"] = {} - b["Ap,ProductType"] = "ComputeModule14,2" - b["Ap,Target"] = "VRESEARCH101AP" - b["Ap,TargetType"] = "vresearch101" - b["ApBoardID"] = "0x90" - b["ApChipID"] = "0xFE01" - b["ApSecurityDomain"] = "0x01" - for k in ("NeRDEpoch", "RestoreAttestationMode"): - b.pop(k, None) - b.get("Info", {}).pop(k, None) - b["Info"]["FDRSupport"] = False - b["Info"]["Variant"] = "Darwin Cloud Customer Erase Install (IPSW)" - b["Info"]["VariantContents"] = { - "BasebandFirmware": "Release", "DCP": "DarwinProduction", - "DFU": "DarwinProduction", "Firmware": "DarwinProduction", - "InitiumBaseband": "Production", "InstalledKernelCache": "Production", - "InstalledSPTM": "Production", "OS": "Production", - "RestoreKernelCache": "Production", "RestoreRamDisk": "Production", - "RestoreSEP": "DarwinProduction", "RestoreSPTM": "Production", - "SEP": "DarwinProduction", "VinylFirmware": "Release", - } - return b - -# Shared manifest blocks — cloudOS boot infra -def boot_infra(m, llb_src=3, sep_src=2, boot_variant="release"): - """Add SPTM/TXM/DeviceTree/KernelCache/LLB/iBoot/iBEC/iBSS/SEP entries.""" - research = 4 # cloudOS research identity index - m["Ap,RestoreSecurePageTableMonitor"] = entry(C, 3, "Ap,RestoreSecurePageTableMonitor") - m["Ap,RestoreTrustedExecutionMonitor"] = entry(C, 3, "Ap,RestoreTrustedExecutionMonitor") - m["Ap,SecurePageTableMonitor"] = entry(C, 3, "Ap,SecurePageTableMonitor") - m["Ap,TrustedExecutionMonitor"] = entry(C, research, "Ap,TrustedExecutionMonitor") - m["DeviceTree"] = entry(C, 2, "DeviceTree") - m["KernelCache"] = entry(C, research, "KernelCache") - idx = 3 if boot_variant == "release" else research - m["LLB"] = entry(C, idx, "LLB") - m["iBEC"] = entry(C, idx, "iBEC") - m["iBSS"] = entry(C, idx, "iBSS") - m["iBoot"] = entry(C, research, "iBoot") - m["RecoveryMode"] = entry(I, 0, "RecoveryMode") - m["RestoreDeviceTree"] = entry(C, 2, "RestoreDeviceTree") - m["RestoreKernelCache"] = entry(C, 2, "RestoreKernelCache") - m["RestoreSEP"] = entry(C, sep_src, "RestoreSEP") - m["SEP"] = entry(C, sep_src, "SEP") - -# Shared manifest block — iPhone OS images -def iphone_os(m, os_src=0): - m["Ap,SystemVolumeCanonicalMetadata"] = entry(I, os_src, "Ap,SystemVolumeCanonicalMetadata") - m["OS"] = entry(I, os_src, "OS") - m["StaticTrustCache"] = entry(I, os_src, "StaticTrustCache") - m["SystemVolume"] = entry(I, os_src, "SystemVolume") - -# ── 5 Build Identities ─────────────────────────────────────────────── -def identity_0(): - """Erase — Cryptex1 identity keys, RELEASE LLB/iBEC/iBSS, cloudOS erase ramdisk.""" - bi = make_base() - for k in ("Cryptex1,ChipID", "Cryptex1,NonceDomain", "Cryptex1,PreauthorizationVersion", - "Cryptex1,ProductClass", "Cryptex1,SubType", "Cryptex1,Type", "Cryptex1,Version"): - bi[k] = I[0][k] - bi["Info"]["Cryptex1,AppOSSize"] = I[0]["Info"]["Cryptex1,AppOSSize"] - bi["Info"]["Cryptex1,SystemOSSize"] = I[0]["Info"]["Cryptex1,SystemOSSize"] - bi["Info"]["VariantContents"]["Cryptex1,AppOS"] = "CryptexOne" - bi["Info"]["VariantContents"]["Cryptex1,SystemOS"] = "CryptexOne" - m = bi["Manifest"] - boot_infra(m, llb_src=3, sep_src=2, boot_variant="release") - m["RestoreRamDisk"] = entry(C, 3, "RestoreRamDisk") - m["RestoreTrustCache"] = entry(C, 3, "RestoreTrustCache") - iphone_os(m) - return bi - -def identity_1(): - """Upgrade — Cryptex1 manifest entries, RESEARCH boot chain, iPhone upgrade ramdisk.""" - bi = make_base() - m = bi["Manifest"] - boot_infra(m, llb_src=4, sep_src=3, boot_variant="research") - m["AppleLogo"] = entry(C, 4, "AppleLogo") - m["RestoreLogo"] = entry(C, 4, "RestoreLogo") - for k in ("Cryptex1,AppOS", "Cryptex1,AppTrustCache", "Cryptex1,AppVolume", - "Cryptex1,SystemOS", "Cryptex1,SystemTrustCache", "Cryptex1,SystemVolume"): - m[k] = entry(I, 0, k) - m["RestoreRamDisk"] = entry(I, 1, "RestoreRamDisk") - m["RestoreTrustCache"] = entry(I, 1, "RestoreTrustCache") - iphone_os(m) - return bi - -def identity_2(): - """Research erase — RESEARCH boot chain, cloudOS erase ramdisk, no Cryptex1.""" - bi = make_base() - m = bi["Manifest"] - boot_infra(m, llb_src=4, sep_src=3, boot_variant="research") - m["AppleLogo"] = entry(C, 4, "AppleLogo") - m["RestoreLogo"] = entry(C, 4, "RestoreLogo") - m["RestoreRamDisk"] = entry(C, 3, "RestoreRamDisk") - m["RestoreTrustCache"] = entry(C, 3, "RestoreTrustCache") - iphone_os(m) - return bi - -def identity_3(): - """Research upgrade — same as identity_2 but with iPhone upgrade ramdisk.""" - bi = identity_2() - m = bi["Manifest"] - m["RestoreRamDisk"] = entry(I, 1, "RestoreRamDisk") - m["RestoreTrustCache"] = entry(I, 1, "RestoreTrustCache") - return bi - -def identity_4(): - """Recovery — stripped down, iPhone Recovery OS.""" - bi = make_base() - m = bi["Manifest"] - boot_infra(m, llb_src=4, sep_src=3, boot_variant="research") - # Recovery has no RestoreDeviceTree/RestoreSEP/SEP/RecoveryMode/iBoot - for k in ("RestoreDeviceTree", "RestoreSEP", "SEP", "RecoveryMode", "iBoot"): - m.pop(k, None) - m["AppleLogo"] = entry(C, 4, "AppleLogo") - m["RestoreRamDisk"] = entry(C, 3, "RestoreRamDisk") - m["RestoreTrustCache"] = entry(C, 3, "RestoreTrustCache") - iphone_os(m, os_src=4) - return bi - -# ── Assemble BuildManifest ──────────────────────────────────────────── -build_manifest = { - "BuildIdentities": [identity_0(), identity_1(), identity_2(), identity_3(), identity_4()], - "ManifestVersion": cloudos_bm["ManifestVersion"], - "ProductBuildVersion": cloudos_bm["ProductBuildVersion"], - "ProductVersion": cloudos_bm["ProductVersion"], - "SupportedProductTypes": ["iPhone99,11"], -} - -# ── Assemble Restore.plist ──────────────────────────────────────────── -restore = copy.deepcopy(cloudos_rp) -restore["DeviceMap"] = [iphone_rp["DeviceMap"][0]] + [ - d for d in cloudos_rp["DeviceMap"] if d["BoardConfig"] in ("vphone600ap", "vresearch101ap") -] -restore["SystemRestoreImageFileSystems"] = copy.deepcopy(iphone_rp["SystemRestoreImageFileSystems"]) -restore["SupportedProductTypeIDs"] = { - cat: iphone_rp["SupportedProductTypeIDs"][cat] + cloudos_rp["SupportedProductTypeIDs"][cat] - for cat in ("DFU", "Recovery") -} -restore["SupportedProductTypes"] = ( - iphone_rp.get("SupportedProductTypes", []) + cloudos_rp.get("SupportedProductTypes", []) -) - -# ── Write output ────────────────────────────────────────────────────── -for name, data in [("BuildManifest.plist", build_manifest), ("Restore.plist", restore)]: - path = os.path.join(iphone_dir, name) - with open(path, "wb") as f: - plistlib.dump(data, f, sort_keys=True) - print(f" wrote {name}") -PYEOF - -# ── Cleanup (keep IPSWs, remove intermediate files) ────────────────── -echo "==> Cleaning up ..." -rm -rf "$CLOUDOS_DIR" - -echo "==> Done. Restore directory ready: $IPHONE_DIR/" diff --git a/boot.sh b/boot.sh deleted file mode 100755 index ac6838c..0000000 --- a/boot.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/zsh - -set -euo pipefail -cd "$(dirname "$0")" - -./build_and_sign.sh - -./.build/release/vphone-cli \ - --rom ./VM/AVPBooter.vresearch1.bin \ - --disk ./VM/Disk.img \ - --nvram ./VM/nvram.bin \ - --cpu 4 \ - --memory 4096 \ - --serial-log ./VM/serial.log \ - --stop-on-panic \ - --stop-on-fatal-error \ - --sep-rom ./VM/AVPSEPBooter.vresearch1.bin \ - --sep-storage ./VM/SEPStorage \ - --no-graphics diff --git a/boot_dfu.sh b/boot_dfu.sh deleted file mode 100755 index b6b6a56..0000000 --- a/boot_dfu.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/zsh - -set -euo pipefail -cd "$(dirname "$0")" - -./build_and_sign.sh - -./.build/release/vphone-cli \ - --rom ./VM/AVPBooter.vresearch1.bin \ - --disk ./VM/Disk.img \ - --nvram ./VM/nvram.bin \ - --cpu 4 \ - --memory 4096 \ - --serial-log ./VM/serial.log \ - --stop-on-panic \ - --stop-on-fatal-error \ - --sep-rom ./VM/AVPSEPBooter.vresearch1.bin \ - --sep-storage ./VM/SEPStorage \ - --no-graphics \ - --dfu diff --git a/boot_sweet.sh b/boot_sweet.sh deleted file mode 100755 index 0d0d18d..0000000 --- a/boot_sweet.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/zsh - -set -euo pipefail -cd "$(dirname "$0")" - -IPROXY_PIDS=() - -cleanup() { - for pid in "${IPROXY_PIDS[@]}"; do - kill "$pid" 2>/dev/null && wait "$pid" 2>/dev/null - done -} -trap cleanup EXIT INT TERM HUP - -iproxy 22222:22 & -IPROXY_PIDS+=($!) -iproxy 5901:5901 & -IPROXY_PIDS+=($!) - -echo "iproxy started: 22222->22, 5901->5901 (pids: ${IPROXY_PIDS[*]})" - -./vphone-cli \ - --rom ./contents/AVPBooter.vresearch1.bin \ - --disk ./contents/Disk.img \ - --nvram ./contents/nvram.bin \ - --cpu 4 \ - --memory 4096 \ - --stop-on-panic \ - --stop-on-fatal-error \ - --sep-rom ./contents/AVPSEPBooter.vresearch1.bin \ - --sep-storage ./contents/SEPStorage \ - --no-graphics - diff --git a/build_and_sign.sh b/build_and_sign.sh deleted file mode 100755 index 35ad2ed..0000000 --- a/build_and_sign.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/zsh -# build_and_sign.sh — Build vphone-cli and sign with private entitlements. -# -# Requires: SIP/AMFI disabled (amfi_get_out_of_my_way=1) -# -# Usage: -# zsh build_and_sign.sh # build + sign -# zsh build_and_sign.sh --install # also copy to ../bin/vphone-cli -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BINARY="${SCRIPT_DIR}/.build/release/vphone-cli" -ENTITLEMENTS="${SCRIPT_DIR}/vphone.entitlements" - -print "=== Building vphone-cli ===" -cd "${SCRIPT_DIR}" -swift build -c release 2>&1 | tail -5 - -print "" -print "=== Signing with entitlements ===" -print " entitlements: ${ENTITLEMENTS}" -codesign --force --sign - --entitlements "${ENTITLEMENTS}" "${BINARY}" -print " signed OK" - -# Verify entitlements -print "" -print "=== Entitlement verification ===" -codesign -d --entitlements - "${BINARY}" 2>/dev/null | head -20 - -print "" -print "=== Binary ===" -ls -lh "${BINARY}" - -if [[ "${1:-}" == "--install" ]]; then - REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - mkdir -p "${REPO_ROOT}/bin" - cp -f "${BINARY}" "${REPO_ROOT}/bin/vphone-cli" - print "" - print "Installed to ${REPO_ROOT}/bin/vphone-cli" -fi - -print "" -print "Done. Run with:" -print " ${BINARY} --rom --disk --serial" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f97f56b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +capstone +keystone-engine +pyimg4 diff --git a/researchs/binary_patches_kernelcache.md b/researchs/binary_patches_kernelcache.md new file mode 100644 index 0000000..a22fc6f --- /dev/null +++ b/researchs/binary_patches_kernelcache.md @@ -0,0 +1,121 @@ +# Binary Kernelcache Patch Verification Report + +Date: 2026-02-27 + +## Scope +Verify that the dynamic kernel patch finder (`Scripts/kernel_patcher.py`) produces +the same binary result as the legacy hardcoded patch list on vphone600, then +apply the dynamic patcher to a freshly extracted vresearch101 kernelcache. + +## Inputs +- Original vphone600 raw kernel: `/tmp/kc_vphone600_orig.raw` +- vphone600 upstream hardcoded patch list: `super-tart-vphone-private/CFW/patch_fw.py` +- Dynamic patcher: `Scripts/kernel_patcher.py` +- VM kernelcache image (vresearch101): `VM/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vresearch101` + +## Method +1. Apply legacy hardcoded patches to `/tmp/kc_vphone600_orig.raw` using binary + replacement (32-bit writes) and save as `/tmp/kc_vphone600_upstream.raw`. +2. Run `KernelPatcher.find_all()` on `/tmp/kc_vphone600_orig.raw`, apply all + dynamic patches, and save as `/tmp/kc_vphone600_dynamic.raw`. +3. Compare the two patched binaries with `cmp -l`. +4. Re-extract a clean vresearch101 kernelcache using `pyimg4 im4p extract`, save + as `/tmp/kc_vresearch1_orig.raw`. +5. Run the dynamic patcher on `/tmp/kc_vresearch1_orig.raw`, save as + `/tmp/kc_vresearch1_dynamic.raw`. + +## Outputs +- `/tmp/kc_vphone600_upstream.raw` +- `/tmp/kc_vphone600_dynamic.raw` +- `/tmp/kc_vresearch1_orig.raw` +- `/tmp/kc_vresearch1_dynamic.raw` + +## Checksums (SHA-256) +- `/tmp/kc_vphone600_orig.raw`: + `b6846048f3a60eab5f360fcc0f3dcb5198aa0476c86fb06eb42f6267cdbfcae0` +- `/tmp/kc_vphone600_upstream.raw`: + `373e016d34ae5a2d8ba7ba96c920f4f6700dea503e3689d06a99e90ebec701c8` +- `/tmp/kc_vphone600_dynamic.raw`: + `373e016d34ae5a2d8ba7ba96c920f4f6700dea503e3689d06a99e90ebec701c8` +- `/tmp/kc_vresearch1_orig.raw`: + `c673c9b8226ea774d1d935427760e2e9a48200fd1daf0ef584dc88df0dccefde` +- `/tmp/kc_vresearch1_dynamic.raw`: + `f36a78ce59c658df85ecdead56d46370a1107181689091cf798e529664f6e2b5` + +## vphone600: Hardcoded vs Dynamic +Result: **byte-identical** output between hardcoded and dynamic patching. + +- `KernelPatcher` patches found: 25 +- Hardcoded patches applied: 25 +- `cmp -l /tmp/kc_vphone600_upstream.raw /tmp/kc_vphone600_dynamic.raw`: + no output (files identical) + +### Hardcoded Patch List (vphone600) +Offsets and 32-bit patch values, taken from `patch_fw.py`: + +| # | Offset (hex) | Patch value | Purpose | +|---|-------------:|------------:|---------| +| 1 | 0x2476964 | 0xD503201F | _apfs_vfsop_mount root snapshot NOP | +| 2 | 0x23CFDE4 | 0xD503201F | _authapfs_seal_is_broken NOP | +| 3 | 0x00F6D960 | 0xD503201F | _bsd_init rootvp NOP | +| 4 | 0x163863C | 0x52800000 | _proc_check_launch_constraints mov w0,#0 | +| 5 | 0x1638640 | 0xD65F03C0 | _proc_check_launch_constraints ret | +| 6 | 0x12C8138 | 0xD2800020 | _PE_i_can_has_debugger mov x0,#1 | +| 7 | 0x12C813C | 0xD65F03C0 | _PE_i_can_has_debugger ret | +| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) | +| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 | +| 10 | 0x16410BC | 0x52800020 | _check_dyld_policy_internal mov w0,#1 (1) | +| 11 | 0x16410C8 | 0x52800020 | _check_dyld_policy_internal mov w0,#1 (2) | +| 12 | 0x242011C | 0x52800000 | _apfs_graft mov w0,#0 | +| 13 | 0x2475044 | 0xEB00001F | _apfs_vfsop_mount cmp x0,x0 | +| 14 | 0x2476C00 | 0x52800000 | _apfs_mount_upgrade_checks mov w0,#0 | +| 15 | 0x248C800 | 0x52800000 | _handle_fsioc_graft mov w0,#0 | +| 16 | 0x23AC528 | 0xD2800000 | _hook_file_check_mmap mov x0,#0 | +| 17 | 0x23AC52C | 0xD65F03C0 | _hook_file_check_mmap ret | +| 18 | 0x23AAB58 | 0xD2800000 | _hook_mount_check_mount mov x0,#0 | +| 19 | 0x23AAB5C | 0xD65F03C0 | _hook_mount_check_mount ret | +| 20 | 0x23AA9A0 | 0xD2800000 | _hook_mount_check_remount mov x0,#0 | +| 21 | 0x23AA9A4 | 0xD65F03C0 | _hook_mount_check_remount ret | +| 22 | 0x23AA80C | 0xD2800000 | _hook_mount_check_umount mov x0,#0 | +| 23 | 0x23AA810 | 0xD65F03C0 | _hook_mount_check_umount ret | +| 24 | 0x23A5514 | 0xD2800000 | _hook_vnode_check_rename mov x0,#0 | +| 25 | 0x23A5518 | 0xD65F03C0 | _hook_vnode_check_rename ret | + +## TXM Patch Details +Dynamic patcher locates the `"TXM [Error]: CodeSignature"` string, finds the +following `tbnz` in the log/error path, and NOPs it. + +### vphone600 disassembly around the patch (0xFFAB98) +Before: +``` +0x00FFAB90: mov w0, #5 +0x00FFAB94: ldrb w8, [x19, #6] +0x00FFAB98: tbnz w8, #0, #0xffac80 +0x00FFAB9C: ldp x29, x30, [sp, #0x100] +``` +After: +``` +0x00FFAB90: mov w0, #5 +0x00FFAB94: ldrb w8, [x19, #6] +0x00FFAB98: nop +0x00FFAB9C: ldp x29, x30, [sp, #0x100] +``` + +## vresearch101: Dynamic Patch Run +Extraction: +``` +pyimg4 im4p extract \ + -i VM/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vresearch101 \ + -o /tmp/kc_vresearch1_orig.raw +``` + +Dynamic patcher results: +- Patches found/applied: 25 +- TXM patch location: `0xFA6B98` (NOP `tbnz w8, #0, #0xfa6c80`) +- Patched output: `/tmp/kc_vresearch1_dynamic.raw` + +## Conclusion +For vphone600, the dynamic patcher output is byte-identical to the legacy +hardcoded patch list, indicating functional equivalence on this kernelcache. +The same dynamic patcher also successfully patches the freshly extracted +vresearch101 kernelcache with the expected TXM NOP and a full 25-patch set. diff --git a/researchs/build_manifest.md b/researchs/build_manifest.md new file mode 100644 index 0000000..ce3ebe9 --- /dev/null +++ b/researchs/build_manifest.md @@ -0,0 +1,178 @@ + +# BuildManifest.plist Research + +## 1. Multi-Source Comparison + +### Identity Count Overview + +| Source | Identities | DeviceClasses | +|--------|-----------|---------------| +| iPhone 26.1 | 5 | All d47ap | +| iPhone 26.3 | 5 | All d47ap | +| CloudOS 26.1 | 6 | j236cap, j475dap, vphone600ap (x2), vresearch101ap (x2) | +| KnownWork 26.1 | 5 | All vresearch101ap | + +### CloudOS 26.1 Identity Structure (6 identities) + +| Index | DeviceClass | Variant | BuildStyle | Manifest Keys | +|-------|-------------|---------|------------|---------------| +| [0] | j236cap | Darwin Cloud Customer Erase Install (IPSW) | RELEASE build | 37 keys (server hardware) | +| [1] | j475dap | Darwin Cloud Customer Erase Install (IPSW) | unknown (no path) | 0 keys (empty placeholder) | +| [2] | vphone600ap | Darwin Cloud Customer Erase Install (IPSW) | RELEASE build | 29 keys (includes UI assets) | +| [3] | vresearch101ap | Darwin Cloud Customer Erase Install (IPSW) | RELEASE build | 20 keys (no UI assets) | +| [4] | vphone600ap | Research Darwin Cloud Customer Erase Install (IPSW) | RESEARCH_RELEASE build | 29 keys (research kernel) | +| [5] | vresearch101ap | Research Darwin Cloud Customer Erase Install (IPSW) | RESEARCH_RELEASE build | 20 keys (research kernel) | + +Key distinctions: +- CloudOS[2] vs [4] (vphone600ap): [2] uses RELEASE boot chain + release kernelcache; [4] uses RESEARCH_RELEASE + research kernelcache + txm.iphoneos.research.im4p +- CloudOS[3] vs [5] (vresearch101ap): Same pattern — [3] is RELEASE, [5] is RESEARCH_RELEASE +- **vphone600ap has components vresearch101ap lacks**: RecoveryMode, AppleLogo, Battery*, RestoreLogo, SEP (vphone600 variant) +- vresearch101ap has only 20 manifest keys (no UI assets, no RecoveryMode) + +### vphone600ap vs vresearch101ap Key Differences + +| Property | vphone600ap | vresearch101ap | +|----------|-------------|----------------| +| Ap,ProductType | iPhone99,11 | ComputeModule14,2 | +| Ap,Target | VPHONE600AP | VRESEARCH101AP | +| ApBoardID | 0x91 | 0x90 | +| DeviceTree | DeviceTree.vphone600ap.im4p | DeviceTree.vresearch101ap.im4p | +| SEP | sep-firmware.vphone600.RELEASE.im4p | sep-firmware.vresearch101.RELEASE.im4p | +| RecoveryMode | recoverymode@2556~iphone-USBc.im4p | **NOT PRESENT** | +| MKB dt flag | dt=1 (keybag-less boot OK) | dt=0 (fatal keybag error) | + +--- + +## 2. Component Source Tracing (Corrected) + +### Hybrid Identity: vresearch101 boot chain + vphone600 runtime + +The working configuration mixes components from both board configs: + +| Component | Source Identity | File | Why This Source | +|-----------|---------------|------|-----------------| +| LLB | PROD (vresearch101 release) | `LLB.vresearch101.RELEASE.im4p` | Matches DFU hardware (BDID 0x90) | +| iBSS | PROD | `iBSS.vresearch101.RELEASE.im4p` | Matches DFU hardware | +| iBEC | PROD | `iBEC.vresearch101.RELEASE.im4p` | Matches DFU hardware | +| iBoot | RES (vresearch101 research) | `iBoot.vresearch101.RESEARCH_RELEASE.im4p` | Only research identity has iBoot | +| SPTM (all) | PROD | `sptm.vresearch1.release.im4p` | Shared across board configs | +| TXM restore | PROD | `txm.iphoneos.release.im4p` | RELEASE for restore | +| TXM installed | RES | `txm.iphoneos.research.im4p` | Research variant, patched | +| **DeviceTree** | **VP (vphone600 release)** | `DeviceTree.vphone600ap.im4p` | Sets MKB dt=1 | +| **SEP/RestoreSEP** | **VP** | `sep-firmware.vphone600.RELEASE.im4p` | Must match device tree | +| **KernelCache** | **VPR (vphone600 research)** | `kernelcache.research.vphone600` | Patched by fw_patch.py | +| **RestoreKernelCache** | **VP (vphone600 release)** | `kernelcache.release.vphone600` | Unpatched, restore-time only | +| **RecoveryMode** | **VP** | `recoverymode@2556~iphone-USBc.im4p` | Only vphone600ap has it | +| RestoreRamDisk | PROD | cloudOS erase ramdisk | PCC restore ramdisk | +| OS / SVC / etc. | I_ERASE (iPhone) | iPhone OS image | iPhone system | + +### Why Not All-vresearch101 or All-vphone600? + +**Problem with all-vresearch101**: The vresearch101ap device tree sets MKB `dt=0`, +causing `MKB_INIT: FATAL KEYBAG ERROR` on first boot (no system keybag exists yet). +Also missing RecoveryMode entry. + +**Problem with all-vphone600**: The DFU hardware identifies as BDID 0x90 +(vresearch101ap). Using vphone600ap identity (BDID 0x91) fails TSS/SHSH signing +and idevicerestore identity matching (`Unable to find a matching build identity`). + +**Solution**: vresearch101ap identity fields for DFU/TSS + vphone600 runtime +components for a working boot environment. + +--- + +## 3. idevicerestore Identity Selection Logic + +Source: `idevicerestore/src/idevicerestore.c` lines 2195-2242 + +### Matching Algorithm + +idevicerestore selects a Build Identity by iterating through all `BuildIdentities` and returning the **first match** based on two fields: + +1. **`Info.DeviceClass`** — case-insensitive match against device `hardware_model` +2. **`Info.Variant`** — substring match against the requested variant string + +For DFU erase restore, the search variant is `"Erase Install (IPSW)"` (defined in `idevicerestore.h`). + +### Matching Modes + +```c +// Exact match +if (strcmp(str, variant) == 0) return ident; + +// Partial match (when exact=0) +if (strstr(str, variant) && !strstr(str, "Research")) return ident; +``` + +**Critical**: Partial matching **excludes** variants containing `"Research"`. This means: +- `"Darwin Cloud Customer Erase Install (IPSW)"` — matches (contains "Erase Install (IPSW)", no "Research") +- `"Research Darwin Cloud Customer Erase Install (IPSW)"` — skipped (contains "Research") + +### What idevicerestore Does NOT Check +- ApBoardID / ApChipID (used after selection, not for matching) +- Identity index or count (no hardcoded indices) + +### Conclusion for Single Identity + +A BuildManifest with **one identity** works fine. The loop iterates once, and if +DeviceClass and Variant match, it's returned. No minimum identity count required. + +--- + +## 4. TSS/SHSH Signing + +The TSS request sent to `gs.apple.com` includes: +- `ApBoardID = 144` (0x90) — must match vresearch101ap +- `ApChipID = 65025` (0xFE01) +- `Ap,ProductType = ComputeModule14,2` +- `Ap,Target = VRESEARCH101AP` +- Digests for all 21 manifest components + +Apple's TSS server signs based on these identity fields + component digests. +Using vphone600ap identity (BDID 0x91) would fail because the DFU device +reports BDID 0x90. + +--- + +## 5. Final Design: Single DFU Erase Identity + +### Identity Metadata (fw_manifest.py) +``` +DeviceClass = vresearch101ap (from C[PROD] deep copy) +Variant = Darwin Cloud Customer Erase Install (IPSW) +Ap,ProductType = ComputeModule14,2 +Ap,Target = VRESEARCH101AP +Ap,TargetType = vresearch101 +ApBoardID = 0x90 +ApChipID = 0xFE01 +FDRSupport = False +``` + +### Source Variable Map +``` +PROD = C[vresearch101ap release] — boot chain, SPTM, ramdisk +RES = C[vresearch101ap research] — iBoot, TXM research +VP = C[vphone600ap release] — DeviceTree, SEP, RestoreKernelCache, RecoveryMode +VPR = C[vphone600ap research] — KernelCache (patched by fw_patch.py) +I_ERASE = I[iPhone erase] — OS, trust caches, system volume +``` + +### All 21 Manifest Entries +``` +Boot chain (PROD): LLB, iBSS, iBEC +Research iBoot (RES): iBoot +Security monitors (PROD): Ap,RestoreSPTM, Ap,RestoreTXM, Ap,SPTM +Research TXM (RES): Ap,TXM +Device tree (VP): DeviceTree, RestoreDeviceTree +SEP (VP): SEP, RestoreSEP +Kernel (VPR/VP): KernelCache (research), RestoreKernelCache (release) +Recovery (VP): RecoveryMode +Ramdisk (PROD): RestoreRamDisk, RestoreTrustCache +iPhone OS (I_ERASE): OS, StaticTrustCache, SystemVolume, Ap,SVC Metadata +``` + +### Restore.plist +``` +DeviceMap: [d47ap (iPhone), vphone600ap, vresearch101ap] +ProductTypes: [iPhone17,3, ComputeModule14,1, ComputeModule14,2, Mac14,14, iPhone99,11] +``` diff --git a/researchs/erase_install_component_origins.md b/researchs/erase_install_component_origins.md new file mode 100644 index 0000000..f6cd6af --- /dev/null +++ b/researchs/erase_install_component_origins.md @@ -0,0 +1,179 @@ +# Erase Install — Component Origins + +The erase install firmware is a **hybrid** of three source sets: + +1. **PCC vresearch101ap** — boot chain (LLB/iBSS/iBEC/iBoot) and security monitors (SPTM/TXM) +2. **PCC vphone600ap** — runtime components (DeviceTree, SEP, KernelCache, RecoveryMode) +3. **iPhone 17,3** — OS image, trust caches, filesystem + +The VM hardware identifies as **vresearch101ap** (BDID 0x90) in DFU mode, so the +BuildManifest identity must use vresearch101ap fields for TSS/SHSH signing. However, +runtime components use the **vphone600** variant because: +- Its DeviceTree sets MKB `dt=1` (allows boot without system keybag) +- Its SEP firmware matches the vphone600 device tree +- `hardware target` reports as `vphone600ap` → proper iPhone emulation + +`fw_prepare.sh` downloads both IPSWs, merges cloudOS firmware into the iPhone +restore directory, then `fw_manifest.py` generates the hybrid BuildManifest. + +--- + +## Component Source Table + +### Boot Chain (from PCC vresearch101ap) + +| Component | Source Identity | File | Patches Applied | +|-----------|---------------|------|-----------------| +| **AVPBooter** | PCC vresearch1 | `AVPBooter*.bin` (vm dir) | DGST validation bypass (`mov x0, #0`) | +| **iBSS** | PROD (vresearch101ap release) | `Firmware/dfu/iBSS.vresearch101.RELEASE.im4p` | Serial labels + image4 callback bypass | +| **iBEC** | PROD (vresearch101ap release) | `Firmware/dfu/iBEC.vresearch101.RELEASE.im4p` | Serial labels + image4 callback + boot-args | +| **LLB** | PROD (vresearch101ap release) | `Firmware/all_flash/LLB.vresearch101.RELEASE.im4p` | Serial labels + image4 callback + boot-args + rootfs + panic (6 patches) | +| **iBoot** | RES (vresearch101ap research) | `Firmware/all_flash/iBoot.vresearch101.RESEARCH_RELEASE.im4p` | Not patched (only research identity carries iBoot) | + +### Security Monitors (from PCC, shared across board configs) + +| Component | Source Identity | File | Patches Applied | +|-----------|---------------|------|-----------------| +| **Ap,RestoreSecurePageTableMonitor** | PROD | `Firmware/sptm.vresearch1.release.im4p` | Not patched | +| **Ap,RestoreTrustedExecutionMonitor** | PROD | `Firmware/txm.iphoneos.release.im4p` | Not patched | +| **Ap,SecurePageTableMonitor** | PROD | `Firmware/sptm.vresearch1.release.im4p` | Not patched | +| **Ap,TrustedExecutionMonitor** | RES (research) | `Firmware/txm.iphoneos.research.im4p` | Trustcache bypass (`mov x0, #0` at 0x2C1F8) | + +### Runtime Components (from PCC vphone600ap) + +| Component | Source Identity | File | Patches Applied | +|-----------|---------------|------|-----------------| +| **DeviceTree** | VP (vphone600ap release) | `Firmware/all_flash/DeviceTree.vphone600ap.im4p` | Not patched | +| **RestoreDeviceTree** | VP | `Firmware/all_flash/DeviceTree.vphone600ap.im4p` | Not patched | +| **SEP** | VP | `Firmware/all_flash/sep-firmware.vphone600.RELEASE.im4p` | Not patched | +| **RestoreSEP** | VP | `Firmware/all_flash/sep-firmware.vphone600.RELEASE.im4p` | Not patched | +| **KernelCache** | VPR (vphone600ap research) | `kernelcache.research.vphone600` | 25 dynamic patches via KernelPatcher | +| **RestoreKernelCache** | VP (vphone600ap release) | `kernelcache.release.vphone600` | Not patched (used during restore only) | +| **RecoveryMode** | VP | `Firmware/all_flash/recoverymode@2556~iphone-USBc.im4p` | Not patched | + +> **Important**: KernelCache (installed to disk, patched) uses the **research** variant. +> RestoreKernelCache (used during restore process only) uses the **release** variant. +> Only vphone600ap identities carry RecoveryMode — vresearch101ap does not. + +### OS / Filesystem (from iPhone) + +| Component | Source | Notes | +|-----------|--------|-------| +| **OS** | iPhone `iPhone17,3` erase identity | iPhone OS image | +| **SystemVolume** | iPhone erase | Root hash | +| **StaticTrustCache** | iPhone erase | Static trust cache | +| **Ap,SystemVolumeCanonicalMetadata** | iPhone erase | Metadata / mtree | + +### Ramdisk (from PCC) + +| Component | Source | Notes | +|-----------|--------|-------| +| **RestoreRamDisk** | PROD (vresearch101ap release) | CloudOS erase ramdisk | +| **RestoreTrustCache** | PROD | Ramdisk trust cache | + +--- + +## Patched Components Summary + +All 6 patched components in `fw_patch.py` come from **PCC (cloudOS)**: + +| # | Component | Source Board | Patch Count | Purpose | +|---|-----------|-------------|-------------|---------| +| 1 | AVPBooter | vresearch1 | 1 | Bypass DGST signature validation | +| 2 | iBSS | vresearch101 | 2 | Enable serial output + bypass image4 verification | +| 3 | iBEC | vresearch101 | 3 | Enable serial + bypass image4 + inject boot-args | +| 4 | LLB | vresearch101 | 6 | Serial + image4 + boot-args + rootfs mount + panic handler | +| 5 | TXM | shared (iphoneos) | 1 | Bypass trustcache validation | +| 6 | KernelCache | vphone600 | 25 | APFS seal, MAC policy, debugger, launch constraints, etc. | + +All 4 CFW-patched binaries in `patch_cfw.py` / `install_cfw.sh` come from **iPhone**: + +| # | Binary | Source | Purpose | +|---|--------|--------|---------| +| 1 | seputil | iPhone (Cryptex SystemOS) | Gigalocker UUID patch (`/%s.gl` → `/AA.gl`) | +| 2 | launchd_cache_loader | iPhone (Cryptex SystemOS) | NOP cache validation check | +| 3 | mobileactivationd | iPhone (Cryptex SystemOS) | Force `should_hactivate` to return true | +| 4 | launchd.plist | iPhone (Cryptex SystemOS) | Inject bash/dropbear/trollvnc daemons | + +--- + +## Why vphone600 Runtime Components? + +The vresearch101ap device tree causes a **fatal keybag error** during boot: +``` +MKB_INIT: dt = 0, bootarg = 0 +MKB_INIT: FATAL KEYBAG ERROR: failed to load system bag +REBOOTING INTO RECOVERY MODE. +``` + +The vphone600ap device tree sets `dt=1`, allowing boot without a pre-existing +system keybag: +``` +MKB_INIT: dt = 1, bootarg = 0 +MKB_INIT: No system keybag loaded. +``` + +The SEP firmware must match the device tree (vphone600 SEP with vphone600 DT). + +--- + +## Build Identity (Single DFU Erase) + +Since vphone-cli always boots via DFU restore, only one Build Identity is needed. + +### Identity Metadata (must match DFU hardware for TSS) +``` +DeviceClass = vresearch101ap +Variant = Darwin Cloud Customer Erase Install (IPSW) +Ap,ProductType = ComputeModule14,2 +Ap,Target = VRESEARCH101AP +Ap,TargetType = vresearch101 +ApBoardID = 0x90 +ApChipID = 0xFE01 +FDRSupport = False +``` + +### Identity Source Map (fw_manifest.py variables) +``` +PROD = vresearch101ap release — boot chain, SPTM, ramdisk +RES = vresearch101ap research — iBoot, TXM (research) +VP = vphone600ap release — DeviceTree, SEP, RestoreKernelCache, RecoveryMode +VPR = vphone600ap research — KernelCache (research, patched by fw_patch.py) +I_ERASE = iPhone erase identity — OS image, trust caches, system volume +``` + +### Manifest Components (21 total) +``` +LLB ← PROD +iBSS ← PROD +iBEC ← PROD +iBoot ← RES +Ap,RestoreSecurePageTableMonitor ← PROD +Ap,RestoreTrustedExecutionMonitor← PROD +Ap,SecurePageTableMonitor ← PROD +Ap,TrustedExecutionMonitor ← RES +DeviceTree ← VP +RestoreDeviceTree ← VP +SEP ← VP +RestoreSEP ← VP +KernelCache ← VPR (research, patched) +RestoreKernelCache ← VP (release, unpatched) +RecoveryMode ← VP +RestoreRamDisk ← PROD +RestoreTrustCache ← PROD +Ap,SystemVolumeCanonicalMetadata ← I_ERASE +OS ← I_ERASE +StaticTrustCache ← I_ERASE +SystemVolume ← I_ERASE +``` + +--- + +## TL;DR + +**Boot chain = vresearch101 (matches DFU hardware); runtime = vphone600 (keybag-less boot); OS = iPhone.** + +The firmware is a PCC shell wrapping an iPhone core. The vresearch101 boot chain +handles DFU/TSS signing. The vphone600 device tree + SEP + kernel provide the +runtime environment. The iPhone userland is patched post-install for activation +bypass, jailbreak tools, and persistent SSH/VNC. diff --git a/Scripts/install_cfw.sh b/scripts/cfw_install.sh similarity index 86% rename from Scripts/install_cfw.sh rename to scripts/cfw_install.sh index 6014da2..5d6df27 100755 --- a/Scripts/install_cfw.sh +++ b/scripts/cfw_install.sh @@ -1,5 +1,5 @@ #!/bin/zsh -# install_cfw.sh — Install CFW modifications on vphone via SSH ramdisk. +# cfw_install.sh — Install CFW modifications on vphone via SSH ramdisk. # # Installs Cryptexes, patches system binaries, installs jailbreak tools # and configures LaunchDaemons for persistent SSH/VNC access. @@ -8,13 +8,13 @@ # keeps decrypted Cryptex DMGs cached, handles already-mounted filesystems. # # Prerequisites: -# - Device booted into SSH ramdisk (ramdisk_send.sh) +# - Device booted into SSH ramdisk (make ramdisk_send) # - `ipsw` tool installed (brew install blacktop/tap/ipsw) # - `aea` tool available (macOS 12+) -# - Python: pip install capstone keystone-engine -# - cfw_input/ or cfw_input.tar.zst present +# - Python: make setup_venv && source .venv/bin/activate +# - cfw_input/ or resources/cfw_input.tar.zst present # -# Usage: ./install_cfw.sh [vm_directory] +# Usage: make cfw_install set -euo pipefail VM_DIR="${1:-.}" @@ -96,7 +96,7 @@ find_restore_dir() { setup_cfw_input() { [[ -d "$VM_DIR/$CFW_INPUT" ]] && return local archive - for search_dir in "$SCRIPT_DIR" "$VM_DIR"; do + for search_dir in "$SCRIPT_DIR/resources" "$SCRIPT_DIR" "$VM_DIR"; do archive="$search_dir/$CFW_ARCHIVE" if [[ -f "$archive" ]]; then echo " Extracting $CFW_ARCHIVE..." @@ -126,7 +126,7 @@ trap cleanup_on_exit EXIT # ════════════════════════════════════════════════════════════════ # Main # ════════════════════════════════════════════════════════════════ -echo "[*] install_cfw.sh — Installing CFW on vphone..." +echo "[*] cfw_install.sh — Installing CFW on vphone..." check_prereqs @@ -141,8 +141,8 @@ mkdir -p "$TEMP_DIR" # ── Parse Cryptex paths from BuildManifest ───────────────────── echo "" -echo "[*] Parsing BuildManifest for Cryptex paths..." -CRYPTEX_PATHS=$(python3 "$SCRIPT_DIR/patch_cfw.py" cryptex-paths "$RESTORE_DIR/BuildManifest.plist") +echo "[*] Parsing iPhone BuildManifest for Cryptex paths..." +CRYPTEX_PATHS=$(python3 "$SCRIPT_DIR/patchers/cfw.py" cryptex-paths "$RESTORE_DIR/BuildManifest-iPhone.plist") CRYPTEX_SYSOS=$(echo "$CRYPTEX_PATHS" | head -1) CRYPTEX_APPOS=$(echo "$CRYPTEX_PATHS" | tail -1) echo " SystemOS: $CRYPTEX_SYSOS" @@ -189,6 +189,29 @@ sudo hdiutil attach -mountpoint "$MNT_APPOS" "$APPOS_DMG" -owners off echo " Mounting device rootfs rw..." remote_mount /dev/disk1s1 /mnt1 +# Rename APFS update snapshot to orig-fs (idempotent) +echo " Checking APFS snapshots..." +SNAP_LIST=$(ssh_cmd "snaputil -l /mnt1 2>/dev/null" || true) +if echo "$SNAP_LIST" | grep -q "^orig-fs$"; then + echo " Snapshot 'orig-fs' already exists, skipping rename" +else + UPDATE_SNAP=$(echo "$SNAP_LIST" | grep "^com\.apple\.os\.update-" | head -1) + if [[ -n "$UPDATE_SNAP" ]]; then + echo " Renaming snapshot: $UPDATE_SNAP -> orig-fs" + ssh_cmd "snaputil -n '$UPDATE_SNAP' orig-fs /mnt1" + # Verify rename succeeded + if ! ssh_cmd "snaputil -l /mnt1 2>/dev/null" | grep -q "^orig-fs$"; then + die "Failed to rename snapshot to orig-fs" + fi + echo " Snapshot renamed, remounting..." + ssh_cmd "/sbin/umount /mnt1" + remote_mount /dev/disk1s1 /mnt1 + echo " [+] Snapshot renamed to orig-fs" + else + echo " No com.apple.os.update- snapshot found, skipping" + fi +fi + ssh_cmd "/bin/rm -rf /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" ssh_cmd "/bin/mkdir -p /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" ssh_cmd "/bin/chmod 0755 /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" @@ -223,7 +246,7 @@ if ! remote_file_exists "/mnt1/usr/libexec/seputil.bak"; then fi scp_from "/mnt1/usr/libexec/seputil.bak" "$TEMP_DIR/seputil" -python3 "$SCRIPT_DIR/patch_cfw.py" patch-seputil "$TEMP_DIR/seputil" +python3 "$SCRIPT_DIR/patchers/cfw.py" patch-seputil "$TEMP_DIR/seputil" ldid_sign "$TEMP_DIR/seputil" "com.apple.seputil" scp_to "$TEMP_DIR/seputil" "/mnt1/usr/libexec/seputil" ssh_cmd "/bin/chmod 0755 /mnt1/usr/libexec/seputil" @@ -279,7 +302,7 @@ if ! remote_file_exists "/mnt1/usr/libexec/launchd_cache_loader.bak"; then fi scp_from "/mnt1/usr/libexec/launchd_cache_loader.bak" "$TEMP_DIR/launchd_cache_loader" -python3 "$SCRIPT_DIR/patch_cfw.py" patch-launchd-cache-loader "$TEMP_DIR/launchd_cache_loader" +python3 "$SCRIPT_DIR/patchers/cfw.py" patch-launchd-cache-loader "$TEMP_DIR/launchd_cache_loader" ldid_sign "$TEMP_DIR/launchd_cache_loader" "com.apple.launchd_cache_loader" scp_to "$TEMP_DIR/launchd_cache_loader" "/mnt1/usr/libexec/launchd_cache_loader" ssh_cmd "/bin/chmod 0755 /mnt1/usr/libexec/launchd_cache_loader" @@ -297,7 +320,7 @@ if ! remote_file_exists "/mnt1/usr/libexec/mobileactivationd.bak"; then fi scp_from "/mnt1/usr/libexec/mobileactivationd.bak" "$TEMP_DIR/mobileactivationd" -python3 "$SCRIPT_DIR/patch_cfw.py" patch-mobileactivationd "$TEMP_DIR/mobileactivationd" +python3 "$SCRIPT_DIR/patchers/cfw.py" patch-mobileactivationd "$TEMP_DIR/mobileactivationd" ldid_sign "$TEMP_DIR/mobileactivationd" scp_to "$TEMP_DIR/mobileactivationd" "/mnt1/usr/libexec/mobileactivationd" ssh_cmd "/bin/chmod 0755 /mnt1/usr/libexec/mobileactivationd" @@ -322,7 +345,7 @@ if ! remote_file_exists "/mnt1/System/Library/xpc/launchd.plist.bak"; then fi scp_from "/mnt1/System/Library/xpc/launchd.plist.bak" "$TEMP_DIR/launchd.plist" -python3 "$SCRIPT_DIR/patch_cfw.py" inject-daemons "$TEMP_DIR/launchd.plist" "$INPUT_DIR/jb/LaunchDaemons" +python3 "$SCRIPT_DIR/patchers/cfw.py" inject-daemons "$TEMP_DIR/launchd.plist" "$INPUT_DIR/jb/LaunchDaemons" scp_to "$TEMP_DIR/launchd.plist" "/mnt1/System/Library/xpc/launchd.plist" ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/xpc/launchd.plist" @@ -346,3 +369,6 @@ echo "" echo "[+] CFW installation complete!" echo " Reboot the device for changes to take effect." echo " After boot, SSH will be available on port 22222 (password: alpine)" + +ssh_cmd "/sbin/halt" || true + diff --git a/scripts/fw_manifest.py b/scripts/fw_manifest.py new file mode 100755 index 0000000..1ab22bb --- /dev/null +++ b/scripts/fw_manifest.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Generate hybrid BuildManifest.plist and Restore.plist for vresearch1 restore. + +Merges cloudOS boot-chain (vresearch101ap) with vphone600 runtime components +(device tree, SEP, kernel) and iPhone OS images into a single DFU erase-install +Build Identity. + +The VM hardware identifies as vresearch101ap (BDID 0x90) in DFU mode, so the +identity fields must match for TSS/SHSH signing. Runtime components use the +vphone600 variant because its device tree sets MKB dt=1 (keybag-less boot). + +Usage: + python3 fw_manifest.py +""" + +import copy, os, plistlib, sys + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def load(path): + with open(path, "rb") as f: + return plistlib.load(f) + + +def entry(identities, idx, key): + """Deep-copy a single Manifest entry from a build identity.""" + return copy.deepcopy(identities[idx]["Manifest"][key]) + + +# --------------------------------------------------------------------------- +# Identity discovery +# --------------------------------------------------------------------------- + +def _is_research(bi): + """Determine whether a build identity is a research variant.""" + for comp in ("LLB", "iBSS", "iBEC"): + path = bi.get("Manifest", {}).get(comp, {}).get("Info", {}).get("Path", "") + if not path: + continue + parts = os.path.basename(path).split(".") + if len(parts) == 4: + return "RESEARCH" in parts[2] + variant = bi.get("Info", {}).get("Variant", "") + return "research" in variant.lower() + + +def find_cloudos(identities, device_class): + """Find release and research identity indices for the given DeviceClass.""" + release = research = None + for i, bi in enumerate(identities): + dc = bi.get("Info", {}).get("DeviceClass", "") + if dc != device_class: + continue + if _is_research(bi): + if research is None: + research = i + else: + if release is None: + release = i + if release is None: + raise KeyError(f"No release identity for DeviceClass={device_class}") + if research is None: + raise KeyError(f"No research identity for DeviceClass={device_class}") + return release, research + + +def find_iphone_erase(identities): + """Return the index of the first iPhone erase identity.""" + for i, bi in enumerate(identities): + var = bi.get("Info", {}).get("Variant", "").lower() + if "research" not in var and "upgrade" not in var and "recovery" not in var: + return i + raise KeyError("No erase identity found in iPhone manifest") + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} ", + file=sys.stderr) + sys.exit(1) + + iphone_dir, cloudos_dir = sys.argv[1], sys.argv[2] + + cloudos_bm = load(os.path.join(cloudos_dir, "BuildManifest.plist")) + iphone_bm = load(os.path.join(iphone_dir, "BuildManifest.plist")) + cloudos_rp = load(os.path.join(cloudos_dir, "Restore.plist")) + iphone_rp = load(os.path.join(iphone_dir, "Restore.plist")) + + C = cloudos_bm["BuildIdentities"] + I = iphone_bm["BuildIdentities"] + + # ── Discover source identities ─────────────────────────────────── + # PROD / RES = vresearch101ap release / research (boot chain) + # VP / VPR = vphone600ap release / research (runtime) + PROD, RES = find_cloudos(C, "vresearch101ap") + VP, VPR = find_cloudos(C, "vphone600ap") + I_ERASE = find_iphone_erase(I) + + print(f" cloudOS vresearch101ap: release=#{PROD}, research=#{RES}") + print(f" cloudOS vphone600ap: release=#{VP}, research=#{VPR}") + print(f" iPhone erase: #{I_ERASE}") + + # ── Build the single DFU erase identity ────────────────────────── + # Identity base from vresearch101ap PROD — must match DFU hardware + # (BDID 0x90) for TSS/SHSH signing. + bi = copy.deepcopy(C[PROD]) + bi["Manifest"] = {} + bi["Ap,ProductType"] = "ComputeModule14,2" + bi["Ap,Target"] = "VRESEARCH101AP" + bi["Ap,TargetType"] = "vresearch101" + bi["ApBoardID"] = "0x90" + bi["ApChipID"] = "0xFE01" + bi["ApSecurityDomain"] = "0x01" + for k in ("NeRDEpoch", "RestoreAttestationMode"): + bi.pop(k, None) + bi.get("Info", {}).pop(k, None) + bi["Info"]["FDRSupport"] = False + bi["Info"]["Variant"] = "Darwin Cloud Customer Erase Install (IPSW)" + bi["Info"]["VariantContents"] = { + "BasebandFirmware": "Release", + "DCP": "DarwinProduction", + "DFU": "DarwinProduction", + "Firmware": "DarwinProduction", + "InitiumBaseband": "Production", + "InstalledKernelCache": "Production", + "InstalledSPTM": "Production", + "OS": "Production", + "RestoreKernelCache": "Production", + "RestoreRamDisk": "Production", + "RestoreSEP": "DarwinProduction", + "RestoreSPTM": "Production", + "SEP": "DarwinProduction", + "VinylFirmware": "Release", + } + + m = bi["Manifest"] + + # ── Boot chain (vresearch101 — matches DFU hardware) ───────────── + m["LLB"] = entry(C, PROD, "LLB") + m["iBSS"] = entry(C, PROD, "iBSS") + m["iBEC"] = entry(C, PROD, "iBEC") + m["iBoot"] = entry(C, RES, "iBoot") # research iBoot + + # ── Security monitors (shared across board configs) ────────────── + m["Ap,RestoreSecurePageTableMonitor"] = entry(C, PROD, "Ap,RestoreSecurePageTableMonitor") + m["Ap,RestoreTrustedExecutionMonitor"] = entry(C, PROD, "Ap,RestoreTrustedExecutionMonitor") + m["Ap,SecurePageTableMonitor"] = entry(C, PROD, "Ap,SecurePageTableMonitor") + m["Ap,TrustedExecutionMonitor"] = entry(C, RES, "Ap,TrustedExecutionMonitor") + + # ── Device tree (vphone600ap — sets MKB dt=1 for keybag-less boot) + m["DeviceTree"] = entry(C, VP, "DeviceTree") + m["RestoreDeviceTree"] = entry(C, VP, "RestoreDeviceTree") + + # ── SEP (vphone600 — matches device tree) ──────────────────────── + m["SEP"] = entry(C, VP, "SEP") + m["RestoreSEP"] = entry(C, VP, "RestoreSEP") + + # ── Kernel (vphone600, patched by fw_patch.py) ──────────────────── + m["KernelCache"] = entry(C, VPR, "KernelCache") # research + m["RestoreKernelCache"] = entry(C, VP, "RestoreKernelCache") # release + + # ── Recovery mode (vphone600ap carries this entry) ──────────────── + m["RecoveryMode"] = entry(C, VP, "RecoveryMode") + + # ── CloudOS erase ramdisk ──────────────────────────────────────── + m["RestoreRamDisk"] = entry(C, PROD, "RestoreRamDisk") + m["RestoreTrustCache"] = entry(C, PROD, "RestoreTrustCache") + + # ── iPhone OS image ────────────────────────────────────────────── + m["Ap,SystemVolumeCanonicalMetadata"] = entry(I, I_ERASE, "Ap,SystemVolumeCanonicalMetadata") + m["OS"] = entry(I, I_ERASE, "OS") + m["StaticTrustCache"] = entry(I, I_ERASE, "StaticTrustCache") + m["SystemVolume"] = entry(I, I_ERASE, "SystemVolume") + + # ── Assemble BuildManifest ─────────────────────────────────────── + build_manifest = { + "BuildIdentities": [bi], + "ManifestVersion": cloudos_bm["ManifestVersion"], + "ProductBuildVersion": cloudos_bm["ProductBuildVersion"], + "ProductVersion": cloudos_bm["ProductVersion"], + "SupportedProductTypes": ["iPhone99,11"], + } + + # ── Assemble Restore.plist ─────────────────────────────────────── + restore = { + "ProductBuildVersion": cloudos_rp["ProductBuildVersion"], + "ProductVersion": cloudos_rp["ProductVersion"], + "DeviceMap": [iphone_rp["DeviceMap"][0]] + [ + d for d in cloudos_rp["DeviceMap"] + if d["BoardConfig"] in ("vphone600ap", "vresearch101ap") + ], + "SupportedProductTypeIDs": { + cat: (iphone_rp["SupportedProductTypeIDs"][cat] + + cloudos_rp["SupportedProductTypeIDs"][cat]) + for cat in ("DFU", "Recovery") + }, + "SupportedProductTypes": ( + iphone_rp.get("SupportedProductTypes", []) + + cloudos_rp.get("SupportedProductTypes", []) + ), + "SystemRestoreImageFileSystems": copy.deepcopy( + iphone_rp["SystemRestoreImageFileSystems"]), + } + + # ── Write output ───────────────────────────────────────────────── + for name, data in [("BuildManifest.plist", build_manifest), + ("Restore.plist", restore)]: + path = os.path.join(iphone_dir, name) + with open(path, "wb") as f: + plistlib.dump(data, f, sort_keys=True) + print(f" wrote {name}") + + +if __name__ == "__main__": + main() diff --git a/scripts/fw_patch.py b/scripts/fw_patch.py new file mode 100755 index 0000000..0b71330 --- /dev/null +++ b/scripts/fw_patch.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +""" +patch_firmware.py — Patch all boot-chain components for vphone600. + +Run this AFTER prepare_firmware.sh from the VM directory. + +Usage: + python3 patch_firmware.py [vm_directory] + + vm_directory defaults to the current working directory. + The script auto-discovers the iPhone*_Restore directory and all + firmware files by searching for known patterns. + +Components patched (ALL dynamically — no hardcoded offsets): + 1. AVPBooter — DGST validation bypass (mov x0, #0) + 2. iBSS — serial labels + image4 callback bypass + 3. iBEC — serial labels + image4 callback + boot-args + 4. LLB — serial labels + image4 callback + boot-args + rootfs + panic + 5. TXM — trustcache bypass (mov x0, #0) + 6. kernelcache — 25 patches (APFS, MAC, debugger, launch constraints, etc.) + +Dependencies: + pip install keystone-engine capstone pyimg4 +""" + +import sys, os, glob, subprocess, tempfile + +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN +from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE +from pyimg4 import IM4P + +from patchers.kernel import KernelPatcher +from patchers.iboot import IBootPatcher +from patchers.txm import TXMPatcher + +# ══════════════════════════════════════════════════════════════════ +# Assembler helpers (for AVPBooter only — iBoot/TXM/kernel are +# handled by their own patcher classes) +# ══════════════════════════════════════════════════════════════════ + +_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE) + + +def _asm(s): + enc, _ = _ks.asm(s) + if not enc: + raise RuntimeError(f"asm failed: {s}") + return bytes(enc) + + +MOV_X0_0 = _asm("mov x0, #0") +RET_MNEMONICS = {"ret", "retaa", "retab"} + + +# ══════════════════════════════════════════════════════════════════ +# IM4P / raw file helpers — auto-detect format +# ══════════════════════════════════════════════════════════════════ + +def load_firmware(path): + """Load firmware file, auto-detecting IM4P vs raw. + + Returns (im4p_or_None, raw_bytearray, is_im4p_bool, original_bytes). + """ + with open(path, "rb") as f: + raw = f.read() + + try: + im4p = IM4P(raw) + if im4p.payload.compression: + im4p.payload.decompress() + return im4p, bytearray(im4p.payload.data), True, raw + except Exception: + return None, bytearray(raw), False, raw + + +def save_firmware(path, im4p_obj, patched_data, was_im4p, original_raw=None): + """Save patched firmware, repackaging as IM4P if the original was IM4P.""" + if was_im4p and im4p_obj is not None: + if original_raw is not None: + _save_im4p_with_payp(path, im4p_obj.fourcc, patched_data, original_raw) + else: + new_im4p = IM4P( + fourcc=im4p_obj.fourcc, + description=im4p_obj.description, + payload=bytes(patched_data), + ) + with open(path, "wb") as f: + f.write(new_im4p.output()) + else: + with open(path, "wb") as f: + f.write(patched_data) + + +def _save_im4p_with_payp(path, fourcc, patched_data, original_raw): + """Repackage as lzfse-compressed IM4P and append PAYP from original.""" + with tempfile.NamedTemporaryFile(suffix=".raw", delete=False) as tmp_raw, \ + tempfile.NamedTemporaryFile(suffix=".im4p", delete=False) as tmp_im4p: + tmp_raw_path = tmp_raw.name + tmp_im4p_path = tmp_im4p.name + tmp_raw.write(bytes(patched_data)) + + try: + subprocess.run( + ["pyimg4", "im4p", "create", + "-i", tmp_raw_path, "-o", tmp_im4p_path, + "-f", fourcc, "--lzfse"], + check=True, capture_output=True, + ) + output = bytearray(open(tmp_im4p_path, "rb").read()) + finally: + os.unlink(tmp_raw_path) + os.unlink(tmp_im4p_path) + + payp_offset = original_raw.rfind(b"PAYP") + if payp_offset >= 0: + payp_data = original_raw[payp_offset - 10:] + output.extend(payp_data) + old_len = int.from_bytes(output[2:5], "big") + output[2:5] = (old_len + len(payp_data)).to_bytes(3, "big") + print(f" [+] preserved PAYP ({len(payp_data)} bytes)") + + with open(path, "wb") as f: + f.write(output) + + +# ══════════════════════════════════════════════════════════════════ +# Per-component patch functions +# ══════════════════════════════════════════════════════════════════ + +# ── 1. AVPBooter ────────────────────────────────────────────────── +# Already dynamic — finds DGST constant, locates x0 setter before +# ret, replaces with mov x0, #0. Base address is irrelevant +# (cancels out in the offset calculation). + +AVP_SEARCH = "0x4447" + + +def patch_avpbooter(data): + md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) + md.skipdata = True + insns = list(md.disasm(bytes(data), 0)) + + hits = [i for i in insns if AVP_SEARCH in f"{i.mnemonic} {i.op_str}"] + if not hits: + print(" [-] DGST constant not found") + return False + + addr2idx = {insn.address: i for i, insn in enumerate(insns)} + idx = addr2idx[hits[0].address] + + ret_idx = None + for i in range(idx, min(idx + 512, len(insns))): + if insns[i].mnemonic in RET_MNEMONICS: + ret_idx = i + break + if ret_idx is None: + print(" [-] epilogue not found") + return False + + x0_idx = None + for i in range(ret_idx - 1, max(ret_idx - 32, -1), -1): + op, mn = insns[i].op_str, insns[i].mnemonic + if mn == "mov" and op.startswith(("x0,", "w0,")): + x0_idx = i + break + if mn in ("cset", "csinc", "csinv", "csneg") and op.startswith(("x0,", "w0,")): + x0_idx = i + break + if mn in RET_MNEMONICS or mn in ("b", "bl", "br", "blr"): + break + if x0_idx is None: + print(" [-] x0 setter not found") + return False + + target = insns[x0_idx] + file_off = target.address + data[file_off:file_off + 4] = MOV_X0_0 + print(f" 0x{file_off:X}: {target.mnemonic} {target.op_str} -> mov x0, #0") + return True + + +# ── 2–4. iBSS / iBEC / LLB ─────────────────────────────────────── +# Fully dynamic via IBootPatcher — no hardcoded offsets. + +def patch_ibss(data): + p = IBootPatcher(data, mode='ibss', label="Loaded iBSS") + n = p.apply() + print(f" [+] {n} iBSS patches applied dynamically") + return n > 0 + + +def patch_ibec(data): + p = IBootPatcher(data, mode='ibec', label="Loaded iBEC") + n = p.apply() + print(f" [+] {n} iBEC patches applied dynamically") + return n > 0 + + +def patch_llb(data): + p = IBootPatcher(data, mode='llb', label="Loaded LLB") + n = p.apply() + print(f" [+] {n} LLB patches applied dynamically") + return n > 0 + + +# ── 5. TXM ─────────────────────────────────────────────────────── +# Fully dynamic via TXMPatcher — no hardcoded offsets. + +def patch_txm(data): + p = TXMPatcher(data) + n = p.apply() + print(f" [+] {n} TXM patches applied dynamically") + return n > 0 + + +# ── 6. Kernelcache ─────────────────────────────────────────────── +# Fully dynamic via KernelPatcher — no hardcoded offsets. + +def patch_kernelcache(data): + kp = KernelPatcher(data) + n = kp.apply() + print(f" [+] {n} kernel patches applied dynamically") + return n > 0 + + +# ══════════════════════════════════════════════════════════════════ +# File discovery +# ══════════════════════════════════════════════════════════════════ + +def find_restore_dir(base_dir): + for entry in sorted(os.listdir(base_dir)): + full = os.path.join(base_dir, entry) + if os.path.isdir(full) and "Restore" in entry: + return full + return None + + +def find_file(base_dir, patterns, label): + for pattern in patterns: + matches = sorted(glob.glob(os.path.join(base_dir, pattern))) + if matches: + return matches[0] + print(f"[-] {label} not found. Searched patterns:") + for p in patterns: + print(f" {os.path.join(base_dir, p)}") + sys.exit(1) + + +# ══════════════════════════════════════════════════════════════════ +# Main +# ══════════════════════════════════════════════════════════════════ + +COMPONENTS = [ + # (name, search_base_is_restore, search_patterns, patch_function, preserve_payp) + ("AVPBooter", False, ["AVPBooter*.bin"], patch_avpbooter, False), + ("iBSS", True, ["Firmware/dfu/iBSS.vresearch101.RELEASE.im4p"], patch_ibss, False), + ("iBEC", True, ["Firmware/dfu/iBEC.vresearch101.RELEASE.im4p"], patch_ibec, False), + ("LLB", True, ["Firmware/all_flash/LLB.vresearch101.RELEASE.im4p"], patch_llb, False), + ("TXM", True, ["Firmware/txm.iphoneos.research.im4p"], patch_txm, True), + ("kernelcache", True, ["kernelcache.research.vphone600"], patch_kernelcache, True), +] + + +def patch_component(path, patch_fn, name, preserve_payp): + print(f"\n{'=' * 60}") + print(f" {name}: {path}") + print(f"{'=' * 60}") + + im4p, data, was_im4p, original_raw = load_firmware(path) + fmt = "IM4P" if was_im4p else "raw" + extra = "" + if was_im4p and im4p: + extra = f", fourcc={im4p.fourcc}" + print(f" format: {fmt}{extra}, {len(data)} bytes") + + if not patch_fn(data): + print(f" [-] FAILED: {name}") + sys.exit(1) + + save_firmware(path, im4p, data, was_im4p, + original_raw if preserve_payp else None) + print(f" [+] saved ({fmt})") + + +def main(): + vm_dir = sys.argv[1] if len(sys.argv) > 1 else os.getcwd() + vm_dir = os.path.abspath(vm_dir) + + if not os.path.isdir(vm_dir): + print(f"[-] Not a directory: {vm_dir}") + sys.exit(1) + + restore_dir = find_restore_dir(vm_dir) + if not restore_dir: + print(f"[-] No *Restore* directory found in {vm_dir}") + print(" Run prepare_firmware_v2.sh first.") + sys.exit(1) + + print(f"[*] VM directory: {vm_dir}") + print(f"[*] Restore directory: {restore_dir}") + print(f"[*] Patching {len(COMPONENTS)} boot-chain components ...") + + for name, in_restore, patterns, patch_fn, preserve_payp in COMPONENTS: + search_base = restore_dir if in_restore else vm_dir + path = find_file(search_base, patterns, name) + patch_component(path, patch_fn, name, preserve_payp) + + print(f"\n{'=' * 60}") + print(f" All {len(COMPONENTS)} components patched successfully!") + print(f"{'=' * 60}") + + +if __name__ == "__main__": + main() diff --git a/scripts/fw_prepare.sh b/scripts/fw_prepare.sh new file mode 100755 index 0000000..b53dcd1 --- /dev/null +++ b/scripts/fw_prepare.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# fw_prepare.sh — Download/copy, merge, and generate hybrid restore firmware. +# Combines cloudOS boot chain with iPhone OS images for vresearch101. +# +# Accepts URLs or local file paths. Local paths are copied instead of downloaded. +# All output goes to the current working directory. +# +# Usage: +# make fw_prepare +# +# Environment variables (override positional args): +# IPHONE_SOURCE — URL or local path to iPhone IPSW +# CLOUDOS_SOURCE — URL or local path to cloudOS IPSW +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +IPHONE_SOURCE="${IPHONE_SOURCE:-${1:-https://updates.cdn-apple.com/2025FallFCS/fullrestores/089-13864/668EFC0E-5911-454C-96C6-E1063CB80042/iPhone17,3_26.1_23B85_Restore.ipsw}}" +CLOUDOS_SOURCE="${CLOUDOS_SOURCE:-${2:-https://updates.cdn-apple.com/private-cloud-compute/399b664dd623358c3de118ffc114e42dcd51c9309e751d43bc949b98f4e31349}}" + +# Derive local filenames from source basename +IPHONE_IPSW="${IPHONE_SOURCE##*/}" +IPHONE_DIR="${IPHONE_IPSW%.ipsw}" +CLOUDOS_IPSW="${CLOUDOS_SOURCE##*/}" +# Fallback name if the source basename has no extension (e.g. raw CDN hash URL) +[[ "$CLOUDOS_IPSW" == *.ipsw ]] || CLOUDOS_IPSW="pcc-base.ipsw" +CLOUDOS_DIR="${CLOUDOS_IPSW%.ipsw}" + +echo "=== prepare_firmware ===" +echo " iPhone: $IPHONE_SOURCE" +echo " CloudOS: $CLOUDOS_SOURCE" +echo " Output: $(pwd)/$IPHONE_DIR/" +echo "" + +# ── Fetch (download or copy) ───────────────────────────────────────── +is_local() { [[ "$1" != http://* && "$1" != https://* ]]; } + +fetch() { + local src="$1" out="$2" + if [[ -f "$out" ]]; then + echo "==> Skipping: '$out' already exists." + return + fi + if is_local "$src"; then + echo "==> Copying ${src##*/} ..." + cp -- "$src" "$out" + else + echo "==> Downloading $out ..." + if ! wget --no-check-certificate --show-progress -O "$out" "$src"; then + echo "ERROR: Failed to download '$src'" >&2 + rm -f "$out" + exit 1 + fi + fi +} + +fetch "$IPHONE_SOURCE" "$IPHONE_IPSW" +fetch "$CLOUDOS_SOURCE" "$CLOUDOS_IPSW" + +# ── Extract ─────────────────────────────────────────────────────────── +extract() { + local zip="$1" dir="$2" + rm -rf "$dir" + echo "==> Extracting $zip ..." + mkdir -p "$dir" + unzip -oq "$zip" -d "$dir" + chmod -R u+w "$dir" +} + +extract "$IPHONE_IPSW" "$IPHONE_DIR" +extract "$CLOUDOS_IPSW" "$CLOUDOS_DIR" + +# ── Merge cloudOS firmware into iPhone restore directory ────────────── +echo "==> Importing cloudOS firmware components ..." + +cp ${CLOUDOS_DIR}/kernelcache.* "$IPHONE_DIR"/ + +for sub in agx all_flash ane dfu pmp; do + cp ${CLOUDOS_DIR}/Firmware/${sub}/* "$IPHONE_DIR/Firmware/${sub}"/ +done + +cp ${CLOUDOS_DIR}/Firmware/*.im4p "$IPHONE_DIR/Firmware"/ + +# CloudOS ramdisk DMGs and trustcaches (RestoreRamDisk / RestoreTrustCache) +cp -n ${CLOUDOS_DIR}/*.dmg "$IPHONE_DIR"/ 2>/dev/null || true +cp -n ${CLOUDOS_DIR}/Firmware/*.dmg.trustcache "$IPHONE_DIR/Firmware"/ 2>/dev/null || true + +# ── Preserve original iPhone BuildManifest (cfw_install.sh reads Cryptex paths) ── +cp "$IPHONE_DIR/BuildManifest.plist" "$IPHONE_DIR/BuildManifest-iPhone.plist" + +# ── Generate hybrid BuildManifest.plist & Restore.plist ─────────────── +echo "==> Generating hybrid plists ..." + +python3 "$SCRIPT_DIR/fw_manifest.py" "$IPHONE_DIR" "$CLOUDOS_DIR" + +# ── Cleanup (keep IPSWs, remove intermediate files) ────────────────── +echo "==> Cleaning up ..." +rm -rf "$CLOUDOS_DIR" + +echo "==> Done. Restore directory ready: $IPHONE_DIR/" +echo " Run 'make fw_patch' to patch boot-chain components." diff --git a/scripts/patchers/__init__.py b/scripts/patchers/__init__.py new file mode 100644 index 0000000..7c87acc --- /dev/null +++ b/scripts/patchers/__init__.py @@ -0,0 +1,5 @@ +from .iboot import IBootPatcher +from .kernel import KernelPatcher +from .txm import TXMPatcher + +__all__ = ["IBootPatcher", "KernelPatcher", "TXMPatcher"] diff --git a/Scripts/patch_cfw.py b/scripts/patchers/cfw.py old mode 100644 new mode 100755 similarity index 100% rename from Scripts/patch_cfw.py rename to scripts/patchers/cfw.py diff --git a/scripts/patchers/iboot.py b/scripts/patchers/iboot.py new file mode 100755 index 0000000..1c64aa4 --- /dev/null +++ b/scripts/patchers/iboot.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +""" +iboot_patcher.py — Dynamic patcher for iBoot-based images (iBSS, iBEC, LLB). + +Finds all patch sites by string anchors, instruction patterns, and unique +error-code constants — NO hardcoded offsets. Works across iBoot variants +as long as the code structure is preserved. + +iBSS, iBEC, and LLB share the same raw binary; the difference is which +patches are applied: + - iBSS: serial labels + image4 callback bypass + - iBEC: iBSS + boot-args + - LLB: iBEC + rootfs bypass (6 patches) + panic bypass + +Dependencies: keystone-engine, capstone +""" + +import struct +from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN + +# ── Assembly / disassembly singletons ────────────────────────── +_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE) +_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) +_cs.detail = True +_cs.skipdata = True + + +def _asm(s): + enc, _ = _ks.asm(s) + if not enc: + raise RuntimeError(f"asm failed: {s}") + return bytes(enc) + + +NOP = _asm("nop") +MOV_X0_0 = _asm("mov x0, #0") +PACIBSP = _asm("hint #27") + + +def _rd32(buf, off): + return struct.unpack_from("> 2 + return 0x14000000 | (offset & 0x3FFFFFF) + + +def _encode_adrp(rd, pc, target): + imm = ((target & ~0xFFF) - (pc & ~0xFFF)) >> 12 + imm &= (1 << 21) - 1 + return 0x90000000 | ((imm & 3) << 29) | ((imm >> 2) << 5) | (rd & 0x1F) + + +def _encode_add_imm12(rd, rn, imm12): + return 0x91000000 | ((imm12 & 0xFFF) << 10) | ((rn & 0x1F) << 5) | (rd & 0x1F) + + +# ── IBootPatcher ─────────────────────────────────────────────── + +class IBootPatcher: + """Dynamic patcher for iBoot binaries (iBSS / iBEC / LLB). + + mode controls which patches are applied: + 'ibss' — serial labels + image4 callback + 'ibec' — ibss + boot-args + 'llb' — ibec + rootfs bypass + panic bypass + """ + + BOOT_ARGS = b"serial=3 -v debug=0x2014e %s" + CHUNK_SIZE, OVERLAP = 0x2000, 0x100 + + def __init__(self, data, mode='ibss', label=None, verbose=True): + self.data = data # bytearray (mutable) + self.raw = bytes(data) # immutable snapshot + self.size = len(data) + self.mode = mode + self.label = label or f"Loaded {mode.upper()}" + self.verbose = verbose + self.patches = [] + + def _log(self, msg): + if self.verbose: + print(msg) + + # ── emit / apply ─────────────────────────────────────────── + def emit(self, off, patch_bytes, desc): + self.patches.append((off, patch_bytes, desc)) + if self.verbose: + original = self.raw[off:off + len(patch_bytes)] + before_insns = _disasm_n(self.raw, off, len(patch_bytes) // 4) + after_insns = list(_cs.disasm(patch_bytes, off)) + b_str = "; ".join(f"{i.mnemonic} {i.op_str}" for i in before_insns) or "???" + a_str = "; ".join(f"{i.mnemonic} {i.op_str}" for i in after_insns) or "???" + print(f" 0x{off:06X}: {b_str} → {a_str} [{desc}]") + + def emit_string(self, off, data_bytes, desc): + """Record a string/data patch (not disassemblable).""" + self.patches.append((off, data_bytes, desc)) + if self.verbose: + try: + txt = data_bytes.decode('ascii') + except Exception: + txt = data_bytes.hex() + print(f" 0x{off:06X}: → {repr(txt)} [{desc}]") + + def apply(self): + """Find all patches, apply them, return count.""" + self.find_all() + for off, pb, _ in self.patches: + self.data[off:off + len(pb)] = pb + + if self.verbose and self.patches: + self._log(f"\n [{len(self.patches)} {self.mode.upper()} patches applied]") + return len(self.patches) + + # ── Master find ──────────────────────────────────────────── + def find_all(self): + self.patches = [] + + self.patch_serial_labels() + self.patch_image4_callback() + + if self.mode in ('ibec', 'llb'): + self.patch_boot_args() + + if self.mode == 'llb': + self.patch_rootfs_bypass() + self.patch_panic_bypass() + + return self.patches + + # ═══════════════════════════════════════════════════════════ + # 1. Serial labels — find two long '====...' banner runs + # ═══════════════════════════════════════════════════════════ + def patch_serial_labels(self): + label_bytes = self.label.encode() if isinstance(self.label, str) else self.label + eq_runs = [] + i = 0 + while i < self.size: + if self.raw[i] == ord('='): + start = i + while i < self.size and self.raw[i] == ord('='): + i += 1 + if i - start >= 20: + eq_runs.append(start) + else: + i += 1 + + if len(eq_runs) < 2: + self._log(" [-] serial labels: <2 banner runs found") + return + + for run_start in eq_runs[:2]: + write_off = run_start + 1 + self.emit_string(write_off, label_bytes, f"serial label") + + # ═══════════════════════════════════════════════════════════ + # 2. image4_validate_property_callback + # Pattern: b.ne + mov x0, x22 (preceded by cmp within 8 insns) + # Patch: b.ne → NOP, mov x0, x22 → mov x0, #0 + # ═══════════════════════════════════════════════════════════ + def patch_image4_callback(self): + candidates = [] + for insns in self._chunked_disasm(): + for i in range(len(insns) - 1): + if insns[i].mnemonic != "b.ne": + continue + if not (insns[i + 1].mnemonic == "mov" + and insns[i + 1].op_str == "x0, x22"): + continue + addr = insns[i].address + if not any(insns[j].mnemonic == "cmp" + for j in range(max(0, i - 8), i)): + continue + # Prefer candidate with movn w22 (sets -1) earlier + neg1 = any( + (insns[j].mnemonic == "movn" + and insns[j].op_str.startswith("w22,")) + or (insns[j].mnemonic == "mov" + and "w22" in insns[j].op_str + and ("#-1" in insns[j].op_str + or "#0xffffffff" in insns[j].op_str)) + for j in range(max(0, i - 64), i) + ) + candidates.append((addr, neg1)) + + if not candidates: + self._log(" [-] image4 callback: pattern not found") + return + + # Prefer the candidate with the movn w22 (error return -1) + off = None + for a, n in candidates: + if n: + off = a + break + if off is None: + off = candidates[-1][0] + + self.emit(off, NOP, "image4 callback: b.ne → nop") + self.emit(off + 4, MOV_X0_0, "image4 callback: mov x0,x22 → mov x0,#0") + + # ═══════════════════════════════════════════════════════════ + # 3. Boot-args — redirect ADRP+ADD x2 to custom string + # ═══════════════════════════════════════════════════════════ + def patch_boot_args(self, new_args=None): + if new_args is None: + new_args = self.BOOT_ARGS + + # Find the standalone "%s" format string near "rd=md0" + fmt_off = self._find_boot_args_fmt() + if fmt_off < 0: + self._log(" [-] boot-args: format string not found") + return + + # Find ADRP+ADD x2 referencing it + adrp_off, add_off = self._find_boot_args_adrp(fmt_off) + if adrp_off < 0: + self._log(" [-] boot-args: ADRP+ADD x2 not found") + return + + # Find a NUL slot for the new string + new_off = self._find_string_slot(len(new_args)) + if new_off < 0: + self._log(" [-] boot-args: no NUL slot") + return + + self.emit_string(new_off, new_args, "boot-args string") + new_adrp = struct.pack("= anchor + 0x40: + return -1 + if self.raw[off - 1] == 0 and self.raw[off + 2] == 0: + return off + off += 1 + return -1 + + def _find_boot_args_adrp(self, fmt_off): + for insns in self._chunked_disasm(): + for i in range(len(insns) - 1): + a, b = insns[i], insns[i + 1] + if a.mnemonic != "adrp" or b.mnemonic != "add": + continue + if a.op_str.split(",")[0].strip() != "x2": + continue + if len(a.operands) < 2 or len(b.operands) < 3: + continue + if a.operands[0].reg != b.operands[1].reg: + continue + if a.operands[1].imm + b.operands[2].imm == fmt_off: + return a.address, b.address + return -1, -1 + + def _find_string_slot(self, string_len, search_start=0x14000): + off = search_start + while off < self.size: + if self.raw[off] == 0: + run_start = off + while off < self.size and self.raw[off] == 0: + off += 1 + if off - run_start >= 64: + write_off = (run_start + 8 + 15) & ~15 + if write_off + string_len <= off: + return write_off + else: + off += 1 + return -1 + + # ═══════════════════════════════════════════════════════════ + # 4. LLB rootfs bypass — 6 patches in two functions + # ═══════════════════════════════════════════════════════════ + def patch_rootfs_bypass(self): + # ── 4a: cbz w0 → unconditional b (error code 0x3B7) ── + self._patch_cbz_before_error(0x3B7, "rootfs: skip sig check (0x3B7)") + + # ── 4b: cmp x8, #0x400; b.hs → nop ──────────────────── + self._patch_bhs_after_cmp_0x400() + + # ── 4c: cbz w0 → unconditional b (error code 0x3C2) ── + self._patch_cbz_before_error(0x3C2, "rootfs: skip sig verify (0x3C2)") + + # ── 4d: cbz x8 → nop (ldr xR, [xN, #0x78]) ────────── + self._patch_null_check_0x78() + + # ── 4e: cbz w0 → unconditional b (error code 0x110) ── + self._patch_cbz_before_error(0x110, "rootfs: skip size verify (0x110)") + + def _patch_cbz_before_error(self, error_code, desc): + """Find unique 'mov w8, #', cbz/cbnz is 4 bytes before. + Convert conditional branch to unconditional b to same target.""" + locs = _find_asm_pattern(self.raw, f"mov w8, #{error_code}") + if len(locs) != 1: + self._log(f" [-] {desc}: expected 1 'mov w8, #{error_code:#x}', " + f"found {len(locs)}") + return + + err_off = locs[0] + cbz_off = err_off - 4 + insn = _disasm_one(self.raw, cbz_off) + if not insn or insn.mnemonic not in ('cbz', 'cbnz'): + self._log(f" [-] {desc}: expected cbz/cbnz at 0x{cbz_off:X}, " + f"got {insn.mnemonic if insn else '???'}") + return + + # Extract the branch target from the conditional instruction + target = insn.operands[1].imm + b_word = _encode_b(cbz_off, target) + self.emit(cbz_off, struct.pack(" 0: + self.code_ranges.append((fileoff, fileoff + filesize)) + off += cmdsize + + if self.base_va is None: + raise ValueError("__TEXT segment not found — cannot determine BASE_VA") + + self.code_ranges.sort() + total_mb = sum(e - s for s, e in self.code_ranges) / (1024 * 1024) + self._log(f" BASE_VA = 0x{self.base_va:016X}") + self._log(f" {len(self.code_ranges)} executable ranges, total {total_mb:.1f} MB") + + def _va(self, foff): + return self.base_va + foff + + def _foff(self, va): + return va - self.base_va + + # ── Kext range discovery ───────────────────────────────────── + def _discover_kext_ranges(self): + """Parse __PRELINK_INFO + embedded kext Mach-Os to find code section ranges.""" + self.kext_ranges = {} # bundle_id -> (text_start, text_end) + + # Find __PRELINK_INFO segment + prelink_info = None + for name, vmaddr, fileoff, filesize, _ in self.all_segments: + if name == "__PRELINK_INFO": + prelink_info = (fileoff, filesize) + break + + if prelink_info is None: + self._log(" [-] __PRELINK_INFO not found, using __TEXT_EXEC for all") + self._set_fallback_ranges() + return + + foff, fsize = prelink_info + pdata = self.raw[foff:foff + fsize] + + # Parse the XML plist + xml_start = pdata.find(b"") + if xml_start < 0 or xml_end < 0: + self._log(" [-] __PRELINK_INFO plist not found") + self._set_fallback_ranges() + return + + xml = pdata[xml_start:xml_end + len(b"")] + pl = plistlib.loads(xml) + items = pl.get("_PrelinkInfoDictionary", []) + + # Kexts we need ranges for + WANTED = { + "com.apple.filesystems.apfs": "apfs", + "com.apple.security.sandbox": "sandbox", + "com.apple.driver.AppleMobileFileIntegrity": "amfi", + } + + for item in items: + bid = item.get("CFBundleIdentifier", "") + tag = WANTED.get(bid) + if tag is None: + continue + + exec_addr = item.get("_PrelinkExecutableLoadAddr", 0) & 0xFFFFFFFFFFFFFFFF + kext_foff = exec_addr - self.base_va + if kext_foff < 0 or kext_foff >= self.size: + continue + + # Parse this kext's embedded Mach-O to find __TEXT_EXEC.__text + text_range = self._parse_kext_text_exec(kext_foff) + if text_range: + self.kext_ranges[tag] = text_range + self._log(f" {tag:10s} __text: 0x{text_range[0]:08X} - 0x{text_range[1]:08X} " + f"({(text_range[1]-text_range[0])//1024} KB)") + + # Derive the ranges used by patch methods + self._set_ranges_from_kexts() + + def _parse_kext_text_exec(self, kext_foff): + """Parse an embedded kext Mach-O header and return (__text start, end) in file offsets.""" + if kext_foff + 32 > self.size: + return None + magic = _rd32(self.raw, kext_foff) + if magic != 0xFEEDFACF: + return None + + ncmds = struct.unpack_from(" self.size: + break + cmd, cmdsize = struct.unpack_from(" self.size: + break + sectname = self.raw[sect_off:sect_off+16].split(b'\x00')[0].decode() + if sectname == "__text": + sect_addr = struct.unpack_from("> 5) & 0x7FFFF + immlo = (insn >> 29) & 0x3 + imm = (immhi << 2) | immlo + if imm & (1 << 20): + imm -= (1 << 21) + pc = self._va(off) + page = (pc & ~0xFFF) + (imm << 12) + self.adrp_by_page[page].append((off, rd)) + + n = sum(len(v) for v in self.adrp_by_page.values()) + self._log(f" {n} ADRP entries, {len(self.adrp_by_page)} distinct pages") + + def _build_bl_index(self): + """Index BL instructions by target offset.""" + self.bl_callers = defaultdict(list) # target_off -> [caller_off, ...] + for rng_start, rng_end in self.code_ranges: + for off in range(rng_start, rng_end, 4): + insn = _rd32(self.raw, off) + if (insn & 0xFC000000) != 0x94000000: + continue + imm26 = insn & 0x3FFFFFF + if imm26 & (1 << 25): + imm26 -= (1 << 26) + target = off + imm26 * 4 + self.bl_callers[target].append(off) + + def _find_panic(self): + """Find _panic: most-called function whose callers reference '@%s:%d' strings.""" + candidates = sorted(self.bl_callers.items(), key=lambda x: -len(x[1]))[:15] + for target_off, callers in candidates: + if len(callers) < 2000: + break + confirmed = 0 + for caller_off in callers[:30]: + for back in range(caller_off - 4, max(caller_off - 32, 0), -4): + insn = _rd32(self.raw, back) + # ADD x0, x0, #imm + if (insn & 0xFFC003E0) == 0x91000000: + add_imm = (insn >> 10) & 0xFFF + if back >= 4: + prev = _rd32(self.raw, back - 4) + if (prev & 0x9F00001F) == 0x90000000: # ADRP x0 + immhi = (prev >> 5) & 0x7FFFF + immlo = (prev >> 29) & 0x3 + imm = (immhi << 2) | immlo + if imm & (1 << 20): + imm -= (1 << 21) + pc = self._va(back - 4) + page = (pc & ~0xFFF) + (imm << 12) + str_foff = self._foff(page + add_imm) + if 0 <= str_foff < self.size - 10: + snippet = self.raw[str_foff:str_foff + 60] + if b"@%s:%d" in snippet or b"%s:%d" in snippet: + confirmed += 1 + break + break + if confirmed >= 3: + self.panic_off = target_off + return + self.panic_off = candidates[2][0] if len(candidates) > 2 else candidates[0][0] + + # ── Helpers ────────────────────────────────────────────────── + def _disas_at(self, off, count=1): + """Disassemble *count* instructions at file offset. Returns a list.""" + end = min(off + count * 4, self.size) + if off < 0 or off >= self.size: + return [] + code = bytes(self.raw[off:end]) + return list(_cs.disasm(code, off, count)) + + def _is_bl(self, off): + """Return BL target file offset, or -1 if not a BL.""" + insns = self._disas_at(off) + if insns and insns[0].mnemonic == "bl": + return insns[0].operands[0].imm + return -1 + + def _is_cond_branch_w0(self, off): + """Return True if instruction is a conditional branch on w0 (cbz/cbnz/tbz/tbnz).""" + insns = self._disas_at(off) + if not insns: + return False + i = insns[0] + if i.mnemonic in ("cbz", "cbnz", "tbz", "tbnz"): + return i.operands[0].type == ARM64_OP_REG and i.operands[0].reg == ARM64_REG_W0 + return False + + def find_string(self, s, start=0): + """Find string, return file offset of the enclosing C string start.""" + if isinstance(s, str): + s = s.encode() + off = self.raw.find(s, start) + if off < 0: + return -1 + # Walk backward to the preceding NUL — that's the C string start + cstr = off + while cstr > 0 and self.raw[cstr - 1] != 0: + cstr -= 1 + return cstr + + def find_string_refs(self, str_off, code_start=None, code_end=None): + """Find all (adrp_off, add_off, dest_reg) referencing str_off via ADRP+ADD.""" + target_va = self._va(str_off) + target_page = target_va & ~0xFFF + page_off = target_va & 0xFFF + + refs = [] + for adrp_off, rd in self.adrp_by_page.get(target_page, []): + if code_start is not None and adrp_off < code_start: + continue + if code_end is not None and adrp_off >= code_end: + continue + if adrp_off + 4 >= self.size: + continue + nxt = _rd32(self.raw, adrp_off + 4) + # ADD (imm) 64-bit: 1001_0001_00_imm12_Rn_Rd + if (nxt & 0xFFC00000) != 0x91000000: + continue + add_rn = (nxt >> 5) & 0x1F + add_imm = (nxt >> 10) & 0xFFF + if add_rn == rd and add_imm == page_off: + add_rd = nxt & 0x1F + refs.append((adrp_off, adrp_off + 4, add_rd)) + return refs + + def find_function_start(self, off, max_back=0x4000): + """Walk backwards to find PACIBSP or STP x29,x30,[sp,#imm]. + + When STP x29,x30 is found, continues backward up to 0x20 more + bytes to look for PACIBSP (ARM64e functions may have several STP + instructions in the prologue before STP x29,x30). + """ + for o in range(off - 4, max(off - max_back, 0), -4): + insn = _rd32(self.raw, o) + if insn == _PACIBSP_U32: + return o + dis = self._disas_at(o) + if dis and dis[0].mnemonic == "stp" and "x29, x30, [sp" in dis[0].op_str: + # Check further back for PACIBSP (prologue may have + # multiple STP instructions before x29,x30) + for k in range(o - 4, max(o - 0x24, 0), -4): + if _rd32(self.raw, k) == _PACIBSP_U32: + return k + return o + return -1 + + def _disas_n(self, buf, off, count): + """Disassemble *count* instructions from *buf* at file offset *off*.""" + end = min(off + count * 4, len(buf)) + if off < 0 or off >= len(buf): + return [] + code = bytes(buf[off:end]) + return list(_cs.disasm(code, off, count)) + + def _fmt_insn(self, insn, marker=""): + """Format one capstone instruction for display.""" + raw = insn.bytes + hex_str = " ".join(f"{b:02x}" for b in raw) + s = f" 0x{insn.address:08X}: {hex_str:12s} {insn.mnemonic:8s} {insn.op_str}" + if marker: + s += f" {marker}" + return s + + def _print_patch_context(self, off, patch_bytes, desc): + """Print disassembly before/after a patch site for debugging.""" + ctx = 3 # instructions of context before and after + # -- BEFORE (original bytes) -- + lines = [f" ┌─ PATCH 0x{off:08X}: {desc}"] + lines.append(" │ BEFORE:") + start = max(off - ctx * 4, 0) + before_insns = self._disas_n(self.raw, start, ctx + 1 + ctx) + for insn in before_insns: + if insn.address == off: + lines.append(self._fmt_insn(insn, " ◄━━ PATCHED")) + elif off < insn.address < off + len(patch_bytes): + lines.append(self._fmt_insn(insn, " ◄━━ PATCHED")) + else: + lines.append(self._fmt_insn(insn)) + + # -- AFTER (new bytes) -- + lines.append(" │ AFTER:") + after_insns = self._disas_n(self.raw, start, ctx) + for insn in after_insns: + lines.append(self._fmt_insn(insn)) + # Decode the patch bytes themselves + patch_insns = list(_cs.disasm(patch_bytes, off, len(patch_bytes) // 4)) + for insn in patch_insns: + lines.append(self._fmt_insn(insn, " ◄━━ NEW")) + # Trailing context after the patch + trail_start = off + len(patch_bytes) + trail_insns = self._disas_n(self.raw, trail_start, ctx) + for insn in trail_insns: + lines.append(self._fmt_insn(insn)) + lines.append(f" └─") + self._log("\n".join(lines)) + + def emit(self, off, patch_bytes, desc): + """Record a patch and print before/after disassembly context.""" + self.patches.append((off, patch_bytes, desc)) + if self.verbose: + self._print_patch_context(off, patch_bytes, desc) + + def _find_by_string_in_range(self, string, code_range, label): + """Find string, find ADRP+ADD ref in code_range, return ref list.""" + str_off = self.find_string(string) + if str_off < 0: + self._log(f" [-] string not found: {string!r}") + return [] + refs = self.find_string_refs(str_off, code_range[0], code_range[1]) + if not refs: + self._log(f" [-] no code refs to {label} (str at 0x{str_off:X})") + return refs + + # ── Chained fixup pointer decoding ─────────────────────────── + def _decode_chained_ptr(self, val): + """Decode an arm64e chained fixup pointer to a file offset. + + - auth rebase (bit63=1): foff = bits[31:0] + - non-auth rebase (bit63=0): VA = (bits[50:43] << 56) | bits[42:0] + """ + if val == 0: + return -1 + if val & (1 << 63): # auth rebase + return val & 0xFFFFFFFF + else: # non-auth rebase + target = val & 0x7FFFFFFFFFF # bits[42:0] + high8 = (val >> 43) & 0xFF + full_va = (high8 << 56) | target + if full_va > self.base_va: + return full_va - self.base_va + return -1 + + # ═══════════════════════════════════════════════════════════════ + # Per-patch finders + # ═══════════════════════════════════════════════════════════════ + + def patch_apfs_root_snapshot(self): + """Patch 1: NOP the tbnz w8,#5 that gates sealed-volume root snapshot panic.""" + self._log("\n[1] _apfs_vfsop_mount: root snapshot sealed volume check") + + refs = self._find_by_string_in_range( + b"Rooting from snapshot with xid", + self.apfs_text, "apfs_vfsop_mount log") + if not refs: + refs = self._find_by_string_in_range( + b"Failed to find the root snapshot", + self.apfs_text, "root snapshot panic") + if not refs: + return False + + for adrp_off, add_off, _ in refs: + for scan in range(add_off, min(add_off + 0x200, self.size), 4): + insns = self._disas_at(scan) + if not insns: + continue + i = insns[0] + if i.mnemonic not in ("tbnz", "tbz"): + continue + # Check: tbz/tbnz w8, #5, ... + ops = i.operands + if (len(ops) >= 2 + and ops[0].type == ARM64_OP_REG + and ops[1].type == ARM64_OP_IMM + and ops[1].imm == 5): + self.emit(scan, NOP, + f"NOP {i.mnemonic} {i.op_str} " + "(sealed vol check) [_apfs_vfsop_mount]") + return True + + self._log(" [-] tbz/tbnz w8,#5 not found near xref") + return False + + def patch_apfs_seal_broken(self): + """Patch 2: NOP the conditional branch leading to 'root volume seal is broken' panic.""" + self._log("\n[2] _authapfs_seal_is_broken: seal broken panic") + + str_off = self.find_string(b"root volume seal is broken") + if str_off < 0: + self._log(" [-] string not found") + return False + + refs = self.find_string_refs(str_off, *self.apfs_text) + if not refs: + self._log(" [-] no code refs") + return False + + for adrp_off, add_off, _ in refs: + # Find BL _panic after string ref + bl_off = -1 + for scan in range(add_off, min(add_off + 0x40, self.size), 4): + bl_target = self._is_bl(scan) + if bl_target == self.panic_off: + bl_off = scan + break + + if bl_off < 0: + continue + + # Search backwards for a conditional branch that jumps INTO the + # panic path. The error block may set up __FILE__/line args + # before the string ADRP, so allow target up to 0x40 before it. + err_lo = adrp_off - 0x40 + for back in range(adrp_off - 4, max(adrp_off - 0x200, 0), -4): + target, kind = self._decode_branch_target(back) + if target is not None and err_lo <= target <= bl_off + 4: + self.emit(back, NOP, + f"NOP {kind} (seal broken) " + "[_authapfs_seal_is_broken]") + return True + + self._log(" [-] could not find conditional branch to NOP") + return False + + _COND_BRANCH_MNEMONICS = frozenset(( + "b.eq", "b.ne", "b.cs", "b.hs", "b.cc", "b.lo", + "b.mi", "b.pl", "b.vs", "b.vc", "b.hi", "b.ls", + "b.ge", "b.lt", "b.gt", "b.le", "b.al", + "cbz", "cbnz", "tbz", "tbnz", + )) + + def _decode_branch_target(self, off): + """Decode conditional branch at off via capstone. Returns (target, mnemonic) or (None, None).""" + insns = self._disas_at(off) + if not insns: + return None, None + i = insns[0] + if i.mnemonic in self._COND_BRANCH_MNEMONICS: + # Target is always the last IMM operand + for op in reversed(i.operands): + if op.type == ARM64_OP_IMM: + return op.imm, i.mnemonic + return None, None + + def patch_bsd_init_rootvp(self): + """Patch 3: NOP the conditional branch guarding the 'rootvp not authenticated' panic.""" + self._log("\n[3] _bsd_init: rootvp not authenticated panic") + + str_off = self.find_string(b"rootvp not authenticated after mounting") + if str_off < 0: + self._log(" [-] string not found") + return False + + refs = self.find_string_refs(str_off, *self.kern_text) + if not refs: + self._log(" [-] no code refs in kernel __text") + return False + + for adrp_off, add_off, _ in refs: + # Find the BL _panic after the string ref + bl_panic_off = -1 + for scan in range(add_off, min(add_off + 0x40, self.size), 4): + bl_target = self._is_bl(scan) + if bl_target == self.panic_off: + bl_panic_off = scan + break + + if bl_panic_off < 0: + continue + + # Search backwards for a conditional branch whose target is in + # the error path (the block ending with BL _panic). + # The error path is typically a few instructions before BL _panic. + err_lo = bl_panic_off - 0x40 # error block start (generous) + err_hi = bl_panic_off + 4 # error block end + + for back in range(adrp_off - 4, max(adrp_off - 0x400, 0), -4): + target, kind = self._decode_branch_target(back) + if target is not None and err_lo <= target <= err_hi: + self.emit(back, NOP, + f"NOP {kind} (rootvp auth) [_bsd_init]") + return True + + self._log(" [-] conditional branch into panic path not found") + return False + + def patch_proc_check_launch_constraints(self): + """Patches 4-5: mov w0,#0; ret at _proc_check_launch_constraints start. + + The AMFI function does NOT reference the symbol name string + '_proc_check_launch_constraints' — only the kernel wrapper does. + Instead, use 'AMFI: Validation Category info' which IS referenced + from the actual AMFI function. + """ + self._log("\n[4-5] _proc_check_launch_constraints: stub with mov w0,#0; ret") + + str_off = self.find_string(b"AMFI: Validation Category info") + if str_off < 0: + self._log(" [-] 'AMFI: Validation Category info' string not found") + return False + + refs = self.find_string_refs(str_off, *self.amfi_text) + if not refs: + self._log(" [-] no code refs in AMFI") + return False + + for adrp_off, add_off, _ in refs: + func_start = self.find_function_start(adrp_off) + if func_start < 0: + continue + self.emit(func_start, MOV_W0_0, + "mov w0,#0 [_proc_check_launch_constraints]") + self.emit(func_start + 4, RET, + "ret [_proc_check_launch_constraints]") + return True + + self._log(" [-] function start not found") + return False + + def _get_kernel_text_range(self): + """Return (start, end) file offsets of the kernel's own __TEXT_EXEC.__text. + + Parses fileset entries (LC_FILESET_ENTRY) to find the kernel component, + then reads its Mach-O header to get the __TEXT_EXEC.__text section. + Falls back to the full __TEXT_EXEC segment. + """ + # Try fileset entries + ncmds = struct.unpack_from("= 0: + linkedit = None + for name, vmaddr, fileoff, filesize, _ in self.all_segments: + if name == "__LINKEDIT": + linkedit = (fileoff, fileoff + filesize) + if linkedit and linkedit[0] <= str_off < linkedit[1]: + name_end = self.raw.find(b'\x00', str_off + 1) + if name_end > 0: + for probe in range(name_end + 1, min(name_end + 32, self.size - 7)): + val = _rd64(self.raw, probe) + func_foff = val - self.base_va + if self.kern_text[0] <= func_foff < self.kern_text[1]: + first_insn = _rd32(self.raw, func_foff) + if first_insn != 0 and first_insn != 0xD503201F: + self.emit(func_foff, MOV_X0_1, + "mov x0,#1 [_PE_i_can_has_debugger]") + self.emit(func_foff + 4, RET, + "ret [_PE_i_can_has_debugger]") + return True + + # Strategy 2: code pattern — function starts with ADRP x8, + # preceded by a function boundary, has many BL callers, + # and reads a 32-bit (w-register) value within first few instructions. + self._log(" [*] trying code pattern search...") + + # Determine kernel-only __text range from fileset entries if available + kern_text_start, kern_text_end = self._get_kernel_text_range() + + best_off = -1 + best_callers = 0 + for off in range(kern_text_start, kern_text_end - 12, 4): + dis = self._disas_at(off) + if not dis or dis[0].mnemonic != "adrp": + continue + # Must target x8 + if dis[0].operands[0].reg != ARM64_REG_X8: + continue + # Must be preceded by function boundary + if off >= 4: + prev = _rd32(self.raw, off - 4) + if not self._is_func_boundary(prev): + continue + # Must read a w-register (32-bit) from [x8, #imm] within first 6 instructions + has_w_load = False + for k in range(1, 7): + if off + k * 4 >= self.size: + break + dk = self._disas_at(off + k * 4) + if dk and dk[0].mnemonic == "ldr" and dk[0].op_str.startswith("w") and "x8" in dk[0].op_str: + has_w_load = True + break + if not has_w_load: + continue + # Count callers — _PE_i_can_has_debugger has ~80-200 callers + # (widely used but not a basic kernel primitive) + n_callers = len(self.bl_callers.get(off, [])) + if 50 <= n_callers <= 250 and n_callers > best_callers: + best_callers = n_callers + best_off = off + + if best_off >= 0: + self._log(f" [+] code pattern match at 0x{best_off:X} ({best_callers} callers)") + self.emit(best_off, MOV_X0_1, "mov x0,#1 [_PE_i_can_has_debugger]") + self.emit(best_off + 4, RET, "ret [_PE_i_can_has_debugger]") + return True + + self._log(" [-] function not found") + return False + + def patch_post_validation_nop(self): + """Patch 8: NOP the TBNZ after TXM CodeSignature error logging. + + The 'TXM [Error]: CodeSignature: selector: ...' string is followed + by a BL (printf/log), then a TBNZ that branches to an additional + validation path. NOP the TBNZ to skip it. + """ + self._log("\n[8] post-validation NOP (txm-related)") + + str_off = self.find_string(b"TXM [Error]: CodeSignature") + if str_off < 0: + self._log(" [-] 'TXM [Error]: CodeSignature' string not found") + return False + + refs = self.find_string_refs(str_off, *self.kern_text) + if not refs: + refs = self.find_string_refs(str_off) + if not refs: + self._log(" [-] no code refs") + return False + + for adrp_off, add_off, _ in refs: + # Scan forward past the BL (printf/log) for a TBNZ + for scan in range(add_off, min(add_off + 0x40, self.size), 4): + insns = self._disas_at(scan) + if not insns: + continue + if insns[0].mnemonic == "tbnz": + self.emit(scan, NOP, + f"NOP {insns[0].mnemonic} {insns[0].op_str} " + "[txm post-validation]") + return True + + self._log(" [-] TBNZ not found after TXM error string ref") + return False + + def patch_post_validation_cmp(self): + """Patch 9: cmp w0,w0 in postValidation (AMFI code signing). + + The 'AMFI: code signature validation failed' string is in the CALLER + function, not in postValidation itself. We find the caller, collect + its BL targets, then look inside each target for CMP W0, #imm + B.NE. + """ + self._log("\n[9] postValidation: cmp w0,w0 (AMFI code signing)") + + str_off = self.find_string(b"AMFI: code signature validation failed") + if str_off < 0: + self._log(" [-] string not found") + return False + + refs = self.find_string_refs(str_off, *self.amfi_text) + if not refs: + refs = self.find_string_refs(str_off) + if not refs: + self._log(" [-] no code refs") + return False + + caller_start = self.find_function_start(refs[0][0]) + if caller_start < 0: + self._log(" [-] caller function start not found") + return False + + # Collect unique BL targets from the caller function + # Only stop at PACIBSP (new function), not at ret/retab (early returns) + bl_targets = set() + for scan in range(caller_start, min(caller_start + 0x2000, self.size), 4): + if scan > caller_start + 8 and _rd32(self.raw, scan) == _PACIBSP_U32: + break + target = self._is_bl(scan) + if target >= 0: + bl_targets.add(target) + + # In each BL target in AMFI, look for: BL ... ; CMP W0, #imm ; B.NE + # The CMP must check W0 (return value of preceding BL call). + for target in sorted(bl_targets): + if not (self.amfi_text[0] <= target < self.amfi_text[1]): + continue + for off in range(target, min(target + 0x200, self.size), 4): + if off > target + 8 and _rd32(self.raw, off) == _PACIBSP_U32: + break + dis = self._disas_at(off, 2) + if len(dis) < 2: + continue + i0, i1 = dis[0], dis[1] + if i0.mnemonic != "cmp" or i1.mnemonic != "b.ne": + continue + # Must be CMP W0, #imm (first operand = w0, second = immediate) + ops = i0.operands + if len(ops) < 2: + continue + if ops[0].type != ARM64_OP_REG or ops[0].reg != ARM64_REG_W0: + continue + if ops[1].type != ARM64_OP_IMM: + continue + # Must be preceded by a BL within 2 instructions + has_bl = False + for gap in (4, 8): + if self._is_bl(off - gap) >= 0: + has_bl = True + break + if not has_bl: + continue + self.emit(off, CMP_W0_W0, + f"cmp w0,w0 (was {i0.mnemonic} {i0.op_str}) " + "[postValidation]") + return True + + self._log(" [-] CMP+B.NE pattern not found in caller's BL targets") + return False + + def patch_check_dyld_policy(self): + """Patches 10-11: Replace two BL calls in _check_dyld_policy_internal with mov w0,#1. + + The function is found via its reference to the Swift Playgrounds + entitlement string. The two BLs immediately preceding that string + reference (each followed by a conditional branch on w0) are patched. + """ + self._log("\n[10-11] _check_dyld_policy_internal: mov w0,#1 (two BLs)") + + # Anchor: entitlement string referenced from within the function + str_off = self.find_string( + b"com.apple.developer.swift-playgrounds-app.development-build") + if str_off < 0: + self._log(" [-] swift-playgrounds entitlement string not found") + return False + + refs = self.find_string_refs(str_off, *self.amfi_text) + if not refs: + refs = self.find_string_refs(str_off) + if not refs: + self._log(" [-] no code refs in AMFI") + return False + + for adrp_off, add_off, _ in refs: + # Walk backward from the ADRP, looking for BL + conditional-on-w0 pairs + bls_with_cond = [] # [(bl_off, bl_target), ...] + for back in range(adrp_off - 4, max(adrp_off - 80, 0), -4): + bl_target = self._is_bl(back) + if bl_target < 0: + continue + if self._is_cond_branch_w0(back + 4): + bls_with_cond.append((back, bl_target)) + + if len(bls_with_cond) >= 2: + bl2_off, bl2_tgt = bls_with_cond[0] # closer to ADRP + bl1_off, bl1_tgt = bls_with_cond[1] # farther from ADRP + # The two BLs must call DIFFERENT functions — this + # distinguishes _check_dyld_policy_internal from other + # functions that repeat calls to the same helper. + if bl1_tgt == bl2_tgt: + continue + self.emit(bl1_off, MOV_W0_1, + "mov w0,#1 (was BL) [_check_dyld_policy_internal @1]") + self.emit(bl2_off, MOV_W0_1, + "mov w0,#1 (was BL) [_check_dyld_policy_internal @2]") + return True + + self._log(" [-] _check_dyld_policy_internal BL pair not found") + return False + + def _find_validate_root_hash_func(self): + """Find validate_on_disk_root_hash function via 'authenticate_root_hash' string.""" + str_off = self.find_string(b"authenticate_root_hash") + if str_off < 0: + return -1 + refs = self.find_string_refs(str_off, *self.apfs_text) + if not refs: + return -1 + return self.find_function_start(refs[0][0]) + + def patch_apfs_graft(self): + """Patch 12: Replace BL to validate_on_disk_root_hash with mov w0,#0. + + Instead of stubbing _apfs_graft at entry, find the specific BL + that calls the root hash validation and neutralize just that call. + """ + self._log("\n[12] _apfs_graft: mov w0,#0 (validate_root_hash BL)") + + # Find _apfs_graft function + exact = self.raw.find(b"\x00apfs_graft\x00") + if exact < 0: + self._log(" [-] 'apfs_graft' string not found") + return False + str_off = exact + 1 + + refs = self.find_string_refs(str_off, *self.apfs_text) + if not refs: + self._log(" [-] no code refs") + return False + + graft_start = self.find_function_start(refs[0][0]) + if graft_start < 0: + self._log(" [-] _apfs_graft function start not found") + return False + + # Find validate_on_disk_root_hash function + vrh_func = self._find_validate_root_hash_func() + if vrh_func < 0: + self._log(" [-] validate_on_disk_root_hash not found") + return False + + # Scan _apfs_graft for BL to validate_on_disk_root_hash + # Don't stop at ret/retab (early returns) — only stop at PACIBSP (new function) + for scan in range(graft_start, min(graft_start + 0x2000, self.size), 4): + if scan > graft_start + 8 and _rd32(self.raw, scan) == _PACIBSP_U32: + break + bl_target = self._is_bl(scan) + if bl_target == vrh_func: + self.emit(scan, MOV_W0_0, "mov w0,#0 [_apfs_graft]") + return True + + self._log(" [-] BL to validate_on_disk_root_hash not found in _apfs_graft") + return False + + def patch_apfs_vfsop_mount_cmp(self): + """Patch 13: cmp x0,x0 in _apfs_vfsop_mount (current_thread == kernel_task check). + + The target CMP follows the pattern: BL (returns current_thread in x0), + ADRP + LDR + LDR (load kernel_task global), CMP x0, Xm, B.EQ. + We require x0 as the first CMP operand to distinguish it from other + CMP Xn,Xm instructions in the same function. + """ + self._log("\n[13] _apfs_vfsop_mount: cmp x0,x0 (mount rw check)") + + refs_upgrade = self._find_by_string_in_range( + b"apfs_mount_upgrade_checks\x00", + self.apfs_text, "apfs_mount_upgrade_checks") + if not refs_upgrade: + return False + + func_start = self.find_function_start(refs_upgrade[0][0]) + if func_start < 0: + return False + + # Find BL callers of _apfs_mount_upgrade_checks + callers = self.bl_callers.get(func_start, []) + if not callers: + for off_try in [func_start, func_start + 4]: + callers = self.bl_callers.get(off_try, []) + if callers: + break + + if not callers: + self._log(" [-] no BL callers of _apfs_mount_upgrade_checks found") + for off in range(self.apfs_text[0], self.apfs_text[1], 4): + bl_target = self._is_bl(off) + if bl_target >= 0 and func_start <= bl_target <= func_start + 4: + callers.append(off) + + for caller_off in callers: + if not (self.apfs_text[0] <= caller_off < self.apfs_text[1]): + continue + # Scan a wider range — the CMP can be 0x800+ bytes before the BL + caller_func = self.find_function_start(caller_off) + scan_start = caller_func if caller_func >= 0 else max(caller_off - 0x800, self.apfs_text[0]) + scan_end = min(caller_off + 0x100, self.apfs_text[1]) + + for scan in range(scan_start, scan_end, 4): + dis = self._disas_at(scan) + if not dis or dis[0].mnemonic != "cmp": + continue + ops = dis[0].operands + if len(ops) < 2: + continue + # Require CMP Xn, Xm (both register operands) + if ops[0].type != ARM64_OP_REG or ops[1].type != ARM64_OP_REG: + continue + # Require x0 as first operand (return value from BL) + if ops[0].reg != ARM64_REG_X0: + continue + # Skip CMP x0, x0 (already patched or trivial) + if ops[0].reg == ops[1].reg: + continue + self.emit(scan, CMP_X0_X0, + f"cmp x0,x0 (was {dis[0].mnemonic} {dis[0].op_str}) " + "[_apfs_vfsop_mount]") + return True + + self._log(" [-] CMP x0,Xm not found near mount_upgrade_checks caller") + return False + + def patch_apfs_mount_upgrade_checks(self): + """Patch 14: Replace TBNZ w0,#0xe with mov w0,#0 in _apfs_mount_upgrade_checks. + + Within the function, a BL calls a small flag-reading leaf function, + then TBNZ w0,#0xe branches to the error path. Replace the TBNZ + with mov w0,#0 to force the success path. + """ + self._log("\n[14] _apfs_mount_upgrade_checks: mov w0,#0 (tbnz bypass)") + + refs = self._find_by_string_in_range( + b"apfs_mount_upgrade_checks\x00", + self.apfs_text, "apfs_mount_upgrade_checks") + if not refs: + return False + + func_start = self.find_function_start(refs[0][0]) + if func_start < 0: + self._log(" [-] function start not found") + return False + + # Scan for BL followed by TBNZ w0 + # Don't stop at ret/retab (early returns) — only stop at PACIBSP (new function) + for scan in range(func_start, min(func_start + 0x200, self.size), 4): + if scan > func_start + 8 and _rd32(self.raw, scan) == _PACIBSP_U32: + break + bl_target = self._is_bl(scan) + if bl_target < 0: + continue + # Check if BL target is a small leaf function (< 0x20 bytes, ends with ret) + is_leaf = False + for k in range(0, 0x20, 4): + if bl_target + k >= self.size: + break + dis = self._disas_at(bl_target + k) + if dis and dis[0].mnemonic == "ret": + is_leaf = True + break + if not is_leaf: + continue + # Check next instruction is TBNZ w0, #0xe + next_off = scan + 4 + insns = self._disas_at(next_off) + if not insns: + continue + i = insns[0] + if i.mnemonic == "tbnz" and len(i.operands) >= 1: + if (i.operands[0].type == ARM64_OP_REG and + i.operands[0].reg == ARM64_REG_W0): + self.emit(next_off, MOV_W0_0, + "mov w0,#0 [_apfs_mount_upgrade_checks]") + return True + + self._log(" [-] BL + TBNZ w0 pattern not found") + return False + + def _find_validate_payload_manifest_func(self): + """Find the AppleImage4 validate_payload_and_manifest function.""" + str_off = self.find_string(b"validate_payload_and_manifest") + if str_off < 0: + return -1 + refs = self.find_string_refs(str_off, *self.apfs_text) + if not refs: + return -1 + return self.find_function_start(refs[0][0]) + + def patch_handle_fsioc_graft(self): + """Patch 15: Replace BL to validate_payload_and_manifest with mov w0,#0. + + Instead of stubbing _handle_fsioc_graft at entry, find the specific + BL that calls AppleImage4 validation and neutralize just that call. + """ + self._log("\n[15] _handle_fsioc_graft: mov w0,#0 (validate BL)") + + exact = self.raw.find(b"\x00handle_fsioc_graft\x00") + if exact < 0: + self._log(" [-] 'handle_fsioc_graft' string not found") + return False + str_off = exact + 1 + + refs = self.find_string_refs(str_off, *self.apfs_text) + if not refs: + self._log(" [-] no code refs") + return False + + fsioc_start = self.find_function_start(refs[0][0]) + if fsioc_start < 0: + self._log(" [-] function start not found") + return False + + # Find the validation function + val_func = self._find_validate_payload_manifest_func() + if val_func < 0: + self._log(" [-] validate_payload_and_manifest not found") + return False + + # Scan _handle_fsioc_graft for BL to validation function + for scan in range(fsioc_start, min(fsioc_start + 0x400, self.size), 4): + insns = self._disas_at(scan) + if not insns: + continue + if scan > fsioc_start + 8 and insns[0].mnemonic == "pacibsp": + break + bl_target = self._is_bl(scan) + if bl_target == val_func: + self.emit(scan, MOV_W0_0, "mov w0,#0 [_handle_fsioc_graft]") + return True + + self._log(" [-] BL to validate_payload_and_manifest not found") + return False + + # ── Sandbox MACF hooks ─────────────────────────────────────── + + def _find_sandbox_ops_table_via_conf(self): + """Find Sandbox mac_policy_ops table via mac_policy_conf struct.""" + self._log("\n[*] Finding Sandbox mac_policy_ops via mac_policy_conf...") + + seatbelt_off = self.find_string(b"Seatbelt sandbox policy") + sandbox_raw = self.raw.find(b"\x00Sandbox\x00") + sandbox_off = sandbox_raw + 1 if sandbox_raw >= 0 else -1 + if seatbelt_off < 0 or sandbox_off < 0: + self._log(" [-] Sandbox/Seatbelt strings not found") + return None + self._log(f" [*] Sandbox string at foff 0x{sandbox_off:X}, " + f"Seatbelt at 0x{seatbelt_off:X}") + + data_ranges = [] + for name, vmaddr, fileoff, filesize, prot in self.all_segments: + if name in ("__DATA_CONST", "__DATA") and filesize > 0: + data_ranges.append((fileoff, fileoff + filesize)) + + for d_start, d_end in data_ranges: + for i in range(d_start, d_end - 40, 8): + val = _rd64(self.raw, i) + if val == 0 or (val & (1 << 63)): + continue + if (val & 0x7FFFFFFFFFF) != sandbox_off: + continue + val2 = _rd64(self.raw, i + 8) + if (val2 & (1 << 63)) or (val2 & 0x7FFFFFFFFFF) != seatbelt_off: + continue + val_ops = _rd64(self.raw, i + 32) + if not (val_ops & (1 << 63)): + ops_off = val_ops & 0x7FFFFFFFFFF + self._log(f" [+] mac_policy_conf at foff 0x{i:X}, " + f"mpc_ops -> 0x{ops_off:X}") + return ops_off + + self._log(" [-] mac_policy_conf not found") + return None + + def _read_ops_entry(self, table_off, index): + """Read a function pointer from the ops table, handling chained fixups.""" + off = table_off + index * 8 + if off + 8 > self.size: + return -1 + val = _rd64(self.raw, off) + if val == 0: + return 0 + return self._decode_chained_ptr(val) + + def patch_sandbox_hooks(self): + """Patches 16-25: Stub Sandbox MACF hooks with mov x0,#0; ret. + + Uses mac_policy_ops struct indices from XNU source (xnu-11215+). + """ + self._log("\n[16-25] Sandbox MACF hooks") + + ops_table = self._find_sandbox_ops_table_via_conf() + if ops_table is None: + return False + + HOOK_INDICES = { + "file_check_mmap": 36, + "mount_check_mount": 87, + "mount_check_remount": 88, + "mount_check_umount": 91, + "vnode_check_rename": 120, + } + + sb_start, sb_end = self.sandbox_text + patched_count = 0 + + for hook_name, idx in HOOK_INDICES.items(): + func_off = self._read_ops_entry(ops_table, idx) + if func_off is None or func_off <= 0: + self._log(f" [-] ops[{idx}] {hook_name}: NULL or invalid") + continue + if not (sb_start <= func_off < sb_end): + self._log(f" [-] ops[{idx}] {hook_name}: foff 0x{func_off:X} " + f"outside Sandbox (0x{sb_start:X}-0x{sb_end:X})") + continue + + self.emit(func_off, MOV_X0_0, f"mov x0,#0 [_hook_{hook_name}]") + self.emit(func_off + 4, RET, f"ret [_hook_{hook_name}]") + self._log(f" [+] ops[{idx}] {hook_name} at foff 0x{func_off:X}") + patched_count += 1 + + return patched_count > 0 + + # ═══════════════════════════════════════════════════════════════ + # Main entry point + # ═══════════════════════════════════════════════════════════════ + + def find_all(self): + """Find and record all kernel patches. Returns list of (offset, bytes, desc).""" + self.patch_apfs_root_snapshot() # 1 + self.patch_apfs_seal_broken() # 2 + self.patch_bsd_init_rootvp() # 3 + self.patch_proc_check_launch_constraints() # 4-5 + self.patch_PE_i_can_has_debugger() # 6-7 + self.patch_post_validation_nop() # 8 + self.patch_post_validation_cmp() # 9 + self.patch_check_dyld_policy() # 10-11 + self.patch_apfs_graft() # 12 + self.patch_apfs_vfsop_mount_cmp() # 13 + self.patch_apfs_mount_upgrade_checks() # 14 + self.patch_handle_fsioc_graft() # 15 + self.patch_sandbox_hooks() # 16-25 + return self.patches + + def apply(self): + """Find all patches and apply them to self.data. Returns patch count.""" + patches = self.find_all() + for off, patch_bytes, desc in patches: + self.data[off:off + len(patch_bytes)] = patch_bytes + + if self.verbose and patches: + self._log(f"\n{'═'*60}") + self._log(f"VERIFICATION: {len(patches)} patches applied") + self._log(f"{'═'*60}") + for off, patch_bytes, desc in sorted(patches): + insns = self._disas_n(self.data, off, len(patch_bytes) // 4) + if insns: + dis_str = "; ".join(f"{i.mnemonic} {i.op_str}" for i in insns) + else: + dis_str = "???" + self._log(f" 0x{off:08X}: {dis_str:40s} — {desc}") + + return len(patches) + + +# ── CLI entry point ────────────────────────────────────────────── +if __name__ == "__main__": + import sys, argparse + + parser = argparse.ArgumentParser( + description="Dynamic kernel patcher — find & apply patches on iOS kernelcaches") + parser.add_argument("kernelcache", help="Path to raw or IM4P kernelcache") + parser.add_argument("-c", "--context", type=int, default=5, + help="Instructions of context before/after each patch (default: 5)") + parser.add_argument("-q", "--quiet", action="store_true", + help="Suppress index-building progress (only show patches)") + args = parser.parse_args() + + path = args.kernelcache + print(f"Loading {path}...") + file_raw = open(path, "rb").read() + + # Auto-detect IM4P vs raw Mach-O + if file_raw[:4] == b"\xcf\xfa\xed\xfe": + payload = file_raw + print(f" format: raw Mach-O") + else: + try: + from pyimg4 import IM4P + im4p = IM4P(file_raw) + if im4p.payload.compression: + im4p.payload.decompress() + payload = im4p.payload.data + print(f" format: IM4P (fourcc={im4p.fourcc})") + except Exception: + payload = file_raw + print(f" format: unknown (treating as raw)") + + data = bytearray(payload) + print(f" size: {len(data)} bytes ({len(data)/1024/1024:.1f} MB)\n") + + kp = KernelPatcher(data, verbose=not args.quiet) + patches = kp.find_all() + + # ── Print ranged before / after disassembly for every patch ── + ctx = args.context + + print(f"\n{'═'*72}") + print(f" {len(patches)} PATCHES — before / after disassembly (context={ctx})") + print(f"{'═'*72}") + + # Apply patches to get the "after" image + after = bytearray(kp.raw) # start from original + for off, pb, _ in patches: + after[off:off + len(pb)] = pb + + for i, (off, patch_bytes, desc) in enumerate(sorted(patches), 1): + n_insns = len(patch_bytes) // 4 + start = max(off - ctx * 4, 0) + end = off + n_insns * 4 + ctx * 4 + total = (end - start) // 4 + + before_insns = kp._disas_n(kp.raw, start, total) + after_insns = kp._disas_n(after, start, total) + + print(f"\n ┌{'─'*70}") + print(f" │ [{i:2d}] 0x{off:08X}: {desc}") + print(f" ├{'─'*34}┬{'─'*35}") + print(f" │ {'BEFORE':^33}│ {'AFTER':^34}") + print(f" ├{'─'*34}┼{'─'*35}") + + # Build line pairs + max_lines = max(len(before_insns), len(after_insns)) + for j in range(max_lines): + def fmt(insn): + if insn is None: + return " " * 33 + h = insn.bytes.hex() + return f"0x{insn.address:07X} {h:8s} {insn.mnemonic:6s} {insn.op_str}" + + bi = before_insns[j] if j < len(before_insns) else None + ai = after_insns[j] if j < len(after_insns) else None + + bl = fmt(bi) + al = fmt(ai) + + # Mark if this address is inside the patched range + addr = (bi.address if bi else ai.address) if (bi or ai) else 0 + in_patch = off <= addr < off + len(patch_bytes) + marker = " ◄" if in_patch else " " + + print(f" │ {bl:33s}│ {al:33s}{marker}") + + print(f" └{'─'*34}┴{'─'*35}") diff --git a/scripts/patchers/txm.py b/scripts/patchers/txm.py new file mode 100755 index 0000000..7fa69ad --- /dev/null +++ b/scripts/patchers/txm.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" +txm_patcher.py — Dynamic patcher for TXM (Trusted Execution Monitor) images. + +Finds the trustcache hash lookup (binary search) in the AMFI certificate +verification function and bypasses it. NO hardcoded offsets. + +Dependencies: keystone-engine, capstone +""" + +import struct +from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN + +# ── Assembly / disassembly singletons ────────────────────────── +_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE) +_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) +_cs.detail = True +_cs.skipdata = True + + +def _asm(s): + enc, _ = _ks.asm(s) + if not enc: + raise RuntimeError(f"asm failed: {s}") + return bytes(enc) + + +MOV_X0_0 = _asm("mov x0, #0") + + +def _disasm_one(data, off): + insns = list(_cs.disasm(data[off:off + 4], off)) + return insns[0] if insns else None + + +def _find_asm_pattern(data, asm_str): + enc, _ = _ks.asm(asm_str) + pattern = bytes(enc) + results = [] + off = 0 + while True: + idx = data.find(pattern, off) + if idx < 0: + break + results.append(idx) + off = idx + 4 + return results + + +# ── TXMPatcher ───────────────────────────────────────────────── + +class TXMPatcher: + """Dynamic patcher for TXM images. + + Patches: + 1. Trustcache binary-search BL → mov x0, #0 + (in the AMFI cert verification function identified by the + unique constant 0x20446 loaded into w19) + """ + + def __init__(self, data, verbose=True): + self.data = data + self.raw = bytes(data) + self.size = len(data) + self.verbose = verbose + self.patches = [] + + def _log(self, msg): + if self.verbose: + print(msg) + + def emit(self, off, patch_bytes, desc): + self.patches.append((off, patch_bytes, desc)) + if self.verbose: + before_insns = list(_cs.disasm(self.raw[off:off + 4], off)) + after_insns = list(_cs.disasm(patch_bytes, off)) + b_str = (f"{before_insns[0].mnemonic} {before_insns[0].op_str}" + if before_insns else "???") + a_str = (f"{after_insns[0].mnemonic} {after_insns[0].op_str}" + if after_insns else "???") + print(f" 0x{off:06X}: {b_str} → {a_str} [{desc}]") + + def apply(self): + self.find_all() + for off, pb, _ in self.patches: + self.data[off:off + len(pb)] = pb + if self.verbose and self.patches: + self._log(f"\n [{len(self.patches)} TXM patches applied]") + return len(self.patches) + + def find_all(self): + self.patches = [] + self.patch_trustcache_bypass() + return self.patches + + # ═══════════════════════════════════════════════════════════ + # Trustcache bypass + # + # The AMFI cert verification function has a unique constant: + # mov w19, #0x2446; movk w19, #2, lsl #16 (= 0x20446) + # + # Within that function, a binary search calls a hash-compare + # function with SHA-1 size: + # mov w2, #0x14; bl ; cbz w0, + # followed by: + # tbnz w0, #0x1f, (sign bit = search direction) + # + # Patch: bl → mov x0, #0 + # This makes cbz always branch to , bypassing the + # trustcache lookup entirely. + # ═══════════════════════════════════════════════════════════ + def patch_trustcache_bypass(self): + # Step 1: Find the unique function marker (mov w19, #0x2446) + locs = _find_asm_pattern(self.raw, "mov w19, #0x2446") + if len(locs) != 1: + self._log(f" [-] TXM: expected 1 'mov w19, #0x2446', " + f"found {len(locs)}") + return + marker_off = locs[0] + + # Step 2: Find the containing function (scan back for PACIBSP) + pacibsp = _asm("hint #27") + func_start = None + for scan in range(marker_off & ~3, max(0, marker_off - 0x200), -4): + if self.raw[scan:scan + 4] == pacibsp: + func_start = scan + break + if func_start is None: + self._log(" [-] TXM: function start not found") + return + + # Step 3: Within the function, find mov w2, #0x14; bl; cbz w0; tbnz w0, #0x1f + func_end = min(func_start + 0x2000, self.size) + insns = list(_cs.disasm(self.raw[func_start:func_end], func_start)) + + for i, ins in enumerate(insns): + if not (ins.mnemonic == 'mov' and ins.op_str == 'w2, #0x14'): + continue + if i + 3 >= len(insns): + continue + bl_ins = insns[i + 1] + cbz_ins = insns[i + 2] + tbnz_ins = insns[i + 3] + if (bl_ins.mnemonic == 'bl' + and cbz_ins.mnemonic == 'cbz' and 'w0' in cbz_ins.op_str + and tbnz_ins.mnemonic in ('tbnz', 'tbz') + and '#0x1f' in tbnz_ins.op_str): + self.emit(bl_ins.address, MOV_X0_0, + "trustcache bypass: bl → mov x0, #0") + return + + self._log(" [-] TXM: binary search pattern not found in function") + + +# ── CLI entry point ──────────────────────────────────────────── +if __name__ == "__main__": + import sys, argparse + + parser = argparse.ArgumentParser( + description="Dynamic TXM patcher") + parser.add_argument("txm", help="Path to raw or IM4P TXM image") + parser.add_argument("-q", "--quiet", action="store_true") + args = parser.parse_args() + + print(f"Loading {args.txm}...") + file_raw = open(args.txm, "rb").read() + + try: + from pyimg4 import IM4P + im4p = IM4P(file_raw) + if im4p.payload.compression: + im4p.payload.decompress() + payload = im4p.payload.data + print(f" format: IM4P (fourcc={im4p.fourcc})") + except Exception: + payload = file_raw + print(f" format: raw") + + data = bytearray(payload) + print(f" size: {len(data)} bytes ({len(data)/1024:.1f} KB)\n") + + patcher = TXMPatcher(data, verbose=not args.quiet) + n = patcher.apply() + print(f"\n {n} patches applied.") diff --git a/scripts/patches/libirecovery-pcc-vm.patch b/scripts/patches/libirecovery-pcc-vm.patch new file mode 100644 index 0000000..ad14266 --- /dev/null +++ b/scripts/patches/libirecovery-pcc-vm.patch @@ -0,0 +1,13 @@ +diff --git a/src/libirecovery.c b/src/libirecovery.c +index bf9a0d6..1323891 100644 +--- a/src/libirecovery.c ++++ b/src/libirecovery.c +@@ -480,6 +480,8 @@ static struct irecv_device irecv_devices[] = { + /* Apple Vision Pro */ + { "RealityDevice14,1", "n301ap", 0x42, 0x8112, "Apple Vision Pro" }, + { "RealityDevice17,1", "n301aap", 0x42, 0x8142, "Apple Vision Pro (M5)" }, ++ /* Private Cloud Compute Research Environment */ ++ { "iPhone99,11", "vresearch101ap", 0x90, 0xFE01, "iPhone 99,11" }, + { NULL, NULL, -1, -1, NULL } + }; + diff --git a/Scripts/build_ramdisk.py b/scripts/ramdisk_build.py old mode 100644 new mode 100755 similarity index 93% rename from Scripts/build_ramdisk.py rename to scripts/ramdisk_build.py index b1dc587..385239c --- a/Scripts/build_ramdisk.py +++ b/scripts/ramdisk_build.py @@ -22,23 +22,29 @@ Prerequisites: import gzip import glob import os +import plistlib import shutil import struct import subprocess import sys import tempfile +# Ensure sibling modules (patch_firmware) are importable when run from any CWD +_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +if _SCRIPT_DIR not in sys.path: + sys.path.insert(0, _SCRIPT_DIR) + from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE from pyimg4 import IM4P -from patch_firmware import ( +from fw_patch import ( load_firmware, _save_im4p_with_payp, patch_txm, find_restore_dir, find_file, - IBOOT_BASE, ) +from patchers.iboot import IBootPatcher # ══════════════════════════════════════════════════════════════════ # ARM64 assembler @@ -76,18 +82,10 @@ CFW_DIR = os.path.expanduser( # Ramdisk boot-args RAMDISK_BOOT_ARGS = b"serial=3 rd=md0 debug=0x2014e -v wdt=-1 %s" -# Normal boot-args (what patch_firmware.py sets) — used to find & replace -NORMAL_BOOT_ARGS = b"serial=3 -v debug=0x2014e %s" - # IM4P fourccs for restore mode TXM_FOURCC = "trxm" KERNEL_FOURCC = "rkrn" -# iBEC boot-args patch offsets (vresearch101 26.1) -IBEC_BOOTARGS_ADRP_OFF = 0x122D4 -IBEC_BOOTARGS_ADD_OFF = 0x122D8 -IBEC_BOOTARGS_STR_OFF = 0x24070 - # Files to remove from ramdisk to save space RAMDISK_REMOVE = [ "usr/bin/img4tool", "usr/bin/img4", @@ -118,8 +116,7 @@ def setup_input(vm_dir): return input_dir # Look for archive next to this script, then in vm_dir - script_dir = os.path.dirname(os.path.abspath(__file__)) - for search_dir in (script_dir, vm_dir): + for search_dir in (os.path.join(_SCRIPT_DIR, "resources"), _SCRIPT_DIR, vm_dir): archive = os.path.join(search_dir, INPUT_ARCHIVE) if os.path.isfile(archive): print(f" Extracting {INPUT_ARCHIVE}...") @@ -216,32 +213,26 @@ def create_im4p_uncompressed(raw_data, fourcc, description, output_path): def patch_ibec_bootargs(data): """Replace normal boot-args with ramdisk boot-args in already-patched iBEC. - Searches for the existing boot-args string and overwrites it. - Also patches ADRP+ADD to point to the string at the hardcoded offset, - ensuring consistent output regardless of where patch_firmware.py wrote it. + Finds the boot-args string written by patch_firmware.py (via IBootPatcher) + and overwrites it in-place. No hardcoded offsets needed — the ADRP+ADD + instructions already point to the string location. """ - # Patch ADRP+ADD x2 to point to IBEC_BOOTARGS_STR_OFF - adrp_pc = IBOOT_BASE + IBEC_BOOTARGS_ADRP_OFF - target = IBOOT_BASE + IBEC_BOOTARGS_STR_OFF - target_page = target & ~0xFFF + normal_args = IBootPatcher.BOOT_ARGS + off = data.find(normal_args) + if off < 0: + print(f" [-] boot-args: existing string not found ({normal_args.decode()!r})") + return False - adrp_insn = asm_u32(f"adrp x2, 0x{target_page:x}", adrp_pc) - add_insn = asm_u32(f"add x2, x2, #{target & 0xFFF}") - - struct.pack_into(" "{RAMDISK_BOOT_ARGS.decode()}" at 0x{IBEC_BOOTARGS_STR_OFF:X}') + print(f' boot-args -> "{RAMDISK_BOOT_ARGS.decode()}" at 0x{off:X}') return True @@ -251,7 +242,12 @@ def patch_ibec_bootargs(data): def build_ramdisk(restore_dir, im4m_path, vm_dir, input_dir, output_dir, temp_dir): """Build custom SSH ramdisk from restore DMG.""" - ramdisk_src = find_file(restore_dir, ["043-53775-129.dmg"], "ramdisk DMG") + # Read RestoreRamDisk path dynamically from BuildManifest.plist + bm_path = os.path.join(restore_dir, "BuildManifest.plist") + with open(bm_path, "rb") as f: + bm = plistlib.load(f) + ramdisk_rel = bm["BuildIdentities"][0]["Manifest"]["RestoreRamDisk"]["Info"]["Path"] + ramdisk_src = os.path.join(restore_dir, ramdisk_rel) mountpoint = os.path.join(vm_dir, "SSHRD") ramdisk_raw = os.path.join(temp_dir, "ramdisk.raw.dmg") ramdisk_custom = os.path.join(temp_dir, "ramdisk1.dmg") diff --git a/Scripts/ramdisk_send.sh b/scripts/ramdisk_send.sh similarity index 57% rename from Scripts/ramdisk_send.sh rename to scripts/ramdisk_send.sh index 5fcdceb..f8143db 100755 --- a/Scripts/ramdisk_send.sh +++ b/scripts/ramdisk_send.sh @@ -7,11 +7,12 @@ # SPTM, TXM, trustcache, ramdisk, device tree, SEP, and kernel. set -euo pipefail +IRECOVERY="${IRECOVERY:-irecovery}" RAMDISK_DIR="${1:-Ramdisk}" if [ ! -d "$RAMDISK_DIR" ]; then echo "[-] Ramdisk directory not found: $RAMDISK_DIR" - echo " Run build_ramdisk.py first." + echo " Run 'make ramdisk_build' first." exit 1 fi @@ -19,48 +20,48 @@ echo "[*] Sending ramdisk from $RAMDISK_DIR ..." # 1. Load iBSS + iBEC (DFU → recovery) echo " [1/8] Loading iBSS..." -irecovery -f "$RAMDISK_DIR/iBSS.vresearch101.RELEASE.img4" +"$IRECOVERY" -f "$RAMDISK_DIR/iBSS.vresearch101.RELEASE.img4" echo " [2/8] Loading iBEC..." -irecovery -f "$RAMDISK_DIR/iBEC.vresearch101.RELEASE.img4" -irecovery -c go +"$IRECOVERY" -f "$RAMDISK_DIR/iBEC.vresearch101.RELEASE.img4" +"$IRECOVERY" -c go sleep 1 # 2. Load SPTM echo " [3/8] Loading SPTM..." -irecovery -f "$RAMDISK_DIR/sptm.vresearch1.release.img4" -irecovery -c firmware +"$IRECOVERY" -f "$RAMDISK_DIR/sptm.vresearch1.release.img4" +"$IRECOVERY" -c firmware # 3. Load TXM echo " [4/8] Loading TXM..." -irecovery -f "$RAMDISK_DIR/txm.img4" -irecovery -c firmware +"$IRECOVERY" -f "$RAMDISK_DIR/txm.img4" +"$IRECOVERY" -c firmware # 4. Load trustcache echo " [5/8] Loading trustcache..." -irecovery -f "$RAMDISK_DIR/trustcache.img4" -irecovery -c firmware +"$IRECOVERY" -f "$RAMDISK_DIR/trustcache.img4" +"$IRECOVERY" -c firmware # 5. Load ramdisk echo " [6/8] Loading ramdisk..." -irecovery -f "$RAMDISK_DIR/ramdisk.img4" +"$IRECOVERY" -f "$RAMDISK_DIR/ramdisk.img4" sleep 2 -irecovery -c ramdisk +"$IRECOVERY" -c ramdisk # 6. Load device tree echo " [7/8] Loading device tree..." -irecovery -f "$RAMDISK_DIR/DeviceTree.vphone600ap.img4" -irecovery -c devicetree +"$IRECOVERY" -f "$RAMDISK_DIR/DeviceTree.vphone600ap.img4" +"$IRECOVERY" -c devicetree # 7. Load SEP echo " [8/8] Loading SEP..." -irecovery -f "$RAMDISK_DIR/sep-firmware.vresearch101.RELEASE.img4" -irecovery -c firmware +"$IRECOVERY" -f "$RAMDISK_DIR/sep-firmware.vresearch101.RELEASE.img4" +"$IRECOVERY" -c firmware # 8. Load kernel and boot echo " [*] Booting kernel..." -irecovery -f "$RAMDISK_DIR/krnl.img4" -irecovery -c bootx +"$IRECOVERY" -f "$RAMDISK_DIR/krnl.img4" +"$IRECOVERY" -c bootx echo "[+] Boot sequence complete. Device should be booting into ramdisk." diff --git a/scripts/setup_libimobiledevice.sh b/scripts/setup_libimobiledevice.sh new file mode 100755 index 0000000..d9ab647 --- /dev/null +++ b/scripts/setup_libimobiledevice.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# setup_libimobiledevice.sh — Build libimobiledevice toolchain (static) +# +# Produces: idevicerestore, irecovery, and related idevice* tools +# Prefix: .limd/ (override with LIMD_PREFIX env var) +# Requires: autoconf automake libtool pkg-config cmake git + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +PREFIX="${LIMD_PREFIX:-$PROJECT_DIR/.limd}" +SRC="$PREFIX/src" +LOG="$PREFIX/log" + +NPROC="$(sysctl -n hw.logicalcpu)" +SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" + +export PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" +export CFLAGS="-mmacosx-version-min=14.0 -isysroot $SDKROOT" +export CPPFLAGS="$CFLAGS" +export LDFLAGS="-mmacosx-version-min=14.0" + +mkdir -p "$SRC" "$LOG" + +# ── Helpers ────────────────────────────────────────────────────── + +die() { echo "[-] $*" >&2; exit 1; } + +check_tools() { + local missing=() + for cmd in autoconf automake libtool pkg-config cmake git; do + command -v "$cmd" &>/dev/null || missing+=("$cmd") + done + (( ${#missing[@]} == 0 )) || die "Missing: ${missing[*]} — brew install ${missing[*]}" +} + +clone() { + local url=$1 dir=$2 + if [[ -d "$dir/.git" ]]; then + git -C "$dir" fetch --depth 1 origin --quiet + git -C "$dir" reset --hard FETCH_HEAD --quiet + git -C "$dir" clean -fdx --quiet + else + git clone --depth 1 "$url" "$dir" --quiet + fi +} + +build_lib() { + local name=$1; shift + echo " $name" + cd "$SRC/$name" + ./autogen.sh --prefix="$PREFIX" \ + --enable-shared=no --enable-static=yes \ + "$@" > "$LOG/$name-configure.log" 2>&1 + make -j"$NPROC" > "$LOG/$name-build.log" 2>&1 + make install > "$LOG/$name-install.log" 2>&1 + cd "$SRC" +} + +# ── Preflight ──────────────────────────────────────────────────── + +check_tools +echo "Building libimobiledevice toolchain → $PREFIX" +echo "" + +# ── 1. OpenSSL (static) ───────────────────────────────────────── + +echo "[1/4] OpenSSL" +OPENSSL_TAG=$(curl -sS "https://api.github.com/repos/openssl/openssl/releases/latest" \ + | grep '"tag_name"' | cut -d'"' -f4) +if [[ ! -d "$SRC/openssl/.git" ]]; then + git clone --depth 1 --branch "$OPENSSL_TAG" \ + "https://github.com/openssl/openssl" "$SRC/openssl" --quiet +else + cd "$SRC/openssl" + git fetch --depth 1 origin tag "$OPENSSL_TAG" --quiet 2>/dev/null || true + git checkout "$OPENSSL_TAG" --quiet 2>/dev/null || true + git clean -fdx --quiet + cd "$SRC" +fi +echo " openssl ($OPENSSL_TAG)" +cd "$SRC/openssl" +./config --prefix="$PREFIX" no-shared no-tests \ + > "$LOG/openssl-configure.log" 2>&1 +make -j"$NPROC" > "$LOG/openssl-build.log" 2>&1 +make install_sw > "$LOG/openssl-install.log" 2>&1 +cd "$SRC" + +# ── 2. Core libraries ─────────────────────────────────────────── + +echo "[2/4] Core libraries" +for lib in libplist libimobiledevice-glue libusbmuxd libtatsu libimobiledevice; do + clone "https://github.com/libimobiledevice/$lib" "$SRC/$lib" + case "$lib" in + libplist|libimobiledevice) build_lib "$lib" --without-cython ;; + *) build_lib "$lib" ;; + esac +done + +# ── 3. libirecovery (+ PCC research VM patch) ─────────────────── + +echo "[3/4] libirecovery + libzip" +clone "https://github.com/libimobiledevice/libirecovery" "$SRC/libirecovery" + +# PR #150: register iPhone99,11 / vresearch101ap for PCC research VMs +if ! grep -q 'vresearch101ap' "$SRC/libirecovery/src/libirecovery.c"; then + cd "$SRC/libirecovery" + git apply "$SCRIPT_DIR/patches/libirecovery-pcc-vm.patch" \ + || die "Failed to apply libirecovery PCC patch — check context" + cd "$SRC" +fi +build_lib libirecovery + +# ── libzip (static, for idevicerestore) ────────────────────────── + +LIBZIP_VER="1.11.4" +if [[ ! -f "$PREFIX/lib/pkgconfig/libzip.pc" ]]; then + echo " libzip" + [[ -d "$SRC/libzip-$LIBZIP_VER" ]] || \ + curl -LfsS "https://github.com/nih-at/libzip/releases/download/v$LIBZIP_VER/libzip-$LIBZIP_VER.tar.gz" \ + | tar xz -C "$SRC" + cmake -S "$SRC/libzip-$LIBZIP_VER" -B "$SRC/libzip-$LIBZIP_VER/build" \ + -DCMAKE_INSTALL_PREFIX="$PREFIX" -DCMAKE_OSX_SYSROOT="$SDKROOT" \ + -DBUILD_SHARED_LIBS=OFF -DBUILD_DOC=OFF -DBUILD_EXAMPLES=OFF \ + -DBUILD_REGRESS=OFF -DBUILD_TOOLS=OFF \ + -DENABLE_BZIP2=OFF -DENABLE_LZMA=OFF -DENABLE_ZSTD=OFF \ + -DENABLE_GNUTLS=OFF -DENABLE_MBEDTLS=OFF -DENABLE_OPENSSL=OFF \ + > "$LOG/libzip-cmake.log" 2>&1 + cmake --build "$SRC/libzip-$LIBZIP_VER/build" -j"$NPROC" \ + > "$LOG/libzip-build.log" 2>&1 + cmake --install "$SRC/libzip-$LIBZIP_VER/build" \ + > "$LOG/libzip-install.log" 2>&1 +fi + +# ── 4. idevicerestore ─────────────────────────────────────────── + +echo "[4/4] idevicerestore" +clone "https://github.com/libimobiledevice/idevicerestore" "$SRC/idevicerestore" +build_lib idevicerestore \ + libcurl_CFLAGS="-I$SDKROOT/usr/include" \ + libcurl_LIBS="-lcurl" \ + libcurl_VERSION="$(/usr/bin/curl-config --version | cut -d' ' -f2)" \ + zlib_CFLAGS="-I$SDKROOT/usr/include" \ + zlib_LIBS="-lz" \ + zlib_VERSION="1.2" + +# ── Done ───────────────────────────────────────────────────────── + +echo "" +echo "Installed to $PREFIX/bin/:" +ls "$PREFIX/bin/" | sed 's/^/ /' diff --git a/scripts/setup_venv.sh b/scripts/setup_venv.sh new file mode 100755 index 0000000..b0a831e --- /dev/null +++ b/scripts/setup_venv.sh @@ -0,0 +1,80 @@ +#!/bin/zsh +# setup_venv.sh — Create a self-contained Python venv at project root. +# +# Installs all dependencies including the keystone native library. +# Requires: python3, clang, Homebrew keystone (brew install keystone) +# +# Usage: +# make setup_venv +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +VENV_DIR="${PROJECT_ROOT}/.venv" +REQUIREMENTS="${PROJECT_ROOT}/requirements.txt" + +# Use system Python3 +PYTHON="$(command -v python3)" +if [[ -z "${PYTHON}" ]]; then + echo "Error: python3 not found in PATH" + exit 1 +fi + +echo "=== Creating venv ===" +echo " Python: ${PYTHON} ($(${PYTHON} --version 2>&1))" +echo " venv: ${VENV_DIR}" +echo " deps: ${REQUIREMENTS}" +echo "" + +# Create venv from system Python +"${PYTHON}" -m venv "${VENV_DIR}" + +# Activate and install pip packages +source "${VENV_DIR}/bin/activate" +pip install --upgrade pip > /dev/null +pip install -r "${REQUIREMENTS}" + +# --- Build keystone native library --- +# The keystone-engine pip package is Python bindings only. +# It needs libkeystone.dylib at runtime. Homebrew ships only the static .a, +# so we build a dylib from it and place it inside the venv. +echo "" +echo "=== Building keystone dylib ===" + +KEYSTONE_STATIC="$(find /opt/homebrew/Cellar/keystone -name 'libkeystone.a' -type f 2>/dev/null | head -1)" +if [[ -z "${KEYSTONE_STATIC}" ]]; then + echo "Error: libkeystone.a not found. Install with: brew install keystone" + exit 1 +fi + +PYVER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" +KS_PKG_DIR="${VENV_DIR}/lib/python${PYVER}/site-packages/keystone" +KS_DYLIB="${KS_PKG_DIR}/libkeystone.dylib" + +echo " static lib: ${KEYSTONE_STATIC}" +echo " dylib dest: ${KS_DYLIB}" + +clang -shared -o "${KS_DYLIB}" \ + -Wl,-all_load "${KEYSTONE_STATIC}" \ + -lc++ \ + -install_name @rpath/libkeystone.dylib + +echo " dylib built OK" + +# --- Verify --- +echo "" +echo "=== Verifying imports ===" +python3 -c " +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN +from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN +from pyimg4 import IM4P +print(' capstone OK') +print(' keystone OK') +print(' pyimg4 OK') +" + +echo "" +echo "=== venv ready ===" +echo " Activate: source ${VENV_DIR}/bin/activate" +echo " Deactivate: deactivate" diff --git a/scripts/vm_create.sh b/scripts/vm_create.sh new file mode 100755 index 0000000..1d59e17 --- /dev/null +++ b/scripts/vm_create.sh @@ -0,0 +1,137 @@ +#!/bin/zsh +# vm_create.sh — Create a new vphone VM directory with all required files. +# +# Mirrors the vrevm VM creation process: +# 1. Create VM directory structure +# 2. Create sparse disk image (default 64 GB) +# 3. Create SEP storage (512 KB flat file) +# 4. Copy AVPBooter and AVPSEPBooter ROMs +# +# machineIdentifier and NVRAM are auto-created on first boot by vphone-cli. +# +# Usage: +# make vm_new # Create VM/ with framework ROMs +# make vm_new VM_DIR=MyVM # Custom directory name +# make vm_new DISK_SIZE=32 # 32 GB disk +set -euo pipefail + +# --- Defaults --- +VM_DIR="vm" +DISK_SIZE_GB=64 +SEP_STORAGE_SIZE=$((512 * 1024)) # 512 KB (same as vrevm) + +# Framework-bundled ROMs (vresearch1 / research1 chip) +FW_ROM_DIR="/System/Library/Frameworks/Virtualization.framework/Versions/A/Resources" +ROM_SRC="${FW_ROM_DIR}/AVPBooter.vresearch1.bin" +SEPROM_SRC="${FW_ROM_DIR}/AVPSEPBooter.vresearch1.bin" + +# --- Parse args --- +while [[ $# -gt 0 ]]; do + case "$1" in + --dir) VM_DIR="$2"; shift 2 ;; + --disk-size) DISK_SIZE_GB="$2"; shift 2 ;; + --rom) ROM_SRC="$2"; shift 2 ;; + --seprom) SEPROM_SRC="$2"; shift 2 ;; + -h|--help) + echo "Usage: $0 [--dir VM] [--disk-size 64] [--rom path] [--seprom path]" + echo "" + echo "Options:" + echo " --dir VM directory name (default: VM)" + echo " --disk-size Disk image size in GB (default: 64)" + echo " --rom Path to AVPBooter ROM (default: framework built-in)" + echo " --seprom Path to AVPSEPBooter ROM (default: framework built-in)" + exit 0 + ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +DISK_SIZE_BYTES=$((DISK_SIZE_GB * 1024 * 1024 * 1024)) + +echo "=== vphone create_vm ===" +echo "Directory : ${VM_DIR}" +echo "Disk size : ${DISK_SIZE_GB} GB" +echo "AVPBooter : ${ROM_SRC}" +echo "AVPSEPBooter: ${SEPROM_SRC}" +echo "" + +# --- Validate ROM sources --- +if [[ ! -f "${ROM_SRC}" ]]; then + echo "ERROR: AVPBooter ROM not found: ${ROM_SRC}" + echo " On Apple Internal macOS, this should be at:" + echo " ${FW_ROM_DIR}/AVPBooter.vresearch1.bin" + exit 1 +fi + +if [[ ! -f "${SEPROM_SRC}" ]]; then + echo "ERROR: AVPSEPBooter ROM not found: ${SEPROM_SRC}" + echo " On Apple Internal macOS, this should be at:" + echo " ${FW_ROM_DIR}/AVPSEPBooter.vresearch1.bin" + exit 1 +fi + +# --- Create VM directory --- +if [[ -d "${VM_DIR}" ]]; then + echo "WARNING: ${VM_DIR}/ already exists" + # Check for existing disk to avoid accidental overwrite + if [[ -f "${VM_DIR}/Disk.img" ]]; then + echo " Disk.img already exists — skipping disk creation" + echo " Delete ${VM_DIR}/Disk.img manually to recreate" + fi +else + echo "[1/4] Creating ${VM_DIR}/" + mkdir -p "${VM_DIR}" +fi + +# --- Create sparse disk image --- +if [[ ! -f "${VM_DIR}/Disk.img" ]]; then + echo "[2/4] Creating sparse disk image (${DISK_SIZE_GB} GB)" + # Use dd with seek to create a sparse file (same approach as vrevm) + dd if=/dev/zero of="${VM_DIR}/Disk.img" bs=1 count=0 seek="${DISK_SIZE_BYTES}" 2>/dev/null + echo " -> ${VM_DIR}/Disk.img ($(du -h "${VM_DIR}/Disk.img" | cut -f1) on disk)" +else + echo "[2/4] Disk.img exists — skipping" +fi + +# --- Create SEP storage --- +if [[ ! -f "${VM_DIR}/SEPStorage" ]]; then + echo "[3/4] Creating SEP storage (512 KB)" + dd if=/dev/zero of="${VM_DIR}/SEPStorage" bs=1 count="${SEP_STORAGE_SIZE}" 2>/dev/null +else + echo "[3/4] SEPStorage exists — skipping" +fi + +# --- Copy ROMs --- +echo "[4/4] Copying ROMs" + +ROM_DST="${VM_DIR}/AVPBooter.vresearch1.bin" +SEPROM_DST="${VM_DIR}/AVPSEPBooter.vresearch1.bin" + +if [[ -f "${ROM_DST}" ]] && cmp -s "${ROM_SRC}" "${ROM_DST}"; then + echo " AVPBooter.vresearch1.bin — up to date" +else + cp "${ROM_SRC}" "${ROM_DST}" + echo " AVPBooter.vresearch1.bin — copied ($(wc -c < "${ROM_DST}" | tr -d ' ') bytes)" +fi + +if [[ -f "${SEPROM_DST}" ]] && cmp -s "${SEPROM_SRC}" "${SEPROM_DST}"; then + echo " AVPSEPBooter.vresearch1.bin — up to date" +else + cp "${SEPROM_SRC}" "${SEPROM_DST}" + echo " AVPSEPBooter.vresearch1.bin — copied ($(wc -c < "${SEPROM_DST}" | tr -d ' ') bytes)" +fi + +# --- Create .gitkeep --- +touch "${VM_DIR}/.gitkeep" + +echo "" +echo "=== VM created at ${VM_DIR}/ ===" +echo "" +echo "Contents:" +ls -lh "${VM_DIR}/" +echo "" +echo "Next steps:" +echo " 1. Prepare firmware: make fw_prepare" +echo " 2. Patch firmware: make fw_patch" +echo " 3. Boot DFU: make boot_dfu" +echo " 4. Boot normal: make boot" diff --git a/Sources/vphone-cli/VPhoneCLI.swift b/sources/vphone-cli/VPhoneCLI.swift similarity index 100% rename from Sources/vphone-cli/VPhoneCLI.swift rename to sources/vphone-cli/VPhoneCLI.swift diff --git a/Sources/vphone-cli/VPhoneHardwareModel.swift b/sources/vphone-cli/VPhoneHardwareModel.swift similarity index 100% rename from Sources/vphone-cli/VPhoneHardwareModel.swift rename to sources/vphone-cli/VPhoneHardwareModel.swift diff --git a/Sources/vphone-cli/VPhoneVM.swift b/sources/vphone-cli/VPhoneVM.swift similarity index 100% rename from Sources/vphone-cli/VPhoneVM.swift rename to sources/vphone-cli/VPhoneVM.swift diff --git a/Sources/vphone-cli/VPhoneVMWindow.swift b/sources/vphone-cli/VPhoneVMWindow.swift similarity index 100% rename from Sources/vphone-cli/VPhoneVMWindow.swift rename to sources/vphone-cli/VPhoneVMWindow.swift diff --git a/Sources/VPhoneObjC/VPhoneObjC.m b/sources/vphone-objc/VPhoneObjC.m similarity index 100% rename from Sources/VPhoneObjC/VPhoneObjC.m rename to sources/vphone-objc/VPhoneObjC.m diff --git a/Sources/VPhoneObjC/include/VPhoneObjC.h b/sources/vphone-objc/include/VPhoneObjC.h similarity index 100% rename from Sources/VPhoneObjC/include/VPhoneObjC.h rename to sources/vphone-objc/include/VPhoneObjC.h diff --git a/vphone.entitlements b/sources/vphone.entitlements similarity index 100% rename from vphone.entitlements rename to sources/vphone.entitlements