Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
MemFiles — Un toolkit per CobaltStrike che scrive in memoria, invece che su disco, i file prodotti da Beacon. | Kitploit
Strumenti/GitHubGitHub/octoberfest7/memfiles
Esfiltrazione DatiPost-ExploitCommand and ControlRed Teaming
GitHuboctoberfest7/memfiles

MemFiles

Un toolkit per CobaltStrike che scrive in memoria, invece che su disco, i file prodotti da Beacon.

Vedi Repository
477622 anni faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

MemFiles

DICHIARAZIONE DI NON RESPONSABILITÀ:

Questo progetto è complesso e se non si capisce come funziona e non lo si testa adeguatamente, si rischia di mandare in crash i Beacon e perdere l'accesso!

Ti consiglio vivamente di leggere tutta la documentazione fino alla sezione "Dettagli tecnici, considerazioni di progettazione e commenti"!

Introduzione

MemFiles è un toolkit per CobaltStrike che consente agli Operatori di scrivere i file prodotti dal processo Beacon in memoria, invece di scriverli su disco nel sistema target. È stato testato con successo su Windows 7, 10 e 11; le corrispondenti versioni server dovrebbero funzionare senza problemi. MemFiles è limitato ai Beacon x64.

Funziona hookando diverse NtAPI all'interno di NTDLL.dll e reindirizzando le chiamate a quelle API verso funzioni che sono state iniettate nello spazio di memoria del processo Beacon.

MemFiles presuppone una copia pulita/senza hook di NTDLL nel processo Beacon. Non vengono fornite garanzie sulla funzionalità di MemFiles in un processo Beacon in cui siano ancora presenti hook EDR. Riparare/aggiornare NTDLL prima di usare MemFiles!

All'interno del toolkit MemFiles viene definita una directory "speciale", inesistente; qualsiasi file scritto in questa directory speciale verrà catturato da MemFiles e scritto in memoria, dove potrà poi essere scaricato sul Teamserver.

MemFiles è compatibile con la maggior parte (non tutti) degli strumenti che girano all'interno del processo Beacon e che possono essere istruiti per scrivere il proprio output in una directory specifica. NON richiede privilegi elevati per funzionare.

Questo include: -BOF -Assembly .NET eseguiti inline usando qualcosa come inline-executeAssembly -PE eseguiti inline usando qualcosa come Inline-Execute-PE

Tutti questi sono compatibili perché girano all'interno del processo Beacon, dove le NtAPI rilevanti sono state hookate.

MemFiles NON funziona con cose come: -execute-assembly -shell -run

Nessuno di questi è compatibile perché tutti generano altri processi le cui NtAPI NON sono state hookate.

MemFiles è stato testato con successo con strumenti come Rubeus, SharpHound, Procdump e Powershell quando vengono eseguiti all'interno del processo Beacon.

Installazione

Clonare il repository e, opzionalmente, modificare la variabile hookdir definita alla riga 56 sia in /PIC/Source/NtCreateFile.c che in /PIC/Source/NtOpenFile.c. Questa variabile è la directory "speciale" che segnala a MemFiles di intercettare il file in creazione. La variabile hookdir è impostata su "redteam" per impostazione predefinita. Assicurarsi che questa variabile non sia una directory reale sul sistema target e che sia la stessa in entrambi i file!

image

Eseguire 'make all' per compilare sia i BOF necessari che le funzioni PIC.

Caricare MemFiles.cna nel CobaltStrike Client. Assicurarsi che la directory da cui viene eseguito CobaltStrike sia scrivibile dal proprio utente; MemFiles crea lì un file di testo (memfiles.txt) per garantire la disponibilità dei dati necessari al funzionamento di MemFiles.

MemFiles può essere configurato per installarsi in ogni nuovo Beacon che effettua una chiamata al Teamserver; questo si ottiene usando la voce di menu MemFiles->Config. Per impostazione predefinita, MemFiles NON si installa automaticamente nei nuovi Beacon. Nota che questa è un'impostazione globale; se due Client sono connessi al Teamserver ed entrambi hanno caricato MemFiles.cna, se il Client A attiva l'impostazione "Install on beacon initial", la modifica avrà effetto anche per il Client B!

image

Comandi

MemFiles comprende 4 comandi rivolti al target che eseguono BOF e 1 comando interno che manipola la struttura dati del progetto.

Rivolti al target:

  1. meminit
  2. memlist
  3. memfetch
  4. memclean

Struttura dati interna:

  1. memtable

meminit

meminit è responsabile dell'installazione di MemFiles nel processo Beacon.

L'elenco delle NtAPI hookate da MemFiles è il seguente:

  1. NtCreateFile
  2. NtWriteFile
  3. NtClose
  4. NtQueryVolumeInformationFile
  5. NtQueryInformationFile
  6. NtSetInformationFile
  7. NtOpenFile
  8. NtReadFile
  9. NtFlushBuffersFile

meminit esegue le seguenti azioni principali:

  1. Invia al Beacon una funzione sostitutiva indipendente dalla posizione per ogni NtAPI hookata
  2. Crea una struttura nella memoria del Beacon per contenere vari valori richiesti da MemFiles durante il suo ciclo di vita
  3. Patcha l'indirizzo di questa struttura in ciascuna delle funzioni sostitutive PIC
  4. Alloca memoria e inietta ogni funzione sostitutiva PIC nella memoria del processo Beacon
  5. Crea un trampoline per ogni NtAPI hookata
  6. Hooka ogni NtAPI elencata sovrascrivendo alcuni/tutti i byte, reindirizzando l'esecuzione alla funzione sostitutiva PIC.

memlist

memlist viene utilizzato per visualizzare tutti i file attualmente memorizzati in memoria da MemFiles per un dato Beacon. image Vengono visualizzati diversi campi; il più rilevante e di interesse per l'utente è il nome del file e la lunghezza dei dati memorizzati.

memfetch

memfetch viene utilizzato per recuperare effettivamente i file memorizzati in memoria da MemFiles per un dato Beacon.

Per impostazione predefinita, memfetch recupera tutti i file memorizzati da MemFiles il cui "handle" è stato chiuso. Questa scelta progettuale è stata fatta per evitare problemi legati al tentativo di scaricare un file che un programma/applicazione non ha ancora finito di scrivere.

Questo significa che se un programma/applicazione non riesce a chiudere l'handle che apre al file, il file non verrà scaricato da memfetch. Questo può essere mitigato usando l'argomento "force" con memfetch, ad esempio 'memfetch force', per recuperare tutti i file dalla memoria indipendentemente dallo stato del suo handle.

I file che memfetch recupera dalla memoria vengono inviati al Teamserver come download e possono essere sincronizzati dal Teamserver al Client tramite la scheda Downloads in CobaltStrike.

Una volta che un file è stato scaricato dal Teamserver, viene cancellato dalla memoria del processo Beacon e la sua voce mostrata tramite memlist viene rimossa.

memclean

memclean è responsabile della pulizia e della rimozione di MemFiles da un processo Beacon.

Il caso d'uso standard di MemFiles prevede l'installazione e il mantenimento per l'intera durata di vita del Beacon; tuttavia, se si desidera usare MemFiles insieme a uno strumento per catturare e recuperare l'output dei file, e poi disinstallare MemFiles così che i suoi artefatti non rimangano in memoria, memclean può essere usato per riportare il processo Beacon al suo stato originale prima dell'esecuzione di meminit.

Questo comporta:

  1. Rimuovere l'hook da ogni NtAPI hookata
  2. Azzerare e liberare ogni trampoline creato
  3. Azzerare e liberare ogni funzione sostitutiva PIC iniettata
  4. Azzerare e liberare la struct di MemFiles

Nota che prima di eseguire queste azioni, memclean scaricherà forzatamente qualsiasi file memorizzato in memoria da MemFiles. Se si intende usare MemFiles con un singolo strumento e poi rimuoverlo, si può saltare l'uso di memfetch e usare direttamente memclean sia per recuperare i file che per rimuovere MemFiles dal processo Beacon in un colpo solo.

memtable

memtable viene utilizzato per visualizzare e tracciare le informazioni relative ai Beacon in cui MemFiles è attualmente installato. Visualizza anche informazioni di configurazione globali.

Ogni CobaltStrike Client ha la propria memtable; MemFiles fa di tutto per garantire la sincronizzazione dei suoi dati tra tutti i Client CobaltStrike connessi, così che MemFiles possa essere utilizzato da tutti gli Operatori in tutti i Beacon. Per maggiori dettagli, vedere "Considerazioni di progettazione e commenti".

image

Utilizzo

Inizializzare MemFiles in un Beacon usando il comando meminit. Questo può essere configurato per avvenire automaticamente attivando l'opzione nel menu MemFiles->Config.

image

Con MemFiles inizializzato, ora puoi usare i tuoi strumenti preferiti per scrivere file in memoria! Il modo in cui lo fai dipenderà dallo strumento specifico; alcuni consentono di specificare una directory in cui outputtare più file, mentre altri consentono di specificare un percorso assoluto per un singolo file creato dallo strumento. Alcuni esempi sono riportati di seguito:

SharpHound:

Qui specifichiamo che SharpHound deve outputtare tutti i file prodotti nella directory c:\redteam\ (la nostra directory speciale MemFiles) e che non deve comprimere i file in zip; MemFiles non supporta la lettura di file dalla memoria da parte dei programmi, ma solo la scrittura, quindi la funzionalità zip di SharpHound non funziona.

image

Rubeus:

Il comando "dump" viene usato con Rubeus e gli si dice di inviare tutto l'output della console a un file (situato nella nostra directory speciale)

image

Powershell:

In questo esempio, Inline-Execute-PE viene usato per caricare powershell.exe nel processo Beacon ed eseguire 'Get-ADUser' per recuperare un elenco di utenti di dominio. Usando una pipe e 'out-file', i dati possono essere scritti in memoria e poi recuperati.

image

Quando vuoi recuperare i tuoi file, esegui memfetch:

image

Quando hai finito con MemFiles e/o non vuoi lasciarlo installato in un processo Beacon, esegui memclean:

image

Nota che nell'esempio precedente c'era un file che non era stato ancora scaricato; memclean scarica questo file e lo cancella dalla memoria prima di disinstallare MemFiles.

Interroga lo stato e la configurazione di MemFiles usando memtable. Durante operazioni lunghe, rimuovi le voci dei beacon morti/vecchi dalla memtable per evitare disordine.

image

Capacità e limitazioni

Come sottolineato nell'Introduzione, MemFiles richiede una copia pulita di NTDLL nel processo Beacon per funzionare. Questo è necessario perché legge i byte originali nella NtFunction e ne copia alcuni nel trampoline, che viene successivamente usato per completare le chiamate normali alla NtFunction che MemFiles non deve interferire. Questo argomento è trattato più in dettaglio nella sezione "Dettagli tecnici, considerazioni di progettazione e commenti".

MemFiles effettua un'allocazione iniziale di 1048576 byte per ogni file; man mano che i dati vengono scritti in memoria, può e dovrà espandere questa allocazione secondo necessità per contenere file più grandi.

Il nome file memorizzato nella struct di MemFiles viene estratto da un argomento passato alla funzione sostitutiva NtCreateFile. MemFiles lo fa in modo abbastanza semplicistico: individua la directory "speciale" nell'argomento del percorso del file, cerca di raggiungerne la fine, quindi incrementa il puntatore di 1 per tenere conto del carattere '\' che separa la directory "speciale" e il nome del file. Ad esempio, nel percorso 'C:\users\tom\redteam\myfile.txt', MemFiles individua 'redteam', tiene conto del carattere backslash e seleziona 'myfile.txt' come nome del file.

A MemFiles non importano le directory precedenti nel percorso del file; 'C:\redteam\myfile.txt' e 'c:\users\tom\appdata\local\redteam\myfile.txt' sono percorsi ugualmente validi per quanto riguarda MemFiles.

Visto come MemFiles estrae i nomi dei file dai percorsi, va notato che MemFiles non supporta la creazione di sottodirectory; questo significa che MemFiles non funzionerà correttamente con strumenti che, ad esempio, cercano di creare c:\redteam\mynewdir\file1.txt, c:\redteam\mynewdir\file2.txt, c:\redteam\mysecondir\file3.txt, ecc.

Le NtAPI hookate da MemFiles sono state identificate come quelle utilizzate da vari programmi per le operazioni di I/O. Come accennato in precedenza, gli strumenti/capacità testati con successo con MemFiles/questo set di NtAPI hookate sono: SharpHound, Rubeus, Powershell, Procdump, BOF, programmi C generici che eseguono operazioni di scrittura di file e il comando bupload_raw di CobaltStrike (che consente all'Operatore di specificare la posizione remota del file). Ci sono sicuramente altri strumenti in giro che funzioneranno con MemFiles senza modifiche; altri saranno incompatibili.

A prima vista, il processo di creazione dei file in Windows sembra semplice: NtCreateFile->NtWriteFile->NtClose. Subito dopo essermi addentrato in questo progetto ho scoperto che c'era una serie di altre API coinvolte e, per rendere le cose più interessanti, le altre API coinvolte differiscono tra i programmi. Alcuni programmi, come parte del loro processo di I/O, chiamano l'API Win32 SetFilePointerEx, che a sua volta chiama la NtAPI NtSetInformationFile. Altri, come i programmi .NET tra cui SharpHound, finiscono per chiamare NtFlushBuffersFile.

La mancanza di un'unica catena comune di chiamate API per creare e scrivere file apre spazio a incompatibilità a seconda dello strumento con cui MemFiles viene utilizzato. Un programma potrebbe chiamare un'altra NtAPI che MemFiles non ha hookato, passando l'handle fittizio creato da MemFiles nella funzione sostitutiva NtCreateFile e provocando un errore "Invalid Handle" che blocca l'esecuzione. Altri programmi eseguono azioni più complesse che MemFiles non riesce a simulare/sostituire adeguatamente. Una di queste incompatibilità già identificate è ADExplorer.exe.

ADExplorer.exe è un binario Microsoft firmato utilizzato per l'enumerazione di Active Directory. Durante l'esecuzione, ADExplorer scrive dati nel file di output specificato e in seguito vi fa riferimento prima di scrivere infine l'output finale nel file al termine dell'esecuzione. Poiché usa il file di output come una sorta di cache, deve essere in grado di leggere i dati che ha già scritto nel file, ed è molto improbabile che lo faccia in un modo semplice e prevedibile.

MemFiles attualmente non supporta la lettura dei file in memoria da parte di programmi o applicazioni, ma questo dovrebbe essere possibile sviluppando ulteriormente la funzione NtReadFile personalizzata e aggiungendo alcune variabili/tracciamento dei dati aggiuntivi alla struct di MemFiles.

ADExplorer presenta un'altra sfida nelle dimensioni dei file che produce. In grandi ambienti aziendali, il file di output può superare 1GB; anche se MemFiles dovrebbe essere in grado di gestirlo a livello programmatico, certamente non è pensato per questi casi d'uso.

Strumenti incompatibili

Indubbiamente la community scoprirà strumenti con cui MemFiles non funziona correttamente; ti incoraggio ad aprire una issue descrivendo il programma/strumento incompatibile e le circostanze in cui l'hai eseguito, ad esempio se è un BOF, tramite inline-executeAssembly, Inline-Execute-PE, ecc., così posso vedere se riesco a espandere MemFiles e farlo funzionare.

IOC e AV/EDR

Gli IOC associati a MemFiles includono, ma non sono limitati a:

Allocare memoria usando VirtualAlloc Scrivere dati usando WriteProcessMemory Cambiare le protezioni della memoria allocata tra RW e RX Sovrascrivere la memoria all'interno di NTDLL.dll

AV/EDR

MemFiles non è stato sviluppato né testato contro un vero EDR; Microsoft Defender è ciò che era disponibile. Detto questo, azzarderei a dire che qualunque strumento/programma che Beacon sta eseguendo per produrre un file ha più probabilità di essere segnalato rispetto a MemFiles che cattura o memorizza quel file in memoria. La sovrascrittura della memoria in NTDLL/l'hook delle NtAPI mi sembra qualcosa che alcuni prodotti potrebbero contestare, ma non ho prove per confermarlo. Per le chiamate a NtFunction hookate che non riguardano file che sono/dovrebbero essere catturati da MemFiles, la syscall viene comunque emessa dallo spazio di indirizzi di NTDLL.dll, poiché i prodotti di sicurezza rilevano e segnalano le syscall fatte dall'esterno di quest'area.

Va notato che i file memorizzati in memoria da MemFiles NON sono codificati o crittografati; questa funzionalità potrebbe essere aggiunta se venisse identificato un caso d'uso/istanza reale in cui AV/EDR segnala un file prodotto in memoria.

Dettagli tecnici, considerazioni di progettazione e commenti

Sono stato introdotto per la prima volta al concetto di file system in memoria da un intervento a una conferenza diversi mesi fa al Tradecraftcon di KFiveFour, dove un relatore (@DexterGerig) ha dimostrato un POC che creava un file system in memoria usando un modello client-server. Metà della funzionalità che avevo immaginato per un progetto del genere è stata coperta dalla mia ultima grande release, Inline-Execute-PE. L'altra metà, l'idea di poter catturare i file prodotti dagli strumenti e memorizzarli in memoria invece che su disco, non è stata realizzata da quel progetto ed è rimasta una capacità estremamente desiderabile per ovvi motivi.

MemFiles è stata un'impresa incredibilmente impegnativa per me, perché prima di questo progetto avevo passato molto poco tempo con un debugger, non avevo alcuna conoscenza dell'assembly e non capivo l'API hooking. Ho incontrato diversi ostacoli della durata di 10-20 ore durante questo progetto che, con perseveranza, sono riuscito a superare per fortuna. Anche se probabilmente non è il modo più efficiente per farlo, ho acquisito molta familiarità con i debugger e una maggiore comprensione di come funzionano i computer nel profondo per quanto riguarda assembly, registri, stack e convenzioni di chiamata.

Quello che segue è un'analisi tecnica approfondita di alcuni dei dettagli tecnici e delle considerazioni di progettazione più importanti che sono confluiti in MemFiles.

File in Windows e come funziona MemFiles

La creazione di file su Windows inizia con NtCreateFile, a cui viene dato il percorso del file desiderato e in cambio Windows crea un file in quella posizione e fornisce un handle ad esso. L'handle restituito viene utilizzato in tutte le chiamate successive che coinvolgono il file, ad esempio a NtWriteFile e NtClose.

Nel pensare a come separare le chiamate a tutte queste API tra quelle che vogliamo intercettare e manipolare e quelle che vogliamo lasciare da sole, ho optato per cercare una parola chiave nella chiamata NtCreateFile. Questo è stato ottenuto specificando una directory unica e inesistente come parte del percorso del file nella chiamata NtCreateFile. Quando il nostro hook reindirizza l'esecuzione alla funzione sostitutiva NtCreateFile, il percorso del file passato come argomento a NtCreateFile viene esaminato per la presenza di quella "parola chiave" unica; se la trova, MemFiles sa che questa chiamata NtCreateFile riguarda un file che dovrebbe essere collocato in memoria invece che su disco. Quando ciò accade, MemFiles inizializza diverse variabili e alloca una memoria iniziale di 1MB per l'uso da parte del file, ma soprattutto associa un handle fittizio al nome file specificato nella struct di MemFiles e restituisce questo handle fittizio al chiamante.

Per tutte le altre NtAPI hookate da MemFiles, le corrispondenti funzioni Nt sostitutive esaminano l'handle passato come argomento e verificano se esiste nella struct di MemFiles; se l'handle esiste (gli handle fittizi prodotti da MemFiles sono sufficientemente fittizi da non sovrapporsi mai a uno reale) nella struct di MemFiles, MemFiles identifica questa chiamata come relativa a un file in memoria e agisce di conseguenza.### Teoria dell'hooking e funzioni di sostituzione

Per scrivere in memoria file che erano destinati al disco, MemFiles deve intercettare le chiamate ad alcune API che vengono effettuate dai programmi quando tentano di creare un file. L'hooking delle API esiste da molto tempo ed è attivamente utilizzato da molti prodotti EDR come parte fondamentale delle loro funzionalità; le chiamate a determinate API vengono reindirizzate allo spazio di indirizzi dell'EDR, dove vengono analizzate la chiamata API e le variabili passate. Se l'EDR determina che la chiamata è dannosa, ad esempio parte di uno strumento di attacco o di una kill chain, impedirà il completamento della chiamata e solleverà un avviso. Se l'EDR decide che la chiamata è benigna, ripristinerà l'esecuzione al punto da cui era stata reindirizzata e consentirà alla chiamata API di completarsi come originariamente previsto. Un'analogia semplicistica: spedisci una lettera a un amico, ma prima che l'amico la riceva un terzo la apre, la legge e decide se c'è qualcosa di illegale; in tal caso l'amico non riceve mai la lettera e la polizia viene avvisata.

MemFiles segue la stessa teoria, senza gli avvisi (o il teorico coinvolgimento della polizia). L'hooking delle API è tipicamente implementato al livello più basso possibile nello userland; le NtFunctions all'interno di NTDLL.dll. Diamo un'occhiata a NtCreateFile, prima che venga effettuato qualsiasi hooking:

image

Tutte le NtFunctions sono identiche, a eccezione del numero di syscall, che in questo esempio è 55. Il numero di syscall cambia tra le NtFunctions, e va anche notato che questo numero può cambiare tra le versioni di Windows; il numero di syscall per NtCreateFile su questo sistema operativo (Windows 11) è 55, tuttavia potrebbe essere diverso su Windows 10 (e certamente su Windows 7).

Vale la pena notare le istruzioni TEST e JNE. Queste esistono per determinare se la NtFunction deve usare la normale istruzione syscall o la legacy istruzione INT 2E. Citerò dal post di klezvirus SysWhispers è morto, lunga vita a SysWhispers!:

Ora la parte interessante: la funzione controlla se SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) è impostato a 1. SharedUserData è un simbolo che si riferisce alla struttura in modalità kernel KUSER_SHARED_DATA.

La struttura KUSER_SHARED_DATA definisce uno spazio di memoria fisso (o predefinito) utilizzato per condividere informazioni con il software in modalità utente. Questo, ovviamente, è stato fatto per rendere disponibili alcune informazioni globali di sistema al codice user-land senza l'overhead di passare ogni volta dall'esecuzione in modalità utente a quella in modalità kernel.

Il valore all'indice 0x308 rappresenta l'istruzione syscall, supportata in tutte le versioni di Windows dalla 1511 in poi. Come puoi immaginare, in tutte le versioni di Windows precedenti alla 1511, il modo standard per eseguire una syscall era chiamare l'interrupt int 2Eh.

...

Se ti stai chiedendo perché questo int 2Eh sia ancora presente, anche se Windows è ormai molto oltre la versione 1511, è perché questa istruzione è ancora utilizzata. Infatti, quando HVCI (Hypervisor-protected Code Integrity) è abilitato, SharedUserData[0x308] è impostato a 0 e si usa int 2Eh invece dell'istruzione syscall. Questo viene fatto principalmente per motivi di prestazioni, a seconda di come viene effettuato il passaggio da Ring3 a Ring0 usando l'una o l'altra istruzione.

Ho chiesto ulteriori chiarimenti su questo argomento su Twitter, e @yarden_shafir ha detto quanto segue:

image

In breve, ogni istruzione nella NtFunction potrebbe essere necessaria in qualche momento (con la possibile eccezione del NOP multi-byte alla fine che non sembra raggiungibile), e se sovrascriviamo istruzioni nella NtFunction dobbiamo assicurarci di salvarle ed eseguirle a un certo punto prima di effettuare la syscall finale (o INT 2E, a seconda dei casi).

Vale la pena notare che prima di effettuare la syscall, il numero di syscall viene spostato in RAX (mostrato come EAX nello screenshot). Poiché non vediamo RAX spinto nello stack prima di questo, ho (forse ingenuamente) assunto che il valore contenuto in RAX prima che il numero di syscall venga spostato lì non sia importante o richiesto successivamente dopo che la syscall è stata emessa. Questa è una buona notizia, perché significa che possiamo usare liberamente il registro RAX purché ci assicuriamo che contenga il numero di syscall prima che la syscall venga emessa.

Per reindirizzare l'esecuzione al nostro codice personalizzato/NtFunction di sostituzione, sovrascriveremo parte della NtAPI originale, spostando l'indirizzo della nostra NtFunction di sostituzione in RAX e usando poi un'istruzione JMP per andare a quel codice:

image

Sono necessari 12 byte per le istruzioni MOV e JMP; poiché stiamo alterando altre istruzioni sovrascrivendo i primi 12 byte della NtAPI, quelle istruzioni sono state sostituite con NOP per mantenere la corretta spaziatura e l'allineamento della NtAPI.

Quando il programma chiama ora NtCreateFile, salterà l'esecuzione alla nostra NtFunction di sostituzione.

Codice personalizzato e NtFunctions di sostituzione

MemFiles si discosta dal modo in cui gli EDR eseguono l'hooking per quanto riguarda il luogo in cui risiedono le funzioni di sostituzione verso cui vengono reindirizzate le API hookate. Molti EDR caricano la propria DLL in un processo. Le API hookate vengono reindirizzate allo spazio di indirizzi di questa DLL caricata, dove può avvenire l'analisi. Dal momento che l'intero scopo di questo progetto era evitare di scrivere file su disco, mettere una DLL su disco e far sì che il nostro processo Beacon la carichi per avere accesso alle nostre NtFunctions di sostituzione sembrava una strada poco valida. Esiste una POC di diversi anni fa che consente il caricamento di DLL dalla memoria, una strategia praticabile per le nostre esigenze, ma il progetto non è mantenuto e sembrano esserci diversi problemi. Inoltre sono 1200 righe di codice e sarebbe un'impresa convertirlo in formato BOF.

Come breve parentesi, le nostre NtFunctions di sostituzione non possono risiedere in un BOF; CobaltStrike carica, esegue e poi cancella i BOF dalla memoria del processo quando hanno finito. Poiché abbiamo bisogno di una o più funzioni persistenti in memoria che possano essere chiamate ogni volta che il processo chiama una delle NtAPI hookate, i BOF non funzionano.

La risposta a cui sono arrivato è stato il codice position independent (PIC). Come suggerisce il nome, a differenza dei normali eseguibili che richiedono di essere caricati in un determinato punto o con parti in una certa relazione tra loro, il PIC può essere posizionato ed eseguito ovunque in memoria. Questo apre la porta alla scrittura delle nostre NtFunctions di sostituzione come eseguibili PIC, alla loro iniezione nel processo Beacon e al reindirizzamento dell'esecuzione verso di esse da parte dei nostri hook quando vengono effettuate chiamate alle nostre NTAPI hookate.

Il modello per queste NtFunction PIC proviene dal progetto ShellcodeTemplate di Cracked5pider.

Una deviazione notevole rispetto al progetto base è che il progetto base è progettato per creare un eseguibile PIC completo; vale a dire che include ASM per salvare lo stack pointer, creare spazio nello stack, chiamare la NtFunction di sostituzione designata contenuta nell'exe e quindi ripristinare lo stack pointer una volta che la funzione è terminata. L'istruzione call effettuata dall'exe PIC presenta un problema: facendo ciò, l'indirizzo di ritorno del punto in cui è stata effettuata la chiamata (nell'ASM dell'exe PIC) viene spinto nello stack, il che porterà l'istruzione ret incontrata successivamente a far tornare l'esecuzione nell'exe PIC, piuttosto che al chiamante della NtAPI originale.

Per mitigare questo problema, il file ASM nel progetto ShellcodeTemplate è stato modificato per rimuovere l'ASM relativo alla preparazione dello stack, alla chiamata della funzione e al successivo ripristino dello stack pointer dopo la fine dell'esecuzione della funzione. Il risultato è che l'hook inserito nella NtAPI ora salta direttamente l'esecuzione nella NtFunction di sostituzione, con lo stack e i registri impostati come quando la NtAPI originale è stata chiamata dal programma (con l'eccezione di RAX, che viene usato per il nostro JMP).

ASM originale di ShellcodeTemplate:
image

ASM di MemFiles:
image

Ogni NtAPI hookata ha la propria NtFunction PIC che contiene la logica necessaria per:

A. Eseguire azioni specifiche di MemFiles come creare un handle fittizio, scrivere dati in memoria, alterare variabili nella struct MemFiles, ecc.
oppure
B. Dirigere l'esecuzione a un trampoline che riporti la chiamata API sulla giusta strada e la reindirizzi nuovamente in NTDLL dove può essere effettuata la syscall

Alcune delle NtFunctions di sostituzione sono più complesse di altre; quando una chiamata a una NtAPI hookata riguarda MemFiles, alcune, come NtCreateFile e NtQueryVolumeInformationFile, modificano variabili passate come argomenti alla NtAPI in base alla documentazione MSDN, ai risultati dei test e a qualche ipotesi/senso comune. Altre, come NtClose e NtReadFile, restituiscono semplicemente STATUS_SUCCESS al chiamante originale per evitare l'inevitabile errore "Invalid Handle" che altrimenti sorgerebbe passando un handle fittizio creato da MemFiles.

Quando una chiamata a una NtAPI hookata NON riguarda MemFiles, dobbiamo dirigere l'esecuzione a un trampoline per rimettere le cose sulla giusta strada:

image

Trampoline

Il trampoline è responsabile dell'esecuzione di tutte le istruzioni che non sono state eseguite nella NtAPI originale a causa dell'hooking di quell'API; questo include qualsiasi istruzione parzialmente o completamente sovrascritta dall'hook originale. L'hooking delle API può portare rapidamente a problemi in cui non abbiamo abbastanza spazio per eseguire tutte le istruzioni di cui abbiamo bisogno. I trampoline possono aiutare ad alleviare anche questo problema, poiché possiamo eseguire un numero qualsiasi di azioni per impostare i nostri registri e/o lo stack prima di saltare di nuovo alla NtAPI originale. Il trampoline di NtCreateFile può essere visto di seguito:

image

È abbastanza evidente che le tre istruzioni sovrascritte dal nostro hook iniziale si possono vedere come le prime tre istruzioni del trampoline:

MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1

Come accennato in precedenza, il grande requisito in tutta questa manipolazione è che il numero di syscall (55 nell'esempio precedente) risieda in RAX(EAX) prima di emettere la syscall. Abbiamo però un problema: dobbiamo comunque usare un'istruzione JMP per riportare l'esecuzione alla NtAPI originale. Anche se potrebbe esserci un altro registro che non contiene informazioni importanti e potrebbe essere sfruttato a tal fine, non ho trovato un'opzione costantemente sicura dato il numero di NtAPI che stiamo hookando, ognuna delle quali potrebbe usare i registri in modo diverso. L'opzione sicura è continuare a usare RAX; possiamo farlo spingendo il numero di syscall contenuto in RAX nello stack. Possiamo quindi spostare in RAX l'indirizzo a cui vogliamo saltare di nuovo nella NtAPI originale e usare un'istruzione JMP per tornare in NTDLL:

image

Nell'hook installato nella NtAPI è inclusa un'istruzione POP RAX, ed è qui che saltiamo usando il nostro trampoline. Eseguendo questa istruzione, il numero di syscall viene ripristinato in RAX dalla cima dello stack e ci prepara a emettere la syscall. Nota che l'istruzione JNE della NtAPI originale non hookata è ancora qui; la corrispondente istruzione TEST, che imposta il flag ZF e determina se un JNE viene eseguito (il che ci farebbe saltare oltre la syscall fino alla INT 2E), è stata eseguita nel trampoline. Organizzare le cose in questo modo ci consente sia di hookare con successo la NtAPI e reindirizzare l'esecuzione alla nostra NtFunction PIC di sostituzione, sia di garantire che non stiamo saltando nulla o perdendo funzionalità a causa del nostro hooking.

Il problema del carro davanti al cavallo

Per riassumere brevemente quanto trattato finora, quando MemFiles viene inizializzato viene creata una struct nella memoria del processo Beacon che contiene informazioni importanti per il funzionamento di MemFiles. Le informazioni in questa struct vengono continuamente referenziate durante l'intero ciclo di vita di MemFiles, incluse quelle da ciascuna delle NtFunction PIC di sostituzione e dai BOF utilizzati per interrogare e recuperare i file memorizzati in memoria da MemFiles. A tal fine, quando la struct viene creata, l'indirizzo di memoria in cui risiede la struct viene comunicato al Teamserver:

image

Dopo aver memorizzato questo indirizzo in memtable, i successivi comandi MemFiles (memlist, memfetch, memclean) inviano questo indirizzo come argomento al BOF così che la struct possa essere localizzata e referenziata. Ma come fanno le NtFunction PIC a localizzare la struct?

Il problema evidente è che le NtFunction PIC richiedono l'indirizzo di memoria della struct di MemFiles, ma sono già compilate quando la struct viene creata. Una prima implementazione di MemFiles ha affrontato questo problema dividendo l'inizializzazione di MemFiles in due BOF separati. Il primo creava la struct e inviava l'indirizzo al Teamserver, che tramite un po' di magia degli script Aggressor avrebbe estratto l'indirizzo, lo avrebbe inserito nel file sorgente di ciascuna NtFunction e poi le avrebbe ricompilate nella NtFunction PIC finale. Il secondo BOF avrebbe poi trasmesso le NtFunction PIC finite ed eseguito l'effettiva iniezione e l'hooking delle NtAPI.

Oltre a essere brutto e a richiedere tempo extra, potrebbero sorgere problemi reali nella situazione in cui più beacon tentano di inizializzare MemFiles contemporaneamente. Se il Beacon 2 richiamasse con l'indirizzo della propria struct MemFiles mentre il Beacon 1 è nel bel mezzo del patching e della ricompilazione dei file sorgente delle NtFunction, le cose potrebbero complicarsi.

La soluzione elegante a questo problema consiste nell'effettuare una patch binaria sulla NtFunction PIC, in cui l'indirizzo della struct di MemFiles viene applicato alla NtFunction compilata e risulta accessibile durante l'esecuzione. Per facilitare ciò, una stringa segnaposto viene scritta in ciascuna NtFunction:

image

Questa variabile può essere vista nel codice compilato usando uno strumento come xxd:

image

Quando viene eseguito il comando meminit, ciascuna delle NtFunction PIC viene inviata al Beacon insieme al BOF InstallHooks. Questo BOF è responsabile della creazione della struct di MemFiles; dopo averlo fatto, chiama la funzione patchAddr su ciascuna delle NtFunction PIC. patchAddr è responsabile di individuare la stringa di A e sostituirla con la rappresentazione in stringa dell'indirizzo della struct. In effetti, usando l'indirizzo di memoria dello screenshot precedente, la variabile pFileInfoStr ora appare così:

char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );

Questa rappresentazione in stringa può poi essere trasformata nel valore esadecimale effettivo, che è il nostro indirizzo di memoria. I BOF riescono a farlo abbastanza semplicemente usando l'API _strtoi64:

image

Ovviamente le cose non potevano essere così facili per le NtFunction PIC.

Questo breve frammento di assembly mostra la fine di una delle NtFunction PIC. Nota l'istruzione JMP RAX, che è la NtFunction PIC che chiama il trampoline (il che significa che questa è una chiamata a cui MemFiles NON ha falsificato né interferito):

image

Per ragioni sconosciute, quando ho tentato di usare _strtoi64 (o una qualsiasi delle sue API cugine come stroull o atoll), questa istruzione JMP RAX diventava un'istruzione CALL RAX. Sono sicuro che ci sia una valida ragione per questo, che coinvolge qualche livello profondo di "come funzionano i computer", ma questo cambiamento apparentemente insignificante rompe parecchie cose. Dopo aver passato più di 15 ore a cercare un modo per trasformare la rappresentazione in stringa dell'indirizzo della struct di MemFiles nel valore esadecimale effettivo per poter utilizzare i valori in essa contenuti, mi sono imbattuto in questo post su StackOverflow in cui un commentatore ha fornito una routine personalizzata progettata per microcontrollori per convertire una stringa in uint32; fortunatamente funzionava anche per uint64 senza modifiche e, cosa più importante, preservava l'istruzione JMP RAX successiva nella NtFunction PIC senza trasformarla in CALL. Il frammento finale:

image

Vecchie NtAPI vs Nuove NtAPI

Per semplicità non è stato menzionato prima, ma il formato delle NtAPI è cambiato nel corso degli anni. Le versioni precedenti, come quelle usate da Windows 7, sono molto più corte delle controparti moderne, essendo lunghe solo 16 byte invece di 32:

image

Questo richiede modifiche a come le NtAPI vengono hookate da MemFiles e a come viene costruito il trampoline. Affinché MemFiles possa identificare la versione delle NtAPI con cui ha a che fare, il BOF InstallHooks prima risolve l'indirizzo della NtAPI in questione e poi legge 32 byte da quella posizione. Il diverso formato comporta che l'istruzione syscall si trovi in posizioni diverse tra le versioni delle NtAPI; controllando la presenza o l'assenza della syscall a un certo offset di byte, MemFiles è in grado di determinare se ha a che fare con l'implementazione delle NtAPI moderna o legacy e agire di conseguenza:

image

Dopo l'hooking, l'API NtCreateFile legacy appare così:

image

E il trampoline usato per impostare i registri e poi saltare di nuovo a NtCreateFile:

image

Nel complesso la tecnica è molto simile, ma le cose sono molto più strette e senza molto margine di manovra. Vale la pena notare che per far sì che tutto si adatti e funzioni correttamente, l'istruzione syscall ha dovuto essere spostata all'interno di NtCreateFile; risiede ancora nello spazio di memoria dell'API NtCreateFile in NTDLL, ma si è spostata dal 9° e 10° byte dell'API al 14° e 15° byte dell'API, sacrificando il NOP multi-byte per riuscire a far entrare tutto correttamente.

Trovare NtAPI relative a I/O

Alcune delle API relative alle operazioni di I/O sui file erano intuitive e quindi facili da identificare e hookare; altre erano molto più elusive, richiedendo ore e ore trascorse in WinDbg e x64dbg a scorrere passo passo l'assembly cercando di identificare quali API venivano chiamate. Devo credere che ci fosse un modo più efficiente per svolgere il compito, ma stavo imparando strada facendo.Come nota extra, ho pensato di descrivere il processo — che col senno di poi sarebbe dovuto essere rapidissimo — per identificare la NtAPI che impediva a SharpHound di funzionare per 20 ore. Essendo un programma .NET, SharpHound ha generato un call stack davvero brutto, tutto incentrato sul fatto che aveva stabilito che "The handle is invalid". Sebbene .NET sia un linguaggio amichevole per i programmatori, la maggior parte (tutta?) della funzionalità viene tradotta e alla fine passa attraverso l'API Win32 (e di conseguenza la NtAPI), dove possiamo osservarla e hookarla come qualsiasi altra cosa.

sharphounderror

L'errore "Invalid Handle" è stato un indizio inequivocabile che SharpHound utilizzava una NtAPI che non stavo hookando (a differenza di una delle mie NtFunctions PIC esistenti che non funzionavano correttamente), così ho deciso di cercare di capire quale fosse. La tecnica che avevo usato con altri strumenti fino a quel momento era quella di impostare prima un breakpoint su NtCreateFile e individuare la chiamata relativa alla mia directory "speciale". Da lì eseguivo il programma passo dopo passo (di solito diverse volte perché mi perdevo) e osservavo quali funzioni il programma chiamava successivamente. È seguendo questa metodologia che ho scoperto che NtQueryVolumeInformationFile, NtQueryInformationFile e NtSetInformationFile venivano chiamate e dovevano essere hookate.

SharpHound aggiunge qualche complicazione in più, poiché esegue molte delle sue attività in modo asincrono. Questo rende molto più difficile tracciare i passaggi lineari che un singolo file compie attraverso le chiamate API, perché ci sono più file che attraversano il processo contemporaneamente. Inoltre, eseguendo il programma passo dopo passo dopo la chiamata a NtCreateFile, ho scoperto che alla fine il thread in cui veniva effettuata la chiamata a NtCreateFile termina; le successive chiamate a NtWriteFile (e qualunque altra sconosciuta chiamata API oggetto di questa ricerca) avvengono in un thread diverso, il che complica ulteriormente il processo di ricerca.

Avendo avuto a che fare solo brevemente con .NET, non ero molto pratico nell'analizzare i call stack prodotti dagli errori, tanto più per quelli resi due volte più brutti dall'uso dell'async. Nel corso del problema delle 20 ore, in assenza di progressi con la mia strategia precedente, continuavo a tornarci sopra, e lentamente ma inesorabilmente ne capivo sempre di più. Circa al terzo "blocco" dall'alto, separato dalle righe "--- End of stack trace...", si poteva vedere una riga che recitava "at Sharphound.Writers.JsonDataWriter...". Questo mi ha dato un punto di partenza relativo nel codice di SharpHound vero e proprio, che è open source e disponibile su Github. Come suggeriva il nome, la funzione di SharpHound si occupava di scrivere l'output JSON su file; ero già consapevole che i miei dati non venivano scritti su file correttamente, quindi non era una novità. Risalendo di un livello nel call stack, la riga rilevante successiva era "at System.IO.Streamwriter.". Il prefisso System.IO mi diceva che si trattava di una funzione intrinseca di .NET, a differenza di una funzione specifica di SharpHound. Guardando la parte più in alto del call stack, la riga che mi saltò all'occhio fu "at System.IO.FileStream.FlushOSBuffer()". Decisi di cercare FlushOSBuffer su Google e vedere cosa potevo trovare.

Questo mi ha portato alla documentazione .NET di Microsoft per filestream.cs. Lì ho trovato la definizione di FlushOSBuffer:

image

Sembra che chiami un'API Win32, FlushFileBuffers. La definizione di Win32Native.FlushFileBuffers si trova consultando la documentazione di Win32Native:

image

Chiunque abbia lavorato in .NET con P/Invoke riconoscerà il formato. Ora avevo un'API Win32 che sapevo essere chiamata da System.IO.Filestream.FlushOSBuffer(), la mia problematica funzione .NET. Impostare un breakpoint su KERNEL32!FlushFileBuffers e lanciare SharpHound lo ha confermato, e proseguendo passo dopo passo ho visto rapidamente che sotto il cofano FlushFileBuffers chiama NtFlushBuffersFile. Hookare questa API ha alleviato i problemi che SharpHound stava avendo e gli ha permesso di funzionare correttamente, scrivendo i suoi file di output in memoria.

Scaricare i File dalla Memoria

Una parte critica di questo progetto è la capacità di scaricare effettivamente i file sul Teamserver di CobaltStrike una volta che sono in memoria. Prevedibilmente, il normale comando di download di CobaltStrike non funziona con un percorso file che non esiste davvero. Con quello che so ora, una soluzione probabilmente sta nel completare il codice PIC NtReadFile sostitutivo per consentire la lettura dei file in memoria invece della sola scrittura. Non avendo quella conoscenza in precedenza, riuscire a recuperare effettivamente i file è stato un blocco importante.

Per caso mi sono imbattuto in un BOF scritto da EspressoCake che conteneva una funzione che mi ha colpito:

image

Guardando il codice, sembra che utilizzi un'opzione CALLBACK di Beacon non documentata:

image

La funzione consente a un BOF di avviare il download di un file verso il Teamserver dal target, invece che dal Client. Questa capacità (che in seguito ho scoperto essere il risultato combinato del lavoro di diverse altre persone, tra cui @Cr0Eax, @EthicalChaos e @anthemtotheego) ha rimosso il grosso blocco che esisteva in precedenza, perché ora avevo un modo per avviare un trasferimento di file dal sistema target per il file in memoria. Un grande ringraziamento a tutti i coinvolti per questo frammento di codice che prevedo sarà utile anche in futuro.

Struttura Dati di MemFiles

Una parte impegnativa di questo progetto è stata garantire la disponibilità delle funzionalità di MemFiles a tutti i Client CobaltStrike connessi al Team Server. I dati di MemFiles sono memorizzati in strutture create da MemFiles.cna, che devono essere caricate in ogni Client che desidera utilizzare lo strumento; di conseguenza, queste strutture dati vivono all'interno di ciascun Client, non sul Team Server. Se questi dati vivessero in un'unica posizione centrale (TS), sarebbe banale recuperarli da ogni Client e l'intera faccenda sarebbe un non-problema; se il Team di CobaltStrike integrasse formalmente una funzionalità come MemFiles in CobaltStrike, sono certo che questa sarebbe la direzione che prenderebbero. Ma essendo un componente aggiuntivo della community, ci accontentiamo di quello che abbiamo.

Ci sono un paio di scenari diversi di cui dobbiamo preoccuparci quando si tratta di garantire che ogni Client CobaltStrike abbia i dati più recenti e accurati riguardanti lo stato di MemFiles all'interno dei Beacon e la configurazione:

Nuovi Client che si connettono al TS e necessitano della memtable corrente
Casi in cui un solo Client è connesso al TS e riavvia CobaltStrike (perdendo così la memtable memorizzata nella memoria del Client)
Il Client A apporta una modifica ai dati di MemFiles che deve essere comunicata al Client B

È stato adottato un approccio a più livelli per affrontare questi scenari. Per gestire il caso in cui un solo Client CobaltStrike è connesso al TS (e quindi è l'unica entità ad avere i dati della memtable), ogni volta che il Client modifica la memtable (meminit, memclean) scrive anche il contenuto della sua memtable in un file di testo locale situato nella directory di CobaltStrike. Se il Client esce/riparte, o quando MemFiles.cna viene ricaricato, tenterà prima di tutto di leggere il file locale memtable.txt per popolare la propria memtable in memoria.

Quando più Client sono connessi a un TS e un nuovo Client si unisce (come da Event Log), ogni Client recupera un elenco di tutti gli utenti connessi al TS e lo ordina alfabeticamente. Il Client che compare per primo in quell'elenco viene selezionato come Client "Broadcast" e, dopo aver atteso 5 secondi (per consentire al nuovo Client di inizializzarsi e leggere il proprio memtable.txt locale), invia messaggi (Actions) nell'Event Log per ogni voce della propria memtable. Tutti i Client (tranne quello in Broadcast) leggono questi messaggi e aggiornano la propria memtable con le informazioni trasmesse; questo include l'aggiornamento delle voci esistenti e l'aggiunta di eventuali voci aggiuntive che la rispettiva memtable non contiene.

Le operazioni normali che coinvolgono MemFiles si basano anch'esse sull'invio di messaggi nell'Event Log. Quando il Client A esegue meminit, viene trasmesso un messaggio contenente tutte le informazioni pertinenti della memtable; TUTTI i Client aggiornano la propria memtable analizzando questi messaggi dell'Event Log trasmessi tramite l'hook "on Event_Action". Le modifiche ai dati di MemFiles vengono apportate anche quando meminit termina l'esecuzione del suo BOF; queste modifiche vengono comunicate dal Beacon (ad es. dopo l'esecuzione di meminit, il Beacon richiama la posizione di memoria della struct pMemAddrs) e come tali sono visibili a tutti i Client connessi, che aggiornano la propria memtable tramite l'hook "on Beacon_Output".

Questi sforzi separati, combinati, fanno sì che MemFiles sia in grado di sincronizzare in modo efficiente e affidabile dati critici tra più Client.

Crediti e Ringraziamenti

Questo progetto non sarebbe possibile senza i contributi delle seguenti persone e progetti:

  1. x64-NTAPI-inline-hook di globalpolicy
  2. x64 Function Hooking by Example di Kyle Halladay
  3. ShellcodeTemplate di Cracked5pider AKA @C5pider
  4. @ilove2pwn_, tramite Cracked5pider
  5. DLL-Exports-Extraction-BOF di EspressoCake AKA @the_bit_diddler
  6. @anthemtotheego, tramite EspressoCake
  7. @Cr0Eax, tramite @anthemtotheego
  8. @EthicalChaos, tramite @anthemtotheego
  9. SysWhispers is dead, long live SysWhispers! di KlezVirus
  10. @yarden_shafir
  11. @DexterGerig
Scarica lo strumento