#!/bin/zsh # cfw_install_exp.sh — Install base CFW + JB extensions + EXP experimental # patches on vphone. Files are placed directly on the VM's Disk.img volumes, # which cfw_install_host.sh attaches and mounts on the host; the VM must be off. # # Runs the base CFW installer first (phases 1-7), the JB-specific # modifications (launchd jetsam patch, dylib injection, procursus bootstrap, # BaseBin hook deployment, JB-1..JB-5), and additionally the experimental # phases — labeled `EXP-JB-N` to make their EXP-only scope obvious: # - Pre-step : byte-5 mangle of kern.hv_vmm_present in DSC dylibs # (paired with KernelEXPPatcher's kernel-side rename). # - EXP-JB-3.5 : surgical 2-insn patch of watchdogd's hv_vmm cache + # slot re-attest of the standalone Mach-O. # - EXP-JB-6 : post-restore DT identity rewrite (root model / # target-type / compatible[0]) inside # $MNT5/.../devicetree.img4. # - EXP-JB-7 : optional ProductBuildVersion rewrite in # SystemVersion.plist (gated on the SPOOF_BUILD env var). # # Stages JB-1..JB-5 remain genuine jailbreak phases (inherited from the JB # pipeline). Stages prefixed EXP-JB-* are EXP-exclusive — they do NOT run # when the JB or DEV install scripts are used. JB and DEV remain on their # pre-experimental baseline. # # Prerequisites (in addition to cfw_install.sh requirements): # - cfw_jb_input/ or resources/cfw_jb_input.tar.zst present # - zstd (for bootstrap decompression) # # Usage: make cfw_install_exp set -euo pipefail # ── Restore caller's PATH — Nix /etc/zshenv resets PATH on zsh startup ─ [[ -n "${_VPHONE_PATH:-}" ]] && export PATH="$_VPHONE_PATH" VM_DIR="${1:-.}" SCRIPT_DIR="${0:a:h}" # ── Python resolver — prefer project venv over whatever is in PATH ─ # Resolves to .venv/bin/python3 relative to the project root (parent of # scripts/), falling back to the system python3 when the venv is absent. _resolve_python3() { local venv_py="${SCRIPT_DIR:h}/.venv/bin/python3" if [[ -x "$venv_py" ]]; then echo "$venv_py" else command -v python3 || true fi } PYTHON3="$(_resolve_python3)" # ════════════════════════════════════════════════════════════════ # Step 1: Run base CFW install (skip halt — we continue with JB phases) # ════════════════════════════════════════════════════════════════ echo "[*] cfw_install_exp.sh — Installing CFW + JB extensions + EXP experimental patches..." echo "" # ──────────────────────────────────────────────────────────────────── # Pre-step: patch hv_vmm_present user-mode consumers in the SystemOS # Cryptex's DSC chunks BEFORE running cfw_install.sh. # # cfw_install.sh decrypts the SystemOS Cryptex AEA into # $TEMP_DIR/CryptexSystemOS.dmg and reuses it on subsequent runs. We # pre-create that decrypted DMG here, mount it, patch the DSC chunks # in place, unmount, and let cfw_install.sh pick up the cached # (already-patched) DMG. cfw_install.sh itself is unmodified — this # is the EXP variant's device-like user-mode patching, kept out of # the JB install path so JB remains unaffected. # ──────────────────────────────────────────────────────────────────── VM_DIR_ABS="$(cd "${VM_DIR:-.}" && pwd)" JB_TEMP_DIR="$VM_DIR_ABS/.cfw_temp" JB_SYSOS_DMG="$JB_TEMP_DIR/CryptexSystemOS.dmg" JB_MNT_SYSOS="$JB_TEMP_DIR/mnt_sysos_hv_vmm" mkdir -p "$JB_TEMP_DIR" # Find the restore directory (same logic as cfw_install.sh) JB_RESTORE_DIR="" for d in "$VM_DIR_ABS"/iPhone*_Restore; do [[ -d "$d" ]] && { JB_RESTORE_DIR="$d"; break; } done if [[ -z "$JB_RESTORE_DIR" ]]; then echo "[!] hv_vmm DSC patch: no restore directory found, skipping" elif [[ ! -f "$JB_SYSOS_DMG" ]]; then # Not yet decrypted — decrypt to the cache location cfw_install.sh expects. echo "[*] hv_vmm DSC patch: decrypting SystemOS into cache..." JB_CRYPTEX_SYSOS=$("$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" cryptex-paths "$JB_RESTORE_DIR/iPhone-BuildManifest.plist" | head -1) JB_AEA_KEY=$(ipsw fw aea --key "$JB_RESTORE_DIR/$JB_CRYPTEX_SYSOS") aea decrypt -i "$JB_RESTORE_DIR/$JB_CRYPTEX_SYSOS" -o "$JB_SYSOS_DMG" -key-value "$JB_AEA_KEY" fi if [[ -f "$JB_SYSOS_DMG" ]]; then # Mount, patch chunks (hv_vmm + camera, same cryptex), unmount. # Idempotent: re-running on an already-patched DMG is a no-op # (hv_vmm patcher detects already-patched cstrings; camera patcher # refuses pre-patched function entries unless --force is passed). echo "[*] DSC patches: mounting cached SystemOS DMG..." mkdir -p "$JB_MNT_SYSOS" sudo ${SUDO_ASKPASS:+-A} hdiutil detach "$JB_MNT_SYSOS" -force 2>/dev/null || true sudo ${SUDO_ASKPASS:+-A} hdiutil attach -mountpoint "$JB_MNT_SYSOS" "$JB_SYSOS_DMG" -nobrowse -owners off JB_DSC_CHUNKS_DIR="$JB_MNT_SYSOS/System/Library/Caches/com.apple.dyld" JB_DSC_HEADER="$JB_DSC_CHUNKS_DIR/dyld_shared_cache_arm64e" if [[ -d "$JB_DSC_CHUNKS_DIR" ]]; then echo "[*] hv_vmm DSC patch: patching chunks under $JB_DSC_CHUNKS_DIR..." "$SCRIPT_DIR/patch_hv_vmm_userland.sh" dsc "$JB_DSC_CHUNKS_DIR" echo "[+] hv_vmm DSC patch: chunks patched" # Camera DSC patches: (1) short-circuit the NeutrinoCore # _NUStyleTransfer*Processor methods so Camera.app's style- # thumbnail pipeline doesn't crash on this VM build, and (2) # force +[AVCaptureDevice authorizationStatusForMediaType:] = # Authorized so apps don't bail at the TCC gate. Both are # resolved per-build via `ipsw dyld symaddr` and verified by # pacibsp prologue match — version-portable as long as the # named ObjC symbols continue to exist. Combined with the # /product/camera DT node added at fw_patch time, this lets # Camera.app's icon show on Home Screen and the app launch # without immediately bailing. if [[ -f "$JB_DSC_HEADER" ]]; then echo "[*] camera DSC patch: patching chunks under $JB_DSC_CHUNKS_DIR..." if "$SCRIPT_DIR/patch_camera_userland.sh" dsc "$JB_DSC_CHUNKS_DIR" "$JB_DSC_HEADER"; then echo "[+] camera DSC patch: chunks patched" else echo "[!] camera DSC patch: failed (likely build-version mismatch); continuing" fi else echo "[-] camera DSC patch: $JB_DSC_HEADER not found, skipping" fi else echo "[-] DSC patches: $JB_DSC_CHUNKS_DIR not found, skipping" fi sudo ${SUDO_ASKPASS:+-A} hdiutil detach "$JB_MNT_SYSOS" -force fi # Now run the regular CFW install. It will see the cached (patched) # CryptexSystemOS.dmg and use it as-is, so the patched DSC chunks land # in $MNT1/System/Cryptexes/OS. zsh "$SCRIPT_DIR/cfw_install.sh" "$VM_DIR" # ════════════════════════════════════════════════════════════════ # Step 2: JB-specific phases # ════════════════════════════════════════════════════════════════ # Resolve absolute paths (same as base script) VM_DIR="$(cd "${VM_DIR}" && pwd)" # ── Configuration ─────────────────────────────────────────────── CFW_INPUT="cfw_input" CFW_JB_INPUT="cfw_jb_input" CFW_JB_ARCHIVE="cfw_jb_input.tar.zst" TEMP_DIR="$VM_DIR/.cfw_temp" DISABLE_LAUNCHD_HOOK="${DISABLE_LAUNCHD_HOOK:-0}" # ── Helpers ───────────────────────────────────────────────────── die() { echo "[-] $*" >&2 exit 1 } check_prerequisites() { local missing=() command -v ldid &>/dev/null || missing+=("ldid (brew install ldid-procursus)") command -v xcrun &>/dev/null || missing+=("xcrun (Xcode command line tools)") if ((${#missing[@]} > 0)); then die "Missing required tools: ${missing[*]}. Run: make setup_tools" fi } 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" } ldid_sign_ent() { local file="$1" entitlements_plist="$2" bundle_id="${3:-}" local args=("-S$entitlements_plist" "-K$VM_DIR/$CFW_INPUT/signcert.p12") [[ -n "$bundle_id" ]] && args+=("-I$bundle_id") ldid "${args[@]}" "$file" } build_tweakloader() { local src="$SCRIPT_DIR/tweakloader/TweakLoader.m" local out="$TEMP_DIR/TweakLoader.dylib" local sdk cc [[ -f "$src" ]] || die "Missing tweak loader source at $src" sdk="$(xcrun --sdk iphoneos --show-sdk-path)" cc="$(xcrun --sdk iphoneos -f clang)" "$cc" -isysroot "$sdk" \ -arch arm64 -arch arm64e \ -miphoneos-version-min=15.0 \ -dynamiclib \ -fobjc-arc -O3 \ -framework Foundation \ -o "$out" \ "$src" ldid_sign "$out" echo "$out" } # Builds the libvcamcaptured.dylib injected into /usr/libexec/cameracaptured # via the TweakLoader allowlist. Output goes to TEMP_DIR; caller copies the # binary + companion plist to procursus/Library/MobileSubstrate/DynamicLibraries. build_libvcamcaptured() { local src="$SCRIPT_DIR/vcamcaptured/libvcamcaptured.m" local out="$TEMP_DIR/libvcamcaptured.dylib" local sdk cc [[ -f "$src" ]] || die "Missing libvcamcaptured source at $src" sdk="$(xcrun --sdk iphoneos --show-sdk-path)" cc="$(xcrun --sdk iphoneos -f clang)" "$cc" -isysroot "$sdk" \ -arch arm64e \ -miphoneos-version-min=15.0 \ -dynamiclib \ -fobjc-arc -Os \ -install_name /var/jb/usr/lib/libvcamcaptured.dylib \ -framework CoreMedia \ -framework CoreVideo \ -framework Foundation \ -o "$out" \ "$src" ldid_sign "$out" echo "$out" } # Builds the libcamfix.dylib substrate plugin. Loaded into every process # that links AVFoundation via TweakLoader's Filter.Frameworks key — # Camera.app, third-party apps, anywhere the # documented capture interface is used. Implements the photo-delivery # path through CAMCaptureEngine + the preview/state guards that keep # the viewfinder live on the virtual camera. build_libcamfix() { local src="$SCRIPT_DIR/camfix/libcamfix.m" local out="$TEMP_DIR/libcamfix.dylib" local sdk cc [[ -f "$src" ]] || die "Missing libcamfix source at $src" sdk="$(xcrun --sdk iphoneos --show-sdk-path)" cc="$(xcrun --sdk iphoneos -f clang)" "$cc" -isysroot "$sdk" \ -arch arm64e \ -miphoneos-version-min=15.0 \ -dynamiclib \ -fobjc-arc -Os \ -install_name /var/jb/Library/MobileSubstrate/DynamicLibraries/libcamfix.dylib \ -framework AVFoundation \ -framework CoreImage \ -framework CoreGraphics \ -framework CoreMedia \ -framework CoreVideo \ -framework Foundation \ -framework ImageIO \ -framework IOSurface \ -framework MobileCoreServices \ -framework Photos \ -framework QuartzCore \ -framework UIKit \ -o "$out" \ "$src" ldid_sign "$out" echo "$out" } get_boot_manifest_hash() { /bin/ls $MNT5 2>/dev/null | awk 'length($0)==96{print; exit}' } # ── Setup JB input resources ────────────────────────────────── setup_cfw_jb_input() { [[ -d "$VM_DIR/$CFW_JB_INPUT" ]] && return local archive for search_dir in "$SCRIPT_DIR/resources" "$SCRIPT_DIR" "$VM_DIR"; do archive="$search_dir/$CFW_JB_ARCHIVE" if [[ -f "$archive" ]]; then echo " Extracting $CFW_JB_ARCHIVE..." tar --zstd -xf "$archive" -C "$VM_DIR" return fi done die "JB mode: neither $CFW_JB_INPUT/ nor $CFW_JB_ARCHIVE found" } # ── Apply dev overlay (replace rpcserver_ios in iosbinpack64) ── apply_dev_overlay() { local dev_bin for search_dir in "$SCRIPT_DIR/resources/cfw_dev" "$SCRIPT_DIR/cfw_dev"; do dev_bin="$search_dir/rpcserver_ios" if [[ -f "$dev_bin" ]]; then echo " Applying dev overlay (rpcserver_ios)..." local iosbinpack="$VM_DIR/$CFW_INPUT/jb/iosbinpack64.tar" local tmpdir="$VM_DIR/.iosbinpack_tmp" mkdir -p "$tmpdir" tar -xf "$iosbinpack" -C "$tmpdir" cp "$dev_bin" "$tmpdir/iosbinpack64/usr/local/bin/rpcserver_ios" (cd "$tmpdir" && tar -cf "$iosbinpack" iosbinpack64) rm -rf "$tmpdir" return fi done die "Dev overlay not found (cfw_dev/rpcserver_ios)" } # The VM's Disk.img is attached on the host by cfw_install_host.sh; its APFS # volumes are mounted here and every file is placed with plain cp/chmod/etc. # (the VM is off — nothing runs "on the device"). : "${CFW_HOST_CONTAINER:?CFW_HOST_CONTAINER unset — run via cfw_install_host.sh}" HOST_MNT="${CFW_HOST_MNT:-/private/tmp/cfwhost}" MNT1="$HOST_MNT/mnt1" # disk1s1 (System / rootfs) MNT3="$HOST_MNT/mnt3" # disk1s3 MNT5="$HOST_MNT/mnt5" # disk1s5 (per-boot-manifest OS dir / procursus bootstrap) TAR="$(command -v gtar 2>/dev/null || echo /opt/homebrew/bin/gtar)" # macOS bsdtar lacks GNU tar flags mkdir -p "$HOST_MNT" # Mount an APFS volume of the attached image container at a host mount point. mount_vol() { # mount_vol [opts] local dev="/dev/${CFW_HOST_CONTAINER}$1" mnt="$2" opts="${3:-rw}" /bin/mkdir -p "$mnt" /sbin/mount | /usr/bin/grep -q " on $mnt " && return 0 /sbin/mount_apfs -o "$opts" "$dev" "$mnt" 2>/dev/null || true /sbin/mount | /usr/bin/grep -q " on $mnt " || die "mount failed: $dev -> $mnt" } # ── Check JB prerequisites ──────────────────────────────────── command -v zstd >/dev/null 2>&1 || die "'zstd' not found (required for JB bootstrap phase)" setup_cfw_jb_input JB_INPUT_DIR="$VM_DIR/$CFW_JB_INPUT" echo "" echo "[+] JB input resources: $JB_INPUT_DIR" check_prerequisites mkdir -p "$TEMP_DIR" # Mount the image's System volume (may already be mounted from base install) mount_vol s1 "$MNT1" # ═══════════ JB-1 PATCH LAUNCHD (JETSAM + DYLIB INJECTION) ════ echo "" echo "[JB-1] Patching launchd (jetsam guard + hook injection)..." if ! [[ -e "$MNT1/sbin/launchd.bak" ]]; then echo " Creating backup..." /bin/cp $MNT1/sbin/launchd $MNT1/sbin/launchd.bak fi cp "$MNT1/sbin/launchd.bak" "$TEMP_DIR/launchd" # Extract original entitlements before patching (must preserve for spawn permissions) echo " Extracting original entitlements..." ldid -e "$TEMP_DIR/launchd" > "$TEMP_DIR/launchd.entitlements" 2>/dev/null || true if [[ -s "$TEMP_DIR/launchd.entitlements" ]]; then echo " [+] Preserved launchd entitlements" else echo " [!] No entitlements found on original launchd" fi # Injecting launchdhook into pid 1 is on by default. This boot-critical hook # path has produced boot-analysis failures; set DISABLE_LAUNCHD_HOOK=1 to keep # BaseBin deployed without loading /b from launchd. if [[ "$DISABLE_LAUNCHD_HOOK" == "1" ]]; then echo " [*] Skipping launchdhook dylib injection (DISABLE_LAUNCHD_HOOK=1)" elif [[ -d "$JB_INPUT_DIR/basebin" ]]; then echo " Injecting weak dylib load for /b (short launchdhook alias)..." "$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" inject-dylib "$TEMP_DIR/launchd" "/b" else echo " [!] BaseBin is missing; skipping launchdhook injection" fi "$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-launchd-jetsam "$TEMP_DIR/launchd" # Re-sign with original entitlements to avoid "operation not permitted" on spawn if [[ -s "$TEMP_DIR/launchd.entitlements" ]]; then ldid -S"$TEMP_DIR/launchd.entitlements" -M "-K$VM_DIR/$CFW_INPUT/signcert.p12" "$TEMP_DIR/launchd" else ldid_sign "$TEMP_DIR/launchd" fi cp -R "$TEMP_DIR/launchd" "$MNT1/sbin/launchd" /bin/chmod 0755 $MNT1/sbin/launchd echo " [+] launchd patched" # ═══════════ JB-2 INSTALL IOSBINPACK64 ════════════════════════ echo "" echo "[JB-2] Installing iosbinpack64..." apply_dev_overlay cp -R "$VM_DIR/$CFW_INPUT/jb/iosbinpack64.tar" "$MNT1" "$TAR" --preserve-permissions --no-overwrite-dir \ -xf $MNT1/iosbinpack64.tar -C $MNT1 /bin/rm -f $MNT1/iosbinpack64.tar echo " [+] iosbinpack64 installed" # ═══════════ JB-3 PATCH debugserver entitlements ════ echo "" echo "[JB-3] Patching debugserver entitlements..." cp "$MNT1/usr/libexec/debugserver" "$TEMP_DIR/debugserver" ldid -e "$TEMP_DIR/debugserver" > "$TEMP_DIR/debugserver-entitlements.plist" plutil -remove seatbelt-profiles "$TEMP_DIR/debugserver-entitlements.plist" || true plutil -insert task_for_pid-allow -bool YES "$TEMP_DIR/debugserver-entitlements.plist" || true ldid_sign_ent "$TEMP_DIR/debugserver" "$TEMP_DIR/debugserver-entitlements.plist" cp -R "$TEMP_DIR/debugserver" "$MNT1/usr/libexec/debugserver" /bin/chmod 0755 $MNT1/usr/libexec/debugserver echo " [+] debugserver entitlements patched" # ═══════════ EXP-JB-3.5 PATCH watchdogd hv_vmm_present cache ══ # # Background: the kernel-side OID rename (KernelEXPPatchHvVmmRename) # makes sysctlbyname("kern.hv_vmm_present", ...) return ENOENT on this # image. watchdogd caches that answer at startup and uses it to decide # whether to look for the IOWatchdog kext. The unpatched flow takes # the "not on a VM" branch on ENOENT, fails to find the kext (it # doesn't exist on the VM), calls _os_crash -> brk #1, and launchd's # `_PanicOnCrash` knob in com.apple.watchdogd.plist escalates the # resulting SIGTRAP to a kernel panic. # # Patch shape: two-instruction surgical edit at every site in # watchdogd that has the canonical # adrp/add(kern.hv_vmm_present) -> bl _sysctlbyname -> cbnz w0,skip # -> cset wN,ne -> strb wN,[global] # shape. The edit forces the cached byte to 1 regardless of the # sysctl result, so the downstream branch at +0x58e0 takes watchdogd's # pre-existing "detected virtual machine environment" clean-exit path. # The patcher also recomputes the affected CodeDirectory slot hashes # (cfw_macho_codesign) so TXM still accepts the modified pages on # demand-page-in. We deliberately do NOT re-sign with ldid — the # Apple-issued code-signing identifier ("com.apple.watchdogd") must be # preserved for launchd boot-task identity validation. echo "" echo "[EXP-JB-3.5] Patching watchdogd hv_vmm_present cache..." cp "$MNT1/usr/libexec/watchdogd" "$TEMP_DIR/watchdogd" "$SCRIPT_DIR/patch_hv_vmm_userland.sh" watchdogd "$TEMP_DIR/watchdogd" cp -R "$TEMP_DIR/watchdogd" "$MNT1/usr/libexec/watchdogd" /bin/chmod 0755 $MNT1/usr/libexec/watchdogd echo " [+] watchdogd patched" # ═══════════ JB-4 INSTALL PROCURSUS BOOTSTRAP ══════════════════ echo "" echo "[JB-4] Installing procursus bootstrap..." mount_vol s5 "$MNT5" BOOT_HASH="$(get_boot_manifest_hash)" [[ -n "$BOOT_HASH" ]] || die "Could not find 96-char boot manifest hash in $MNT5" echo " Boot manifest hash: $BOOT_HASH" BOOTSTRAP_ZST="$JB_INPUT_DIR/jb/bootstrap-iphoneos-arm64.tar.zst" SILEO_DEB="$JB_INPUT_DIR/jb/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb" [[ -f "$BOOTSTRAP_ZST" ]] || die "Missing $BOOTSTRAP_ZST" BOOTSTRAP_TAR="$TEMP_DIR/bootstrap-iphoneos-arm64.tar" zstd -d -f "$BOOTSTRAP_ZST" -o "$BOOTSTRAP_TAR" cp -R "$BOOTSTRAP_TAR" "$MNT5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar" if [[ -f "$SILEO_DEB" ]]; then cp -R "$SILEO_DEB" "$MNT5/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb" fi JB_DIR_NAME="jb-vphone" /bin/rm -rf $MNT5/$BOOT_HASH/jb /bin/rm -rf $MNT5/$BOOT_HASH/$JB_DIR_NAME /bin/mkdir -p $MNT5/$BOOT_HASH/$JB_DIR_NAME /bin/chmod 0755 $MNT5/$BOOT_HASH/$JB_DIR_NAME /usr/sbin/chown 0:0 $MNT5/$BOOT_HASH/$JB_DIR_NAME "$TAR" --preserve-permissions -xf $MNT5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar \ -C $MNT5/$BOOT_HASH/$JB_DIR_NAME/ /bin/mv $MNT5/$BOOT_HASH/$JB_DIR_NAME/var $MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus mv "$MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus/jb"/*(N) "$MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus" 2>/dev/null || true /bin/rm -rf $MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus/jb /bin/rm -f $MNT5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar rm -f "$BOOTSTRAP_TAR" # NOTE: /var/jb symlink is created on first normal boot by vphone_jb_setup.sh # (Data volume is encrypted and not mountable at install time). echo " [+] procursus bootstrap installed" # ═══════════ JB-4 DEPLOY BASEBIN HOOKS ═════════════════════════ BASEBIN_DIR="$JB_INPUT_DIR/basebin" if [[ -d "$BASEBIN_DIR" ]]; then echo "" echo "[JB-4] Deploying BaseBin hooks to /cores/..." # Clean previous dylibs before re-uploading echo " Cleaning old /cores/ dylibs..." /bin/rm -rf $MNT1/cores /bin/mkdir -p $MNT1/cores /bin/chmod 0755 $MNT1/cores # Install all pre-built dylibs from basebin payload for dylib in "$BASEBIN_DIR"/*.dylib; do [[ -f "$dylib" ]] || continue dylib_name="$(basename "$dylib")" echo " Installing $dylib_name..." ldid_sign "$dylib" cp -R "$dylib" "$MNT1/cores/$dylib_name" /bin/chmod 0755 $MNT1/cores/$dylib_name done # Short alias for launchdhook (header space is tight) if [[ -f "$BASEBIN_DIR/launchdhook.dylib" ]]; then echo " Installing short launchdhook alias at /b..." cp "$BASEBIN_DIR/launchdhook.dylib" "$TEMP_DIR/b" ldid_sign "$TEMP_DIR/b" /bin/rm -f $MNT1/b cp -R "$TEMP_DIR/b" "$MNT1/b" /bin/chmod 0755 $MNT1/b fi echo " [+] BaseBin hooks deployed" fi # ═══════════ JB-4 INSTALL TWEAKLOADER ════════════════════════════ echo "" echo "[JB-4] Building and installing TweakLoader..." TWEAKLOADER_OUT="$(build_tweakloader)" /bin/mkdir -p $MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus/usr/lib cp -R "$TWEAKLOADER_OUT" "$MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus/usr/lib/TweakLoader.dylib" /usr/sbin/chown 0:0 $MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus/usr/lib/TweakLoader.dylib /bin/chmod 0755 $MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus/usr/lib/TweakLoader.dylib echo " [+] TweakLoader installed to procursus/usr/lib/TweakLoader.dylib" # ═══════════ JB-4.1 INSTALL libvcamcaptured ═════════════════════════ # Inject dylib loaded into /usr/libexec/cameracaptured (via TweakLoader # allowlist in TweakLoader.m's kVPhoneAllowedDaemonPaths). Hosts the # synthetic FigCaptureSource + shm-frame reader paired with vphoned's # vsock 1338 listener on the guest side. echo "" echo "[JB-4.1] Building and installing libvcamcaptured..." LIBVCAM_OUT="$(build_libvcamcaptured)" LIBVCAM_DIR="$MNT5/$BOOT_HASH/$JB_DIR_NAME/procursus/Library/MobileSubstrate/DynamicLibraries" /bin/mkdir -p $LIBVCAM_DIR cp -R "$LIBVCAM_OUT" "$LIBVCAM_DIR/libvcamcaptured.dylib" /usr/sbin/chown 0:0 $LIBVCAM_DIR/libvcamcaptured.dylib /bin/chmod 0755 $LIBVCAM_DIR/libvcamcaptured.dylib # The plist tells TweakLoader to load the dylib only inside cameracaptured # (Filter.Executables = ["cameracaptured"]); keep it next to the dylib. LIBVCAM_PLIST="$SCRIPT_DIR/vcamcaptured/libvcamcaptured.plist" if [[ -f "$LIBVCAM_PLIST" ]]; then cp -R "$LIBVCAM_PLIST" "$LIBVCAM_DIR/libvcamcaptured.plist" /bin/chmod 0644 $LIBVCAM_DIR/libvcamcaptured.plist fi echo " [+] libvcamcaptured installed to procursus/Library/MobileSubstrate/DynamicLibraries/" # ═══════════ JB-4.2 INSTALL libcamfix ═══════════════════════════════ # Substrate plugin loaded by TweakLoader into every process that links # AVFoundation. The companion plist's Filter.Frameworks = ["AVFoundation"] # tells TweakLoader to defer the dlopen until AVFoundation actually # appears in the loaded image list (via _dyld_register_func_for_add_image), # so processes that don't use AVF don't pay any cost. echo "" echo "[JB-4.2] Building and installing libcamfix..." LIBCAMFIX_OUT="$(build_libcamfix)" cp -R "$LIBCAMFIX_OUT" "$LIBVCAM_DIR/libcamfix.dylib" /usr/sbin/chown 0:0 $LIBVCAM_DIR/libcamfix.dylib /bin/chmod 0755 $LIBVCAM_DIR/libcamfix.dylib LIBCAMFIX_PLIST="$SCRIPT_DIR/camfix/libcamfix.plist" if [[ -f "$LIBCAMFIX_PLIST" ]]; then cp -R "$LIBCAMFIX_PLIST" "$LIBVCAM_DIR/libcamfix.plist" /bin/chmod 0644 $LIBVCAM_DIR/libcamfix.plist fi echo " [+] libcamfix installed to procursus/Library/MobileSubstrate/DynamicLibraries/" # ═══════════ JB-5 DEPLOY FIRST-BOOT SETUP ══════════════════════ echo "" echo "[JB-5] Deploying first-boot setup..." # Deploy first-boot JB setup script + LaunchDaemon SETUP_SCRIPT="$SCRIPT_DIR/vphone_jb_setup.sh" SETUP_PLIST="$SCRIPT_DIR/vphone_jb_setup.plist" if [[ -f "$SETUP_SCRIPT" ]]; then cp -R "$SETUP_SCRIPT" "$MNT1/cores/vphone_jb_setup.sh" /bin/chmod 0755 $MNT1/cores/vphone_jb_setup.sh echo " [+] vphone_jb_setup.sh -> /cores/" fi if [[ -f "$SETUP_PLIST" ]]; then cp -R "$SETUP_PLIST" "$MNT1/System/Library/LaunchDaemons/com.vphone.jb-setup.plist" /bin/chmod 0644 $MNT1/System/Library/LaunchDaemons/com.vphone.jb-setup.plist # Inject into launchd.plist so launchd starts it at boot echo " Injecting com.vphone.jb-setup into launchd.plist..." cp "$MNT1/System/Library/xpc/launchd.plist" "$TEMP_DIR/launchd.plist" "$PYTHON3" -c " import plistlib, sys with open(sys.argv[1], 'rb') as f: target = plistlib.load(f) with open(sys.argv[2], 'rb') as f: daemon = plistlib.load(f) target.setdefault('LaunchDaemons', {})['/System/Library/LaunchDaemons/com.vphone.jb-setup.plist'] = daemon with open(sys.argv[1], 'wb') as f: plistlib.dump(target, f, sort_keys=False) " "$TEMP_DIR/launchd.plist" "$SETUP_PLIST" cp -R "$TEMP_DIR/launchd.plist" "$MNT1/System/Library/xpc/launchd.plist" /bin/chmod 0644 $MNT1/System/Library/xpc/launchd.plist echo " [+] com.vphone.jb-setup.plist injected into launchd.plist" fi # ═══════════ EXP-JB-6 POST-RESTORE DT IDENTITY REWRITE ════════ # # Apply the three restore-unsafe DT property edits that broke earlier # attempts when applied at fw_patch time: # root/model iPhone99,11 -> iPhone17,3 # root/target-type VPHONE600 -> D47 # root/compatible reordered to [D47AP, VPHONE600AP, AppleVirtualPlatformARM] # # These are restore-time-fatal (restored_external / iBoot's restore mode # cross-checks model+target-type against the BuildManifest's signed # identity) but NOT boot-time-fatal — the existing iBSS/iBEC/LLB # image4_validate_property_callback bypass patches accept any IM4P # contents on subsequent boots. # # $MNT5 is still mounted at this point in the install flow (the umount # happens in the CLEANUP block below). We copy the live devicetree.img4 # out, patch it on the host, copy it back. Next boot, iBoot loads the # modified DT, kernel populates machine_info from the new values, and # sysctl hw.machine / hw.product / hw.model flip to iPhone17,3 / D47 / # (whatever IOPlatformExpert resolves from compatible[0]=D47AP). echo "" echo "[EXP-JB-6] Post-restore DT identity rewrite..." if [[ -z "$BOOT_HASH" ]]; then BOOT_HASH="$(get_boot_manifest_hash)" fi if [[ -z "$BOOT_HASH" ]]; then echo " [-] BOOT_HASH not discoverable, skipping EXP-JB-6" else JB6_DT_REMOTE="$MNT5/$BOOT_HASH/usr/standalone/firmware/devicetree.img4" JB6_DT_LOCAL="$TEMP_DIR/devicetree.img4" if [[ -e "$JB6_DT_REMOTE" ]]; then cp "$JB6_DT_REMOTE" "$JB6_DT_LOCAL" "$PYTHON3" "$SCRIPT_DIR/patchers/cfw_patch_post_restore_dt.py" "$JB6_DT_LOCAL" cp -R "$JB6_DT_LOCAL" "$JB6_DT_REMOTE" /usr/sbin/chown 0:0 $JB6_DT_REMOTE /bin/chmod 0644 $JB6_DT_REMOTE echo " [+] devicetree.img4 rewritten in place" else echo " [-] $JB6_DT_REMOTE not found, skipping EXP-JB-6" fi fi # ═══════════ EXP-JB-7 BUILD-VERSION REWRITE (SystemVersion.plist) ══ # # OPT-IN. Only runs when SPOOF_BUILD is set in the environment (e.g. # `make setup_machine JB=1 SPOOF_BUILD=23F77` or `make cfw_install_jb # SPOOF_BUILD=23F77`). When SPOOF_BUILD is unset/empty the step is # skipped entirely and the build identifier stays at whatever the IPSW # shipped. # # What it does (when enabled): flips ProductBuildVersion in the two # SystemVersion.plist files iOS reads for "Build" display and # MGCopyAnswer("BuildVersion"): # /System/Library/CoreServices/SystemVersion.plist (rootfs) # /private/preboot/Cryptexes/OS/System/Library/CoreServices/SystemVersion.plist (Cryptex) # # Both are plain plist files (no Apple signature on individual plists), # so no image4 / cdHash / TXM concerns. Both volumes are writable at # install time: # $MNT1 (rootfs) — writable before the install-time seal is established # $MNT5 (preboot) — apfs writable # # After this, Settings -> About -> Build, MG BuildVersion key, and every # framework that reads SystemVersion.plist see the new identifier. # `sysctl kern.osversion` still reports the kernel image's own build # (e.g. 23B78 from the PCC vphone600/vresearch101 kernel) — that comes # from a kernel global populated at boot from boot args, not from this # plist. if [[ -n "${SPOOF_BUILD:-}" ]]; then echo "" echo "[EXP-JB-7] Rewriting ProductBuildVersion to $SPOOF_BUILD in SystemVersion plists..." for jb7_remote in \ "$MNT1/System/Library/CoreServices/SystemVersion.plist" \ "$MNT5/Cryptexes/OS/System/Library/CoreServices/SystemVersion.plist" do if [[ -e "$jb7_remote" ]]; then jb7_local="$TEMP_DIR/$(echo "$jb7_remote" | tr '/' '_').plist" cp "$jb7_remote" "$jb7_local" "$PYTHON3" "$SCRIPT_DIR/patchers/cfw_patch_build_version.py" \ "$jb7_local" "$SPOOF_BUILD" cp -R "$jb7_local" "$jb7_remote" else echo " [-] $jb7_remote not found, skipping" fi done else echo "" echo "[EXP-JB-7] Skipped — SPOOF_BUILD not set (pass SPOOF_BUILD= to enable)" fi # ═══════════ CLEANUP ═════════════════════════════════════════ echo "" echo "[*] Unmounting image volumes..." /sbin/umount $MNT1 2>/dev/null || true /sbin/umount $MNT3 2>/dev/null || true /sbin/umount $MNT5 2>/dev/null || true echo "[*] Cleaning up temp binaries..." rm -f "$TEMP_DIR/launchd" \ "$TEMP_DIR/bootstrap-iphoneos-arm64.tar" echo "" echo "[+] CFW + JB + EXP installation complete!" echo " Boot to apply changes." echo " After boot, SSH will be available on port 22222 (password: alpine)"