Wyciek bazy modułu

https://chacker.pl/

Wyciek adresu bazy modułu jest prosty, jeśli mamy wskaźnik funkcji. Po prostu skanujemy pamięć, aż znajdziemy nagłówek ELF. Dodajmy prymityw leak_module_base (/labs/qemu_xpl.py):

Ta implementacja wykorzystuje mechanizm leak_multiple w celu ujawnienia zawartości 160 adresów na iterację.

Pełny wyciek przestrzeni adresowej

https://chacker.pl/

Jeśli naszym zamiarem jest zbudowanie ROP lub ret2lib, który działa niezależnie od znajomości układu pliku binarnego, będziemy potrzebować prymitywu odczytu zdolnego do dostępu do pełnej przestrzeni adresowej. Jednym ze sposobów osiągnięcia tego jest manipulowanie wskaźnikiem używanym przez standardowe żądanie urządzenia, które zwraca dane do hosta. Dobrym kandydatem na standardowe żądanie jest GET_DESCRIPTOR. Kiedy przekazujemy USB_DT_STRING w wValue żądania, jest ono przetwarzane przez usb_desc_string (zdefiniowane w /qemu/hw/usb/desc.c):

Funkcja usb_desc_string wywołuje usb_desc_get_string (1), aby iterować po liście dev->strings (2), aż do znalezienia indeksu. Następnie usb_desc_string kopiuje zawartość zwróconego wskaźnika, aż do znalezienia bajtu zerowego lub osiągnięcia maksymalnego rozmiaru bufora (3). Możliwe jest nadpisanie nagłówka listy w dev->strings i sprawienie, aby wskazywał na obiekt USBDescString utworzony przez nas. Później możemy wysłać żądanie GET_DESCRIPTOR i zwrócić dane z naszego kontrolowanego wskaźnika s->str. Dobrym miejscem na umieszczenie naszego fałszywego obiektu USBDescString jest wnętrze s->data_buf. Zawartość może zostać zapisana w jednym ujęciu podczas wyzwalania przepełnienia bufora. Aby to zrobić, wymagane są pewne zmiany w prymitywie relative_write (/labs/qemu_xpl.py), więc napiszmy nowy:

Teraz dowolny zapis przyjmuje nowy argument (data_buf_contents), który jest IOVector przekazanym do usb_out (1) podczas wyzwalania przepełnienia bufora. W ten sposób możemy umieścić dodatkowe dane w s->data_buf. Może to być przypadek, gdy wiemy z góry, z których adresów chcemy wyciekać. Zamiast tworzyć pojedynczy USBDescString i wywoływać prymityw raz dla każdego adresu, możemy skorzystać z argumentu indeksu usb_desc_string:

Metoda pomocnicza descr_build (1) przyjmuje listę adresów i produkuje listę powiązaną elementów USBDescString, a każdy element (2) ma numer indeksu przypisany do określonego adresu. Drugi argument (start_addr) to adres wewnątrz s->data_buf. Nowy prymityw leak_multiple (3) buduje tę listę powiązaną i nadpisuje s->strings (4) adresem nagłówka listy. Lista powiązana zaczyna się od &s->data_buf[256] , pozostawiając pierwsze 256 bajtów bufora wolnych dla zawartości zwróconej przez desc_string. Na koniec desc_string jest wielokrotnie wywoływany dla każdego numeru indeksu (5) powiązanego z adresami jednej listy, które mają przeciekać.

UWAGA: desc_string jest zaimplementowany w ehci.py i służy do wysyłania żądania GET_DESCRIPTOR z argumentami USB_DT_STRING i index.

Arbitralny odczyt

https://chacker.pl/

Aby zmienić względny odczyt na dowolny odczyt, potrzebujemy adresu s->data_buf. Możemy uzyskać adres s (USBDevice) z pola urządzenia struktury punktu końcowego zero (USBEndpoint) w s->ep_ctl. Ta struktura jest zdefiniowana w /qemu/include/hw/usb.h:

Jeśli wycieknie cała struktura USBDevice (w przyszłości będziemy potrzebować więcej z tej struktury), możemy użyć s->ep_ctl.dev do obliczenia s->data_buf. Pierwszą rzeczą, którą zrobi nasz exploit, będzie wyciek tej struktury (/labs/qemu_xpl.py):

Następnie przekształcenie względnego odczytu w odczyt dowolny jest proste

Metoda addr_of (1) służy do rozwiązywania adresu bezwzględnego dowolnego pola należącego do USBDevice, więc w arbitrary_read_near (2) używamy jej do uzyskania adresu s->data_buf. Należy pamiętać, że ten prymityw jest nadal ograniczony do zakresu ±2 GB. Dlatego nazwa metody zawiera sufiks „near”.

Relative Read Primitive

https://chacker.pl/

Prymitive odczytu wymaga wysłania pakietu IN. Funkcja QEMU, która przetwarza pakiety IN, to do_token_in (zdefiniowana w /qemu/hw/usb/core.c):

Wywołanie usb_packet_copy jest osiągane, jeśli USB_DIR_IN jest obecne w s->setup_buf[0]. Ponieważ użyliśmy pakietu OUT do uszkodzenia s->setup_index, nie jest to możliwe. W tym momencie jest za późno, aby wysłać więcej pakietów SETUP w celu ustawienia USB_DIR_IN, ponieważ stan był już uszkodzony; możemy jednak przekazać ujemne przesunięcie do relative_write i rozbić s->setup_buf. Zobaczmy, jak zaimplementować relative_read (/labs/qemu_xpl.py) zgodnie z tym podejściem:

Prymityw relative_read jest konstruowany przez przygotowanie danych (1) do zarówno niedopełnienia, jak i przepełnienia s->data_buf w jednym ujęciu. Wycinek niedopełnienia (setup_buf) (2) rozbija zawartość s->setup_buf, aby ustawić USB_DIR_IN, podczas gdy wycinek przepełnienia (3) rozbija s->setup_len i s->setup_index. Następnie relative_write (4) jest używany do uszkodzenia stanu i teraz możemy wysłać pakiet IN, aby wyciekły dane.

WSKAZÓWKA: Użyj GDB do przetestowania i zbadania wszystkich prymitywów eksploitów i wyświetl zawartość USBDevice w taki sam sposób, jak zrobiliśmy to tutaj. W sesji debugowania możemy zobaczyć, że przepełnienie bufora jest używane do ustawienia s->setup_len (1) na 0x1010 bajtów i s->setup_index (2) na -0x1018, co po dodaniu długości wynosi -8 (to początek s->setup_buf). Następnie dowolny zapis ustawia s->setup_len i s->setup_index z rzeczywistą długością i przesunięciem, z których chcemy odczytać, ale także ustawia s->setup_buf[0] (3) na 0x80 (USB_DIR_IN), więc teraz możemy wysłać pakiet IN, aby odczytać dane.

Relative Write Primitive

https://chacker.pl/

Aby stworzyć nasz pierwszy prymityw, zacznijmy od przyjrzenia się strukturze USBDevice (zdefiniowanej w /qemu/include/hw/usb.h). W szczególności przyjrzymy się polom zaraz po data_buf, które można rozbić za pomocą przepełnienia:

Możemy kontrolować setup_index i setup_len i zawsze ustawiać setup_state na SETUP_STATE_DATA. Pamiętaj, że dane są kopiowane w następujący sposób:

Kontrolując s->setup_index, możemy zmienić przepełnienie bufora na względny zapis ±2GB z adresu s->data_buf. Bazując na naszej metodzie trigger_overflow, możemy zbudować prymityw relative_write w następujący sposób (/labs/qemu_xpl.py):

Najpierw mamy metodę pomocniczą overflow_build (1) do budowania danych binarnych potrzebnych do rozbicia s->setup_len i s->setup_index. (2) Argument overflow_len jest wymagany do dostosowania s->setup_index, który jest zwiększany po wywołaniu usb_packet_copy. Nasz prymityw relative_write (3) przyjmuje względne przesunięcie do s->data_buf (dodatnie lub ujemne) i dane (IOVector) do zapisu.

Wykorzystanie

https://chacker.pl/

Ta sekcja obejmuje wszystkie kroki wymagane do pełnego wykorzystania tej luki. Zaczniemy od poprzedniego kodu wyzwalacza i będziemy iteracyjnie budować bardziej zaawansowane prymitywy, aż w końcu uzyskamy wykonanie kodu za pomocą techniki ret2lib. Aby z łatwością manipulować strukturami C, w pliku cstruct.py udostępniono klasę CStruct. Jest to dostosowana podklasa construct.Struct, która zapewnia informacje o wyrównaniu i przesunięciu podobne do C dla pól struktury.

Uruchamianie wyzwalacza

https://chacker.pl/

Zobaczmy w debugerze, jak wyzwalacz przepełnia bufor:

Po połączeniu z GDB ustawiamy warunkowy punkt przerwania w usb_packet_copy (1), który ulega przerwaniu, jeśli długość kopii wynosi 0x1020 bajtów. Gdy punkt przerwania zostanie osiągnięty, pozwalamy funkcji wykonać kopię (2) i powrócić do wywołującego. Następnie możemy zbadać zawartość po zakończeniu s- >data_buf (3) i potwierdzić, że zawartość została zniszczona za pomocą wzorca 0xff.

Wyzwalanie błędu

https://chacker.pl/

Wcześniej ustaliliśmy, że błąd pozwala nam ustawić nieprawidłowy s->setup_len poprzez wysłanie pakietu SETUP z polem wLength większym niż 4096. Na etapie danych s->data_buf może zostać przepełniony podczas przetwarzania pakietu OUT. Funkcja QEMU, która przetwarza pakiety OUT, nazywa się do_token_out i można ją znaleźć w /qemu/hw/usb/core.c (wewnątrz kontenera Docker). Zobaczmy, w jakich warunkach może zostać wywołane to przepełnienie:

Jeśli wyślemy pakiet SETUP zawierający prawidłowy wLength, może to doprowadzić do ścieżki kodu, która ustawia s->setup_state na SETUP_STATE_DATA. Oznacza to, że przepełnienie bufora może zostać wywołane przez wysłanie dwóch kolejnych pakietów SETUP (z których pierwszy zawiera prawidłowy wLength) i jednego pakietu OUT. Faktyczna operacja kopiowania jest wykonywana przez usb_packet_copy (/qemu/hw/usb/core.c). Jeśli przyjrzymy się tej funkcji bliżej, możemy zobaczyć, że kierunek kopiowania jest określany przez PID pakietu:

W przypadku pakietów SETUP lub OUT wywoływane jest iov_to_buf (1). Tymczasem w przypadku pakietów IN wywoływane jest iov_from_buf (2). Na podstawie tego, co już wiemy, napiszmy dowód koncepcji, który wyzwala przepełnienie i rozbija 32 bajty poza bufor. Poniższy kod można znaleźć w /labs/trigger.py:

Pierwszy pakiet SETUP(1) (prawidłowa długość) powoduje ustawienie s->state za pomocą SETUP_STATE_DATA, a drugi pakiet SETUP (2) uszkadza s->setup_len do overflow_len. Pakiet OUT (3) zapisuje zawartość data do s->data_buf i przepełnia go o 32 bajty.

Kontroler EHCI

https://chacker.pl/

Kontroler EHCI zarządza komunikacją między urządzeniami USB a stosem oprogramowania hosta. Jego przestrzeń rejestrów składa się z dwóch zestawów rejestrów: rejestrów zdolności i rejestrów operacyjnych. Musimy uzyskać dostęp do rejestru CAPLENGTH (zdolność), aby uzyskać przesunięcie, w którym zaczynają się rejestry operacyjne. Rejestry z zestawu rejestrów operacyjnych służą do kontrolowania stanu operacyjnego kontrolera. EHCI zapewnia dwa interfejsy harmonogramu dla transferów danych: harmonogram okresowy i harmonogram asynchroniczny. Oba mechanizmy opierają się na kontrolerze EHCI przechodzącym przez struktury danych obecne w pamięci hosta, reprezentujące kolejki elementów roboczych. Użyjemy harmonogramu asynchronicznego, ponieważ jest prostszy, ale każdy interfejs harmonogramu może spełnić nasze potrzeby. Harmonogram asynchroniczny przechodzi przez strukturę danych znaną jako lista transferów asynchronicznych, która jest cykliczną listą elementów Queue Head (QH). Rejestr operacyjny ASYNCLISTADDR przechowuje wskaźnik do następnego elementu QH, który ma zostać przetworzony przez harmonogram asynchroniczny. Queue Head zaczyna się od typu elementu (w tym przypadku typu QH) i wskaźnika do następnego QH. Następne pola to cechy i możliwości punktu końcowego, po których następuje wskaźnik do bieżącego deskryptora transferu elementu kolejki (qTD). Reszta QH to obszar nakładki transferu powiązany z bieżącym qTD. QTD jest używany do reprezentowania jednej lub większej liczby transakcji USB. Zawiera wskaźnik do następnego qTD i alternatywnych qTD, token qTD i pięć wskaźników bufora umożliwiających transfery do 20 KB. Token qTD koduje (między innymi) całkowitą liczbę bajtów do przesłania (z/do wskaźników bufora) oraz kod PID, który jest używany do generowania tokenów IN, OUT lub SETUP. Aby włączyć lub wyłączyć harmonogram asynchroniczny, a także go uruchomić lub zatrzymać, używamy rejestru operacyjnego USBCMD. Polecenia wydane przez USBCMD nie mają natychmiastowego efektu, więc musimy sprawdzić zmiany statusu, sondując rejestr USBSTS. Kontroler obsługuje kilka rejestrów „Statusu portu i kontroli” (PORTSCn); będziemy używać tylko rejestru PORTSC0 do włączania i resetowania portu zerowego. Logikę obsługi EHCI naszego frameworka można znaleźć w module ehci.py (nie pokażemy jego kodu ze względu na ograniczenia miejsca), oddzielonym od kodu exploita. Oto metody dostarczane przez ten moduł:

  • qtd_single Generuje pojedynczy qTD na podstawie tokena i argumentu danych.
  • qh_single Generuje pojedynczy (samoodwołujący się) QH na podstawie qTD.
  • port_reset Ustawia „port reset”, po którym następuje „port enabled” w PORTSC0.
  • async_sched_stop Zatrzymuje harmonogram asynchroniczny.
  • async_sched_run Po podaniu argumentu QH ustawia ASYNCLISTADDR i uruchamia asynchroniczny harmonogram.
  • run_single Przyjmuje token i argument danych, a następnie uruchamia transakcję przy użyciu poprzednich metod.
  • request Generuje 8-bajtowe informacje o polu danych dla standardowych żądań.
  • setup Przyjmuje argument żądania, generuje token SETUP i wywołuje run_single.
  • usb_in Przyjmuje argument długości danych, generuje token IN i wywołuje run_single. Odczytuje dane przesłane z funkcji i zwraca je jako ciąg bajtów.
  • usb_out Przyjmuje argument danych (IOVector) i przesyła go do funkcji (OUT).

UWAGA: Klasy IOVector i Chunk są zdefiniowane w remotemem.py. Umożliwiają one reprezentację zakresów pamięci z „dziurami”, unikając w ten sposób nadmiernego przesyłania danych przez wirtualny port szeregowy.

Skanowanie magistrali PCI

https://chacker.pl/

Dzięki tej nowej konfiguracji kontroler Enhanced Host Controller Interface (EHCI) powinien być obecny na magistrali PCI gościa. Możemy to zweryfikować za pomocą modułu pci.py dołączonego do naszego frameworka. Ten moduł wstrzykuje kod skanujący magistralę PCI po uruchomieniu jądra naszego gościa:

UWAGA :Nasza nowa klasa bazowa nazywa się teraz Session i jest uogólnieniem klasy Fuzzer, którą zaimplementowaliśmy wcześniej

Kontroler EHCI ma kod klasy 0x0c (1), podklasę 0x03 (2) i interfejs (prog_if) 0x20 (3). BAR0 wskazuje na podstawę przestrzeni rejestrów EHCI pod adresem 0xfebf1000 (4).