mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
863 lines
30 KiB
Python
Executable File
863 lines
30 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
build_ramdisk.py — Build a signed SSH ramdisk for vphone600.
|
|
|
|
Expects the VM restore tree to have already been patched by the Swift firmware pipeline.
|
|
Extracts patched components, signs with SHSH, and builds SSH ramdisk.
|
|
|
|
Usage:
|
|
python3 build_ramdisk.py [vm_directory]
|
|
|
|
Directory structure:
|
|
./shsh/ — SHSH blobs (auto-discovered)
|
|
./ramdisk_input/ — Tools and SSH resources (auto-setup from CFW)
|
|
./ramdisk_builder_temp/ — Intermediate .raw files (cleaned up)
|
|
./Ramdisk/ — Final signed IMG4 output
|
|
|
|
Prerequisites:
|
|
pip install pyimg4
|
|
Run make fw_patch / make fw_patch_dev / make fw_patch_jb first to patch boot-chain components.
|
|
"""
|
|
|
|
import gzip
|
|
import glob
|
|
import os
|
|
import plistlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
from pyimg4 import IM4M, IM4P, IMG4
|
|
|
|
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# Configuration
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
OUTPUT_DIR = "Ramdisk"
|
|
TEMP_DIR = "ramdisk_builder_temp"
|
|
INPUT_DIR = "ramdisk_input"
|
|
RESTORED_EXTERNAL_PATH = "usr/local/bin/restored_external"
|
|
RESTORED_EXTERNAL_SERIAL_MARKER = b"SSHRD_Script Sep 22 2022 18:56:50"
|
|
DEFAULT_IBEC_BOOT_ARGS = b"serial=3 -v debug=0x2014e %s"
|
|
|
|
# Ramdisk boot-args
|
|
RAMDISK_BOOT_ARGS = b"serial=3 rd=md0 debug=0x2014e -v wdt=-1 %s"
|
|
|
|
# IM4P fourccs for restore mode
|
|
TXM_FOURCC = "trxm"
|
|
KERNEL_FOURCC = "rkrn"
|
|
RAMDISK_KERNEL_SUFFIX = ".ramdisk"
|
|
RAMDISK_KERNEL_IMG4 = "krnl.ramdisk.img4"
|
|
SUDO_PASSWORD = os.environ.get("VPHONE_SUDO_PASSWORD", None)
|
|
|
|
# Files to remove from ramdisk to save space
|
|
RAMDISK_REMOVE = [
|
|
"usr/bin/img4tool",
|
|
"usr/bin/img4",
|
|
"usr/sbin/dietappleh13camerad",
|
|
"usr/sbin/dietappleh16camerad",
|
|
"usr/local/bin/wget",
|
|
"usr/local/bin/procexp",
|
|
]
|
|
|
|
# Directories to re-sign in ramdisk
|
|
SIGN_DIRS = [
|
|
"usr/local/bin/*",
|
|
"usr/local/lib/*",
|
|
"usr/bin/*",
|
|
"bin/*",
|
|
"usr/lib/*",
|
|
"sbin/*",
|
|
"usr/sbin/*",
|
|
"usr/libexec/*",
|
|
]
|
|
|
|
# Compressed archive of ramdisk_input/ (located next to this script)
|
|
INPUT_ARCHIVE = "ramdisk_input.tar.zst"
|
|
PATCHER_BINARY_ENV = "VPHONE_PATCHER_BINARY"
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# Setup — extract ramdisk_input/ from zstd archive if needed
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def setup_input(vm_dir):
|
|
"""Ensure ramdisk_input/ exists, extracting from .tar.zst if needed."""
|
|
input_dir = os.path.join(vm_dir, INPUT_DIR)
|
|
|
|
if os.path.isdir(input_dir):
|
|
return input_dir
|
|
|
|
# Look for archive next to this script, then in vm_dir
|
|
for search_dir in (os.path.join(_SCRIPT_DIR, "resources"), _SCRIPT_DIR, vm_dir):
|
|
archive = os.path.join(search_dir, INPUT_ARCHIVE)
|
|
if os.path.isfile(archive):
|
|
print(f" Extracting {INPUT_ARCHIVE}...")
|
|
subprocess.run(
|
|
["tar", "--zstd", "-xf", archive, "-C", vm_dir],
|
|
check=True,
|
|
)
|
|
return input_dir
|
|
|
|
print(f"[-] Neither {INPUT_DIR}/ nor {INPUT_ARCHIVE} found.")
|
|
print(f" Place {INPUT_ARCHIVE} next to this script or in the VM directory.")
|
|
sys.exit(1)
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# SHSH / signing helpers
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def find_shsh(shsh_dir):
|
|
"""Find first SHSH blob in directory."""
|
|
for ext in ("*.shsh", "*.shsh2"):
|
|
matches = sorted(glob.glob(os.path.join(shsh_dir, ext)))
|
|
if matches:
|
|
return matches[0]
|
|
return None
|
|
|
|
|
|
def extract_im4m(shsh_path, im4m_path):
|
|
"""Extract IM4M manifest from SHSH blob (handles gzip-compressed)."""
|
|
raw = open(shsh_path, "rb").read()
|
|
if raw[:2] == b"\x1f\x8b":
|
|
raw = gzip.decompress(raw)
|
|
tmp = shsh_path + ".tmp"
|
|
try:
|
|
open(tmp, "wb").write(raw)
|
|
subprocess.run(
|
|
["pyimg4", "im4m", "extract", "-i", tmp, "-o", im4m_path],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
finally:
|
|
if os.path.exists(tmp):
|
|
os.remove(tmp)
|
|
|
|
|
|
def sign_img4(im4p_path, img4_path, im4m_path, tag=None):
|
|
"""Create IMG4 from IM4P + IM4M using pyimg4 Python API."""
|
|
im4p = IM4P(open(im4p_path, "rb").read())
|
|
if tag:
|
|
im4p.fourcc = tag
|
|
im4m = IM4M(open(im4m_path, "rb").read())
|
|
img4 = IMG4(im4p=im4p, im4m=im4m)
|
|
with open(img4_path, "wb") as f:
|
|
f.write(img4.output())
|
|
|
|
|
|
def run(cmd, **kwargs):
|
|
"""Run a command, raising on failure."""
|
|
return subprocess.run(cmd, check=True, **kwargs)
|
|
|
|
|
|
def run_sudo(cmd, **kwargs):
|
|
"""Run sudo command non-interactively using VPHONE_SUDO_PASSWORD."""
|
|
if SUDO_PASSWORD:
|
|
return run(
|
|
["sudo", "-S", *cmd],
|
|
input=f"{SUDO_PASSWORD}\n",
|
|
text=True,
|
|
**kwargs,
|
|
)
|
|
return run(["sudo", *cmd], **kwargs)
|
|
|
|
|
|
def ensure_path_within_vm(path, vm_dir, label):
|
|
"""Fail if path escapes vm_dir."""
|
|
vm_real = os.path.realpath(vm_dir)
|
|
path_real = os.path.realpath(path)
|
|
if path_real == vm_real or path_real.startswith(vm_real + os.sep):
|
|
return
|
|
print(f"[-] {label} must be inside VM dir")
|
|
print(f" VM dir: {vm_real}")
|
|
print(f" Path: {path_real}")
|
|
sys.exit(1)
|
|
|
|
|
|
def check_prerequisites():
|
|
"""Verify required host tools are available."""
|
|
missing = []
|
|
for tool, pkg in [("gtar", "gnu-tar"), ("ldid", "ldid-procursus"), ("trustcache", "trustcache (make setup_tools)")]:
|
|
if not shutil.which(tool):
|
|
missing.append(f" {tool:12s} — {pkg}")
|
|
if missing:
|
|
print("[-] Missing required tools:")
|
|
for m in missing:
|
|
print(m)
|
|
print("\n Run: make setup_tools")
|
|
sys.exit(1)
|
|
|
|
|
|
def project_root():
|
|
return os.path.abspath(os.path.join(_SCRIPT_DIR, ".."))
|
|
|
|
|
|
def patcher_binary_path():
|
|
override = os.environ.get(PATCHER_BINARY_ENV, "").strip()
|
|
if override:
|
|
return os.path.abspath(override)
|
|
return os.path.join(project_root(), ".build", "debug", "vphone-cli")
|
|
|
|
|
|
def run_swift_patch_component(component, src_path, output_path):
|
|
"""Patch a single component via the Swift FirmwarePatcher CLI."""
|
|
binary = patcher_binary_path()
|
|
if not os.path.isfile(binary):
|
|
print(f"[-] Swift patcher binary not found: {binary}")
|
|
print(" Run: make patcher_build")
|
|
sys.exit(1)
|
|
|
|
run(
|
|
[
|
|
binary,
|
|
"patch-component",
|
|
"--component",
|
|
component,
|
|
"--input",
|
|
src_path,
|
|
"--output",
|
|
output_path,
|
|
"--quiet",
|
|
]
|
|
)
|
|
|
|
|
|
def load_firmware(path):
|
|
"""Load firmware file, auto-detecting IM4P vs raw."""
|
|
with open(path, "rb") as f:
|
|
raw = f.read()
|
|
|
|
try:
|
|
im4p = IM4P(raw)
|
|
if im4p.payload.compression:
|
|
im4p.payload.decompress()
|
|
return im4p, bytearray(im4p.payload.data), True, raw
|
|
except Exception:
|
|
return None, bytearray(raw), False, raw
|
|
|
|
|
|
def _save_im4p_with_payp(path, fourcc, patched_data, original_raw):
|
|
"""Repackage as LZFSE-compressed IM4P and append PAYP from original."""
|
|
with (
|
|
tempfile.NamedTemporaryFile(suffix=".raw", delete=False) as tmp_raw,
|
|
tempfile.NamedTemporaryFile(suffix=".im4p", delete=False) as tmp_im4p,
|
|
):
|
|
tmp_raw_path = tmp_raw.name
|
|
tmp_im4p_path = tmp_im4p.name
|
|
tmp_raw.write(bytes(patched_data))
|
|
|
|
try:
|
|
subprocess.run(
|
|
[
|
|
"pyimg4",
|
|
"im4p",
|
|
"create",
|
|
"-i",
|
|
tmp_raw_path,
|
|
"-o",
|
|
tmp_im4p_path,
|
|
"-f",
|
|
fourcc,
|
|
"--lzfse",
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
output = bytearray(open(tmp_im4p_path, "rb").read())
|
|
finally:
|
|
os.unlink(tmp_raw_path)
|
|
os.unlink(tmp_im4p_path)
|
|
|
|
payp_offset = original_raw.rfind(b"PAYP")
|
|
if payp_offset >= 0:
|
|
payp_data = original_raw[payp_offset - 10 :]
|
|
output.extend(payp_data)
|
|
old_len = int.from_bytes(output[2:5], "big")
|
|
output[2:5] = (old_len + len(payp_data)).to_bytes(3, "big")
|
|
print(f" [+] preserved PAYP ({len(payp_data)} bytes)")
|
|
|
|
with open(path, "wb") as f:
|
|
f.write(output)
|
|
|
|
|
|
def find_restore_dir(base_dir):
|
|
for entry in sorted(os.listdir(base_dir)):
|
|
full = os.path.join(base_dir, entry)
|
|
if os.path.isdir(full) and "Restore" in entry:
|
|
return full
|
|
return None
|
|
|
|
|
|
def find_file(base_dir, patterns, label):
|
|
for pattern in patterns:
|
|
matches = sorted(glob.glob(os.path.join(base_dir, pattern)))
|
|
if matches:
|
|
return matches[0]
|
|
print(f"[-] {label} not found. Searched patterns:")
|
|
for pattern in patterns:
|
|
print(f" {os.path.join(base_dir, pattern)}")
|
|
sys.exit(1)
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# Firmware extraction and IM4P creation
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def extract_to_raw(src_path, raw_path):
|
|
"""Extract IM4P payload to .raw file. Returns (im4p_obj, data, original_raw)."""
|
|
im4p, data, was_im4p, original_raw = load_firmware(src_path)
|
|
with open(raw_path, "wb") as f:
|
|
f.write(bytes(data))
|
|
return im4p, data, original_raw
|
|
|
|
|
|
def create_im4p_uncompressed(raw_data, fourcc, description, output_path):
|
|
"""Create uncompressed IM4P from raw data."""
|
|
new_im4p = IM4P(
|
|
fourcc=fourcc,
|
|
description=description,
|
|
payload=bytes(raw_data),
|
|
)
|
|
with open(output_path, "wb") as f:
|
|
f.write(new_im4p.output())
|
|
|
|
|
|
def build_kernel_img4(kernel_src, output_dir, temp_dir, im4m_path, output_name, temp_tag):
|
|
"""Build one signed kernel IMG4 from a kernelcache source file."""
|
|
kc_raw = os.path.join(temp_dir, f"{temp_tag}.raw")
|
|
kc_im4p = os.path.join(temp_dir, f"{temp_tag}.im4p")
|
|
_, data, original_raw = extract_to_raw(kernel_src, kc_raw)
|
|
print(f" source: {kernel_src}")
|
|
print(f" format: IM4P, {len(data)} bytes")
|
|
_save_im4p_with_payp(kc_im4p, KERNEL_FOURCC, data, original_raw)
|
|
sign_img4(kc_im4p, os.path.join(output_dir, output_name), im4m_path)
|
|
print(f" [+] {output_name}")
|
|
|
|
|
|
def _find_pristine_cloudos_kernel():
|
|
"""Find a pristine CloudOS vphone600 research kernel from project ipsws/."""
|
|
env_path = os.environ.get("RAMDISK_BASE_KERNEL", "").strip()
|
|
if env_path:
|
|
p = os.path.abspath(env_path)
|
|
if os.path.isfile(p):
|
|
return p
|
|
print(f" [!] RAMDISK_BASE_KERNEL set but not found: {p}")
|
|
|
|
project_root = os.path.abspath(os.path.join(_SCRIPT_DIR, ".."))
|
|
patterns = [
|
|
os.path.join(project_root, "ipsws", "PCC-CloudOS*", "kernelcache.research.vphone600"),
|
|
os.path.join(project_root, "ipsws", "*CloudOS*", "kernelcache.research.vphone600"),
|
|
]
|
|
for pattern in patterns:
|
|
matches = sorted(glob.glob(pattern))
|
|
if matches:
|
|
return matches[0]
|
|
return None
|
|
|
|
|
|
def derive_ramdisk_kernel_source(kc_src, temp_dir):
|
|
"""Get source kernel for krnl.ramdisk.img4 entirely within ramdisk_build flow.
|
|
|
|
Priority:
|
|
1) Existing legacy snapshot next to restore kernel (`*.ramdisk`)
|
|
2) Derive from pristine CloudOS kernel by applying base KernelPatcher
|
|
"""
|
|
legacy_snapshot = f"{kc_src}{RAMDISK_KERNEL_SUFFIX}"
|
|
if os.path.isfile(legacy_snapshot):
|
|
print(f" found legacy ramdisk kernel snapshot: {legacy_snapshot}")
|
|
return legacy_snapshot
|
|
|
|
pristine = _find_pristine_cloudos_kernel()
|
|
if not pristine:
|
|
print(" [!] pristine CloudOS kernel not found; skipping ramdisk-specific kernel image")
|
|
return None
|
|
|
|
print(f" deriving ramdisk kernel from pristine source: {pristine}")
|
|
out_path = os.path.join(temp_dir, f"kernelcache.research.vphone600{RAMDISK_KERNEL_SUFFIX}")
|
|
run_swift_patch_component("kernel-base", pristine, out_path)
|
|
print(" [+] base kernel patches applied for ramdisk variant")
|
|
return out_path
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# iBEC boot-args patching
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def patch_ibec_bootargs(data):
|
|
"""Replace normal boot-args with ramdisk boot-args in already-patched iBEC.
|
|
|
|
Finds the boot-args string written by the Swift firmware pipeline
|
|
and overwrites it in-place. No hardcoded offsets needed — the ADRP+ADD
|
|
instructions already point to the string location.
|
|
"""
|
|
off = data.find(DEFAULT_IBEC_BOOT_ARGS)
|
|
if off < 0:
|
|
print(f" [-] boot-args: existing string not found ({DEFAULT_IBEC_BOOT_ARGS.decode()!r})")
|
|
return False
|
|
|
|
args = RAMDISK_BOOT_ARGS + b"\x00"
|
|
data[off : off + len(args)] = args
|
|
|
|
# Zero out any leftover from the previous string
|
|
end = off + len(args)
|
|
while end < len(data) and data[end] != 0:
|
|
data[end] = 0
|
|
end += 1
|
|
|
|
print(f' boot-args -> "{RAMDISK_BOOT_ARGS.decode()}" at 0x{off:X}')
|
|
return True
|
|
|
|
|
|
def patch_restored_external_usbmux_label(mountpoint):
|
|
"""Patch restored_external USBMux serial label when RAMDISK_UDID is provided."""
|
|
target_udid = os.environ.get("RAMDISK_UDID", "").strip()
|
|
if not target_udid:
|
|
print(" [*] RAMDISK_UDID not set; keeping default restored_external USBMux label")
|
|
return
|
|
|
|
try:
|
|
target_bytes = target_udid.encode("ascii")
|
|
except UnicodeEncodeError:
|
|
print(f"[-] RAMDISK_UDID must be ASCII, got: {target_udid!r}")
|
|
sys.exit(1)
|
|
|
|
marker_len = len(RESTORED_EXTERNAL_SERIAL_MARKER)
|
|
if len(target_bytes) > marker_len:
|
|
print(f"[-] RAMDISK_UDID too long for restored_external label ({len(target_bytes)} > {marker_len})")
|
|
print(f" RAMDISK_UDID={target_udid}")
|
|
sys.exit(1)
|
|
|
|
restored_external = os.path.join(mountpoint, RESTORED_EXTERNAL_PATH)
|
|
if not os.path.isfile(restored_external):
|
|
print(f"[-] Missing restored_external for USBMux label patch: {restored_external}")
|
|
sys.exit(1)
|
|
|
|
with open(restored_external, "rb") as f:
|
|
data = f.read()
|
|
|
|
off = data.find(RESTORED_EXTERNAL_SERIAL_MARKER)
|
|
if off < 0:
|
|
print("[-] Could not find default USBMux serial marker in restored_external")
|
|
sys.exit(1)
|
|
|
|
if data.find(RESTORED_EXTERNAL_SERIAL_MARKER, off + 1) >= 0:
|
|
print("[!] Multiple USBMux serial markers found in restored_external; patching first occurrence")
|
|
|
|
replacement = target_bytes + (b"\x00" * (marker_len - len(target_bytes)))
|
|
patched = data[:off] + replacement + data[off + marker_len :]
|
|
|
|
with open(restored_external, "wb") as f:
|
|
f.write(patched)
|
|
|
|
print(f" [+] Patched restored_external USBMux label to: {target_udid}")
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# Ramdisk DMG building
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def build_ramdisk(restore_dir, im4m_path, vm_dir, input_dir, output_dir, temp_dir):
|
|
"""Build custom SSH ramdisk from restore DMG."""
|
|
# Read RestoreRamDisk path dynamically from BuildManifest.plist
|
|
bm_path = os.path.join(restore_dir, "BuildManifest.plist")
|
|
with open(bm_path, "rb") as f:
|
|
bm = plistlib.load(f)
|
|
ramdisk_rel = bm["BuildIdentities"][0]["Manifest"]["RestoreRamDisk"]["Info"]["Path"]
|
|
ramdisk_src = os.path.join(restore_dir, ramdisk_rel)
|
|
mountpoint = os.path.join(vm_dir, "SSHRD")
|
|
ramdisk_raw = os.path.join(temp_dir, "ramdisk.raw.dmg")
|
|
ramdisk_custom = os.path.join(temp_dir, "ramdisk1.dmg")
|
|
gtar_bin = shutil.which("gtar")
|
|
ldid_bin = shutil.which("ldid")
|
|
tc_bin = shutil.which("trustcache")
|
|
|
|
# Extract base ramdisk
|
|
print(" Extracting base ramdisk...")
|
|
run(
|
|
["pyimg4", "im4p", "extract", "-i", ramdisk_src, "-o", ramdisk_raw],
|
|
capture_output=True,
|
|
)
|
|
|
|
ensure_path_within_vm(mountpoint, vm_dir, "Ramdisk mountpoint")
|
|
os.makedirs(mountpoint, exist_ok=True)
|
|
|
|
try:
|
|
# Mount, create expanded copy
|
|
print(" Mounting base ramdisk...")
|
|
run_sudo(
|
|
[
|
|
"hdiutil",
|
|
"attach",
|
|
"-mountpoint",
|
|
mountpoint,
|
|
ramdisk_raw,
|
|
"-nobrowse",
|
|
"-owners",
|
|
"off",
|
|
]
|
|
)
|
|
|
|
print(" Creating expanded ramdisk (254 MB)...")
|
|
run_sudo(
|
|
[
|
|
"hdiutil",
|
|
"create",
|
|
"-size",
|
|
"254m",
|
|
"-imagekey",
|
|
"diskimage-class=CRawDiskImage",
|
|
"-format",
|
|
"UDZO",
|
|
"-fs",
|
|
"APFS",
|
|
"-layout",
|
|
"NONE",
|
|
"-srcfolder",
|
|
mountpoint,
|
|
"-copyuid",
|
|
"root",
|
|
ramdisk_custom,
|
|
]
|
|
)
|
|
run_sudo(["hdiutil", "detach", "-force", mountpoint])
|
|
|
|
# Mount expanded, inject SSH
|
|
print(" Mounting expanded ramdisk...")
|
|
run_sudo(
|
|
[
|
|
"hdiutil",
|
|
"attach",
|
|
"-mountpoint",
|
|
mountpoint,
|
|
ramdisk_custom,
|
|
"-nobrowse",
|
|
"-owners",
|
|
"off",
|
|
]
|
|
)
|
|
|
|
print(" Injecting SSH tools...")
|
|
ssh_tar = os.path.join(input_dir, "ssh.tar.gz")
|
|
run_sudo(
|
|
[gtar_bin, "-x", "--no-overwrite-dir", "-f", ssh_tar, "-C", mountpoint]
|
|
)
|
|
patch_restored_external_usbmux_label(mountpoint)
|
|
|
|
# Remove unnecessary files
|
|
for rel_path in RAMDISK_REMOVE:
|
|
full = os.path.join(mountpoint, rel_path)
|
|
if os.path.exists(full):
|
|
os.remove(full)
|
|
|
|
# Re-sign Mach-O binaries
|
|
print(" Re-signing Mach-O binaries...")
|
|
signcert = os.path.join(input_dir, "signcert.p12")
|
|
|
|
for pattern in SIGN_DIRS:
|
|
for path in glob.glob(os.path.join(mountpoint, pattern)):
|
|
if os.path.isfile(path) and not os.path.islink(path):
|
|
if (
|
|
"Mach-O"
|
|
in subprocess.run(
|
|
["file", path],
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
):
|
|
subprocess.run(
|
|
[ldid_bin, "-S", "-M", f"-K{signcert}", path],
|
|
capture_output=True,
|
|
)
|
|
|
|
# Fix sftp-server entitlements
|
|
sftp_ents = os.path.join(input_dir, "sftp_server_ents.plist")
|
|
sftp_server = os.path.join(mountpoint, "usr/libexec/sftp-server")
|
|
if os.path.exists(sftp_server):
|
|
run([ldid_bin, f"-S{sftp_ents}", "-M", f"-K{signcert}", sftp_server])
|
|
|
|
# Build trustcache
|
|
print(" Building trustcache...")
|
|
tc_raw = os.path.join(temp_dir, "sshrd.raw.tc")
|
|
tc_im4p = os.path.join(temp_dir, "trustcache.im4p")
|
|
|
|
run([tc_bin, "create", tc_raw, mountpoint])
|
|
run(
|
|
["pyimg4", "im4p", "create", "-i", tc_raw, "-o", tc_im4p, "-f", "rtsc"],
|
|
capture_output=True,
|
|
)
|
|
sign_img4(
|
|
tc_im4p,
|
|
os.path.join(output_dir, "trustcache.img4"),
|
|
im4m_path,
|
|
)
|
|
print(f" [+] trustcache.img4")
|
|
|
|
finally:
|
|
run_sudo(["hdiutil", "detach", "-force", mountpoint], capture_output=True)
|
|
|
|
# Shrink and sign ramdisk
|
|
run_sudo(["hdiutil", "resize", "-sectors", "min", ramdisk_custom])
|
|
|
|
print(" Signing ramdisk...")
|
|
rd_im4p = os.path.join(temp_dir, "ramdisk.im4p")
|
|
run(
|
|
["pyimg4", "im4p", "create", "-i", ramdisk_custom, "-o", rd_im4p, "-f", "rdsk"],
|
|
capture_output=True,
|
|
)
|
|
sign_img4(
|
|
rd_im4p,
|
|
os.path.join(output_dir, "ramdisk.img4"),
|
|
im4m_path,
|
|
)
|
|
print(f" [+] ramdisk.img4")
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# Main
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def main():
|
|
vm_dir = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.getcwd())
|
|
|
|
if not os.path.isdir(vm_dir):
|
|
print(f"[-] Not a directory: {vm_dir}")
|
|
sys.exit(1)
|
|
|
|
# Find SHSH
|
|
shsh_path = find_shsh(vm_dir)
|
|
if not shsh_path:
|
|
print(f"[-] No SHSH blob found in {shsh_dir}/")
|
|
print(" Place your .shsh file in the shsh/ directory.")
|
|
sys.exit(1)
|
|
|
|
# Find restore directory
|
|
restore_dir = find_restore_dir(vm_dir)
|
|
if not restore_dir:
|
|
print(f"[-] No *Restore* directory found in {vm_dir}")
|
|
sys.exit(1)
|
|
|
|
# Check host tools
|
|
check_prerequisites()
|
|
|
|
# Setup input resources (copy from CFW if needed)
|
|
print(f"[*] Setting up {INPUT_DIR}/...")
|
|
input_dir = setup_input(vm_dir)
|
|
|
|
# Create temp and output directories
|
|
temp_dir = os.path.join(vm_dir, TEMP_DIR)
|
|
output_dir = os.path.join(vm_dir, OUTPUT_DIR)
|
|
ensure_path_within_vm(temp_dir, vm_dir, "Temp directory")
|
|
ensure_path_within_vm(output_dir, vm_dir, "Output directory")
|
|
for d in (temp_dir, output_dir):
|
|
if os.path.exists(d):
|
|
shutil.rmtree(d)
|
|
os.makedirs(d)
|
|
|
|
print(f"[*] VM directory: {vm_dir}")
|
|
print(f"[*] Restore directory: {restore_dir}")
|
|
print(f"[*] SHSH blob: {shsh_path}")
|
|
|
|
# Extract IM4M from SHSH
|
|
im4m_path = os.path.join(temp_dir, "vphone.im4m")
|
|
print(f"\n[*] Extracting IM4M from SHSH...")
|
|
extract_im4m(shsh_path, im4m_path)
|
|
|
|
# ── 1. iBSS (already patched by patch_firmware.py) ───────────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 1. iBSS (already patched — extract & sign)")
|
|
print(f"{'=' * 60}")
|
|
ibss_src = find_file(
|
|
restore_dir,
|
|
[
|
|
"Firmware/dfu/iBSS.vresearch101.RELEASE.im4p",
|
|
],
|
|
"iBSS",
|
|
)
|
|
ibss_raw = os.path.join(temp_dir, "iBSS.raw")
|
|
ibss_im4p = os.path.join(temp_dir, "iBSS.im4p")
|
|
im4p_obj, data, _ = extract_to_raw(ibss_src, ibss_raw)
|
|
create_im4p_uncompressed(data, im4p_obj.fourcc, im4p_obj.description, ibss_im4p)
|
|
sign_img4(
|
|
ibss_im4p,
|
|
os.path.join(output_dir, "iBSS.vresearch101.RELEASE.img4"),
|
|
im4m_path,
|
|
)
|
|
print(f" [+] iBSS.vresearch101.RELEASE.img4")
|
|
|
|
# ── 2. iBEC (already patched — just fix boot-args for ramdisk)
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 2. iBEC (patch boot-args for ramdisk)")
|
|
print(f"{'=' * 60}")
|
|
ibec_src = find_file(
|
|
restore_dir,
|
|
[
|
|
"Firmware/dfu/iBEC.vresearch101.RELEASE.im4p",
|
|
],
|
|
"iBEC",
|
|
)
|
|
ibec_raw = os.path.join(temp_dir, "iBEC.raw")
|
|
ibec_im4p = os.path.join(temp_dir, "iBEC.im4p")
|
|
im4p_obj, data, _ = extract_to_raw(ibec_src, ibec_raw)
|
|
patch_ibec_bootargs(data)
|
|
create_im4p_uncompressed(data, im4p_obj.fourcc, im4p_obj.description, ibec_im4p)
|
|
sign_img4(
|
|
ibec_im4p,
|
|
os.path.join(output_dir, "iBEC.vresearch101.RELEASE.img4"),
|
|
im4m_path,
|
|
)
|
|
print(f" [+] iBEC.vresearch101.RELEASE.img4")
|
|
|
|
# ── 3. SPTM (sign only) ─────────────────────────────────────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 3. SPTM (sign only)")
|
|
print(f"{'=' * 60}")
|
|
sptm_src = find_file(
|
|
restore_dir,
|
|
[
|
|
"Firmware/sptm.vresearch1.release.im4p",
|
|
],
|
|
"SPTM",
|
|
)
|
|
sign_img4(
|
|
sptm_src,
|
|
os.path.join(output_dir, "sptm.vresearch1.release.img4"),
|
|
im4m_path,
|
|
tag="sptm",
|
|
)
|
|
print(f" [+] sptm.vresearch1.release.img4")
|
|
|
|
# ── 4. DeviceTree (sign only) ────────────────────────────────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 4. DeviceTree (sign only)")
|
|
print(f"{'=' * 60}")
|
|
dt_src = find_file(
|
|
restore_dir,
|
|
[
|
|
"Firmware/all_flash/DeviceTree.vphone600ap.im4p",
|
|
],
|
|
"DeviceTree",
|
|
)
|
|
sign_img4(
|
|
dt_src,
|
|
os.path.join(output_dir, "DeviceTree.vphone600ap.img4"),
|
|
im4m_path,
|
|
tag="rdtr",
|
|
)
|
|
print(f" [+] DeviceTree.vphone600ap.img4")
|
|
|
|
# ── 5. SEP (sign only) ───────────────────────────────────────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 5. SEP (sign only)")
|
|
print(f"{'=' * 60}")
|
|
sep_src = find_file(
|
|
restore_dir,
|
|
[
|
|
"Firmware/all_flash/sep-firmware.vresearch101.RELEASE.im4p",
|
|
],
|
|
"SEP",
|
|
)
|
|
sign_img4(
|
|
sep_src,
|
|
os.path.join(output_dir, "sep-firmware.vresearch101.RELEASE.img4"),
|
|
im4m_path,
|
|
tag="rsep",
|
|
)
|
|
print(f" [+] sep-firmware.vresearch101.RELEASE.img4")
|
|
|
|
# ── 6. TXM (release variant — needs patching) ────────────────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 6. TXM (patch release variant)")
|
|
print(f"{'=' * 60}")
|
|
txm_src = find_file(
|
|
restore_dir,
|
|
[
|
|
"Firmware/txm.iphoneos.release.im4p",
|
|
],
|
|
"TXM",
|
|
)
|
|
txm_raw = os.path.join(temp_dir, "txm.raw")
|
|
txm_patched_raw = os.path.join(temp_dir, "txm.patched.raw")
|
|
im4p_obj, data, _, original_raw = load_firmware(txm_src)
|
|
with open(txm_raw, "wb") as f:
|
|
f.write(bytes(data))
|
|
print(f" source: {txm_src}")
|
|
print(f" format: IM4P, {len(data)} bytes")
|
|
run_swift_patch_component("txm", txm_src, txm_patched_raw)
|
|
with open(txm_patched_raw, "rb") as f:
|
|
patched_txm = f.read()
|
|
txm_im4p = os.path.join(temp_dir, "txm.im4p")
|
|
_save_im4p_with_payp(txm_im4p, TXM_FOURCC, patched_txm, original_raw)
|
|
sign_img4(
|
|
txm_im4p, os.path.join(output_dir, "txm.img4"), im4m_path
|
|
)
|
|
print(f" [+] txm.img4")
|
|
|
|
# ── 7. Kernelcache (already patched — repack with rkrn) ──────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 7. Kernelcache (already patched — repack as rkrn)")
|
|
print(f"{'=' * 60}")
|
|
kc_src = find_file(
|
|
restore_dir,
|
|
[
|
|
"kernelcache.research.vphone600",
|
|
],
|
|
"kernelcache",
|
|
)
|
|
kc_ramdisk_src = derive_ramdisk_kernel_source(kc_src, temp_dir)
|
|
if kc_ramdisk_src:
|
|
print(f" building {RAMDISK_KERNEL_IMG4} from ramdisk kernel source")
|
|
build_kernel_img4(
|
|
kc_ramdisk_src,
|
|
output_dir,
|
|
temp_dir,
|
|
im4m_path,
|
|
RAMDISK_KERNEL_IMG4,
|
|
"kcache_ramdisk",
|
|
)
|
|
print(" building krnl.img4 from restore kernel")
|
|
|
|
build_kernel_img4(
|
|
kc_src,
|
|
output_dir,
|
|
temp_dir,
|
|
im4m_path,
|
|
"krnl.img4",
|
|
"kcache",
|
|
)
|
|
|
|
# ── 8. Ramdisk + Trustcache ──────────────────────────────────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" 8. Ramdisk + Trustcache")
|
|
print(f"{'=' * 60}")
|
|
build_ramdisk(restore_dir, im4m_path, vm_dir, input_dir, output_dir, temp_dir)
|
|
|
|
# ── Cleanup ──────────────────────────────────────────────────
|
|
print(f"\n[*] Cleaning up {TEMP_DIR}/...")
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
sshrd_dir = os.path.join(vm_dir, "SSHRD")
|
|
if os.path.exists(sshrd_dir):
|
|
shutil.rmtree(sshrd_dir, ignore_errors=True)
|
|
|
|
# ── Summary ──────────────────────────────────────────────────
|
|
print(f"\n{'=' * 60}")
|
|
print(f" Ramdisk build complete!")
|
|
print(f" Output: {output_dir}/")
|
|
print(f"{'=' * 60}")
|
|
for f in sorted(os.listdir(output_dir)):
|
|
size = os.path.getsize(os.path.join(output_dir, f))
|
|
print(f" {f:45s} {size:>10,} bytes")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|