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,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""First-look recon of the RT-4D 4MB SPI dump."""
|
||||
import sys, string
|
||||
|
||||
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
|
||||
data = open(DUMP, "rb").read()
|
||||
N = len(data)
|
||||
print(f"== FILE == {N} bytes (0x{N:X}) = {N/1024/1024:.2f} MB\n")
|
||||
|
||||
# Known map (from jcalado constants.py)
|
||||
SPI_REGIONS = {
|
||||
"calibration": (0x000000, 0x001000, 0x40),
|
||||
"main_settings": (0x002000, 0x001000, 0x90),
|
||||
"channels": (0x004000, 0x00C000, 0x91),
|
||||
"zones": (0x01C000, 0x020000, 0x92),
|
||||
"contacts": (0x05C000, 0x010000, 0x93),
|
||||
"groups": (0x07C000, 0x003000, 0x94),
|
||||
"dmr_keys": (0x082000, 0x003000, 0x95),
|
||||
"call_log": (0x088000, 0x00C000, 0x96),
|
||||
"default_sms": (0x094000, 0x001000, 0x97),
|
||||
"msg_drafts": (0x095000, 0x010000, None),
|
||||
"msg_inbox": (0x0A5000, 0x010000, None),
|
||||
"msg_outbox": (0x0B5000, 0x010000, None),
|
||||
"schedules": (0x0C6000, 0x008000, 0x98),
|
||||
"dtmf_names": (0x0C7000, 0x000100, 0x80),
|
||||
"fm_settings": (0x0D6000, 0x001000, 0x99),
|
||||
}
|
||||
|
||||
def classify(b):
|
||||
if all(x == 0xFF for x in b): return "empty(FF)"
|
||||
if all(x == 0x00 for x in b): return "zero(00)"
|
||||
return "DATA"
|
||||
|
||||
# ---- occupancy map at 4KB granularity ----
|
||||
PAGE = 0x1000
|
||||
pages = N // PAGE
|
||||
kinds = [classify(data[i*PAGE:(i+1)*PAGE]) for i in range(pages)]
|
||||
# collapse to runs
|
||||
print("== OCCUPANCY (4KB pages) ==")
|
||||
runs = []
|
||||
s = 0
|
||||
for i in range(1, pages+1):
|
||||
if i == pages or kinds[i] != kinds[s]:
|
||||
runs.append((s*PAGE, i*PAGE, kinds[s]))
|
||||
s = i
|
||||
data_bytes = sum(1 for k in kinds if k == "DATA")*PAGE
|
||||
for a, b, k in runs:
|
||||
tag = ""
|
||||
if k == "DATA":
|
||||
# which known regions overlap
|
||||
names = [n for n,(ra,rs,_) in SPI_REGIONS.items() if ra < b and ra+rs > a]
|
||||
tag = " <- " + (",".join(names) if names else "??? UNKNOWN")
|
||||
print(f" 0x{a:06X}-0x{b:06X} {(b-a)//1024:>5}KB {k}{tag}")
|
||||
print(f"\n DATA pages total: {data_bytes/1024:.0f}KB of {N/1024:.0f}KB")
|
||||
|
||||
# ---- known region occupancy check ----
|
||||
print("\n== KNOWN REGIONS (populated?) ==")
|
||||
for name,(addr,size,rid) in SPI_REGIONS.items():
|
||||
if addr+size > N:
|
||||
print(f" {name:14} 0x{addr:06X} OUT OF RANGE"); continue
|
||||
reg = data[addr:addr+size]
|
||||
nonff = sum(1 for x in reg if x != 0xFF)
|
||||
first = reg[:12].hex(' ')
|
||||
rids = "None" if rid is None else hex(rid)
|
||||
print(f" {name:14} 0x{addr:06X}+0x{size:05X} rid={rids:>4} nonFF={nonff:>6}/{size:<6} first={first}")
|
||||
|
||||
# ---- DTCN magic (settings bank detection) ----
|
||||
print("\n== DTCN magic (beta bank markers) ==")
|
||||
for off in (0x002FFC, 0x003FFC):
|
||||
print(f" @0x{off:06X}: {data[off:off+4]!r}")
|
||||
mcount = data.count(b'DTCN')
|
||||
print(f" total 'DTCN' occurrences in dump: {mcount}")
|
||||
|
||||
# ---- calibration hexdump ----
|
||||
print("\n== CALIBRATION region 0x000000..0x000100 ==")
|
||||
for r in range(0, 0x100, 16):
|
||||
chunk = data[r:r+16]
|
||||
hexs = ' '.join(f'{x:02X}' for x in chunk)
|
||||
asc = ''.join(chr(x) if 32<=x<127 else '.' for x in chunk)
|
||||
print(f" {r:04X} {hexs} {asc}")
|
||||
|
||||
# ---- address book / high region probe ----
|
||||
print("\n== HIGH REGION (0x0E0000+) probe ==")
|
||||
for addr in (0x0D7000, 0x0E0000, 0x100000, 0x200000, 0x300000, 0x3F0000):
|
||||
reg = data[addr:addr+64]
|
||||
nonff = sum(1 for x in reg if x != 0xFF)
|
||||
asc = ''.join(chr(x) if 32<=x<127 else '.' for x in reg[:32])
|
||||
print(f" 0x{addr:06X} nonFF={nonff}/64 ascii={asc!r}")
|
||||
|
||||
# ---- strings ----
|
||||
print("\n== STRINGS (printable runs >=4, first 60) ==")
|
||||
printable = set(bytes(string.printable[:-6], 'ascii'))
|
||||
runs_s = []
|
||||
cur = bytearray(); start = 0
|
||||
for i,x in enumerate(data):
|
||||
if 32 <= x < 127:
|
||||
if not cur: start = i
|
||||
cur.append(x)
|
||||
else:
|
||||
if len(cur) >= 4: runs_s.append((start, bytes(cur)))
|
||||
cur = bytearray()
|
||||
if len(cur) >= 4: runs_s.append((start, bytes(cur)))
|
||||
print(f" total string runs: {len(runs_s)}")
|
||||
for off, s in runs_s[:60]:
|
||||
try: txt = s.decode('ascii')
|
||||
except: txt = repr(s)
|
||||
print(f" 0x{off:06X} {txt}")
|
||||
|
||||
# ---- first channel records ----
|
||||
print("\n== CHANNEL records @0x004000 (48 bytes each) ==")
|
||||
CH = 0x004000; SZ = 48
|
||||
for i in range(6):
|
||||
rec = data[CH+i*SZ:CH+i*SZ+SZ]
|
||||
if all(x==0xFF for x in rec):
|
||||
print(f" ch{i}: <empty>"); continue
|
||||
print(f" ch{i}: {rec.hex(' ')}")
|
||||
asc = ''.join(chr(x) if 32<=x<127 else '.' for x in rec)
|
||||
print(f" ascii: {asc}")
|
||||
Ссылка в новой задаче
Block a user