183 lines
7.0 KiB
Python
183 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Carve SquashFS-RootFS und FIT-Image (kernel@1 + fdt@w2044ax) aus dem
|
|
Bintec W20XXax Full-Flash-Image ('kaio', Qualcomm IPQ60xx).
|
|
Liest nur die Quelldatei, schreibt Output nach $WORK (default /build/work)."""
|
|
import struct, sys, os, gzip, re
|
|
|
|
WORK = os.environ.get('WORK', '/build/work')
|
|
SQFS_MAGIC = b'hsqs'
|
|
FIT_MAGIC = b'\xd0\x0d\xfe\xed'
|
|
ARM64_IMG_MAGIC = b'\x41\x52\x4d\x64' # "ARM\x64" an Offset 0x38 des Images
|
|
|
|
|
|
def find_squashfs(data):
|
|
"""Sucht das SquashFS versionsunabhaengig: erst Kandidat bei bekanntem
|
|
Offset (v3.6.1.2), sonst den ganzen Datei-Scan mit Plausibilitaetspruefung."""
|
|
cands = []
|
|
if len(data) > 0x18c7854 and data[0x18c7854:0x18c7854+4] == SQFS_MAGIC:
|
|
cands.append(0x18c7854)
|
|
s = 0
|
|
while True:
|
|
i = data.find(SQFS_MAGIC, s)
|
|
if i < 0:
|
|
break
|
|
cands.append(i)
|
|
s = i + 1
|
|
for off in cands:
|
|
try:
|
|
(magic, inodes, mkfs, bsz, fsz) = struct.unpack_from('<5I', data, off)
|
|
(comp, blog, flags, noids, major, minor) = struct.unpack_from('<6H', data, off + 20)
|
|
(root_inode, bytes_used) = struct.unpack_from('<QQ', data, off + 32)
|
|
except struct.error:
|
|
continue
|
|
if major != 4 or bytes_used < 0x1000:
|
|
continue
|
|
end = off + bytes_used
|
|
if end > len(data) or bsz & (bsz - 1) != 0 or bsz == 0:
|
|
continue
|
|
return off, major, minor, bsz, comp, bytes_used, end
|
|
raise SystemExit("kein plausibles SquashFS gefunden")
|
|
|
|
|
|
def carve_squashfs(data, hint=0x18c7854):
|
|
off, major, minor, bsz, comp, bytes_used, end = find_squashfs(data)
|
|
print(f"[sqfs] offset={hex(off)} v{major}.{minor} blocksize={bsz} "
|
|
f"comp_id={comp} bytes_used={hex(bytes_used)} end={hex(end)} "
|
|
f"(img_size={hex(len(data))})")
|
|
out = f"{WORK}/rootfs.squashfs"
|
|
open(out, "wb").write(data[off:end])
|
|
print(f"[sqfs] geschrieben: {out}")
|
|
return off, end
|
|
|
|
|
|
def parse_fdt(data, base):
|
|
"""Parst einen FDT-Blob (Offsets sind blob-relativ); liefert props und Totalsize."""
|
|
(magic, totalsize, off_struct, off_strings, off_rsv, ver, lastcomp,
|
|
cpuid, sz_strings, sz_struct) = struct.unpack_from('>10I', data, base)
|
|
blob = data[base:base + totalsize]
|
|
props = []
|
|
path, stack = '', []
|
|
p = off_struct
|
|
while True:
|
|
(tok,) = struct.unpack_from('>I', blob, p)
|
|
p += 4
|
|
if tok == 1: # BEGIN_NODE
|
|
end = blob.index(b'\x00', p)
|
|
name = blob[p:end].decode()
|
|
p = (end + 4) & ~3
|
|
stack.append(path)
|
|
path = name if path == '' else path + '/' + name
|
|
elif tok == 2: # END_NODE
|
|
path = stack.pop()
|
|
elif tok == 3: # PROP
|
|
(plen,) = struct.unpack_from('>I', blob, p)
|
|
(nameoff,) = struct.unpack_from('>I', blob, p + 4)
|
|
val = blob[p + 8:p + 8 + plen]
|
|
p = (p + 8 + plen + 3) & ~3
|
|
nstart = off_strings + nameoff
|
|
pname = blob[nstart:blob.index(b'\x00', nstart)].decode()
|
|
props.append((path, pname, val))
|
|
elif tok in (4, 9): # NOP/END
|
|
if tok == 4:
|
|
break
|
|
else:
|
|
# manche FIT-Generatoren lassen das FDT_END-Token weg;
|
|
# wir enden toleriert am Struct-Block-Ende
|
|
print(f"[fdt] Hinweis: Struct-Block ohne FDT_END (Token {tok} @ {hex(p)}); ok")
|
|
break
|
|
return props, totalsize
|
|
|
|
|
|
def carve_fit(data, start_scan, end_scan):
|
|
"""Findet das FIT (kernel@1 + irgendein fdt@*) und legt kernel.raw / dtb ab."""
|
|
hits = []
|
|
s = start_scan
|
|
while True:
|
|
i = data.find(FIT_MAGIC, s, end_scan)
|
|
if i < 0:
|
|
break
|
|
hits.append(i)
|
|
s = i + 4
|
|
print(f"[fit] FDT-Magic-Hits im Scanbereich: {[hex(h) for h in hits]}")
|
|
for base in hits:
|
|
try:
|
|
props, totalsize = parse_fdt(data, base)
|
|
except Exception as e:
|
|
print(f"[fit] skip {hex(base)}: {e}")
|
|
continue
|
|
paths = {p for p, n, v in props}
|
|
if not any(p.endswith('/kernel@1') for p in paths):
|
|
continue
|
|
# fdt-Knoten generisch: w2044ax bevorzugt, sonst das erste fdt@*
|
|
fdt_paths = [p for p in paths
|
|
if re.match(r'^images/fdt@[\w.-]+$', p)]
|
|
fdt_pick = next((p for p in fdt_paths if 'w2044ax' in p),
|
|
fdt_paths[0] if fdt_paths else None)
|
|
print(f"[fit] Kandidat @ {hex(base)} totalsize={hex(totalsize)} "
|
|
f"fdt={fdt_pick}")
|
|
meta = []
|
|
kdata = fdata = None
|
|
fdt_name = 'dtb'
|
|
for p, n, v in props:
|
|
if n in ('load', 'entry', 'timestamp') and len(v) == 4:
|
|
meta.append((p, n, hex(struct.unpack('>I', v)[0])))
|
|
elif n in ('description', 'type', 'arch', 'compression', 'algo', 'os') and len(v) < 64:
|
|
meta.append((p, n, v.rstrip(b'\x00').decode('latin1')))
|
|
if p.endswith('/kernel@1') and n == 'data':
|
|
kdata = v
|
|
if fdt_pick and p == fdt_pick and n == 'data':
|
|
fdata = v
|
|
fdt_name = fdt_pick.rsplit('/', 1)[-1].replace('fdt@', '')
|
|
for m in meta:
|
|
print(f"[fit] {m[0]}: {m[1]} = {m[2]}")
|
|
open(f'{WORK}/fit.img', 'wb').write(data[base:base + totalsize])
|
|
print(f"[fit] geschrieben: {WORK}/fit.img ({totalsize} bytes)")
|
|
if kdata:
|
|
open(f'{WORK}/kernel.raw', 'wb').write(kdata)
|
|
print(f"[fit] kernel@1 data: {len(kdata)} bytes -> {WORK}/kernel.raw")
|
|
if fdata:
|
|
open(f'{WORK}/{fdt_name}.dtb', 'wb').write(fdata)
|
|
print(f"[fit] {fdt_pick} data: {len(fdata)} bytes -> {WORK}/{fdt_name}.dtb")
|
|
return kdata, fdata
|
|
return None, None
|
|
|
|
|
|
def to_boot_image(kdata):
|
|
"""Dekomprimiert kernel.raw falls noetig; ARM64-Image direkt durchreichen.
|
|
(ARM64-Image-Magic 'ARM\\x64' liegt bei Offset 0x38.)"""
|
|
if kdata is None:
|
|
print("[kernel] kein kernel@1 im FIT -> nur RootFS extrahiert; FIT-Analyse faellt aus")
|
|
return
|
|
def is_arm64(b):
|
|
return len(b) > 0x3c and b[0x38:0x3c] == ARM64_IMG_MAGIC
|
|
if is_arm64(kdata):
|
|
open(f'{WORK}/kernel.img', 'wb').write(kdata)
|
|
print(f"[kernel] unkomprimiertes ARM64 Image -> {WORK}/kernel.img")
|
|
return
|
|
if kdata[:2] == b'\x1f\x8b':
|
|
out = gzip.decompress(kdata)
|
|
if is_arm64(out):
|
|
open(f'{WORK}/kernel.img', 'wb').write(out)
|
|
print(f"[kernel] gzip -> {len(out)} bytes ARM64 Image -> {WORK}/kernel.img")
|
|
return
|
|
print(f"[kernel] WARNUNG: unbekanntes Format {kdata[:8].hex()}; roh als {WORK}/kernel.img")
|
|
open(f'{WORK}/kernel.img', 'wb').write(kdata)
|
|
|
|
|
|
def main():
|
|
src = sys.argv[1]
|
|
os.makedirs(WORK, exist_ok=True)
|
|
data = open(src, 'rb').read()
|
|
print(f"[img] {src}: {len(data)} bytes")
|
|
sq_off, sq_end = carve_squashfs(data)
|
|
k, f = carve_fit(data, 0x1000, sq_off)
|
|
to_boot_image(k)
|
|
if f:
|
|
print("[dtb] OK")
|
|
else:
|
|
print("[dtb] WARNUNG: kein fdt@* im FIT gefunden")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|