Dlaczego PowerShell

https://chacker.pl/

Chociaż język PowerShell był błogosławieństwem dla automatyzacji systemów Windows, daje hakerom przewagę. PowerShell zapewnia nam dostęp do niemal wszystkich funkcji systemu Windows w sposób programowy. Jest rozszerzalny i może być używany do administrowania usługą Active Directory, systemami poczty e-mail, programem SharePoint, stacjami roboczymi i nie tylko. PowerShell zapewnia nam dostęp do bibliotek .NET z poziomu interfejsu skryptowego, co czyni go jednym z najbardziej elastycznych narzędzi, których można używać w środowisku Windows.

Wykorzystanie PowerShell

https://chacker.pl/

Większość systemów korporacyjnych opiera się na systemie Windows, dlatego ważne jest, abyśmy dobrze znali narzędzia dostępne w systemach Windows. Jednym z najpotężniejszych z tych narzędzi jest PowerShell. W tym rozdziale dowiesz się, co sprawia, że ​​PowerShell jest tak potężnym narzędziem, i przyjrzymy się kilku sposobom jego wykorzystania jako części naszego zestawu narzędzi do eksploatacji luk.

Podsumowanie

https://chacker.pl/

Jądro systemu Windows może być trudne, ale możliwe do opanowania przy odpowiednich zasobach i pod ręką debugera. Nieudokumentowana natura samego jądra sprawia, że ​​badanie lub wykorzystywanie go jest jeszcze bardziej czasochłonne. W laboratoriach skonfigurowałeś debugowanie jądra, wybrałeś znany podatny sterownik jądra jako cel, wykonałeś inżynierię wsteczną sterownika, napisałeś narzędzie do interakcji ze sterownikiem, a następnie napisałeś exploit LPE, wykorzystując kradzież tokenów za pośrednictwem funkcjonalności w sterowniku. Miejmy nadzieję, że dało Ci to punkt wyjścia do rozpoczęcia dalszych badań jądra!

 

Pisanie exploita jądra

https://chacker.pl/

Kontynuując pracę nad naszym kodem exploita, musimy znaleźć bazę jądra i symbol PsInitialSystemProcess. Ponieważ zakładamy, że mamy dostęp na poziomie użytkownika do tego exploita, możemy poprosić system, aby powiedział nam, gdzie znajduje się baza każdego załadowanego sterownika za pomocą funkcji EnumDeviceDrivers, a następnie możemy uzyskać nazwę sterownika pod każdym adresem bazowym za pomocą funkcji GetDeviceDriverBaseNameA:

Tutaj jest sporo do rozpakowania! Pierwsze wywołanie numDeviceDrivers umieszcza wymagany rozmiar bufora (w bajtach) w needed (1). Następnie bufor jest przydzielany do przechowywania oczekiwanego wyjścia (2), a bufor ten jest wypełniany poprzez drugie wywołanie EnumDeviceDrivers (3). Następnie adresy bazowe są iterowane, a nazwa każdego z nich jest pobierana poprzez GetDeviceDriverBaseNameA (4). Namebuf ma długość 260 bajtów (5), co możesz rozpoznać jako MAX_PATH; powinno to wystarczyć, aby zmieścić nazwę sterownika. Jeśli nazwa pasuje do ntoskrnl.exe (6), wówczas baza w tej bieżącej iteracji może zostać zwrócona jako baza jądra (7). Ponownie, ta technika działa tylko dla LPE o średniej integralności lub lepszej. Zdalne i/lub eksploity o niskiej integralności muszą znaleźć inny sposób na uzyskanie wskaźnika _EPROCESS, np. poprzez wyciek pamięci i dowolny prymityw odczytu. Na koniec możemy skonstruować eksploit. Utwórz plik w katalogu src/bin swojego projektu o nazwie exploit.rs i dodaj następujące elementy:

W głównych nawiasach najpierw wywołaj funkcję open_dev, aby uzyskać HANDLE do urządzenia. Ponieważ zadeklarowaliśmy tę funkcję jako unsafe, wywołanie musi zostać opakowane w blok unsafe:

Jako ćwiczenie wstaw po wywołaniu LoadLibraryA następujący kod:

Spowoduje to wstrzymanie programu do momentu naciśnięcia klawisza, co da Ci czas na zbadanie programu. Zdefiniuj to jako funkcję, jeśli chcesz wstrzymać program w wielu punktach. Uruchom program za pomocą cargo run –bin exploit. Następnie załaduj Sysinternals Process Explorer, znajdź proces exploita i otwórz dolny panel widoku DLL. Wyszukaj „ntoskrnl.exe” i zwróć uwagę, że adres bazowy jest adresem w trybie użytkownika. Obraz jądra, do którego odwołujesz się jako hkernel, jest tą kopią w trybie użytkownika, a nie tą w działającym jądrze. Aby uzyskać adres PsInitialSystemProcess w działającym jądrze, najpierw znajdziemy względny adres wirtualny (RVA) symbolu. RVA to po prostu przesunięcie symbolu od podstawy obrazu. Aby je obliczyć, możemy odjąć adres bazowy modułu (hkernel) (2) od adresu PsInitialSystemProcess wewnątrz kopii jądra w trybie użytkownika. W trybie użytkownika GetProcAddress (1) może zostać użyte do znalezienia dowolnego wyeksportowanego symbolu w załadowanym obrazie PE, więc możemy użyć go do znalezienia adresu symbolu. Aby uzyskać adres, którego potrzebujemy w działającym jądrze, dodaj obliczony RVA do wartości zwracanej przez get_kernel_base (3). Ponieważ każda operacja w tym procesie wymaga znacznika unsafe, możemy uwzględnić go w tym samym bloku i zakończyć go adresem PsInitialSystemProcess w działającym jądrze:

UWAGA: Brakujący średnik na końcu ostatniego wiersza, w punkcie (3), nie jest literówką i jest zamierzony. W Rust wiersz, który nie kończy się średnikiem, jest zwracany z tego bloku. W tym przypadku wartość ostatniego wiersza jest umieszczana w zmiennej lpisp. Ponieważ wartość w lpisp jest po prostu wskaźnikiem do PsInitialSystemProcess, następną rzeczą, którą musimy zrobić, jest wykorzystanie dowolnego prymitywu odczytu, aby pobrać adres wewnątrz niego:

Używa to dowolnego odczytu jądra, aby uzyskać adres struktury _EPROCESS reprezentującej proces SYSTEM (PID 4). Możesz chcieć sprawdzić, czy wartość jest poprawna. Aby to zrobić, dodaj polecenie print i pauzę (jak opisano wcześniej), a następnie zrzuć wartość wewnątrz PsInitialSystemProcess w debugerze za pomocą polecenia dq nt!PsInitialSystemProcess L1. Ponieważ kradniemy tokeny z procesu SYSTEM, następnym krokiem jest odczytanie pola Token _EPROCESS za pomocą dowolnego odczytu. W tym momencie powinieneś sprawdzić przesunięcia od podstawy struktury _EPROCESS dla pól UniqueProcessId, ActiveProcessLinks i Token. Można to łatwo zrobić w debugerze jądra za pomocą następującego polecenia:

Następnie zdefiniuj te stałe w górnej części pliku exploits.rs

Teraz odczytaj token SYSTEM z procesu systemowego za pomocą dowolnego wskaźnika:

Następny krok jest nieco bardziej skomplikowany, ponieważ teraz musimy przejść listę procesów przez podwójnie łączoną listę ActiveProcessLinks, aby znaleźć aktualnie wykonywany proces. Przechodzenie listy procesów jest nieco trudne, ponieważ lista ActiveProcessLinks nie wskazuje na początek następnego procesu; zamiast tego wskazuje na strukturę ActiveProcessLinks w następnym procesie! Aby to rozwiązać, musimy odczytać wartość w ActiveProcessLinks (2), a następnie odjąć przesunięcie ActiveProcessLinks od tej wartości, aby dostać się na początek struktury _EPROCESS dla następnego procesu na liście (3). Następnie, gdy już jesteśmy w następnym procesie, odczytujemy (4) i porównujemy pole UniqueProcessId z bieżącym identyfikatorem procesu (1). Jeśli identyfikator procesu jest zgodny, bieżący proces został znaleziony i możemy przejść do ostatniego kroku exploita. Jeśli bieżący proces nie zostanie znaleziony, program musi przejść do następnego procesu na liście i kontynuować, aż zostanie znaleziony.

W tym momencie pozostało już tylko skopiowanie tokena SYSTEM do pola Token struktury _EPROCESS bieżącego procesu, którą właśnie znaleźliśmy w ostatnim kroku. Aby to zrobić, użyj dowolnej funkcji zapisu wskaźnika, którą napisaliśmy w poprzednim laboratorium. Po nadpisaniu tokena utwórz podproces, taki jak cmd.exe:

Jeśli wszystko pójdzie dobrze, exploit powinien utworzyć powłokę i nie spowodować awarii komputera. Uruchom whoami, jak pokazano poniżej, aby sprawdzić, czy masz SYSTEM!

Możesz napotkać problemy z tym, że program Windows Defender nazwie Twój exploit malware. To malware, więc program Defender po prostu wykonuje swoją pracę! Upewnij się jednak, że po kliknięciu opcji Zezwalaj na urządzeniu wybierasz exploit z listy, a nie rzeczywisty fragment malware.

Arbitralny odczyt/zapis wskaźnika

https://chacker.pl/

Po dokładnym zrozumieniu celu kradzieży tokenów musimy złożyć elementy w całość, aby utworzyć w pełni funkcjonalny exploit eskalacji uprawnień lokalnych. Ponieważ kradzież tokenów obejmuje tylko odczyt i zapis wartości o rozmiarze wskaźnika, możemy uniknąć kłopotów związanych ze wskaźnikami struktury, dodając element usize na końcu struktury DbMemmove:

Teraz możemy napisać jedną funkcję do odczytu wskaźnika w jądrze i inną do zapisu wskaźnika w jądrze. Funkcja odczytu musi przyjąć HANDLE do urządzenia DBUtil i adres do odczytu, a następnie zwrócić zawartość tego adresu:

Ponieważ wyprowadziliśmy Default w definicji struktury, możemy wypełnić jedno pole wymagane do odczytu (1), a następnie zaakceptować wartość domyślną dla pozostałych (0 dla typów całkowitych) (2). Następnie otrzymujemy zmienny surowy wskaźnik do struktury (3), który należy przekazać do funkcji ioctl jako bufor wraz z uchwytem urządzenia, kodem IOCTL dla dowolnego odczytu i rozmiarem struktury (4). Na koniec zwracamy wartość wyjściową (5). Funkcja write musi również przyjąć DBUtil HANDLE, wskaźnik do zapisu, a następnie wartość do zapisu do tego wskaźnika. Ma on bardzo podobny format do poprzedniej funkcji; tym razem wypełnimy element danych struktury i wywołamy funkcję ioctl z dowolnym kodem IOCTL zapisu:

Dzięki tym dwóm funkcjom w naszej bibliotece mamy teraz dwa bardzo wydajne prymitywy i prawie wszystkie składniki, których potrzebujemy, aby podnieść uprawnienia zwykłego użytkownika do SYSTEM.

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.