Scaffold: licenses, Makefile, README, PLAN w/ decision function; extractor parses container + hidden build-string trailer; findings log F1-F4 + unknowns registry
This commit is contained in:
commit
b2ac192df2
8 changed files with 472 additions and 0 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Blob-derived outputs: carved firmware bytes must never be committed
|
||||
extracted*/
|
||||
out/
|
||||
firmware/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# RE tooling state
|
||||
.ghidra/
|
||||
*.rep
|
||||
*.gpr
|
||||
*.id*
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Zachery Aaron Shores-Chmielewski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
12
Makefile
Normal file
12
Makefile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
BLOBS ?= $(wildcard /lib/firmware/mediatek/mt79*_wm.bin) \
|
||||
$(wildcard /lib/firmware/mediatek/mt79*_wa.bin) \
|
||||
$(wildcard /lib/firmware/mediatek/mt79*_rom_patch.bin)
|
||||
|
||||
.PHONY: help extract
|
||||
|
||||
help:
|
||||
@echo "make extract - carve all MediaTek Connac2 blobs found on this host"
|
||||
@echo " (set BLOBS=... to override)"
|
||||
|
||||
extract:
|
||||
python3 tools/mtk_fw_extract.py $(BLOBS)
|
||||
140
PLAN.md
Normal file
140
PLAN.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# mtk-wifi-fw (working name) — Project Plan
|
||||
|
||||
Goal: cut teeth on real firmware RE while producing community-useful tooling,
|
||||
docs, and update tracking for MediaTek Connac2 WiFi firmware. The product is
|
||||
the tooling/knowledge and the skills built making it — not a modified router.
|
||||
|
||||
## Target facts (verified, keep cited)
|
||||
|
||||
- Hardware: GL-MT3000 (Beryl AX), MT7981B (Filogic 820), OpenWrt 24.10.5.
|
||||
Radios on-SoC at `platform/soc/18000000.wifi`, driver `mt7915e` (mt76).
|
||||
- "The blob" = 4 images, 3 NDS32LE cores inside the SoC:
|
||||
- `mt7981_wm.bin` 2.05MB — WM, main MAC/PHY MCU ("neptune", build 20240823)
|
||||
- `mt7981_wa.bin` 494KB — WA, offload/stats MCU
|
||||
- `mt7981_wo.bin` 2.45MB — WED packet offload core (FreeRTOS v10, loaded by
|
||||
`mtk_wed`, not mt76 — separate container format, TODO)
|
||||
- `mt7981_rom_patch.bin` 9.8KB — boot-ROM patch ("ALPS")
|
||||
- NDS32LE evidence: `GCC: (2018-10-23_nds32le-elf-mculib-v3) 4.9.4` in wa;
|
||||
`portable/GCC/NDS32/port.c` in wo.
|
||||
- No signature/encryption wall: loader is the kernel driver
|
||||
(`mt7915_load_firmware` → `mt76_connac2_load_patch` + `load_ram`); protocol
|
||||
supports per-region encrypt (`FW_FEATURE_SET_ENCRYPT`) but blobs ship
|
||||
plaintext. Source-verified; empirical proof = M3 milestone.
|
||||
- Container formats (from `mt76_connac_mcu.h`):
|
||||
- RAM images: region data from offset 0, n×40B region table, 36B trailer at
|
||||
EOF. `fw_ver` stripped (`____000000`); build_date is the only versioning.
|
||||
- Patch: 92B BE header + 64B BE section table at start.
|
||||
- Family coverage: mt7915/7916/7981/7986 all parse. mt7915 blobs = 2022
|
||||
build, others = 2024 — free pilot for version diffing.
|
||||
- Tooling: Ghidra has an official NDS32 module (`NDS32:LE:32:default`, some
|
||||
V3 instr gaps); Andes GNU toolchain public (exact 2018 GCC vintage exists).
|
||||
- Prior art: cyrozap/mediatek-wifi-re (older chip gen; CC-BY-SA notes,
|
||||
compatible). No public RE of Connac2-generation firmware exists.
|
||||
|
||||
## Strategy shape
|
||||
|
||||
RE first, fork later. Nobody can price replacement firmware before the boot
|
||||
and core-boundary picture exists, so the plan front-loads cheap-certain RE and
|
||||
gates expensive authoring behind a data-informed go/no-go.
|
||||
|
||||
- Phase 1 static RE: carve → ELF → Ghidra → boot/ABI map (no decision needed)
|
||||
- Phase 2 dynamic RE: patch bytes, reload, observe (patching = verification
|
||||
instrument, no decision needed)
|
||||
- Fork point (after boot/handshake + WM↔WA boundary mapped): choose between
|
||||
a) audit & document track, and/or
|
||||
b) authoring ladder, easiest-first: custom WO (self-contained, no radio)
|
||||
→ WM handshake-only → beacon TX → association → data path.
|
||||
Each rung independently valuable/stoppable.
|
||||
- Version diffing runs parallel throughout (pure static, high community value).
|
||||
|
||||
## MVP v0.1 — "toolkit + changelog" (no hardware required, ~8–12 evenings)
|
||||
|
||||
Scope: extract (family-wide, correct) + to-elf + diff report over
|
||||
linux-firmware git history + format/boot docs + CI cron that tracks upstream
|
||||
blob drops. The repo becomes the public changelog for silently-updated
|
||||
firmware.
|
||||
|
||||
Worklist in order:
|
||||
1. Repo scaffolding: git init, dirs, dual LICENSE (MIT code / CC-BY-SA
|
||||
docs), Makefile; commit existing extractor.
|
||||
2. Parser correctness: resolve or document-as-unknown the 100/36B gap,
|
||||
region `type` semantics, feature bit 0x80.
|
||||
3. Test fixtures: golden manifests per blob (metadata only, no blob bytes).
|
||||
4. ELF emitter at true load addresses (Ghidra/objdump acceptance test).
|
||||
5. Dataset: walk linux-firmware git history for mediatek WiFi files.
|
||||
6. Diff engine + pilot report (mt7915 2022 vs 2024; string-table add/remove
|
||||
is the human-readable signal).
|
||||
7. Docs: format.md (every field cited), boot.md, README quickstart.
|
||||
8. CI: fixtures on commit; weekly cron vs linux-firmware HEAD regenerating
|
||||
reports.
|
||||
9. Polish, tag v0.1.0, undersold announcement (OpenWrt forum + r/re).
|
||||
|
||||
Not in v0.1: Ghidra annotation projects, symbol DBs, patched blobs, anything
|
||||
router-dependent.
|
||||
|
||||
## Quality gates ("not noise")
|
||||
|
||||
- Every claim cites evidence (kernel line, blob offset) or is marked unknown.
|
||||
No invented semantics.
|
||||
- Family coverage verified in CI, not asserted in README.
|
||||
- Reproducible in ~3 commands, plain Linux, no blobs committed
|
||||
(`make fetch-fw` pulls from kernel.org).
|
||||
- Undersell announcements.
|
||||
|
||||
## Status log
|
||||
|
||||
- 2026-08-20: Extractor `tools/mtk_fw_extract.py` written and run on
|
||||
mt7981/7915/7916/7986 wm+wa+patch (7 files, 4 chips) — all parse clean.
|
||||
Output in `extracted*/`, `extracted/manifest.json`. Solved same day: the
|
||||
"gap" is a second trailer family-wide (wm 100B, wa 36B) sitting between
|
||||
region data and the kernel-parsed trailer; WM's carries the full build
|
||||
string MTK strips from the kernel-visible trailer (branch, MP tag, build
|
||||
timestamp) — recovered version metadata for the changelog. Region type:
|
||||
all parsed blobs are FW_TYPE_DEFAULT(0); loader does use `type` elsewhere
|
||||
(mt7921 FW_TYPE_CLC). Remaining unknowns: feature bit 0x80 (wm 0xf0xxxxxx
|
||||
regions), hidden-trailer non-string fields. Verified claims + citations:
|
||||
docs/findings.md.
|
||||
- Router unreachable (thinkpad jump host down). Not blocking: v0.1 is pure
|
||||
static. Hardware options when needed: fix thinkpad / wire devuan-hpz to
|
||||
router LAN / join `zach` WiFi from devuan-hpz (key + likely MAC-whitelisted,
|
||||
but changes this machine's network path mid-session).
|
||||
|
||||
## Open items
|
||||
|
||||
- Repo name + hosting (GitHub reach vs Codeberg/self-hosted Forgejo;
|
||||
mirroring makes this non-blocking).
|
||||
- Router access path (needed from M3/dynamic phase).
|
||||
- Fork decision (audit vs authoring ladder mix) — after boot/boundary map.
|
||||
- WO container format (from `mtk_wed_mcu.c`, different loader) — M2-ish.
|
||||
|
||||
## Decision function & finish criteria
|
||||
|
||||
Project has no natural completion point; finish = satisficing set + per-node
|
||||
gates. All checks binary.
|
||||
|
||||
**Finish conditions (both required):**
|
||||
- F1 public artifact: v0.1 shipped passing quality gates, then one of
|
||||
community uptake / cron keeps it alive / documented sunset ("last verified
|
||||
against linux-firmware X"). Silent rot is noise; sunset is finish.
|
||||
- F2 teeth cut, each backed by an artifact: container parsed (done); one
|
||||
*verified cited* claim from disassembly; changed firmware behavior and
|
||||
observed it on hardware (P2); optionally self-written code on target.
|
||||
|
||||
**Universal gate (every checkpoint):** advance iff next gate is reachable
|
||||
with known techniques AND has a stated cost ceiling AND its failure would not
|
||||
invalidate shipped artifacts. Slipped >2x twice → stop-and-replan. Outputs
|
||||
ship as verified or documented-unknown, never "probably"; unknowns are valid
|
||||
P1 exits but never authoring foundations.
|
||||
|
||||
**Node exits:** MVP = worklist 1–9 + quality gates. P1 = boot chain writable
|
||||
step-by-step with citations; ≥30 functions named via dispatch anchors, 3
|
||||
traced end-to-end; memory map complete; WM↔WA characterized; fork memo.
|
||||
Authoring rung N+1 opens iff rung N demonstrated on hardware AND N+1
|
||||
interface mapped AND unbrick path proven.
|
||||
|
||||
**Kill criteria:** 3 consecutive flash cycles with no hypothesis advance →
|
||||
rung closed, findings published. ROM-verified lockout discovered → authoring
|
||||
dead, audit track unaffected. Router unreachable >4 weeks → P2 paused only.
|
||||
|
||||
**Zombie rule:** no commits and no CI activity for 2 quarters → archive with
|
||||
last-verified statement.
|
||||
23
README.md
Normal file
23
README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# mtk-wifi-fw (working name)
|
||||
|
||||
Tooling, documentation, and update tracking for MediaTek Connac2 WiFi
|
||||
firmware blobs (`mt7915/7916/7981/7986_{wm,wa,rom_patch}.bin` as shipped in
|
||||
linux-firmware) — the WiFi firmware running on Filogic 820/830 routers
|
||||
(GL.iNet, Banana Pi, Xiaomi, Netgear, TP-Link, ...).
|
||||
|
||||
Status: pre-release. Working now: container extractor (`tools/mtk_fw_extract.py`).
|
||||
Planned: ELF export for Ghidra/objdump, cross-version diff reports, CI that
|
||||
tracks upstream blob updates, format + boot-architecture documentation.
|
||||
|
||||
No proprietary firmware bytes are committed to this repository. Tools operate
|
||||
on blobs from your own linux-firmware copy (see `make extract`).
|
||||
|
||||
Quickstart:
|
||||
|
||||
make extract # carve every MediaTek Connac2 blob on this host
|
||||
python3 tools/mtk_fw_extract.py /lib/firmware/mediatek/mt7981_wm.bin -o extracted
|
||||
|
||||
Project plan and working notes: [PLAN.md](PLAN.md) (development log, quality
|
||||
gates, decision function). Documentation as it lands: [docs/](docs/).
|
||||
|
||||
License: MIT (code), CC-BY-SA-4.0 (docs).
|
||||
13
docs/LICENSE-DOCS
Normal file
13
docs/LICENSE-DOCS
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Documentation in the docs/ directory (including format specifications,
|
||||
architecture notes, and reports) is licensed under the Creative Commons
|
||||
Attribution-ShareAlike 4.0 International License (CC-BY-SA-4.0).
|
||||
|
||||
You should have received a copy of the license along with this work; if not,
|
||||
see: https://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
Copyright (c) 2026 Zachery Aaron Shores-Chmielewski
|
||||
|
||||
Note: derived works must carry attribution; CC-BY-SA is chosen for
|
||||
compatibility with prior art (cyrozap/mediatek-wifi-re notes).
|
||||
|
||||
Code in tools/ and tests/ is MIT-licensed; see the repository root LICENSE.
|
||||
88
docs/findings.md
Normal file
88
docs/findings.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Findings log
|
||||
|
||||
One entry per claim. Every claim carries evidence (kernel source reference or
|
||||
blob offset + observed bytes). Claims that cannot be evidenced are registered
|
||||
in the Unknowns section instead. Blobs referenced by filename are the
|
||||
linux-firmware copies listed in `manifest.json` (SHA-256 TBD in fixtures).
|
||||
|
||||
Blobs observed: mt7981/mt7915/mt7916/mt7986 × {wm, wa, rom_patch}
|
||||
(linux-firmware snapshot 2026-04-20, Debian).
|
||||
|
||||
## F1 — Connac2 RAM container layout
|
||||
|
||||
`[region data, packed from offset 0 in table order][optional hidden trailer
|
||||
(see F2)][n_region × 40-byte region table][36-byte trailer at EOF]`
|
||||
|
||||
Evidence: `mt76_connac_mcu.c` `mt76_connac2_load_ram()` +
|
||||
`mt76_connac2_send_ram()` (region data streamed sequentially from offset 0;
|
||||
table and trailer located from EOF); `mt76_connac_mcu.h`
|
||||
`struct mt76_connac2_fw_region` / `_fw_trailer`. Confirmed by exact layout
|
||||
match on 8 RAM blobs (sum(region.len) + table + trailer (+ hidden trailer) ==
|
||||
file size).
|
||||
|
||||
Region fields: `decomp_crc/decomp_len/decomp_blk_sz` (MCU-side decompression,
|
||||
unused when decomp_len==0 — all observed blobs), `addr`, `len`,
|
||||
`feature_set`, `type`.
|
||||
|
||||
## F2 — Hidden second trailer carries the full build string
|
||||
|
||||
Between region data and the kernel-parsed table sits a trailer the upstream
|
||||
loader never reads. WM blobs: 100 bytes; WA blobs: 36 bytes (no string).
|
||||
Observed family-wide (all 4 chips).
|
||||
|
||||
WM hidden trailer = 16×'#' + `40 00 00 00` + 1-byte string length + flags +
|
||||
full build string + '#'-padding. Recovered strings:
|
||||
|
||||
- mt7981_wm: `t-neptune-main-mt7915-1953-MT7981_MP2111_IMP-20240823161204`
|
||||
- mt7916_wm: `t-neptune-main-mt7915-1953-MT7916_MP2111_IMP-20240823170147`
|
||||
- mt7986_wm: `t-neptune-main-mt7915-1953-MT7986_MP2111_IMP-20240823160608`
|
||||
- mt7915_wm: `t-neptune-mp-mt7915-2045-MT7915_MP_7_4_2045-20220929103802`
|
||||
|
||||
Significance: the kernel-visible trailer's `fw_ver[10]` is stripped
|
||||
(`____000000` WM / `DEV_000000` WA); the hidden trailer restores branch
|
||||
("main"/"mp"), internal build number (1953/2045), MP tag, and build
|
||||
timestamp. This is the version metadata the changelog tooling keys on.
|
||||
|
||||
Evidence: bytes at offset `sum(region.len)` in each blob (see
|
||||
`manifest.json` → `hidden_trailer.raw_head`).
|
||||
|
||||
## F3 — region `type` is a content tag used by some loaders
|
||||
|
||||
All parsed mt79-family blobs: `type == 0` (`FW_TYPE_DEFAULT`). It is not
|
||||
ignored upstream: mt7921's loader selects
|
||||
`feature_set & FW_FEATURE_NON_DL && type == FW_TYPE_CLC` regions as
|
||||
country/location-calibration data instead of downloading them to the MCU.
|
||||
|
||||
Evidence: `mt7921/mcu.c` CLC handling; `FW_TYPE_CLC` definition in
|
||||
`mt76_connac_mcu.h`.
|
||||
|
||||
## F4 — Patch container format
|
||||
|
||||
92-byte big-endian header (16-byte build date, 4-byte platform tag —
|
||||
"ALPS" on all four — hw_sw_ver, patch_ver, checksum, descriptor), 64-byte BE
|
||||
section table, plaintext section data at explicit offsets. All observed
|
||||
patch sections: `enc_type == 0` (plaintext).
|
||||
|
||||
Evidence: `mt76_connac2_load_patch()` + `struct mt76_connac2_patch_hdr/_sec`
|
||||
in `mt76_connac_mcu.h`; exact parse of 4 patch blobs.
|
||||
|
||||
## Unknowns registry
|
||||
|
||||
- **U1 — `feature_set` bit 7 (0x80):** observed only on WM regions at
|
||||
`0xf0xxxxxx` addresses (mt7981 r7–r10, mt7986 equivalents). Upstream
|
||||
defines bits 0–6 only; vendor-SDK search found no definition (2026-08-20).
|
||||
Correlation with address family noted; semantics undetermined. May relate
|
||||
to a second download destination or content class.
|
||||
- **U2 — hidden-trailer non-string fields:** the `40 00 00 00` and flag
|
||||
bytes before/around the string (F2) are uninterpreted.
|
||||
- **U3 — WO container:** `mt7981_wo.bin` uses the mtk_wed loader
|
||||
(`mtk_wed_mcu.c`), a different format; not yet parsed.
|
||||
|
||||
## Related verified facts
|
||||
|
||||
- NDS32LE target: `GCC: (2018-10-23_nds32le-elf-mculib-v3) 4.9.4` string in
|
||||
mt7981_wa region data; FreeRTOS v10.0.0 + `portable/GCC/NDS32/port.c`
|
||||
strings in mt7981_wo.
|
||||
- No per-region encryption in any observed blob (`FW_FEATURE_SET_ENCRYPT`
|
||||
clear; patch `enc_type == 0`). Optional protocol support exists in the
|
||||
loader (key from EFUSE path) but is not exercised by shipped images.
|
||||
161
tools/mtk_fw_extract.py
Executable file
161
tools/mtk_fw_extract.py
Executable file
|
|
@ -0,0 +1,161 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue