From cfee3ea076c7f5a1eb49655507838225c9648b49 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:34:49 +0800 Subject: [PATCH] Add JB finalizer script; remove IPA signing UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/cfw_install_jb_post.sh — an idempotent SSH-based finalizer to complete JB bootstrap on a normally-booted vphone (creates /var/jb symlink, fixes ownership, runs prep_bootstrap, creates markers, installs Sileo, and runs apt; requires sshpass). Add Makefile help, .PHONY and target cfw_install_jb_finalize to invoke the script. Remove host-side IPA signing/installing and related UI: delete VPhoneSigner, VPhoneIPAInstaller, VPhoneMenuInstall and remove signer/ipaInstaller fields and menu items/callbacks from the vphone-cli UI (also removed the DevMode enable WIP flow). Misc: minor table/formatting tweaks in AGENTS.md and research docs. --- AGENTS.md | 8 +- Makefile | 6 +- research/0_binary_patch_comparison.md | 37 ++-- research/kernel_jb_patch_notes.md | 1 + research/kernel_patcher_verification.md | 54 ++--- scripts/cfw_install_jb_post.sh | 191 ++++++++++++++++++ sources/vphone-cli/VPhoneAppDelegate.swift | 4 - sources/vphone-cli/VPhoneControl.swift | 12 -- sources/vphone-cli/VPhoneIPAInstaller.swift | 164 --------------- sources/vphone-cli/VPhoneMenuConnect.swift | 18 -- sources/vphone-cli/VPhoneMenuController.swift | 3 - sources/vphone-cli/VPhoneMenuInstall.swift | 116 ----------- sources/vphone-cli/VPhoneSigner.swift | 157 -------------- 13 files changed, 246 insertions(+), 525 deletions(-) create mode 100755 scripts/cfw_install_jb_post.sh delete mode 100644 sources/vphone-cli/VPhoneIPAInstaller.swift delete mode 100644 sources/vphone-cli/VPhoneMenuInstall.swift delete mode 100644 sources/vphone-cli/VPhoneSigner.swift diff --git a/AGENTS.md b/AGENTS.md index ef98741..2a1d850 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,10 +29,10 @@ For any changes applying new patches, also update research/0_binary_patch_compar ## Firmware Variants -| Variant | Boot Chain | CFW | Make Targets | -| ------------------- | :--------: | :-------: | ---------------------------------- | -| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` | -| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | +| Variant | Boot Chain | CFW | Make Targets | +| ------------------- | :---------: | :-------: | ---------------------------------- | +| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` | +| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | | **Jailbreak (WIP)** | 126 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` | See `research/` for detailed firmware pipeline, component origins, patch breakdowns, and boot flow documentation. diff --git a/Makefile b/Makefile index 1821237..183e001 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,7 @@ help: @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 " make cfw_install_jb_finalize Finalize JB bootstrap on live device (symlinks/sileo/apt)" @echo "" @echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)" @@ -243,7 +244,7 @@ ramdisk_send: # CFW # ═══════════════════════════════════════════════════════════════════ -.PHONY: cfw_install cfw_install_dev cfw_install_jb +.PHONY: cfw_install cfw_install_dev cfw_install_jb cfw_install_jb_finalize cfw_install: cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" . @@ -253,3 +254,6 @@ cfw_install_dev: cfw_install_jb: cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" . + +cfw_install_jb_finalize: + $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") $(if $(SSH_PASS),SSH_PASS="$(SSH_PASS)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb_post.sh" diff --git a/research/0_binary_patch_comparison.md b/research/0_binary_patch_comparison.md index eb8cc45..513c489 100644 --- a/research/0_binary_patch_comparison.md +++ b/research/0_binary_patch_comparison.md @@ -77,7 +77,7 @@ | # | Group | Method | Function | Purpose | JB Enabled | | ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: | | JB-01 | A | `patch_amfi_cdhash_in_trustcache` | `AMFIIsCDHashInTrustCache` | Always return true + store hash | Y | -| JB-02 | A | `patch_amfi_execve_kill_path` | AMFI execve kill return site | Convert shared kill return from deny to allow (superseded by C21; standalone only) | N | +| JB-02 | A | `patch_amfi_execve_kill_path` | AMFI execve kill return site | Convert shared kill return from deny to allow (superseded by C21; standalone only) | N | | JB-03 | C | `patch_cred_label_update_execve` | `_cred_label_update_execve` | Reworked C21-v3: C21-v1 already boots; v3 keeps split late exits and additionally ORs success-only helper bits `0xC` after clearing `0x3F00`; still disabled pending boot validation | N | | JB-04 | C | `patch_hook_cred_label_update_execve` | sandbox `mpo_cred_label_update_execve` wrapper (`ops[18]` -> `sub_FFFFFE00093BDB64`) | Faithful upstream C23 trampoline: copy `VSUID`/`VSGID` owner state into pending cred, set `P_SUGID`, then branch back to wrapper | Y | | JB-05 | C | `patch_kcall10` | `sysent[439]` (`SYS_kas_info` replacement) | Rebuilt ABI-correct kcall cave: `target + 7 args -> uint64 x0`; re-enabled after focused dry-run validation | Y | @@ -130,19 +130,19 @@ ### CFW Installer Flow Matrix (Script-Level) -| Flow Item | Regular (`cfw_install.sh`) | Dev (`cfw_install_dev.sh`) | JB (`cfw_install_jb.sh`) | -| ---------------------------------------------------- | ------------------------------- | ----------------------------------------------- | --------------------------------------------- | -| Base CFW phases (1/7 -> 7/7) | Runs directly | Runs directly | Runs via `CFW_SKIP_HALT=1 zsh cfw_install.sh` | -| Dev overlay (`rpcserver_ios` replacement) | - | Y (`apply_dev_overlay`) | - | -| SSH readiness wait before install | Y (`wait_for_device_ssh_ready`) | - | Y (inherited from base run) | -| launchd jetsam patch (`patch-launchd-jetsam`) | - | Y (base-flow injection) | Y (JB-1) | -| launchd dylib injection (`inject-dylib /b`) | - | - | Y (JB-1) | +| Flow Item | Regular (`cfw_install.sh`) | Dev (`cfw_install_dev.sh`) | JB (`cfw_install_jb.sh`) | +| --------------------------------------------- | ------------------------------- | -------------------------- | --------------------------------------------- | +| Base CFW phases (1/7 -> 7/7) | Runs directly | Runs directly | Runs via `CFW_SKIP_HALT=1 zsh cfw_install.sh` | +| Dev overlay (`rpcserver_ios` replacement) | - | Y (`apply_dev_overlay`) | - | +| SSH readiness wait before install | Y (`wait_for_device_ssh_ready`) | - | Y (inherited from base run) | +| launchd jetsam patch (`patch-launchd-jetsam`) | - | Y (base-flow injection) | Y (JB-1) | +| launchd dylib injection (`inject-dylib /b`) | - | - | Y (JB-1) | -| Procursus bootstrap deployment | - | - | Y (JB-2) | -| BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) | -| Additional input resources | `cfw_input` | `cfw_input` + `resources/cfw_dev/rpcserver_ios` | `cfw_input` + `cfw_jb_input` | -| Extra tool requirement beyond base | - | - | `zstd` | -| Halt behavior | Halts unless `CFW_SKIP_HALT=1` | Halts unless `CFW_SKIP_HALT=1` | Always halts after JB phases | +| Procursus bootstrap deployment | - | - | Y (JB-2) | +| BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) | +| Additional input resources | `cfw_input` | `cfw_input` + `resources/cfw_dev/rpcserver_ios` | `cfw_input` + `cfw_jb_input` | +| Extra tool requirement beyond base | - | - | `zstd` | +| Halt behavior | Halts unless `CFW_SKIP_HALT=1` | Halts unless `CFW_SKIP_HALT=1` | Always halts after JB phases | ## Summary @@ -163,11 +163,11 @@ ## Ramdisk Variant Matrix -| Variant | Pre-step | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` | -| ------------- | ------------------- | -------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- | -| `RAMDISK` | `make fw_patch` | release TXM + base TXM patch (1) | base kernel (28), legacy `*.ramdisk` preferred else derive from pristine CloudOS | restore kernel from `fw_patch` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | -| `DEV+RAMDISK` | `make fw_patch_dev` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_dev` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | -| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (28+59) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | +| Variant | Pre-step | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` | +| ------------- | ------------------- | -------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------- | +| `RAMDISK` | `make fw_patch` | release TXM + base TXM patch (1) | base kernel (28), legacy `*.ramdisk` preferred else derive from pristine CloudOS | restore kernel from `fw_patch` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | +| `DEV+RAMDISK` | `make fw_patch_dev` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_dev` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | +| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (28+59) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | ## Cross-Version Dynamic Snapshot @@ -177,4 +177,3 @@ | PCC 26.3 (`23D128`) | 14 | 59 | | iOS 26.1 (`23B85`) | 14 | 59 | | iOS 26.3 (`23D127`) | 14 | 59 | - diff --git a/research/kernel_jb_patch_notes.md b/research/kernel_jb_patch_notes.md index cf40409..051f07b 100644 --- a/research/kernel_jb_patch_notes.md +++ b/research/kernel_jb_patch_notes.md @@ -13,6 +13,7 @@ Two methods added since initial document: `patch_shared_region_map`, `patch_io_s Three previously failing patches (`patch_nvram_verify_permission`, `patch_thid_should_crash`, `patch_hook_cred_label_update_execve`) have been implemented — see details below. On 2026-03-06, three patches were retargeted after IDA-MCP re-analysis revealed their matchers were hitting wrong sites: + - `patch_bsd_init_auth` — was hitting `exec_handle_sugid` instead of the real `bsd_init` rootauth gate - `patch_io_secure_bsd_root` — was patching the `"SecureRoot"` dispatch branch instead of the `"SecureRootName"` deny-return - `patch_vm_fault_enter_prepare` — was NOPing a `pmap_lock_phys_page()` call instead of the upstream `cs_bypass` gate diff --git a/research/kernel_patcher_verification.md b/research/kernel_patcher_verification.md index fcde947..2e893d0 100644 --- a/research/kernel_patcher_verification.md +++ b/research/kernel_patcher_verification.md @@ -60,34 +60,34 @@ Result: **byte-identical** output between hardcoded and dynamic patching. Offsets and 32-bit patch values, taken from `patch_fw.py`: -| # | Offset (hex) | Patch value | Purpose | -| --- | -----------: | ----------: | ------------------------------------------ | -| 1 | 0x2476964 | 0xD503201F | \_apfs_vfsop_mount root snapshot NOP | -| 2 | 0x23CFDE4 | 0xD503201F | \_authapfs_seal_is_broken NOP | -| 3 | 0x00F6D960 | 0xD503201F | \_bsd_init rootvp NOP | -| 4 | 0x163863C | 0x52800000 | \_proc_check_launch_constraints mov w0,#0 | -| 5 | 0x1638640 | 0xD65F03C0 | \_proc_check_launch_constraints ret | -| 6 | 0x12C8138 | 0xD2800020 | \_PE_i_can_has_debugger mov x0,#1 | -| 7 | 0x12C813C | 0xD65F03C0 | \_PE_i_can_has_debugger ret | -| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) | -| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 | -| 10 | 0x16410BC | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (1) | -| 11 | 0x16410C8 | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (2) | -| 12 | 0x242011C | 0x52800000 | \_apfs_graft mov w0,#0 | -| 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 | -| 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 | -| 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 | +| # | Offset (hex) | Patch value | Purpose | +| --- | -----------: | ----------: | ------------------------------------------- | +| 1 | 0x2476964 | 0xD503201F | \_apfs_vfsop_mount root snapshot NOP | +| 2 | 0x23CFDE4 | 0xD503201F | \_authapfs_seal_is_broken NOP | +| 3 | 0x00F6D960 | 0xD503201F | \_bsd_init rootvp NOP | +| 4 | 0x163863C | 0x52800000 | \_proc_check_launch_constraints mov w0,#0 | +| 5 | 0x1638640 | 0xD65F03C0 | \_proc_check_launch_constraints ret | +| 6 | 0x12C8138 | 0xD2800020 | \_PE_i_can_has_debugger mov x0,#1 | +| 7 | 0x12C813C | 0xD65F03C0 | \_PE_i_can_has_debugger ret | +| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) | +| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 | +| 10 | 0x16410BC | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (1) | +| 11 | 0x16410C8 | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (2) | +| 12 | 0x242011C | 0x52800000 | \_apfs_graft mov w0,#0 | +| 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 | +| 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 | +| 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 | | 16 | | | \_handle_get_dev_by_role entitlement bypass | -| 17 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 | -| 18 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret | -| 19 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 | -| 20 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret | -| 21 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 | -| 22 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret | -| 23 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 | -| 24 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret | -| 25 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 | -| 26 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret | +| 17 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 | +| 18 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret | +| 19 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 | +| 20 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret | +| 21 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 | +| 22 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret | +| 23 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 | +| 24 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret | +| 25 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 | +| 26 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret | ## TXM Patch Details diff --git a/scripts/cfw_install_jb_post.sh b/scripts/cfw_install_jb_post.sh new file mode 100755 index 0000000..de82474 --- /dev/null +++ b/scripts/cfw_install_jb_post.sh @@ -0,0 +1,191 @@ +#!/bin/zsh +# cfw_install_jb_post.sh — Finalize JB bootstrap on a normally-booted vphone. +# +# Runs after `cfw_install_jb` + first normal boot. Connects to the live device +# via SSH and sets up procursus symlinks, markers, Sileo, and apt packages. +# +# Every step is idempotent — safe to re-run at any point. +# All binary paths are discovered dynamically (no hardcoded /bin, /sbin, etc.). +# +# Usage: make cfw_install_jb_finalize [SSH_PORT=22222] [SSH_PASS=alpine] +set -euo pipefail + +SCRIPT_DIR="${0:a:h}" + +# ── Configuration ─────────────────────────────────────────────── +SSH_PORT="${SSH_PORT:-22222}" +SSH_PASS="${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 +} + +_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 + echo " [${label}] connection lost (attempt $attempt/$SSH_RETRY), retrying in 3s..." >&2 + sleep 3 + done + return 255 +} + +# Raw ssh — no PATH prefix +ssh_raw() { + _ssh_retry _sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@" +} + +# ssh with discovered PATH prepended +ssh_cmd() { + ssh_raw "$RENV $*" +} + +# ── Prerequisites ────────────────────────────────────────────── +command -v sshpass &>/dev/null || die "Missing sshpass. Run: make setup_tools" +SSHPASS_BIN="$(command -v sshpass)" + +echo "[*] cfw_install_jb_post.sh — Finalizing JB bootstrap..." +echo " Target: ${SSH_USER}@${SSH_HOST}:${SSH_PORT}" +echo "" + +# ── Verify SSH connectivity ──────────────────────────────────── +echo "[*] Checking SSH connectivity..." +ssh_raw "echo ready" >/dev/null 2>&1 || die "Cannot reach device on ${SSH_HOST}:${SSH_PORT}. Is the VM booted normally?" +echo "[+] Device reachable" + +# ── Discover remote PATH ────────────────────────────────────── +# Uses only shell builtins (test -d, echo) — works with empty PATH. +echo "[*] Discovering remote binary directories..." +DISCOVERED_PATH=$(ssh_raw 'P=""; \ + for d in \ + /var/jb/usr/bin /var/jb/bin /var/jb/sbin /var/jb/usr/sbin \ + /iosbinpack64/bin /iosbinpack64/usr/bin /iosbinpack64/sbin /iosbinpack64/usr/sbin \ + /usr/bin /usr/sbin /bin /sbin; do \ + [ -d "$d" ] && P="$P:$d"; \ + done; \ + echo "${P#:}"') + +[[ -n "$DISCOVERED_PATH" ]] || die "Could not discover any binary directories on device" +echo " PATH=$DISCOVERED_PATH" + +# This gets prepended to every ssh_cmd call +RENV="export PATH='$DISCOVERED_PATH' TERM='xterm-256color';" + +# Quick sanity: verify we can run ls now +ssh_cmd "ls / >/dev/null" || die "PATH discovery succeeded but 'ls' still not found" +echo "[+] Remote environment ready" + +# ═══════════ 1/6 SYMLINK /var/jb ══════════════════════════════ +echo "" +echo "[1/6] Creating /private/var/jb symlink..." + +# Find 96-char boot manifest hash — use shell glob (no ls dependency) +BOOT_HASH=$(ssh_cmd 'for d in /private/preboot/*/; do \ + b="${d%/}"; b="${b##*/}"; \ + [ "${#b}" = 96 ] && echo "$b" && break; \ +done') +[[ -n "$BOOT_HASH" ]] || die "Could not find 96-char boot manifest hash in /private/preboot" +echo " Boot manifest hash: $BOOT_HASH" + +JB_TARGET="/private/preboot/$BOOT_HASH/jb-vphone/procursus" +ssh_cmd "test -d '$JB_TARGET'" || die "Procursus directory not found at $JB_TARGET. Run cfw_install_jb first." + +CURRENT_LINK=$(ssh_cmd "readlink /private/var/jb 2>/dev/null || true") +if [[ "$CURRENT_LINK" == "$JB_TARGET" ]]; then + echo " [*] Symlink already correct, skipping" +else + ssh_cmd "ln -sf '$JB_TARGET' /private/var/jb" + echo " [+] /private/var/jb -> $JB_TARGET" +fi + +# ═══════════ 2/6 FIX OWNERSHIP / PERMISSIONS ═════════════════ +echo "" +echo "[2/6] Fixing mobile Library ownership..." + +ssh_cmd "mkdir -p /var/jb/var/mobile/Library/Preferences" +ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library" +ssh_cmd "chmod 0755 /var/jb/var/mobile/Library" +ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library/Preferences" +ssh_cmd "chmod 0755 /var/jb/var/mobile/Library/Preferences" + +echo " [+] Ownership set" + +# ═══════════ 3/6 RUN prep_bootstrap.sh ════════════════════════ +echo "" +echo "[3/6] Running prep_bootstrap.sh..." + +if ssh_cmd "test -f /var/jb/prep_bootstrap.sh"; then + # Skip interactive password prompt (uses uialert GUI — not usable over SSH) + ssh_cmd "NO_PASSWORD_PROMPT=1 /var/jb/prep_bootstrap.sh" + echo " [+] prep_bootstrap.sh completed" + echo " [!] Terminal password was NOT set (automated mode)." + echo " To set it manually: ssh in and run: passwd" +else + echo " [*] prep_bootstrap.sh already ran (deleted itself), skipping" +fi + +# ═══════════ 4/6 CREATE MARKER FILES ═════════════════════════ +echo "" +echo "[4/6] Creating marker files..." + +for marker in .procursus_strapped .installed_dopamine; do + if ssh_cmd "test -f /var/jb/$marker"; then + echo " [*] $marker already exists, skipping" + else + ssh_cmd "touch /var/jb/$marker && chown 0:0 /var/jb/$marker && chmod 0644 /var/jb/$marker" + echo " [+] $marker created" + fi +done + +# ═══════════ 5/6 INSTALL SILEO ══════════════════════════════ +echo "" +echo "[5/6] Installing Sileo..." + +SILEO_DEB_PATH="/private/preboot/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb" + +if ssh_cmd "dpkg -s org.coolstar.sileo >/dev/null 2>&1"; then + echo " [*] Sileo already installed, skipping" +else + ssh_cmd "test -f '$SILEO_DEB_PATH'" || die "Sileo deb not found at $SILEO_DEB_PATH. Was it uploaded by cfw_install_jb?" + ssh_cmd "dpkg -i '$SILEO_DEB_PATH'" + echo " [+] Sileo installed" +fi + +ssh_cmd "uicache -a 2>/dev/null || true" +echo " [+] uicache refreshed" + +# ═══════════ 6/6 APT SETUP ═════════════════════════════════ +echo "" +echo "[6/6] Running apt setup..." + +ssh_cmd "apt-get update -qq && apt-get install -y -qq libkrw0-tfp0 2>/dev/null || true" +echo " [+] apt update + libkrw0-tfp0 done" + +ssh_cmd "apt-get upgrade -y -qq 2>/dev/null || true" +echo " [+] apt upgrade done" + +# ═══════════ DONE ═══════════════════════════════════════════ +echo "" +echo "[+] JB finalization complete!" +echo " Next: open Sileo on device, add source https://ellekit.space, install ElleKit" +echo " Then reboot the device for full JB environment." diff --git a/sources/vphone-cli/VPhoneAppDelegate.swift b/sources/vphone-cli/VPhoneAppDelegate.swift index 99696eb..409ce2b 100644 --- a/sources/vphone-cli/VPhoneAppDelegate.swift +++ b/sources/vphone-cli/VPhoneAppDelegate.swift @@ -135,10 +135,6 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { mc.locationProvider = provider } mc.screenRecorder = VPhoneScreenRecorder() - if let signer = VPhoneSigner() { - mc.signer = signer - mc.ipaInstaller = VPhoneIPAInstaller(signer: signer) - } menuController = mc // Wire location toggle through onConnect/onDisconnect diff --git a/sources/vphone-cli/VPhoneControl.swift b/sources/vphone-cli/VPhoneControl.swift index de18358..8e735fc 100644 --- a/sources/vphone-cli/VPhoneControl.swift +++ b/sources/vphone-cli/VPhoneControl.swift @@ -261,24 +261,12 @@ class VPhoneControl { let enabled: Bool } - struct DevModeEnableResult { - let alreadyEnabled: Bool - let message: String - } - func sendDevModeStatus() async throws -> DevModeStatus { let (resp, _) = try await sendRequest(["t": "devmode", "action": "status"]) let enabled = resp["enabled"] as? Bool ?? false return DevModeStatus(enabled: enabled) } - func sendDevModeEnable() async throws -> DevModeEnableResult { - let (resp, _) = try await sendRequest(["t": "devmode", "action": "enable"]) - let alreadyEnabled = resp["already_enabled"] as? Bool ?? false - let message = resp["msg"] as? String ?? "" - return DevModeEnableResult(alreadyEnabled: alreadyEnabled, message: message) - } - func sendPing() async throws { _ = try await sendRequest(["t": "ping"]) } diff --git a/sources/vphone-cli/VPhoneIPAInstaller.swift b/sources/vphone-cli/VPhoneIPAInstaller.swift deleted file mode 100644 index 0a08736..0000000 --- a/sources/vphone-cli/VPhoneIPAInstaller.swift +++ /dev/null @@ -1,164 +0,0 @@ -import Foundation - -// MARK: - IPA Installer - -/// Host-side IPA installer. Uses VPhoneSigner for re-signing, -/// ideviceinstaller for USB installation via usbmuxd. -@MainActor -class VPhoneIPAInstaller { - let signer: VPhoneSigner - private let ideviceInstallerURL: URL - private let ideviceIdURL: URL - - init?(signer: VPhoneSigner, bundle: Bundle = .main) { - guard let execURL = bundle.executableURL else { return nil } - let macosDir = execURL.deletingLastPathComponent() - - ideviceInstallerURL = macosDir.appendingPathComponent("ideviceinstaller") - ideviceIdURL = macosDir.appendingPathComponent("idevice_id") - - let fm = FileManager.default - guard fm.fileExists(atPath: ideviceInstallerURL.path), - fm.fileExists(atPath: ideviceIdURL.path) - else { return nil } - - self.signer = signer - } - - // MARK: - Install - - /// Install an IPA. If `resign` is true, re-sign all Mach-O binaries - /// preserving their original entitlements before installing. - func install(ipaURL: URL, resign: Bool) async throws { - let udid = try await getUDID() - print("[ipa] device UDID: \(udid)") - - var installURL = ipaURL - var tempDir: URL? - - if resign { - let dir = FileManager.default.temporaryDirectory - .appendingPathComponent("vphone-cli-resign-\(UUID().uuidString)") - tempDir = dir - installURL = try await resignIPA(ipaURL: ipaURL, tempDir: dir) - } - - defer { - if let tempDir { - try? FileManager.default.removeItem(at: tempDir) - } - } - - print("[ipa] installing \(installURL.lastPathComponent) to \(udid)...") - let result = try await signer.run( - ideviceInstallerURL, - arguments: ["-u", udid, "install", installURL.path] - ) - guard result.status == 0 else { - let msg = result.stderr.isEmpty ? result.stdout : result.stderr - throw IPAError.installFailed(msg.trimmingCharacters(in: .whitespacesAndNewlines)) - } - print("[ipa] installed successfully") - } - - // MARK: - UDID Discovery - - private func getUDID() async throws -> String { - let result = try await signer.run(ideviceIdURL, arguments: ["-l"]) - guard result.status == 0 else { - throw IPAError.noDevice - } - let udids = result.stdout - .components(separatedBy: .newlines) - .map { $0.trimmingCharacters(in: .whitespaces) } - .filter { !$0.isEmpty } - guard let first = udids.first else { - throw IPAError.noDevice - } - return first - } - - // MARK: - Re-sign IPA - - private func resignIPA(ipaURL: URL, tempDir: URL) async throws -> URL { - let fm = FileManager.default - try fm.createDirectory(at: tempDir, withIntermediateDirectories: true) - - // Unzip - print("[ipa] extracting \(ipaURL.lastPathComponent)...") - let unzip = try await signer.run( - URL(fileURLWithPath: "/usr/bin/unzip"), - arguments: ["-o", ipaURL.path, "-d", tempDir.path] - ) - guard unzip.status == 0 else { - throw IPAError.extractFailed(unzip.stderr) - } - - // Remove macOS resource fork files that break iOS installd - _ = try? await signer.run( - URL(fileURLWithPath: "/usr/bin/find"), - arguments: [tempDir.path, "-name", "._*", "-delete"] - ) - _ = try? await signer.run( - URL(fileURLWithPath: "/usr/bin/find"), - arguments: [tempDir.path, "-name", ".DS_Store", "-delete"] - ) - - // Find Payload/*.app - let payloadDir = tempDir.appendingPathComponent("Payload") - guard fm.fileExists(atPath: payloadDir.path) else { - throw IPAError.invalidIPA("no Payload directory") - } - let contents = try fm.contentsOfDirectory(atPath: payloadDir.path) - guard let appName = contents.first(where: { $0.hasSuffix(".app") }) else { - throw IPAError.invalidIPA("no .app bundle in Payload") - } - let appDir = payloadDir.appendingPathComponent(appName) - - // Walk and re-sign all Mach-O files - let machoFiles = signer.findMachOFiles(in: appDir) - print("[ipa] re-signing \(machoFiles.count) Mach-O binaries...") - - for file in machoFiles { - do { - try await signer.signFile(at: file, tempDir: tempDir) - } catch { - print("[ipa] warning: \(error)") - } - } - - // Re-zip (use zip from the temp dir so Payload/ is at the root) - let outputIPA = tempDir.appendingPathComponent("resigned.ipa") - print("[ipa] re-packaging...") - let zip = try await signer.run( - URL(fileURLWithPath: "/usr/bin/zip"), - arguments: ["-r", "-y", outputIPA.path, "Payload"], - currentDirectory: tempDir - ) - guard zip.status == 0 else { - throw IPAError.repackFailed(zip.stderr) - } - - return outputIPA - } - - // MARK: - Errors - - enum IPAError: Error, CustomStringConvertible { - case noDevice - case extractFailed(String) - case invalidIPA(String) - case repackFailed(String) - case installFailed(String) - - var description: String { - switch self { - case .noDevice: "no device found (is the VM running?)" - case let .extractFailed(msg): "failed to extract IPA: \(msg)" - case let .invalidIPA(msg): "invalid IPA: \(msg)" - case let .repackFailed(msg): "failed to repackage IPA: \(msg)" - case let .installFailed(msg): "install failed: \(msg)" - } - } - } -} diff --git a/sources/vphone-cli/VPhoneMenuConnect.swift b/sources/vphone-cli/VPhoneMenuConnect.swift index 3312d05..8af497b 100644 --- a/sources/vphone-cli/VPhoneMenuConnect.swift +++ b/sources/vphone-cli/VPhoneMenuConnect.swift @@ -9,7 +9,6 @@ extension VPhoneMenuController { menu.addItem(makeItem("File Browser", action: #selector(openFiles))) menu.addItem(NSMenuItem.separator()) menu.addItem(makeItem("Developer Mode Status", action: #selector(devModeStatus))) - menu.addItem(makeItem("Enable Developer Mode [WIP]", action: #selector(devModeEnable))) menu.addItem(NSMenuItem.separator()) menu.addItem(makeItem("Ping", action: #selector(sendPing))) menu.addItem(makeItem("Guest Version", action: #selector(queryGuestVersion))) @@ -36,23 +35,6 @@ extension VPhoneMenuController { } } - @objc func devModeEnable() { - Task { - do { - let result = try await control.sendDevModeEnable() - showAlert( - title: "Developer Mode", - message: result.message.isEmpty - ? (result.alreadyEnabled ? "Developer Mode already enabled." : "Developer Mode enabled.") - : result.message, - style: .informational - ) - } catch { - showAlert(title: "Developer Mode", message: "\(error)", style: .warning) - } - } - } - @objc func sendPing() { Task { do { diff --git a/sources/vphone-cli/VPhoneMenuController.swift b/sources/vphone-cli/VPhoneMenuController.swift index 6fb4813..a672a1b 100644 --- a/sources/vphone-cli/VPhoneMenuController.swift +++ b/sources/vphone-cli/VPhoneMenuController.swift @@ -17,8 +17,6 @@ class VPhoneMenuController { var locationReplayStopItem: NSMenuItem? var screenRecorder: VPhoneScreenRecorder? var recordingItem: NSMenuItem? - var signer: VPhoneSigner? - var ipaInstaller: VPhoneIPAInstaller? init(keyHelper: VPhoneKeyHelper, control: VPhoneControl) { self.keyHelper = keyHelper @@ -47,7 +45,6 @@ class VPhoneMenuController { mainMenu.addItem(buildKeysMenu()) mainMenu.addItem(buildTypeMenu()) mainMenu.addItem(buildConnectMenu()) - mainMenu.addItem(buildInstallMenu()) mainMenu.addItem(buildLocationMenu()) mainMenu.addItem(buildRecordMenu()) mainMenu.addItem(buildBatteryMenu()) diff --git a/sources/vphone-cli/VPhoneMenuInstall.swift b/sources/vphone-cli/VPhoneMenuInstall.swift deleted file mode 100644 index 443cf94..0000000 --- a/sources/vphone-cli/VPhoneMenuInstall.swift +++ /dev/null @@ -1,116 +0,0 @@ -import AppKit -import UniformTypeIdentifiers - -// MARK: - Install Menu - -extension VPhoneMenuController { - func buildInstallMenu() -> NSMenuItem { - let item = NSMenuItem() - let menu = NSMenu(title: "Install") - menu.addItem(makeItem("Install Package (.ipa) [WIP]", action: #selector(installPackage))) - menu.addItem(makeItem("Install Package with Resign (.ipa) [WIP]", action: #selector(installPackageResign))) - menu.addItem(NSMenuItem.separator()) - menu.addItem(makeItem("Upload Binary to Guest", action: #selector(uploadBinary))) - menu.addItem(makeItem("Upload Binary with Resign to Guest", action: #selector(uploadBinaryResign))) - item.submenu = menu - return item - } - - // MARK: - IPA Install - - @objc func installPackage() { - pickAndInstall(resign: false) - } - - @objc func installPackageResign() { - pickAndInstall(resign: true) - } - - private func pickAndInstall(resign: Bool) { - let panel = NSOpenPanel() - panel.title = "Select IPA" - panel.allowedContentTypes = [.init(filenameExtension: "ipa")!] - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - - guard panel.runModal() == .OK, let url = panel.url else { return } - - guard let installer = ipaInstaller else { - showAlert( - title: "Install Package", - message: "IPA installer not available (bundled tools missing).", - style: .warning - ) - return - } - - Task { - do { - try await installer.install(ipaURL: url, resign: resign) - showAlert( - title: "Install Package", - message: "Successfully installed \(url.lastPathComponent).", - style: .informational - ) - } catch { - showAlert( - title: "Install Package", - message: "\(error)", - style: .warning - ) - } - } - } - - // MARK: - Upload Binary - - @objc func uploadBinary() { - pickAndUploadBinary(resign: false) - } - - @objc func uploadBinaryResign() { - pickAndUploadBinary(resign: true) - } - - private func pickAndUploadBinary(resign: Bool) { - let panel = NSOpenPanel() - panel.title = "Select Binary to Upload" - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - - guard panel.runModal() == .OK, let url = panel.url else { return } - - Task { - do { - var data = try Data(contentsOf: url) - - if resign { - guard let signer else { - showAlert( - title: "Upload Binary", - message: "Signing tools not available (bundled tools missing).", - style: .warning - ) - return - } - data = try await signer.resign(data: data, filename: url.lastPathComponent) - } - - let filename = url.lastPathComponent - let remotePath = "/var/root/Library/Caches/\(filename)" - try await control.uploadFile(path: remotePath, data: data, permissions: "755") - showAlert( - title: "Upload Binary", - message: "Uploaded \(filename) to \(remotePath) (\(data.count) bytes)\(resign ? " [resigned]" : "").", - style: .informational - ) - } catch { - showAlert( - title: "Upload Binary", - message: "\(error)", - style: .warning - ) - } - } - } -} diff --git a/sources/vphone-cli/VPhoneSigner.swift b/sources/vphone-cli/VPhoneSigner.swift deleted file mode 100644 index e4ca68c..0000000 --- a/sources/vphone-cli/VPhoneSigner.swift +++ /dev/null @@ -1,157 +0,0 @@ -import Foundation - -// MARK: - Code Signer - -/// Host-side code signing using bundled ldid + signcert.p12. -/// Preserves existing entitlements when re-signing. -@MainActor -class VPhoneSigner { - private let ldidURL: URL - private let signcertURL: URL - - init?(bundle: Bundle = .main) { - guard let execURL = bundle.executableURL else { return nil } - let macosDir = execURL.deletingLastPathComponent() - let resourcesDir = macosDir - .deletingLastPathComponent() - .appendingPathComponent("Resources") - - ldidURL = macosDir.appendingPathComponent("ldid") - signcertURL = resourcesDir.appendingPathComponent("signcert.p12") - - let fm = FileManager.default - guard fm.fileExists(atPath: ldidURL.path), - fm.fileExists(atPath: signcertURL.path) - else { return nil } - } - - // MARK: - Sign Binary - - /// Re-sign a single Mach-O binary in-memory. Preserves existing entitlements. - func resign(data: Data, filename: String) async throws -> Data { - let tempDir = FileManager.default.temporaryDirectory - .appendingPathComponent("vphone-cli-sign-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: tempDir) } - - let binaryURL = tempDir.appendingPathComponent(filename) - try data.write(to: binaryURL) - - try await signFile(at: binaryURL, tempDir: tempDir) - - return try Data(contentsOf: binaryURL) - } - - /// Re-sign a Mach-O binary on disk in-place. Preserves existing entitlements. - func signFile(at url: URL, tempDir: URL) async throws { - let entsResult = try await run(ldidURL, arguments: ["-e", url.path]) - let entsXML = entsResult.stdout.trimmingCharacters(in: .whitespacesAndNewlines) - let cert = signcertURL.path - - var args: [String] - if !entsXML.isEmpty, entsXML.hasPrefix(" [URL] { - let fm = FileManager.default - guard let enumerator = fm.enumerator( - at: directory, - includingPropertiesForKeys: [.isRegularFileKey], - options: [.skipsHiddenFiles] - ) else { return [] } - - var results: [URL] = [] - for case let url as URL in enumerator { - guard let values = try? url.resourceValues(forKeys: [.isRegularFileKey]), - values.isRegularFile == true, - Self.isMachO(at: url) - else { continue } - results.append(url) - } - return results - } - - static func isMachO(at url: URL) -> Bool { - guard let fh = try? FileHandle(forReadingFrom: url) else { return false } - defer { try? fh.close() } - guard let data = try? fh.read(upToCount: 4), data.count == 4 else { return false } - let magic = data.withUnsafeBytes { $0.load(as: UInt32.self) } - return magic == 0xFEED_FACF // MH_MAGIC_64 - || magic == 0xCFFA_EDFE // MH_CIGAM_64 - || magic == 0xFEED_FACE // MH_MAGIC - || magic == 0xCEFA_EDFE // MH_CIGAM - || magic == 0xCAFE_BABE // FAT_MAGIC - || magic == 0xBEBA_FECA // FAT_CIGAM - } - - // MARK: - Process Runner - - struct ProcessResult: Sendable { - let stdout: String - let stderr: String - let status: Int32 - } - - func run( - _ executable: URL, - arguments: [String], - currentDirectory: URL? = nil - ) async throws -> ProcessResult { - let execPath = executable.path - let args = arguments - let dirPath = currentDirectory?.path - - return try await Task.detached { - let process = Process() - process.executableURL = URL(fileURLWithPath: execPath) - process.arguments = args - if let dirPath { - process.currentDirectoryURL = URL(fileURLWithPath: dirPath) - } - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - - try process.run() - process.waitUntilExit() - - let outData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - let errData = stderrPipe.fileHandleForReading.readDataToEndOfFile() - - return ProcessResult( - stdout: String(data: outData, encoding: .utf8) ?? "", - stderr: String(data: errData, encoding: .utf8) ?? "", - status: process.terminationStatus - ) - }.value - } - - // MARK: - Errors - - enum SignError: Error, CustomStringConvertible { - case ldidFailed(String, String) - - var description: String { - switch self { - case let .ldidFailed(file, msg): "failed to sign \(file): \(msg)" - } - } - } -}