162 lines
6.3 KiB
Python
162 lines
6.3 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Carve MediaTek Connac2 WiFi firmware containers.
|
||
|
|
|
||
|
|
Layouts transcribed from Linux:
|
||
|
|
drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
|
||
|
|
(struct mt76_connac2_fw_trailer / fw_region / patch_hdr / patch_sec)
|
||
|
|
|
||
|
|
RAM images (mt7981_wm.bin, mt7981_wa.bin): raw region data packed from
|
||
|
|
offset 0, optional hidden second trailer (100B WM / 36B WA; the WM one
|
||
|
|
carries the full build string), then n_region * 40-byte region table,
|
||
|
|
then 36-byte trailer at EOF. Patch image (mt7981_rom_patch.bin): 92-byte BE
|
||
|
|
header, 64-byte BE section table, section data at explicit offsets.
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import math
|
||
|
|
import re
|
||
|
|
import struct
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
TRAILER = struct.Struct('<5B2s10s15sI') # 36 bytes
|
||
|
|
REGION = struct.Struct('<III4sIIBB14s') # 40 bytes
|
||
|
|
PATCH_HDR = struct.Struct('>16s4sIIHH5I11I') # 92 bytes
|
||
|
|
PATCH_SEC = struct.Struct('>III13I') # 64 bytes
|
||
|
|
|
||
|
|
FEAT_BITS = {0: 'ENCRYPT', 4: 'ENCRY_MODE', 5: 'OVERRIDE_ADDR', 6: 'NON_DL'}
|
||
|
|
|
||
|
|
|
||
|
|
def entropy(data: bytes) -> float:
|
||
|
|
if not data:
|
||
|
|
return 0.0
|
||
|
|
freq = [0] * 256
|
||
|
|
for b in data:
|
||
|
|
freq[b] += 1
|
||
|
|
n = len(data)
|
||
|
|
return -sum((c / n) * math.log2(c / n) for c in freq if c)
|
||
|
|
|
||
|
|
|
||
|
|
def feature_names(feat: int):
|
||
|
|
names = [name for bit, name in FEAT_BITS.items() if feat & (1 << bit)]
|
||
|
|
if feat & 0b00000110:
|
||
|
|
names.append(f'KEY_IDX={(feat >> 1) & 3}')
|
||
|
|
return names
|
||
|
|
|
||
|
|
|
||
|
|
def carve_ram(path: Path, outdir: Path) -> dict:
|
||
|
|
d = path.read_bytes()
|
||
|
|
t = TRAILER.unpack_from(d, len(d) - TRAILER.size)
|
||
|
|
chip_id, eco, n_region, fmt_ver, fmt_flag, _rsv, fw_ver, bdate, crc = t
|
||
|
|
info = {
|
||
|
|
'file': str(path), 'format': 'connac2-ram',
|
||
|
|
'chip_id': chip_id, 'eco': eco, 'n_region': n_region,
|
||
|
|
'format_ver': fmt_ver, 'format_flag': fmt_flag,
|
||
|
|
'fw_ver': fw_ver.decode(errors='replace'),
|
||
|
|
'build_date': bdate.decode(errors='replace'),
|
||
|
|
'trailer_crc': f'{crc:08x}',
|
||
|
|
'size': len(d),
|
||
|
|
}
|
||
|
|
print(f"{path.name}: chip=0x{chip_id:02x} eco={eco} n_region={n_region} "
|
||
|
|
f"ver='{info['fw_ver']}' date='{info['build_date']}' crc=0x{crc:08x}")
|
||
|
|
regions = []
|
||
|
|
off = 0
|
||
|
|
table_base = len(d) - TRAILER.size - n_region * REGION.size
|
||
|
|
for i in range(n_region):
|
||
|
|
r = REGION.unpack_from(d, table_base + i * REGION.size)
|
||
|
|
decomp_crc, decomp_len, blk, _r, addr, ln, feat, typ, _r1 = r
|
||
|
|
data = d[off:off + ln]
|
||
|
|
fn = outdir / f'r{i}_t{typ}_a{addr:08x}.bin'
|
||
|
|
fn.write_bytes(data)
|
||
|
|
reg = {
|
||
|
|
'idx': i, 'type': typ, 'addr': f'0x{addr:08x}', 'len': ln,
|
||
|
|
'compressed': bool(decomp_len), 'decomp_len': decomp_len,
|
||
|
|
'decomp_blk_sz': blk, 'decomp_crc': f'{decomp_crc:08x}',
|
||
|
|
'feature_set': f'0x{feat:02x}',
|
||
|
|
'features': feature_names(feat),
|
||
|
|
'entropy': round(entropy(data[:65536]), 2),
|
||
|
|
'file': fn.name,
|
||
|
|
}
|
||
|
|
print(f" r{i}: type={typ} addr={reg['addr']} len={ln:>8} "
|
||
|
|
f"comp={int(reg['compressed'])} feat={reg['feature_set']}"
|
||
|
|
f"{(' [' + ','.join(reg['features']) + ']') if reg['features'] else ''} "
|
||
|
|
f"entropy={reg['entropy']:.2f}")
|
||
|
|
regions.append(reg)
|
||
|
|
off += ln
|
||
|
|
gap = len(d) - off - n_region * REGION.size - TRAILER.size
|
||
|
|
info['regions'] = regions
|
||
|
|
if gap > 0:
|
||
|
|
# Second trailer between region data and the kernel-parsed table.
|
||
|
|
# Observed family-wide: 100B on WM blobs (carries the full build
|
||
|
|
# string stripped from the kernel-visible trailer), 36B on WA blobs
|
||
|
|
# (no string). Layout: 16 x '#', 4-byte fields, optional
|
||
|
|
# length-prefixed version string, '#'-padded to size.
|
||
|
|
g = d[off:off + gap]
|
||
|
|
runs = [r.strip(b'#') for r in re.findall(rb'[\x20-\x7e]{8,}', g)]
|
||
|
|
runs = [r for r in runs if len(r) >= 8]
|
||
|
|
hidden = {'size': gap, 'string': runs[0].decode() if runs else None,
|
||
|
|
'raw_head': f'{g[:24].hex()}'}
|
||
|
|
info['hidden_trailer'] = hidden
|
||
|
|
print(f" hidden trailer: {gap}B ver='{hidden['string']}'")
|
||
|
|
return info
|
||
|
|
|
||
|
|
|
||
|
|
def carve_patch(path: Path, outdir: Path) -> dict:
|
||
|
|
d = path.read_bytes()
|
||
|
|
h = PATCH_HDR.unpack_from(d, 0)
|
||
|
|
bdate, plat, hw_sw, pver, cksum, _rsv, dver, subsys, feat, n_region, dcrc = h[:11]
|
||
|
|
info = {
|
||
|
|
'file': str(path), 'format': 'connac2-patch',
|
||
|
|
'platform': plat.decode(errors='replace'),
|
||
|
|
'build_date': bdate.decode(errors='replace'),
|
||
|
|
'hw_sw_ver': f'0x{hw_sw:08x}', 'patch_ver': f'0x{pver:08x}',
|
||
|
|
'checksum': f'0x{cksum:04x}', 'desc_patch_ver': f'0x{dver:08x}',
|
||
|
|
'subsys': f'0x{subsys:08x}', 'feature': f'0x{feat:08x}',
|
||
|
|
'n_region': n_region, 'desc_crc': f'{dcrc:08x}', 'size': len(d),
|
||
|
|
}
|
||
|
|
print(f"{path.name}: platform={info['platform']} date='{info['build_date']}' "
|
||
|
|
f"hw_sw={info['hw_sw_ver']} patch_ver={info['patch_ver']} n_region={n_region}")
|
||
|
|
secs = []
|
||
|
|
for i in range(n_region):
|
||
|
|
s = PATCH_SEC.unpack_from(d, PATCH_HDR.size + i * PATCH_SEC.size)
|
||
|
|
typ, offs, size = s[0], s[1], s[2]
|
||
|
|
addr, ln, key_idx, align = s[3:7]
|
||
|
|
enc_type = (key_idx >> 24) & 0xff
|
||
|
|
data = d[offs:offs + size]
|
||
|
|
fn = outdir / f's{i}_a{addr:08x}.bin'
|
||
|
|
fn.write_bytes(data)
|
||
|
|
sec = {
|
||
|
|
'idx': i, 'type': f'0x{typ:x}', 'offset': offs, 'size': size,
|
||
|
|
'addr': f'0x{addr:08x}', 'len': ln,
|
||
|
|
'enc_type': enc_type, 'key': key_idx & 0xff,
|
||
|
|
'align_len': align, 'entropy': round(entropy(data), 2),
|
||
|
|
'file': fn.name,
|
||
|
|
}
|
||
|
|
print(f" s{i}: type={sec['type']} addr={sec['addr']} len={ln:>8} "
|
||
|
|
f"enc={enc_type} key={sec['key']} entropy={sec['entropy']:.2f}")
|
||
|
|
secs.append(sec)
|
||
|
|
info['sections'] = secs
|
||
|
|
return info
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument('blobs', nargs='+')
|
||
|
|
ap.add_argument('-o', '--out', default='extracted')
|
||
|
|
args = ap.parse_args()
|
||
|
|
outdir = Path(args.out)
|
||
|
|
manifest = []
|
||
|
|
for blob in args.blobs:
|
||
|
|
p = Path(blob)
|
||
|
|
d = outdir / p.stem
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
if 'patch' in p.name:
|
||
|
|
manifest.append(carve_patch(p, d))
|
||
|
|
else:
|
||
|
|
manifest.append(carve_ram(p, d))
|
||
|
|
(outdir / 'manifest.json').write_text(json.dumps(manifest, indent=2))
|
||
|
|
print(f"wrote {outdir / 'manifest.json'}")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|