Add JB finalizer script; remove IPA signing UI

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.
This commit is contained in:
Lakr
2026-03-07 18:34:49 +08:00
parent 048f4c7cc1
commit cfee3ea076
13 changed files with 246 additions and 525 deletions

View File

@@ -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.

View File

@@ -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"

View File

@@ -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 |

View File

@@ -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

View File

@@ -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

191
scripts/cfw_install_jb_post.sh Executable file
View File

@@ -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."

View File

@@ -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

View File

@@ -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"])
}

View File

@@ -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)"
}
}
}
}

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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
)
}
}
}
}

View File

@@ -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("<?xml") || entsXML.hasPrefix("<!DOCTYPE") {
let entsFile = tempDir.appendingPathComponent("ents-\(UUID().uuidString).plist")
try entsXML.write(to: entsFile, atomically: true, encoding: .utf8)
args = ["-S\(entsFile.path)", "-M", "-K\(cert)", url.path]
} else {
args = ["-S", "-M", "-K\(cert)", url.path]
}
let result = try await run(ldidURL, arguments: args)
guard result.status == 0 else {
throw SignError.ldidFailed(url.lastPathComponent, result.stderr)
}
print("[sign] signed \(url.lastPathComponent)")
}
// MARK: - Mach-O Detection
/// Recursively find all Mach-O files in a directory.
func findMachOFiles(in directory: URL) -> [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)"
}
}
}
}