#!/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(' 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()