
Reverse ed Exploitation di Driver
⚠️ Avvertenza : Questo progetto è strettamente educativo e dimostrativo. Non è destinato a essere utilizzato in un contesto dannoso. L'obiettivo è apprendere la metodologia del reverse engineering e le fasi di exploitation di un driver Windows.
Qui spiego l'approccio che ho seguito per risolvere l'esercizio proposto da d1rk(SaadAhla) https://github.com/SaadAhla, che consiste nell'effettuare reverse engineering ed exploitation su un driver legittimo, firmato e non presente nelle blocklist (HVCI, LOLBIN...). È disponibile un programma C in grado di terminare qualsiasi processo attivo sul sistema tramite questo driver in kernel-mode; ne descrivo il funzionamento più avanti.

📃 Uso : DriverKiller.exe <nome_processo.exe> [-d]
Opzione -d : consente di eliminare il servizio e il driver dal sistema dopo l'exploitation.
La modalità testsigning deve essere attivata sulla macchina target perché il certificato del driver è scaduto.
Parte 1 - Reverse engineering :
L'esercizio fornisce un file .sys, denominato con il suo hash SHA-256.
Il primo passo consiste nell'aprire il file con IDA.
IDA è disponibile gratuitamente. Basta andare sul sito di Hex-Rays per generare una licenza e scaricare il software.
Si inizia elencando la IAT (Import Address Table) del driver e cercando la chiamata all'API che ci interessa : ZwTerminateProcess.
Facendo doppio clic su ZwTerminateProcess, IDA ci reindirizza al codice compilato di questa funzione. Selezionando la voce e visualizzando i cross-reference, si ottiene l'elenco delle funzioni del driver che la chiamano.
Si osserva che è la funzione sub_12EF4, all'offset 1CE, a utilizzare ZwTerminateProcess. Dopo un doppio clic, IDA mostra il suo codice compilato.
Il codice decompilato rivela le chiamate a ZwOpenProcess (che apre un handle verso il processo target) e a ZwTerminateProcess (che termina il processo tramite tale handle).
Consultando la documentazione di ZwOpenProcess (https://learn.microsoft.com/fr-fr/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess), si nota che il parametro ClientID corrisponde a un puntatore che indica il PID del processo interessato.
Sulla riga sopra, ClientId.UniqueProcess viene inizializzato con la variabile v22. Quest'ultima è definita poco sopra :
v22 = (void )((_QWORD *)i + 10);
Per comprendere questa assegnazione, bisogna identificare la variabile i e il campo +10.
Più sopra in questa funzione, si osserva una chiamata a ZwQuerySystemInformation con il parametro SYSTEM_PROCESS_INFORMATION. Si comprende inoltre che i è l'iteratore sulle voci di questa struttura insieme alla variabile v6.
Secondo la documentazione di ZwQuerySystemInformation : (https://learn.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation), questa funzione restituisce un array contenente una voce per ogni processo attivo sul sistema.
La struttura SYSTEM_PROCESS_INFORMATION è descritta qui : 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;
Promemoria : dimensioni di alcuni tipi su Windows x64
typedef struct _UNICODE_STRING {
USHORT Length; -> 2
USHORT MaximumLength; -> + 2 = 4
PWSTR Buffer; -> + 8 = 12 (12 non è un multiplo di 8 quindi padding di 4 aggiunto prima di Buffer) = 16
} UNICODE_STRING;
Calcolo dell'offset di UniqueProcessId :
ULONG NextEntryOffset; -> 4
ULONG NumberOfThreads; -> + 4 = 8
BYTE Reserved1[48]; -> + 48 = 56
UNICODE_STRING ImageName; -> + 16 = 72
KPRIORITY BasePriority; -> + 4 = 76 (76 non è un multiplo di 8 quindi padding di 4 aggiunto) = 80
HANDLE UniqueProcessId; -> + 8 = 88
Il membro UniqueProcessId si trova quindi all'offset 0x50 (80 decimale).
Osservando l'assegnazione della nostra variabile v22, si nota che i viene convertito (cast) in un puntatore QWORD (8 byte)
v22 = (void )((_QWORD *)i + 10);Quindi
v22 corrisponde all'indirizzo di i + 10 * 8 = 80 byte. Questa variabile contiene quindi il PID recuperato dalla struttura SYSTEM_PROCESS_INFORMATION.
Per sapere quale PID verrà passato a ZwTerminateProcess, bisogna analizzare la condizione che circonda questa assegnazione.
Si nota che il nome dell'immagine del processo viene prima recuperato :
v9 = (wchar_t )((_QWORD *)i + 8);Perché
v9 = indirizzo di i + 8 × 8 = 64 byte. Ciò corrisponde al Buffer del membro ImageName, dato che tale membro si trova all'offset 56 + 2 (USHORT) + 2 (USHORT) + 4 (padding) = 64
Viste le manipolazioni e i loop qui sotto, si può ipotizzare che venga effettuato un confronto tra il nome del processo passato come argomento (a2) e i processi attivi sul sistema v9/String.
sub_1C078(String, v9, (int)v13); v17 = strupr(a2); v18 = strupr(String);
È quindi il parametro a2 a dover contenere il nome del processo da terminare tramite ZwTerminateProcess.
Si nota che a2 è un parametro della funzione sub_12EF4. Per andare oltre, bisogna esaminare i riferimenti di questa funzione (l'ho rinominata ZwTerminateProcessCaller per una migliore leggibilità).
