mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
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:
@@ -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`)
|
||||
|
||||
|
||||
@@ -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" .
|
||||
|
||||
@@ -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
@@ -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
@@ -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)을 부팅합니다.
|
||||
|
||||

|
||||
|
||||
## 테스트된 환경
|
||||
|
||||
| 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
@@ -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
@@ -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() {
|
||||
|
||||
Executable
+467
@@ -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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
Executable
+481
@@ -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
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user