Ad alcuni piace, ad altri no, ma a questo punto non dovrebbe sorprendere che i tradecraft .NET siano qui per restare un po' più a lungo del previsto. Il framework .NET è parte integrante del sistema operativo di Microsoft e la versione più recente di .NET è .NET core. Core è il successore multipiattaforma di .NET Framework che porta .NET anche su Linux e macOS. Ciò rende .NET più popolare che mai per le tecniche di post-sfruttamento tra aggressori e red team. Questo blog approfondirà un nuovo Beacon Object File (BOF) che permette agli operatori di eseguire assembly .NET in processo tramite Cobalt Strike, invece che il tradizionale modulo esecuzione-assemblaggio integrato, che utilizza la tecnica fork and run.
Cobalt Strike, un popolare software di simulazione delle aggressioni, ha riconosciuto la tendenza dei red team di allontanarsi dagli strumenti PowerShell a favore di C# a causa dell'aumento della capacità di rilevamento per PowerShell, e nel 2018 con Cobalt Strike versione 3.11 ha introdotto il modulo execute-assembly. Questo ha permesso agli operatori di utilizzare al meglio la potenza degli assembly .NET post-sfruttamento, eseguendoli in memoria senza il rischio aggiuntivo di scaricare questi strumenti su disco. Sebbene la capacità di caricare gli assembly .NET in memoria tramite codice non gestito non fosse nuova o sconosciuta al momento del rilascio, direi che Cobalt Strike ha portato la funzionalità nel mainstream e ha contribuito a mantenere la popolarità di .NET per il tradecraft post-sfruttamento.
Il modulo execute-assembly di Cobalt Strike utilizza la tecnica fork and run, ovvero generare un nuovo processo sacrificale, iniettare il codice dannoso post-sfruttamento in quel nuovo processo, eseguire il codice dannoso e, una volta terminato, eliminare il nuovo processo. Questo presenta sia vantaggi che svantaggi. Il vantaggio del metodo fork and run è che l'esecuzione avviene al di fuori del nostro processo di impianto Beacon. Ciò significa che se qualcosa nella nostra azione post-sfruttamento va storto o viene scoperto, le probabilità che il nostro impianto sopravviva sono molto maggiori. Per semplificare, supporta molto la stabilità generale dell'impianto. Tuttavia, poiché i fornitori di sicurezza hanno scoperto questo comportamento fork and run, ora hanno aggiunto quello che Cobalt Strike ammette essere un costoso modello OPSEC.
A partire dalla versione 4.1 rilasciata a giugno 2020, Cobalt Strike ha introdotto una nuova caratteristiche per cercare di risolvere questo problema con l'introduzione dei Beacon Object Files (BOF). I BOF permettono agli operatori di evitare i noti modelli di esecuzione descritti sopra o altri fallimenti OPSEC, come l'uso di cmd.exe/powershell.exe eseguendo file oggetto in memoria all'interno dello stesso processo del nostro impianto beacon. Anche se non mi addentrerò nei meccanismi interni dei BOF, ecco alcuni post sul blog che ho trovato interessanti:
Se hai letto i post precedenti, dovresti aver capito che i BOF non sono stati esattamente la salvezza che speravamo, e se sognavi di riscrivere tutti quegli straordinari strumenti .NET e trasformarli in BOF, quei sogni sono andati in frantumi. Peccato. La speranza però è sempre l'ultima a morire, dato che, secondo me, i BOF possono offrire cose fantastiche, e ultimamente mi sono divertito molto (anche se non nego che ci sia stata anche un po' di frustrazione) a sperimentare cosa si può fare con loro. La prima cosa da fare è creare CredBandit, che esegue un dump completo in memoria di un processo come LSASS e lo invia attraverso il canale di comunicazione Beacon esistente. Oggi rilascio InlineExecute-Assembly, che può essere utilizzato per eseguire assembly .NET all'interno del tuo processo beacon senza alcuna modifica ai tuoi strumenti .NET preferiti. Scopriamo perché ho scritto questo BOF, alcune delle sue caratteristiche principali, le avvertenze e come potrebbe essere utile quando si conducono simulazioni di avversari/red team.
Newsletter di settore
Resta al passo con le tendenze più importanti e interessanti del settore relative ad AI, automazione, dati e oltre con la newsletter Think. Leggi l' Informativa sulla privacy IBM.
L'abbonamento sarà fornito in lingua inglese. Troverai un link per annullare l'iscrizione in tutte le newsletter. Puoi gestire i tuoi abbonamenti o annullarli qui. Per ulteriori informazioni, consulta l'Informativa sulla privacy IBM.
Il motivo per cui ho costruito InlineExecute-Assembly è piuttosto semplice. Volevo che il nostro team avesse modo di simulare gli avversari per eseguire gli assembly .NET nel processo, evitare alcune delle insidie OPSEC di cui abbiamo già parlato quando si utilizza Cobalt Strike per operare in ambienti maturi. Avevo anche bisogno di questo strumento per non sovraccaricare il nostro team con tempi di sviluppo extra, chiedendogli di apportare modifiche alla maggior parte dei nostri attuali strumenti .NET. Inoltre doveva essere stabile. O perlomeno, stabile quanto può esserlo un BOF complesso, poiché l'ultima cosa che vogliamo è perdere uno dei nostri pochi Beacon nell'ambiente. Fondamentalmente, dovrebbe funzionare in modo più fluido possibile per l'operatore come il modulo di execute-assembly di Cobalt Strike.
Lo so, è un po' scontato. Senza di esso non andremmo molto lontano, giusto? Scherzi a parte, le complessità di come funziona il CLR e cosa accade in dettaglio potrebbero essere un post a parte, quindi rivediamo cosa usa il BOF a un livello molto alto quando carica il CLR tramite codice non gestito.
Caricamento del CLR
Come mostrato nella schermata semplificata qui sopra, i passaggi principali che il BOF seguirà per caricare il CLR sono i seguenti:
Quindi ora il CLR è inizializzato, ma c'è ancora un po' di cose da fare prima di arrivare effettivamente a eseguire i nostri assembly .NET preferiti. Dobbiamo creare la nostra istanza AppDomain, ovvero ciò che Microsoft definisce "un ambiente isolato in cui vengono eseguite le applicazioni". In altre parole, questo verrà utilizzato per caricare ed eseguire i nostri assembly .NET post-sfruttamento.
AppDomain in fase di creazione e assembly caricato/eseguito
Come mostrato nella schermata semplificata qui sopra, i passaggi principali che il BOF eseguirà per caricare e richiamare il nostro assembly .NET sono i seguenti:
Speriamo che tu abbia una conoscenza approfondita dell'esecuzione di .NET tramite codice non gestito, ma questo non ci porta ancora ad avere uno strumento valido dal punto di vista operativo, quindi esamineremo alcune caratteristiche che sono state implementate nel BOF per portarlo da meh a totes legit.
Probabilmente ti starai chiedendo perché sia importante. Beh, se sei come me e tieni al tuo tempo, non vorrai sprecarlo a modificare praticamente ogni assembly .NET in modo che il punto di ingresso restituisca una stringa con tutti i dati che normalmente verrebbero inviati solo all'output standard della console, giusto? Lo immaginavo. Per evitare ciò, dobbiamo reindirizzare il nostro output standard verso una pipe denominata o uno slot mail, leggere l'output dopo che è stato scritto e poi riportarlo al suo stato originale. In questo modo possiamo eseguire i nostri assembly non modificati proprio come faremmo da cmd.exe o powershell.exe. Ora, prima di passare al codice, devo ringraziare @N4k3dTurtl3 e il suo post riguardante l'esecuzione in corso di assembly e gli slot di posta. Questo è ciò che mi ha spinto inizialmente a implementare questa tecnica nel mio impianto C privato quando è uscito per la prima volta e molti mesi dopo ho trasferito la stessa funzionalità su un BOF. Ok, ora che i prop sono stati dati, vediamo un esempio semplificato di come questo si ottiene reindirizzando stdout a una pipe denominata:
Reindirizzando l'output standard della console verso una pipe denominata e tornando indietro
Ricordi quando caricando il CLR tramite ICLRMetaHost ->GetRuntime abbiamo dovuto specificare quale versione del framework ci serviva? Ricordi che dipende dalla versione con cui è stato compilato il nostro assembly .NET? Non sarebbe molto divertente dover specificare manualmente ogni volta quale versione è necessaria, vero? Per nostra fortuna, @b4rtik ha implementato una funzione interessante per gestire questo problema nel suo modulo execute-assembly per il framework Metasploit che possiamo facilmente implementare nei nostri strumenti mostrati qui sotto:
Funzione che legge il nostro assembly .NET e aiuta a determinare quale versione .NET ci serve quando carichiamo il CLR
Essenzialmente, questa funzione fa in modo che, quando gli vengono passati i nostri byte di assemblaggio, li legga e cerchi i valori esadecimali di 76 34 2E 30 2E 33 30 33 31 39, che convertiti in ASCII corrispondono a v4.0.30319. Speriamo che abbia un aspetto familiare. Se quel valore viene trovato leggendo l'assembly, la funzione restituisce 1 o vero, e se non viene trovato restituisce 0 o falso. Possiamo utilizzarlo per determinare facilmente quale versione caricare con il ritorno di 1/true o 0/false, come mostrato nell'esempio di codice qui sotto:
istruzione if/else per impostare la variabile di versione .NET
Non potevamo di certo affrontare i traffici .NET offensivi e non parlare di AMSI. Anche se non entreremo nei dettagli su cosa sia AMSI e su tutti i modi in cui può essere bypassato, dato che è stato trattato molte volte, parleremo un po' del motivo per cui patchare AMSI potrebbe essere necessario a seconda di cosa decidi di eseguire tramite il BOF. Ad esempio, se decidi di eseguire Seatbelt senza alcun offuscamento, noterai rapidamente che non hai ricevuto alcun output e che il tuo beacon è morto. Sì, morto *morto*. Questo perché l'AMSI ha intercettato il tuo assembly, ha stabilito che era dannoso e ti ha bloccato come i poliziotti quando ti fanno sgombrare una festa in casa perché fate troppo rumore e date fastidio ai vicini. Non è l'ideale, vero? Ora abbiamo due buone opzioni per quanto riguarda AMSI: possiamo offuscare i nostri strumenti.NET tramite qualcosa come ConfuserX o Invisibility Cloak oppure possiamo disabilitare AMSI utilizzando diverse tecniche. Nel nostro caso, useremo uno di RastaMouse, che serve a patchare il amsi.dll in memoria in modo che restituisca E_INVALIDARG e renda il risultato della scansione 0. Che, come sottolineato nel loro post, di solito viene interpretato come AMSI_RESULT_CLEAN. Vediamo una versione semplificata del codice per un processo x64 qui sotto:
Patching in memoria di AmsiScanBuffer
Come puoi vedere nello screenshot qui sopra, ci basta fare quanto segue:
Implementando questa funzionalità nel nostro strumento, dovremmo ora essere in grado di eseguire la versione predefinita di Seatbelt.exe utilizzando il flag –amsi per bypassare il rilevamento AMSI, come mostrato di seguito:
Esempio di bypass AMSI InlineExecute-Assemby
Fortunatamente per i difensori, non c'è solo AMSI a dare una mano quando si tratta di individuare il tradecraft .NET dannoso utilizzando ETW. Purtroppo, come AMSI, anche questo può essere abbastanza facile da aggirare per gli avversari e @xpn ha condotto ricerche davvero interessanti su come farlo. Di seguito, un esempio semplificato di come è possibile applicare una patch a ETW per disattivarlo completamente:
Patching di EtwEventWrite in memoria
Come puoi vedere dalla schermata qui sopra, i passaggi sono praticamente identici a come abbiamo patchato AMSI, quindi non ripeterò i passaggi per questo. Puoi vedere una schermata prima e dopo dell'esecuzione del flag –etw qui sotto:
Utilizzo di Process Hacker per visualizzare le proprietà di PowerShell.exe prima di eseguire inlineExecute-Assembly con il flag –etw
Esecuzione di inline-Execute-Assembly utilizzando il flag –etw
Utilizzare Process Hacker per visualizzare le stesse proprietà PowerShell.exe dopo aver eseguito inlineExecute-Assembly
Per impostazione predefinita, l'AppDomain, Named Pipe o Mail Slot creato utilizza il valore predefinito "totesLegit". Questi valori possono essere modificati per integrarsi meglio nell'ambiente che stai testando, modificandoli sia nello script aggressor fornito sia tramite flag da riga di comando rapidamente. Di seguito è riportato un esempio di modifica tramite la riga di comando:
Esempio di InlineExecute-Assembly che utilizza un nome di AppDomain univoco e un nome di pipe denominata.
Esempio di nome AppDomain univoco ChangedMe
Esempio di pipe denominata LookAtMe univoca
Esempio di rimozione di AppDomain al termine dell'esecuzione corretta
Esempio di rimozione di una pipe denominata al termine di un'esecuzione di successo
Questa sezione sarà praticamente una ripetizione di quanto menzionato nel repository GitHub, ma ho ritenuto importante ribadire alcune cose da tenere a mente quando si usa questo strumento:
Di seguito sono riportate alcune considerazioni difensive: