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
|
*.rep
|
||||||
*.gpr
|
*.gpr
|
||||||
*.id*
|
*.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
|
~95% changed, new RA/DPD/thermal strings), docs (format/boot), CI
|
||||||
(push + weekly track), v0.1.0 tagged. Announcement drafted, not posted —
|
(push + weekly track), v0.1.0 tagged. Announcement drafted, not posted —
|
||||||
hosting decision still open.
|
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
|
- Repo name + hosting: RESOLVED — mtk-wifi-fw at
|
||||||
https://zachery.lol/code/zacheryasc/mtk-wifi-fw (self-hosted Forgejo 9,
|
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
|
constant `0x30002` on every observed section (loader downloads all sections
|
||||||
regardless; field semantics otherwise unobserved).
|
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
|
## Unknowns registry
|
||||||
|
|
||||||
- **U1 — `feature_set` bit 7 (0x80):** observed only on WM regions at
|
- **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