RT-4D: реверс прошивки, русификация, кастомный UI, флешеры
- Полный RE стока V3.25 (Cortex-M4F) + FM100B: карта памяти, протокол, codeplug, UI-архитектура - Русификация: свой CP1251-шрифт + патч рендера, перевод меню и надписей, ребренд Ru-4D V3.25 - Блюпринт переделки UI + C-тулчейн (clang thumbv7em), доказан инъекцией - Готовые флешеры: WebSerial .html и Windows .exe со вшитой прошивкой - Дамп SPI рации, стоковая прошивка, инструменты сборки Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этот коммит содержится в:
@@ -0,0 +1,82 @@
|
||||
#!/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 <in.bin> <out.bin> <patchset>
|
||||
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("<II", data, 0)
|
||||
rep = apply_patches(data, patches)
|
||||
# invariants
|
||||
assert len(data) == n0, "size changed!"
|
||||
sp1, rst1 = struct.unpack_from("<II", data, 0)
|
||||
assert (sp1, rst1) == (sp0, rst0), "vector table (SP/reset) changed!"
|
||||
open(outp, "wb").write(data)
|
||||
print(f"[i] patchset '{setname}': {len(rep)} patch(es) applied")
|
||||
for va, orig, new in rep:
|
||||
print(f" 0x{va:08X}: {orig.hex(' ')} -> {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()
|
||||
Ссылка в новой задаче
Block a user