P1-B: Ghidra/PyGhidra toolchain scripts; patch blob structure + first decompilation findings (F5): ROM replacement table, assert anchors, N9/CCIF/mailbox evidence
This commit is contained in:
parent
069a26019f
commit
eba408d0c3
5 changed files with 146 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,3 +12,4 @@ __pycache__/
|
|||
*.rep
|
||||
*.gpr
|
||||
*.id*
|
||||
ghidra-proj/
|
||||
|
|
|
|||
7
PLAN.md
7
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
36
tools/ghidra_scripts/ExportOverview.py
Normal file
36
tools/ghidra_scripts/ExportOverview.py
Normal 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'))
|
||||
63
tools/ghidra_scripts/dump_decomp.py
Executable file
63
tools/ghidra_scripts/dump_decomp.py
Executable 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())
|
||||
Loading…
Reference in a new issue