diff --git a/research/kernel_patch_jb/patch_amfi_execve_kill_path.md b/research/kernel_patch_jb/patch_amfi_execve_kill_path.md index bf1ec2c..aecd8a6 100644 --- a/research/kernel_patch_jb/patch_amfi_execve_kill_path.md +++ b/research/kernel_patch_jb/patch_amfi_execve_kill_path.md @@ -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). diff --git a/scripts/patchers/kernel_jb_patch_amfi_execve.py b/scripts/patchers/kernel_jb_patch_amfi_execve.py index 93efb03..a8bc1c6 100644 --- a/scripts/patchers/kernel_jb_patch_amfi_execve.py +++ b/scripts/patchers/kernel_jb_patch_amfi_execve.py @@ -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