Files
vphone-cli/researchs/txm_jb_patches.md
T
Lakr fa054ca719 Add Git LFS instructions and fix Makefile help alignment
- 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
2026-03-03 13:09:31 +08:00

17 KiB
Raw Blame History

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 (151). 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 12: 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()

  1. Finds the "debugger gate function" via string refs to "com.apple.private.cs.debugger"
  2. Locates the dispatch stub by matching BTI j / MOV X0, X20 / BL / MOV X1, X21 / MOV X2, X22 / BL debugger_gate / B
  3. Finds a zero-filled code cave via _find_udf_cave() near the stub
  4. 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 TBNZ would jump to loc_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 12: CodeSignature Hash Bypass (selector 24)
         Independent — bypasses CS hash validation in the signature chain

Boot-time flow

  1. TXM initializes → sub_FFFFFFF017023A20 runs → Patch 6 forces devmode ON
  2. Process loads → selector 24 validates CodeSignature → Patches 12 skip hash check
  3. Process requests entitlements → selector 41 → Patch 3 grants get-task-allow
  4. Debugger attaches → selector 42 → Patch 4 pre-sets flag + Patch 5 grants debugger ent
  5. 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 12)
  • Universal get-task-allow (patches 34)
  • Universal debugger entitlement (patch 5)
  • Forced developer mode (patch 6)