P1-B: Ghidra/PyGhidra toolchain scripts; patch blob structure + first decompilation findings (F5): ROM replacement table, assert anchors, N9/CCIF/mailbox evidence
Some checks are pending
ci / test (push) Waiting to run
ci / track (push) Waiting to run

This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-08-20 21:51:36 +04:00
parent 069a26019f
commit eba408d0c3
5 changed files with 146 additions and 0 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ __pycache__/
*.rep
*.gpr
*.id*
ghidra-proj/

View file

@ -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,

View file

@ -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

View file

@ -0,0 +1,36 @@
# Ghidra headless post-script: dump function/strings/block overview.
# Usage: analyzeHeadless <proj> <name> -process <prog> -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'))

View file

@ -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 \
<elf> <project_dir> <project_name> [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())