
Driver Reverse & Exploitation
⚠️ Warnung: Dieses Projekt ist streng bildungspädagogisch und demonstrativ. Es ist nicht dafür gedacht, in einem böswilligen Kontext verwendet zu werden. Ziel ist es, die Methodik des Reverse Engineering und die Schritte zur Ausnutzung eines Windows-Treibers zu erlernen.
Hier erkläre ich das Vorgehen, das ich zur Lösung der von d1rk(SaadAhla) https://github.com/SaadAhla vorgeschlagenen Übung gewählt habe. Dabei geht es darum, Reverse Engineering und Exploitation an einem legitimen, signierten Treiber durchzuführen, der nicht in den Blocklisten (HVCI, LOLBIN...) enthalten ist. Ein C-Programm, mit dem sich über diesen Kernel-Mode-Treiber jeder aktive Prozess auf dem System beenden lässt, ist verfügbar; ich erläutere seine Funktionsweise weiter unten.

📃 Verwendung: DriverKiller.exe <nom_processus.exe> [-d]
Option -d: Ermöglicht das Entfernen des Dienstes und des Treibers vom System nach der Exploitation. Der Testsigning-Modus muss auf dem Zielcomputer aktiviert sein, da das Zertifikat des Treibers abgelaufen ist.
Teil 1 - Reverse Engineering:
Die Übung stellt eine .sys-Datei bereit, die nach ihrem SHA-256-Hash benannt ist.
Der erste Schritt besteht darin, diese Datei mit IDA zu öffnen.
IDA ist kostenlos verfügbar. Es genügt, die Website von Hex-Rays zu besuchen, um eine Lizenz zu generieren und die Software herunterzuladen.
Wir beginnen damit, die IAT (Import Address Table) des Treibers aufzulisten und den Aufruf der relevanten API zu suchen: ZwTerminateProcess.
Durch einen Doppelklick auf ZwTerminateProcess leitet IDA uns zum kompilierten Code dieser Funktion weiter. Wenn man den Eintrag auswählt und dann die Cross-References anzeigt, erhält man die Liste der Treiberfunktionen, die ihn aufrufen.
Man sieht, dass es die Funktion sub_12EF4 bei Offset 1CE ist, die ZwTerminateProcess verwendet. Nach einem Doppelklick zeigt IDA ihren kompilierten Code an.
Der dekompilierte Code zeigt die Aufrufe von ZwOpenProcess (das einen Handle zum Zielprozess öffnet) und ZwTerminateProcess (das den Prozess über diesen Handle beendet).
Wenn man die Dokumentation von ZwOpenProcess (https://learn.microsoft.com/fr-fr/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess) konsultiert, stellt man fest, dass der Parameter ClientID einem Zeiger entspricht, der die PID des Zielprozesses angibt.
In der Zeile darüber wird ClientId.UniqueProcess mit der Variablen v22 initialisiert. Diese wird direkt darüber definiert:
v22 = (void )((_QWORD *)i + 10);
Um diese Zuweisung zu verstehen, muss man die Variable i und das Feld +10 identifizieren.
Weiter oben in dieser Funktion sieht man einen Aufruf von ZwQuerySystemInformation mit dem Parameter SYSTEM_PROCESS_INFORMATION. Man versteht auch, dass i zusammen mit der Variablen v6 der Iterator über die Einträge dieser Struktur ist.
Laut der Dokumentation von ZwQuerySystemInformation: (https://learn.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation) gibt diese Funktion ein Array zurück, das einen Eintrag pro aktivem Prozess auf dem System enthält.
Die Struktur SYSTEM_PROCESS_INFORMATION wird hier beschrieben: https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
PVOID Reserved2;
ULONG HandleCount;
ULONG SessionId;
PVOID Reserved3;
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG Reserved4;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
PVOID Reserved5;
SIZE_T QuotaPagedPoolUsage;
PVOID Reserved6;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;
Zur Erinnerung: Größen einiger Typen unter Windows x64
typedef struct _UNICODE_STRING {
USHORT Length; -> 2
USHORT MaximumLength; -> + 2 = 4
PWSTR Buffer; -> + 8 = 12 (12 ist kein Vielfaches von 8, daher wird 4 Bytes Padding vor dem Buffer hinzugefügt) = 16
} UNICODE_STRING;Berechnung des Offsets von UniqueProcessId:
ULONG NextEntryOffset; -> 4
ULONG NumberOfThreads; -> + 4 = 8
BYTE Reserved1[48]; -> + 48 = 56
UNICODE_STRING ImageName; -> + 16 = 72
KPRIORITY BasePriority; -> + 4 = 76 (76 ist kein Vielfaches von 8, daher werden 4 Bytes Padding hinzugefügt) = 80
HANDLE UniqueProcessId; -> + 8 = 88
Das Element UniqueProcessId befindet sich also bei Offset 0x50 (80 dezimal).
Wenn wir uns die Zuweisung unserer Variablen v22 ansehen, stellen wir fest, dass i in einen QWORD-Zeiger (8 Bytes) gecastet wird
v22 = (void )((_QWORD *)i + 10);
v22 entspricht also der Adresse von i + 10 * 8 = 80 Bytes. Diese Variable enthält also die aus der Struktur SYSTEM_PROCESS_INFORMATION abgerufene PID.
Um herauszufinden, welche PID an ZwTerminateProcess übergeben wird, muss die Bedingung analysiert werden, die diese Zuweisung umgibt.
Man sieht, dass zunächst der Name des Prozessimages abgerufen wird:
v9 = (wchar_t )((_QWORD *)i + 8);
v9 = Adresse von i + 8 × 8 = 64 Bytes. Das entspricht dem Buffer des Elements ImageName, da sich dieses Element bei Offset 56 + 2 (USHORT) + 2 (USHORT) + 4 (Padding) = 64 befindet.
Angesichts der Manipulationen und Schleifen unten kann man die Hypothese aufstellen, dass ein Vergleich zwischen dem als Argument übergebenen Prozessnamen (a2) und den aktiven Prozessen auf dem System v9/String durchgeführt wird.
sub_1C078(String, v9, (int)v13); v17 = strupr(a2); v18 = strupr(String);
Es ist also der Parameter a2, der den Namen des über ZwTerminateProcess zu beendenden Prozesses enthalten soll.
Man beachte, dass a2 ein Parameter der Funktion sub_12EF4 ist. Um weiterzugehen, müssen die Referenzen dieser Funktion untersucht werden (ich habe sie zur besseren Lesbarkeit in ZwTerminateProcessCaller umbenannt).
Man sieht, dass ZwTerminateProcessCaller von der Funktion sub_13624 bei Offset 61A aufgerufen wird.
Bevor ich diesen dekompilierten Code analysiere, suche ich die Referenzen der Funktion sub_13624 (umbenannt in ZwTerminateProcessCallerCaller), um sicherzustellen, dass dieser Code tatsächlich nach einem API-Aufruf von DeviceIoControl aus dem UserMode verwendet wird.
Man sieht, dass ZwTerminateProcessCallerCaller von der Funktion sub_14130 aufgerufen wird (umbenannt in ZwTerminateProcessCallerCallerCaller ... zum Glück für uns ist es die letzte vor dem Einstiegspunkt 😅).
Man sieht, dass ZwTerminateProcessCallerCallerCaller von der Funktion sub_1A4A8 bei Offset 306 aufgerufen wird.
Man findet die Zuweisung der Funktion ZwTerminateProcessCallerCallerCaller:
memset64(DriverObject->MajorFunction, (unsigned __int64)ZwTerminateProcessCallerCallerCaller, 0x1Cu);
Bevor wir zur Funktion sub_13624 (alias ZwTerminateProcessCallerCaller) zurückkehren, werden der Symbolic Name und der Device Name (hier identisch) abgerufen: Viragtlt.
Wenn wir zu ZwTerminateProcessCallerCaller zurückkehren, stellen wir fest, dass sein zweiter Parameter (also a2) MasterIrp->AssociatedIrp.SystemBuffer entspricht.
Direkt über dem Aufruf von ZwTerminateProcessCaller befindet sich der IOCTL-Code: -2106392528 (hexadezimal: 0x82730030).
Dank dieser Informationen lässt sich ableiten, dass zum Ausnutzen dieses Treibers ein DeviceIoControl-API-Aufruf an den Treiber gesendet werden muss, wobei der Name des zu beendenden Prozesses im SystemBuffer steht.
🔷 Durch das Reverse Engineering gewonnene Informationen:
0x82730030ViragtltViragtltTeil 2 - Ausnutzung
Um diesen Treiber auszunutzen (sofern er auf dem Zielcomputer installiert und aktiv ist), muss ein Handle auf ihn geöffnet und dann ein DeviceIoControl-API-Aufruf mit einem Buffer gesendet werden, der den Namen des Prozesses enthält, den man beenden möchte.
Für diese Übung habe ich ein C-Projekt entwickelt, das:
Ich habe außerdem eine Option -d hinzugefügt, die es ermöglicht, den Dienst und den Treiber nach der Ausnutzung vom System zu entfernen.
Hier ist das Verhalten des C-Programms in seinem vollständigen Ausführungszyklus:
AV/EDR-Umgehung
In diesem Fall wird DriverKiller.exe weder statisch noch dynamisch von Microsoft Defender erkannt. Die Umgehung ergibt hier nicht wirklich Sinn, da der ausgenutzte Treiber ein abgelaufenes Zertifikat besitzt; sein Einsatz unter realen Bedingungen ist daher kaum denkbar. Für eine bessere Tarnung hätte man jedoch implementieren können:
Erkennung des Treibers am 29.08.2025 (bereits vorhandenes Ergebnis, ich habe aus offensichtlichen Gründen nichts bei VirusTotal eingereicht):
⚠️ Dieses Projekt wurde im Rahmen des Lernens erstellt. Es kann Ungenauigkeiten oder Fehler enthalten. Jeder Vorschlag, jede Korrektur oder Diskussion ist willkommen! 😃 Danke an d1rk(SaadAhla): https://github.com/SaadAhla!