Tym razem prezentuję budżetowy tuner DVB-T/DVB-T2 Manta DVBT018, który trafił do mnie do naprawy bez oznak życia. Z naprawy zrobiła się zabawa w emulację - udało się nawet zaemulować konsolę (linię komend) systemu operacyjnego! Ale od początku. Urządzenie, choć bardzo tanie, do kupienia za raptem kilkadziesiąt złotych, jest w pełni zgodne z telewizją naziemną drugiej generacji (DVB-T2 w formacie HEVC) i oferuje możliwość ustawienia własnej listy kanałów. Niby przy tak niskiej cenie nie opłaca się naprawiać, ale może jednak? Sprawdźmy!
Prezentacja wnętrza
Zrywamy plombę i zaglądamy do środka. Winnego widać już po chwili - to ten podejrzanie wysoki kondensator. Najpierw jednak wyjmiemy płytkę - od spodu nie ma na niej elementów, ale trzeba dostać się do lutów.
Na płytce wyróżniłbym trzy sekcje - zasilacz impulsowy w topologii flyback, główny mikrokontroler z pamięcią Flash, oraz tuner RF:
No i mamy winnego:
Swoją drogą, przetwornice w tych czasach, przy tak niskich mocach, nawet nie używają transoptorów. To tzw. PSR - Primary Side Regulation. Ten mały układ LY2318 zawiera w sobie nawet tranzystor kluczujący.
Nie zasila on bezpośrednio MCU, generuje on 5 V, potem są jeszcze przetwornice obniżające napięcie, pewnie do 3.3 V lub niżej.
Wyświetlaczem steruje FD650, który już wcześniej omawiałem.
Końcówka RF opiera się na Rafael Micro R836.
Jest o nim trochę informacji w sieci:
https://github.com/siddolo/R836-reverse-engineering
Naprawa
Teraz sama naprawa - po prostu trzeba wylutować kondensator, do tego pomagam sobie topnikiem. Potem potrzebny jest zamiennik, nie może być byle jaki, tylko Low ESR. Odpowiedni na wyjście przetwornicy. Pojemność najlepiej zgodna, nie mniejsza, napięcie też nie mniejsze.
Raczej nie ma zaskoczenia, że po tej naprawie tuner działa.
Zgrywanie flash
Zostało najciekawsze - zrzut pamięci Flash. Ma ona prosty interfejs SPI. Wylutowuję ją gorącym powietrzem, a potem lutuję na podstawkę od CH341 i tak zgrywam ją na komputer.
Po stronie PC używam NeoProgrammer.
Kopia wsadu:
https://github.com/openshwprojects/FlashDumps/commit/7c15d12800ae4b96f645c9da617f6da586fd9ab9
Analiza firmware
Pierwszym polecanym przeze mnie krokiem jest użycie binwalk, omawiałem go dwa lata temu. W tym przypadku binwalk też uruchomiłem poprzez WSL:
Kod: Bash
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
23072 0x5A20 SHA256 hash constants, little endian
46336 0xB500 LZMA compressed data, properties: 0x6C, dictionary size: 524288 bytes, uncompressed size: 405280 bytes
184320 0x2D000 JPEG image data, JFIF standard 1.01
204828 0x3201C LZMA compressed data, properties: 0x6C, dictionary size: 8388608 bytes, uncompressed size: 6328740 bytesW ten sposób odkryłem osobne sekcje. Dwa strumienie LZMA i JPEG - czyli logo startowe.
Napisy, zwykły strings:
Kod: Bash
MBOT-1106.0.10.936a0fd.201806111716
Board=K5AP_BD_MST297B_D01A
MBoot_IN=SPI_FLASH
Kernel=Unkown
Security=Non-TEEPłytka K5AP_BD_MST297B_D01A, czyli platforma K5AP od MStara. Samo MST297B to zapewne oznaczenie projektu płyty bazowej (Board Design), a procesor to najpewniej MStar MSA7T10E, choć ostatecznie nawet nie zdjąłem radiatora, bo tuner po naprawie musiał szybko wrócić do właściciela (niektórzy potrzebują mieć telewizję na już i nie chcą czekać dodatkowo długo). Bootloader to MBoot oparty na U-Boot 2011.06. Non-TEE, więc wsad nie jest szyfrowany ani podpisany.
Zmienne środowiskowe leżą pod 0x3FE000 (i kopia pod 0x3FF000):
bootcmd=usb exit;spi_rdc 0x80B00000 0x3201C 0x2A6BDE; LzmaDec 0x80B00000 0x2A6BDE 0x80000180 0x81000000; go 0x80000224;
bootdelay=0To gotowa instrukcja startu: wczytaj 0x2A6BDE bajtów spod 0x3201C do 0x80B00000, rozpakuj LZMA pod 0x80000180, skocz na 0x80000224.
Potwierdza to nagłówek pod 0x32000, pola big-endian mimo little-endianowego CPU:
DE AD BE EF magic
00 2A 6B DE rozmiar spakowany
80 00 01 80 adres ładowania
80 00 02 24 punkt wejścia
00 03 20 1C offset danychRozpakowanie obu sekcji to kilka linii w Pythonie:
Kod: Python
Procesor określiłem deasemblując początek wsadu (pierwsze 0x20 bajtów to nagłówek, kod leci dalej). To MIPS32 little-endian - inicjalizacja cache i zapisy do przestrzeni peryferiów MStar (RIU) pod 0xBF200000:
0x94000054: lui $t0, 0xbf00
0x94000068: lui $t0, 0xbf20
0x94000070: sw $t1, 0x6700($t0)
0x940000B0: cache 5, ($s0)
0x94000110: lui $ra, 0x9400 ; skok do 0x94000600I jeszcze jedno - to nie jest Linux. Zmienna console=ttyS2,115200n8 i napis Kernel=Unkown mylą, ale bootloader używa go, a nie bootm, czyli skacze do surowego binarnego obrazu. W rozpakowanej aplikacji nie ma ciągu "Linux version", są za to eCos, MsOS i Utopia - system czasu rzeczywistego eCos z warstwą MStar Utopia.
Kto chce podłubać w Ghidrze, wrzucam gotowe obrazy - trzeba je wczytać jako Raw Binary, MIPS:LE:32:default, z podanymi adresami bazowymi:
ipl_0x94000000.bin 46336 B baza 0x94000000 kod startowy
mboot_0x830F0180.bin 405280 B baza 0x830F0180 MBoot / U-Boot:
app_0x80000180.bin 6328740 B baza 0x80000180 eCos, wejście 0x80000224
Teraz już jest na czym pracować, ale co docelowo z tym zrobimy?
Emulacja firmware
Pomysł emulacji bazowałem na dokładnie tej samej idei, co we wczesniejszych prezentacjach:
Uruchomienie prostego emulatora MIPS w Python dla firmware ALI M3801 (Hello World)
Emulator BK7231: wstępne eksperymenty z bibliotekami Capstone i Unicorn w języku Python
czyli Capstone do deasemblacji i Unicorn do wykonywania kodu.
Najpierw trzeba znaleźć UART - nie jest to trudne. Stałe "stringowe" są od razu widoczne po dekompilacji. Zapewne 94000cec to funkcja print lub podobna:
Iteruje ona napis i wywołuje na każdym znaku putchar. Widać przy okazji, że przed znakiem 0x0A (LF) dosyła 0x0D (CR) - klasyczne zawijanie linii dla terminala:
Sam putchar to już podręcznikowy schemat - dwie pętle czekania i jeden zapis pomiędzy nimi:
Pierwsza pętla czeka, aż w rejestrze 0xBF201328 zapali się bit 0x20. Potem znak leci do 0xBF201300. Druga pętla czeka na 0x60, czyli dwa bity naraz - i to jest podpowiedź, co to za układ.
Tak zachowuje się 16550: bit 0x20 to THRE ("jest miejsce w buforze nadawczym"), bit 0x40 to TEMT ("nadajnik całkiem pusty"), a 0x60 to oba razem. Czyli pierwsza pętla czeka na miejsce w buforze, druga aż znak fizycznie wyjdzie na linię. Blokujący, w pełni opróżniający putchar - dlatego log nigdy się nie gubi, nawet gdy procesor zaraz potem resetuje peryferia. Zgadzają się też offsety: THR to rejestr 0, LSR to rejestr 5, rejestry leżą co 8 bajtów, stąd 0x1300 + 5*8 = 0x1328. Bit 0x01 to "są dane do odczytu" - przyda się później do wpisywania komend.
Adres da się potwierdzić wprost, bo źródła MBoota są publiczne - github.com/neuschaefer/mstar-mboot, plik sboot/src/amber5/c_riubase.h:
Kod: C / C++
RIU adresuje się słowami 16-bitowymi (w kodzie MStara to wskaźnik unsigned short), więc 0x100980 * 2 = 0x201300, a po dodaniu bazy RIU 0xBF000000 wychodzi dokładnie 0xBF201300. Trafiłem w ten adres deasemblacją, zanim znalazłem ten nagłówek.
Czyli: podstawiam pod odczyt 0xBF201328 wartość z ustawionymi bitami 0x60, a każdy zapis do 0xBF201300 wypisuję na ekran. To cały mechanizm zdobycia logu. Ostateczny dowód, że trafiłem, jest empiryczny - z przechwyconych zapisów sypie się czytelny tekst, a nie śmieci.
REKLAMA
Mapowanie pamięci ma jeden haczyk - Unicorn dla MIPS sam tłumaczy KSEG, więc mapuje się pod adresami fizycznymi: 0x94000000 to 0x14000000, a 0xBF200000 to 0x1F200000.
Minimalna wersja, która dochodzi do bannera U-Boota, to niecałe 30 linii:
Kod: Python
Trzy rzeczy, na które straciłem najwięcej czasu wraz z ciekawymi rozwiązaniami:
1. Nieznany rejestr nie może zwracać stałej. Firmware pełen jest pętli "czekaj aż bit się ustawi". Jeśli rejestr zwraca w kółko to samo, emulacja wisi. Naprzemienne 0xFFFFFFFF i 0 zaspokajają zarówno "czekaj na 1", jak i "czekaj na 0".
2. BIST pamięci musi przejść. Rejestr 0x1F2025C0: bit 15 to "zakończony", bity 0x6000 to błędy. Bez tego IPL wypisuje BIST0_FAIL i zatrzymuje się na pętli
b .3. Unicorn nie ma licznika CP0 Count. Odczyt "mfc0 rt, $9" kończy się wyjątkiem. Pierwszy raz wywala się w procedurze opóźnienia pod 0x94000C80, więc najprościej podmienić ją na natychmiastowy powrót - i to załatwia sprawę w powyższym kodzie. Gdyby trzeba było podmieniać instrukcje w locie, jest pułapka: sam mem_write nie wystarczy, bo Unicorn trzyma przetłumaczone bloki w cache i dalej wykonuje stary kod. Konieczne jest ctl_flush_tb(), inaczej podmiana po cichu nic nie robi.
Pełna wersja dokłada jeszcze obsługę odczytów z SPI Flash (inaczej U-Boot nie znajdzie obrazu aplikacji), podbijanie zmiennej timera (bez przerwań get_timer() nigdy nie rusza i udelay() kręci się w nieskończoność) oraz konsolę - o tym za chwilę.
Prezentacja - boot log
Uruchamiamy:
Kod: Bash
I dostajemy log - szkoda, że nie było czasu zebrać go z oryginału, ale naprawiałem sprzęt dla kogoś.
B
BOOTSPI
BIST0_OK
_OK!decomp
_done
done
[AT][MB][start ub][0]
U-Boot 2011.06 (Sep 23 2021 - 14:07:21) MBOT-1106.0.10.936a0fd.201806111716
DRAM:
Hello U-Boot
Stack Pointer at: 83627740
mem initial, start 0x82CEE180, len 0x402000
SPI: uboot held at [8F000000~90000000]
Now running in RAM - U-Boot at: 830F0180
In: serial
Out: serial
Err: serial
[ERROR] do_spi_init_partition:1221: This function doesn't support
============= set bootargs ===============
Unknown command 'panel_pre_init' - try 'help'
[ERROR] raw_read:551: len is zero
[ERROR] LogoInMboot2Dram:146: LogoInMboot2Dram Fail!!
[ERROR] MsDrv_JpdStartDecode:577: MsDrv_JpdStartDecode: Init failed
[ERROR] MsDrv_SetStatus:401: MsDrv_SetStatus:E_JPEG_DECODE_ERR
spi_rdc 0x80B00000 0xB000 0x400
offset 0xB000, size 0x400
spi_rdc 0x80B00000 0x32000 0x400
offset 0x32000, size 0x400
setenv bootcmd ' usb exit;spi_rdc 0x80B00000 0x3201C 0x2A6BDE; LzmaDec 0x80B00000 0x2A6BDE 0x80000180 0x81000000; go 0x80000224;
Saving Environment to SPI Flash...
Write addr=0x003FE000, size=0x00001000
sector erase
Write addr=0x003FF000, size=0x00001000
sector erase
@@@[main_loop][297]@@@
============= set bootargs ===============
Hit any key to stop autoboot: 0
enable dont overwrite function
AC on
Write addr=0x003FE000, size=0x00001000
sector erase
Write addr=0x003FF000, size=0x00001000
sector erase
Check USB port[0]:
[USB] usb_lowlevel_init++
[USB] USB EHCI LIB VER: 2017.08.11-k5ap
[USB] Port 0 is Enabled
[USB] TV_usb_init (UTMI Init) ++
[USB] UTMI Base BF207500
[USB] UHC Base BF204800
[USB] USBC Base BF200E00
[USB] BC Base BF226E00
[USB] init squelch level 0x2
[USB] TV_usb_init--
[USB] Usb_host_Init++
[USB] No USB is connecting
[USB] USB init failed
[USB] usb_lowlevel_init--
Error, couldn't init Lowlevel part
FAIL : can not init usb!!
Write addr=0x003FE000, size=0x00001000
sector erase
Write addr=0x003FF000, size=0x00001000
sector erase
USB is stopped. Please issue 'usb start' first.
offset 0x3201C, size 0x2A6BDE
LZMA Decompression...CRC check success !!
ok
disable interrupts
## Starting application at 0x80000224 ...
B1999 drvEMAC_GetMacAddr
cyg_if_attach.225 - After initialize list 0x804ac364
IFP: 0x804ac354 (eth0)
cyg_if_attach.290 - After inserting 0x8079fa30 into list 0x804ac364
IFP: 0x804ac354 (eth0)
IFA: 0x8079fa30 - <<0x8079faac>>
cyg_if_attach.225 - After initialize list 0x808553c8
IFP: 0x808553b8 (lo0)
cyg_if_attach.290 - After inserting 0x8079f930 into list 0x808553c8
IFP: 0x808553b8 (lo0)
IFA: 0x8079f930 - <<0x8079f9ac>>
Register_netisr [2] fun=0x800bfa79
[eCos][CPU INFO] : CPU Clock = 754000000 : RTC Period = 377000
[MsOS_CPU_DisableInterrupt][3512] MsOS_CPU_DisableInterrupt is not supported
[MsOS_CPU_RestoreInterrupt][3533] MsOS_CPU_RestoreInterrupt is not supported
[MsOS_CPU_DisableInterrupt][3512] MsOS_CPU_DisableInterrupt is not supported
[MsOS_CPU_DisableInterrupt][3512] MsOS_CPU_DisableInterrupt is not supported
[MsOS_CPU_RestoreInterrupt][3533] MsOS_CPU_RestoreInterrupt is not supported
[MsOS_CPU_RestoreInterrupt][3533] MsOS_CPU_RestoreInterrupt is not supported
[MsOS_CPU_DisableInterrupt][3512] MsOS_CPU_DisableInterrupt is not supported
[MsOS_CPU_DisableInterrupt][3512] MsOS_CPU_DisableInterrupt is not supported
[MsOS_CPU_RestoreInterrupt][3533] MsOS_CPU_RestoreInterrupt is not supported
[MsOS_CPU_RestoreInterrupt][3533] MsOS_CPU_RestoreInterrupt is not supported
[eCos] UtopiaInit
shmem name not found.
shmem name not found.
shmem name not found.
(... "shmem name not found" powtarza się 60 razy ...)
[SYS][ERR][AESDMARegisterToUtopia:2270] Not support AESDMA
<Bad format string: 8029D9F4 : 8039DC2C 803A6324 803A63B4 1C7 80179E27 200000 80161AD1 400000>
<Bad format string: 8029D9F4 : 803A9730 803A96BC 803A9D40 A7E 800A85C4 8060A9DE FF 2>
detect flash type (0xFF, 0x00, 0xFF)
Unknown flash type (0Kilka rzeczy wartych wyłapania z tego logu:
Now running in RAM - U-Boot at: 830F0180 - to ten sam adres, który wcześniej wyliczyłem ze wskaźników w tablicy komend. Dwa niezależne źródła się zgadzają, więc baza dla Ghidry jest pewna.
LZMA Decompression...CRC check success !! - bootloader rozpakował 6 MB obrazu i policzył jego sumę kontrolną. To najlepszy dowód, że podstawowa emulacja jest wierna: gdyby choć jeden bajt wyszedł źle, CRC by nie przeszło.
## Starting application at 0x80000224 - dokładnie punkt wejścia z nagłówka spod 0x32000.
CPU Clock = 754000000 - procesor taktowany 754 MHz. W tunerze za kilkadziesiąt złotych.
eth0, lo0, Register_netisr - aplikacja podnosi interfejsy sieciowe i stos TCP/IP, choć to urządzenie nie ma gniazda Ethernet. Zostało po większych układach z tej samej rodziny.
Emulacja kończy się w okolicy inicjalizacji sterowników eCos. Powód jest konkretny: aplikacja wygląda na skompilowaną w MIPS32 ze wsparciem 16-bitowych instrukcji (MIPS16e), a Unicorn gubi się przy obsłudze tego trybu i przełączaniu ISA. Widać to po instrukcjach o długości 2 bajtów i po adresach powrotu z ustawionym bitem 0 (np. $ra = 0x8016E9B7, co w MIPS sygnalizuje tryb instrukcji 16-bitowych). Żaden z dostępnych rdzeni w użytej bibliotece nie chciał z tym ruszyć...
Konsola U-Boota
Skoro emulator ma UART, to działa on w obie strony. Bit 0x01 w LSR oznacza "są dane do odczytu", więc wystarczy podstawiać tam wciśnięte klawisze - i mamy interaktywną konsolę bootloadera.
Jedna pułapka: spacja nie działa, mimo że bootloader prosi o "any key". MStar przerobił tę procedurę - znak jest odczytywany i wyrzucany, a przerwanie autobootu wywołuje dopiero Enter. Sprawdziłem to przeglądem klawiszy: spacja, ESC, Ctrl-C i litery są ignorowane, reaguje tylko CR (0x0D).
Demko uruchamiamy run_console.bat:
Hit any key to stop autoboot: 0
k5ap# printenv
AP_SW_MODEL=0x0001
AP_SW_VERSION=0x0001
CM_DVBT_ADDR=0x003e8000
CM_DVBT_ORDER=0x003e6000
CM_DVBT_SIZE=0x00015000
CUSTOMER_OUI=0x169B
HW_MODEL=0x0001
HW_VERSION=0x0001
MstarUpgrade_complete=0
UARTOnOff=on
baudrate=115200
bl_dfb_framebuffer_addr=0x01828000
bl_jpd_inter_addr=0x02118800
(...)Prompt "k5ap#" to pełnoprawna konsola - działa edycja linii, Backspace kasuje, strzałki w górę i w dół przewijają historię komend.
Komenda help nie jest w tym wsadzie zarejestrowana, więc listę poleceń wyciągnąłem wprost z tablicy cmd_tbl_t w pamięci. Wyszło 68 komend, m.in.:
printenv setenv saveenv loadenv spi_rdc spi_wrc spi2usb
bootm go bootcheck read_boot_info fatload fatls fatwrite usb
mversion showversion checkVersion ustar austar custar update_modeKilka z nich sprawdziłem od razu w emulatorze:
setenv dopisuje do zmiennych środowiskowych, można je potem odczytać przez printenv. Ustawiłem zmienną "test" na "123". Poniżej rezultat, za długi na zrzut ekranu:
k5ap# printenv
AP_SW_MODEL=0x0001
AP_SW_VERSION=0x0001
CM_DVBT_ADDR=0x003e8000
CM_DVBT_ORDER=0x003e6000
CM_DVBT_SIZE=0x00015000
CUSTOMER_OUI=0x169B
HW_MODEL=0x0001
HW_VERSION=0x0001
MstarUpgrade_complete=0
UARTOnOff=on
baudrate=115200
bl_dfb_framebuffer_addr=0x01828000
bl_jpd_inter_addr=0x02118800
bl_jpd_inter_size=0x00630000
bl_jpd_read_addr=0x01c1c800
bl_jpd_read_size=0x00100000
bl_jpd_write_addr=0x01d1c800
bl_jpd_write_size=0x003fc000
bootargs=BOOTLOGO_IN_MBOOT ENV_VAR_OFFSET=0x3FE000 ENV_VAR_SIZE=0x1000 ENV=SERIAL SECURITY=OFF
bootcmd=usb exit;spi_rdc 0x80B00000 0x3201C 0x2A6BDE; LzmaDec 0x80B00000 0x2A6BDE 0x80000180 0x81000000; go 0x80000224;
bootdelay=0
bootscript=echo Running bootscript from mmc${mmcdev} ...; source ${loadaddr}
console=ttyS2,115200n8
hdmicolor=9
info_exchange=spi
loadaddr=0x82000000
loadbootscript=fatload mmc ${mmcdev} ${loadaddr} boot.scr
loaduimage=fatload mmc ${mmcdev} ${loadaddr} uImage
mmcargs=setenv bootargs console=${console} vram=${vram} root=${mmcroot} rootfstype=${mmcrootfstype}
mmcboot=echo Booting from mmc${mmcdev} ...; run mmcargs; bootm ${loadaddr}
mmcdev=0
mmcroot=/dev/mmcblk0p2 rw
mmcrootfstype=ext3 rootwait
osd_language=English
panel_cmd=12
resolution=8
stderr=serial
stdin=serial
stdout=serial
test=123
tv_format=6
ubispeedup=UBI
upgrade_mode=null
usbtty=cdc_acm
ve_buffer_addr=0x02748800
ve_buffer_size=0x0021c000
vram=16M
Environment size: 1465/4092 bytes
Moja zmienna jest widziana - tuner wypisał na UART test=123. Niewątpliwie emulacja działa i dociera dalej, niż bym się spodziewał.
Podsumowanie
Zaczęło się od prostej naprawy, a ostatecznie stanęło na emulatorze, który nawet jest w stanie obsłużyć interaktywną konsolę bootloadera U-Boot / MBoot znajdującą się na urządzeniu. Nawet zmienne środowiskowe działają! Nieźle. Można by spróbować to pociągnąć nieco dalej, ale co byłoby wtedy następnym krokiem? Chyba się muszę zastanowić. Jeśli coś więcej się uda zrobić, to pokażę w następnym temacie.
Czy spotkaliście się już z tą platformą K5AP / procesorami MSA7T10E w tunerach?
Repozytorium:
https://github.com/openshwprojects/MST297B_Emulator
Fajne? Ranking DIY Pomogłem? Kup mi kawę.