mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 13:09:06 +08:00
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:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.tar.zst filter=lfs diff=lfs merge=lfs -text
|
||||
309
.gitignore
vendored
Normal file
309
.gitignore
vendored
Normal 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
13
LICENSE
Normal 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
39
Package.swift
Normal 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
961
README.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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
519
Scripts/build_ramdisk.py
Normal 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
348
Scripts/install_cfw.sh
Executable 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
740
Scripts/patch_cfw.py
Normal 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
614
Scripts/patch_firmware.py
Normal 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
240
Scripts/prepare_firmware.sh
Executable 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
66
Scripts/ramdisk_send.sh
Executable 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."
|
||||
256
Sources/VPhoneObjC/VPhoneObjC.m
Normal file
256
Sources/VPhoneObjC/VPhoneObjC.m
Normal 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);
|
||||
}
|
||||
79
Sources/VPhoneObjC/include/VPhoneObjC.h
Normal file
79
Sources/VPhoneObjC/include/VPhoneObjC.h
Normal 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
|
||||
130
Sources/vphone-cli/VPhoneCLI.swift
Normal file
130
Sources/vphone-cli/VPhoneCLI.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Sources/vphone-cli/VPhoneHardwareModel.swift
Normal file
24
Sources/vphone-cli/VPhoneHardwareModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
212
Sources/vphone-cli/VPhoneVM.swift
Normal file
212
Sources/vphone-cli/VPhoneVM.swift
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
249
Sources/vphone-cli/VPhoneVMWindow.swift
Normal file
249
Sources/vphone-cli/VPhoneVMWindow.swift
Normal 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
19
boot.sh
Executable 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
20
boot_dfu.sh
Executable 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
33
boot_sweet.sh
Executable 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
45
build_and_sign.sh
Executable 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"
|
||||
16
vphone.entitlements
Normal file
16
vphone.entitlements
Normal 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>
|
||||
Reference in New Issue
Block a user