Add JB install pipeline and update docs

Add jailbreak extension patchers and targets:
- kernel_jb.py: 22 dynamic kernel patches (trustcache, execve cs_flags,
  sandbox ops, task/VM, kcall10 syscall hook, ~160 total modifications)
- txm_jb.py: 13 TXM patches (CS validation, get-task-allow, debugger
  entitlement, dev mode bypass)
- iboot_jb.py: iBSS nonce generation skip
- cfw.py: launchd jetsam patch, dylib injection commands
- fw_patch_jb.py: orchestrator running base + JB extension patches
- cfw_install_jb.sh: JB install phases (launchd jetsam fix, procursus
  bootstrap + Sileo deployment)

3 kernel patches still WIP (nvram_verify_permission, thid_should_crash,
hook_cred_label_update_execve) — strategies documented in
researchs/kernel_jb_remaining_patches.md.

All base (non-JB) code paths verified unaffected — kernel.py produces
identical 25 patches, cfw.py base commands unchanged.

Add Linux venv setup script; tweak Makefile help

Add scripts/setup_venv_linux.sh to create a Python virtualenv on Debian/Ubuntu (or dnf-based) systems, install system packages and Python requirements, and verify core imports (capstone, keystone, pyimg4). Also update Makefile help text to mark the fw_patch_jb target as WIP. This simplifies local development setup on Linux and clarifies that the JB extension patches are a work in progress.

Update AGENTS.md: mark cfw_install_jb.sh as complete
This commit is contained in:
Lakr
2026-03-01 15:01:32 +09:00
parent 7741821698
commit 154d5064ec
14 changed files with 4066 additions and 25 deletions
+5
View File
@@ -311,3 +311,8 @@ __marimo__/
/VM
.limd/
/.swiftpm
*.ipsw
/updates-cdn
/researchs/jb_asm_refs
TODO.md
/references/
+138 -11
View File
@@ -13,6 +13,13 @@ Virtual iPhone boot tool using Apple's Virtualization.framework with PCC researc
- **Language:** Swift 6.0 (SwiftPM), private APIs via [Dynamic](https://github.com/mhdhejazi/Dynamic)
- **Python deps:** `capstone`, `keystone-engine`, `pyimg4` (see `requirements.txt`)
## Workflow Rules
- Always read `/TODO.md` before starting any substantial work.
- Always update `/TODO.md` when plan, progress, assumptions, blockers, or open questions change.
- 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.
## Project Overview
CLI tool that boots virtual iPhones (PV=3) via Apple's Virtualization.framework, targeting Private Cloud Compute (PCC) research VMs. Used for iOS security research — firmware patching, boot chain modification, and runtime instrumentation.
@@ -38,23 +45,31 @@ sources/
scripts/
├── patchers/ # Python patcher package
│ ├── iboot.py # Dynamic iBoot patcher (iBSS/iBEC/LLB)
│ ├── iboot_jb.py # JB extension iBoot patcher (nonce skip)
│ ├── kernel.py # Dynamic kernel patcher (25 patches)
│ ├── kernel_jb.py # JB extension kernel patcher (~34 patches)
│ ├── txm.py # Dynamic TXM patcher
── cfw.py # CFW binary patcher
── txm_jb.py # JB extension TXM patcher (~13 patches)
│ └── cfw.py # CFW binary patcher (base + JB jetsam)
├── resources/ # Resource archives
│ ├── cfw_input.tar.zst
│ ├── cfw_jb_input.tar.zst # JB: procursus bootstrap + Sileo
│ └── ramdisk_input.tar.zst
├── fw_prepare.sh # Downloads IPSWs, merges cloudOS into iPhone
├── fw_manifest.py # Generates hybrid BuildManifest.plist & Restore.plist
├── fw_patch.py # Patches 6 boot-chain components (41+ modifications)
├── fw_patch_jb.py # Runs fw_patch + JB extension patches (iBSS/TXM/kernel)
├── ramdisk_build.py # Builds SSH ramdisk with trustcache
├── ramdisk_send.sh # Sends ramdisk to device via irecovery
├── cfw_install.sh # Installs custom firmware to VM disk
├── cfw_install_jb.sh # Wrapper: cfw_install with JB phases enabled
├── vm_create.sh # Creates VM directory (disk, SEP storage, ROMs)
├── setup_venv.sh # Creates Python venv with native keystone dylib
└── setup_libimobiledevice.sh # Builds libimobiledevice toolchain from source
researchs/ # Component analysis and architecture docs
researchs/
├── jailbreak_patches.md # JB vs base patch comparison table
└── ... # Component analysis and architecture docs
```
### Key Patterns
@@ -77,6 +92,7 @@ The firmware is a **PCC/iPhone hybrid** — PCC boot infrastructure wrapping iPh
1. make fw_prepare Download iPhone + cloudOS IPSWs, merge, generate hybrid plists
2. make fw_patch Patch 6 boot-chain components for signature bypass + debug
OR make fw_patch_jb Base patches + JB extensions (iBSS nonce, TXM CS, kernel JB)
3. make ramdisk_build Build SSH ramdisk from SHSH blob, inject tools, sign with IM4M
@@ -86,7 +102,8 @@ The firmware is a **PCC/iPhone hybrid** — PCC boot infrastructure wrapping iPh
6. make ramdisk_send Load boot chain + ramdisk via irecovery
7. make cfw_install Mount Cryptex, patch userland, install jailbreak tools
7. make cfw_install Mount Cryptex, patch userland, install base tools
OR make cfw_install_jb Base CFW + JB phases (jetsam patch, procursus bootstrap)
```
### Component Origins
@@ -153,17 +170,35 @@ idevicerestore selects this identity by partial-matching `Info.Variant` against
| TXM | 1 | Dynamic via `patchers/txm.py` (trustcache hash lookup bypass) |
| KernelCache | 25 | Dynamic via `patchers/kernel.py` (string anchors, ADRP+ADD xrefs, BL frequency) |
**CFW patches** (`patchers/cfw.py` / `cfw_install.sh`) — all 4 targets from **iPhone** Cryptex SystemOS:
**JB extension patches** (`fw_patch_jb.py`) — runs base patches first, then adds:
| Binary | Technique | Purpose |
|--------|-----------|---------|
| seputil | String patch (`/%s.gl``/AA.gl`) | Gigalocker UUID fix |
| launchd_cache_loader | NOP (disassembly-anchored) | Bypass cache validation |
| mobileactivationd | Return true (disassembly-anchored) | Skip activation check |
| launchd.plist | Plist injection | Add bash/dropbear/trollvnc daemons |
| Component | JB Patches | Technique |
|-----------|-----------|-----------|
| iBSS | +1 | `patchers/iboot_jb.py` (skip nonce generation) |
| TXM | +13 | `patchers/txm_jb.py` (CS validation bypass, get-task-allow, debugger ent, dev mode) |
| KernelCache | +34 | `patchers/kernel_jb.py` (trustcache, execve, sandbox, task/VM, kcall10) |
**CFW patches** (`patchers/cfw.py` / `cfw_install.sh`) — targets from **iPhone** Cryptex SystemOS:
| Binary | Technique | Purpose | Mode |
|--------|-----------|---------|------|
| seputil | String patch (`/%s.gl``/AA.gl`) | Gigalocker UUID fix | Base |
| launchd_cache_loader | NOP (disassembly-anchored) | Bypass cache validation | Base |
| mobileactivationd | Return true (disassembly-anchored) | Skip activation check | Base |
| launchd.plist | Plist injection | Add bash/dropbear/trollvnc daemons | Base |
| launchd | Branch (skip jetsam guard) + LC_LOAD_DYLIB injection | Prevent jetsam panic + load launchdhook.dylib | JB |
**JB install phases** (`cfw_install_jb.sh``cfw_install.sh` with `CFW_JB_MODE=1`):
| Phase | Action |
|-------|--------|
| JB-1 | Patch `/mnt1/sbin/launchd`: inject `launchdhook.dylib` LC_LOAD_DYLIB + jetsam guard bypass |
| JB-2 | Install procursus bootstrap to `/mnt5/<hash>/jb-vphone/procursus` |
| JB-3 | Deploy BaseBin hooks (`systemhook.dylib`, `launchdhook.dylib`, `libellekit.dylib`) to `/mnt1/cores/` |
### Boot Flow
**Base** (`fw_patch` + `cfw_install`):
```
AVPBooter (ROM, PCC)
→ LLB (PCC, patched)
@@ -175,6 +210,18 @@ AVPBooter (ROM, PCC)
→ iOS userland (iPhone, CFW-patched)
```
**Jailbreak** (`fw_patch_jb` + `cfw_install_jb`):
```
AVPBooter (ROM, PCC)
→ LLB (PCC, patched)
→ iBSS (PCC, patched + nonce skip)
→ iBEC (PCC, patched, DFU)
→ SPTM + TXM (PCC, TXM patched + CS/ent/devmode bypass)
→ KernelCache (PCC, 25 base + ~34 JB patches)
→ Ramdisk (SSH-injected)
→ iOS userland (CFW + jetsam fix + procursus)
```
### Ramdisk Build (`ramdisk_build.py`)
1. Extract IM4M from SHSH blob
@@ -184,7 +231,7 @@ AVPBooter (ROM, PCC)
### CFW Installation (`cfw_install.sh`)
7 phases, safe to re-run (idempotent):
7 phases (+ 2 JB phases), safe to re-run (idempotent):
1. Decrypt/mount Cryptex SystemOS and AppOS DMGs (`ipsw` + `aea`)
2. Patch seputil (gigalocker UUID)
3. Install GPU driver (AppleParavirtGPUMetalIOGPUFamily)
@@ -193,6 +240,10 @@ AVPBooter (ROM, PCC)
6. Patch mobileactivationd (activation bypass)
7. Install LaunchDaemons (bash, dropbear SSH, trollvnc)
**JB-only phases** (enabled via `make cfw_install_jb` or `CFW_JB_MODE=1`):
- JB-1: Patch launchd jetsam guard (prevents jetsam panic on boot)
- JB-2: Install procursus bootstrap + optional Sileo to `/mnt5/<hash>/jb-vphone/`
---
## Coding Conventions
@@ -295,3 +346,79 @@ Rationale: Dark surfaces match the terminal-adjacent workflow. Status colors bor
- **VM display:** Full-bleed within its container. No rounded corners on the display itself.
- **Log output:** Scrolling monospace region, bottom-anchored (newest at bottom). No line numbers unless requested.
- **Toolbar (if present):** Icon-only, 32px touch targets, subtle hover state (`#2e2e2e` -> `#3a3a3a`).
---
## JB Kernel Patcher Status (`patches-jb` branch)
Branch is 8 commits ahead of `main`. All changes are **additive** — non-JB code paths are unaffected.
### Diff vs Main
| File | Change | Impact on non-JB |
|------|--------|-----------------|
| `kernel.py` | +1 line: `self.patches = []` reset in `find_all()` | None (harmless init) |
| `cfw.py` | +`patch-launchd-jetsam`, +`inject-dylib` commands | None (new commands only) |
| `kernel_jb.py` | **New file** — 2128 lines | N/A |
| `txm_jb.py` | **New file** — 335 lines | N/A |
| `iboot_jb.py` | **New file** — 105 lines | N/A |
| `fw_patch_jb.py` | **New file** — 115 lines (WIP) | N/A |
| `cfw_install_jb.sh` | **New file** — 214 lines | N/A |
| `cfw_jb_input.tar.zst` | **New file** — JB resources | N/A |
| `Makefile` | +JB targets (`fw_patch_jb`, `cfw_install_jb`) | None (additive) |
| `AGENTS.md` | Documentation updates | N/A |
### Patch Counts
**Base patcher** (`kernel.py`): **25 patches** — verified identical to main.
**JB patcher** (`kernel_jb.py`): **160 patches** from 22 methods:
- **19 of 22 PASSING** — Groups A (sandbox hooks, AMFI, execve), B (string-anchored), C (shellcode)
- **3 FAILING** — see below
### 3 Remaining Failures
| Patch | Upstream Offset | Root Cause | Proposed Strategy |
|-------|----------------|------------|-------------------|
| `patch_nvram_verify_permission` | NOP BL at `0x1234034` | 332 identical IOKit methods match structural filter; "krn." string leads to wrong function | Find via "IONVRAMController" string → metaclass ctor → PAC disc `#0xcda1` → search `__DATA_CONST` vtable entries (first entry after 3 nulls) with matching PAC disc + BL to memmove |
| `patch_thid_should_crash` | Zero `0x67EB50` | String in `__PRELINK_INFO` plist (no code refs); value already `0x00000000` in PCC kernel | Safe to return True (no-op); or find via `sysctl_oid` struct search in `__DATA` |
| `patch_hook_cred_label_update_execve` | Shellcode at `0xAB17D8` + ops table at `0xA54518` | Needs `_vfs_context_current` (`0xCC5EAC`) and `_vnode_getattr` (`0xCC91C0`) — 0 symbols available | Find via sandbox ops table → original hook func → BL targets by caller count (vfs_context_current = highest, vnode_getattr = near `mov wN, #0x380`) |
### Key Findings (from `researchs/kernel_jb_remaining_patches.md`)
**All offsets in `kernel.py` are file offsets**`bl_callers` dict, `_is_bl()`, `_disas_at()`, `find_string_refs()` all use file offsets, not VAs.
**IONVRAMController vtable discovery chain**:
```
"IONVRAMController" string @ 0xA2FEB
→ ADRP+ADD refs → metaclass ctor @ 0x125D2C0
→ PAC discriminator: movk x17, #0xcda1, lsl #48
→ instance size: mov w3, #0x88
→ class vtable in __DATA_CONST @ 0x7410B8 (preceded by 3 null entries)
→ vtable[0] = 0x1233E40 = verifyPermission
→ BL to memmove (3114 callers) at +0x1F4 = 0x1234034 ← NOP this
```
**vfs_context_current / vnode_getattr resolution**:
```
sandbox ops table → entry[16] = original hook @ 0x239A0B4
→ disassemble hook → find BL targets:
- _vfs_context_current: BL target with >1000 callers, short function
- _vnode_getattr: BL target near "mov wN, #0x380", moderate callers
```
### Upstream Reference Offsets (iPhone17,3 26.1)
| Symbol | File Offset | Notes |
|--------|-------------|-------|
| kern_text | `0xA74000``0x24B0000` | |
| base_va | `0xFFFFFE0007004000` | |
| verifyPermission func | `0x1233E40` | vtable @ `0x7410B8` |
| verifyPermission patch | `0x1234034` | NOP BL to memmove |
| _thid_should_crash var | `0x67EB50` | already 0 |
| _vfs_context_current | `0xCC5EAC` | from BL encoding |
| _vnode_getattr | `0xCC91C0` | from BL encoding |
| hook_cred_label orig | `0x239A0B4` | from B encoding |
| sandbox ops entry | `0xA54518` | index 16 |
| OSMetaClass::OSMetaClass() | `0x10EA790` | 5236 callers |
| memmove | `0x12CB0D0` | 3114 callers |
+10 -2
View File
@@ -45,6 +45,7 @@ help:
@echo "Firmware pipeline:"
@echo " make fw_prepare Download IPSWs, extract, merge"
@echo " make fw_patch Patch boot chain (6 components)"
@echo " make fw_patch_jb Run fw_patch + JB extension patches (WIP)"
@echo ""
@echo "Restore:"
@echo " make restore_get_shsh Fetch SHSH blob from device"
@@ -56,6 +57,7 @@ help:
@echo ""
@echo "CFW:"
@echo " make cfw_install Install CFW mods via SSH"
@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)"
@@ -130,7 +132,7 @@ boot_dfu: build
# Firmware pipeline
# ═══════════════════════════════════════════════════════════════════
.PHONY: fw_prepare fw_patch
.PHONY: fw_prepare fw_patch fw_patch_jb
fw_prepare:
cd $(VM_DIR) && bash "$(CURDIR)/$(SCRIPTS)/fw_prepare.sh"
@@ -138,6 +140,9 @@ fw_prepare:
fw_patch:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch.py" .
fw_patch_jb:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch_jb.py" .
# ═══════════════════════════════════════════════════════════════════
# Restore
# ═══════════════════════════════════════════════════════════════════
@@ -166,7 +171,10 @@ ramdisk_send:
# CFW
# ═══════════════════════════════════════════════════════════════════
.PHONY: cfw_install
.PHONY: cfw_install cfw_install_jb
cfw_install:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
cfw_install_jb:
cd $(VM_DIR) && zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .
+84
View File
@@ -105,6 +105,19 @@ No additional JB patches for LLB.
| 3 | mov x0,#1; ret | mobileactivationd | Activation bypass | Y | Y |
| 4 | Plist injection | launchd.plist | bash/dropbear/trollvnc daemons | Y | Y |
| 5 | b (skip jetsam) | launchd | Prevent jetsam panic on boot | — | Y |
| 6 | procursus bootstrap | `/mnt5/<hash>/jb-vphone` | Install procursus userspace + optional Sileo payload | — | Y |
### JB Install Flow (`make cfw_install_jb`)
- Entry: `scripts/cfw_install_jb.sh` (wrapper) -> `scripts/cfw_install.sh` with `CFW_JB_MODE=1`.
- Added JB phases in install pipeline:
- `JB-1`: patch `/mnt1/sbin/launchd` via `patch-launchd-jetsam` (dynamic string+xref).
- `JB-2`: unpack procursus bootstrap (`bootstrap-iphoneos-arm64.tar.zst`) into `/mnt5/<bootManifestHash>/jb-vphone/procursus`.
- JB resources now packaged in:
- `scripts/resources/cfw_jb_input.tar.zst`
- contains:
- `jb/bootstrap-iphoneos-arm64.tar.zst`
- `jb/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb`
## Summary
@@ -117,3 +130,74 @@ No additional JB patches for LLB.
| Kernelcache | 25 | ~23+ | ~48+ |
| CFW | 4 | 1 | 5 |
| **Total** | **41** | **~38+** | **~79+** |
## Dynamic Implementation Log (fw_patch_jb)
### TXM (Completed)
All TXM JB patches are now implemented with dynamic binary analysis and
keystone/capstone-encoded instructions only.
1. `selector24 hashcmp` (`bl -> mov x0,#0`, 2 residual sites in JB stage)
- Locator: global instruction motif `mov w2,#0x14 ; bl ; cbz w0`.
- Patch bytes: keystone `mov x0, #0`.
2. `selector24 A1` (`b.lo/cbz -> nop`)
- Locator: unique guarded `mov w0,#0xa1` site with nearby `b.lo` and `cbz x9`.
- Patch bytes: keystone `nop`.
3. `selector41|29 get-task-allow`
- Locator: xref to `"get-task-allow"` + nearby `bl` followed by `tbnz w0,#0`.
- Patch bytes: keystone `mov x0, #1`.
4. `selector42|29 shellcode trampoline`
- Locator:
- Find dispatch stub pattern `bti j ; mov x0,x20 ; bl ; mov x1,x21 ; mov x2,x22 ; bl ; b`.
- Select stub whose second `bl` target is the debugger-gate function (pattern verified by string-xref + call-shape).
- Find executable UDF cave dynamically.
- Patch bytes:
- Stub head -> keystone `b #cave`.
- Cave payload -> `nop ; mov x0,#1 ; strb w0,[x20,#0x30] ; mov x0,x20 ; b #return`.
5. `selector42|37 debugger entitlement`
- Locator: xref to `"com.apple.private.cs.debugger"` + strict nearby call-shape
(`mov x0,#0 ; mov x2,#0 ; bl ; tbnz w0,#0`).
- Patch bytes: keystone `mov w0, #1`.
6. `developer mode bypass`
- Locator: xref to `"developer mode enabled due to system policy configuration"`
+ nearest guard branch on `w9`.
- Patch bytes: keystone `nop`.
#### TXM Binary-Alignment Validation
- `patch.upstream.raw` generated from upstream-equivalent TXM static patch semantics.
- `patch.dyn.raw` generated by `TXMJBPatcher` on the same input.
- Result: byte-identical (`cmp -s` success, SHA-256 matched).
### Kernelcache (In Progress, Dynamic Ports Added)
Implemented in `scripts/patchers/kernel_jb.py` with capstone semantic matching
and keystone-generated patch bytes only:
1. `AMFIIsCDHashInTrustCache` function rewrite
- Locator: semantic function-body matcher in AMFI text.
- Patch: `mov x0,#1 ; cbz x2,+8 ; str x0,[x2] ; ret`.
2. AMFI execve kill path bypass (2 BL sites)
- Locator: string xref to `"AMFI: hook..execve() killing"` (fallback `"execve() killing"`),
then function-local early `bl` + `cbz/cbnz w0` pair matcher.
- Patch: `bl -> mov x0,#0` at two helper callsites.
3. `task_conversion_eval_internal` guard bypass
- Locator: unique cmp/branch motif:
`ldr xN,[xN,#imm] ; cmp xN,x0 ; b.eq ; cmp xN,x1 ; b.eq`.
- Patch: `cmp xN,x0 -> cmp xzr,xzr`.
4. Extended sandbox MACF hook stubs (JB-only set)
- Locator: dynamic `mac_policy_conf -> mpc_ops` discovery, then hook-index resolution.
- Patch per hook function: `mov x0,#0 ; ret`.
- JB extended indices include vnode/proc hooks beyond base 5 hooks.
#### Cross-Version Dynamic Snapshot
Validated using pristine inputs from `updates-cdn/`:
| Case | TXM_JB_PATCHES | KERNEL_JB_PATCHES |
|------|----------------:|------------------:|
| PCC 26.1 (`23B85`) | 14 | 59 |
| PCC 26.3 (`23D128`) | 14 | 59 |
| iOS 26.1 (`23B85`) | 14 | 59 |
| iOS 26.3 (`23D127`) | 14 | 59 |
+442
View File
@@ -0,0 +1,442 @@
# Kernel JB Remaining Patches — Research Notes
Last updated: 2026-03-01
## Overview
`scripts/patchers/kernel_jb.py` has 22 patch methods in `find_all()`. As of this writing:
- **19 PASSING**: All Group A + most Group B + some Group C patches
- **3 FAILING**: `patch_nvram_verify_permission`, `patch_thid_should_crash`, `patch_hook_cred_label_update_execve`
- **1 FIXED this session**: `patch_syscallmask_apply_to_proc` (bl_callers key bug + now passing)
- **2 FIXED prior session**: `patch_task_for_pid`, `patch_load_dylinker` (complete rewrites)
Upstream reference: `/Users/qaq/Documents/GitHub/super-tart-vphone/CFW/patch_fw.py`
Test kernel: `vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600` (IM4P-wrapped, bvx2 compressed)
Key facts about the kernel:
- **0 symbols resolved** (fully stripped)
- `base_va = 0xFFFFFE0007004000` (typical PCC)
- `kern_text = 0xA74000 - 0x24B0000`
- All offsets in `kernel.py` helpers are **file offsets** (not VA)
- `bl_callers` dict: keyed by file offset → list of caller file offsets
---
## Patch 1: `patch_nvram_verify_permission` — FAILING
### Upstream Reference
```python
# patch __ZL16verifyPermission16IONVRAMOperationPKhPKcb
patch(0x1234034, 0xd503201f) # NOP
```
One single NOP at file offset `0x1234034`. The BL being NOPed calls memmove (3114 callers).
### Function Analysis
**Function start**: `0x1233E40` (PACIBSP)
**Function end**: `0x1234094` (next PACIBSP)
**Size**: `0x254` bytes
**BL callers**: 0 (IOKit virtual method, dispatched via vtable)
**Instruction**: `retab` at end
#### Full BL targets in the function:
| Offset | Delta | Target | Callers | Likely Identity |
|--------|-------|--------|---------|-----------------|
| 0x1233F0C | +0x0CC | 0x0AD10DC | 6190 | lck_rw_done / lock_release |
| 0x1234034 | +0x1F4 | 0x12CB0D0 | 3114 | **memmove** ← PATCH THIS |
| 0x1234048 | +0x208 | 0x0ACB418 | 423 | OSObject::release |
| 0x1234070 | +0x230 | 0x0AD029C | 4921 | lck_rw_lock_exclusive |
| 0x123407C | +0x23C | 0x0AD10DC | 6190 | lck_rw_done |
| 0x123408C | +0x24C | 0x0AD10DC | 6190 | lck_rw_done |
#### Key instructions in the function:
- `CASA` at +0x54 (offset 0x1233E94) — atomic compare-and-swap for lock acquisition
- `CASL` at 3 locations — lock release
- 4x `BLRAA` — authenticated indirect calls through vtable pointers
- `movk x17, #0xcda1, lsl #48` — PAC discriminator for IONVRAMController class
- `RETAB` — PAC return
- `mov x8, #-1; str x8, [x19]` — cleanup pattern near end
- `ubfiz x2, x8, #3, #0x20` before BL memmove — size = count * 8
#### "Remove from array" pattern (at patch site):
```
0x1233FD8: adrp x8, #0x272f000
0x1233FDC: ldr x8, [x8, #0x10] ; load observer list struct
0x1233FE0: cbz x8, skip ; if null, skip
0x1233FE4: ldr w11, [x8, #0x10] ; load count
0x1233FE8: cbz w11, skip ; if 0, skip
0x1233FEC: mov x10, #0 ; index = 0
0x1233FF0: ldr x9, [x8, #0x18] ; load array base
loop:
0x1233FF4: add x12, x9, x10, lsl #3
0x1233FF8: ldr x12, [x12] ; array[index]
0x1233FFC: cmp x12, x19 ; compare with self
0x1234000: b.eq found
0x1234004: add x10, x10, #1 ; index++
0x1234008: cmp x11, x10
0x123400C: b.ne loop
found:
0x1234014: sub w11, w11, #1 ; count--
0x1234018: str w11, [x8, #0x10] ; store
0x123401C: subs w8, w11, w10 ; remaining
0x1234020: b.ls skip
0x1234024: ubfiz x2, x8, #3, #0x20 ; size = remaining * 8
0x1234028: add x0, x9, w10, uxtw #3
0x123402C: add w8, w10, #1
0x1234030: add x1, x9, w8, uxtw #3
0x1234034: bl memmove ; ← NOP THIS
```
### What I've Tried (and Failed)
1. **"krn." string anchor** → Leads to function at `0x11F7EE8`, NOT `0x1233E40`. Wrong function entirely.
2. **"nvram-write-access" entitlement string** → Also leads to a different function.
3. **CASA + 0 callers + retab + ubfiz + memmove filter****332 matches**. All IOKit virtual methods follow the same "remove observer from array" pattern with CASA locking.
4. **IONVRAMController metaclass string** → Found at `0xA2FEB`. Has ADRP+ADD refs at `0x125D2C0`, `0x125D310`, `0x125D38C` (metaclass constructors). These set up the metaclass, NOT instance methods.
5. **Chained fixup pointer search for IONVRAMController string** → Failed (different encoding).
### Findings That DO Work
**IONVRAMController vtable found via chained fixup search:**
The verifyPermission function at `0x1233E40` is referenced as a chained fixup pointer in `__DATA_CONST`:
```
__DATA_CONST @ 0x7410B8: raw=0x8011377101233E40 → decoded=0x1233E40 (verifyPermission)
```
**Vtable layout at 0x7410B8:**
| Vtable Idx | File Offset | Content | First Insn |
|------------|-------------|---------|------------|
| [-3] 0x7410A0 | | NULL | |
| [-2] 0x7410A8 | | NULL | |
| [-1] 0x7410B0 | | NULL | |
| [0] 0x7410B8 | 0x1233E40 | **verifyPermission** | pacibsp |
| [1] 0x7410C0 | 0x1233BF0 | sister method | pacibsp |
| [2] 0x7410C8 | 0x10EA4E0 | | ret |
| [3] 0x7410D0 | 0x10EA4D8 | | mov |
**IONVRAMController metaclass constructor pattern:**
```
0x125D2C0: pacibsp
adrp x0, #0x26fe000
add x0, x0, #0xa38 ; x0 = metaclass obj @ 0x26FEA38
adrp x1, #0xa2000
add x1, x1, #0xfeb ; x1 = "IONVRAMController" @ 0xA2FEB
adrp x2, #0x26fe000
add x2, x2, #0xbf0 ; x2 = superclass metaclass @ 0x26FEBF0
mov w3, #0x88 ; w3 = instance size = 136
bl OSMetaClass::OSMetaClass() ; [5236 callers]
adrp x16, #0x76d000
add x16, x16, #0xd60
add x16, x16, #0x10 ; x16 = metaclass vtable @ 0x76DD70
movk x17, #0xcda1, lsl #48 ; PAC discriminator
pacda x16, x17
str x16, [x0] ; store PAC'd metaclass vtable
retab
```
**There's ALSO a combined class registration function at 0x12376D8** that registers multiple classes and references the instance vtable:
```
0x12377F8: adrp x16, #0x741000
add x16, x16, #0x0a8 ; → 0x7410A8 (vtable[-2])
```
Wait — it actually points to `0x7410A8`, not `0x7410B8`. The vtable pointer with the +0x10 adjustment gives `0x7410A8 + 0x10 = 0x7410B8` which is entry [0]. This is how IOKit vtables work: the isa pointer stores `vtable_base + 0x10` to skip the RTTI header.
### Proposed Dynamic Strategy
**Chain**: "IONVRAMController" string → ADRP+ADD refs → metaclass constructor → extract instance size `0x88` → find the combined class registration function (0x12376D8) that calls OSMetaClass::OSMetaClass() with `mov w3, #0x88` AND uses "IONVRAMController" name → extract the vtable base from the ADRP+ADD+ADD that follows → vtable[0] = verifyPermission → find BL to memmove-like target (>2000 callers) and NOP it.
**Alternative (simpler)**: From the metaclass constructor, extract the PAC discriminator `#0xcda1` and the instance size `#0x88`. Then search __DATA_CONST for chained fixup pointer entries where:
- The preceding 3 entries (at -8, -16, -24) are NULL (vtable header)
- The decoded function pointer has 0 BL callers
- The function contains CASA
- The function ends with RETAB
- The function contains a BL to memmove (>2000 callers)
- **The function contains `movk x17, #0xcda1`** (the IONVRAMController PAC discriminator)
This last filter is the KEY discriminator. Among the 332 candidate functions, only IONVRAMController methods use PAC disc `0xcda1`. Combined with "first entry in vtable" (preceded by 3 nulls), this should be unique.
**Simplest approach**: Search all chained fixup pointers in __DATA_CONST where:
1. Preceded by 3 null entries (vtable start)
2. Decoded target is a function in kern_text
3. Function contains `movk x17, #0xcda1, lsl #48`
4. Function contains BL to target with >2000 callers (memmove)
5. NOP that BL
---
## Patch 2: `patch_thid_should_crash` — FAILING
### Upstream Reference
```python
# patch _thid_should_crash to 0
patch(0x67EB50, 0x0)
```
Writes 4 bytes of zero at file offset `0x67EB50`.
### Analysis
- Offset `0x67EB50` is in a **DATA segment** (not code)
- The current value at this offset is **already 0x00000000** in the test kernel
- This is a sysctl boolean variable (`kern.thid_should_crash`)
- The patch is effectively a **no-op** on this kernel
### What I've Tried
1. **Symbol resolution** → 0 symbols, fails.
2. **"thid_should_crash" string** → Found, but has **no ADRP+ADD code references**. The string is in `__PRELINK_INFO` (XML plist), not in a standalone `__cstring` section.
3. **Sysctl structure search** → Searched for a raw VA pointer to the string in DATA segments. Failed because the string VA is in the plist text, not a standalone pointer.
4. **Pattern search for value=1** → The value is already 0 at the upstream offset, so searching for value=1 finds nothing.
### Proposed Dynamic Strategy
The variable at `0x67EB50` is in the kernel's `__DATA` segment (BSS or initialized data). Since:
- The string is only in `__PRELINK_INFO` (plist), not usable as a code anchor
- The variable has no symbols
- The value is already 0
**Option A: Skip this patch gracefully.** If the value is already 0, the patch has no effect. Log a message and return True (success, nothing to do).
**Option B: Find via sysctl table structure.** The sysctl_oid structure in __DATA contains:
- A pointer to the name string
- A pointer to the data variable
- Various flags
But the name string pointer would be a chained fixup pointer to the string in __PRELINK_INFO, which is hard to search for.
**Option C: Find via `__PRELINK_INFO` plist parsing.** Parse the XML plist to find the `_PrelinkKCID` or sysctl registration info. This is complex and fragile.
**Recommended: Option A** — the variable is already 0 in PCC kernels. Emit a write-zero anyway at the upstream-equivalent location if we can find it, or just return True if we can't find the variable (safe no-op).
Actually, better approach: search `__DATA` segments for a `sysctl_oid` struct. The struct layout includes:
```c
struct sysctl_oid {
struct sysctl_oid_list *oid_parent; // +0x00
SLIST_ENTRY(sysctl_oid) oid_link; // +0x08
int oid_number; // +0x10
int oid_kind; // +0x14
void *oid_arg1; // +0x18 → points to the variable
int oid_arg2; // +0x20
const char *oid_name; // +0x28 → points to "thid_should_crash" string
...
};
```
So search all `__DATA` segments for an 8-byte value at offset +0x28 that decodes to the "thid_should_crash" string offset. Then read +0x18 to get the variable pointer.
But the string is in __PRELINK_INFO, which complicates decoding the chained fixup pointer.
---
## Patch 3: `patch_hook_cred_label_update_execve` — FAILING
### Upstream Reference
```python
# Shellcode at 0xAB17D8 (46 instructions, ~184 bytes)
# Two critical BL targets:
# BL _vfs_context_current at idx 9: 0x940851AC → target = 0xCC5EAC
# BL _vnode_getattr at idx 17: 0x94085E69 → target = 0xCC91C0
# Ops table patch at 0xA54518: redirect to shellcode
# B _hook_cred_label_update_execve at idx 44: 0x146420B7 → target = 0x239A0B4
```
### Why It Fails
The patch needs two kernel functions that have **no symbols**:
- `_vfs_context_current` at file offset `0xCC5EAC`
- `_vnode_getattr` at file offset `0xCC91C0`
Without these, the shellcode can't be assembled (the BL offsets depend on the target addresses).
### Analysis of _vfs_context_current (0xCC5EAC)
```
Expected: A very short function (2-4 instructions) that:
- Reads the current thread (mrs xN, TPIDR_EL1 or load from per-CPU data)
- Loads the VFS context from the thread struct
- Returns it in x0
Should have extremely high caller count (VFS is used everywhere).
```
Let me verify: check `bl_callers.get(0xCC5EAC, [])` — should have many callers.
### Analysis of _vnode_getattr (0xCC91C0)
```
Expected: A moderate-sized function that:
- Takes (vnode, vnode_attr, vfs_context) parameters
- Calls the vnode op (VNOP_GETATTR)
- Returns error code
Should have moderate caller count (hundreds).
```
### Finding Strategy for _vfs_context_current
1. **From sandbox ops table**: We already have `_find_sandbox_ops_table_via_conf()`. The hook_cred_label_update_execve entry (index 16) in the ops table points to the original sandbox hook function (at `0x239A0B4` per upstream).
2. **From the original hook function**: Disassemble the original hook function. It likely calls `_vfs_context_current` (to get the VFS context for vnode operations). Find the BL target in the hook that has a very high caller count — that's likely `_vfs_context_current`.
3. **Pattern match**: Search kern_text for short functions (size < 0x20) with:
- `mrs xN, TPIDR_EL1` instruction
- Very high caller count (>1000)
- Return type is pointer (loads from struct offset)
### Finding Strategy for _vnode_getattr
1. **From the original hook function**: The hook function likely also calls `_vnode_getattr`. Find BL targets in the hook that have moderate caller count.
2. **String anchor**: Search for `"vnode_getattr"` string (not in plist but in `__cstring`). Find ADRP+ADD refs, trace to function.
3. **Pattern match**: The function signature includes a `vnode_attr` structure initialization with size `0x380`.
### Proposed Implementation
```
1. Find sandbox ops table → read entry at index 16 → get original hook func
2. Disassemble original hook function
3. Find _vfs_context_current: BL target in the hook with highest caller count (>1000)
4. Find _vnode_getattr: BL target that:
- Has moderate callers (50-1000)
- The calling site has nearby `mov wN, #0x380` (vnode_attr struct size)
5. With both functions found, build shellcode and patch ops table
```
---
## Patch Status Summary
| Patch | Status | Blocker | Strategy |
|-------|--------|---------|----------|
| nvram_verify_permission | FAILING | Can't distinguish among 332 identical IOKit methods | Use PAC disc `#0xcda1` + vtable header (3 nulls) to find unique IONVRAMController vtable entry |
| thid_should_crash | FAILING | String in __PRELINK_INFO, no code refs, value already 0 | Option A: return True (safe no-op); Option B: sysctl_oid struct search |
| hook_cred_label_update_execve | FAILING | Can't find vfs_context_current and vnode_getattr without symbols | Find via sandbox ops table → original hook → BL targets by caller count |
---
## Previously Fixed Patches (This Session)
### patch_task_for_pid — FIXED
**Problem**: Old code searched for "proc_ro_ref_task" string → wrong function.
**Solution**: Pattern search: 0 BL callers + 2x ldadda + 2x `ldr wN,[xN,#0x490]; str wN,[xN,#0xc]` + movk #0xc8a2 + non-panic BL >500 callers. NOP the second `ldr wN,[xN,#0x490]`.
**Upstream**: `patch(0xFC383C, 0xd503201f)` — NOP in function at `0xFC3718`.
### patch_load_dylinker — FIXED
**Problem**: Old code searched for "/usr/lib/dyld" → wrong function (0 BL callers, no string ref).
**Solution**: Search for functions with 3+ `TST xN, #-0x40000000000000; B.EQ; MOVK xN, #0xc8a2` triplets and 0 BL callers. Replace LAST TST with unconditional B to B.EQ target.
**Upstream**: `patch(0x1052A28, B #0x44)` — in function at `0x105239C`.
### patch_syscallmask_apply_to_proc — FIXED
**Problem**: `bl_callers` key bug: code used `target + self.base_va` but bl_callers is keyed by file offset.
**Fix**: Changed to `self.bl_callers.get(target, [])` at line ~1661.
**Status**: Now PASSING (40 patches emitted for shellcode + redirect).
---
## Environment Notes
### Running on macOS (current)
```bash
cd /Users/qaq/Documents/GitHub/vphone-cli
source .venv/bin/activate
python3 -c "
import sys; sys.path.insert(0, 'scripts')
from fw_patch import load_firmware
from patchers.kernel_jb import KernelJBPatcher
_, data, _, _ = load_firmware('vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600')
p = KernelJBPatcher(data)
patches = p.find_all()
print(f'Total patches: {len(patches)}')
"
```
### Running on Linux (cloud)
Requirements:
- Python 3.10+
- `pip install capstone keystone-engine pyimg4`
- Note: `keystone-engine` may need `cmake` and C++ compiler on Linux
- Copy the kernelcache file and upstream reference
- The `setup_venv.sh` script has macOS-specific keystone dylib handling — on Linux, pip install should work directly
Files needed:
- `scripts/patchers/kernel.py` (base class)
- `scripts/patchers/kernel_jb.py` (JB patcher)
- `scripts/patchers/__init__.py`
- `scripts/fw_patch.py` (for `load_firmware()`)
- `vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600` (test kernel)
- `/Users/qaq/Documents/GitHub/super-tart-vphone/CFW/patch_fw.py` (upstream reference)
### Quick Test Script
```python
#!/usr/bin/env python3
"""Quick test for failing patches."""
import sys
sys.path.insert(0, 'scripts')
from fw_patch import load_firmware
from patchers.kernel_jb import KernelJBPatcher
_, data, _, _ = load_firmware('vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600')
p = KernelJBPatcher(data, verbose=True)
failing = ['patch_nvram_verify_permission', 'patch_thid_should_crash',
'patch_hook_cred_label_update_execve']
for name in failing:
p.patches = []
result = getattr(p, name)()
status = "PASS" if result else "FAIL"
print(f'\n>>> {name}: {status} ({len(p.patches)} patches)')
```
---
## Upstream Offsets Reference (iPhone17,3 26.1 23B85)
| Symbol / Patch | File Offset | Notes |
|----------------|-------------|-------|
| kern_text start | 0xA74000 | |
| kern_text end | 0x24B0000 | |
| base_va | 0xFFFFFE0007004000 | |
| _thid_should_crash var | 0x67EB50 | DATA, value=0 |
| _task_for_pid func | 0xFC3718 | patch at 0xFC383C |
| _load_dylinker patch | 0x1052A28 | TST → B |
| verifyPermission func | 0x1233E40 | patch BL at 0x1234034 |
| verifyPermission vtable | 0x7410B8 | __DATA_CONST |
| IONVRAMController metaclass | 0x26FEA38 | |
| IONVRAMController metaclass ctor | 0x125D2C0 | refs "IONVRAMController" string |
| IONVRAMController PAC disc | 0xcda1 | movk x17, #0xcda1 |
| IONVRAMController instance size | 0x88 | mov w3, #0x88 |
| _vfs_context_current | 0xCC5EAC | (from upstream BL encoding) |
| _vnode_getattr | 0xCC91C0 | (from upstream BL encoding) |
| shellcode cave (upstream) | 0xAB1740 | syscallmask |
| shellcode cave 2 (upstream) | 0xAB17D8 | hook_cred_label |
| sandbox ops table (hook entry) | 0xA54518 | index 16 |
| _hook_cred_label_update_execve | 0x239A0B4 | original hook func |
| memmove | 0x12CB0D0 | 3114 callers |
| OSMetaClass::OSMetaClass() | 0x10EA790 | 5236 callers |
| _panic | varies | 8000+ callers typically |
+7 -3
View File
@@ -1,5 +1,5 @@
#!/bin/zsh
# cfw_install.sh — Install CFW modifications on vphone via SSH ramdisk.
# cfw_install.sh — Install base CFW modifications on vphone via SSH ramdisk.
#
# Installs Cryptexes, patches system binaries, installs jailbreak tools
# and configures LaunchDaemons for persistent SSH/VNC access.
@@ -19,6 +19,7 @@ set -euo pipefail
VM_DIR="${1:-.}"
SCRIPT_DIR="${0:a:h}"
CFW_SKIP_HALT="${CFW_SKIP_HALT:-0}"
# Resolve absolute paths
VM_DIR="$(cd "$VM_DIR" && pwd)"
@@ -370,5 +371,8 @@ echo "[+] CFW installation complete!"
echo " Reboot the device for changes to take effect."
echo " After boot, SSH will be available on port 22222 (password: alpine)"
ssh_cmd "/sbin/halt" || true
if [[ "$CFW_SKIP_HALT" == "1" ]]; then
echo "[*] CFW_SKIP_HALT=1, skipping halt."
else
ssh_cmd "/sbin/halt" || true
fi
+214
View File
@@ -0,0 +1,214 @@
#!/bin/zsh
# cfw_install_jb.sh — Install base CFW + JB extensions on vphone via SSH ramdisk.
#
# Runs the base CFW installer first (phases 1-7), then applies JB-specific
# modifications: launchd jetsam patch, dylib injection, procursus bootstrap,
# and BaseBin hook deployment.
#
# 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_jb
set -euo pipefail
VM_DIR="${1:-.}"
SCRIPT_DIR="${0:a:h}"
# ════════════════════════════════════════════════════════════════
# Step 1: Run base CFW install (skip halt — we continue with JB phases)
# ════════════════════════════════════════════════════════════════
echo "[*] cfw_install_jb.sh — Installing CFW + JB extensions..."
echo ""
CFW_SKIP_HALT=1 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"
SSH_PORT=2222
SSH_PASS="alpine"
SSH_USER="root"
SSH_HOST="localhost"
SSH_OPTS=(
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o PreferredAuthentications=password
-o ConnectTimeout=30
-q
)
# ── Helpers ─────────────────────────────────────────────────────
die() { echo "[-] $*" >&2; exit 1; }
_sshpass() {
"$VM_DIR/$CFW_INPUT/tools/sshpass" -p "$SSH_PASS" "$@"
}
ssh_cmd() {
_sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@"
}
scp_to() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" -r "$1" "$SSH_USER@$SSH_HOST:$2"
}
scp_from() {
_sshpass scp -q "${SSH_OPTS[@]}" -P "$SSH_PORT" "$SSH_USER@$SSH_HOST:$1" "$2"
}
remote_file_exists() {
ssh_cmd "test -f '$1'" 2>/dev/null
}
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")
"$VM_DIR/$CFW_INPUT/tools/ldid_macosx_arm64" "${args[@]}" "$file"
}
remote_mount() {
local dev="$1" mnt="$2" opts="${3:-rw}"
ssh_cmd "/sbin/mount_apfs -o $opts $dev $mnt 2>/dev/null || true"
}
get_boot_manifest_hash() {
ssh_cmd "/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"
}
# ── 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"
mkdir -p "$TEMP_DIR"
# Mount device rootfs (may already be mounted from base install)
remote_mount /dev/disk1s1 /mnt1
# ═══════════ JB-1 PATCH LAUNCHD (JETSAM + DYLIB INJECTION) ════
echo ""
echo "[JB-1] Patching launchd (jetsam guard + hook injection)..."
if ! remote_file_exists "/mnt1/sbin/launchd.bak"; then
echo " Creating backup..."
ssh_cmd "/bin/cp /mnt1/sbin/launchd /mnt1/sbin/launchd.bak"
fi
scp_from "/mnt1/sbin/launchd.bak" "$TEMP_DIR/launchd"
# Inject launchdhook.dylib load command (idempotent — skips if already present)
if [[ -d "$JB_INPUT_DIR/basebin" ]]; then
echo " Injecting LC_LOAD_DYLIB for /cores/launchdhook.dylib..."
python3 "$SCRIPT_DIR/patchers/cfw.py" inject-dylib "$TEMP_DIR/launchd" "/cores/launchdhook.dylib"
fi
python3 "$SCRIPT_DIR/patchers/cfw.py" patch-launchd-jetsam "$TEMP_DIR/launchd"
ldid_sign "$TEMP_DIR/launchd"
scp_to "$TEMP_DIR/launchd" "/mnt1/sbin/launchd"
ssh_cmd "/bin/chmod 0755 /mnt1/sbin/launchd"
echo " [+] launchd patched"
# ═══════════ JB-2 INSTALL PROCURSUS BOOTSTRAP ══════════════════
echo ""
echo "[JB-2] Installing procursus bootstrap..."
remote_mount /dev/disk1s5 /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"
scp_to "$BOOTSTRAP_TAR" "/mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar"
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"
ssh_cmd "/bin/rm -f /mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar"
rm -f "$BOOTSTRAP_TAR"
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/..."
ssh_cmd "/bin/mkdir -p /mnt1/cores"
ssh_cmd "/bin/chmod 0755 /mnt1/cores"
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"
done
echo " [+] BaseBin hooks deployed"
fi
# ═══════════ CLEANUP ═════════════════════════════════════════
echo ""
echo "[*] Unmounting device filesystems..."
ssh_cmd "/sbin/umount /mnt1 2>/dev/null || true"
ssh_cmd "/sbin/umount /mnt3 2>/dev/null || true"
ssh_cmd "/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 installation complete!"
echo " Reboot the device for changes to take effect."
echo " After boot, SSH will be available on port 22222 (password: alpine)"
ssh_cmd "/sbin/halt" || true
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
fw_patch_jb.py — Apply jailbreak extension patches after base fw_patch.
Usage:
python3 fw_patch_jb.py [vm_directory]
This script runs base `fw_patch.py` first, then applies additional JB-oriented
patches found dynamically.
"""
import os
import subprocess
import sys
from fw_patch import (
find_file,
find_restore_dir,
load_firmware,
save_firmware,
)
from patchers.iboot_jb import IBootJBPatcher
from patchers.kernel_jb import KernelJBPatcher
from patchers.txm_jb import TXMJBPatcher
def patch_ibss_jb(data):
p = IBootJBPatcher(data, mode="ibss", label="Loaded iBSS", verbose=True)
n = p.apply()
print(f" [+] {n} iBSS JB patches applied dynamically")
return n > 0
def patch_kernelcache_jb(data):
kp = KernelJBPatcher(data, verbose=True)
n = kp.apply()
print(f" [+] {n} kernel JB patches applied dynamically")
return n > 0
def patch_txm_jb(data):
p = TXMJBPatcher(data, verbose=True)
n = p.apply()
print(f" [+] {n} TXM JB patches applied dynamically")
return n > 0
COMPONENTS = [
# (name, search_base_is_restore, search_patterns, patch_function, preserve_payp)
("iBSS (JB)", True,
["Firmware/dfu/iBSS.vresearch101.RELEASE.im4p"],
patch_ibss_jb, False),
("TXM (JB)", True,
["Firmware/txm.iphoneos.research.im4p"],
patch_txm_jb, True),
("kernelcache (JB)", True,
["kernelcache.research.vphone600"],
patch_kernelcache_jb, True),
]
def patch_component(path, patch_fn, name, preserve_payp):
print(f"\n{'=' * 60}")
print(f" {name}: {path}")
print(f"{'=' * 60}")
im4p, data, was_im4p, original_raw = load_firmware(path)
fmt = "IM4P" if was_im4p else "raw"
extra = f", fourcc={im4p.fourcc}" if was_im4p and im4p else ""
print(f" format: {fmt}{extra}, {len(data)} bytes")
if not patch_fn(data):
print(f" [-] FAILED: {name}")
sys.exit(1)
save_firmware(path, im4p, data, was_im4p,
original_raw if preserve_payp else None)
print(f" [+] saved ({fmt})")
def main():
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)
script_dir = os.path.dirname(os.path.abspath(__file__))
fw_patch_script = os.path.join(script_dir, "fw_patch.py")
print("[*] Running base fw_patch first ...", flush=True)
subprocess.run([sys.executable, fw_patch_script, vm_dir], check=True)
restore_dir = find_restore_dir(vm_dir)
if not restore_dir:
print(f"[-] No *Restore* directory found in {vm_dir}")
sys.exit(1)
print(f"[*] VM directory: {vm_dir}")
print(f"[*] Restore directory: {restore_dir}")
print(f"[*] Applying {len(COMPONENTS)} JB extension components ...")
for name, in_restore, patterns, patch_fn, preserve_payp in COMPONENTS:
search_base = restore_dir if in_restore else vm_dir
path = find_file(search_base, patterns, name)
patch_component(path, patch_fn, name, preserve_payp)
print(f"\n{'=' * 60}")
print(" JB extension patching complete!")
print(f"{'=' * 60}")
if __name__ == "__main__":
main()
+424 -9
View File
@@ -20,9 +20,16 @@ Commands:
patch-mobileactivationd <binary>
Patch -[DeviceType should_hactivate] to always return true.
patch-launchd-jetsam <binary>
Patch launchd jetsam panic guard to avoid initproc crash loop.
inject-daemons <launchd.plist> <daemon_dir>
Inject bash/dropbear/trollvnc into launchd.plist.
inject-dylib <binary> <dylib_path>
Inject LC_LOAD_DYLIB into Mach-O binary (thin or universal).
Equivalent to: optool install -c load -p <dylib_path> -t <binary>
Dependencies:
pip install capstone keystone-engine
"""
@@ -34,6 +41,7 @@ import subprocess
import sys
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
from capstone.arm64_const import ARM64_OP_IMM
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
# ══════════════════════════════════════════════════════════════════
@@ -52,6 +60,13 @@ def asm(s):
return bytes(enc)
def asm_at(s, addr):
enc, _ = _ks.asm(s, addr=addr)
if not enc:
raise RuntimeError(f"asm failed at 0x{addr:X}: {s}")
return bytes(enc)
NOP = asm("nop")
MOV_X0_1 = asm("mov x0, #1")
RET = asm("ret")
@@ -70,6 +85,14 @@ def disasm_at(data, off, n=8):
return list(_cs.disasm(bytes(data[off : off + n * 4]), off))
def _log_asm(data, offset, count=5, marker_off=-1):
"""Log disassembly of `count` instructions at file offset for before/after comparison."""
insns = disasm_at(data, offset, count)
for insn in insns:
tag = " >>>" if insn.address == marker_off else " "
print(f" {tag} 0x{insn.address:08X}: {insn.mnemonic:8s} {insn.op_str}")
# ══════════════════════════════════════════════════════════════════
# Mach-O helpers
# ══════════════════════════════════════════════════════════════════
@@ -210,10 +233,14 @@ def patch_seputil(filepath):
original = bytes(data[offset : offset + len(anchor)])
print(f" Found format string at 0x{offset:X}: {original!r}")
print(f" Before: {bytes(data[offset:offset+7]).hex(' ')}")
# Replace %s (2 bytes) with AA — turns "/%s.gl" into "/AA.gl"
data[pct_s_off] = ord("A")
data[pct_s_off + 1] = ord("A")
print(f" After: {bytes(data[offset:offset+7]).hex(' ')}")
open(filepath, "wb").write(data)
print(f" [+] Patched at 0x{pct_s_off:X}: %s -> AA")
print(f" /{anchor[1:-1].decode()} -> /AA.gl")
@@ -307,12 +334,15 @@ def patch_launchd_cache_loader(filepath):
# So only search forward from the ref, not backwards.
branch_foff = _find_nearby_branch(data, ref_foff, text_foff, text_size)
if branch_foff >= 0:
insns = disasm_at(data, branch_foff, 1)
if insns:
print(
f" Patching: {insns[0].mnemonic} {insns[0].op_str} -> nop"
)
ctx_start = max(text_foff, branch_foff - 8)
print(f" Before:")
_log_asm(data, ctx_start, 5, branch_foff)
data[branch_foff : branch_foff + 4] = NOP
print(f" After:")
_log_asm(data, ctx_start, 5, branch_foff)
open(filepath, "wb").write(data)
print(f" [+] NOPped at 0x{branch_foff:X}")
return True
@@ -463,19 +493,160 @@ def patch_mobileactivationd(filepath):
print(f" [-] IMP offset 0x{imp_foff:X} out of bounds")
return False
insns = disasm_at(data, imp_foff, 4)
if insns:
print(f" Original: {insns[0].mnemonic} {insns[0].op_str}")
print(f" Before:")
_log_asm(data, imp_foff, 4, imp_foff)
# Patch to: mov x0, #1; ret
data[imp_foff : imp_foff + 4] = MOV_X0_1
data[imp_foff + 4 : imp_foff + 8] = RET
print(f" After:")
_log_asm(data, imp_foff, 4, imp_foff)
open(filepath, "wb").write(data)
print(f" [+] Patched at 0x{imp_foff:X}: mov x0, #1; ret")
return True
# ══════════════════════════════════════════════════════════════════
# 4. launchd — Jetsam panic bypass
# ══════════════════════════════════════════════════════════════════
def _extract_branch_target_off(insn):
for op in reversed(insn.operands):
if op.type == ARM64_OP_IMM:
return op.imm
return -1
def _is_return_block(data, foff, text_foff, text_size):
"""Check if foff points to a function return sequence (ret/retab within 8 insns)."""
for i in range(8):
check = foff + i * 4
if check >= text_foff + text_size:
break
insns = disasm_at(data, check, 1)
if not insns:
continue
if insns[0].mnemonic in ("ret", "retab"):
return True
# Stop at unconditional branches (different block)
if insns[0].mnemonic in ("b", "bl", "br", "blr"):
break
return False
def patch_launchd_jetsam(filepath):
"""Bypass launchd jetsam panic path via dynamic string-xref branch rewrite.
Anchor strategy:
1. Find jetsam panic string in cstring-like data.
2. Find ADRP+ADD xref to the string start in __TEXT,__text.
3. Search backward for a conditional branch whose target is the function's
return/success path (basic block containing ret/retab).
4. Rewrite that conditional branch to unconditional `b <same_target>`,
so the function always returns success and never reaches the panic.
"""
data = bytearray(open(filepath, "rb").read())
sections = parse_macho_sections(data)
text_sec = find_section(sections, "__TEXT,__text")
if not text_sec:
print(" [-] __TEXT,__text not found")
return False
text_va, text_size, text_foff = text_sec
code = bytes(data[text_foff : text_foff + text_size])
cond_mnemonics = {
"b.eq", "b.ne", "b.cs", "b.hs", "b.cc", "b.lo",
"b.mi", "b.pl", "b.vs", "b.vc", "b.hi", "b.ls",
"b.ge", "b.lt", "b.gt", "b.le",
"cbz", "cbnz", "tbz", "tbnz",
}
anchors = [
b"jetsam property category (Daemon) is not initialized",
b"jetsam property category",
b"initproc exited -- exit reason namespace 7 subcode 0x1",
]
for anchor in anchors:
hit_off = data.find(anchor)
if hit_off < 0:
continue
sec_foff = -1
sec_va = -1
for _, (sva, ssz, sfoff) in sections.items():
if sfoff <= hit_off < sfoff + ssz:
sec_foff = sfoff
sec_va = sva
break
if sec_foff < 0:
continue
str_start_off = _find_cstring_start(data, hit_off, sec_foff)
str_start_va = sec_va + (str_start_off - sec_foff)
ref_va = _find_adrp_add_ref(code, text_va, str_start_va)
if ref_va < 0:
continue
ref_foff = text_foff + (ref_va - text_va)
print(f" Found jetsam anchor '{anchor.decode(errors='ignore')}'")
print(f" string start: va:0x{str_start_va:X}")
print(f" xref at foff:0x{ref_foff:X}")
# Search backward from xref for conditional branches targeting
# the function's return path (block containing ret/retab).
# Pick the earliest (farthest back) one — it skips the most
# jetsam-related code and matches the upstream patch strategy.
scan_lo = max(text_foff, ref_foff - 0x300)
patch_off = -1
patch_target = -1
for back in range(ref_foff - 4, scan_lo - 1, -4):
insns = disasm_at(data, back, 1)
if not insns:
continue
insn = insns[0]
if insn.mnemonic not in cond_mnemonics:
continue
tgt = _extract_branch_target_off(insn)
if tgt < 0:
continue
# Target must be a valid file offset within __text
if tgt < text_foff or tgt >= text_foff + text_size:
continue
# Target must be a return block (contains ret/retab)
if _is_return_block(data, tgt, text_foff, text_size):
patch_off = back
patch_target = tgt
# Don't break — keep scanning for an earlier match
if patch_off < 0:
continue
ctx_start = max(text_foff, patch_off - 8)
print(f" Before:")
_log_asm(data, ctx_start, 5, patch_off)
data[patch_off : patch_off + 4] = asm_at(f"b #0x{patch_target:X}", patch_off)
print(f" After:")
_log_asm(data, ctx_start, 5, patch_off)
open(filepath, "wb").write(data)
print(f" [+] Patched at 0x{patch_off:X}: jetsam panic guard bypass")
return True
print(" [-] Dynamic jetsam anchor/xref not found")
return False
def _find_via_objc_metadata(data):
"""Find method IMP through ObjC runtime metadata."""
sections = parse_macho_sections(data)
@@ -574,6 +745,235 @@ def _find_via_objc_metadata(data):
return -1
# ══════════════════════════════════════════════════════════════════
# 5. Mach-O dylib injection (optool replacement)
# ══════════════════════════════════════════════════════════════════
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
# ══════════════════════════════════════════════════════════════════
@@ -676,16 +1076,31 @@ def main():
if not patch_mobileactivationd(sys.argv[2]):
sys.exit(1)
elif cmd == "patch-launchd-jetsam":
if len(sys.argv) < 3:
print("Usage: patch_cfw.py patch-launchd-jetsam <binary>")
sys.exit(1)
if not patch_launchd_jetsam(sys.argv[2]):
sys.exit(1)
elif cmd == "inject-daemons":
if len(sys.argv) < 4:
print("Usage: patch_cfw.py inject-daemons <launchd.plist> <daemon_dir>")
sys.exit(1)
inject_daemons(sys.argv[2], sys.argv[3])
elif cmd == "inject-dylib":
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]):
sys.exit(1)
else:
print(f"Unknown command: {cmd}")
print("Commands: cryptex-paths, patch-seputil, patch-launchd-cache-loader,")
print(" patch-mobileactivationd, inject-daemons")
print(" patch-mobileactivationd, patch-launchd-jetsam,")
print(" inject-daemons, inject-dylib")
sys.exit(1)
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
iboot_jb.py — Jailbreak extension patcher for iBoot-based images.
Currently adds iBSS-only nonce generation bypass used by fw_patch_jb.py.
"""
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
from capstone.arm64_const import ARM64_OP_IMM, ARM64_OP_REG, ARM64_REG_W0
from .iboot import IBootPatcher, _disasm_one
_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE)
class IBootJBPatcher(IBootPatcher):
"""JB-only patcher for iBoot images."""
def _asm_at(self, asm_line, addr):
enc, _ = _ks.asm(asm_line, addr=addr)
if not enc:
raise RuntimeError(f"asm failed at 0x{addr:X}: {asm_line}")
return bytes(enc)
def apply(self):
self.patches = []
if self.mode == "ibss":
self.patch_skip_generate_nonce()
for off, pb, _ in self.patches:
self.data[off:off + len(pb)] = pb
if self.verbose and self.patches:
self._log(f"\n [{len(self.patches)} {self.mode.upper()} JB patches applied]")
return len(self.patches)
def _find_refs_to_offset(self, target_off):
refs = []
for insns in self._chunked_disasm():
for i in range(len(insns) - 1):
a, b = insns[i], insns[i + 1]
if a.mnemonic != "adrp" or b.mnemonic != "add":
continue
if len(a.operands) < 2 or len(b.operands) < 3:
continue
if a.operands[0].reg != b.operands[1].reg:
continue
if a.operands[1].imm + b.operands[2].imm == target_off:
refs.append((a.address, b.address, b.operands[0].reg))
return refs
def _find_string_refs(self, needle):
if isinstance(needle, str):
needle = needle.encode()
seen = set()
refs = []
off = 0
while True:
s_off = self.raw.find(needle, off)
if s_off < 0:
break
off = s_off + 1
for r in self._find_refs_to_offset(s_off):
if r[0] not in seen:
seen.add(r[0])
refs.append(r)
return refs
def patch_skip_generate_nonce(self):
refs = self._find_string_refs(b"boot-nonce")
if not refs:
self._log(" [-] iBSS JB: no refs to 'boot-nonce'")
return False
for _, add_off, _ in refs:
for scan in range(add_off, min(add_off + 0x100, self.size - 12), 4):
i0 = _disasm_one(self.raw, scan)
i1 = _disasm_one(self.raw, scan + 4)
i2 = _disasm_one(self.raw, scan + 8)
if not i0 or not i1 or not i2:
continue
if i0.mnemonic not in ("tbz", "tbnz"):
continue
if len(i0.operands) < 3:
continue
if not (i0.operands[0].type == ARM64_OP_REG
and i0.operands[0].reg == ARM64_REG_W0):
continue
if not (i0.operands[1].type == ARM64_OP_IMM
and i0.operands[1].imm == 0):
continue
if i1.mnemonic != "mov" or i1.op_str != "w0, #0":
continue
if i2.mnemonic != "bl":
continue
target = i0.operands[2].imm
self.emit(scan, self._asm_at(f"b #0x{target:X}", scan),
"JB: skip generate_nonce")
return True
self._log(" [-] iBSS JB: generate_nonce branch pattern not found")
return False
+1
View File
@@ -1288,6 +1288,7 @@ class KernelPatcher:
def find_all(self):
"""Find and record all kernel patches. Returns list of (offset, bytes, desc)."""
self.patches = []
self.patch_apfs_root_snapshot() # 1
self.patch_apfs_seal_broken() # 2
self.patch_bsd_init_rootvp() # 3
File diff suppressed because it is too large Load Diff
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
txm_jb.py Jailbreak extension patcher for TXM images.
All patch sites are found dynamically via string xrefs + instruction pattern
matching. No fixed byte offsets.
"""
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
from .txm import TXMPatcher, MOV_X0_0, _asm, _disasm_one
_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE)
NOP = _asm("nop")
MOV_X0_1 = _asm("mov x0, #1")
MOV_W0_1 = _asm("mov w0, #1")
MOV_X0_X20 = _asm("mov x0, x20")
STRB_W0_X20_30 = _asm("strb w0, [x20, #0x30]")
PACIBSP = _asm("hint #27")
class TXMJBPatcher(TXMPatcher):
"""JB-only TXM patcher."""
def apply(self):
self.find_all()
for off, pb, _ in self.patches:
self.data[off:off + len(pb)] = pb
if self.verbose and self.patches:
self._log(f"\n [{len(self.patches)} TXM JB patches applied]")
return len(self.patches)
def find_all(self):
self.patches = []
self.patch_selector24_hashcmp_calls()
self.patch_selector24_a1_path()
self.patch_get_task_allow_force_true()
self.patch_selector42_29_shellcode()
self.patch_debugger_entitlement_force_true()
self.patch_developer_mode_bypass()
return self.patches
# ── helpers ──────────────────────────────────────────────────
def _asm_at(self, asm_line, addr):
enc, _ = _ks.asm(asm_line, addr=addr)
if not enc:
raise RuntimeError(f"asm failed at 0x{addr:X}: {asm_line}")
return bytes(enc)
def _find_func_start(self, off, back=0x1000):
start = max(0, off - back)
for scan in range(off & ~3, start - 1, -4):
if self.raw[scan:scan + 4] == PACIBSP:
return scan
return None
def _find_func_end(self, func_start, forward=0x1200):
end = min(self.size, func_start + forward)
for scan in range(func_start + 4, end, 4):
if self.raw[scan:scan + 4] == PACIBSP:
return scan
return end
def _find_refs_to_offset(self, target_off):
refs = []
for off in range(0, self.size - 8, 4):
a = _disasm_one(self.raw, off)
b = _disasm_one(self.raw, off + 4)
if not a or not b:
continue
if a.mnemonic != "adrp" or b.mnemonic != "add":
continue
if len(a.operands) < 2 or len(b.operands) < 3:
continue
if a.operands[0].reg != b.operands[1].reg:
continue
if a.operands[1].imm + b.operands[2].imm == target_off:
refs.append((off, off + 4))
return refs
def _find_string_refs(self, needle):
if isinstance(needle, str):
needle = needle.encode()
refs = []
seen = set()
off = 0
while True:
s_off = self.raw.find(needle, off)
if s_off < 0:
break
off = s_off + 1
for r in self._find_refs_to_offset(s_off):
if r[0] not in seen:
seen.add(r[0])
refs.append((s_off, r[0], r[1]))
return refs
def _ref_in_function(self, refs, func_start):
out = []
for s_off, adrp_off, add_off in refs:
fs = self._find_func_start(adrp_off)
if fs == func_start:
out.append((s_off, adrp_off, add_off))
return out
def _find_debugger_gate_func_start(self):
refs = self._find_string_refs(b"com.apple.private.cs.debugger")
starts = set()
for _, _, add_off in refs:
for scan in range(add_off, min(add_off + 0x20, self.size - 8), 4):
i = _disasm_one(self.raw, scan)
n = _disasm_one(self.raw, scan + 4)
p1 = _disasm_one(self.raw, scan - 4) if scan >= 4 else None
p2 = _disasm_one(self.raw, scan - 8) if scan >= 8 else None
if not all((i, n, p1, p2)):
continue
if not (i.mnemonic == "bl"
and n.mnemonic == "tbnz" and n.op_str.startswith("w0, #0,")
and p1.mnemonic == "mov" and p1.op_str == "x2, #0"
and p2.mnemonic == "mov" and p2.op_str == "x0, #0"):
continue
fs = self._find_func_start(scan)
if fs is not None:
starts.add(fs)
if len(starts) != 1:
return None
return next(iter(starts))
def _find_udf_cave(self, min_insns=6, near_off=None, max_distance=0x80000):
need = min_insns * 4
start = 0 if near_off is None else max(0, near_off - 0x1000)
end = self.size if near_off is None else min(self.size, near_off + max_distance)
best = None
best_dist = None
off = start
while off < end:
run = off
while run < end and self.raw[run:run + 4] == b"\x00\x00\x00\x00":
run += 4
if run - off >= need:
prev = _disasm_one(self.raw, off - 4) if off >= 4 else None
if prev and prev.mnemonic in (
"b", "b.eq", "b.ne", "b.lo", "b.hs", "cbz", "cbnz", "tbz", "tbnz"
):
return off
if near_off is not None and _disasm_one(self.raw, off):
dist = abs(off - near_off)
if best is None or dist < best_dist:
best = off
best_dist = dist
off = run + 4 if run > off else off + 4
return best
# ── JB patches ───────────────────────────────────────────────
def patch_selector24_hashcmp_calls(self):
"""Patch remaining selector-24 hashcmp BL callsites: bl -> mov x0,#0."""
patched = 0
for off in range(0, self.size - 8, 4):
i0 = _disasm_one(self.raw, off)
i1 = _disasm_one(self.raw, off + 4)
i2 = _disasm_one(self.raw, off + 8)
if not i0 or not i1 or not i2:
continue
if not (i0.mnemonic == "mov" and i0.op_str == "w2, #0x14"):
continue
if not (i1.mnemonic == "bl" and i2.mnemonic == "cbz"
and i2.op_str.startswith("w0,")):
continue
self.emit(off + 4, MOV_X0_0,
f"selector24 hashcmp bypass #{patched + 1}: bl -> mov x0,#0")
patched += 1
if patched > 3:
self._log(f" [-] TXM JB: selector24 hashcmp sites too many ({patched})")
return False
if patched == 0:
self._log(" [-] TXM JB: no selector24 hashcmp BL sites to patch")
return False
return True
def patch_selector24_a1_path(self):
"""Selector-24 A1 path bypass: NOP b.lo + cbz around mov w0,#0xa1."""
locs = []
for scan in range(0, self.size - 4, 4):
ins = _disasm_one(self.raw, scan)
if ins and ins.mnemonic == "mov" and ins.op_str == "w0, #0xa1":
i_blo = _disasm_one(self.raw, scan - 0xC)
i_cbz = _disasm_one(self.raw, scan - 0x4)
if not i_blo or not i_cbz:
continue
if (i_blo.mnemonic == "b.lo"
and i_cbz.mnemonic == "cbz"
and i_cbz.op_str.startswith("x9,")):
locs.append(scan)
if len(locs) != 1:
self._log(f" [-] TXM JB: expected 1 selector24 A1 site, found {len(locs)}")
return False
off = locs[0]
self.emit(off - 0xC, NOP, "selector24 A1: b.lo -> nop")
self.emit(off - 0x4, NOP, "selector24 A1: cbz x9 -> nop")
return True
def patch_get_task_allow_force_true(self):
"""Force get-task-allow entitlement call to return true."""
refs = self._find_string_refs(b"get-task-allow")
if not refs:
self._log(" [-] TXM JB: get-task-allow string refs not found")
return False
cands = []
for _, _, add_off in refs:
for scan in range(add_off, min(add_off + 0x20, self.size - 4), 4):
i = _disasm_one(self.raw, scan)
n = _disasm_one(self.raw, scan + 4)
if not i or not n:
continue
if i.mnemonic == "bl" and n.mnemonic == "tbnz" and n.op_str.startswith("w0, #0,"):
cands.append(scan)
if len(cands) != 1:
self._log(f" [-] TXM JB: expected 1 get-task-allow BL site, found {len(cands)}")
return False
self.emit(cands[0], MOV_X0_1, "get-task-allow: bl -> mov x0,#1")
return True
def patch_selector42_29_shellcode(self):
"""Selector 42|29 patch via dynamic cave shellcode + branch redirect."""
fn = self._find_debugger_gate_func_start()
if fn is None:
self._log(" [-] TXM JB: debugger-gate function not found (selector42|29)")
return False
stubs = []
for off in range(4, self.size - 24, 4):
p = _disasm_one(self.raw, off - 4)
i0 = _disasm_one(self.raw, off)
i1 = _disasm_one(self.raw, off + 4)
i2 = _disasm_one(self.raw, off + 8)
i3 = _disasm_one(self.raw, off + 12)
i4 = _disasm_one(self.raw, off + 16)
i5 = _disasm_one(self.raw, off + 20)
if not all((p, i0, i1, i2, i3, i4, i5)):
continue
if not (p.mnemonic == "bti" and p.op_str == "j"):
continue
if not (i0.mnemonic == "mov" and i0.op_str == "x0, x20"):
continue
if not (i1.mnemonic == "bl" and i2.mnemonic == "mov"
and i2.op_str == "x1, x21"):
continue
if not (i3.mnemonic == "mov" and i3.op_str == "x2, x22"
and i4.mnemonic == "bl" and i5.mnemonic == "b"):
continue
if i4.operands and i4.operands[0].imm == fn:
stubs.append(off)
if len(stubs) != 1:
self._log(f" [-] TXM JB: selector42|29 stub expected 1, found {len(stubs)}")
return False
stub_off = stubs[0]
cave = self._find_udf_cave(min_insns=6, near_off=stub_off)
if cave is None:
self._log(" [-] TXM JB: no UDF cave found for selector42|29 shellcode")
return False
self.emit(stub_off, self._asm_at(f"b #0x{cave:X}", stub_off),
"selector42|29: branch to shellcode")
self.emit(cave, NOP, "selector42|29 shellcode pad: udf -> nop")
self.emit(cave + 4, MOV_X0_1, "selector42|29 shellcode: mov x0,#1")
self.emit(cave + 8, STRB_W0_X20_30, "selector42|29 shellcode: strb w0,[x20,#0x30]")
self.emit(cave + 12, MOV_X0_X20, "selector42|29 shellcode: mov x0,x20")
self.emit(cave + 16, self._asm_at(f"b #0x{stub_off + 4:X}", cave + 16),
"selector42|29 shellcode: branch back")
return True
def patch_debugger_entitlement_force_true(self):
"""Force debugger entitlement call to return true."""
refs = self._find_string_refs(b"com.apple.private.cs.debugger")
if not refs:
self._log(" [-] TXM JB: debugger refs not found")
return False
cands = []
for _, _, add_off in refs:
for scan in range(add_off, min(add_off + 0x20, self.size - 4), 4):
i = _disasm_one(self.raw, scan)
n = _disasm_one(self.raw, scan + 4)
p1 = _disasm_one(self.raw, scan - 4) if scan >= 4 else None
p2 = _disasm_one(self.raw, scan - 8) if scan >= 8 else None
if not all((i, n, p1, p2)):
continue
if (i.mnemonic == "bl"
and n.mnemonic == "tbnz" and n.op_str.startswith("w0, #0,")
and p1.mnemonic == "mov" and p1.op_str == "x2, #0"
and p2.mnemonic == "mov" and p2.op_str == "x0, #0"):
cands.append(scan)
if len(cands) != 1:
self._log(f" [-] TXM JB: expected 1 debugger BL site, found {len(cands)}")
return False
self.emit(cands[0], MOV_W0_1, "debugger entitlement: bl -> mov w0,#1")
return True
def patch_developer_mode_bypass(self):
"""Developer-mode bypass: NOP conditional guard before deny log path."""
refs = self._find_string_refs(
b"developer mode enabled due to system policy configuration")
if not refs:
self._log(" [-] TXM JB: developer-mode string ref not found")
return False
cands = []
for _, _, add_off in refs:
for back in range(add_off - 4, max(add_off - 0x20, 0), -4):
ins = _disasm_one(self.raw, back)
if not ins:
continue
if ins.mnemonic not in ("tbz", "tbnz", "cbz", "cbnz"):
continue
if not ins.op_str.startswith("w9, #0,"):
continue
cands.append(back)
if len(cands) != 1:
self._log(f" [-] TXM JB: expected 1 developer mode guard, found {len(cands)}")
return False
self.emit(cands[0], NOP, "developer mode bypass")
return True
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# setup_venv_linux.sh — Create Python venv on Linux (Debian/Ubuntu).
#
# On Linux, keystone-engine pip package ships prebuilt .so — no manual build needed.
#
# Usage:
# bash scripts/setup_venv_linux.sh
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
VENV_DIR="${PROJECT_ROOT}/.venv"
REQUIREMENTS="${PROJECT_ROOT}/requirements.txt"
echo "=== Installing system deps ==="
if command -v apt-get &>/dev/null; then
apt-get update -qq
apt-get install -y -qq python3 python3-venv python3-pip cmake gcc g++ pkg-config 2>/dev/null
elif command -v dnf &>/dev/null; then
dnf install -y python3 python3-pip cmake gcc gcc-c++ 2>/dev/null
fi
PYTHON="$(command -v python3)"
if [[ -z "${PYTHON}" ]]; then
echo "Error: python3 not found in PATH"
exit 1
fi
echo ""
echo "=== Creating venv ==="
echo " Python: ${PYTHON} ($(${PYTHON} --version 2>&1))"
echo " venv: ${VENV_DIR}"
echo " deps: ${REQUIREMENTS}"
echo ""
"${PYTHON}" -m venv "${VENV_DIR}"
source "${VENV_DIR}/bin/activate"
pip install --upgrade pip > /dev/null
pip install -r "${REQUIREMENTS}"
# --- Verify ---
echo ""
echo "=== Verifying imports ==="
python3 -c "
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN
from pyimg4 import IM4P
print(' capstone OK')
print(' keystone OK')
print(' pyimg4 OK')
"
echo ""
echo "=== venv ready ==="
echo " Activate: source ${VENV_DIR}/bin/activate"
echo " Deactivate: deactivate"