Rival 100 – USB reverse engineeringOkoło 11 minut

Wstęp

Cóż dożyliśmy czasów w których nawet myszka optyczna (jestem z pokolenia które pamięta kulkowe 😉 ) jest do pewnego stopnia programowalnym urządzeniem. Mój nabytek Rival 100 posiada nawet niepozorny element jakim jest dioda RGB. Producent dostarcza oprogramowanie do jej obsługi jednakże nie wspiera systemu Linux. Oprogramowanie to jest do pewnego stopnia skryptowalne, ale do pełnej swobody obsługi jeszcze sporo mu brakuje. No i co istotniejsze jest „ciężkie”.

Celem tego postu nie jest przekazanie Ci wzorcowego podejścia do zagadnienia inżynierii wstecznej ani przedstawienie złożoności protokołu USB. Jest to bardziej zdanie relacji z wykonywanego at-hoc projekciku mogące pomóc w podobnych zmaganiach.

Identyfikacja urządzenia i analiza deskryptora HID

Wracając do meritum sprawy nie udało mi się znaleźć dokumentacji sterownika do obsługi tej myszki w którym jasno byłby opisany sposób wysyłania komend wykraczających poza ramy standardowego HID’owskiego sterownika. Postanowiłem zatem podejść do sprawy z odrobinę innej strony i podejrzeć co właściwie „idzie po kablu”. Pobrałem do tego celu narzędzie Free Device Monitoring Studio pozwalające na nasłuchiwanie komunikacji po USB i nie tylko.

Po uruchomieniu aplikacji widoczne są następujące urządzenia: Na podstawie zawartości zakładki HID Descriptor dla urządzenia pierwszego i ostatniego (grupa Urządzenia interfejsu HID) można bez problemu wysnuć wniosek, że są to standardowe deskryptory kolejno klawiatury i myszki. Pozostaje zatem zagadkowe Urządzenie wejściowe numer 2, którego deskryptor na pierwszy rzut oka nie zawiera wskazówek do czego mogłoby służyć:

HID Descriptor
Usage Page: 65472 (Vendor Specific)
Usage: 1 (Unknown)
Collection 
    Usage Page: 65473 (Vendor Specific)
    Logical Minimum: 0
    Logical Maximum: 255
    Report Size: 8
    Usage: 240 (Unknown)
    Report Count: 32
    Input: 2 (Data,Variable,Absolute,No Wrap,Linear,Preferred State,No Null Position)

    Usage: 241 (Unknown)
    Report Count: 32
    Output: 2 (Data,Variable,Absolute,No Wrap,Linear,Preferred State,No Null Position,Non Volatile)
End Collection 

Co więcej jeśli porównamy zawartość zakładki Device Descriptor tego elementu oraz SteelSeries Rival 100 okazuje się, że odnoszą się one do tego samego urządzenia fizycznego:

Connection Information
 Port: 7
 Speed: Full Speed
 Device address: 5
 Open pipes: 2
 Connection status: Device connected

Device Descriptor
 USB version: 1.10
 Device class: 0x0 - (Defined at Interface level)
 Device subclass: 0x0 - Unknown
 Device protocol: 0x0 - Unknown
 Control pipe max size: 8 bytes
 Vendor ID: 0x1038 (SteelSeries ApS)
 Product ID: 0x1702 (Unknown)
 Product version: 0.68
 Manufacturer: String descriptor 1 (unable to read)
 Product: String descriptor 2 (unable to read)
 Serial Number: Not specified
 Configurations: 1

Nasza myszka widoczna jest zatem jako urządzenie kompozytowe które jest cytując:

Peripheral device that supports more than one device class. Many different devices are implemented as composite devices. For example they consist of a certain device class, but also an USB disk that has all the necessary drivers stored so that the device can be installed automatically, without the need to have access to a certain driver software.

A w prostszych słowach grupą wirtualnych urządzeń dostarczających różne funkcjonalności z których część może być obsługiwana przy pomocy standardowych systemowych sterowników, a pozostałe wymagają dodatkowego oprogramowania. Hmm „it makes sense” 😀 .

Warto spróbować przeanalizować wspomniany wcześniej deskryptor USB pomimo, zawsze to jakiś punkt startowy:

HID Descriptor
Usage Page: 65472 (Vendor Specific)
Usage: 1 (Unknown)
Collection 
    Usage Page: 65473 (Vendor Specific)
    Logical Minimum: 0
    Logical Maximum: 255
    Report Size: 8
    Usage: 240 (Unknown)
    Report Count: 32
    Input: 2 (Data,Variable,Absolute,No Wrap,Linear,Preferred State,No Null Position)

    Usage: 241 (Unknown)
    Report Count: 32
    Output: 2 (Data,Variable,Absolute,No Wrap,Linear,Preferred State,No Null Position,Non Volatile)
End Collection 

Sensowny opis HID’a możesz znaleźć na stronie www.rennes.supelec.fr. Ale wracając do deskryptora mamy tutaj hmm:

  • 32 pola
  • każde pole ma 8 bitów, a wartości zmieniają się w przedziale 0..255
  • pola te są składnikiem zarówno raportu wejściowego i wyjściowego

Przypomina to serializację struktury którą można zaimplementować następująco:

struct MyConfiguration{
  uint8_t data[32];
}

Bez wątpienia jest to jakaś poszlaka biorąc pod uwagę to, że jest to jedyny raport który nie jest elementem standardowym, a producent nie połasił się bardziej szczegółowo przedstawić nam jego struktury.

Sprawdźmy zatem w jaki sposób przebiega komunikacja z zagadkowym urządzeniem. W tym przypadku powinien wystarczyć processing umożliwiający wyświetlenie zinterpretowanej zawartości danych na podstawie deskryptora tzw: HID View: Po rozpoczęciu nasłuchu pojawia się okno: I tyle 🙁 żadnych danych … cisza.

Ale chwila jeszcze nie uruchomiliśmy aplikacji która mogłaby rozmawiać z myszką jaką jest Steel Series Engine 3. Po jej uruchomieniu nagle urządzenie ożywa i dostajemy dość dużą ilość komunikatów głównie wysyłanych do myszki (pełna lista):

000005: Report Arrived (DOWN), 2018-03-04 14:45:06,1568136 +0,0001799
Report Name:Unknown
Unknown[0..255]: 33
Unknown[0..255]: 9
Unknown[0..255]: 0
Unknown[0..255]: 2
Unknown[0..255]: 0
Unknown[0..255]: 0
Unknown[0..255]: 32
Unknown[0..255]: 0
Unknown[0..255]: 22
Unknown[0..255]: 0
Unknown[0..255]: 0
...

000006: Class-Specific Request Sent (DOWN), 2018-03-04 14:45:06,2167774 +0,0599638
Request Type:Set Report (Data Field)
Report Type:Output
ReportID:0x0
Parsed Report:
Report Name:Unknown
Unknown[0..255]: 5
Unknown[0..255]: 0
Unknown[0..255]: 0
Unknown[0..255]: 0
Unknown[0..255]: 0
...
000007: Class-Specific Data (UP), 2018-03-04 14:45:06,2180246 +0,0012472
Request Type:Set Report (Data Field)


000008: Class-Specific Request Sent (DOWN), 2018-03-04 14:45:06,2685296 +0,0505050
Request Type:Set Report (Data Field)
Report Type:Output
ReportID:0x0
Parsed Report:
Report Name:Unknown
Unknown[0..255]: 11
Unknown[0..255]: 0
Unknown[0..255]: 0
Unknown[0..255]: 0
...

No i to już jest coś !!!

Aplikacja w momencie uruchomienia z założenia powinna wysłać myszce profil gracza, jego preferowane nastawy CPI i inne informacje. Mamy zatem już dość ogólny pogląd na to czego się spodziewać. Czas pobawić się w detektywa i zacząć szukać który pakiet jest odpowiedzialny za jaką funkcjonalność.

Inżynieria wsteczna otrzymywanych raportów

Zacznijmy zatem od CPI (czułość) w aplikacji mamy dwa konfigurowalne tryby pracy. Na pierwszy ogień weźmy CPI 1 ustawiamy najmniejszą czułość i przeklikujemy się przez kolejne pozycje otrzymując trochę danych do przeanalizowania link.

Po analizie komunikacji przebiegającej z myszką można wyodrębnić dwie różniące się od siebie grupy raportów. Pierwszy zawiera liczbę 5 w polu o indeksie 0 oraz pozostałe pola o zerowej wartości (lub innej ale stałej):

Unknown[0..255]: 5
Unknown[0..255]: 0
Unknown[0..255]: 0
...

Druga grupa rozpoczyna się zawsze od bajtów mających wartość 3 i 1 następnie w porządku malejącym występują kolejno liczby 8,7..1 według schematu:

Unknown[0..255]: 3
Unknown[0..255]: 1
Unknown[0..255]: x
...   

Hmm zobaczmy co się stanie dla drugiego trybu CPI. Powtarzamy zatem wspomniane wcześniej kroki dla drugiego pokrętła. Otrzymany raport wygląda identycznie z tą tylko różnicą, że wartość drugiego bajtu wynosi 2:

Unknown[0..255]: 3
Unknown[0..255]: 2
Unknown[0..255]: x
...   

W sumie logiczne konfigurujemy w końcu pierwszy i drugi tryb CGI.

Podsumowując kolejnym trybą począwszy od 4000 do 250 przypisane są kolejne liczby 1..8.

Skoro wiemy już jak ustawiana jest czułość możemy zająć się czymś innym. Kolejną kuszącą zakładką jest ACCELERATION/DECELERATION przeklikowujemy oba slidery i … nic.

Widoczne są tylko zagadkowe ramki rozpoczynające się od liczby 5 i tyle. Możliwe, że ta funkcjonalność jest realizowana po stronie komputera.

Z widocznych na pierwszy rzut oka opcji pozostał nam POLLING RATE i tutaj już ponownie możemy zobaczyć raporty zawierające jakieś sensowne informacje. I znowu można wyróżnić grupę zachowujących się zgodnie z naszymi oczekiwaniami raportów tym razem posiadających 4 w pierwszym bajcie:

Unknown[0..255]: 4
Unknown[0..255]: 0
Unknown[0..255]: y
...  

Wartość trzeciego bajta zmienia się od 1 do 4 odpowiednio dla wybranej częstotliwości raportowania pozycji myszki: 1000,500,250,125 Hz.

No to mamy już rozkodowany drugi parametr, ale ile pozostało ? Zastanówmy się jaka jest rola pierwszego bajtu. W moim odczuciu jest to indeks umożliwiający wybór właściwości którą chcemy zmienić, a drugi bajt może indeksować podkategorię tak, jak to miało miejsce w przypadku CPI. Możemy zatem wrócić do zrzutu komunikatów z procesu uruchomienia aplikacji i zobaczyć czego jeszcze możemy się spodziewać. Wysłane przez aplikację raporty zaczynają się kolejno od wartości:

  • 22,11,7,9
  • 5 – już kilka razy się pojawił, ale nie znamy jego przeznaczenia
  • 4 – polling rate
  • 3 – CPI

Przeklikowujemy dalej. Może pora pomygać wbudowaną w myszkę diodą RGB. Zmagania rozpocząłem od najprostszego trybu pracy czyli Steady przypisującego myszce stały kolor wpisując kolejno skrajne wartości RGB otrzymałem następujący dump.

No i nareszcie wiadomo do czego służy ten tajemniczy raport rozpoczynający się od liczby 5. Zmienia on wartość koloru diody na podstawie przekazanych wartości w przestrzeni barw RGB:

Unknown[0..255]: 5
Unknown[0..255]: 0
Unknown[0..255]: R_value
Unknown[0..255]: G_value
Unknown[0..255]: B_value
...  

Po wejściu w tryb Disable Ilumination można zauważyć, że został wysłany raport ustawiający diodę na kolor o wartościach (0,0,0). Wejście w tryby Multi color Breathe i ColorShift skutkuje istną eksplozją raportów, które programowo zmieniają kolor iluminacji wysyłając kolejne wartości kolorków na takiej samej zasadzie co w przypadku Steady.

Inaczej sytuacja wygląda w przypadku trybu Single Color Breathe po przełączeniu diody w ten tryb pracy wysyłane są dwa raporty. Pierwszy z nich to dobrze znany nam kolor świecenia pozostaje zatem rozkodować znaczenie drugiego. Odpowiada on za szybkość zmian jasności diody:

Unknown[0..255]: 7
Unknown[0..255]: 0
Unknown[0..255]: speed
... 

W tym przypadku wartości rozpoczynając się od 2 (SLOW), a kończą na 4 (FAST). Pozostaje pytanie skąd taka rozbieżność z dość logicznym dotychczas formatem raportów. Odpowiedź można uzyskać przechodząc do dowolnej innej zakładki. Wartość 1 odpowiada za wyłączenie tego efektu i oddanie kontroli nad zmianą koloru komputerowi.

Udało nam się zatem rozkodować znaczenie najbardziej istotnych z naszego punktu widzenia raportów. Dodam jeszcze, że raport rozpoczynający się od bajtu o wartości 9 odpowiada za zapisanie konfiguracji w pamięci urządzenia, a 11-tka jest powiązana z funkcjonalnością klawisza zmiany trybu CPI któremu można przypisać inną funkcję.

Spróbujmy zatem podsumować otrzymane informacje w postaci hipotetycznego fragmentu kodu w C realizującego rozkodowywanie raportów po stronie urządzenia:

struct MyConfiguration{
  uint8_t data[32];
}

enum CPI_MODE {CPI_4000=1, CPI_2000, CPI_1750, CPI_1500, CPI_1250, CPI_1000, CPI_500, CPI_250};
enum POLLING_RATE {POLL_1000=1, POLL_500, POLL_250, POLL_125};
enum BLINK_SPEED {BLINK_OFF=1, BLINK_SLOW, BLINK_MEDIUM, BLINK_FAST};

void onNewData(uint8_t* data){
  switch(data[0]){
    case 3:
      setCPI(data[1],data[2]);
      break;
    case 4:
      setPollingRate(data[2]);
      break;
    case 7:
      setLEDBlinkSpeed(data[2]);
      break; 
    case 5:
      setLedColor(data[2],data[3],data[4]);
      break;
    case 9:
      saveConfiguration();
      break;
    default:
      break;
  }
}