- Update firmware variant table (CLAUDE.md, AGENTS.md): correct patch totals to 51/64/126, add VPhoneMenuBattery.swift, setup_venv_linux.sh, tail_jb_patch_logs.sh to architecture tree - kernel_patcher_verification.md: 25→26 patches (patch_apfs_get_dev_by_role) - kernel_patch_sandbox.py: docstring 16-25→17-26 - kernel_patch_sandbox_hooks: consolidate to single 17-26 validation file - txm_jb_patches.md: fix txm_jb.py references → txm_dev.py - 0_binary_patch_comparison.md: split kernel counts (28 base + 59 JB methods) - kernel_jb_patch_notes.md: add 2026-03-06 retarget notes for bsd_init_auth, io_secure_bsd_root, vm_fault_enter_prepare - Remove 7 boot investigation process notes (boot_*.md)
27 KiB
TXM Jailbreak Patch Analysis
Analysis of 6 logical TXM jailbreak patches (11 instruction modifications) applied by txm_dev.py on the RESEARCH variant
of TXM from iPhone17,3 / PCC-CloudOS 26.x.
TXM Execution Model
TXM runs at a guested exception level (GL) under SPTM's supervision:
SPTM (GL2) — Secure Page Table Monitor
↕ svc #0
TXM (GL1) — Trusted Execution Monitor
↕ trap
Kernel (EL1/GL0)
SPTM dispatches selector calls into TXM. TXM cannot execute SPTM code
(instruction fetch permission fault). TXM must return to SPTM via svc #0.
Return Path: TXM → SPTM
All TXM functions return through this chain:
TXM function
→ bl return_helper (0x26c04)
→ bl return_trap_stub (0x49b40)
→ movk x16, ... (set SPTM return code)
→ b trampoline (0x60000)
→ pacibsp
→ svc #0 ← traps to SPTM
→ retab ← resumes after SPTM returns
Trampoline at 0x60000 (__TEXT_BOOT_EXEC)
0x060000: pacibsp
0x060004: svc #0 ; supervisor call → SPTM handles the trap
0x060008: retab ; return after SPTM gives control back
Return Trap Stub at 0x49B40 (__TEXT_EXEC)
0x049B40: bti c
0x049B44: movk x16, #0, lsl #48
0x049B48: movk x16, #0xfd, lsl #32 ; x16 = 0x000000FD00000000
0x049B4C: movk x16, #0, lsl #16 ; (SPTM return code identifier)
0x049B50: movk x16, #0
0x049B54: b #0x60000 ; → trampoline → svc #0
x16 carries a return code that SPTM uses to identify which TXM operation completed.
Return Helper at 0x26C04 (__TEXT_EXEC)
0x026C04: pacibsp
0x026C08: stp x20, x19, [sp, #-0x20]!
0x026C0C: stp x29, x30, [sp, #0x10]
0x026C10: add x29, sp, #0x10
0x026C14: mov x19, x0 ; save result code
0x026C18: bl #0x29024 ; get TXM context
0x026C1C: ldrb w8, [x0] ; check context flag
0x026C20: cbz w8, #0x26c30
0x026C24: mov x20, x0
0x026C28: bl #0x29010 ; cleanup if flag set
0x026C2C: strb wzr, [x20, #0x58]
0x026C30: mov x0, x19 ; restore result
0x026C34: bl #0x49b40 ; → svc #0 → SPTM
Error Handler
Error Handler at 0x25924
0x025924: pacibsp
...
0x025978: stp x19, x20, [sp] ; x19=error_code, x20=param
0x02597C: adrp x0, #0x1000
0x025980: add x0, x0, #0x8d8 ; format string
0x025984: bl #0x25744 ; log error → eventually svc #0
Panic Format
TXM [Error]: CodeSignature: selector: 24 | 0xA1 | 0x30 | 1
This is a kernel-side message (not in TXM binary). The kernel receives the
non-zero return from the svc #0 trap and formats the error:
selector: <selector_num> | <low_byte> | <mid_byte> | <high_byte>
For 0x000130A1: low=0xA1, mid=0x30, high=0x1 → | 0xA1 | 0x30 | 1
Why ret/retab Fails
Historical reference: These three failed attempts document why patching TXM functions to return early via normal return instructions does not work. The only viable path is to let the function proceed through the normal
svc #0return chain (see Return Path above).
Attempt 1: mov x0, #0; retab replacing PACIBSP
0x026C80: mov x0, #0 ; (was pacibsp)
0x026C84: retab ; verify PAC on LR → FAIL
Result: [TXM] Unhandled synchronous exception at pc 0x...6C84
RETAB tries to verify the PAC signature on LR. Since PACIBSP was replaced, LR was never signed. RETAB detects the invalid PAC → exception.
Attempt 2: mov x0, #0; ret replacing PACIBSP
0x026C80: mov x0, #0 ; (was pacibsp)
0x026C84: ret ; jump to LR (SPTM address)
Result: [TXM] Unhandled synchronous exception at pc 0x...FA88 (SPTM space)
ret strips PAC and jumps to clean LR, which points to SPTM code (the caller).
TXM cannot execute SPTM code → instruction fetch permission fault (ESR EC=0x20, IFSC=0xF).
Attempt 3: pacibsp; mov x0, #0; retab
0x026C80: pacibsp ; signs LR correctly
0x026C84: mov x0, #0
0x026C88: retab ; verifies PAC (OK), jumps to LR (SPTM address)
Result: Same permission fault — RETAB succeeds (PAC valid), but the return address is in SPTM space. TXM still cannot execute there.
Conclusion: No form of ret/retab works because the caller is SPTM
and TXM cannot return to SPTM via normal returns. The only way back is svc #0.
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 |
selector24 Full Control Flow (0x026C80 - 0x026E7C)
The selector 24 handler is the entry point for all CodeSignature validation. Understanding its full control flow is essential context for why the NOP approach was chosen over earlier redirect-based patches.
0x026C80: pacibsp ; prologue
0x026C84: sub sp, sp, #0x70
0x026C88-0x026C98: save x19-x30, setup fp
0x026CA0-0x026CB4: save args to x20-x25
0x026CB8: bl #0x29024 ; get TXM context → x0
0x026CBC: adrp x8, #0x6c000 ; flag address (page)
0x026CC0: add x8, x8, #0x5c0 ; flag address (offset)
0x026CC4: ldrb w8, [x8] ; flag check
0x026CC8: cbnz w8, #0x26cfc ; if flag → error 0xA0
0x026CCC: mov x19, x0 ; save context
0x026CD4: cmp w25, #2 ; switch on sub-selector (arg0)
b.gt → check 3,4,5
0x026CDC: cbz w25 → case 0
0x026CE4: b.eq → case 1
0x026CEC: b.ne → default (0xA1 error)
case 0: setup, b 0x26dc0
case 1: flag check, setup, b 0x26dfc
case 2: bl 0x1e0e8, b 0x26db8
case 3: bl 0x1e148, b 0x26db8
case 4: bl 0x1e568, b 0x26db8
case 5: flag → { mov x0,#0; b 0x26db8 } or { bl 0x1e70c; b 0x26db8 }
default: mov w0, #0xa1; b 0x26d00 (error path)
0x026DB8: and w8, w0, #0xffff ; result processing
0x026DBC: and x9, x0, #0xffffffffffff0000
0x026DC0: mov w10, w8
0x026DC4: orr x9, x9, x10
0x026DC8: str x9, [x19, #8] ; store result to context
0x026DCC: cmp w8, #0
0x026DD0: csetm x0, ne ; x0 = 0 (success) or -1 (error)
0x026DD4: bl #0x26c04 ; return via svc #0 ← SUCCESS RETURN
ERROR PATH:
0x026D00: mov w1, #0
0x026D04: bl #0x25924 ; error handler → svc #0
Existing Success Path
The function already has a success path at 0x026D30 (reached by case 5 when flag is set):
0x026D30: mov x0, #0 ; success result
0x026D34: b #0x26db8 ; → process result → str [x19,#8] → bl return_helper
Historical note: An earlier approach tried redirecting to this success path by patching 2 instructions at
0x26CBC(mov x19, x0/b #0x26D30). This was replaced with the more surgical NOP approach below because the redirect did not properly handle the hash validation return value.
Error Codes
| Return Value | Meaning |
|---|---|
0x00 |
Success (only via case 5 flag path) |
0xA0 |
Early flag check failure |
0xA1 |
Unknown sub-selector / validation failure |
0x130A1 |
Hash mismatch (hash presence != flag) |
0x22DA1 |
Version-dependent validation failure |
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_dev.py dynamic finder: patch_selector24_force_pass()
Scans for mov w0, #0xa1 as a unique anchor to locate the CS hash validator function,
finds PACIBSP to determine function start, then matches the pattern
LDR X1,[Xn,#0x38] / ADD X2,... / BL / LDP within it. NOPs the LDR X1
(arg setup) and the BL hash_flags_extract (call).
UUID Canary Verification
To confirm which TXM variant is loaded during boot, XOR the last byte of LC_UUID:
| UUID | |
|---|---|
| Original research | 0FFA437D-376F-3F8E-AD26-317E2111655D |
| Original release | 3C1E0E65-BFE2-3113-9C65-D25926C742B4 |
| Canary (research XOR 0x01) | 0FFA437D-376F-3F8E-AD26-317E2111655C |
Panic log TXM UUID: line confirmed canary ...655C → patched research TXM IS loaded.
The problem was exclusively in the selector24 patch logic (the earlier redirect approach
did not properly handle the hash validation return value).
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_dev.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_dev.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_dev.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_dev.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, 11 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)