diff --git a/.gitignore b/.gitignore index 9c5e326..3718155 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ __pycache__/ *.rep *.gpr *.id* +ghidra-proj/ diff --git a/PLAN.md b/PLAN.md index 31dbf21..5c63a76 100644 --- a/PLAN.md +++ b/PLAN.md @@ -107,6 +107,13 @@ router-dependent. ~95% changed, new RA/DPD/thermal strings), docs (format/boot), CI (push + weekly track), v0.1.0 tagged. Announcement drafted, not posted — hosting decision still open. +- 2026-08-20 (P1-B started): Ghidra 12.1.3 + JDK21 + PyGhidra pipeline + working (venv at ~/data/tools). Patch blob reversed to structure level: + 10-entry ROM function replacement table + 49 NDS32 functions decompiling + cleanly, assert anchors (2 source files w/ line numbers), N9 core name + confirmed, CCIF/mailbox/WDT host-comm hooks mapped (findings F5). + Full decomp kept out of repo (blob-derived); notes only. WA import + running. - Repo name + hosting: RESOLVED — mtk-wifi-fw at https://zachery.lol/code/zacheryasc/mtk-wifi-fw (self-hosted Forgejo 9, diff --git a/docs/findings.md b/docs/findings.md index 80e129c..e03b0b2 100644 --- a/docs/findings.md +++ b/docs/findings.md @@ -70,6 +70,45 @@ Section counts: mt7915 patch = 2 sections, all others = 1; section `type` constant `0x30002` on every observed section (loader downloads all sections regardless; field semantics otherwise unobserved). +## F5 — mt7981_rom_patch internal structure (first disassembly, 2026-08-20) + +The 9.7KB patch section (downloaded to 0x00900000) is a boot-ROM function +replacement table + code + strings, little-endian NDS32: + +| offset | content | +|---|---| +| 0x000 | `0x000003ff`, 0 | +| 0x008 | 10 LE pointers into boot ROM (`0x00801xxx–0x0082bxxx`) | +| 0x108 | 10 LE pointers into patch RAM (`0x009002xx–0x009012xx`) — replacements | +| ~0x1e0 | NDS32 code (Ghidra: 49 functions + entry marker) | +| 0x901000+ | string table | + +Decompilation is viable end-to-end (Ghidra 12 `NDS32:LE:32`, official +module; our ELF imports directly — deferred acceptance check now PASSED). +Evidence: `ghidra-proj/patch_decomp.txt` (unpublished, blob-derived). + +Verified observations: +- Assert anchors: `patch/wf/wm/sys_patch_mcu.c` (lines 0x426, 0x5de...), + `common/sys_patch_common_mcu.c` (0x20f, 0x248...) via ROM `func_0x008004b8`. +- 20+ distinct direct calls into boot ROM (`func_0x0080xxxx`) + GP-relative + indirect calls (`unaff_gp - 0x116xx`) — ROM provides the runtime library. +- Named-entry strings: `MCU_Patch_init`, `ENTRY_wsysMboxSendMsg`, + `ENTRY_wsysMboxRcvAllMsg` (mailbox IPC), `From_CCIF__host_cpu_sw_interrupt` + (CCIF = host↔MCU channel), `WDT_to_Host` / `WDT_to_N9` (confirms the MCU + core is called N9), `Patch_dic_handler_extend`, `WF_Lt_Sec_handler`. +- Diagnostics suite strings: `AXI_Bus_monitor_detect`, `APB_AHB_bus_timeout`, + `IDLM_monitor`, `CPU_UTLZ_CNT_*` (utilization counters), + `cache_miss_ratio`, register-dump prints (`0x8800_0430` etc.). +- `FUN_009002a6`: bus-register writer — LE dword writes to offsets + 0x110/0x114/0x118/0x11c on bus 5, RMW `| 0x400000c0`, busy-poll bit + `0x40000000`. `FUN_0090044e`: bounded delay loop via ROM timer reads + (`func_0x00801e20`/`func_0x00801e16`). + +Interpretation (labelled): the patch extends boot ROM with host-comm hooks +(CCIF interrupt, mailboxes, watchdog-to-host) and bus diagnostics, plus +download-plumbing register programming — the glue the ROM needs before +WM/WA firmware arrives. + ## Unknowns registry - **U1 — `feature_set` bit 7 (0x80):** observed only on WM regions at diff --git a/tools/ghidra_scripts/ExportOverview.py b/tools/ghidra_scripts/ExportOverview.py new file mode 100644 index 0000000..e89093a --- /dev/null +++ b/tools/ghidra_scripts/ExportOverview.py @@ -0,0 +1,36 @@ +# Ghidra headless post-script: dump function/strings/block overview. +# Usage: analyzeHeadless -process -noanalysis +# -scriptPath tools/ghidra_scripts -postScript ExportOverview.py +#@category Analysis + +from ghidra.program.model.symbol import SymbolType + +fm = currentProgram.getFunctionManager() +listing = currentProgram.getListing() + +funcs = fm.getFunctions(True) +n = 0 +print('=== FUNCTIONS ===') +for f in funcs: + print('%s %s' % (f.getEntryPoint(), f.getName())) + n += 1 +print('total functions: %d' % n) + +print('=== STRINGS (>=6 chars) ===') +di = listing.getDefinedData(True) +ns = 0 +for d in di: + dt = d.getDataType().getName().lower() + if 'char' in dt or 'unicode' in dt: + v = d.getValue() + if v and len(str(v)) >= 6: + print('%s %r' % (d.getAddress(), str(v)[:120])) + ns += 1 +print('total strings: %d' % ns) + +print('=== MEMORY BLOCKS ===') +for b in currentProgram.getMemory().getBlocks(): + print('%s %s %s r%s w%s x%s' % (b.getName(), b.getStart(), b.getSize(), + '1' if b.isRead() else '0', + '1' if b.isWrite() else '0', + '1' if b.isExecute() else '0')) diff --git a/tools/ghidra_scripts/dump_decomp.py b/tools/ghidra_scripts/dump_decomp.py new file mode 100755 index 0000000..d9393c8 --- /dev/null +++ b/tools/ghidra_scripts/dump_decomp.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Dump disassembly + decompilation for functions of an imported program. + +Plain PyGhidra API (venv with pyghidra). Usage: + GHIDRA_INSTALL_DIR=... venv/bin/python tools/ghidra_scripts/dump_decomp.py \ + [entry_only|all|0xADDR ...] +""" +import sys + +import pyghidra + +pyghidra.start() + +from ghidra.app.decompiler import DecompInterface # noqa: E402 +from ghidra.util.task import ConsoleTaskMonitor # noqa: E402 + +elf, proj_dir, proj_name = sys.argv[1], sys.argv[2], sys.argv[3] +mode = sys.argv[4] if len(sys.argv) > 4 else 'entry_only' + +with pyghidra.open_program(elf, project_location=proj_dir, + project_name=proj_name, analyze=True) as flat: + prog = flat.getCurrentProgram() + fm = prog.getFunctionManager() + + def addr(a): + return prog.getAddressFactory().getAddress(hex(a) if isinstance(a, int) else a) + + dec = DecompInterface() + dec.openProgram(prog) + + all_funcs = list(fm.getFunctions(True)) + by_ep = {int('%x' % f.getEntryPoint().getOffset(), 16): f + for f in all_funcs} + targets = [] + if mode == 'entry_only': + targets = all_funcs[:1] + elif mode == 'all': + targets = all_funcs + else: + for s in sys.argv[4:]: + targets.append(by_ep.get(int(s, 16))) + targets = [t for t in targets if t is not None] + + # entry listing: first 40 instructions + ent = fm.getFunctions(True).next() + listing = prog.getListing() + it = listing.getInstructions(ent.getEntryPoint(), True) + print('=== ENTRY LISTING %s ===' % ent.getEntryPoint()) + for i in range(40): + x = it.next() + if x is None: + break + print('%s %s' % (x.getAddress(), x)) + + for f in targets: + if f is None: + continue + r = dec.decompileFunction(f, 60, ConsoleTaskMonitor()) + print('\n=== %s %s ===' % (f.getEntryPoint(), f.getName())) + if r.decompileCompleted(): + print(r.getDecompiledFunction().getC()) + else: + print('// decompile failed:', r.getErrorMessage())