Merge pull request #73 from p1atdev/ssh-retry

Add SSH retry logic to cfw_install and cfw_install_jb scripts
This commit is contained in:
Lakr
2026-03-04 11:07:30 +08:00
committed by Lakr
parent fa054ca719
commit 55a53e7179
12 changed files with 1348 additions and 36 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ Virtual iPhone boot tool using Apple's Virtualization.framework with PCC researc
- **Boot (DFU):** `make boot_dfu`
- **All targets:** `make help`
- **Python venv:** `make setup_venv` (installs to `.venv/`, activate with `source .venv/bin/activate`)
- **Platform:** macOS 14+ (Sequoia), SIP/AMFI disabled
- **Platform:** macOS 15+ (Sequoia), SIP/AMFI disabled
- **Language:** Swift 6.0 (SwiftPM), private APIs via [Dynamic](https://github.com/mhdhejazi/Dynamic)
- **Python deps:** `capstone`, `keystone-engine`, `pyimg4` (see `requirements.txt`)
+19 -2
View File
@@ -40,6 +40,7 @@ help:
@echo "LazyCat (AIO):"
@echo " make setup_machine Full setup through First Boot"
@echo " Options: JB=1 Jailbreak firmware/CFW path (WIP)"
@echo " DEV=1 Dev firmware/CFW path (dev TXM + cfw_install_dev)"
@echo " SKIP_PROJECT_SETUP=1 Skip setup_tools/build"
@echo ""
@echo "Setup (one-time):"
@@ -60,6 +61,7 @@ help:
@echo " Options: IPHONE_SOURCE= URL or local path to iPhone IPSW"
@echo " CLOUDOS_SOURCE= URL or local path to cloudOS IPSW"
@echo " make fw_patch Patch boot chain (6 components)"
@echo " make fw_patch_dev Patch boot chain (dev mode TXM patcher)"
@echo " make fw_patch_jb Run fw_patch + JB extension patches (WIP)"
@echo ""
@echo "Restore:"
@@ -72,6 +74,7 @@ help:
@echo ""
@echo "CFW:"
@echo " make cfw_install Install CFW mods via SSH"
@echo " make cfw_install_dev Install CFW mods via SSH (dev mode)"
@echo " make cfw_install_jb Install CFW + JB extensions (jetsam/procursus/basebin)"
@echo ""
@echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)"
@@ -83,8 +86,13 @@ help:
.PHONY: setup_machine setup_tools
setup_machine:
@if [ "$(filter 1 true yes YES TRUE,$(JB))" != "" ] && [ "$(filter 1 true yes YES TRUE,$(DEV))" != "" ]; then \
echo "Error: JB=1 and DEV=1 are mutually exclusive"; \
exit 1; \
fi
zsh $(SCRIPTS)/setup_machine.sh \
$(if $(filter 1 true yes YES TRUE,$(JB)),--jb,) \
$(if $(filter 1 true yes YES TRUE,$(DEV)),--dev,) \
$(if $(filter 1 true yes YES TRUE,$(SKIP_PROJECT_SETUP)),--skip-project-setup,)
setup_tools:
@@ -125,6 +133,9 @@ bundle: build $(INFO_PLIST)
@cp -f $$(command -v ldid) $(BUNDLE)/Contents/MacOS/ldid
@cp -f $$(command -v ideviceinstaller) $(BUNDLE)/Contents/MacOS/ideviceinstaller
@cp -f $$(command -v idevice_id) $(BUNDLE)/Contents/MacOS/idevice_id
@codesign --force --sign - $(BUNDLE)/Contents/MacOS/ldid
@codesign --force --sign - $(BUNDLE)/Contents/MacOS/ideviceinstaller
@codesign --force --sign - $(BUNDLE)/Contents/MacOS/idevice_id
@codesign --force --sign - --entitlements $(ENTITLEMENTS) $(BUNDLE_BIN)
@echo " bundled → $(BUNDLE)"
@@ -176,7 +187,7 @@ boot_dfu: build
# Firmware pipeline
# ═══════════════════════════════════════════════════════════════════
.PHONY: fw_prepare fw_patch fw_patch_jb
.PHONY: fw_prepare fw_patch fw_patch_dev fw_patch_jb
fw_prepare:
cd $(VM_DIR) && bash "$(CURDIR)/$(SCRIPTS)/fw_prepare.sh"
@@ -184,6 +195,9 @@ fw_prepare:
fw_patch:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch.py" .
fw_patch_dev:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch_dev.py" .
fw_patch_jb:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch_jb.py" .
@@ -215,10 +229,13 @@ ramdisk_send:
# CFW
# ═══════════════════════════════════════════════════════════════════
.PHONY: cfw_install cfw_install_jb
.PHONY: cfw_install cfw_install_dev cfw_install_jb
cfw_install:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
cfw_install_dev:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_dev.sh" .
cfw_install_jb:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .
+4 -2
View File
@@ -1,4 +1,4 @@
<div align="right"><strong><a href="./README_ja.md">🇯🇵日本語</a></strong> | <strong><a href="./README_zh.md">🇨🇳中文</a></strong> | <strong>🇬🇧English</strong></div>
<div align="right"><strong><a href="./README_ko.md">🇰🇷한국어</a></strong> | <strong><a href="./README_ja.md">🇯🇵日本語</a></strong> | <strong><a href="./README_zh.md">🇨🇳中文</a></strong> | <strong>🇬🇧English</strong></div>
# vphone-cli
@@ -16,6 +16,8 @@ Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC re
## Prerequisites
**Host OS:** macOS 15+ (Sequoia) is required for PV=3 virtualization.
**Disable SIP and AMFI** — required for private Virtualization.framework entitlements.
Boot into Recovery (long press power button), open Terminal:
@@ -36,7 +38,7 @@ Restart once more.
**Install dependencies:**
```bash
brew install wget gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
brew install ideviceinstaller wget gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
```
**Git LFS** — this repo uses Git LFS for large resource archives. Install and pull before building:
+4 -2
View File
@@ -1,4 +1,4 @@
<div align="right"><strong>🇯🇵日本語</strong> | <strong><a href="./README_zh.md">🇨🇳中文</a></strong> | <strong><a href="./README.md">🇬🇧English</a></strong></div>
<div align="right"><strong><a href="./README_ko.md">🇰🇷한국어</a></strong> | <strong>🇯🇵日本語</strong> | <strong><a href="./README_zh.md">🇨🇳中文</a></strong> | <strong><a href="./README.md">🇬🇧English</a></strong></div>
# vphone-cli
@@ -16,6 +16,8 @@ Apple の Virtualization.framework と PCC の研究用 VM インフラを使用
## 前提条件
**ホストOS:** PV=3 仮想化には macOS 15+Sequoia)が必要です。
**SIPとAMFIを無効化** — プライベートな Virtualization.framework の entitlement を使うために必要です。
復旧モードで起動し(電源ボタンを長押し)、ターミナルを開いて以下を実行します:
@@ -36,7 +38,7 @@ sudo nvram boot-args="amfi_get_out_of_my_way=1 -v"
**依存関係のインストール:**
```bash
brew install gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
brew install ideviceinstaller wget gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
```
**Git LFS** — このリポジトリは大きなリソースアーカイブに Git LFS を使用しています。ビルド前にインストールと pull を行ってください:
+222
View File
@@ -0,0 +1,222 @@
<div align="right"><strong>🇰🇷한국어</strong> | <strong><a href="./README_ja.md">🇯🇵日本語</a></strong> | <strong><a href="./README_zh.md">🇨🇳中文</a></strong> | <strong><a href="./README.md">🇬🇧English</a></strong></div>
# vphone-cli
PCC 리서치 VM 인프라와 Apple의 Virtualization.framework를 사용하여 가상 iPhone(iOS 26)을 부팅합니다.
![poc](./demo.png)
## 테스트된 환경
| Host | iPhone | CloudOS |
| ------------- | ------------------ | ------------- |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
## 사전 요구 사항
**호스트 OS:** PV=3 가상화를 위해 macOS 15+(Sequoia)가 필요합니다.
**SIP 및 AMFI 비활성화** — Private Virtualization.framework 권한을 사용하기 위해 필요합니다.
복구 모드(전원 버튼 길게 누르기)로 부팅한 후 터미널을 엽니다:
```bash
csrutil disable
csrutil allow-research-guests enable
```
macOS로 다시 시작한 후:
```bash
sudo nvram boot-args="amfi_get_out_of_my_way=1 -v"
```
한 번 더 재시작합니다.
**의존성(Dependencies) 설치:**
```bash
brew install ideviceinstaller wget gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
```
**Git LFS** — 이 저장소는 대용량 리소스 아카이브를 위해 Git LFS를 사용합니다. 빌드하기 전에 설치 및 pull을 진행하세요:
```bash
git lfs install
git lfs pull
```
## 초기 설정
```bash
make setup_machine # "First Boot"까지의 전체 과정 자동화 (복원/Ramdisk/커스텀 펌웨어 포함)
# 수동 단계(위 명령과 동일):
make setup_tools # brew 의존성 설치, trustcache + libimobiledevice 빌드, Python venv 생성
source .venv/bin/activate
```
`make setup_machine`을 사용하더라도 **복구 모드에서의 SIP/research-guest 설정**과 "First Boot" 명령어를 입력하기 위한 대화형 VM 콘솔 작업은 여전히 수동으로 필요합니다. 이 스크립트는 보안 설정 여부를 별도로 검증하지 않습니다.
## 빠른 시작
```bash
make build # vphone-cli 빌드 및 서명
make vm_new # vm/ 디렉토리 생성 (ROM, 디스크, SEP 저장소)
make fw_prepare # IPSW 다운로드, 추출, 병합, manifest 생성
make fw_patch # 부트 체인 패치 (6개 구성 요소, 41개 이상의 수정 사항)
```
## 복원
복원 프로세스를 위해 **두 개의 터미널**이 필요합니다. 터미널 2를 사용하는 동안 터미널 1을 계속 실행 상태로 두세요.
```bash
# 터미널 1
make boot_dfu # VM을 DFU 모드로 부팅 (계속 실행 유지)
```
```bash
# 터미널 2
make restore_get_shsh # SHSH blob 가져오기
make restore # idevicerestore를 통해 펌웨어 플래싱
```
## 램디스크 및 커스텀 펌웨어
터미널 1의 DFU 부팅을 중단(Ctrl+C)한 다음, 램디스크를 위해 다시 DFU로 부팅합니다:
```bash
# 터미널 1
make boot_dfu # 계속 실행 유지
```
```bash
# 터미널 2
make ramdisk_build # 서명된 SSH 램디스크 빌드
make ramdisk_send # 장치로 전송
```
램디스크가 실행되면(출력에 `Running server`가 표시됨), **세 번째 터미널**을 열어 iproxy 터널을 시작한 후, 터미널 2에서 커스텀 펌웨어를 설치합니다:
```bash
# 터미널 3 — 계속 실행 유지
iproxy 2222 22
```
```bash
# 터미널 2
make cfw_install
```
## 첫 부팅
터미널 1의 DFU 부팅을 중단(Ctrl+C)한 후 다음을 실행합니다:
```bash
make boot
```
그러면 VM에서 **direct console**이 나타납니다. `bash-4.4#`이 보이면 엔터를 누르고 다음 명령어를 실행하여 쉘 환경을 초기화하고 SSH 호스트 키를 생성하세요:
```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'
mkdir -p /var/dropbear
cp /iosbinpack64/etc/profile /var/profile
cp /iosbinpack64/etc/motd /var/motd
# generate SSH host keys (required for SSH to work)
dropbearkey -t rsa -f /var/dropbear/dropbear_rsa_host_key
dropbearkey -t ecdsa -f /var/dropbear/dropbear_ecdsa_host_key
shutdown -h now
```
> **참고:** 호스트 키 생성 단계를 거치지 않으면 dropbear(SSH 서버)가 연결을 수락하더라도 SSH 핸드셰이크를 수행할 키가 없어 즉시 연결을 종료합니다.
## 이후 부팅
```bash
make boot
```
별도의 터미널에서 iproxy 터널을 시작합니다:
```bash
iproxy 22222 22222 # SSH
iproxy 5901 5901 # VNC
iproxy 5910 5910 # RPC
```
다음을 통해 연결합니다:
- **SSH:** `ssh -p 22222 [email protected]` (password: `alpine`)
- **VNC:** `vnc://127.0.0.1:5901`
- [**RPC:**](http://github.com/doronz88/rpc-project) `rpcclient -p 5910 127.0.0.1`
## 전체 Make 타겟
전체 목록을 보려면 `make help`를 실행하세요. 주요 타겟은 다음과 같습니다:
| 타겟 | 설명 |
| ------------------- | ---------------------------- |
| `build` | vphone-cli 빌드 및 서명 |
| `vm_new` | VM 디렉토리 생성 |
| `fw_prepare` | IPSW 다운로드 및 병합 |
| `fw_patch` | 부트 체인 패치 |
| `boot` / `boot_dfu` | VM 부팅 (GUI / DFU headless) |
| `restore_get_shsh` | SHSH blob 가져오기 |
| `restore` | 펌웨어 플래싱 |
| `ramdisk_build` | SSH 램디스크 빌드 |
| `ramdisk_send` | 장치에 램디스크 전송 |
| `cfw_install` | CFW mods 설치 |
| `clean` | 빌드 아티팩트 제거 |
## FAQ
> **무엇보다 먼저 — `git pull`을 실행하여 최신 버전인지 확인하세요.**
**Q: 실행하려고 하면 `zsh: killed ./vphone-cli` 오류가 발생합니다.**
AMFI가 비활성화되지 않았습니다. boot-arg를 설정하고 재시작하세요:
```bash
sudo nvram boot-args="amfi_get_out_of_my_way=1 -v"
```
**Q: "Press home to continue" 화면에서 멈췄습니다.**
VNC(`vnc://127.0.0.1:5901`)로 접속하여 화면의 아무 곳이나 우클릭(Mac 트랙패드에서는 두 손가락 클릭)하세요. 이것이 홈 버튼 누르기를 시뮬레이션합니다.
**Q: SSH가 연결되자마자 종료됩니다 (`Connection closed by 127.0.0.1`).**
첫 부팅 시 Dropbear 호스트 키가 생성되지 않았습니다. VNC나 `make boot` 콘솔을 통해 연결하여 다음을 실행하세요:
```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'
mkdir -p /var/dropbear
dropbearkey -t rsa -f /var/dropbear/dropbear_rsa_host_key
dropbearkey -t ecdsa -f /var/dropbear/dropbear_ecdsa_host_key
killall dropbear
dropbear -R -p 22222
```
**Q: 최신 iOS 버전으로 업데이트할 수 있나요?**
네. `fw_prepare`를 원하는 버전의 IPSW URL로 덮어쓰세요:
```bash
export IPHONE_SOURCE=/path/to/some_os.ipsw
export CLOUDOS_SOURCE=/path/to/some_os.ipsw
make fw_prepare
make fw_patch
```
저희의 패치는 정적 오프셋이 아닌 바이너리 분석을 통해 적용되므로, 최신 버전에서도 작동할 것입니다. 만약 문제가 발생하면 AI에게 도움을 요청하세요.
## 감사 인사
- [wh1te4ever/super-tart-vphone-writeup](https://github.com/wh1te4ever/super-tart-vphone-writeup)
+10 -2
View File
@@ -1,4 +1,4 @@
<div align="right"><strong><a href="./README_ja.md">🇯🇵日本語</a></strong> | <strong>🇨🇳中文</strong> | <strong><a href="./README.md">🇬🇧English</a></strong></div>
<div align="right"><strong><a href="./README_ko.md">🇰🇷한국어</a></strong> | <strong><a href="./README_ja.md">🇯🇵日本語</a></strong> | <strong>🇨🇳中文</strong> | <strong><a href="./README.md">🇬🇧English</a></strong></div>
# vphone-cli
@@ -16,6 +16,8 @@
## 先决条件
**主机系统:** PV=3 虚拟化要求 macOS 15+Sequoia)。
**禁用 SIP 和 AMFI** —— 需要私有的 Virtualization.framework 权限。
重启到恢复模式(长按电源键),打开终端:
@@ -36,7 +38,7 @@ sudo nvram boot-args="amfi_get_out_of_my_way=1 -v"
**安装依赖:**
```bash
brew install gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
brew install ideviceinstaller wget gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
```
**Git LFS** —— 本仓库使用 Git LFS 存储大型资源文件。构建前请先安装并拉取:
@@ -210,3 +212,9 @@ export CLOUDOS_SOURCE=/path/to/some_os.ipsw
make fw_prepare
make fw_patch
```
我们的补丁是通过二进制分析(binary analysis)而非静态偏移(static offsets)应用的,因此更新的版本应该也能正常工作。如果出现问题,可以寻求 AI 的帮助。
## 致谢
- [wh1te4ever/super-tart-vphone-writeup](https://github.com/wh1te4ever/super-tart-vphone-writeup)
+19 -7
View File
@@ -33,6 +33,7 @@ SSH_PORT=2222
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"
SSH_RETRY="${SSH_RETRY:-3}"
SSHPASS_BIN=""
SSH_OPTS=(
-o StrictHostKeyChecking=no
@@ -62,16 +63,27 @@ _sshpass() {
"$SSHPASS_BIN" -p "$SSH_PASS" "$@"
}
ssh_cmd() {
_sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@"
_ssh_retry() {
local attempt rc label
label=${2:-cmd}
for ((attempt = 1; attempt <= SSH_RETRY; attempt++)); do
"$@" && return 0
rc=$?
[[ $rc -ne 255 ]] && return $rc # real command failure — don't retry
echo " [${label}] connection lost (attempt $attempt/$SSH_RETRY), retrying in 3s..." >&2
sleep 3
done
return 255
}
scp_to() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" -r "$1" "$SSH_USER@$SSH_HOST:$2"
ssh_cmd() {
_ssh_retry _sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@";
}
scp_from() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" "$SSH_USER@$SSH_HOST:$1" "$2"
scp_to() {
_ssh_retry _sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" -r "$1" "$SSH_USER@$SSH_HOST:$2";
}
scp_from() {
_ssh_retry _sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" "$SSH_USER@$SSH_HOST:$1" "$2";
}
remote_file_exists() {
+467
View File
@@ -0,0 +1,467 @@
#!/bin/zsh
# cfw_install.sh — Install base 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 (make ramdisk_send)
# - `ipsw` tool installed (brew install blacktop/tap/ipsw)
# - `aea` tool available (macOS 12+)
# - Python: make setup_venv && source .venv/bin/activate
# - cfw_input/ or resources/cfw_dev_input.tar.zst present
#
# Usage: make cfw_install_dev
set -euo pipefail
VM_DIR="${1:-.}"
SCRIPT_DIR="${0:a:h}"
CFW_SKIP_HALT="${CFW_SKIP_HALT:-0}"
# Resolve absolute paths
VM_DIR="$(cd "$VM_DIR" && pwd)"
# ── Configuration ───────────────────────────────────────────────
CFW_INPUT="cfw_input"
CFW_ARCHIVE="cfw_dev_input.tar.zst"
TEMP_DIR="$VM_DIR/.cfw_temp"
SSH_PORT=2222
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"
SSH_RETRY="${SSH_RETRY:-3}"
SSHPASS_BIN=""
SSH_OPTS=(
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o PreferredAuthentications=password
-o ConnectTimeout=30
-q
)
# ── Helpers ─────────────────────────────────────────────────────
die() {
echo "[-] $*" >&2
exit 1
}
check_prerequisites() {
local missing=()
command -v sshpass &>/dev/null || missing+=("sshpass")
command -v ldid &>/dev/null || missing+=("ldid (brew install ldid-procursus)")
if ((${#missing[@]} > 0)); then
die "Missing required tools: ${missing[*]}. Run: make setup_tools"
fi
SSHPASS_BIN="$(command -v sshpass)"
}
_sshpass() {
"$SSHPASS_BIN" -p "$SSH_PASS" "$@"
}
_ssh_retry() {
local attempt rc label
label=${2:-cmd}
for ((attempt = 1; attempt <= SSH_RETRY; attempt++)); do
"$@" && return 0
rc=$?
[[ $rc -ne 255 ]] && return $rc # real command failure — don't retry
echo " [${label}] connection lost (attempt $attempt/$SSH_RETRY), retrying in 3s..." >&2
sleep 3
done
return 255
}
ssh_cmd() {
_ssh_retry _sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@";
}
scp_to() {
_ssh_retry _sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" -r "$1" "$SSH_USER@$SSH_HOST:$2";
}
scp_from() {
_ssh_retry _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")
ldid "${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/resources" "$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 "[*] cfw_install.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"
check_prerequisites
mkdir -p "$TEMP_DIR"
# ── Parse Cryptex paths from BuildManifest ─────────────────────
echo ""
echo "[*] Parsing iPhone BuildManifest for Cryptex paths..."
CRYPTEX_PATHS=$(python3 "$SCRIPT_DIR/patchers/cfw.py" cryptex-paths "$RESTORE_DIR/BuildManifest-iPhone.plist")
CRYPTEX_SYSOS=$(echo "$CRYPTEX_PATHS" | head -1)
CRYPTEX_APPOS=$(echo "$CRYPTEX_PATHS" | tail -1)
echo " SystemOS: $CRYPTEX_SYSOS"
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
# Patch launchd jetsum guard
echo ""
echo " Patching launchd (jetsam guard)..."
if ! remote_file_exists "/mnt1/sbin/launchd.bak"; then
echo " Creating backup..."
ssh_cmd "/bin/cp /mnt1/sbin/launchd /mnt1/sbin/launchd.bak"
fi
scp_from "/mnt1/sbin/launchd.bak" "$TEMP_DIR/launchd"
python3 "$SCRIPT_DIR/patchers/cfw.py" patch-launchd-jetsam "$TEMP_DIR/launchd"
ldid_sign "$TEMP_DIR/launchd"
scp_to "$TEMP_DIR/launchd" "/mnt1/sbin/launchd"
ssh_cmd "/bin/chmod 0755 /mnt1/sbin/launchd"
echo " [+] launchd patched"
# Rename APFS update snapshot to orig-fs (idempotent)
echo " Checking APFS snapshots..."
SNAP_LIST=$(ssh_cmd "snaputil -l /mnt1 2>/dev/null" || true)
if echo "$SNAP_LIST" | grep -q "^orig-fs$"; then
echo " Snapshot 'orig-fs' already exists, skipping rename"
else
UPDATE_SNAP=$(echo "$SNAP_LIST" | grep "^com\.apple\.os\.update-" | head -1)
if [[ -n "$UPDATE_SNAP" ]]; then
echo " Renaming snapshot: $UPDATE_SNAP -> orig-fs"
ssh_cmd "snaputil -n '$UPDATE_SNAP' orig-fs /mnt1"
# Verify rename succeeded
if ! ssh_cmd "snaputil -l /mnt1 2>/dev/null" | grep -q "^orig-fs$"; then
die "Failed to rename snapshot to orig-fs"
fi
echo " Snapshot renamed, remounting..."
ssh_cmd "/sbin/umount /mnt1"
remote_mount /dev/disk1s1 /mnt1
echo " [+] Snapshot renamed to orig-fs"
else
echo " No com.apple.os.update- snapshot found, skipping"
fi
fi
ssh_cmd "/bin/rm -rf /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
ssh_cmd "/bin/mkdir -p /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
ssh_cmd "/bin/chmod 0755 /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
# 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/patchers/cfw.py" patch-seputil "$TEMP_DIR/seputil"
ldid_sign "$TEMP_DIR/seputil" "com.apple.seputil"
scp_to "$TEMP_DIR/seputil" "/mnt1/usr/libexec/seputil"
ssh_cmd "/bin/chmod 0755 /mnt1/usr/libexec/seputil"
# 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/patchers/cfw.py" patch-launchd-cache-loader "$TEMP_DIR/launchd_cache_loader"
ldid_sign "$TEMP_DIR/launchd_cache_loader" "com.apple.launchd_cache_loader"
scp_to "$TEMP_DIR/launchd_cache_loader" "/mnt1/usr/libexec/launchd_cache_loader"
ssh_cmd "/bin/chmod 0755 /mnt1/usr/libexec/launchd_cache_loader"
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/patchers/cfw.py" patch-mobileactivationd "$TEMP_DIR/mobileactivationd"
ldid_sign "$TEMP_DIR/mobileactivationd"
scp_to "$TEMP_DIR/mobileactivationd" "/mnt1/usr/libexec/mobileactivationd"
ssh_cmd "/bin/chmod 0755 /mnt1/usr/libexec/mobileactivationd"
echo " [+] mobileactivationd patched"
# ═══════════ 7/7 LAUNCHDAEMONS + LAUNCHD.PLIST ══════════════
echo ""
echo "[7/7] Installing LaunchDaemons..."
# Install vphoned (vsock HID injector daemon)
VPHONED_SRC="$SCRIPT_DIR/vphoned"
VPHONED_BIN="$VPHONED_SRC/vphoned"
VPHONED_SRCS=(
"$VPHONED_SRC/vphoned.m"
"$VPHONED_SRC/vphoned_protocol.m"
"$VPHONED_SRC/vphoned_hid.m"
"$VPHONED_SRC/vphoned_devmode.m"
"$VPHONED_SRC/vphoned_location.m"
"$VPHONED_SRC/vphoned_files.m"
)
needs_vphoned_build=0
if [[ ! -f "$VPHONED_BIN" ]]; then
needs_vphoned_build=1
else
for src in "${VPHONED_SRCS[@]}"; do
if [[ "$src" -nt "$VPHONED_BIN" ]]; then
needs_vphoned_build=1
break
fi
done
fi
if [[ "$needs_vphoned_build" == "1" ]]; then
echo " Building vphoned for arm64..."
xcrun -sdk iphoneos clang -arch arm64 -Os -fobjc-arc \
-I"$VPHONED_SRC" \
-o "$VPHONED_BIN" "${VPHONED_SRCS[@]}" \
-framework Foundation
fi
cp "$VPHONED_BIN" "$TEMP_DIR/vphoned"
ldid \
-S"$VPHONED_SRC/entitlements.plist" \
-M "-K$VM_DIR/$CFW_INPUT/signcert.p12" \
"$TEMP_DIR/vphoned"
scp_to "$TEMP_DIR/vphoned" "/mnt1/usr/bin/vphoned"
ssh_cmd "/bin/chmod 0755 /mnt1/usr/bin/vphoned"
# Keep a copy of the signed binary for host-side auto-update
cp "$TEMP_DIR/vphoned" "$VM_DIR/.vphoned.signed"
echo " [+] vphoned installed (signed copy at .vphoned.signed)"
# Send daemon plists (overwrite on re-run)
for plist in bash.plist dropbear.plist trollvnc.plist rpcserver_ios.plist; do
scp_to "$INPUT_DIR/jb/LaunchDaemons/$plist" "/mnt1/System/Library/LaunchDaemons/"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/LaunchDaemons/$plist"
done
scp_to "$VPHONED_SRC/vphoned.plist" "/mnt1/System/Library/LaunchDaemons/"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/LaunchDaemons/vphoned.plist"
# 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"
cp "$VPHONED_SRC/vphoned.plist" "$INPUT_DIR/jb/LaunchDaemons/"
python3 "$SCRIPT_DIR/patchers/cfw.py" inject-daemons "$TEMP_DIR/launchd.plist" "$INPUT_DIR/jb/LaunchDaemons"
scp_to "$TEMP_DIR/launchd.plist" "/mnt1/System/Library/xpc/launchd.plist"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/xpc/launchd.plist"
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/vphoned" \
"$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)"
if [[ "$CFW_SKIP_HALT" == "1" ]]; then
echo "[*] CFW_SKIP_HALT=1, skipping halt."
else
ssh_cmd "/sbin/halt" || true
fi
+19 -7
View File
@@ -39,6 +39,7 @@ SSH_PORT=2222
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"
SSH_RETRY="${SSH_RETRY:-3}"
SSHPASS_BIN=""
SSH_OPTS=(
-o StrictHostKeyChecking=no
@@ -68,16 +69,27 @@ _sshpass() {
"$SSHPASS_BIN" -p "$SSH_PASS" "$@"
}
ssh_cmd() {
_sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@"
_ssh_retry() {
local attempt rc label
label=${2-cmd}
for ((attempt = 1; attempt <= SSH_RETRY; attempt++)); do
"$@" && return 0
rc=$?
[[ $rc -ne 255 ]] && return $rc # real command failure — don't retry
echo " [${label}] connection lost (attempt $attempt/$SSH_RETRY), retrying in 3s..." >&2
sleep 3
done
return 255
}
scp_to() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" -r "$1" "$SSH_USER@$SSH_HOST:$2"
ssh_cmd() {
_ssh_retry _sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@";
}
scp_from() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" "$SSH_USER@$SSH_HOST:$1" "$2"
scp_to() {
_ssh_retry _sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" -r "$1" "$SSH_USER@$SSH_HOST:$2";
}
scp_from() {
_ssh_retry _sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" "$SSH_USER@$SSH_HOST:$1" "$2";
}
remote_file_exists() {
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
fw_patch_dev.py — Patch boot-chain components using dev TXM patch set.
Usage:
python3 fw_patch_dev.py [vm_directory]
"""
import os
import sys
from fw_patch import (
find_file,
find_restore_dir,
patch_avpbooter,
patch_ibec,
patch_ibss,
patch_kernelcache,
patch_llb,
patch_component,
)
from patchers.txm_dev import TXMPatcher
def patch_txm_dev(data):
p = TXMPatcher(data)
n = p.apply()
print(f" [+] {n} TXM dev patches applied dynamically")
return n > 0
COMPONENTS = [
# (name, search_base_is_restore, search_patterns, patch_function, preserve_payp)
("AVPBooter", False, ["AVPBooter*.bin"], patch_avpbooter, False),
("iBSS", True, ["Firmware/dfu/iBSS.vresearch101.RELEASE.im4p"], patch_ibss, False),
("iBEC", True, ["Firmware/dfu/iBEC.vresearch101.RELEASE.im4p"], patch_ibec, False),
(
"LLB",
True,
["Firmware/all_flash/LLB.vresearch101.RELEASE.im4p"],
patch_llb,
False,
),
("TXM", True, ["Firmware/txm.iphoneos.research.im4p"], patch_txm_dev, True),
("kernelcache", True, ["kernelcache.research.vphone600"], patch_kernelcache, True),
]
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}")
sys.exit(1)
print(f"[*] VM directory: {vm_dir}")
print(f"[*] Restore directory: {restore_dir}")
print(f"[*] Patching {len(COMPONENTS)} boot-chain components (dev mode) ...")
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 (dev mode)!")
print(f"{'=' * 60}")
if __name__ == "__main__":
main()
+481
View File
@@ -0,0 +1,481 @@
#!/usr/bin/env python3
"""
txm_patcher.py — Dynamic patcher for TXM (Trusted Execution Monitor) images.
Finds TXM patch sites dynamically and applies trustcache/entitlement/developer
mode bypasses. NO hardcoded offsets.
Dependencies: keystone-engine, capstone
"""
import struct
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
# ── Assembly / disassembly singletons ──────────────────────────
_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE)
_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
_cs.detail = True
_cs.skipdata = True
def _asm(s):
enc, _ = _ks.asm(s)
if not enc:
raise RuntimeError(f"asm failed: {s}")
return bytes(enc)
MOV_X0_0 = _asm("mov x0, #0")
MOV_X0_1 = _asm("mov x0, #1")
MOV_W0_1 = _asm("mov w0, #1")
MOV_X0_X20 = _asm("mov x0, x20")
STRB_W0_X20_30 = _asm("strb w0, [x20, #0x30]")
NOP = _asm("nop")
PACIBSP = _asm("hint #27")
def _disasm_one(data, off):
insns = list(_cs.disasm(data[off : off + 4], off))
return insns[0] if insns else None
def _find_asm_pattern(data, asm_str):
enc, _ = _ks.asm(asm_str)
pattern = bytes(enc)
results = []
off = 0
while True:
idx = data.find(pattern, off)
if idx < 0:
break
results.append(idx)
off = idx + 4
return results
# ── TXMPatcher ─────────────────────────────────────────────────
class TXMPatcher:
"""Dynamic patcher for TXM images.
Patches:
1. Trustcache binary-search BL → mov x0, #0
(in the AMFI cert verification function identified by the
unique constant 0x20446 loaded into w19)
2. Selector24 hash extraction: nop LDR X1 + nop BL
3. get-task-allow entitlement check BL → mov x0, #1
4. Selector42|29: shellcode hook + manifest flag force
5. debugger entitlement check BL → mov w0, #1
6. developer-mode guard branch → nop
"""
def __init__(self, data, verbose=True):
self.data = data
self.raw = bytes(data)
self.size = len(data)
self.verbose = verbose
self.patches = []
def _log(self, msg):
if self.verbose:
print(msg)
def emit(self, off, patch_bytes, desc):
self.patches.append((off, patch_bytes, desc))
if self.verbose:
before_insns = list(_cs.disasm(self.raw[off : off + 4], off))
after_insns = list(_cs.disasm(patch_bytes, off))
b_str = (
f"{before_insns[0].mnemonic} {before_insns[0].op_str}"
if before_insns
else "???"
)
a_str = (
f"{after_insns[0].mnemonic} {after_insns[0].op_str}"
if after_insns
else "???"
)
print(f" 0x{off:06X}: {b_str}{a_str} [{desc}]")
def apply(self):
self.find_all()
for off, pb, _ in self.patches:
self.data[off : off + len(pb)] = pb
if self.verbose and self.patches:
self._log(f"\n [{len(self.patches)} TXM patches applied]")
return len(self.patches)
def find_all(self):
self.patches = []
self.patch_trustcache_bypass()
self.patch_get_task_allow_force_true()
self.patch_selector42_29_shellcode()
self.patch_debugger_entitlement_force_true()
self.patch_developer_mode_bypass()
return self.patches
# ── helpers ──────────────────────────────────────────────────
def _asm_at(self, asm_line, addr):
enc, _ = _ks.asm(asm_line, addr=addr)
if not enc:
raise RuntimeError(f"asm failed at 0x{addr:X}: {asm_line}")
return bytes(enc)
def _find_func_start(self, off, back=0x1000):
start = max(0, off - back)
for scan in range(off & ~3, start - 1, -4):
if self.raw[scan : scan + 4] == PACIBSP:
return scan
return None
def _find_refs_to_offset(self, target_off):
refs = []
for off in range(0, self.size - 8, 4):
a = _disasm_one(self.raw, off)
b = _disasm_one(self.raw, off + 4)
if not a or not b:
continue
if a.mnemonic != "adrp" or b.mnemonic != "add":
continue
if len(a.operands) < 2 or len(b.operands) < 3:
continue
if a.operands[0].reg != b.operands[1].reg:
continue
if a.operands[1].imm + b.operands[2].imm == target_off:
refs.append((off, off + 4))
return refs
def _find_string_refs(self, needle):
if isinstance(needle, str):
needle = needle.encode()
refs = []
seen = set()
off = 0
while True:
s_off = self.raw.find(needle, off)
if s_off < 0:
break
off = s_off + 1
for r in self._find_refs_to_offset(s_off):
if r[0] not in seen:
seen.add(r[0])
refs.append((s_off, r[0], r[1]))
return refs
def _find_debugger_gate_func_start(self):
refs = self._find_string_refs(b"com.apple.private.cs.debugger")
starts = set()
for _, _, add_off in refs:
for scan in range(add_off, min(add_off + 0x20, self.size - 8), 4):
i = _disasm_one(self.raw, scan)
n = _disasm_one(self.raw, scan + 4)
p1 = _disasm_one(self.raw, scan - 4) if scan >= 4 else None
p2 = _disasm_one(self.raw, scan - 8) if scan >= 8 else None
if not all((i, n, p1, p2)):
continue
if not (
i.mnemonic == "bl"
and n.mnemonic == "tbnz"
and n.op_str.startswith("w0, #0,")
and p1.mnemonic == "mov"
and p1.op_str == "x2, #0"
and p2.mnemonic == "mov"
and p2.op_str == "x0, #0"
):
continue
fs = self._find_func_start(scan)
if fs is not None:
starts.add(fs)
if len(starts) != 1:
return None
return next(iter(starts))
def _find_udf_cave(self, min_insns=6, near_off=None, max_distance=0x80000):
need = min_insns * 4
start = 0 if near_off is None else max(0, near_off - 0x1000)
end = self.size if near_off is None else min(self.size, near_off + max_distance)
best = None
best_dist = None
off = start
while off < end:
run = off
while run < end and self.raw[run : run + 4] == b"\x00\x00\x00\x00":
run += 4
if run - off >= need:
prev = _disasm_one(self.raw, off - 4) if off >= 4 else None
if prev and prev.mnemonic in (
"b",
"b.eq",
"b.ne",
"b.lo",
"b.hs",
"cbz",
"cbnz",
"tbz",
"tbnz",
):
# Leave 2-word safety gap after the preceding branch.
padded = off + 8
if padded + need <= run:
return padded
return off
if near_off is not None and _disasm_one(self.raw, off):
dist = abs(off - near_off)
if best is None or dist < best_dist:
best = off
best_dist = dist
off = run + 4 if run > off else off + 4
return best
# ═══════════════════════════════════════════════════════════
# Trustcache bypass
#
# The AMFI cert verification function has a unique constant:
# mov w19, #0x2446; movk w19, #2, lsl #16 (= 0x20446)
#
# Within that function, a binary search calls a hash-compare
# function with SHA-1 size:
# mov w2, #0x14; bl <hash_cmp>; cbz w0, <match>
# followed by:
# tbnz w0, #0x1f, <lower_half> (sign bit = search direction)
#
# Patch: bl <hash_cmp> → mov x0, #0
# This makes cbz always branch to <match>, bypassing the
# trustcache lookup entirely.
# ═══════════════════════════════════════════════════════════
def patch_trustcache_bypass(self):
# Step 1: Find the unique function marker (mov w19, #0x2446)
locs = _find_asm_pattern(self.raw, "mov w19, #0x2446")
if len(locs) != 1:
self._log(f" [-] TXM: expected 1 'mov w19, #0x2446', found {len(locs)}")
return
marker_off = locs[0]
# Step 2: Find the containing function (scan back for PACIBSP)
pacibsp = _asm("hint #27")
func_start = None
for scan in range(marker_off & ~3, max(0, marker_off - 0x200), -4):
if self.raw[scan : scan + 4] == pacibsp:
func_start = scan
break
if func_start is None:
self._log(" [-] TXM: function start not found")
return
# Step 3: Within the function, find mov w2, #0x14; bl; cbz w0; tbnz w0, #0x1f
func_end = min(func_start + 0x2000, self.size)
insns = list(_cs.disasm(self.raw[func_start:func_end], func_start))
for i, ins in enumerate(insns):
if not (ins.mnemonic == "mov" and ins.op_str == "w2, #0x14"):
continue
if i + 3 >= len(insns):
continue
bl_ins = insns[i + 1]
cbz_ins = insns[i + 2]
tbnz_ins = insns[i + 3]
if (
bl_ins.mnemonic == "bl"
and cbz_ins.mnemonic == "cbz"
and "w0" in cbz_ins.op_str
and tbnz_ins.mnemonic in ("tbnz", "tbz")
and "#0x1f" in tbnz_ins.op_str
):
self.emit(
bl_ins.address, MOV_X0_0, "trustcache bypass: bl → mov x0, #0"
)
return
self._log(" [-] TXM: binary search pattern not found in function")
def patch_get_task_allow_force_true(self):
"""Force get-task-allow entitlement call to return true."""
refs = self._find_string_refs(b"get-task-allow")
if not refs:
self._log(" [-] TXM: get-task-allow string refs not found")
return False
cands = []
for _, _, add_off in refs:
for scan in range(add_off, min(add_off + 0x20, self.size - 4), 4):
i = _disasm_one(self.raw, scan)
n = _disasm_one(self.raw, scan + 4)
if not i or not n:
continue
if (
i.mnemonic == "bl"
and n.mnemonic == "tbnz"
and n.op_str.startswith("w0, #0,")
):
cands.append(scan)
if len(cands) != 1:
self._log(
f" [-] TXM: expected 1 get-task-allow BL site, found {len(cands)}"
)
return False
self.emit(cands[0], MOV_X0_1, "get-task-allow: bl -> mov x0,#1")
return True
def patch_selector42_29_shellcode(self):
"""Selector 42|29 patch via dynamic cave shellcode + branch redirect."""
fn = self._find_debugger_gate_func_start()
if fn is None:
self._log(" [-] TXM: debugger-gate function not found (selector42|29)")
return False
stubs = []
for off in range(4, self.size - 24, 4):
p = _disasm_one(self.raw, off - 4)
i0 = _disasm_one(self.raw, off)
i1 = _disasm_one(self.raw, off + 4)
i2 = _disasm_one(self.raw, off + 8)
i3 = _disasm_one(self.raw, off + 12)
i4 = _disasm_one(self.raw, off + 16)
i5 = _disasm_one(self.raw, off + 20)
if not all((p, i0, i1, i2, i3, i4, i5)):
continue
if not (p.mnemonic == "bti" and p.op_str == "j"):
continue
if not (i0.mnemonic == "mov" and i0.op_str == "x0, x20"):
continue
if not (
i1.mnemonic == "bl" and i2.mnemonic == "mov" and i2.op_str == "x1, x21"
):
continue
if not (
i3.mnemonic == "mov"
and i3.op_str == "x2, x22"
and i4.mnemonic == "bl"
and i5.mnemonic == "b"
):
continue
if i4.operands and i4.operands[0].imm == fn:
stubs.append(off)
if len(stubs) != 1:
self._log(f" [-] TXM: selector42|29 stub expected 1, found {len(stubs)}")
return False
stub_off = stubs[0]
cave = self._find_udf_cave(min_insns=6, near_off=stub_off)
if cave is None:
self._log(" [-] TXM: no UDF cave found for selector42|29 shellcode")
return False
self.emit(
stub_off,
self._asm_at(f"b #0x{cave:X}", stub_off),
"selector42|29: branch to shellcode",
)
self.emit(cave, NOP, "selector42|29 shellcode pad: udf -> nop")
self.emit(cave + 4, MOV_X0_1, "selector42|29 shellcode: mov x0,#1")
self.emit(
cave + 8, STRB_W0_X20_30, "selector42|29 shellcode: strb w0,[x20,#0x30]"
)
self.emit(cave + 12, MOV_X0_X20, "selector42|29 shellcode: mov x0,x20")
self.emit(
cave + 16,
self._asm_at(f"b #0x{stub_off + 4:X}", cave + 16),
"selector42|29 shellcode: branch back",
)
return True
def patch_debugger_entitlement_force_true(self):
"""Force debugger entitlement call to return true."""
refs = self._find_string_refs(b"com.apple.private.cs.debugger")
if not refs:
self._log(" [-] TXM: debugger refs not found")
return False
cands = []
for _, _, add_off in refs:
for scan in range(add_off, min(add_off + 0x20, self.size - 4), 4):
i = _disasm_one(self.raw, scan)
n = _disasm_one(self.raw, scan + 4)
p1 = _disasm_one(self.raw, scan - 4) if scan >= 4 else None
p2 = _disasm_one(self.raw, scan - 8) if scan >= 8 else None
if not all((i, n, p1, p2)):
continue
if (
i.mnemonic == "bl"
and n.mnemonic == "tbnz"
and n.op_str.startswith("w0, #0,")
and p1.mnemonic == "mov"
and p1.op_str == "x2, #0"
and p2.mnemonic == "mov"
and p2.op_str == "x0, #0"
):
cands.append(scan)
if len(cands) != 1:
self._log(f" [-] TXM: expected 1 debugger BL site, found {len(cands)}")
return False
self.emit(cands[0], MOV_W0_1, "debugger entitlement: bl -> mov w0,#1")
return True
def patch_developer_mode_bypass(self):
"""Developer-mode bypass: NOP conditional guard before deny log path."""
refs = self._find_string_refs(
b"developer mode enabled due to system policy configuration"
)
if not refs:
self._log(" [-] TXM: developer-mode string ref not found")
return False
cands = []
for _, _, add_off in refs:
for back in range(add_off - 4, max(add_off - 0x20, 0), -4):
ins = _disasm_one(self.raw, back)
if not ins:
continue
if ins.mnemonic not in ("tbz", "tbnz", "cbz", "cbnz"):
continue
if not ins.op_str.startswith("w9, #0,"):
continue
cands.append(back)
if len(cands) != 1:
self._log(
f" [-] TXM: expected 1 developer mode guard, found {len(cands)}"
)
return False
self.emit(cands[0], NOP, "developer mode bypass")
return True
# ── CLI entry point ────────────────────────────────────────────
if __name__ == "__main__":
import sys, argparse
parser = argparse.ArgumentParser(description="Dynamic TXM patcher")
parser.add_argument("txm", help="Path to raw or IM4P TXM image")
parser.add_argument("-q", "--quiet", action="store_true")
args = parser.parse_args()
print(f"Loading {args.txm}...")
file_raw = open(args.txm, "rb").read()
try:
from pyimg4 import IM4P
im4p = IM4P(file_raw)
if im4p.payload.compression:
im4p.payload.decompress()
payload = im4p.payload.data
print(f" format: IM4P (fourcc={im4p.fourcc})")
except Exception:
payload = file_raw
print(f" format: raw")
data = bytearray(payload)
print(f" size: {len(data)} bytes ({len(data) / 1024:.1f} KB)\n")
patcher = TXMPatcher(data, verbose=not args.quiet)
n = patcher.apply()
print(f"\n {n} patches applied.")
+25 -13
View File
@@ -3,9 +3,9 @@
#
# Runs README flow up to (but not including) "Subsequent Boots":
# 1) Host deps + project setup/build
# 2) vm_new + fw_prepare + fw_patch (or fw_patch_jb with --jb)
# 2) vm_new + fw_prepare + fw_patch (or fw_patch_dev/ fw_patch_jb with --dev/--jb)
# 3) DFU restore (boot_dfu + restore_get_shsh + restore)
# 4) Ramdisk + CFW (boot_dfu + ramdisk_build + ramdisk_send + iproxy + cfw_install / cfw_install_jb)
# 4) Ramdisk + CFW (boot_dfu + ramdisk_build + ramdisk_send + iproxy + cfw_install / cfw_install_dev / cfw_install_jb)
# 5) First boot launch (`make boot`) with printed in-guest commands
set -euo pipefail
@@ -36,6 +36,7 @@ RAMDISK_SSH_PORT="${RAMDISK_SSH_PORT:-2222}"
RAMDISK_SSH_USER="${RAMDISK_SSH_USER:-root}"
RAMDISK_SSH_PASS="${RAMDISK_SSH_PASS:-alpine}"
JB_MODE=0
DEV_MODE=0
SKIP_PROJECT_SETUP=0
die() {
@@ -219,22 +220,19 @@ check_platform() {
local major
major="$(sw_vers -productVersion | cut -d. -f1)"
if [[ -z "$major" || "$major" -lt 14 ]]; then
die "macOS 14+ required (detected: $(sw_vers -productVersion))"
if [[ -z "$major" || "$major" -lt 15 ]]; then
die "macOS 15+ required (detected: $(sw_vers -productVersion))"
fi
xcrun -sdk iphoneos --show-sdk-path >/dev/null 2>&1 \
|| die "iOS SDK not found. Full Xcode is required (Command Line Tools alone does not include the iOS SDK).\n Install Xcode from the App Store, then run:\n sudo xcode-select -s /Applications/Xcode.app/Contents/Developer"
}
install_brew_deps() {
require_cmd brew
local deps=(
autoconf
automake
cmake
git
keystone
libtool
pkg-config
ideviceinstaller wget gnu-tar openssl@3 ldid-procursus sshpass keystone autoconf automake pkg-config libtool git-lfs
[email protected]
)
@@ -443,15 +441,19 @@ parse_args() {
--jb)
JB_MODE=1
;;
--dev)
DEV_MODE=1
;;
--skip-project-setup)
SKIP_PROJECT_SETUP=1
;;
-h|--help)
cat <<'EOF'
Usage: setup_machine.sh [--jb] [--skip-project-setup]
Usage: setup_machine.sh [--jb] [--dev] [--skip-project-setup]
Options:
--jb Use jailbreak firmware patching + jailbreak CFW install.
--dev Use dev firmware patching + dev CFW install.
--skip-project-setup Skip setup_tools/build stage.
EOF
exit 0
@@ -468,13 +470,23 @@ main() {
local fw_patch_target="fw_patch"
local cfw_install_target="cfw_install"
local mode_label="base"
if [[ "$JB_MODE" -eq 1 && "$DEV_MODE" -eq 1 ]]; then
die "--jb and --dev are mutually exclusive"
fi
if [[ "$JB_MODE" -eq 1 ]]; then
fw_patch_target="fw_patch_jb"
cfw_install_target="cfw_install_jb"
mode_label="jailbreak"
elif [[ "$DEV_MODE" -eq 1 ]]; then
fw_patch_target="fw_patch_dev"
cfw_install_target="cfw_install_dev"
mode_label="dev"
fi
echo "[*] setup_machine mode: $([[ "$JB_MODE" -eq 1 ]] && echo "jailbreak" || echo "base"), project_setup=$([[ "$SKIP_PROJECT_SETUP" -eq 1 ]] && echo "skip" || echo "run")"
echo "[*] setup_machine mode: ${mode_label}, project_setup=$([[ "$SKIP_PROJECT_SETUP" -eq 1 ]] && echo "skip" || echo "run")"
if [[ "$SKIP_PROJECT_SETUP" -eq 1 ]]; then
echo ""