mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
Fix A2: rewrite AMFI execve kill path patch to target shared epilogue
Old approach patched vnode-type assertion BLs (CBZ→panic). New approach scans backward from function end for the shared MOV W0,#1 kill return before the LDP x29,x30 epilogue and changes it to MOV W0,#0. Single instruction converts all 5+ kill paths to success. Boot-tested OK.
This commit is contained in:
@@ -1,28 +1,73 @@
|
||||
# A2 `patch_amfi_execve_kill_path` (currently commented as PANIC in the main flow)
|
||||
# A2 `patch_amfi_execve_kill_path`
|
||||
|
||||
## 1) How the Patch Is Applied
|
||||
- Source implementation: `scripts/patchers/kernel_jb_patch_amfi_execve.py`
|
||||
- Match strategy:
|
||||
- String anchor: `"AMFI: hook..execve() killing"` (fallback: `"execve() killing"`).
|
||||
- After finding the function that references this string, scan its early region for **two** `BL + (cbz/cbnz w0,...)` check sites.
|
||||
- Rewrite: replace both `BL` instructions with `mov x0, #0` (direct success-result semantics).
|
||||
- Find function containing the string reference.
|
||||
- Scan backward from function end for `MOV W0, #1` (0x52800020) immediately
|
||||
followed by `LDP x29, x30, [sp, #imm]` (epilogue start).
|
||||
- Rewrite: replace `MOV W0, #1` with `MOV W0, #0` — converts kill return to allow.
|
||||
|
||||
## 2) Expected Behavior
|
||||
- Short-circuit failure decisions in the AMFI execve kill helper and avoid entering the kill path.
|
||||
- All kill paths in the AMFI execve hook converge on a shared `MOV W0, #1` before
|
||||
the function epilogue. Changing this single instruction to `MOV W0, #0` converts
|
||||
every kill path to a success return:
|
||||
- "completely unsigned code" → allowed
|
||||
- Restricted Execution Mode violations → allowed
|
||||
- "Legacy VPN Plugin" → allowed
|
||||
- "dyld signature cannot be verified" → allowed
|
||||
- Generic `%s` kill message → allowed
|
||||
|
||||
## 3) Target
|
||||
- Target function: execve-related AMFI helper (log string indicates the `hook..execve() killing ...` path).
|
||||
- Security objective: relax execution-path checks and reduce termination on unsigned/invalid signatures.
|
||||
- Target function: `sub_FFFFFE000863FC6C` (AMFI `hook..execve()` handler)
|
||||
- Located in `com.apple.driver.AppleMobileFileIntegrity:__text`
|
||||
- Target instruction: `MOV W0, #1` at `0xFFFFFE00086400FC` (shared kill return)
|
||||
- Followed by LDP x29,x30 → epilogue → RETAB
|
||||
|
||||
## 4) IDA MCP Binary Evidence
|
||||
- String hits (examples):
|
||||
- `0xfffffe00071f71c2` `AMFI: hook..execve() killing ... unsigned code ...`
|
||||
- `0xfffffe00071f73b8` `... Legacy VPN Plugin ...`
|
||||
- `0xfffffe00071f740b` `... dyld signature cannot be verified ...`
|
||||
- Xrefs for these strings all land in the same function:
|
||||
- xref: `0xfffffe000863fcfc / 0xfffffe000863feb4 / 0xfffffe000863fef4`
|
||||
- function start: `0xfffffe000863fc6c`
|
||||
|
||||
## 5) Risks and Side Effects
|
||||
- This patch is already commented as `PANIC` in `find_all()`, indicating known stability risk on the current sample.
|
||||
- Flattening two check return values may break downstream assumptions (for example, uninitialized state being treated as success).
|
||||
### Function structure
|
||||
- Prologue: PACIBSP + SUB SP + STP register saves
|
||||
- Assertions (NOT patched): Two vnode type checks at early offsets:
|
||||
- `BL sub_0x7CCC40C` (checks `*(vnode+113) == 1` i.e. regular file)
|
||||
- `BL sub_0x7CCC41C` (checks `*(vnode+113) == 2` i.e. directory)
|
||||
- These branch to assertion panic handlers on failure
|
||||
- Kill paths: 5+ conditional branches to `B loc_FFFFFE00086400F8` (print + kill):
|
||||
- `0xFE000863FD1C`: unsigned code path → B directly to `0x86400FC`
|
||||
- `0xFE000863FE00`: restricted exec mode = 2 → B `0x86400F8`
|
||||
- `0xFE000863FE4C`: restricted exec mode = 4 → B `0x86400F8`
|
||||
- `0xFE000863FEBC`: Legacy VPN Plugin → B `0x86400F8`
|
||||
- `0xFE000863FF38`: restricted exec mode = 3 → B `0x86400F8`
|
||||
- Shared kill epilogue at `0xFE00086400F8`:
|
||||
```
|
||||
0x86400F8: BL sub_81A1134 ; printf the kill message
|
||||
0x86400FC: MOV W0, #1 ; ← PATCH TARGET (kill return value)
|
||||
0x8640100: LDP X29, X30, [SP,#0x80]
|
||||
...
|
||||
0x864011C: RETAB
|
||||
```
|
||||
|
||||
### String hits
|
||||
- `0xFE00071F71C2`: "AMFI: hook..execve() killing %s (pid %u): Attempt to execute completely unsigned code..."
|
||||
- `0xFE00071F73B8`: "...Attempt to execute a Legacy VPN Plugin."
|
||||
- `0xFE00071F740B`: "...dyld signature cannot be verified..."
|
||||
- `0xFE00071F74DF`: "AMFI: hook..execve() killing %s (pid %u): %s\n"
|
||||
|
||||
## 5) Previous Bug (PANIC root cause)
|
||||
The original implementation searched for `BL + CBZ/CBNZ w0` patterns in the
|
||||
first 0x120 bytes and found the vnode-type assertion BLs:
|
||||
1. `BL sub_0x7CCC40C` + `CBZ W0` → checks if vnode is a regular file
|
||||
2. `BL sub_0x7CCC41C` + `CBNZ W0` → checks if vnode is a directory
|
||||
|
||||
Replacing the first BL with `MOV X0, #0` made W0=0, triggering `CBZ W0` →
|
||||
jumped to `BL sub_FFFFFE000865A5C4` (assertion panic handler) → kernel panic.
|
||||
|
||||
These are **precondition assertions**, not AMFI kill checks. The actual kill
|
||||
logic is deeper in the function and uses `return 1` via the shared epilogue.
|
||||
|
||||
## 6) Fix Applied
|
||||
- Replaced the BL+CBZ/CBNZ pattern matching with backward epilogue scan.
|
||||
- Single-instruction patch: `MOV W0, #1` → `MOV W0, #0` at the shared kill return.
|
||||
- All kill paths now return 0 (allow) instead of 1 (kill).
|
||||
- Assertion checks remain untouched (they pass naturally for valid executables).
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
"""Mixin: KernelJBPatchAmfiExecveMixin."""
|
||||
|
||||
from .kernel_jb_base import MOV_X0_0
|
||||
from .kernel_jb_base import MOV_W0_0, _rd32
|
||||
|
||||
|
||||
class KernelJBPatchAmfiExecveMixin:
|
||||
def patch_amfi_execve_kill_path(self):
|
||||
"""Bypass AMFI execve kill helpers (string xref -> function local pair)."""
|
||||
self._log("\n[JB] AMFI execve kill path: BL -> mov x0,#0 (2 sites)")
|
||||
"""Bypass AMFI execve kill by changing the shared kill return value.
|
||||
|
||||
All kill paths in the AMFI execve hook converge on a shared epilogue
|
||||
that does ``MOV W0, #1`` (kill) then returns. We change that single
|
||||
instruction to ``MOV W0, #0`` (allow), which converts every kill path
|
||||
to a success return without touching the rest of the function.
|
||||
|
||||
Previous approach (patching early BL+CBZ/CBNZ sites) was incorrect:
|
||||
those are vnode-type precondition assertions, not the actual kill
|
||||
checks. Replacing BL with MOV X0,#0 triggered the CBZ → panic.
|
||||
"""
|
||||
self._log("\n[JB] AMFI execve kill path: shared MOV W0,#1 → MOV W0,#0")
|
||||
|
||||
str_off = self.find_string(b"AMFI: hook..execve() killing")
|
||||
if str_off < 0:
|
||||
@@ -37,31 +47,41 @@ class KernelJBPatchAmfiExecveMixin:
|
||||
func_end = p
|
||||
break
|
||||
|
||||
early_window_end = min(func_start + 0x120, func_end)
|
||||
hits = []
|
||||
for off in range(func_start, early_window_end - 4, 4):
|
||||
d0 = self._disas_at(off)
|
||||
# Scan backward from function end for MOV W0, #1 (0x52800020)
|
||||
# followed by LDP x29, x30 (epilogue start).
|
||||
MOV_W0_1_ENC = 0x52800020
|
||||
target_off = -1
|
||||
for off in range(func_end - 8, func_start, -4):
|
||||
if _rd32(self.raw, off) != MOV_W0_1_ENC:
|
||||
continue
|
||||
# Verify next instruction is LDP x29, x30, [sp, #imm]
|
||||
d1 = self._disas_at(off + 4)
|
||||
if not d0 or not d1:
|
||||
if not d1:
|
||||
continue
|
||||
i0, i1 = d0[0], d1[0]
|
||||
if i0.mnemonic != "bl":
|
||||
continue
|
||||
if i1.mnemonic in ("cbz", "cbnz") and i1.op_str.startswith("w0,"):
|
||||
hits.append(off)
|
||||
i1 = d1[0]
|
||||
if i1.mnemonic == "ldp" and "x29, x30" in i1.op_str:
|
||||
target_off = off
|
||||
break
|
||||
|
||||
if len(hits) != 2:
|
||||
if target_off < 0:
|
||||
self._log(
|
||||
f" [-] execve helper at 0x{func_start:X}: "
|
||||
f"expected 2 early BL+W0-branch sites, found {len(hits)}"
|
||||
f" [-] MOV W0,#1 + epilogue not found in "
|
||||
f"func 0x{func_start:X}"
|
||||
)
|
||||
continue
|
||||
|
||||
self.emit(hits[0], MOV_X0_0, "mov x0,#0 [AMFI execve helper A]")
|
||||
self.emit(hits[1], MOV_X0_0, "mov x0,#0 [AMFI execve helper B]")
|
||||
self.emit(
|
||||
target_off,
|
||||
MOV_W0_0,
|
||||
"mov w0,#0 [AMFI kill return → allow]",
|
||||
)
|
||||
self._log(
|
||||
f" [+] Patched kill return at 0x{target_off:X} "
|
||||
f"(func 0x{func_start:X})"
|
||||
)
|
||||
patched = True
|
||||
break
|
||||
|
||||
if not patched:
|
||||
self._log(" [-] AMFI execve helper patch sites not found")
|
||||
self._log(" [-] AMFI execve kill return not found")
|
||||
return patched
|
||||
|
||||
Reference in New Issue
Block a user