mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
461
research/txm_fullchain_analysis.md
Normal file
461
research/txm_fullchain_analysis.md
Normal 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.
|
||||
285
research/txm_selector24_analysis.md
Normal file
285
research/txm_selector24_analysis.md
Normal 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>`.
|
||||
@@ -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,
|
||||
# ),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user