- 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
96 lines
3.0 KiB
Python
96 lines
3.0 KiB
Python
#!/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()
|