Add vphone CLI, ObjC wrappers, and scripts

Introduce a new vphone virtual iPhone project:

- Add VPhoneObjC (.m/.h) providing Objective-C wrappers around private Virtualization.framework APIs (PV=3 hardware model creation, bootloader ROM setting, NVRAM helpers, PL011 serial port, SEP coprocessor, debug/panic devices, and production-mode toggle).
- Add Swift CLI and VM implementation (VPhoneCLI.swift, VPhoneVM.swift, VPhoneHardwareModel.swift) to configure and boot a PV=3 VM, capture serial console, start in DFU, and manage SEP/storage. Includes validation and minimal device config (graphics, storage, networking).
- Add helper scripts (build_and_sign.sh, boot.sh, boot_dfu.sh) to build, codesign with entitlements, and launch the VM.
- Add vphone.entitlements enabling the required private virtualization entitlements.

Notes: this targets macOS 15+ and requires appropriate entitlements and disabled SIP/AMFI to use private virtualization APIs.

Create .gitignore

Update README.md

Update README.md

Update README.md

README: add demo image and fix formatting

Add demo.png and embed it in the README; clean up markdown and code snippet formatting, remove stray backticks/bold markers, normalize list bullets, fix a resource path (vrevm), tidy whitespace/trailing chars, and add an Acknowledgements section. These changes improve readability and correctness of the setup instructions.

Create LICENSE

Create README.md

Add Package.swift; use interactive serial console

Add a Swift Package manifest for vphone-cli (macOS v14) with targets VPhoneObjC and vphone-cli, dependency on swift-argument-parser, and necessary linker/swift settings. Modify VPhoneVM to remove the intermediate Pipe-based serial capture: attach the PL011 serial port directly to FileHandle.standardInput/standardOutput for an interactive console, remove the readabilityHandler-based console capture and related plumbing, and update log/print messages accordingly.

Add CFW installer and patching tools

Add scripts and input archive to install a custom firmware (CFW) on vphone via an SSH ramdisk. Includes:
- Scripts/install_cfw.sh: zsh installer that extracts Cryptex DMGs from a restore, mounts device filesystems, copies Cryptexes, installs GPU driver and iosbinpack64, patches system binaries, injects LaunchDaemons and configures persistent SSH/VNC. Idempotent, caches decrypted DMGs and signs patched binaries with provided tools.
- Scripts/patch_cfw.py: Python tool using capstone/keystone to locate and patch binaries (seputil, launchd_cache_loader, mobileactivationd), parse BuildManifest for Cryptex paths, and inject daemon plists.
- Scripts/cfw_input.tar.zst: bundled input resources (Cryptexs, daemons, signing cert, and helper tools).
The installer requires ipsw, aea, python3 with capstone/keystone-engine and is designed to be safe to re-run. Intended to automate CFW deployment and runtime patches on the target device.

Add ramdisk build and deployment scripts

Add Scripts/build_ramdisk.py to build a signed SSH ramdisk for vphone600 from a patched restore set: it extracts firmware components, patches iBEC boot-args, repacks/signs IM4P/IMG4 artifacts (iBSS, iBEC, TXM, kernel, DeviceTree, SEP, trustcache, ramdisk) using an IM4M from an SHSH blob. Include a prepackaged ramdisk_input.tar.zst with tooling/resources and a helper Scripts/ramdisk_send.sh to load the generated IMG4 files to a device via irecovery in the correct order. The Python script expects firmware patched by patch_firmware.py and requires keystone-engine, capstone, pyimg4, and the pyimg4 CLI; usage and temp/output directories are documented at the top of the script.

Add firmware prepare and patch scripts

Add two tools for building and modifying a hybrid restore image for vphone600:

Scripts/prepare_firmware.sh
- Bash helper to download an iPhone IPSW and a cloudOS IPSW, extract them, merge cloudOS boot components into the iPhone restore directory, and generate hybrid BuildManifest.plist and Restore.plist tailored for vresearch101/vphone600 use-cases.
- Produces a ready Restore directory. Usage: ./prepare_firmware.sh [iphone_ipsw_url] [cloudos_url]

Scripts/patch_firmware.py
- Python tool that runs after prepare_firmware.sh to patch boot-chain components (AVPBooter, iBSS, iBEC, LLB, TXM, kernelcache).
- Auto-detects IM4P vs raw payloads, supports recompressing/repacking IM4P (preserving PAYP metadata when required), and applies a variety of binary patches (image4 callback bypass, serial labels, boot-args injection, trustcache/ DGST bypasses, many kernelcache fixes).
- Implements assembler/disassembler helpers (keystone/capstone) and multiple heuristics for locating patch sites.
- Usage: python3 patch_firmware.py [vm_directory]
- Dependencies: keystone-engine, capstone, pyimg4 (pip install keystone-engine capstone pyimg4)

These scripts automate preparing a hybrid restore and applying the required boot-chain patches for research devices.

Update demo.png

Create boot_sweet.sh

Add multi-touch support and VM window

Enable multi-touch input and a GUI VM window: add ObjC helpers to configure a _VZUSBTouchScreenConfiguration, create _VZTouch objects via KVC (workaround for init crash), build _VZMultiTouchEvent instances, and send/get multi-touch devices. Expose these APIs in the VPhoneObjC header and invoke VPhoneConfigureMultiTouch from VPhoneVM before VM start. Add a VPhoneVMWindow implementing a touch-enabled VZVirtualMachineView that maps mouse/right-click/drag events to multi-touch phases (with edge detection for swipe aim) and a window controller to show the VM. Also update the CLI to present the window in GUI mode.
This commit is contained in:
Lakr
2026-02-27 00:56:38 +09:00
commit ddd9b9d83c
23 changed files with 4933 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.tar.zst filter=lfs diff=lfs merge=lfs -text

309
.gitignore vendored Normal file
View File

@@ -0,0 +1,309 @@
# General
.DS_Store
__MACOSX/
.AppleDouble
.LSOverride
Icon[
]
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
## User settings
xcuserdata/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml

13
LICENSE Normal file
View File

@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2026 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

39
Package.swift Normal file
View File

@@ -0,0 +1,39 @@
// swift-tools-version:5.10
import PackageDescription
let package = Package(
name: "vphone-cli",
platforms: [
.macOS(.v14),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"),
],
targets: [
// ObjC module: wraps private Virtualization.framework APIs
.target(
name: "VPhoneObjC",
path: "Sources/VPhoneObjC",
publicHeadersPath: "include",
linkerSettings: [
.linkedFramework("Virtualization"),
],
),
// Swift executable
.executableTarget(
name: "vphone-cli",
dependencies: [
"VPhoneObjC",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
swiftSettings: [
.unsafeFlags(["-parse-as-library"]),
],
linkerSettings: [
.linkedFramework("Virtualization"),
.linkedFramework("AppKit"),
],
),
],
)

961
README.md Normal file
View File

@@ -0,0 +1,961 @@
# pcc-vmapple
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.
![poc](./demo.png)
## Prepare Development Environment
> **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.
### Reboot into Recovery Mode
`csrutil disable`
`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.ipswhttps://updates.cdn-apple.com/private-cloud-compute/399b664dd623358c3de118ffc114e42dcd51c9309e751d43bc949b98f4e31349)
- [https://updates.cdn-apple.com/private-cloud-compute/399b664dd623358c3de118ffc114e42dcd51c9309e751d43bc949b98f4e31349](https://updates.cdn-apple.com/2025FallFCS/fullrestores/089-13864/668EFC0E-5911-454C-96C6-E1063CB80042/iPhone17,3_26.1_23B85_Restore.ipswhttps://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.
```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 <rom> --disk <disk> --serial
➜ vphone-cli
```
```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 [<options>] --rom <rom> --disk <disk>
OPTIONS:
--rom <rom> Path to the AVPBooter / ROM binary
--disk <disk> Path to the disk image
--nvram <nvram> Path to NVRAM storage (created/overwritten) (default: nvram.bin)
--cpu <cpu> Number of CPU cores (default: 4)
--memory <memory> Memory size in MB (default: 4096)
--serial Allocate a PTY for serial console
--serial-path <serial-path>
Path to an existing serial device
--gdb-port <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 <sep-storage>
Path to SEP storage file (created if missing)
--sep-rom <sep-rom> Path to SEP ROM binary
--no-graphics Run without GUI (headless)
-h, --help Show help information.
```
### Prepare VM Boot Firmware
Create a folder to store these files.
```bash
➜ vphone-cli tree VM
├── AVPBooter.vresearch1.bin
├── AVPSEPBooter.vresearch1.bin
├── AuxiliaryStorage
├── Disk.img
├── SEPStorage
└── config.plist
1 directory, 6 files
```
- 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
```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
```
Please confirm the Chip ID in the System Information.
```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
```
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" }, `
```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
```
At this point, you can query the virtual machine for device hardware information.
```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
```
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: <private>
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 <vol> (List all snapshots)
snaputil -c <snap> <vol> (Create snapshot)
snaputil -n <snap> <newname> <vol> (Rename snapshot)
snaputil -d <snap> <vol> (Delete snapshot)
snaputil -r <snap> <vol> (Revert to snapshot)
snaputil -s <snap> <vol> <mntpnt> (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
<Omitted>
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) <Notice>: Doing boot task
com.apple.xpc.launchd|2026-02-26 05:34:50.948556 (finish-demo-restore) <Notice>: Doing boot task
com.apple.xpc.launchd|2026-02-26 05:34:50.951290 (sysstatuscheck) <Notice>: Doing boot task
com.apple.xpc.launchd|2026-02-26 05:34:50.953692 (prng_seedctl) <Notice>: Doing boot task
com.apple.xpc.launchd|2026-02-26 05:34:50.956821 (launchd_cache_loader) <Notice>: Doing boot task
com.apple.xpc.launchd|2026-02-26 05:34:50.968980 (workload-properties-init) <Notice>: Doing boot task
com.apple.xpc.launchd|2026-02-26 05:34:50.968988 (init-exclavekit) <Notice>: Doing boot task
com.apple.xpc.launchd|2026-02-26 05:34:51.015964 (boot) <Notice>: Early boot complete. Continuing system boot.
com.apple.xpc.launchd|2026-02-26 05:34:51.048686 <Notice>: Got first unlock unregistering for AKS events
bash-4.4#
```
After entering bash, you need to initialize the shell environment.
```bash
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'
/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.
```bash
iproxy 5901 5901
iproxy 22222 22222
```
## Appendix
### Boot pcc vm
```bash
pccvre release download --release 35622
pccvre instance create -N pcc-research -R 35622 --variant research
```
- <https://appledb.dev/firmware/cloudOS/23B85.html>
- <https://updates.cdn-apple.com/private-cloud-compute/399b664dd623358c3de118ffc114e42dcd51c9309e751d43bc949b98f4e31349>
```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
```
```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 <<PTR>>: bdev <<PTR>> type illb offset 0x20000 len 0x4cbe4
78faf5021313e82:74
78faf5021313e82:85
ae71af5ee32b84:129
=======================================
::
:: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 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. ========
```
## Acknowledgements
- [wh1te4ever/super-tart-vphone-writeup](https://github.com/wh1te4ever/super-tart-vphone-writeup)

519
Scripts/build_ramdisk.py Normal file
View File

@@ -0,0 +1,519 @@
#!/usr/bin/env python3
"""
build_ramdisk.py — Build a signed SSH ramdisk for vphone600.
Expects firmware already patched by patch_firmware.py.
Extracts patched components, signs with SHSH, and builds SSH ramdisk.
Usage:
python3 build_ramdisk.py [vm_directory]
Directory structure:
./shsh/ — SHSH blobs (auto-discovered)
./ramdisk_input/ — Tools and SSH resources (auto-setup from CFW)
./ramdisk_builder_temp/ — Intermediate .raw files (cleaned up)
./Ramdisk/ — Final signed IMG4 output
Prerequisites:
pip install keystone-engine capstone pyimg4
Run patch_firmware.py first to patch boot-chain components.
"""
import gzip
import glob
import os
import shutil
import struct
import subprocess
import sys
import tempfile
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
from pyimg4 import IM4P
from patch_firmware import (
load_firmware,
_save_im4p_with_payp,
patch_txm,
find_restore_dir,
find_file,
IBOOT_BASE,
)
# ══════════════════════════════════════════════════════════════════
# ARM64 assembler
# ══════════════════════════════════════════════════════════════════
_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE)
def asm(s, addr=0):
"""Assemble an ARM64 instruction string to bytes."""
enc, _ = _ks.asm(s, addr)
if not enc:
raise RuntimeError(f"asm failed: {s}")
return bytes(enc)
def asm_u32(s, addr=0):
"""Assemble an ARM64 instruction and return as little-endian u32."""
return struct.unpack("<I", asm(s, addr))[0]
# ══════════════════════════════════════════════════════════════════
# Configuration
# ══════════════════════════════════════════════════════════════════
OUTPUT_DIR = "Ramdisk"
TEMP_DIR = "ramdisk_builder_temp"
INPUT_DIR = "ramdisk_input"
# Default location to copy resources from
CFW_DIR = os.path.expanduser(
"~/Documents/GitHub/super-tart-vphone-private/CFW"
)
# 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",
"usr/sbin/dietappleh13camerad", "usr/sbin/dietappleh16camerad",
"usr/local/bin/wget", "usr/local/bin/procexp",
]
# Directories to re-sign in ramdisk
SIGN_DIRS = [
"usr/local/bin/*", "usr/local/lib/*",
"usr/bin/*", "bin/*",
"usr/lib/*", "sbin/*", "usr/sbin/*", "usr/libexec/*",
]
# Compressed archive of ramdisk_input/ (located next to this script)
INPUT_ARCHIVE = "ramdisk_input.tar.zst"
# ══════════════════════════════════════════════════════════════════
# Setup — extract ramdisk_input/ from zstd archive if needed
# ══════════════════════════════════════════════════════════════════
def setup_input(vm_dir):
"""Ensure ramdisk_input/ exists, extracting from .tar.zst if needed."""
input_dir = os.path.join(vm_dir, INPUT_DIR)
if os.path.isdir(input_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):
archive = os.path.join(search_dir, INPUT_ARCHIVE)
if os.path.isfile(archive):
print(f" Extracting {INPUT_ARCHIVE}...")
subprocess.run(
["tar", "--zstd", "-xf", archive, "-C", vm_dir],
check=True,
)
return input_dir
print(f"[-] Neither {INPUT_DIR}/ nor {INPUT_ARCHIVE} found.")
print(f" Place {INPUT_ARCHIVE} next to this script or in the VM directory.")
sys.exit(1)
# ══════════════════════════════════════════════════════════════════
# SHSH / signing helpers
# ══════════════════════════════════════════════════════════════════
def find_shsh(shsh_dir):
"""Find first SHSH blob in directory."""
for ext in ("*.shsh", "*.shsh2"):
matches = sorted(glob.glob(os.path.join(shsh_dir, ext)))
if matches:
return matches[0]
return None
def extract_im4m(shsh_path, im4m_path):
"""Extract IM4M manifest from SHSH blob (handles gzip-compressed)."""
raw = open(shsh_path, "rb").read()
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
tmp = shsh_path + ".tmp"
try:
open(tmp, "wb").write(raw)
subprocess.run(
["pyimg4", "im4m", "extract", "-i", tmp, "-o", im4m_path],
check=True, capture_output=True,
)
finally:
if os.path.exists(tmp):
os.remove(tmp)
def sign_img4(im4p_path, img4_path, im4m_path, tag=None, input_dir="."):
"""Create IMG4 from IM4P + IM4M. Uses tools/img4 for tag override."""
if tag:
img4_tool = os.path.join(input_dir, "tools/img4")
subprocess.run(
[img4_tool, "-i", im4p_path, "-o", img4_path,
"-M", im4m_path, "-T", tag],
check=True, capture_output=True,
)
else:
subprocess.run(
["pyimg4", "img4", "create",
"-p", im4p_path, "-o", img4_path, "-m", im4m_path],
check=True, capture_output=True,
)
def run(cmd, **kwargs):
"""Run a command, raising on failure."""
return subprocess.run(cmd, check=True, **kwargs)
# ══════════════════════════════════════════════════════════════════
# Firmware extraction and IM4P creation
# ══════════════════════════════════════════════════════════════════
def extract_to_raw(src_path, raw_path):
"""Extract IM4P payload to .raw file. Returns (im4p_obj, data, original_raw)."""
im4p, data, was_im4p, original_raw = load_firmware(src_path)
with open(raw_path, "wb") as f:
f.write(bytes(data))
return im4p, data, original_raw
def create_im4p_uncompressed(raw_data, fourcc, description, output_path):
"""Create uncompressed IM4P from raw data."""
new_im4p = IM4P(
fourcc=fourcc,
description=description,
payload=bytes(raw_data),
)
with open(output_path, "wb") as f:
f.write(new_im4p.output())
# ══════════════════════════════════════════════════════════════════
# iBEC boot-args patching
# ══════════════════════════════════════════════════════════════════
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.
"""
# 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
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("<I", data, IBEC_BOOTARGS_ADRP_OFF, adrp_insn)
struct.pack_into("<I", data, IBEC_BOOTARGS_ADD_OFF, add_insn)
# Write ramdisk boot-args string at the hardcoded offset
args = RAMDISK_BOOT_ARGS + b"\x00"
data[IBEC_BOOTARGS_STR_OFF:IBEC_BOOTARGS_STR_OFF + len(args)] = args
# Zero out any leftover from a longer previous string
end = IBEC_BOOTARGS_STR_OFF + len(args)
while end < len(data) and data[end] != 0:
data[end] = 0
end += 1
print(f' boot-args -> "{RAMDISK_BOOT_ARGS.decode()}" at 0x{IBEC_BOOTARGS_STR_OFF:X}')
return True
# ══════════════════════════════════════════════════════════════════
# Ramdisk DMG building
# ══════════════════════════════════════════════════════════════════
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")
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")
# Extract base ramdisk
print(" Extracting base ramdisk...")
run(["pyimg4", "im4p", "extract", "-i", ramdisk_src, "-o", ramdisk_raw],
capture_output=True)
os.makedirs(mountpoint, exist_ok=True)
try:
# Mount, create expanded copy
print(" Mounting base ramdisk...")
run(["sudo", "hdiutil", "attach", "-mountpoint", mountpoint,
ramdisk_raw, "-owners", "off"])
print(" Creating expanded ramdisk (254 MB)...")
run(["sudo", "hdiutil", "create", "-size", "254m",
"-imagekey", "diskimage-class=CRawDiskImage",
"-format", "UDZO", "-fs", "APFS", "-layout", "NONE",
"-srcfolder", mountpoint, "-copyuid", "root",
ramdisk_custom])
run(["sudo", "hdiutil", "detach", "-force", mountpoint])
# Mount expanded, inject SSH
print(" Mounting expanded ramdisk...")
run(["sudo", "hdiutil", "attach", "-mountpoint", mountpoint,
ramdisk_custom, "-owners", "off"])
print(" Injecting SSH tools...")
gtar = os.path.join(input_dir, "tools/gtar")
ssh_tar = os.path.join(input_dir, "ssh.tar.gz")
run(["sudo", gtar, "-x", "--no-overwrite-dir",
"-f", ssh_tar, "-C", mountpoint])
# Remove unnecessary files
for rel_path in RAMDISK_REMOVE:
full = os.path.join(mountpoint, rel_path)
if os.path.exists(full):
os.remove(full)
# Re-sign Mach-O binaries
print(" Re-signing Mach-O binaries...")
ldid = os.path.join(input_dir, "tools/ldid_macosx_arm64")
signcert = os.path.join(input_dir, "signcert.p12")
for pattern in SIGN_DIRS:
for path in glob.glob(os.path.join(mountpoint, pattern)):
if os.path.isfile(path) and not os.path.islink(path):
if "Mach-O" in subprocess.getoutput(f'file "{path}"'):
subprocess.run(
[ldid, "-S", "-M", f"-K{signcert}", path],
capture_output=True,
)
# Fix sftp-server entitlements
sftp_ents = os.path.join(input_dir, "sftp_server_ents.plist")
sftp_server = os.path.join(mountpoint, "usr/libexec/sftp-server")
if os.path.exists(sftp_server):
run([ldid, f"-S{sftp_ents}", "-M", f"-K{signcert}", sftp_server])
# Build trustcache
print(" Building trustcache...")
tc_tool = os.path.join(input_dir, "tools/trustcache_macos_arm64")
tc_raw = os.path.join(temp_dir, "sshrd.raw.tc")
tc_im4p = os.path.join(temp_dir, "trustcache.im4p")
run([tc_tool, "create", tc_raw, mountpoint])
run(["pyimg4", "im4p", "create", "-i", tc_raw, "-o", tc_im4p,
"-f", "rtsc"], capture_output=True)
sign_img4(tc_im4p, os.path.join(output_dir, "trustcache.img4"),
im4m_path, input_dir=input_dir)
print(f" [+] trustcache.img4")
finally:
subprocess.run(["sudo", "hdiutil", "detach", "-force", mountpoint],
capture_output=True)
# Shrink and sign ramdisk
run(["sudo", "hdiutil", "resize", "-sectors", "min", ramdisk_custom])
print(" Signing ramdisk...")
rd_im4p = os.path.join(temp_dir, "ramdisk.im4p")
run(["pyimg4", "im4p", "create", "-i", ramdisk_custom, "-o", rd_im4p,
"-f", "rdsk"], capture_output=True)
sign_img4(rd_im4p, os.path.join(output_dir, "ramdisk.img4"),
im4m_path, input_dir=input_dir)
print(f" [+] ramdisk.img4")
# ══════════════════════════════════════════════════════════════════
# Main
# ══════════════════════════════════════════════════════════════════
def main():
vm_dir = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.getcwd())
if not os.path.isdir(vm_dir):
print(f"[-] Not a directory: {vm_dir}")
sys.exit(1)
# Find SHSH
shsh_dir = os.path.join(vm_dir, "shsh")
shsh_path = find_shsh(shsh_dir)
if not shsh_path:
print(f"[-] No SHSH blob found in {shsh_dir}/")
print(" Place your .shsh file in the shsh/ directory.")
sys.exit(1)
# Find restore directory
restore_dir = find_restore_dir(vm_dir)
if not restore_dir:
print(f"[-] No *Restore* directory found in {vm_dir}")
sys.exit(1)
# Check pyimg4 CLI
try:
subprocess.run(["pyimg4", "--help"], capture_output=True, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
print("[-] pyimg4 CLI not found. Install with: pip install pyimg4")
sys.exit(1)
# Setup input resources (copy from CFW if needed)
print(f"[*] Setting up {INPUT_DIR}/...")
input_dir = setup_input(vm_dir)
# Create temp and output directories
temp_dir = os.path.join(vm_dir, TEMP_DIR)
output_dir = os.path.join(vm_dir, OUTPUT_DIR)
for d in (temp_dir, output_dir):
if os.path.exists(d):
shutil.rmtree(d)
os.makedirs(d)
print(f"[*] VM directory: {vm_dir}")
print(f"[*] Restore directory: {restore_dir}")
print(f"[*] SHSH blob: {shsh_path}")
# Extract IM4M from SHSH
im4m_path = os.path.join(temp_dir, "vphone.im4m")
print(f"\n[*] Extracting IM4M from SHSH...")
extract_im4m(shsh_path, im4m_path)
# ── 1. iBSS (already patched by patch_firmware.py) ───────────
print(f"\n{'=' * 60}")
print(f" 1. iBSS (already patched — extract & sign)")
print(f"{'=' * 60}")
ibss_src = find_file(restore_dir, [
"Firmware/dfu/iBSS.vresearch101.RELEASE.im4p",
], "iBSS")
ibss_raw = os.path.join(temp_dir, "iBSS.raw")
ibss_im4p = os.path.join(temp_dir, "iBSS.im4p")
im4p_obj, data, _ = extract_to_raw(ibss_src, ibss_raw)
create_im4p_uncompressed(data, im4p_obj.fourcc, im4p_obj.description, ibss_im4p)
sign_img4(ibss_im4p, os.path.join(output_dir, "iBSS.vresearch101.RELEASE.img4"),
im4m_path, input_dir=input_dir)
print(f" [+] iBSS.vresearch101.RELEASE.img4")
# ── 2. iBEC (already patched — just fix boot-args for ramdisk)
print(f"\n{'=' * 60}")
print(f" 2. iBEC (patch boot-args for ramdisk)")
print(f"{'=' * 60}")
ibec_src = find_file(restore_dir, [
"Firmware/dfu/iBEC.vresearch101.RELEASE.im4p",
], "iBEC")
ibec_raw = os.path.join(temp_dir, "iBEC.raw")
ibec_im4p = os.path.join(temp_dir, "iBEC.im4p")
im4p_obj, data, _ = extract_to_raw(ibec_src, ibec_raw)
patch_ibec_bootargs(data)
create_im4p_uncompressed(data, im4p_obj.fourcc, im4p_obj.description, ibec_im4p)
sign_img4(ibec_im4p, os.path.join(output_dir, "iBEC.vresearch101.RELEASE.img4"),
im4m_path, input_dir=input_dir)
print(f" [+] iBEC.vresearch101.RELEASE.img4")
# ── 3. SPTM (sign only) ─────────────────────────────────────
print(f"\n{'=' * 60}")
print(f" 3. SPTM (sign only)")
print(f"{'=' * 60}")
sptm_src = find_file(restore_dir, [
"Firmware/sptm.vresearch1.release.im4p",
], "SPTM")
sign_img4(sptm_src, os.path.join(output_dir, "sptm.vresearch1.release.img4"),
im4m_path, tag="sptm", input_dir=input_dir)
print(f" [+] sptm.vresearch1.release.img4")
# ── 4. DeviceTree (sign only) ────────────────────────────────
print(f"\n{'=' * 60}")
print(f" 4. DeviceTree (sign only)")
print(f"{'=' * 60}")
dt_src = find_file(restore_dir, [
"Firmware/all_flash/DeviceTree.vphone600ap.im4p",
], "DeviceTree")
sign_img4(dt_src, os.path.join(output_dir, "DeviceTree.vphone600ap.img4"),
im4m_path, tag="rdtr", input_dir=input_dir)
print(f" [+] DeviceTree.vphone600ap.img4")
# ── 5. SEP (sign only) ───────────────────────────────────────
print(f"\n{'=' * 60}")
print(f" 5. SEP (sign only)")
print(f"{'=' * 60}")
sep_src = find_file(restore_dir, [
"Firmware/all_flash/sep-firmware.vresearch101.RELEASE.im4p",
], "SEP")
sign_img4(sep_src, os.path.join(output_dir, "sep-firmware.vresearch101.RELEASE.img4"),
im4m_path, tag="rsep", input_dir=input_dir)
print(f" [+] sep-firmware.vresearch101.RELEASE.img4")
# ── 6. TXM (release variant — needs patching) ────────────────
print(f"\n{'=' * 60}")
print(f" 6. TXM (patch release variant)")
print(f"{'=' * 60}")
txm_src = find_file(restore_dir, [
"Firmware/txm.iphoneos.release.im4p",
], "TXM")
txm_raw = os.path.join(temp_dir, "txm.raw")
im4p_obj, data, original_raw = extract_to_raw(txm_src, txm_raw)
patch_txm(data)
txm_im4p = os.path.join(temp_dir, "txm.im4p")
_save_im4p_with_payp(txm_im4p, TXM_FOURCC, data, original_raw)
sign_img4(txm_im4p, os.path.join(output_dir, "txm.img4"),
im4m_path, input_dir=input_dir)
print(f" [+] txm.img4")
# ── 7. Kernelcache (already patched — repack with rkrn) ──────
print(f"\n{'=' * 60}")
print(f" 7. Kernelcache (already patched — repack as rkrn)")
print(f"{'=' * 60}")
kc_src = find_file(restore_dir, [
"kernelcache.research.vphone600",
], "kernelcache")
kc_raw = os.path.join(temp_dir, "kcache.raw")
im4p_obj, data, original_raw = extract_to_raw(kc_src, kc_raw)
print(f" format: IM4P, {len(data)} bytes")
kc_im4p = os.path.join(temp_dir, "krnl.im4p")
_save_im4p_with_payp(kc_im4p, KERNEL_FOURCC, data, original_raw)
sign_img4(kc_im4p, os.path.join(output_dir, "krnl.img4"),
im4m_path, input_dir=input_dir)
print(f" [+] krnl.img4")
# ── 8. Ramdisk + Trustcache ──────────────────────────────────
print(f"\n{'=' * 60}")
print(f" 8. Ramdisk + Trustcache")
print(f"{'=' * 60}")
build_ramdisk(restore_dir, im4m_path, vm_dir, input_dir, output_dir, temp_dir)
# ── Cleanup ──────────────────────────────────────────────────
print(f"\n[*] Cleaning up {TEMP_DIR}/...")
shutil.rmtree(temp_dir, ignore_errors=True)
sshrd_dir = os.path.join(vm_dir, "SSHRD")
if os.path.exists(sshrd_dir):
shutil.rmtree(sshrd_dir, ignore_errors=True)
# ── Summary ──────────────────────────────────────────────────
print(f"\n{'=' * 60}")
print(f" Ramdisk build complete!")
print(f" Output: {output_dir}/")
print(f"{'=' * 60}")
for f in sorted(os.listdir(output_dir)):
size = os.path.getsize(os.path.join(output_dir, f))
print(f" {f:45s} {size:>10,} bytes")
if __name__ == "__main__":
main()

348
Scripts/install_cfw.sh Executable file
View File

@@ -0,0 +1,348 @@
#!/bin/zsh
# install_cfw.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.
#
# Safe to run multiple times — always patches from original .bak files,
# keeps decrypted Cryptex DMGs cached, handles already-mounted filesystems.
#
# Prerequisites:
# - Device booted into SSH ramdisk (ramdisk_send.sh)
# - `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
#
# Usage: ./install_cfw.sh [vm_directory]
set -euo pipefail
VM_DIR="${1:-.}"
SCRIPT_DIR="${0:a:h}"
# Resolve absolute paths
VM_DIR="$(cd "$VM_DIR" && pwd)"
# ── Configuration ───────────────────────────────────────────────
CFW_INPUT="cfw_input"
CFW_ARCHIVE="cfw_input.tar.zst"
TEMP_DIR="$VM_DIR/.cfw_temp"
SSH_PORT=2222
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"
SSH_OPTS=(
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o PreferredAuthentications=password
-o ConnectTimeout=30
-q
)
# ── Helpers ─────────────────────────────────────────────────────
die() { echo "[-] $*" >&2; exit 1; }
_sshpass() {
"$VM_DIR/$CFW_INPUT/tools/sshpass" -p "$SSH_PASS" "$@"
}
ssh_cmd() {
_sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@"
}
scp_to() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" -r "$1" "$SSH_USER@$SSH_HOST:$2"
}
scp_from() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" "$SSH_USER@$SSH_HOST:$1" "$2"
}
remote_file_exists() {
ssh_cmd "test -f '$1'" 2>/dev/null
}
ldid_sign() {
local file="$1" bundle_id="${2:-}"
local args=(-S -M "-K$VM_DIR/$CFW_INPUT/signcert.p12")
[[ -n "$bundle_id" ]] && args+=("-I$bundle_id")
"$VM_DIR/$CFW_INPUT/tools/ldid_macosx_arm64" "${args[@]}" "$file"
}
# Detach a DMG mountpoint if currently mounted, ignore errors
safe_detach() {
local mnt="$1"
if mount | grep -q "$mnt"; then
sudo hdiutil detach -force "$mnt" 2>/dev/null || true
fi
}
# Mount device filesystem, tolerate already-mounted
remote_mount() {
local dev="$1" mnt="$2" opts="${3:-rw}"
ssh_cmd "/sbin/mount_apfs -o $opts $dev $mnt 2>/dev/null || true"
}
# ── Find restore directory ─────────────────────────────────────
find_restore_dir() {
for dir in "$VM_DIR"/iPhone*_Restore; do
[[ -f "$dir/BuildManifest.plist" ]] && echo "$dir" && return
done
die "No restore directory found in $VM_DIR"
}
# ── Setup input resources ──────────────────────────────────────
setup_cfw_input() {
[[ -d "$VM_DIR/$CFW_INPUT" ]] && return
local archive
for search_dir in "$SCRIPT_DIR" "$VM_DIR"; do
archive="$search_dir/$CFW_ARCHIVE"
if [[ -f "$archive" ]]; then
echo " Extracting $CFW_ARCHIVE..."
tar --zstd -xf "$archive" -C "$VM_DIR"
return
fi
done
die "Neither $CFW_INPUT/ nor $CFW_ARCHIVE found"
}
# ── Check prerequisites ────────────────────────────────────────
check_prereqs() {
command -v ipsw >/dev/null 2>&1 || die "'ipsw' not found. Install: brew install blacktop/tap/ipsw"
command -v aea >/dev/null 2>&1 || die "'aea' not found (requires macOS 12+)"
command -v python3 >/dev/null 2>&1 || die "python3 not found"
python3 -c "import capstone, keystone" 2>/dev/null \
|| die "Missing Python deps. Install: pip install capstone keystone-engine"
}
# ── Cleanup trap (unmount DMGs on error) ───────────────────────
cleanup_on_exit() {
safe_detach "$TEMP_DIR/mnt_sysos"
safe_detach "$TEMP_DIR/mnt_appos"
}
trap cleanup_on_exit EXIT
# ════════════════════════════════════════════════════════════════
# Main
# ════════════════════════════════════════════════════════════════
echo "[*] install_cfw.sh — Installing CFW on vphone..."
check_prereqs
RESTORE_DIR=$(find_restore_dir)
echo "[+] Restore directory: $RESTORE_DIR"
setup_cfw_input
INPUT_DIR="$VM_DIR/$CFW_INPUT"
echo "[+] Input resources: $INPUT_DIR"
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")
CRYPTEX_SYSOS=$(echo "$CRYPTEX_PATHS" | head -1)
CRYPTEX_APPOS=$(echo "$CRYPTEX_PATHS" | tail -1)
echo " SystemOS: $CRYPTEX_SYSOS"
echo " AppOS: $CRYPTEX_APPOS"
# ═══════════ 1/7 INSTALL CRYPTEX ══════════════════════════════
echo ""
echo "[1/7] Installing Cryptex (SystemOS + AppOS)..."
SYSOS_DMG="$TEMP_DIR/CryptexSystemOS.dmg"
APPOS_DMG="$TEMP_DIR/CryptexAppOS.dmg"
MNT_SYSOS="$TEMP_DIR/mnt_sysos"
MNT_APPOS="$TEMP_DIR/mnt_appos"
# Decrypt SystemOS AEA (cached — skip if already decrypted)
if [[ ! -f "$SYSOS_DMG" ]]; then
echo " Extracting AEA key..."
AEA_KEY=$(ipsw fw aea --key "$RESTORE_DIR/$CRYPTEX_SYSOS")
echo " key: $AEA_KEY"
echo " Decrypting SystemOS..."
aea decrypt -i "$RESTORE_DIR/$CRYPTEX_SYSOS" -o "$SYSOS_DMG" -key-value "$AEA_KEY"
else
echo " Using cached SystemOS DMG"
fi
# Copy AppOS (unencrypted, cached)
if [[ ! -f "$APPOS_DMG" ]]; then
cp "$RESTORE_DIR/$CRYPTEX_APPOS" "$APPOS_DMG"
else
echo " Using cached AppOS DMG"
fi
# Detach any leftover mounts from previous runs
safe_detach "$MNT_SYSOS"
safe_detach "$MNT_APPOS"
mkdir -p "$MNT_SYSOS" "$MNT_APPOS"
echo " Mounting SystemOS..."
sudo hdiutil attach -mountpoint "$MNT_SYSOS" "$SYSOS_DMG" -owners off
echo " Mounting AppOS..."
sudo hdiutil attach -mountpoint "$MNT_APPOS" "$APPOS_DMG" -owners off
# Mount device rootfs (tolerate already-mounted)
echo " Mounting device rootfs rw..."
remote_mount /dev/disk1s1 /mnt1
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"
# Copy Cryptex files to device
echo " Copying Cryptexes to device (this takes ~3 minutes)..."
scp_to "$MNT_SYSOS/." "/mnt1/System/Cryptexes/OS"
scp_to "$MNT_APPOS/." "/mnt1/System/Cryptexes/App"
# Create dyld symlinks (ln -sf is idempotent)
echo " Creating dyld symlinks..."
ssh_cmd "/bin/ln -sf ../../../System/Cryptexes/OS/System/Library/Caches/com.apple.dyld \
/mnt1/System/Library/Caches/com.apple.dyld"
ssh_cmd "/bin/ln -sf ../../../../System/Cryptexes/OS/System/DriverKit/System/Library/dyld \
/mnt1/System/DriverKit/System/Library/dyld"
# Unmount Cryptex DMGs
echo " Unmounting Cryptex DMGs..."
safe_detach "$MNT_SYSOS"
safe_detach "$MNT_APPOS"
echo " [+] Cryptex installed"
# ═══════════ 2/7 PATCH SEPUTIL ════════════════════════════════
echo ""
echo "[2/7] Patching seputil..."
# Always patch from .bak (original unpatched binary)
if ! remote_file_exists "/mnt1/usr/libexec/seputil.bak"; then
echo " Creating backup..."
ssh_cmd "/bin/cp /mnt1/usr/libexec/seputil /mnt1/usr/libexec/seputil.bak"
fi
scp_from "/mnt1/usr/libexec/seputil.bak" "$TEMP_DIR/seputil"
python3 "$SCRIPT_DIR/patch_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"
# Rename gigalocker (mv to same name is fine on re-run)
echo " Renaming gigalocker..."
remote_mount /dev/disk1s3 /mnt3
ssh_cmd '/bin/mv /mnt3/*.gl /mnt3/AA.gl 2>/dev/null || true'
echo " [+] seputil patched"
# ═══════════ 3/7 INSTALL GPU DRIVER ══════════════════════════
echo ""
echo "[3/7] Installing AppleParavirtGPUMetalIOGPUFamily..."
scp_to "$INPUT_DIR/custom/AppleParavirtGPUMetalIOGPUFamily.tar" "/mnt1"
ssh_cmd "/usr/bin/tar --preserve-permissions --no-overwrite-dir \
-xf /mnt1/AppleParavirtGPUMetalIOGPUFamily.tar -C /mnt1"
BUNDLE="/mnt1/System/Library/Extensions/AppleParavirtGPUMetalIOGPUFamily.bundle"
# Clean macOS resource fork files (._* files from tar xattrs)
ssh_cmd "find $BUNDLE -name '._*' -delete 2>/dev/null || true"
ssh_cmd "/usr/sbin/chown -R 0:0 $BUNDLE"
ssh_cmd "/bin/chmod 0755 $BUNDLE"
ssh_cmd "/bin/chmod 0755 $BUNDLE/libAppleParavirtCompilerPluginIOGPUFamily.dylib"
ssh_cmd "/bin/chmod 0755 $BUNDLE/AppleParavirtGPUMetalIOGPUFamily"
ssh_cmd "/bin/chmod 0755 $BUNDLE/_CodeSignature"
ssh_cmd "/bin/chmod 0644 $BUNDLE/_CodeSignature/CodeResources"
ssh_cmd "/bin/chmod 0644 $BUNDLE/Info.plist"
ssh_cmd "/bin/rm -f /mnt1/AppleParavirtGPUMetalIOGPUFamily.tar"
echo " [+] GPU driver installed"
# ═══════════ 4/7 INSTALL IOSBINPACK64 ════════════════════════
echo ""
echo "[4/7] Installing iosbinpack64..."
scp_to "$INPUT_DIR/jb/iosbinpack64.tar" "/mnt1"
ssh_cmd "/usr/bin/tar --preserve-permissions --no-overwrite-dir \
-xf /mnt1/iosbinpack64.tar -C /mnt1"
ssh_cmd "/bin/rm -f /mnt1/iosbinpack64.tar"
echo " [+] iosbinpack64 installed"
# ═══════════ 5/7 PATCH LAUNCHD_CACHE_LOADER ══════════════════
echo ""
echo "[5/7] Patching launchd_cache_loader..."
# Always patch from .bak (original unpatched binary)
if ! remote_file_exists "/mnt1/usr/libexec/launchd_cache_loader.bak"; then
echo " Creating backup..."
ssh_cmd "/bin/cp /mnt1/usr/libexec/launchd_cache_loader /mnt1/usr/libexec/launchd_cache_loader.bak"
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"
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"
echo " [+] launchd_cache_loader patched"
# ═══════════ 6/7 PATCH MOBILEACTIVATIOND ═════════════════════
echo ""
echo "[6/7] Patching mobileactivationd..."
# Always patch from .bak (original unpatched binary)
if ! remote_file_exists "/mnt1/usr/libexec/mobileactivationd.bak"; then
echo " Creating backup..."
ssh_cmd "/bin/cp /mnt1/usr/libexec/mobileactivationd /mnt1/usr/libexec/mobileactivationd.bak"
fi
scp_from "/mnt1/usr/libexec/mobileactivationd.bak" "$TEMP_DIR/mobileactivationd"
python3 "$SCRIPT_DIR/patch_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"
echo " [+] mobileactivationd patched"
# ═══════════ 7/7 LAUNCHDAEMONS + LAUNCHD.PLIST ══════════════
echo ""
echo "[7/7] Installing LaunchDaemons..."
# Send daemon plists (overwrite on re-run)
for plist in bash.plist dropbear.plist trollvnc.plist; do
scp_to "$INPUT_DIR/jb/LaunchDaemons/$plist" "/mnt1/System/Library/LaunchDaemons/"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/LaunchDaemons/$plist"
done
# Always patch launchd.plist from .bak (original)
echo " Patching launchd.plist..."
if ! remote_file_exists "/mnt1/System/Library/xpc/launchd.plist.bak"; then
echo " Creating backup..."
ssh_cmd "/bin/cp /mnt1/System/Library/xpc/launchd.plist /mnt1/System/Library/xpc/launchd.plist.bak"
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"
scp_to "$TEMP_DIR/launchd.plist" "/mnt1/System/Library/xpc/launchd.plist"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/xpc/launchd.plist"
echo " [+] LaunchDaemons installed"
# ═══════════ CLEANUP ═════════════════════════════════════════
echo ""
echo "[*] Unmounting device filesystems..."
ssh_cmd "/sbin/umount /mnt1 2>/dev/null || true"
ssh_cmd "/sbin/umount /mnt3 2>/dev/null || true"
# Keep .cfw_temp/Cryptex*.dmg cached (slow to re-create)
# Only remove temp binaries
echo "[*] Cleaning up temp binaries..."
rm -f "$TEMP_DIR/seputil" \
"$TEMP_DIR/launchd_cache_loader" \
"$TEMP_DIR/mobileactivationd" \
"$TEMP_DIR/launchd.plist"
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)"

740
Scripts/patch_cfw.py Normal file
View File

@@ -0,0 +1,740 @@
#!/usr/bin/env python3
"""
patch_cfw.py — Dynamic binary patching for CFW installation on vphone600.
Uses capstone for disassembly-based anchoring and keystone for instruction
assembly, producing reliable, upgrade-proof patches.
Called by install_cfw.sh during CFW installation.
Commands:
cryptex-paths <BuildManifest.plist>
Print SystemOS and AppOS DMG paths from BuildManifest.
patch-seputil <binary>
Patch seputil gigalocker UUID to "AA".
patch-launchd-cache-loader <binary>
NOP the cache validation check in launchd_cache_loader.
patch-mobileactivationd <binary>
Patch -[DeviceType should_hactivate] to always return true.
inject-daemons <launchd.plist> <daemon_dir>
Inject bash/dropbear/trollvnc into launchd.plist.
Dependencies:
pip install capstone keystone-engine
"""
import os
import plistlib
import re
import struct
import sys
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
# ══════════════════════════════════════════════════════════════════
# ARM64 assembler / disassembler
# ══════════════════════════════════════════════════════════════════
_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
_cs.detail = True
_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)
NOP = asm("nop")
MOV_X0_1 = asm("mov x0, #1")
RET = asm("ret")
def rd32(data, off):
return struct.unpack_from("<I", data, off)[0]
def wr32(data, off, val):
struct.pack_into("<I", data, off, val)
def disasm_at(data, off, n=8):
"""Disassemble n instructions at file offset."""
return list(_cs.disasm(bytes(data[off : off + n * 4]), off))
# ══════════════════════════════════════════════════════════════════
# Mach-O helpers
# ══════════════════════════════════════════════════════════════════
def parse_macho_sections(data):
"""Parse Mach-O 64-bit to extract section info.
Returns dict: "segment,section" -> (vm_addr, size, file_offset)
"""
magic = struct.unpack_from("<I", data, 0)[0]
if magic != 0xFEEDFACF:
raise ValueError(f"Not a 64-bit Mach-O (magic=0x{magic:X})")
ncmds = struct.unpack_from("<I", data, 16)[0]
sections = {}
offset = 32 # sizeof(mach_header_64)
for _ in range(ncmds):
cmd, cmdsize = struct.unpack_from("<II", data, offset)
if cmd == 0x19: # LC_SEGMENT_64
segname = data[offset + 8 : offset + 24].split(b"\x00")[0].decode()
nsects = struct.unpack_from("<I", data, offset + 64)[0]
sect_off = offset + 72
for _ in range(nsects):
sectname = (
data[sect_off : sect_off + 16].split(b"\x00")[0].decode()
)
addr = struct.unpack_from("<Q", data, sect_off + 32)[0]
size = struct.unpack_from("<Q", data, sect_off + 40)[0]
file_off = struct.unpack_from("<I", data, sect_off + 48)[0]
sections[f"{segname},{sectname}"] = (addr, size, file_off)
sect_off += 80
offset += cmdsize
return sections
def va_to_foff(data, va):
"""Convert virtual address to file offset using LC_SEGMENT_64 commands."""
ncmds = struct.unpack_from("<I", data, 16)[0]
offset = 32
for _ in range(ncmds):
cmd, cmdsize = struct.unpack_from("<II", data, offset)
if cmd == 0x19: # LC_SEGMENT_64
vmaddr = struct.unpack_from("<Q", data, offset + 24)[0]
vmsize = struct.unpack_from("<Q", data, offset + 32)[0]
fileoff = struct.unpack_from("<Q", data, offset + 40)[0]
if vmaddr <= va < vmaddr + vmsize:
return fileoff + (va - vmaddr)
offset += cmdsize
return -1
def find_section(sections, *candidates):
"""Find the first matching section from candidates."""
for name in candidates:
if name in sections:
return sections[name]
return None
def find_symtab(data):
"""Parse LC_SYMTAB from Mach-O header.
Returns (symoff, nsyms, stroff, strsize) or None.
"""
ncmds = struct.unpack_from("<I", data, 16)[0]
offset = 32
for _ in range(ncmds):
cmd, cmdsize = struct.unpack_from("<II", data, offset)
if cmd == 0x02: # LC_SYMTAB
symoff = struct.unpack_from("<I", data, offset + 8)[0]
nsyms = struct.unpack_from("<I", data, offset + 12)[0]
stroff = struct.unpack_from("<I", data, offset + 16)[0]
strsize = struct.unpack_from("<I", data, offset + 20)[0]
return symoff, nsyms, stroff, strsize
offset += cmdsize
return None
def find_symbol_va(data, name_fragment):
"""Search Mach-O symbol table for a symbol containing name_fragment.
Returns the symbol's VA, or -1 if not found.
"""
st = find_symtab(data)
if not st:
return -1
symoff, nsyms, stroff, strsize = st
for i in range(nsyms):
entry_off = symoff + i * 16 # sizeof(nlist_64)
n_strx = struct.unpack_from("<I", data, entry_off)[0]
n_value = struct.unpack_from("<Q", data, entry_off + 8)[0]
if n_strx >= strsize or n_value == 0:
continue
# Read null-terminated symbol name
end = data.index(0, stroff + n_strx)
sym_name = data[stroff + n_strx : end].decode("ascii", errors="replace")
if name_fragment in sym_name:
return n_value
return -1
# ══════════════════════════════════════════════════════════════════
# 1. seputil — Gigalocker UUID patch
# ══════════════════════════════════════════════════════════════════
def patch_seputil(filepath):
"""Dynamically find and patch the gigalocker path format string in seputil.
Anchor: The format string "/%s.gl" used by seputil to construct the
gigalocker file path as "{mountpoint}/{uuid}.gl".
Patching "%s" to "AA" in "/%s.gl" makes it "/AA.gl", so the
full path becomes /mnt7/AA.gl regardless of the device's UUID.
The actual .gl file on disk is also renamed to AA.gl.
"""
data = bytearray(open(filepath, "rb").read())
# Search for the format string "/%s.gl\0" — this is the gigalocker
# filename pattern where %s gets replaced with the device UUID.
anchor = b"/%s.gl\x00"
offset = data.find(anchor)
if offset < 0:
print(" [-] Format string '/%s.gl' not found in seputil")
return False
# The %s is at offset+1 (2 bytes: 0x25 0x73)
pct_s_off = offset + 1
original = bytes(data[offset : offset + len(anchor)])
print(f" Found format string at 0x{offset:X}: {original!r}")
# Replace %s (2 bytes) with AA — turns "/%s.gl" into "/AA.gl"
data[pct_s_off] = ord("A")
data[pct_s_off + 1] = ord("A")
open(filepath, "wb").write(data)
print(f" [+] Patched at 0x{pct_s_off:X}: %s -> AA")
print(f" /{anchor[1:-1].decode()} -> /AA.gl")
return True
# ══════════════════════════════════════════════════════════════════
# 2. launchd_cache_loader — Unsecure cache bypass
# ══════════════════════════════════════════════════════════════════
def patch_launchd_cache_loader(filepath):
"""NOP the cache validation check in launchd_cache_loader.
Anchor strategies (in order):
1. Search for "unsecure_cache" substring, resolve to full null-terminated
string start, find ADRP+ADD xref to it, NOP the nearby cbz/cbnz branch
2. Verified known offset fallback
The binary checks boot-arg "launchd_unsecure_cache=" — if not found,
it skips the unsecure path via a conditional branch. NOPping that branch
allows modified launchd.plist to be loaded.
"""
data = bytearray(open(filepath, "rb").read())
sections = parse_macho_sections(data)
text_sec = find_section(sections, "__TEXT,__text")
if not text_sec:
print(" [-] __TEXT,__text not found")
return _launchd_cache_fallback(filepath, data)
text_va, text_size, text_foff = text_sec
# Strategy 1: Search for anchor strings in __cstring
# Code always references the START of a C string, so after finding a
# substring match, back-scan to the enclosing string's first byte.
cstring_sec = find_section(sections, "__TEXT,__cstring")
anchor_strings = [
b"unsecure_cache",
b"unsecure",
b"cache_valid",
b"validation",
]
for anchor_str in anchor_strings:
anchor_off = data.find(anchor_str)
if anchor_off < 0:
continue
# Find which section this belongs to and compute VA
anchor_sec_foff = -1
anchor_sec_va = -1
for sec_name, (sva, ssz, sfoff) in sections.items():
if sfoff <= anchor_off < sfoff + ssz:
anchor_sec_foff = sfoff
anchor_sec_va = sva
break
if anchor_sec_foff < 0:
continue
# Back-scan to the start of the enclosing null-terminated C string.
# Code loads strings from their beginning, not from a substring.
str_start_off = _find_cstring_start(data, anchor_off, anchor_sec_foff)
str_start_va = anchor_sec_va + (str_start_off - anchor_sec_foff)
substr_va = anchor_sec_va + (anchor_off - anchor_sec_foff)
if str_start_off != anchor_off:
end = data.index(0, str_start_off)
full_str = data[str_start_off:end].decode("ascii", errors="replace")
print(f" Found anchor '{anchor_str.decode()}' inside \"{full_str}\"")
print(f" String start: va:0x{str_start_va:X} (match at va:0x{substr_va:X})")
else:
print(f" Found anchor '{anchor_str.decode()}' at va:0x{str_start_va:X}")
# Search __TEXT for ADRP+ADD that resolves to the string START VA
code = bytes(data[text_foff : text_foff + text_size])
ref_off = _find_adrp_add_ref(code, text_va, str_start_va)
if ref_off < 0:
# Also try the exact substring VA as fallback
ref_off = _find_adrp_add_ref(code, text_va, substr_va)
if ref_off < 0:
continue
ref_foff = text_foff + (ref_off - text_va)
print(f" Found string ref at 0x{ref_foff:X}")
# Find conditional branch AFTER the string ref (within +32 instructions).
# The pattern is: ADRP+ADD (load string) -> BL (call check) -> CBZ/CBNZ (branch on result)
# So only search forward from the ref, not backwards.
branch_foff = _find_nearby_branch(data, ref_foff, text_foff, text_size)
if branch_foff >= 0:
insns = disasm_at(data, branch_foff, 1)
if insns:
print(
f" Patching: {insns[0].mnemonic} {insns[0].op_str} -> nop"
)
data[branch_foff : branch_foff + 4] = NOP
open(filepath, "wb").write(data)
print(f" [+] NOPped at 0x{branch_foff:X}")
return True
# Strategy 2: Fallback to verified known offset
print(" Dynamic anchor not found, trying verified fallback...")
return _launchd_cache_fallback(filepath, data)
def _find_cstring_start(data, match_off, section_foff):
"""Find the start of the null-terminated C string containing match_off.
Scans backwards from match_off to find the previous null byte (or section
start). Returns the file offset of the first byte of the enclosing string.
This is needed because code always references the start of a string, not
a substring within it.
"""
pos = match_off - 1
while pos >= section_foff and data[pos] != 0:
pos -= 1
return pos + 1
def _find_adrp_add_ref(code, base_va, target_va):
"""Find ADRP+ADD pair that computes target_va in code.
Handles non-adjacent pairs: tracks recent ADRP results per register
and matches them with ADD instructions up to 8 instructions later.
"""
target_page = target_va & ~0xFFF
target_pageoff = target_va & 0xFFF
# Track recent ADRP instructions: reg -> (insn_va, page_value, instruction_index)
adrp_cache = {}
for off in range(0, len(code) - 4, 4):
insns = list(_cs.disasm(code[off : off + 4], base_va + off))
if not insns:
continue
insn = insns[0]
idx = off // 4
if insn.mnemonic == "adrp" and len(insn.operands) >= 2:
reg = insn.operands[0].reg
page = insn.operands[1].imm
adrp_cache[reg] = (insn.address, page, idx)
elif insn.mnemonic == "add" and len(insn.operands) >= 3:
src_reg = insn.operands[1].reg
imm = insn.operands[2].imm
if src_reg in adrp_cache:
adrp_va, page, adrp_idx = adrp_cache[src_reg]
# Only match if ADRP was within 8 instructions
if page == target_page and imm == target_pageoff and idx - adrp_idx <= 8:
return adrp_va
return -1
def _find_nearby_branch(data, ref_foff, text_foff, text_size):
"""Find a conditional branch after a BL (function call) near ref_foff.
The typical pattern is:
ADRP+ADD (load string argument) ← ref_foff points here
... (setup other args)
BL (call check function)
CBZ/CBNZ (branch on return value)
Searches forward from ref_foff for a BL, then finds the first
conditional branch after it (within 8 instructions of the BL).
Falls back to first conditional branch within +32 instructions.
"""
branch_mnemonics = {"cbz", "cbnz", "tbz", "tbnz"}
# Strategy A: find BL → then first conditional branch after it
for delta in range(0, 16):
check_foff = ref_foff + delta * 4
if check_foff >= text_foff + text_size:
break
insns = disasm_at(data, check_foff, 1)
if not insns:
continue
if insns[0].mnemonic == "bl":
# Found a function call; scan the next 8 instructions for a branch
for d2 in range(1, 9):
br_foff = check_foff + d2 * 4
if br_foff >= text_foff + text_size:
break
br_insns = disasm_at(data, br_foff, 1)
if not br_insns:
continue
mn = br_insns[0].mnemonic
if mn in branch_mnemonics or mn.startswith("b."):
return br_foff
break # Found BL but no branch after it
# Strategy B: fallback — first conditional branch forward within 32 insns
for delta in range(1, 33):
check_foff = ref_foff + delta * 4
if check_foff >= text_foff + text_size:
break
insns = disasm_at(data, check_foff, 1)
if not insns:
continue
mn = insns[0].mnemonic
if mn in branch_mnemonics or mn.startswith("b."):
return check_foff
return -1
def _launchd_cache_fallback(filepath, data):
"""Fallback: verify known offset and NOP."""
KNOWN_OFF = 0xB58
if KNOWN_OFF + 4 > len(data):
print(f" [-] Known offset 0x{KNOWN_OFF:X} out of bounds")
return False
insns = disasm_at(data, KNOWN_OFF, 1)
if insns:
mn = insns[0].mnemonic
print(f" Fallback: {mn} {insns[0].op_str} at 0x{KNOWN_OFF:X}")
# Verify it's a branch-type instruction (expected for this patch)
branch_types = {"cbz", "cbnz", "tbz", "tbnz", "b"}
if mn not in branch_types and not mn.startswith("b."):
print(f" [!] Warning: unexpected instruction type '{mn}' at known offset")
print(f" Expected a conditional branch. Proceeding anyway.")
data[KNOWN_OFF : KNOWN_OFF + 4] = NOP
open(filepath, "wb").write(data)
print(f" [+] NOPped at 0x{KNOWN_OFF:X} (fallback)")
return True
# ══════════════════════════════════════════════════════════════════
# 3. mobileactivationd — Hackivation bypass
# ══════════════════════════════════════════════════════════════════
def patch_mobileactivationd(filepath):
"""Dynamically find -[DeviceType should_hactivate] and patch to return YES.
Anchor strategies (in order):
1. Search LC_SYMTAB for symbol containing "should_hactivate"
2. Parse ObjC metadata: methnames -> selrefs -> method_list -> IMP
3. Verified known offset fallback
The method determines if the device should self-activate (hackivation).
Patching it to always return YES bypasses activation lock.
"""
data = bytearray(open(filepath, "rb").read())
imp_foff = -1
# Strategy 1: Symbol table lookup (most reliable)
imp_va = find_symbol_va(bytes(data), "should_hactivate")
if imp_va > 0:
imp_foff = va_to_foff(bytes(data), imp_va)
if imp_foff >= 0:
print(f" Found via symtab: va:0x{imp_va:X} -> foff:0x{imp_foff:X}")
# Strategy 2: ObjC metadata chain
if imp_foff < 0:
imp_foff = _find_via_objc_metadata(data)
# Strategy 3: Fallback
if imp_foff < 0:
print(" Dynamic anchor not found, trying verified fallback...")
return _mobileactivationd_fallback(filepath, data)
# Verify the target looks like code
if imp_foff + 8 > len(data):
print(f" [-] IMP offset 0x{imp_foff:X} out of bounds")
return _mobileactivationd_fallback(filepath, data)
insns = disasm_at(data, imp_foff, 4)
if insns:
print(f" Original: {insns[0].mnemonic} {insns[0].op_str}")
# Patch to: mov x0, #1; ret
data[imp_foff : imp_foff + 4] = MOV_X0_1
data[imp_foff + 4 : imp_foff + 8] = RET
open(filepath, "wb").write(data)
print(f" [+] Patched at 0x{imp_foff:X}: mov x0, #1; ret")
return True
def _find_via_objc_metadata(data):
"""Find method IMP through ObjC runtime metadata."""
sections = parse_macho_sections(data)
# Find "should_hactivate\0" string
selector = b"should_hactivate\x00"
sel_foff = data.find(selector)
if sel_foff < 0:
print(" [-] Selector 'should_hactivate' not found in binary")
return -1
# Compute selector VA
sel_va = -1
for sec_name, (sva, ssz, sfoff) in sections.items():
if sfoff <= sel_foff < sfoff + ssz:
sel_va = sva + (sel_foff - sfoff)
break
if sel_va < 0:
print(f" [-] Could not compute VA for selector at foff:0x{sel_foff:X}")
return -1
print(f" Selector at foff:0x{sel_foff:X} va:0x{sel_va:X}")
# Find selref that points to this selector
selrefs = find_section(
sections,
"__DATA_CONST,__objc_selrefs",
"__DATA,__objc_selrefs",
"__AUTH_CONST,__objc_selrefs",
)
selref_foff = -1
selref_va = -1
if selrefs:
sr_va, sr_size, sr_foff = selrefs
for i in range(0, sr_size, 8):
ptr = struct.unpack_from("<Q", data, sr_foff + i)[0]
# Handle chained fixups: try exact and masked match
if ptr == sel_va or (ptr & 0x0000FFFFFFFFFFFF) == sel_va:
selref_foff = sr_foff + i
selref_va = sr_va + i
break
# Also try: lower 32 bits might encode the target in chained fixups
if (ptr & 0xFFFFFFFF) == (sel_va & 0xFFFFFFFF):
selref_foff = sr_foff + i
selref_va = sr_va + i
break
if selref_foff < 0:
print(" [-] Selref not found (chained fixups may obscure pointers)")
return -1
print(f" Selref at foff:0x{selref_foff:X} va:0x{selref_va:X}")
# Search for relative method list entry pointing to this selref
# Relative method entries: { int32 name_rel, int32 types_rel, int32 imp_rel }
# name_field_va + name_rel = selref_va
objc_const = find_section(
sections,
"__DATA_CONST,__objc_const",
"__DATA,__objc_const",
"__AUTH_CONST,__objc_const",
)
if objc_const:
oc_va, oc_size, oc_foff = objc_const
for i in range(0, oc_size - 12, 4):
entry_foff = oc_foff + i
entry_va = oc_va + i
rel_name = struct.unpack_from("<i", data, entry_foff)[0]
target_va = entry_va + rel_name
if target_va == selref_va:
# Found the method entry! Read IMP relative offset
imp_field_foff = entry_foff + 8
imp_field_va = entry_va + 8
rel_imp = struct.unpack_from("<i", data, imp_field_foff)[0]
imp_va = imp_field_va + rel_imp
imp_foff = va_to_foff(bytes(data), imp_va)
if imp_foff >= 0:
print(
f" Found via relative method list: IMP va:0x{imp_va:X} foff:0x{imp_foff:X}"
)
return imp_foff
else:
print(
f" [!] IMP va:0x{imp_va:X} could not be mapped to file offset"
)
return -1
def _mobileactivationd_fallback(filepath, data):
"""Fallback: verify known offset and patch."""
KNOWN_OFF = 0x2F5F84
if KNOWN_OFF + 8 > len(data):
print(f" [-] Known offset 0x{KNOWN_OFF:X} out of bounds (size: {len(data)})")
return False
insns = disasm_at(data, KNOWN_OFF, 4)
if insns:
print(f" Fallback: {insns[0].mnemonic} {insns[0].op_str} at 0x{KNOWN_OFF:X}")
data[KNOWN_OFF : KNOWN_OFF + 4] = MOV_X0_1
data[KNOWN_OFF + 4 : KNOWN_OFF + 8] = RET
open(filepath, "wb").write(data)
print(f" [+] Patched at 0x{KNOWN_OFF:X} (fallback): mov x0, #1; ret")
return True
# ══════════════════════════════════════════════════════════════════
# BuildManifest parsing
# ══════════════════════════════════════════════════════════════════
def parse_cryptex_paths(manifest_path):
"""Extract Cryptex DMG paths from BuildManifest.plist.
Searches ALL BuildIdentities for:
- Cryptex1,SystemOS -> Info -> Path
- Cryptex1,AppOS -> Info -> Path
vResearch IPSWs may have Cryptex entries in a non-first identity.
"""
with open(manifest_path, "rb") as f:
manifest = plistlib.load(f)
# Search all BuildIdentities for Cryptex paths
for bi in manifest.get("BuildIdentities", []):
m = bi.get("Manifest", {})
sysos = m.get("Cryptex1,SystemOS", {}).get("Info", {}).get("Path", "")
appos = m.get("Cryptex1,AppOS", {}).get("Info", {}).get("Path", "")
if sysos and appos:
return sysos, appos
print("[-] Cryptex1,SystemOS/AppOS paths not found in any BuildIdentity",
file=sys.stderr)
sys.exit(1)
# ══════════════════════════════════════════════════════════════════
# LaunchDaemon injection
# ══════════════════════════════════════════════════════════════════
def inject_daemons(plist_path, daemon_dir):
"""Inject bash/dropbear/trollvnc entries into launchd.plist."""
# Convert to XML first (macOS binary plist -> XML)
os.system(f'plutil -convert xml1 "{plist_path}" 2>/dev/null')
with open(plist_path, "rb") as f:
target = plistlib.load(f)
for name in ("bash", "dropbear", "trollvnc"):
src = os.path.join(daemon_dir, f"{name}.plist")
if not os.path.exists(src):
print(f" [!] Missing {src}, skipping")
continue
with open(src, "rb") as f:
daemon = plistlib.load(f)
key = f"/System/Library/LaunchDaemons/{name}.plist"
target.setdefault("LaunchDaemons", {})[key] = daemon
print(f" [+] Injected {name}")
with open(plist_path, "wb") as f:
plistlib.dump(target, f, sort_keys=False)
# ══════════════════════════════════════════════════════════════════
# CLI
# ══════════════════════════════════════════════════════════════════
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
cmd = sys.argv[1]
if cmd == "cryptex-paths":
if len(sys.argv) < 3:
print("Usage: patch_cfw.py cryptex-paths <BuildManifest.plist>")
sys.exit(1)
sysos, appos = parse_cryptex_paths(sys.argv[2])
print(sysos)
print(appos)
elif cmd == "patch-seputil":
if len(sys.argv) < 3:
print("Usage: patch_cfw.py patch-seputil <binary>")
sys.exit(1)
if not patch_seputil(sys.argv[2]):
sys.exit(1)
elif cmd == "patch-launchd-cache-loader":
if len(sys.argv) < 3:
print("Usage: patch_cfw.py patch-launchd-cache-loader <binary>")
sys.exit(1)
if not patch_launchd_cache_loader(sys.argv[2]):
sys.exit(1)
elif cmd == "patch-mobileactivationd":
if len(sys.argv) < 3:
print("Usage: patch_cfw.py patch-mobileactivationd <binary>")
sys.exit(1)
if not patch_mobileactivationd(sys.argv[2]):
sys.exit(1)
elif cmd == "inject-daemons":
if len(sys.argv) < 4:
print("Usage: patch_cfw.py inject-daemons <launchd.plist> <daemon_dir>")
sys.exit(1)
inject_daemons(sys.argv[2], sys.argv[3])
else:
print(f"Unknown command: {cmd}")
print("Commands: cryptex-paths, patch-seputil, patch-launchd-cache-loader,")
print(" patch-mobileactivationd, inject-daemons")
sys.exit(1)
if __name__ == "__main__":
main()

614
Scripts/patch_firmware.py Normal file
View File

@@ -0,0 +1,614 @@
#!/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("<I", val)
NOP = asm("nop")
MOV_X0_0 = asm("mov x0, #0")
CHUNK_SIZE, OVERLAP = 0x2000, 0x100
def chunked_disasm(buf, base=0):
md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
md.detail = True
off = 0
while off < len(buf):
insns = list(md.disasm(buf[off:min(off + CHUNK_SIZE, len(buf))], base + off))
yield insns
off += CHUNK_SIZE - OVERLAP
def disasm_at(buf, off, n=12, base=0):
md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
md.skipdata = True
return list(md.disasm(buf[off:min(off + n * 4, len(buf))], base + off))
def disasm_one(buf, off):
md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
md.skipdata = True
insns = list(md.disasm(buf[off:off + 4], off))
return f"{insns[0].mnemonic} {insns[0].op_str}" if insns else "???"
def rd32(buf, off):
return struct.unpack_from("<I", buf, off)[0]
def wr32(buf, off, v):
struct.pack_into("<I", buf, off, v)
# ══════════════════════════════════════════════════════════════════
# IM4P / raw file helpers — auto-detect format
# ══════════════════════════════════════════════════════════════════
def is_im4p(data):
"""Check if data is an IM4P container (ASN.1 DER with IM4P tag)."""
try:
IM4P(data)
return True
except Exception:
return False
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.
When original_raw is provided (preserve_payp=True), uses pyimg4 CLI to
recompress with lzfse and then appends the PAYP structure from the original.
This matches the approach used by the known-working patch_fw.py.
"""
if was_im4p and im4p_obj is not None:
if original_raw is not None:
# Use pyimg4 CLI + lzfse recompression + PAYP preservation
# (matches the working patch_fw.py approach exactly)
_save_im4p_with_payp(path, im4p_obj.fourcc, patched_data, original_raw)
else:
# Simple IM4P repackage (no PAYP needed — boot chain components)
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:
# Recompress with lzfse via pyimg4 CLI
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)
# Append PAYP from original
payp_offset = original_raw.rfind(b"PAYP")
if payp_offset >= 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()

240
Scripts/prepare_firmware.sh Executable file
View File

@@ -0,0 +1,240 @@
#!/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/"

66
Scripts/ramdisk_send.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/bin/zsh
# ramdisk_send.sh — Send signed ramdisk components to device via irecovery.
#
# Usage: ./ramdisk_send.sh [ramdisk_dir]
#
# Expects device in DFU mode. Loads iBSS/iBEC, then boots with
# SPTM, TXM, trustcache, ramdisk, device tree, SEP, and kernel.
set -euo pipefail
RAMDISK_DIR="${1:-Ramdisk}"
if [ ! -d "$RAMDISK_DIR" ]; then
echo "[-] Ramdisk directory not found: $RAMDISK_DIR"
echo " Run build_ramdisk.py first."
exit 1
fi
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"
echo " [2/8] Loading iBEC..."
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
# 3. Load TXM
echo " [4/8] Loading TXM..."
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
# 5. Load ramdisk
echo " [6/8] Loading ramdisk..."
irecovery -f "$RAMDISK_DIR/ramdisk.img4"
sleep 2
irecovery -c ramdisk
# 6. Load device tree
echo " [7/8] Loading device tree..."
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
# 8. Load kernel and boot
echo " [*] Booting kernel..."
irecovery -f "$RAMDISK_DIR/krnl.img4"
irecovery -c bootx
echo "[+] Boot sequence complete. Device should be booting into ramdisk."

View File

@@ -0,0 +1,256 @@
// VPhoneObjC.m ObjC wrappers for private Virtualization.framework APIs
#import "VPhoneObjC.h"
#import <objc/message.h>
// Private class forward declarations
@interface _VZMacHardwareModelDescriptor : NSObject
- (instancetype)init;
- (void)setPlatformVersion:(unsigned int)version;
- (void)setISA:(long long)isa;
- (void)setBoardID:(unsigned int)boardID;
@end
@interface VZMacHardwareModel (Private)
+ (instancetype)_hardwareModelWithDescriptor:(id)descriptor;
@end
@interface VZMacOSVirtualMachineStartOptions (Private)
- (void)_setForceDFU:(BOOL)force;
- (void)_setPanicAction:(BOOL)stop;
- (void)_setFatalErrorAction:(BOOL)stop;
- (void)_setStopInIBootStage1:(BOOL)stop;
- (void)_setStopInIBootStage2:(BOOL)stop;
@end
@interface VZMacOSBootLoader (Private)
- (void)_setROMURL:(NSURL *)url;
@end
@interface VZVirtualMachineConfiguration (Private)
- (void)_setDebugStub:(id)stub;
- (void)_setPanicDevice:(id)device;
- (void)_setCoprocessors:(NSArray *)coprocessors;
- (void)_setMultiTouchDevices:(NSArray *)devices;
@end
@interface VZMacPlatformConfiguration (Private)
- (void)_setProductionModeEnabled:(BOOL)enabled;
@end
// --- Implementation ---
VZMacHardwareModel *VPhoneCreateHardwareModel(void) {
// Create descriptor with PV=3, ISA=2, boardID=0x90 (matches vrevm vresearch101)
_VZMacHardwareModelDescriptor *desc = [[_VZMacHardwareModelDescriptor alloc] init];
[desc setPlatformVersion:3];
[desc setBoardID:0x90];
[desc setISA:2];
VZMacHardwareModel *model = [VZMacHardwareModel _hardwareModelWithDescriptor:desc];
return model;
}
void VPhoneSetBootLoaderROMURL(VZMacOSBootLoader *bootloader, NSURL *romURL) {
[bootloader _setROMURL:romURL];
}
void VPhoneConfigureStartOptions(VZMacOSVirtualMachineStartOptions *opts,
BOOL forceDFU,
BOOL stopOnPanic,
BOOL stopOnFatalError) {
[opts _setForceDFU:forceDFU];
[opts _setStopInIBootStage1:NO];
[opts _setStopInIBootStage2:NO];
// Note: _setPanicAction: / _setFatalErrorAction: don't exist on
// VZMacOSVirtualMachineStartOptions. Panic handling is done via
// _VZPvPanicDeviceConfiguration set on VZVirtualMachineConfiguration.
}
void VPhoneSetGDBDebugStub(VZVirtualMachineConfiguration *config, NSInteger port) {
Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration");
if (!stubClass) {
NSLog(@"[vphone] WARNING: _VZGDBDebugStubConfiguration not found");
return;
}
// Use objc_msgSend to call initWithPort: with an NSInteger argument
id (*initWithPort)(id, SEL, NSInteger) = (id (*)(id, SEL, NSInteger))objc_msgSend;
id stub = initWithPort([stubClass alloc], NSSelectorFromString(@"initWithPort:"), port);
[config _setDebugStub:stub];
}
void VPhoneSetPanicDevice(VZVirtualMachineConfiguration *config) {
Class panicClass = NSClassFromString(@"_VZPvPanicDeviceConfiguration");
if (!panicClass) {
NSLog(@"[vphone] WARNING: _VZPvPanicDeviceConfiguration not found");
return;
}
id device = [[panicClass alloc] init];
[config _setPanicDevice:device];
}
void VPhoneSetCoprocessors(VZVirtualMachineConfiguration *config, NSArray *coprocessors) {
[config _setCoprocessors:coprocessors];
}
void VPhoneDisableProductionMode(VZMacPlatformConfiguration *platform) {
[platform _setProductionModeEnabled:NO];
}
// --- NVRAM ---
@interface VZMacAuxiliaryStorage (Private)
- (BOOL)_setDataValue:(NSData *)value forNVRAMVariableNamed:(NSString *)name error:(NSError **)error;
@end
BOOL VPhoneSetNVRAMVariable(VZMacAuxiliaryStorage *auxStorage, NSString *name, NSData *value) {
NSError *error = nil;
BOOL ok = [auxStorage _setDataValue:value forNVRAMVariableNamed:name error:&error];
if (!ok) {
NSLog(@"[vphone] NVRAM set '%@' failed: %@", name, error);
}
return ok;
}
// --- PL011 Serial Port ---
@interface _VZPL011SerialPortConfiguration : VZSerialPortConfiguration
@end
VZSerialPortConfiguration *VPhoneCreatePL011SerialPort(void) {
Class cls = NSClassFromString(@"_VZPL011SerialPortConfiguration");
if (!cls) {
NSLog(@"[vphone] WARNING: _VZPL011SerialPortConfiguration not found");
return nil;
}
return [[cls alloc] init];
}
// --- SEP Coprocessor ---
@interface _VZSEPCoprocessorConfiguration : NSObject
- (instancetype)initWithStorageURL:(NSURL *)url;
- (void)setRomBinaryURL:(NSURL *)url;
- (void)setDebugStub:(id)stub;
@end
id VPhoneCreateSEPCoprocessorConfig(NSURL *storageURL) {
Class cls = NSClassFromString(@"_VZSEPCoprocessorConfiguration");
if (!cls) {
NSLog(@"[vphone] WARNING: _VZSEPCoprocessorConfiguration not found");
return nil;
}
_VZSEPCoprocessorConfiguration *config = [[cls alloc] initWithStorageURL:storageURL];
return config;
}
void VPhoneSetSEPRomBinaryURL(id sepConfig, NSURL *romURL) {
if ([sepConfig respondsToSelector:@selector(setRomBinaryURL:)]) {
[sepConfig performSelector:@selector(setRomBinaryURL:) withObject:romURL];
}
}
void VPhoneConfigureSEP(VZVirtualMachineConfiguration *config,
NSURL *sepStorageURL,
NSURL *sepRomURL) {
id sepConfig = VPhoneCreateSEPCoprocessorConfig(sepStorageURL);
if (!sepConfig) {
NSLog(@"[vphone] Failed to create SEP coprocessor config");
return;
}
if (sepRomURL) {
VPhoneSetSEPRomBinaryURL(sepConfig, sepRomURL);
}
// Set debug stub on SEP (same as vrevm)
Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration");
if (stubClass) {
id sepDebugStub = [[stubClass alloc] init];
[sepConfig performSelector:@selector(setDebugStub:) withObject:sepDebugStub];
}
[config _setCoprocessors:@[sepConfig]];
NSLog(@"[vphone] SEP coprocessor configured (storage: %@)", sepStorageURL.path);
}
void VPhoneSetGDBDebugStubDefault(VZVirtualMachineConfiguration *config) {
Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration");
if (!stubClass) {
NSLog(@"[vphone] WARNING: _VZGDBDebugStubConfiguration not found");
return;
}
id stub = [[stubClass alloc] init]; // default init, no specific port (same as vrevm)
[config _setDebugStub:stub];
}
// --- Multi-Touch (VNC click fix) ---
@interface _VZMultiTouchDeviceConfiguration : NSObject <NSCopying>
@end
@interface _VZUSBTouchScreenConfiguration : _VZMultiTouchDeviceConfiguration
- (instancetype)init;
@end
void VPhoneConfigureMultiTouch(VZVirtualMachineConfiguration *config) {
Class cls = NSClassFromString(@"_VZUSBTouchScreenConfiguration");
if (!cls) {
NSLog(@"[vphone] WARNING: _VZUSBTouchScreenConfiguration not found");
return;
}
id touchConfig = [[cls alloc] init];
[config _setMultiTouchDevices:@[touchConfig]];
NSLog(@"[vphone] USB touch screen configured");
}
// VZTouchHelper: create _VZTouch using KVC to avoid crash in initWithView:...
// The _VZTouch initializer does a struct copy (objc_copyStruct) that causes
// EXC_BAD_ACCESS (SIGBUS) when called from Swift Dynamic framework.
// Using alloc+init then KVC setValue:forKey: bypasses the problematic initializer.
id VPhoneCreateTouch(NSInteger index,
NSInteger phase,
CGPoint location,
NSInteger swipeAim,
NSTimeInterval timestamp) {
Class touchClass = NSClassFromString(@"_VZTouch");
if (!touchClass) {
return nil;
}
id touch = [[touchClass alloc] init];
[touch setValue:@((unsigned char)index) forKey:@"_index"];
[touch setValue:@(phase) forKey:@"_phase"];
[touch setValue:@(swipeAim) forKey:@"_swipeAim"];
[touch setValue:@(timestamp) forKey:@"_timestamp"];
[touch setValue:[NSValue valueWithPoint:location] forKey:@"_location"];
return touch;
}
id VPhoneCreateMultiTouchEvent(NSArray *touches) {
Class cls = NSClassFromString(@"_VZMultiTouchEvent");
if (!cls) {
return nil;
}
// _VZMultiTouchEvent initWithTouches:
SEL sel = NSSelectorFromString(@"initWithTouches:");
id event = [cls alloc];
id (*initWithTouches)(id, SEL, NSArray *) = (id (*)(id, SEL, NSArray *))objc_msgSend;
return initWithTouches(event, sel, touches);
}
NSArray *VPhoneGetMultiTouchDevices(VZVirtualMachine *vm) {
SEL sel = NSSelectorFromString(@"_multiTouchDevices");
if (![vm respondsToSelector:sel]) {
return nil;
}
NSArray * (*getter)(id, SEL) = (NSArray * (*)(id, SEL))objc_msgSend;
return getter(vm, sel);
}
void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events) {
SEL sel = NSSelectorFromString(@"sendMultiTouchEvents:");
if (![multiTouchDevice respondsToSelector:sel]) {
return;
}
void (*send)(id, SEL, NSArray *) = (void (*)(id, SEL, NSArray *))objc_msgSend;
send(multiTouchDevice, sel, events);
}

View File

@@ -0,0 +1,79 @@
// VPhoneObjC.h — ObjC wrappers for private Virtualization.framework APIs
#import <Foundation/Foundation.h>
#import <Virtualization/Virtualization.h>
NS_ASSUME_NONNULL_BEGIN
/// Create a PV=3 (vphone) VZMacHardwareModel using private _VZMacHardwareModelDescriptor.
VZMacHardwareModel *VPhoneCreateHardwareModel(void);
/// Set _setROMURL: on a VZMacOSBootLoader.
void VPhoneSetBootLoaderROMURL(VZMacOSBootLoader *bootloader, NSURL *romURL);
/// Configure VZMacOSVirtualMachineStartOptions.
/// Sets _setForceDFU:, _setPanicAction:, _setFatalErrorAction:
void VPhoneConfigureStartOptions(VZMacOSVirtualMachineStartOptions *opts,
BOOL forceDFU,
BOOL stopOnPanic,
BOOL stopOnFatalError);
/// Set _setDebugStub: with a _VZGDBDebugStubConfiguration on the VM config (specific port).
void VPhoneSetGDBDebugStub(VZVirtualMachineConfiguration *config, NSInteger port);
/// Set _setDebugStub: with default _VZGDBDebugStubConfiguration (system-assigned port, same as vrevm).
void VPhoneSetGDBDebugStubDefault(VZVirtualMachineConfiguration *config);
/// Set _VZPvPanicDeviceConfiguration on the VM config.
void VPhoneSetPanicDevice(VZVirtualMachineConfiguration *config);
/// Set _setCoprocessors: on the VM config (empty array = no coprocessors).
void VPhoneSetCoprocessors(VZVirtualMachineConfiguration *config, NSArray *coprocessors);
/// Set _setProductionModeEnabled:NO on VZMacPlatformConfiguration.
void VPhoneDisableProductionMode(VZMacPlatformConfiguration *platform);
/// Create a _VZSEPCoprocessorConfiguration with the given storage URL.
/// Returns the config object, or nil on failure.
id _Nullable VPhoneCreateSEPCoprocessorConfig(NSURL *storageURL);
/// Set romBinaryURL on a _VZSEPCoprocessorConfiguration.
void VPhoneSetSEPRomBinaryURL(id sepConfig, NSURL *romURL);
/// Configure SEP coprocessor on the VM config.
/// Creates storage at sepStorageURL, optionally sets sepRomURL, and calls _setCoprocessors:.
void VPhoneConfigureSEP(VZVirtualMachineConfiguration *config,
NSURL *sepStorageURL,
NSURL *_Nullable sepRomURL);
/// Set an NVRAM variable on VZMacAuxiliaryStorage using the private _setDataValue API.
/// Returns YES on success.
BOOL VPhoneSetNVRAMVariable(VZMacAuxiliaryStorage *auxStorage, NSString *name, NSData *value);
/// Create a _VZPL011SerialPortConfiguration (ARM PL011 UART serial port).
/// Returns nil if the private class is unavailable.
VZSerialPortConfiguration *_Nullable VPhoneCreatePL011SerialPort(void);
// --- Multi-Touch (VNC click fix) ---
/// Configure _VZUSBTouchScreenConfiguration on the VM config.
/// Must be called before VM starts to enable touch input.
void VPhoneConfigureMultiTouch(VZVirtualMachineConfiguration *config);
/// Create a _VZTouch object using KVC (avoids crash in _VZTouch initWithView:...).
/// Returns nil if the _VZTouch class is unavailable.
id _Nullable VPhoneCreateTouch(NSInteger index,
NSInteger phase,
CGPoint location,
NSInteger swipeAim,
NSTimeInterval timestamp);
/// Create a _VZMultiTouchEvent from an array of _VZTouch objects.
id _Nullable VPhoneCreateMultiTouchEvent(NSArray *touches);
/// Get the _multiTouchDevices array from a running VZVirtualMachine.
NSArray *_Nullable VPhoneGetMultiTouchDevices(VZVirtualMachine *vm);
/// Send multi-touch events to a multi-touch device.
void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events);
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,130 @@
import AppKit
import ArgumentParser
import Foundation
import Virtualization
@main
struct VPhoneCLI: AsyncParsableCommand {
static var configuration = CommandConfiguration(
commandName: "vphone-cli",
abstract: "Boot a virtual iPhone (PV=3)",
discussion: """
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
""",
)
@Option(help: "Path to the AVPBooter / ROM binary")
var rom: String
@Option(help: "Path to the disk image")
var disk: String
@Option(help: "Path to NVRAM storage (created/overwritten)")
var nvram: String = "nvram.bin"
@Option(help: "Number of CPU cores")
var cpu: Int = 4
@Option(help: "Memory size in MB")
var memory: Int = 4096
@Option(help: "Path to write serial console log file")
var serialLog: String? = nil
@Flag(help: "Stop VM on guest panic")
var stopOnPanic: Bool = false
@Flag(help: "Stop VM on fatal error")
var stopOnFatalError: Bool = false
@Flag(help: "Skip SEP coprocessor setup")
var skipSep: Bool = false
@Option(help: "Path to SEP storage file (created if missing)")
var sepStorage: String? = nil
@Option(help: "Path to SEP ROM binary")
var sepRom: String? = nil
@Flag(help: "Boot into DFU mode")
var dfu: Bool = false
@Flag(help: "Run without GUI (headless)")
var noGraphics: Bool = false
@MainActor
mutating func run() async throws {
let romURL = URL(fileURLWithPath: rom)
guard FileManager.default.fileExists(atPath: romURL.path) else {
throw VPhoneError.romNotFound(rom)
}
let diskURL = URL(fileURLWithPath: disk)
let nvramURL = URL(fileURLWithPath: nvram)
print("=== vphone-cli ===")
print("ROM : \(rom)")
print("Disk : \(disk)")
print("NVRAM : \(nvram)")
print("CPU : \(cpu)")
print("Memory: \(memory) MB")
let sepStorageURL = sepStorage.map { URL(fileURLWithPath: $0) }
let sepRomURL = sepRom.map { URL(fileURLWithPath: $0) }
print("SEP : \(skipSep ? "skipped" : "enabled")")
if !skipSep {
print(" storage: \(sepStorage ?? "(auto)")")
if let r = sepRom { print(" rom : \(r)") }
}
print("")
let options = VPhoneVM.Options(
romURL: romURL,
nvramURL: nvramURL,
diskURL: diskURL,
cpuCount: cpu,
memorySize: UInt64(memory) * 1024 * 1024,
skipSEP: skipSep,
sepStorageURL: sepStorageURL,
sepRomURL: sepRomURL,
serialLogPath: serialLog,
stopOnPanic: stopOnPanic,
stopOnFatalError: stopOnFatalError,
)
let vm = try VPhoneVM(options: options)
// Handle Ctrl+C
signal(SIGINT, SIG_IGN)
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
sigintSrc.setEventHandler {
print("\n[vphone] SIGINT — shutting down")
vm.stopConsoleCapture()
Foundation.exit(0)
}
sigintSrc.activate()
// Start VM
try await vm.start(forceDFU: dfu, stopOnPanic: stopOnPanic, stopOnFatalError: stopOnFatalError)
if noGraphics {
// Headless: just wait
NSApplication.shared.setActivationPolicy(.prohibited)
await vm.waitUntilStopped()
} else {
// GUI: show VM window with touch support
let windowController = VPhoneWindowController()
windowController.showWindow(for: vm.virtualMachine)
await vm.waitUntilStopped()
}
}
}

View File

@@ -0,0 +1,24 @@
import Foundation
import Virtualization
import VPhoneObjC
/// Wrapper around the ObjC private API call to create a PV=3 hardware model.
///
/// The Virtualization.framework checks:
/// default_configuration_for_platform_version(3) validity byte =
/// (entitlements & 0x12) != 0
/// where bit 1 = com.apple.private.virtualization
/// bit 4 = com.apple.private.virtualization.security-research
///
/// Minimum host OS for PV=3: macOS 15.0 (Sequoia)
///
enum VPhoneHardware {
/// Create a PV=3 VZMacHardwareModel. Throws if isSupported is false.
static func createModel() throws -> VZMacHardwareModel {
let model = VPhoneCreateHardwareModel()
guard model.isSupported else {
throw VPhoneError.hardwareModelNotSupported
}
return model
}
}

View File

@@ -0,0 +1,212 @@
import Foundation
import Virtualization
import VPhoneObjC
/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode.
class VPhoneVM: NSObject, VZVirtualMachineDelegate {
let virtualMachine: VZVirtualMachine
private var done = false
struct Options {
var romURL: URL
var nvramURL: URL
var diskURL: URL
var cpuCount: Int = 4
var memorySize: UInt64 = 4 * 1024 * 1024 * 1024
var skipSEP: Bool = true
var sepStorageURL: URL?
var sepRomURL: URL?
var serialLogPath: String? = nil
var stopOnPanic: Bool = false
var stopOnFatalError: Bool = false
}
private var consoleLogFileHandle: FileHandle?
init(options: Options) throws {
// --- Hardware model (PV=3) ---
let hwModel = try VPhoneHardware.createModel()
print("[vphone] PV=3 hardware model: isSupported = true")
// --- Platform ---
let platform = VZMacPlatformConfiguration()
// Persist machineIdentifier for stable ECID (same as vrevm)
let machineIDPath = options.nvramURL.deletingLastPathComponent()
.appendingPathComponent("machineIdentifier.bin")
if let savedData = try? Data(contentsOf: machineIDPath),
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData) {
platform.machineIdentifier = savedID
print("[vphone] Loaded machineIdentifier (ECID stable)")
} else {
let newID = VZMacMachineIdentifier()
platform.machineIdentifier = newID
try newID.dataRepresentation.write(to: machineIDPath)
print("[vphone] Created new machineIdentifier -> \(machineIDPath.lastPathComponent)")
}
let auxStorage = try VZMacAuxiliaryStorage(
creatingStorageAt: options.nvramURL,
hardwareModel: hwModel,
options: .allowOverwrite,
)
platform.auxiliaryStorage = auxStorage
platform.hardwareModel = hwModel
// platformFusing = prod (same as vrevm config)
// Set NVRAM boot-args to enable serial output (same as vrevm restore)
let bootArgs = "serial=3 debug=0x104c04"
if let bootArgsData = bootArgs.data(using: .utf8) {
if VPhoneSetNVRAMVariable(auxStorage, "boot-args", bootArgsData) {
print("[vphone] NVRAM boot-args: \(bootArgs)")
}
}
// --- Boot loader with custom ROM ---
let bootloader = VZMacOSBootLoader()
VPhoneSetBootLoaderROMURL(bootloader, options.romURL)
// --- VM Configuration ---
let config = VZVirtualMachineConfiguration()
config.bootLoader = bootloader
config.platform = platform
config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
// Display (vresearch101: 1290x2796 @ 460 PPI matches vrevm)
let gfx = VZMacGraphicsDeviceConfiguration()
gfx.displays = [
VZMacGraphicsDisplayConfiguration(widthInPixels: 1290, heightInPixels: 2796, pixelsPerInch: 460),
]
config.graphicsDevices = [gfx]
// Storage
if FileManager.default.fileExists(atPath: options.diskURL.path) {
let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false)
config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]
}
// Network (shared NAT)
let net = VZVirtioNetworkDeviceConfiguration()
net.attachment = VZNATNetworkDeviceAttachment()
config.networkDevices = [net]
// Serial port (PL011 UART always configured)
// Connect host stdin/stdout directly for interactive serial console
do {
if let serialPort = VPhoneCreatePL011SerialPort() {
serialPort.attachment = VZFileHandleSerialPortAttachment(
fileHandleForReading: FileHandle.standardInput,
fileHandleForWriting: FileHandle.standardOutput
)
config.serialPorts = [serialPort]
print("[vphone] PL011 serial port attached (interactive)")
}
// Set up log file if requested
if let logPath = options.serialLogPath {
let logURL = URL(fileURLWithPath: logPath)
FileManager.default.createFile(atPath: logURL.path, contents: nil)
self.consoleLogFileHandle = FileHandle(forWritingAtPath: logURL.path)
print("[vphone] Serial log: \(logPath)")
}
}
// Multi-touch (USB touch screen for VNC click support)
VPhoneConfigureMultiTouch(config)
// GDB debug stub (default init, system-assigned port same as vrevm)
VPhoneSetGDBDebugStubDefault(config)
// Coprocessors
if options.skipSEP {
print("[vphone] SKIP_SEP=1 — no coprocessor")
} else if let sepStorageURL = options.sepStorageURL {
VPhoneConfigureSEP(config, sepStorageURL, options.sepRomURL)
print("[vphone] SEP coprocessor enabled (storage: \(sepStorageURL.path))")
} else {
// Create default SEP storage next to NVRAM
let defaultSEPURL = options.nvramURL.deletingLastPathComponent()
.appendingPathComponent("sep_storage.bin")
VPhoneConfigureSEP(config, defaultSEPURL, options.sepRomURL)
print("[vphone] SEP coprocessor enabled (storage: \(defaultSEPURL.path))")
}
// Validate
try config.validate()
print("[vphone] Configuration validated")
virtualMachine = VZVirtualMachine(configuration: config)
super.init()
virtualMachine.delegate = self
}
// MARK: - DFU start
@MainActor
func start(forceDFU: Bool, stopOnPanic: Bool, stopOnFatalError: Bool) async throws {
let opts = VZMacOSVirtualMachineStartOptions()
VPhoneConfigureStartOptions(opts, forceDFU, stopOnPanic, stopOnFatalError)
print("[vphone] Starting\(forceDFU ? " DFU" : "")...")
try await virtualMachine.start(options: opts)
if forceDFU {
print("[vphone] VM started in DFU mode — connect with irecovery")
} else {
print("[vphone] VM started — booting normally")
}
}
// MARK: - Wait
func waitUntilStopped() async {
while !done {
try? await Task.sleep(nanoseconds: 500_000_000)
}
}
// MARK: - Delegate
func guestDidStop(_: VZVirtualMachine) {
print("[vphone] Guest stopped")
done = true
}
func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) {
print("[vphone] Stopped with error: \(error)")
done = true
}
func virtualMachine(_: VZVirtualMachine, networkDevice _: VZNetworkDevice,
attachmentWasDisconnectedWithError error: Error)
{
print("[vphone] Network error: \(error)")
}
// MARK: - Cleanup
func stopConsoleCapture() {
consoleLogFileHandle?.closeFile()
}
}
// MARK: - Errors
enum VPhoneError: Error, CustomStringConvertible {
case hardwareModelNotSupported
case romNotFound(String)
var description: String {
switch self {
case .hardwareModelNotSupported:
"""
PV=3 hardware model not supported. Check:
1. macOS >= 15.0 (Sequoia)
2. Signed with com.apple.private.virtualization + \
com.apple.private.virtualization.security-research
3. SIP/AMFI disabled
"""
case let .romNotFound(p):
"ROM not found: \(p)"
}
}
}

View File

@@ -0,0 +1,249 @@
import AppKit
import Foundation
import Virtualization
import VPhoneObjC
// MARK: - Touch-enabled VZVirtualMachineView
struct NormalizedResult {
var point: CGPoint
var isInvalid: Bool
}
class VPhoneVMView: VZVirtualMachineView {
var currentTouchSwipeAim: Int64 = 0
// 1. Mouse dragged -> touch phase 1 (moving)
override func mouseDragged(with event: NSEvent) {
handleMouseDragged(event)
super.mouseDragged(with: event)
}
private func handleMouseDragged(_ event: NSEvent) {
guard let vm = self.virtualMachine,
let devices = VPhoneGetMultiTouchDevices(vm),
devices.count > 0 else { return }
let normalized = normalizeCoordinate(event.locationInWindow)
let swipeAim = self.currentTouchSwipeAim
guard let touch = VPhoneCreateTouch(0, 1, normalized.point, Int(swipeAim), event.timestamp) else { return }
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return }
let device = devices[0]
VPhoneSendMultiTouchEvents(device, [touchEvent])
}
// 2. Mouse down -> touch phase 0 (began)
override func mouseDown(with event: NSEvent) {
handleMouseDown(event)
super.mouseDown(with: event)
}
private func handleMouseDown(_ event: NSEvent) {
guard let vm = self.virtualMachine,
let devices = VPhoneGetMultiTouchDevices(vm),
devices.count > 0 else { return }
let normalized = normalizeCoordinate(event.locationInWindow)
let localPoint = self.convert(event.locationInWindow, from: nil)
let edgeResult = hitTestEdge(at: localPoint)
self.currentTouchSwipeAim = Int64(edgeResult)
guard let touch = VPhoneCreateTouch(0, 0, normalized.point, edgeResult, event.timestamp) else { return }
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return }
let device = devices[0]
VPhoneSendMultiTouchEvents(device, [touchEvent])
}
// 3. Right mouse down -> two-finger touch began
override func rightMouseDown(with event: NSEvent) {
handleRightMouseDown(event)
super.rightMouseDown(with: event)
}
private func handleRightMouseDown(_ event: NSEvent) {
guard let vm = self.virtualMachine,
let devices = VPhoneGetMultiTouchDevices(vm),
devices.count > 0 else { return }
let normalized = normalizeCoordinate(event.locationInWindow)
guard !normalized.isInvalid else { return }
let localPoint = self.convert(event.locationInWindow, from: nil)
let edgeResult = hitTestEdge(at: localPoint)
self.currentTouchSwipeAim = Int64(edgeResult)
guard let touch = VPhoneCreateTouch(0, 0, normalized.point, edgeResult, event.timestamp),
let touch2 = VPhoneCreateTouch(1, 0, normalized.point, edgeResult, event.timestamp) else { return }
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch, touch2]) else { return }
let device = devices[0]
VPhoneSendMultiTouchEvents(device, [touchEvent])
}
// 4. Mouse up -> touch phase 3 (ended)
override func mouseUp(with event: NSEvent) {
handleMouseUp(event)
super.mouseUp(with: event)
}
private func handleMouseUp(_ event: NSEvent) {
guard let vm = self.virtualMachine,
let devices = VPhoneGetMultiTouchDevices(vm),
devices.count > 0 else { return }
let normalized = normalizeCoordinate(event.locationInWindow)
let swipeAim = self.currentTouchSwipeAim
guard let touch = VPhoneCreateTouch(0, 3, normalized.point, Int(swipeAim), event.timestamp) else { return }
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return }
let device = devices[0]
VPhoneSendMultiTouchEvents(device, [touchEvent])
}
// 5. Right mouse up -> two-finger touch ended
override func rightMouseUp(with event: NSEvent) {
handleRightMouseUp(event)
super.rightMouseUp(with: event)
}
private func handleRightMouseUp(_ event: NSEvent) {
guard let vm = self.virtualMachine,
let devices = VPhoneGetMultiTouchDevices(vm),
devices.count > 0 else { return }
let normalized = normalizeCoordinate(event.locationInWindow)
guard !normalized.isInvalid else { return }
let swipeAim = self.currentTouchSwipeAim
guard let touch = VPhoneCreateTouch(0, 3, normalized.point, Int(swipeAim), event.timestamp),
let touch2 = VPhoneCreateTouch(1, 3, normalized.point, Int(swipeAim), event.timestamp) else { return }
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch, touch2]) else { return }
let device = devices[0]
VPhoneSendMultiTouchEvents(device, [touchEvent])
}
// MARK: - Coordinate normalization
func normalizeCoordinate(_ point: CGPoint) -> NormalizedResult {
let bounds = self.bounds
if bounds.size.width <= 0 || bounds.size.height <= 0 {
return NormalizedResult(point: .zero, isInvalid: true)
}
let localPoint = self.convert(point, from: nil)
var nx = Double(localPoint.x / bounds.size.width)
var ny = Double(localPoint.y / bounds.size.height)
nx = max(0.0, min(1.0, nx))
ny = max(0.0, min(1.0, ny))
if !self.isFlipped {
ny = 1.0 - ny
}
return NormalizedResult(point: CGPoint(x: nx, y: ny), isInvalid: false)
}
// MARK: - Edge detection for swipe aim
func hitTestEdge(at point: CGPoint) -> Int {
let bounds = self.bounds
let width = bounds.size.width
let height = bounds.size.height
let distLeft = point.x
let distRight = width - point.x
var minDist: Double
var edgeCode: Int
if distRight < distLeft {
minDist = distRight
edgeCode = 4 // Right
} else {
minDist = distLeft
edgeCode = 8 // Left
}
let topCode = self.isFlipped ? 2 : 1
let bottomCode = self.isFlipped ? 1 : 2
let distTop = point.y
if distTop < minDist {
minDist = distTop
edgeCode = topCode
}
let distBottom = height - point.y
if distBottom < minDist {
minDist = distBottom
edgeCode = bottomCode
}
return minDist < 32.0 ? edgeCode : 0
}
}
// MARK: - Window management
class VPhoneWindowController {
private var windowController: NSWindowController?
@MainActor
func showWindow(for vm: VZVirtualMachine) {
let vmView: NSView
if #available(macOS 16.0, *) {
let view = VZVirtualMachineView()
view.virtualMachine = vm
view.capturesSystemKeys = true
vmView = view
} else {
let view = VPhoneVMView()
view.virtualMachine = vm
view.capturesSystemKeys = true
vmView = view
}
let pixelWidth: CGFloat = 1179
let pixelHeight: CGFloat = 2556
let windowSize = NSSize(width: pixelWidth, height: pixelHeight)
let window = NSWindow(
contentRect: NSRect(origin: .zero, size: windowSize),
styleMask: [.titled, .closable, .resizable, .miniaturizable],
backing: .buffered,
defer: false
)
window.contentAspectRatio = windowSize
window.title = "vphone"
window.contentView = vmView
window.center()
let controller = NSWindowController(window: window)
controller.showWindow(nil)
self.windowController = controller
if NSApp == nil {
_ = NSApplication.shared
}
NSApp.setActivationPolicy(.regular)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
func close() {
DispatchQueue.main.async {
self.windowController?.close()
self.windowController = nil
}
}
}

19
boot.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/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

20
boot_dfu.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/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

33
boot_sweet.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/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

45
build_and_sign.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/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 <rom> --disk <disk> --serial"

BIN
demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

16
vphone.entitlements Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.virtualization</key>
<true/>
<key>com.apple.private.virtualization</key>
<true/>
<key>com.apple.private.virtualization.security-research</key>
<true/>
<key>com.apple.vm.networking</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>