SaeLog #5: Testy, enumeracja i deskryptory USB w FX2
• tech • 2508 słów • 12 minut czytania
Ta notatka jest częścią serii Saleae Logic Hack. Zapoznaj się z pozostałymi wpisami.
Kolejny odcinek z serii SaeLog odnośnie hackowania sprzętu i oprogramowania (głównie) analizatora logicznego. Od ostatniego wpisu minęło sporo czasu, dokładnie 4 miesiące. Jakoś tak się złożyło, że nawał obowiązków i innych spraw (wyjazdów, urlopów i przede wszystkim praca) nie pozwoliły mi szybciej wrócić do tego tematu. Ale udało się i mam nadzieję, że kilka kolejnych wpisów oznaczonych tagiem #SaeLog w miarę szybko się pojawi.
Korzystając z informacji zawartych w SaeLog#4 bez problemu udało mi się wyekstrahować kod firmware-u z aplikacji. Do testów i eksperymentów uznałem, że najlepiej będzie wykorzystać CyConsole i wprost z niej ładować zmodyfikowany kod do urządzenia oraz przeprowadzać komunikację. Ale po załadowaniu kodu do układu, następuje renumeracja i urządzenie przedstawia się jako analizator po Sealogic-owych identyfikatorach. I tym samym tracimy możliwość zabawy przez konsolę Cypressa.
QucikHack
Szybki hack to znalezienie identyfikatorów w kodzie firmware i ich podmiana. Jest to najprostszy sposób bez wnikania w techniczne aspekty FX2 i enumeracji. Szukane dane identyfikatorów znajdują się pod offsetem 0x5E
kolejne 4 bajty to VID i PID, czyli 0x0925
i 0x3881
.
Bajty te wystarczy podmienić na wartości reprezentujące standardowe urządzenie Cypressa, czyli B4 04 13 86
. Powinno ruszyć bez problemu. Istnieją jeszcze inne opcje, ale wymagają one głębszych analiz.
Na tym można byłoby zamknąć temat, ale myślę, że warto trochę po drążyć temat i spojrzeć głebiej pod maskę, aby się przekonać na własnej skórze jak to wszystko działa. Zrozumienie kilku rzeczy może pomoc w przyszłym wykorzystaniu EZ-USB w innych projektach.
RTFM!
Żeby co nieco więcej poznać w kontekście FX2 i komunikacji z hostem (komputerem) przydaje się minimalna wiedza na temat sposobu działania USB. Oficjalna dokumentacja jest dobrym miejscem na początek, ale prócz tego, w przystępny sposób można znaleźć ciekawe informacje na MSDN-ie, a także na stronie USB in a NutShell i USB Made Simple, prezentujące świat bebechów USB. Polecam!
Podobnie w przypadku USB w EZ-USB, obszerne informacje można znaleźć w datasheecie układu CY7C68013A oraz dokumencie EZ-USB® Technical Reference Manual, który jest podstawowym źródłem informacji. W materiałach tych przedstawiono wszystko co niezbędne, aby poznać i umieć wykorzystać możliwości tej platformy.
W chwili obecnej interesują mnie tylko te elementy jakie dotykają bezpośrednio omawiany temat i mogą ułatwić zrozumienie działanie, więc tylko na nich będę skupiał swoją uwagę.
Deskryptory USB
Wartości identyfikatorów zawartych w firmware, te jakie zaznaczyłem na obrazku wyżej, są częścią większych struktur zwanych w świecie USB deskryptorami. Każde urządzenie USB dostarcza informacji o sobie i swoich możliwościach właśnie za pomocą takich struktur. Zawierają one informacje o urządzeniu, konfiguracji i dostępnych interfejsach. Podstawowe deskryptory USB to:
Device Descriptors USB_DEVICE_DESCRIPTOR
Configuration Descriptors USB_CONFIGURATION_DESCRIPTOR
Interface Descriptors USB_INTERFACE_DESCRIPTOR
Endpoint Descriptors USB_ENDPOINT_DESCRIPTOR
String Descriptors USB_STRING_DESCRIPTOR
Hierarchia deskryptorów jest drzewiasta, korzeniem jest deskryptor urządzenia, a później już może być wiele konfiguracji, interfejsów i punktów końcowych.
Każdy deskryptor składa się takiego samego nagłówka, który identyfikuje jego rozmiar i rodzaj. Można to zapisać w postaci prostej struktury:
struct USB_COMMON_DESCRIPTOR {
UCHAR bLength;
UCHAR bDescriptorType;
};
Przyglądając się bliżej fragmentowi kodu firmware z zaznaczonymi identyfikatorami, z łatwością można wydedukować z niego lokalizację i ułożenie poszczególnych deskryptorów. Tym sposobem wyłania się pełny obraz tablicy deskryptorów opisujących możliwości badanego urządzenia.
IDA potraktował miejsce ze zmiennymi jako kod, ale wystarczy dodać definicje struktur i przekonwertować te fragmenty na dane. Prawdopodobnie dane będą zawarte aż do granicy strony, a dopiero dalej będzie znajdował się typowy kod wykonywalny.
Zaciekawił mnie deskryptor o typie równym wartości 0x06
, oznaczony w dokumentacji USB2 jako USB_DEVICE_QUALIFIER_DESCRIPTOR
. Związany jest on z możliwościami zmiany prędkości na high-speed i odwrotnie, jeśli urządzenie operuje na innych prędkościach. To by świadczyło, że prawdopodobnie możliwe jest używanie analizatora na USB1.1.
Dostęp do deskryptorów urządzenia mozliwy jest przez żądania hosta odnośnie pobrania i ustawienia deskryptorów - GET_DESCRIPTOR
(0x06
) i SET_DESCRIPTOR
(0x07
) z grupy “Standard Device Requests” (rozdział 9.4 w dokumentacji USB2).
Enumeracja USB i ReNumeracja FX2
Znam miejsce przechowywania deskryptorów, fajnie, ale co to ma wspólnego z całym procesem inicjalizacji urządzenia, skoro po jego podłączeniu do portu komputera wykrywany jest poprawnie na podstawie zapisanych w EEPROM identyfikatorów (lub po defaultowych, jeśli niema pamięci nieulotnej). Czemu później nagle zmienia się jego identyfikacja? Odpowiedz prosta - bo tak skonfigurowano i poinstruowano układ.
Żeby zrozumieć cały ten mechanizm, powinienem wspomnieć o procesie inicjalizacji urządzenia USB jaki nastepuje po jego podłączeniu do hosta. Proces inicjalizacji nosi nazwę enumeracji, polega na zidentyfikowaniu i przypisaniu unikalnego adresu urządzenia przez hosta, co pozwala na odróżnienie od innych podłączonych urządzeń do magistrali. Nazwa zapewne jest nawiązaniem do ostatniego etapu - enumerowania deskryptorów urządzenia. W skrócie, pomijając kwestie typowo elektryczne i sygnałowe, całość na wyższym poziomie sprowadza się do kilku kroków.
Najpierw host wysyła żądanie GET_DESCRIPTOR
z adresem domyślnym - zerowym - każdy urządzenie musi odpowiedzieć na takowe żądanie z zerowym adresem, kiedy zostało podłączone do hosta. Po otrzymaniu danych host przydziela unikalny adres urządzenia poprzez SET_ADDRESS
. A potem już leci enumeracja - kolejka z GET_DESCRIPTOR
pytającą o szczegółowe informacje na temat urządzenia, punktów końcowych itd. Szczegóły w dokumentacji (rozdział 9.1.2 “Bus Enumeration”).
Enumeracja następuje również po podłączeniu urządzenia z układem FX2. Wtedy zależnie od ustawień bootowania, chipset zidentyfikuje się jako domyślne typowe urządzenie USB-EZ lub zgodnie z zawartymi w pamięci EEPROM identyfikatorami. Po załadowaniu firmware do pamięci układu, kontrolę nad urządzeniem przejmuje nowy kod, który może wykonać rozłączenie i ponowne podłączenie do magistrali USB, a tym samym wywołać ponowną enumerację. Proces ten nazywa się ReNumeracją i jest opatentowany/zastrzeżony przez Cypressa.
Oczywiście kod firmware może w dowolnej chwili wykonać ReNumerację. Szczegóły można znaleźć w dokumentacji technicznej EZ-USB (rozdział 3. “Enumeration and ReNumeration™”).
W istocie kontrolę nad rozłączaniem i renumeracją dokonuje się za pomocą bitu DISCON
rejestru USBCS
(USB Control and Status). Symulacje rozłączenia USB następuje przy ustawieniu 1
, a reconnect bicie równym 0
. Przed ponownym podłączeniem można ustawić lub wyczyści bit RENUM
znajdujący się także w tym rejestrze. Bit ten specyfikuje sposób obsługi ReNumeracji, czy firmware (RENUM = 1
) ma przejąć obsługę żądań USB w EP0, czy Standardowe Urządzenie USB (RENUM = 0
) (domyślna obsługa wbudowana w układ Cypressa).
Ciekawostka taka, że DISCON
istnieje również w bajcie konfiguracyjnym EEPROM lub I2C (różne nazwy w różnych dokumentach). Jest to bajt zapisany w pamięci EEPROM tuż za 2-bajowymi VID
/PID
i 1-bajtowym DID
.
W wielu urządzeniach wykorzystujących EZ-USB, po załadowaniu firmware w razie potrzeby wykonuje renumeracje. W większości kodów źródłowych tych urządzeń i przykładów wykorzystania chipsetów Cypressa, znajdziemy poniższy fragment kodu, gdzieś na początku funkcji main
:
#ifndef NO_RENUM
// Renumerate if necessary. Do this by checking the renum bit. If it
// is already set, there is no need to renumerate. The renum bit will
// already be set if this firmware was loaded from an eeprom.
if(!(USBCS & bmRENUM))
EZUSB_Discon(TRUE); // renumerate
#endif
// unconditionally re-connect. If we loaded from eeprom we are
// disconnected and need to connect. If we just renumerated this
// is not necessary but doesn't hurt anything
USBCS &=~bmDISCON;
Funkcja EZUSB_Discon
pochodzi z frameworka (biblioteki) rozpowszechnianej wraz z SDK EZ-USB. Zgodnie z dokumentacją, funkcja ta przeprowadza rozłączenie urządzenia od magistrali USB i jego ponowne przyłączenie, wraz z ewentualną modyfikacją bitu RENUM
.
Podobnie jest w firmware Saleae Logic, w głównej funkcji znajdziemy adekwatny fragment kodu:
00000992 mov DPTR, #0xE680 ; USBCS
00000995 movx A, @DPTR
00000996 mov R7, A
00000997 mov A, R7
00000998 jb ACC1, code_9A0 ; jmp if RENUM == 1
0000099B setb RAM_20.5 ; renum = 1
0000099D lcall code_1411 ; EZUSB_Discon(renum)
000009A0
000009A0 code_9A0:
000009A0 mov DPTR, #0xE680
000009A3 movx A, @DPTR
000009A4 mov R7, A
000009A5 mov A, R7
000009A6 anl A, #0xF7
000009A8 mov R7, A
000009A9 mov DPTR, #0xE680
000009AC mov A, R7
000009AD movx @DPTR, A ; USBCS &= ~DISCON
Kod funkcji spod adresu 0x1411
odpowiada funkcji EZUSB_Discon()
, o czym można się przekonać analizując i porównując ciała obu funkcji.
Przerwania USB i AutoVectoring
EZ-USB rozszerza standardową tablicę przerwań 8051 o kilka dodatkowych pozycji. Jednym z nich są przerwania USB, które dosyć ciekawie zostały zaimplementowane. Korzystają z mechanizmu zwanego AutoVectoring, przez co 27 przerwań USB, których źródłem są żądania USB, współdzieli jedno fizyczne przerwanie USBINT
w wektorze przerwań pod adresem 0x43
. Mechanizm ten dodaję jakby kolejną tablicę przerwań/skoków na wyższym poziomie.
Kiedy następuje przerwanie USB, mikrokontroler szuka w tablicy przerwań adresu pod którym spodziewa się funkcji obsługi przerwania, czyli pod adresem 0x43
powinien znaleźć się 3 bajtowy kod skoku do kodu funkcji - ljmp addr
(02 AddrH AddrL
). Gdy w tej sytuacji włączony jest AutoVectoring - ustawiony bit AV2EN
w rejestrze INTSETUP
- pod spodziewanym adresem obsługi przerwania w rzeczywistości znajduje się kolejna tablica, ze skokami do poszczególnych funkcji obsługi żądań-przerwań USB. A tuż przed samym skokiem do kodu obsługi przerwania USBINT
hardware modyfikuje 2-gi bajt adresu (AddrL
) o offset względem odpowiedniej pozycji w tablicy skoków USB. Tym samym cała obsługa przerwań USB jest szybka (8 cykli) i bardzo prosta.
Do zrozumienia tego mechanizmu może pomóc poniższy schemat (zaczerpnięty z dokumentacji):
Prezentuje on obsługę żądania EP1-OUT
(na rysunku jest bład - EP2), dla którego EZ-USB modyfikuje adres skoku, dokonując zmianę kodu z LJMP 0x400
na LJMP 0x42C
i obsługa przerwania trafia w kod pod tym adresem. Kod ten jest wykonywany i EZ-USB skacze do dedykowanej funkcji zlokalizowanej pod adresem 0x0119
obsługującej przerwanie USB dla punktu końcowego 1.
Szczegóły techniczne całego mechanizmu dokładnie opisano w dokumentacji (rozdział 4.5 “USB-Interrupt Autovectors”).
Zaglądając do kodu firmware analizatora logicznego, w wektorze przerwań pod 0x43
znajduję podobny kod, adres skoku pozwala określić pozycje tablicy skoków USB_Jmp_Table
.
00000043 USBINT:
00000043 ljmp USBINT_0
Znajduje się ona pod adresem 0x0F00
:
00000F00 USBINT_0:
00000F00 ljmp USBINT_0_0 ; SUDAV
00000F03 nop
00000F04 ljmp code_151B ; SOF
00000F07 nop
00000F08 ljmp code_1505 ; SUTOK
00000F0B nop
00000F0C ljmp code_14D5 ; SUSPEND
00000F0F nop
00000F10 ljmp code_133E ; USB RESET
[...]
Poszczególne adresy prowadza do fragmentów kodu związanych z fizyczną obsługą przerwań USB.
SUDAV Interrupt
Przy obsłudze żądań przez firmware, gdy RENUM = 1
, wszystkie requesty USB z jakimi musi poradzić sobie kod firmware’u wywołują przerwanie SUDAV
(Setup Data Available). A jedynie Set Address jest automatycznie obsługiwany przez hardware.
Przerwanie żądania następuje dopiero po poprawnym odbiorze 8 bajtów z pakietu USB SETUP i załadowaniu ich do bufora SETUPDAT
(0xE6B8
). Który można przedstawić w postaci struktury:
struct USB_SETUP_PACKET {
UCHAR RequestType;
UCHAR Request;
USHORT Value;
USHORT Index;
USHORT Length;
};
W odpowiedzi na żądanie USB, firmware musi podjąć odpowiednio zareagować, w czym EZ-USB bardzo pomaga i wszystko upraszcza do minimum.
W większości spotykanych implementacji, kod obsługi przerwania SUDAV
ogranicza się do zasygnalizowania takiego faktu przez ustawienie odpowiedniej flagi, a realne działanie podejmowane jest w pętli głównej programu. Co można przedstawić w postaci kodu:
volatile BOOL GotSUD; // Received setup data flag
// Setup Data Available Interrupt Handler
void ISR_Sudav(void) interrupt 0
{
GotSUD = TRUE; // Set flag
EZUSB_IRQ_CLEAR();
USBIRQ = bmSUDAV; // Clear SUDAV IRQ
}
void main(void) {
// ...
GotSUD = FALSE; // Clear "Got setup data" flag
// ...
while (TRUE) {
if (GotSUD) { // Wait for SUDAV
SetupCommand(); // Implement setup command
GotSUD = FALSE; // Clear SUDAV flag
}
// ...
}
}
Oczywiście w kodzie analizatora nie ma wyjątku i całość działa tak samo.
Patrząc pod kod obsługi przerwania SUBDAV
ulokowany pod adresem 0x14BD
, wyraźnie widać, że dokładnie odzwierciedla on przedstawiony wyżej kod funkcji ISR_Sudav
:
000014BD USBINT_0_0: ; SUDAV Interrupt
000014BD push ACC
000014BF push DPH0
000014C1 push DPL0
000014C3 setb RAM_20.1 ; GotSUD = TRUE
000014C5 anl EXIF, #0xEF ; External Interrupt Flag(s)
000014C8 mov DPTR, #0xE65D ; USBIRQ
000014CB mov A, #1 ; SUDAV
000014CD movx @DPTR, A ; USBIRQ = SUDAV
000014CE pop DPL0
000014D0 pop DPH0
000014D2 pop ACC
000014D4 reti
W głównej funkcji main
, tuż na początku pętli adekwatny kod do tego przedstawionego w języku C:
00001069 RESET_0: ; main
[...]
000009B3 code_9B3:
000009B3 jnb RAM_20.1, code_9BB ; jmp if GotSUD = FALSE
000009B6 lcall code_F6 ; SetupCommand()
000009B9 clr RAM_20.1 ; GotSUD = FALSE
000009BB code_9BB:
[...]
000009F8 sjmp code_9B3
000009FA ret
Wszystko przebiega w typowy i standardowy sposób.
Get Descriptor
Jako, że w poruszanym temacie najbardziej interesuje mnie obsługa deskryptorów, a uściślając to ich pobieranie przez hosta, dochodzę do sedna tematu. W przypadku żądań Get_Descriptor
, urządzenie odsyła do hosta bazując na tablicy deskryptorów wymagane informacje za pomocą punktu EP0-IN
.
Wspominałem wcześnie o ułatwianiu, jakie hardware nam oferuje w wielu elementach związanych z obsługą żądań USB, bardzo widoczne jest to przy odsyłaniu deskryptorów do hosta. EZ-USB do tego celu udostępnia specjalny wskaźnik do danych konfiguracyjnych - Setup Data Pointer - do którego firmware przekazuje 16-bitowy adres na początek żądanego deskryptora. A następnie czyszcząc flagę HSNAK
(ustawiając 1
) pozwala EZ-USB przetransferować cały deskryptor do hosta. Wskaźnik Setup Data Pointer implementowany jest przez 2 rejestry SUDPTRH
(0xE6B3
) i SUDPTRL
(0xE6B4
), zawierające po połówce słowa, wysoki i niski bajt adresu.
Funkcja SetupCommand
, która to realnie zajmuje się obsługą przerwań USB, odpalana w main
-ie, co wcześniej udało mi się pokazać, w typowych implementacjach oparta jest na kilku instrukcjach switch
, co można przedstawić w postaci:
void SetupCommand(void) {
switch(SETUPDAT[1]) {
case SC_GET_DESCRIPTOR: // *** Get Descriptor
switch(SETUPDAT[3]) {
case GD_DEVICE: // Device
SUDPTRH = MSB(pDeviceDscr);
SUDPTRL = LSB(pDeviceDscr);
break;
case GD_DEVICE_QUALIFIER: // Device Qualifier
SUDPTRH = MSB(pDeviceQualDscr);
SUDPTRL = LSB(pDeviceQualDscr);
break;
case GD_CONFIGURATION: // Configuration
SUDPTRH = MSB(pConfigDscr);
SUDPTRL = LSB(pConfigDscr);
break;
// ...
}
// Acknowledge handshake phase of device request
EP0CS |= bmHSNAK;
}
Kod firware zawarty pod adresem 0x00F6
jest odpowiednikiem typowej funckji SetupCommand
, pobiera on typ żądania (SETUPDAT + 1
) i w kolejnej funkcji 0x1043
, która na pierwszy rzut oka wydaje się dosyć skomplikowana (jakaś większa implementacja optymalnego switcha?), gdzie bazując na danych z pakietu USB SETUP
skacze finalnie do odpowiedniego fragmentu kodu.
Dla deskryptora urządzenia kod ten rozpoczyna się od adresu 0x013F
i jak można się domyśleć pakuje on do wskaźnika danych koniguracyjnych adres deskryptora urządzenia:
0000013F mov A, RAM_23 ; pDeviceDscr high
00000141 mov R7, A
00000142 mov R6, #0
00000144 mov A, R7
00000145 anl A, #0xFF
00000147 mov R7, A
00000148 mov DPTR, #0xE6B3 ; SUDPTRH
0000014B mov A, R7
0000014C movx @DPTR, A
0000014D mov R7, RAM_24 ; pDeviceDscr low
0000014F mov A, R7
00000150 anl A, #0xFF
00000152 mov R7, A
00000153 mov DPTR, #0xE6B4 ; SUDPTRL
00000156 mov A, R7
00000157 movx @DPTR, A
Adres deskryptora urządzenia (0x0056
) na początku programu ładowany jest do zmiennej RAM_23
i RAM_24
:
00000799 mov R2, #0
0000079B mov R1, #0x56
0000079D mov R6, RAM_2
0000079F mov R7, RAM_1
000007A1 mov RAM_23, R6
000007A3 mov RAM_24, R7
Podobnie wygląda kod związany z pozostałymi deskryptorami.
…
Znając dokładnie cały proces związany z deskryptorami mogę poszukać innego również prostego sposobu na opisywany problem, niż ten przedstawiony na początku quick hack.
A opcji jest wiele, choć wydaje mi się, że najprostsze są najlepsze. Można zmodyfikować fragment kod firmware w funkcji main
, gdzie odbywa się cała logika wokół EZUSB_Discon
. Cel łatwo osiągnąć modyfikując skok, na przykład zamiast jb
pod 0x0998
wstawić jmp
do 0x09A0
. Albo jeszcze wyżej, po co w ogóle sprawdzać i modyfikować rejestr USBCS
. Można rownież walnąć kilka (3) NOP
-ów zamiast wspomnianego calla EZUSB_Discon
pod 0x099D
. Albo po prostu przed załadowaniem firmware do urządzenia, ustawić bit RE NUM na 1, wtedy cały ten problem będzie pomijany.
Gdy już można bezproblemowo bawić się konsolą CyConsole, to czas wracać do eksperymentów i hacków przeprowadzanych na kodzie firmware. Bo głównym celem jest obsługa 2-bajtowo-adresowanych kostek EEPROM.
Komentarze (0)