iOS 27.2 kernel differential + NTL fix-boundary sweep

- docs/IOS27.2-KERNEL-DIFF.md: 27.0.1 vs 27.2 (iPhone19,7), 162 advisory
  CVEs, 93 size-changed kexts; confirmed fix sites for 112705 (NTL 0..7
  assertion), 113910 (produced vs expected chunk bytes), 112507 (ipr/table
  NULL checks in ipc_misc.c); __DATA_SPTM byte-identical, no PPL/SPTM/MIE
  code change or bypass fix
- tools/ipsw/: Range-based IPSW reader, IM4P/LZFSE + fileset parser, kext
  extractor, llvm-objdump diff, advisory mapper
- CVE-2026-112705: --sweep mode (0..12, no frame) in both POCs; app module
  now sweeps 0/1/7/8/64/1024/16462/16463
- app 2.3 build 7
This commit is contained in:
JBJF committed 2026-10-31 17:09:48 +08:00
1 parent 030df9a8b1
commit 3ac5108928
15 files changed
+957 -4

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
# IPSW kernel-diff toolkit
Read-only helpers used for the 27.0.1 → 27.2 kernel comparison in
[`docs/IOS27.2-KERNEL-DIFF.md`](../../docs/IOS27.2-KERNEL-DIFF.md).
| Script | Purpose |
|---|---|
| `remote_ipsw.py` | HTTP-Range ZIP reader: list/extract a member without downloading the whole IPSW |
| `ipswlib.py` | IM4P/LZFSE decode, Mach-O/fileset parse, cstring extraction |
| `extract_kext.py` | dump one fileset entry as a standalone Mach-O (linkedit offsets rebased) |
| `disasm_diff.py` | normalized `llvm-objdump` instruction diff for two kexts |
| `diff_kernel.py` | kext-level diff (bundle id, size, strings, function hashes) |
| `map_advisory.py` | advisory CVE list → component/binary candidate table |
Requirements: Python 3, `pip install lzfse macholib`, Xcode command line tools
(`llvm-objdump`). Nothing writes to a device.
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Diff two prelinked kernelcaches (27.0.1 vs 27.2) at kext and function level.
Usage:
python3 diff_kernel.py <a.ipsw> <a-member> <b.ipsw> <b-member> [outdir]
Outputs:
kexts.json per-kext uuid/text hash/bundle id for both builds
changed.txt human-readable summary
strings-<id>.txt string adds/removes for changed kexts
"""
import hashlib
import json
import os
import re
import sys
import zipfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ipswlib
def ascii_strings(data, minlen=6):
out = []
cur = bytearray()
for b in data:
if 32 <= b < 127:
cur.append(b)
else:
if len(cur) >= minlen:
out.append(cur.decode("ascii", "ignore"))
cur = bytearray()
if len(cur) >= minlen:
out.append(cur.decode("ascii", "ignore"))
return out
BID_RE = re.compile(r"^com\.apple\.[A-Za-z0-9_.-]{4,}$")
def bundle_id(m):
"""Heuristic: the kext identifier is the longest com.apple.* string in __TEXT."""
cands = [s for s in ascii_strings(m.text_bytes()) if BID_RE.match(s)]
cands.sort(key=lambda s: (-s.count("."), -len(s)))
return cands[0] if cands else None
def func_hashes(m):
starts = m.function_offsets()
text = m.text_bytes()
t = m.text_segment()
out = []
for i, addr in enumerate(starts):
end = starts[i + 1] if i + 1 < len(starts) else t["vmaddr"] + t["vmsize"]
off = m.vm_to_file(addr)
endoff = m.vm_to_file(end)
if off is None or endoff is None or endoff < off:
continue
chunk = m.data[off:endoff]
out.append((addr, hashlib.sha256(chunk).hexdigest()[:16], len(chunk)))
return out
def load(ipsw, member):
data = ipswlib.read_kernelcache(ipsw, member)
ms = ipswlib.scan_machos(data)
rows = []
for m in ms:
rows.append({
"uuid": m.uuid,
"cpu": ipswlib.CPU_NAMES.get(m.cputype, hex(m.cputype)),
"bid": bundle_id(m),
"text_sha": hashlib.sha256(m.text_bytes()).hexdigest()[:16],
"text_size": len(m.text_bytes()),
"nfuncs": len(m.function_offsets()) if m.function_starts else 0,
"_m": m,
})
return data, rows
def main():
a_ipsw, a_mem, b_ipsw, b_mem = sys.argv[1:5]
outdir = sys.argv[5] if len(sys.argv) > 5 else "work/kernel-diff"
os.makedirs(outdir, exist_ok=True)
print("== extracting", a_mem)
a, ra = load(a_ipsw, a_mem)
print("== extracting", b_mem)
b, rb = load(b_ipsw, b_mem)
def index(rows):
by_bid = {}
no_bid = []
for r in rows:
if r["bid"]:
by_bid.setdefault(r["bid"], []).append(r)
else:
no_bid.append(r)
return by_bid, no_bid
ia, na = index(ra)
ib, nb = index(rb)
changed, added, removed, same = [], [], [], []
for bid in sorted(set(ia) | set(ib)):
la = ia.get(bid, [])
lb = ib.get(bid, [])
if la and lb:
# pair in order; usually one Mach-O per kext
for x, y in zip(la, lb):
if x["uuid"] == y["uuid"] and x["text_sha"] == y["text_sha"]:
same.append(bid)
else:
changed.append((bid, x, y))
elif la:
removed += [bid]
else:
added += [bid]
lines = []
lines.append(f"kexts: a={len(ra)} b={len(rb)} same={len(same)} changed={len(changed)} added={len(added)} removed={len(removed)}")
lines.append("")
for bid, x, y in changed:
lines.append(f"CHANGED {bid}: uuid {x['uuid']} -> {y['uuid']} text {x['text_size']} -> {y['text_size']} funcs {x['nfuncs']} -> {y['nfuncs']}")
for bid in added:
lines.append(f"ADDED {bid}")
for bid in removed:
lines.append(f"REMOVED {bid}")
open(os.path.join(outdir, "changed.txt"), "w").write("\n".join(lines) + "\n")
print("\n".join(lines[:80]))
# string + function diff for changed kexts
for bid, x, y in changed:
ma = x["_m"]
mb = y["_m"]
sa = set(ascii_strings(ma.text_bytes(), 8))
sb = set(ascii_strings(mb.text_bytes(), 8))
new = sorted(sb - sa)
gone = sorted(sa - sb)
safe = bid.replace("/", "_")
with open(os.path.join(outdir, f"strings-{safe}.txt"), "w") as f:
f.write(f"# {bid}\n## new strings ({len(new)})\n")
for s in new[:400]:
f.write(s + "\n")
f.write(f"\n## removed strings ({len(gone)})\n")
for s in gone[:200]:
f.write(s + "\n")
fa = func_hashes(ma)
fb = func_hashes(mb)
changed_funcs = sum(1 for p, q in zip(fa, fb) if p[1] != q[1])
lines.append(f" {bid}: functions {len(fa)} -> {len(fb)}, hash-changed in common prefix {changed_funcs}")
open(os.path.join(outdir, "changed.txt"), "w").write("\n".join(lines) + "\n")
json.dump([
{"bid": bid, "a": {k: v for k, v in x.items() if not k.startswith("_")},
"b": {k: v for k, v in y.items() if not k.startswith("_")}}
for bid, x, y in changed
], open(os.path.join(outdir, "kexts-changed.json"), "w"), indent=1)
print("wrote", outdir)
if __name__ == "__main__":
main()
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Disassembly diff for two standalone Mach-O kexts (same bundle id, two builds).
Usage: python3 disasm_diff.py <a.macho> <b.macho> [max_lines]
Normalizes branch targets and absolute addresses so the diff shows real
instruction changes rather than layout noise.
"""
import difflib
import re
import subprocess
import sys
ADDR = re.compile(r"\b[0-9a-f]{8,16}\b")
def disasm(path):
p = subprocess.run(["xcrun", "llvm-objdump", "--disassemble", path],
capture_output=True, text=True)
lines = []
for line in p.stdout.splitlines():
m = re.match(r"^\s*([0-9a-f]{8,16}):\s+([0-9a-f ]+?)\s{2,}(\S.*)$", line)
if not m:
continue
ins = m.group(3).strip()
ins = re.sub(r"0x[0-9a-f]{6,}", "0xADDR", ins)
ins = re.sub(r"#0x[0-9a-f]{4,}", "#0xIMM", ins)
lines.append(ins)
return lines
def main():
a, b = sys.argv[1], sys.argv[2]
limit = int(sys.argv[3]) if len(sys.argv) > 3 else 400
la, lb = disasm(a), disasm(b)
print(f"# {a}: {len(la)} insns")
print(f"# {b}: {len(lb)} insns")
sm = difflib.SequenceMatcher(a=la, b=lb, autojunk=False)
shown = 0
for tag, i1, i2, j1, j2 in sm.get_opcodes():
if tag == "equal":
continue
if shown >= limit:
print("... truncated")
break
print(f"\n## {tag} a[{i1}:{i2}] b[{j1}:{j2}]")
for x in la[i1:i2][:14]:
print(" - " + x)
for x in lb[j1:j2][:14]:
print(" + " + x)
shown += 1
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Extract one fileset entry (kext) from a kernelcache into a standalone Mach-O.
Usage: python3 extract_kext.py <kernel> <entries.json> <bundle-id> <out.macho>
"""
import json
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ipswlib
LC_SEGMENT_64 = 0x19
def extract(data, off, out):
m = ipswlib.MachO(data, off)
end = 32 + m.sizeofcmds
for s in m.segments:
base = s["fileoff"] if s["fileoff"] >= off else off + s["fileoff"]
end = max(end, base + s["filesize"])
blob = bytearray(data[off:end])
o = 32
for _ in range(m.ncmds):
cmd, cmdsize = struct.unpack_from("<II", blob, o)
if cmd == LC_SEGMENT_64:
# segment_command_64: cmd8 cmdsize8 segname16 vmaddr8 vmsize8 fileoff8 ...
fileoff = struct.unpack_from("<Q", blob, o + 40)[0]
if fileoff >= off:
struct.pack_into("<Q", blob, o + 40, fileoff - off)
nsects = struct.unpack_from("<I", blob, o + 64)[0]
so = o + 72
for _s in range(nsects):
soff = struct.unpack_from("<I", blob, so + 48)[0]
if soff >= off:
struct.pack_into("<I", blob, so + 48, soff - off)
so += 80
elif cmd == 0x2: # LC_SYMTAB
for f in (8, 16):
v = struct.unpack_from("<I", blob, o + f)[0]
if v >= off: struct.pack_into("<I", blob, o + f, v - off)
elif cmd == 0xb: # LC_DYSYMTAB
for f in (32, 40, 48, 56, 64, 72):
v = struct.unpack_from("<I", blob, o + f)[0]
if v >= off: struct.pack_into("<I", blob, o + f, v - off)
elif cmd in (0x22, 0x80000022): # LC_DYLD_INFO / _ONLY
for f in (8, 16, 24, 32, 40):
v = struct.unpack_from("<I", blob, o + f)[0]
if v >= off: struct.pack_into("<I", blob, o + f, v - off)
elif cmd in (0x26, 0x1d, 0x80000033, 0x80000034): # FUNCTION_STARTS, CODE_SIG, EXPORTS_TRIE, CHAINED_FIXUPS
v = struct.unpack_from("<I", blob, o + 8)[0]
if v >= off: struct.pack_into("<I", blob, o + 8, v - off)
o += cmdsize
with open(out, "wb") as f:
f.write(blob)
return len(blob)
if __name__ == "__main__":
kernel, entries, bid, out = sys.argv[1:5]
data = open(kernel, "rb").read()
table = {n: o for n, o, v in json.load(open(entries))}
n = extract(data, table[bid], out)
print("wrote", out, n, "bytes")
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Minimal IPSW / IM4P / Mach-O toolkit for the 27.0.1 vs 27.2 kernel diff.
No external ipsw binary is available on this host, so this implements the pieces
needed for the kernelcache comparison:
* IM4P container parse + LZFSE payload decode
* embedded Mach-O (kext) scan inside a prelinked kernelcache
* UUID / segment / function-start extraction
* function-level hashing and a two-build diff
Only reads files; nothing here writes to a device.
"""
import hashlib
import os
import struct
import sys
import zipfile
try:
import lzfse
except ImportError:
lzfse = None
# ── IM4P ────────────────────────────────────────────────────────────────────
def im4p_payload(data: bytes) -> bytes:
if data[:4] != b"IM4P":
return data
# magic(4) type(4) header_len(4 BE) header payload
hlen = struct.unpack(">I", data[8:12])[0]
return data[12 + hlen:]
def decompress(payload: bytes) -> bytes:
# LZFSE streams start with bvx2 / bvx- / bvx$
for magic in (b"bvx2", b"bvx-", b"bvx$"):
i = payload.find(magic)
if i >= 0:
if lzfse is None:
raise RuntimeError("lzfse module missing")
return lzfse.decompress(payload[i:])
if payload[:2] == b"\x1f\x8b":
import gzip
return gzip.decompress(payload)
return payload
def read_kernelcache(ipsw_path: str, member: str) -> bytes:
with zipfile.ZipFile(ipsw_path) as z:
raw = z.read(member)
return decompress(im4p_payload(raw))
# ── Mach-O ──────────────────────────────────────────────────────────────────
MH_MAGIC_64 = 0xFEEDFACF
LC_SEGMENT_64 = 0x19
LC_SYMTAB = 0x2
LC_UUID = 0x1B
LC_FUNCTION_STARTS = 0x26
LC_UNIXTHREAD = 0x5
CPU_NAMES = {0x0100000C: "arm64", 0x0100000C | 0x02000000: "arm64e"}
class MachO:
def __init__(self, data: bytes, off: int = 0):
self.data = data
self.off = off
magic, cputype, cpusub, filetype, ncmds, sizeofcmds, flags, _ = struct.unpack_from(
"<IiiIIIII", data, off)
if magic != MH_MAGIC_64:
raise ValueError("not a 64-bit Mach-O")
self.magic = magic
self.cputype = cputype
self.cpusub = cpusub
self.filetype = filetype
self.ncmds = ncmds
self.sizeofcmds = sizeofcmds
self.flags = flags
self.segments = []
self.uuid = None
self.function_starts = None
self.symtab = None
self._parse_commands()
def _parse_commands(self):
o = self.off + 32
for _ in range(self.ncmds):
cmd, cmdsize = struct.unpack_from("<II", self.data, o)
if cmdsize < 8:
break
if cmd == LC_SEGMENT_64:
name = self.data[o + 8:o + 24].split(b"\0")[0].decode("ascii", "replace")
vmaddr, vmsize, fileoff, filesize = struct.unpack_from("<QQQQ", self.data, o + 24)
maxprot, initprot, nsects, flags = struct.unpack_from("<IIII", self.data, o + 56)
seg = {
"name": name, "vmaddr": vmaddr, "vmsize": vmsize,
"fileoff": fileoff, "filesize": filesize,
"maxprot": maxprot, "initprot": initprot, "nsects": nsects,
"sections": [],
}
so = o + 72
for _s in range(nsects):
sect = self.data[so:so + 16].split(b"\0")[0].decode("ascii", "replace")
segn = self.data[so + 16:so + 32].split(b"\0")[0].decode("ascii", "replace")
addr, size, offset = struct.unpack_from("<QQI", self.data, so + 32)
seg["sections"].append({"seg": segn, "sect": sect, "addr": addr,
"size": size, "offset": offset})
so += 80
self.segments.append(seg)
elif cmd == LC_UUID:
self.uuid = self.data[o + 8:o + 24].hex()
elif cmd == LC_FUNCTION_STARTS:
dataoff, datasize = struct.unpack_from("<II", self.data, o + 8)
self.function_starts = (dataoff, datasize)
elif cmd == LC_SYMTAB:
symoff, nsyms, stroff, strsize = struct.unpack_from("<IIII", self.data, o + 8)
self.symtab = (symoff, nsyms, stroff, strsize)
o += cmdsize
def text_segment(self):
for s in self.segments:
if s["name"] == "__TEXT_EXEC":
return s
for s in self.segments:
if s["name"] == "__TEXT":
return s
return None
def function_offsets(self):
"""Absolute file offsets of function starts, decoded from LC_FUNCTION_STARTS."""
if not self.function_starts:
return []
dataoff, datasize = self.function_starts
base = self.off + dataoff
end = base + datasize
text = self.text_segment()
if text is None:
return []
# LC_FUNCTION_STARTS dataoff is relative to the Mach-O header (linkedit).
addr = text["vmaddr"]
out = []
p = base
while p < end and self.data[p] != 0:
delta = 0
shift = 0
while True:
b = self.data[p]
p += 1
delta |= (b & 0x7F) << shift
shift += 7
if not (b & 0x80):
break
addr += delta
out.append(addr)
return out
def _file_base(self, s):
# In a MH_FILESET, segment fileoffs are absolute in the container; in a
# standalone Mach-O they are relative to the header (which sits at self.off).
return s["fileoff"] if s["fileoff"] >= self.off else self.off + s["fileoff"]
def vm_to_file(self, addr, seg=None):
for s in self.segments:
if s["vmaddr"] <= addr < s["vmaddr"] + s["vmsize"]:
return self._file_base(s) + (addr - s["vmaddr"])
return None
def cstring_bytes(self):
out = bytearray()
for seg in self.segments:
for sec in seg.get("sections", []):
if sec["sect"] in ("__cstring", "__oslogstring"):
base = sec["offset"] if sec["offset"] >= self.off else self.off + sec["offset"]
out += self.data[base: base + sec["size"]]
out += b"\0"
return bytes(out)
def text_bytes(self):
t = self.text_segment()
if not t:
return b""
base = self._file_base(t)
return self.data[base: base + t["filesize"]]
def scan_machos(data: bytes, limit=20000):
"""Scan a prelinked kernelcache for embedded Mach-O headers (page aligned)."""
out = []
off = 0
n = len(data)
while off + 32 <= n and len(out) < limit:
if data[off:off + 4] == b"\xcf\xfa\xed\xfe":
try:
m = MachO(data, off)
out.append(m)
except Exception:
pass
off += 0x1000 # kexts are page aligned
return out
def macho_summary(m: MachO) -> dict:
text = m.text_bytes()
return {
"off": m.off,
"cpu": CPU_NAMES.get(m.cputype, hex(m.cputype)),
"sub": m.cpusub,
"filetype": m.filetype,
"uuid": m.uuid,
"text_size": len(text),
"text_sha256": hashlib.sha256(text).hexdigest()[:16],
"segments": [(s["name"], s["vmsize"]) for s in m.segments if s["name"].startswith("__")],
"nfuncs": len(m.function_offsets()) if m.function_starts else 0,
}
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
if cmd == "list":
with zipfile.ZipFile(sys.argv[2]) as z:
for i in z.infolist():
if any(k in i.filename for k in ("kernelcache", "dyld_shared_cache", "BuildManifest")):
print(i.filename, i.file_size)
elif cmd == "kernel":
data = read_kernelcache(sys.argv[2], sys.argv[3])
print("kernelcache bytes", len(data), "sha256", hashlib.sha256(data).hexdigest()[:16])
ms = scan_machos(data)
print("embedded Mach-Os:", len(ms))
for m in ms[:10]:
print(macho_summary(m))
else:
print("usage: ipswlib.py list <ipsw> | kernel <ipsw> <member>")
def dump_macho(m: MachO, path: str) -> int:
"""Write the Mach-O image (header through the last segment's file extent)."""
end = 32 + m.sizeofcmds
for s in m.segments:
end = max(end, s["fileoff"] + s["filesize"])
blob = m.data[m.off:m.off + end]
with open(path, "wb") as f:
f.write(blob)
return len(blob)
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Map the iOS 27.2 advisory entries to binary candidates and emit a report skeleton.
Usage: python3 map_advisory.py <advisory.json> <out.md>
The mapping is a heuristic name table; it is refined later by the actual kext and
dylib inventory extracted from the IPSWs.
"""
import json
import sys
# component -> (kind, candidate binary / kext)
MAP = {
"Kernel": ("kernel", "kernelcache core (pmap/ipc/vm/bsd)"),
"AVEVideoEncoder": ("kernel+user", "com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin"),
"Apple Neural Engine": ("kernel+user", "AppleH16ANEInterface / AppleNeuralEngine.framework"),
"IOKit": ("kernel+user", "IOKit.framework / IOKit kexts"),
"IOHIDFamily": ("kernel", "com.apple.iokit.IOHIDFamily"),
"IOStorageFamily": ("kernel", "com.apple.iokit.IOStorageFamily"),
"IOMobileFrameBuffer": ("kernel", "com.apple.driver.IOMobileFrameBuffer"),
"IOSurface": ("kernel+user", "IOSurface.framework / IOSurface kext"),
"HFS": ("kernel", "com.apple.filesystems.hfs.kext"),
"Compression": ("user", "libcompression.dylib"),
"CoreMedia": ("user", "CoreMedia.framework / mediaplaybackd"),
"CoreGraphics": ("user", "CoreGraphics.framework"),
"CoreText": ("user", "CoreText.framework"),
"CoreAudio": ("user", "CoreAudio.framework / AudioToolbox"),
"Audio": ("user", "AudioToolbox / AudioCodecs"),
"ImageIO": ("user", "ImageIO.framework"),
"FontParser": ("user", "FontParser.framework"),
"libarchive": ("user", "libarchive.2.dylib"),
"LinkPresentation": ("user", "LinkPresentation.framework"),
"WebKit": ("user", "JavaScriptCore / WebKit / WebCore"),
"WebKit Canvas": ("user", "WebCore (canvas)"),
"WebKit JavaScript Bindings": ("user", "JavaScriptCore bindings"),
"WebKit Process Model": ("user", "WebKit process model"),
"WebRTC": ("user", "WebRTC.framework"),
"Security": ("user", "Security.framework"),
"Wi-Fi": ("user+kernel", "WiFi frameworks / IO80211 kext"),
"USB": ("kernel", "USB kexts"),
"Power Management": ("kernel", "com.apple.driver.AppleARMPlatform / PM kexts"),
"Bluetooth": ("user+kernel", "Bluetooth.framework / IOBluetooth kexts"),
"mDNSResponder": ("user", "mDNSResponder"),
"Thread": ("user", "ThreadNetwork.framework"),
"MobileGestalt": ("user", "MobileGestalt"),
"SpringBoard": ("user", "SpringBoard"),
"Messages": ("user", "Messages / IMSharedUtilities"),
"Mail": ("user", "Mail"),
"Maps": ("user", "Maps"),
"Photos Storage": ("user", "Photos"),
"Spotlight": ("user", "Spotlight"),
"Siri": ("user", "Siri"),
"Shortcuts": ("user", "Shortcuts"),
"HomeKit": ("user", "HomeKit"),
"CloudKit": ("user", "CloudKit"),
"Contacts": ("user", "Contacts"),
"Calendar": ("user", "CalendarFoundation"),
"App Store": ("user", "AppStore"),
"App Intents": ("user", "AppIntents"),
"Apple Intelligence": ("user", "AppleIntelligence"),
"AccessorySetupKit": ("user", "AccessorySetupKit"),
"Accounts": ("user", "Accounts"),
"FileProvider": ("user", "FileProvider"),
"Files": ("user", "Files"),
"Heimdal": ("user", "Heimdal (Kerberos)"),
"MediaAnalysis": ("user", "MediaAnalysis"),
"MediaRemote": ("user", "MediaRemote"),
"Quick Look": ("user", "QuickLook"),
"RealityKit": ("user", "RealityKit"),
"ReplayKit": ("user", "ReplayKit"),
"Safety Check": ("user", "SafetyCheck"),
"SceneKit": ("user", "SceneKit"),
"Screen Time": ("user", "ScreenTime"),
"SwiftUI": ("user", "SwiftUI"),
"System Preferences": ("user", "SystemPreferences"),
"TextKit": ("user", "TextKit"),
"Accelerate Framework": ("user", "Accelerate"),
"ColorSync": ("user", "ColorSync"),
}
def main():
rows = json.load(open(sys.argv[1]))
out = sys.argv[2]
lines = ["# iOS 27.2 advisory — component → binary map", ""]
lines.append(f"Entries: {len(rows)} Components: {len(set(r['component'] for r in rows))}")
lines.append("")
lines.append("| CVE | Component | Impact | Binary candidate |")
lines.append("|---|---|---|---|")
for r in sorted(rows, key=lambda x: (x["component"], x["cve"])):
kind, cand = MAP.get(r["component"], ("unknown", "?"))
lines.append(f"| {r['cve']} | {r['component']} | {r['impact'][:70]} | {cand} |")
open(out, "w").write("\n".join(lines) + "\n")
print("wrote", out, len(rows), "entries")
missing = sorted(set(r["component"] for r in rows) - set(MAP))
print("unmapped components:", missing)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Remote ZIP reader over HTTP Range, so the kernelcache can be inspected before the
full IPSW finishes downloading.
Usage:
python3 remote_ipsw.py list <url>
python3 remote_ipsw.py extract <url> <member-substring> <out>
"""
import io
import sys
import urllib.request
import zipfile
class HTTPRangeFile(io.RawIOBase):
def __init__(self, url, block=4 << 20):
self.url = url
self.pos = 0
self.block = block
self.cache = {}
req = urllib.request.Request(url, method="HEAD")
with urllib.request.urlopen(req) as r:
self.size = int(r.headers["Content-Length"])
self.ranges = r.headers.get("Accept-Ranges", "").lower() == "bytes"
def seekable(self): return True
def readable(self): return True
def tell(self): return self.pos
def seek(self, off, whence=0):
if whence == 0: self.pos = off
elif whence == 1: self.pos += off
else: self.pos = self.size + off
return self.pos
def _fetch(self, start, end):
key = (start // self.block) * self.block
if key in self.cache:
return self.cache[key]
stop = min(self.size - 1, key + self.block - 1)
req = urllib.request.Request(self.url, headers={"Range": f"bytes={key}-{stop}"})
with urllib.request.urlopen(req) as r:
data = r.read()
self.cache[key] = data
if len(self.cache) > 8:
self.cache.pop(next(iter(self.cache)))
return data
def read(self, n=-1):
if n is None or n < 0:
n = self.size - self.pos
if n == 0 or self.pos >= self.size:
return b""
out = bytearray()
while n > 0 and self.pos < self.size:
block = self._fetch(self.pos, self.pos)
base = (self.pos // self.block) * self.block
off = self.pos - base
take = min(n, len(block) - off)
if take <= 0:
break
out += block[off:off + take]
self.pos += take
n -= take
return bytes(out)
def main():
cmd, url = sys.argv[1], sys.argv[2]
f = HTTPRangeFile(url)
print("size", f.size, "range", f.ranges)
z = zipfile.ZipFile(f)
if cmd == "list":
for i in z.infolist():
if any(k in i.filename for k in ("kernelcache", "dyld_shared_cache", "BuildManifest")):
print(i.filename, i.file_size, i.compress_type)
elif cmd == "extract":
pat, out = sys.argv[3], sys.argv[4]
names = [i.filename for i in z.infolist() if pat in i.filename and not i.is_dir()]
print("matches:", names[:10], "count", len(names))
if not names:
return
name = names[0]
with z.open(name) as src, open(out, "wb") as dst:
while True:
chunk = src.read(8 << 20)
if not chunk: break
dst.write(chunk)
print("wrote", out)
else:
print("usage: list|extract")
if __name__ == "__main__":
main()