mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
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:
@@ -311,3 +311,8 @@ __marimo__/
|
||||
/VM
|
||||
.limd/
|
||||
/.swiftpm
|
||||
*.ipsw
|
||||
/updates-cdn
|
||||
/researchs/jb_asm_refs
|
||||
TODO.md
|
||||
/references/
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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" .
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
|
||||
Executable
+214
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user