Files
qunexus-firmware/verify_patch_asm.py
nils ae2de3a787 Add recompilable sdas8051 source; round-trips to stock v2.2.1 binary
roundtrip.py converts the radare2 disassembly (qunexus_v2.2.1.asm) into a
single sdas8051 assembler source (firmware.asm) and verifies it re-assembles
byte-identical to the 54,279-byte stock image (base 0x2400).

  31,051 of 31,346 items (99.06%) re-assemble from mnemonics; 295 are
  pinned to .db where r2's display syntax doesn't round-trip through
  sdas8051 (256 ajmp/acall that sdas8051 mis-encodes, 39 data-in-code).
  Converges in 3 iterations.

Also adds patch_usb1_to_cv.asm (the USB-1->CV patch as a standalone
sdas8051 source) and verify_patch_asm.py (proves the assembled patch
matches the bytes written to the device over C2).

Reproduce:  python3 roundtrip.py
2026-08-17 23:00:36 +02:00

104 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""verify_patch_asm.py -- prove the sdas8051-assembled patch matches the
bytes actually written to the device by patch_c2.py.
Pipeline:
1. Parse patch_usb1_to_cv.lst (sdas8051 listing) -> {addr: bytes}.
2. Extract the assembled stub (0x8126, 23 B) and retarget (0xDF28, 3 B).
3. Compare against patch_usb1_to_cv.STUB / NEW_CALL (the authoritative
bytes patch_c2.py verified on the device over C2).
4. Splice the assembled bytes into the stock binary and diff the result
against the expected patched image (stock + STUB + NEW_CALL).
Run from this directory:
sdas8051 -l -s patch_usb1_to_cv.asm
python3 verify_patch_asm.py
"""
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
FWTOOLS = HERE.parent
sys.path.insert(0, str(FWTOOLS))
import patch_usb1_to_cv as p # noqa: E402
BASE = 0x2400 # app image load address (flat binary base)
def parse_lst_bytes(lst_path):
"""Return {addr: bytes} for every instruction/data line in the listing."""
out = {}
# " 008126 12 A5 7B [24] 30 lcall ..."
line_re = re.compile(r'^\s+([0-9a-fA-F]{6})\s+([0-9a-fA-F]{2}(?:\s+[0-9a-fA-F]{2})*)\s+\[\d+\]')
for ln in Path(lst_path).read_text(errors="replace").splitlines():
m = line_re.match(ln)
if not m:
continue
addr = int(m.group(1), 16)
bs = bytes(int(b, 16) for b in m.group(2).split())
out[addr] = bs
return out
def splice(stock, addr, data):
"""Overlay `data` at file offset (addr - BASE) in a copy of stock."""
off = addr - BASE
img = bytearray(stock)
img[off:off + len(data)] = data
return bytes(img)
def main():
lst = HERE / "patch_usb1_to_cv.lst"
if not lst.exists():
sys.exit("missing patch_usb1_to_cv.lst -- run: sdas8051 -l -s patch_usb1_to_cv.asm")
mem = parse_lst_bytes(lst)
# Assembled patch bytes (contiguous).
asm_stub = mem[p.STUB_ADDR]
asm_call = mem[p.CALL_SITE]
# Sanity: stub must be exactly 23 contiguous bytes 0x8126..0x813C.
expected_stub_len = len(p.STUB)
assert len(asm_stub) == 3, asm_stub # first instruction is lcall (3 B); listing gives per-line bytes
# Reassemble the full stub by walking consecutive listing entries.
stub_bytes = b""
a = p.STUB_ADDR
while len(stub_bytes) < expected_stub_len:
chunk = mem[a]
stub_bytes += chunk
a += len(chunk)
assert len(stub_bytes) == expected_stub_len, (len(stub_bytes), expected_stub_len)
print("assembled stub :", stub_bytes.hex(' '))
print("expected STUB :", p.STUB.hex(' '))
print("assembled call :", asm_call.hex(' '))
print("expected CALL :", p.NEW_CALL.hex(' '))
ok_stub = stub_bytes == bytes(p.STUB)
ok_call = asm_call == bytes(p.NEW_CALL)
print(f"\nstub match : {ok_stub}")
print(f"call match : {ok_call}")
# Full-image diff: stock + assembled patch vs stock + authoritative patch.
stock = (HERE / "QuNexus_Firmware_v2.2.1.bin").read_bytes()
img_asm = splice(stock, p.STUB_ADDR, stub_bytes)
img_asm = splice(img_asm, p.CALL_SITE, asm_call)
img_exp = splice(stock, p.STUB_ADDR, bytes(p.STUB))
img_exp = splice(img_exp, p.CALL_SITE, bytes(p.NEW_CALL))
print(f"\nstock size : {len(stock)} bytes")
print(f"patched (asm) : {len(img_asm)} bytes")
print(f"patched (expect): {len(img_exp)} bytes")
print(f"full-image match: {img_asm == img_exp}")
if ok_stub and ok_call and img_asm == img_exp:
print("\nPASS: sdas8051-assembled patch is byte-identical to the patch "
"written to the device.")
return 0
print("\nFAIL")
return 1
if __name__ == "__main__":
sys.exit(main())