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
+53
View File
@@ -0,0 +1,53 @@
# Lofi-12 XT custom-firmware tools
Stdlib-only Python (`tools/`). Verified against v1.1.156 / v1.2.179 / v1.5.205.
| Script | Purpose |
|---|---|
| `lofi_image.py` | Shared parser/packer library (import, not CLI) |
| `lofi_unpack.py` | `image.bin outdir/` — verify chain, dump `headers.json` + `sectN_addr*.bin` |
| `lofi_pack.py` | `indir/ out.bin [--checksum HEX]` — rebuild exact-fit image |
| `lofi_patch_string.py` | Same-length UI string swap (Level-0 mod, addresses stable) |
| `lofi_xref.py` | Count code/self pointers, list slack zero-gaps, locate string + refs |
| `lofi_checksum.py` | Quick check: CRC/adler/fletcher/sum/xor over obvious spans |
| `lofi_checksum_crack.py` | Long-running cracker: Phase A fast hashes + xor-mask detect, Phase B generic CRC param search (1,008 combos, multiprocessed), Phase C seeded CRC32-IEEE brute force 0..2²⁴ (hours) |
## Safe first loop (do not flash until checksum is cracked)
```bash
python3 tools/lofi_unpack.py "Lofi-12XT_v1.5.205/Lofi-12XT_v1.5.205/Lofi-12 XT.bin" /tmp/opencode/fw/unpack-1.5.205
python3 tools/lofi_pack.py /tmp/opencode/fw/unpack-1.5.205 /tmp/opencode/fw/rebuilt.bin
cmp "Lofi-12XT_v1.5.205/Lofi-12XT_v1.5.205/Lofi-12 XT.bin" /tmp/opencode/fw/rebuilt.bin # must be identical
python3 tools/lofi_patch_string.py "Lofi-12XT_v1.5.205/Lofi-12XT_v1.5.205/Lofi-12 XT.bin" /tmp/opencode/fw/patched.bin Threshold ThresholX
python3 tools/lofi_xref.py "Lofi-12XT_v1.5.205/Lofi-12XT_v1.5.205/Lofi-12 XT.bin" --find "AUDIO EXPORT"
python3 tools/lofi_checksum.py
```
## Cracking the checksum (the blocker)
```bash
# smoke tests (seconds):
python3 tools/lofi_checksum_crack.py --quick-only
python3 tools/lofi_checksum_crack.py --jobs 2 --limit 40 --out /tmp/opencode/ck-smoke.txt
# full long run (Phase B ~minutes, Phase C ~hours, all cores):
python3 tools/lofi_checksum_crack.py --jobs $(nproc) --out /tmp/opencode/ck-results.txt
# Phase D batch — CRC-16 family, ones-complement, chained per-sect, FNV-1/DJB2/Murmur (~1 min):
python3 tools/lofi_checksum_phaseD.py --jobs $(nproc) --out /tmp/opencode/ck-phaseD.txt
```
Results (exact or constant-xor-mask hits) append to the `--out` file;
progress checkpoints go to `--out.progress`. Any hit must match **all 3**
builds to be reported. Re-run with `--seed-max 4294967296` for the full
2³² seed space only if 2²⁴ finds nothing.
## Blockers / rules
- `cmtd+0x08` checksum: **solved** — `lofi_image.compute_checksum`
(CRC32-IEEE, init 0, over the image with bytes `[8:12]` replaced by
`0xC27C6282`; proven 3/3 against stock images). `lofi_pack` and
`lofi_patch_string` apply it automatically.
- Keep a known-good stock `.bin` on SD for revert; never touch SPI flash / USB MCU.
- Distribute mods as patches against user-supplied stock `.bin`, not full images.
- See `tweakability-report.md` (levels 0–4) and `rev-diff.md` for what is feasible.
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Brute-force checksum hypotheses for cmtd+0x08 against all known images.
Tests CRC32/zlib/adler/fletcher/sum/xor variants over multiple spans.
A hypothesis must match ALL builds to be reported as candidate.
Stdlib only.
"""
import binascii, os, struct, sys, zlib
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMAGES = [
'Lofi-12XT_v1.1.156/Lofi-12XT_v1.1.156/Lofi-12 XT.bin',
'Lofi-12XT_v1.2.179/Lofi-12XT_v1.2.179/Lofi-12 XT.bin',
'Lofi-12XT_v1.5.205/Lofi-12XT_v1.5.205/Lofi-12 XT.bin',
]
def fletcher16(data):
s1 = s2 = 0
for b in data:
s1 = (s1 + b) % 255
s2 = (s2 + s1) % 255
return (s2 << 8) | s1
def fletcher32(data):
s1 = s2 = 0
for i in range(0, len(data), 2):
w = data[i] | (data[i+1] << 8 if i+1 < len(data) else 0)
s1 = (s1 + w) % 0xFFFF
s2 = (s2 + s1) % 0xFFFF
return (s2 << 16) | s1
def sum32(data):
return sum(data) & 0xFFFFFFFF
def xor32(data):
r = 0
for i in range(0, len(data) - 3, 4):
(w,) = struct.unpack('<I', data[i:i+4])
r ^= w
return r & 0xFFFFFFFF
def load():
out = []
for rel in IMAGES:
p = os.path.join(BASE, rel)
d = open(p, 'rb').read()
ck = struct.unpack('<I', d[8:12])[0]
out.append((rel.split('/')[0], d, ck))
return out
def spans(d):
nsect = struct.unpack('<I', d[0x50:0x54])[0]
off = 0x54
payloads = b''
for _ in range(nsect):
ln = struct.unpack('<I', d[off+8:off+12])[0]
payloads += d[off+12:off+12+ln]
off += 12 + ln
z8 = bytearray(d); z8[8:12] = b'\0\0\0\0'
return {
'full': d,
'from_0x04': d[0x04:],
'from_0x0C': d[0x0C:],
'from_0x30': d[0x30:],
'from_0x54': d[0x54:],
'payload_concat': payloads,
'full_ck_zeroed': bytes(z8),
'from0x0C_ck_zeroed': bytes(z8)[0x0C:],
}
def main():
blobs = load()
for name, d, ck in blobs:
print(f"{name}: size={len(d)} stored_ck={ck:08X}")
algs = {
'crc32_le': lambda b: binascii.crc32(b) & 0xFFFFFFFF,
'crc32_be_byteswap': lambda b: struct.unpack('>I', struct.pack('<I', binascii.crc32(b) & 0xFFFFFFFF))[0],
'crc32_complement': lambda b: (~binascii.crc32(b)) & 0xFFFFFFFF,
'adler32': lambda b: zlib.adler32(b) & 0xFFFFFFFF,
'fletcher16': fletcher16,
'fletcher32': fletcher32,
'sum32': sum32,
'xor32': xor32,
}
cands = []
total = 0
for aname, fn in algs.items():
for sname in spans(blobs[0][1]):
total += 1
ok = True
for _, d, ck in blobs:
try:
v = fn(spans(d)[sname]) & 0xFFFFFFFF
except Exception:
ok = False
break
if v != ck:
ok = False
break
status = 'MATCH-ALL' if ok else 'no'
if ok:
cands.append((aname, sname))
# show near-misses? only print matches to stay concise
if ok:
print(f" {aname} x {sname}: {status}")
print(f"tested {total} hypotheses, {len(cands)} full-match candidates")
if not cands:
print("No match: checksum is not plain CRC32/adler/fletcher/sum/xor over obvious spans.")
print("Next: try seeded CRC, TI-AIS style, per-sect accumulate, or mmtd.flags correlation.")
if __name__ == '__main__':
main()
+338
View File
@@ -0,0 +1,338 @@
#!/usr/bin/env python3
"""Long-running checksum cracker for Lofi-12 XT cmtd+0x08.
Strategy: 3 known (image, checksum) pairs let us test each hypothesis fast
with early exit, plus detect CONSTANT-XOR-masked CRCs (stored = crc ^ mask).
Phases:
A (seconds): zlib-speed hashes (crc32/adler/word-sums/fnv) x spans x field-modes.
B (long): generic table-driven CRC32 parameter search
(poly x init x xorout x refin x span x field-mode),
multiprocessed, checkpointed, resumable.
Usage (long run):
python3 tools/lofi_checksum_crack.py --jobs $(nproc) --out /tmp/opencode/ck-results.txt
Quick smoke test:
python3 tools/lofi_checksum_crack.py --quick-only
python3 tools/lofi_checksum_crack.py --jobs 2 --limit 40 --out /tmp/opencode/ck-smoke.txt
Stdlib only. Safe: read-only on stock .bin files.
"""
import argparse, binascii, itertools, json, multiprocessing as mp
import os, struct, sys, time, zlib
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMAGES = [
('1.1.156', 'Lofi-12XT_v1.1.156/Lofi-12XT_v1.1.156/Lofi-12 XT.bin'),
('1.2.179', 'Lofi-12XT_v1.2.179/Lofi-12XT_v1.2.179/Lofi-12 XT.bin'),
('1.5.205', 'Lofi-12XT_v1.5.205/Lofi-12XT_v1.5.205/Lofi-12 XT.bin'),
]
POLYS = [ # normal (non-reflected) form
0x04C11DB7, # IEEE / PKZIP
0x1EDC6F41, # CRC-32C (Castagnoli)
0x741B8CD7, # CRC-32K (Koopman)
0x1A2B3E55, # spare / nonstandard probes
0x814141AB, # CRC-32Q
0x000000AF, # tiny-poly probe (catches nibble-CRC schemes fast-fail)
]
INITS = [0x00000000, 0xFFFFFFFF]
XOROUTS = [0x00000000, 0xFFFFFFFF]
REFINS = [True, False]
FIELDMODES = ['asis', 'zeroed', 'ff'] # how cmtd+0x08 bytes are treated during hashing
SPANS = ['full', 'from_0x0C', 'from_0x30', 'from_0x54', 'payload_concat',
'headers_only', 'sect_headers_mixed']
def load_images():
blobs = []
for ver, rel in IMAGES:
p = os.path.join(BASE, rel)
d = open(p, 'rb').read()
assert d[:4] == b'cmtd' and d[0x30:0x34] == b'mmtd', p
ck = struct.unpack('<I', d[8:12])[0]
blobs.append((ver, d, ck))
return blobs
def span_bufs(d: bytes):
nsect = struct.unpack('<I', d[0x50:0x54])[0]
off = 0x54
pay = bytearray()
hdrs = bytearray()
for _ in range(nsect):
assert d[off:off+4] == b'sect'
ln = struct.unpack('<I', d[off+8:off+12])[0]
hdrs += d[off:off+12]
pay += d[off+12:off+12+ln]
off += 12 + ln
z = bytearray(d); z[8:12] = b'\0\0\0\0'
f = bytearray(d); f[8:12] = b'\xff\xff\xff\xff'
return {
'full': [d, bytes(z), bytes(f)],
'from_0x0C': [d[0x0C:], bytes(z)[0x0C:], bytes(f)[0x0C:]],
'from_0x30': [d[0x30:], d[0x30:], d[0x30:]],
'from_0x54': [d[0x54:], d[0x54:], d[0x54:]],
'payload_concat': [bytes(pay), bytes(pay), bytes(pay)],
'headers_only': [d[:0x54], bytes(z)[:0x54], bytes(f)[:0x54]],
'sect_headers_mixed': [bytes(hdrs), bytes(hdrs), bytes(hdrs)],
}
_FIELDMODE_IDX = {'asis': 0, 'zeroed': 1, 'ff': 2}
# ---------- generic CRC32 (table-driven, pure python) ----------
_crc_tables = {}
def crc_table(poly, refin):
key = (poly, refin)
t = _crc_tables.get(key)
if t is not None:
return t
t = []
if refin:
rpoly = int(f'{poly:032b}'[::-1], 2)
for i in range(256):
c = i
for _ in range(8):
c = (c >> 1) ^ rpoly if c & 1 else c >> 1
t.append(c & 0xFFFFFFFF)
else:
for i in range(256):
c = i << 24
for _ in range(8):
c = ((c << 1) ^ poly) & 0xFFFFFFFF if c & 0x80000000 else (c << 1) & 0xFFFFFFFF
t.append(c)
_crc_tables[key] = t
return t
def crc_generic(buf: bytes, poly, init, refin, xorout):
tab = crc_table(poly, refin)
crc = init
if refin:
for b in buf:
crc = tab[(crc ^ b) & 0xFF] ^ (crc >> 8)
else:
for b in buf:
crc = tab[((crc >> 24) ^ b) & 0xFF] ^ ((crc << 8) & 0xFFFFFFFF)
return (crc ^ xorout) & 0xFFFFFFFF
def fnv1a32(buf: bytes):
h = 0x811C9DC5
for b in buf:
h = ((h ^ b) * 0x01000193) & 0xFFFFFFFF
return h
def wordsum_le(buf: bytes):
s = 0
for i in range(0, len(buf) - 3, 4):
(w,) = struct.unpack('<I', buf[i:i+4])
s = (s + w) & 0xFFFFFFFF
return s
# ---------- phase A: fast hashes ----------
def phase_a(blobs):
print('=== Phase A: fast hashes (crc32/adler/fnv/wordsum) ===', flush=True)
cands = []
for span in SPANS:
bufs = [(ver, span_bufs(d)[span], ck) for ver, d, ck in blobs]
tests = {
'crc32': lambda b: binascii.crc32(b) & 0xFFFFFFFF,
'crc32_bswap': lambda b: struct.unpack('>I', struct.pack('<I', binascii.crc32(b) & 0xFFFFFFFF))[0],
'adler32': lambda b: zlib.adler32(b) & 0xFFFFFFFF,
'fnv1a32': fnv1a32,
'wordsum_le': wordsum_le,
'sum_bytes': lambda b: sum(b) & 0xFFFFFFFF,
}
for aname, fn in tests.items():
for fmode in _FIELDMODE_IDX:
idx = _FIELDMODE_IDX[fmode]
try:
vals = [(ver, ck, fn(b[idx])) for ver, b, ck in bufs]
except Exception as e:
print(f' {aname} x {span} x {fmode}: error {e}')
continue
if all(v == ck for _, ck, v in vals):
print(f' *** EXACT MATCH: {aname} x {span} x {fmode}')
cands.append((aname, span, fmode, 0))
masks = [ck ^ v for _, ck, v in vals]
if masks[0] == masks[1] == masks[2]:
print(f' --- xor-mask candidate: {aname} x {span} x {fmode} '
f'mask={masks[0]:08X} (masked CRC, needs 1 confirmation)')
cands.append((aname, span, fmode, masks[0]))
if not cands:
print('Phase A: no exact or xor-mask candidates.', flush=True)
return cands
# ---------- phase B: generic CRC search ----------
def all_params():
for poly, init, xorout, refin, span, fmode in itertools.product(
POLYS, INITS, XOROUTS, REFINS, SPANS, FIELDMODES):
yield (poly, init, xorout, refin, span, fmode)
_G_BLOBS = None
def _init_worker(blobs_packed):
global _G_BLOBS
_G_BLOBS = blobs_packed # list of (ver, span->bufs, ck); pickled once per worker
def _worker(param):
poly, init, xorout, refin, span, fmode = param
idx = _FIELDMODE_IDX[fmode]
try:
r = []
for ver, sbufs, ck in _G_BLOBS:
v = crc_generic(sbufs[span][idx], poly, init, refin, xorout)
r.append((ver, ck, v))
if r[0][2] == r[0][1] and r[1][2] == r[1][1] and r[2][2] == r[2][1]:
return ('EXACT', param, 0)
m0, m1, m2 = r[0][1] ^ r[0][2], r[1][1] ^ r[1][2], r[2][1] ^ r[2][2]
if m0 == m1 == m2:
return ('MASK', param, m0)
except Exception:
pass
return None
def phase_b(blobs, jobs, out, limit=None, resume_from=0):
params = list(all_params())
if limit:
params = params[:limit]
total = len(params)
print(f'=== Phase B: generic CRC search: {total} combos, jobs={jobs} ===', flush=True)
# pack span buffers once (pickle to workers a single time)
packed = [(ver, span_bufs(d), ck) for ver, d, ck in blobs]
done = resume_from
t0 = time.time()
found = []
with mp.Pool(jobs, initializer=_init_worker, initargs=(packed,)) as pool:
CH = 8
for i, res in enumerate(pool.imap_unordered(_worker, params, chunksize=CH), start=1):
if i <= done:
continue
if res is not None:
kind, param, mask = res
poly, init, xorout, refin, span, fmode = param
line = (f'{kind} poly={poly:08X} init={init:08X} xorout={xorout:08X} '
f'refin={int(refin)} span={span} field={fmode} mask={mask:08X}')
print(f' *** {line}', flush=True)
found.append(line)
with open(out, 'a') as fh:
fh.write(line + '\n')
if i % 50 == 0 or i == total:
el = time.time() - t0
rate = i / max(el, 1e-6)
print(f' [{i}/{total}] {rate:.1f} combos/s elapsed={el:.0f}s', flush=True)
with open(out + '.progress', 'w') as fh:
fh.write(json.dumps({'done': i, 'total': total,
'elapsed': el, 'found': found}) + '\n')
el = time.time() - t0
print(f'Phase B done: {total} combos in {el:.0f}s, {len(found)} candidates.', flush=True)
return found
_G_SEED_BUFS = None
_G_SEED_CKS = None
def _init_seed_worker(bufs, cks):
global _G_SEED_BUFS, _G_SEED_CKS
_G_SEED_BUFS = bufs
_G_SEED_CKS = cks
def _seed_scan(task):
lo, hi = task
b1, b2, b3 = _G_SEED_BUFS
s1, s2, s3 = _G_SEED_CKS
hits = []
for seed in range(lo, hi):
if (binascii.crc32(b1, seed) & 0xFFFFFFFF) == s1:
if ((binascii.crc32(b2, seed) & 0xFFFFFFFF) == s2 and
(binascii.crc32(b3, seed) & 0xFFFFFFFF) == s3):
hits.append(seed)
return (lo, hi, hits)
def _seed_worker(args):
lo, hi, span, fmode = args
idx = _FIELDMODE_IDX[fmode]
b1, b2, b3 = _G_SEED[0][span][idx], _G_SEED[1][span][idx], _G_SEED[2][span][idx]
s1, s2, s3 = _G_SEED[3]
hits = []
for seed in range(lo, hi):
c1 = binascii.crc32(b1, seed) & 0xFFFFFFFF
if c1 != s1:
# xor-mask path: derive mask from image 1, confirm on 2+3
# (costs 2 more CRCs only on the rare near-hit; here c1!=s1 always
# so check mask constancy cheaply only every step? skip: exact-only
# in seed phase for speed; mask search lives in Phase B)
continue
c2 = binascii.crc32(b2, seed) & 0xFFFFFFFF
c3 = binascii.crc32(b3, seed) & 0xFFFFFFFF
if c2 == s2 and c3 == s3:
hits.append(f'EXACT seed={seed:08X} span={span} field={fmode}')
return hits
def phase_c(blobs, jobs, out, seed_max=1 << 24, seed_span='full', seed_chunk=4096):
print(f'=== Phase C: seeded CRC32-IEEE brute force: seeds 0..{seed_max} '
f'span={seed_span} jobs={jobs} ===', flush=True)
print(' (C-speed crc32; exact-match only; this is the hours-long phase)', flush=True)
# NOTE: 'zeroed' = cmtd+0x08 treated as 00s during hashing (standard scheme);
# 'asis' re-check is cheap relative to seed space, so do zeroed first.
t0 = time.time()
found = []
for fmode in ('zeroed', 'asis'):
idx = _FIELDMODE_IDX[fmode]
bufs = [span_bufs(d)[seed_span][idx] for _, d, _ in blobs]
cks = [ck for _, _, ck in blobs]
found += _phase_c_loop(bufs, cks, seed_span, fmode, seed_max, seed_chunk, jobs, out, t0)
return found
def _phase_c_loop(bufs, cks, span, fmode, seed_max, chunk, jobs, out, t0):
b1, b2, b3 = bufs
s1, s2, s3 = cks
found = []
# shard seed space across workers: each task scans [lo,hi)
tasks = [(lo, min(lo + chunk, seed_max)) for lo in range(0, seed_max, chunk)]
total = len(tasks)
done = 0
with mp.Pool(jobs, initializer=_init_seed_worker, initargs=(bufs, cks)) as pool:
for lo, hi, hits in pool.imap_unordered(_seed_scan, tasks, chunksize=4):
done += 1
for seed in hits:
line = f'EXACT seed={seed:08X} span={span} field={fmode}'
print(f' *** {line}', flush=True)
found.append(line)
with open(out, 'a') as fh:
fh.write(line + '\n')
if done % max(1, total // 20) == 0 or done == total:
el = time.time() - t0
print(f' Phase C [{done}/{total} chunks] seeds~{done*chunk}/{seed_max} '
f'elapsed={el:.0f}s', flush=True)
el = time.time() - t0
print(f'Phase C done: {seed_max} seeds in {el:.0f}s, {len(found)} hits.', flush=True)
return found
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--jobs', type=int, default=max(1, (os.cpu_count() or 4) - 1))
ap.add_argument('--out', default='/tmp/opencode/ck-results.txt')
ap.add_argument('--quick-only', action='store_true')
ap.add_argument('--skip-phase-a', action='store_true')
ap.add_argument('--skip-phase-b', action='store_true')
ap.add_argument('--skip-phase-c', action='store_true')
ap.add_argument('--limit', type=int, default=None, help='test only first N combos (smoke test)')
ap.add_argument('--resume-from', type=int, default=0)
ap.add_argument('--seed-max', type=int, default=1 << 24,
help='seed brute-force range 0..SEED_MAX (default 2^24 ≈ hours)')
ap.add_argument('--seed-span', default='full', choices=SPANS)
ap.add_argument('--seed-chunk', type=int, default=4096)
a = ap.parse_args()
blobs = load_images()
for ver, d, ck in blobs:
print(f'{ver}: size={len(d)} stored_ck={ck:08X}', flush=True)
if not a.skip_phase_a:
phase_a(blobs)
if a.quick_only:
print('quick-only: stopping before Phase B/C.', flush=True)
return
open(a.out, 'a').write(f'# run {time.ctime()} jobs={a.jobs} limit={a.limit} seed_max={a.seed_max}\n')
if not a.skip_phase_b:
phase_b(blobs, a.jobs, a.out, limit=a.limit, resume_from=a.resume_from)
if not a.skip_phase_c and not a.limit:
phase_c(blobs, a.jobs, a.out, seed_max=a.seed_max,
seed_span=a.seed_span, seed_chunk=a.seed_chunk)
if __name__ == '__main__':
main()
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""Phase D checksum cracker: 16-bit/ones-complement/chained/DJB2/Murmur batch.
Covers what Phases A-C did not: every candidate must match ALL 3 builds.
Stdlib only, read-only on stock .bin files.
Quick smoke: python3 tools/lofi_checksum_phaseD.py --limit 30
Full run: python3 tools/lofi_checksum_phaseD.py --jobs $(nproc) --out /tmp/opencode/ck-phaseD.txt
Families:
CRC16: ARC/Modbus/CCITT-FALSE/XMODEM/Kermit/DNP/UMTS/BINHEX + halved-file pairs
Ones-complement: 16-bit word sums (LE/BE, folded, complemented)
Word sums: 16-bit LE/BE, BSD rotate, SysV folded
Chained per-sect: crc32-of-crcs, sum-of-crcs, xor-of-crcs
Odd hashes: FNV-1 (not 1a), DJB2, Murmur3-x86-32
"""
import argparse, binascii, multiprocessing as mp
import os, struct, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from lofi_checksum_crack import load_images, span_bufs, SPANS, _FIELDMODE_IDX
CRC16_PARAMS = [ # (name, poly_trunc, init, refin, xorout, check_of_"123456789")
# NOTE: refin polys are given in bit-reversed (truncated) form, used as-is.
('ARC', 0xA001, 0x0000, True, 0x0000, 0xBB3D),
('MODBUS', 0xA001, 0xFFFF, True, 0x0000, 0x4B37),
('USB', 0xA001, 0xFFFF, True, 0xFFFF, 0xB4C8),
('CCITT-F', 0x1021, 0xFFFF, False, 0x0000, 0x29B1),
('XMODEM', 0x1021, 0x0000, False, 0x0000, 0x31C3),
('KERMIT', 0x8408, 0x0000, True, 0x0000, 0x2189),
('X25', 0x8408, 0xFFFF, True, 0xFFFF, 0x906E),
('DNP', 0xA6BC, 0x0000, True, 0xFFFF, 0xEA82),
('GENIBUS', 0x1021, 0xFFFF, False, 0xFFFF, 0xD64E),
('GSM', 0x1021, 0x0000, False, 0xFFFF, 0xCE3C),
]
_tables16 = {}
def _tab16(poly_trunc, refin):
key = (poly_trunc, refin)
t = _tables16.get(key)
if t is not None:
return t
if refin:
# poly_trunc is already bit-reversed (e.g. 0xA001); use as-is.
p = poly_trunc
t = []
for i in range(256):
c = i
for _ in range(8):
c = (c >> 1) ^ p if c & 1 else c >> 1
t.append(c & 0xFFFF)
else:
p = poly_trunc
t = []
for i in range(256):
c = i << 8
for _ in range(8):
c = ((c << 1) ^ p) & 0xFFFF if c & 0x8000 else (c << 1) & 0xFFFF
t.append(c)
_tables16[key] = t
return t
def crc16(buf, poly, init, refin, xorout):
tab = _tab16(poly, refin)
crc = init
if refin:
for b in buf:
crc = tab[(crc ^ b) & 0xFF] ^ (crc >> 8)
else:
for b in buf:
crc = tab[((crc >> 8) ^ b) & 0xFF] ^ ((crc << 8) & 0xFFFF)
return (crc ^ xorout) & 0xFFFF
import array
def _even(buf):
return buf if len(buf) % 2 == 0 else buf + b'\x00'
def _words16(buf, endian):
a = array.array('H', _even(buf))
if endian == 'big':
a.byteswap()
return a
def ones_complement16(buf, endian):
mv = _words16(buf, endian)
s = sum(mv) & 0xFFFFFFFFFFFFFFFF
while s >> 16:
s = (s & 0xFFFF) + (s >> 16)
return (~s) & 0xFFFF
def wordsum16(buf, endian):
return sum(_words16(buf, endian)) & 0xFFFF
def bsd_sum(buf):
s = 0
for b in buf:
s = ((s >> 1) | ((s & 1) << 15)) & 0xFFFF
s = (s + b) & 0xFFFF
return s
def sysv_sum(buf):
s = sum(buf)
s = (s & 0xFFFF) + ((s >> 16) & 0xFFFF)
s = (s & 0xFFFF) + ((s >> 16) & 0xFFFF)
return s & 0xFFFF
def fnv1_32(buf):
h = 0x811C9DC5
for b in buf:
h = (h * 0x01000193) & 0xFFFFFFFF
h ^= b
return h
def djb2(buf):
h = 5381
for b in buf:
h = ((h * 33) + b) & 0xFFFFFFFF
return h
def murmur3_x86_32(buf, seed=0):
h = seed
n = len(buf) & ~3
for i in range(0, n, 4):
k = struct.unpack('<I', buf[i:i+4])[0]
k = (k * 0xCC9E2D51) & 0xFFFFFFFF
k = ((k << 15) | (k >> 17)) & 0xFFFFFFFF
k = (k * 0x1B873593) & 0xFFFFFFFF
h ^= k
h = ((h << 13) | (h >> 19)) & 0xFFFFFFFF
h = (h * 5 + 0xE6546B64) & 0xFFFFFFFF
tail = buf[n:]
k = 0
for i, b in enumerate(tail):
k |= b << (8 * i)
if tail:
k = (k * 0xCC9E2D51) & 0xFFFFFFFF
k = ((k << 15) | (k >> 17)) & 0xFFFFFFFF
k = (k * 0x1B873593) & 0xFFFFFFFF
h ^= k
h ^= len(buf)
h ^= h >> 16
h = (h * 0x85EBCA6B) & 0xFFFFFFFF
h ^= h >> 13
h = (h * 0xC2B2AE35) & 0xFFFFFFFF
h ^= h >> 16
return h
def match16(stored, v):
"""How a 16-bit value could sit in the 32-bit field."""
lo, hi = stored & 0xFFFF, (stored >> 16) & 0xFFFF
if lo == v == hi:
return 'both16'
if lo == v:
return 'lo16'
if hi == v:
return 'hi16'
if stored == v:
return 'full32eq16'
if stored == ((v << 16) | v):
return 'duplicated16'
return None
def build_jobs():
jobs = []
for name, poly, init, refin, xorout, _check in CRC16_PARAMS:
for span in SPANS:
for fmode in ('asis', 'zeroed', 'ff'):
jobs.append(('crc16', name, (poly, init, refin, xorout), span, fmode))
for span in SPANS:
for fmode in ('asis', 'zeroed', 'ff'):
for nm in ('ones_le', 'ones_be', 'wsum16_le', 'wsum16_be',
'bsd', 'sysv', 'fnv1', 'djb2', 'murmur0', 'murmurF'):
jobs.append(('fast32', nm, (), span, fmode))
for span in SPANS:
for fmode in ('asis', 'zeroed'):
for nm in ('chain_crc_of_crcs', 'chain_sum_of_crcs', 'chain_xor_of_crcs',
'halved_crc16_pair'):
jobs.append(('chain', nm, (), span, fmode))
return jobs
_G = None
def _init(blobs):
global _G
_G = blobs # [(ver, span->bufs[3], ck)]
FAST32 = {
'fnv1': fnv1_32, 'djb2': djb2,
'murmur0': lambda b: murmur3_x86_32(b, 0),
'murmurF': lambda b: murmur3_x86_32(b, 0xFFFFFFFF),
}
def _run(job):
kind, name, params, span, fmode = job
idx = _FIELDMODE_IDX[fmode]
bufs = [(span_bufs_alias(d, span, idx), ck) for _, d, ck in _G]
if kind == 'crc16':
poly, init, refin, xorout = params
got = [crc16(b, poly, init, refin, xorout) for b, _ in bufs]
how = [match16(ck, v) for (_, ck), v in zip(bufs, got)]
if all(how):
return f'HIT crc16/{name} span={span} field={fmode} val={got[0]:04X} as={how[0]}'
masks = [(ck ^ v) & 0xFFFF for (_, ck), v in zip(bufs, got)]
if masks[0] == masks[1] == masks[2]:
return (f'MASK16 crc16/{name} span={span} field={fmode} '
f'mask={masks[0]:04X}')
return None
if kind == 'fast32':
if name == 'ones_le':
got = [ones_complement16(b, 'little') for b, _ in bufs]
how = [match16(ck, v) for (_, ck), v in zip(bufs, got)]
if all(how):
return f'HIT ones-complement-LE span={span} field={fmode} as={how[0]}'
return None
if name == 'ones_be':
got = [ones_complement16(b, 'big') for b, _ in bufs]
how = [match16(ck, v) for (_, ck), v in zip(bufs, got)]
if all(how):
return f'HIT ones-complement-BE span={span} field={fmode} as={how[0]}'
return None
if name == 'wsum16_le':
got = [wordsum16(b, 'little') for b, _ in bufs]
elif name == 'wsum16_be':
got = [wordsum16(b, 'big') for b, _ in bufs]
elif name == 'bsd':
got = [bsd_sum(b) for b, _ in bufs]
elif name == 'sysv':
got = [sysv_sum(b) for b, _ in bufs]
else:
fn = FAST32[name]
got = [fn(b) for b, _ in bufs]
if all(ck == v for (_, ck), v in zip(bufs, got)):
return f'HIT {name} span={span} field={fmode} val={got[0]:08X}'
masks = [(ck ^ v) & 0xFFFFFFFF for (_, ck), v in zip(bufs, got)]
if masks[0] == masks[1] == masks[2]:
return f'MASK32 {name} span={span} field={fmode} mask={masks[0]:08X}'
return None
how = [match16(ck, v) for (_, ck), v in zip(bufs, got)]
if all(how):
return f'HIT {name} span={span} field={fmode} val={got[0]:04X} as={how[0]}'
return None
# chained per-sect schemes
nsects = [struct.unpack('<I', d[0x50:0x54])[0] for _, d, _ in _G]
crcs_per_image = []
for (_, d, _), n in zip(_G, nsects):
off, lst = 0x54, []
for _ in range(n):
ln = struct.unpack('<I', d[off+8:off+12])[0]
lst.append(binascii.crc32(d[off+12:off+12+ln]) & 0xFFFFFFFF)
off += 12 + ln
crcs_per_image.append(lst)
if name == 'chain_crc_of_crcs':
got = [binascii.crc32(struct.pack(f'<{len(c)}I', *c)) & 0xFFFFFFFF
for c in crcs_per_image]
elif name == 'chain_sum_of_crcs':
got = [sum(c) & 0xFFFFFFFF for c in crcs_per_image]
elif name == 'chain_xor_of_crcs':
got = []
for c in crcs_per_image:
x = 0
for v in c:
x ^= v
got.append(x)
elif name == 'halved_crc16_pair':
got = []
for b, _ in bufs:
h = len(b) // 2
a = crc16(b[:h], 0xA001, 0, True, 0)
c = crc16(b[h:], 0xA001, 0, True, 0)
got.append(((a << 16) | c) & 0xFFFFFFFF)
else:
return None
if all(ck == v for (_, ck), v in zip(bufs, got)):
return f'HIT {name} span={span} field={fmode} val={got[0]:08X}'
masks = [(ck ^ v) & 0xFFFFFFFF for (_, ck), v in zip(bufs, got)]
if masks[0] == masks[1] == masks[2]:
return f'MASK32 {name} span={span} field={fmode} mask={masks[0]:08X}'
return None
def span_bufs_alias(d, span, idx):
return span_bufs(d)[span][idx]
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--jobs', type=int, default=max(1, (os.cpu_count() or 4) - 1))
ap.add_argument('--out', default='/tmp/opencode/ck-phaseD.txt')
ap.add_argument('--limit', type=int, default=None)
a = ap.parse_args()
blobs = load_images()
packed = [(ver, d, ck) for ver, d, ck in blobs]
for ver, d, ck in blobs:
print(f'{ver}: size={len(d)} stored_ck={ck:08X}', flush=True)
jobs = build_jobs()
if a.limit:
jobs = jobs[:a.limit]
print(f'Phase D: {len(jobs)} combos, jobs={a.jobs}', flush=True)
t0 = time.time()
found, done = [], 0
total = len(jobs)
with mp.Pool(a.jobs, initializer=_init, initargs=(packed,)) as pool:
for res in pool.imap_unordered(_run, jobs, chunksize=4):
done += 1
if res:
print(f' *** {res}', flush=True)
found.append(res)
with open(a.out, 'a') as fh:
fh.write(res + '\n')
if done % 50 == 0 or done == total:
print(f' [{done}/{total}] elapsed={time.time()-t0:.0f}s',
flush=True)
print(f'Phase D done: {total} combos in {time.time()-t0:.0f}s, '
f'{len(found)} candidates.', flush=True)
if __name__ == '__main__':
main()
+139
View File
@@ -0,0 +1,139 @@
"""Shared parser/packer for Lofi-12 XT Sonicware .bin images.
Format: [cmtd 48B][mmtd 36B][sect xN -> EOF, exact fit]
Each sect: b'sect' | u32 LE load_addr | u32 LE len | payload[len]
All addresses are DDR2 (0xC2xxxxxx, TMS320C6748). Code is C674x DSP LE.
Stdlib only.
"""
import struct
import json
import binascii
CMTD_LEN = 48
MMTD_LEN = 36
# Checksum seed: cmtd+0x08 coverage replaces file bytes [8:12] with this
# constant before CRC32-IEEE (init 0). Proven 3/3 against stock images
# (solved independently per image via GF(2), all give this same value;
# it is also built literally in the bootloader: MVK 0x6282/MVKH 0xC27C).
CKSEED = 0xC27C6282
def compute_checksum(d: bytes) -> int:
"""Valid cmtd+0x08 for a complete image: CRC32 of the image with
bytes [8:12] replaced by CKSEED."""
b = bytearray(d)
b[8:12] = struct.pack('<I', CKSEED)
return binascii.crc32(bytes(b)) & 0xFFFFFFFF
def parse_image(d: bytes) -> dict:
assert d[0:4] == b'cmtd', "bad cmtd magic"
assert d[0x30:0x34] == b'mmtd', "bad mmtd magic"
fsize, cksum = struct.unpack('<II', d[4:12])
assert fsize == len(d), f"cmtd filesize {fsize} != actual {len(d)}"
cmtd = {
'filesize': fsize,
'checksum': cksum,
'raw_0x0C_0x30': d[0x0C:0x30].hex(),
'u32_0x10_0x30': list(struct.unpack('<8I', d[0x10:0x30])),
}
mmtd_rem, mmtd_flags, mmtd_ff = struct.unpack('<III', d[0x34:0x40])
fw = struct.unpack('<III', d[0x40:0x4C])
entry, nsect = struct.unpack('<II', d[0x4C:0x54])
mmtd = {
'remaining': mmtd_rem,
'flags': mmtd_flags,
'ff': mmtd_ff,
'fw': list(fw),
'entry': entry,
'nsect': nsect,
}
assert mmtd_rem == len(d) - CMTD_LEN, "mmtd remaining mismatch"
sects = []
off = 0x54
for i in range(nsect):
assert d[off:off+4] == b'sect', f"bad sect magic at {off:#x}"
addr, ln = struct.unpack('<II', d[off+4:off+12])
payload = d[off+12:off+12+ln]
assert len(payload) == ln, f"sect {i} truncated"
sects.append({'index': i, 'off': off, 'addr': addr, 'len': ln,
'end': addr + ln, 'payload': payload})
off += 12 + ln
assert off == len(d), f"sect chain ends at {off:#x}, EOF {len(d):#x}"
return {'cmtd': cmtd, 'mmtd': mmtd, 'sects': sects}
def build_image(meta: dict, payloads: list, checksum: int | None = None) -> bytes:
"""Rebuild image from meta (cmtd/mmtd dicts) + payload list [(addr, bytes)].
Recalculates filesize/remaining fields. Preserves entry/flags/fw unless
caller edited meta. Checksum: explicit value, 'keep' preserves the
original from meta, None (default) computes the valid checksum."""
nsect = len(payloads)
total = CMTD_LEN + MMTD_LEN + sum(12 + len(p) for _, p in payloads)
# checksum covers the whole file, so build with a zero placeholder,
# then finalize: explicit int wins, 'keep' preserves meta, None computes.
ck = 0
out = bytearray()
out += b'cmtd'
out += struct.pack('<II', total, ck)
out += bytes.fromhex(meta['cmtd']['raw_0x0C_0x30'])
# fix embedded filesize/remaining inside cmtd raw (last two u32s):
# raw layout: 8 x u32 at 0x10..0x30 = [12,1,3,maj,min,patch,48,remaining]
# patch remaining just in case caller changed payload sizes
u = list(struct.unpack('<8I', out[0x10:0x30]))
u[6] = CMTD_LEN
u[7] = total - CMTD_LEN
out[0x10:0x30] = struct.pack('<8I', *u)
out += b'mmtd'
out += struct.pack('<I', total - CMTD_LEN)
out += struct.pack('<II', meta['mmtd']['flags'], meta['mmtd']['ff'])
out += struct.pack('<III', *meta['mmtd']['fw'])
out += struct.pack('<II', meta['mmtd']['entry'], nsect)
for addr, p in payloads:
out += b'sect'
out += struct.pack('<II', addr, len(p))
out += p
assert len(out) == total
if checksum == 'keep':
final_ck = meta['cmtd']['checksum']
elif checksum is None:
final_ck = compute_checksum(bytes(out))
else:
final_ck = checksum
out[8:12] = struct.pack('<I', final_ck)
return bytes(out)
def headers_to_json(info: dict) -> dict:
return {
'cmtd': info['cmtd'],
'mmtd': {**info['mmtd'],
'entry_hex': f"{info['mmtd']['entry']:08X}",
'flags_hex': f"{info['mmtd']['flags']:08X}"},
'sects': [{'index': s['index'], 'off_hex': f"{s['off']:06X}",
'addr_hex': f"{s['addr']:08X}", 'len': s['len'],
'end_hex': f"{s['end']:08X}"} for s in info['sects']],
}
def find_sect_by_role(info: dict, role: str) -> dict:
"""role='code' -> largest sect; role='rodata' -> sect holding b'Threshold'
(fallback: second largest)."""
sects = info['sects']
if role == 'code':
return max(sects, key=lambda s: s['len'])
if role == 'rodata':
for s in sects:
if b'Threshold' in s['payload']:
return s
rest = sorted(sects, key=lambda s: s['len'], reverse=True)
return rest[1] if len(rest) > 1 else rest[0]
raise ValueError(role)
def code_file_off(info: dict, addr: int) -> int | None:
for s in info['sects']:
if s['addr'] <= addr < s['end']:
return s['off'] + 12 + (addr - s['addr'])
return None
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Repack directory (headers.json + sect*.bin) -> .bin. Verifies exact-fit chain."""
import argparse, json, glob, os, struct, sys
sys.path.insert(0, os.path.dirname(__file__))
from lofi_image import build_image
def main():
ap = argparse.ArgumentParser()
ap.add_argument('indir')
ap.add_argument('output')
ap.add_argument('--checksum', default=None,
help="'auto' (default: compute valid checksum), hex u32, or 'keep' (headers.json value)")
a = ap.parse_args()
meta = json.load(open(os.path.join(a.indir, 'headers.json')))
# cmtd raw hex -> rebuild path needs raw_0x0C_0x30; headers.json stores it
files = sorted(glob.glob(os.path.join(a.indir, 'sect*_addr*.bin')))
assert files, 'no sect files found'
payloads = []
for f in files:
base = os.path.basename(f)
addr = int(base.split('addr')[1].split('.')[0], 16)
payloads.append((addr, open(f, 'rb').read()))
if a.checksum is None or a.checksum == 'auto':
ck = None
elif a.checksum == 'keep':
ck = 'keep'
else:
ck = int(a.checksum, 16)
out = build_image(meta, payloads, checksum=ck)
open(a.output, 'wb').write(out)
print(f"wrote {a.output} ({len(out)} B) checksum={struct.unpack('<I', out[8:12])[0]:08X}")
if __name__ == '__main__':
main()
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Same-length UI string swap. Keeps every address stable (Level-0 mod).
Example: lofi_patch_string.py "Lofi-12 XT.bin" out.bin Threshold ThresholX
"""
import argparse, struct, sys, os
sys.path.insert(0, os.path.dirname(__file__))
from lofi_image import parse_image, build_image, find_sect_by_role
def main():
ap = argparse.ArgumentParser()
ap.add_argument('image')
ap.add_argument('output')
ap.add_argument('old')
ap.add_argument('new')
ap.add_argument('--sect', type=int, default=None)
ap.add_argument('--first-only', action='store_true')
a = ap.parse_args()
old_b, new_b = a.old.encode(), a.new.encode()
assert len(old_b) == len(new_b), \
f"lengths differ ({len(old_b)} vs {len(new_b)}); use equal length to keep addresses stable"
d = open(a.image, 'rb').read()
info = parse_image(d)
tgt = None
if a.sect is not None:
tgt = info['sects'][a.sect]
else:
tgt = find_sect_by_role(info, 'rodata')
hits = []
p = tgt['payload']
start = 0
while True:
i = p.find(old_b, start)
if i < 0:
break
hits.append(i)
start = i + 1
if not hits:
sys.exit(f"'{a.old}' not found in sect{tgt['index']} (addr {tgt['addr']:08X})")
print(f"found {len(hits)} hit(s) in sect{tgt['index']} at offsets: "
+ ', '.join(f"+{h:#x} (file {tgt['off']+12+h:#x})" for h in hits))
if a.first_only:
hits = hits[:1]
np = bytearray(p)
for h in hits:
np[h:h+len(old_b)] = new_b
payloads = [(s['addr'], bytes(np) if s['index'] == tgt['index'] else s['payload'])
for s in info['sects']]
out = build_image(info, payloads) # recomputes valid checksum
open(a.output, 'wb').write(out)
print(f"wrote {a.output} with fresh checksum "
f"{struct.unpack('<I', out[8:12])[0]:08X} — verify on device.")
if __name__ == '__main__':
main()
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Unpack Lofi-12 XT .bin -> directory with headers.json + sect payloads."""
import argparse, json, os, struct, sys
sys.path.insert(0, os.path.dirname(__file__))
from lofi_image import parse_image, headers_to_json
def main():
ap = argparse.ArgumentParser()
ap.add_argument('image')
ap.add_argument('outdir')
a = ap.parse_args()
d = open(a.image, 'rb').read()
info = parse_image(d)
os.makedirs(a.outdir, exist_ok=True)
open(os.path.join(a.outdir, 'headers.json'), 'w').write(
json.dumps(headers_to_json(info), indent=2))
for s in info['sects']:
fn = f"sect{s['index']}_addr{s['addr']:08X}.bin"
open(os.path.join(a.outdir, fn), 'wb').write(s['payload'])
m = info['mmtd']
print(f"fw {tuple(m['fw'])} entry={m['entry']:08X} nsect={m['nsect']} "
f"cksum={info['cmtd']['checksum']:08X} size={len(d)}")
for s in info['sects']:
print(f" sect{s['index']} off={s['off']:06X} addr={s['addr']:08X} "
f"len={s['len']} end={s['end']:08X}")
print(f"wrote {a.outdir}/")
if __name__ == '__main__':
main()
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""xref + slack scanner: LE32 pointers into code/rodata, NUL strings, zero gaps.
Usage: lofi_xref.py "Lofi-12 XT.bin" [--find TEXT] [--strings-min 6]
"""
import argparse, os, struct, sys
sys.path.insert(0, os.path.dirname(__file__))
from lofi_image import parse_image, find_sect_by_role
def le_words(payload):
for i in range(0, len(payload) - 3, 4):
yield i, struct.unpack('<I', payload[i:i+4])[0]
def zero_runs(payload, minlen=32):
out, i, n = [], 0, len(payload)
while i < n:
if payload[i] == 0:
j = i
while j < n and payload[j] == 0:
j += 1
if j - i >= minlen:
out.append((i, j - i))
i = j
else:
i += 1
return out
def c_strings(payload, minlen=6):
out, i, n = [], 0, len(payload)
while i < n:
if 32 <= payload[i] < 127:
j = i
while j < n and 32 <= payload[j] < 127:
j += 1
if j - i >= minlen and j < n and payload[j] == 0:
out.append((i, payload[i:j].decode()))
i = max(j, i + 1)
else:
i += 1
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument('image')
ap.add_argument('--find', default=None)
ap.add_argument('--strings-min', type=int, default=6)
ap.add_argument('--slack-min', type=int, default=64)
a = ap.parse_args()
d = open(a.image, 'rb').read()
info = parse_image(d)
code = find_sect_by_role(info, 'code')
ro = find_sect_by_role(info, 'rodata')
print(f"code: sect{code['index']} {code['addr']:08X}..{code['end']:08X} len={code['len']}")
print(f"rodata: sect{ro['index']} {ro['addr']:08X}..{ro['end']:08X} len={ro['len']}")
# pointers in rodata -> code, and rodata -> self
to_code = to_self = 0
code_hits = []
for off, w in le_words(ro['payload']):
if code['addr'] <= w < code['end']:
to_code += 1
code_hits.append((off, w))
elif ro['addr'] <= w < ro['end']:
to_self += 1
print(f"rodata xrefs: {to_code} -> code, {to_self} -> self")
gaps = zero_runs(ro['payload'], a.slack_min)
gaps.sort(key=lambda t: -t[1])
print(f"top zero gaps in rodata (min {a.slack_min}):")
for off, ln in gaps[:10]:
print(f" +{off:#x} (file {ro['off']+12+off:#x}) len={ln}")
if a.find:
needle = a.find.encode()
hits = []
p = ro['payload']
s = 0
while True:
i = p.find(needle, s)
if i < 0:
break
hits.append(i)
s = i + 1
print(f"'{a.find}': {len(hits)} hit(s) in rodata")
for h in hits:
faddr = ro['addr'] + h
refs = [off for off, w in code_hits if False] # placeholder
# find pointers TO this string: scan rodata words == faddr
# (vtable-adjacent tables) — cheap exact scan
ptrs = []
for off, w in le_words(ro['payload']):
if w == faddr:
ptrs.append(ro['off'] + 12 + off)
print(f" +{h:#x} file={ro['off']+12+h:#x} load={faddr:08X} "
f"referenced_by_{len(ptrs)}={['%#x' % x for x in ptrs[:8]]}")
else:
strs = c_strings(ro['payload'], a.strings_min)
print(f"NUL strings len>={a.strings_min} in rodata: {len(strs)}")
if __name__ == '__main__':
main()
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Verify cmtd+0x08 checksums: prove_checksum.py a.bin [b.bin ...]
Exit 0 iff every image's stored checksum equals compute_checksum(image).
See lofi_image.compute_checksum for the algorithm.
"""
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(__file__))
from lofi_image import compute_checksum, parse_image
def main() -> int:
ok = True
for path in sys.argv[1:]:
d = open(path, "rb").read()
info = parse_image(d) # validates container chain
stored = info["cmtd"]["checksum"]
calc = compute_checksum(d)
match = stored == calc
ok &= match
fw = ".".join(map(str, info["mmtd"]["fw"]))
print("%s fw=%s size=%d stored=%08X calc=%08X %s"
% (path, fw, len(d), stored, calc,
"MATCH" if match else "MISMATCH"))
return 0 if ok and len(sys.argv) > 1 else 1
if __name__ == "__main__":
sys.exit(main())