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:
Lakr
2026-03-04 20:43:36 +08:00
parent a3756e29a2
commit 4cfeca3a7e
2 changed files with 100 additions and 35 deletions

View File

@@ -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).

View File

@@ -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