diff --git a/docs/fm100b-firmware-re.md b/docs/fm100b-firmware-re.md index eec6f2d..6ee9655 100644 --- a/docs/fm100b-firmware-re.md +++ b/docs/fm100b-firmware-re.md @@ -105,6 +105,18 @@ Bootloader | Application | RF Firmware | NET info Вывод: модуль — универсальный Beken-378x DMR/intercom SoC, используемый разными брендами (Radtel/Kirisun/iRadio). Многое из этого можно дёргать по 0x68 через мост (осторожно — только Query/Read вслепую). +## ⭐ Формат образа: Beken CRC16-каждые-32-байта (де-интерливинг) +Образы `FM100B_*.bin` — сырой дамп Beken-флеша: **32 байта данных + 2 байта CRC16 = блоки по 34 байта** +(подтверждено: непечатные пары строго через 34 байта). Те «битые» 2 байта в строках (`ef 10`, +`a8 2c`) — это CRC, не порча. Снятие CRC даёт ЧИСТУЮ прошивку: +- `tools/bk_deinterleave.py raw.bin clean.bin` (выкинуть 2 CRC из каждых 34 байт, фаза 0). +- Чистый образ: `research/module-fw/FM100B_V1.2.0.32_clean.bin` (1 437 938 Б, снято 89 870 CRC). +- После снятия строки целые («RF Firmware», «check failed» и т.д.). **Для будущего реверса — брать + чистый образ.** + +Это **окончательно подтверждает стандартный Beken-флеш** → для дампа живого модуля подойдут +**ltchiptool / bk7231tools** (они читают этот формат и сами снимают CRC). Путь варианта A ниже. + ## Достоверность образов и версии - Оба образа (.13 2025-04, .32 2026-01) — plaintext, надёжны (в обоих `beken378`/`EasyFlash`/партиции/ `check failed`). Встречаются одиночные не-ASCII байты в строках (в разных местах у .13 и .32) — diff --git a/research/module-fw/FM100B_V1.2.0.32_clean.bin b/research/module-fw/FM100B_V1.2.0.32_clean.bin new file mode 100644 index 0000000..d83e924 Binary files /dev/null and b/research/module-fw/FM100B_V1.2.0.32_clean.bin differ diff --git a/tools/bk_deinterleave.py b/tools/bk_deinterleave.py new file mode 100644 index 0000000..a3ab054 --- /dev/null +++ b/tools/bk_deinterleave.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Снять CRC16 из сырого дампа Beken-флеша (FM100B/HF6853 модуль RT-4D). + +Формат Beken (bootloader/app партиции): каждые 32 байта данных сопровождаются 2 байтами +CRC16 -> физические блоки по 34 байта. Сырой дамп/образ (FM100B_*.bin) содержит эти CRC, +что «рвёт» строки и код. Этот тул выкидывает 2 CRC-байта из каждого 34-байтного блока -> +чистая прошивка для реверса. + +Подтверждено на FM100B_V1.2.0.32: непечатные пары строго через 34 байта; после снятия +строки («RF Firmware», «check failed», «beken378\\intercom») становятся целыми. + + python3 bk_deinterleave.py FM100B_raw.bin FM100B_clean.bin + python3 bk_deinterleave.py --crc FM100B_raw.bin # только проверить период CRC + +Совместимо по формату с ltchiptool/bk7231tools (они делают то же при чтении флеша). +""" +import sys, argparse + +BLK = 34 # 32 данных + 2 CRC16 +DATA = 32 + +def deinterleave(raw: bytes, phase: int = 0) -> bytes: + out = bytearray(raw[:phase]) + body = raw[phase:] + for i in range(0, len(body), BLK): + out += body[i:i+DATA] + return bytes(out) + +def find_crc_period(raw: bytes, start=0, length=0x400): + """вернуть наиболее частый шаг между непечатными байтами (ожидаем 34)""" + from collections import Counter + poss = [i for i in range(start, min(start+length, len(raw))) + if not (32 <= raw[i] < 127 or raw[i] in (0, 10, 13))] + steps = Counter(b-a for a, b in zip(poss, poss[1:])) + return steps.most_common(5) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("infile") + ap.add_argument("outfile", nargs="?") + ap.add_argument("--phase", type=int, default=0, help="смещение начала блоков (обычно 0)") + ap.add_argument("--crc", action="store_true", help="только показать период CRC") + a = ap.parse_args() + raw = open(a.infile, "rb").read() + if a.crc or not a.outfile: + # поискать текстовую область для оценки периода + for s in (0x15c000, 0x100000, 0x80000, 0): + if s < len(raw): + p = find_crc_period(raw, s) + if p and p[0][0] == BLK: + print(f"CRC-период {BLK} подтверждён @0x{s:x}: {p}") + break + else: + print("период 34 не найден — возможно, образ уже чистый или другой формат") + if not a.outfile: + return + clean = deinterleave(raw, a.phase) + open(a.outfile, "wb").write(clean) + print(f"{a.infile} ({len(raw)}) -> {a.outfile} ({len(clean)}), снято {len(raw)-len(clean)} CRC-байт") + +if __name__ == "__main__": + main()