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,141 @@
|
||||
import re, sys, json
|
||||
|
||||
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
|
||||
data = open(DUMP, "rb").read()
|
||||
assert len(data) == 4194304
|
||||
|
||||
REGIONS = [
|
||||
("calibration", 0x000000, 0x001000),
|
||||
("main_settings", 0x002000, 0x001000),
|
||||
("channels", 0x004000, 0x00C000),
|
||||
("zones", 0x01C000, 0x020000),
|
||||
("contacts", 0x05C000, 0x010000),
|
||||
("groups", 0x07C000, 0x003000),
|
||||
("dmr_keys", 0x082000, 0x003000),
|
||||
("call_log", 0x088000, 0x00C000),
|
||||
("default_sms", 0x094000, 0x001000),
|
||||
("msg_drafts", 0x095000, 0x010000),
|
||||
("msg_inbox", 0x0A5000, 0x010000),
|
||||
("msg_outbox", 0x0B5000, 0x010000),
|
||||
("schedules", 0x0C6000, 0x008000),
|
||||
("dtmf_names", 0x0C7000, 0x000100),
|
||||
("fm_settings", 0x0D6000, 0x001000),
|
||||
]
|
||||
def region_of(addr):
|
||||
for name, a, s in REGIONS:
|
||||
if a <= addr < a+s:
|
||||
return name
|
||||
return "UNMAPPED"
|
||||
|
||||
# printable set for ascii
|
||||
def is_print(b):
|
||||
return 0x20 <= b <= 0x7E
|
||||
|
||||
def looks_real(s):
|
||||
"""Filter out punctuation-run / repeated-char noise."""
|
||||
s2 = s.strip()
|
||||
if len(s2) < 3:
|
||||
return False
|
||||
# must contain at least one letter or digit
|
||||
if not re.search(r"[A-Za-z0-9一-鿿]", s2):
|
||||
return False
|
||||
# count alnum ratio
|
||||
alnum = sum(1 for c in s2 if c.isalnum() or ord(c) > 0x2000)
|
||||
if alnum / len(s2) < 0.4:
|
||||
return False
|
||||
# reject if single char repeated (e.g. "7777", "((((")
|
||||
uniq = set(s2)
|
||||
if len(uniq) <= 2 and len(s2) >= 4:
|
||||
# allow things like "10" no; but "AA" reject; require >2 unique for long
|
||||
return False
|
||||
# reject runs of same punctuation
|
||||
if re.fullmatch(r"(.)\1*", s2):
|
||||
return False
|
||||
return True
|
||||
|
||||
results = [] # (addr, encoding, region, text)
|
||||
|
||||
# ---- ASCII runs (min 3) ----
|
||||
run_start = None
|
||||
for i, b in enumerate(data):
|
||||
if is_print(b):
|
||||
if run_start is None:
|
||||
run_start = i
|
||||
else:
|
||||
if run_start is not None:
|
||||
s = data[run_start:i].decode("latin-1")
|
||||
if len(s) >= 3 and looks_real(s):
|
||||
results.append((run_start, "ascii", region_of(run_start), s))
|
||||
run_start = None
|
||||
if run_start is not None:
|
||||
s = data[run_start:len(data)].decode("latin-1")
|
||||
if len(s) >= 3 and looks_real(s):
|
||||
results.append((run_start, "ascii", region_of(run_start), s))
|
||||
|
||||
# ---- UTF-16LE runs ----
|
||||
# pattern: printable-ascii byte followed by 0x00, repeated
|
||||
i = 0
|
||||
n = len(data)
|
||||
while i < n-1:
|
||||
if is_print(data[i]) and data[i+1] == 0x00:
|
||||
j = i
|
||||
chars = []
|
||||
while j < n-1 and data[j+1] == 0x00 and (is_print(data[j]) or data[j]==0x00):
|
||||
if data[j] == 0x00:
|
||||
break
|
||||
chars.append(chr(data[j]))
|
||||
j += 2
|
||||
s = "".join(chars)
|
||||
if len(s) >= 3 and looks_real(s):
|
||||
results.append((i, "utf16le", region_of(i), s))
|
||||
i = j + 2 if j > i else i+2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# ---- GBK / chinese: try decode ascii runs that had high bytes? separate scan ----
|
||||
# Scan for GBK multibyte sequences (lead 0x81-0xFE, trail 0x40-0xFE)
|
||||
def gbk_scan():
|
||||
out=[]
|
||||
i=0
|
||||
while i < n:
|
||||
b=data[i]
|
||||
# start a candidate if ascii-print or gbk lead
|
||||
if is_print(b) or (0x81<=b<=0xFE and i+1<n and 0x40<=data[i+1]<=0xFE and data[i+1]!=0x7F):
|
||||
start=i
|
||||
raw=bytearray()
|
||||
while i<n:
|
||||
b=data[i]
|
||||
if is_print(b):
|
||||
raw.append(b); i+=1
|
||||
elif 0x81<=b<=0xFE and i+1<n and 0x40<=data[i+1]<=0xFE and data[i+1]!=0x7F:
|
||||
raw.append(b); raw.append(data[i+1]); i+=2
|
||||
else:
|
||||
break
|
||||
try:
|
||||
s=bytes(raw).decode("gbk")
|
||||
except Exception:
|
||||
s=None
|
||||
if s and re.search(r"[一-鿿]", s): # only keep if real chinese char present
|
||||
if looks_real(s):
|
||||
out.append((start,"gbk",region_of(start),s))
|
||||
else:
|
||||
i+=1
|
||||
return out
|
||||
results += gbk_scan()
|
||||
|
||||
# dedup: prefer utf16/gbk over ascii at same-ish addr
|
||||
results.sort(key=lambda r:(r[0], r[1]))
|
||||
|
||||
# Remove ascii substrings that are actually part of utf16 (every other byte) -- keep both but mark
|
||||
for addr, enc, reg, text in results:
|
||||
pass
|
||||
|
||||
# Print
|
||||
with open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/strings_out.txt","w",encoding="utf-8") as f:
|
||||
for addr, enc, reg, text in results:
|
||||
f.write(f"0x{addr:06X}\t{enc}\t{reg}\t{repr(text)}\n")
|
||||
|
||||
print(f"TOTAL candidate real strings: {len(results)}")
|
||||
from collections import Counter
|
||||
print("By region:", dict(Counter(r[2] for r in results)))
|
||||
print("By encoding:", dict(Counter(r[1] for r in results)))
|
||||
Ссылка в новой задаче
Block a user