
Exploit proof-of-concept per CVE-2026-29923, un'elevazione dei privilegi BYOVD in pstrip64.sys. Dimostra la lettura/scrittura della memoria fisica tramite IOCTL per rubare il token SYSTEM e generare una shell con privilegi elevati.
Disclaimer: Questo codice è fornito esclusivamente a scopo educativo e di ricerca difensiva. È stato scritto per approfondire la comprensione dello sfruttamento del kernel e aiutare i difensori a proteggersi da vulnerabilità simili. Qualsiasi uso non autorizzato, illegale o dannoso di questo progetto è severamente vietato.
Hash: ab01485bb7c8bc1a9c86096eeea6d31d8fad557bf4d44072b46373d2203faa6e
Nome del driver: pstrip64.sys
CVE: CVE-2026-29923
Un attacco "Bring Your Own Vulnerable Driver" (BYOVD) è un metodo vecchio ma estremamente efficace con cui gli aggressori possono aggirare le moderne protezioni di sicurezza di Windows utilizzando un driver legacy che il sistema operativo considera ancora ufficialmente affidabile. Una volta caricato il driver, l'aggressore sfrutta le sue falle per colmare il divario tra un processo standard senza privilegi e il controllo totale a livello di sistema.
All'inizio di questa settimana è stata divulgata una nuova vulnerabilità nel driver pstrip64.sys, tracciata come CVE-2026-29923. Questo post analizza l'intero ciclo di vita dello sfruttamento: dalla ricerca iniziale sulle vulnerabilità e lo sviluppo della Prova di Concetto (PoC) alle strategie di mitigazione attuabili per i difensori che proteggono i propri ambienti.
Il driver pstrip64.sys è un componente legacy in modalità kernel associato a EnTech Taiwan PowerStrip (fino alla versione 3.90.736). Sebbene il suo scopo legittimo sia quello di consentire regolazioni avanzate della scheda grafica, i suoi profondi privilegi di sistema lo rendono un bersaglio molto attraente per gli aggressori.
Quando la vulnerabilità è stata scoperta per la prima volta, ho iniziato analizzando la sua funzione DriverEntry. Questa funge da routine di inizializzazione principale per il driver del kernel, creando l'oggetto dispositivo \Device\PSTRIP64 e esponendolo alle applicazioni in modalità utente tramite il collegamento simbolico \DosDevices\PSTRIP64. Ancora più importante, configura la tabella di dispatch del driver. La voce che ha subito attirato la mia attenzione era all'indice 14 (IRP_MJ_DEVICE_CONTROL), che instrada tutte le richieste IOCTL fornite dall'utente direttamente nel gestore della funzione sub_11340, la nostra area di interesse principale.
La funzione sub_11340 funge da dispatcher IOCTL principale, interpretando le richieste dalla modalità utente.
Tra tutte le IOCTL esposte, 0x80002008 è senza dubbio la più interessante. Mentre i casi predefiniti gestiscono interazioni minori con le porte I/O, 0x80002008 funge da gateway per sub_11000, passando direttamente SystemBuffer a questa funzione.
Questa routine sub_11000 è la prova schiacciante. Innanzitutto, utilizza HalTranslateBusAddress per prendere l'indirizzo fornito dall'utente e tradurlo in un indirizzo fisico di sistema valido. Poi, apre \Device\PhysicalMemory e lo mappa utilizzando ZwMapViewOfSection. Impostando a livello di codice l'handle del processo di destinazione a (HANDLE)0xFFFFFFFFFFFFFFFFLL (che rappresenta ZwCurrentProcess()), il driver mappa questa memoria fisica direttamente nello spazio di indirizzi virtuali del nostro processo chiamante. In modo cruciale, scrive quindi questo indirizzo virtuale appena mappato nuovamente in SystemBuffer per restituirlo all'utente, consegnando ufficialmente alla nostra applicazione un puntatore diretto per leggere e scrivere la memoria fisica.
Una volta compresa appieno la vulnerabilità e stabilita una primitiva di lettura/scrittura fisica, ho tutti i pezzi del puzzle necessari. Ora è il momento di iniziare a scrivere la Prova di Concetto.
Nota: Questa PoC è stata sviluppata e testata specificamente su un ambiente Windows 10 22H2. Poiché lo sfruttamento si basa sulla manipolazione diretta della memoria fisica, gli offset delle strutture del kernel e i limiti della memoria fisica sono attualmente impostati a livello di codice per la mia configurazione. Per testarlo sulla propria macchina, è necessario aggiornare gli offset del kernel di Windows e regolare gli intervalli di scansione degli indirizzi fisici in base alla propria versione del sistema operativo e alla configurazione della RAM.
Il primo passo nel mio sfruttamento è stabilire una comunicazione con il driver. L'ho fatto chiamando CreateFileA sul collegamento simbolico del driver (\\.\PSTRIP64). Una volta ottenuto un handle valido, avevo bisogno di un modo pulito per abusare dell'IOCTL 0x80002008 che avevo analizzato in precedenza. Ho creato una funzione wrapper chiamata MapPhysicalMemory(). Questa funzione popola la mia struttura personalizzata PSTRIP_MAP_REQUEST con l'indirizzo fisico di destinazione e la lunghezza del blocco di memoria che voglio leggere.
Invio quindi questa struttura direttamente al driver tramite DeviceIoControl. In caso di successo, il driver mappa quella memoria fisica direttamente nella mia applicazione in modalità utente e restituisce l'indirizzo virtuale di base nel campo OutputResult. Posso ora convertire questo indirizzo restituito in un puntatore C++ standard, dandomi accesso diretto e senza privilegi alla RAM fisica del sistema.
Con la mia primitiva di lettura/scrittura fisica pienamente operativa, il mio obiettivo era trovare le strutture dati del kernel che contengono i privilegi dei processi. In Windows, ogni processo in esecuzione è rappresentato da una struttura EPROCESS.
Windows alloca le strutture EPROCESS nel pool del kernel utilizzando un identificatore di 4 byte specifico chiamato Pool Tag. Per i processi, questo tag è la stringa Proc (che in esadecimale corrisponde a 0x636F7250). Scansionando la RAM fisica del sistema, potevo cercare esattamente questa stringa.
Il mio sfruttamento scorre lo spazio di memoria fisica da 0x10000000 a 0x140000000, mappando la memoria in blocchi da 2 MB (STEP_SIZE = 0x200000). Converto ogni blocco mappato in un array di byte grezzi e lo scansiono in blocchi di 16 byte (sizeof(_POOL_HEADER)).
Tuttavia, trovare il tag Proc nella memoria fisica non è sufficiente. La memoria è disordinata: quel tag potrebbe essere un artefatto residuo di un processo terminato, o semplicemente dati casuali che coincidono con il valore esadecimale. Se assumessi ciecamente che ogni tag Proc sia una struttura EPROCESS valida e iniziassi a modificare la memoria, provocherei immediatamente un BSOD.
Per garantire la stabilità, ho dovuto convalidare la struttura utilizzando euristiche. Innanzitutto, calcolo l'inizio della struttura EPROCESS (che si trova leggermente spostata rispetto al tag del pool). Da lì, controllo alcune costanti note per un processo in esecuzione:
0x2 (Priorità Normale).0x0.Se tutte queste euristiche vengono superate, posso essere molto sicuro di trovarmi di fronte a un processo valido e attivo. Leggo quindi il suo ID univoco del processo (PID). Se il PID corrisponde al mio processo di sfruttamento, salvo l'indirizzo fisico del suo puntatore al token. Se il PID è 4 (il processo System di Windows), estraggo e salvo il valore effettivo del suo token altamente privilegiato.
Infine, allineo l'indirizzo fisico salvato del puntatore al token del mio processo al page boundary di 4 KB più vicino e utilizzo MapPhysicalMemory() un'ultima volta per mappare solo quella pagina specifica.
Successivamente, mi sposto all'offset esatto e sovrascrivo il mio token con il valore del token di Sistema. Istantaneamente, il kernel di Windows tratta il mio processo di sfruttamento come NT AUTHORITY\SYSTEM.
Dopo aver smappato la pagina per garantire la stabilità del sistema, chiamo semplicemente CreateProcessA per avviare cmd.exe. Poiché il mio processo corrente è elevato, il nuovo prompt dei comandi eredita questi privilegi di alto livello, completando con successo l'attacco!
Nota: Un dettaglio critico che ho scoperto durante la fase iniziale di debug è come il driver gestisce il puntatore mappato. Eseguendo SystemBuffer->LowPart = (unsigned int)BaseAddress;, il driver converte l'indirizzo virtuale di base a 64 bit in un valore a 32 bit prima di restituirlo. Questo troncamento perde i bit alti dell'indirizzo, causando violazioni di accesso immediate quando ho tentato di dereferenziarlo nel mio sfruttamento a 64 bit. Per aggirare questo problema in modo pulito, ho semplicemente compilato la mia PoC in modalità utente come applicazione a 32 bit, assicurandomi che il puntatore restituito rimanesse perfettamente valido.
Nota: Durante i miei test iniziali, mi sono imbattuto in un caso limite affascinante: la mia PoC ha localizzato con successo il mio processo di sfruttamento in memoria, ma non è riuscita a trovare il processo System (PID 4).
Per capirne il motivo, dovevo ispezionare direttamente la memoria fisica. Ho collegato un debugger del kernel (WinDbg) e utilizzato comandi per recuperare l'indirizzo virtuale e la directory base del processo System. Ho quindi usato !vtop per tradurre quell'indirizzo virtuale nel suo esatto indirizzo fisico nella RAM.
Sono tornato al mio debugger in modalità utente collegato alla mia PoC. Ho impostato un breakpoint condizionale sul mio ciclo di scansione della memoria, istruendolo a mettere in pausa l'esecuzione nel momento in cui la mia funzione MapPhysicalMemory() acquisiva il blocco da 2 MB contenente l'indirizzo fisico del processo System.
Una volta raggiunto il breakpoint, ho iniziato a ispezionare manualmente i byte grezzi della memoria mappata. È qui che ho scoperto un dettaglio cruciale sulle allocazioni del pool del kernel di Windows.
Quando Windows alloca memoria per un processo, inizia con un _POOL_HEADER (contenente il nostro tag Proc), seguito da un _OBJECT_HEADER e infine dalla struttura EPROCESS stessa. Per le applicazioni standard in modalità utente, queste intestazioni contengono dati di tracciamento aggiuntivi, il che significa che la struttura EPROCESS effettiva inizia 0x80 byte dopo il tag del pool.
Tuttavia, esaminando la memoria del processo System è emerso un layout diverso. Il processo System manca di alcune di queste intestazioni di tracciamento standard. L'offset dal tag Proc all'inizio della struttura EPROCESS era di soli 0x40 byte!
La soluzione è stata semplice. Ho aggiornato la mia PoC per gestire entrambe le dimensioni delle intestazioni del pool iterando su un array di possibili offset (0x40 e 0x80) ogni volta che incontra un tag Proc.
La cybersecurity è un gioco infinito del gatto e del topo tra aggressori e difensori. Mentre gli aggressori cercano costantemente driver vulnerabili, i moderni prodotti di sicurezza e i team blu dispongono di diversi metodi robusti per rilevare e bloccare esattamente questa operazione.
Il modo più efficace per fermare un attacco BYOVD (Bring Your Own Vulnerable Driver) è impedire che il driver venga caricato in primo luogo.
Se il driver è già caricato, i prodotti di sicurezza possono comunque rilevare lo sfruttamento durante la fase di manipolazione dei token.
NT AUTHORITY\SYSTEM senza una legittima catena di autenticazione è un enorme segnale d'allarme.cmd.exe), specialmente quando il processo padre non ha motivo di essere eseguito come SYSTEM.