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
104 lines
3.7 KiB
Python
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()) |