- Add git-lfs to brew deps and document git lfs install/pull steps in both English and Chinese READMEs - Fix continuation line alignment in make help (off by one) - Add missing blank line before VM management section
17 KiB
TXM Jailbreak Patch Analysis
Analysis of 13 TXM jailbreak patches applied by txm_jb.py on the RESEARCH variant
of TXM from iPhone17,3 / PCC-CloudOS 26.x.
Address Mapping
| Segment | VM Address | File Offset | Size |
|---|---|---|---|
__TEXT_EXEC |
0xFFFFFFF017020000 |
0x1c000 |
0x44000 |
__TEXT_BOOT_EXEC |
0xFFFFFFF017064000 |
0x60000 |
0xc000 |
Conversion: VA = file_offset - 0x1c000 + 0xFFFFFFF017020000 (for __TEXT_EXEC)
TXM Selector Dispatch
All TXM operations enter through a single dispatch function (sub_FFFFFFF01702AE80),
a large switch on the selector number (1–51). Each case validates arguments and calls
a dedicated handler. Relevant selectors:
| Selector | Handler | Purpose |
|---|---|---|
| 24 | sub_FFFFFFF017024834 → validation chain |
CodeSignature validation |
| 41 | sub_FFFFFFF017023558 |
Process entitlement setup (get-task-allow) |
| 42 | sub_FFFFFFF017023368 |
Debug memory mapping |
| — | sub_FFFFFFF017023A20 |
Developer mode configuration (called during init) |
The dispatcher passes raw page pointers through sub_FFFFFFF0170280A4 (a bounds
validator that returns the input pointer unchanged) before calling handlers.
Patch 1–2: CodeSignature Hash Comparison Bypass (selector 24)
Error: TXM [Error]: CodeSignature: selector: 24 | 0xA1 | 0x30 | 1
Addresses
| File Offset | VA | Original Instruction | Patch |
|---|---|---|---|
0x313ec |
0xFFFFFFF0170353EC |
LDR X1, [X20, #0x38] |
NOP |
0x313f4 |
0xFFFFFFF0170353F4 |
BL sub_FFFFFFF0170335F8 |
NOP |
Function: sub_FFFFFFF0170353B8 — CS hash flags validator
Call chain: selector 24 → sub_FFFFFFF017024834 (CS handler) →
sub_FFFFFFF0170356F8 (CS validation pipeline) → sub_FFFFFFF017035A00
(multi-step validation, step 4 of 8) → sub_FFFFFFF0170353B8
Decompiled (pre-patch)
// sub_FFFFFFF0170353B8(manifest_ptr, version)
__int64 __fastcall sub_FFFFFFF0170353B8(__int64 **a1, unsigned int a2)
{
__int64 v4 = **a1;
__int64 v7 = 0; // hash data pointer
int v6 = 0; // hash flags
// Patch 1: NOP removes arg load (LDR X1, [X20, #0x38])
// Patch 2: NOP removes this call entirely:
sub_FFFFFFF0170335F8(a1[6], a1[7], &v6); // extract hash flags from CS blob
sub_FFFFFFF017033718(a1[6], a1[7], &v7); // extract hash data pointer
if ( a2 >= 6 && *(v4 + 8) )
return 0xA1; // 161
// Critical comparison: does hash presence match flags?
if ( (v7 != 0) == ((v6 & 2) >> 1) )
return 0x130A1; // 77985 — hash mismatch
// ... further version-dependent checks return 0xA1 or 0x22DA1
}
What sub_FFFFFFF0170335F8 does
Extracts hash flags from the CodeSignature blob header. Reads bswap32(*(blob + 12))
into the output parameter (the flags bitmask). Bit 1 of the flags indicates whether
a code hash is present.
What sub_FFFFFFF017033718 does
Locates the hash data within the CodeSignature blob. Validates blob header version
(bswap32(*(blob+8)) >> 9 >= 0x101), then follows a length-prefixed string pointer
at offset 48 to find the hash data. Returns the hash data pointer via output param.
Effect of NOP
With sub_FFFFFFF0170335F8 NOPed, v6 stays at its initialized value of 0.
This means (v6 & 2) >> 1 = 0 (hash-present flag is cleared). As long as
sub_FFFFFFF017033718 returns a non-null hash pointer (v7 != 0), the comparison
becomes (1 == 0) → false, so the 0x130A1 error is skipped. The function
falls through to the version checks which return success for version ≤ 5.
This effectively bypasses CodeSignature hash validation — the hash data exists in the blob but the hash-present flag is suppressed, so the consistency check passes.
txm_jb.py dynamic finder: patch_selector24_hashcmp_calls()
Scans for the instruction pattern mov w2, #0x14 / bl X / cbz w0, Y to find
hashcmp BL callsites, then patches the BL to mov x0, #0. This also includes
patch_selector24_a1_path() which NOPs the b.lo and cbz x9 guards around
the mov w0, #0xa1 error path.
Patch 3: get-task-allow Force True (selector 41)
Error: TXM [Error]: selector: 41 | 29
Address
| File Offset | VA | Original Instruction | Patch |
|---|---|---|---|
0x1f5d4 |
0xFFFFFFF0170235D4 |
BL sub_FFFFFFF017022A30 |
MOV X0, #1 |
Function: sub_FFFFFFF017023558 — selector 41 handler
Call chain: selector 41 → sub_FFFFFFF0170280A4 (ptr validation) →
sub_FFFFFFF017023558
Decompiled (pre-patch)
// sub_FFFFFFF017023558(manifest)
__int64 __fastcall sub_FFFFFFF017023558(__int64 a1)
{
// Check developer mode is enabled (byte_FFFFFFF017070F24)
if ( (byte_FFFFFFF017070F24 & 1) == 0 )
return 27; // developer mode not enabled
// Check license-to-operate entitlement (always first)
sub_FFFFFFF017022A30(0, "research.com.apple.license-to-operate", 0);
// Lock manifest
sub_FFFFFFF017027074(a1, 0, 0);
if ( *(a1 + 36) == 1 ) // special manifest type
goto error_path; // return via panic(0x81)
// === PATCHED INSTRUCTION ===
// Original: BL sub_FFFFFFF017022A30 — entitlement_lookup(manifest, "get-task-allow", 0)
// Patched: MOV X0, #1
if ( (sub_FFFFFFF017022A30(a1, "get-task-allow", 0) & 1) != 0 ) // TBNZ w0, #0
{
v3 = 0; // success
*(a1 + 0x30) = 1; // set get-task-allow flag on manifest
}
else
{
v3 = 29; // ERROR 29: no get-task-allow entitlement
}
sub_FFFFFFF01702717C(a1, 0); // unlock manifest
return v3;
}
Assembly at patch site
FFFFFFF0170235C4 ADRL X1, "get-task-allow"
FFFFFFF0170235CC MOV X0, X19 ; manifest object
FFFFFFF0170235D0 MOV X2, #0
FFFFFFF0170235D4 BL sub_FFFFFFF017022A30 ; <-- PATCHED to MOV X0, #1
FFFFFFF0170235D8 TBNZ W0, #0, loc_... ; always taken when x0=1
Effect
Replaces the entitlement lookup call with a constant 1. The subsequent TBNZ W0, #0
always takes the branch to the success path, which sets *(manifest + 0x30) = 1
(the get-task-allow flag byte). Every process now has get-task-allow, enabling
debugging via task_for_pid and LLDB attach.
What sub_FFFFFFF017022A30 does
Universal entitlement lookup function. When a1 != 0, it resolves the manifest's
entitlement dictionary and searches for the named key via sub_FFFFFFF017036294.
Returns a composite status word where bit 0 indicates the entitlement was found.
txm_jb.py dynamic finder: patch_get_task_allow_force_true()
Searches for string refs to "get-task-allow", then scans forward for the pattern
BL X / TBNZ w0, #0, Y. Patches the BL to MOV X0, #1.
Patch 4: selector 42|29 Shellcode (Debug Mapping Gate)
Error: TXM [Error]: selector: 42 | 29
Addresses
| File Offset | VA | Patch |
|---|---|---|
0x2717c |
0xFFFFFFF01702B17C |
B #0x36238 (→ shellcode) |
0x5d3b4 |
0xFFFFFFF0170613B4 |
NOP (pad) |
0x5d3b8 |
0xFFFFFFF0170613B8 |
MOV X0, #1 |
0x5d3bc |
0xFFFFFFF0170613BC |
STRB W0, [X20, #0x30] |
0x5d3c0 |
0xFFFFFFF0170613C0 |
MOV X0, X20 |
0x5d3c4 |
0xFFFFFFF0170613C4 |
B #-0x36244 (→ 0xB180) |
Context: Dispatcher case 42
; jumptable case 42 entry in sub_FFFFFFF01702AE80:
FFFFFFF01702B178 BTI j
FFFFFFF01702B17C MOV X0, X20 ; <-- PATCHED to B shellcode
FFFFFFF01702B180 BL sub_FFFFFFF0170280A4 ; validate pointer
FFFFFFF01702B184 MOV X1, X21
FFFFFFF01702B188 MOV X2, X22
FFFFFFF01702B18C BL sub_FFFFFFF017023368 ; selector 42 handler
FFFFFFF01702B190 B loc_FFFFFFF01702B344 ; return result
Shellcode (at zero-filled code cave in __TEXT_EXEC)
; 0xFFFFFFF0170613B4 — cave was all zeros
NOP ; pad (original 0x00000000)
MOV X0, #1 ; value to store
STRB W0, [X20, #0x30] ; force manifest->get_task_allow = 1
MOV X0, X20 ; restore original instruction (was at 0xB17C)
B #-0x36244 ; jump back to 0xFFFFFFF01702B180 (BL validate)
Why this is needed
Selector 42's handler sub_FFFFFFF017023368 checks the get-task-allow byte early:
// sub_FFFFFFF017023368(manifest, addr, size)
// ... after debugger entitlement check ...
v8 = atomic_load((unsigned __int8 *)(a1 + 48)); // offset 0x30
if ( (v8 & 1) == 0 )
{
v6 = 29; // ERROR 29: get-task-allow not set
goto unlock_and_return;
}
// ... proceed with debug memory mapping ...
Selector 41 (patch 3) sets this byte during entitlement validation, but there are code paths where selector 42 can be called before selector 41 has run for a given manifest. The shellcode ensures the flag is always set at the dispatch level before the handler even sees it.
sub_FFFFFFF0170280A4 — pointer validator
// Validates page alignment and bounds, returns input pointer unchanged
unsigned __int64 sub_FFFFFFF0170280A4(unsigned __int64 a1) {
if ( (a1 & ~0x3FFF) == 0 ) panic(64);
if ( a1 >= 0xFFFFFFFFFFFFC000 ) panic(66);
// ... bounds checks ...
return (a1 & ~0x3FFF) + (a1 & 0x3FFF); // == a1
}
Since the validator returns the pointer unchanged, x20 (raw arg) and the validated
pointer both refer to the same object. The shellcode's STRB W0, [X20, #0x30]
writes to the correct location.
txm_jb.py dynamic finder: patch_selector42_29_shellcode()
- Finds the "debugger gate function" via string refs to
"com.apple.private.cs.debugger" - Locates the dispatch stub by matching
BTI j / MOV X0, X20 / BL / MOV X1, X21 / MOV X2, X22 / BL debugger_gate / B - Finds a zero-filled code cave via
_find_udf_cave()near the stub - Emits the branch + shellcode + branch-back
Patch 5: Debugger Entitlement Force True (selector 42)
Error: TXM [Error]: selector: 42 | 37
Address
| File Offset | VA | Original Instruction | Patch |
|---|---|---|---|
0x1f3b8 |
0xFFFFFFF0170233B8 |
BL sub_FFFFFFF017022A30 |
MOV W0, #1 |
Function: sub_FFFFFFF017023368 — selector 42 handler (debug memory mapping)
Assembly at patch site
; Check com.apple.private.cs.debugger entitlement
FFFFFFF0170233A8 ADRL X1, "com.apple.private.cs.debugger"
FFFFFFF0170233B0 MOV X0, #0 ; check global manifest (a1=0)
FFFFFFF0170233B4 MOV X2, #0
FFFFFFF0170233B8 BL sub_FFFFFFF017022A30 ; <-- PATCHED to MOV W0, #1
FFFFFFF0170233BC TBNZ W0, #0, loc_... ; always taken when w0=1
FFFFFFF0170233C0 ADRL X8, fallback_flag ; secondary check (also bypassed)
FFFFFFF0170233C8 LDRB W8, [X8, #offset]
FFFFFFF0170233CC TBNZ W8, #0, loc_... ; secondary bypass path
FFFFFFF0170233D0 ADRL X0, "disallowed non-debugger initiated debug mapping"
FFFFFFF0170233D8 BL sub_FFFFFFF017025B7C ; log error
FFFFFFF0170233DC MOV W20, #0x25 ; error 37
FFFFFFF0170233E0 B unlock_return
Decompiled (pre-patch)
// First check in sub_FFFFFFF017023368 after input validation:
if ( (sub_FFFFFFF017022A30(0, "com.apple.private.cs.debugger", 0) & 1) == 0 )
{
// Fallback: check a static byte flag
if ( (fallback_flag & 1) == 0 )
{
log("disallowed non-debugger initiated debug mapping");
return 37; // 0x25
}
}
// Continue with debug mapping...
Effect
Replaces the entitlement lookup with MOV W0, #1. The TBNZ W0, #0 always
branches to the success path, bypassing both the entitlement check and the
fallback flag check. This allows any process to create debug memory mappings
regardless of whether it has com.apple.private.cs.debugger.
txm_jb.py dynamic finder: patch_debugger_entitlement_force_true()
Searches for string refs to "com.apple.private.cs.debugger", then matches
the pattern: mov x0, #0 / mov x2, #0 / bl X / tbnz w0, #0, Y. Patches the BL
to MOV W0, #1.
Patch 6: Developer Mode Bypass
Address
| File Offset | VA | Original Instruction | Patch |
|---|---|---|---|
0x1FA58 |
0xFFFFFFF017023A58 |
TBNZ W9, #0, loc_FFFFFFF017023A6C |
NOP |
Function: sub_FFFFFFF017023A20 — developer mode configuration
Called during TXM initialization to determine and store the developer mode state.
The result is stored in byte_FFFFFFF017070F24, which is the gate flag checked by
selector 41 (sub_FFFFFFF017023558).
Assembly at patch site
; Check system policy configuration
FFFFFFF017023A50 LDR X9, [X8, #off_FFFFFFF0170146C0]
FFFFFFF017023A54 LDRB W9, [X9, #0x4D] ; load system policy byte
FFFFFFF017023A58 TBNZ W9, #0, loc_FFFFFFF017023A6C ; <-- PATCHED to NOP
; Fall through to force-enable:
FFFFFFF017023A5C MOV W20, #1 ; developer_mode = ENABLED
FFFFFFF017023A60 ADRL X0, "developer mode enabled due to system policy configuration"
FFFFFFF017023A68 B log_and_store
Decompiled (pre-patch)
__int64 sub_FFFFFFF017023A20(__int64 manifest)
{
char devmode;
// Check 1: PCC research variant flag
if ( pcc_research_flag )
{
devmode = 1;
goto apply;
}
// Check 2: System policy (patched here)
byte policy = *(system_config_ptr + 0x4D);
if ( (policy & 1) != 0 ) // <-- TBNZ jumps past force-enable
goto normal_path; // to xART / user-config checks
// Force-enable path (reached by NOPing the TBNZ):
devmode = 1;
log("developer mode enabled due to system policy configuration");
goto apply;
normal_path:
// ... xART availability check ...
// ... user configuration check ...
// May set devmode = 0 (disabled) based on config
apply:
byte_FFFFFFF017070F24 = devmode; // global developer mode state
return result;
}
Effect
NOPing the TBNZ makes execution always fall through to MOV W20, #1, forcing
developer mode enabled regardless of the system policy byte. Without this:
- The
TBNZwould jump toloc_FFFFFFF017023A6C(the normal path) - The normal path checks xART availability, device tree flags, and user configuration
- On PCC VMs, this can result in developer mode being disabled
Developer mode is a prerequisite for selectors 41 and 42 — the selector 41
handler returns error 27 immediately if byte_FFFFFFF017070F24 is not set:
// In sub_FFFFFFF017023558 (selector 41):
if ( (byte_FFFFFFF017070F24 & 1) == 0 )
return 27; // developer mode not enabled
txm_jb.py dynamic finder: patch_developer_mode_bypass()
Searches for string refs to "developer mode enabled due to system policy configuration", then scans backwards for a tbz/tbnz/cbz/cbnz instruction
matching w9, #0. NOPs it.
Patch Dependency Chain
The patches have a logical ordering — later patches depend on earlier ones:
Patch 6: Developer Mode Bypass
│ Forces byte_FFFFFFF017070F24 = 1
│
├──► Patch 3: get-task-allow Force True (selector 41)
│ Requires developer mode (checks byte_FFFFFFF017070F24)
│ Forces manifest[0x30] = 1
│
├──► Patch 4: selector 42|29 Shellcode
│ Forces manifest[0x30] = 1 at dispatch level
│ Safety net for Patch 3 (covers cases where sel 42 runs before sel 41)
│
├──► Patch 5: Debugger Entitlement Force True (selector 42)
│ Bypasses com.apple.private.cs.debugger check
│ Allows debug memory mapping for all processes
│
└──► Patches 1–2: CodeSignature Hash Bypass (selector 24)
Independent — bypasses CS hash validation in the signature chain
Boot-time flow
- TXM initializes →
sub_FFFFFFF017023A20runs → Patch 6 forces devmode ON - Process loads → selector 24 validates CodeSignature → Patches 1–2 skip hash check
- Process requests entitlements → selector 41 → Patch 3 grants get-task-allow
- Debugger attaches → selector 42 → Patch 4 pre-sets flag + Patch 5 grants debugger ent
- Debug mapping succeeds → LLDB can attach to any process
Summary Table
| # | File Offset | VA | Function | Patch | Purpose |
|---|---|---|---|---|---|
| 1 | 0x313ec |
0xFFFFFFF0170353EC |
sub_FFFFFFF0170353B8 (CS hash validator) |
NOP | Remove hash flag load |
| 2 | 0x313f4 |
0xFFFFFFF0170353F4 |
sub_FFFFFFF0170353B8 (CS hash validator) |
NOP | Skip hash flag extraction call |
| 3 | 0x1f5d4 |
0xFFFFFFF0170235D4 |
sub_FFFFFFF017023558 (selector 41) |
MOV X0, #1 |
Force get-task-allow = true |
| 4 | 0x2717c |
0xFFFFFFF01702B17C |
sub_FFFFFFF01702AE80 (dispatcher, case 42) |
B shellcode |
Redirect to shellcode cave |
| 4a | 0x5d3b4 |
0xFFFFFFF0170613B4 |
code cave (zeros) | NOP |
Shellcode padding |
| 4b | 0x5d3b8 |
0xFFFFFFF0170613B8 |
code cave | MOV X0, #1 |
Set value for flag |
| 4c | 0x5d3bc |
0xFFFFFFF0170613BC |
code cave | STRB W0, [X20,#0x30] |
Force get-task-allow flag |
| 4d | 0x5d3c0 |
0xFFFFFFF0170613C0 |
code cave | MOV X0, X20 |
Restore original instruction |
| 4e | 0x5d3c4 |
0xFFFFFFF0170613C4 |
code cave | B back |
Return to dispatcher |
| 5 | 0x1f3b8 |
0xFFFFFFF0170233B8 |
sub_FFFFFFF017023368 (selector 42) |
MOV W0, #1 |
Force debugger entitlement = true |
| 6 | 0x1FA58 |
0xFFFFFFF017023A58 |
sub_FFFFFFF017023A20 (devmode init) |
NOP | Force developer mode ON |
Total: 6 logical patches, 10 instruction modifications (counting shellcode), enabling:
- CodeSignature bypass (patches 1–2)
- Universal get-task-allow (patches 3–4)
- Universal debugger entitlement (patch 5)
- Forced developer mode (patch 6)