SaeLog #5: Testy, enumeracja i deskryptory USB w FX2

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.

saelog5-pid-vid

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.

saelog5-descriptors-tree

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.

saelog5-descriptors

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):

saelog5-autovectoring

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.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *