106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""ffu_parse.py — разбор FFU v1 (Lumia): security header, manifest, GPT, store.
|
|
Использование: ./tools/ffu_parse.py fw/dl/XXX.ffu [--gpt] [--manifest]
|
|
WIP: только чтение, без проверки подписей/hashtable.
|
|
"""
|
|
import struct
|
|
import sys
|
|
import uuid
|
|
import mmap
|
|
|
|
ACC = {0: "BEGIN", 1: "SEQ", 2: "END"}
|
|
|
|
|
|
def guid_le(b):
|
|
return str(uuid.UUID(bytes_le=b))
|
|
|
|
|
|
def parse(path):
|
|
raw = open(path, "rb")
|
|
f = mmap.mmap(raw.fileno(), 0, access=mmap.ACCESS_READ)
|
|
sec = f.read(32)
|
|
cb, sig, chunk_kb, alg, cat_sz, hash_sz = struct.unpack("<I12sIIII", sec)
|
|
assert sig == b"SignedImage ", "not an FFU"
|
|
chunk = chunk_kb * 1024
|
|
off = 32 + cat_sz + hash_sz
|
|
off += (chunk - off % chunk) % chunk
|
|
f.seek(off)
|
|
ih = f.read(24)
|
|
mlen, csize = struct.unpack("<II", ih[16:24])
|
|
man = f.read(mlen).decode("ascii", errors="replace")
|
|
hdr_size = off + 24 + mlen
|
|
hdr_size += (chunk - hdr_size % chunk) % chunk
|
|
info = {
|
|
"chunk": chunk, "catalog": cat_sz, "hashes": hash_sz // 32,
|
|
"manifest_len": mlen, "header_size": hdr_size,
|
|
}
|
|
for tag in ["DevicePlatformId0", "OSVersion", "AntiTheftVersion",
|
|
"SectorSize", "MinSectorCount"]:
|
|
import re
|
|
m = re.search(tag + r"\s*=\s*([^\r\n]+)", man)
|
|
info[tag] = m.group(1).strip() if m else "?"
|
|
# store header
|
|
f.seek(hdr_size)
|
|
sh = f.read(264)
|
|
bs, wdc = struct.unpack("<I", sh[224:228])[0], struct.unpack("<I", sh[228:232])[0]
|
|
info["block_size"] = bs
|
|
descs = []
|
|
for _ in range(wdc):
|
|
loc_cnt, blk_cnt = struct.unpack("<II", f.read(8))
|
|
locs = [struct.unpack("<II", f.read(8)) for _ in range(loc_cnt)]
|
|
descs.append((blk_cnt, locs))
|
|
info["write_descs"] = len(descs)
|
|
info["payload_blocks"] = sum(b for b, _ in descs)
|
|
# GPT lives somewhere in payload (sparse); find VALID header
|
|
gpt_off = -1
|
|
o = hdr_size
|
|
while True:
|
|
o = f.find(b"EFI PART", o)
|
|
if o < 0:
|
|
break
|
|
hdr = bytes(f[o:o + 92])
|
|
rev, hsz = struct.unpack("<II", hdr[8:16])
|
|
nent0, esz0 = struct.unpack("<II", hdr[80:88])
|
|
if rev == 0x10000 and hsz == 92 and nent0 == 128 and esz0 == 128:
|
|
gpt_off = o
|
|
break
|
|
o += 8
|
|
assert gpt_off > 0, "no valid GPT"
|
|
assert gpt_off > 0, "no GPT"
|
|
info["gpt_chunk"] = (gpt_off - hdr_size) // chunk
|
|
hdr = bytes(f[gpt_off:gpt_off + 92])
|
|
assert hdr[:8] == b"EFI PART", "no GPT"
|
|
nent, esz = struct.unpack("<II", hdr[80:88])
|
|
parts = []
|
|
ent_off = gpt_off + 512 # entries start at next sector after header
|
|
for _ in range(nent):
|
|
e = bytes(f[ent_off:ent_off + esz])
|
|
ent_off += esz
|
|
t, u = e[:16], e[16:32]
|
|
if t == b"\x00" * 16:
|
|
continue
|
|
lba0, lba1, attr = struct.unpack("<QQQ", e[32:56])
|
|
name = e[56:128].decode("utf-16-le").rstrip("\x00")
|
|
parts.append((name, lba0, lba1, guid_le(t)))
|
|
f.close()
|
|
return info, parts, man
|
|
|
|
|
|
def main():
|
|
path = sys.argv[1]
|
|
info, parts, man = parse(path)
|
|
for k, v in info.items():
|
|
if k != "write_descs":
|
|
print(f"{k}: {v}")
|
|
if "--manifest" in sys.argv:
|
|
print("---- manifest ----")
|
|
print(man)
|
|
if "--gpt" in sys.argv or True:
|
|
print(f"---- GPT: {len(parts)} partitions ----")
|
|
for name, a, b, t in parts:
|
|
print(f"{name:16s} LBA {a:10d}..{b:<10d} size {(b-a+1)*512//1024:7d}K {t}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|