P1-C: WA dispatch table located (65-entry MCU_EXT_CMD @0x10201304, ~30 ABI-named handlers); scan_tables.py structural method; ForceDisasmPost; findings F7
This commit is contained in:
parent
b9c46bc11d
commit
4042aeda6c
7 changed files with 251 additions and 3 deletions
39
tools/ghidra_scripts/ForceDisasmPost.py
Normal file
39
tools/ghidra_scripts/ForceDisasmPost.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Headless post-script: force-disassemble all executable blocks and
|
||||
# re-run auto-analysis. Run with:
|
||||
# analyzeHeadless <proj_dir> <proj> -process <prog> -noanalysis \
|
||||
# -scriptPath tools/ghidra_scripts -postScript ForceDisasmPost.py
|
||||
#@category Analysis
|
||||
|
||||
from ghidra.app.cmd.disassemble import DisassembleCommand
|
||||
from ghidra.app.plugin.core.analysis import AutoAnalysisManager
|
||||
from ghidra.program.model.address import AddressSet
|
||||
|
||||
listing = currentProgram.getListing()
|
||||
mem = currentProgram.getMemory()
|
||||
monitor.setMessage('collecting executable blocks')
|
||||
|
||||
total = AddressSet()
|
||||
for b in mem.getBlocks():
|
||||
if b.isExecute() and 'elf' not in b.getName():
|
||||
total.addRange(b.getStart(), b.getEnd())
|
||||
|
||||
before_ins = listing.getNumInstructions()
|
||||
before_fun = currentProgram.getFunctionManager().getFunctionCount()
|
||||
print('ForceDisasm before: %d instructions, %d functions'
|
||||
% (before_ins, before_fun))
|
||||
|
||||
cmd = DisassembleCommand(total, total, False)
|
||||
tx = currentProgram.startTransaction('force disasm')
|
||||
try:
|
||||
ok = cmd.applyTo(currentProgram, monitor)
|
||||
finally:
|
||||
currentProgram.endTransaction(tx, True)
|
||||
print('ForceDisasm disasm apply: %s' % ok)
|
||||
|
||||
mgr = AutoAnalysisManager.getAnalysisManager(currentProgram)
|
||||
mgr.reAnalyzeAll(None)
|
||||
mgr.startAnalysis(monitor)
|
||||
|
||||
print('ForceDisasm after: %d instructions, %d functions'
|
||||
% (listing.getNumInstructions(),
|
||||
currentProgram.getFunctionManager().getFunctionCount()))
|
||||
|
|
@ -17,9 +17,9 @@ 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()
|
||||
with pyghidra.program_context(pyghidra.open_project(
|
||||
__import__('os').path.abspath(proj_dir), proj_name),
|
||||
'/' + __import__('os').path.basename(elf)) as prog:
|
||||
fm = prog.getFunctionManager()
|
||||
|
||||
def addr(a):
|
||||
|
|
|
|||
46
tools/ghidra_scripts/find_string_xref.py
Executable file
46
tools/ghidra_scripts/find_string_xref.py
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Find strings matching a regex and decompile their referencing functions.
|
||||
|
||||
Usage: dump-like launcher args: <elf> <proj_dir> <proj_name> <regex> [max_funcs]
|
||||
"""
|
||||
import re
|
||||
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, pat = sys.argv[1:5]
|
||||
maxf = int(sys.argv[5]) if len(sys.argv) > 5 else 3
|
||||
|
||||
with pyghidra.program_context(pyghidra.open_project(
|
||||
__import__('os').path.abspath(proj_dir), proj_name),
|
||||
'/' + __import__('os').path.basename(elf)) as prog:
|
||||
listing = prog.getListing()
|
||||
fm = prog.getFunctionManager()
|
||||
rm = prog.getReferenceManager()
|
||||
rx = re.compile(pat)
|
||||
dec = DecompInterface()
|
||||
dec.openProgram(prog)
|
||||
seen = set()
|
||||
for d in listing.getDefinedData(True):
|
||||
dt = d.getDataType().getName().lower()
|
||||
if 'string' not in dt and 'char' not in dt:
|
||||
continue
|
||||
v = d.getValue()
|
||||
if v is None or not rx.search(str(v)):
|
||||
continue
|
||||
print('STRING %s %r' % (d.getAddress(), str(v)[:100]))
|
||||
for ref in rm.getReferencesTo(d.getAddress()):
|
||||
f = fm.getFunctionContaining(ref.getFromAddress())
|
||||
if f is None or f.getEntryPoint() in seen:
|
||||
continue
|
||||
seen.add(f.getEntryPoint())
|
||||
print(' <- %s %s' % (f.getEntryPoint(), f.getName()))
|
||||
if len(seen) <= maxf:
|
||||
r = dec.decompileFunction(f, 90, ConsoleTaskMonitor())
|
||||
if r.decompileCompleted():
|
||||
print(r.getDecompiledFunction().getC()[:6000])
|
||||
50
tools/ghidra_scripts/force_disasm.py
Executable file
50
tools/ghidra_scripts/force_disasm.py
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Force-disassemble every executable block, then re-analyze.
|
||||
|
||||
Firmware images have no entry graph into most code, so default analysis
|
||||
leaves large undisassembled regions (no xrefs to their strings).
|
||||
|
||||
Usage: <proj_dir> <proj_name> <program_path_in_project>
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pyghidra
|
||||
|
||||
pyghidra.start()
|
||||
|
||||
from ghidra.app.cmd.disassemble import DisassembleCommand # noqa: E402
|
||||
from ghidra.app.plugin.core.analysis import AutoAnalysisManager # noqa: E402
|
||||
from ghidra.program.model.address import AddressSet # noqa: E402
|
||||
from ghidra.util.task import ConsoleTaskMonitor # noqa: E402
|
||||
|
||||
proj_dir, proj_name, prog_path = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
monitor = ConsoleTaskMonitor()
|
||||
|
||||
proj = pyghidra.open_project(os.path.abspath(proj_dir), proj_name)
|
||||
with pyghidra.program_context(proj, prog_path) as prog:
|
||||
mem = prog.getMemory()
|
||||
listing = prog.getListing()
|
||||
total = AddressSet()
|
||||
for b in mem.getBlocks():
|
||||
if b.isExecute() and 'elf' not in b.getName():
|
||||
total.addRange(b.getStart(), b.getEnd())
|
||||
before_ins = listing.getNumInstructions()
|
||||
before_fun = prog.getFunctionManager().getFunctionCount()
|
||||
print('before: %d instructions, %d functions'
|
||||
% (before_ins, before_fun))
|
||||
cmd = DisassembleCommand(total, total, False)
|
||||
with pyghidra.transaction(prog, 'force disasm'):
|
||||
ok = cmd.applyTo(prog, monitor)
|
||||
print('disasm apply:', ok)
|
||||
from ghidra.app.script import GhidraScriptUtil
|
||||
from ghidra.program.flatapi import FlatProgramAPI
|
||||
GhidraScriptUtil.acquireBundleHostReference()
|
||||
try:
|
||||
FlatProgramAPI(prog).analyzeAll(prog)
|
||||
finally:
|
||||
GhidraScriptUtil.releaseBundleHostReference()
|
||||
print('after: %d instructions, %d functions'
|
||||
% (listing.getNumInstructions(),
|
||||
prog.getFunctionManager().getFunctionCount()))
|
||||
proj.close()
|
||||
54
tools/ref/mcu_ext_cmd.enum
Normal file
54
tools/ref/mcu_ext_cmd.enum
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# MCU_EXT_CMD_* ids from mt76_connac_mcu.h (torvalds master)
|
||||
# Same enum serves WM and WA: MCU_WA_EXT_CMD(t) = MCU_EXT_CMD(t) | WA-routing bit.
|
||||
enum {
|
||||
MCU_EXT_CMD_EFUSE_ACCESS = 0x01,
|
||||
MCU_EXT_CMD_RF_REG_ACCESS = 0x02,
|
||||
MCU_EXT_CMD_RF_TEST = 0x04,
|
||||
MCU_EXT_CMD_ID_RADIO_ON_OFF_CTRL = 0x05,
|
||||
MCU_EXT_CMD_PM_STATE_CTRL = 0x07,
|
||||
MCU_EXT_CMD_CHANNEL_SWITCH = 0x08,
|
||||
MCU_EXT_CMD_SET_TX_POWER_CTRL = 0x11,
|
||||
MCU_EXT_CMD_FW_LOG_2_HOST = 0x13,
|
||||
MCU_EXT_CMD_TXBF_ACTION = 0x1e,
|
||||
MCU_EXT_CMD_EFUSE_BUFFER_MODE = 0x21,
|
||||
MCU_EXT_CMD_THERMAL_PROT = 0x23,
|
||||
MCU_EXT_CMD_STA_REC_UPDATE = 0x25,
|
||||
MCU_EXT_CMD_BSS_INFO_UPDATE = 0x26,
|
||||
MCU_EXT_CMD_EDCA_UPDATE = 0x27,
|
||||
MCU_EXT_CMD_DEV_INFO_UPDATE = 0x2A,
|
||||
MCU_EXT_CMD_THERMAL_CTRL = 0x2c,
|
||||
MCU_EXT_CMD_WTBL_UPDATE = 0x32,
|
||||
MCU_EXT_CMD_SET_DRR_CTRL = 0x36,
|
||||
MCU_EXT_CMD_SET_RDD_CTRL = 0x3a,
|
||||
MCU_EXT_CMD_ATE_CTRL = 0x3d,
|
||||
MCU_EXT_CMD_PROTECT_CTRL = 0x3e,
|
||||
MCU_EXT_CMD_DBDC_CTRL = 0x45,
|
||||
MCU_EXT_CMD_MAC_INIT_CTRL = 0x46,
|
||||
MCU_EXT_CMD_RX_HDR_TRANS = 0x47,
|
||||
MCU_EXT_CMD_MUAR_UPDATE = 0x48,
|
||||
MCU_EXT_CMD_BCN_OFFLOAD = 0x49,
|
||||
MCU_EXT_CMD_RX_AIRTIME_CTRL = 0x4a,
|
||||
MCU_EXT_CMD_SET_RX_PATH = 0x4e,
|
||||
MCU_EXT_CMD_EFUSE_FREE_BLOCK = 0x4f,
|
||||
MCU_EXT_CMD_TX_POWER_FEATURE_CTRL = 0x58,
|
||||
MCU_EXT_CMD_RXDCOC_CAL = 0x59,
|
||||
MCU_EXT_CMD_GET_MIB_INFO = 0x5a,
|
||||
MCU_EXT_CMD_TXDPD_CAL = 0x60,
|
||||
MCU_EXT_CMD_CAL_CACHE = 0x67,
|
||||
MCU_EXT_CMD_RED_ENABLE = 0x68,
|
||||
MCU_EXT_CMD_CP_SUPPORT = 0x75,
|
||||
MCU_EXT_CMD_SET_RADAR_TH = 0x7c,
|
||||
MCU_EXT_CMD_SET_RDD_PATTERN = 0x7d,
|
||||
MCU_EXT_CMD_MWDS_SUPPORT = 0x80,
|
||||
MCU_EXT_CMD_SET_SER_TRIGGER = 0x81,
|
||||
MCU_EXT_CMD_TWT_AGRT_UPDATE = 0x94,
|
||||
MCU_EXT_CMD_FW_DBG_CTRL = 0x95,
|
||||
MCU_EXT_CMD_OFFCH_SCAN_CTRL = 0x9a,
|
||||
MCU_EXT_CMD_SET_RDD_TH = 0x9d,
|
||||
MCU_EXT_CMD_MURU_CTRL = 0x9f,
|
||||
MCU_EXT_CMD_SET_SPR = 0xa8,
|
||||
MCU_EXT_CMD_GROUP_PRE_CAL_INFO = 0xab,
|
||||
MCU_EXT_CMD_DPD_PRE_CAL_INFO = 0xac,
|
||||
MCU_EXT_CMD_PHY_STAT_INFO = 0xad,
|
||||
MCU_EXT_CMD_WF_RF_PIN_CTRL = 0xbd,
|
||||
};
|
||||
0
tools/ref/mcu_wa_ext_cmd.enum
Normal file
0
tools/ref/mcu_wa_ext_cmd.enum
Normal file
59
tools/scan_tables.py
Executable file
59
tools/scan_tables.py
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Scan firmware regions for consecutive function-pointer tables.
|
||||
|
||||
Finds runs of >= min_run consecutive little-endian dwords that all fall in a
|
||||
given code range — the shape of MCU command-handler dispatch tables.
|
||||
|
||||
Usage: scan_tables.py <region.bin> <code_lo> <code_hi> [min_run] [tolerance]
|
||||
tolerance = max absolute non-code dwords allowed inside a run (0/NULL counted).
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
lo, hi = int(sys.argv[2], 0), int(sys.argv[3], 0)
|
||||
min_run = int(sys.argv[4]) if len(sys.argv) > 4 else 8
|
||||
tol = int(sys.argv[5]) if len(sys.argv) > 5 else 0
|
||||
|
||||
d = path.read_bytes()
|
||||
base = 0
|
||||
# If the region file corresponds to a load address, pass that as lo bound
|
||||
# via argv; region offsets here are file offsets, so also print file offset.
|
||||
words = struct.unpack_from('<%dI' % (len(d) // 4), d)
|
||||
run_start = None
|
||||
run = []
|
||||
tables = []
|
||||
|
||||
|
||||
def flush(end):
|
||||
global run, run_start
|
||||
code = sum(1 for w in run if lo <= w < hi)
|
||||
if len(run) >= min_run and code >= min_run:
|
||||
tables.append((run_start, end, len(run), code))
|
||||
run = []
|
||||
run_start = None
|
||||
|
||||
|
||||
for i, w in enumerate(words):
|
||||
ok = lo <= w < hi or w == 0
|
||||
if ok:
|
||||
if run_start is None:
|
||||
run_start = i * 4
|
||||
run.append(w)
|
||||
else:
|
||||
if run_start is not None and tol:
|
||||
run.append(w) # tolerate, next check trims
|
||||
if sum(1 for x in run[-3:] if not (lo <= x < hi or x == 0)) >= 2:
|
||||
flush(i * 4)
|
||||
else:
|
||||
flush(i * 4)
|
||||
|
||||
for t in tables:
|
||||
start_off, end_off, n, code = t
|
||||
print('table @ file+0x%x..0x%x (%d words, %d code ptrs)'
|
||||
% (start_off, end_off, n, code))
|
||||
vals = words[start_off // 4:end_off // 4]
|
||||
for j, w in enumerate(vals[:64]):
|
||||
mark = '' if lo <= w < hi else ('(0)' if w == 0 else '(?)')
|
||||
print(' [%2d] 0x%08x %s' % (j, w, mark))
|
||||
Loading…
Reference in a new issue