diff --git a/research/0_binary_patch_comparison.md b/research/0_binary_patch_comparison.md index 019a547..3b20bb7 100644 --- a/research/0_binary_patch_comparison.md +++ b/research/0_binary_patch_comparison.md @@ -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, ` (the conditional branch whose fall-through sets the `mov w,#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,` → `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,` → `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 ` in libSystem `___os_lockdown_mode_enabled_block_invoke` (`cmn w0,#1; b.eq ` → `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 diff --git a/scripts/cfw_install.sh b/scripts/cfw_install.sh index 582fdf0..83a94a0 100755 --- a/scripts/cfw_install.sh +++ b/scripts/cfw_install.sh @@ -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 ;; *) diff --git a/scripts/patchers/cfw.py b/scripts/patchers/cfw.py index 345f165..a9c076f 100755 --- a/scripts/patchers/cfw.py +++ b/scripts/patchers/cfw.py @@ -59,6 +59,7 @@ Commands: installs. Self-gating (no-op on pre-iOS-27 userlands where the method is absent). patch-xpc-lwcr [--dry-run] + patch-lockdown-mode [--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 [--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) diff --git a/scripts/patchers/cfw_patch_lockdown_mode.py b/scripts/patchers/cfw_patch_lockdown_mode.py new file mode 100644 index 0000000..4531da6 --- /dev/null +++ b/scripts/patchers/cfw_patch_lockdown_mode.py @@ -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 + cmn w0, #1 ; w0 == -1 ? + b.eq ; -> 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 ` 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)