cfw: patchers: Make iomfb patch dynamic instead of hard coded bytes

This commit is contained in:
zqxwce
2026-07-08 11:28:36 +03:00
committed by zqxwce
parent d97f4f6045
commit aa39b7194d
5 changed files with 203 additions and 95 deletions
+12 -11
View File
@@ -8,17 +8,18 @@ Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC re
## Tested Environments
| Host | iPhone | CloudOS |
| ------------- | --------------------- | --------------- |
| Mac16,8 26.5.1 | `17,3_26.0_23A341` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.0.1_23A355` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| Host | iPhone | CloudOS |
| --------------- | --------------------- | --------------- |
| Mac16,8 26.5.1 | `17,3_26.0_23A341` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.0.1_23A355` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| Mac16,11 27.0b2 | `17,3_26.5.2_23F84` | `26.4-23E5207q` |
iOS 26.0 and 26.0.1 use the 26.1 PCC vphone600 stack plus the CFW-time
`IOMobileFramebuffer` SwapEnd payload-size patch.
+12 -8
View File
@@ -8,14 +8,18 @@ Apple の Virtualization.framework と PCC の研究用 VM インフラを使用
## 検証済み環境
| ホスト | iPhone | CloudOS |
| ------------- | --------------------- | --------------- |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| ホスト | iPhone | CloudOS |
| --------------- | --------------------- | --------------- |
| Mac16,8 26.5.1 | `17,3_26.0_23A341` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.0.1_23A355` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| Mac16,11 27.0b2 | `17,3_26.5.2_23F84` | `26.4-23E5207q` |
## ファームウェアバリアント
+12 -8
View File
@@ -8,14 +8,18 @@ PCC 리서치 VM 인프라와 Apple의 Virtualization.framework를 사용하여
## 테스트된 환경
| Host | iPhone | CloudOS |
| ------------- | --------------------- | --------------- |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| Host | iPhone | CloudOS |
| --------------- | --------------------- | --------------- |
| Mac16,8 26.5.1 | `17,3_26.0_23A341` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.0.1_23A355` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| Mac16,11 27.0b2 | `17,3_26.5.2_23F84` | `26.4-23E5207q` |
## 펌웨어 변형
+11 -7
View File
@@ -9,13 +9,17 @@
## 测试环境
| 主机 | iPhone 系统 | CloudOS |
| ------------- | --------------------- | --------------- |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| --------------- | --------------------- | --------------- |
| Mac16,8 26.5.1 | `17,3_26.0_23A341` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.0.1_23A355` | `26.1-23B85` |
| Mac16,8 26.5.1 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
| Mac16,12 26.3 | `17,3_26.3.1_23D8133` | `26.3-23D128` |
| Mac16,11 26.2 | `17,3_26.4_23E246` | `26.4-23E5207q` |
| Mac16,11 26.2 | `17,3_26.5_23F77` | `26.4-23E5207q` |
| Mac16,11 27.0b2 | `17,3_26.5.2_23F84` | `26.4-23E5207q` |
## 固件变体
+156 -61
View File
@@ -1,90 +1,185 @@
"""Patch iOS 26.0 and 26.0.1 IOMobileFramebuffer SwapEnd payload size.
The PCC vphone600 26.1 kernel-side IOMobileFramebuffer external method 5
expects the newer 0x560-byte swap state. iOS 26.0 and 26.0.1 userland send 0x548,
so SwapEnd returns kIOReturnBadArgument and the VM display stays black.
expects the newer 0x560-byte swap state. iOS 26.0 and 26.0.1 userland send a
smaller state (0x548), so SwapEnd returns kIOReturnBadArgument and the VM
display stays black.
`_kern_SwapEnd` sets up an external-method-5 call:
ldr w0, [x0,#0x14]
add x2, x19,#0x18
mov w1,#5 <- external method selector 5
mov w3,#<size> <- input-state size (0x548 on 26.0; 0x588 on 26.5)
mov x4,#0
mov x5,#0
bl _io_connect_method
The `mov w3,#<size>` immediate is what this patcher rewrites to 0x560, the
size the 26.1 userclient accepts. The site is located dynamically: resolve
`_kern_SwapEnd`, disassemble it with Capstone, and anchor on the semantic
call-setup shape (selector `mov w1,#5` then `mov w3,#imm` then the zeroed
`mov x4,#0`/`mov x5,#0` and the `bl`). Nothing about the source size is
hardcoded, so this fires on 26.0 and 26.0.1 alike; the replacement immediate
comes from the Keystone-backed `asm()` helper.
"""
import os
import re
import shutil
import subprocess
from capstone.arm64_const import ARM64_OP_REG, ARM64_OP_IMM
try:
from .cfw_asm import asm, _cs
from .cfw_dsc_chunks import DSCChunks
from .cfw_dsc_codesign import reattest_modified_pages
except ImportError: # direct self-test execution
from cfw_asm import asm, _cs
from cfw_dsc_chunks import DSCChunks
from cfw_dsc_codesign import reattest_modified_pages
IOMFB = "/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/IOMobileFramebuffer"
SWAPEND_SYMBOL = "_kern_SwapEnd"
# _kern_SwapEnd:
# ldr w0, [x0,#0x14]
# add x2, x19,#0x18
# mov w1,#5
# mov w3,#0x548 <-- patch to 0x560
# mov x4,#0
# mov x5,#0
OLD = bytes.fromhex("001440b962620091a100805203a98052040080d2050080d2")
NEW_INSN = bytes.fromhex("03ac8052")
# External-method selector for SwapEnd, and the input-state size the 26.1
# vphone600 userclient accepts. TARGET_SIZE is the semantic goal, not an
# anchor — the source immediate (0x548 on 26.0) is discovered, never matched.
SWAPEND_SELECTOR = 5
TARGET_SIZE = 0x560
def patch_iomfb_swapend(chunks_dir, *, dry_run=False):
def _resolve_symbol(dsc_path, image, symbol):
"""Resolve a single symbol's vmaddr in `image` via `ipsw dyld symaddr`.
Returns the vmaddr, or raises if unresolved. Mirrors the resolution
idiom in cfw_patch_camera_dsc."""
ipsw_bin = shutil.which("ipsw")
if not ipsw_bin:
raise RuntimeError("`ipsw` not in PATH")
cmd = [ipsw_bin, "dyld", "symaddr", dsc_path, "--image", image, symbol]
out = subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
for line in out.splitlines():
line = re.sub(r"\x1b\[[0-9;]*m", "", line).rstrip()
m = re.match(r"\s*(0x[0-9A-Fa-f]+):.*\b" + re.escape(symbol) + r"\b", line)
if m:
return int(m.group(1), 16)
raise RuntimeError(f"could not resolve {symbol} in {image}")
def _mov_reg_imm(insn):
"""If `insn` is `mov <reg>, #<imm>`, return (reg_name, imm); else None."""
if insn.mnemonic != "mov":
return None
ops = insn.operands
if len(ops) != 2 or ops[0].type != ARM64_OP_REG or ops[1].type != ARM64_OP_IMM:
return None
return insn.reg_name(ops[0].reg), ops[1].imm
def _disasm_function(chunks, vma, max_insns=64):
"""Disassemble from `vma` up to the first ret/retab (function end) or
`max_insns`, whichever comes first."""
buf = chunks.bytes_at_vma(vma, max_insns * 4)
insns = []
for insn in _cs.disasm(buf, vma):
insns.append(insn)
if insn.mnemonic in ("ret", "retab"):
break
return insns
def _find_swap_size_insn(insns):
"""Locate the `mov w3,#imm` that sets the SwapEnd input-state size.
Anchored on the external-method call setup: `mov w1,#5` (selector),
immediately followed by `mov w3,#imm`, then `mov x4,#0`, `mov x5,#0`,
and a `bl`. Returns that insn, or None if the shape isn't found.
"""
for i in range(len(insns) - 4):
sel = _mov_reg_imm(insns[i])
if sel != ("w1", SWAPEND_SELECTOR):
continue
size = _mov_reg_imm(insns[i + 1])
if size is None or size[0] != "w3":
continue
if _mov_reg_imm(insns[i + 2]) != ("x4", 0):
continue
if _mov_reg_imm(insns[i + 3]) != ("x5", 0):
continue
if insns[i + 4].mnemonic != "bl":
continue
return insns[i + 1]
return None
def patch_iomfb_swapend(chunks_dir, *, dsc_path=None, dry_run=False):
chunks = DSCChunks(chunks_dir)
print(f" [.] {chunks!r}")
modified = []
already = 0
refused = 0
for _cp, _foff, vma_start, buf in chunks.iter_executable_mapping_bytes():
i = 0
while True:
p = buf.find(OLD, i)
if p < 0:
break
insn_vma = vma_start + p + 12
header = chunks.find_macho_header_before(insn_vma)
install = chunks.read_install_name_at(header) if header is not None else None
if install != IOMFB:
refused += 1
print(f" [-] refusing non-IOMFB hit at 0x{insn_vma:X}: {install}")
else:
if not dry_run:
chunks.write_at_vma(insn_vma, NEW_INSN)
modified.append(insn_vma)
print(f" [+] {'would patch' if dry_run else 'patched'} {IOMFB} _kern_SwapEnd size 0x548 -> 0x560 at 0x{insn_vma:X}")
i = p + 4
if dsc_path is None:
dsc_path = os.path.join(chunks_dir, "dyld_shared_cache_arm64e")
# Idempotent rerun support.
patched = OLD[:12] + NEW_INSN + OLD[16:]
i = 0
while True:
p = buf.find(patched, i)
if p < 0:
break
insn_vma = vma_start + p + 12
header = chunks.find_macho_header_before(insn_vma)
install = chunks.read_install_name_at(header) if header is not None else None
if install == IOMFB:
already += 1
modified.append(insn_vma)
print(f" [=] already patched at 0x{insn_vma:X}")
i = p + 4
fn_vma = _resolve_symbol(dsc_path, IOMFB, SWAPEND_SYMBOL)
print(f" [.] {SWAPEND_SYMBOL} @ 0x{fn_vma:X}")
if not modified:
raise ValueError("IOMobileFramebuffer 26.0/26.0.1 SwapEnd size site not found")
if modified and not dry_run:
print(f" [.] re-attesting {len(set(modified))} modified page(s)...")
reattest_modified_pages(chunks, sorted(set(modified)), dry_run=False)
elif modified:
print(f" [.] dry-run: would re-attest {len(set(modified))} page(s)")
insns = _disasm_function(chunks, fn_vma)
target = _find_swap_size_insn(insns)
if target is None:
raise ValueError(
f"{SWAPEND_SYMBOL} SwapEnd size site (mov w1,#{SWAPEND_SELECTOR} "
f"-> mov w3,#imm -> ... -> bl) not found"
)
print(f" [+] IOMFB SwapEnd patch complete: {len(modified) - already} patched, {already} already, {refused} refused")
return len(modified)
_reg, cur_size = _mov_reg_imm(target)
insn_vma = target.address
new_bytes = asm(f"mov w3, #{TARGET_SIZE}")
if len(new_bytes) != 4:
raise RuntimeError(f"expected 4 bytes, got {len(new_bytes)}")
if cur_size == TARGET_SIZE:
print(f" [=] already 0x{TARGET_SIZE:X} at 0x{insn_vma:X}; "
f"re-attesting page only")
else:
action = "would patch" if dry_run else "patched"
print(f" [+] {action} {IOMFB} {SWAPEND_SYMBOL} size "
f"0x{cur_size:X} -> 0x{TARGET_SIZE:X} at 0x{insn_vma:X}")
if not dry_run:
chunks.write_at_vma(insn_vma, new_bytes)
if not dry_run:
print(f" [.] re-attesting modified page...")
reattest_modified_pages(chunks, [insn_vma], dry_run=False)
if chunks.bytes_at_vma(insn_vma, 4) != new_bytes:
raise RuntimeError(f"post-write verify failed at 0x{insn_vma:X}")
else:
print(f" [.] dry-run: would re-attest page for 0x{insn_vma:X}")
print(f" [+] IOMFB SwapEnd patch complete")
return 1
def _self_test():
assert OLD[12:16] == bytes.fromhex("03a98052")
assert NEW_INSN == bytes.fromhex("03ac8052")
patched = OLD[:12] + NEW_INSN + OLD[16:]
assert patched == bytes.fromhex("001440b962620091a100805203ac8052040080d2050080d2")
"""Validate the finder against a synthetic call-setup sequence built
from the assembler, with a source size (0x548) unlike the target."""
seq = (
asm("ldr w0, [x0, #0x14]")
+ asm("add x2, x19, #0x18")
+ asm("mov w1, #5")
+ asm("mov w3, #0x548")
+ asm("mov x4, #0")
+ asm("mov x5, #0")
+ asm("bl #0x40")
)
insns = list(_cs.disasm(seq, 0x1000))
target = _find_swap_size_insn(insns)
assert target is not None, "finder failed to locate size insn"
reg, imm = _mov_reg_imm(target)
assert (reg, imm) == ("w3", 0x548), (reg, hex(imm))
assert target.address == 0x1000 + 12, hex(target.address)
assert asm(f"mov w3, #{TARGET_SIZE}") == bytes.fromhex("03ac8052")
print("self-test OK")
if __name__ == "__main__":