mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
cfw: patchers: Make iomfb patch dynamic instead of hard coded bytes
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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` |
|
||||
|
||||
## 固件变体
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user