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