fix TXM selector24 bypass: return 0xA1 (PASS) instead of NOP/fail

The original selector24 patches (NOP ldr + NOP bl) broke the hash flags
extraction, causing the consistency check to fail. The second attempt
(return 0x130A1) also failed because the return code semantics were
inverted — byte 1 != 0 means FAIL, not success.

Correct approach: insert `mov w0, #0xa1; b <epilogue>` after the prologue.
0xA1 has byte 1 = 0 which the caller checks via `tst w0, #0xff00` as PASS.

Update AGENTS.md

move selector24 bypass from txm_jb.py to txm_dev.py, delete TXMJBPatcher

Selector24 CS validation bypass now applies to both dev and JB variants
via txm_dev.py. The separate txm_jb.py patcher is removed since it had
no other patches. Dev boot chain: 47→49 patches.

Create txm_fullchain_analysis.md
This commit is contained in:
Lakr
2026-03-04 18:48:46 +08:00
parent 557486845c
commit 03cb2a8389
7 changed files with 869 additions and 115 deletions

View File

@@ -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)

View File

@@ -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 <epilogue>` | 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 <epilogue>` 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.

View File

@@ -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.

View File

@@ -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 <error>`:
- **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 <epilogue>` 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 <epilogue>`.

View File

@@ -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,
# ),
]

View File

@@ -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 <epilogue>` 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")

View File

@@ -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