mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
JB install: use external insert_dylib, preserve launchd entitlements, deploy pre-built dylibs
- Replace Python cfw_inject_dylib.py with tyilo/insert_dylib (built by setup_tools) - Use --weak flag for LC_LOAD_WEAK_DYLIB injection (avoids crash on missing dylib) - Preserve original launchd entitlements on re-sign (fixes "operation not permitted") - Deploy dylibs from pre-built basebin payload instead of building from source - Remove launchdhook, systemhook, treblehook sources (no longer needed) - Print GDB debug stub port after VM starts - Cleanup: remove test scripts, rename patch comparison doc
This commit is contained in:
@@ -20,7 +20,7 @@ Virtual iPhone boot tool using Apple's Virtualization.framework with PCC researc
|
||||
- If blocked or waiting on user input, write the exact blocker and next action in `/TODO.md`.
|
||||
- If not exists, continue existing work until complete. If exists, follow `/TODO.md` instructions.
|
||||
|
||||
For any changes applying new patches, also update research/00_patch_comparison_all_variants.md. Dont forget this.
|
||||
For any changes applying new patches, also update research/0_binary_patch_comparison.md. Dont forget this.
|
||||
|
||||
## Local Skills
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ CFW_INPUT ?= cfw_input
|
||||
RESTORE_UDID ?=
|
||||
RESTORE_ECID ?=
|
||||
IRECOVERY_ECID ?=
|
||||
SSH_PORT ?= 2222
|
||||
|
||||
# ─── Build info ──────────────────────────────────────────────────
|
||||
GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
@@ -48,10 +47,9 @@ help:
|
||||
@echo " SKIP_PROJECT_SETUP=1 Skip setup_tools/build"
|
||||
@echo " NONE_INTERACTIVE=1 Auto-continue prompts + boot analysis"
|
||||
@echo " SUDO_PASSWORD=... Preload sudo credential for setup flow"
|
||||
@echo " PATCH=patch_xxx Apply single JB patch test on top of dev patch"
|
||||
@echo ""
|
||||
@echo "Setup (one-time):"
|
||||
@echo " make setup_tools Install all tools (brew, trustcache, libimobiledevice, venv)"
|
||||
@echo " make setup_tools Install all tools (brew, trustcache, insert_dylib, libimobiledevice, venv)"
|
||||
@echo ""
|
||||
@echo "Build:"
|
||||
@echo " make build Build + sign vphone-cli"
|
||||
@@ -70,8 +68,6 @@ help:
|
||||
@echo " make fw_patch Patch boot chain (6 components)"
|
||||
@echo " make fw_patch_dev Patch boot chain (dev mode TXM patcher)"
|
||||
@echo " make fw_patch_jb Run fw_patch + JB extension patches (WIP)"
|
||||
@echo " make fw_patch_test PATCH=... Apply one JB kernel patch method (after fw_patch_dev)"
|
||||
@echo " make jb_patch_autotest Run setup_machine per JB patch method with logs"
|
||||
@echo ""
|
||||
@echo "Restore:"
|
||||
@echo " make restore_get_shsh Fetch SHSH blob from device"
|
||||
@@ -86,7 +82,7 @@ help:
|
||||
@echo " make cfw_install_dev Install CFW mods via SSH (dev mode)"
|
||||
@echo " make cfw_install_jb Install CFW + JB extensions (jetsam/procursus/basebin)"
|
||||
@echo ""
|
||||
@echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE) SSH_PORT=$(SSH_PORT)"
|
||||
@echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Setup
|
||||
@@ -101,7 +97,6 @@ setup_machine:
|
||||
fi
|
||||
SUDO_PASSWORD="$(SUDO_PASSWORD)" \
|
||||
NONE_INTERACTIVE="$(NONE_INTERACTIVE)" \
|
||||
PATCH="$(PATCH)" \
|
||||
zsh $(SCRIPTS)/setup_machine.sh \
|
||||
$(if $(filter 1 true yes YES TRUE,$(JB)),--jb,) \
|
||||
$(if $(filter 1 true yes YES TRUE,$(DEV)),--dev,) \
|
||||
@@ -199,7 +194,7 @@ boot_dfu: build
|
||||
# Firmware pipeline
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
.PHONY: fw_prepare fw_patch fw_patch_dev fw_patch_jb fw_patch_test jb_patch_autotest
|
||||
.PHONY: fw_prepare fw_patch fw_patch_dev fw_patch_jb
|
||||
|
||||
fw_prepare:
|
||||
cd $(VM_DIR) && bash "$(CURDIR)/$(SCRIPTS)/fw_prepare.sh"
|
||||
@@ -213,12 +208,6 @@ fw_patch_dev:
|
||||
fw_patch_jb:
|
||||
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch_jb.py" .
|
||||
|
||||
fw_patch_test:
|
||||
cd $(VM_DIR) && PATCH="$(PATCH)" $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch_test.py" .
|
||||
|
||||
jb_patch_autotest:
|
||||
zsh "$(CURDIR)/$(SCRIPTS)/jb_patch_autotest.sh"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Restore
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
@@ -257,10 +246,10 @@ ramdisk_send:
|
||||
.PHONY: cfw_install cfw_install_dev cfw_install_jb
|
||||
|
||||
cfw_install:
|
||||
cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
|
||||
cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
|
||||
|
||||
cfw_install_dev:
|
||||
cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_dev.sh" .
|
||||
cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_dev.sh" .
|
||||
|
||||
cfw_install_jb:
|
||||
cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .
|
||||
cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .
|
||||
|
||||
@@ -7,6 +7,7 @@ let package = Package(
|
||||
platforms: [
|
||||
.macOS(.v15),
|
||||
],
|
||||
products: [],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"),
|
||||
.package(url: "https://github.com/mhdhejazi/Dynamic", from: "1.2.0"),
|
||||
|
||||
@@ -26,7 +26,7 @@ Three patch variants are available with increasing levels of security bypass:
|
||||
|
||||
`66` = default JB kernel method plan; `78` = default + optional kernel methods (`VPHONE_JB_ENABLE_OPTIONAL=1`).
|
||||
|
||||
See [research/00_patch_comparison_all_variants.md](./research/00_patch_comparison_all_variants.md) for the detailed per-component breakdown.
|
||||
See [research/0_binary_patch_comparison.md](./research/0_binary_patch_comparison.md) for the detailed per-component breakdown.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -85,21 +85,19 @@ git clone --recurse-submodules https://github.com/Lakr233/vphone-cli.git
|
||||
|
||||
```bash
|
||||
make setup_machine # full automation through "First Boot" (includes restore/ramdisk/CFW)
|
||||
# options: NONE_INTERACTIVE=1 SUDO_PASSWORD=... PATCH=patch_xxx
|
||||
# options: NONE_INTERACTIVE=1 SUDO_PASSWORD=...
|
||||
```
|
||||
|
||||
## Manual Setup
|
||||
|
||||
```bash
|
||||
make setup_tools # install brew deps, build trustcache + libimobiledevice, create Python venv
|
||||
make setup_tools # install brew deps, build trustcache, clone insert_dylib, build libimobiledevice, create Python venv
|
||||
make build # build + sign vphone-cli
|
||||
make vm_new # create vm/ directory (ROMs, disk, SEP storage)
|
||||
make fw_prepare # download IPSWs, extract, merge, generate manifest
|
||||
make fw_patch # patch boot chain (regular variant)
|
||||
# or: make fw_patch_dev # dev variant (+ TXM entitlement/debug bypasses)
|
||||
# or: make fw_patch_jb # jailbreak variant (+ full security bypass) (WIP)
|
||||
# or: make fw_patch_test PATCH=patch_xxx # one JB kernel method on top of dev patch
|
||||
# or: make jb_patch_autotest # test all JB methods (single-thread, full setup flow)
|
||||
```
|
||||
|
||||
## Restore
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ Apple の Virtualization.framework と PCC の研究用 VM インフラを使用
|
||||
|
||||
`66` は JB のデフォルトカーネルパッチ計画、`78` はデフォルト + オプションカーネルパッチ(`VPHONE_JB_ENABLE_OPTIONAL=1`)です。
|
||||
|
||||
詳細なコンポーネントごとの内訳については [research/00_patch_comparison_all_variants.md](../research/00_patch_comparison_all_variants.md) を参照してください。
|
||||
詳細なコンポーネントごとの内訳については [research/0_binary_patch_comparison.md](../research/0_binary_patch_comparison.md) を参照してください。
|
||||
|
||||
## 前提条件
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ PCC 리서치 VM 인프라와 Apple의 Virtualization.framework를 사용하여
|
||||
|
||||
`66`은 JB 기본 커널 패치 플랜, `78`은 기본 + 선택 커널 패치(`VPHONE_JB_ENABLE_OPTIONAL=1`)입니다.
|
||||
|
||||
컴포넌트별 상세 분류는 [research/00_patch_comparison_all_variants.md](../research/00_patch_comparison_all_variants.md)를 참조하세요.
|
||||
컴포넌트별 상세 분류는 [research/0_binary_patch_comparison.md](../research/0_binary_patch_comparison.md)를 참조하세요.
|
||||
|
||||
## 사전 요구 사항
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
`66` 表示 JB 默认内核补丁计划;`78` 表示默认 + 可选内核补丁(`VPHONE_JB_ENABLE_OPTIONAL=1`)。
|
||||
|
||||
详见 [research/00_patch_comparison_all_variants.md](../research/00_patch_comparison_all_variants.md) 了解各组件的详细分项对比。
|
||||
详见 [research/0_binary_patch_comparison.md](../research/0_binary_patch_comparison.md) 了解各组件的详细分项对比。
|
||||
|
||||
## 先决条件
|
||||
|
||||
|
||||
+1
-46
@@ -74,8 +74,6 @@
|
||||
|
||||
### JB-Only Kernel Methods (Reference List)
|
||||
|
||||
Current default schedule note (2026-03-06): `patch_cred_label_update_execve` remains temporarily excluded from `_PATCH_METHODS` pending staged re-validation. `patch_syscallmask_apply_to_proc` has been rebuilt around the real syscallmask apply wrapper and is re-enabled after focused PCC 26.1 dry-run validation plus user-side boot confirmation; refreshed XNU/IDA review also confirms historical C22 was the all-ones-mask variant, not a `NULL`-mask install. `patch_hook_cred_label_update_execve` has also been rebuilt as a faithful upstream C23 wrapper trampoline: it retargets sandbox `mac_policy_ops[18]` to a cave that copies `VSUID`/`VSGID` owner state into the pending credential, sets `P_SUGID`, and branches back to the original wrapper. `patch_iouc_failed_macf` has been rebuilt as a narrow branch-level gate patch: the old repo-only entry early-return on `0xFFFFFE000825B0C0` was discarded, and A5-v2 now patches the post-`mac_iokit_check_open` `CBZ W0, allow` gate at `0xFFFFFE000825BA98` to unconditional allow while preserving the surrounding IOUserClient setup flow. `patch_vm_fault_enter_prepare` was retargeted to the upstream PCC 26.1 research `cs_bypass` gate and re-enabled for dry-run validation. `patch_bsd_init_auth` has been retargeted to the real `_bsd_init` rootauth failure branch and re-enabled for staged validation. Fresh IDA re-analysis shows JB-14 previously used a false-positive matcher; it now targets the real `_bsd_init` rootauth failure branch using in-function Capstone-decoded control-flow semantics and is semantically redundant with base patch #3 when JB is layered on top of `fw_patch`. For JB-16, the historical hit at `0xFFFFFE000836E1F0` is now treated as semantically wrong: it patches the `"SecureRoot"` name-check gate inside `AppleARMPE::callPlatformFunction`, not the `"SecureRootName"` deny return consumed by `IOSecureBSDRoot()`. The implementation was retargeted on 2026-03-06 to `0xFFFFFE000836E464` (`CSEL W22, WZR, W9, NE -> MOV W22, #0`) and re-enabled in `KernelJBPatcher._GROUP_B_METHODS` pending restore/boot validation.
|
||||
|
||||
| # | Group | Method | Function | Purpose | JB Enabled |
|
||||
| ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: |
|
||||
| JB-01 | A | `patch_amfi_cdhash_in_trustcache` | `AMFIIsCDHashInTrustCache` | Always return true + store hash | Y |
|
||||
@@ -104,12 +102,6 @@ Current default schedule note (2026-03-06): `patch_cred_label_update_execve` rem
|
||||
| JB-24 | B | `patch_vm_fault_enter_prepare` | `_vm_fault_enter_prepare` | Force `cs_bypass` fast path in runtime fault validation | Y |
|
||||
| JB-25 | B | `patch_vm_map_protect` | `_vm_map_protect` | Skip upstream write-downgrade gate in `vm_map_protect` | Y |
|
||||
|
||||
JB rework note (2026-03-06, remaining active methods): `JB-01`, `JB-08`, `JB-09`, `JB-06`, `JB-11`, `JB-12`, `JB-13`, `JB-17`, `JB-19`, and `JB-23` have now also been rechecked against `/Users/qaq/Desktop/patch_fw.py`, IDA PCC 26.1 research, `research/reference/xnu`, and focused dry-runs on both PCC 26.1 research/release. Of these, `JB-09` was materially pulled back to the upstream `mac_policy_ops` table-entry rewrite model (common allow stub retarget, matching `patch_fw.py` offsets) instead of per-hook body stubs; `JB-06` dropped its broad AMFI-text fallback; `JB-12` tightened to the exact early `ldr/cbz/bl/cbz` guard pair; and `JB-19` now requires a unique `krn.`-anchored verifyPermission gate across all string refs. The remaining six (`JB-01`, `JB-08`, `JB-11`, `JB-13`, `JB-17`, `JB-23`) matched upstream offsets and semantics without further retarget.
|
||||
|
||||
JB retarget note (2026-03-06): `JB-15`, `JB-18`, `JB-20`, `JB-21`, `JB-22`, and `JB-25` were rechecked against `/Users/qaq/Desktop/patch_fw.py`, IDA PCC 26.1 research, and `research/reference/xnu`. Current preferred runtime behavior is to match the known-good upstream semantic gate unless binary+source evidence clearly disproves it. In this pass, `JB-22` was pulled back from a helper-return rewrite to the upstream early `pid == 0` gate, and `JB-20` was pulled back from the later preboot-fallback compare to the upstream first root-mount compare.
|
||||
|
||||
JB-24 note (2026-03-06): the old derived matcher hit the `VM_PAGE_CONSUME_CLUSTERED()` lock/unlock sequence inside `vm_fault_enter_prepare`, i.e. `pmap_lock_phys_page()` / `pmap_unlock_phys_page()`. The implementation is now retargeted to the upstream PCC 26.1 research `cs_bypass` gate at `0x00BA9E1C` / `0xFFFFFE0007BADE1C`.
|
||||
|
||||
## CFW Installation Patches
|
||||
|
||||
### Binary Patches Applied Over SSH Ramdisk
|
||||
@@ -145,6 +137,7 @@ JB-24 note (2026-03-06): the old derived matcher hit the `VM_PAGE_CONSUME_CLUSTE
|
||||
| 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` |
|
||||
@@ -184,41 +177,3 @@ JB-24 note (2026-03-06): the old derived matcher hit the `VM_PAGE_CONSUME_CLUSTE
|
||||
| iOS 26.1 (`23B85`) | 14 | 59 |
|
||||
| iOS 26.3 (`23D127`) | 14 | 59 |
|
||||
|
||||
## Automation Notes (2026-03-06)
|
||||
|
||||
- `scripts/setup_machine.sh` non-interactive flow fix: renamed local variable `status` to `boot_state` in first-boot log wait and boot-analysis wait helpers to avoid zsh `status` read-only special parameter collision.
|
||||
- `scripts/setup_machine.sh` non-interactive first-boot wait fix: replaced `(( waited++ ))` with `(( ++waited ))` in `monitor_boot_log_until` to avoid `set -e` abort when arithmetic expression evaluates to `0`.
|
||||
- `scripts/jb_patch_autotest.sh` loop fix for sweep stability under `set -e`: replaced `((idx++))` with `(( ++idx ))`.
|
||||
- `scripts/jb_patch_autotest.sh` zsh compatibility fix: renamed per-case result variable `status` to `case_status` to avoid `status` read-only special parameter collision.
|
||||
- `scripts/jb_patch_autotest.sh` selection logic update:
|
||||
- default run now excludes methods listed in `KernelJBPatcher._DEV_SINGLE_WORKING_METHODS` (pending-only sweep).
|
||||
- set `JB_AUTOTEST_INCLUDE_WORKING=1` to include already-working methods and run the full list.
|
||||
- Sweep run record:
|
||||
- `setup_logs/jb_patch_tests_20260306_114417` (2026-03-06): aborted at `[1/20]` with `read-only variable: status` in `jb_patch_autotest.sh`.
|
||||
- `setup_logs/jb_patch_tests_20260306_115027` (2026-03-06): rerun after `status` fix, pending-only mode (`Total methods: 19`).
|
||||
- Final run result from `jb_patch_tests_20260306_115027` at `2026-03-06 13:17`:
|
||||
- Finished: 19/19 (`PASS=15`, `FAIL=4`, all fails `rc=2`).
|
||||
- Failing methods at that time: `patch_bsd_init_auth`, `patch_io_secure_bsd_root`, `patch_vm_fault_enter_prepare`, `patch_cred_label_update_execve`.
|
||||
- 2026-03-06 follow-up: `patch_io_secure_bsd_root` failure is now attributed to a wrong-site patch in `AppleARMPE::callPlatformFunction` (`"SecureRoot"` gate at `0xFFFFFE000836E1F0`), not the intended `"SecureRootName"` deny-return path. The code was retargeted the same day to `0xFFFFFE000836E464` and re-enabled for the next restore/boot check.
|
||||
- 2026-03-06 follow-up: `patch_bsd_init_auth` was retargeted after confirming the old matcher was hitting unrelated code; keep disabled in default schedule until a fresh clean-baseline boot test passes.
|
||||
- Final case: `[19/19] patch_syscallmask_apply_to_proc` (`PASS`).
|
||||
- 2026-03-06 re-analysis: that historical `PASS` is now treated as a false positive for functionality, because the recorded bytes landed at `0xfffffe00093ae6e4`/`0xfffffe00093ae6e8` inside `_profile_syscallmask_destroy` underflow handling, not in `_proc_apply_syscall_masks`.
|
||||
- 2026-03-06 code update: `scripts/patchers/kernel_jb_patch_syscallmask.py` was rebuilt to target the real syscallmask apply wrapper structurally and now dry-runs on `PCC-CloudOS-26.1-23B85 kernelcache.research.vphone600` with 3 writes: `0x02395530`, `0x023955E8`, and cave `0x00AB1720`. User-side boot validation succeeded the same day.
|
||||
- 2026-03-06 follow-up: `patch_kcall10` was rebuilt from the old ABI-unsafe pseudo-10-arg design into an ABI-correct `sysent[439]` cave. Focused dry-run on `PCC-CloudOS-26.1-23B85 kernelcache.research.vphone600` now emits 4 writes: cave `0x00AB1720`, `sy_call` `0x0073E180`, `sy_arg_munge32` `0x0073E188`, and metadata `0x0073E190`; the method was re-enabled in `_GROUP_C_METHODS`.
|
||||
- Observed failure symptom in current failing set: first boot panic before command injection (or boot process early exit).
|
||||
- Post-run schedule change (per user request):
|
||||
- commented out failing methods from default `KernelJBPatcher._PATCH_METHODS` schedule in `scripts/patchers/kernel_jb.py`:
|
||||
- `patch_bsd_init_auth`
|
||||
- `patch_io_secure_bsd_root`
|
||||
- `patch_vm_fault_enter_prepare`
|
||||
- `patch_cred_label_update_execve`
|
||||
- 2026-03-06 re-research note for `patch_cred_label_update_execve`:
|
||||
- old entry-time early-return strategy was identified as boot-unsafe because it skipped AMFI exec-time `csflags` and entitlement propagation entirely.
|
||||
- implementation was reworked to a success-tail trampoline that preserves normal AMFI processing and only clears restrictive `csflags` bits on the success path.
|
||||
- default JB schedule still keeps the method disabled until the reworked strategy is boot-validated.
|
||||
- Manual DEV+single (`setup_machine` + `PATCH=<method>`) working set now includes:
|
||||
- `patch_amfi_cdhash_in_trustcache`
|
||||
- `patch_amfi_execve_kill_path`
|
||||
- `patch_task_conversion_eval_internal`
|
||||
- `patch_sandbox_hooks_extended`
|
||||
- `patch_post_validation_additional`
|
||||
@@ -51,7 +51,7 @@ Patch action per entry remains:
|
||||
Updated:
|
||||
|
||||
- `research/kernel_patch_jb/patch_sandbox_hooks_extended.md`
|
||||
- `research/00_patch_comparison_all_variants.md`
|
||||
- `research/0_binary_patch_comparison.md`
|
||||
|
||||
## Local Validation (static)
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ The triage results from those knobs are preserved in this document and in:
|
||||
|
||||
- `vm/ab_matrix_b19_mnt_20260305_034127.csv`
|
||||
- `TODO.md` (Boot Hang Research + Progress Update sections)
|
||||
- `research/00_patch_comparison_all_variants.md` (Kernelcache section)
|
||||
- `research/0_binary_patch_comparison.md` (Kernelcache section)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# Launchdhook Assertion Handoff (2026-03-06)
|
||||
|
||||
## Scope
|
||||
|
||||
This note captures the current userspace-side findings for the failing `fw_patch_jb + cfw_install_jb` path.
|
||||
It is intended as a handoff artifact for follow-up work on the `fix-boot` branch.
|
||||
|
||||
The current symptom is no longer "launchd does not start".
|
||||
The updated symptom is:
|
||||
|
||||
- `launchd` starts
|
||||
- injected `launchdhook.dylib` definitely loads
|
||||
- `launchd` then hits an early internal assertion before the expected `bash` / follow-on job chain stabilizes
|
||||
|
||||
## Executive Summary
|
||||
|
||||
### Confirmed
|
||||
|
||||
- The original JB `LC_LOAD_DYLIB /cores/launchdhook.dylib` approach is structurally unsafe on the current `launchd` sample because there is not enough load-command slack.
|
||||
- A short-path alias experiment fixed the Mach-O header-space problem:
|
||||
- `/cores/launchdhook.dylib` requires 56 bytes and overruns into `__TEXT,__text`
|
||||
- `/cores/b` still requires 40 bytes and also overruns
|
||||
- `/b` requires 32 bytes and fits exactly after removing `LC_CODE_SIGNATURE`
|
||||
- Runtime test with `/b` proves the short-path alias loads successfully, but the main failure remains:
|
||||
- `launchdhook.dylib` prints its startup logs
|
||||
- `launchd` then asserts early: `launchd + 59944 ... 0xffffffffffffffff`
|
||||
|
||||
### Current conclusion
|
||||
|
||||
The short-path `/b` alias fixes the **injection-space** problem, but does **not** fix the **launchd assertion**.
|
||||
So the remaining problem is now more likely in the hook logic (especially early XPC / daemon config hooks) than in the raw load-command insertion path.
|
||||
|
||||
## Evidence Collected
|
||||
|
||||
### 1. Mach-O injection space audit
|
||||
|
||||
Local dry-run against `vm/.cfw_temp/launchd` established the following:
|
||||
|
||||
- Existing load-command slack before the first section: 16 bytes
|
||||
- After stripping `LC_CODE_SIGNATURE`: 32 bytes
|
||||
- Required command sizes:
|
||||
- `/cores/launchdhook.dylib` -> 56 bytes
|
||||
- `/cores/b` -> 40 bytes
|
||||
- `/b` -> 32 bytes
|
||||
|
||||
Observed effect of the original long-path injection:
|
||||
|
||||
- `LC_LOAD_DYLIB /cores/launchdhook.dylib` overwrote the beginning of `__TEXT,__text`
|
||||
- first instructions at the start of the text section were replaced by injected path bytes
|
||||
|
||||
Observed effect of the short-path injection:
|
||||
|
||||
- `LC_LOAD_DYLIB /b` fits exactly in the available 32 bytes after `LC_CODE_SIGNATURE` removal
|
||||
- no additional overwrite into `__TEXT,__text` is needed for that path
|
||||
|
||||
### 2. Device-side mount and payload verification
|
||||
|
||||
Inside ramdisk shell, manual mount and inspection showed:
|
||||
|
||||
- `/dev/disk1s1` mounted at `/mnt1`
|
||||
- `/dev/disk1s5` mounted at `/mnt5`
|
||||
- `/mnt1/b` exists and is a Mach-O dylib
|
||||
- `/mnt1/cores/launchdhook.dylib` exists and is a Mach-O dylib
|
||||
- `/mnt1/cores/systemhook.dylib` and `/mnt1/cores/libellekit.dylib` are also present
|
||||
|
||||
Important clarification:
|
||||
|
||||
- `/.b` is an existing hidden root directory on this filesystem and is unrelated to the alias experiment
|
||||
- the experiment path is `/b`, not `/.b`
|
||||
|
||||
### 3. Runtime serial log after switching to `/b`
|
||||
|
||||
The following lines appeared during boot:
|
||||
|
||||
- `set JB_ROOT_PATH = /private/preboot/<hash>/jb-vphone/procursus`
|
||||
- `=========== hello from launchdhook.dylib ===========`
|
||||
- `=========== bye from launchdhook.dylib ===========`
|
||||
- `com.apple.xpc.launchd ... assertion failed: ... launchd + 59944 ... 0xffffffffffffffff`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- `/b` injection is working
|
||||
- `launchdhook.dylib` is loaded and runs its initializer path
|
||||
- the failure is no longer attributable to the long path not loading or to the Mach-O injection missing outright
|
||||
|
||||
## Source-Backed Analysis from Dopamine BaseBin
|
||||
|
||||
Source tree used:
|
||||
|
||||
- `/Users/qaq/Documents/GitHub/Dopamine/BaseBin`
|
||||
|
||||
### 1. launchdhook initialization order
|
||||
|
||||
From `Dopamine/BaseBin/launchdhook/src/main.m`, the constructor initializes hooks in this order:
|
||||
|
||||
1. `initXPCHooks();`
|
||||
2. `initDaemonHooks();`
|
||||
3. `initSpawnHooks();`
|
||||
4. `initIPCHooks();`
|
||||
5. `initJetsamHook();`
|
||||
|
||||
This matters because the current assertion happens very early, after `launchdhook` has definitely run.
|
||||
That makes the earlier hooks higher-priority suspects than spawn-time behavior.
|
||||
|
||||
### 2. What `initDaemonHooks()` actually does
|
||||
|
||||
From `Dopamine/BaseBin/launchdhook/src/daemon_hook.m`:
|
||||
|
||||
- hooks `xpc_dictionary_get_value`
|
||||
- rewrites behavior for these keys:
|
||||
- `LaunchDaemons`
|
||||
- `Paths`
|
||||
- `com.apple.private.xpc.launchd.userspace-reboot`
|
||||
|
||||
Behavior summary:
|
||||
|
||||
- appends jailbreak daemon plist entries from:
|
||||
- `JBROOT_PATH("/basebin/LaunchDaemons")`
|
||||
- `JBROOT_PATH("/Library/LaunchDaemons")`
|
||||
- appends those same directories to `Paths`
|
||||
- conditionally returns `com.apple.private.iowatchdog.user-access` when `userspace-reboot` is false/missing
|
||||
|
||||
This hook touches exactly the kind of launchd configuration objects that are consulted during early daemon/bootstrap setup.
|
||||
|
||||
### 3. What `initSpawnHooks()` actually does
|
||||
|
||||
From `Dopamine/BaseBin/launchdhook/src/spawn_hook.c`:
|
||||
|
||||
- hooks `__posix_spawn`
|
||||
- during early boot, it intentionally avoids broad injection until `xpcproxy` appears
|
||||
- once `xpcproxy` is seen, it flips out of early-boot mode and uses `posix_spawn_hook_shared(...)`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- spawn hook is real, but it is comparatively later than the daemon config hook
|
||||
- given the current assertion timing, `initSpawnHooks()` is no longer the top suspect
|
||||
|
||||
### 4. What `initXPCHooks()` does
|
||||
|
||||
From `Dopamine/BaseBin/launchdhook/src/xpc_hook.c`:
|
||||
|
||||
- hooks `xpc_receive_mach_msg`
|
||||
- participates in jbserver message handling and filtering inside launchd/XPC path
|
||||
|
||||
This is also an early-launchd hook and remains a second-tier suspect if daemon-hook isolation does not clear the assertion.
|
||||
|
||||
### 5. Runtime jetsam hook vs our static jetsam patch
|
||||
|
||||
From `Dopamine/BaseBin/launchdhook/src/jetsam_hook.c`:
|
||||
|
||||
- Dopamine also installs a runtime hook on `memorystatus_control`
|
||||
- this is separate from the repo's static `scripts/patchers/cfw_patch_jetsam.py` binary patch
|
||||
|
||||
Therefore two different "jetsam" mechanisms now exist in the failing path:
|
||||
|
||||
- static launchd branch patch
|
||||
- runtime `memorystatus_control` hook
|
||||
|
||||
This does not prove either is the current cause, but it means the term "jetsam patch" must be disambiguated in future debugging.
|
||||
|
||||
## Current Suspect Ranking
|
||||
|
||||
### Highest probability
|
||||
|
||||
1. **`initDaemonHooks()` / `daemon_hook.m`**
|
||||
- hooks `xpc_dictionary_get_value`
|
||||
- mutates `LaunchDaemons` and `Paths`
|
||||
- timing matches the observed early `launchd` assertion better than spawn-time logic
|
||||
|
||||
### Medium probability
|
||||
|
||||
2. **`initXPCHooks()` / `xpc_hook.c`**
|
||||
- also runs before spawn hook
|
||||
- directly changes launchd/XPC message handling
|
||||
|
||||
3. **static `patch-launchd-jetsam` matcher**
|
||||
- still considered risky because its matching strategy is heuristic and not CFG-constrained
|
||||
- but the `/b` experiment shows the assertion survives after fixing the obvious load-command overflow issue
|
||||
|
||||
### Lower probability for the current symptom timing
|
||||
|
||||
4. **`initSpawnHooks()` / `spawn_hook.c`**
|
||||
- still relevant for later `bash` / job launch failures
|
||||
- but no longer the best first suspect for the early `launchd + 59944` assertion
|
||||
|
||||
## Recommended Isolation Order for `fix-boot`
|
||||
|
||||
### Stage 1: no-daemon-hook control
|
||||
|
||||
Goal:
|
||||
|
||||
- keep `launchdhook.dylib` loading
|
||||
- keep `/b` short-path alias experiment in place
|
||||
- disable only `initDaemonHooks()`
|
||||
|
||||
Reason:
|
||||
|
||||
- this is the cleanest test of the current top suspect
|
||||
- if the assertion disappears, the root issue is inside `daemon_hook.m`
|
||||
|
||||
### Stage 2: no-xpc-hook control
|
||||
|
||||
If stage 1 still asserts:
|
||||
|
||||
- restore daemon hook or keep it off, but disable `initXPCHooks()` next
|
||||
- test whether the assertion is tied to XPC receive hook path instead
|
||||
|
||||
### Stage 3: no-spawn-hook control
|
||||
|
||||
Only after stages 1 and 2:
|
||||
|
||||
- disable `initSpawnHooks()`
|
||||
- use this to isolate later `bash` / child-process failures if the launchd assertion is already gone or moves later
|
||||
|
||||
### Stage 4: revisit static launchd jetsam patch
|
||||
|
||||
If all runtime-hook controls still fail:
|
||||
|
||||
- re-audit `scripts/patchers/cfw_patch_jetsam.py`
|
||||
- prefer a source-backed or CFG-backed site selection instead of the current backward-scan heuristic
|
||||
|
||||
## Concrete Handoff Notes for Claude
|
||||
|
||||
### Facts
|
||||
|
||||
- `/b` injection is confirmed working on-device
|
||||
- `launchdhook.dylib` definitely runs
|
||||
- launchd still asserts at `launchd + 59944`
|
||||
- Dopamine source confirms `initDaemonHooks()` runs before `initSpawnHooks()`
|
||||
|
||||
### Inference
|
||||
|
||||
- the early assertion is more likely to be caused by `daemon_hook.m` or `xpc_hook.c` than by `spawn_hook.c`
|
||||
|
||||
### Best next change
|
||||
|
||||
Implement a **minimal no-daemon-hook build** first:
|
||||
|
||||
- edit `Dopamine/BaseBin/launchdhook/src/main.m`
|
||||
- temporarily disable only `initDaemonHooks();`
|
||||
- rebuild `launchdhook.dylib`
|
||||
- keep `/b` alias loading strategy unchanged for the control run
|
||||
|
||||
## Related Files
|
||||
|
||||
- `scripts/cfw_install_jb.sh`
|
||||
- `scripts/patchers/cfw_inject_dylib.py`
|
||||
- `scripts/patchers/cfw_patch_jetsam.py`
|
||||
- `research/boot_jb_mount_failure_investigation.md`
|
||||
- `research/boot_hang_b19_mount_dounmount_strategy_compare.md`
|
||||
- `Dopamine/BaseBin/launchdhook/src/main.m`
|
||||
- `Dopamine/BaseBin/launchdhook/src/daemon_hook.m`
|
||||
- `Dopamine/BaseBin/launchdhook/src/spawn_hook.c`
|
||||
- `Dopamine/BaseBin/launchdhook/src/xpc_hook.c`
|
||||
@@ -132,7 +132,7 @@ Observed output:
|
||||
|
||||
- `scripts/patchers/kernel_jb_patch_hook_cred_label.py` now implements faithful upstream C23 semantics
|
||||
- `scripts/patchers/kernel_jb.py` includes `patch_hook_cred_label_update_execve` in the active Group C schedule
|
||||
- `research/00_patch_comparison_all_variants.md` should describe C23 as a faithful wrapper trampoline, not as a mis-targeted early-return patch
|
||||
- `research/0_binary_patch_comparison.md` should describe C23 as a faithful wrapper trampoline, not as a mis-targeted early-return patch
|
||||
|
||||
## Practical Effect
|
||||
|
||||
|
||||
+72
-56
@@ -198,8 +198,8 @@ check_prereqs() {
|
||||
|
||||
# ── Cleanup trap (unmount DMGs on error) ───────────────────────
|
||||
cleanup_on_exit() {
|
||||
safe_detach "$TEMP_DIR/mnt_sysos"
|
||||
safe_detach "$TEMP_DIR/mnt_appos"
|
||||
safe_detach "$TEMP_DIR/mnt_sysos" 2>/dev/null || true
|
||||
safe_detach "$TEMP_DIR/mnt_appos" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup_on_exit EXIT
|
||||
|
||||
@@ -234,42 +234,7 @@ echo " AppOS: $CRYPTEX_APPOS"
|
||||
echo ""
|
||||
echo "[1/7] Installing Cryptex (SystemOS + AppOS)..."
|
||||
|
||||
SYSOS_DMG="$TEMP_DIR/CryptexSystemOS.dmg"
|
||||
APPOS_DMG="$TEMP_DIR/CryptexAppOS.dmg"
|
||||
MNT_SYSOS="$TEMP_DIR/mnt_sysos"
|
||||
MNT_APPOS="$TEMP_DIR/mnt_appos"
|
||||
|
||||
# Decrypt SystemOS AEA (cached — skip if already decrypted)
|
||||
if [[ ! -f "$SYSOS_DMG" ]]; then
|
||||
echo " Extracting AEA key..."
|
||||
AEA_KEY=$(ipsw fw aea --key "$RESTORE_DIR/$CRYPTEX_SYSOS")
|
||||
echo " key: $AEA_KEY"
|
||||
echo " Decrypting SystemOS..."
|
||||
aea decrypt -i "$RESTORE_DIR/$CRYPTEX_SYSOS" -o "$SYSOS_DMG" -key-value "$AEA_KEY"
|
||||
else
|
||||
echo " Using cached SystemOS DMG"
|
||||
fi
|
||||
|
||||
# Copy AppOS (unencrypted, cached)
|
||||
if [[ ! -f "$APPOS_DMG" ]]; then
|
||||
cp "$RESTORE_DIR/$CRYPTEX_APPOS" "$APPOS_DMG"
|
||||
else
|
||||
echo " Using cached AppOS DMG"
|
||||
fi
|
||||
|
||||
# Detach any leftover mounts from previous runs
|
||||
safe_detach "$MNT_SYSOS"
|
||||
safe_detach "$MNT_APPOS"
|
||||
mkdir -p "$MNT_SYSOS" "$MNT_APPOS"
|
||||
assert_mount_under_vm "$MNT_SYSOS" "SystemOS mountpoint"
|
||||
assert_mount_under_vm "$MNT_APPOS" "AppOS mountpoint"
|
||||
|
||||
echo " Mounting SystemOS..."
|
||||
sudo hdiutil attach -mountpoint "$MNT_SYSOS" "$SYSOS_DMG" -nobrowse -owners off
|
||||
echo " Mounting AppOS..."
|
||||
sudo hdiutil attach -mountpoint "$MNT_APPOS" "$APPOS_DMG" -nobrowse -owners off
|
||||
|
||||
# Mount device rootfs (tolerate already-mounted)
|
||||
# Mount device rootfs first to check existing state
|
||||
echo " Mounting device rootfs rw..."
|
||||
remote_mount /dev/disk1s1 /mnt1
|
||||
|
||||
@@ -296,28 +261,79 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
ssh_cmd "/bin/rm -rf /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
|
||||
ssh_cmd "/bin/mkdir -p /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
|
||||
# Check if Cryptexes already exist on device (skip the slow copy if so)
|
||||
CRYPTEX_OS_COUNT=$(ssh_cmd "/bin/ls /mnt1/System/Cryptexes/OS/ 2>/dev/null | /usr/bin/wc -l" | tr -d ' ')
|
||||
CRYPTEX_APP_COUNT=$(ssh_cmd "/bin/ls /mnt1/System/Cryptexes/App/ 2>/dev/null | /usr/bin/wc -l" | tr -d ' ')
|
||||
|
||||
# Copy Cryptex files to device
|
||||
echo " Copying Cryptexes to device (this takes ~3 minutes)..."
|
||||
scp_to "$MNT_SYSOS/." "/mnt1/System/Cryptexes/OS"
|
||||
scp_to "$MNT_APPOS/." "/mnt1/System/Cryptexes/App"
|
||||
if [[ "${CRYPTEX_OS_COUNT:-0}" -gt 0 && "${CRYPTEX_APP_COUNT:-0}" -gt 0 ]]; then
|
||||
echo " [*] Cryptexes already installed on device (OS=${CRYPTEX_OS_COUNT} entries, App=${CRYPTEX_APP_COUNT} entries), skipping"
|
||||
|
||||
# Create dyld symlinks (ln -sf is idempotent)
|
||||
echo " Creating dyld symlinks..."
|
||||
ssh_cmd "/bin/ln -sf ../../../System/Cryptexes/OS/System/Library/Caches/com.apple.dyld \
|
||||
/mnt1/System/Library/Caches/com.apple.dyld"
|
||||
ssh_cmd "/bin/ln -sf ../../../../System/Cryptexes/OS/System/DriverKit/System/Library/dyld \
|
||||
/mnt1/System/DriverKit/System/Library/dyld"
|
||||
# Still ensure dyld symlinks exist
|
||||
ssh_cmd "/bin/ln -sf ../../../System/Cryptexes/OS/System/Library/Caches/com.apple.dyld \
|
||||
/mnt1/System/Library/Caches/com.apple.dyld"
|
||||
ssh_cmd "/bin/ln -sf ../../../../System/Cryptexes/OS/System/DriverKit/System/Library/dyld \
|
||||
/mnt1/System/DriverKit/System/Library/dyld"
|
||||
|
||||
# Unmount Cryptex DMGs
|
||||
echo " Unmounting Cryptex DMGs..."
|
||||
safe_detach "$MNT_SYSOS"
|
||||
safe_detach "$MNT_APPOS"
|
||||
echo " [+] Cryptex skipped (already present)"
|
||||
else
|
||||
SYSOS_DMG="$TEMP_DIR/CryptexSystemOS.dmg"
|
||||
APPOS_DMG="$TEMP_DIR/CryptexAppOS.dmg"
|
||||
MNT_SYSOS="$TEMP_DIR/mnt_sysos"
|
||||
MNT_APPOS="$TEMP_DIR/mnt_appos"
|
||||
|
||||
echo " [+] Cryptex installed"
|
||||
# Decrypt SystemOS AEA (cached — skip if already decrypted)
|
||||
if [[ ! -f "$SYSOS_DMG" ]]; then
|
||||
echo " Extracting AEA key..."
|
||||
AEA_KEY=$(ipsw fw aea --key "$RESTORE_DIR/$CRYPTEX_SYSOS")
|
||||
echo " key: $AEA_KEY"
|
||||
echo " Decrypting SystemOS..."
|
||||
aea decrypt -i "$RESTORE_DIR/$CRYPTEX_SYSOS" -o "$SYSOS_DMG" -key-value "$AEA_KEY"
|
||||
else
|
||||
echo " Using cached SystemOS DMG"
|
||||
fi
|
||||
|
||||
# Copy AppOS (unencrypted, cached)
|
||||
if [[ ! -f "$APPOS_DMG" ]]; then
|
||||
cp "$RESTORE_DIR/$CRYPTEX_APPOS" "$APPOS_DMG"
|
||||
else
|
||||
echo " Using cached AppOS DMG"
|
||||
fi
|
||||
|
||||
# Detach any leftover mounts from previous runs
|
||||
safe_detach "$MNT_SYSOS"
|
||||
safe_detach "$MNT_APPOS"
|
||||
mkdir -p "$MNT_SYSOS" "$MNT_APPOS"
|
||||
assert_mount_under_vm "$MNT_SYSOS" "SystemOS mountpoint"
|
||||
assert_mount_under_vm "$MNT_APPOS" "AppOS mountpoint"
|
||||
|
||||
echo " Mounting SystemOS..."
|
||||
sudo hdiutil attach -mountpoint "$MNT_SYSOS" "$SYSOS_DMG" -nobrowse -owners off
|
||||
echo " Mounting AppOS..."
|
||||
sudo hdiutil attach -mountpoint "$MNT_APPOS" "$APPOS_DMG" -nobrowse -owners off
|
||||
|
||||
ssh_cmd "/bin/rm -rf /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
|
||||
ssh_cmd "/bin/mkdir -p /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS"
|
||||
|
||||
# Copy Cryptex files to device
|
||||
echo " Copying Cryptexes to device (this takes ~3 minutes)..."
|
||||
scp_to "$MNT_SYSOS/." "/mnt1/System/Cryptexes/OS"
|
||||
scp_to "$MNT_APPOS/." "/mnt1/System/Cryptexes/App"
|
||||
|
||||
# Create dyld symlinks (ln -sf is idempotent)
|
||||
echo " Creating dyld symlinks..."
|
||||
ssh_cmd "/bin/ln -sf ../../../System/Cryptexes/OS/System/Library/Caches/com.apple.dyld \
|
||||
/mnt1/System/Library/Caches/com.apple.dyld"
|
||||
ssh_cmd "/bin/ln -sf ../../../../System/Cryptexes/OS/System/DriverKit/System/Library/dyld \
|
||||
/mnt1/System/DriverKit/System/Library/dyld"
|
||||
|
||||
# Unmount Cryptex DMGs
|
||||
echo " Unmounting Cryptex DMGs..."
|
||||
safe_detach "$MNT_SYSOS"
|
||||
safe_detach "$MNT_APPOS"
|
||||
|
||||
echo " [+] Cryptex installed"
|
||||
fi
|
||||
|
||||
# ═══════════ 2/7 PATCH SEPUTIL ════════════════════════════════
|
||||
echo ""
|
||||
|
||||
+46
-18
@@ -159,9 +159,18 @@ fi
|
||||
|
||||
scp_from "/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
|
||||
|
||||
# Inject launchdhook via short root alias to avoid Mach-O header overflow.
|
||||
# Keep the full /cores/launchdhook.dylib copy on disk for compatibility, but
|
||||
# load /b from launchd because this launchd sample only has room for a 32-byte
|
||||
# load /b from launchd because this launchd sample only has room for a short
|
||||
# LC_LOAD_DYLIB command after stripping LC_CODE_SIGNATURE.
|
||||
if [[ -d "$JB_INPUT_DIR/basebin" ]]; then
|
||||
echo " Injecting LC_LOAD_DYLIB for /b (short launchdhook alias)..."
|
||||
@@ -169,7 +178,13 @@ if [[ -d "$JB_INPUT_DIR/basebin" ]]; then
|
||||
fi
|
||||
|
||||
python3 "$SCRIPT_DIR/patchers/cfw.py" patch-launchd-jetsam "$TEMP_DIR/launchd"
|
||||
ldid_sign "$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
|
||||
scp_to "$TEMP_DIR/launchd" "/mnt1/sbin/launchd"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt1/sbin/launchd"
|
||||
|
||||
@@ -196,45 +211,58 @@ if [[ -f "$SILEO_DEB" ]]; then
|
||||
scp_to "$SILEO_DEB" "/mnt5/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb"
|
||||
fi
|
||||
|
||||
ssh_cmd "/bin/mkdir -p /mnt5/$BOOT_HASH/jb-vphone"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt5/$BOOT_HASH/jb-vphone"
|
||||
ssh_cmd "/usr/sbin/chown 0:0 /mnt5/$BOOT_HASH/jb-vphone"
|
||||
ssh_cmd "/usr/bin/tar --preserve-permissions -xkf /mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar \
|
||||
-C /mnt5/$BOOT_HASH/jb-vphone/"
|
||||
ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/jb-vphone/var /mnt5/$BOOT_HASH/jb-vphone/procursus"
|
||||
ssh_cmd "/bin/mkdir -p /mnt5/$BOOT_HASH/jb-vphone/procursus"
|
||||
ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/jb-vphone/procursus/jb/* /mnt5/$BOOT_HASH/jb-vphone/procursus 2>/dev/null || true"
|
||||
ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/jb-vphone/procursus/jb"
|
||||
JB_DIR_NAME="jb-vphone"
|
||||
ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/jb"
|
||||
ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/$JB_DIR_NAME"
|
||||
ssh_cmd "/bin/mkdir -p /mnt5/$BOOT_HASH/$JB_DIR_NAME"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt5/$BOOT_HASH/$JB_DIR_NAME"
|
||||
ssh_cmd "/usr/sbin/chown 0:0 /mnt5/$BOOT_HASH/$JB_DIR_NAME"
|
||||
ssh_cmd "/usr/bin/tar --preserve-permissions -xf /mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar \
|
||||
-C /mnt5/$BOOT_HASH/$JB_DIR_NAME/"
|
||||
ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/$JB_DIR_NAME/var /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus"
|
||||
ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus/jb/* /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus 2>/dev/null || true"
|
||||
ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus/jb"
|
||||
ssh_cmd "/bin/rm -f /mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar"
|
||||
rm -f "$BOOTSTRAP_TAR"
|
||||
|
||||
# NOTE: /var/jb symlink is created at runtime by launchdhook.dylib
|
||||
# (Data volume is encrypted and not mountable from ramdisk).
|
||||
|
||||
echo " [+] procursus bootstrap installed"
|
||||
|
||||
# ═══════════ JB-3 DEPLOY BASEBIN HOOKS ═════════════════════════
|
||||
BASEBIN_DIR="$JB_INPUT_DIR/basebin"
|
||||
|
||||
if [[ -d "$BASEBIN_DIR" ]]; then
|
||||
echo ""
|
||||
echo "[JB-3] Deploying BaseBin hooks to /cores/..."
|
||||
|
||||
# Clean previous dylibs before re-uploading
|
||||
echo " Cleaning old /cores/ dylibs..."
|
||||
ssh_cmd "/bin/rm -rf /mnt1/cores"
|
||||
ssh_cmd "/bin/mkdir -p /mnt1/cores"
|
||||
ssh_cmd "/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..."
|
||||
# Re-sign with our certificate before deploying
|
||||
ldid_sign "$dylib"
|
||||
scp_to "$dylib" "/mnt1/cores/$dylib_name"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt1/cores/$dylib_name"
|
||||
|
||||
if [[ "$dylib_name" == "launchdhook.dylib" ]]; then
|
||||
echo " Installing short launchdhook alias at /b..."
|
||||
scp_to "$dylib" "/mnt1/b"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt1/b"
|
||||
fi
|
||||
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"
|
||||
ssh_cmd "/bin/rm -f /mnt1/b"
|
||||
scp_to "$TEMP_DIR/b" "/mnt1/b"
|
||||
ssh_cmd "/bin/chmod 0755 /mnt1/b"
|
||||
fi
|
||||
|
||||
echo " [+] BaseBin hooks deployed"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
fw_patch_test.py — apply a single JB kernel patch method onto a dev-patched image.
|
||||
|
||||
Usage:
|
||||
PATCH=patch_xxx python3 fw_patch_test.py [vm_directory]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fw_patch import find_file, find_restore_dir, load_firmware, save_firmware
|
||||
from patchers.kernel_jb import KernelJBPatcher
|
||||
|
||||
|
||||
def _build_single_patch_plan(patcher, method_name):
|
||||
all_methods = getattr(KernelJBPatcher, "_PATCH_METHODS", ())
|
||||
if method_name not in all_methods:
|
||||
available = "\n".join(f" - {name}" for name in all_methods)
|
||||
raise ValueError(
|
||||
f"Unknown JB patch method: {method_name}\nAvailable methods:\n{available}"
|
||||
)
|
||||
if not callable(getattr(patcher, method_name, None)):
|
||||
raise ValueError(f"Method is not callable on patcher: {method_name}")
|
||||
return (method_name,)
|
||||
|
||||
|
||||
def patch_kernelcache_single(data, method_name):
|
||||
patcher = KernelJBPatcher(data)
|
||||
plan = _build_single_patch_plan(patcher, method_name)
|
||||
original_plan = patcher._PATCH_METHODS
|
||||
patcher._PATCH_METHODS = plan
|
||||
try:
|
||||
patches = list(patcher.find_all())
|
||||
finally:
|
||||
patcher._PATCH_METHODS = original_plan
|
||||
|
||||
if not patches:
|
||||
print(f" [-] No patches emitted by method: {method_name}")
|
||||
return False
|
||||
|
||||
for off, patch_bytes, _ in patches:
|
||||
data[off : off + len(patch_bytes)] = patch_bytes
|
||||
|
||||
print(f" [+] {len(patches)} patch(es) emitted by {method_name}")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
method_name = os.environ.get("PATCH", "").strip()
|
||||
if not method_name:
|
||||
print("[-] PATCH environment variable is required (example: PATCH=<jb_patch_method>)")
|
||||
sys.exit(1)
|
||||
|
||||
vm_dir = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
|
||||
vm_dir = os.path.abspath(vm_dir)
|
||||
if not os.path.isdir(vm_dir):
|
||||
print(f"[-] Not a directory: {vm_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
restore_dir = find_restore_dir(vm_dir)
|
||||
if not restore_dir:
|
||||
print(f"[-] No *Restore* directory found in {vm_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
kernel_path = find_file(restore_dir, ["kernelcache.research.vphone600"], "kernelcache")
|
||||
|
||||
print(f"[*] VM directory: {vm_dir}")
|
||||
print(f"[*] Restore directory: {restore_dir}")
|
||||
print(f"[*] Testing JB method: {method_name}")
|
||||
print(f"[*] Target file: {kernel_path}")
|
||||
|
||||
im4p, data, was_im4p, original_raw = load_firmware(kernel_path)
|
||||
if not patch_kernelcache_single(data, method_name):
|
||||
sys.exit(1)
|
||||
|
||||
save_firmware(kernel_path, im4p, data, was_im4p, original_raw)
|
||||
print("[+] Single JB patch test applied successfully")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/bin/zsh
|
||||
# jb_patch_autotest.sh — run full setup_machine flow for each JB kernel patch method.
|
||||
# Strategy: apply each single JB kernel method on top of the dev baseline, one case at a time.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
LOG_ROOT="${PROJECT_ROOT}/setup_logs/jb_patch_tests_$(date +%Y%m%d_%H%M%S)"
|
||||
SUMMARY_CSV="${LOG_ROOT}/summary.csv"
|
||||
MASTER_LOG="${LOG_ROOT}/run.log"
|
||||
INCLUDE_WORKING="${JB_AUTOTEST_INCLUDE_WORKING:-0}"
|
||||
|
||||
mkdir -p "$LOG_ROOT"
|
||||
touch "$MASTER_LOG"
|
||||
|
||||
if [[ -x "${PROJECT_ROOT}/.venv/bin/python3" ]]; then
|
||||
PYTHON_BIN="${PROJECT_ROOT}/.venv/bin/python3"
|
||||
else
|
||||
PYTHON_BIN="$(command -v python3)"
|
||||
fi
|
||||
|
||||
PATCH_METHODS=("${(@f)$(
|
||||
cd "${PROJECT_ROOT}/scripts" && "$PYTHON_BIN" - <<'PY'
|
||||
import os
|
||||
from patchers.kernel_jb import KernelJBPatcher
|
||||
|
||||
def _env_enabled(name, default=False):
|
||||
raw = os.environ.get(name)
|
||||
if raw is None:
|
||||
return default
|
||||
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
include_working = _env_enabled("JB_AUTOTEST_INCLUDE_WORKING", default=False)
|
||||
all_methods = list(getattr(KernelJBPatcher, "_PATCH_METHODS", ()))
|
||||
if include_working:
|
||||
selected_methods = all_methods
|
||||
else:
|
||||
working = set(getattr(KernelJBPatcher, "_DEV_SINGLE_WORKING_METHODS", ()))
|
||||
selected_methods = [m for m in all_methods if m not in working]
|
||||
|
||||
for method in selected_methods:
|
||||
print(method)
|
||||
PY
|
||||
)}")
|
||||
|
||||
if (( ${#PATCH_METHODS[@]} == 0 )); then
|
||||
echo "[*] No JB patch methods selected (all already marked working or list empty)" | tee -a "$MASTER_LOG"
|
||||
echo "[*] Set JB_AUTOTEST_INCLUDE_WORKING=1 to run the full list." | tee -a "$MASTER_LOG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "index,patch,status,exit_code,log_file" >"$SUMMARY_CSV"
|
||||
echo "[*] JB patch single-method automation started" | tee -a "$MASTER_LOG"
|
||||
echo "[*] Logs: $LOG_ROOT" | tee -a "$MASTER_LOG"
|
||||
echo "[*] Include already-working methods: ${INCLUDE_WORKING}" | tee -a "$MASTER_LOG"
|
||||
echo "[*] Total methods: ${#PATCH_METHODS[@]}" | tee -a "$MASTER_LOG"
|
||||
|
||||
idx=0
|
||||
for patch_method in "${PATCH_METHODS[@]}"; do
|
||||
(( ++idx ))
|
||||
case_log="${LOG_ROOT}/$(printf '%02d' "$idx")_${patch_method}.log"
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "[*] [$idx/${#PATCH_METHODS[@]}] Testing PATCH=${patch_method}"
|
||||
echo "============================================================"
|
||||
} | tee -a "$MASTER_LOG"
|
||||
|
||||
set +e
|
||||
# Test matrix assumption: each JB kernel method is validated on top of dev patch baseline.
|
||||
case_skip_project_setup="${SKIP_PROJECT_SETUP:-1}"
|
||||
echo "[*] Env: NONE_INTERACTIVE=1 DEV=1 SKIP_PROJECT_SETUP=${case_skip_project_setup} PATCH=${patch_method}" | tee -a "$MASTER_LOG"
|
||||
SUDO_PASSWORD="${SUDO_PASSWORD:-}" \
|
||||
NONE_INTERACTIVE=1 \
|
||||
DEV=1 \
|
||||
SKIP_PROJECT_SETUP="${case_skip_project_setup}" \
|
||||
PATCH="$patch_method" \
|
||||
make setup_machine >"$case_log" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
if (( rc == 0 )); then
|
||||
case_status="PASS"
|
||||
else
|
||||
case_status="FAIL"
|
||||
fi
|
||||
|
||||
echo "${idx},${patch_method},${case_status},${rc},${case_log}" >>"$SUMMARY_CSV"
|
||||
echo "[*] Result: ${case_status} (rc=${rc}) log=${case_log}" | tee -a "$MASTER_LOG"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "[*] Completed JB patch automation. Summary: $SUMMARY_CSV" | tee -a "$MASTER_LOG"
|
||||
+15
-3
@@ -46,14 +46,12 @@ if __name__ == "__main__":
|
||||
from patchers.cfw_patch_cache_loader import patch_launchd_cache_loader
|
||||
from patchers.cfw_patch_mobileactivationd import patch_mobileactivationd
|
||||
from patchers.cfw_patch_jetsam import patch_launchd_jetsam
|
||||
from patchers.cfw_inject_dylib import inject_dylib
|
||||
from patchers.cfw_daemons import parse_cryptex_paths, inject_daemons
|
||||
else:
|
||||
from .cfw_patch_seputil import patch_seputil
|
||||
from .cfw_patch_cache_loader import patch_launchd_cache_loader
|
||||
from .cfw_patch_mobileactivationd import patch_mobileactivationd
|
||||
from .cfw_patch_jetsam import patch_launchd_jetsam
|
||||
from .cfw_inject_dylib import inject_dylib
|
||||
from .cfw_daemons import parse_cryptex_paths, inject_daemons
|
||||
|
||||
|
||||
@@ -110,8 +108,22 @@ def main():
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: patch_cfw.py inject-dylib <binary> <dylib_path>")
|
||||
sys.exit(1)
|
||||
if not inject_dylib(sys.argv[2], sys.argv[3]):
|
||||
import subprocess, shutil
|
||||
insert_dylib_bin = shutil.which("insert_dylib")
|
||||
if not insert_dylib_bin:
|
||||
# Check .tools/bin/ relative to project root
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
candidate = os.path.join(project_root, ".tools", "bin", "insert_dylib")
|
||||
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
||||
insert_dylib_bin = candidate
|
||||
if not insert_dylib_bin:
|
||||
print("[-] insert_dylib not found. Run: make setup_tools")
|
||||
sys.exit(1)
|
||||
rc = subprocess.run(
|
||||
[insert_dylib_bin, "--weak", "--inplace", "--all-yes", sys.argv[3], sys.argv[2]],
|
||||
).returncode
|
||||
if rc != 0:
|
||||
sys.exit(rc)
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
"""LC_LOAD_DYLIB injection module."""
|
||||
|
||||
from .cfw_asm import *
|
||||
|
||||
def _align(n, alignment):
|
||||
return (n + alignment - 1) & ~(alignment - 1)
|
||||
|
||||
|
||||
def _find_first_section_offset(data):
|
||||
"""Find the file offset of the earliest section data in the Mach-O.
|
||||
|
||||
This tells us how much space is available after load commands.
|
||||
For fat/universal binaries, we operate on the first slice.
|
||||
"""
|
||||
magic = struct.unpack_from("<I", data, 0)[0]
|
||||
if magic != 0xFEEDFACF:
|
||||
return -1
|
||||
|
||||
ncmds = struct.unpack_from("<I", data, 16)[0]
|
||||
offset = 32 # sizeof(mach_header_64)
|
||||
earliest = len(data)
|
||||
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", data, offset)
|
||||
if cmd == 0x19: # LC_SEGMENT_64
|
||||
nsects = struct.unpack_from("<I", data, offset + 64)[0]
|
||||
sect_off = offset + 72
|
||||
for _ in range(nsects):
|
||||
file_off = struct.unpack_from("<I", data, sect_off + 48)[0]
|
||||
size = struct.unpack_from("<Q", data, sect_off + 40)[0]
|
||||
if file_off > 0 and size > 0 and file_off < earliest:
|
||||
earliest = file_off
|
||||
sect_off += 80
|
||||
offset += cmdsize
|
||||
return earliest
|
||||
|
||||
|
||||
def _get_fat_slices(data):
|
||||
"""Parse FAT (universal) binary header and return list of (offset, size) tuples.
|
||||
|
||||
Returns [(0, len(data))] for thin binaries.
|
||||
"""
|
||||
magic = struct.unpack_from(">I", data, 0)[0]
|
||||
if magic == 0xCAFEBABE: # FAT_MAGIC
|
||||
nfat = struct.unpack_from(">I", data, 4)[0]
|
||||
slices = []
|
||||
for i in range(nfat):
|
||||
off = 8 + i * 20
|
||||
slice_off = struct.unpack_from(">I", data, off + 8)[0]
|
||||
slice_size = struct.unpack_from(">I", data, off + 12)[0]
|
||||
slices.append((slice_off, slice_size))
|
||||
return slices
|
||||
elif magic == 0xBEBAFECA: # FAT_MAGIC_64
|
||||
nfat = struct.unpack_from(">I", data, 4)[0]
|
||||
slices = []
|
||||
for i in range(nfat):
|
||||
off = 8 + i * 32
|
||||
slice_off = struct.unpack_from(">Q", data, off + 8)[0]
|
||||
slice_size = struct.unpack_from(">Q", data, off + 16)[0]
|
||||
slices.append((slice_off, slice_size))
|
||||
return slices
|
||||
else:
|
||||
return [(0, len(data))]
|
||||
|
||||
|
||||
def _check_existing_dylib(data, base, dylib_path):
|
||||
"""Check if the dylib is already loaded in this Mach-O slice."""
|
||||
magic = struct.unpack_from("<I", data, base)[0]
|
||||
if magic != 0xFEEDFACF:
|
||||
return False
|
||||
|
||||
ncmds = struct.unpack_from("<I", data, base + 16)[0]
|
||||
offset = base + 32
|
||||
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", data, offset)
|
||||
if cmd in (0xC, 0xD, 0x18, 0x1F, 0x80000018):
|
||||
# LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, LC_LAZY_LOAD_DYLIB,
|
||||
# LC_REEXPORT_DYLIB, LC_LOAD_UPWARD_DYLIB
|
||||
name_offset = struct.unpack_from("<I", data, offset + 8)[0]
|
||||
name_end = data.index(0, offset + name_offset)
|
||||
name = data[offset + name_offset : name_end].decode(
|
||||
"ascii", errors="replace"
|
||||
)
|
||||
if name == dylib_path:
|
||||
return True
|
||||
offset += cmdsize
|
||||
return False
|
||||
|
||||
|
||||
def _strip_codesig(data, base):
|
||||
"""Strip LC_CODE_SIGNATURE if it's the last load command.
|
||||
|
||||
Zeros out the command bytes and decrements ncmds/sizeofcmds.
|
||||
Returns the cmdsize of the removed command, or 0 if not stripped.
|
||||
Since the binary will be re-signed by ldid, this is always safe.
|
||||
"""
|
||||
ncmds = struct.unpack_from("<I", data, base + 16)[0]
|
||||
sizeofcmds = struct.unpack_from("<I", data, base + 20)[0]
|
||||
|
||||
offset = base + 32
|
||||
last_offset = -1
|
||||
last_cmd = 0
|
||||
last_cmdsize = 0
|
||||
|
||||
for i in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", data, offset)
|
||||
if i == ncmds - 1:
|
||||
last_offset = offset
|
||||
last_cmd = cmd
|
||||
last_cmdsize = cmdsize
|
||||
offset += cmdsize
|
||||
|
||||
if last_cmd != 0x1D: # LC_CODE_SIGNATURE
|
||||
return 0
|
||||
|
||||
# Zero out the LC_CODE_SIGNATURE command
|
||||
data[last_offset : last_offset + last_cmdsize] = b"\x00" * last_cmdsize
|
||||
|
||||
# Update header
|
||||
struct.pack_into("<I", data, base + 16, ncmds - 1)
|
||||
struct.pack_into("<I", data, base + 20, sizeofcmds - last_cmdsize)
|
||||
|
||||
print(f" Stripped LC_CODE_SIGNATURE ({last_cmdsize} bytes freed)")
|
||||
return last_cmdsize
|
||||
|
||||
|
||||
def _inject_lc_load_dylib(data, base, dylib_path):
|
||||
"""Inject LC_LOAD_DYLIB into a single Mach-O slice starting at `base`.
|
||||
|
||||
Strategy (matches optool/insert_dylib behavior):
|
||||
1. Try to fit new LC in existing zero-padding after load commands.
|
||||
2. If not enough space, strip LC_CODE_SIGNATURE (re-signed by ldid anyway).
|
||||
3. If still not enough, allow header to overflow into section data
|
||||
(same approach as optool — the overwritten bytes are typically stub
|
||||
code that the jailbreak hook replaces).
|
||||
|
||||
Returns True on success.
|
||||
"""
|
||||
magic = struct.unpack_from("<I", data, base)[0]
|
||||
if magic != 0xFEEDFACF:
|
||||
print(f" [-] Not a 64-bit Mach-O at offset 0x{base:X}")
|
||||
return False
|
||||
|
||||
ncmds = struct.unpack_from("<I", data, base + 16)[0]
|
||||
sizeofcmds = struct.unpack_from("<I", data, base + 20)[0]
|
||||
|
||||
# Build the LC_LOAD_DYLIB command
|
||||
name_bytes = dylib_path.encode("ascii") + b"\x00"
|
||||
name_offset_in_cmd = 24 # sizeof(dylib_command) header
|
||||
cmd_size = _align(name_offset_in_cmd + len(name_bytes), 8)
|
||||
lc_data = bytearray(cmd_size)
|
||||
|
||||
struct.pack_into("<I", lc_data, 0, 0xC) # cmd = LC_LOAD_DYLIB
|
||||
struct.pack_into("<I", lc_data, 4, cmd_size) # cmdsize
|
||||
struct.pack_into("<I", lc_data, 8, name_offset_in_cmd) # name offset
|
||||
struct.pack_into("<I", lc_data, 12, 2) # timestamp
|
||||
struct.pack_into("<I", lc_data, 16, 0) # current_version
|
||||
struct.pack_into("<I", lc_data, 20, 0) # compat_version
|
||||
lc_data[name_offset_in_cmd : name_offset_in_cmd + len(name_bytes)] = name_bytes
|
||||
|
||||
# Check available space
|
||||
header_end = base + 32 + sizeofcmds # end of current load commands
|
||||
first_section = _find_first_section_offset(data[base:])
|
||||
if first_section < 0:
|
||||
print(f" [-] Could not determine section offsets")
|
||||
return False
|
||||
first_section_abs = base + first_section
|
||||
available = first_section_abs - header_end
|
||||
|
||||
print(
|
||||
f" Header end: 0x{header_end:X}, first section: 0x{first_section_abs:X}, "
|
||||
f"available: {available}, need: {cmd_size}"
|
||||
)
|
||||
|
||||
if available < cmd_size:
|
||||
# Strip LC_CODE_SIGNATURE to reclaim header space (re-signed by ldid)
|
||||
freed = _strip_codesig(data, base)
|
||||
if freed > 0:
|
||||
ncmds = struct.unpack_from("<I", data, base + 16)[0]
|
||||
sizeofcmds = struct.unpack_from("<I", data, base + 20)[0]
|
||||
header_end = base + 32 + sizeofcmds
|
||||
available = first_section_abs - header_end
|
||||
print(f" After strip: available={available}, need={cmd_size}")
|
||||
|
||||
if available < cmd_size:
|
||||
overflow = cmd_size - available
|
||||
# Allow up to 256 bytes overflow (same behavior as optool/insert_dylib)
|
||||
if overflow > 256:
|
||||
print(f" [-] Would overflow {overflow} bytes into section data (too much)")
|
||||
return False
|
||||
print(
|
||||
f" [!] Header overflow: {overflow} bytes into section data "
|
||||
f"(same as optool — binary will be re-signed)"
|
||||
)
|
||||
|
||||
# Write the new load command at the end of existing commands
|
||||
data[header_end : header_end + cmd_size] = lc_data
|
||||
|
||||
# Update header: ncmds += 1, sizeofcmds += cmd_size
|
||||
struct.pack_into("<I", data, base + 16, ncmds + 1)
|
||||
struct.pack_into("<I", data, base + 20, sizeofcmds + cmd_size)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def inject_dylib(filepath, dylib_path):
|
||||
"""Inject LC_LOAD_DYLIB into a Mach-O binary (thin or universal/FAT).
|
||||
|
||||
Equivalent to: optool install -c load -p <dylib_path> -t <filepath>
|
||||
"""
|
||||
data = bytearray(open(filepath, "rb").read())
|
||||
slices = _get_fat_slices(bytes(data))
|
||||
|
||||
injected = 0
|
||||
for slice_off, slice_size in slices:
|
||||
if _check_existing_dylib(data, slice_off, dylib_path):
|
||||
print(f" [!] Dylib already loaded in slice at 0x{slice_off:X}, skipping")
|
||||
injected += 1
|
||||
continue
|
||||
|
||||
if _inject_lc_load_dylib(data, slice_off, dylib_path):
|
||||
print(
|
||||
f" [+] Injected LC_LOAD_DYLIB '{dylib_path}' at slice 0x{slice_off:X}"
|
||||
)
|
||||
injected += 1
|
||||
|
||||
if injected == len(slices):
|
||||
open(filepath, "wb").write(data)
|
||||
print(f" [+] Wrote {filepath} ({injected} slice(s) patched)")
|
||||
return True
|
||||
else:
|
||||
print(f" [-] Only {injected}/{len(slices)} slices patched")
|
||||
return False
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# BuildManifest parsing
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@@ -875,7 +875,6 @@ Options:
|
||||
|
||||
Environment:
|
||||
NONE_INTERACTIVE=1 Auto-continue first-boot prompts + run final boot analysis.
|
||||
PATCH=patch_xxx Run `make fw_patch_test` after the main fw_patch target.
|
||||
SUDO_PASSWORD=... Preload sudo credential via askpass.
|
||||
EOF
|
||||
exit 0
|
||||
@@ -930,9 +929,6 @@ main() {
|
||||
run_make "Firmware prep" vm_new
|
||||
run_make "Firmware prep" fw_prepare
|
||||
run_make "Firmware patch" "$fw_patch_target"
|
||||
if [[ -n "${PATCH:-}" ]]; then
|
||||
run_make "Firmware patch test" fw_patch_test
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Restore phase ==="
|
||||
|
||||
+35
-5
@@ -2,7 +2,7 @@
|
||||
# setup_tools.sh — Install all required host tools for vphone-cli
|
||||
#
|
||||
# Installs brew packages, builds trustcache from source,
|
||||
# builds libimobiledevice toolchain, and creates Python venv.
|
||||
# clones insert_dylib, builds libimobiledevice toolchain, and creates Python venv.
|
||||
#
|
||||
# Run: make setup_tools
|
||||
|
||||
@@ -12,9 +12,22 @@ SCRIPT_DIR="${0:a:h}"
|
||||
PROJECT_DIR="${SCRIPT_DIR:h}"
|
||||
TOOLS_PREFIX="${TOOLS_PREFIX:-$PROJECT_DIR/.tools}"
|
||||
|
||||
clone_or_update() {
|
||||
local url="$1"
|
||||
local dir="$2"
|
||||
|
||||
if [[ -d "$dir/.git" ]]; then
|
||||
git -C "$dir" fetch --depth 1 origin --quiet
|
||||
git -C "$dir" reset --hard FETCH_HEAD --quiet
|
||||
git -C "$dir" clean -fdx --quiet
|
||||
else
|
||||
git clone --depth 1 "$url" "$dir" --quiet
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Brew packages ──────────────────────────────────────────────
|
||||
|
||||
echo "[1/4] Checking brew packages..."
|
||||
echo "[1/5] Checking brew packages..."
|
||||
|
||||
BREW_PACKAGES=(gnu-tar openssl@3 ldid-procursus sshpass)
|
||||
BREW_MISSING=()
|
||||
@@ -34,7 +47,7 @@ fi
|
||||
|
||||
# ── Trustcache ─────────────────────────────────────────────────
|
||||
|
||||
echo "[2/4] trustcache"
|
||||
echo "[2/5] trustcache"
|
||||
|
||||
TRUSTCACHE_BIN="$TOOLS_PREFIX/bin/trustcache"
|
||||
if [[ -x "$TRUSTCACHE_BIN" ]]; then
|
||||
@@ -58,14 +71,31 @@ else
|
||||
echo " Installed: $TRUSTCACHE_BIN"
|
||||
fi
|
||||
|
||||
# ── insert_dylib ───────────────────────────────────────────────
|
||||
|
||||
echo "[3/5] insert_dylib"
|
||||
|
||||
INSERT_DYLIB_BIN="$TOOLS_PREFIX/bin/insert_dylib"
|
||||
if [[ -x "$INSERT_DYLIB_BIN" ]]; then
|
||||
echo " Already built: $INSERT_DYLIB_BIN"
|
||||
else
|
||||
INSERT_DYLIB_DIR="$TOOLS_PREFIX/src/insert_dylib"
|
||||
mkdir -p "${INSERT_DYLIB_DIR:h}"
|
||||
clone_or_update "https://github.com/tyilo/insert_dylib" "$INSERT_DYLIB_DIR"
|
||||
echo " Building insert_dylib..."
|
||||
mkdir -p "$TOOLS_PREFIX/bin"
|
||||
clang -o "$INSERT_DYLIB_BIN" "$INSERT_DYLIB_DIR/insert_dylib/main.c" -framework Security -O2
|
||||
echo " Installed: $INSERT_DYLIB_BIN"
|
||||
fi
|
||||
|
||||
# ── Libimobiledevice ──────────────────────────────────────────
|
||||
|
||||
echo "[3/4] libimobiledevice"
|
||||
echo "[4/5] libimobiledevice"
|
||||
bash "$SCRIPT_DIR/setup_libimobiledevice.sh"
|
||||
|
||||
# ── Python venv ────────────────────────────────────────────────
|
||||
|
||||
echo "[4/4] Python venv"
|
||||
echo "[5/5] Python venv"
|
||||
zsh "$SCRIPT_DIR/setup_venv.sh"
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -127,7 +127,6 @@ class VPhoneControl {
|
||||
self?.connection = conn
|
||||
self?.performHandshake(fd: conn.fileDescriptor)
|
||||
case let .failure(error):
|
||||
print("[control] vsock: \(error.localizedDescription), retrying...")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
self?.attemptConnect()
|
||||
}
|
||||
|
||||
@@ -292,6 +292,13 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
|
||||
} else {
|
||||
print("[vphone] VM started — booting normally")
|
||||
}
|
||||
|
||||
// Print auto-assigned debug stub port after VM starts
|
||||
if let debugStub = Dynamic(virtualMachine)._configuration._debugStub.asAnyObject {
|
||||
if let port = Dynamic(debugStub).port.asInt, port > 0 {
|
||||
print("[vphone] Kernel GDB debug stub listening on tcp://127.0.0.1:\(port)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delegate
|
||||
|
||||
Reference in New Issue
Block a user