mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 13:09:06 +08:00
Included commits: - f8a54b8 Update JB kernel patch research notes Refresh and revalidate jailbreak kernel-patcher documentation and runtime-verification notes. Key updates: re-analyzed B13 (patch_bsd_init_auth) and retargeted recommended site to the FSIOC_KERNEL_ROOTAUTH return check in bsd_init rather than the old ldr/cbz/bl heuristic; clarified preferred NOP-of-CBNZ vs forcing ioctl return. Reworked C21 (patch_cred_label_update_execve) to preserve AMFI exec-time flow and instead clear restrictive csflags in a success-tail trampoline; disabled in default schedule until boot validation. Documented that C23 (patch_hook_cred_label_update_execve) was mis-targeting the wrapper (sub_FFFFFE00093D2CE4) instead of the real hook body (_hook_cred_label_update_execve), explaining boot failures and recommending retargeting. Noted syscallmask and vm_fault matcher problems (patch_syscallmask_apply_to_proc historical hit targeted _profile_syscallmask_destroy; patch_vm_fault_enter_prepare matcher resolves to pmap_lock_phys_page path), and updated the runtime-verification summary with follow-up findings and which methods are temporarily commented out/disabled in the default KernelJBPatcher schedule pending staged re-validation. - 6ebac65 fix: patch_bsd_init_auth - 5b224d3 fix: patch_io_secure_bsd_root - e6806bf docs: update patch notes - 0d89c5c Retarget vm_fault_enter_prepare jailbreak patch - 6b9d79b Rework C21 late-exit cred_label patch - ece8cc0 Clean C21 mov matcher encodings - ad2ea7c enabled fixed patch_cred_label_update_execve - c37b6b1 Rebuild syscallmask C22 patch - 363dd7a Rebuild JB C23 as faithful upstream trampoline - 129e648 Disable IOUC MACF; rebuild kcall10 & C22 docs Re-evaluate and rework several JB kernel patches and docs: mark patch_iouc_failed_macf as reverted/disabled (repo-local, over-broad early-return) and replace its patcher with a no-op implementation to emit zero writes by default; update research notes to explain the reanalysis and rationale. Rebuild patch_kcall10: replace the historical 10-arg design with an ABI-correct syscall-439 cave (target + 7 args -> uint64 return), add a new cave builder and munge32 reuse logic in the kcall10 patcher, and enable the method in KernelJBPatcher group. Clarify syscallmask (C22) semantics in docs: upstream C22 is an all-ones-mask retarget (not a NULL install) and keep the rebuilt all-ones wrapper as the authoritative baseline. Misc: minor refactors and helper additions (chained-pointer helpers, cave size/constants, validation and dry-run safeguards) to improve correctness and alignment with IDA/runtime verification. - e1b2365 Rebuild kcall10 as ABI-correct syscall cave - 23090d0 fix patch_iouc_failed_macf - 0056be2 Normalize formatting in research docs Apply whitespace and formatting cleanup across research markdown files for consistency and readability. Adjust table alignment and spacing in 00_patch_comparison_all_variants.md, normalize list/indentation spacing in patch_bsd_init_auth.md and patch_syscallmask_apply_to_proc.md, and add/clean blank lines and minor spacing in patch_kcall10.md. These are non-functional documentation changes only.
324 lines
12 KiB
Python
324 lines
12 KiB
Python
"""Mixin: KernelJBPatchCredLabelMixin."""
|
|
|
|
from .kernel_jb_base import asm, _rd32
|
|
|
|
|
|
class KernelJBPatchCredLabelMixin:
|
|
_RET_INSNS = (0xD65F0FFF, 0xD65F0BFF, 0xD65F03C0)
|
|
_MOV_W0_0_U32 = int.from_bytes(asm("mov w0, #0"), "little")
|
|
_MOV_W0_1_U32 = int.from_bytes(asm("mov w0, #1"), "little")
|
|
_RELAX_CSMASK = 0xFFFFC0FF
|
|
_RELAX_SETMASK = 0x0000000C
|
|
|
|
def _is_cred_label_execve_candidate(self, func_off, anchor_refs):
|
|
"""Validate candidate function shape for _cred_label_update_execve."""
|
|
func_end = self._find_func_end(func_off, 0x1000)
|
|
if func_end - func_off < 0x200:
|
|
return False, 0, func_end
|
|
|
|
anchor_hits = sum(1 for r in anchor_refs if func_off <= r < func_end)
|
|
if anchor_hits == 0:
|
|
return False, 0, func_end
|
|
|
|
has_arg9_load = False
|
|
has_flags_load = False
|
|
has_flags_store = False
|
|
|
|
for off in range(func_off, func_end, 4):
|
|
d = self._disas_at(off)
|
|
if not d:
|
|
continue
|
|
i = d[0]
|
|
op = i.op_str.replace(" ", "")
|
|
if i.mnemonic == "ldr" and op.startswith("x26,[x29"):
|
|
has_arg9_load = True
|
|
break
|
|
|
|
for off in range(func_off, func_end, 4):
|
|
d = self._disas_at(off)
|
|
if not d:
|
|
continue
|
|
i = d[0]
|
|
op = i.op_str.replace(" ", "")
|
|
if i.mnemonic == "ldr" and op.startswith("w") and ",[x26" in op:
|
|
has_flags_load = True
|
|
elif i.mnemonic == "str" and op.startswith("w") and ",[x26" in op:
|
|
has_flags_store = True
|
|
if has_flags_load and has_flags_store:
|
|
break
|
|
|
|
ok = has_arg9_load and has_flags_load and has_flags_store
|
|
score = anchor_hits * 10 + (1 if has_arg9_load else 0) + (1 if has_flags_load else 0) + (1 if has_flags_store else 0)
|
|
return ok, score, func_end
|
|
|
|
def _find_cred_label_execve_func(self):
|
|
"""Locate _cred_label_update_execve by AMFI kill-path string cluster."""
|
|
anchor_strings = (
|
|
b"AMFI: hook..execve() killing",
|
|
b"Attempt to execute completely unsigned code",
|
|
b"Attempt to execute a Legacy VPN Plugin",
|
|
b"dyld signature cannot be verified",
|
|
)
|
|
|
|
anchor_refs = set()
|
|
candidates = set()
|
|
s, e = self.amfi_text
|
|
|
|
for anchor in anchor_strings:
|
|
str_off = self.find_string(anchor)
|
|
if str_off < 0:
|
|
continue
|
|
refs = self.find_string_refs(str_off, s, e)
|
|
if not refs:
|
|
refs = self.find_string_refs(str_off)
|
|
for adrp_off, _, _ in refs:
|
|
anchor_refs.add(adrp_off)
|
|
func_off = self.find_function_start(adrp_off)
|
|
if func_off >= 0 and s <= func_off < e:
|
|
candidates.add(func_off)
|
|
|
|
best_func = -1
|
|
best_score = -1
|
|
for func_off in sorted(candidates):
|
|
ok, score, _ = self._is_cred_label_execve_candidate(func_off, anchor_refs)
|
|
if ok and score > best_score:
|
|
best_score = score
|
|
best_func = func_off
|
|
|
|
return best_func
|
|
|
|
def _find_cred_label_return_site(self, func_off):
|
|
"""Pick a return site with full epilogue restore (SP/frame restored)."""
|
|
func_end = self._find_func_end(func_off, 0x1000)
|
|
fallback = -1
|
|
for off in range(func_end - 4, func_off, -4):
|
|
val = _rd32(self.raw, off)
|
|
if val not in self._RET_INSNS:
|
|
continue
|
|
if fallback < 0:
|
|
fallback = off
|
|
|
|
saw_add_sp = False
|
|
saw_ldp_fp = False
|
|
for prev in range(max(func_off, off - 0x24), off, 4):
|
|
d = self._disas_at(prev)
|
|
if not d:
|
|
continue
|
|
i = d[0]
|
|
op = i.op_str.replace(" ", "")
|
|
if i.mnemonic == "add" and op.startswith("sp,sp,#"):
|
|
saw_add_sp = True
|
|
elif i.mnemonic == "ldp" and op.startswith("x29,x30,[sp"):
|
|
saw_ldp_fp = True
|
|
|
|
if saw_add_sp and saw_ldp_fp:
|
|
return off
|
|
|
|
return fallback
|
|
|
|
def _find_cred_label_epilogue(self, func_off):
|
|
"""Locate the canonical epilogue start (`ldp x29, x30, [sp, ...]`)."""
|
|
func_end = self._find_func_end(func_off, 0x1000)
|
|
for off in range(func_end - 4, func_off, -4):
|
|
d = self._disas_at(off)
|
|
if not d:
|
|
continue
|
|
i = d[0]
|
|
op = i.op_str.replace(" ", "")
|
|
if i.mnemonic == "ldp" and op.startswith("x29,x30,[sp"):
|
|
return off
|
|
|
|
return -1
|
|
|
|
def _find_cred_label_csflags_ptr_reload(self, func_off):
|
|
"""Recover the stack-based `u_int *csflags` reload used by the function.
|
|
|
|
We reuse the same `ldr x26, [x29, #imm]` form in the trampoline so the
|
|
common C21-v1 cave works for both deny and success exits, even when the
|
|
live x26 register has not been initialized on a deny-only path.
|
|
"""
|
|
func_end = self._find_func_end(func_off, 0x1000)
|
|
for off in range(func_off, func_end, 4):
|
|
d = self._disas_at(off)
|
|
if not d:
|
|
continue
|
|
i = d[0]
|
|
op = i.op_str.replace(" ", "")
|
|
if i.mnemonic != "ldr" or not op.startswith("x26,[x29"):
|
|
continue
|
|
mem_op = i.op_str.split(",", 1)[1].strip()
|
|
return off, mem_op
|
|
|
|
return -1, None
|
|
|
|
def _decode_b_target(self, off):
|
|
"""Return target of unconditional `b`, or -1 if instruction is not `b`."""
|
|
insn = _rd32(self.raw, off)
|
|
if (insn & 0x7C000000) != 0x14000000:
|
|
return -1
|
|
imm26 = insn & 0x03FFFFFF
|
|
if imm26 & (1 << 25):
|
|
imm26 -= 1 << 26
|
|
return off + imm26 * 4
|
|
|
|
def _find_cred_label_deny_return(self, func_off, epilogue_off):
|
|
"""Find the shared `mov w0,#1` kill-return right before the epilogue."""
|
|
mov_w0_1 = self._MOV_W0_1_U32
|
|
scan_start = max(func_off, epilogue_off - 0x40)
|
|
for off in range(epilogue_off - 4, scan_start - 4, -4):
|
|
if _rd32(self.raw, off) == mov_w0_1 and off + 4 == epilogue_off:
|
|
return off
|
|
|
|
return -1
|
|
|
|
def _find_cred_label_success_exits(self, func_off, epilogue_off):
|
|
"""Find late success edges that already decided to return 0.
|
|
|
|
On the current vphone600 AMFI body these are the final `b epilogue`
|
|
instructions in the success tail, reached after the original
|
|
`tst/orr/str` cleanup has already run.
|
|
"""
|
|
exits = []
|
|
func_end = self._find_func_end(func_off, 0x1000)
|
|
for off in range(func_off, func_end, 4):
|
|
target = self._decode_b_target(off)
|
|
if target != epilogue_off:
|
|
continue
|
|
saw_mov_w0_0 = False
|
|
for prev in range(max(func_off, off - 0x10), off, 4):
|
|
if _rd32(self.raw, prev) == self._MOV_W0_0_U32:
|
|
saw_mov_w0_0 = True
|
|
break
|
|
if saw_mov_w0_0:
|
|
exits.append(off)
|
|
|
|
return tuple(exits)
|
|
|
|
def patch_cred_label_update_execve(self):
|
|
"""C21-v3: split late exits and add minimal helper bits on success.
|
|
|
|
This version keeps the boot-safe late-exit structure from v2, but adds
|
|
a small success-only extension inspired by the older upstream shellcode:
|
|
|
|
- keep `_cred_label_update_execve`'s body intact;
|
|
- redirect the shared deny return into a tiny deny cave that only
|
|
forces `w0 = 0` and returns through the original epilogue;
|
|
- redirect the late success exits into a success cave;
|
|
- reload `u_int *csflags` from the stack only on the success cave;
|
|
- clear only `CS_HARD|CS_KILL|CS_CHECK_EXPIRATION|CS_RESTRICT|
|
|
CS_ENFORCEMENT|CS_REQUIRE_LV` on the success cave;
|
|
- then OR only `CS_GET_TASK_ALLOW|CS_INSTALLER` (`0xC`) on the
|
|
success cave;
|
|
- return through the original epilogue in both cases.
|
|
|
|
This preserves AMFI's exec-time analytics / entitlement handling and
|
|
avoids the boot-unsafe entry-time early return used by older variants.
|
|
"""
|
|
self._log("\n[JB] _cred_label_update_execve: C21-v3 split exits + helper bits")
|
|
|
|
func_off = -1
|
|
|
|
# Try symbol first, but still validate shape.
|
|
for sym, off in self.symbols.items():
|
|
if "cred_label_update_execve" in sym and "hook" not in sym:
|
|
ok, _, _ = self._is_cred_label_execve_candidate(off, set([off]))
|
|
if ok:
|
|
func_off = off
|
|
break
|
|
|
|
if func_off < 0:
|
|
func_off = self._find_cred_label_execve_func()
|
|
|
|
if func_off < 0:
|
|
self._log(" [-] function not found, skipping shellcode patch")
|
|
return False
|
|
|
|
epilogue_off = self._find_cred_label_epilogue(func_off)
|
|
if epilogue_off < 0:
|
|
self._log(" [-] epilogue not found")
|
|
return False
|
|
|
|
deny_off = self._find_cred_label_deny_return(func_off, epilogue_off)
|
|
if deny_off < 0:
|
|
self._log(" [-] shared deny return not found")
|
|
return False
|
|
|
|
success_exits = self._find_cred_label_success_exits(func_off, epilogue_off)
|
|
if not success_exits:
|
|
self._log(" [-] success exits not found")
|
|
return False
|
|
|
|
_, csflags_mem_op = self._find_cred_label_csflags_ptr_reload(func_off)
|
|
if not csflags_mem_op:
|
|
self._log(" [-] csflags stack reload not found")
|
|
return False
|
|
|
|
deny_cave = self._find_code_cave(8)
|
|
if deny_cave < 0:
|
|
self._log(" [-] no code cave found for C21-v3 deny trampoline")
|
|
return False
|
|
|
|
success_cave = self._find_code_cave(32)
|
|
if success_cave < 0 or success_cave == deny_cave:
|
|
self._log(" [-] no code cave found for C21-v3 success trampoline")
|
|
return False
|
|
|
|
deny_branch_back = self._encode_b(deny_cave + 4, epilogue_off)
|
|
if not deny_branch_back:
|
|
self._log(" [-] branch from deny trampoline back to epilogue is out of range")
|
|
return False
|
|
|
|
success_branch_back = self._encode_b(success_cave + 28, epilogue_off)
|
|
if not success_branch_back:
|
|
self._log(" [-] branch from success trampoline back to epilogue is out of range")
|
|
return False
|
|
|
|
deny_shellcode = asm("mov w0, #0") + deny_branch_back
|
|
success_shellcode = (
|
|
asm(f"ldr x26, {csflags_mem_op}")
|
|
+ asm("cbz x26, #0x10")
|
|
+ asm("ldr w8, [x26]")
|
|
+ asm(f"and w8, w8, #{self._RELAX_CSMASK:#x}")
|
|
+ asm(f"orr w8, w8, #{self._RELAX_SETMASK:#x}")
|
|
+ asm("str w8, [x26]")
|
|
+ asm("mov w0, #0")
|
|
+ success_branch_back
|
|
)
|
|
|
|
for index in range(0, len(deny_shellcode), 4):
|
|
self.emit(
|
|
deny_cave + index,
|
|
deny_shellcode[index : index + 4],
|
|
f"deny_trampoline+{index} [_cred_label_update_execve C21-v3]",
|
|
)
|
|
|
|
for index in range(0, len(success_shellcode), 4):
|
|
self.emit(
|
|
success_cave + index,
|
|
success_shellcode[index : index + 4],
|
|
f"success_trampoline+{index} [_cred_label_update_execve C21-v3]",
|
|
)
|
|
|
|
deny_branch_to_cave = self._encode_b(deny_off, deny_cave)
|
|
if not deny_branch_to_cave:
|
|
self._log(f" [-] branch from 0x{deny_off:X} to deny trampoline is out of range")
|
|
return False
|
|
self.emit(
|
|
deny_off,
|
|
deny_branch_to_cave,
|
|
f"b deny cave [_cred_label_update_execve C21-v3 exit @ 0x{deny_off:X}]",
|
|
)
|
|
|
|
for off in success_exits:
|
|
branch_to_cave = self._encode_b(off, success_cave)
|
|
if not branch_to_cave:
|
|
self._log(f" [-] branch from 0x{off:X} to success trampoline is out of range")
|
|
return False
|
|
self.emit(
|
|
off,
|
|
branch_to_cave,
|
|
f"b success cave [_cred_label_update_execve C21-v3 exit @ 0x{off:X}]",
|
|
)
|
|
|
|
return True
|