Kradzież tokenów

https://chacker.pl/

Co możemy zrobić z dowolnym odczytem i zapisem w jądrze? Co najmniej możemy eskalować nasze uprawnienia do SYSTEMU poprzez kradzież tokenów. Proces jest reprezentowany w jądrze systemu Windows za pomocą struktury _EPROCESS (jak w Executive Process). Pierwszym członkiem _EPROCESS jest pole Pcb, które jest zagnieżdżoną strukturą _KPROCESS (jak w Kernel Process). _EPROCESS i _KPROCESS zawierają ogromną ilość informacji o każdym procesie, takich jak identyfikator procesu, nazwa obrazu, token bezpieczeństwa, informacje o sesji, informacje o zadaniu i informacje o wykorzystaniu pamięci. Każdy proces systemu Windows ma powiązany z nim obiekt tokena bezpieczeństwa. Monitor referencyjny bezpieczeństwa jądra używa tokenów do określania dostępnych i aktywnych uprawnień, członkostwa w grupach i innych informacji związanych z uprawnieniami, gdy trzeba podjąć decyzje związane z bezpieczeństwem. Poszczególne wątki również mogą mieć powiązane z nimi tokeny. Struktura _EPROCESS zawiera członka Token, który jest wskaźnikiem zliczanym przez referencje do struktury _TOKEN. To jest podstawowy token procesu. Jeśli nie zostanie nadpisany, proces dziedziczy swój podstawowy token od procesu nadrzędnego. Pomysł kradzieży tokenów polega na nadpisaniu pola Token bieżącego procesu o niższych uprawnieniach wskaźnikiem _TOKEN z procesu o wyższych uprawnieniach. Łatwym i spójnym procesem, z którego można ukraść token o wysokich uprawnieniach, jest proces System. Proces System zawsze ma identyfikator procesu 4 i zawsze ma w pełni uprzywilejowany token z uprawnieniami SYSTEM. Kiedy proces potomny zostanie utworzony pod procesem, którego token został nadpisany skradzionym tokenem SYSTEM, będzie miał uprawnienia SYSTEM. Struktura _EPROCESS zawiera kilka członków, które pomogą nam w tej technice. Pole UniqueProcessId zawiera identyfikator procesu procesu, taki jak można zobaczyć w Menedżerze zadań, więc możemy go użyć do zidentyfikowania procesu systemowego i naszego własnego procesu. Członek ActiveProcessLinks jest podwójnie powiązaną listą, która łączy całą listę procesów. Wskaźniki Flink i Blink nie wskazują na szczyt następnej struktury _EPROCESS, ale raczej na członka ActiveProcessLinks następnej struktury _EPROCESS, jak pokazano poniżej. Oznacza to, że podczas przeglądania listy procesów będziemy musieli odczytać członka ActiveProcessLinks w określonym przesunięciu do _EPROCESS, a następnie odjąć to samo przesunięcie od odczytanej wartości, aby dostać się na szczyt następnego procesu na liście.

Plan zakłada znalezienie wskaźnika do procesu systemowego (PID 4), skopiowanie jego tokena, przejrzenie listy ActiveProcessLinks w celu znalezienia struktury _EPROCESS bieżącego PID, nadpisanie bieżącego tokena procesu tokenem systemowym, a następnie wykonanie podprocesu z podwyższonymi uprawnieniami. Wszystkie pola, z których należy odczytać dane lub do których należy zapisać dane, mają rozmiar wskaźnika, co nieco uprości kod exploita. Aby znaleźć wskaźnik do procesu systemowego, możemy zajrzeć do samego obrazu jądra, który ma wskaźnik do procesu systemowego w symbolu PsInitialSystemProcess. Przesunięcia do każdego pola w _EPROCESS i do symbolu PsInitialSystemProcess zmieniają się między wersjami jądra, więc będziemy musieli uwzględnić to w exploit.

Interakcja ze sterownikiem

https://chacker.pl/

Teraz, gdy wykonaliśmy statyczną inżynierię wsteczną kodu i zidentyfikowaliśmy ścieżkę do dowolnego memmove, napiszmy kod do interakcji ze sterownikiem. Analiza dynamiczna jest bardzo potężnym narzędziem w inżynierii wstecznej i procesie rozwoju exploitów, więc będziemy wywoływać sterownik za pomocą kodu i używać debugera, aby obserwować, co się stanie. Podłącz debuger jądra i uzyskaj przesunięcie funkcji memmove, umieszczając kursor na funkcji i uruchamiając get_screen_ea() – idaapi.get_imagebase() w IDAPython. Spowoduje to przesunięcie od podstawy sterownika do funkcji, którą chcesz debugować. Następnie ustaw punkt przerwania dla funkcji w WinDbg, wydając polecenie bp z nazwą sterownika i względnym przesunięciem: bp dbutil_2_3+0x5294. Jeśli WinDbg zgłasza, że ​​nie może rozwiązać wyrażenia, upewnij się, że sterownik jest załadowany i spróbuj wydać polecenie .reload do debugera, a następnie sprawdź, czy punkt przerwania jest ustawiony poprawnie za pomocą bl. Po ustawieniu punktu przerwania potrzebujemy kodu, aby go faktycznie uruchomić. Aby nieco zmienić tempo, użyjemy Rust do napisania tego narzędzia. Na docelowej maszynie wirtualnej pobierz i zainstaluj albo Visual Studio Community, albo Build Tools for Visual Studio. Zainstalowane narzędzia są wymagane przez łańcuch narzędzi Rust Windows MSVC do kompilowania i łączenia programów. Zainstaluj Rust na maszynie docelowej, pobierając rustup-init.exe z https://rustup.rs i używając go do zainstalowania łańcucha narzędzi x86_64-pc-windows-msvc. Utwórz nowy projekt za pomocą cargo new –lib dbutil. Napiszemy narzędzie, które pozwoli nam określić IOCTL i bufor do przekazania do sterownika DBUtil za pośrednictwem funkcji DeviceIoControl. Dodaj następujące wiersze do pliku Cargo.toml w obszarze [dependencies]:

„Crate” w Rust to pakiet. Skrzynia winapi zapewnia powiązania interfejsu funkcji obcych (FFI) z interfejsem API systemu Windows, umożliwiając nam interakcję z interfejsem API systemu Windows bez konieczności ręcznego deklarowania wszystkich typów systemu Windows i prototypów funkcji, których potrzebujemy. Możesz również spróbować użyć oficjalnej skrzyni Microsoft Rust Windows-rs. Ta deklaracja włączy wszystkie funkcje, których potrzebujemy zarówno dla skryptu wywołującego IOCTL, jak i exploita. Moduł hex pozwoli nam zamienić ciąg heksadecymalny na bajty, które zostaną przekazane do sterownika. Najpierw musimy móc otworzyć uchwyt urządzenia za pomocą CreateFileA. Dodaj poniższe do pliku lib.rs w katalogu src projektu:

Importy u góry (1) zostaną wykorzystane do wszystkich funkcji zarówno wywołującego IOCTL, jak i exploita, więc nie martw się o nieużywane ostrzeżenia podczas kompilacji. Funkcja open_dev otworzy uchwyt do sterownika DBUtil za pośrednictwem jego nazwy łącza symbolicznego, którą widzieliśmy podczas statycznego inżynierii wstecznej sterownika. Prefiks \\\\.\\ to współczesny sposób na powiedzenie „DosDevices”, który reprezentuje globalną przestrzeń nazw. Do ciągu dodano \0, ponieważ funkcja oczekuje ciągu C zakończonego znakiem NULL.(2) Funkcja null_mut() jest odpowiednikiem przekazania wartości NULL jako argumentu(3). Następnie napiszmy inną funkcję w lib.rs, aby wywołać kontrolkę IO urządzenia za pośrednictwem DeviceIoControl:

Ta funkcja przyjmuje HANDLE do urządzenia DBUtil (1), kod IOCTL (2), bufor wejścia/wyjścia (3) i rozmiar bufora wejścia/wyjścia (4). Pamiętaj, że sterownik oczekuje, że rozmiary wejścia i wyjścia zawsze będą takie same podczas wywoływania IOCTL, więc po prostu przekażemy ten sam bufor i długość dla argumentów bufora wejścia i wyjścia oraz rozmiaru. Funkcja zwraca wartość logiczną reprezentującą, czy się powiedzie (5). Teraz, gdy mamy dwie funkcje, musimy wejść w interakcję ze sterownikiem, utwórz folder w katalogu src projektu o nazwie bin, a następnie utwórz w nim plik o nazwie ioctlcall.rs. Wypełnij plik następującą zawartością:

Główna funkcja binarnego pliku ioctlcall otworzy urządzenie (1), pobierze argumenty programu jako wektor ciągów znaków (2), przeanalizuje numer IOCTL z pierwszego argumentu (3), zdekoduje dane wejściowe heksadecymalne z drugiego argumentu (4), wywoła określony IOCTL (5), a następnie wydrukuje bufor wejścia/wyjścia (3). Spróbujmy osiągnąć punkt przerwania, uruchamiając cargo run –bin ioctlcall 0x9B0C1EC4 112233445566778899101112131415161718192021222324. Pamiętaj, że określamy dane wejściowe w formacie heksadecymalnym, więc potrzeba 48 znaków, aby przedstawić 24 bajty. Punkt przerwania powinien zostać osiągnięty! Ale dlaczego 24 bajty? Przypomnij sobie funkcję memmove w sterowniku: oczekuje ona co najmniej 0x18 (24) bajtów dla bufora wejściowego i wyjściowego. Sprawdź zawartość pierwszego argumentu tej funkcji (rcx) za pomocą polecenia dqs @rcx. Powinieneś zobaczyć 8-bajtowy wskaźnik jądra, a następnie 0000000000000018. Jak odkryliśmy poprzez analizę statyczną, pierwszym argumentem tej funkcji jest wskaźnik do DeviceExtension. Przypomnij sobie początek kodu obsługi IOCTL: bufor wejściowy/wyjściowy (_IRP->AssociatedIrp.SystemBuffer) został przeniesiony do DeviceExtension przy przesunięciu 0, a rozmiar bufora wejściowego (_IO_STACK_LOCATION- >Parameters.DeviceIoControl.InputBufferLength) został przeniesiony do DeviceExtension przy przesunięciu 8. Zanotuj adres rozszerzenia SystemBuffer na później! Pojedynczy krok 26 razy (p 0n26) do wywołania memmove i zbadaj argumenty memmove w rejestrach rcx, rdx i r8: r rcx,rdx,r8. Od razu powinieneś zauważyć, że kontrolujemy część rdx; górne 4 bajty (16151413) wydają się pasować do bajtów 13–16 naszego wejścia (pamiętaj: little endian). Użyjmy WinDbg, aby zobaczyć różnicę między wartością, którą widzimy w rdx, a naszym rzeczywistym wejściem w tej lokalizacji w buforze:

Główna funkcja binarnego pliku ioctlcall otworzy urządzenie (1), pobierze argumenty programu jako wektor ciągów znaków (2), przeanalizuje numer IOCTL z pierwszego argumentu (3), zdekoduje dane wejściowe heksadecymalne z drugiego argumentu (4), wywoła określony IOCTL (5), a następnie wydrukuje bufor wejścia/wyjścia (3). Spróbujmy osiągnąć punkt przerwania, uruchamiając cargo run –bin ioctlcall 0x9B0C1EC4 112233445566778899101112131415161718192021222324. Pamiętaj, że określamy dane wejściowe w formacie heksadecymalnym, więc potrzeba 48 znaków, aby przedstawić 24 bajty. Punkt przerwania powinien zostać osiągnięty! Ale dlaczego 24 bajty? Przypomnij sobie funkcję memmove w sterowniku: oczekuje ona co najmniej 0x18 (24) bajtów dla bufora wejściowego i wyjściowego. Sprawdź zawartość pierwszego argumentu tej funkcji (rcx) za pomocą polecenia dqs @rcx. Powinieneś zobaczyć 8-bajtowy wskaźnik jądra, a następnie 0000000000000018. Jak odkryliśmy poprzez analizę statyczną, pierwszym argumentem tej funkcji jest wskaźnik do DeviceExtension. Przypomnij sobie początek kodu obsługi IOCTL: bufor wejściowy/wyjściowy (_IRP->AssociatedIrp.SystemBuffer) został przeniesiony do DeviceExtension przy przesunięciu 0, a rozmiar bufora wejściowego (_IO_STACK_LOCATION- >Parameters.DeviceIoControl.InputBufferLength) został przeniesiony do DeviceExtension przy przesunięciu 8. Zanotuj adres rozszerzenia SystemBuffer na później! Pojedynczy krok 26 razy (p 0n26) do wywołania memmove i zbadaj argumenty memmove w rejestrach rcx, rdx i r8: r rcx,rdx,r8. Od razu powinieneś zauważyć, że kontrolujemy część rdx; górne 4 bajty (16151413) wydają się pasować do bajtów 13–16 naszego wejścia (pamiętaj: little endian). Użyjmy WinDbg, aby zobaczyć różnicę między wartością, którą widzimy w rdx, a naszym rzeczywistym wejściem w tej lokalizacji w buforze:

Przejdź do debugera jądra i pobierz bazę jądra za pomocą polecenia ? nt. Ustaw bajty 9–16 bufora wejścia/wyjścia programu ioctlcall na bazę jądra w little-endian, bajty 17–20 na 0, a następnie upewnij się, że bufor ma co najmniej 24 + 8 (32) bajtów długości. Ustaw pozostałe bajty na 0. Na przykład, jeśli podstawa jądra znajduje się w 0xfffff80142000000, bufor wejściowy powinien wynosić

000000000000000000000004201f8ffff0000000000000000000000000000000000 00.

Spójrz na bufor wyjściowy wydrukowany jako wynik, a następnie porównaj ostatnie 8 bajtów bufora z pierwszymi 8 bajtami podstawy jądra:

Powinny pasować! Znaki MZ powinny być Ci znane, ponieważ stanowią pierwsze 2 bajty pliku PE (podobnie jak jądro). Jako dodatkowe ćwiczenie przetestuj w podobny sposób możliwość dowolnego zapisu. Nie będziesz w stanie zapisywać do bazy jądra, ponieważ znajduje się ona w pamięci tylko do odczytu, ale możesz znaleźć inne miejsca do zapisu, w tym w trybie użytkownika. Po potwierdzeniu, że możemy pomyślnie kontrolować parametry memmove, aby uzyskać dowolny odczyt i zapis, możemy przetłumaczyć strukturę na kod. Wznów dodawanie kodu do pliku lib.rs:

Tag repr(C) mówi kompilatorowi Rust, aby wyrównał strukturę jak strukturę C. Tag derive(Default) mówi, że wszystkie typy w tej strukturze implementują cechę core::default::Default, więc cała struktura również. Domyślna wartość dla liczb całkowitych to po prostu 0. Możesz się zastanawiać, dlaczego element ptr jest usize, a nie LPVOID (*mut c_void); usize zawsze ma rozmiar wskaźnika, a rzutowanie z dowolnych typów wskaźników na usize jest łatwiejsze niż rzutowanie z dowolnych typów wskaźników na LPVOID. Teraz, gdy mamy pojęcie, jak wygląda oczekiwana struktura i jak wywołać memmove z dowolnymi parametrami, możemy zacząć pisać exploit, aby podnieść się do poziomu SYSTEM. Zanim jednak napiszesz więcej kodu, musisz zrozumieć, co tak naprawdę oznacza „podniesienie się do poziomu SYSTEM”.

Inżynieria wsteczna sterownika

https://chacker.pl/

Mając plik sterownika w ręku, załaduj go do wybranego deasemblera i zbadaj punkt wejścia — w przykładach użyto IDA Pro.

UWAGA: Proces inżynierii wstecznej w tym laboratorium ma na celu wskazanie odpowiednich części programu. Być może będziesz musiał poświęcić czas na zapoznanie się z dokumentacją i inżynierią wsteczną, aby dojść do tych samych wniosków!

Wszystkie sterowniki zaczynają się od funkcji DriverEntry. W zależności od ustawień kompilatora, funkcja DriverEntry będzie albo tym, co napisał programista, albo automatycznie wstawionym stubem, który inicjuje plik cookie bezpieczeństwa dla całego sterownika, a następnie przechodzi do oryginalnego DriverEntry. Ten sterownik w rzeczywistości ma automatycznie wstawiony stub znany jako GsDriverEntry. Znajdź ostatnią instrukcję tej funkcji (jmp) i przejdź do funkcji, do której się odwołuje; ta funkcja jest prawdziwym DriverEntry. Na górze prawdziwego DriverEntry powinieneś zobaczyć kilka wywołań memmove i RtlInitUnicodeString, jak pokazano poniżej. Twój deasembler może lub nie pokazywać ciągów znaków, do których się odwołujesz.

Pokazane ciągi są ważne, ponieważ są następnie przekazywane do IoCreateDevice i IoCreateSymbolicLink. Oznacza to, że będziemy mogli wchodzić w interakcję z utworzonym urządzeniem z trybu użytkownika za pośrednictwem symlinku. Wywołanie IoCreateDevice pokazuje kilka innych informacji, takich jak DeviceType (0x9B0C) i DeviceExtensionSize (0xA0), jak pokazano poniżej.

Jeśli zarówno urządzenie, jak i utworzenie dowiązania symbolicznego powiedzie się, sterownik przeniesie wskaźnik funkcji do rax, a następnie wskaźnik funkcji zostanie przeniesiony do różnych przesunięć od rdi, jak pokazano na poniższej ilustracji. Prześledź, co znajduje się w rdi, a powinieneś odkryć, że jest to wskaźnik do _DRIVER_OBJECT, pierwotnie w rcx. Zaczynając od przesunięcia 0x70 w _DRIVER_OBJECT znajduje się tablica MajorFunction, więc funkcja przenoszona do rax musi być głównym programem obsługi funkcji dla sterownika. Obsługuje on cztery główne funkcje:

IRP_MJ_CREATE (0), IRP_MJ_CLOSE (2),

IRP_MJ_DEVICE_CONTROL (14) i

IRP_MJ_INTERNAL_DEVICE_CONTROL (15).

Następnie spójrz na górę głównego handlera funkcji, pokazanego tutaj. Dla ułatwienia zrozumienia, niektóre instrukcje zostały opatrzone adnotacjami o przesunięciach struktury i odpowiednimi wartościami stałych.

Jak widać, funkcja odwołuje się do pól w obu argumentach przekazanych do głównego programu obsługi funkcji: _DEVICE_OBJECT w rcx i _IRP w rdx. Pamiętaj, że struktura _IRP zawiera szczegóły dotyczące wykonywanego żądania. Najpierw _IO_STACK_LOCATION jest przenoszone do r8, a DeviceExtension jest przenoszone do rdi. Następnie stała 14 (IRP_MJ_DEVICE_CONTROL) jest porównywana z pierwszym bajtem _IO_STACK_LOCATION, który jest polem MajorFunction. Gdy sterowanie urządzeniem I/O jest przekazywane do sterownika, to sprawdzenie nie wykona skoku, zamiast tego kontynuując do następnego bloku. W następnym bloku bufor wejściowy (_IRP->AssociatedIrp.SystemBuffer) jest przenoszony do rax, a następnie umieszczany w rdi+0, czyli DeviceExtension+0. Następnie długość bufora wejściowego (_IO_STACK_LOCATION->Parameters.DeviceIoControl.InputBufferLength) jest przenoszona do DeviceExtension+8. Zobaczymy te dwie wartości odwoływane później, więc miej je na uwadze. Następnie długość bufora wejściowego jest porównywana z długością bufora wyjściowego (_IO_STACK_LOCATION->Parameters.DeviceIoControl.OutputBufferLength) i nie kontynuuje przetwarzania żądania sterowania I/O, jeśli nie są równe. Ta informacja stanie się ważna, gdy później spróbujemy napisać kod do interakcji ze sterownikiem. Podczas wyszukiwania luk w skompilowanych programach dobrą praktyką jest rozpoczęcie od wyszukiwania wywołań funkcji, które manipulują pamięcią, takich jak strcpy, memcpy i memmove. Otwórz odwołania krzyżowe do funkcji memmove w swoim deasemblerze. W IDA naciśnij klawisz X na funkcji, aby wyświetlić okno pokazane obok.

Poświęć trochę czasu na przejrzenie wszystkich tych wywołań memmove. Prześledź ich argumenty (rcx, rdx i r8), aby sprawdzić, czy możesz kontrolować którykolwiek z nich. Pamiętaj, że wartości pobrane ze struktur _IRP->AssociatedIrp.SystemBuffer i _IO_STACK_LOCATION->Parameters.DeviceIoControl można bezpośrednio kontrolować z trybu użytkownika. Pamiętaj również, że SystemBuffer i InputBufferSize zostały przeniesione do DeviceExtension odpowiednio na przesunięciach 0 i 8. Mam nadzieję, że po pewnych poszukiwaniach uznasz wywołanie memmove w sub_15294 za interesujące.

W zależności od wartości dl, wartość w r9+0x18 jest przenoszona do rcx (miejsce docelowe) lub rdx (źródło). Na początku tego fragmentu kodu, inny argument memmove znajduje się w rcx, a rozmiar ruchu jest przenoszony z eax do r8d. Prześledź dalej, aby zobaczyć, skąd pochodzą r9, rcx i eax, jak pokazano poniżej.

Wygląda na to, że rax pochodzi z rbx+0x10, a r9 pochodzi z rbx. W tym momencie wiemy, że zarówno argument rozmiaru, jak i źródło lub miejsce docelowe pochodzą z bufora w rbx. Kontynuuj śledzenie, aby znaleźć, że rcx (pierwszy argument) został przeniesiony do rbx w pierwszym bloku funkcji. Śledzenie do wywołującego za pomocą odniesienia krzyżowego pokazuje, że rdi został przeniesiony do rcx wewnątrz głównego programu obsługi funkcji, jak pokazano tutaj.

Wygląda na to, że rax pochodzi z rbx+0x10, a r9 pochodzi z rbx. W tym momencie wiemy, że zarówno argument rozmiaru, jak i źródło lub miejsce docelowe pochodzą z bufora w rbx. Kontynuuj śledzenie, aby znaleźć, że rcx (pierwszy argument) został przeniesiony do rbx w pierwszym bloku funkcji. Śledzenie do wywołującego za pomocą odniesienia krzyżowego pokazuje, że rdi został przeniesiony do rcx wewnątrz głównego programu obsługi funkcji, jak pokazano tutaj.

Prześledzenie jednego bloku z każdego z tych dwóch bloków ujawnia kody IOCTL dla każdego z nich: 0x9B0C1EC4 i 0x9B0C1EC8.

W tym momencie mamy wszystkie informacje, których potrzebujemy, aby przejść do dynamicznej analizy sterownika. Jako dodatkowe ćwiczenie spróbuj dowiedzieć się, co robią inne IOCTL w tym sterowniku. Funkcjonalność, którą właśnie zidentyfikowaliśmy, nie jest jedynym problemem z tym sterownikiem!

Uzyskiwanie sterownika docelowego

https://chacker.pl/

W ostrzeżeniu firmy Dell napisano, że luki w zabezpieczeniach dotyczą „pakietów narzędzi do aktualizacji oprogramowania układowego, w tym narzędzi do aktualizacji BIOS-u, narzędzi do aktualizacji oprogramowania układowego Thunderbolt, narzędzi do aktualizacji oprogramowania układowego TPM i narzędzi do aktualizacji oprogramowania układowego stacji dokującej”.4 Mając to na uwadze, przejdź do witryny firmy Dell i zacznij szukać potencjalnie zagrożonych aktualizacji. Jednym z aktualizatorów, który zawiera sterownik, jest aktualizacja BIOS-u Dell Latitude 7204 Rugged A16. W chwili pisania tego tekstu ta aktualizacja jest najnowszą dla tego systemu i nadal zapisuje podatny sterownik na dysku. Jako dodatkowe ćwiczenie spróbuj znaleźć inną aktualizację, która zawiera podatny sterownik. Jeśli znajdziesz inne sterowniki po drodze, zapisz je do późniejszego ćwiczenia inżynierii wstecznej. Wspomniany aktualizator i kopię sterownika docelowego można znaleźć w repozytorium GitHub książki. Uruchom aktualizator BIOS-u (lub wybraną aktualizację) w systemie Windows i sprawdź w C:\Users\<Twój użytkownik>\AppData\Local\Temp plik o nazwie DBUtil_2_3.sys. Jeśli nie możesz znaleźć pliku, poszukaj w C:\Windows\Temp. Możesz również uruchomić Sysinternals Process Monitor i ustawić filtr dla „Path, Ends With, DBUtil_2_3.sys”, aby zobaczyć, kiedy sterownik jest zapisywany na dysku lub wywoływany.

Wybór celu

https://chacker.pl/

Jednym z najbardziej palących i istotnych pytań w badaniach nad lukami w zabezpieczeniach jest „jak wybrać cel?”. Chociaż możemy nie być w stanie odpowiedzieć na to pytanie, warto się nad nim zastanowić, ponieważ odnosi się ono do bieżącego tematu. Jeśli chcesz wejść w eksploatację jądra systemu Windows i sterowników jądra, od czego zacząć? Rozpoczęcie od próby znalezienia luk w samym jądrze lub sterownikach opracowanych przez Microsoft może być nieco trudne lub zniechęcające. Jednym z łatwiejszych i bardziej dostępnych punktów wyjścia są znane podatne sterowniki. Kiedyś Microsoft miał znacznie mniej rygorystyczny proces podpisywania sterownika. Obecnie Microsoft wymaga od programistów sterowników przesyłania ich sterowników do portalu w celu uzyskania podpisu wydania Windows Hardware Quality Labs (WHQL).2 Microsoft wydawał certyfikaty wydawcy oprogramowania (SPC), aby strony trzecie mogły podpisywać własne sterowniki przed publikacją; zaprzestali programu po wycieku kilku certyfikatów, a niektórzy wydawcy podpisywali źle zaprojektowane lub celowo niedostatecznie zabezpieczone sterowniki. Niektóre z tych podpisanych sterowników SPC są nadal szeroko dystrybuowane, jak zobaczysz w tej sekcji.

W sierpniu 2019 r. na konferencji DEFCON 27 badacze z Eclypsium Labs zaprezentowali szereg sterowników z lukami, podkreślając ten konkretny problem. Na ich liście znajdowało się 39 sterowników, które umożliwiają operacje takie jak dowolny wirtualny i fizyczny odczyt i zapis, dowolny odczyt-zapis-wykonanie alokacji pamięci jądra oraz dowolny odczyt i zapis rejestru specyficznego dla modelu (MSR). Funkcje te nie są z natury lukami, ponieważ uprzywilejowane aplikacje, takie jak aktualizatory BIOS-u, muszą ich używać, aby działać prawidłowo, ale tutaj liczy się dostęp wymagany do ich wykorzystania. Te sterowniki są dostępne z trybu użytkownika na dowolnym poziomie uprawnień w systemie. W niektórych przypadkach nawet procesy działające z niską lub niezaufaną integralnością mogą je wywołać. Oznacza to, że każdy z wykonywaniem kodu może potencjalnie podnieść swoje uprawnienia do SYSTEMU lub jądra. Sterowniki utworzone przy użyciu starszego modelu sterowników systemu Windows (WDM) mają domyślnie otwarte uprawnienia dostępu. Listy kontroli dostępu można ustawić za pośrednictwem interfejsu API systemu Windows lub we wpisie rejestru dla sterownika; jednak twórcy tych sterowników nie zrobili ani jednego, ani drugiego, ujawniając w ten sposób uprzywilejowaną funkcjonalność. W maju 2021 r. badacz Sentinel Labs Kasif Dekel opublikował artykuł szczegółowo opisujący szeroko rozpowszechniony sterownik Dell z podobnymi problemami do tych na liście sterowników Eclypsium.4 Jedną z interesujących rzeczy dotyczących tego sterownika jest zakres dystrybucji — prawie 400 platform zostało dotkniętych tym problemem i ujawnieniem. Sterownik nazywa się DBUtil_2_3.sys i jest dołączony do narzędzi aktualizujących Dell i Alienware od 2009 r. Został podpisany przez zewnętrzną firmę Dell SPC i nie został sprawdzony ani przesłany do firmy Microsoft. Ponieważ jest to niedawna luka w zabezpieczeniach i ma tak duży zakres, jest idealnym celem do nauki wykorzystywania jądra.

Konfigurowanie debugowania jądra

https://chacker.pl/

Aby rozpocząć, będziesz potrzebować dwóch maszyn wirtualnych z systemem Windows 10 i wybranego oprogramowania do wirtualizacji (VMware, VirtualBox, Parallels itd.). Możesz również użyć systemu Windows 11, jeśli masz jego kopię, ponieważ proces i wyniki powinny być takie same. Jeśli masz licencję systemu Windows i maszyny wirtualne są już skonfigurowane, to świetnie! Jeśli w ogóle nie masz maszyn wirtualnych z systemem Windows 10, masz kilka opcji: pobierz obraz ISO systemu Windows 10 od firmy Microsoft i użyj wersji próbnej systemu Windows lub przejdź do zasobów dla programistów systemu Windows i pobierz starszą wersję VM dla programistów programu Internet Explorer. Sprawdź sekcję „Dalsze informacje”, aby uzyskać łącza. Ta ostatnia jest nadal dostarczana przez firmę Microsoft w momencie pisania, chociaż może to ulec zmianie!

UWAGA: Te testowe maszyny wirtualne są dopuszczalne do użytku laboratoryjnego, ale jeśli zamierzasz używać systemu Windows jako swojego systemu operacyjnego lub komercyjnie, musisz kupić licencję. Kradzież jest zła!

Gdy masz jedną maszynę wirtualną Windows 10 skonfigurowaną według własnych upodobań, utwórz pełny lub połączony klon. Jedna maszyna wirtualna będzie maszyną debugera, na której zainstalujesz WinDbg, a druga będzie celem debugowania. WinDbg Preview można zainstalować ze sklepu Microsoft Store, a WinDbg Classic można zainstalować z zestawu Windows SDK. Aby zainstalować WinDbg Classic, pobierz zestaw Windows SDK i wybierz Narzędzia debugowania dla systemu Windows w instalatorze. Po zakończeniu wszelkich niezbędnych instalacji włącz debugowanie jądra sieciowego, używając bcdedit z powłoki administratora na docelowej maszynie wirtualnej:

Hostip można ustawić na dowolną wartość, jeśli łączysz się przez WinDbg Preview lub określ zmienną docelową w ciągu połączenia WinDbg Classic; w przeciwnym razie ustaw ją na adres IP maszyny wirtualnej debugera. Skopiuj zwrócony klucz na maszynę debugera, ponieważ będzie on potrzebny do połączenia zdalnego. Uruchom ponownie maszynę wirtualną docelową, aby uruchomić tryb debugowania. Połącz się z WinDbg Preview, przechodząc do Plik | Dołącz do jądra, a następnie wprowadzając wymagane informacje na karcie Sieć. W przypadku WinDbg Classic lub kd.exe użyj flagi -k w wierszu poleceń i wprowadź ten ciąg połączenia, zastępując wartości w nawiasach kątowych wartościami specyficznymi dla Twojego środowiska:

Jeśli połączenie zostanie nawiązane pomyślnie, powinieneś otrzymać aktywny monit w punkcie przerwania (int 3) znajdującym się pod adresem jądra (zaczynającym się od 0xfffff). Wiersz poleceń również będzie aktywny. Jeśli nie możesz się połączyć, sprawdź adres IP celu, upewnij się, że obie maszyny wirtualne mogą połączyć się przez sieć i spróbuj wyłączyć zaporę systemu Windows na obu komputerach. Po nawiązaniu połączenia możesz poeksperymentować z kilkoma poleceniami. WinDbg jest na początku bardzo przytłaczający, ale nie martw się, z czasem staje się łatwiejszy. Po skonfigurowaniu debugowania jądra jesteś teraz gotowy, aby przejść do identyfikacji celu hakowania jądra!

Debugowanie jądra

https://chacker.pl/

Debuger w przestrzeni użytkownika (pierścień 3) jest w stanie debugować tylko poszczególne programy, które działają na jądrze. Debuger w przestrzeni jądra (pierścień 0) jest wymagany do debugowania jądra. Debugowanie jądra jest zwykle wykonywane między dwoma systemami: jeden uruchamia debuger, a drugi jest systemem, który jest debugowany. Potrzebne są dwa systemy, ponieważ w przeciwieństwie do zawieszenia pojedynczego programu w debugerze pierścienia 3, zatrzymanie całego jądra uniemożliwiłoby interakcję z systemem w celu uruchomienia poleceń lub wznowienia go! Istnieje jeden wyjątek znany jako „lokalne” debugowanie jądra, który umożliwia wygodne debugowanie jądra aktualnie uruchomionego systemu. Główną wadą lokalnego debugowania jądra jest to, że nie można zatrzymać uruchomionego systemu, co oznacza, że ​​nie można ustawić ani wstrzykiwać żadnych punktów przerwania ani debugować w przypadku awarii, a ponieważ system jest stale uruchomiony, wartości w pamięci mogą się szybko zmieniać. Jedynym oficjalnie obsługiwanym (a zatem zalecanym) debugerem pierścienia 0 dla systemu Windows jest WinDbg, który zwykle wymawia się jako win-dee-bee-gee, wind-bag lub win-dee-bug. Jest on rozwijany i utrzymywany przez firmę Microsoft i dołączany jako część pakietów narzędzi programistycznych. WinDbg oferuje szereg różnych transportów, w których można debugować jądro. Debugowanie sieciowe jest najbardziej niezawodną, ​​wydajną i spójną konfiguracją debugowania jądra. WinDbg można uzyskać, instalując zestaw Windows SDK, WDK lub ze sklepu Microsoft Store jako WinDbg Preview. Nowszy WinDbg Preview to ten sam WinDbg, ale z interfejsem podobnym do metro. Laboratoria w tej sekcji będą używać WinDbg Preview. Jeśli jesteś bardziej fanem wiersza poleceń, możesz użyć kd.exe, aby połączyć się z systemem docelowym. Jest on dołączony obok WinDbg w zestawie SDK i WDK. Wszystkie odmiany WinDbg są wspierane przez DbgEng, który stanowi podstawową funkcjonalność WinDbg. Microsoft dołącza pliki nagłówkowe i biblioteki do interakcji z DbgEng w Windows SDK, aby programiści mogli pisać narzędzia, które wykorzystują bibliotekę, która wspiera programowo WinDbg.

Sterowniki jądra

https://chacker.pl/

Sterowniki jądra to rozszerzenia jądra, które mogą pomóc systemowi w interakcji z wcześniej nieznanymi urządzeniami lub systemami plików, zapewnić interfejs do introspekcji jądra do trybu użytkownika i zmodyfikować sposób działania jądra. To ostatnie jest mocno zniechęcane przez Microsoft, tak bardzo, że firma wprowadziła Kernel Patch Protection (znany również jako PatchGuard), aby uniemożliwić programistom ingerencję w podstawowe procedury systemowe i struktury danych. Sterowniki jądra znane jako sterowniki rozruchowe są ładowane podczas rozruchu przez program ładujący. Inne sterowniki są ładowane przez menedżera usług po uruchomieniu systemu. Tylko administratorzy lub osoby z uprawnieniami SeLoadDriverPrivilege mogą ładować sterowniki w systemie Windows. Microsoft nie uważa granicy między administratorem systemu a jądrem za granicę bezpieczeństwa, ponieważ administratorzy mogą po prostu ładować (prawie) dowolne sterowniki. Jednak sterowniki muszą mieć akceptowalny podpis cyfrowy, aby mogły zostać załadowane, ponieważ podpisywanie kodu w trybie jądra (KMCS) jest wymuszane domyślnie na wszystkich maszynach 64-bitowych. Sterownik może udostępniać procedury wejścia/wyjścia (I/O) w formie głównych funkcji. Windows Driver Kit (WDK) definiuje 28 głównych funkcji, w tym tworzenie, zamykanie, zasilanie, sterowanie we/wy, odczyt, zapis, wyszukiwanie informacji, ustawianie informacji i wyłączanie. Obsługujące funkcje dla każdej głównej funkcji są ustawiane wewnątrz struktury _DRIVER_OBJECT sterownika, gdy sterownik jest inicjowany. Ta struktura zawiera różne informacje o sterowniku, takie jak nazwa sterownika, połączona lista urządzeń skojarzonych ze sterownikiem, opcjonalna procedura rozładowania, która jest wywoływana, gdy żądane jest rozładowanie sterownika, oraz ograniczenia pamięci sterownika (start i rozmiar). Sterownik może tworzyć powiązane struktury _DEVICE_OBJECT, które reprezentują urządzenie, za które odpowiada sterownik. Urządzenia mogą być lub nie być obsługiwane przez rzeczywisty sprzęt. Przykładem sterownika nieobsługiwanego sprzętowo jest sterownik, którego Sysinternal Process Explorer używa do uzyskania dodatkowych informacji o systemie. W przypadku Process Explorera sterownik podpisany przez Microsoft jest ładowany podczas uruchamiania narzędzia, a do komunikacji z nim używane są interfejsy API trybu użytkownika. Sterownik tworzy obiekt urządzenia dostępny w trybie użytkownika i obsługuje żądania z trybu użytkownika za pośrednictwem systemu I/O w jądrze. System I/O jądra wysyła żądania do głównej procedury obsługi funkcji zdefiniowanej w _DRIVER_OBJECT, do którego należy urządzenie. Główne kody funkcji to stałe wartości całkowite zdefiniowane w nagłówkach WDK. Wszystkie ich nazwy symboli zaczynają się od IRP_MJ_ i są indeksami w głównej tablicy funkcji _DRIVER_OBJECT, zaczynając od 0x70. Główne procedury obsługi funkcji są również nazywane procedurami obsługi sterowników i mają następujący prototyp:

Pakiet żądania wejścia/wyjścia (IRP) opisuje żądanie wejścia/wyjścia do urządzenia. Zawiera wiele pól, które staną się ważne, gdy będziesz pracować nad laboratorium w dalszej części rozdziału. Kilka godnych uwagi pól obejmuje pole AssociatedIrp.SystemBuffer, które często obejmuje bufor wejściowy i/lub wyjściowy dla żądania, oraz pole Tail.Overlay.CurrentStackLocation, które zawiera informacje o żądaniu odnoszące się do konkretnego wywoływanego urządzenia. Ważne informacje w CurrentStackLocation (_IO_STACK_LOCATION) obejmują pole MajorFunction, które jest bieżącą żądaną funkcją główną, oraz pole Parameters, które jest masywną unią zawierającą różne informacje w zależności od wywoływanej funkcji głównej. W przypadku sterowania wejściem/wyjściem urządzenia, pole MajorFunction będzie miało wartość IRP_MJ_DEVICE_CONTROL (14), a pole Parameters będzie opisywać wywoływany kod sterowania wejściem/wyjściem (IOCTL) oraz rozmiary buforów wejściowych i wyjściowych. W przypadku większości wywołań IOCTL bufor wejściowy i/lub wyjściowy będzie znajdował się w polu AssociatedIrp.SystemBuffer w _IRP. Aby uzyskać więcej informacji na temat kodów IOCTL, zapoznaj się z dokumentacją systemu Windows. W laboratoriach w tym rozdziale wykonasz inżynierię wsteczną i debuggowanie sterownika jądra, aby zlokalizować urządzenie, które tworzy, określisz główne programy obsługi funkcji, które są zarejestrowane, nauczysz się wywoływać główne programy obsługi funkcji z trybu ser i ostatecznie napiszesz exploit, aby wykonać lokalną eskalację uprawnień (LPE).

Jądro systemu Windows

https://chacker.pl

Ponieważ jądro systemu Windows jest tak złożone, będziemy mogli omówić tylko podstawy jądra i pewne informacje, które będą potrzebne do zrozumienia exploita w dalszej części rozdziału. Istnieje wiele bardziej kompleksowych zasobów dotyczących jądra systemu Windows i wewnętrznych mechanizmów jądra, w tym Windows Internals, 7th Edition (Parts 1 and 2), Windows Kernel Programming autorstwa Pavla Yosifovicha oraz różne posty na blogach rozsiane po Internecie. Cennymi materiałami źródłowymi są również Windows Software Development Kit (SDK), Windows Driver Kit (WDK) oraz podręczniki procesorów Intel/AMD/ARM. Ponadto będziemy przeglądać koncepcje i wykorzystywać 64-bitowy system Windows (32-bitowy system Windows jest w niektórych przypadkach nieco inny, ale z czasem staje się coraz mniej istotny). Jądro jest implementowane jako warstwa jądra, warstwa wykonawcza i sterowniki. Warstwy jądra i wykonawcze są implementowane w obrazie jądra, ntoskrnl.exe. Warstwa jądra zawiera kod do planowania wątków, blokowania, synchronizacji i podstawowego zarządzania obiektami jądra. Warstwa wykonawcza zawiera kod do egzekwowania zabezpieczeń, zarządzania obiektami, zarządzania pamięcią, rejestrowania i instrumentacji zarządzania systemem Windows, między innymi. Większość sterowników jądra to pliki .sys, ale kilka komponentów jądra to biblioteki DLL, takie jak hal.dll i ci.dll. Plik .sys to przenośny plik wykonywalny, podobnie jak plik EXE lub DLL. Poniższy diagram systemu pokazuje ogólny układ architektury systemu Windows.

Zaczynając od dołu, istnieją aplikacje i usługi w trybie użytkownika, które są uruchamiane na podsystemie Windows (kernel32.dll, user32.dll itd.), są tworzone bezpośrednio dla natywnego API (ntdll.dll i win32u.dll) lub są uruchamiane jako procesy minimalne/pico i komunikują się bezpośrednio z jądrem za pośrednictwem System Service Dispatcher. System Service Dispatcher (znany również jako system call handler) pobiera żądania z trybu użytkownika i wysyła je do jądra. Przechodząc przez linię, powinieneś zauważyć, że adresy idą od niższych w trybie użytkownika do znacznie wyższych w trybie jądra. Pamięć jest segmentowana w ten sposób z powodów historycznych i specyficznych dla procesora. Tak się składa, że ​​istnieją dwie odrębne kanoniczne przestrzenie pamięci z dużą niekanoniczną luką pośrodku, aby podzielić pamięć należącą do przestrzeni jądra (pierścień 0) i przestrzeni użytkownika (pierścień 3). Po stronie trybu jądra znajduje się warstwa jądra, warstwa wykonawcza i sterowniki, jak wspomniano wcześniej. Niektóre sterowniki, takie jak sterowniki graficzne, mogą komunikować się bezpośrednio ze sprzętem, podczas gdy inne będą używać warstwy abstrakcji sprzętowej (HAL). HAL to niezależna od architektury i platformy biblioteka do interakcji ze sprzętem. W ostatnich wersjach systemu Windows 10 (20H1+) HAL jest implementowany wewnątrz obrazu jądra, a hal.dll to po prostu przekazująca biblioteka DLL, która nadal istnieje ze względu na kompatybilność. Nie martw się, jeśli jest to dużo do przyswojenia, ponieważ jest to tylko przegląd komponentów systemu Windows.

Wykorzystanie jądra systemu Windows

https://chacker.pl/

Jądro systemu Windows i pisanie exploitów jądra to ogromne tematy; nauka wewnętrznych mechanizmów jądra i właściwego stosowania tej wiedzy w celu wykorzystania luk w zabezpieczeniach zajmuje lata. Luki te można znaleźć nie tylko w samym jądrze, ale także w rozszerzeniach znanych jako sterowniki. Tu przyjrzymy się, jak skonfigurować debugowanie jądra między dwoma systemami Windows, przeprowadzić inżynierię wsteczną sterownika jądra, a następnie wykorzystać ten sterownik jądra w celu podniesienia naszych uprawnień.