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,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assemble the fully-russified RT-4D app image (bold font v2).
|
||||
- narrow single-byte CP1251 render (dispatch patch + blitter code-cave)
|
||||
- bold uppercase Cyrillic font: 13 letters reuse stock ASCII glyphs, 20 drawn
|
||||
- all 485 menu/option records translated to uppercase Russian
|
||||
All patches in the recoverable MCU app region; no SPI writes.
|
||||
(Selection-highlight patch intentionally omitted pending correct RE.)
|
||||
"""
|
||||
import struct, json
|
||||
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB
|
||||
import glyphs_ru2 as GR
|
||||
|
||||
BASE = 0x08002800
|
||||
CAVE_ADDR = 0x08028860
|
||||
CYR_BASE = 0x08028900
|
||||
SPI_READ = 0x08021828
|
||||
IMG_END = 0x08029000
|
||||
ROOT = "C:/Users/vikto/Documents/Claude/rt-4d"
|
||||
DUMP = f"{ROOT}/radio-spi-dump.bin"
|
||||
ASCII_FONT = 0x19C000
|
||||
|
||||
def enc_glyph(rows):
|
||||
out = bytearray()
|
||||
for c in range(7):
|
||||
v = sum((1 << r) for r in range(14) if c < len(rows[r]) and rows[r][c] == '#')
|
||||
out += struct.pack('<H', v)
|
||||
return bytes(out)
|
||||
|
||||
def main():
|
||||
stock = open(f"{ROOT}/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin", "rb").read()
|
||||
dump = open(DUMP, "rb").read()
|
||||
ru = json.load(open(f"{ROOT}/menu_ru.json", encoding="utf-8"))
|
||||
img = bytearray(b"\xff" * (IMG_END - BASE)); img[:len(stock)] = stock
|
||||
def w(va, data): img[va-BASE:va-BASE+len(data)] = data
|
||||
def ascii_glyph(ch):
|
||||
a = ASCII_FONT + (ord(ch)-0x20)*14
|
||||
return dump[a:a+14]
|
||||
|
||||
ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
|
||||
cave_asm = f"""
|
||||
movw r3,#0xC540
|
||||
movt r3,#0x0019
|
||||
cmp r1,r3
|
||||
blo do_spi
|
||||
subs r1,r1,r3
|
||||
movw r3,#{CYR_BASE & 0xFFFF}
|
||||
movt r3,#{CYR_BASE >> 16}
|
||||
adds r1,r1,r3
|
||||
copy:
|
||||
ldrb r3,[r1]
|
||||
strb r3,[r0]
|
||||
adds r1,r1,#1
|
||||
adds r0,r0,#1
|
||||
subs r2,r2,#1
|
||||
bne copy
|
||||
bx lr
|
||||
do_spi:
|
||||
movw r3,#{SPI_READ & 0xFFFF}
|
||||
movt r3,#{SPI_READ >> 16}
|
||||
orr r3,r3,#1
|
||||
bx r3
|
||||
"""
|
||||
cave = bytes(ks.asm(cave_asm, CAVE_ADDR)[0]); w(CAVE_ADDR, cave)
|
||||
w(0x08007FDA, bytes(ks.asm(f"bl #{CAVE_ADDR}", 0x08007FDA)[0]))
|
||||
assert img[0x08008A6A-BASE] == 0x80; img[0x08008A6A-BASE] = 0xFF
|
||||
|
||||
# bold Cyrillic font (reuse stock ASCII bytes where identical) --------------
|
||||
for ch in GR.LETTERS:
|
||||
b = ch.encode("cp1251")[0]
|
||||
g = ascii_glyph(GR.REUSE[ch]) if ch in GR.REUSE else enc_glyph(GR.unique_rows(ch))
|
||||
w(CYR_BASE + (b-0x80)*14, g)
|
||||
|
||||
# translate menu records ---------------------------------------------------
|
||||
START = 0x080253ED - BASE; END = 0x0802723D - BASE
|
||||
changed = kept = 0
|
||||
for i in range((END-START)//16):
|
||||
off = START + i*16; r = bytes(img[off:off+16])
|
||||
if r[14:16].isdigit():
|
||||
label = r[:14].rstrip(b' \x00').decode('latin1'); idd = r[14:16]; maxc = 14
|
||||
else:
|
||||
label = r.rstrip(b' \x00').decode('latin1'); idd = b''; maxc = 16
|
||||
if not label or label not in ru: kept += 1; continue
|
||||
rus = ru[label].encode('cp1251'); assert len(rus) <= maxc
|
||||
rec = rus + b' '*(maxc-len(rus)) + idd; assert len(rec) == 16
|
||||
w(BASE+off, rec); changed += 1
|
||||
|
||||
# translate scattered rodata display strings — ALL occurrences (in place,
|
||||
# keep NUL terminator). Lookup covers both menu and rodata translations.
|
||||
rod = json.load(open(f"{ROOT}/rodata_ru.json", encoding="utf-8"))
|
||||
allx = json.load(open(f"{ROOT}/rodata_all.json", encoding="utf-8"))
|
||||
lut = {**ru, **rod}
|
||||
rchg = rskip = 0
|
||||
for it in allx:
|
||||
va, text, maxc = it["va"], it["text"], it["maxc"]
|
||||
rus = lut.get(text)
|
||||
if not rus or rus == text:
|
||||
rskip += 1; continue
|
||||
b = rus.encode("cp1251")
|
||||
if len(b) > maxc or bytes(img[va-BASE:va-BASE+maxc]) != text.encode("latin1"):
|
||||
rskip += 1; continue
|
||||
w(va, b + b" "*(maxc-len(b)))
|
||||
rchg += 1
|
||||
|
||||
# inline fixed-length strings (not NUL-terminated) — exact-length patch
|
||||
INLINE = {
|
||||
0x0800C767: (" Remote Stun: ", " УДАЛ.БЛОК: "),
|
||||
0x0800C77C: ("Remote Stun: ", "УДАЛ.БЛОК: "),
|
||||
0x0800C790: ("Remote Kill: ", "УДАЛ.ОТКЛ: "),
|
||||
0x0800C7B8: ("Wake Up: ", "БУДИЛ: "),
|
||||
0x08028705: ("Freq Mode", "ЧАСТОТА"),
|
||||
0x0802871E: ("DTMF Input: _", "DTMF ВВОД: _"),
|
||||
0x0802872E: ("Dial Number", "НАБОР НОМ."),
|
||||
0x08028767: ("VFO MODE", "VFO РЕЖ"),
|
||||
0x08028815: ("Unknown", "НЕИЗВ."),
|
||||
0x0801FB44: ("Unread SMS : ","НЕПРОЧ.СМС :"),
|
||||
0x08017AB8: ("Sending", "ОТПРАВ."),
|
||||
}
|
||||
ichg = 0
|
||||
for va, (en, rus) in INLINE.items():
|
||||
b = rus.encode("cp1251"); assert len(b) <= len(en), (en, rus)
|
||||
if bytes(img[va-BASE:va-BASE+len(en)]) != en.encode("latin1"):
|
||||
continue
|
||||
w(va, b + b" "*(len(en)-len(b)))
|
||||
ichg += 1
|
||||
|
||||
# version rebrand: "RT-4D V3.25" -> "Ru-4D V3.25"
|
||||
vva = 0x0800BA88
|
||||
if bytes(img[vva-BASE:vva-BASE+16]) == b"VER :RT-4D V3.25":
|
||||
w(vva, b"VER :Ru-4D V3.25")
|
||||
|
||||
assert struct.unpack_from("<II", img, 0) == struct.unpack_from("<II", stock, 0)
|
||||
out = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
|
||||
open(out, "wb").write(img)
|
||||
print(f"cave {len(cave)}B | font {len(GR.LETTERS)} glyphs | menu {changed} | "
|
||||
f"rodata occ {rchg} (skip {rskip}) | inline {ichg}")
|
||||
print(f"image {len(img)}B -> {out}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Ссылка в новой задаче
Block a user