Il Windows Defender Application Control (WDAC) è una caratteristiche di sicurezza di Windows che aiuta a prevenire l'esecuzione di codice non autorizzato (come malware o file eseguibili e script non affidabili) su un sistema. Si tratta di un meccanismo di whitelisting delle applicazioni che applica politiche che consentono di eseguire su un sistema solo i file eseguibili, gli script e i driver esplicitamente affidabili. Viene spesso utilizzato in ambienti ad alta affidabilità o strettamente controllati in cui la sicurezza e l'integrità del sistema sono critici, come quelli che il team di X-Force Red Adversary Simulation è impegnato a testare.
Qualche settimana fa, il mio collega Bobby Cooke ha pubblicato un post sul blog in cui descriveva un metodo per bypassare anche le politiche WDAC più rigide tramite backdoor nelle applicazioni Electron affidabili. Consiglio vivamente di leggere il suo post sul blog per capire come le applicazioni Electron utilizzano Node.js e come possano essere usate da backdoor.
Come parte di quella ricerca, ha anche reso open source Loki C2, un Node.js basato su framework di comando e controllo. Grazie all'eccellente lavoro di Bobby e Dylan Tran nello sviluppo di Loki C2, il team di X-Force Adversary Simulation è riuscito a ottenere l'esecuzione di codice sulle attività in ambienti protetti che utilizzano WDAC.
Quindi, dove entra in gioco questa ricerca? La tecnica sopra menzionata ha un difetto: è limitata all'esecuzione del solo codice JavaScript e non può eseguire codice nativo, come il caricamento di DLL o l'esecuzione di EXE. Non è inoltre possibile eseguire shellcode per avviare un payload C2 di fase 2. Questo post sul blog tratta una tecnica che abbiamo utilizzato per aggirare queste restrizioni.
Per cominciare, Bobby e io abbiamo iniziato con l'ingegneria inversa dei moduli Node.js firmati e caricati dalle applicazioni Electron, cercando vulnerabilità che potessero permettere l'esecuzione di codice a basso livello a livello di istruzione. Dopo alcune prime esplorazioni e su suggerimento di jeffssh, la mia attenzione si è spostata sul motore V8 usato da Node.js e da Chrome.
Invece di trovare una vulnerabilità in un modulo Node.js, perché non utilizzare il motore V8 con un N-day?
Lo scenario di attacco è familiare: utilizzare un binario vulnerabile ma affidabile e sfruttare il fatto che sia affidabile per ottenere un punto d'appoggio sul sistema. In questo caso, utilizziamo un'applicazione Electron affidabile con una versione vulnerabile di V8, sostituendo main.js con un exploit V8 che esegue la fase 2 come payload, e voilà, abbiamo un'esecuzione nativa di shellcode. Se l'applicazione sfruttata è inserita in una whitelist o firmata da un'entità affidabile (come Microsoft) e sarebbe normalmente autorizzata a funzionare in base alla politica WDAC utilizzata, può essere utilizzata come contenitore per il payload dannoso.
Oltre a poter eseguire liberamente shellcode, questo approccio ha anche il vantaggio di eseguire shellcode nel contesto di un processo simile a quello di un browser, il che presenta vantaggi. Un comportamento che altrimenti potrebbe essere segnalato dall'EDR come sospetto, sembra normale per un browser, come ad esempio avere la memoria RWX mappata per il codice Just-In-Time (JIT).
Questo approccio mi è sembrato abbastanza semplice, ma avevo alcune domande non risposte. Un utilizzo pubblico di Chrome V8 N-day funzionerebbe davvero all'interno di un app Electron? In che modo il motore V8 usato in Chrome differisce da quello di Node.js? Quali modifiche avrà bisogno di utilizzare? Come posso risolvere il problema?
A quanto pare esiste un lavoro pubblico sull'utilizzo del V8 nelle app Electron, che, purtroppo per me, ho scoperto solo dopo aver finito. Turb0 fa un lavoro eccellente nel trattare il processo (un po' angosciante) di adattamento di un utilizzo pubblico del v8 e delle sue corrispondenti primitive di lettura/scrittura per funzionare all'interno di un'applicazione Electron. Il post sul blog di Turb0 già tratta molti dettagli tecnici approfonditi di ciò che ho dovuto affrontare, che consiglio vivamente di leggere. Il resto di questo post sul blog si concentrerà sulle fasi rimanenti del ciclo di sviluppo dell'utilizzare per quanto riguarda il targeting di Windows con l'obiettivo specifico di creare un bypass WDAC e i problemi che ho riscontrato nell'utilizzare l'exploit per un uso reale.
La prima cosa che dovevo fare era capire gli obiettivi esatti. Dovevo scegliere un'applicazione affidabile di Electron e scegliere una vulnerabilità per utilizzarla. Prima di questo avevo poca esperienza di sfruttamento dei browser, quindi la vulnerabilità scelta dovrebbe avere un exploit pubblico da sfruttare come punto di partenza.
Non ero sicura di come le versioni V8 corrispondessero alla versione di V8 utilizzata da Electron o di come capire se fosse davvero vulnerabile. La versione di Electron di V8 è spesso in ritardo rispetto all'ultima versione di V8 di Chrome. I manutentori di Electron effettuano il backport di importanti patch di sicurezza dalle versioni più recenti alla versione che hanno congelato per una specifica release di Electron. Ciò significa che anche se Electron utilizza una versione precedente di V8, non significa necessariamente che sia vulnerabile a un bug, poiché si sarebbe potuto eseguire il backport di una correzione. Le patch accuratamente selezionate che applicano sono memorizzate qui.
Ho deciso che l'approccio più semplice sarebbe stato usare una vulnerabilità che era stata corretta dopo il rilascio della versione dell'applicazione. In questo modo, non ci sarebbe assolutamente alcuna possibilità che fosse già stata eseguita la patch della versione dell'app. Dopo aver cercato un po', ho trovato download degli ultimi ~2 anni di uscite di VSCode . Avevo una discreta gamma di applicazioni vulnerabili firmate da Microsoft tra cui scegliere 😊.
Per iniziare, ho semplicemente preso un recente PoC exploit V8 pubblico e lo ho utilizzato come backdoor sull'app vulnerabile , sostituendo main.js con l'exploit, e ho incrociato le dita. Magari fosse così facile, no? Speravo almeno in un crash. Senza sorpresa, non è successo nulla quando ho avviato l'app. A malincuore, sapevo che avrei dovuto costruire un V8 per capire cosa stava succedendo a un livello più profondo. Costruendo io stesso il V8, sarei in grado di creare la versione di debug (d8), di entrare nelle profondità dell'exploit, quindi di adattarlo alla versione specifica a cui mi rivolgo.
Il mio primo obiettivo era stabilire una "verità di base", cioè replicare esattamente l'ambiente in cui si sa che l'exploit funziona. Poi potevo esaminare le differenze tra quella versione e quella a cui puntavo per capire cosa stava andando storto.
La maggior parte degli exploit V8 pubblici che ho trovato erano rivolti a Linux. Ho quindi iniziato compilando V8 su Linux, controllando l'esatto commit a cui era rivolto l'exploit pubblico che ho scelto. Ho poi eseguito l'exploit per assicurarmi che funzionasse e, per fortuna, è stato così. Ora avevo la mia verità di base.
Da lì, ho compilato la versione di V8 a cui mi riferivo (la stessa usata dall'app Electron) ma su Linux. L'exploit non ha funzionato subito. Il beneficio di realizzare un progetto autonomamente è che puoi analizzare il codice a tuo piacimento. In particolare, il V8 ha d8, la shell standalone per il motore JavaScript V8, utilizzata principalmente per testare, debug ed eseguire codice JavaScript e WebAssembly al di fuori di un browser o ambiente Node.js. Il d8 ha caratteristiche di debug interne abilitate contrassegnate con
Con questo, potrei ottenere gli indirizzi degli oggetti di interesse e utilizzare gli offset codificati dell'utilizzare pubblico. Ora ero sulla giusta strada. Avevo solo bisogno di trasferire il mio exploit su Windows.
Compilare una vecchia versione di V8 su Windows mi ha dato un sacco di grattacapi. Dovevo risolvere una serie di problemi con le dipendenze, quindi ho apportato alcune dubbie modifiche al codice interno. Ora i dettagli mi sfuggono: il mio cervello li ha bloccati per proteggermi. Dopo ore difficili, sono finalmente riuscita a compilare la versione di cui avevo bisogno! Con mia grande sorpresa, il Linux utilizzato ha funzionato su Windows senza modifiche.
Ora, non restava che utilizzare l'exploit sull'app e trattenere il respiro, ma non funzionava ancora. Perché?
All'inizio ero fiduciosa perché l'biettivo era andato in crash. Dopotutto, non avevo adattato il payload Linux per Windows, quindi non potevo aspettarmi che succedesse qualcosa di interessante. Per confermare il comportamento, ho modificato il payload exploit per eseguire all'indirizzo 0x4141414141. Questa è una tecnica comune usata dagli sviluppatori di exploit per poter vedere/dimostrare di aver ottenuto il controllo del programma controllando l'indirizzo del puntatore dell'istruzione. Tuttavia, dopo aver guardato il crash in WinDbg, non vedevo ciò che volevo. Stavo ricevendo un errore di segmentazione quando sovrascrivevo il puntatore della funzione target.
Ricordi quella cosa di Electron che seleziona i commit V8 di cui parlavo prima? Si è scoperto che, anche se l'app era vulnerabile al bug che stavo usando per utilizzare, il metodo di fuga sandbox usato dall'exploit pubblico era già stato corretto tramite cherry pick. Se non conosci il sandbox o la memory cage del V8, puoi informarti qui. In sostanza, si tratta di un modo per rendere più difficile lo sfruttamento del V8 in caso di vulnerabilità.
Per capire cosa stava succedendo, ho dovuto ricostruire la versione target del V8, questa volta applicando le patch selezionate a memoria. Oltre alle patch di sicurezza, Node.js applica anche specifiche patch Node.js alla versione del V8 utilizzata da Electron. Ci ho messo molto tempo a capire che dovevo davvero farlo, dato che non era chiaro subito come Electron ed Node.js affrontassero le loro varie dipendenze.
Dopo aver provato per un giorno o due ad assicurarmi che la versione di V8 che stavo compilando fosse *identica* al mio obiettivo e aver letto le recenti tecniche di escape sandbox, ho fatto dei progressi. Sono riuscita a trovare una tecnica di escape che funzionasse per il mio obiettivo. Dopo aver regolato l'exploit, sono finalmente riuscito a bloccare l'app controllando il puntatore delle istruzioni. Una dolce vittoria, ero quasi al termine...
A questo punto, tutto ciò che restava da fare era modificare il payload pubblico per eseguire il nostro payload C2 al suo posto. Questa modifica apparentemente semplice si è rivelata più problematica di quanto pensassi. Il payload Linux dell'exploit pubblico era un semplice payload per aprire una shell, che aveva una dimensione di pochi byte. Il payload del C2 era... molto più grande.
Se hai familiarità con la codifica in shellcode, saprai che scrivere shellcode in Windows è più fastidioso che scrivere shellcode in Linux, principalmente perché non esiste un modo semplice per effettuare chiamate di sistema dirette in modo indipendente dalla posizione, come avviene in Linux. Il payload doveva anche essere "contrabbandato tramite JOP" all'interno di un array in virgola mobile:
Ovviamente, l'intero payload della fase C2 (che aveva diverse migliaia di byte) non poteva essere eseguito in questo modo. Quindi, dovevo scrivere un payload bootstrap che mappasse una pagina eseguibile, copiasse il payload finale e poi passare a esso.
Il problema con il payload di bootstrap è che, pur avendo il controllo del programma, non avevo un modo per passare gli argomenti al payload che veniva eseguito. Quindi, il mio shellcode contrabbandato non conoscerebbe l'indirizzo dell'ultimo payload da cui copiare. Ho aggirato il problema con un espediente che ho definito "contrabbando di argomenti".
Sapevo che l'indirizzo dell'oggetto JSFunction sovrascritto sarebbe stato memorizzato nel registro rcx. Quindi, usando la primitiva di scrittura arbitraria, ho memorizzato la pagina mappata in uno dei campi dell'oggetto che non sarebbe stato necessario. Questo ha richiesto svariati tentativi, in quanto la sovrascrittura di alcuni offset provocava dei crash. Ho fatto la stessa cosa per il valore da copiare e per l'offset dove copiarlo. L'offset del campo potrebbe essere hardcoded nello shellcode, in modo da sapere da dove copiare il payload. Ho chiamato il payload n un numero di volte, dove n è il numero di byte da copiare.
TurboFan, il compilatore di ottimizzazione del V8, ha messo a rischio i miei piani. A causa delle ottimizzazioni di TurboFan, contrabbandare sequenze di istruzioni tradotte in più virgole mobile dello stesso valore avrebbe portato a una sola istanza di quel valore in memoria. Ciò ha imposto delle limitazioni sulla frequenza con cui le istruzioni potevano essere ripetute. Ho aggirato questo problema rendendo il mio shellcode il più compatto possibile e variando anche la posizione delle istruzioni contrabbandate se avevo assolutamente bisogno di ripetere un'istruzione, in modo che il valore in virgola mobile fosse diverso e non ci fossero voci ripetute.
Ho anche riscontrato problemi nella copia dello shellcode se il payload della fase 2 era troppo grande, probabilmente a causa del numero di volte in cui ho dovuto chiamare la stessa JSFunction e TurboFan stomped, cercando di ottimizzarlo. Alla fine ho risolto il problema copiando e incollando più cicli in "WriteShellcode" invece di un unico grande ciclo. Non la soluzione più elegante, ma ha funzionato! Successivamente, Bobby e Dylan hanno scambiato il payload C2 con uno stager che recuperava il payload più grande dallo storage del blob, così che il payload finale non dovesse essere memorizzato su disco. Ciò ha anche contribuito a mantenere le dimensioni del file main.js entro limiti ragionevoli.
La preparazione all'uso operativo reale degli exploit dovrebbe sempre includere test su ambienti diversi. Per il contesto dell'ingaggio, non sapevamo in quale ambiente il payload sarebbe stato eseguito, solo che si trattava di un sistema Windows che probabilmente aveva abilitato il WDAC. Pertanto, la vulnerabilità doveva funzionare indipendentemente dal sistema operativo. Ero sicura che, poiché la versione V8 dell'applicazione e tutte le dipendenze erano contenute nell'app, non si sarebbe riscontrata molta variabilità. Ma mi sbagliavo.
Per ragioni a me ignote, l'offset del puntatore della funzione vulnerabile da sovrascrivere era cambiato nelle varie versioni di Windows. Questo non aveva senso perché, da quanto ho capito, la distanza di offset è determinata dal motore JIT V8, le cui librerie vengono caricate direttamente dal pacchetto applicativo. Questo significa che le stesse librerie V8 vengono caricate indipendentemente dal sistema operativo. Per rendere le cose ancora più confuse, la variazione non sembrava seguire alcun tipo di schema. L'offset a volte era sbilanciato di 4 byte in alcune versioni di Windows (sia vecchie che recenti). Ciò era particolarmente fastidioso perché non c'era modo (da quello che ho capito) di ricavare l'offset corretto dall'utilizzare JavaScript. L'unico modo per calcolarlo era utilizzare il debugging shell per leggere l'indirizzo di memoria e fare i calcoli, cosa che ovviamente non era un'opzione all'interno dell'applicazione di produzione Electron. TLDR: la variazione degli offset non può essere calcolata nel tempo di esecuzione dell'exploit.
Per aggirare il problema dell'offset incoerente, Bobby e Dylan hanno ri-ingegnerizzato l'exploit in modo che main.js lo utilizzasse più volte, provando i diversi possibili offset fino a ottenere successo. Ciò è stato fatto facendo in modo che il processo del codice iniziale eseguisse un ciclo. Questo ciclo ha generato processi secondari che avrebbero tentato di utilizzare con un offset unico. Se l'exploit falliva, il processo secondario veniva terminato. Se l'exploit fosse stato utilizzato con successo, lo shellcode eseguirebbe e scriverebbe un file Mutex prima di implementare la fase 2 del C2. Una volta che l'exploit aveva successo, il processo iniziale sarebbe uscito dal ciclo e sarebbe rimasto inattivo per sempre.
Sebbene ciò significasse che un tentativo di offset sbagliato avrebbe causato un crash, i nostri test hanno rivelato che non c'erano errori visibili per l'utente e che l'applicazione sembrava comunque funzionare senza problemi. Anche se non era la soluzione più pulita e un po' rumorosa a causa dei crash, il tempo era essenziale. Questo è ciò che nel settore chiamiamo "JIT xdev" e ha funzionato perfettamente per le nostre esigenze.
Ovviamente non volevamo che l'exploit fosse evidente se fossimo stati scoperti e qualcuno avesse analizzato il punto di ingresso main.js dell'applicazione. Per evitare ciò, abbiamo applicato un obfuscator JavaScript sul codice dell'exploit, rendendolo praticamente incomprensibile all'occhio umano. Grazie al talento e alla dedizione di Chris Spehn, che gestisce la pipeline CI/CD del payload del team, siamo riusciti a semplificare la consegna di questo payload e a ri-offuscare il codice ogni volta che veniva generato, così da poter riutilizzare l'applicazione indefinitamente con diversi codici di exploit ogni volta. Ciò ha impedito che il payload venisse firmato. Questo si è rivelato particolarmente utile, poiché purtroppo, la prima volta che abbiamo cercato di utilizzare la funzionalità, siamo stati scoperti perché l'utente ha segnalato l'e-mail di phishing 🙁. Curiosamente, mentre il team blu del cliente ha analizzato l'applicazione dall'e-mail di phishing, non ha capito lo scopo dell'applicazione, né ha identificato l'exploit integrato del V8.
Non capisco ancora bene perché gli offset delle funzioni JITed dipendono dal sistema operativo, dato che tutte le librerie V8 rilevanti dovrebbero essere raggruppate nell'applicazione Electron. Se qualcuno ha idea del motivo per cui è così, me lo faccia sapere!
Electron ha lanciato una funzione sperimentale per l'integrità che verifica l'integrità di tutti i file dell'applicazione a tempo di esecuzione. È disponibile per macOS dalla versione 16 e per Windows dalla versione 30. Gli sviluppatori di applicazione possono abilitare questo fuse Electron per assicurarsi che nessun file di applicazione venga manomesso. In tal caso, il processo verrà automaticamente terminato e non verrà eseguito nulla.
Questa funzione impedisce di modificare qualsiasi file in un pacchetto dell'app, inclusi i main.js, e ostacola le tecniche discusse. Tuttavia, deve ancora essere implementato nelle applicazioni più popolari. Se e quando questa caratteristica verrà utilizzata più ampiamente, va comunque notato che le versioni precedenti dell'applicazione, pre-integrity fuse, rimarranno vulnerabili e utilizzabili per questo attacco.
Bobby Cooke e Dylan Tran - Per aver contribuito a rendere l'exploit operativo
Dylan Tran – Creazione di diagrammi
Chris Spehn— Integrazione di questo payload nella nostra pipeline CI/CD (e in tutto l'altro ingrato lavoro DevOps che ha svolto per il team)
jeffssh – Ispirazione
j j - Per essere un maestro hacker V8 i cui prolifici V8 PoC hanno aiutato enormemente