Rejestrowanie modułów

https://chacker.pl/

Rejestrowanie modułów umożliwia szereg funkcji dotyczących sposobu ładowania skryptów i podstawowych informacji o tym, co zostało wykonane. Obejmuje to, jakie moduły i zmienne zostały załadowane, a nawet niektóre informacje o skrypcie. Rejestrowanie to znacznie zwiększa szczegółowość podczas uruchamiania skryptów programu PowerShell i może być przytłaczające dla administratora. Rejestrowanie modułów jest dostępne od wersji PowerShell v3.0 i nie jest domyślnie włączone, więc aby uzyskać to rejestrowanie, należy włączyć obiekt zasad grupy (GPO) w systemach. Chociaż ten typ rejestrowania zwiększa widoczność tego, co zostało uruchomione, w większości przypadków nie zapewnia faktycznego kodu, który został uruchomiony. Dlatego też w przypadku dochodzenia kryminalistycznego ten poziom rejestrowania jest nadal niewystarczający. Będzie jednak naprowadzał śledczych na typy rzeczy, które robiłeś, chociaż szczegóły prawdopodobnie nie zostaną zarejestrowane.

Rejestrowanie w programie PowerShell

https://chacker.pl/

We wcześniejszych wersjach programu PowerShell (przed v4.0) dostępnych było tylko kilka opcji rejestrowania. Pozwalało nam to działać bez tworzenia wielu alertów dziennika podczas ładowania programu PowerShell, a także bardzo utrudniało osobom zajmującym się analizą kryminalistyczną ustalenie, co robiliśmy. Rejestrowanie rejestrowało tylko fakt załadowania programu PowerShell. W nowszych wersjach programu PowerShell dostępne są dodatkowe opcje zwiększające rejestrowanie w programie PowerShell. Z tego powodu ukierunkowanie na najnowszą wersję systemu Windows może zdradzić więcej informacji o tym, co robisz, niż w starszych wersjach.

UWAGA: Omawiamy tylko kilka aspektów rejestrowania w programie PowerShell, które mogą mieć wpływ na wykrywanie włamań. Aby uzyskać więcej informacji, dodaliśmy odniesienie z FireEye, które bardziej szczegółowo opisuje różne opcje i wyjaśnia, jak je włączyć.

Życie z ziemi

https://chacker.pl/

Kiedy mówimy o „życiu z ziemi”, mamy na myśli korzystanie z narzędzi już obecnych w systemach w celu dalszego wykorzystania luk. Jest to cenne, ponieważ zawsze, gdy dodajemy coś do systemu, zwiększamy prawdopodobieństwo wykrycia. Co więcej, gdy zostawiamy narzędzia, pomaga to ujawnić nasze taktyki, techniki i procedury (TTP), dzięki czemu łatwiej jest znaleźć naszą aktywność w innych systemach. Gdy żyjemy z ziemi, możemy zostawić mniej artefaktów i ograniczyć narzędzia, które musimy przenosić z systemu do systemu. PowerShell jest przydatny jako już istniejące narzędzie w systemie, ponieważ daje nam możliwość łatwego pisania skryptów, a także obejmuje integrację z .NET, więc prawie wszystko, co możemy napisać w .NET, możemy napisać w PowerShell. Oznacza to, że możemy wyjść poza podstawowe skrypty i wchodzić w interakcje z funkcjami jądra i nie tylko. Daje nam to dodatkową elastyczność, która normalnie wymagałaby użycia oddzielnych programów. Jedną z głównych zalet PowerShell jest to, że może on korzystać z opcji przeglądarki Internet Explorer, więc takie rzeczy jak obsługa proxy są wbudowane w PowerShell. W rezultacie możemy używać wbudowanych bibliotek internetowych do zdalnego ładowania kodu, co oznacza, że ​​nie musimy ręcznie pobierać żadnego kodu do systemu docelowego. Dlatego gdy ktoś spojrzy na oś czasu systemu plików, te pobrania ze stron internetowych nie będą widoczne, co pozwoli nam być jeszcze bardziej dyskretnym.

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”.