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-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-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-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-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 |
|
| 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 final class KernelJBPatcher: KernelJBPatcherBase, Patcher {
|
||||||
public let component = "kernelcache_jb"
|
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] {
|
public func findAll() throws -> [PatchRecord] {
|
||||||
try parseMachO()
|
try parseMachO()
|
||||||
buildADRPIndex()
|
buildADRPIndex()
|
||||||
@@ -49,6 +58,13 @@ public final class KernelJBPatcher: KernelJBPatcherBase, Patcher {
|
|||||||
patchKcall10()
|
patchKcall10()
|
||||||
patchSyscallmaskApplyToProc()
|
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
|
return patches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ public final class FirmwarePipeline {
|
|||||||
/// Computed in `patchAll()` before `buildComponentList()` runs.
|
/// Computed in `patchAll()` before `buildComponentList()` runs.
|
||||||
private var iosBaseIs18 = false
|
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
|
// MARK: - Init
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -122,7 +127,10 @@ public final class FirmwarePipeline {
|
|||||||
// version, not the base). iOS 18 bases need the EXC_GUARD patch.
|
// version, not the base). iOS 18 bases need the EXC_GUARD patch.
|
||||||
let baseVersion = Self.readBaseProductVersion(restoreDir)
|
let baseVersion = Self.readBaseProductVersion(restoreDir)
|
||||||
iosBaseIs18 = baseVersion?.hasPrefix("18.") ?? false
|
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()
|
let components = buildComponentList()
|
||||||
log("[*] Patching \(components.count) boot-chain components ...")
|
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.
|
// capturing self). True only for iOS 18 bases; gates the EXC_GUARD patch.
|
||||||
let applyExcGuard = iosBaseIs18
|
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
|
// iOS 18 bases: disable the skywalk flowswitch netagents via boot-arg so
|
||||||
// Network.framework uses the BSD path (the 26.1-kernel skywalk
|
// Network.framework uses the BSD path (the 26.1-kernel skywalk
|
||||||
// channel-create traps in the 18.x Network.framework and crash-loops
|
// 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)
|
KernelPatcher(data: data, verbose: verbose, isDev: false, applyExcGuard: applyExcGuard)
|
||||||
},
|
},
|
||||||
{ data, verbose in
|
{ data, verbose in
|
||||||
KernelJBPatcher(data: data, verbose: verbose)
|
let p = KernelJBPatcher(data: data, verbose: verbose)
|
||||||
|
p.applyIOS27 = applyIOS27
|
||||||
|
return p
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
case .exp:
|
case .exp:
|
||||||
@@ -324,7 +338,9 @@ public final class FirmwarePipeline {
|
|||||||
KernelPatcher(data: data, verbose: verbose, isDev: false, applyExcGuard: applyExcGuard)
|
KernelPatcher(data: data, verbose: verbose, isDev: false, applyExcGuard: applyExcGuard)
|
||||||
},
|
},
|
||||||
{ data, verbose in
|
{ data, verbose in
|
||||||
KernelJBPatcher(data: data, verbose: verbose)
|
let p = KernelJBPatcher(data: data, verbose: verbose)
|
||||||
|
p.applyIOS27 = applyIOS27
|
||||||
|
return p
|
||||||
},
|
},
|
||||||
{ data, verbose in
|
{ data, verbose in
|
||||||
KernelEXPPatcher(data: data, verbose: verbose)
|
KernelEXPPatcher(data: data, verbose: verbose)
|
||||||
|
|||||||
@@ -238,6 +238,12 @@ struct PatchComponentCLI: ParsableCommand {
|
|||||||
)
|
)
|
||||||
var recordsOut: String?
|
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 {
|
mutating func run() throws {
|
||||||
let payload = try IM4PHandler.load(contentsOf: input).payload
|
let payload = try IM4PHandler.load(contentsOf: input).payload
|
||||||
let count: Int
|
let count: Int
|
||||||
@@ -262,6 +268,11 @@ struct PatchComponentCLI: ParsableCommand {
|
|||||||
// KernelJBPatcher standalone faithfully reproduces JB hook behavior
|
// KernelJBPatcher standalone faithfully reproduces JB hook behavior
|
||||||
// without the base patcher or the rest of the boot chain.
|
// without the base patcher or the rest of the boot chain.
|
||||||
let patcher = KernelJBPatcher(data: payload, verbose: !quiet)
|
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()
|
count = try patcher.apply()
|
||||||
patchedData = patcher.buffer.data
|
patchedData = patcher.buffer.data
|
||||||
records = patcher.patches
|
records = patcher.patches
|
||||||
|
|||||||
Reference in New Issue
Block a user