Studiare il codice dei malware dal punto di vista dell’analista è utile per comprendere il funzionamento dei software malevoli e, di conseguenza, identificarli prima che si manifestino in tutta la loro pericolosità all’interno dell’hard disk dei nostri computer.
Indice degli argomenti
Il codice dei malware: gli attrezzi per “smontarlo”
Come ogni professionista, l’analista di malware costruisce nel tempo il proprio set di strumenti. Dove li tiene? Normalmente in una virtual machine (VM) che viene sigillata in uno stato “pulito” e clonata ad ogni nuova analisi.
Vedremo in seguito che, come un analista cerca di conoscere il suo avversario, avviene anche il contrario, quindi può essere necessario a volte creare macchine specifiche per alcune analisi.
Di solito la VM in uso non è connessa alla rete, per evitare diffusioni accidentali del malware o fornire informazioni agli autori del malware. Spesso, però, è necessario analizzare il traffico generato, ad esempio per vedere dove il software si connette e cosa scarica; a tal proposito, è possibile utilizzare dei segmenti di rete appositi separati da tutto o, quando si devono ad esempio forzare alcuni tipi di risposte al malware, dei simulatori di Internet (come Fakenet-ng) o programmi scritti appositamente, come script Python o PHP.
Alcuni malware hanno funzioni specifiche per cercare di capire dove sono collocati geograficamente, per attivarsi solo in certi contesti: per farlo utilizzano servizi di risoluzione IP Geolocation esterni, spesso forniti dai C&C (vedi in seguito). È quindi necessario utilizzare dei gateway di rete che reindirizzino il traffico ad esempio su specifiche VPN, così che l’IP pubblico visto dal malware o dal suo server C&C sia quello dell’area geografica interessata.
Non inizio l’annosa discussione su quali siano gli strumenti “migliori” per svolgere i vari compiti, vengono tutti in secondo piano rispetto alla competenza. Mi limiterò ad elencare le categorie principali in cui ricadono ed alcuni rappresentanti tra i più diffusi. Dove necessario, successivamente verranno espansi in paragrafi dedicati:
- Prodotti di virtualizzazione dei sistemi operativi: di uso ormai comune, servono per creare le virtual machine “usa e getta” per l’analisi, mantenendo i contenuti dannosi quanto più possibile lontani dalla workstation dell’analista. Molte volte si lavora con snapshot di VM a vari livelli di patching, per verificare l’utilizzo di specifiche vulnerabilità. La libreria di VM diventa quindi piuttosto voluminosa nel tempo. In questa categoria: VirtualBox (multipiattaforma e gratuito), VMWare Workstation, Hyper-V per chi usa Windows 10.
- Disassemblatori: prendono il programma vero e proprio e ne rappresentano la struttura e la sequenza di istruzioni di basso livello (qualche forma di assembler) all’analista. Per i più puntuali: esistono certamente anche i cosiddetti “decompilers” che sotto certe condizioni cercano di ricondurre sequenze di assembler a porzioni di codice di più alto livello, tipo pseudo-C, al fine di facilitare la comprensione. In questa categoria: Hex Rays IDA Pro (disponibile anche in versione Free), NSA Ghidra, Radare, dnSpy e Ilspy per .NET.
- Debuggers: come i precedenti “smontano” il programma e ne mostrano le istruzioni, ma permettono anche di eseguirlo un pezzo alla volta per verificarne il comportamento. In questo modo è possibile analizzare la memoria del programma, i suoi accessi e le modifiche. In questa categoria: x32dbg (gratuito), WinDbg (tra i tool di sviluppo di Microsoft, gratuito), OllyDbg2 (non freschissimo ma viene comodo averlo). IDA (Pro e Free) integra funzioni di debugging.
- Strumenti di analisi base e modifica della struttura dei programmi. I programmi eseguibili altro non sono che dei database contenenti moltissime informazioni, oltre al programma vero e proprio e spesso è necessario analizzare o forzare alcuni valori. In questa categoria: PEBear, Resourcehacker.
- Strumenti di sviluppo (IDE, compilatori, interpreti): servono per automatizzare i processi o per scrivere programmi che facciano cose non coperte dagli strumenti commerciali o comunque reperibili. In questa categoria: è letteralmente una categoria sterminata, ma un singolo strumento ormai inevitabile è un interprete Python, disponibile per qualsiasi piattaforma. Aggiungerei Visual Studio che è disponibile in edizione gratuita.
- Strumenti di analisi e di simulazione della rete: sniffer, proxy espliciti e trasparenti, programmi custom, VPN. In questa categoria: suite di Wireshark, Mitmproxy, OpenVPN, Burp Suite e Fiddler, Fakenet-ng.
La lista è ovviamente limitata e specifica per certi tipi di analisi: mettere mano a malware Android, ad esempio, richiede altri strumenti.
Il codice dei malware: iniziamo a scoprirlo
Riceviamo il malware da uno dei nostri canali/colleghi, ad esempio qualcosa che in origine era un allegato di una e-mail (non discutiamo qui i vettori di ingresso di un malware nella nostra struttura).
Per prima cosa bisogna capire la tipologia di analisi che ci aspetta: si procede un po’ “a naso” e un po’ usando esperienza e gli strumenti di analisi base citati in precedenza. Scopriremo così se abbiamo di fronte un macro virus dentro un documento Office, uno script VBS, Powershell o Bash, piuttosto che un eseguibile per Windows a 32 bit, Java o .Net.
Secondo il tipo di malware, gli strumenti da usare ricadono sempre nelle categorie sopra descritte, ma se ne useranno di specifici: ad esempio disassemblare un eseguibile PE/32 si fa con strumenti diversi rispetto che se fosse un assembly .Net, di norma.
Gli strumenti “informativi” di cui sopra (ad esempio Die, Detect It Easy) ci diranno anche quali strumenti specifici (compilatori, linker, librerie) sono stati utilizzati per la preparazione del malware. Sono tutte informazioni importanti per guidare e velocizzare l’analisi.
Un’altra informazione essenziale che si può ottenere in questa fase è se il programma sia compresso e/o cifrato. Dovendo scegliere uno dei molti pattern di analisi (trattarli tutti è letteralmente impossibile), ed è uno dei possibili scenari tra quelli oggi molto comuni, procederò con questa assunzione: ci concentreremo su un malware di tipo cryptovirus, cifrato per piattaforma Windows. Attenzione sempre ai termini: cryptovirus è la funzione del malware, quella di cifrare i file a scopo di riscatto. Il fatto che esso stesso sia cifrato è cosa differente e oggetto di questa trattazione.
Il codice dei malware: lo smontaggio
Individuata la “razza” del programma che veicola il malware, si comincia a disassemblarlo: due ottimi prodotti sono l’ormai ubiquo IDA Pro e il (relativamente recente) regalo dell’NSA, Ghidra. Questi forniscono una struttura a grafo delle varie parti del programma, in linguaggio Assembler. Ad esempio, IDA ci darà questa rappresentazione della funzione:
Tra le informazioni che otteniamo dal disassemblatore c’è la lista delle funzioni “importate”, cioè tutte quelle funzioni che il programma richiede dal sistema su cui sta girando: da quelle specifiche per la crittografia a quelle basilari come la gestione della memoria, dei thread e dei processi. Ad esempio:
Tuttavia, le informazioni come quella sopra sono parziali, anche nel migliore dei casi, in quanto è possibile durante l’esecuzione del programma mappare nello spazio di indirizzamento dell’applicazione delle librerie non indicate nella tabella di import.
Molte delle tecniche di evasione dell’analisi del malware vertono proprio sul riuscire ad invocare il caricamento di librerie esterne senza farlo in maniera evidente.
Il codice dei malware: la detonazione
Come funziona un malware compresso e cifrato? Non c’è una risposta univoca ed ognuno degli step che indicherò potrebbe avere molte varianti.
I malware solitamente lavorano in più “fasi” successive. Sul PC dell’utente arriva un “dropper”, cioè il programma che si occupa della prima fase dell’infezione ed è quello di cui ci stiamo occupando ora: l’idea è che una volta in esecuzione questo scarichi da Internet i pezzi successivi, dando avvio alle fasi successive.
Nel caso che stiamo modellando (di nuovo, abbastanza comune) le fasi successive sono in realtà degli altri programmi contenuti nel dropper stesso (ad esempio come risorse nell’eseguibile PE), e sono proprio il contenuto compresso e cifrato di cui stiamo parlando. All’avvio il programma esegue perciò due funzioni: decifra e scompatta in memoria il contenuto (payload) del malware vero e proprio, quindi lo esegue.
Tecnicamente le operazioni da compiere sono quelle di allocare un’area di memoria dove mettere il malware decifrato, solitamente invocando la funzione VirtualAlloc dalla libreria KERNEL32 di Windows. Questa funzione riserva uno spazio della dimensione richiesta e fornisce un puntatore all’inizio di quel segmento di memoria.
Per i meccanismi di protezione della memoria dei processori x86, quella memoria è normalmente utilizzata per i dati, quindi leggibile e scrivibile; il malware può quindi utilizzarla come destinazione della procedura di decifratura e decompressione.
Tuttavia, se quello contenuto sarà un programma, è necessario modificare gli attributi di quella zona di memoria marcandola come eseguibile. A questo scopo è predisposta la funzione VirtualProtect. Quando si apre un malware per l’analisi si impostano sempre delle interruzioni proprio su queste due chiamate.
Ma perché deve fare una cosa così complicata, invece che eseguire subito il payload malvagio? Per un paio di motivi. Il primo è che esistono gli antivirus che possono individuare il payload semplicemente cercando le tracce (sequenze di byte) dal proprio database. Fino a che non sono scompattate e decifrate, queste sequenze sono nascoste e non rilevabili.
Perché dunque non cercare la traccia del contenuto compresso e cifrato? Perché le chiavi di cifratura possono essere generate dinamicamente per ogni infezione, quindi lo stesso payload in due sample diversi avrà tracce differenti (anche la compressione stessa ha un ruolo in questo, ne riparleremo).
Il secondo motivo è che ogni antivirus moderno non ragiona più solo con le tracce, ma analizza i programmi in esecuzione per capire se si comportano in modo “strano”, andando ad utilizzare quelle funzioni di sistema che “solitamente” vengono usate nei malware. Se il codice (compresso e cifrato) non dichiara, come avviene per un programma normale, che funzioni vorrà usare, rimane opaco all’antivirus che può avere dei sospetti ma non conferme positive.
Gli “antivirus” (termine oggi alquanto riduttivo), possono in questa fase provare a lavorare sulla parte di codice del dropper che esegue decompressione e decifratura, che solitamente cerca di avere bisogno del minimo numero di componenti del sistema operativo, proprio per non destare sospetti oltre che per essere “compatibile” con quanti più computer possibile.
Ad esempio, le funzioni di crittografia sono reimplementate direttamente nel codice del malware, piuttosto che importate dalle librerie di sistema dove sono di norma disponibili, come la CRYPT32. In questo caso la cifratura serve da velo di copertura del codice maligno (è una delle forme del cosiddetto polimorfismo del codice), quindi non deve essere necessariamente tra quelle considerate forti: molti malware infatti contengono routine RC4 o altri algoritmi anche più banali, e quando usano procedure robuste come AES non sempre lo fanno in modo crittograficamente valido e sicuro in quanto, a tutti gli effetti, non serve.
Per chiarezza ribadisco che non ci si riferisce qui agli algoritmi usati per cifrare i dati del malcapitato utente, ma proprio il codice del programma malevolo stesso.
Molte implementazioni degli algoritmi sono prese tal quale dai repository di software open source: ne condividono quindi la struttura e sono spesso facilmente identificabili. Inoltre, diversi algoritmi fanno uso di S-box o tabelle di riferimento, per cui esistono tabelle di valori sempre uguali in tutte le implementazioni: una volta individuate danno la certezza di quale sia l’algoritmo in uso.
I passi successivi
Abbiamo lasciato il dropper a caricare in memoria nella macchina virtuale il codice maligno che effettivamente farà le azioni negative previste, ad esempio cifratura dei dati e richiesta di riscatto. È da notare che quella che descrivo è una cosiddetta “fileless infection”, perché il malware non passa dal disco dove, per diversi motivi, potrebbe essere intercettata più facilmente.
Decifrare il malware partendo dal dropper ricevuto è solitamente un lavoro improbo. L’analista tuttavia, conoscendo il suo modus operandi, utilizza il debugger per eseguire il codice del dropper fino a fino a che questo non abbia svolto il proprio lavoro e abbia preparato una copia decifrata e decompressa del malware, pronta per l’esecuzione (la realtà è che la procedura è spesso iterata a più livelli). La tecnica usata è quella indicata in precedenza, ossia di mettere dei punti di interruzione (breakpoint) sulle istruzioni sospette o sulle chiamate al sistema operativo che manipolano memoria e processi.
L’analista può quindi catturare quella zona di memoria, su cui è stata eseguita una VirtualProtect per marcarla 0x40 (PAGE_EXECUTE_READWRITE) e metterla in un file. Qui vediamo come ProcessHacker ci permetta di vedere tutte le zone di memoria mappate nello spazio di un programma e il loro stato di protezione. L’indirizzo (0x530000) è quello che ci aspettiamo di aver visto come valore di ritorno della funzione VirtualAlloc del dropper.
Questo perché la procedura normale di caricamento di un eseguibile in memoria, ad opera del loader di Windows, prevede che molti riferimenti vengano costruiti al momento, ma nel caso in esame il caricamento è avvenuto in maniera inusuale.
Non è possibile qui descrivere le logiche e il processo da applicare, ma in sostanza si tratta di modificare l’eseguibile al fine di rendere valide le tabelle di import delle funzioni esterne, così che possa funzionare. In questo modo abbiamo a disposizione il codice del malware così come sarà effettivamente eseguito.
Per ora non vogliamo ancora lanciarlo, cosa che probabilmente avrebbe effetti spiacevoli, ma possiamo darlo in pasto ai nostri disassemblatori che adesso potranno lavorare su un programma ben formato e fornirci la visione della logica del malware.
Nota doverosa: chi scrive malware, di solito, ha delle competenze tecniche profonde, sia sotto l’aspetto della programmazione che della conoscenza del sistema operativo per cui sviluppa. Nell’eterna lotta tra guardie e ladri la fase di decifratura/decompressione inizia praticamente sempre con un insieme di controlli da parte del dropper che non ci sia nessuno che stia tentando un’analisi.
Solitamente il malware verifica di non essere in esecuzione dentro a un debugger (c’è un’API apposita, IsDebuggerPresent), che non ci siano in esecuzione processi particolari usati in analisi, cerca di capire se si tratta di una macchina virtuale e di che tipo, e molto altro.
Con più o meno fatica è possibile procedere oltre questi controlli, ma ricordiamo sempre che stiamo facendo un lavoro complesso che ha nelle tempistiche una discriminante importante: mentre noi studiamo il malware è in rete a far danni.
Quando vengono pubblicate le analisi dei malware spesso si assiste ad una evoluzione nella sua versione successiva che aggiunge controlli anti analisi sempre migliori.
E adesso?
Abbiamo disassemblato il codice del malware, anche se in assembler e non nel linguaggio originale (C, C++…). Da questa analisi possiamo ricavare importanti informazioni:
- capire le condizioni di attivazione del malware;
- capire le somiglianze con altri malware, così da individuarne parentele, provenienza geografica o gruppo di origine;
- capire se la chiave di cifratura (nell’esempio abbiamo pensato di analizzare un cryptovirus) venga in qualche modo registrata localmente e sia quindi recuperabile;
- scrivere, con i nostri strumenti di sviluppo, dei decifratori o dei brute forcers;
- leggere le dediche lasciate nel codice ad alcuni analisti;
- evidenziare l’uso di specifiche falle (vulnerabilities) nei sistemi operativi, così da poter realizzare le patch necessarie;
- scrivere gli IOC già citati, ossia gli indicatori di compromissione. Si tratta di tutte quelle specificità di un malware, dalle già citate tracce (sequenze di bytes, hash vari), a particolari pattern di attività o traffico di rete o di uso di API di sistema. Gli IOC servono agli analisti per scambiarsi informazioni e per preparare gli aggiornamenti che regolarmente otteniamo dagli antivirus;
- recuperare i file di configurazione: essenziali in quanto contengono spesso i riferimenti ai servizi esterni fruiti dal virus, come gli IP o i nomi dei server di C&C (Command and Control), ovvero quei server dove i cattivi raccolgono le informazioni esfiltrate dal malware e da dove questo riceve i comandi sulle azioni da compiere, in caso ad esempio di botnet, o le chiavi di cifratura dei file;
- eventuali bug: ovviamente ce ne sono, come nei software “normali”. Alcuni bug possono essere sfruttati per contrastare il malware o per consentire di decifrare files che sarebbero altrimenti irrecuperabili
Una volta “aperto” il malware e visto come funziona e recuperate le informazioni indicate in precedenza, l’analisi è pressoché completa.
Conclusione
Pur includendo aspetti tecnici, la trattazione dell’argomento è stata fatta ad alto livello e non poteva essere altrimenti. Ho proceduto per sommi capi e semplificando molto le attività, oltre a non citarne completamente altre. La sola estrazione del payload cifrato può a volte richiedere giorni di lavoro e spesso si deve ricorrere ad altre tipologie di analisi rispetto a quella indicata.
Pur assumendo queste premesse, è affascinante capire in profondità come le minacce si evolvano cercando strade sempre più complesse o anche solo originali: mi auguro di aver fornito spunti ad ognuno per provare ad approfondire i mille aspetti dell’attività di analisi che sta dietro al malware.