cfw: Patch os_lockdown_mode_enabled to not crash on iOS 27b5

iOS 27's os_lockdown_mode_enabled() resolves Lockdown Mode via
sysctlbyname("security.mac.lockdown_mode_state_public", ...) and os_crashes
on a -1 return. The vphone base kernel (cloudOS 26.x) does not implement that
MAC sysctl, so the call returns -1/ENOENT and the first daemon to query
Lockdown Mode after "Continuing system boot" -- launchd (pid 1) -- aborts,
panicking the system (initproc exited, namespace 2 subcode 6).

Add cfw_patch_lockdown_mode.py: NOP the `cmn w0,#1; b.eq <os_crash>` gate so
the pre-zeroed output buffer path is taken (Lockdown Mode = disabled);
behavior-neutral on a kernel that implements the sysctl. Wire it into cfw.py
(patch-lockdown-mode) and the cfw_install.sh 27.* DSC-patch block. Also fixes
the 0_binary_patch_comparison.md LWCR note and adds row 16.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01N2bwKrGJWY7o2ArdxibVPe
This commit is contained in:
zqxwce
2026-08-11 14:50:22 +03:00
committed by zqxwce
co-authored by Claude Opus 4.8
parent ab456ac67e
commit f48fc29a27
4 changed files with 132 additions and 2 deletions
+2 -1
View File
@@ -189,7 +189,8 @@ do NOT execute these).
| 12 | NOP `-[_LSDModifyClient clientIsEntitledForEmbeddedRegistrationOperations]` entitlement gate + per-page re-attest — **iOS 27.0** | DSC `CoreServices` (LaunchServices) | **iOS-27 app-registration fix.** lsd gates `-[_LSDModifyClient performPostInstallationRegistration:operationUUID:reply:]` (and the containerized/rebuild registration paths) behind `clientIsEntitledForEmbeddedRegistrationOperations`, which does `xpc_connection_copy_entitlement_value` on the XPC peer for any of `com.apple.private.coreservices.lsaw` / `com.apple.private.installcoordinationd.daemon` / `com.apple.private.coreservices.can-register-install-results`. A client without one gets `NSOSStatusErrorDomain -54` (permErr, `LSDModifyService.mm:1639`), so `registerApplicationDictionary:` / `registerContainerizedApplicationWithInfoDictionaries:` fail and no app can (re)register — blocking vphoned's installer, TrollStore, and uicache/Sileo alike. The entitlement route is a dead end even for a launchd platform daemon (vphoned) whose validated csblob (`csops CS_OPS_ENTITLEMENTS_BLOB`) contains all three: LS registration is proxied, so the XPC peer lsd inspects is not the registering process. Fix: NOP the final `cbz w0, <not_entitled>` (the conditional branch whose fall-through sets the `mov w<reg>,#1` result) so the method always returns YES. Fully dynamic: method resolved via the DSC's own `.symbols` in-image local-symbol table (ipsw `symaddr -a`/`a2s` time out on this cache), gate located by control-flow shape in Capstone, NOP from Keystone `asm("nop")`, modified 16 KiB page re-attested (`cfw_dsc_codesign.py`; TXM enforces per-page). The resulting CDHash change is accepted by the JB always-true AMFI cdhash-trust patch. Install gate: **`27.*`** (hard-gated in `cfw_install.sh` as of 2026-07-20 — an 18.x/26.x base does not apply it). The patcher additionally self-gates (`patch-lsd-embedded-reg`): no-op on pre-iOS-27 userlands where the method is absent. **Pairs with vphoned's `vp_register_path` containerized-registration fallback** (`registerContainerizedApplicationWithInfoDictionaries:...:registrationError:`, treating a nil `registrationError` as success since it returns NO even when it registers). Also paired with **`/cores/vpregister`** (built + deployed by `cfw_install_jb.sh` / `cfw_install_exp.sh`, invoked by `vphone_jb_setup.sh` at first boot — 27-gated by DEPLOYMENT: `cfw_install_jb.sh`/`cfw_install_exp.sh` copy `/cores/vpregister` only when the mounted rootfs `SystemVersion.plist` is `27.*`, and the setup script's `[ -x /cores/vpregister ]` presence check is the runtime gate. Do NOT gate the invocation on a guest `sw_vers` check — the hybrid guest does not reliably report the 27 userland version at first boot, which silently skipped registration): it registers JB app bundles (Sileo) via the same containerized API, because `uicache -a`'s `registerApplicationDictionary:` is a deprecated no-op on iOS 27 (lsd logs *"you cannot use ... to register applications anymore. These interfaces have been deprecated for years."*). **VALIDATED (2026-07-17, `17,3_27.0_24A5380h` + cloudOS 26.4, JB): -54 gone; Sileo registers (`uicache -l` 0→1) via `vpregister`; vphoned installs+registers a test IPA (`com.vphone.vptest`) to `/var/containers/Bundle/Application/` end-to-end. Clean boot (re-attest correct; no CoreServices page rejection).** See `scripts/patchers/cfw_patch_lsd_embedded_reg.py` and `scripts/vphoned/vpregister.m`. | Y | Y | Y |
| 13 | `mov x0,#1; ret` on `-[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:]`**iOS 27.0** | `diskimagesiod` | **iOS-27 DDI (`/System/Developer`) auto-mount — mount-gate.** After the personalized DDI attaches (kernel side: JB-28 + JB-09), MobileStorageMounter waits on diskimagesiod's `-[DIDiskArb waitForDAMountWithExpectedCount:diskTracker:]` before it does the real (nobrowse) mount at `/System/Developer`. That wait loops until `isMountComplete` (= `callbackReached \|\| (appearedDiskCount>=expectedCount && mountedDiskCount>=mountableDiskCount)`) is YES; on the 26.4-kernel / 27-userland hybrid it never becomes true (not all of the DMG's IOMedia "appear" to diskimagesiod's DiskArbitration session, and diskarbitrationd never auto-mounts the volume), so the wait hangs and pmd3 times out. diskimagesiod itself does NOT mount the DDI (its `-[DIDiskArb mountWithDeviceName:...]` is dead code) — it only gates MobileStorageMounter. Forcing `isMountComplete` → YES lets the wait return so MobileStorageMounter proceeds. IMP resolved via LC_SYMTAB or ObjC metadata (selector → `__objc_selrefs``__TEXT,__objc_methlist` relative method list → IMP); prologue overwritten `mov x0,#1 ; ret` (safe — the method returns to the caller's unsigned LR without pushing a frame). **Gated to 27.\*** in `cfw_install.sh` (same `$IOS_VERSION` as the DSC patches): on a version-matched userland the native wait completes correctly and forcing it early could race the real mount, so it is NOT applied there. Embedded sandbox profile + private DA/apfs entitlements preserved on re-sign (`ldid_sign_ent`). **Validated on `c0ecdb4b` 26.4 + 27.0 userland: `pmd3 mounter auto-mount` → rc=0, DDI at `/System/Developer`, idempotent across fresh boots.** See `scripts/patchers/cfw_patch_diskimagesiod.py`. | Y | Y | Y |
| 14 | Merge backboard/frontboard launch mach-services into `com.apple.security.exception.mach-lookup.global-name` + `ldid_sign_ent` re-sign — **iOS 27.0** | `/Applications/Campo.app/Campo` | **iOS-27 Campo (wallpaper renderer) crash-loop fix — sandbox half.** Campo declares `com.apple.private.sandbox.profile:embedded = temporary-sandbox` + `no-container`, so it runs under the `temporary-sandbox` profile. On the 26.4 vphone600 kernel that builtin profile predates iOS 27 and DENIES the `mach-lookup` of the backboard/frontboard launch services, so `BKSDisplayServicesStart` (looks up `com.apple.backboard.display.services`) and then `+[BKSHIDEventDeliveryManager sharedInstance]` (HID) fail, log *"backboardd isn't running -- or we couldn't talk to it"*, and `brk #0` ~34 ms after launch → continuous crash-loop, no wallpaper (launchd eventually throttles it off). **JB-02d is NOT sufficient here:** it stops the exec-time container-manager-upcall autobox *KILL*, but Campo is still placed in `temporary-sandbox` (its own declared profile — confirmed the JB-02d `b`-flip is present in the running kernel yet Campo still traps), and that profile still denies the lookups. Fix: append the needed services to Campo's OWN `com.apple.security.exception.mach-lookup.global-name` array — the Apple-sanctioned escape hatch, which `temporary-sandbox` honors (Campo already ships ~20 such exceptions; these launch services simply aren't among them because 27's profile allows them directly). Services added (backboard names exact, from `com.apple.backboardd.plist` MachServices): `com.apple.backboard.display.services`, `com.apple.iohideventsystem`, `com.apple.CARenderServer`, `com.apple.backboard.hid.services`, `com.apple.backboard.hid-services.xpc`, `com.apple.backboard.TouchDeliveryPolicyServer`, `com.apple.backboard.system-app-server`, `com.apple.backboard.watchdog`, `com.apple.backboard.oswatchdog`, `com.apple.backboard.altsysapp`, `com.apple.AttentionAwareness`, `PurpleSystemEventPort`, `PurpleWorkspacePort`, `com.apple.frontboard.systemappservices`, `com.apple.frontboard.workspace`, `com.apple.frontboardservices.systemappmanager`, `com.apple.frontboard.watchdog`. Merge is done by the external helper `scripts/patchers/campo_mach_lookup_exceptions.py` (Python `plistlib`, not `plutil` — the entitlement key contains dots that `plutil` keypaths would mis-split; idempotent, preserves the existing array); re-signed with the JB `signcert.p12` via `ldid_sign_ent` (AMFI enforcement is relaxed on the research VM, so the re-signed cdhash loads and the entitlements are honored). Applied at host-mount build time in `cfw_install_jb.sh` / `cfw_install_exp.sh` step **[JB-3b]**, **hard-gated to `27.*`** via the mounted rootfs `SystemVersion.plist` `ProductVersion` (same gate as the vpregister/DSC patches) — skipped entirely on 26.x/18.x, which don't need it and where `Campo.app` also exists. **VALIDATED on-device (2026-07-21, `17,3_27.0_24A5390f` + cloudOS 26.4, JB): Campo launches and stays up (stable pid, no BKSDisplayServicesStart/BKSHIDEventDeliveryManager trap, no crash-loop); first-cut display-only exception advanced the trap from display→HID, confirming the mechanism, then the full service set cleared it.** See `scripts/cfw_install_jb.sh` / `scripts/cfw_install_exp.sh` step JB-3b and the merge helper `scripts/patchers/campo_mach_lookup_exceptions.py`. | - | - | Y |
| 15 | Derive `matched` from `error_code` + drop the `brk #1` in libxpc `_xpc_token_satisfies_lwcr` (`cset w8,ne; eor w8,w0,w8; tbz w8,#0``cset w0,eq; nop; nop`) + per-page re-attest — **iOS 27.0** | DSC `libxpc.dylib` | **iOS-27 daemon crash-loop fix (Lightweight Code Requirement).** iOS 27 lets an XPC server pin a "lightweight code requirement" (LWCR) on its listener — `xpc_connection_set_peer_lightweight_code_requirement`, or the Swift `XPCPeerRequirement.hasEntitlement(_:)` wrapper (→ `xpc_peer_requirement_create_entitlement_exists``_xpc_peer_requirement_create_lwcr_entitlement_requirement``xpc_peer_requirement_create_lwcr`). Creating the requirement runs a self-check, `_xpc_token_satisfies_lwcr`, which calls an internal matcher returning a `matched` bool (w0) plus a `match_result.error_code` (`AICMR_MATCH == 0`), then hard-asserts they agree (`matched == (error_code == 0)`) via `_os_crash_msg``brk #1`. On stock iOS the two always agree; under our JB code-signing environment the matcher's query writes `error_code = MATCH(0)` yet returns a failure status, so the matcher yields the forbidden `(matched=0, error_code=0)` pair and libxpc aborts. Because EVERY daemon that pins an entitlement peer-requirement at startup hits it, `intelligencetasksd` / `searchpartyd` / `transparencyd` / `bluetoothd` (and others) crash-loop continuously from boot (launchd re-spawn + ReportCrash churn). Fix: recompute `matched` from `error_code` and make the abort unreachable — `cset w8,ne; eor w8,w0,w8; tbz w8,#0,<abort>``cset w0,eq; nop; nop`; the function now returns `(error_code == 0)`, which reproduces stock's verdict when the two agree and resolves the contradiction toward "satisfied" when error_code says MATCH (real allow/deny for genuinely (un)satisfied peers is unchanged, since those set `error_code` != 0). Fully dynamic: `_xpc_token_satisfies_lwcr` resolved via the DSC's own `.symbols` in-image local-symbol table, the check located by control-flow shape in Capstone (`cset wC,ne; eor wE,w0,wC; tbz wE,#0`), replacements from Keystone, modified 16 KiB page re-attested (`cfw_dsc_codesign.py`; TXM enforces per-page; CDHash change accepted by the JB always-true AMFI cdhash-trust patch). Install gate: **`27.*`** (in `cfw_install.sh`, same block as maxSlide/lsd). Patcher additionally self-gates (`patch-xpc-lwcr`): no-op on pre-iOS-27 userlands where the symbol is absent. **VALIDATED on-device (2026-07-22, `17,3_27.0_24A5390f` + cloudOS 26.4, JB, host-mount deploy): patched bytes live (`e0179f1a 1f2003d5 1f2003d5`); the four LWCR crash-loopers disappear from the crash census after boot; launchd itself (which links libxpc) boots clean past first unlock — patch does not brick boot. NOTE: does NOT stop the resprings — those are a separate `FileProviderResolver` ResolverService memory-balloon → jetsam → backboardd kill (see investigation notes).** See `scripts/patchers/cfw_patch_xpc_lwcr.py`. | Y | Y | Y |
| 15 | Derive `matched` from `error_code` + drop the `brk #1` in libxpc `_xpc_token_satisfies_lwcr` (`cset w8,ne; eor w8,w0,w8; tbz w8,#0``cset w0,eq; nop; nop`) + per-page re-attest — **iOS 27.0** | DSC `libxpc.dylib` | **iOS-27 daemon crash-loop fix (Lightweight Code Requirement).** iOS 27 lets an XPC server pin a "lightweight code requirement" (LWCR) on its listener — `xpc_connection_set_peer_lightweight_code_requirement`, or the Swift `XPCPeerRequirement.hasEntitlement(_:)` wrapper (→ `xpc_peer_requirement_create_entitlement_exists``_xpc_peer_requirement_create_lwcr_entitlement_requirement``xpc_peer_requirement_create_lwcr`). Creating the requirement runs a self-check, `_xpc_token_satisfies_lwcr`, which calls an internal matcher returning a `matched` bool (w0) plus a `match_result.error_code` (`AICMR_MATCH == 0`), then hard-asserts they agree (`matched == (error_code == 0)`) via `_os_crash_msg``brk #1`. On stock iOS the two always agree; under our JB code-signing environment the matcher's query writes `error_code = MATCH(0)` yet returns a failure status, so the matcher yields the forbidden `(matched=0, error_code=0)` pair and libxpc aborts. Because EVERY daemon that pins an entitlement peer-requirement at startup hits it, `intelligencetasksd` / `searchpartyd` / `transparencyd` / `bluetoothd` (and others) crash-loop continuously from boot (launchd re-spawn + ReportCrash churn). Fix: recompute `matched` from `error_code` and make the abort unreachable — `cset w8,ne; eor w8,w0,w8; tbz w8,#0,<abort>``cset w0,eq; nop; nop`; the function now returns `(error_code == 0)`, which reproduces stock's verdict when the two agree and resolves the contradiction toward "satisfied" when error_code says MATCH (real allow/deny for genuinely (un)satisfied peers is unchanged, since those set `error_code` != 0). Fully dynamic: resolved via the DSC's own `.symbols` in-image local-symbol table as `__xpc_token_satisfies_lwcr` (the double-underscore mangled name; a single-underscore lookup silently skipped every iOS-27 build until fixed 2026-08-11), the check located by control-flow shape in Capstone (`cset wC,ne; eor wE,w0,wC; tbz wE,#0`), replacements from Keystone, modified 16 KiB page re-attested (`cfw_dsc_codesign.py`; TXM enforces per-page; CDHash change accepted by the JB always-true AMFI cdhash-trust patch). Install gate: **`27.*`** (in `cfw_install.sh`, same block as maxSlide/lsd). Patcher additionally self-gates (`patch-xpc-lwcr`): no-op on pre-iOS-27 userlands where the symbol is absent. **VALIDATED on-device (2026-07-22, `17,3_27.0_24A5390f` + cloudOS 26.4, JB, host-mount deploy): patched bytes live (`e0179f1a 1f2003d5 1f2003d5`); the four LWCR crash-loopers disappear from the crash census after boot; launchd itself (which links libxpc) boots clean past first unlock — patch does not brick boot. NOTE: does NOT stop the resprings — those are a separate `FileProviderResolver` ResolverService memory-balloon → jetsam → backboardd kill (see investigation notes).** See `scripts/patchers/cfw_patch_xpc_lwcr.py`. | Y | Y | Y |
| 16 | NOP the sysctl-error `b.eq <os_crash>` in libSystem `___os_lockdown_mode_enabled_block_invoke` (`cmn w0,#1; b.eq <crash>``nop`) + per-page re-attest — **iOS 27.0** | DSC `libSystem` (`lockdown_mode.c`) | **iOS-27 launchd (pid 1) boot-panic fix.** iOS 27's `os_lockdown_mode_enabled()` resolves Lockdown Mode once via `sysctlbyname("security.mac.lockdown_mode_state_public", &out, &len, 0, 0)`; on a -1 return it `os_crash`es (`lockdown_mode.c:os_lockdown_mode_enabled_block_invoke:47`). The vphone base kernel (cloudOS 26.x) does not implement that MAC sysctl, so the call returns -1/ENOENT and the first process to query Lockdown Mode after "Continuing system boot" aborts — that process is launchd (pid 1), so the kernel panics `initproc exited -- exit reason namespace 2 subcode 0x6 description: none`. b4 (24A5390f) boots on the same kernel; the sysctl query is new in b5 (24A5408d). The block pre-zeroes its output buffer (`stp x8, xzr, [sp]`), so NOPping the error branch falls through to the normal path, reads 0, records "Lockdown Mode disabled", and returns cleanly; on a kernel that implements the sysctl the branch is never taken (w0==0), so the patch is behavior-neutral. Dynamic: `___os_lockdown_mode_enabled_block_invoke` resolved via the DSC's own `.symbols` local-symbol table; the `cmn wR,#1; b.eq` sysctl-error idiom located by control-flow shape in Capstone; NOP from Keystone; modified 16 KiB page re-attested (`cfw_dsc_codesign.py`). Install gate: **`27.*`** (same block as maxSlide/lsd/lwcr). Self-gates: no-op where the symbol is absent (pre-iOS-27 userlands). **Root-caused + verified on-device 2026-08-11** (`17,3_27.0_24A5408d` + cloudOS 26.4, JB): abort message read live via the kernel GDB stub (patched `_abort``b .` to freeze launchd's spinning vCPU, then read its registers + the libSystem crash-info global) = `lockdown_mode.c:os_lockdown_mode_enabled_block_invoke:47: No such file or directory`; with the NOP applied the panic is gone and boot continues past "Got first unlock" into normal daemon startup. See `scripts/patchers/cfw_patch_lockdown_mode.py`. | Y | Y | Y |
### Installed Components
+2
View File
@@ -371,6 +371,8 @@ case "$IOS_VERSION" in
"$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-lsd-embedded-reg "$DSC_DIR"
echo " [*] Patching libxpc LWCR self-check (iOS 27 daemon crash-loop)..."
"$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-xpc-lwcr "$DSC_DIR"
echo " [*] Patching os_lockdown_mode_enabled (missing MAC sysctl -> launchd abort)..."
"$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-lockdown-mode "$DSC_DIR"
fi
;;
*)
+11 -1
View File
@@ -59,6 +59,7 @@ Commands:
installs. Self-gating (no-op on pre-iOS-27 userlands where the method is absent).
patch-xpc-lwcr <chunks_dir> [--dry-run]
patch-lockdown-mode <chunks_dir> [--dry-run]
Stop libxpc's Lightweight Code Requirement self-check (_xpc_token_satisfies_lwcr)
from brk-aborting on our JB. iOS 27's LWCR matcher returns the contradictory
(matched=0, error_code=MATCH) pair under our code-signing environment; the
@@ -127,6 +128,7 @@ if __name__ == "__main__":
from patchers.cfw_patch_dsc_maxslide import patch_dsc_maxslide
from patchers.cfw_patch_lsd_embedded_reg import patch_lsd_embedded_reg
from patchers.cfw_patch_xpc_lwcr import patch_xpc_lwcr
from patchers.cfw_patch_lockdown_mode import patch_lockdown_mode
from patchers.cfw_patch_camera_dsc import apply_all_camera_patches
from patchers.cfw_patch_watchdogd import patch_watchdogd
from patchers.cfw_patch_diskimagesiod import patch_diskimagesiod
@@ -142,6 +144,7 @@ else:
from .cfw_patch_dsc_maxslide import patch_dsc_maxslide
from .cfw_patch_lsd_embedded_reg import patch_lsd_embedded_reg
from .cfw_patch_xpc_lwcr import patch_xpc_lwcr
from .cfw_patch_lockdown_mode import patch_lockdown_mode
from .cfw_patch_camera_dsc import apply_all_camera_patches
from .cfw_patch_watchdogd import patch_watchdogd
from .cfw_patch_diskimagesiod import patch_diskimagesiod
@@ -251,6 +254,13 @@ def main():
sys.exit(1)
dry_run = "--dry-run" in sys.argv[3:]
patch_xpc_lwcr(sys.argv[2], dry_run=dry_run)
elif cmd == "patch-lockdown-mode":
if len(sys.argv) < 3:
print("Usage: patch_cfw.py patch-lockdown-mode <chunks_dir> [--dry-run]")
sys.exit(1)
dry_run = "--dry-run" in sys.argv[3:]
patch_lockdown_mode(sys.argv[2], dry_run=dry_run)
sys.exit(0)
elif cmd == "patch-camera-dsc":
@@ -321,7 +331,7 @@ def main():
print(f"Unknown command: {cmd}")
print("Commands: cryptex-paths, patch-seputil, patch-launchd-cache-loader, patch-camera-dsc,")
print(" patch-mobileactivationd, patch-launchd-jetsam,")
print(" patch-hv-vmm-dsc, patch-iomfb-swapend, patch-iomfb-force-kern, patch-dsc-maxslide, patch-lsd-embedded-reg, patch-xpc-lwcr, patch-watchdogd,")
print(" patch-hv-vmm-dsc, patch-iomfb-swapend, patch-iomfb-force-kern, patch-dsc-maxslide, patch-lsd-embedded-reg, patch-xpc-lwcr, patch-lockdown-mode, patch-watchdogd,")
print(" patch-diskimagesiod, inject-daemons, patch-dropbear-plist, inject-dylib")
sys.exit(1)
+117
View File
@@ -0,0 +1,117 @@
"""Stop libSystem's `os_lockdown_mode_enabled` from crashing on the vphone kernel.
iOS 27's `os_lockdown_mode_enabled()` resolves Lockdown Mode once via
`sysctlbyname("security.mac.lockdown_mode_state_public", ...)` and, if the
sysctl call returns -1, calls `os_crash` (lockdown_mode.c). The vphone base
kernel (cloudOS 26.x) does not implement that MAC sysctl, so the call fails
with ENOENT and every process that queries Lockdown Mode aborts including
launchd (pid 1), which panics the system right after "Continuing system boot".
The block pre-zeroes its output buffer (`stp x8, xzr, [sp]`), so dropping the
error branch makes the failure path fall through to the normal path, read 0,
and record "Lockdown Mode disabled". On a kernel that does implement the sysctl
the branch is never taken, so the patch is behavior-neutral there.
Shape (in `___os_lockdown_mode_enabled_block_invoke`):
bl <sysctlbyname>
cmn w0, #1 ; w0 == -1 ?
b.eq <os_crash> ; -> NOP
Anchored on the in-image local symbol; the sysctl-error idiom is located by
control-flow shape via Capstone; the NOP comes from Keystone; the modified page
is re-attested (`cfw_dsc_codesign.py`).
"""
from capstone.arm64_const import ARM64_OP_IMM
try:
from .cfw_asm import asm, _cs
from .cfw_dsc_chunks import DSCChunks
from .cfw_dsc_codesign import reattest_modified_pages
from .cfw_patch_xpc_lwcr import _resolve_local_symbol
except ImportError:
from cfw_asm import asm, _cs
from cfw_dsc_chunks import DSCChunks
from cfw_dsc_codesign import reattest_modified_pages
from cfw_patch_xpc_lwcr import _resolve_local_symbol
SYMBOL_CANDIDATES = (
"___os_lockdown_mode_enabled_block_invoke",
"__os_lockdown_mode_enabled_block_invoke",
)
def _imm(insn, idx):
ops = insn.operands
return ops[idx].imm if idx < len(ops) and ops[idx].type == ARM64_OP_IMM else None
def _disasm(chunks, vma, n=60):
buf = chunks.bytes_at_vma(vma, n * 4)
out = []
for insn in _cs.disasm(buf, vma):
out.append(insn)
if insn.mnemonic in ("ret", "retab"):
break
return out
def _find_error_gate(insns):
"""The `cmn wR, #1; b.eq` sysctl-error idiom, preceded by a bl."""
saw_bl = False
for i in range(len(insns) - 1):
if insns[i].mnemonic == "bl":
saw_bl = True
if not saw_bl:
continue
if insns[i].mnemonic == "cmn" and _imm(insns[i], 1) == 1:
beq = insns[i + 1]
if beq.mnemonic == "b.eq":
return beq
return None
def patch_lockdown_mode(chunks_dir, *, dry_run=False):
chunks = DSCChunks(chunks_dir)
print(f" [.] {chunks!r}")
fn_vma = None
for name in SYMBOL_CANDIDATES:
try:
fn_vma = _resolve_local_symbol(chunks_dir, name)
break
except RuntimeError:
continue
if fn_vma is None:
print(" [=] os_lockdown_mode_enabled not present (pre-iOS-27 userland); nothing to patch")
return 0
print(f" [.] {name} @ 0x{fn_vma:X}")
gate = _find_error_gate(_disasm(chunks, fn_vma))
if gate is None:
raise ValueError("lockdown_mode: `cmn wR,#1; b.eq <crash>` sysctl-error gate not found")
print(f" [.] gate @ 0x{gate.address:X}: {gate.mnemonic} {gate.op_str}")
nop = asm("nop")
cur = chunks.bytes_at_vma(gate.address, 4)
if cur == nop:
print(" [=] already patched")
return 1
action = "would write" if dry_run else "wrote"
print(f" [+] {action} nop at 0x{gate.address:X} ({cur.hex()} -> {nop.hex()})")
if not dry_run:
chunks.write_at_vma(gate.address, nop)
reattest_modified_pages(chunks, [gate.address], dry_run=False)
if chunks.bytes_at_vma(gate.address, 4) != nop:
raise RuntimeError(f"post-write verify failed at 0x{gate.address:X}")
print(" [+] lockdown-mode crash patch complete")
return 1
if __name__ == "__main__":
import sys
dry = "--apply" not in sys.argv
args = [a for a in sys.argv[1:] if not a.startswith("--")]
d = args[0] if args else "/private/tmp/cryptex27/System/Library/Caches/com.apple.dyld"
patch_lockdown_mode(d, dry_run=dry)