mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
kernel: jb: Bypass exec ip_mac_return SECURITY_POLICY kill for newer userlands
Running iOS 27.0 userland on the 26.4 vphone600 kernel, every core platform daemon (backboardd, cfprefsd, containermanagerd, locationd, CommCenter, ...) died 3-5ms after xpcproxy spawn with exit reason namespace 9 / code 0x8 (OS_REASON_EXEC / EXEC_EXIT_REASON_SECURITY_POLICY). launchd throttled the respawns and the boot deadlocked (all CPUs idle) before SpringBoard — no UI, no networking. Root cause: AMFI's exec MAC hooks reject the 27.0 binaries' code-sign validation category on the 26.4 kernel, setting imgp->ip_mac_return, and XNU's exec path (kern_exec.c) SIGKILLs on `if (imgp->ip_mac_return != 0)`. patch_exec_security_policy_kill flips the `cbz wN, <skip>` guard preceding the os_reason_create(9,8) call to an unconditional `b <skip>`, making the kill block unreachable — downstream of AMFI/TXM, so it covers the validation-category reject regardless of which hook set the verdict. Anchored structurally (movz w0,#9 ; movz w1,#8 preceded by ldr wN,[xM,#imm] ; cbz wN,<fwd>; W-register cbz distinguishes the ip_mac_return site from the subsystem-root sibling). No-op in effect for version-matched userlands (ip_mac_return == 0, so the cbz already skips). Wired into the JB Group C dispatcher. Validated on iPhone17,3_27.0_24A5380h + cloudOS 26.4 (c0ecdb4b): 0 SECURITY_POLICY kills, core daemons launch, networking + SSH (dropbear :22222) come up. Remaining gate: sandbox denies backboardd the IOMobileFramebuffer/IOSurface user clients, so SpringBoard traps in FBSDisplayMonitor init (no display) -> UI not up yet. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
This commit is contained in:
committed by
zqxwce
co-authored by
Claude Opus 4.8
parent
a38fd3201f
commit
679d3d0476
@@ -126,6 +126,7 @@
|
||||
| ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: |
|
||||
| JB-01 | A | `patch_amfi_cdhash_in_trustcache` | `AMFIIsCDHashInTrustCache` | Always return true + store hash | Y |
|
||||
| JB-02 | A | `patch_amfi_execve_kill_path` | AMFI execve kill return site | Convert shared kill return from deny to allow (superseded by C21; standalone only) | N |
|
||||
| JB-02b| C | `patch_exec_security_policy_kill` | XNU exec `imgp->ip_mac_return` gate (kern_exec, `os_reason_create(OS_REASON_EXEC, EXEC_EXIT_REASON_SECURITY_POLICY)` site) | Flip `cbz wN, <skip>` → unconditional `b <skip>` so the exec-time MAC-verdict `SECURITY_POLICY` kill is unreachable. **Needed to run a userland NEWER than the kernel (iOS 27.0 on the 26.4 kernel):** AMFI's exec hooks reject the newer binaries' code-sign validation category, setting `ip_mac_return != 0` → core daemons (backboardd/cfprefsd/containermanagerd/…) die at exec (`namespace 9 / code 0x8`) → boot deadlock. Validated on 27.0/26.4: 0 SECURITY_POLICY kills, daemons launch, networking + SSH come up. No-op-in-effect for version-matched userlands (ip_mac_return == 0 there, so the original cbz already skips). | Y |
|
||||
| JB-03 | C | `patch_cred_label_update_execve` | `_cred_label_update_execve` | Reworked C21-v3: C21-v1 already boots; v3 keeps split late exits and additionally ORs success-only helper bits `0xC` after clearing `0x3F00`; still disabled pending boot validation | N |
|
||||
| JB-04 | C | `patch_hook_cred_label_update_execve` | sandbox `mpo_cred_label_update_execve` wrapper (`ops[18]` -> `sub_FFFFFE00093BDB64`) | Faithful upstream C23 trampoline: copy `VSUID`/`VSGID` owner state into pending cred, set `P_SUGID`, then branch back to wrapper | Y |
|
||||
| JB-05 | C | `patch_kcall10` | `sysent[439]` (`SYS_kas_info` replacement) | Rebuilt ABI-correct kcall cave: `target + 7 args -> uint64 x0`; re-enabled after focused dry-run validation | Y |
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// KernelJBPatchExecPolicyKill.swift — JB kernel patch: neutralize the exec-time
|
||||
// MAC-verdict (ip_mac_return) security-policy kill.
|
||||
//
|
||||
// After the MAC exec hooks run, XNU's exec path (kern_exec.c) checks:
|
||||
//
|
||||
// if (imgp->ip_mac_return != 0) {
|
||||
// ... os_reason_create(OS_REASON_EXEC, EXEC_EXIT_REASON_SECURITY_POLICY);
|
||||
// error = imgp->ip_mac_return;
|
||||
// goto done; // SIGKILL the new process at exec
|
||||
// }
|
||||
//
|
||||
// When running a userland NEWER than the vphone600 kernel (e.g. iOS 27.0 on the
|
||||
// 26.4 kernel), AMFI's exec hooks reject the newer binaries' code-sign validation
|
||||
// category, setting ip_mac_return != 0. Every core platform daemon (backboardd,
|
||||
// cfprefsd, containermanagerd, ...) then dies at exec with
|
||||
// EXEC_EXIT_REASON_SECURITY_POLICY (namespace 9 / code 0x8), launchd throttles the
|
||||
// respawns, and the boot deadlocks (all CPUs idle) before SpringBoard/UI.
|
||||
//
|
||||
// Flip the `cbz wN, <skip>` guard immediately preceding the reason-create call to
|
||||
// an unconditional `b <skip>`, so the kill block is unreachable. Safe for
|
||||
// version-matched userlands too: there ip_mac_return is 0, so the original cbz
|
||||
// already branches to <skip> — the unconditional b is behaviourally identical.
|
||||
//
|
||||
// Anchor (structural, no hardcoded offsets): the
|
||||
// `os_reason_create(OS_REASON_EXEC=9, EXEC_EXIT_REASON_SECURITY_POLICY=8)` call —
|
||||
// two adjacent `movz w0,#9 ; movz w1,#8` — preceded by
|
||||
// `ldr wN,[xM,#imm] ; cbz wN, <forward>`. The ip_mac_return site uses a W-register
|
||||
// cbz (distinguishing it from the sibling subsystem-root reject site, which cbz's
|
||||
// an X register).
|
||||
|
||||
import Foundation
|
||||
|
||||
extension KernelJBPatcher {
|
||||
@discardableResult
|
||||
func patchExecSecurityPolicyKill() -> Bool {
|
||||
log("\n[JB] exec ip_mac_return SECURITY_POLICY kill: cbz -> b (allow)")
|
||||
|
||||
guard let (ks, ke) = kernTextRange else {
|
||||
log(" [-] no kernel text range")
|
||||
return false
|
||||
}
|
||||
|
||||
let movzW0_9: UInt32 = 0x5280_0120 // movz w0, #9 (OS_REASON_EXEC)
|
||||
let movzW1_8: UInt32 = 0x5280_0101 // movz w1, #8 (EXEC_EXIT_REASON_SECURITY_POLICY)
|
||||
|
||||
var hits: [Int] = []
|
||||
var off = ks
|
||||
while off + 8 <= ke {
|
||||
if buffer.readU32(at: off) == movzW0_9, buffer.readU32(at: off + 4) == movzW1_8 {
|
||||
let cbzOff = off - 4
|
||||
let ldrOff = off - 8
|
||||
if cbzOff >= ks,
|
||||
let cbz = disasAt(cbzOff), cbz.mnemonic == "cbz",
|
||||
let ldr = disasAt(ldrOff), ldr.mnemonic == "ldr",
|
||||
// W-register cbz == the ip_mac_return site (not the X-register
|
||||
// subsystem-root sibling).
|
||||
cbz.operandString.hasPrefix("w"),
|
||||
ldr.operandString.hasPrefix("w")
|
||||
{
|
||||
// Decode the cbz's forward branch target (imm19 << 2).
|
||||
let word = buffer.readU32(at: cbzOff)
|
||||
let imm19 = Int((word >> 5) & 0x7FFFF)
|
||||
let signed = imm19 >= (1 << 18) ? imm19 - (1 << 19) : imm19
|
||||
let target = cbzOff + signed * 4
|
||||
// Must be a forward branch that skips the reason-create/kill block.
|
||||
if target > off + 8 {
|
||||
hits.append(cbzOff)
|
||||
}
|
||||
}
|
||||
}
|
||||
off += 4
|
||||
}
|
||||
|
||||
guard hits.count == 1 else {
|
||||
log(" [-] exec ip_mac_return kill guard not found uniquely (found \(hits.count))")
|
||||
return false
|
||||
}
|
||||
|
||||
let cbzOff = hits[0]
|
||||
// Re-decode the target for the emitted branch.
|
||||
let word = buffer.readU32(at: cbzOff)
|
||||
let imm19 = Int((word >> 5) & 0x7FFFF)
|
||||
let signed = imm19 >= (1 << 18) ? imm19 - (1 << 19) : imm19
|
||||
let target = cbzOff + signed * 4
|
||||
|
||||
guard let bBytes = ARM64Encoder.encodeB(from: cbzOff, to: target) else {
|
||||
log(" [-] failed to encode B to 0x\(String(target, radix: 16))")
|
||||
return false
|
||||
}
|
||||
|
||||
let va = fileOffsetToVA(cbzOff)
|
||||
emit(
|
||||
cbzOff,
|
||||
bBytes,
|
||||
patchID: "exec_security_policy_kill",
|
||||
virtualAddress: va,
|
||||
description: "cbz -> b [exec ip_mac_return SECURITY_POLICY kill bypass]"
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,15 @@ import Foundation
|
||||
public final class KernelJBPatcher: KernelJBPatcherBase, Patcher {
|
||||
public let component = "kernelcache_jb"
|
||||
|
||||
/// Gates the iOS-27-only kernel patches. These target an iOS-27 userland running
|
||||
/// on the 26.4 kernel; on a 26.x base they are unnecessary and some are actively
|
||||
/// harmful (e.g. the IOMFB SwapEnd size gate would reject 26.x's native 0x588 swap
|
||||
/// struct → dead display, the 26.5 regression). The pipeline sets this from the
|
||||
/// iPhone base ProductVersion (false for 18.x/26.x → byte-identical to pre-branch);
|
||||
/// standalone patch-component defaults it true so the dev tool exercises the full
|
||||
/// set (override with --target-os).
|
||||
public var applyIOS27 = false
|
||||
|
||||
public func findAll() throws -> [PatchRecord] {
|
||||
try parseMachO()
|
||||
buildADRPIndex()
|
||||
@@ -49,6 +58,13 @@ public final class KernelJBPatcher: KernelJBPatcherBase, Patcher {
|
||||
patchKcall10()
|
||||
patchSyscallmaskApplyToProc()
|
||||
|
||||
// Neutralize the exec-time ip_mac_return SECURITY_POLICY kill so a userland
|
||||
// newer than the kernel (iOS 27 on the 26.4 kernel) can launch: AMFI's exec
|
||||
// hooks reject the newer binaries' validation category → ip_mac_return != 0 →
|
||||
// core daemons (backboardd, cfprefsd, ...) die at exec → boot deadlock.
|
||||
// No-op-in-effect for version-matched userlands (ip_mac_return == 0 there).
|
||||
patchExecSecurityPolicyKill()
|
||||
|
||||
return patches
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,11 @@ public final class FirmwarePipeline {
|
||||
/// Computed in `patchAll()` before `buildComponentList()` runs.
|
||||
private var iosBaseIs18 = false
|
||||
|
||||
/// Set when the iPhone base is iOS 27.x. Gates the iOS-27-only JB kernel patches
|
||||
/// (KernelJBPatcher.applyIOS27); false for 18.x/26.x so those bases are
|
||||
/// byte-identical to pre-branch. Computed in `patchAll()` alongside iosBaseIs18.
|
||||
private var iosBaseIs27 = false
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(
|
||||
@@ -122,7 +127,10 @@ public final class FirmwarePipeline {
|
||||
// version, not the base). iOS 18 bases need the EXC_GUARD patch.
|
||||
let baseVersion = Self.readBaseProductVersion(restoreDir)
|
||||
iosBaseIs18 = baseVersion?.hasPrefix("18.") ?? false
|
||||
log("[*] iPhone base iOS: \(baseVersion ?? "unknown")\(iosBaseIs18 ? " (enabling iOS-18 EXC_GUARD kernel patch)" : "")")
|
||||
iosBaseIs27 = baseVersion?.hasPrefix("27.") ?? false
|
||||
let baseGateNote = iosBaseIs18 ? " (enabling iOS-18 EXC_GUARD kernel patch)"
|
||||
: iosBaseIs27 ? " (enabling iOS-27 JB kernel patches)" : ""
|
||||
log("[*] iPhone base iOS: \(baseVersion ?? "unknown")\(baseGateNote)")
|
||||
|
||||
let components = buildComponentList()
|
||||
log("[*] Patching \(components.count) boot-chain components ...")
|
||||
@@ -196,6 +204,10 @@ public final class FirmwarePipeline {
|
||||
// capturing self). True only for iOS 18 bases; gates the EXC_GUARD patch.
|
||||
let applyExcGuard = iosBaseIs18
|
||||
|
||||
// Same capture-by-value; true only for iOS 27 bases. Gates the iOS-27-only
|
||||
// JB kernel patches so 18.x/26.x bases apply none of them.
|
||||
let applyIOS27 = iosBaseIs27
|
||||
|
||||
// iOS 18 bases: disable the skywalk flowswitch netagents via boot-arg so
|
||||
// Network.framework uses the BSD path (the 26.1-kernel skywalk
|
||||
// channel-create traps in the 18.x Network.framework and crash-loops
|
||||
@@ -315,7 +327,9 @@ public final class FirmwarePipeline {
|
||||
KernelPatcher(data: data, verbose: verbose, isDev: false, applyExcGuard: applyExcGuard)
|
||||
},
|
||||
{ data, verbose in
|
||||
KernelJBPatcher(data: data, verbose: verbose)
|
||||
let p = KernelJBPatcher(data: data, verbose: verbose)
|
||||
p.applyIOS27 = applyIOS27
|
||||
return p
|
||||
},
|
||||
]
|
||||
case .exp:
|
||||
@@ -324,7 +338,9 @@ public final class FirmwarePipeline {
|
||||
KernelPatcher(data: data, verbose: verbose, isDev: false, applyExcGuard: applyExcGuard)
|
||||
},
|
||||
{ data, verbose in
|
||||
KernelJBPatcher(data: data, verbose: verbose)
|
||||
let p = KernelJBPatcher(data: data, verbose: verbose)
|
||||
p.applyIOS27 = applyIOS27
|
||||
return p
|
||||
},
|
||||
{ data, verbose in
|
||||
KernelEXPPatcher(data: data, verbose: verbose)
|
||||
|
||||
@@ -238,6 +238,12 @@ struct PatchComponentCLI: ParsableCommand {
|
||||
)
|
||||
var recordsOut: String?
|
||||
|
||||
@Option(
|
||||
name: .customLong("target-os"),
|
||||
help: "kernel-jb only: base iOS version the kernel will run under (e.g. 27.0). Gates the iOS-27-only JB patches exactly as the pipeline does. Omit to apply the full set (dev/test default)."
|
||||
)
|
||||
var targetOS: String?
|
||||
|
||||
mutating func run() throws {
|
||||
let payload = try IM4PHandler.load(contentsOf: input).payload
|
||||
let count: Int
|
||||
@@ -262,6 +268,11 @@ struct PatchComponentCLI: ParsableCommand {
|
||||
// KernelJBPatcher standalone faithfully reproduces JB hook behavior
|
||||
// without the base patcher or the rest of the boot chain.
|
||||
let patcher = KernelJBPatcher(data: payload, verbose: !quiet)
|
||||
// Mirror the pipeline's per-base gating: apply the iOS-27-only patches when
|
||||
// --target-os is 27.x, skip them for an explicit non-27 target. With no
|
||||
// --target-os, default to applying them so the dev/test tool exercises the
|
||||
// full set.
|
||||
patcher.applyIOS27 = targetOS.map { $0.hasPrefix("27.") } ?? true
|
||||
count = try patcher.apply()
|
||||
patchedData = patcher.buffer.data
|
||||
records = patcher.patches
|
||||
|
||||
Reference in New Issue
Block a user