mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
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:
committed by
zqxwce
co-authored by
Claude Opus 4.8
parent
ab456ac67e
commit
f48fc29a27
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user