#!/usr/bin/env python3 """RT-4D firmware binary patcher. Applies in-place, equal-size byte patches to the stock V3.25 app image (base 0x08002800). All patches are verified against expected original bytes before writing, the file size is asserted unchanged, and the Cortex-M vector table (SP + reset) is re-checked afterwards so a bad patch can never silently corrupt the load image. Usage: python patch_fw.py patchset = name of a list defined in PATCHSETS below (e.g. 'rebrand') """ import sys, struct BASE = 0x08002800 # Each patch: (virtual_address, expected_original_bytes, new_bytes) # new_bytes MUST equal len(expected) — size never changes. PATCHSETS = { # Warm-up: flip the version-string NUL terminator to 'c' so the # radio shows "RT-4D V3.25c" — a 1-byte, fully reversible marker # that proves the patch->flash pipeline end to end. "rebrand": [ (0x0800BA98, b"\x00", b"c"), ], # Visible warm-up: the on-screen version field is fixed 16-wide, so a # trailing char is clipped. Change the last VISIBLE glyph '5' -> 'C' # so the radio shows "RT-4D V3.2C" — same length, in-field, unmistakable. "rebrand2": [ (0x0800BA97, b"5", b"C"), ], # Russification PoC: menu item "Zone Set 06" -> "Зоны" (GBK A7-row # Cyrillic, 8 bytes) + 6 spaces + preserved 2-byte id "06". Stays exactly # 16 bytes. Resolves the A7 byte->glyph mapping on-target. "ru_poc": [ (0x0802543D, b"Zone Set 06", bytes.fromhex("a7a9a7e0a7dfa7ed") + b" " + b"06"), ], } def apply_patches(data: bytearray, patches): report = [] for va, orig, new in patches: assert len(orig) == len(new), f"length mismatch at 0x{va:08X}" off = va - BASE assert 0 <= off < len(data), f"0x{va:08X} out of range" cur = bytes(data[off:off + len(orig)]) if cur != orig: raise SystemExit( f"[E] verify failed at 0x{va:08X}: have {cur.hex(' ')}, " f"expected {orig.hex(' ')} (wrong input or already patched)") data[off:off + len(new)] = new report.append((va, orig, new)) return report def main(): if len(sys.argv) != 4: raise SystemExit(__doc__) inp, outp, setname = sys.argv[1], sys.argv[2], sys.argv[3] patches = PATCHSETS[setname] data = bytearray(open(inp, "rb").read()) n0 = len(data) sp0, rst0 = struct.unpack_from(" {new.hex(' ')}") print(f"[i] size unchanged: {n0} bytes; vectors intact " f"(SP=0x{sp1:08X} reset=0x{rst1:08X})") print(f"[i] wrote {outp}") if __name__ == "__main__": main()