This commit is contained in:
Dejvino
2026-09-30 22:48:06 +02:00
commit 8913cb4314
37 changed files with 3190 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""vtable-anchored function map + disassembly report for Lofi-12XT (capstone).
Usage:
python3 analysis/gen_funcmap.py "Lofi-12XT_v1.5.205/.../Lofi-12 XT.bin" /tmp/opencode/fw
Writes: funcmap.json, funcmap.md, asm-linear.txt into outdir.
"""
import os
import struct
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools'))
from lofi_image import parse_image, find_sect_by_role, code_file_off
from capstone import Cs, CS_ARCH_TMS320C64X, CS_MODE_LITTLE_ENDIAN
def collect_vfuncs(ro, code):
p = ro['payload']
words = [struct.unpack('<I', p[i:i + 4])[0]
for i in range(0, len(p) - 3, 4)]
is_ptr = [code['addr'] <= w < code['end'] for w in words]
runs, i, n = [], 0, len(words)
while i < n:
if is_ptr[i]:
j = i
while j < n and is_ptr[j]:
j += 1
if j - i >= 3:
runs.append((ro['addr'] + i * 4, words[i:j]))
i = j
else:
i += 1
funcs = {}
for vaddr, ws in runs:
for k, w in enumerate(ws):
funcs.setdefault(w, []).append((vaddr, k))
return runs, funcs
def main():
image, outdir = sys.argv[1], sys.argv[2]
os.makedirs(outdir, exist_ok=True)
d = open(image, 'rb').read()
info = parse_image(d)
code = find_sect_by_role(info, 'code')
ro = find_sect_by_role(info, 'rodata')
entry = info['mmtd']['entry']
runs, funcs = collect_vfuncs(ro, code)
cs = Cs(CS_ARCH_TMS320C64X, CS_MODE_LITTLE_ENDIAN)
rows = []
def head_at(faddr):
foff = code_file_off(info, faddr)
if foff is None:
return ["<outside code>"]
for base_off, base_addr in ((foff, faddr),
(foff - (faddr % 32), faddr - (faddr % 32))):
if base_off < 0:
continue
try:
insns = list(cs.disasm(d[base_off:base_off + 64], base_addr))
except Exception as e:
return ["<capstone err %s>" % e]
for ins in insns:
if ins.address == faddr or base_addr != faddr:
return ["%08X %s %s" % (i.address, i.mnemonic, i.op_str)
for i in insns[:6]] or ["<undecodable>"]
if insns and base_addr == faddr:
return ["%08X %s %s" % (i.address, i.mnemonic, i.op_str)
for i in insns[:6]]
return ["<undecodable>"]
for faddr in sorted(funcs):
rows.append({"addr": faddr, "addr_hex": "%08X" % faddr,
"refs": funcs[faddr], "nrefs": len(funcs[faddr]),
"head": head_at(faddr)})
# entry must be present even if not vtable-referenced
if entry not in funcs:
foff = code_file_off(info, entry)
head = ["%08X %s %s" % (i.address, i.mnemonic, i.op_str)
for i in list(cs.disasm(d[foff:foff + 64], entry))[:6]]
rows.append({"addr": entry, "addr_hex": "%08X" % entry,
"refs": [], "nrefs": 0, "head": head})
rows.sort(key=lambda r: r["addr"])
import json
json.dump({"image_size": len(d), "entry_hex": "%08X" % entry,
"code": {"addr_hex": "%08X" % code['addr'], "len": code['len']},
"rodata": {"addr_hex": "%08X" % ro['addr'], "len": ro['len']},
"nvtable": len(runs), "nfunc": len(rows), "funcs": rows},
open(os.path.join(outdir, "funcmap.json"), "w"), indent=1)
with open(os.path.join(outdir, "funcmap.md"), "w") as f:
f.write("# funcmap v1.5.205 — %d vtables, %d unique vfuncs (+entry %08X)\n\n"
% (len(runs), len(rows), entry))
f.write("| func addr | xrefs | first insn |\n|---|---|---|\n")
for r in rows:
f.write("| %s | %d | `%s` |\n"
% (r["addr_hex"], r["nrefs"], r["head"][0].replace("|", "/")))
# linear sweep of whole code sect (fetch-packet granularity)
with open(os.path.join(outdir, "asm-linear.txt"), "w") as f:
cpay = code['payload']
for i in range(0, len(cpay), 8):
chunk = cpay[i:i + 8]
if len(chunk) < 8:
break
for ins in cs.disasm(chunk, code['addr'] + i):
f.write("%08X: %s %s\n" % (ins.address, ins.mnemonic, ins.op_str))
print("vtables=%d funcs=%d entry=%08X" % (len(runs), len(rows), entry))
print("wrote %s/{funcmap.json,funcmap.md,asm-linear.txt}" % outdir)
if __name__ == '__main__':
main()
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Solve the checksum seed: solve_ckseed.py image.bin
CRC32 is affine, so one image's 32-bit equality is 32 linear constraints on
the 32 unknown bits at file[8:12]. Solves via GF(2) Gaussian elimination and
prints the constant K such that
stored == CRC32(file with [8:12] := K).
Run on several stock images: all must print the same K (here: C27C6282).
Stdlib only.
"""
import binascii
import struct
import sys
def crc(b: bytes, init: int = 0) -> int:
return binascii.crc32(b, init) & 0xFFFFFFFF
def solve_for_K(d: bytes, target: int):
n = len(d)
d0 = bytearray(d)
d0[8:12] = b"\0\0\0\0"
rhs = target ^ crc(bytes(d0))
c0 = crc(bytes(n))
cols = []
for p in range(32):
m = bytearray(n)
m[8 + p // 8] = 1 << (p % 8)
cols.append(crc(bytes(m)) ^ c0)
rows = []
for r in range(32):
mask = 0
for p in range(32):
if (cols[p] >> r) & 1:
mask |= 1 << p
rows.append([mask, (rhs >> r) & 1])
where = [-1] * 32
row = 0
for col in range(32):
sel = next((i for i in range(row, 32)
if (rows[i][0] >> col) & 1), -1)
if sel < 0:
continue
rows[row], rows[sel] = rows[sel], rows[row]
where[col] = row
for i in range(32):
if i != row and ((rows[i][0] >> col) & 1):
rows[i][0] ^= rows[row][0]
rows[i][1] ^= rows[row][1]
row += 1
for i in range(32):
if rows[i][0] == 0 and rows[i][1] != 0:
return None # inconsistent: wrong structural hypothesis
if any(w < 0 for w in where):
return None # underdetermined
return sum((rows[where[p]][1] << p) for p in range(32))
def main() -> int:
d = open(sys.argv[1], "rb").read()
target = struct.unpack("<I", d[8:12])[0]
K = solve_for_K(d, target)
print("%08X" % K if K is not None else "NO_SOLUTION")
return 0 if K is not None else 1
if __name__ == "__main__":
sys.exit(main())