diff --git a/AGENTS.md b/AGENTS.md index 16dd7cb..8917e67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ For any changes applying new patches, also update research/patch_comparison_all_ | Variant | Boot Chain | CFW | Make Targets | | ------------------- | :--------: | :-------: | ---------------------------------- | | **Regular** | 38 patches | 10 phases | `fw_patch` + `cfw_install` | -| **Development** | 47 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | +| **Development** | 49 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | | **Jailbreak (WIP)** | 84 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` | See `research/` for detailed firmware pipeline, component origins, patch breakdowns, and boot flow documentation. @@ -88,7 +88,7 @@ scripts/ │ ├── kernel_jb.py # JB: kernel patches (~34) │ ├── txm.py # TXM patcher │ ├── txm_dev.py # Dev: TXM entitlements/debugger/dev mode -│ ├── txm_jb.py # JB: TXM CS bypass (~13) + │ └── cfw.py # CFW binary patcher ├── resources/ # Resource archives (git submodule) ├── patches/ # Build-time patches (libirecovery) diff --git a/research/patch_comparison_all_variants.md b/research/patch_comparison_all_variants.md index a70be55..649e7eb 100644 --- a/research/patch_comparison_all_variants.md +++ b/research/patch_comparison_all_variants.md @@ -44,14 +44,14 @@ Three firmware variants are available, each building on the previous: TXM patch composition by variant: - Regular: `txm.py` (1 patch). -- Dev: `txm_dev.py` (10 patches total). -- JB: base `txm.py` (1 patch) + `txm_jb.py` extension (11 patches) = 12 total. +- Dev: `txm.py` (1 patch) + `txm_dev.py` (11 patches) = 12 total. +- JB: same as Dev (selector24 bypass now in `txm_dev.py`, no separate JB patcher). | # | Patch | Purpose | Regular | Dev | JB | | --- | ------------------------------------------------- | ----------------------------------------------------------- | :-----: | :-: | :-: | | 1 | Trustcache binary-search bypass | `bl hash_cmp` → `mov x0, #0` | Y | Y | Y | -| 2 | Selector24 hash extraction: NOP LDR X1 | Bypass CS hash flag extraction | — | — | Y | -| 3 | Selector24 hash extraction: NOP BL | Bypass CS hash flag check | — | — | Y | +| 2 | Selector24 bypass: `mov w0, #0xa1` | Return PASS (byte 1 = 0) after prologue | — | Y | Y | +| 3 | Selector24 bypass: `b ` | Skip validation, jump to register restore | — | Y | Y | | 4 | get-task-allow (selector 41\|29) | `bl` → `mov x0, #1` — allow get-task-allow | — | Y | Y | | 5 | Selector42\|29 shellcode: branch to cave | Redirect dispatch stub to shellcode | — | Y | Y | | 6 | Selector42\|29 shellcode: NOP pad | UDF → NOP in code cave | — | Y | Y | @@ -146,27 +146,27 @@ Regular and Dev share the same 25 base kernel patches. JB adds 34 additional pat | iBSS | 2 | 2 | 3 | | iBEC | 3 | 3 | 3 | | LLB | 6 | 6 | 6 | -| TXM | 1 | 10 | 12 | +| TXM | 1 | 12 | 12 | | Kernel | 25 | 25 | 59 | -| **Boot chain total** | **38** | **47** | **84** | +| **Boot chain total** | **38** | **49** | **84** | | | | | | | CFW binary patches | 4 | 5 | 6 | | CFW installed components | 6 | 7 | 8 | | **CFW total** | **10** | **12** | **14** | | | | | | -| **Grand total** | **48** | **59** | **98** | +| **Grand total** | **48** | **61** | **98** | ### What each variant adds -**Regular → Dev** (+11 patches): +**Regular → Dev** (+13 patches): -- TXM: +9 patches (get-task-allow, selector42|29 shellcode, debugger entitlement, developer mode bypass) +- TXM: +11 patches (selector24 force-pass, get-task-allow, selector42|29 shellcode, debugger entitlement, developer mode bypass) - CFW: +1 binary patch (launchd jetsam), +1 component (dev rpcserver_ios overlay) **Regular → JB** (+50 patches): - iBSS: +1 (nonce skip) -- TXM: +11 (hash extraction NOP, get-task-allow, selector42|29 shellcode, debugger entitlement, dev mode bypass) +- TXM: +11 (same as dev — selector24, get-task-allow, selector42|29 shellcode, debugger entitlement, dev mode bypass) - Kernel: +34 (trustcache, execve, sandbox, task/VM, memory, kcall10) - CFW: +2 binary patches (launchd jetsam + dylib injection), +2 components (procursus + BaseBin hooks) @@ -186,14 +186,16 @@ Regular and Dev share the same 25 base kernel patches. JB adds 34 additional pat ## Dynamic Implementation Log (JB Patchers) -### TXM (`txm_jb.py`) +### TXM (`txm_dev.py`) -All TXM JB patches are implemented with dynamic binary analysis and +All TXM dev patches are implemented with dynamic binary analysis and keystone/capstone-encoded instructions only. -1. `selector24 A1` (2x nop: LDR + BL) - - Locator: unique guarded `mov w0,#0xa1` site, scan for `ldr x1,[xN,#0x38] ; add x2 ; bl ; ldp` pattern. - - Patch bytes: keystone `nop` on the LDR and the BL. +1. `selector24 force-pass` (2 instructions after prologue) + - Locator: unique guarded `mov w0,#0xa1` site, scan for `ldr x1,[xN,#0x38] ; add x2 ; bl ; ldp` pattern, walk back to PACIBSP. + - Patch bytes: `mov w0, #0xa1 ; b ` after prologue — returns 0xA1 (PASS) unconditionally. + - Return code semantics: caller checks `tst w0, #0xff00` — byte 1 = 0 is PASS, non-zero is FAIL. + - History: v1 was 2x NOP (LDR + BL) which broke flags extraction. v2 was `mov w0, #0x30a1; movk; ret` which returned FAIL (0x130A1 has byte 1 = 0x30). v3 (current) returns 0xA1 (byte 1 = 0 = PASS). See `research/txm_selector24_analysis.md`. 2. `selector41/29 get-task-allow` - Locator: xref to `"get-task-allow"` + nearby `bl` followed by `tbnz w0,#0`. - Patch bytes: keystone `mov x0, #1`. @@ -217,7 +219,7 @@ keystone/capstone-encoded instructions only. #### 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. +- `patch.dyn.raw` generated by `TXMPatcher` (txm_dev.py) on the same input. - Result: byte-identical (`cmp -s` success, SHA-256 matched). ### Kernelcache (`kernel_jb.py`) @@ -282,7 +284,8 @@ Validated using pristine inputs from `updates-cdn/`: > Note: These emit counts were captured at validation time and may differ from > the current source if methods were subsequently refactored. The TXM JB patcher -> currently has 5 methods emitting 11 patches; the kernel JB patcher has 24 -> methods. Actual emit counts depend on how many dynamic targets resolve per binary. +> currently has 5 methods emitting 11 patches in txm_dev.py (selector24 force-pass = 2 emits); +> the kernel JB patcher has 24 methods. Actual emit counts depend on how many +> dynamic targets resolve per binary. All patches are applied dynamically via string anchors, instruction patterns, and cross-reference analysis — no hardcoded offsets — ensuring portability across iOS versions. diff --git a/research/txm_fullchain_analysis.md b/research/txm_fullchain_analysis.md new file mode 100644 index 0000000..73332b5 --- /dev/null +++ b/research/txm_fullchain_analysis.md @@ -0,0 +1,461 @@ +# TXM Selector 24 — Full Chain Analysis + +## Overview + +This document maps the complete TXM code signature validation chain for selector 24, +from the top-level entry point down to the hash extraction functions. IDA base for +this analysis: `0xfffffff017020000` (VA = raw_offset + `0xfffffff017004000`). + +## Return Code Convention + +TXM uses a multi-byte return code convention: + +| Byte | Purpose | +|----------|--------------------------------------------------------| +| Byte 0 | Check identity (e.g., `0xA1` = check_hash_flags) | +| Byte 1 | Error indicator: `0x00` = pass, non-zero = fail | +| Byte 2+ | Additional error info | + +The caller checks `(result & 0xFF00) == 0` — if byte 1 is zero, the check passed. + +**Important**: The previous `txm_selector24_analysis.md` had SUCCESS/ERROR labels +**swapped**. Corrected mappings: + +| Return Value | Byte 1 | Meaning in Caller | Description | +|-------------|--------|-------------------|------------------------------| +| `0xA1` | `0x00` | **PASS** | Hash check passed | +| `0x130A1` | `0x30` | **FAIL** | Hash consistency mismatch | +| `0x22DA1` | `0x2D` | **FAIL** | Hash flags type violation | + +The panic log `selector: 24 | 0xA1 | 0x30 | 1` decodes to return code `0x130A1`. + +--- + +## Full Call Chain + +``` +cs_evaluate (0xfffffff017024834) + | + +-- cs_blob_init (0xfffffff0170356D8) + | Parse CS blob, extract hash, setup entitlements + | + +-- cs_determine_selector (0xfffffff0170358C0) + | Determine selector byte (written to ctx+161) + | Also calls hash_flags_extract internally + | + +-- cs_selector24_validate (0xfffffff0170359E0) + Sequential validation chain (8 checks + type switch): + | + | Each check returns low-byte-only (0xNN) on PASS, + | multi-byte (0xNNNNN) on FAIL. Chain stops on first FAIL. + | + +-- [1] check_library_validation (0x...5594) ret 0xA9 on pass + +-- [2] check_runtime_flag (0x...552C) ret 0xA8 on pass + +-- [3] check_jit_entitlement (0x...5460) ret 0xA0 on pass + +-- [4] check_hash_flags (0x...5398) ret 0xA1 on pass <<<< PATCH TARGET + +-- [5] check_team_id (0x...52F4) ret 0xA2 on pass + +-- [6] check_srd_entitlement (0x...5254) ret 0xAA on pass + +-- [7] check_extended_research (0x...51D8) ret 0xAB on pass + +-- [8] check_hash_type (0x...5144) ret 0xAC on pass + | + +-- switch(selector_type): + case 1: sub_...4FC4 + case 2: sub_...4E60 + case 3: sub_...4D60 + case 4: sub_...4CC4 + case 5: (no additional check) + case 6-10: sub_...504C +``` + +--- + +## Check #4: check_hash_flags (0xfffffff017035398) + +**This is the function containing both JB patch sites.** + +### IDA Addresses → Raw File Offsets + +| IDA VA | Raw Offset | Content | +|----------------------|-----------|-----------------------------------| +| `0xfffffff017035398` | `0x31398` | Function start (PACIBSP) | +| `0xfffffff0170353B0` | `0x313B0` | `mov x19, x1` (after prologue) | +| `0xfffffff0170353CC` | `0x313CC` | PATCH 1: `ldr x1, [x20, #0x38]` | +| `0xfffffff0170353D4` | `0x313D4` | PATCH 2: `bl hash_flags_extract` | +| `0xfffffff017035418` | `0x31418` | `mov w0, #0x30A1; movk ...#1` | +| `0xfffffff017035420` | `0x31420` | Exit path (LDP epilogue) | +| `0xfffffff017035454` | `0x31454` | `mov w0, #0x2DA1; movk ...#2` | + +### Decompiled Pseudocode + +```c +// IDA: check_hash_flags @ 0xfffffff017035398 +// Raw: 0x31398, Size: 0xC8 +// +// a1 = CS context pointer (x0) +// a2 = selector_type (x1, from ctx+161) +// +uint32_t check_hash_flags(cs_ctx **a1, uint32_t a2) { + void *chain = **a1; // 0x353B8-0x353BC + + uint32_t flags = 0; // 0x353C4: str wzr, [sp, #4] + uint64_t hash_data = 0; // 0x353C0: str xzr, [sp, #8] + + // Extract hash flags from CS blob + hash_flags_extract(a1[6], a1[7], &flags); // 0x353C8-0x353D4 + // ^^^^ ^^^^ + // cs_blob cs_size + // PATCH 1 @ 0x353CC: loads cs_size into x1 + // PATCH 2 @ 0x353D4: calls hash_flags_extract + + // Extract hash data pointer from CS blob + hash_data_extract(a1[6], a1[7], &hash_data); // 0x353D8-0x353E0 + + // --- Decision logic --- + + // High-type shortcut: type >= 6 with chain data → pass + if (a2 >= 6 && *(uint64_t*)(chain + 8) != 0) + return 0xA1; // PASS + + // Consistency check: hash data presence vs flag bit 1 + // flag bit 1 = "hash data exempt" (when set, no hash data expected) + bool has_data = (hash_data != 0); + bool exempt = (flags >> 1) & 1; + + if (has_data == exempt) + return 0x130A1; // FAIL — inconsistent (data exists but exempt, or vice versa) + + // Type-specific logic + if (a2 > 5) + return 0xA1; // PASS — types 6+ always pass here + + if (a2 == 1 || a2 == 2) { + if (exempt) + return 0xA1; // PASS — type 1-2 with exempt flag + return 0x22DA1; // FAIL — type 1-2 must have exempt flag + } + + // Types 3-5 + if (!exempt) + return 0xA1; // PASS — type 3-5 without exempt + return 0x22DA1; // FAIL — type 3-5 must not be exempt +} +``` + +### Assembly (Complete) + +``` +0x31398: pacibsp +0x3139C: sub sp, sp, #0x40 +0x313A0: stp x22, x21, [sp, #0x10] +0x313A4: stp x20, x19, [sp, #0x20] +0x313A8: stp x29, x30, [sp, #0x30] +0x313AC: add x29, sp, #0x30 +0x313B0: mov x19, x1 ; x19 = selector_type +0x313B4: mov x20, x0 ; x20 = cs_ctx +0x313B8: ldr x8, [x0] ; x8 = *ctx +0x313BC: ldr x21, [x8] ; x21 = **ctx (chain) +0x313C0: str xzr, [sp, #8] ; hash_data = 0 +0x313C4: str wzr, [sp, #4] ; flags = 0 +0x313C8: ldr x0, [x0, #0x30] ; x0 = ctx->cs_blob +0x313CC: ldr x1, [x20, #0x38] ; x1 = ctx->cs_size <<< PATCH 1 +0x313D0: add x2, sp, #4 ; x2 = &flags +0x313D4: bl hash_flags_extract ; <<< PATCH 2 +0x313D8: ldp x0, x1, [x20, #0x30] ; reload blob + size +0x313DC: add x2, sp, #8 ; x2 = &hash_data +0x313E0: bl hash_data_extract ; +0x313E4: ldr w8, [sp, #4] ; w8 = flags +0x313E8: cmp w19, #6 +0x313EC: b.lo 0x31400 +0x313F0: ldr x9, [x21, #8] +0x313F4: cbz x9, 0x31400 +0x313F8: mov w0, #0xA1 ; PASS +0x313FC: b 0x31420 ; → exit +0x31400: and w8, w8, #2 ; flags & 2 +0x31404: ldr x9, [sp, #8] ; hash_data +0x31408: cmp x9, #0 +0x3140C: cset w9, ne ; has_data = (hash_data != 0) +0x31410: cmp w9, w8, lsr #1 ; has_data vs exempt +0x31414: b.ne 0x31434 ; mismatch → continue checks +0x31418: mov w0, #0x30A1 ; FAIL (0x130A1) +0x3141C: movk w0, #1, lsl #16 +0x31420: ldp x29, x30, [sp, #0x30] ; EXIT +0x31424: ldp x20, x19, [sp, #0x20] +0x31428: ldp x22, x21, [sp, #0x10] +0x3142C: add sp, sp, #0x40 +0x31430: retab +0x31434: cmp w19, #5 +0x31438: b.hi 0x313F8 ; type > 5 → PASS +0x3143C: sub w9, w19, #1 +0x31440: cmp w9, #1 +0x31444: b.hi 0x31450 ; type > 2 → check 3-5 +0x31448: cbnz w8, 0x313F8 ; type 1-2 + exempt → PASS +0x3144C: b 0x31454 +0x31450: cbz w8, 0x313F8 ; type 3-5 + !exempt → PASS +0x31454: mov w0, #0x2DA1 ; FAIL (0x22DA1) +0x31458: movk w0, #2, lsl #16 +0x3145C: b 0x31420 ; → exit +``` + +--- + +## Sub-function: hash_flags_extract (0xfffffff0170335D8) + +**Raw offset: 0x2F5D8, Size: 0x40** + +```c +// Extracts 32-bit hash flags from CS blob at offset 0xC (big-endian) +uint32_t hash_flags_extract(uint8_t *blob, uint32_t size, uint32_t *out) { + if (!out) + return 0x31; // no output pointer, noop + + if (blob + 44 <= blob + size) { + // Blob is large enough + *out = bswap32(*(uint32_t*)(blob + 12)); // flags at offset 0xC + return 0x31; + } + + // Blob too small — error + return error_handler(25); +} +``` + +## Sub-function: hash_data_extract (0xfffffff0170336F8) + +**Raw offset: 0x2F6F8, Size: 0xA8** + +```c +// Extracts hash data pointer from CS blob +// Reads offset at blob+48 (big-endian), validates bounds, finds null-terminated data +uint32_t hash_data_extract(uint8_t *blob, uint32_t size, uint64_t *out) { + uint8_t *end = blob + size; + + if (blob + 44 > end) + goto bounds_error; + + // Check version field at blob+8 + if (bswap32(*(uint32_t*)(blob + 8)) >> 9 < 0x101) + return 0x128B9; // version too low + + if (blob + 52 > end) + goto bounds_error; + + uint32_t offset_raw = *(uint32_t*)(blob + 48); + if (!offset_raw) + return 0x224B9; // no hash data (offset = 0) + + uint32_t offset = bswap32(offset_raw); + uint8_t *data = blob + offset; + + if (data < blob || data >= end) + goto bounds_error; + + // Find null terminator + uint8_t *scan = data + 1; + while (scan <= end) { + if (*(scan - 1) == 0) { + *out = (uint64_t)data; + return 0x39; + } + scan++; + } + +bounds_error: + return error_handler(25); +} +``` + +--- + +## Caller: cs_selector24_validate (0xfffffff0170359E0) + +```c +uint32_t cs_selector24_validate(cs_ctx *a1) { + uint8_t selector_type = *(uint8_t*)((char*)a1 + 161); + + if (!selector_type) + return 0x10503; // no selector set + + if (*((uint8_t*)a1 + 162)) + return 0x23403; // already validated + + uint32_t r; + + r = check_library_validation(*a1, selector_type); + if ((r & 0xFF00) != 0) return r; + + r = check_runtime_flag(*a1, selector_type); + if ((r & 0xFF00) != 0) return r; + + r = check_jit_entitlement(a1, selector_type); + if ((r & 0xFF00) != 0) return r; + + r = check_hash_flags(a1, selector_type); // <<<< CHECK #4 + if ((r & 0xFF00) != 0) return r; + + r = check_team_id(a1); + if ((r & 0xFF00) != 0) return r; + + r = check_srd_entitlement(a1); + if ((r & 0xFF00) != 0) return r; + + r = check_extended_research(a1); + if ((r & 0xFF00) != 0) return r; + + r = check_hash_type(a1); + if ((r & 0xFF00) != 0) return r; + + // All pre-checks passed — now type-specific validation + switch (selector_type) { + case 1: r = validate_type1(a1); break; + case 2: r = validate_type2(a1); break; + case 3: r = validate_type3(a1); break; + case 4: r = validate_type4(a1); break; + case 5: /* no extra check */ break; + case 6..10: r = validate_type6_10(a1, selector_type); break; + default: return 0x40103; + } + + if ((r & 0xFF00) != 0) return r; + + // Mark as validated + *((uint8_t*)a1 + 162) = selector_type; + return 3; // success +} +``` + +--- + +## Top-level: cs_evaluate (0xfffffff017024834) + +```c +uint64_t cs_evaluate(cs_session *session) { + update_state(session, 1, 0); + + if (session->flags & 1) { + log_error(80, 0); + goto fatal; + } + + cs_ctx *ctx = &session->ctx; + + uint32_t r = cs_blob_init(ctx); + if (BYTE1(r)) goto handle_error; + + r = cs_determine_selector(ctx, NULL); + if (BYTE1(r)) goto handle_error; + + r = cs_selector24_validate(ctx); + if (!BYTE1(r)) { + // Success — return 0 + finalize(session, 1); + return 0 | (packed_status); + } + // ... error handling ... +} +``` + +--- + +## Validation Sub-check Details + +### [1] check_library_validation (ret 0xA9) +Checks library validation flag. If `*(*a1 + 5) & 1` and selector not in [7..10], returns `0x130A9` (fail). + +### [2] check_runtime_flag (ret 0xA8) +For selector <= 5: checks runtime hardened flag via function pointer at `a1[1]()`. +Returns `0x130A8` if runtime enabled and runtime flag set. + +### [3] check_jit_entitlement (ret 0xA0) +Checks `com.apple.developer.cs.allow-jit` and `com.apple.developer.web-browser-engine.webcontent` +entitlements. For selector <= 5, also checks against a list of 4 platform entitlements. + +### [4] check_hash_flags (ret 0xA1) — PATCH TARGET +See detailed analysis above. + +### [5] check_team_id (ret 0xA2) +Checks team ID against 6 known Apple team IDs using entitlement lookup functions. + +### [6] check_srd_entitlement (ret 0xAA) +Checks `com.apple.private.security-research-device` entitlement. + +### [7] check_extended_research (ret 0xAB) +Checks `com.apple.private.security-research-device.extended-research-mode` entitlement. + +### [8] check_hash_type (ret 0xAC) +Re-extracts hash data and validates hash algorithm type via `sub_FFFFFFF01702EDF4`. + +--- + +## Why the NOP Patches Failed + +### Test results: +| Patch 1 (NOP LDR) | Patch 2 (NOP BL) | Result | +|--------------------|-------------------|-----------| +| OFF | OFF | **Boots** | +| ON | OFF | Panic | +| OFF | ON | Panic | +| ON | ON | Panic | + +### Root cause analysis: + +**With no patches (dev-only)**: The function runs normally. For our binaries: +- `hash_flags_extract` returns proper flags from CS blob +- `hash_data_extract` returns the hash data pointer +- The consistency check `has_data == exempt` evaluates to `1 == 0` (has data, not exempt) → **mismatch** → passes (B.NE taken) +- The type-specific logic returns 0xA1 (pass) + +**NOP LDR only (Patch 1)**: `x1` retains the value of `a2` (selector type, a small number like 5 or 10) instead of `cs_size` (the actual blob size). When `hash_flags_extract` runs, the bounds check `blob + 44 <= blob + size` uses the wrong size. If `a2 < 44`, the check fails → error path → `flags` stays 0. Then `exempt = 0`, and if `has_data = 1` → mismatch passes, but later type-specific logic with `(flags & 2) == 0` may return `0x22DA1` (FAIL) depending on selector type. + +**NOP BL only (Patch 2)**: `hash_flags_extract` never runs → `flags = 0` (initialized to 0). So `exempt = 0`. If hash data exists (`has_data = 1`), consistency check passes (mismatch: `1 != 0`). But then: +- For type 1-2: `(flags & 2) == 0` → returns `0x22DA1` **FAIL** +- For type 3-5: `(flags & 2) == 0` → returns `0xA1` **PASS** +- For type > 5: returns `0xA1` **PASS** + +So if the binary's selector type is 1 or 2, NOP'ing the BL causes failure. + +**Both patches**: Similar to NOP BL — `hash_flags_extract` is NOP'd so flags=0, but NOP LDR also corrupts x1 (which is unused since BL is also NOP'd, so no effect). Net result same as NOP BL only. + +**Conclusion**: The patches were **counterproductive**. The function already returns PASS for legitimately signed binaries with dev patches. The NOPs corrupt state and cause it to FAIL. + +--- + +## Correct Patch Strategy (for future unsigned code) + +When JB payloads run unsigned/modified code, `check_hash_flags` may legitimately fail. +The correct fix is to make it always return `0xA1` (PASS). + +### Recommended: Early-return after prologue + +Patch 2 instructions at raw offset `0x313B0`: + +``` +BEFORE: + 0x313B0: mov x19, x1 (E1 03 17 AA → actually this encodes to different bytes) + 0x313B4: mov x20, x0 + +AFTER: + 0x313B0: mov w0, #0xa1 (20 14 80 52) + 0x313B4: b +0x6C (1B 00 00 14) → jumps to exit at 0x31420 +``` + +The prologue (0x31398-0x313AC) has already saved all callee-saved registers. +The exit path at 0x31420 restores them and does RETAB. This is safe. + +### Alternative: Patch error returns to PASS + +Replace both error-returning MOVs with the PASS value: + +``` +0x31418: mov w0, #0xA1 (20 14 80 52) was: mov w0, #0x30A1 +0x3141C: nop (1F 20 03 D5) was: movk w0, #1, lsl #16 +0x31454: mov w0, #0xA1 (20 14 80 52) was: mov w0, #0x2DA1 +0x31458: nop (1F 20 03 D5) was: movk w0, #2, lsl #16 +``` + +This preserves the original logic flow but makes all paths return PASS. + +--- + +## Current Status + +The 2 JB TXM patches (`patch_selector24_hash_extraction_nop`) are **disabled** (commented out in `txm_jb.py`). The JB variant now boots identically to dev for TXM validation. When unsigned code execution is needed, apply one of the recommended patches above. diff --git a/research/txm_selector24_analysis.md b/research/txm_selector24_analysis.md new file mode 100644 index 0000000..21a0275 --- /dev/null +++ b/research/txm_selector24_analysis.md @@ -0,0 +1,285 @@ +# TXM Selector24 CS Hash Extraction — Patch Analysis + +## Problem + +Original JB TXM patches (2 NOPs in selector24 handler) cause kernel panic: +``` +TXM [Error]: CodeSignature: selector: 24 | 0xA1 | 0x30 | 1 +panic: unexpected SIGKILL of init with reason -- namespace 9 code 0x1 +``` + +Both patches individually cause the panic. With both disabled (= dev only), boot succeeds. + +## The Function + +- Raw offset: `0x031398` +- IDA address (base `0xFFFFFFF017004000`): `0xFFFFFFF017035398` + +Selector24 CS hash-flags validation function. Takes a context struct (x0) and a hash type (x1). + +### Disassembly + +``` +0x031398: pacibsp +0x03139C: sub sp, sp, #0x40 +0x0313A0: stp x22, x21, [sp, #0x10] +0x0313A4: stp x20, x19, [sp, #0x20] +0x0313A8: stp x29, x30, [sp, #0x30] +0x0313AC: add x29, sp, #0x30 +0x0313B0: mov x19, x1 ; x19 = hash_type arg +0x0313B4: mov x20, x0 ; x20 = context struct +0x0313B8: ldr x8, [x0] ; x8 = ctx->chain_ptr +0x0313BC: ldr x21, [x8] ; x21 = *ctx->chain_ptr +0x0313C0: str xzr, [sp, #8] ; local_hash_result = 0 +0x0313C4: str wzr, [sp, #4] ; local_flags = 0 +0x0313C8: ldr x0, [x0, #0x30] ; x0 = ctx->cs_blob (a1[6]) +0x0313CC: ldr x1, [x20, #0x38] ; x1 = ctx->cs_blob_size (a1[7]) +0x0313D0: add x2, sp, #4 ; x2 = &local_flags +0x0313D4: bl #0x2f5d8 ; hash_flags_extract(blob, size, &flags) +0x0313D8: ldp x0, x1, [x20, #0x30] ; reload blob + size +0x0313DC: add x2, sp, #8 ; x2 = &local_hash_result +0x0313E0: bl #0x2f6f8 ; cs_blob_get_hash(blob, size, &result) +0x0313E4: ldr w8, [sp, #4] ; w8 = flags +0x0313E8: cmp w19, #6 ; if hash_type >= 6 ... +0x0313EC: b.lo #0x31400 +0x0313F0: ldr x9, [x21, #8] ; check table->field_8 +0x0313F4: cbz x9, #0x31400 ; if field_8 != 0: +0x0313F8: mov w0, #0xa1 ; RETURN 0xa1 (ERROR!) +0x0313FC: b #0x31420 +0x031400: and w8, w8, #2 ; flags_bit1 = flags & 2 +0x031404: ldr x9, [sp, #8] ; hash_result +0x031408: cmp x9, #0 +0x03140C: cset w9, ne ; has_result = (hash_result != 0) +0x031410: cmp w9, w8, lsr #1 ; if has_result == flags_bit1: +0x031414: b.ne #0x31434 +0x031418: mov w0, #0x30a1 ; RETURN 0x130a1 (SUCCESS) +0x03141C: movk w0, #1, lsl #16 +0x031420: ldp x29, x30, [sp, #0x30] +0x031424: ldp x20, x19, [sp, #0x20] +0x031428: ldp x22, x21, [sp, #0x10] +0x03142C: add sp, sp, #0x40 +0x031430: retab +0x031434: cmp w19, #5 ; further checks based on type +0x031438: b.hi #0x313f8 ; type > 5 → return 0xa1 +0x03143C: sub w9, w19, #1 +0x031440: cmp w9, #1 +0x031444: b.hi #0x31450 ; type > 2 → goto other +0x031448: cbnz w8, #0x313f8 ; type 1-2: flags_bit1 set → 0xa1 +0x03144C: b #0x31454 +0x031450: cbz w8, #0x313f8 ; type 3-5: flags_bit1 clear → 0xa1 +0x031454: mov w0, #0x2da1 ; RETURN 0x22da1 (SUCCESS variant) +0x031458: movk w0, #2, lsl #16 +0x03145C: b #0x31420 +``` + +### IDA Pseudocode + +```c +__int64 __fastcall sub_FFFFFFF017035398(__int64 **a1, unsigned int a2) +{ + __int64 v4; // x21 + int v6; // [xsp+4h] [xbp-2Ch] BYREF — flags + __int64 v7; // [xsp+8h] [xbp-28h] BYREF — hash_result + + v4 = **a1; + v7 = 0; + v6 = 0; + sub_FFFFFFF0170335D8(a1[6], a1[7], &v6); // hash_flags_extract + sub_FFFFFFF0170336F8(a1[6], a1[7], &v7); // cs_blob_get_hash + if ( a2 >= 6 && *(_QWORD *)(v4 + 8) ) + return 161; // 0xA1 — ERROR + if ( (v7 != 0) == (unsigned __int8)(v6 & 2) >> 1 ) + return 77985; // 0x130A1 — SUCCESS + if ( a2 > 5 ) + return 161; + if ( a2 - 1 <= 1 ) + { + if ( (v6 & 2) == 0 ) + return 142753; // 0x22DA1 — SUCCESS variant + return 161; + } + if ( (v6 & 2) == 0 ) + return 161; + return 142753; +} +``` + +### Annotated Pseudocode + +```c +// selector24 handler: validates CS blob hash flags consistency +int selector24_validate(struct cs_context **ctx, uint32_t hash_type) { + void *table = **ctx; + int flags = 0; + int64_t hash_ptr = 0; + + void *cs_blob = ctx[6]; // ctx + 0x30 + uint32_t cs_size = ctx[7]; // ctx + 0x38 + + // ① Extract hash flags from CS blob offset 0xC (big-endian) + hash_flags_extract(cs_blob, cs_size, &flags); + + // ② Get hash data pointer from CS blob + cs_blob_get_hash(cs_blob, cs_size, &hash_ptr); + + // ③ type >= 6 with table data → PASS (early out) + if (hash_type >= 6 && *(table + 8) != 0) + return 0xA1; // PASS (byte 1 = 0) + + // ④ Core consistency: hash existence must match flags bit 1 + bool has_hash = (hash_ptr != 0); + bool flag_bit1 = (flags & 2) >> 1; + + if (has_hash == flag_bit1) + return 0x130A1; // FAIL (byte 1 = 0x30) ← panic trigger! + + // ⑤ Inconsistent — type-specific handling + if (hash_type > 5) return 0xA1; // PASS + if (hash_type == 1 || hash_type == 2) { + if (!(flags & 2)) return 0x22DA1; // FAIL (byte 1 = 0x2D) + return 0xA1; // PASS + } + // type 3-5 + if (flags & 2) return 0x22DA1; // FAIL + return 0xA1; // PASS +} +``` + +## hash_flags_extract (0x02F5D8 / IDA 0xFFFFFFF0170335D8) + +``` +0x02F5D8: bti c +0x02F5DC: cbz x2, #0x2f5fc ; if out_ptr == NULL, skip +0x02F5E0: add x8, x0, w1, uxtw ; end = blob + size +0x02F5E4: add x9, x0, #0x2c ; min_end = blob + 0x2c +0x02F5E8: cmp x9, x8 ; if blob too small: +0x02F5EC: b.hi #0x2f604 ; goto error +0x02F5F0: ldr w8, [x0, #0xc] ; raw_flags = blob[0xc] (big-endian) +0x02F5F4: rev w8, w8 ; flags = bswap32(raw_flags) +0x02F5F8: str w8, [x2] ; *out_ptr = flags +0x02F5FC: mov w0, #0x31 ; return 0x31 (success) +0x02F600: ret +``` + +Reads a 32-bit big-endian flags field from cs_blob offset 0xC, byte-swaps, stores to output. + +## cs_blob_get_hash (0x02F6F8 / IDA 0xFFFFFFF0170336F8) + +``` +0x02F6F8: pacibsp +0x02F6FC: stp x29, x30, [sp, #-0x10]! +0x02F700: mov x29, sp +0x02F704: add x8, x0, w1, uxtw ; end = blob + size +0x02F708: add x9, x0, #0x2c ; min_end = blob + 0x2c +0x02F70C: cmp x9, x8 +0x02F710: b.hi #0x2f798 ; blob too small → error +0x02F714: ldr w9, [x0, #8] +0x02F718: rev w9, w9 +0x02F71C: lsr w9, w9, #9 +0x02F720: cmp w9, #0x101 +0x02F724: b.hs #0x2f734 +0x02F728: mov w0, #0x2839 ; version too old → error +0x02F72C: movk w0, #1, lsl #16 +0x02F730: b #0x2f790 +0x02F734: add x9, x0, #0x34 +0x02F738: cmp x9, x8 +0x02F73C: b.hi #0x2f798 +0x02F740: ldr w9, [x0, #0x30] ; hash_offset (big-endian) +0x02F744: cbz w9, #0x2f788 ; no hash → return special +0x02F748: cbz x2, #0x2f780 ; no output ptr → skip +0x02F74C: rev w10, w9 +0x02F750: add x9, x0, x10 ; hash_ptr = blob + bswap(hash_offset) +0x02F754: cmp x9, x0 ; bounds check +0x02F758: ccmp x9, x8, #2, hs +0x02F75C: b.hs #0x2f798 +0x02F760: add x10, x10, x0 +0x02F764: add x10, x10, #1 +0x02F768: cmp x10, x8 +0x02F76C: b.hi #0x2f798 +0x02F770: ldurb w11, [x10, #-1] ; scan for NUL terminator +0x02F774: add x10, x10, #1 +0x02F778: cbnz w11, #0x2f768 +0x02F77C: str x9, [x2] ; *out_ptr = hash_ptr +0x02F780: mov w0, #0x39 ; return 0x39 (success) +0x02F784: b #0x2f790 +0x02F788: mov w0, #0x2439 ; return 0x22439 (no hash) +0x02F78C: movk w0, #2, lsl #16 +0x02F790: ldp x29, x30, [sp], #0x10 +0x02F794: retab +0x02F798: mov w0, #0x19 ; error → panic/abort +0x02F79C: bl #0x25a74 +``` + +## Why the Original NOP Patches Were Wrong + +### PATCH 1 only (NOP ldr x1, [x20, #0x38]): +- x1 retains incoming arg value (hash_type) instead of cs_blob_size +- hash_flags_extract called with WRONG size → garbage flags or OOB +- Consistency check fails → 0xA1 + +### PATCH 2 only (NOP bl hash_flags_extract): +- flags stays 0 (initialized at 0x0313C4) +- hash_result from second BL is non-zero (valid hash exists) +- flags_bit1 = 0, has_result = 1 → mismatch +- For type > 5 → return 0xA1 + +### Both patches disabled: +- Function runs normally, hash_flags_extract extracts correct flags +- flags_bit1 matches has_result → returns 0x130A1 (success) +- Boot succeeds (same as dev variant) + +## Return Code Semantics (CORRECTED) + +The caller checks return values via `tst w0, #0xff00; b.ne `: +- **0xA1** (byte 1 = 0x00) → **PASS** — `0xA1 & 0xFF00 = 0` → continues +- **0x130A1** (byte 1 = 0x30) → **FAIL** — `0x130A1 & 0xFF00 = 0x3000` → branches to error +- **0x22DA1** (byte 1 = 0x2D) → **FAIL** — `0x22DA1 & 0xFF00 = 0x2D00` → branches to error + +The initial fix attempt (returning 0x130A1) was wrong — it returned a FAIL code. + +### Caller context (0x031A60) + +``` +0x031A4C: bl #0x31460 ; call previous validator +0x031A50: tst w0, #0xff00 ; check byte 1 +0x031A54: b.ne #0x31b44 ; non-zero → error path +0x031A58: mov x0, x19 +0x031A5C: mov x1, x20 +0x031A60: bl #0x31398 ; call selector24_validate (our target) +0x031A64: tst w0, #0xff00 ; check byte 1 +0x031A68: b.ne #0x31b44 ; non-zero → error path +``` + +## Fix Applied + +Insert `mov w0, #0xa1; b ` after the prologue, returning PASS immediately: + +```asm +;; prologue (preserved — sets up stack frame for clean epilogue) +0x031398: pacibsp +0x03139C: sub sp, sp, #0x40 +0x0313A0: stp x22, x21, [sp, #0x10] +0x0313A4: stp x20, x19, [sp, #0x20] +0x0313A8: stp x29, x30, [sp, #0x30] +0x0313AC: add x29, sp, #0x30 + +;; PATCH: early return with PASS +0x0313B0: mov w0, #0xa1 ; return PASS (byte 1 = 0) +0x0313B4: b #0x31420 ; jump to epilogue + +;; epilogue (existing — restores registers and returns) +0x031420: ldp x29, x30, [sp, #0x30] +0x031424: ldp x20, x19, [sp, #0x20] +0x031428: ldp x22, x21, [sp, #0x10] +0x03142C: add sp, sp, #0x40 +0x031430: retab +``` + +### Patcher implementation (`txm_jb.py`) + +Method `patch_selector24_force_pass()`: +- Locator: finds `mov w0, #0xa1`, walks back to PACIBSP, verifies selector24 + characteristic pattern (LDR X1,[Xn,#0x38] / ADD X2 / BL / LDP). +- Finds prologue end dynamically (`add x29, sp, #imm` → next instruction). +- Finds epilogue dynamically (scan for `retab`, walk back to `ldp x29, x30`). +- Patch: 2 instructions after prologue: `mov w0, #0xa1 ; b `. diff --git a/scripts/fw_patch_jb.py b/scripts/fw_patch_jb.py index 8911572..0aaed22 100644 --- a/scripts/fw_patch_jb.py +++ b/scripts/fw_patch_jb.py @@ -24,7 +24,6 @@ from fw_patch import ( from fw_patch_dev import patch_txm_dev from patchers.iboot_jb import IBootJBPatcher from patchers.kernel_jb import KernelJBPatcher -from patchers.txm_jb import TXMJBPatcher def patch_ibss_jb(data): @@ -34,13 +33,6 @@ def patch_ibss_jb(data): 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 - - def patch_kernelcache_jb(data): kp = KernelJBPatcher(data) n = kp.apply() @@ -48,7 +40,7 @@ def patch_kernelcache_jb(data): return n > 0 -# Base components — same as fw_patch_dev (dev TXM instead of base TXM). +# Base components — same as fw_patch_dev (dev TXM includes selector24 bypass). COMPONENTS = [ # (name, search_base_is_restore, search_patterns, patch_function, preserve_payp) ("AVPBooter", False, ["AVPBooter*.bin"], patch_avpbooter, False), @@ -69,14 +61,13 @@ COMPONENTS = [ JB_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, - ), + # ( + # "kernelcache (JB)", + # True, + # ["kernelcache.research.vphone600"], + # patch_kernelcache_jb, + # True, + # ), ] diff --git a/scripts/patchers/txm_dev.py b/scripts/patchers/txm_dev.py index 8665e4d..99018ea 100755 --- a/scripts/patchers/txm_dev.py +++ b/scripts/patchers/txm_dev.py @@ -58,13 +58,14 @@ def _find_asm_pattern(data, asm_str): class TXMPatcher: - """Dev-only dynamic patcher for TXM images. + """Dev/JB dynamic patcher for TXM images. - Patches (dev-specific only — base trustcache bypass is in txm.py): - 1. get-task-allow entitlement check BL → mov x0, #1 - 2. Selector42|29: shellcode hook + manifest flag force - 3. debugger entitlement check BL → mov w0, #1 - 4. developer-mode guard branch → nop + Patches (base trustcache bypass is in txm.py): + 1. Selector24: force PASS return (mov w0, #0xa1 + b epilogue) + 2. get-task-allow entitlement check BL → mov x0, #1 + 3. Selector42|29: shellcode hook + manifest flag force + 4. debugger entitlement check BL → mov w0, #1 + 5. developer-mode guard branch → nop """ def __init__(self, data, verbose=True): @@ -105,6 +106,7 @@ class TXMPatcher: def find_all(self): self.patches = [] + self.patch_selector24_force_pass() self.patch_get_task_allow_force_true() self.patch_selector42_29_shellcode() self.patch_debugger_entitlement_force_true() @@ -285,6 +287,90 @@ class TXMPatcher: self._log(" [-] TXM: binary search pattern not found in function") + def patch_selector24_force_pass(self): + """Force selector24 handler to return 0xA1 (PASS) immediately. + + Return code semantics (checked by caller via `tst w0, #0xff00`): + - 0xA1 (byte 1 = 0x00) → PASS + - 0x130A1 (byte 1 = 0x30) → FAIL + - 0x22DA1 (byte 1 = 0x2D) → FAIL + + We insert `mov w0, #0xa1 ; b ` right after the prologue, + skipping all validation logic while preserving the stack frame for + clean register restore via the existing epilogue. + """ + for off in range(0, self.size - 4, 4): + ins = _disasm_one(self.raw, off) + if not (ins and ins.mnemonic == "mov" and ins.op_str == "w0, #0xa1"): + continue + + func_start = self._find_func_start(off) + if func_start is None: + continue + + # Verify this is the selector24 handler by checking for the + # characteristic pattern: LDR X1,[Xn,#0x38] / ADD X2,... / BL / LDP + for scan in range(func_start, off, 4): + i0 = _disasm_one(self.raw, scan) + i1 = _disasm_one(self.raw, scan + 4) + i2 = _disasm_one(self.raw, scan + 8) + i3 = _disasm_one(self.raw, scan + 12) + if not all((i0, i1, i2, i3)): + continue + if not ( + i0.mnemonic == "ldr" + and "x1," in i0.op_str + and "#0x38]" in i0.op_str + ): + continue + if not (i1.mnemonic == "add" and i1.op_str.startswith("x2,")): + continue + if i2.mnemonic != "bl": + continue + if i3.mnemonic != "ldp": + continue + + # Find prologue end: scan for `add x29, sp, #imm` + body_start = None + for p in range(func_start + 4, func_start + 0x30, 4): + pi = _disasm_one(self.raw, p) + if pi and pi.mnemonic == "add" and pi.op_str.startswith("x29, sp,"): + body_start = p + 4 + break + if body_start is None: + self._log(" [-] TXM: selector24 prologue end not found") + return False + + # Find epilogue: scan for retab/ret, walk back to first ldp x29 + epilogue = None + for r in range(off, min(off + 0x200, self.size), 4): + ri = _disasm_one(self.raw, r) + if ri and ri.mnemonic in ("retab", "ret"): + for e in range(r - 4, max(r - 0x20, func_start), -4): + ei = _disasm_one(self.raw, e) + if ei and ei.mnemonic == "ldp" and "x29, x30" in ei.op_str: + epilogue = e + break + break + if epilogue is None: + self._log(" [-] TXM: selector24 epilogue not found") + return False + + self.emit( + body_start, + _asm("mov w0, #0xa1"), + "selector24 bypass: mov w0, #0xa1 (PASS)", + ) + self.emit( + body_start + 4, + self._asm_at(f"b #0x{epilogue:x}", body_start + 4), + "selector24 bypass: b epilogue", + ) + return True + + self._log(" [-] TXM: selector24 handler not found") + return False + 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") diff --git a/scripts/patchers/txm_jb.py b/scripts/patchers/txm_jb.py deleted file mode 100644 index 33feb9d..0000000 --- a/scripts/patchers/txm_jb.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -""" -txm_jb.py — Jailbreak extension patcher for TXM images. - -Reuses shared TXM logic from txm_dev.py and adds the selector24 CodeSignature -hash-extraction bypass used only by the JB variant. -""" - -from .txm_dev import TXMPatcher as TXMDevPatcher, _asm, _disasm_one - - -NOP = _asm("nop") - - -class TXMJBPatcher(TXMDevPatcher): - """JB-only TXM patcher: selector24 CS hash-extraction bypass. - - Dev patches are applied separately by txm_dev.py; this class only - adds the JB-exclusive selector24 extension. - """ - - 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_hash_extraction_nop() - return self.patches - - def patch_selector24_hash_extraction_nop(self): - """NOP hash-flags extraction setup/call in selector24 path.""" - for off in range(0, self.size - 4, 4): - ins = _disasm_one(self.raw, off) - if not (ins and ins.mnemonic == "mov" and ins.op_str == "w0, #0xa1"): - continue - - func_start = self._find_func_start(off) - if func_start is None: - continue - - # Scan function for: LDR X1,[Xn,#0x38] / ADD X2,... / BL / LDP - for scan in range(func_start, off, 4): - i0 = _disasm_one(self.raw, scan) - i1 = _disasm_one(self.raw, scan + 4) - i2 = _disasm_one(self.raw, scan + 8) - i3 = _disasm_one(self.raw, scan + 12) - if not all((i0, i1, i2, i3)): - continue - if not ( - i0.mnemonic == "ldr" - and "x1," in i0.op_str - and "#0x38]" in i0.op_str - ): - continue - if not (i1.mnemonic == "add" and i1.op_str.startswith("x2,")): - continue - if i2.mnemonic != "bl": - continue - if i3.mnemonic != "ldp": - continue - - self.emit(scan, NOP, "selector24 CS: nop ldr x1,[xN,#0x38]") - self.emit(scan + 8, NOP, "selector24 CS: nop bl hash_flags_extract") - return True - - self._log(" [-] TXM JB: selector24 hash extraction site not found") - return False