Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Software vulnerabilities
Secure programming
Static Analysis
Fuzzing
Ed in più, rispetto all’anno scorso, visto che il corso è aumentato a 6 CFU vediamo anche:
Ci sono un po’ di prerequisiti per il corso e il prof cercherà di fare qualche ripetizione ma se riusciamo ad
avere alcune nozioni su Linux, Intel x86, programmazione web, etc sono tutte super utili.
Di seguito c’è un ipotetico calendario di lezioni con un approccio Bottom-Up, partiremo dalle vulnerabilità
più importanti come il XSS e il buffer overflow fino a salire. Le lezioni segnate come LAB saranno dove
consolideremo attraverso il pc le informazioni acquisite.
I libri consigliati sono diversi e sono mostrati sulle slide, il primo è molto pratico e prenderemo alcuni
esercizi di laboratorio da qui. Il secondo è vecchiotto ma copre bene le basi di Malware analysis.
Il ricevimento è su appuntamento il giovedì mattina alle 11, online su Microsoft teams e c’è il link per il
calendario.
Nella software security quelli di nostro interesse saranno gli assets intangibili; quindi, dati sensibili o le
proprietà intellettuali ad esempio. Gli assets sono acceduti tramite software e rendere sicuro quest’ultimo
non fa altro che influenzare il primo, addirittura in alcuni casi addirittura il nostro software potrebbe essere
un assets.
Ripetiamo ovviamente la trinità CIA (Confidenciality, Integrity e Availability), ja wajù non fatemeli sbobinare
di nuovo so almeno quattro esami che li vediamo.
Ad un threat associamo un pericolo, una potenziale azione negativa, o evento causato da una vulnerabilità
che comporta un impatto non voluto su un elaboratore o un’applicazione.
Ma andiamo a vedere nello specifico i threat associati alla software security, visto che quelli sopra sono
quelli più generali. Quelli mostrati nell’immagine successiva provengono dall’ENISA che è una ente dal
quale possiamo approfondire alcuni dettagli.
L’ENISA oltre ad elencare threat elenca anche i così detti Threat actor ovvero coloro i quali attuano
effettivamente queste minacce e sono:
State-sponsored actors, sono i gruppi di attaccanti malevoli sponsorizzati dallo stato. Non si
intende qualcuno pagato in maniera pubblica gli stessi, ma sovvenzionati in maniera nascosta dagli
stati per evitare problemi di relazioni internazionale. Sono ovviamente super pagati e strutturati in
maniera sofisticata. I loro tipi di attacchi sono i più disparati.
Spesso fanno uso di APT (Advanced Persistent Threats) ovvero metodi di attacco furtivi e
sofisticati applicati contro bersagli di grosse dimensioni. Gli attaccanti, una volta entrati in un sito,
non buttano giù un sito o rubano le informazioni ma rimangono silenti e cercano di massimizzare i
danni prima di poter mostrarsi.
Cybercriminals, quest’ultimi sono motivati solamente dal profitto e quindi il loro interesse
principale è il furto di dati legati alle carte di credito, personali, etc. Le tecniche più usate sono
ovviamente l’attacco attraverso ransomware e criptominer.
Hacktivist, persone mobilitate da ideologie politiche, ambientali, religiosi etc. Tendono ad attaccare
attraverso DDoS o doxing per attirare l’attenzione.
Hacker-for-hire, di solito persone singole che lavorano su commissione creando exploit o malware.
Uno famoso è un certo Gookee, si crede russo, che creava exploit per i bancomat.
Il punto in comune tra tutti i threat actors visti sono lo sfruttare le vulnerabilità software.
Vulnerabilità Software
Esistono diverse definizioni, in base ai vari standard e documenti, ma per noi va bene usare quello di ENISA
che li definisce come: “L’esistenza di una debolezza, problema di design o implementazioni errate che
possono portare ad un evento indesiderato ed inatteso che compromette la sicurezza del computer, rete,
applicazione o protocollo”.
Le debolezze devono essere accessibili ed attuabili dagli attaccanti, altrimenti non sono debolezze.
Chiaramente il corso consisterà nel cercare di prevenire o mitigare, il più possibile, queste vulnerabilità
attraverso programmazione e sviluppo sicuro. Le categorie di vulnerabilità sono quattro, ma qualcuno
considera solo le prime due come vulnerabilità software (Design Flaws & bugs)
I difetti di design possono essere la mancanza di access control quali la mancanza di log in o
protezione dei canali di comunicazione.
I bug, che vedremo inizialmente, sono il buffer overflow o l’uso sbagliato di API etc.
La misconfiguration è dovuta ad un uso di configurazioni standard facilmente attaccabili oppure
l’esposizione, non necessaria, di dati o superfici di attacco.
Gli utenti che non posseggono buone pratiche di cybersecurity, quali password management o
l’usare i propri dispositivi in ambienti di lavoro.
Un’altra lista pubblicata da OWASP mostra i TOP 10 web application security risk, noi ci concentreremo
però sulle prime tre:
Broken access control, capita quando la gestione delle sessioni sono implementate in maniera
errata. Ad esempio, usare API senza autenticazione o non proteggere i token d’accesso
Cryptographic failures, i dati sensibili o segreti non sono sufficientemente protetti. Questo capita
perché le password non sono conservate in maniera hashata ma in chiaro, oppure perché le
comunicazione non sono protette, etc.
Injection, quando capita di inserire dei dati in un sito attraverso forms o altro e se quest’ultimi non
vengono sanitificati è possibile che il nostro server esegui il codice inviato attraverso delle form.
Formalmente viene definito come il percorso o il mezzo attraverso il quale un attaccante può ottenere
accesso ad un computer e consegnare un payload (un qualcosa che contiene il software malevolo) o
ottenere un risultato malevolo.
Per exploit intendiamo un insieme di comandi, che sfrutta l’attack vector, attraverso il quale si ottengono
dei vantaggi attraverso le vulnerabilità.
Vediamo un esempio:
Ma una vulnerabilità che aspettativa di vita possiede? Vediamo insieme al grafico l’evoluzione temporale
della stessa. Il software viene rilasciato con all’interno un qualche tipo di difetto, ad un certo punto tale
difetto viene scoperto e viene inventato un exploit per attaccarlo (“in the wild” indica che siamo in una
situazione senza regolamentazione), la vulnerabilità viene scoperta dal produttore e ad un certo punto
verrà scoperta anche dal pubblico.
Il periodo di tempo in cui gli utenti non sono consapevoli della vulnerabilità ma qualcuno la sfrutta, si
chiama zero day.
Quando la vulnerabilità è nota qualcuno ci applica una pezza attraverso gli IDS, finalmente il creatore
rilascia una patch di correzione per eliminare la vulnerabilità ed infine la patch dovrebbe essere installata
dagli utenti.
L’arco chiamato follow on attack consiste in quel periodo nel quale, nonostante la vulnerabilità sia nota
essa può comunque essere sfruttata. La windows of exposure indica tutto l’arco temporale, dalla
introduzione fino alla sua rimozione.
I team di analisi hanno il compito di scoprire le vulnerabilità software ed una volta scoperte devono essere
pubblicate attraverso dei bugiardini di sicurezza, ed archiviate in database online. Per evitare uno sforzo
inutile e di pubblicare duplicati, il MITRE ha introdotto lo schema di common vulnerabilities and exposures
(CVE). Non è l’unico ma è quello più usato, esso fornisce enumerazione e catalogazione.
Per chi vuole approfondire c’è il sito CVEdetails.com, nel dettaglio abbiamo che il CVE è strutturato come:
Lo score è di 7.8.
L’attack vector è locale o network? Uno potrebbe pensare: “va beh io lo mando online e quindi è network”,
si è vero ma per abilitarlo ci deve essere l’utente che apre il file e quindi è locale. La complessità
dell’attacco? Bassa. Privilegi richiesti? Nessuno, chiunque può aprire. User interaction? Richiesta, è l’utente
che deve aprire.
Le metriche temporali tengono conto del tempo per cui la vulnerabilità è conosciuta, più tempo
rimane in giro e più è conosciuta
Le metriche ambientali che dipendono in quale ambiente si trova la vulnerabilità, se nella mia
azienda ci sono degli assets di un certo tipo sono più o meno vulnerabile a tale attacco
Il CWE è strutturato come un’enciclopedia con gli oggetti ordinati per diversi elementi, la vulnerabilità
generica è conosciuta come classe. In alcuni casi abbiamo gli oggetti base che sono più specifici e spiegano
come rilevare/prevenire tale vulnerabilità. Variant sono invece super specifici, magari per un determinato
linguaggio. Infine, abbiamo le categorie che mostrano, ad esempio, un gruppo di oggetti con un attributo in
comune.
1) Responsible disclosure, qui l’analista che trova la vulnerabilità prima comunica con chi vende e gli
fornisce un periodo di tempo per sistemare il problema. Scaduto il tempo fornito si informa il
pubblico di tale vulnerabilità, durante questo periodo gli utenti sono molto penalizzati.
2) Full disclosure, l’analista subito la pubblica e delle volte anche con un bel exploit disponibile in
modo da forzare una riparazione il più velocemente possibile.
I CSIRT (computer security incident response teams) sono i team di cybersecurity e si occupano di tutta una
serie di operazioni quali:
Vulnerability management
Cosa devono fare le aziende per gestire tali vulnerabilità? Aggiornare senza pietà, non ci protegge dagli zero
day ma almeno ci protegge da quelli noti. Sulle slide ci sono piccole analisi per le cose specifiche.
Il buffer overflow è uno dei primi tipi di attacchi della storia dell’informatica, addirittura con un tipo di
Worm (il quale si replica da una macchina all’altra) che sfrutta vulnerabilità di tipo buffer overflow. Ancora
oggi ci sono un sacco di buffer overflow, addirittura SUDO di Linux può essere soggetta ad attacco
permettendo di vedere la password durante la dicitura.
I linguaggi di programmazione di sistema sono intrinsecamente soggetti al buffer overflow mentre quelli di
più alto livello prevengono o fanno auto resize del buffer.
Per vedere in che modo funziona il buffer overflow ripetiamo prima alcuni concetti di base C/C++, nello
specifico come funzionano le stringhe:
Vediamo un buffer di 6 caratteri dei quali 5 sono il carattere “Hello” e poi c’è il terminatore che possiede un
byte pari a zero. Il NUL character è diverso da NULL ma per sicurezza lo chiameremo NIL per evitare
confusione con il valore NULL.
Allocare un array
Per allocare un buffer in C scriviamo la classica notazione per array e nell’esempio abbiamo 10 caratteri di
cui 9 al massimo sono disponibili e uno per terminatore. Il linguaggio C prevede che dobbiamo creare
sempre un array di dimensione almeno pari a tale dimensione, ma lo standard non dice nulla se ne
allochiamo uno di dimensione minore.
Vediamo adesso un esempio di funzione che legge da tastiera una stringa gets( ), e di come a livello di
codice esista una vulnerabilità di tipo buffer overflow.
Quindi se scriviamo qualcosa con meno di 9 caratteri tutto va per il meglio, se ne usiamo di più il
programma ci restituisce segmentation fault e quindi il processo viene ucciso dal SO.
Questo capita perché lo standard C non definisce cosa accade in caso di errore, fissare un comportamento
richiede una VM come Java.
Purtroppo, per capire l’attacco dobbiamo rivedere nuovamente come funziona il processore, quello che
vedremo è basato su “Smashing the stack for fun”.
Area testo (in verde), corrisponde ad una copia del programma che lanciamo
Area dati globali, tutte le variabili dichiarate fuori dal main o dalle funzioni
o Global data inizializzati
o Global data non inizializzati
Area heap, usata quando usiamo funzioni come malloc()
Area stack
Parleremo poi da qui in avanti del processore Intel x86. Ricordiamo poi che ci sono tantissime variazioni,
come la programmazione multi thread che comporta la creazione di stack diversi.
Stackoverflow
Concentriamoci per ora sullo stack overflow, lo stack viene richiamato ogni volta che chiamiamo una
funzione e lo stack salva:
“f” è l’indirizzo del codice ed è un numero. Abbiamo in questa immagine quattro push, perché si conta
anche call che fa una push sullo stack.
Tipicamente lo stack cresce dal basso verso l’alto, SP (stack pointer) indica la cima dello stack e l’indirizzo
di ritorno viene messo sopra i tre parametri.
La funzione f() possiede due variabili locali, la dimensione totale del buffer richiesto sarà da 15 byte ed
infatti il codice assebly userà una subl con un valore pari alla somma delle due variabili locali.
Lo SP viene fatto salire sopra a tutto e vengono collocati i due buffer, ma viene aggiunto un altro valore ed è
il frame pointer (FP) ovvero un registro dove segniamo l’indirizzo delle variabili locali e viene salvato in
%ebp frame stackpointer (fotografia dello stack prima di mettere gli array sopra).
Ma cosa sta succedendo? Ora quando si ha un buffer overflow abbiamo che esso sfonda il buffer e quindi
partendo dall’alto scende fino alla base dello stack. Questo vuol dire che tutti i pezzi sotto vengono
sovrascritti ed il punto critico che stiamo modificando è il return value, che sarà intenzione dell’attaccante
il cambiarlo. Il nuovo valore di ritorno sarà del codice specifico scelto dall’attaccante.
La cosa più importante da capire oggi è: come calcolare l’indirizzo da mettere nel return pointer.
Domanda, se io ho caricato i dati nel buffer perché dovrei avere tutti questi problemi a farli eseguire?
Semplicemente perché i dati nei buffer vengono solo letti e non eseguiti e quindi si trovano nella parte
verde del primo grafico della struttura in memoria.
L’arte di creare un messaggio di buffer overflow è sartoriale per cui un pezzo di codice finisce nel mezzo
dello stack, alla fine invece abbiamo un indirizzo e lo NOP Sled (che è un riempitivo)
Vedremo nelle prossime settimane gli altri tipi di buffer overflow ma incominciamo a vedere un’immagine
dei vari tipi:
Prevenzione buffer overflow
Ora la domanda è più che lecita, come si provengono i buffer overflow?
La cosa più naturale e semplice è quella di scartare tutte le funzioni che non sono sicure by-design, quindi:
In questo caso quando c’è un overflow non ci dice che è minore di zero ma ci restituisce il secondo ramo e
quindi il numero di caratteri che avrebbe scritto. Allora qual è l’unico modo corretto? Questo seguente
dove usiamo snprintf () con ulteriori parametri ovvero la lunghezza della sorgente % .∗s.
La lunghezza serve ad evitare, come succede nella snprintf () normale, che un attaccante inserisca un
messaggio senza terminatore.
Esistono poi librerie esterne che sistemano alcune problematiche, ma non sono interoperabili o non sono
già installate, etc.
Vi è poi un altro elenco di difese e attacchi, ma spesso sembra giocare al gatto e al topo
L’esempio riguarda un programma vulnerabile che si mette in ascolto su un porto di rete, il programma è di
tipo “echo” e per fare ciò usa una funzione chiamata copier che ha un buffer di 1024 byte e fa operazioni
non sicure.
La stringa può essere scritta su 4000 byte ma può essere copiata su una di mille; quindi, ci sono tutti gli
ingredienti per un disastro.
Gli screen successivi saranno quelli della macchina del prof. Nello specifico lancia un eseguibile $ ./vuln server
che si mette ad ascoltare una socket, apriamo un’altra shell ed utilizziamo l’applicativo netcat
nc localhost 4001 e vediamo che si collega.
Le PIPE in Linux concatenano le informazioni creando un fork di processi. Prima di fare l’attacco dobbiamo
disattivare tutta una serie di difese:
il parametro -g è comodo per vedere il debugging in maniera più pulita, verrà così generato un file chiamato
“vuln_server”.
Domanda: Questo file generato usando i seguenti flag è sempre vulnerabile, oppure dobbiamo rilanciarlo
ogni volta? No, è ormai vulnerabile.
Scarichiamo ed usiamo poi altri due pacchetti mostrati di seguito
Se noi invece di mandare una stringa limitata a poi byte ne mandiamo una da 1200 (che è maggiore di 1024
lunghezza del buffer), che succede? Il server viene ucciso dal SO
E già così abbiamo realizzato un DoS, ma noi vogliamo fare di più e vogliamo iniettare del malicious code.
Usiamo adesso Pwndbg, e lo lanciamo usando $ gdb ./vuln server il programma però per eseguire ha bisogno
anche del comando $ run.
Reinseriamo il comando di python 3 della slide precedente ed otteniamo diverse cose, ovviamente il
processo è nuovamente ucciso ma gdb mi dà ulteriori informazioni. Vediamole tutte nelle successive
immagini:
Il programma si è interrotto nella funzione copier() alla riga 14, senza aprire il sorgente scendendo vediamo
il pezzo di codice dove si trovava il processore al momento del crash. Di seguito abbiamo anche l’immagine
dei registri RAX , RBX , RCX , RDX che sono i registri dati (non li vedremo tutti) mentre i più importanti
sono il RSP (stack pointer register) e RIP (instruction pointer register) che indica l’indirizzo del codice che
stiamo eseguendo.
Se facciamo un passo indietro abbiamo che dove punta il RIP, 0 x 5555 etc è l’area verde di testo mentre lo
stack si trova all’indirizzo molto distante 0 xFFFF .
Di seguito invece abbiamo lo stack inondato di A
Nel registro RBP troviamo invece dei valori 0 x 4141414141414141 che è la rappresentazione numerica
di “A”. Per avere più informazioni scriviamo disass più la funzione della quale vogliamo avere più
informazioni, nello specifico vediamo il codice assembly. La stringa sottolineata (con la freccia sulla sinistra)
dice dove ci trovavamo prima della morte.
Il punto chiave è ret il quale prende l’indirizzo di ritorno dallo stack e fa sì che il processore lo esegua,
normalmente su questo registro dovremmo scrivere qualcosa che è del tipo 0 x 5555 ovvero quello del
main; mentre adesso si trova il 0 x 4141414141414141 ed è questo il problema.
Le slide mostrano le stesse cose, forse c’è solo qualche valore diverso perché c’è qualche altra roba nei
registri.
Vediamo alcune immagini per capire meglio com’è realizzato a livello tecnico l’attacco
Quello che è successo è questo, l’immagine è un po’ una rappresentazione di tutta la mia vita, un urlo
continuo.
Se volessimo fare un attacco serio non inseriremmo delle semplice “A” ma inseriremo all’interno
dell’Instruction pointer l’indirizzo dell’inizio del buffer. La freccia nell’immagine successiva sarà più in basso
e nello specifico alla zona NOP, ma come calcoliamo e cosa ci mettiamo in questi registri?
Le sequenze possono sembrare casuali ma in realtà ogni 8 caratteri abbiamo una stringa definita in
maniera unica rispetto ad un qualsiasi altro ottetto. È matematicamente dimostrato, non è magia.
Quindi noi inseriamo questo ciclo e prendiamo la stringa che vediamo nel registro RSP. Giustamente non
cercheremo a mano dove si trova la stringa ma useremo il comando $ cyclic−n 8−l eaaaaaaf e
l’algoritmo ci restituirà un valore decimale, in questo caso pari a 1032, da sottrarre come offset.
Facciamo un po’ di conti, abbiamo i 1032 byte + 8 byte (contenenti l’indirizzo di ritorno). Dei 1032 byte
dobbiamo mettere:
Lo shellcode che non generemo noi, ne useremo uno già fatto e la sua dimensione è di importanza
fondamentale, perché se occupasse troppo potrebbe sovrascrivere le sue stesse aree dati.
Potremmo scriverlo ma non è necessario (in questo caso 57 byte).
Le NOP sled che vanno calcolate
NOPs di 64 byte (un valore arbitrario)
Come generiamo lo shellcode? Utilizziamo l’altro programma che abbiamo scaricato, sull’immagine c’è
tutto il processo di creazione
Sulla slide successiva c’è segnato di cambiare l’indirizzo di ritorno dello script, e lo troviamo nelle prossima
slide:
Il +128 è importante e lo spiegherà dopo.
Se tutto è andato bene, vedremo come dall’immagine che il server non è morto ma è uscito normalmente,
senza accorgersi di nulla, mostrando un “hello world!!”
Sulla documentazione troviamo numerosi shellcode library, nello specifico noi useremo quelle segnate nel
quadrato azzurro e faremo collegare il bersaglio (aprendo una shell) verso di noi.
Nell’esecuzione live apriamo un terzo terminale per avere tutto attivo, il 3° è il server, il 2° è chi ha lanciato
il messaggio, infine, il 1° mostra la connessione attiva attraverso una shell.
Se in 1° scriviamo $ ls otteniamo l’esecuzione di tale comando sul server e vediamo i file disponibili
Sulle slide finali ci sono altri esempi ed informazioni
Spieghiamo finalmente perché abbiamo aggiunto quel +128, se ripetessimo l’attacco senza gdb non
avremmo successo perché l’indirizzo visto vale solo se eseguito in gdb. L’aggiunta del valore +128 è fatta
appositamente per consentire l’attacco fuori da gdb e lo spazio aggiuntivo funziona proprio come un
cuscinetto.
Il programma che andremo ad attaccare è “wisdom-alt.c” ed è trovabile sul link di gitHub visto il primo
giorno. Questo programma funziona in maniera molto banale, con 1) inseriamo una perla di saggezza e con
2) ci mostra le frasi inserite fino a quel momento.
Lo scopo dell’esercizio è fare in modo che il programma esegua due funzioni: write_secret(),
pat_on_back(). Che sono nel programma ma non vengono mai chiamate, e il nostro buffer overflow
dovrebbe fare in modo che ritornino su queste.
Bonus dell’esercizio è iniettare dello shellcode nel programma. Nelle slide ci sono dei suggerimenti,
innanzitutto il nostro payload sarà diverso perché dobbiamo prima la stringa “2” e poi invio.
Attenzione l’eseguibile è a 32bit e quindi gli indirizzi sono di 4 byte invece che 8 come visti l’ultima volta.
Ci sono poi i suggerimenti per chiamare la funzione “write_secret” e il template per l’exploit. Bisogna
inserire l’indirizzo di write_secret() o dello shellcode infine bisogna mettere l’offset (l’altra volta era 1032
tipo).
In questo caso l’istruzione RET non legge il registro SP, ma quello IP perché il modo in cui cresce lo stack è
differente con questi processori.
Dopo ottenuto il valore classico su RIP calcoliamo l’offset (nel caso qui era 151) e lo inseriamo nello script
“gen_shellcode.py”
Dobbiamo inserire anche l’indirizzo della write_secret(), attenzione per ricavare il valore seguiamo le slide
perché abbiamo bisogno che il programma sia in run altrimenti il valore ritornato non è corretto.
Lezione 4 18/03 (3-Sanitizers)
L’argomento di oggi sarà una categoria di tool che prende il nome di sanitizer, vedremo anche un’ulteriore
panoramica degli attacchi a basso livello.
Il problema dei buffer overflow è subdolo, perché immaginiamo di avere un programma che sovrascrive un
buffer potremmo avere che lo stesso potrebbe continuare a funzionare tranquillamente nonostante ne
abbia subito un buffer overflow. Il fatto che un programma non venga ucciso da un SO può capitare per
diversi motivi:
Se vedessimo i vari CWE noteremmo che esistono numerosissimi tipi di buffer overflow. Osserviamo adesso
probabilmente il più recente e famoso:
Heartbleed
È un buffer overflow di tipo overread, e non essendo una sovrascrittura non avremo bisogno di uno
shellcode, ovvero leggiamo dati che non dovremmo leggere. Questo tipo di attacco riguarda la libreria
OpennSSL che viene struttata da server SSH.
La vulnerabilità consta del fatto che è possibile leggere altre informazioni oltre a quelle che richiede, e visto
che SSH contiene al proprio interno molte informazioni sensibili è chiaro che tale vulnerabilità è gravissima.
Ma capiamo come funziona; il server accetta le richieste di un client di heartbeat ovvero risponde alle
richiede di “sei vivo?”, il server risponde alla richiesta (stringa più la sua lunghezza) ma se l’attaccante
manda un messaggio di 3 lettere e ne richiede 500 avrà tranquillamente più lettere di quante ne dovrebbe
ricevere.
Nell’immagine allochiamo un puntatore a X con 100 di dimensione del buffer, ma se l’attaccante passa un
valore più grande avremo un buffer overflow solo che stando nell’area heap è chiamato heap buffer
overflow. Ma capiamo meglio con l’esempio in esecuzione
Perché in un caso crasha ed in uno no? Vediamolo nel debugger gdb.
Se stampiamo l’indice del ciclo notiamo che si è fermato a 134496 iterazioni e possiamo raccogliere altri
dati, come l’indirizzo del buffer e l’indirizzo di fine.
Notiamo però una cosa anomala, i tre “000” alla riga di gdb $2, e vediamola per bene con un disegno.
Ricordiamo che l’heap è un’area del processo preallocata dal SO e quando facciamo malloc () abbiamo
che viene tagliata una fettina di quest’area. Facendo il ciclo for sovrascriviamo i 100 elementi e tutto
quello che ci sta vicino, e si ferma all’indirizzo 0 x 55555557 a 000 . Come mai? Perché i SO usano per le
pagine quando creano l’area di memoria per un processo, non allocano i singoli bit, e le stesse sono
tipicamente 4 Kb (linee rette nella figura). Sono da vedere come se fossero delle mattonelle e noi
possiamo accedere solo alle zone verdi e non bianche.
Questa cosa però è controproducente perché i buffer overflow piccoli fanno sì che si rimanga all’interno
dell’area verde non si nota il problema.
Integer overflow
Non parliamo più di buffer ma in questo caso stiamo parlando di numeri, e sappiamo che i numeri sono su
un numero fisso di byte. Cosa succederebbe se facessimo arrivare una variabile al massimo rappresentabile
e poi usassimo le notazioni come “++”?
È un bel casino, la tabella alla destra, infatti, riassume un po’ uno studio in cui si mostrano le casistiche dei
comportamenti di C per determinati compilatori e processori.
Questa cosa è super pericolosa, perché se sbagliamo il calcolo di un intero e questo viene usato in un array
possiamo avere un buffer overflow. Vediamo un esempio ipotetico: Il programma prende un numero ed
una stringa e taglia quest’ultima in base al numero che le abbiamo dato e li mette in un buffer di
dimensione 10.
Questo programma è poi scritto in maniera tale che se passiamo il valore 65536, non a caso, abbiamo il
buffer overflow.
Perché quel valore non è casuale? Il programma passa quel valore e poi si ha la conversione, il programma
usa un ∫ ¿ con segno e quindi 4 byte; il programma ha poi un bug voluto dove n viene copiata in len che è
ti tipo unsigned short di 2 byte (su alcune architetture) tagliano il numero.
L’oggetto è stato deleted, ma il puntatore è stato cambiato? No, “p” contiene ancora l’indirizzo dell’oggetto
precedente. Questa mancanza di pulizia crea la vulnerabilità, i dati non vengono puliti per una questione di
velocità.
Questo metodo myMethod() esegue? Si, ma funziona con i dati sporchi di prima. L’attaccante potrebbe
quindi iniettare la stringa rossa, magari tramite buffer overflow, con “q” stessa dimensione di “p”.
Purtroppo, l’allocatore del SO riusa l’area creata precedentemente, se della stessa dimensione, e c’è quindi
una probabilità che “MaliciousClass()” sovrascriva “p”, è una specie di polimorfismo malefico per cui il
puntatore p viene risolto a tempo di compilazione.
Vediamo un esempio, spesso sono i browser ad essere soggetti a questi attacchi perché sono programmi
legati agli eventi.
In questo caso il browser Safari è vulnerabile ad un attacco di use-after-free che porta ad una possessione
del browser.
L’attaccante può scegliere l’ordine di esecuzione dei tag con il quale eseguire l’attacco, quando il browser
legge queste cose segnate in rosso abbiamo:
Ultima cosa è ovviamente usare l’oggetto malevolo, quindi l’attaccante potrebbe fare un “for” per allocare
oggetti malevoli
Tale operazione di allocare tutti questi oggetti nell’heap si chiama heap spraying.
Information leakage
In C/C++ le variabili non sono inizializzate in maniera automatica, se un programma non le assegna sono
undefined e prendono i valori che si trovano sullo stack, nell’esempio successivo avremo che
stamperemmo spazzatura.
Vediamo un altro caso, una chiamata di sistema che prende in ingresso “skb” che è un buffer per i pacchetti
di rete ed un parametro d’uscita. Ci troviamo nel kernel, e lo stack del kernel è separato da quello dei
programmi normali, e sono state già fatte delle chiamate di sistema; quando scende il puntatore abbiamo
che i dati non vengono cancellati e quindi la memoria risulta sporca. La funzione crea un oggetto map di 32
byte e dentro di essi vi sono dei valori, la loro somma è 28 byte e non 32; il compilatore arrotonderà il
valore di 28 a 32 e questo per motivi di architettura ed efficienza.
La map avrà quindi 28 byte inizializzati e 4 byte che sono LEAKED cioè rubabili. Ma quindi che ci può fare un
attaccante? Potrebbe rubare gli indirizzi dello stack e usarlo poi per un altro attacco come il buffer
overflow.
Ma allora come si risolve questa situazione? Semplice avremmo dovuto prima scrivere tutti “0” in tutti e
32 byte e poi dopo assegnare i valori.
Dirty pipe
Fa i buchi in terra (cit.), vi è un problema con il kernel di Linux < 5.8 e con una chiamata a sistema abbiamo
la privilage escalation.
Tutte queste cose che abbiamo visto sono chiamate undefined behaviour perché dipendono dal
processore, compilatore etc.
Per poter rilevare tutti questi problemi si fa uso quindi dei tool chiamati Sanitizers
Sanitize
Sono un gruppo di dynamic bug finding tool che rilevano bug in C/C++ nascosti. Questi programmi avanzati
lavorano iniettando delle righe aggiuntive (instrumentation, nell’esempio sono quelle segnate in rosso) nel
programma in zone specifiche quali:
Oltre alla coverage possiamo controllare tutti i buffer mettendo un controllo su ogni indice di array e
controllano sei i confini sono rispettati, oppure controllare i valori non definiti, etc.
Il suo uso è molto semplice, immaginiamo un programma Linux come “date” che stampa l’ora del sistema.
Per analizzare il comando lanciamo Valgrind usiamo il nome, e le opzioni per l’analisi; ci fornisce la stampa
del programma + info derivanti dall’analisi di Valgrind. Fa quello che abbiamo chiamato prima
instrumentation.
Il tool funziona anche “black box” non richiede di ricompilare il programma, funziona anche con il binario
quindi è super potente.
Quando riportato nella frase di prima è riassumibile dicendo che Valgrind usa Dynamic Binary
Instrumentation
Con tante virgolette è come se fosse una macchina virtuale di C/C++, abbiamo infatti che:
Abbiamo nuovamente un malloc (100) e proviamo a scrivere all’indirizzo 200. L’istrumentation crea una
struttura dati allocations e ogni volta che c’è un malloc () ci segniamo l’indirizzo dell’area e la dimensione.
Poi ogni volta che devo leggere il puntatore iniettiamo una if
Visto che si occupa di lettura binaria lui lavora sul codice binario, quindi MOVE etc. Il prof mostra il codice
C per comodità.
Memcheck
È un tool che si occupa di rilevare errori di memoria, può rilevare i tipici problemi di programmazione in
C/C++. I lavori che fa sono diversi e sono raccolti nella slide
Vediamo un esempio di utilizzo, basandoci sempre sul puntatore di dimensione di 100 come prima.
Ma come funziona questa magia? Memcheck implementa una “CPU virtuale” nella quale abbiamo:
Ogni bit in memoria/registri ha associato un “valore valido” (V) bit. V = 1 se il bit contiene un valore
inizializzato
Ogni bit in memoria ha associato “un indirizzo valido” (A) bit. A = 1 se il programma può in maniera
legittima leggere o scrivere in quella locazione di memoria
Il processore virtuale ha i suoi R registri e il fatto di avere tutte queste repliche di byte comporta il
rallentamento e l’uso intensivo di risorse visto prima.
Ma come si sfruttano questi bit? Come sappiamo non possiamo usare la memoria se non è stata già
allocata, sfruttiamo malloc() che alloca spazio e imposta il bit A ad 1.
Con l’animazione possiamo vedere che quando lanciamo un programma l’area codice mette tutti i bit di
quell’area alti. Poi abbiamo l’area heap che di default all’inizio è tutto a zero.
Supponiamo poi di fare una malloc() e otteniamo una fettina di heap che diventa con un indirizzo valido ma
con un valore non valido.
Se facciamo memset ( p , 0 , N) scrivendo tutti zero nel buffer, anche i bit V diventano verdi. Stesso identico
discorso per lo stack che appena chiamato è con tutti i bit rossi e quindi non disponibili, se però chiamiamo
una funzione gli indirizzi diventano validi e saliamo con lo stack.
Quando poi inizializziamo il valore “a” abbiamo che i valori validi crescono
Un'altra cosa molto potente di Valgrind è che quando copiamo una variabile in un’altra variabile, anche i bit
vengono propagati alle copie. Vediamo l’esempio in cui poniamo b = p[0]
Il programma copierà il valore di “p” in un registro, ad esempio R2 e poi lui ricopierà il valore sullo stack.
Vi è scritto depends perché non è detto che questo errore si veda subito ma solo se le variabili generano un
effetto visibile.
Vediamo un esempio concreto, il nostro programmino in C++ che facciamo a fondamenti e che adesso ci
vergogniamo di vedere.
Tutti questi esempi e chiacchiere per spiegare bene come Valgrind mostra i messaggi di errore, nel caso di
valore non inizializzato otteniamo un report del genere.
Cosa ci dice? Che quando facciamo la stampa di “x”, abbiamo che dentro printf() c’è l’istruzione di selezione
inserita in automatico che porta a quest’errore. Ovviamente l’errore è che tale valore non è inizializzato.
La stampa è simile al “call stack” di gdb e vediamo che la chiamata parte dal main, che chiama printf.c che
chiama _itoa.h.
Un altro esempio è qui dove c’è il write con una variabile non inizializzata.
SGCheck
Il tool di prima funziona solo per lo stack overflow, per l’heap dovremo usare lui. Quindi i due sono
complementari.
Infatti, vediamo un problema di stack array con sempre la traduzione in linguaggio macchina con la singola
istruzione “Sub” ma, purtroppo, non riconosce le due arre separatamente quindi cosa dobbiamo fare?
SGCheck usa un approccio euristico, se un’istruzione legge/scrive dentro un array globale o sullo stack
anche solo una volta, allora dovrebbe accedere sempre allo stesso.
Alle 01:31:30. Fa vedere l’esempio di utilizzo di Valgrind tramite l’esempio del Titanic. Se volete provare a
fare gli stessi esempi la sua VM possiede già i tool.
I vari Sanitizer
Scriviamo “vari sanitizer” perché sono appunto un gruppo di tool con il nome Sanitizer alla fine, e si
occupano di lavorare su codice sorgente quindi quando compiliamo e sono diversi da Valgrind. Sono super
facili sa usare, basta usare l’opzione “-fsanitize = address”; esistono altri tipi di opzioni attivabili ma soltanto
uno alla volta.
Ricompila il programma facendo sempre instrumentation però è più veloce proprio perché è statico
Vediamo un esempio di funzionamento:
Il risultato è molto simile a Valgrid. Ma come fa ad essere così veloce? Il tool è meno preciso perché lavora
per word (quindi gruppo di 8 byte) rispetto al singolo come capitava prima. Crea quello che in gergo è
chiamato shadow byte, ovviamente come detto sarà meno preciso nel rilevare un problema.
Vediamo cosa succede in memoria, supponiamo un heap overflow abbiamo che l’address sanitizer mette
vicino ad ogni buffer gli shadow byte diversi da 0. Così se il programma scrive fuori dalla zona rossa del
buffer abbiamo un avviso.
Quando effettuiamo free(), ASanitizer mette il buffer in quarantena, per rilevare use-after-free.
Nelle restanti slide ci sono le parti di codice che vengono iniettate e altri differenti tipi di tool sempre del
Sanitizer.
Gli attacchi che vedremo prescindono da http/HTTPS, sappiamo che l’ultimo protocollo è http che usa la
cifratura su TLS/SSL, poiché si concentrano sulle web application e quindi “soprassediamo” al tipo di
protocollo usato.
Sappiamo anche che internet si basa sul protocollo client-server schematizzato come segue:
Il web server quando genera una risposta può recuperarla da un disco oppure eseguendo una web
application, ma il risultato finale è lo stesso.
In altri casi più sofisticati, come il dynamic content, il link non è solo un file da leggere ma è un percorso ad
un programma da eseguire.
In generale l’URL è un oggetto molto complesso, di cui noi usiamo molto poco ed il minimo che possiamo
avere è “http: + percorso”. Infatti, se lanciato così ci cerca nel local host.
Tutto quello dopo i “:” è opzionale e abbiamo, come mostrato dall’immagine, un sacco di diverse opzioni.
Ma concentriamoci sulla parte dopo il dominio, quindi da “path” in poi poiché prima ha più a che fare con i
protocolli di rete. Dopo il “?” vi è la possibilità di trovare una query che può essere usata per inviare
informazioni al server, ed è un dizionario con una rappresentazione chiave-valore nella forma:
Problema: Cosa succede se noi in questi parametri ci aggiungiamo valori come “?, #, &”? Visto che sono
caratteri speciali? La regola semplice richiede di usare i valori esadecimali (dell’ASCII) preceduti da un %.
Ricapitolando, se pure scrivessimo come la parte sottolineata quello che manderebbe il browser è quello
scritto con i valori esadecimali
http Protocol
I messaggi http di tipo request and response sono composti da tre parti:
1. Request/response line
2. Header fields
3. Body (optional)
Ogni linea dei messaggi viene terminata con i caratteri ¿ ¿ o da 0 x 0 d 0 a in binario. Una linea vuota separa
l’ultimo campo header dal body.
Come abbiamo detto la prima riga è la richiesta e vi è il percorso con il numero di protocollo, infine ci sono i
caratteri terminatori e a capo di tutto l’azione da effettuare. In questo caso GET.
Gli header sono stringhe di testo con vari campi e l’ultima parte è il body che tipicamente è vuoto, se
facciamo un upload.
Concentriamoci sulla prima riga, il così detto metodo
Si usa la GET nei casi si voglia recuperare una risorsa per la lettura solo per convenzione, ma non è scritto
nelle regole di funzionamento del protocollo. Vediamo un esempio:
Supponiamo di aver aperto una pagina contenente un tag “img”, il browser quando legge il tag fa una
richiesta GET per ottenere l’immagine, oppure c’è un link, se ci clicco sopra il browser fa una GET.
POST, abbiamo una particolarità ovvero mandiamo i parametri separati dal body ed è quindi da
usare quando ci sono dei dati sensibili. Quando proviamo ad aggiornare una pagina che usa POST ci
sarà sempre un avviso di tentativo di riconsegna modulo.
Vista la caratteristica di POST di gestire contenuti sensibili sarà il metodo usato per quando si dovranno
gestire degli ipotetici effetti collaterali.
Vediamo come al solito un esempio, il tag <form> prevede a sua volta un link che il browser visiterà dopo
aver premuto “submit” e lo visiterà con il metodo POST.
Quindi ricapitoliamo il passaggio dei parametri, nel caso della GET, fanno parte del path e sono conservati
nella cronologia del browser e spesso scritti all’interno dei log file.
Ed è per questo motivo che non si usano le GET per mandare dati sensibili, visto che verrebbero salvati in
chiaro sia nelle richieste sia nel log.
Lo stesso caso, ma visto con il metodo POST, è come detto un passaggio tramite body e non header; in più
l’azione non dovrebbe essere ripetibile, infatti, verremo avvisati con un warning.
Quando mandiamo qualcosa in un body dobbiamo specificare cosa stiamo mandando, altrimenti chi legge
non sa interpretare cosa sia. Per fare ciò dobbiamo usare delle parole chiave come “content-type/content-
lenght”.
Gli headers sono come delle variabili non nel senso che ci inventiamo gli header ma sono variabili del
protocollo http e seguono delle regole:
Ce ne sono innumerevoli, uno che conosciamo è l’user agent che informa il server del tipo del browser che
stiamo usando.
Il tag refered non viene incluso sempre, ma quando clicchiamo su un sito e ce ne porta su un altro
abbiamo che il secondo viene informato del sito dal quale proveniamo. Questo tag pone dei problemi di
privacy.
Http RESPONSE
È simile alla request, l’unica differenza sta nella prima linea la “status line” che informa del risultato della
richiesta
Javascript
Javascript è un linguaggio di scripting lato client, questo vuol dire che quando riceviamo una pagina essa
non conterrà solo tag di immagini e testo ma anche dei piccoli programmi.
L’output sarà mostrato all’interno del browser, ma solo il risultato non il codice. Javascript può in realtà fare
moltissime cose:
Lato server invece è più probabile usare Java o Python ma noi, nei nostri esempi, useremo PHP il qual
codice verrà processato da un interprete PHP. Il risultato del codice PHP è tipicamente un testo HTML.
Session management in web applications & Cookies
Sappiamo che http è un protocollo stateless e ogni richiesta che facciamo è indipendente l’una all’altra, le
Web applications creano una sessione per ogni utente identificandolo con un token per poter soprassedere
a tale problema.
La web application conserva il token e lo manda al client attraverso un header http (cookie), il browser
conserva il token e automaticamente lo rinvia al server ad ogni richiesta http.
La sintassi del cookie è quella mostrata di seguito, ed è un header quindi si trova dopo la riga di request e
ha sempre la coppia variabile-qualcosa.
Il fatto che i cookie servano per l’autenticazione li rende automaticamente super importanti; infatti, se
qualcuno li rubasse potrebbe tranquillamente impersonarci.
Questo perché abbiamo detto che il server controlla il cookie per mantenere lo stato delle richieste, se
rileva che non è cambiato pensa sia sempre lo stesso utente.
Per configurare quanto detto, di solito in linguaggi come Java, c’è il file web.xml che permette si configurare
tutti questi parametri.
Sessione demo di furto di cookie
Spiegazione a 48:07 come al solito si trova tutto sul github.
Ad esempio, una web page (non Facebook) che include un link Facebook. Quando l’utente clicca sul link una
richiesta HTTP viene mandata a Facebook.
Questa cosa può creare non pochi problemi proprio perché legato al passaggio dei cookie. Vediamo un
esempio con due siti A e B e siamo anche autenticati su entrambi quindi abbiamo due cookie, nel caso (1) e
(2) ognuno ha i suoi cookie ma nel caso (3) i cookie che verranno mandati sono quelli di B.
Ma perché si crea il problema? Beh, supponiamo che un sito web sia stato creato da un utente malevolo e
supponiamo anche che l’attaccante abbia fatto del social engineering invogliando l’utente ad accedere sul
suo sito.
La richiesta parte in automatico perché legge il tag e lo visita. È possibile fare una demo di attacco CSRF
c’è sempre il link su GitHub visione a 1:16:00.
Contromisure Cross Site
Il problema fondamentale è che il server non conosce la distinzione tra same-site request e cross-site
request, ma quando il browser fa una richiesta cross site lo sa che lo sta facendo? Si, la nota perché sa cosa
abbiamo cliccato. Ci sono differenti soluzioni, dunque, per risolvere tale problema:
Come contromisura di referer header abbiamo ormai molti browser che non lo inseriscono più per motivi
di privacy e quindi non viene riportato. La soluzione usata e migliore è quella che usa degli speciali tipi di
cookie con l’attributo “SameSite”, dove all’atto della creazione di un cookie inseriremo nella variabile di
configurazione un impedimento di condivisione cookie, impedendo così l’attacco.
Oggi la contromisure più comune è il secret token, questo valore è segreto (per l’attaccante) e permette al
server di capire se la richiesta è cross site o meno.
Vediamo lo scenario, come quello di prima, ma con l’aggiunta di un token. La prima volta che un utente
visita il sito B ottiene la pagina e da qualche parte vi è il codice che contiene il token. Quando l’utente clicca
sul link, partendo da una pagina di B, il link include il token. Se invece Alice visita un’altra tab malevola e
clicca sul link quest’ultimo token non ci sarà.
Così facendo il server sarà in grado di riconoscere se vi è una richiesta cross site o no. Domanda: Visto che il
token viene mostrato nella pagina HTML, è dinamico? Sì, perché è in chiaro e quindi a meno che
l’attaccante non abbia un virus non se ne fa nulla.
Lezione 6 25/03
Cross site scripting attacks (XSS)
Completiamo prima la parte finale della presentazione scorsa, quella del client side attack. Poi passiamo al
prossimo blocco di slide.
In qualche modo il browser deve limitare la capacità di Javascript di eseguire arbitrariamente quello che
vuole; infatti, uno script di un sito come “attacker.com” non dovrebbe essere capace di alterare il layout di
una pagina di una banca, etc. Questo è il caso di malware di tipo advertising (pubblicità) che può spingere a
frodi informatiche.
Contro tale problematica abbiamo il Same Origin Policy, ovvero un meccanismo che ci garantisce che il
Javascript proveniente da un sito non possa modificare o leggere risorse di altri siti.
Ad esempio, il Javascript della pagina di reppublica.it non potrà leggere i cookie messi a disposizione dal
corriere. Vediamo l’esempio dei cookie schematizzandolo
Se repubblica ci manda un file good.js contenente codice Javascript esso potrà leggere, tramite classi, i
cookie ed il browser lo consente perché proveniente dalla stessa origine. Ora se aprissi un'altra tab
chiamata evil.com e ricevessi un suo .js esso non potrà leggere i cookie di website.com perché l’origine è
differente.
Il XSS ha lo scopo di sovvertire la SOP, e l’attaccante non invia il suo Javascript attraverso del social
engineering ma fa dei giri particolari per evitare le regole della SOP. Facendo così il browser della vittima si
fidi del codice malevolo inviatogli dal server e lo attui.
Web defacing, codice Javascript può usare API DOM per effettuare cambiamenti arbitrari alla
pagina, ad esempio cambiare le immagini o parti di testo
Spoofing requests
Stealing information
Esistono vari tipi di XSS e i due principali sono:
È per questo motivo che SOP non funziona, proprio perché il codice arriva dal sito originale.
La chiave del reflected XSS è quella di trovare una web application che effettua l’eco degli input dell’utente
in una risposta html, ovvero avere un “reflected behaviour”.
Per capire l’effetto immaginiamo di cercare su un e-commerce la parola “calzini” e nella pagina abbiamo
“hai cercato: calzini”. Noi al posto di “calzini” mettiamo del codice javascript e portiamo avanti l’attacco.
Invece che la risposta come “calzini” possiamo mettere del codice Javascript che farà lo stesso percorso di
prima e la risposta avrà il codice dell’attaccante.
Persistente (Stored)
Nel persistente l’attaccante invia direttamente del codice malevolo al sito, come ad esempio la bacheca
vista nell’esercitazione di ieri e qualcuno nella propria biografia immette codice Javascript che andrò poi
salvato su un DB. Quando qualcuno visiterà quella bacheca preleverà del codice malevolo.
Vediamo adesso una demo di XSS, sempre sul social network Elgg. Minuto 22:00
Nel codice malevolo invece che mettere un alert inseriamo qualcosa di extra alla pagina, come l’url del
server dell’attaccante. Questo tag, quando l’utente lo riceve sul suo browser, farà una GET su l’url e nei
parametri concateniamo i cookie, l’escape serve per fare URL encoding.
Per fare l’attacco abbiamo bisogno poi di un server in ascolto, in questo caso usiamo netcat per aspettare il
risultato.
Il valore del cookie recuperato è quello segnato, ricordiamo che sono delle coppie chiave = valore e il “=” è
codificato come %3D
Il secondo esempio mostrato è che quando alice visita la bacheca di Sammy ne diventa automaticamente
amico
Di seguito infatti vediamo come MySpace, che ha subito l’attacco da Samy, implementasse male il sistema
di filtering e di come spezzare il tag Javascript funzionasse.
Come prima preferiamo l’uso di moduli/funzioni già esistenti che sicuramente sono più completi di quelli
che scriveremo noi.
Contromisure XSS: usare Content Security Policy
Il problema, come descritto prima, è che il browser riceve il codice HTML mischiato con Javascript e nella
risposta il browser non sa distinguere se quel Javascript gli viene da una fonte fidata o no. In questo
approccio evitiamo di far eseguire al browser script scritti come mostrati nell’immagine e forziamo i dati e il
codice ad essere separati.
Vediamolo con lo schema, abbiamo due server e due porzioni di Javascript. Noi in questo caso abbiamo già
bloccato il Javascript inlined, e supponiamo anche che l’attaccante non sia in grado di uploadare il file sul
server e quindi ne deve usare uno suo.
Nel http header segnato con (2) il server dice al client di chi si deve fidare, e l’attaccante supponiamo che
non possa modificarlo perché il server è stato configurato in maniera corretta.
Ma come si setta una regola CSP? Se siamo in un server Apache si aggiunge questa direttiva con i vari tipi di
domini. Oppure possiamo farlo anche in PHP.
5-WebSecurity-ServerSideAttacks
In generale nel OWASP Top 10 Application security risk parliamo di injection in generale, questo copre un
cappello enorme di differenti attacchi, noi ovviamente non li vedremo tutti. Anche il XSS è considerabile un
injection visto che iniettiamo all’interno del browser mentre adesso lo faremo nel server.
Vediamo nuovamente a volo SQL, è un linguaggio che lavora sulle tabelle e sono strutturate come segue e
con diversi modi di visita.
Select
Update
Insert
Drop
-- o # come commenti
La SQL injection sono un insieme di attacchi che puntano ad accedere, e a danneggiare, un DB al quale non
abbiamo accesso. Normalmente un DB non è accessibile da un indirizzo pubblico e l’unica cosa che può fare
è mandare una richiesta all’applicazione web in maniera malevola.
Immaginiamo di avere un’applicazione con il classico username e password, una volta premuto il tasto
“submit” una richiesta http viene inviata con i dati inseriti
Il codice PHP che legge lo statement SQL è la seguente, e ci restituisce un solo valore per via della AND
Notiamo in che modo la parte fissa della query è concatenata con la parte variabile, gli input dell’utente:
$ eid , $ pwd sono inseriti (interpolati) nella stringa query ( $ sql ). È possibile per un attaccante cambiare la
struttura di una richiesta SQL manipolando la stringa.
Se un utente inserisse EID 5002' ¿ quello che succederebbe, all’interno del codice sarebbe comunque un
attacco perché modifica il comportamento del DB. Questo perché normalmente si dovrebbe inserire anche
la password ma il “#” provoca la generazione di un commento.
La query che verrà eseguita è quella mostrata di seguito, il problema è che ritornerà dei valori anche se
l’utente non conosce la password dell’impiegato. Abbiamo una falla nella sicurezza
Possiamo ovviamente fare anche cose più complesse, ad esempio mettiamo caso di non conoscere
nemmeno l’username a questo punto per ottenere tutti i record del database modifichiamo le richieste del
WHERE per far sì che tutto sia vero. Il nostro payload sarà “a' OR 1=1.
Ovviamente non esistono soltanto attacchi alle SELECT, ma anche alle UPDATE o INSERT INFO e tutte le
opzioni viste prima.
È anche possibile concatenare più query e per ottenere ciò si utilizza il “;” questo viene fatto perché spesso
con una sola query non si può fare tutto. Fortunatamente questo tipo di attacco, ovvero il concatenamento
non funziona su mySQL questo perché l'estensione di PHP non consente multiple query.
Per fare il filtraggio PHP mySQL extension ha incluso un metodo: mysqli ::real ¿ (). Ma la soluzione migliore
rimane il prepared statement.
Per prepared statement intendiamo una feature che viene implementata per migliorare le performance di
richieste SQL ripetute:
Si manda un template di richiesta SQL al database con certi parametri lasciati generici
Dopo preleviamo i dati per modificare l’operazione, in questo modo le due strutture sono separate.
I dati utenti non hanno la possibilità di cambiare la struttura della query.
Anche questa volta abbiamo che il problema è sempre lo stesso, vengono mischiate istruzioni e dati
insieme e lo possiamo vedere nello schema.
L’impatto di tale attacco è critico visto che comporta l’esecuzione di codice arbitrario, come scritto sopra il
primo caso può applicare anche quando una web application inoltra dati unsafe ad una shell di sistema.
Quindi nel caso reale si avrà un utente che scriverà example . come la pagina PHP eseguirà il comando di
sistema, ma senza la sanificazione dell’input un attaccante potrebbe scrivere:
example . com;codiceRandom e Bash o altri interpreti di shell vedranno il carattere “;” come un
separatore ed eseguiranno la seconda parte.
Per il secondo caso, quindi la code injection, abbiamo che i linguaggi di scripting forniscono feature
avanzate per lanciare una stringa variabile come programma, queste funzioni sono spesso indicate come
“eval(),evaluate(), assert(), etc”
In questi casi il codice iniettato è eseguito dall’interprete invece che dalla system shell.
Potrei usare la web app pure per effettuare DoS, senza che si possa rintracciare l’attaccante.
Questo tipo di attacco può essere devastante perché spesso le reti interne non effettuano input validation e
quindi compromesso uno vengono compromessi facilmente tutti. In più abbiamo anche un'altra possibilità,
se la rete è cloud abbiamo che alcuni indirizzi speciali contengono delle informazioni (come quella riportata
sulla slide di AWS che è fissa e locale), è possibile leggere password ed informazioni critiche.
Ogni programma che consente una connessione è potenzialmente vulnerabile all’SSRF. Un esempio e
alcune contromisure sono sulle slide, ma come detto dal prof questo voleva essere solo un accenno.
Lezione 7 30/03
Esercitazione Web security (5-Lab-WebSecurity)
La scaletta di esercitazione sarà:
https://docs.docker.com/compose/install/
È necessario configurare gli hostname (es. www.csrflabelgg.com), modificando il file /etc/hosts in Linux.
Potete trovare tutte le indicazioni nel README.md che si trova nella cartella di ogni esercizio (ad esempio:
https://github.com/rnatella/swsec/tree/main/web-security/csrf-elgg). Sono riportate anche le istruzioni
per il sistema operativo Mac OS X.
Il sito mette in automatico tre cookie, ci sono due link che fanno arrivare a vedere quali cookie sono same
site quali no. L’obiettivo dell’esercizio è rispondere alle domande:
È possibile vedere anche il codice sorgente della pagina, la troviamo sempre nelle cartelle.
Se abbiamo già avviato docker in lezioni o situazioni precedenti il “docker compose” potrebbe dare
problemi, per risolvere tale situazione bisogna ripulire e i comandi sono i seguenti:
Alle 36:50 vi è il secondo esempio con l’opzione di edit-profile request. Nel power point vi è lo sniffing delle
richieste e poi vi un template di partenza di ajax request di tipo POST. Ovviamente anche qui, prima
spegniamo il docker e poi riavviamo però nell’altra cartella
Se abbiamo problemi con il passaggio dei parametri proviamo a fare in questo modo:
l’url lo scopriamo facilmente, mentre in content mettiamo tutti i parametri, non solo la description.
Il 47 in guid si deve ricavare, poi chiede se fosse possibile togliere “l’if” e si tramite l’uso di CURL che
permette di fare richieste GET e POST da shell e non viene vista dai browser. In più permette di evitare di
autoattaccarci.
All’ora 1:22:05 parte l’esempio di CSP, si trova nello stesso container di elgg, le pagine elencate hanno
codice HTML e Javascript e il nostro obiettivo è effettuare modifiche all’applicazione per far vedere che
tutto venga eseguito, restituendo “OK” invece che “failed”.
All’ora 1:44:50 parte con la SQL Injection, lanciamo nuovamente il docker sulla terza cartella e ci sono le
varie richieste da fare.
Lezione 8 01/04 (6-InputValidation)
Input Validation
Abbiamo appena concluso la prima parte del corso in cui abbiamo visto un po’ di vulnerabilità importanti,
ce ne sarebbero altre e le vediamo nell’immagine. Ma il punto in comune è il problema di mischiare dati
con codice, e la soluzione comune a queste vulnerabilità è quella di sanitizzare gli input.
Spesso si usano due termini, anche intercambiabili tra loro, ed in alcuni contesti vi è una lieve differenza:
Ma cos’è una superfice d’attacco? Informalmente è l’insieme degli attack vector che un attaccante può
usare per inviare flussi di dati malevoli.
Ovviamente il concetto principale è che: Più è larga la superfice d’attacco => Più è semplice fare danni o
subire exploit.
Anche le applicazioni da linea di comando hanno una attack surface, la cosa potrebbe lasciarci interdetti
ma se proviamo a lanciare $ sudo cat non avremo una grande superficie, a prima vista, però vi sono in
realtà moltissimi canali aperti.
Ad esempio, l’utente con il quale ci siamo loggati, l’attuale cartella di lavoro, le variabili d’ambiente, etc e
tutte queste cose l’applicazione le legge come se fosse la roba proveniente dalle GET e POST.
Molte librerie e programmi sono controllate attraverso le, appena citate, variabili d’ambiente le quali sono
spesso oscurate o nascoste o addirittura non documentate. In alcune circostante gli attaccanti possono
prendere controllo delle variabili d’ambiente per effettuare comandi con l’uso di privilegi.
Ma come funziona?
Ogni processo in Linux ha una struttura dati nel kernel chiamata Process control block (PCB) e sia windows
che Linux hanno la possibilità di settare, per ogni processo, le variabili d’ambente. Quando noi diamo il
comando $ export * stiamo avendo a che fare con il processo della shell (quello che a noi è il rettangolo
grigio), lanciando poi una fork avremo che i dati (come myprog) copiano tutti gli elementi della shell.
Arriva il genio che dice: Va beh facciamo il controllo. Eh, bravo ma non è facile, se vediamo il continuo
dell’esempio scopriamo che purtroppo Linux è pieno di variabili d’ambiente di default e che spesso sono
sconosciute.
Tipicamente però noi svilupperemo applicazioni web e dall’OWASP possiamo vedere quali sono i tipici
attacchi che potremmo subire.
Un altro consiglio che non fa mai male è effettuare uno screen completo dell’Attack Surface, magari
attraverso tool come l’attack surface Detector (ASD) che consiste in un plugin per OWASP ZAP e Burp Suite
e ci informa sull’AS del nostro sito. Ciò che è in grado di fare consiste in:
Quindi effettuare una separazione dei privilegi del sistema ci aiuta contro l’input validation e tale tecnica si
chiama defence in dept (difesa in compartimenti)
Tra la creazione della stringa e la shell c’era un controllo sui caratteri, rimuovendo quelli ostili però non il
controllo a capo. Questo è un esempio di blacklist, ovvero diciamo cosa non va entrando.
Ma quindi le blacklist sono sempre sbagliate da usare o inutili? No, sono molto comode per effettuare
testing, infatti, sono utili per identificare i dati che non dovremmo accettare.
Encoding e decoding
Il decoding degli input va fatto sempre prima dell’input validation questo perché in caso contrario sorgono
problemi.
Infatti, se vediamo la nostra stringa di URL in input sappiamo che è costituita da byte codificati (senza spazi)
e dopo il coding troviamo la stringa ASCII.
Vi è un esempio di vulnerabilità su zoom, ma il pdf non ha la registrazione del video quindi potete vederlo al
minuto 28:00 della lezione.
Spieghiamo cos’è successo, la vittima riceve un link zoom che apre una pagina HTML contenente un altro
link, più lungo, non di tipo http ma zoom. La stringa verrà usata in una SQL Injection, ma come SQL
Injection? Si, perché Zoom usa un database interno per conservare informazioni quali configurazioni
fotocamera, etc.
Il comando “strings” usato nella shell mostra le stringhe presenti nel programma e le stampa, nello
specifico qui ha rilevato le query presenti (quelle con select). Domanda, questo usa i prepared statement?
Sì e no, alcune parti ce le hanno come la 3° Select.
L’attaccante poi ha provato le solite configurazioni per vedere se qualcuna di esse funzionava; quindi, ha
provato l’escaping con un singolo apice ed il commento alla fine. Vedremo che viene filtrata usando la
sanitification
L’attacco però ha successo se mandiamo una stringa come success¿ 2' ∨1=1 (Da inviare con un
programma perché ovviamente non ci permetterà di inviare una codifica del genere).
L’errore è che qui si fa prima sanitificazione e poi decoding, questo perché il decoding prende C2 e uno
degli apici e lo trasforma in un carattere.
Come risolviamo questa cosa? Si utilizza il così detto unicode sandwich, nella slide ci sono anche due link
utili per approfondire. Per capire la figura abbiamo che si è schematizzato nella seguente maniera:
Unicode
Partiamo con il dire che non è un modo di codificare, ma è una tabella/repertorio di caratteri e associa ad
un numero un simbolo MA NON fornisce una rappresentazione in byte. UTF-8 è lo schema di enconding
più conosciuto per Unicode e utilizza una codifica variabile da 1 a 4 byte.
Il fatto della codifica a lunghezza variabile implica anche che se prendiamo un gruppo qualsiasi di byte, non
è detto che il codice sia per forza associato ad un carattere valido (quello che vediamo con il ?).
Per capirlo meglio vediamo un esempio di codifica con python e perché c’è stato il problema di passaggio
da python2 a python3
Python2 quando si usano i doppi apici crea un oggetto chiamato “str”, se prima degli apici usavamo
“u” avevamo una stringa di tipo unicode.
Per essere pure più userfriendly quando si concatenavano due stringe di natura diversa realizza una
conversione automatica, con lil problema dell’eccezione mostrata nel secondo blocco.
Python3 quando si usano i doppi apici ora abbiamo una stringa unicode e quindi c’è un casino
assurdo, se vogliamo usare una stringa di byte usiamo il carattere “b”. In più non fa conversioni
implicite e forza la correzione.
La codifica va poi concordata con gli utenti, quindi, bisogna informare in anticipo il tipo di schema di
codifica.
Supponiamo ora di aver decodificato e di aver usato una whitelist, adesso dobbiamo fare altri controlli
Numeri
Dobbiamo sempre convertire i numeri nel tipo desiderato, come abbiamo visto non fare una cosa del
genere potrebbe portare a buffer overflow:
Nel caso di interi, con una macchina a 64 bit, abbiamo tipicamente overflow quando si immette
18446744073709551615 (2^64-1) => -1
Per interi non negativi bisogna usare i tipi unsigned integer
Se le frazioni non sono ammesse usare i l tipo integer
Vogliamo calcolare un minimo ed un massimo? Consideriamo l’uso di una whitelist
Floating point? Quello imparato a Calcolo numerico.
Paths,URLs
I dati in forma canonica sono quelli più semplici ed in formato standard. Il percorso dei file o gli URL sono
particolarmente vulnerabili ai canonicalization bugs (path traversal, URL manipulation, …). Nell’immagine
che vediamo abbiamo tre esempi di percorsi non canonici del percorso canonico.
Se nell’esempio successivo uso i “..” posso leggere un file che non sta nella web app, ma uno che sta nella
sotto-sottocartella. L’attaccante invece che leggere un file normale prende e legge un file di configurazione
segreto.
Ovviamente il modo per contrastare tale tipo di attacchi è non accettare percorsi arbitrari ma forzare l’uso
della forma canonica. Quando si caricano file, il file path dovrebbe essere deciso sempre dal server non dal
cliente.
Ma quando avviene l’input validation? Quando dobbiamo fare l’upload dei file. Anche in questo caso non
accettiamo tipi di file non voluti ma sempre solo quelli che conosciamo noi, attraverso whitelist.
Stringhe
Dove possibile riordiamoci di usare sempre whitelisting per le stringe, in più cerchiamo anche sempre di
convertire le stringhe nel tipo enumeration (enum). Se non ci ricordiamo come funziona l’enum c’è un
esempio di seguito ma in generale serve ad associare determinate stringhe a numeri. In più se possibile
usare classi già esistenti.
Limitiamo la lunghezza massima, facciamo così per evitare buffer size & counter DoS.
Se le stringhe sono di tipo comune (e-mail, url, etc) utilizziamo il riuso di librerie standard
Tool comuni per l’utilizzo di regular expression (RE)
Se la situazione è complessa usiamo una compilation di tool.
Anche l’OWASP ci mette in guarda da e-mail ed URL che potrebbero sembrare semplici, ma non lo sono.
Regular expression
Sono strumenti molto potenti e ci vengono in aiuto quando non abbiamo un filtro che possiamo usare e
siamo costretti ad utilizzarne uno ex novo.
Per essere più formali le regular expression sono un oggetto che definiscono un pattern, immaginiamo di
voler filtrare le stringhe e accettare solo quelle che iniziano per “hello”. Regex è un mini-linguaggio usato
per la definizione di pattern testuali, in più fornisce la risposta Vero o Falso.
Una parola come “amaca” non passerebbe perché non c’è nulla dopo “ca”. Se vogliamo provare le regular
expression possiamo usare attraverso la shell linux con “grep”.
Ovviamente noi non vogliamo ricercare sottostringhe ma vederlo sull’uso dell’input validation, facendo in
modo che:
Ci sono poi sulle slide altri esempi di espressioni, quella sulla quale facciamo più attenzione è quella dove
potevamo mettere l’anno mille come ingresso:
Quindi qui siamo già sicuri che ci possono essere fino a 4 caratteri, poi abbiamo la possibilità di salvare
dentro delle variabili dei pezzi che abbiamo associato (la sintassi dipende dal linguaggio) e le utilizziamo per
fare i controlli multi-stage.
Se vogliamo fare pratica possiamo usare il sito https://regexper.com/ che ci permette di visualizzare le
regular expression, si basa sulle macchine a stati finiti (cosa che le RE sono). Vi è anche la possibilità di usare
un altro sito per il debugging https://regex101.com/
Purtroppo, le RE sono vulnerabili ad attacchi di tipo DoS e questo prende il nome di ReDoS. Se noi
scriviamo male le RE e un attaccante ci invia una stringa malevola il processore viene mangiato dalla RE;
nonostante il grafico mostri effettivamente una crescita esponenziale abbiamo che la crescita dipende da
come sia tata scritta la RE.
Com’è possibile intuire dal grafico la qualità delle RE dipende dal backtraking, ma cos’è? È il ripercorrere il
grafo della macchina a stati finiti al fine di ritrovare la soluzione alla RE. Infatti, alcune RE possono avere più
di una soluzione, nel caso questo succeda si ha che il sistema ritorna fino all’ultima soluzione trovata e
riprova a trovare una soluzione fino a quando tutte le opzioni sono esaurite.
Vediamo un esempio: Vogliamo il match della regola “.*”! ovvero vogliamo una parola con doppi apici e
punto esclamativo.
Ad occhio è facile notare che non c’è un match, ma l’algoritmo ci metterà molto tempo e risorse.
Ma quindi come dovremmo scriverla questa benedetta RE? Se vediamo la macchina a stati finiti avremo che
essa è rappresentata come segue, e le parti segnate in rosso solo le parti più critiche.
Questo capita perché la scritta è non deterministica (NFA) causando quindi sovrapposizione, allora per
risolvere tale problema scriviamo una RE in questo modo: Tutto quello che non è doppio apice, ripetuto
zero o più volte, più !
Infatti, se ripetiamo l’esercizio di prima il numero di passaggi è di gran lunga inferiore. Sulle slide ci sono
altri esempi di cattive RE.
Immaginiamo di dover effettuare il filtraggio per delle reti di pacchetto o per il linguaggio C, è super difficile,
usiamo una grammatica in BNF che ne indica la sintassi. Per il C abbiamo che ogni parentesi tonda aperta
deve avere una parentesi tonda chiusa, o cose così. Quindi abbiamo che i parser engine prendono un
pacchetto in ingresso e lo analizzano per verificarlo con la grammatica, se viene rifiutato lo scartiamo; ma
se il pacchetto viene accettato viene trasformato direttamente in una struttura dati già organizzata.
Cosa intendiamo per fuzzing? È un approccio di tecniche di testing per la robustezza/security e come
caratteristica ha quello di usare moltissimi input, è un po’ diverso dai test funzionali visti ad ingegneria del
software perché lì il test è più centellinato.
Vista l’enorme quantità di sforzo richiesto è altamente improbabile che venga svolta in maniera manuale, è
molto più probabile che la si effettui in maniera automatica. Gli elementi che diamo al sistema per fare
questo tipo di testing sono chiamati fuzz inputs, il termine fuzzy indica (nel campo ingegneristico) un
segnale rumoroso.
Ma qual è la caratteristica principale per la quale è così importante questo tipo di testing? L’essere molto
efficace per grossi volumi di dati e quindi perfetto per trovare vulnerabilità di sicurezza. In più è un
approccio popolare per la scoperta di vulnerabilità sconosciute (0-day).
Il fuzzing si applica in tutte le attack surface, queste mostrate nell’immagine sono le aree tipiche:
Digital media, si intende windows media player o qualsiasi applicazione che si occupa di contenuti
multimediali e quindi l’input è molto complesso.
Vediamo un'altra prospettiva dell’attack surface, tipicamente il fuzzing si applica ad attack surface di basso
livello come il traffico di rete su IP/Wireless ma in altri casi anche dall’alto come i testi del file system.
Di Fuzzers ne sono già disponibili per centinaia di differenti attack vector ed un elenco di tecnologie
proprietarie ed emergenti (immature) sono i migliori target:
Oggi abbiamo che l’uso del fuzzing si è evoluto e abbiamo aziende come Google e Microsoft che lo
impiegano per trovare più del 50% dei bug nei loro prodotti.
Facciamo una ripetizione di IS, quando riceviamo dei requisiti essi sono spesso positivi ovvero il “software
deve fare x” mentre i negati non vengono richiesti esplicitamente. Dopo i requisiti passiamo
all’implementazione che cerca di coprirli tutti evitando quelli negativi, quello che otteniamo però non è
detto che sia quello desiderato; infatti, nel cerchio di destra abbiamo due cerchi concentrici:
Ovviamente noi vorremmo che i due cerchi si sovrappongano il più possibile, ma non si riesce mai.
Il positive test si colloca quindi nel cerchio bianco, mentre il negative si colloca fuori dal cerchio grigio.
Ricapitoliamo il tutto:
Funcional testing (positive): si va per requisiti positivi e si fanno dei test scritti a mano
Performance e stress testing: visto al corso d’impianti, si va sulla quantità
Robustness e fuzz testing: si va sulla quantità e sul tipo diverso di test. Rispetto al test funzionale
qui è tutto automatico.
Ma quindi come si scrivono fuzz test? Il modo più basilare è quello di inserire stringhe molto lunghe a caso
nelle richieste del programma, oppure stringhe sempre lunghe ma casuali.
Profondità del fuzz testing
Quando testiamo un programma come word non gli mandiamo un .mp4 ma uno pseudo word, questo
concetto è chiamato profondità del fuzz testing. Vediamo un esempio, consideriamo il protocollo FTP dove
ci logghiamo con username e password e poi chiediamo di vedere i file con LIST.
Se facciamo fuzzing la prima cosa che possiamo provare a fuzzare è la linea di comando, con il comando
USERR mionome è probabile che l’FTP server rigetti l’input (se non robusto crasherà). In questo caso il
fuzz è stato rigettato per INVALID SYNTAX, ma questo tipo di problema viene scremato spesso e volentieri
subito; quindi, non è proprio di grande interesse.
Un test più subdolo potrebbe essere quello segnato con (3) nell’immagine, in cui non diamo il comando
PASS oppure lo omettiamo/scambiamo. Questo è più efficace come test ed è di invalid state.
• Purely-random: Ovvero quelli iniziali del prof. Miller con caratteri completamente a caso. Il target
scarterà facilmente la richiesta
• Generation fuzzers: Sono quelli che costano anche più soldi e sono tool realizzati ad-hoc per cosa voglio
testare, partendo da un modello per generare gli attacchi
• Mutation fuzzers: Non parte da un modello ma parte da input “validi” (seed) come file validi o tracce
Wireshark, da questi input aggiunge poi del fuzz iniettando dei valori anormali.
Generation fuzzers
Il più costoso e nell’immagine di sotto vediamo peachtech che all’interno ha i protocolli di rete più usati e
per ognuno di essi ha una FSM (macchina a stati finiti). Quindi è un approccio molto potente ma che
richiede enormi conoscenze, in più il testing così effettuato segue il protocollo ma spesso i prodotti reali
deviano dallo standard.
Mutation fuzzers
Molto più economico e si comporta un po’ come man in the middle prendendo una traccia che noi gli
diamo e iniettando poi rumore a caso, questo fino a quando per pura casualità non troviamo un messaggio
che fa crashare il sistema.
Ma quindi cosa fa un tool di fuzzing? Da un punto di vista teorico è come se fosse un ciclo while infinito.
Failure monitoring
Ovvero come facciamo a capire se il sistema è vivo o morto? Vediamo i casi:
Denial of Service: Crash e reboot logs provenienti dal SO, Network timeouts
Problemi relativi al Filesystem: File access monitoring (es. "../../../../../etc/passwd")
Command injection: Come abbiamo visto le fork copiano il contenuto dello stack, Injected fuzz
inputs all’interno delle DB queries, OS commands, etc.
Vulnerabilità associate alla Memory: Valgrind, AddressSanitizer
Tool
Vedremo una panoramica di tre tool diversi per coprire le varie caratteristiche. Iniziamo con:
Oltre a già tutto quello che sappiamo su metasploit esso contiene anche una collezione di fuzzers.
L’esempio di come si apre e funziona è a 1:08:40
Le limitazioni di questo tool sono dovute al fatto che ha pochi protocolli; quindi, se non vi è il protocollo
basta è finita non possiamo testare nulla mentre l’altra è dovuta alla mancanza di automatismi come il
monitoring, in più il sistema non gira all’infinito ma si ferma al primo crash.
Noi faremo fuzzing sul primo messaggio di ClientHello, procediamo con l’apertura di Wireshark per leggere
il messaggio e notiamo che ci sono alcune parti del messaggio che sono costanti; ad esempio, i primi tre
byte che rappresentano il tipo di messaggio (nel nostro caso 22 handshake) e gli altri segnati nella slide.
Tali valori costanti non sono i migliori candidati per fare fuzzing, ricordiamoci la questione della
profondità di fuzzing.
Questo di seguito è un programma python che utilizza delle funzioni “s_”, la prima è l’inizialize.
La lezione continua leggendo le altre slide ma fa anche delle esercitazioni. Consiglio la visione per vederli
passo passo.
Non è molto famoso, non supporta python3 addirittura potrebbe essere un progetto abbandonato. Come
detto per i mutation noi forniamo un pick up e lo utilizzerà come base. Alle 1:44:50 inizia con l’esempio.
Vediamo un esempio sulle sessioni di rete, FTP richiede di inviare ogni volta delle stringhe ed in questo caso
abbiamo una sessione rappresentata in questa maniera:
Più sessioni possiamo raggrupparle in pool:
Lo scopo è massimizzare la fitness, ma vediamo il processo passo per passo. Lo stato iniziale di questi
diversi pool lo chiamiamo generation 0 e si eseguono tutte queste sessioni sul sistema, per ogni esecuzione
misuriamo la metrica di fitness.
Mischiate le sessioni ottengo una generation 1 dove le sessioni migliori sono mantenute così come sono, le
altre invece vengono mischiate attraverso il crossover. Non tutte vengono pescate con la stessa probabilità
ed in più non si ha solo un mixaggio casuale ma si aggiungono anche delle mutazioni randomiche.
Vediamo meglio cosa comporta effettuare crossover:
Mentre per un esempio di mutation è la seguente, sopra partiamo senza mutazioni poi abbiamo A’ dove
inseriamo spazzatura.
Poi abbiamo il crossover anche per i pool
Vi è poi anche sempre la mutazione dei pool che può consistere in un aggiunta/eliminazione randomica di
una o più sessioni, spesso quelle con una fitness più bassa.
Black box fuzzers: visti la settimana scorsa, non monitorano minimamente il programma interno e
quindi non usano feedback ma guardano solo gli input di fuzzing precedenti e i fallimenti del
programma.
White box fuzzer: li usiamo adesso e usano la coverage, quindi, guardano in parte all’interno del
programma.
Grey box fuzzer (non verranno usati):
Vediamo un esempio nel quale il target è un parser di immagini JPEG e l’input era un file di testo
contenente la parola “hello”. Da questo è partito con un algoritmo genetico ed è riuscito a trovare valori
validi. Nell’immagine a lato ci sono però delle immagini, senza senso, ma valide.
Si è scoperto che il primo byte deve essere 0 xFF (magic bytes) e sono utili per determinare sempre che
tali file con questi byte sono di tipo JPEG.
Come funziona AFL? Prima di tutto non dobbiamo usare un compilatore standard ma uno fornitoci:
Per il tipo di file in ingresso la cosa migliore sarebbe avere un file con la coverage più alta possibile ma con
la dimensione più piccola. Il professore lancia un esempio a 50:28.
Vediamo però un overview del processo realizzato da AFL. Si parte da una cartella di test contenente degli
XML, li prova uno alla volta mettendoli in coda e poi ne effettua la mutazioni ottenendo dei file diversi. Il
programma viene eseguito con i file mutati e a questo punto o il programma crasha oppure no, nel primo
caso salviamo cosa è successo e salviamo anche quando rimane hanged (appeso).
AFL può usare come input più dizionari per iniettare mutazioni
Feature aggiuntive – Crash de-duplication
Tornando ai problemi di prima abbiamo visto che un programma potrebbe crashare molte volte ma
differenti tipi di crash possono triggherare le stesse vulnerabilità e quindi avere casi come in figura dove
97 mila crash con in realtà uno solo.
Per capire come fare ci possono essere diversi modi, quello più semplice è vedere il punto di crash del
sistema, come in Valgrind, e lo usiamo come punto di partenza per il backtrace.
Un altro approccio, più intelligente, è vedere gli hit counter dove praticamente contiamo quali sono i punti
uguali che si sono toccati per l’errore. Se sono gli stessi allora non si conterà il nuovo errore come qualcosa
di diverso.
Infatti, nell’esempio di seguito vediamo di come venga tagliata tutta la parte inutile del file che non
contribuisce al crashing dell’applicazione.
L’operazione è tutta automatica ma per farlo bisogna seguire questi comandi:
Per il corretto svolgimento dell’esercizio dobbiamo, da fare anche se abbiamo la sua macchina virtuale.
- Installare AFL
- Andare sul repository del corso e fare submodule init e update che servono per scaricare OpenSSL.
- Compilare OpenSSL e rivedendo la lezione dell’altra volta settiamo le variabili d’ambiente.
Qualcuno invece di usare lo STDIN preferisce inviare un file, è un alternativa valida ma non cambia nulla.
afl-g++
afl-clang-fast++
Analisi Dinamica, nome un po’ accademico per riferirsi a quello che normalmente chiamiamo
testing. Prendiamo un software e lo testiamo usando specifici input e verificando gli output.
Analisi Statica, prendiamo il software così com’è senza eseguirlo e cerchiamo di estrapolare delle
informazioni. Non è associato a specifici input
Potrebbe sembrare che l’analisi dinamica sia migliore di quella statica ma non è così, come in ogni cosa ci
sono i pro e i contro. Un contro dell’analisi dinamica, che non si ha in quella statica, è che possiamo vedere
solo quali sono i percorsi del software e delle sue uscite. In riferimento a quanto detto l’altra lezione
parliamo sempre di Coverage e se non copriamo tutto, cosa spesso probabile, non sappiamo se siamo
vulnerabili su quella determinata porzione di codice o meno.
In una prossima lezione vedremo il ciclo di sviluppo di software in ottica security, di seguito ne vediamo uno
per capire dove si effettua l’analisi statica. La zona interessata in questa lezione è la zona di code dove
effettuiamo la così detta code review, qui vi è un qualcuno/più persone che guardano il codice ed
effettuano code inspection.
Un’altra fase legata all’analisi statica è il design architetturale dove si decide la code review da dove deve
iniziare e cosa deve vedere, evitando di analizzare parti a vuoto. Intendiamo che se abbiamo un sistema
software che si occupa di servizi online, ha senso sapere quali componenti potrebbero essere soggetti a XSS
e quindi mi conviene analizzare questi invece che altri pezzi.
Fare analisi del codice, per un essere umano, è un lavoraccio e per questo motivo si farà una combinazione
di software più quella umana. Ovviamente tool di analisi statica permettono di effettuare code review in
maniera più veloce e con più risultati ma l’uso di un essere umano è fondamentale per notare potenziali
problemi o errori.
Limitata capacità di analisi, ovviamente noteranno solo vulnerabilità standard perché purtroppo
hanno “i paraocchi” e non posseggono la logica del prodotto.
L'analisi statica può comunque produrre degli errori:
o Linea di falso allarme, magari c’è una input validation che impedisce a questa linea di
essere eseguita ma l’analisi statica non la nota
o Linea di falsi contrari, cioè una vulnerabità vera che non viene notata come ad esempio
heartbleed
Time consuming.
Di tool ne abbiamo ovviamente tantissimi e le aziende li osservano con grande attenzione perché i loro costi
non sono indifferenti e valutano pure in bisogno di ore uomo.
Oggi faremo un po’ una panoramica di questi tool, soprattutto per capire come funzionano e lavorano.
In generale un tool parte da un codice sorgente, però delle volte potrebbe essere anche l’eseguibile o il
bytecode (come in java). Oltre a queste cose vi è il sistema di build del programma, intendiamo Maven o il
makefile, se un programma non lo riusciamo a compilare probabilmente non riusciamo nemmeno ad
analizzarlo.
Tutto il file viene letto/estratto da un componente chiamato Parser/Extractor, oltre al programma che
dobbiamo analizzare, per fare un’analisi accurata, prende altri due elementi:
Le modeling rules, cioè deve sapere quali sono i canali di input per il software o meglio definito
come l’environment, dove valuta l’ambiente etc.
Quello che produce il parser è un database dove ci sono le informazioni del programma chiamato
Intermediate Representation (IR), veramente immaginabile come una tabella SQL. Quindi le analisi che
andremo a fare sono delle vere e proprie query, che non saremo obbligati a scrivere ma è sempre possibile
estendere le regole built-in. Il risultato finale è una review dal quale faremo l’analisi.
Type checker
La prima categoria dei controlli statici viene fatta già dai compilatori, in maniera più o meno simile da quella
fatta dai tool commerciali. Permette di prevenire alcune classi di vulnerabilità ma vediamo un esempio.
Nel primo caso, a sinistra, abbiamo un controllo sulle dimensioni dei tipi ed è tutto corretto perché la
dimensione di short è inferiore di quella di int. Vediamo a destra il caso opposto, ovviamente è pericoloso
perché questo potrebbe causare buffer overflow ma alcuni linguaggi (come Java ci mostra) ci da errori,
mentre C/C++ fa casting implicito e permette una cosa del genere.
Il compilatore, se usato senza una configurazione particolare, purtroppo non ci aiuta moltissimo in casi con
linguaggi come il C. Se vogliamo essere avvisati di situazioni come quella precedente dobbiamo aggiungere
dei flag come “-Wall -pendantic “ dove si abilitano i warning.
Vediamo adesso un esempio su un bug conosciuto come “goto fail” riguardante SSL del browser Safari,
nello scaricare il certificato di validità. Purtroppo, la pessima gestione delle parentesi causava una zona di
codice morto che non veniva mai eseguita
Se vediamo un esempio in JAVA sul confronto delle stringhe abbiamo che non si usa “==” per valutare
l’uguaglianza ma si deve usare il metodo.
Spesso la quality e la sicurezza vanno a braccetto, infatti se andiamo a vedere un po’ su dei diagrammi per
osservare come sono in relazione possiamo constatare che in parte sono sovrapposti.
Static analysis: Source vs. Executable code
Abbiamo detto che alcuni programmi prendono il codice sorgente altri prendono il bytecode, ma qual è la
differenza? Tipicamente è meglio avere il codice sorgente poiché abbiamo più informazioni, si può delle
volte ricostruire dal binario ma è difficile. I contro sono che non è possibile analizzare codice di terze parti.
Passiamo poi ad un altro tipo di tool conosciuti come text scanner che sono sconosciuti ma in realtà ne
esistono di diversi, questi tool funzionano come il comando “grep” e quindi prendono il testo del codice e
controllano la presenza di testo specifico, vediamo un esempio:
Security defect finders
È il modo più avanzato poiché non guarda il programma come un file di testo ma creano un modello e lo
analizzano per valutarne le proprietà di sicurezza. Tale approccio è molto pesante e potrebbe comunque
creare falsi positivi.
Esistono sia molti progetti accademici che professionali che lo realizzano, ma vediamo effettivamente qual è
il tipo di modelli che vengono creati. Ci ragioneremo anche la settimana prossima quando apriremo gli
analizzatori e creeremo delle query.
Prima di vederli dobbiamo ripetere un attimo come funziona un compilatore, nella parte alta abbiamo un
linguaggio sorgente con una serie di informazioni. Abbiamo poi due passi fondamentali per la creazione di
un modello e sono:
Lexical analysis, passaggio preliminare dove vengono tolti i commenti e gli spazi
Parsing, partendo dai token si crea il modello
Ogni parte del programma (dichiarazioni, assegnazioni, etc) sono rappresentati come nodi all’interno
dell’albero. L’analisi statica consiste a questo punto in una visita dell’albero in cerca di vulnerabilità.
Vediamo altri modelli con i quali abbiamo a che fare, come i grafi. Da un AST possono essere generati più
tipi di graph data structres come, ad esempio, il control flow graph.
Altri modelli sono il data flow graph, sembra simile al precedente ma è leggermente diverso poiché qui
vediamo tutte le istruzioni che analizzano dati.
1. Taint propagation analysis (analisi della propagazione del veleno), serve per trovare problemi
come la SQL injection e XSS. Partiamo dai dati che arrivano dall’attack surface, che sono considerati
avvelenati (input non fidati), e vediamo se è possibile raggiungere degli “scoli” ovvero dei punti
critici (come le query SQL).
Partiamo dall’esempio nel quale leggiamo con “fgets” dallo standard input e all’interno vi è un comando
“system” che esegue un comando nella shell del sistema operativo. Vogliamo capire se i dati dell'attaccante
possono raggiungere system.
2. Range analysis, determina il range di potenziali valori delle variabili o dei buffer e qui capiamo
anche qualche collegamento in più rispetto alla taint propagation. La vulnerabilità associata è
quella dei buffer overflow.
3. Type state analysis, vediamo direttamente un esempio.
Immaginiamo di avere un programma che usa una lista linkata con variabili di tipo node. Si chiama type
state poiché ad ogni nodo si abbina una macchina a stati finiti, quando facciamo due free su due nodi
abbiamo un errore.
Falsi negativi. Il nostro programma presenta delle vulnerabilità (cerchio più largo) ma il tool non le
rileva tutte.
Falsi positivi, l’esatto contrario di quanto detto sopra.
Per capirlo per bene vediamo un esempio. Immaginiamo di essere un analizzatore e vedere questo testo. In
questo programma c’è un problema di buffer overflow “x” è di cinque caratteri. Il compilatore per capirlo
come fa? A prescindere dalla copia si effettua un’asserzione e poi prova a navigare attraverso un DB che si
crea.
Tale tecnica viene fatta anche per valori non fissi e con variabili.
Vediamo un altro esempio che possiede però due possibili percorsi.
Questo esempio è per capire che questa piccola porzione di codice è probabilmente affogata in altre 10M
righe di codice; quindi, il numero di percorsi cresce esponenzialmente con i vari percorsi e siamo costretti
quindi ad accettare delle approssimazioni. Le approssimazioni che verranno fatte saranno fatte in diversi
modi:
Context sensitive algorithm, se ad esempio abbiamo due porzioni di codice (rappresentati con i
triangolini arancione più piccoli) dovremmo effettuare due analisi diverse perché dipendono dai
parametri d’ingresso.
Context Insensitive algorithm, effettuiamo un’approssimazione e viene analizzato il triangolo
arancione una sola volta e quest’unica analisi mi porta a dire che z è maggiore o uguale a zero per
entrambi i casi.
Se guardiamo l’immagine successiva abbiamo “fgets” che sappiamo essere una funzione da evitare e poi
una “prinft” che sappiamo essere vulnerabile. Il programma è vulnerabile? No, perché per come sono fatti
gli if, avendo entrambi nella condizione “x” eseguiamo sempre la “y=hello” e poi la print mai avrà le due
assegnazioni di y in contemporanea.
Flow insensitive
Nel semplificare l’analisi introduciamo anche il concetto di errore poiché altrimenti il consumo di memoria
sarebbe troppo pesante. I tool, quindi, trovano un trade off il più possibile e spesso il costo del tool deriva
proprio da questo.
Limiti teorici
Anche se avessimo un super computer avremmo sempre dei falsi positivi/negativi, ma com’è possibile?
Questo è dovuto a dei limiti proprio matematici dimostrati da Godel attraverso dei teoremi undecidable
ovvero non è dimostrabile se un teorema è vero o falso.
Lezione 13 27/04 (10-StaticAnalysisTools)
Prima di iniziare alcune informazioni di servizio, tra oggi e venerdì finiremo l’analisi statica e venerdì 29 sarà
esercitazione e vedremo la parte pratica con questi tool.
I tool abbiamo detto che non sono tutto nella vita ma è importante il processo di sviluppo degli stessi, cosa
che vedremo la settimana prossima. Oggi vedremo due tool, FindBugs e GitHubCodeQL.
Findbugs
Il nome originale del progetto è sì FindBugs ma adesso se vogliamo usarlo lo troviamo con il nome di
SpotBugs, può essere usato in tre modi:
Find Security Bugs è un’estensione orientata alla sicurezza per Findbugs. Sulle slide ci sono i passaggi e i link
per il download e per l’installazione.
Vediamo il progetto WebGoat che è un’applicazione web volutamente vulnerabile usata per approfondire e
capire i problemi di tali applicazioni. Nel caso lo cercassimo noi attenzione, la versione più nuova è
differente da quella vecchia che noi usiamo; questo perché ripetiamo per fare l’analisi statica bisogna fare
la build del progetto.
Al minuto 14:00 apre WebGoat e lo naviga spiegando com’è strutturato e funziona. Possiamo vedere che è
strutturato in sotto lezioni in base al tipo di attacco; quindi, lo stesso file appare più volte nelle stesse
lezioni. I numeri vicino alle lezioni sono i warning che ha trovato.
1. Scariest
2. Scary
3. Troubling
4. Of concern
High confidence
Normal confidence
Questo perché non è sempre sicuro al 100% dell’allarme dato, sempre per i discorsi fatti la scorsa lezione, e
per questo separa le due categorie di risultati.
Lo scarrafone che troviamo a riga 82 riguarda la funzione che gestisce la query sul JDBC e usa una classe
statement e prende in ingresso una stringa dall’oggetto sqlStatement convertito in stringa.
Questo è un caso senza prepared statement. Salendo troviamo che il problema è a riga 68, consiglio
l’ascolto per capire per bene.
C’è poi un’altra cosa super utile, se andiamo in Window/Show View/Bug info abbiamo che quando
selezioniamo una voce troviamo una cosa utilissima, ovvero un’intera finestra che ci aiuta a come
programmare.
Continuiamo poi al minuto 28:12 con l’altro tool.
GitHub CodeQL
Già solo che c’è GitHub è un marchio di qualità, e sfrutta un piccolo linguaggio chiamato LGTM (looks good
to me). È un tool che nasce come piattaforma di analisi statica continua per CI/CD (ovvero il ciclo di
sviluppo è integrato in strumenti che ogni volta che effettuano i commit testano e fanno altro per
aumentare la sicurezza del codice.)
Il fatto che GitHub analizzi tutti i codici sulla sua piattaforma, per vedere se ci sono delle CVE comuni, si
chiama variant analysis. Questo è utile per sradicare intere categorie di bug e quindi ogni volta che si
scova una nuova categoria di attacco, come le injection, si crea una query con il linguaggio di CodeQL e le si
eliminano.
Quindi questo linguaggio è il punto cardine del tool, ma come scriviamo una query? È piuttosto simile
all’SQL dei database relazionali, ha poi una spruzzata di Object Oriented e c’è poi la logica.
La figura di seguito è identica a quella dell’altra volta, partiamo sempre da un sorgente che viene elaborato
da un estrattore, il quale lo mette in un database e poi abbiamo le query che vengono prese da un
compilatore e svolte automaticamente. Useremo Visual Code per vedere i risultati.
Abbiamo detto che per partire dobbiamo compilare il programma, questa cosa era facile in eclipse perché
lo fa tutto in automatico, per farlo in questo tool dobbiamo usare il comando $ mvn cleaninstall .
Quindi prima ancora di fare l’analisi statica dobbiamo verificare che compili sulla macchina statica. Dopo
fatto il build lo dobbiamo ribuildare però facendo l’estrazione: $codeql database create “webgoat-db” –
language=java”.
Quello che succede con il secondo comando è che CodeQL chiama Maven e quello che verrà compilato in
Maven verrà indicizzato in un Database.
Visto che la query è specifica per JAVA faremo l’import, premessa: nelle query troveremo gli uguali scritti =
ma vale la stessa cosa per il Sql, QUESTA NON è UN ASSEGNAZIONE è un’equazione da risolvere
Partiamo dal “from”, che stavolta parte prima di “select” , e definisce il punto di partenza della query ed in
questo caso partiamo da tutte le chiamate di tutti i metodi.
In “where” iniziamo a ritagliare lo spazio come in SQL e prenderemo tutti gli statement indipendentemente
dall’oggetto.
Con “select” possiamo applicare delle funzioni ai risultati, ad esempio in questo caso mostriamo tutte le
entry.
Vedremo mano a mano quelle più complesse ma partiamo per adesso dalle structural queries, si chiamano
strutturali perché non stiamo vedendo ancora il dataflow e vogliamo solo vedere metodi che si chiamano
in un certo modo.
Partiamo sempre da quello visto l’altra volta, quindi immaginiamo che abbiamo un programma che ha
generato un AST come riportato in figura. Nel DB abbiamo delle relazioni, ognuna per le tipologie
dell’albero.
Usiamo il codice di seguito come partenza, il problema subito lo notiamo è che il metodo “write” scrive
dentro un array e alla posizione “loc” scrive un valore. Il bug di questo programma è che il corpo dell’if è
commentato, per cui l’obiettivo di questa query è trovare tutti gli if senza istruzioni all’interno
Nel “from” metteremo gli “if” ed i blocchi, nel “where” metteremo che il blocco deve essere parte dell’if e
che il blocco presenti zero istruzioni. Se ne troviamo uno scriviamo “this if-statement is redundant”.
ATTENZIONE FORSE LA CLASSE BLOCK è SBAGLIATA E SI DEVE USARE Blockstmt. Lo accenna nell’esempio
al minuto 1:17:00
Ci sono modi anche per strutturare le query che invece di scrivere tutto nel “where” possiamo scrivere
delle funzioni, come in questo caso “isEmpty” che prede in ingresso l’if con il getThen(). Abbiamo modo di
vedere pure un altro concetto che è quello di predicato, ovvero una condizione logica.
In riferimento all’AST vediamo un esempio a 1:20:00 per farlo clicchiamo tasto destro su un file/CodeQL:
View AST.
È possibile creare anche delle nuove classi, come la “EmptyBlock” rendendo più affinata la classe “Block”
Continua con la correlata di esempi di codici CodeQL che vedremo meglio durante l’esercitazione.
La tainted analysis ricostruisce il percorso ed è in grado di collegarci il parametro d’ingresso “tainted” con la
“y” passata in “callFoo”.
Intra procedurale – local data flow, il modello controlla una sola funzione come l’esempio di prima.
Inter procedurale – global data flow, il modello controlla il flusso attraverso le varie chiamate di
funzione
La classe Node rappresenta gli oggetti del nostro programma, quindi può essere una variabile/un method
access/etc. è come se fosse una classe padre. Poi vi è il predicato localFlow il quale, a partire da due
elementi, ci dirà se sono collegati dal dataflow.
localflow (tainted , y ) sarebbe un predicato vero o falso? Sarà vero perché CodeQL ha già creato un DB e
ha visto che tainted e “y” sono collegati.
Vi è poi anche “localflowStep” che controlla se due variabili sono immediatamente collegate tra loro.
Dataflow graph node NON SONO NODI AST. Abbiamo bisogno di mappare i nodi attraverso l’uso di
predicati.
Vediamo ora l’ultimo esempio riguardante proprio la taint analysis. Abbiamo a disposizione la classe
TaintTracking con un altro predicato “localTaint” che ci permette di capire se due variabili sono taintate tra
loro. Ma prima di tutto, perché non usiamo il Dataflow e abbiamo bisogno di una classe ad-hoc? Perché
la taint propagation ha a che fare con gli input esterni e quindi è un flow particolare, vogliamo i flussi
dall’attack surface.
Nelle slide poi vi sono tutte le librerie usate ed implementate in CodeQL.
Iniziamo adesso con il nostro esempio, anticipiamo che dovremo creare la nostra sottoclasse di
TaintTracking e dovremmo poi creare dei predicati (questo per creare dei source e dei sink).
Supponiamo che “mySource()” e “mySink()” siano la mia sorgente e la mia destinazione dei dati.
Domanda: c’è della taint propagation nel programma? Sì, giustamente prendo un dato dall’esterno però c’è
comunque della sanitification e quindi vi è una protezione.
Stiamo cercando, dove c’è “exists”, un MethodAccess “ma” tale che il nome sia “mySource” e la classe deve
vare un nome “myClassA”.
Possiamo poi aggiungere un altro predicato chiamato “isSanitizer” in modo da filtrare i flussi del dataflow
che rispettano determinate regole. L’obiettivo è che se non tenessimo conto di questi controlli ci
troveremmo tantissimi falsi positivi.
A 1:46:20 fa vedere l’esempio su visual studio.
3. Installare l'estensione di Visual Studio Code per CodeQL (tramite la sezione "Estensioni", oppure
tramite il link https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql)
4. Clonare sul proprio computer la cartella di lavoro per CodeQL, tramite il comando: git clone --
recursive https://github.com/github/vscode-codeql-starter
5. Aprire il file "vscode-codeql-starter.code-workspace" (si trova nella cartella creata dal passo
precedente) tramite Visual Studio Code
Vediamo oggi l’esercizio di analisi statica sul software U-Boot, il quale è scritto in C e non cambia molto da
quanto visto durante il corso con Java. U-boot è un bootloader che prende, quando accendiamo una
scheda, l’immagine del SO dalla rete e in questa vulnerabilità di oggi se l’attaccante ha il controllo del
server; può sovvertire il processo di boot ostacolandolo.
In particolare, la vulnerabilità riguarda l’uso di memcpy , l’analisi che faremo sarà di tipo taint propagation.
Nel codice troveremo le classiche configurazioni delle socket in C, infatti quando dobbiamo analizzare un
pacchetto di rete i byte in esso sono ordinati in modo diverso rispetto al processore (Little endian & big
endian) e la funzione ntohl sta per conversione net to host.
Vogliamo quindi scrivere query che collegano questi due punti del programma.
Per prima cosa ci iscriviamo su git hub e ci logghiamo per poter partecipare alla U-Boot Challenge e andare
sul sito mostrato sul PDF.
La challenge crea un repository sull’account, indifferente se pubblico o privato, nella voce issues c’è un
messaggio automatico che spiega i primi passaggi.
Usare le pull request (PR): ovvero un modo per contribuire in maniera più veloce ad un progetto, il flusso di
lavoro tipico prevede il branch nel quale facciamo tutte le modifiche e poi una volta finito facciamo la pull
request. La richiesta andrà ad una persona che la valuta e permette poi la modifica.
Noi impersoneremo sia il controllore che quello che modifica, per abilitarlo sconsiglia l’uso della linea di
comando. Usiamo VSCode per creare un branch (lo vediamo in basso a sinistra, c’è scritto “main”) e ci
facciamo poi anche i commit etc.
Quando facciamo una Pull request scatta una git hub action che prende la query e la esegue sul progetto.
Se quest’ultimo funziona apparirà un bottone verde con scritto “merge pull request”.
Per importare il database scaricato bisogna andare su VSCode, casella QL -> From an archive. Quand’è tutto
pronto facciamo “close issue” e ogni volta che facciamo close esce il prossimo step.
Alcune volte potrebbe presentarsi il problema di non riuscire ad effettuare le pull request, questo è un
problema proprio di git hub ma tranquilli che lo stesso si può andare avanti con l’esercizio.
Se continua a presentare problemi sembra essere dovuto alla versione del punto 4 ad inizio lezione: “git
clone --recursive https://github.com/github/vscode-codeql-starter”, per sicurezza sulla VM possiamo
cancellare la cartella “codeql-starter” e riscaricarlo.
Testing
Analisi statica
Vulnerabilità
In maniera professionale, enterprise.
Siamo agli albori degli anni 90 e fare sicurezza voleva dire fare due cose:
1) Sicurezza di rete
2) Sicurezza di sistemi
L’approccio unico alla software security era il penetrate and patch ed era un approccio reattivo. Questo
significa che il software veniva sviluppato nel modo classico tramite Waterfall o i processi incrementali, poi
prima del rilascio si faceva penetration testing e quindi si cercavano vulnerabilità software.
L’altro approccio, sempre legato al modello reattivo, è far uscire il prodotto e scoprire i problemi dopo
l’uscita.
Questo approccio si chiama reattivo perché non ci stiamo anticipando con le attività di security.
Ovviamente quest’approccio è pessimo, se vediamo i costi ci rendiamo conto di qual è il danno per
un’azienda. Non solo vi è il problema dei costi ma vi è anche il problema delle patches che vengono
rilasciate in fretta e furia e possono tranquillamente introdurre nuove vulnerabilità o problemi. Infine,
l’ultima problematica è che la patch non è sempre installata dai clienti, facendo così moltissimi impiegano
ancora software danneggiati e non funzionanti.
Ma perché non si aggiornano i dispositivi? Eh, il conto è aggiornare la nostra macchina e un conto l’azienda
che ne ha un centinaio. Si parla infatti di Patching resource-intensive, e tale attività risulta in una riduzione
dell’availability del sistema e del servizio. Il NIST per gestire gli aggiornamenti ha proprio un documento
compilato proprio usando le tecniche di risk management; quindi, componente per componente si vaglia
se:
Microsoft SDL
Ad un certo punto della storia Microsoft faceva prodotti che a livello di sicurezza erano i lucchetti della
Kikko; quindi, Bill Gates sprona tutti i livelli dell’azienda a darsi come priorità la security.
Da questa situazione si è avuto il ciclo di sviluppo di Microsoft chiamato Microsoft Security Development
Lifecycle (SDL).
Quindi si è passato da un approccio reattivo, all’approccio proattivo.
1. PROVIDE TRAINING: gli ingegneri, gli sviluppatori, devono conoscere le vulnerabilità e gli aspetti
tecnici. L’istruzione però riguarda anche tutta l’azienda, tutti i livelli gestionali devono essere al
corrente degli obiettivi e delle prospettive di un attaccante, tutti devono conoscere i possibili rischi
di sicurezza. Ogni volta che escono nuovi attack vector bisogna conoscerli, quindi bisogna essere
sempre aggiornati.
2. DEFINE SECURITY REQUIREMENTS: per fare un software bisogna avere dei requisiti di sicurezza.
Come al solito si parte sempre dai requisiti funzionali e li estendiamo con requisiti di sicurezza. Altre
fonti per i requisiti sono gli standard (se ad esempio siamo nell’ambito finanziario, si utilizza lo
standard PCI DSS dedicato alle carte di credito). Altri requisiti possono provenire da incidenti
passati, threats noti.
Vediamo un attimo un approccio, chiamato Abuse Cases e serve come estensione dell’UML. Si parte quindi
da uno Use Case tradizionale, immaginiamo un E-commerce store con due utenti con i loro casi d’uso.
In un Abuse case si aggiungono i security threats (li disegniamo fuori dal rettangolo e con un altro colore, e
non sono use case). Si ragiona in termini di attaccanti. L’omino “unmotivated admin” non è malevolo ma
può comunque influire sulla security.
Ai requisiti normali, ci aggiungiamo requisiti si sicurezza: si aggiungono nuovi use cases legati alla security,
che vengono chiamati countermeasures. Oppure come visto a SSD si parla di security control del NIST (che
nascono dagli use cases gialli).
Vediamo altri esempi: aggiungiamo il monitoring per tenere traccia di tutti gli ordini e quindi posso
ricostruire il tentativo di un ordine illecito, oppure posso in tempo reale rilevare un tentativo di truffa.
Questa è ad esempio una forma di detection.
3) DEFINE METRICS AND COMPLIANCE REPORTING: “Se non puoi misurarlo, non puoi migliorarlo”, se
non abbiamo delle metriche di security non riusciamo a capire se stiamo bene così o no. Si
utilizzano le key performance indicators (KPI), ovvero delle metriche che possono essere tenute
sotto controllo per vedere se lo sviluppo sta procedendo bene. Ad esempio, quando viene trovato
un bug, è importante segnarsi che si è trovato un bug, per capire quali bugs sono stati trovati e
quali sono stati ancora risolti o meno. Non sappiamo niente dei bugs non noti, ma almeno
contiamo quelli noti. Un’altra cosa è tenere traccia dei security work items, ovvero quale dei
controlli sono stati fatti e quali noi. L’obiettivo è l’accountability: lo potremmo tradurre come
“responsabilizzare” le persone.
Vediamo qualche esempio: qualunque software open source deve avere la lista di bugs aperti.
Questo di seguito è OpenStack.
Supponiamo di avere un diagramma UML del sistema, e notiamo che c’è un componente che potrebbe
avere SQL injection, che cosa faremo? Faremo la threat analysis e quando facciamo il testing, facciamo sql
injection su quel componente (ci viene detto quindi quale componente dobbiamo testare e quello che
dobbiamo testare). STRIDE è un approccio per trovare i threat, le minacce. Microsoft propone di effettuare
il diagramma dei componenti, fare un’analisi componente per componente e per ognuno farsi sei
domande: può l’oggetto essere soggetto a
Un altro approccio è STRIDE PER INTERACTION: per ogni flusso di dati (quindi per ogni freccia) si possono
avere quei problemi oppure no, ovviamente essendoci molte più frecce che palle l’operazione sarà più
laboriosa.
L’esito di questa modellazione è fare Risk Analysis o Threat modelling analysis e per ognuno di questi rischi
dobbiamo dare un indice di rischio dato da impatto e likelihood.
Viene dato un valore tra low e high, un numero solitamente tra 1 e 5, per capire quali sono i rischi
importanti e quali meno importanti.
Il prodotto tra impatto e likelihood rappresenta l’indice di rischio: il risultato mi fa capire cosa devo fare, in
quanto posso accettare il rischio (quindi non faccio niente), posso mitigare il rischio, oppure posso trasferire
il rischio (pago qualcuno per prendersi cura di quel rischio), oppure faccio testing per prevenire.
Esistono poi altri approcci e metodi per la threat modelling, sono presenti sulle slide per chi volesse.
5) ESTABLISH DESIGN REQUIREMENTS: c’è un abuso di terminologia, traduciamolo come “bisogna
progettare bene”. Dobbiamo fare un design sicuro:
Questo è un elenco di requisiti, dei principi di progettazione sicura e questi non si applicano tutti bene al
giorno d’oggi.
Ad esempio, “economy of mechanism”, si intende parsimonia nello sviluppare soluzioni: i nostri secure
control non devono essere troppo complessi altrimenti diventano anch’essi vulnerabili. Nelle web
application abbiamo parlato delle espressioni regolari, per fare la validazione dell’input, che possono essere
soggetti a denial of service. IPSEC è molto famoso in negativo, un protocollo per fare traffico IP in modo
sicuro.
Oppure “open design”: il design deve essere aperto, cioè mettere delle password cablate è una violazione
dell’open design. Si parte dall’idea che un attaccante conosca il codice ed il design e potrebbe scovare la
password nascoste nel sorgente. Stesso discorso valido per la crittografia: gli algoritmi di cifratura sono
aperti, l’unica cosa che non è nota è la chiave.
Oppure “least privilege”: un’applicazione non deve fare più di quello che dovrebbe fare; quindi, diamo il
minor privilegio possibile ai componenti software.
Oppure “fail-safe defaults”: se qualcosa va storto, un utente si sta loggando ma capita un’eccezione, nel
dubbio, fail-safe, non lo facciamo entrare.
Noi come ingegneri abbiamo degli utensili importanti da dover utilizzare per mitigare il rischio, li abbiamo
visti anche già a SSD:
6) DEFINE AND USE CRYPTOGRAPHY STANDARDS: Non è banale perché non bisogna fare crittografia
a caso, bisogna sceglierla con cura e in modo appropriato. Non creare algoritmi casuali fai da te, le
librerie di cifratura sono complesse da scrivere, è importante fare riuso. Come fare a capire se
librerie e algoritmi sono aggiornati? Per prima cosa tenere occhio a ciò che dicono le guide di
Microsoft; secondo consiglio, è utilizzare analisi statica che guarda il sorgente e ci può avvertire che
stiamo utilizzando una libreria o un package non aggiornato.
7) MANAGE THE SECURITY RISK OF USING THIRD-PARTY COMPONENTS: non scriveremo mai un
software da zero, ma utilizzeremo componenti di terze parti. Ovviamente anche quest’ultimi
possono essere vulnerabili: possono esserlo già o possono esserlo in futuro. Come si fa? Microsoft
suggerisce innanzitutto dell’avere un inventario di cosa usare; ci vuole un plan to respond, quindi
sempre i responsabili devono avere un piano di contromisure, devono sapere cosa fare se un
componente diventa vulnerabile (disattivare una funzione, fare backup e tornare ad una versione
precedente); bisogna usare tool di software composition analysis (SCA), ovvero software che ci
dicono quali sono le dipendenze nel software, se ci sono vulnerabilità note (ci dicono se utilizziamo
versioni obsolete di una libreria), inoltre fanno license compliance (ad esempio GPL è la licenza di
Linux, se noi lo modifichiamo, siamo obbligati a pubblicare il codice che abbiamo modificato come
open source).
8) USE APPROVED TOOLS: bisogna fare attenzione ai compilatore e ai tools di sviluppo che usiamo.
Oggi una fonte problematica sono proprio i tools di sviluppo: gli attacchi che a tal proposito hanno
luogo si chiamano Software Supply Chain Attacks. Alcuni malware non attaccano il prodotto
mentre sta eseguendo su un cliente, ma lo sviluppatore del prodotto: tentano di infettare la build
infrastructure, ad esempio fanno il commit di una modifica malevola in un progetto open source,
mettono una back door in un processore, furto di certificati o account di uno sviluppatore. Il più
famoso di questi attacchi è SolarWinds: SolarWinds è un’azienda americana che fa monitoraggio
delle reti, qualcuno ha infettato il loro prodotto (si pensa la Russia).
9) PERFORM STATIC ANALYSIS SECURITY TESTING (SAST): effettuiamo analisi statica, in quanto è utile
per trovare algoritmi di cifratura deprecati, numeri casuali deboli.
10) PERFORM DYNAMIC ANALYSIS SECURITY TESTING (DAST): fare fuzzing, ovvero forme di analisi
dinamiche per trovare vulnerabilità e quindi eseguire il programma. A differenza del SAST possiamo
trovare problemi come l’integrazione dei componenti, che a livello statico è quasi impossibile.
11) PERFORM PENETRATION TESTING: effettuiamo il penetration testing, un tipo di analisi manuale (in
contrapposizione al SAST e il DAST che sono automatici) in quanto c’è una persona che prova a
forzare il nostro prodotto. Permette di trovare problemi di progettazione. Per fare il testing ci sono
due approcci:
o testing tradizionale sui security control: OWASP ha un documento su questo, come fare
testing del login, dell’autenticazione, dell’encryption.
o risk-based testing: se abbiamo trovato un componente che può avere una vulnerabilità,
faccio un testing guidato dal rischio; quindi, mi baso sulla threat analysis fatta prima.
12) ESTABLISH A STANDARD INCIDENT RESPONSE PROCESS: cosa succede se troviamo una
vulnerabilità? Chi deve vedere gli aggiornamenti? Come distribuirli? Bisogna darsi un piano prima di
rilasciare un prodotto per ridurre la finestra temporale della vulnerabilità.
Security Touchpoints
SAFECode: da un enfasi sull’uso di pratiche di scrittura codice sicura dandoci una serie di consigli su
come usare Java, C++, etc. Dice le API sicure e quelle non sicure; suggerisce quali linguaggi e quali
framework utilizzare in base a quello che dobbiamo fare.
L’anno scorso Biden ha fatto un decreto sulla cyber security:
Un esempio di queste 36 voci: “come sviluppatore voglio che i buffer siano tutti controllati”. Poi dice chi le
deve risolvere: il progettista, lo sviluppatore. Poi dice cosa fare: fare analisi statica, oppure un compilatore
con funzionalità contro buffer overflow, oppure evitare l’uso di primitive non sicure.
Microsoft ha pubblicato una sua versione, DevOps: ci dice di introdurre analisi statica e dinamica e
integrarle; fare in modo che i tool siano facili da usare; evitare i falsi positivi; mantenere al sicuro le
credenziali, quindi evitare che il codice abbia hard-coded password, usare dei tools appositi e non metterle
insieme al sorgente; fare lo scanning dei file di configurazione; monitoring continuo. Il monitoring continuo
ci serve soprattutto per gestire i falsi positivi.
Esempio: GitHub, se andiamo nella configurazione di un progetto troviamo queste due opzioni:
“secret scanning” significa che GitHub manda un alert se trova nel sorgente una password di qualche tipo
(ovviamente non è che le trova completamente tutte) però chi gestisce gitHub sa com’è fatto JDBC e sa
quali sono le funzioni che usano password. Fa analisi statica per trovarle.
“push protection”, ogni volta che proviamo a fare un push lui se trova un problema simile al precedente,
non ce lo fa fare: se carichiamo una credenziale e vogliamo cancellarla da GitHub è una mazzata in fronte
perché c’è la cronologia dei commit, ci sono delle cash, tantissimi punti dove viene copiata.
Sempre riguardo GitHub sappiamo che è interessato al DevSecOps, ovvero lo sviluppo sicuro, dove le
attività di testing non sono fatte solo nella fase di rilascio ma vengono anticipate il più possibile. Shifting
left, significa anticipare le attività di security.
Consigli per la DevSecOps: ci sono degli standard di maturity model che elencano le cose da fare per
arrivare ai vari livelli di sicurezza. Quindi in un’azienda che non sa fare security, osservano l’immagine,
vediamo cosa serve per arrivare al primo livello e partiamo da lì e così via. È un vero e proprio mini-tool che
analizza le nostre repository per dirci dove siamo forti e dove no.
GitHub ha pubblicato una guida per arrivare al livello 1 di security utilizzando GitHub: “usate l’analisi statica
di default, abilitate i tool di fuzzing, e fate degli scan almeno sul branch master periodicamente e non ad
ogni commit, ad esempio una volta al mese o una volta ogni due settimane”.
Inizieremo a usare questo materiale nella esercitazione dell'11 Maggio. Entro quel giorno, per favore
scaricate la macchina virtuale, e installate il framework Metasploit sul sistema operativo host del
vostro computer (cioè non dentro la macchina virtuale).
Vi chiederei anche di portare con voi il vostro computer o tablet alla lezione del 6 Maggio (questo
venerdì). Non si tratterà di una esercitazione, ma consulteremo insieme dei documenti.
Introduciamo un concetto di “intelligence”: si intende capire il nemico, ed è un concetto molto ampio, non
si tratta solo di informazioni tecniche di basso livello, ma vogliamo capire qual è la strategia dell’attaccante,
i suoi obiettivi, le risorse che ha a disposizione. Se conosciamo queste informazioni è più facile combattere il
nemico (intelligence theory).
Per dare una definizione più formale, l’intelligence, è la capacità di un’organizzazione di prevedere un
cambiamento (qualcosa di ostile che ci viene rivolta contro) in tempo utile per fare qualcosa.
A noi interessa l’intelligence nel contesto cyber, per cui negli ultimi anni si è introdotto il concetto di CTI
(Cyber Threat Intelligence), che consiste nel collezionare quest’intelligenza per fare operazioni pro-attive
contro un attaccante, cioè vogliamo conoscere il nemico prima che faccia qualcosa contro di noi.
Bisogna raccogliere dei dati e fare delle considerazioni, ma da dove li prendiamo questi dati? La CTI può
partire dall’OSIntellingence ovvero, l’attaccante va a vedere quali sono i domini che abbiamo registrato, i
dati che pubblichiamo sul sito, tutto quello che è pubblico. Noi possiamo applicarla a noi stessi per capire
cosa gli attaccanti sanno di noi. Una volta raccolti i dati, li mettiamo tutti insieme e possiamo ottenere una
threat intelligence, in modo da raffinare le informazioni raccolte, capire magari quali punti specifici sono
stati attaccati in modo da cercare di prevenire attacchi futuri.
Intelligence è un ambito molto teorico ma vediamo gli attributi chiave che devono essere:
Potremmo quindi definire brevemente l’intelligence come dati di alta qualità. Oggi viene gestita con tool e
protocolli ben definiti e tipicamente la threat intelligence contiene:
Poi abbiamo l’intelligence tattica, qui siamo a livello più tecnico: per esempio, informazioni sulle
metodologie, i tools e le tecniche utilizzate dagli attaccanti.
Poi abbiamo un’intelligence di ancora più di basso livello, in cui saremo noi all’inizio della nostra carriera,
detto anche di breve termine, che riguarda le informazioni per fare monitoraggio.
Threat actors
Ne abbiamo già parlato, con threat actors facciamo riferimento a gruppi che si occupano spionaggio, di
crimini sul web. Invece con advanced persistent threat (APT), ci riferiamo a gruppi strategici che sono delle
vere e proprie aziende di cybercrimine, gruppi avanzati.
Come si procede per usare la CTI? Innanzitutto, si fa un piano, si raccolgono dei dati su cui fare delle analisi.
Ovviamente, l’approccio potrebbe essere “ok attivo tutti i log possibili, raccolgo dati all’infinito”, ma non è
detto che sia l’approccio migliore; è un approccio buono perché raccolgo, ma avrei problemi di volume, il
costo di storage sarebbe imponente, ma soprattutto, dovrei essere sicuro di aver raccolto tutto quello che
mi serve e non solo cose superflue. Quindi vi è prima una fase di auto-analisi per capire se il monitoraggio
che stiamo facendo bisogna modificare qualcosa.
Tutte le operazioni sui file critici, sui database, tutti i punti critici sono ben osservati? Se sì, poi ci chiediamo
quali sono gli attaccanti più probabili e a cosa sarebbero interessati.
Ci sono aziende che sono miei clienti e che possono essere attaccate? Magari faccio software per Enel,
vogliono attaccare Enel, ma attaccano prima me che sono un’azienda più piccola. Poi cerco di capire se
aziende simili a me in passato hanno subito degli attacchi ed eventualmente come si sono comportati.
Fase numero due, preparation and collection, per raccogliere dati ci possiamo auto-scansionare le reti al
fine di capire quali sono le informazioni pubbliche nostre all’esterno; oppure possiamo raccogliere gli
indicators of compromise ed uno in particolare sono i malware (ad esempio, gli allegati malevoli che
vengono inviati attraverso phishing: li monitoro una volta che scopro che uno è sospetto, cerco di
analizzarlo) utilizzo un approccio proattivo piuttosto che filtrarli semplicemente.
Andare per blacklist sarebbe troppo riduttivo, perché una volta messo qualcosa in una blacklist l’attaccante
crea qualcosa di nuovo per ri-provarci superando il blocco. Invece se opero così io analizzo per evitare
proprio di riceverlo nuovamente.
Indirizzi IP
Nomi di dominio
Valori di hash (impronta digitale dell’attacco che ho ricevuto)
Artefatti host/network
Fonti dove viaggia questo tipo di threat intelligence è VirusTotal che contiene un database di virus, il quale
scannerizza file e ci fornisce una serie di informazioni sul file. AlienVault è molto simile che contiene
campioni di malware, testimonianze degli utentis, screenshot
Lo scenario è molto ricco: ci sono questi siti web come VirusTotal, oppure piattaforme che non sono
pubbliche, ad esempio MISP che è un progetto opensource. Tipicamente le aziende di un certo settore, si
“federano” tra loro, e MISP permette di scambiare le informazioni di threat intelligence, così se io vengo
attaccato ho che gli altri ricevono le informazioni da me in modo da evitare che accada anche a loro.
Tra questi vediamo Digital Shadow, dall’immagine vediamo come siano pregni di informazioni. Vengono
mostrati gli attacchi più recenti, le cybergang più attive e gli attacchi alla quale la mia rete è o è stata
sottoposta.
Un concetto importante è che gli IOCs non sono tutti quali, ovviamente c’è una gerarchia d’importanza e
per questo esiste la “piramide del dolore” per gli attaccanti. Ci sono gli hash, gli indirizzi IP, i domini, fino
ad arrivare su ai TTPS.
Se blocchiamo un certo tipo di IOC, ad esempio un hash values, abbiamo che per un attaccante è un
impatto di tipo minore e può superarlo facilmente; quindi, la piramidi del dolore fa riferimento allo sforzo
che un attaccante deve fare per modificarsi e aggirare. Se riusciamo a scoprire quali sono i tools che
l’attaccante usa nei suoi malware, io posso usare tecniche più potenti per cui l’attaccante avrà molta più
difficoltà a raggirarmi. Se scopriamo i TTPs (Tecniche, Tattiche e Procedure) l’attaccante deve inventarsi
completamente un altro attacco (li vedremo nel dettaglio).
Ora per il resto della lezione ci concentreremo sulla parte di processing and exploitation; quindi, dopo aver
raccolto IOC di basso livello saliremo in alto nella piramide, si utilizzano dei framework per strutturare le
strategie:
Cyber Kill Chain (CKC) model
Il primo, Cyber Kill Chain, è un modello per descrivere gli attacchi: secondo questo modello, ogni attacco è
fatto da sette fasi (anche se qualcuna ogni tanto può essere saltata).
Diamond model
Il secondo modello, Diamond model, è un modello molto ad alto livello ed è realizzato da quattro elementi:
Avversari
Capability (si intendono i metodi usati, la parte strategica)
Infrastruttura (si parla di tutto quello che è tecnologico, a differenza della capability: tool, server,
indirizzi e-mail)
Vittima
Viene usato nei report ad alto livello per descrivere degli attacchi: si parte da uno qualunque di questi
quattro punti e bisogna navigare questo diamante.
Facciamo un esempio concreto: Stuxnet è l’attacco alle centrali nucleari iraniane. Questo malware proviene
da Israle e dagli Stati Uniti. Il malware è arrivato tramite pen USB (infrastruttura). Sono state usate delle
vulnerabilità per compromettere vari nodi (capability). La vittima era la centrale iraniana.
Un altro esempio è quello di WannaCry
MITRE att&ck
L’ultimo modello, ma quello più utilizzato, è MITRE ATT&CK e molto dettagliato e guidato soprattutto
rispetto ai precedenti. È una specie di banca dati che elenca tutte le tecniche e le tattiche usate dagli
attaccanti.
A cosa serve? Abbiamo molti rettangoli bianchi, altri blu, altri gialli. La legende ci da due nome di gruppi APT
Ci sono rettangoli di vario colori perché dalla legenda vediamo la presenza di due gruppi che hanno delle
tecniche differenti:
Carbanak azzurro.
FIN7 rosso.
Ad esempio, la terza colonna è “persistent” ed indica tutte le tecniche che un attaccante utilizza per restare
in modo persistente in una rete, se sono segnate in rosso vuol dire che è usata da FIN7. È importante
studiarlo in quanto è la lingua per parlare tra analisti; esiste in diverse edizioni, mobile, standard o per
sistemi industriali.
In totale sono 12 colonne, quindi abbiamo 12 tattiche (attività malevole) all’interno delle quali abbiamo
decine di tecniche:
Esiste anche la matrice di pre-attack, che contiene quello che viene fatto prima di un attacco.
Un lateral movement significa che dopo aver infettato un nodo, mi muovo per arrivare ad un’altra vittima,
passando per altri nodi (Ad esempio fare lo scanning con nmap)
Ogni tecnica ha delle sotto-tecniche, identificate da numeri. Per ogni tecnica troviamo le mitigation, quindi
come proteggerci, prodotti software utili per fare quell’attacco, gruppi APT famosi legati a quello.
Iniziamo capendo che parte attaccando il nostro browser e anche se utilizziamo https, lui legge le password
prima di utilizzarlo. Anche se le password non sono scritte dalla tastiera, lui le legge lo stesso, quindi
probabilmente le legge dalla memoria. Fa screenshot, fa dei logging e credential stealing.
Andando avanti dobbiamo completare l’analisi da soli usando sempre MITRE ATT&CK, aiutiamoci con
l’analisi usando il link del navigator. Ovviamente nel testo cerchiamo delle keyword come persistence,
execute, gather, e send oppure menzionano elementi tecnici come DLL e Registry key.
Il testo inizia dalla slide 52.
Astaroth malware
Quello che vedremo oggi è una campagna che, nonostante sia chiamata tecnica fileless, presenta dei file
ma nel senso che impiega già i programmi presenti nel SO.
Per capire meglio vediamo un esempio su Linux. Immaginiamo che un malware debba scaricare un file da
internet e invece che scrivere un programma che fa il download, il programma chiama una shell e fa un
wget senza creare una socket o che.
In più questo tipo di attacco è più difficile da rilevare proprio perché non vi è un exe, che spesso sono i primi
ad essere bloccati. Altri nomi con il quale è conosciuto questo tipo di tecnica è: living off the land binaries
(LOLBins) oppure Living off the land binaries, scripts and libraries (LOLBAS).
L’obiettivo dell’esercizio di oggi è replicare il malware che usa queste tecniche, nello specifico le prime tre
presenti in questa lista:
Alternate Data Streams (ADS), è una funzione del File System e con questa funzionalità posso
nascondere un file nel FS senza che qualcuno lo veda.
ExtExport.exe
BITSAdmin, serve per fare manutenzione
Windows Management Instrumentation (WMI), non lo useremo ma per chi vuole si può
approfondire.
Ma vediamo di capire come funziona l’attacco. Come al solito partiamo dal fatto che sarà arrivata una mail
di phishing o un altro attacco di social engineering, la vittima clicka sul file .lnk (collegamento windows),
l’attaccante ha una sua macchina predisposta all’attacco con un server web con la porta 80 e la porta 4444
aperte.
La vittima esegue il programma che prende il nome di “dropper” che scarica ed esegue una prima parte del
malware chiamato “stager” dalla porta 80, poi lo stager scarica a sua volta il DLL (vedremo nelle prossime
lezioni che cosa è). La DLL viene eseguita e realizza la connessione alla reverse shell.
Quindi per l’attacco dobbiamo prima scrivere un file .bat -> che genera un .vbs -> che genera un .lnk ora il
lnk viene lanciato e scarica stager.cmd (che scriveremo noi) sulla porta 80 e il .dll. Il .vsb nasconde i file nel
File System e crea a sua volta un altro lnk che esegue il .dll.
Per alcuni, se si presenta il problema con msfvenom per creare la DLL, usate il comando:
payload/windows/x64/meterpreter/reverse_tcp.
Se metasploit non viene trovavo o non funzionano i comandi, controllare che non sia stato messo in
quarantena dall’antivirus.
Lezione 18 13/05 (13-BasicMalware)
Oggi contrariamente a quanto visto ieri non costruiremo un malware ma lo analizzeremo ma per quale
motivo? Mettiamo caso che veniamo attaccati, se non ne capiamo la struttura non potremo mai essere
in grado di fronteggiare il malware. Tale operazione prende il nome di malware analysis
Malware analysis
Un malware è un software che viene utilizzato come strumento durante intrusioni informatiche (ad
esempio creare una reverse shell che consente all’attaccante di avere il controllo della macchina vittima). La
malware analysis la possiamo definire come “autopsia del software malevolo”, quindi scopriamo di essere
attaccati, non sempre in tempo reale anzi proprio il contrario, allora vogliamo dissezionare il malware per
vedere come sconfiggerlo o ridurre l’impatto dell’incidente (vogliamo scoprire quali macchine sono state
attaccati, quali dati, a chi sono stati inviati i nostri dati).
La settimana scorsa abbiamo introdotto la threat intelligence e in questo momento vogliamo capire qual è
il legame tra malware e threat intelligence? Noi vogliamo fare l’analisi del malware per scoprire qualcosa
sulla threat intelligence. Quindi ad esempio, dato un file malevolo, vogliamo capire se esso è specifico per la
nostra azienda o se fosse un attacco di massa; oppure, cosa comporta questo file malevolo; oppure, come
faccio a rilevare la presenza di questo file anche su altri computer della mia azienda; oppure, capire se
questo file ha informazioni sull’attaccante.
Secondo il NIST un malware è ciò che compromette le tre proprietà CIA della security.
Esistono tanti tipi di malware (un po’ come i pokemon e modestamente il mio pc li ha presi tutti): alcuni
sono generici altri hanno un significato specifico.
TROJAN, richiama il cavallo di Troia che contrariamente a quello che si pensa appartiene all’Eneide
(n.d.r): è un programma apparentemente utile ma che al suo interno ha del codice malevolo.
Quindi è un malware che si nasconde in altri programmi.
VIRUS e WORM, entrambi si replicano sulle macchine; il worm si replica in maniera automatica tra
le macchine, mentre il virus ha bisogno di replicarsi manualmente (ad esempio con l’utilizzo di una
chiave USB)
DOWNLOADER (o dropper): è un codice malevolo che ha come scopo l’operazione di scaricare un
altro codice malevolo
BACKDOOR: è un programma che apre un porta (ad esempio TCP) permettendo a qualcuno di
entrare sulla macchina infettata, fornendone l’accesso.
BOTNET: è un programma che recluta la macchina infettata e la fa partecipare ad altri attacchi, ad
esempio negli attacchi DoS
ROOTKITS: è un programma che cerca di fornire all’attaccante l’accesso, ed il mantenimento, in
modalità amministratore alla macchina attaccata.
Spesso però i malware possono appartenere anche a più categorie contemporaneamente di quelle citate.
In base agli obiettivi, distinguiamo due tipi di malware:
Mass malware (“a strascico”), non hanno un obiettivo specifico, infettano tutto quello che possono
infettare; fanno affidamento su servizi vulnerabili e laddove sono installati, attaccano tali servizi
senza avere una vittima specifica. Questa è la tipologia che solitamente viene riconosciuta dagli
antivirus.
Targeted malware e a questa categoria si fa spesso riferimento quando si parla di APT, si cerca una
specifica vittima e quindi sono malware ad hoc. Sono molto problematici perché gli antivirus non
sono pronti a riconoscerli.
Cosa faremo? Vedremo vai approcci di malware analysis, oggi ci focalizzeremo sulla basic static analysis,
ovvero vogliamo guardare i file malevoli e faremo considerazioni senza eseguirli. Ovviamente un’analisi
statica non basta, ma è una prima contromisura guardare solo l’eseguibile.
Poi c’è la basic dynamic analysis, ovvero eseguiamo il malware su un computer e vediamo cosa succede; è
una cosa pericolosa da fare quindi va fatto in un ambiente sicuro. Non è semplice da fare perché spesso chi
scrive i malware cerca di nascondere le informazioni per essere analizzato.
Poi faremo (nelle prossime lezioni), l’advanced static analysis, facendo proprio del reverse engineering:
andremo nel codice macchina per ricostruire il file cosa voleva fare. Richiede però delle skills avanzate.
Infine, esiste l’advanced dynamic analysis (non la vedremo), con la quale apriamo il malware durante la sua
esecuzione, come se facessimo operazioni di debug. Non è niente di fantascientifico ma semplicemente
non abbiamo tutto questo tempo.
Per quanto riguarda i tool che vedremo, sono presenti nella macchina virtuale che ci ha fornito lui.
Analizziamo gli antivirus, essi si basano sulle signature ovvero se Il file malevolo contiene un URL per pagare
un riscatto; la presenza di quell’URL è una sorta di impronta digitale di quel malware e noi potremmo
rilevare la presenza di quel malware in altri computer, cercando quell’URL. Quindi la signature è la stringa
“magica” che ci consente di capire se un file malevolo si trova anche in altri computer.
In certi casi le signature sono molto precise, ad esempio “www.malware.com”, oppure altre volte sono
approssimate e non molte precise.
Quali sono i problemi di questo approccio? Perché un antivirus non basta? Perché un attaccante può
modificare il codice in modo tale da non far riconoscere la signature.
VirusTotal consente di vedere se un file viene riconosciuto come malevolo o no.
Per quanto riguarda invece le stringhe dell’eseguibile immaginiamo di avere un programma scritto in C con
la printf e strcpy abbiamo che le parti in grigio sono dentro il file eseguibile.
Se noi aprissimo l’exe, troveremo in rappresentazione ASCII le parole “ciao” ed “hello” e possiamo leggerle
proprio per effettuare malware analysis. Per leggere le stringhe possiamo proprio usare i programmi Strings
e BinText, che dovrebbero essere già presenti sulle macchine, le stringhe ASCII sono fatte così: ogni
carattere è un byte.
In Windows ci sono anche le stringhe Unicode, che non è una codifica, ma è uno standard per enumerare i
caratteri. Nello specifico Unicode usa due byte per carattere:
Nell’esempio in alto riconosciamo un indirizzo IP, nomi come GDI32.DLL (che è una DLL di Windows), il
nome della funzione SetLayout (serve a stampare finestre, quindi probabilmente è un programma grafico),
e poi troviamo una stringa che ci fa capire che vengono utilizzate delle e-mail. L’operazione di far uscire
dei dati quando li rubiamo è chiamato exfiltration
Packed/obfuscated malware
Quando applichiamo Strings o qualche altra analisi sull’eseguibile abbiamo che spesso le stringhe
potrebbero essere offuscate, parliamo quindi di “malware impacchettati”.
Abbiamo un programma malevolo, ci sono delle stringhe, un packer (un tool che crea un malware packed)
prende l’exe e lo offusca, come se lo comprimesse, per cui la stringhe non sono più rappresentate come nel
file originale.
In questo si ottiene una sorta di compressione e non riusciamo a riconoscere le stringhe, ma come si fa ad
eseguire se non si riconoscono le istruzioni. Quindi come funziona? Il packed program è fatto di due parti:
una parte offuscata (Packed executable), ed una parte molto piccola che è in grado di decomprimere la
parte offuscata (wrapper program). In questo modo, l’eseguibile viene decompresso nella RAM ed
eseguito.
Esiste un tool PEiD che riconosce i malware impacchettati: quindi se analizziamo un exe e non troviamo
delle stringhe, e ci sono altri sintomi che vedremo tra poco, possiamo pensare di avere difronte un malware
impacchettato, allora ricorriamo a questo tool che ha un piccolo database di firme che gli consentono di
riconoscere i Wrapper Program.
Questo tool però è diventato famoso in negativo, perché spesso per dare in pasto i malware, finisce che li
esege e ovviamente prima abbiamo detto che effettuare l’esecuzione di un malware può comportare
numerosi problemi.
PE Executable files
Innanzitutto, impariamo allora come sono fatti i file exe? I file exe di Windows hanno tutti quanti lo stesso
formato, che si chiama PE (Portable Executable) ed è fatto dalle seguenti informazioni:
Un PE header iniziale
Info sul codice
Tipo di applicazione
Librerie di funzioni richieste
Spazio richiesto
L’exe, se lo aprissimo con NotePad vedremmo dei bytes, che non hanno molto significato, la prima metà di
questi bytes prende il nome di header mentre l’altra parte è definita sections (un header e tanti sections).
All’interno di queste sections, troviamo diverse cose: codice eseguibile, stringhe, imports.
Cosa sono gli imports? Tipicamente i nostri programmi non sono mai autosufficienti, nel senso che
utilizzano librerie o devono chiamare funzioni esterne. Ad esempio, supponiamo che nel nostro main,
nell’header, abbiamo “urlmon.dll” questa è una libreria utilizzata dal nostro main. Quando viene lanciato
l’exe, il sistema operativo va sul disco, trova “urlmon.dll” e lo linka dinamicamente. Quando viene avviato
l’exe, nella RAM viene copiato sia il contenuto dell’exe ma viene copiato anche il contenuto della libreria
dll, e poi devono essere collegati tra loro tramite un puntatore in modo tale che l’exe possa chiamare le
funzioni della libreria.
In questo modo possiamo capire ad esempio, che il nostro exe importa una funzione per scaricare file: ma
se l’exe è un programma per ascoltare la musica sul disco e non online, e scopro che c’è questa funzione,
c’è qualcosa che non va. Quindi ci può essere utili l’analisi di questi imports.
Mercoledì vedremo proprio i tool che vediamo durante la spiegazione, uno di questi è Dependency walker
che mostra gli import e gli export, più tutte le funzioni usate all’interno del programma dalla DLL (PI).
Alcune DLL sono di utilizzo comune, non sono sinonimo di qualcosa di malevolo.
Ad esempio Kernel32.dll serve ad allocare i file, uccidere i processi; oppure User32.dll serve per la grafica;
oppure abbiamo quelle che ci consentano a collegarci sulla rete; oppure Ntdll.dll solitamente non è
importate quindi forse se la vediamo dovremmo preoccuparci, perché implica operazioni avanzate che
solitamente i programmi non fanno.
Sia gli exe che i dll possono importare ed esportare; quindi, una dll può importare un’altra dll. Con
“esportare” intendiamo che può essere chiamata da qualcuno. Fondamentalmente in windows DLL e EXE
sono identici, cambia solo un bit nell’header.
Quali sono altri trucchi degli attaccanti per evitare l’analisi? Se l’attaccante non vuole fare vedere che usa
una determinata DLL che fa, come lo nasconde nell’header? Usa Kernel32, che permette di usare la
funzione “LoadLibary()” che carica una DLL senza dichiararla nell’exe. Di seguito vediamo il caso di prima,
dove nell’eseguibile troviamo come stringa il nome di una DLL.
Il fatto che abbiamo tanti import e tanti DLL è indice del fatto che il file non è impacchettato, se fosse
stato packed tutto questo non lo avremmo visto perché il Wrapper Program è molto piccolo ed importa
molto poco. Spesso di tutti gli import che vediamo sono pochi quelli che ci servono per comprendere le
informazioni utili, ovviamente non ci frega niente di “open/close dialogue()” che aprono e chiudono le
finestrelle.
Vediamo che ci sono funzioni FindFirstFile e FindNext che sono delle funzioni che permettono di cercare file
nel File system; oppure GetCurrentProcess e GetProcess vedono quali programmi sono in esecuzione sul
sistema; poi abbiamo delle funzione sui registri (RegisterClass oppure RegisterHotKey che dice a Windows
di innescare qualcosa quando si premono le combinazioni tipo control + shift + qualcosa); il programma usa
la GUI, si apre la finestrina nera della shell, ma non si apre nessuna interfaccia, questo potrebbe essere un
indizio perché magari questo file mi consente di sniffare le finestre del computer quando premo le
combinazioni che vengono riconosciute con il RegisterHotKey. Inoltre, la funzione SetWindowsHookExW (gli
hook sono delle funzioni che possiamo scrivere nel programma ma che non vengono mai chiamate dal
nostro stesso programma ma le può chiamare il SO quando preme una particolare configurazione di tasti)
prevede quando si muove il mouse o si clicca un tasto della tastiera, vengono chiamate due funzioni dal SO.
Molte funzioni hanno questi nomi strani, che includono “ExW” o “ExA” ma che significano?
“ex” è un suffisso e significa che la funzione sta deprecando un’altra funzione che è diventata
obsoleta. Ad esempio, windows usava una funzione CreateWindows() che è divenuta deprecata e
adesso si usa CreateWindowEx() che rende obsoleta la precedente. Questo serve per essere
compatibili.
“W” oppure “A” sempre usati come suffisso, indica il tipo di stringhe che la funzione prende in
ingresso (A sta ad indicare “ASCII”, W sta ad indicare ad esempio “unicode”, in quanto indica che la
funzione accetta “stringhe a caratteri larghi”).
Vediamo adesso un altro esempio, questa volta analizzando il file exe vediamo:
Questo potrebbe indicare un packed program, in quanto ci sono troppi pochi import.
PE Sections
Cerchiamo di approfondire la struttura di un PE file, quale altre informazioni utili ci può dare? Il file è fatto
dall’header ed altri sezioni. La parte “text” racchiude il codice macchina; data include i dati; rdata
racchiudere gli import e i dll; rsrc sta per “risorse” e racchiude come deve apparire l’esecuzione.
Per vedere queste sezioni e l’header usiamo PEview. A sinistra c’è una vista che ci fa vedere gli header e le
sezioni. Gli header sono divisi in più parti:
1. Le prime due parti sono legacy, quindi le ignoreremo spesso. Windows deriva da DOS e una parte è
dedicata ai DOS proprio. Se clicchiamo su quelli moderni chiamati NT_headers troviamo
informazioni come: quando è stato compilato file, che è un file a 32 bit, ci dice che il PE analizzato è
un exe in quanto è indicato il codice 0002.
2. Se clicchiamo su HEADER test, troviamo le informazioni sulla sezione testo: ci dice la dimensione
della sezione. La cosa interessante è che ci sono due dimensioni:
o Virtual size
o Raw data size
Che significa? Virtual size è la dimensione di quella sezione quando è caricata nella RAM, quindi
lanciamo il programma e il programma occupa tale dimensione. Invece, Raw data, ci dice quella
sezione quanto occupa sul disco. Si può vedere che sono dimensioni leggermente diverse, il che è
normale, il problema lo abbiamo quando sono molto diverse.
Cosa possiamo avere di anomalo? I file packed hanno delle sezioni che risultano essere vuote: per
esempio, nella figura in basso, vediamo che nella sezione “Size of raw data” l’eseguibile non ha un’area
testo, non ha un’area dati, ma invece nella RAM queste ci sono. Questo perché è packed e l’area testo
viene creata decomprimendo qualcosa. Probabilmente, l’eseguibile impacchettato si trova nella
sezione .sdfuok; il Wrapper legge questa sezione ed effettua la decompressione e quindi abbiamo l’area
testo e le altre. Guardare le sezioni allora ci fornisce degli indizi sui packed:
Per la sezione rsrc, ad esempio del programma calcolatrice: dentro l’exe c’è un’immagine con i bottoni
disegnati così. Al posto di questa roba un eseguibile potrebbe mettere un payload compresso. Quindi la
sezione resource è molto usata per nascondere codice in aree che non dovrebbero avere codice.
Esempi di tool per analisi statica sono Resource Hacker che ci consente di analizzare queste resource.
Altro tool è PEstudio, che contiene un po’ tutto quello che abbiamo detto. In realtà un valore interessante
di questo tool è entropy: l’entropia è una metrica di randomicità di un dato. Se ho un dato con tutti i bit
uguali, l’entropia è zero; se tutti i bit sono diversi tra loro, se non c’è nessuna ripetizione, l’entropia è 8
(perché un byte sono 8 bit). Perché è utile questo valore? Perché in un eseguibile solitamente l’entropia è
bassa, in quanto c’è una certe regolarità nelle operazioni e nelle stringhe in inglese. Se non ci sono
ripetizioni, e l’entropia dunque è alta, potrebbe essere sintomo di file packed.
Ci sono delle limitazioni in quest’analisi dinamica: alcuni malware non si attivano in una macchina virtuale
e quindi mandarli semplicemente in esecuzione non ci consente di vederlo in esecuzione. PER L’AMOR DEL
CIELO FATE L’ANALISI IN UN AMBIENTE SICURO, quindi macchina virtuale scollegata da internet e al
massimo collegato in modalità host-only. QUALCUNO PENSI AI BAMBINI. VirtualBox consente di utilizzare
gli snapshot, ovvero una istantanea dello stato della macchina in un determinato istante temporale. Infatti,
se avviamo il malware e questo rompe completamente il disco, dopo aver completato l’analisi io posso
tornare indietro nel tempo e fare in modo di poter ripristinare la situazione precedente alla rottura.
Il primo, Process Monitor, monitora il disco, i thread, i processi e tutto quello che c’è nel sistema Windows.
Usa la RAM per salvare tutti questi dati, è un tool che si riempie velocemente infatti va usato per intervalli
di tempo non troppo lunghi.
Ci consente di attivare dei filtri, per sniffare solo i registri, solo la rete. Ci consente di identificare le
connessioni per capire se si sta parlando con un server remoto.
Possiamo focalizzarci su certe specifiche system calls; oppure possiamo fare dei filtri ad hoc per vedere le
modifiche non di tutto il disco ma andiamo a monitorare le modifiche di specifiche cartelle sia in maniera
black list che white list. I filtri non cancellano i dati si possono sempre recuperare.
Un altro tool è Process Explorer, che ci consente di vedere i processi in esecuzione è come il task manager
di windows, non salva molti dati ma fa vedere le dll e altre cose come mostrato nell’immagine.
Se ad esempio abilitiamo le DLL entrando nella DLL Mode e andiamo nel menù, possiamo vedere le DLL
caricate da un programma e quindi anche quelle non presenti nell’header; oppure ci consente di verificare
le firme dell’eseguibile; possiamo vedere le strings sia sul disco sia in memoria visto che lo stiamo
eseguendo. Nel caso in cui tra l’immagine di un processo e quello che c’è in memoria ci siano delle
differenze sostanziali, potrebbe essere un indizio che qualcosa non quadra: questa tecnica si chiama
process replacement (ma la vedremo più avanti) o process hollowing.
Oppure Process Explorer ci consente, attraverso l’utilizzo di quella che viene chiamata handle table di
vedere tutti i file aperti dal processo, nell’esempio sulle slide sono aperte tre chiavi del registro e vediamo
anche i “mutant” che sono i mutex in Linux. Perché dovrei guardare i semafori/mutex? Perché se un
eseguibile malevolo viene lanciato più volte, le esecuzioni dei malware non si devono pestare i piedi tra
loro.
Un altro tool è Regshot che ci consente di comparare due snapshots del registro, tutto.
Durante le analisi dinamiche dobbiamo spegnere la rete per evitare che il malware si diffonda, ma non
dobbiamo spegnerla del tutto, perché magari ci interessa di capire cosa succede nei confronti della rete.
Allora si crea una fake network per vedere come il malware si interfaccia con l’esterno, se tenta di inviare
qualcosa tramite la rete.
Il tool INetSim, ad esempio, simula i protocolli più tipici e ad esempio fa finta di essere un server SMTP;
quindi, fa finta di ricevere una e-mail e il malware ci casca e prova a mandare quest’email che viene
intercettata.
Come abbiamo detto è un finto internet, quindi il finto server, chiama finti siti internet quindi il malware
potrebbe capirlo; però intanto le richieste sono uscite.
Questo tool riesce a fare fesso addirittura Nmap vedendo i porti ed i servizi aperti.
Sandboxing
Fatta poi sia l’analisi statica che dinamica, ci sono aziende che offrono la possibilità di eseguire i file exe e
fanno un report di cosa è stato trovato. Si parla in tal caso di sandbox.
Se apriamo i report, possiamo vedere che qualcun altro ha collegato questa scansione con altre scansioni:
abbiamo altre versioni di questo eseguibile.
Nell’Incident Response leggiamo le tecniche del MITRE utilizzate in quest’attacco: nell’esempio è stato
analizzato un ransomware. Ci consente di vedere anche gli host che vengono contattati:
Sulle slide c’è come fare uno snapshot, essi sono incolonnati perché ognuno è un aggiornamento del
precedente e quindi ogni snapshot successivo include tutti i file della versione precedente più i nuovi
salvati.
Per quanto riguarda l’esercizio ci fornirà dei programmi su gitHub dei quali non sappiamo molto e
dobbiamo capire cosa fa il malware e che caratteristiche ha. Per renderlo più stimolante vi è un quiz da
compilare.
1. Ovviamente facciamo l’hash del file e vediamo se qualcuno l’ha già analizzato e cosa si sa
2. Quando è stato compilato, se è un packed malware
3. Ci sono indizi su che tipo di malware è?
4. È un po’ prematuro ma mettiamoci nell’ottica di un antivirus, quali caratteristiche troviamo del
malware che ne permettono un successivo riconoscimento?
Una stringa a cui prestare attenzione è quella al minuto 41:21 chiamata “Kerne132.dll” perché Kernel32.dll
è la seria e buona e quella segnalata è praticamente un misleading name.
Perché facciamo questo richiamo? Quando riceviamo un eseguibile malevolo noi non abbiamo il codice
sorgente e una delle tecniche per capire cosa fa è andare a guardare il codice macchina. Facciamo un
esempio:
Chiamiamo una funzione, MessageBox (la printf di Windows che apre un pop-up, con titolo “hello word!” e
messaggio “a simple…”). Tipicamente il processore Intel quando chiama una funzione prevede un’istruzione
di call (che è un’istruzione di salto, e come parametro ha un indirizzo e quindi salta a quell’indirizzo), e poi
delle istruzioni di push (ognuna per i parametri passati alla funzione, quindi ne abbiamo quattro).
Con dei tool, che si chiamano disassemblatori, da quello che sta sotto otteniamo quello che sta sopra a
sinistra (push, push, push, push, call), e a tal punto proviamo a ricostruire quello che c’è sopra a destra
(MessageBox).
Assembler
Per fare l’occhio, consideriamo un programma identico a quello di prima con il MessageBox e ExitProcess
ed infine le due stringhe:
Invece degli indirizzi numerici, per rendere più leggibile un programma assemby sia quando dobbiamo
scrivere, sia quando facciamo reverse engineering, non mettiamo dei numeri grezzi ma delle parole
simboliche chiamate “label”.
L’obiettivo di un assemblatore è quello di passare da sinistra ad un file oggetto, che abbiamo detto essere
il contenuto di PE. Il PE è un vettore di byte, che viene preso dal disco e messo nella RAM.
Inizialmente il file oggetto è vuoto, l’assemblatore dipinge il contenuto di questo file oggetto e la salva su
disco e poi quando viene lanciato il programma l’immagine viene caricata dal disco alla memoria.
L’assemblatore ragiona leggendo un’istruzione alla volta e inserendo il codice macchina all’interno del file
oggetto che contiene l’istruzione da eseguire (6A = push) e poi inserisce i byte (in questo caso zero).
Andiamo avanti così: seconda istruzione e pezzetto di byte che vengono messi subito dopo quelli di prima è
un’istruzione di 5 byte, il 68 rappresenta la push, e poi ci vuole l’indirizzo della stringa (4 byte). In questa
fase l’assemblatore non sa ancora dove sta la stringa quindi ci mette i punti interrogativi. E quindi si
procede così fino alla fine:
Questa fase si chiama “prima passata” dove si mette quello che si può nel file oggetto. Ad un certo punto,
arriviamo nell’area dati, ovvero abbiamo istruzioni che non usano registri ma che mettono dati
nell’immagine di memoria. Ad esempio, il nostro programma deve utilizzare una stringa, come “Hello
world”, che deve essere messa da qualche parte. Per esempio, in questo caso il valore arriva alla posizione
0x403000 (61=A, 20=spazio ecc..). Allora l’assemblatore impara che esiste questa stringa MsgString e si
segna il suo indirizzo in una struttura dati che si chiama symbol table.
Quando abbiamo finito l’eseguibile, l’assemblatore si fa un altro giro e riempie i buchi in sospeso e abbiamo
la “seconda passata”. Quindi 0x403000 viene inserito nel suo apposito spazio (vediamo che viene scritto al
contrario perché è little endian).
Il vettore viene compresso nel file PE e quello che c’è sul disco è mostrato di seguito:
Il caricatore prende questo PE e lo carica nella RAM. Le sezioni nella RAM non sono necessariamente della
stessa dimensione del file PE, però fondamentalmente avviene una copia.
Disassembly
Per fare l’analisi, noi utilizziamo dei tool che sono “disassemblatori”; quindi, qualcosa che fa il lavoro
opposto di questo appena visto, cioè partono dal binario e ricostruiscono l’assembler. Noi utilizziamo IDA
Pro.
Noi ci focalizziamo su Intel x86, quindi 32 bit e poi se avremo tempo vedremo la x64. La scelta è dettata
anche per la semplicità degli esercizi.
X86 ISA
Intel x86 è un processore CISC (abbiamo quindi operazioni complesse, molte tipologie di istruzioni,
abbiamo molti modi di indirizzamento, gli operandi possono essere di tantissimi tipi). L’ottica era quella di
occupare meno memoria (abbiamo codifica a lunghezza variabile) e rendere le istruzioni più leggibili.
Non impariamo troppo a memoria i comandi o che, purtroppo ogni tool disassembla e mostra in maniera
diversa. L’esempio di seguito fa 2 + 2
L’assemblatore ad ogni istruzione abbina un indirizzo, questo per non utilizzare i valori numerici e si
utilizzano le label, che sono degli alias degli indirizzi. Si possono scrivere in due modi:
Le mettiamo all’inizio
Mettiamo i due punti e accapo
Global esporta il simbolo “_main” per fare collegamento e alcune label sono visibili al caricatore del sistema
operativo e ad altri programmi che vogliono chiamare questo eseguibile.
Tutti i programmi sono organizzati in sezioni: tipicamente ci sono il testo, i dati, la rdata (che serve per la
re-location) e il resource (dove ci sono immagini e cose grafiche).
Vediamo la section .data: viene detto all’assemblatore che deve essere messo il valore 2 in memoria che è
una double world (cioe 4 byte).
I tipi che possiamo usare su Intel possono essere:
Come funziona invece la move? Viene presa da destra verso sinistra, il contrario del motorola, e quindi la
destinazione sta a sinistra.
Abbiamo 4 byte da memorizzare, è totalmente irrilevante in quale ordine li disponiamo, ma ogni processore
usa il suo, e Intel è little endian. È importante perché quando facciamo l’analisi e troviamo un indirizzo:
Per gli operandi mettiamo i numeri o i nomi dei registri e le parentesi quadre sono come l’asterisco in C. Si
prende quindi un qualcosa, un registro, e lo si usa come un indirizzo dove prelevare un dato. Una cosa è
scrivere “eax” e un’altra è scrivere “[eax]”.
Quindi se scrivo “eax” copio il contenuto del registro (Ad esempio il valore 3); se invece mettiamo le
parentesi quadre, viene trattato come un puntatore (il processore va in memoria all’indirizzo [eax] prende
quello che c’è in quella locazione e lo copia).
Registri
I registri hanno questi nomi:
Adesso vediamo quelli più importanti.
I registri di dati, quelli generali con le lettere, sono a 32 bit ma possiamo anche utilizzarne solo un
pezzo. Di solito usati per conservare dati o indirizzi di memoria, sono intercambiabili e alcuni sono
particolari come EAX e EDX che vengono rispettivamente usati per moltiplicazione e divisione.
Quello che vedremo più spesso è EAX che contiene il valore di ritorno delle funzioni.
Alcuni registri hanno dei significati speciali per alcune istruzioni, noi possiamo usarli come vogliamo ma
alcune istruzioni danno dei ruoli particolari. Ad esempio, “ECX” viene chiamato loop counter: esistono
istruzioni che ci fanno leggere una stringa intera, per dire quanto è lunga una stringa, la lunghezza la
mettiamo in ECX.
Un esempio di istruzione CISC è movsd: è un’istruzione senza operandi perché gli operandi sono impliciti e
vengono presi direttamente il valore di esi ed il valore di edi per fare il trasferimento. Quindi deve essere
preceduta da due istruzioni di esi e edi.
Poi ci sono delle istruzioni particolari come rep, che non è un istruzione macchina ma è una parola chiave
e può essere messa insieme ad altre istruzioni. Quando l’assemblatore lo vede capisce che deve assemblare
movesb in maniera diversa, ovvero l’operazione va ripetuta come se facesse un loop. Quante volte deve
essere ripetuta? 13 volte, perché è il contenuto del registro ecx.
Il registro di stato è un registro speciale, che contiene vari bit separati gli uni dagli altri che si
chiamano flag e ci fanno capire cosa sta accadendo nel processore:
Ad esempio, c’è un bit, ZF (Zero flags) che ci dice se l’ultima operazione ha dato valore zero. Sono
quindi bit aritmetici, ad esempio memorizzano il riporto, il segno.
Mentre, l’EIP (Extended Instruction Pointer) dice al processore qual è l’istruzione da eseguire, infatti è
l’obiettivo degli attacchi di buffer overflow.
L’istruzione “lea” è una simil-move. Quando noi scriviamo “mov eax,[ebx+8]”, la move non trasferisce il
valore di ebx, ma trasferisce il contenuto della memoria all’indirizzo indicato (cioè all’indirizzo di ebx+8 c’è
qualcosa, lo prendo e lo trasferisco). Invece, se scriviamo lea non va in memoria, mette il valore del
puntatore in eax.
L’istruzione lea viene sfruttata per l’aritmetica dei puntatori per fare operazioni come le moltiplicazioni, più
velocemente. Facciamo un esempio per essere più chiari:
Il valore di eax con la move è 0x20. Perché ebx è 0x00B30040, se a questo aggiungiamo otto, otteniamo
0x00B30048, e troviamo proprio il valore 20. Invece, il valore di eax con lea: troviamo 0x00B30048.
Un’altra parola che vedremo spesso è PTR, ovvero pointer. Quanto noi facciamo un’istruzione come
riportato nella slide, dove copiamo un valore in un certo indirizzo abbiamo che normalmente il valore viene
considerato come una double word, quindi scriviamo 4 byte in memoria.
Se invece vogliamo scrivere esattamente un byte, possiamo usare il prefisso BYTE PTR
Abbiamo le operazioni matematiche
La NOP è la no operation, l’operazione nulla, che ci serve come zona di lancio per i nostri attacchi quando
non conosciamo la destinazione di un salto con precisione.
Lo stack è una zona di memoria cresce all’inverso, cioè cresce verso l’altro. Abbiamo visto anche la
questione del frame dove ogni chiamata ritaglia un pezzo di stack che contiene i parametri, i valori locali di
una funzione e l’indirizzo di ritorno
Vedremo due operazioni condizionali: vedremo test e compare i quali sono due modi di Intel per fare la
comparazione tra due operandi. Test effettua la AND tra i due valori che gli diamo, la cmp ne fa la
sottrazione.
Perché? In molti programmi troviamo test eax,eax, serve per capire se quel registro ha il valore 0, in
quanto essendo la AND, se facciamo la AND tra due valori che sono tutti zeri, il risultato è zero. Invece, la
cmp la utilizziamo per capire se una variabile è maggiore di un’altra, e quindi facciamo la sottrazione e
vediamo il risultato.
Poi abbiamo jump zero (jz) o jump not zero (jnz), tipicamente seguono la cmp o test e servono per fare i
cicli, gli if, etc.
Reverse Engineering
Il nostro obiettivo non è scrivere codice assembly, ma fare reverse engineering dei programmi. In
particolare, non vogliamo ricostruire tutti i singoli registri o chi copia cosa, sarebbe uno spreco di tempo.
L’obiettivo è avere una figura di alto livello di cosa fa il programma.
A noi interessa conoscere la matrice del MITRE, ci interessa capire l’attacco, per cui nell’analisi del reverse
engineering, cercheremo di capire dentro un assembler quali sono i codici di programmazione del C che
l’attaccante ha messo (vogliamo capire se volesse fare una connessione, se volesse aprire un file). Quindi
dobbiamo rapidamente capire dall’assembler se stiamo guardando un while, un for, etc.
Vediamo allora come capire dall’assembler quali di queste operazioni ad alto livello stiamo analizzando.
Possiamo immaginare che nell’area dati ci siano delle istruzioni come viste prima, il disassemblatore mette
delle etichette che sono un po’ strane ad una rapida occhiata (ad esempio dword_40CF60). Questo capita
perché il disassemblatore non conosce il codice sorgente utilizzato dall’autore, quindi quando lo
utilizziamo, produce delle etichette finte che riassumono il tipo di label (se è una double world, un byte) e
l’indirizzo (ad esempio dword_40CF60). I tool consentono, man mano che si fa l’analisi di rinominare
l’etichetta.
Nell’esempio in alto, abbiamo 2 variabili globali, le sommiamo e le stampiamo. Per farlo abbiamo due mov,
che ci consentono da leggere dalla memoria globale, quindi stiamo leggendo variabili globali.
Poi abbiamo la push dei parametri e la printf. Nella push a 0040101A la stringa “aTotallD” dove si trova nel
programma? È il contenuto della printf e la “a” davanti a TotalID indica ASCII. Dopo il “;” si vede la stringa
originale che stiamo passando.
Invece le variabili locali le riconosciamo perché sono sullo stack e allora troviamo [ebp - 4], quindi troviamo
delle sottrazioni. Quando vediamo questa cosa stiamo avendo a che fare con variabili di tipo locali, in
questo caso la somma di uno e due che stanno nella memoria.
OSS: è sbagliata la figura, sono sbagliati i valori, perché avremmo dovuto avere 1 e 2, e non 0 e 1
I disassemblatori però le fanno vedere così, mettono il “+” e non il “-“, però in realtà è una sottrazione
perché lo stack cresce sottraendo.
Quando dobbiamo fare un IF, dobbiamo fare almeno due salti “if … else”. Se l’if è “x==qualcosa”, il primo
salto è l’opposto cioè se x è diverso da qualcosa salta, altrimenti va avanti e poi salta fuori.
Noi non lavoreremo a questo livello: i disassemblatori, ricostruiscono un control flow graph.
Il disassemblatore vede che ci sono le istruzioni di salto, vede dove sono e le spezza in più blocchettini, in
questo caso 4, e li collega così:
Nell’esempio, prendiamo due varabili locali (perché c’è + var8), le compariamo e jnz (jump not zero) e vede
se le variabili sono uguali.
Al minuto 1:08:00 apre IDA pro e mostra il suo funzionamento. Una volta aperto il file crackme-121-1.exe ci
troviamo avanti tantissime cose ma procediamo per gradi. L’immagine centrale è il main e i parametri
classici, sostanzialmente è vuoto e c’è solo una chiamata alla funzione “_main_0”.
La barra lunga è il vettore di byte dell’eseguibile, il file PE, poi il blocchetto azzurro è l’area .texts mentre la
successiva è la .datas. Per vedere meglio basta come al solito passare il cursore.
Torniamo al nostro problema per trovare la stringa fail, dopo averla trovata nella zona dell’assembler,
usiamo tasto destro su “aFail” e usiamo la funzione di cross-reference Xrefs graph to (ovvero si parla punti
nel codice che usano questa label).
Nel caso non riuscissimo a navigare tra le varie opzioni ci basta tornare nel _main_0 e vedere che sopra la
condizione di fail la password è “topsecret”.
Per quanto riguarda i for e i cicli sono i più contorti di tutti, questo di seguito è il classico ciclo che stampa i
numeri da 0 a 99.
Tipicamente sono tre pezzi: abbiamo l’incremento, la condizione di uscita e il body del for e sono collegati
al ciclo.
Il ciclo si ferma con il JUMP che c’è nel mezzo. Come ci appare nel grafo? Ovviamente troveremo un loop
nel grafo ma possiamo anche cercare l’inizializzazione dell’indice, la condizione (che riconosciamo con una
freccia che esce dal loop, la freccia true). Dentro al loop troviamo il body e l’incremento.
Invece, per un while: è molto più semplice, somiglia al FOR, in quanto entro nel while e poi c’è un salto
all’indietro. Se all’inizio del blocco un certo controllo mi dice di uscire, il ciclo si ferma.
A noi interessa sempre la condizione del while per capire quando un malware si attiva e quando si blocca.
Vedremo un esercizio su questo la prossima volta.
1. Il primo approccio è quello più accademico “cdecl”: i parametri vengono messi sullo stack e
quando la funzione finisce il chiamante decrementa lo stack. Il risultato viene poi messo in un
registro EAX. Quindi fa tutto il chiamante, prima fa cresce lo stack e poi lo fa decrescere.
2. Stdcall possiamo considerarlo come regole di chiamata, abbiamo che i parametri vengono messi
sullo stack, ma il decremento viene fatto dal chiamato e non dal chiamante.
3. Fastcall è l’approccio più utilizzato in Windows prevede che i parametri sono in parte passati sui
registri. Se la funzione prende pochi parametri si usano solo i registri altrimenti se i parametri
sono tanti, alcuni parametri sono passati nei registri e altri nello stack. Anche in questo caso il
chiamante non fa il clean dello stack.
Altro avvertimento: potrebbe capitare che diversi compilatori fanno chiamate in modo diverso. Ad
esempio, la stessa chiamata con visual studio e gcc:
Il programma è lo stesso ma vediamo due risultati diversi. Per questo è importante avere un buon
assemblatore, in quanto quelli migliori riescono a capire anche quale compilatore è stato utilizzato.
1. Inizialmente lo stack è vuoto. Il chiamante può usare lo stack per salvare i suoi registri (non è uno
step obbligatorio).
2. Nel caso cdecl , i parametri vengono pushati sullo stack oppure vengono copiati sui registri,
sempre in base alla convenzione. Il base pointer rimane fisso, lo stack pointer sale.
3. Poi c’è il momento della chiamata, l’istruzione call con l’indirizzo della chiamata. A tal punto viene
inserito l’indirizzo di ritorno sullo stack.
4. Nell’Intel, le funzioni chiamate iniziano sempre con due istruzioni: push ebp e mov ebp , esp che
fanno la configurazione del base pointer. Nell’immagine di sopra il base pointer si trovava in
fondo, adesso viene assegnato con esp e così rimarrà per tutta la chiamata. Ci dobbiamo salvare il
vecchio ebp per ripristinarlo alla fine.
5. Il chiamato fa anche lui il salvataggio dei registri perché poi li deve ripristinare.
6. Fa ulteriori push se deve mettere le variabili locali. Tipicamente troviamo “sub” questo perché
stiamo spostando lo stack pointer e non stiamo scrivendo valori sulle variabili locali. Nell’esempio
abbiamo sub esp,8 e l’8 è una sottrazione cumulativa: abbiamo 2 variabili di 4. Questo è quello
che rende difficile l’analisi perché non sappiamo se sono due variabili di 4 o una di 8.
Può servire per capire se un malware è stato già avviato, per non far andare più esecuzioni dello stesso
malware in conflitto.
Array e strutture
Qualche parola veloce su array e strutture, in questo esempio in figura abbiamo un array locale ed uno
globale.
Un intero sono 4 byte, quindi l’array “a” di 5, sarà di 20 byte, perché 5*4 e avremo quindi una sub di 20.
Come li riconosciamo nell’assembly? È qui che entra in gioco il modo dell’indirizzamento, troviamo ebp
seguito da diverse espressioni. L’array “a” è quello rosso (perché c’è ebp), l’array blu è “b” in questo
esempio in figura abbiamo un array locale ed uno globale. (perché c’è dword_40A000).
Quello locale ci dice che l’array parte da ebp+var_14, e poi c’è un terzo addendo “ecx*4”. “ECX” è l’indice
dell’array, moltiplicato per 4 perché ogni elemento è di 4 byte. Quindi per trovare la posizione del primo
elemento mi sposto di 4, per il secondo di 8 e così via. Per quello globale vale una cosa simile ma stavolta
l’indirizzo base non è ebp ma quello nelle parentesi quadre.
Le struct sono molto toste però servono e sono usate nelle system call di windows per le socket. Troviamo
un puntatore ad una struttura ed in essa troviamo diverse cose, nel caso delle socket possiamo trovare
indirizzi ip; porte; etc.
Nell’assembler cosa troviamo? Immaginiamo una struct con un array, un carattere e un double. Crea con
malloc() un area di memoria dove mette questa struttura e passa la funzione test. La funzione test prende il
puntatore “q” e va a mettere i valori nella struct.
in C il carattere è un solo byte, infatti, le istruzioni in rosso presentano “byte ptr” quindi la dimensione del
trasferimento mi dà un suggerimento sul tipo di dato che sto trasferendo. Il char si trova dopo un array di
20 byte (14h è 20), quindi dopo l’array scriviamo un byte (in particolare 61 è la lettera A in ASCII).
Poi ci sono istruzioni floating point, e la sintassi dell’array, bisogna farci l’occhio.
X86-64
Cosa c’è da sapere sui 64 bit? È retrocompatibile, cioè su un 64 bit possiamo lanciare un 32 bit. Cosa
cambia? I registri si allargano:
Ed abbiamo dei registri in più, da R8 a R15. Alcuni registri cambiano nome, tutti possono essere accessi
all’ultimo byte.
Una cosa utile da sapere sono gli indirizzi canonici: è vero che il processore è a 64 bit, ma non si usano 64
fili per gli indirizzi, ma se ne usano 48. Vengono ignorati gli altri 12, tale cosa è chiamata forma canonica e il
processore non permette il non uso della forma canonica. Quindi devono essere un’estensione in segno
sono o tutti 0 o tutti 1. Queste cose le abbiamo viste quando abbiamo fatto il buffer overflow.
Facciamo un esempio:
La parte in rosso sono i primi 12 bit, che non sono usati quindi devono essere o tutti 0 o tutti 1. Se il
48esimo bit è 1, i primi 12 sono 1 (analogamente per lo zero).
L’ultimo esempio in figura non è un’estensione valida: il processore genera un’eccezione.
Questa cosa l’abbiamo vista nell’esercizio del buffer overflow.
Inoltre, lo stack si utilizza di meno, e non si tende a fare pop-push-pop-push ma si fa push di tutto e poi si
utilizza.
Lezione 21 25/05
Analyzing Windows Malware (15-WindowsMalware)
Oggi ci focalizziamo su windows malware e vediamo alcuni aspetti specifici che vengono sfruttati di questo
SO, l’ottica sarà osservarle per trovarle tramite reverse engineering. In fine vorremo scrivere delle signature
anti-malware per proteggerci.
Windows fortunatamente fa attenzione alla retrocompatibilità e quindi quello che studiamo funziona
ancora ed è valido. Divideremo la discussione in due parti:
La prima sulle Windows API ovvero quelle che si usano se si scrive un’applicazione per windows, ad
esempio aprendo e chiudendo le finestre.
Questo di seguito è uno stile di windows per cui non usiamo i tipi nativi del C ma dei typedefs che prendono
i nomi elencati e che ricalcano l’architettura Intel.
Lasciamo anche la tabella con i tipi più ricorrenti, alcuni da tenere d’occhio sono “Handles (H)” che ha
molto a che fare con le primitive del SO e Callback; che riguarda i puntatori a funzione cioè quando noi
chiamiamo il SO e indichiamo una funzione del nostro programma che il SO dovrà chiamare a sua volta.
Praticamente il meccanismo di hocking visto le lezioni scorse.
Notazione Hungerese
Prevede che quando creiamo una variabile di tipo handle nel nome mettiamo una lettera iniziale che si
riferisce al tipo.
Handles
A proposito degli handles, essi sono una sorta di puntatore ad oggetti anche se non sono puntatori perché
sono dei typedef a degli interi. Questo concetto si generalizza a tutti gli oggetti del SO e di questo numero
noi non faremo somme/sottrazioni o che, poche cose ci facciamo:
Noi abbiamo già visto gli oggetti e gli handle, dove? Quando abbiamo utilizzato i tool di Process Explorer e
WinObj ed infatti, quando cliccavamo su un processo, mostrava gli handle usati. Nel gergo di windows
anche i processi ed i thread sono oggetti.
La cartella che più ci interessa di WinObj è BaseNamedObjects perché qui vanno a finire gli handle che
usiamo con le windows API. Il File System di Windows si trova in :\Windows ed è orientato al disco ma i
percorsi sono ancora compatibili e viene tradotto con \GLOBAL??\.....
Infine, l’ultima cartella importante è \Device dove sono posizionati i device fisici. Per concludere tutte le
informazioni sui percorsi di Windows abbiamo che tutti quelli che iniziano con “\\“ sono percorsi di rete, e
quindi non fanno parte della gerarchia classica. Dopo il doppio slash c'è il nome del server e poi il percorso
dentro di esso.
Esistono diversi meccanismi di caricamento, una è l’uso della funzione LoadLibrary() che mettiamo
all’interno del nostro programma C; così facendo non viene mostrata la dll negli import. Il contro è che se io
vedo la funzione intuisco comunque la situazione. Un altro modo è quello di aprire la DLL come se fosse un
file
Le DLL sono molto simili agli EXE, quindi hanno area codice/area dati/import/etc e poi c’è una funzione
speciale chiamata DllMain. Tale funzione viene tipicamente chiamata quando viene chiamata la Dll, le altre
due occasioni nella quale viene chiamata sono:
API nativa
Sappiamo che esiste una Dll un po’ speciale chiamata “Ntdll.dll” perché tutte le altre, come Kernel32 etc, la
chiamano per arrivare al SO. Il compito di questa Dll è appunto fare le chiamate di sistema quindi con un
passaggio da User mode a Kernel mode, mentre la libreria non fa ciò. Normalmente questa Dll non verrà
mai usata dai programmatori, infatti non è nemmeno documentata ma viene usata dal malware.
Normalmente gli antivirus si inseriscono tra l’applicazione e le Dll monitorando un po’ tutto. Per questo
motivo il malware, per non essere rilevato, fa il giro lungo passando per Ischia e andando su Ntdll.ll.
Ma perché è utile chiamarlo? Oltre ad evitare l’antivirus si possono fare cose più sofisticate ad esempio sul
link nella slide c’è una spiegazione per trovare il pid di un processo senza chiamare la syscall
Domanda: perché l’antivirus non lo spostiamo più giù nella gerarchia? Si potrebbe fare ma perderemo altre
informazioni, soprattutto quelle di alto livello.
Per chiudere questa parte vediamo come possiamo, ipoteticamente, caricare la ntdll.dll di nascosto. Ci
creiamo il percorso poi chiamiamo due API per caricarlo, la CreateFileA la uso per aprire il file come se fosse
un file di testo ASCII poi si abbina il file ad un buffer di memoria.
Il resto di oggi sarà tutto su varie API di windows e come vengono usate.
Socket
WiniNet
Oggetti COM
Per le prime, le socket, abbiamo che esse sono identiche a Linux ma cambiano piccolissimi dettagli e dalle
slide abbiamo l’elenco. Se il malware fa da server chiamerà le seguenti funzioni:
Mentre la connect, più altre tre segnate in rosso sono quelle chiamate dal client.
In un disassemblatore vedremo un sacco di righe di codice ma noi dobbiamo essere resistenti al rumore e
notare solo quelle chiamate importanti (che sono riportate in grassetto sulla slide).
Poi vediamo anche la API WinINet, assomiglia a quella di Java, per http e FTP più altri protocolli applicativi.
Non creiamo una socket ma facciamo direttamente una richiesta, ad esempio “InternetReadFile” legge i
dati da un file scaricato.
Ma perché usiamo questo invece delle socket? Perché sono più comode e perché il traffico che viene da
qui è INDISTINGUIBILE da quello di un vero utente, quindi se il traffico viene sniffato non si nota nulla.
Poi ci sono ulteriori dettagli sugli COM identifier che troviamo nelle cartelle del registro di windows.
Malware behavior - Backdoors
Servono per dare accesso ad un attaccante ad una macchina vittima, abbiamo visto la reverse shell l’altra
volta. Come capiamo che abbiamo a che fare con una backdoor? Facciamo reverse Engineering, la prima
funzione da valutare è “CreateProcess” tale funzione è come la fork di Linux più complicata.
“StartupInfo” è il parametro più importante perché, per la reverse shell dobbiamo creare un processo che
non deve stampare nulla a video ma deve stampare sulla socket. Vediamo il codice per creare una remote
shell ottenuta dal disassemblatore:
Partiamo dalla chiamata sospetta di CreateProcessA e cerchiamo di capire se serve per creare una reverse
shell, andiamo quindi a ritroso sui parametri ignorando quelli inutili. Cerchiamo poi StartupInfo, che è una
struct con tre parametri:
StdError
StdOutput
StdInput
Andiamo ulteriormente a ritroso per trovare “eax” e vediamo che è il valore di una socket.
Altra cosa che potremmo trovare, invece di CreateProcess(), è CreateThread() che ha come parametri quelli
elencati di seguito. Il malware per fare una reverse shell crea due thread figli, uno per leggere dalla socket e
l’altro per mandare i dati alla socket.
Ma se è simile perché usare un’altra funzione? Perché il malware non solo comunica con il server ma cerca
anche di nascondersi offuscando il traffico.
Vediamo un esempio di Quasar RAT, openSource e quindi possiamo scriverci il nostro malware per infettare
le cose. Se apriamo l’applicativo vediamo tutte le macchine infettate che si sono collegate al nostro PC.
Le botnet sono una collezione di hosts compromessi, i pc così compromessi sono chiamati bot o zombi. Lo
scopo di una botnet è quello di essere il più esteso possibile per avviare campagne di spam o diffusioni
malware; un altro obiettivo è sicuramente quello di performare un attacco DdoS.
Per il primo caso vediamo un esempio, forse obsoleto perché risale ad Windows XP, attraverso un
meccanismo noto come GINA (Graphical Identification and Authentication). Quando noi logghiamo in
windows facciamo partire un processo chiamato “winlogon.exe”, che poi vediamo nella lista dei processi di
windows, e può essere programmato tramite registro per chiamare una dll di autenticazione; di solito
quella di default. Si può però modificare tale registro per far chiamare un’altra Dll, tale meccanismo si
chiama GINA.
Un attaccante potrebbe modificare il registro mettendo il percorso di una Dll malevola, che sniffa la
password, e poi richiama la Dll ufficiale performando una specie di man-in-the-middle.
Mentre per il secondo punto si parla di hash dumping e performano sempre degli attacchi al sistema di
autenticazione di windows. In pratica quando un utente si è già autenticato abbiamo che le credenziali di
autenticazione sono memorizzate temporaneamente in un processo molto importante, LSASS.exe.
Un attaccante che è già entrato nel computer e vuole rubare le password cerca di attaccare questo
processo prendendo la memoria dello stesso e la salva sul disco. In particolare, una vittima prediletta di
questo tipo di attacchi sono gli elementi contenuti nel riquadro rosso.
SAM Server contiene i così detti NTLM Hashes ma capiamo cosa sono; abbiamo già studiato la struttura PKI
(public Key Infrastructure) che viene usata molto da windows infatti il KDC, a seguito dell’autenticazione
può usufruire di due meccanismi: rimandare indietro l’hash (oppure NTOWF), oppure la coppia TGT e la sua
chiave.
L’invio dell’hash è ormai obsoleto e viene usato ogni volta che vogliamo accedere alla rete locale per
leggere una risorsa. Ora se qualcuno ruba il codice può accedere al File server attraverso un replay attack.
Il succo del discorso è che esistono decine di tool, come Pwdump, che quando lanciati in windows rubano
gli hash permettendo l’accesso alla rete locale. Infatti, Pwdump inietta una dll nel processo di Lsass la quale
viene poi richiamata per leggere all’interno del processo Lsass, per vedere gli hash.
Vediamo adesso l’ultima forma di credential stealing, il Keylogging che si può effettuare sia:
Qual è la differenza tra gli ultimi due meccanismi? Nel primo caso un programma passa al kernel di
Windows il puntatore alla funzione “SetWindowsHookEx”, e ciò avviene ogni volta che viene premuto un
tasto. Il secondo approccio invece è proprio il classico polling, quindi si richiede costantemente l’attenzione.
Cosa troviamo quando facciamo reversing engineering? Ricordiamolo, l’ottica è sempre quella di trovare
particolari funzioni e poi i nomi dei parametri che vengono passati.
Per visualizzarlo usiamo il tool Regedit, le cartelle che vediamo sulla sinistra sono le sei root keys e se ci
navighiamo dentro troviamo delle entry (delle foglie) formate da un nome + un valore; ogni variabile ha un
tipo.
Ma cosa significano queste root keys? Le prime due sono le più importanti e più usate, la prima racchiude
tutte le variabili del registro che hanno a che fare con l’intero computer; mentre il secondo tutte le
configurazioni dell’utente loggato in questo momento
Ma cosa ci interessa di questo registro? Un po’ tutto purtroppo, ci si potrebbe perdere ma una prima voce
sono le “Run Key” e tutte le variabili qui presenti vengono lanciate appena loggati.
Visto che normalmente ci sono decine diverse di elementi il prof suggerisce l’uso di un tool noto come
Autoruns, il quale accentra in un solo tool tutte le run keys possibili ed immaginabili.
Abbiamo poi visto l’altro tool chiamato Procmon che ci consente di vedere tutti gli accessi al registro e
abbiamo visto che ci sono tre funzioni:
Quindi quando vediamo nel codice macchina qualcosa di simile, attenzione! Dobbiamo cercare di capire
cosa viene modificato.
Ci sono tanti modi di scrivere nel registro, uno di questi è .REG file che sono dei piccoli script per la
modifica.
Come detto non ci sono solo le run keys ma un altro meccanismo sono le APPINIT DLLs. Sono sempre chiavi
di registro, si trovano “HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\
Windows” e sono una stronzata bella e buona. Praticamente ogni volta che avviamo un programma grafico
in windows (e che quindi usa User32.dll) AUTOMATICAMENTE carica le dll che sono qui elencate.
Un altro modo è attraverso winlogon.exe che si trova nella cartella “Software”, questa chiave qui viene
usata ogni volta che ci logghiamo e vengono chiamate tutte le DLL elencate qui dentro. Per non
demonizzare sempre, in realtà delle utilità ci sono infatti se vogliamo personalizzare il blocca schermo o il
log in per motivi amministrativi; bisogna mettere la Dll qui dentro.
Services
I servizi di windows sono un altro modo per raggiungere la persistenza, ricordiamolo sono dei processi che
girano in sottofondo ed infatti non li vediamo subito una volta aperto task manager. Per vedere come
funzionano usiamo ProcessExplorer che evidenza i servizi con il colore rosé.
Concludendo, nel nostro malware dobbiamo fare caso se ci sono funzioni che modificano i servizi come:
Vediamo l’assembly della libreria, sulla sinistra, e il codice modificato attraverso un salto.
Come preveniamo questa cosa? Tutti quelli visti prima erano su registro, questo modifica i file di sistema
e quindi dobbiamo garantirne l’integrità attraverso il file hashing.
Un’altra tecnica per la persistenza è il DLL Load-Order Hijacking ovvero una tecnica che non richiede né di
modificare le entry dei registri né di usare un trojan. Semplicemente quando un .Exe viene lanciato,
windows cerca le dll importate nel seguente ordine:
Supponiamo di avere un file “explorer.exe” che si trova in C:\Windows\explorer.exe ed importa una dll
chiamata “ntshrui.dll” localizzata in C:\Windows\System32. Tale dll non è una nota e una malevola
chiamata allo stesso modo, ma messa in C:\Windows verrà chiamata prima.
Windows non è ovviamente stupido, come meccanismo di protezione fa si che le Dll più importanti saltino
questo procedimento. Tale operazione è chiamata KnownDll registry key.
Escalation orizzontale
Escalation verticale
Furto di credenziali
Vulnerabilità ed exploit del SO
Malconfigurazioni del SO
Social engineering.
In windows esistono due tipi di utenti: quelli locali, che lavorano su un singolo computer, e quelli di dominio
che lavorano su una LAN.
Gli utenti che vengono normalmente visti in un computer windows sono, per il locale:
In particolare, quando installiamo windows il primo che configuriamo è anche l’amministratore. Mentre i
System account sono usati dai componenti dei SO, come i servizi. L’utente più privilegiato dal sistema è
proprio system.
Dove entrano in gioco gli utenti e gli amministrators? Tipicamente per accedere ad una risorsa possiede un
security descriptor (quello che vediamo andando in proprietà) e sappiamo, perché LA CASOLA CI ASCOLTA,
che per gestire i permessi associati ad una risorsa si usano le Access Control List (ACL).
Quando lanciamo un processo esso eredita dall’utente un Access Token, il SO ogni volta che apriamo un
file/registro va a prendere l’user e controlla se è scritto nella ACL.
Poi c'è un'altra considerazione da fare sui privilegi, rispetto alle ACL, che appartengono agli utenti e non agli
oggetti e quindi Windows gestisce come due meccanismi diversi. I privilegi sono usate per tutte le altre
operazioni privilegiate presenti nel SO.
Ma quindi come fa il malware a sfruttare i privilegi, e come ci proteggiamo? Consideriamo un windows XP,
senza protezioni, e di usarlo con un account amministratore; lanciamo internet e scarichiamo un malware e
lo lanciamo. Purtroppo, essendo tutti privilegiati succede un casino:
Ad oggi c’è un nuovo meccanismo di funzionamento per evitare la situazione di sopra chiamato integrity
levels dove, i processi non sono tutti uguali e alcuni si avviano con un livello basso di integrity. Anche il
malware scaricato ha bassa priorità e quindi non potrà modificare o attaccare processi con priorità più alte
della propria.
Un altro meccanismo è l’user account control (UAC), introdotto da Windows Vista, e funziona così: Ci si
logga sul pc e anche se siamo amministratori abbiamo che i processi non ereditano i nostri privilegi; quindi,
si crea un secondo token chiamato restricted. La risposta del sistema era il pop up.
Nei framework di attacco useremo tecniche di UAC Bypass i vari link sulle slide aiutano a vedere i modi
possibili di utilizzo. Oppure un’altra funzionalità è la access token manipulation.
Il più potente e malefico di tutti è il debug privilage che provoca la possibilità, ad un programma, di leggere
e scrivere la memoria di qualunque altro programma nel sistema. Di default questo privilegio è disattivato e
i malware cercando di attivarlo per poter poi operare, per farlo usano la funzione “OpenProcessToken”.
Abbiamo detto che c’è questo processo LSASS che contiene un piccolo database contenenti hash value e
kerberos ticket. Sono due meccanismi validi di autenticazione, solo che NTLM è più vecchio ed ancora usato
mentre Kerberos viene usata nei sistemi più moderni.
NTLM funziona sul concetto di cifratura simmetrica e funziona così: si prende la password dell’utente e si
calcola l’hash attraverso il MD4, il server quando riceve una richiesta di una risorsa invia una challenge che
verrà cifrata dall’utente con la chiave simmetrica. Il server non riuscirà a decifrare, visto che non ha la
chiave ma invia la richiesta al domain controller che darà la decisione finale sulla cosa.
Tale meccanismo è abbastanza robusto anche allo sniffing ma come sempre no al replay.
Kerberos rende la cosa un po’ più robusta, nonostante si basa sull’utilizzo di chiavi simmetriche, perché
introduciamo una password ed un hash anche per il domain controller. Kerberos riduce l’uso dell’hash e
introduce il concetto di ticket, che ha una scadenza.
Al minuto 13 fa vedere come rubare le password di sistema usando una shell con privilegio di admin. Come
tool usa mimikatz che lavora bene su kerberos e sugli hash.
Vediamo un esempio, immaginiamo di avere un malware che fa da launcher e poi una dll malevola ed infine
nel nostro sistema abbiamo l’eseguibile explore.exe. Il compito del malware è andare su internet. Se il
malware provasse ad andare su internet potrebbe venire bloccato dal firewall, magari attraverso una regola
di filtraggio di IP tables, a questo punto la process injection modifica la memoria dell’explore e vi copia
dentro la dll malevola; quest’ultima poi scaricherà quanto dovuto.
Per poter fare ciò dobbiamo avere per forza i privilegi di debug, come visto prima. Nello specifico il
processo A attaccante effettua la chiamata alla funzione OpenProcess(), che apre un processo come se
fosse un file, che ritorna un handle. Una volta recuperato l’handle si chiamano le tre funzioni elencate sotto
nell’immagine, questo per creare un thread non nel processo A ma nel processo B vittima.
Al minuto 23:20 mostra un esempio live su macchina virtuale attraverso un programma chiamato Xenos e
Xenos64, questo è importante perché dipende se vogliamo iniettare un processo a 32 o 64 bit.
https://github.com/rnatella/swsec/tree/main/malware/malware-windows
Nel caso cerchiate la password per lo zip esso è “malware”. Se durante l’esercizio non vi trovaste con
determinate cose potrebbe essere dovuto al fatto che la nostra versione di IDA pro è quella studenti, quindi
tranquilli; nello specifico si rifà alla prima domanda
Il prof vede una cosa del genere, noi ignoriamo _DllMain visto che non lo riusciamo a vedere.
Lezione 22 01/06 (16-MalwareDetection)
Oggi è l’ultima lezione, la prossima sarà su un seminario dei CC (common criteria). Per la fine ci lascerà un
laboratorio (16-Lab-MalwareDetection) da fare da soli e nel caso chiedere a lui.
La prima categoria di signatures sono le così dette host based signatures che analizzano pezzi di codice,
file/exe/dll o codice all’interno di un documento, e se questo programma potenzialmente malevolo
modifica o crea certi file, la nostra firma potrebbe riguardare proprio questi file.
L’ottica del discorso è questa: supponiamo che scopriamo durante la malware analysis che il nostro virus
modifica una certa run key, e quindi sia il nome della variabile che il percorso possono far parte della host
based signatures e quindi sapere che viene usato già in altri attacchi. Una volta individuato detect possiamo
fare response e applicare politiche di quarantena.
La seconda categoria sono le network signatures che concettualmente sono identiche, solo che non sono
applicate a degli eseguibili ma al traffico di rete. Per esempio, un certo pacchetto ha una lunghezza strana,
oppure che il contenuto di quel pacchetto contenga dello shell code. Quello che faremo in più, rispetto ad
un corso di reti, è unire l’analisi del traffico con la malware analysis per creare delle regole migliori.
Ovviamente quando scriviamo una regola dobbiamo concentrarci su cosa il malware fa sul sistema e non
le caratteristiche del malware stesso.
Piccola panoramica su dove si usano i network indicator, abbiamo parlato di IDS ma anche i firewall e i
router che filtrano gli indirizzi IP e le porte TCP e UDP. Oppure ovviamente i proxy e i server DNS.
Noi vedremo molto i network indicators chiamati content based, questo perché le regole possono essere
fatte riguardanti proprio le caratteristiche del file oppure del traffico; ma le regole più potenti vanno sul
contenuto.
Una buona practise è non eseguire il malware subito, ma per scrivere delle buone regole è necessario
partire dai sistemi già avviati e catturare del traffico reale delle macchine.
Il vantaggio principale nell’osservare la rete reale è osservare la comunicazione del malware con l’esterno e
quindi, ipoteticamente, risalire all’indirizzo del server C2. Anzi il principale vantaggio è che l’analisi passiva
del traffico non può essere rilevata dall’attaccante, rispetto ad effettuare un’analisi attiva, e questo ci
permette di ottenere ulteriori dettagli sul malware.
Vediamo un esempio di indicator, giusto per capire con cosa abbiamo a che fare. Abbiamo detto che il
malware cerca di contattare dei domini associati all’infrastruttura di controllo, oppure utilizza degli indirizzi
IP; oppure ancora avere traffico di tipo http.
Chiariamo meglio l’aspetto di come un attaccante, quando il malware attiva e si esegue, capisce che sta
venendo analizzato:
Non mandare un malware indiscriminatamente ma inviarlo a specifiche reti, e se nota che c’è
qualcosa che non va subito capisce chi è. Tale tecnica è conosciuta come spear phishing e-mail
Quando il malware viene eseguito logga la propria attività malevola, un sito famoso è PasteBin
dove si possono mettere dei testi in maniera non rintracciabile.
Oppure tramite unused DNS.
Strategie di OPSEC più sicure
Dopo aver avviato un malware siamo in fase di analisi ed esso effettua una richiesta all’attaccante, per
impedire che l’attaccante scopra dov’è stato eseguito il malware possiamo anonimizzare l’indirizzo IP.
Magari attraverso Tor, oppure attraverso VM dedicate per la ricerca usando delle connessioni SIM oppure
usare macchine Cloud.
Quando abbiamo a che fare con un indirizzo o un dominio e vogliamo capire se sia malevolo o no, possiamo
usare Google o qualunque altro motore di ricerca. Però un’avvertenza: quando mettiamo un url in Google
egli potrebbe decidere di aggiungere l’url nel suo database, questo provoca il così detto crawling e questo
potrebbe essere notato dall’attaccante.
Altro modo ancora per ottenere informazioni sul dominio è cercare il registro dei domini, che è un
protocollo distribuito, e chiedere ai server attraverso i comandi come whois o dig ma attenzione perché ci
esponiamo anche noi. È possibile anche il contrario, da un indirizzo IP ricostruire quali domini vi puntano.
Per non esporci più di tanto esistono dei siti specializzati nel fare richieste al DNS etc come DomaniTools,
RobTex, etc.
Brevemente, è una regola che analizza tutto il traffico http, senza destinazione sorgente e destinazione, e il
primo messaggio “TROJAN Malicious…” serve a noi per dare il nome alla regola mentre la regola vera e
propria inizia a “content” e le regole di scrittura sono simili al grep di Linux.
La regola controlla il modo in cui l’user agent è scritto, 0d e 0a è il ritorno a capo e questo è fondamentale
perché altrimenti se capitasse “user agent” in mezzo a del testo questo verrebbe filtrato dalla regola.
A sentimento, è una buona regola? No, manco la chiavica perché basta un user agent diverso e non
funziona più. L’autore di questa regola ha scritto questo perché ha notato che nella sua rete gli unici due
user agent del malware erano: Wefa7e and Wee6a3 e quindi l’espressione regolare è diventata
Una cosa del genere può far scattare falsi allarmi tramite il software Webmin, che altro non fa che generare
traffico con l’user agent che si vuole. Il prodotto è open source e per nulla malevolo. Quindi bisogna
modificare nuovamente la regola, adesso ci sono due sotto-regole ed il contenuto è che non deve essere
Webmin
Ora vediamo un caso più reale, immaginiamo di aver monitorato la rete e aver osservato tutti questi valori
sulla destra dell’immagine. Sembra che la regola vista sopra sia ancora valida, sono tutte lunghe 6 caratteri
e sono tutte lettere e numeri; in più le lettere sono sempre tra a-f e non a-z. Ipotizziamo siano valori
esadecimali.
Se avessimo avuto quest’altro caso di seguito non sarebbe stata una buona regola, perché ci sono User
agent lunghi 7 e altri non iniziano sempre con We. Una cosa tipica che si verifica quando facciamo la
malware analysis, ed eseguiamo sul nostro computer un malware, è che se usiamo una macchina virtuale
avremmo visto una cosa come quella segnata con i due host. Il malware fa tante richieste ma l’user agent è
sempre lo stesso perché il malware invia sempre lo stesso messaggio.
Giusto una premessa, nel fare l’analisi statica non vogliamo verificare la copertura del codice perché non
vogliamo una conoscenza totale dello stesso. Quindi osservando la tabella ci fermiamo più o meno nel
mezzo.
Vediamo un esempio con un messaggio simile a quello di prima, ovvero dei messaggi di Beaconing (Si
pronuncia Bicon perché è un trasmettitore, è non Bagon il pokemon o Bacon la pancetta) quindi dei
messaggi che l’host compromesso manda al server C2.
Abbiamo appena detto che questo messaggio, tipicamente incorpora informazioni sull’host; quindi, se lo
lanciamo su host diversi il messaggio cambierà. Vogliamo capire cosa contiene e come si forma tale
messaggio.
Tipicamente quanto visto prima, ovvero il malware si inserisce in un protocollo già esistente, non è un caso
raro, infatti, l’uso di http è un classico. Nell’immagine di seguito, ad occhio, vediamo subito qualcosa di
strano, infatti, l’user agent è enorme e non contiene spazi.
Il malware per cercare di passare inosservato si fa furbo e cerca di mandare un user agent fisso e valido. Ad
esempio così:
Però una volta capito lo si blocca e finisce qui, ma sapientemente scelgono diversi user agent da assegnare
a rotazione o a caso rendendo più difficile il filtraggio. Oppure come detto le lezioni scorse alcuni malware
usano la libreria COM per costruire richieste con lo stesso codice del browser usato. In questo caso l’user
agent non è un buon indicator, perché ovviamente questo appartiene al computer infettato.
Tecniche di offuscamento
Supponiamo adesso un altro malware, dove l’user agent è tranquillo e l’attaccante ha usato il percorso
nella GET. Supponiamo che l’URI che abbiamo sia malevolo e che abbiamo preso molto traffico da tanti
nodi, i valori sono casuali o sono regolari? Ci sono parti regolari, lunghezza simile e si ripete spesso 58.
Questi appena visti si chiamano Beacon message e vengono costruiti prendendo:
Facciamo un ulteriore analisi sul messaggio di sopra, mettiamo che il messaggio di sopra sia stato ottenuto
usando tre funzioni:
GetTickCount
Random
Gethostbyname
Quindi il nostro URL ha una parte randomica, una parte che è l’host del computer e l’altra sono i secondi
nel quale il pc è acceso. Alcuni malware possono usare delle API di network di basso livello per mimare
traffico comune, questo però richiede più lavoro manuale e creare situazioni di errore come queste:
Analizzando il binary code del malware capiamo che ci sono 4 valori randomici, i primi 3 sono
dell’hostname e il restante è il GetTickCount; infine unisce con i “:”
Ma vediamo per bene come funziona nello specifico la formazione, i malware tipicamente trasformano i
dati attraverso dell’encoding prima di inviarlo sulla rete. Il fatto che i primi 4 valori siano casuali già ci aiuta
sulla formazione dell’espressione regolare; vediamo come il malware parte dai valori esadecimali/lettere e
prende gli ASCII.
Quindi chi scrive malware alla fine fa qualche conversione a casaccio, senza capire o essere troppo
intelligenti. Il nostro scopo è comprendere quali sono le regole per poter poi fare la signature
Finiamo per adesso le tecniche di offuscamento con il data encoding, l’obiettivo non è rendere il messaggio
indecifrabile ma semplicemente il non essere rilevabili da un occhiata rapida dell’uomo. Un altro motivo
deriva anche dal fatto di dover inserire il codice per fare la codifica nel malware e quindi non si può
appesantire troppo lo stesso.
Unendo infine tutte le informazioni raccolte prima creiamo una signature “perfetta” con l’espressione
regolare creata a seguito dell’analisi. Notiamo anche una cosa particolare di questa firma, vi è la presenza
prima di un controllo sul numero 58 e poi di tutta la firma. Perché? L’espressione regolare è pesante da
calcolare e quindi se l’url non contiene il 58 iniziale io non l’analizzo proprio.
Base64
Vediamo un altro esempio nel quale un attaccante una un’infrastruttura già esistente; quindi, non usa un
server C2 preso dal nulla ma ha infettato magari un sito web famoso tipo Repubblica.it . Quello di seguito è
codice HTML ma notiamo subito qualcosa di strano, quello con “!--"è un comento contenente una parola in
base64; ovvero longsleep. È un comando che il malware deve fare sulla macchina vittima.
Qui siamo in una situazione contraria al Beacon, prima abbiamo visto il messaggio beacon che parlava al
server qui invece, è il server che sta parlando con una botnet. Questo è molto stealty perché non parliamo
noi.
Ma come funziona il base64, è una codifica che usa solo caratteri esadecimali, l’obiettivo è tramutare
qualunque tipo di byte (anche i non stampabili) in un qualcosa stampabile. Ad esempio, i ritorno a capo o
qualsiasi cosa non visibile.
Vediamo un esempio, quando facciamo l’upload di un file su un sito web noi facciamo la PUT, in alcuni casi
il file viene codificato in base64 (lo vediamo nel body) del messaggio.
La domanda ora sorge spontanea, come facciamo a capire da un .EXE che ci troviamo a lavorare con
qualcosa a base64? Tra gli esercizi che ci ha dato vediamo che tra le sue stringhe ha una stringa come quella
riportata nell’immagine, il suo compito è proprio fare la conversione e per questo si chiama indexing
string.
Domanda, ma se noi mettessimo questa stringa come mostrata di seguito nella regola avremmo fatto una
buona regola? E no, altrimenti non abbiamo imparato niente.
Quindi apriamo il malware e cerchiamo quali sono le stringhe possibili che l’exe può ricevere. Usiamo IDA e
cerchiamo di capire la parte del codice che si occupa di fare la conversione della base64. In questo esempio
specifico abbiamo che la stringa funziona con una ciclo for + switch case.
Quindi vediamo cinque alternative diverse, una per ogni comando (le stringhe sono tagliate perché
indicano solo la parola “run” o connect”).
Un modo più robusto per scrivere queste regole è farlo come nell’esempio. Si smonta la regola in due parti:
Ora non conviene unire il tutto alla regolar expression di prima, ma ci conviene spezzarla in due regole
diverse, questo sia per la dimensione e quindi lentezza sia perché sarebbe poi aggirabile.
La prospettiva dell’attaccante
In conclusione, come ogni sviluppatore software, anche l’attaccante ha difficoltà ad aggiornare il software e
renderlo poi compatibile con i sistemi che cambiano in continuazione. Ogni cambiamento necessario
dovrebbe essere minimale e quindi le nostre regole se sono robuste resistono a questi tipi di cambiamenti.
La prima esercitazione riguarda la scrittura di una regola su YARA, nelle slide ci sono vari esempi e i task
sono segnati alla slide 16 sui lab. yarGen crea un database di quasi 1 gb ma suggerisce la scrittura delle
regole e nello specifico nel file creato c’è già la risposta alle domande; ma come ragiona il tool? Di base
mette dei metadati e non fanno parte della regola, poi suggerisce 5 stringhe ma non le mette tutte nella
regola se no sarebbe accurato e fa un controllo con le stesse stringhe sul Database.
Il controllo sul DB serve per evitare falsi allarmi. Le stringe sono chiamate $s* se sono stringhe generiche
mentre %x* se specifiche.
In sigma quando le regole sono impilate in una selection sono tutte in AND tra loro mentre se ci sono i “-“
allora le regole sono in OR. Le regole di SIGMA sono meno specifiche di quelle di YARA, le quali sono fatte
ad-hoc sul file exe di un certo malware; mentre per sigma esse sono fatte per i logger che poi devono
inoltrare le informazioni ad altri sistem. Nel caso vediamo in sigma |endswith allora significa che abbiamo
sostituito il “*” e quindi non ci interessa il percorso.