diff --git a/docs/fm100b-firmware-re.md b/docs/fm100b-firmware-re.md index 455f50d..b43c7d9 100644 --- a/docs/fm100b-firmware-re.md +++ b/docs/fm100b-firmware-re.md @@ -157,3 +157,15 @@ download-режим) при владельце, с риском. Пока вла `tools/bk_bootloader_catch.py` (ловит баннер/линк), `tools/bk_deinterleave.py` (снять CRC с дампа). - Дамп флеша модуля разблокируется, как найдём HKLF-хендшейк (реверс прошивки AT32 «Update DMR Chip», либо аккуратный перебор в boot-окне). Тогда получим RF Firmware (AMBE) + NET info (NV). + +## Вариант A (дамп через загрузчик) — РЕЗУЛЬТАТ (24.07, на Linux 10.0.0.63) +Загрузчик HKLFBOOT ловится и держится (нативный Linux, /dev/ttyACM0, tools/bk_module_dump.py): +catch link-check `01 e0 fc 01 00`→`04 0e 05...`, затем **SetBaudRate short CMD 0x0F ** +с РЕАЛЬНОЙ сменой бода (921600/2M) — фиксирует download-режим (иначе модуль грузит app). +Фрейминг: preamble `01 E0 FC`, short ``, long `FF F4 `. Erase=long 0x0F, Write=0x10. +**ПРОБЛЕМА: команды ЧТЕНИЯ флеша нет.** Пробовал read-опкоды 0x03/0x08/0x09/0x0a/0x0b (long, addr:4 LE): +0x09/0x0a/0x0b/0x03 — пусто; 0x08@0x1000 вернул 580 байт ТОЛЬКО из 0x00/0x80 (битовая карта/статус, +не флеш; другие адреса пусто). Сходится с DMR Upgrade Tool — у него только Connect/SetBaud/Erase/Write, +**READ отсутствует**. Вывод: **загрузчик write-only**, дамп флеша через него недостижим. +Остаётся физический дамп (SPI-клипса CH341A на флеш-чип модуля) — при вскрытии. +НО: ВЧ-чип BK4819 уже найден → TX-фикс и калибровка достижимы БЕЗ дампа, через 0x68 SPCali + мануал BK4819. diff --git a/tools/bk_bootloader_catch.py b/tools/bk_bootloader_catch.py index 8a84596..0690adc 100644 --- a/tools/bk_bootloader_catch.py +++ b/tools/bk_bootloader_catch.py @@ -1,61 +1,65 @@ #!/usr/bin/env python3 -"""Поймать Beken-загрузчик модуля во время включения (через мост RT-4D, WSL/powershell). +"""Поймать загрузчик модуля (HKLFBOOT) — ТАЙТОВЫЙ спам Connect (как DMR Upgrade Tool). -Формат флеша модуля = стандартный Beken (32+2 CRC), значит у него Beken UART-загрузчик, -который слушает link-check КОРОТКОЕ окно после сброса. Этот тул одним процессом powershell -в тайтовом цикле спамит link-check `01 E0 FC 01 00` и ловит ответ `04 0E 05 ...`. +DMR Upgrade Tool: жмёшь Upgrade (спам Connect=01 E0 FC 01 00), затем MENU+включение — +загрузчик ловит connect в окне до старта приложения. Наш powershell-цикл со Start-Sleep был +слишком медленный и промахивался. Здесь — спам БЕЗ пауз (тысячи/сек), ловит короткое окно. -Использование: запусти тул, и в течение его работы ВЫКЛЮЧИ и включи рацию в режим -«Обновление DMR чипа» (питание + MENU). Если поймаем ответ — загрузчик достижим через мост. - -ТОЛЬКО ЧТЕНИЕ/handshake — ни erase, ни write не шлём. +Процедура: запусти тул, затем зажми MENU и включи рацию. Успех = ответ загрузчика на link-check +(04 0E 05 ...) и приложение (0x68 06 02) НЕ стартует. Только connect — ни erase, ни write. """ -import argparse, subprocess, sys +import argparse, subprocess PS = r''' -$port="{port}"; $sec={sec} -$p = New-Object System.IO.Ports.SerialPort $port,115200,"None",8,"one" -$p.ReadTimeout=20; $p.WriteTimeout=50 +$p = New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one" +$p.ReadTimeout=1; $p.WriteTimeout=50; $p.ReadBufferSize=8192 try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit }} -$lc=[byte[]]@(1,224,252,1,0) # 01 E0 FC 01 00 +$lc=[byte[]]@(1,224,252,1,0) $sw=[Diagnostics.Stopwatch]::StartNew() -$hit=$false -while($sw.Elapsed.TotalSeconds -lt $sec){{ - try {{ $p.Write($lc,0,$lc.Length) }} catch {{}} - Start-Sleep -Milliseconds 8 +$acc=New-Object System.Collections.Generic.List[byte] +$caught=$false +while($sw.Elapsed.TotalSeconds -lt {sec}){{ + if(-not $caught){{ try {{ $p.Write($lc,0,$lc.Length) }} catch {{}} }} $n=$p.BytesToRead if($n -gt 0){{ $b=New-Object byte[] $n; $p.Read($b,0,$n)|Out-Null + foreach($x in $b){{ $acc.Add($x) }} $hex=($b|ForEach-Object{{"{{0:x2}}"-f $_}}) -join "" - if($hex -match "e0fc"){{ Write-Output ("HIT:"+$hex); $hit=$true; break }} - else {{ Write-Output ("RX:"+$hex) }} + Write-Output ("t"+[int]$sw.Elapsed.TotalSeconds+"|"+$hex) + # ответ загрузчика на link-check: 04 0e 05 ... + if($hex -match "040e05"){{ Write-Output "CAUGHT"; $caught=$true }} }} }} -$p.Close() -if(-not $hit){{ Write-Output "NOHIT" }} +$p.Close(); Write-Output "END" ''' def main(): ap=argparse.ArgumentParser() - ap.add_argument("--port", default="COM4") - ap.add_argument("--sec", type=int, default=25) + ap.add_argument("--port", default="COM4"); ap.add_argument("--sec", type=int, default=30) a=ap.parse_args() - print(f"[спам link-check на {a.port} @115200, {a.sec}с]") - print(">>> СЕЙЧАС выключи рацию и включи в режим «Обновление DMR чипа» (питание+MENU) <<<\n") + print(f"[ТАЙТОВЫЙ спам 01 e0 fc 01 00 на {a.port}, {a.sec}с]") + print(">>> ЗАЖМИ MENU и включи рацию СЕЙЧАС (как при прошивке модуля) <<<\n") r=subprocess.run(["powershell.exe","-NoProfile","-Command",PS.format(port=a.port,sec=a.sec)], capture_output=True, text=True) + banner=app=caught=False; n=0 for line in r.stdout.replace("\r","").split("\n"): line=line.strip() - if line.startswith("HIT:"): - print(f"✅ ЗАГРУЗЧИК ОТВЕТИЛ: {line[4:]}") - print(" -> Beken-загрузчик достижим через мост. Можно читать флеш.") - elif line.startswith("RX:") and line[3:]: - print(f" (эхо/данные: {line[3:][:60]})") - elif line=="NOHIT": - print("❌ Ответа загрузчика не поймано. Либо мост не пробрасывает загрузчик,") - print(" либо окно/бод другие. Попробуй ещё раз или обсудим физ.доступ.") - elif line.startswith("ERR:"): - print(line) + if line.startswith("ERR:"): print(line); return + if line=="CAUGHT": caught=True; continue + if "|" not in line: continue + t,hexs=line.split("|",1); n+=1 + try: asc=bytes.fromhex(hexs).decode("latin1") + except: asc="" + asc="".join(c if 32<=ord(c)<127 else "." for c in asc) + print(f" {t}: {asc[:48]} [{hexs[:40]}]") + if "HKLF" in asc: banner=True + if "680602" in hexs: app=True + print() + if caught: print("✅✅✅ ЗАГРУЗЧИК ПОЙМАН (ответ 04 0e 05)! Держим сессию — можно читать флеш.") + elif banner and app: print("⚠️ Баннер+приложение стартовало. Окно ещё промахнули — повтори (спам уже без пауз).") + elif banner: print("✅ Баннер был, приложение не стартовало — возможно, поймали. Проверю ответ.") + elif not n: print("Тишина — не попал во включение? Порт COM4? Повтори.") + else: print("Данные без баннера — перезагрузка вне окна. Повтори.") if __name__=="__main__": main() diff --git a/tools/bk_module_dump.py b/tools/bk_module_dump.py new file mode 100644 index 0000000..be8036a --- /dev/null +++ b/tools/bk_module_dump.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Beken-загрузчик модуля RT-4D: catch -> SetBaudRate(hold) -> read (Linux, /dev/ttyACM0). +Фрейминг из DMR Upgrade Tool: preamble 01 E0 FC; short: ; +long: FF F4 . SetBaudRate=short CMD 0x0F . +Erase=long CMD 0x0F. Read пробуем long CMD 0x09/0x03/0x0a. ТОЛЬКО чтение (никаких erase/write). +""" +import serial, time, sys, argparse + +def long_frame(cmd, payload=b""): + b=bytes([cmd])+payload; ln=len(b) + return bytes([1,0xE0,0xFC,0xFF,0xF4, ln&0xFF,(ln>>8)&0xFF])+b +def short_frame(cmd, payload=b""): + b=bytes([cmd])+payload; ln=len(b) + return bytes([1,0xE0,0xFC, ln&0xFF])+b +def le32(v): return bytes([v&0xFF,(v>>8)&0xFF,(v>>16)&0xFF,(v>>24)&0xFF]) + +def catch(sp, secs): + lc=bytes([1,0xE0,0xFC,1,0]); roll=b""; t0=time.time(); banner=False + while time.time()-t0 один read4k(0) -> сырой ответ. + +Фрейминг (из DMR Upgrade Tool SendEraseFlashReq): 01 E0 FC FF F4 ... +Erase=0x0F. Read4K пробуем CMD 0x09: 01 E0 FC FF F4 05 00 09 . ТОЛЬКО ЧТЕНИЕ. + +Процедура: запусти, затем зажми MENU и включи рацию. Тул поймает загрузчик и прочитает 1 сектор +по адресу 0 — покажет формат ответа (по нему построим полный дамп). +""" +import argparse, subprocess + +PS = r''' +$p=New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one" +$p.ReadTimeout=1; $p.WriteTimeout=50; $p.ReadBufferSize=16384 +try{{$p.Open()}}catch{{Write-Output "ERR:$($_.Exception.Message)";exit}} +$lc=[byte[]]@(1,224,252,1,0) +# read4k addr=0: 01 E0 FC FF F4 05 00 09 00 00 00 00 +$rd=[byte[]]@(1,224,252,255,244,5,0,{cmd},0,0,0,0) +$sw=[Diagnostics.Stopwatch]::StartNew(); $state="catch"; $roll="" +while($sw.Elapsed.TotalSeconds -lt {sec}){{ + if($state -eq "catch"){{ + try{{$p.Write($lc,0,$lc.Length)}}catch{{}} + $n=$p.BytesToRead + if($n -gt 0){{ + $b=New-Object byte[] $n;$p.Read($b,0,$n)|Out-Null + $hex=($b|ForEach-Object{{"{{0:x2}}"-f $_}}) -join "" + $roll=($roll+$hex); if($roll.Length -gt 400){{$roll=$roll.Substring($roll.Length-400)}} + if($hex -match "484b4c46"){{ Write-Output "BANNER" }} + if($roll -match "040e05"){{ + Write-Output "CATCH" + # слить бэклог link-check ответов до тишины (~250мс без байт) + $q=[Diagnostics.Stopwatch]::StartNew(); $last=0 + while($q.ElapsedMilliseconds -lt 400){{ + $m=$p.BytesToRead + if($m -gt 0){{ $tb=New-Object byte[] $m;$p.Read($tb,0,$m)|Out-Null; $last=$q.ElapsedMilliseconds }} + }} + $p.DiscardInBuffer() + $p.Write($rd,0,$rd.Length) # read4k + $state="read"; $rsw=[Diagnostics.Stopwatch]::StartNew() + $acc=New-Object System.Collections.Generic.List[byte] + }} + }} + }} else {{ + $n=$p.BytesToRead + if($n -gt 0){{ $b=New-Object byte[] $n;$p.Read($b,0,$n)|Out-Null; foreach($x in $b){{$acc.Add($x)}} }} + if($rsw.Elapsed.TotalMilliseconds -gt 2500){{ + $hx=($acc.ToArray()|ForEach-Object{{"{{0:x2}}"-f $_}}) -join "" + Write-Output ("RDLEN:"+$acc.Count) + Write-Output ("RDHEAD:"+$hx.Substring(0,[Math]::Min(96,$hx.Length))) + if($hx.Length -gt 96){{ Write-Output ("RDTAIL:"+$hx.Substring($hx.Length-32)) }} + break + }} + }} +}} +$p.Close(); Write-Output "END" +''' + +def main(): + ap=argparse.ArgumentParser() + ap.add_argument("--port",default="COM4"); ap.add_argument("--sec",type=int,default=30) + ap.add_argument("--cmd",default="9",help="опкод чтения (9=read4k, 3=readsector)") + a=ap.parse_args() + print(f"[зонд чтения, CMD 0x{int(a.cmd):02x}, {a.port}, {a.sec}с] — ЗАЖМИ MENU и включи рацию\n") + r=subprocess.run(["powershell.exe","-NoProfile","-Command", + PS.format(port=a.port,sec=a.sec,cmd=int(a.cmd))],capture_output=True,text=True) + for line in r.stdout.replace("\r","").split("\n"): + line=line.strip() + if line.startswith(("ERR:","CATCH","RDLEN:","RDHEAD:","RDTAIL:")): print(" "+line) + print("\n(RDLEN ~4096+ и RDHEAD с данными = read4k работает. RDLEN 0/мало = опкод неверный, пробуй --cmd 3)") + +if __name__=="__main__": + main()