Files
vphone-cli/scripts/patchers/cfw_patch_hv_vmm.py
T
zqxwce af90c9a903 userland: Add DSC hv_vmm_present byte-5 mangle with sign-in blacklist and slot reattest (EXP only)
Companion user-mode patches to the kernel-side OID rename. Mangles
byte 5 of every `kern.hv_vmm_present` cstring inside DSC dylibs EXCEPT
those in `DONT_PATCH_INSTALL_NAMES` (sign-in / device-likeness consumers,
~15 entries). Patched dylibs query the renamed OID and get the truthful
1 (graphics + accel passthrough); blacklisted dylibs keep the original
cstring, hit ENOENT on the renamed kernel, defensively cache 0 ("not
running on a VM") for sign-in / device-attestation surfaces.

- `scripts/patchers/cfw_patch_hv_vmm_dsc.py` — DSC orchestrator. Walks
  every `kern.hv_vmm_present\\0` cstring in any executable mapping,
  resolves the containing dylib via Mach-O-header walk-back +
  LC_ID_DYLIB, applies the byte-5 mangle to non-blacklisted dylibs, and
  drives slot-hash re-attestation in the chunk's `CS_CodeDirectory`.
- `scripts/patchers/cfw_dsc_chunks.py` — pure-Python helper for the
  chunked DSC layout: vmaddr<->chunk-fileoff mapping, install-name
  walk-back, byte-level read/write at a vmaddr.
- `scripts/patchers/cfw_dsc_codesign.py` — per-page SHA-256 slot-hash
  re-attestation for DSC chunks (16 KiB pages). Required on
  `codeSigningMonitor == 2` (TXM) hardware where per-page hash checks
  would otherwise SIGKILL the patched dylibs at first demand-page-in.
- `scripts/patchers/cfw_patch_hv_vmm.py` — standalone Mach-O variant of
  the cstring mangle (kept for completeness; the historical
  standalone-binary loop step was removed in favor of the
  blacklist-flip-via-kernel-rename design).
- `scripts/patchers/cfw_patch_hv_vmm_rootfs.py` — rootfs-path inventory
  shared with the install scripts (former JB-3.5 / 6.5/7 loop input).
- `scripts/patch_hv_vmm_userland.sh` — thin wrapper used by the install
  script (`dsc` and `standalone` operations; `watchdogd` is added by
  the next commit).
- `scripts/patchers/cfw.py` — new subcommands: `patch-hv-vmm`,
  `patch-hv-vmm-dsc`, `list-hv-vmm-rootfs-paths`.
- `scripts/cfw_install_exp.sh` — adds a pre-step that decrypts the
  SystemOS Cryptex AEA into the cache location `cfw_install.sh` already
  uses, mounts it, applies the DSC patch, and unmounts. The unmodified
  base `cfw_install.sh` then picks up the cached (patched) DMG.

Research docs in this commit describe the full EXP variant comprehensively
(`research/0_binary_patch_comparison.md` top-of-doc note, EXP-Only
Kernel Methods section, DSC userland subsection, plus forward references
to EXP-JB-3.5 / EXP-JB-6 / EXP-JB-7 subsections wired up by the following
commits; `research/firmware_manifest_and_origins.md` sections 9-11
similarly forward-describe the DT and build-version pieces).

JB and DEV variants are NOT affected: their install scripts don't invoke
any of this.
2026-05-18 16:14:55 +03:00

143 lines
5.6 KiB
Python

"""hv_vmm_present user-mode patch module — cstring-mangle approach.
Mangles the cstring "kern.hv_vmm_present\\0" in targeted user-mode
binaries so they query a kernel sysctl with a different name. On a
kernel patched by `KernelJBPatchHvVmmRename` (which renames the OID
from `hv_vmm_present` to `Xv_vmm_present`), the new design is:
- Caller queries `kern.hv_vmm_present` (UNPATCHED binary):
kernel returns ENOENT. The canonical defensive post-call check
(`cbnz w0, skip` / `cmp w0,#0 ; b.ne skip`) takes the skip
path; the cached `is_vmm` byte stays at BSS-zero (0). The
binary thinks it is NOT running in a VM.
- Caller queries `kern.Xv_vmm_present` (PATCHED binary):
kernel returns 1 (the real value of the OID's int variable).
The defensive check passes; the cache stores 1. The binary
thinks it IS running in a VM — same as a stock device.
Patch shape (per cstring occurrence): byte 5 of the cstring
`'h'` (0x68) → `'X'` (0x58). The mangled cstring is
`"kern.Xv_vmm_present\\0"`. Byte 5 is chosen so the `kern.` top-level
sysctl namespace is preserved — without that, the kernel's
name-to-MIB resolver wouldn't route the call to any OID.
Public entrypoints:
patch_hv_vmm(filepath, *, dry_run=False) -> int
Patch a standalone Mach-O. Returns the number of cstring sites
mangled.
find_string_sites(data: bytes) -> list[dict]
Find every cstring occurrence in an in-memory Mach-O.
"""
import struct
from .cfw_asm import parse_macho_sections, _log_asm
NEEDLE = b"kern.hv_vmm_present\x00"
# Mangle byte offset (0-based within NEEDLE). Position 5 is the 'h' of
# "hv_vmm_present" — the first byte after the "kern." namespace prefix.
# Keeping the "kern." prefix intact ensures the kernel's sysctl name-to-MIB
# resolver routes the call to a real OID (registered as `Xv_vmm_present`
# under `kern` by the companion kernel patch). Byte 0 would have produced
# `Xern.hv_vmm_present`, which can never resolve because `Xern` isn't a
# registered top-level sysctl namespace.
MANGLE_OFFSET = 5
ORIGINAL_BYTE = b"h" # 0x68
MANGLED_BYTE = b"X" # 0x58
MANGLED_NEEDLE = NEEDLE[:MANGLE_OFFSET] + MANGLED_BYTE + NEEDLE[MANGLE_OFFSET + 1:]
def find_string_sites(data):
"""Return all unmangled "kern.hv_vmm_present\\0" cstring occurrences.
Each entry: {string_vma, file_offset, section}.
Hits are anchored at a cstring boundary (preceded by a NUL byte or
at the start of the section) so partial-match substrings inside
longer cstrings won't be returned.
"""
sections = parse_macho_sections(data)
out = []
for sec_name, (vma, size, foff) in sections.items():
_, _, sect = sec_name.partition(",")
# __cstring is where the linker puts unique C-string literals.
# Some method-name / class-name pools could in principle hold
# the same bytes too; we include them for safety.
if sect not in ("__cstring", "__objc_methname", "__objc_classname"):
continue
buf = bytes(data[foff:foff + size])
i = 0
while True:
p = buf.find(NEEDLE, i)
if p < 0:
break
if p == 0 or buf[p - 1] == 0:
out.append(
{
"string_vma": vma + p,
"file_offset": foff + p,
"section": sec_name,
}
)
i = p + 1
return out
def is_already_mangled(data):
"""Detect whether the binary already contains the mangled form.
Used purely for friendlier logging — `find_string_sites` already
returns empty on a mangled binary, so the patch flow is idempotent
regardless.
"""
return bytes(data).find(MANGLED_NEEDLE) >= 0
def patch_hv_vmm(filepath, *, dry_run=False):
"""Mangle the kern.hv_vmm_present cstring in a standalone Mach-O.
Returns the count of mangled cstring sites. Idempotent.
"""
data = bytearray(open(filepath, "rb").read())
sites = find_string_sites(bytes(data))
if not sites:
if is_already_mangled(bytes(data)):
print(f" [.] {filepath}: already mangled (no original "
f"'kern.hv_vmm_present' cstring present)")
else:
print(f" [.] {filepath}: 'kern.hv_vmm_present' cstring not "
f"present — nothing to do")
return 0
print(f" [+] {len(sites)} cstring occurrence(s) in {filepath}")
n = 0
for s in sites:
foff = s["file_offset"]
original = bytes(data[foff:foff + len(NEEDLE)])
# Idempotence: if byte at MANGLE_OFFSET is already mangled, skip.
if original[MANGLE_OFFSET:MANGLE_OFFSET + 1] == MANGLED_BYTE:
print(f" [.] string@0x{s['string_vma']:X} already mangled "
f"(byte {MANGLE_OFFSET} is already {MANGLED_BYTE.decode()!r})")
continue
if original != NEEDLE:
print(f" [-] string@0x{s['string_vma']:X} bytes look unexpected "
f"({original!r}); skipping")
continue
print(f" patching string@0x{s['string_vma']:X} (sect={s['section']}): "
f"byte {MANGLE_OFFSET} {ORIGINAL_BYTE.decode()!r} -> "
f"{MANGLED_BYTE.decode()!r} "
f"('kern.hv_vmm_present' -> 'kern.Xv_vmm_present')")
data[foff + MANGLE_OFFSET:foff + MANGLE_OFFSET + 1] = MANGLED_BYTE
n += 1
if dry_run:
print(f" [.] dry-run — not writing back")
return n
if n > 0:
open(filepath, "wb").write(data)
print(f" [+] {filepath}: mangled {n} cstring occurrence(s)")
return n