Un modello integrato control-flow e data-flow per il rilevamento ...
Un modello integrato control-flow e data-flow per il rilevamento ...
Un modello integrato control-flow e data-flow per il rilevamento ...
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
<strong>Un</strong>iversità degli Studi di Udine<br />
Facoltà di Scienze Matematiche Fisiche e Naturali<br />
Corso di Laurea Specialistica in Informatica<br />
Tesi di Laurea<br />
<strong>Un</strong> <strong>modello</strong> <strong>integrato</strong> <strong>control</strong>-<strong>flow</strong> e<br />
<strong>data</strong>-<strong>flow</strong> <strong>per</strong> <strong>il</strong> r<strong>il</strong>evamento automatico<br />
di intrusioni<br />
Candidato:<br />
Matteo Cicuttin<br />
Relatore:<br />
Prof. Marino Miculan<br />
Anno Accademico 2010/2011<br />
<strong>Un</strong>iversità degli Studi di Udine<br />
Via delle Scienze, 206<br />
33100 Udine<br />
Italia
Alla mia famiglia, che mi ha sempre supportato.
Ringraziamenti<br />
In questo <strong>per</strong>corso mi sono trovato ad affrontare molte situazioni impegnative, quin-<br />
di desidero innanzitutto ringraziare chi mi era vicino in quei momenti <strong>per</strong> avermi<br />
sopportato, Ilaria in particolare. Spesso <strong>il</strong> suo aiuto è stato determinante e spesso<br />
non sono stato capace di ricambiarlo.<br />
Il successivo ringraziamento va al mio relatore, Prof. Miculan, <strong>per</strong> l’opportunità<br />
che mi ha dato e <strong>per</strong> <strong>il</strong> “clima” in cui questo lavoro si è svolto. Sono in debito con<br />
lui.<br />
Tra quelli che devo ringraziare ci sono anche gli amici, interni ed esterni all’uni-<br />
versità, compresi quelli che ultimamente hanno <strong>per</strong>so un po’ la testa. Con loro ho<br />
condiviso momenti di divertimento e importanti scambi di idee.<br />
Naturalmente <strong>il</strong> ringraziamento più grande va alla mia famiglia <strong>per</strong> avermi sup-<br />
portato in questo <strong>per</strong>corso, nonostante i momenti diffic<strong>il</strong>i.<br />
Anche se non leggeranno mai questi ringraziamenti, desidero dire grazie a chiun-<br />
que sia coinvolto nello sv<strong>il</strong>uppo dei software che ho usato <strong>per</strong> mettere insieme questa<br />
tesi. Per scriverla ho usato Vim ma mi sono serviti anche un sacco di altri program-<br />
mi, tra cui: L ATEX, GraphViz, GHC, GCC, GDB, NASM, Subversion, le shell Bourne<br />
e Korn, DTrace e anche qualcosina closed source come OmniGraffle. Questi software<br />
mi hanno <strong>per</strong>messo tra l’altro di creare dei tool che hanno automatizzato molte parti<br />
del mio lavoro, semplificandolo enormemente. Il loro denominatore comune <strong>per</strong>ò è<br />
<strong>il</strong> sistema o<strong>per</strong>ativo che li fa girare, ovvero <strong>Un</strong>ix. Ho usato in particolare Mac OS X<br />
<strong>per</strong> elaborare <strong>il</strong> testo e la grafica e Solaris (OpenIndiana) <strong>per</strong> tutta la parte legata<br />
a DTrace. FreeBSD invece sul mio server garantiva tutta una serie di servizi che mi<br />
sono stati assai ut<strong>il</strong>i, storage e backup in primis. Senza <strong>Un</strong>ix e tutto quello che ci<br />
sta sopra tutto questo sarebbe stato infinitamente più diffic<strong>il</strong>e e frustrante.<br />
Desidero infine rivolgere un ringraziamento particolare a Chad Mynhier, che mi<br />
ha fornito materiale e consigli preziosissimi relativamente a DTrace.<br />
Durante questa laurea specialistica poche cose sono andate come pensavo e sono<br />
particolarmente felice di essere finalmente giunto al traguardo, anche se purtroppo<br />
la felicità di questi giorni è offuscata dalla cattiva salute del mio Micio. Con la<br />
laurea triennale avevo visto la punta di un iceberg, con la laurea specialistica ho
iv Ringraziamenti<br />
sco<strong>per</strong>to nuovi mondi. S<strong>per</strong>o che <strong>il</strong> futuro mi riservi una strada che mi consenta di<br />
non smettere di studiare.
Indice<br />
1 Introduzione 1<br />
1.1 Anomaly detection e system call . . . . . . . . . . . . . . . . . . . . 3<br />
1.2 Obiettivo del lavoro . . . . . . . . . . . . . . . . . . . . . . . . . . . 5<br />
1.3 Struttura della tesi . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5<br />
2 Modelli <strong>per</strong> l’anomaly detection 9<br />
2.1 Modelli <strong>control</strong> <strong>flow</strong> . . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />
2.1.1 Automi a stati finiti . . . . . . . . . . . . . . . . . . . . . . . 10<br />
2.1.2 VtPath . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />
2.1.3 Execution graphs . . . . . . . . . . . . . . . . . . . . . . . . . 13<br />
2.1.4 Abstract stack, un <strong>modello</strong> costruito staticamente . . . . . . 16<br />
2.2 Modelli <strong>data</strong> <strong>flow</strong> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17<br />
2.2.1 <strong>Un</strong> <strong>modello</strong> <strong>data</strong> <strong>flow</strong> . . . . . . . . . . . . . . . . . . . . . . 19<br />
2.3 Discussione dei modelli studiati . . . . . . . . . . . . . . . . . . . . . 21<br />
3 <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong> 25<br />
3.1 Debolezze dei modelli esistenti . . . . . . . . . . . . . . . . . . . . . 25<br />
3.1.1 Primo scenario: vulnerab<strong>il</strong>ità nel codice . . . . . . . . . . . . 26<br />
3.1.2 Secondo scenario: errore di configurazione di un server . . . . 31<br />
3.1.3 Terzo scenario: debolezza delle relazioni binarie . . . . . . . . 34<br />
3.2 Proposta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35<br />
3.3 Costruzione del <strong>modello</strong> . . . . . . . . . . . . . . . . . . . . . . . . . 37<br />
3.3.1 Algoritmo di apprendimento delle relazioni unarie . . . . . . 37<br />
3.3.2 Algoritmo di apprendimento delle relazioni binarie . . . . . . 38<br />
3.3.3 Descrizione dell’algoritmo . . . . . . . . . . . . . . . . . . . . 44<br />
3.4 L’algoritmo completo <strong>per</strong> la costruzione del <strong>modello</strong> . . . . . . . . . 46<br />
3.4.1 Relazione con l’algoritmo originale rispetto ai falsi positivi . . 46<br />
3.4.2 <strong>Un</strong>a possib<strong>il</strong>e variante . . . . . . . . . . . . . . . . . . . . . . 47
vi Indice<br />
3.5 L’algoritmo <strong>per</strong> la verifica delle tracce rispetto al <strong>modello</strong> . . . . . . 47<br />
3.5.1 Gestione delle anomalie . . . . . . . . . . . . . . . . . . . . . 49<br />
4 L’implementazione 51<br />
4.1 Struttura generale del sistema . . . . . . . . . . . . . . . . . . . . . . 51<br />
4.2 Introduzione a DTrace . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />
4.2.1 Interfacciamento a DTrace tramite libdtrace(3LIB) . . . . 53<br />
4.2.2 Note riguardo a DTrace . . . . . . . . . . . . . . . . . . . . . 55<br />
4.3 Implementazione del sistema . . . . . . . . . . . . . . . . . . . . . . 56<br />
4.3.1 Lo script di <strong>data</strong> collection . . . . . . . . . . . . . . . . . . . 56<br />
4.3.2 Implementazione di NewArgs() . . . . . . . . . . . . . . . . . 57<br />
4.4 Costo computazionale del <strong>modello</strong> . . . . . . . . . . . . . . . . . . . 59<br />
4.4.1 Costo del learning . . . . . . . . . . . . . . . . . . . . . . . . 59<br />
4.4.2 Costo della verifica . . . . . . . . . . . . . . . . . . . . . . . . 60<br />
4.4.3 Alcuni dati s<strong>per</strong>imentali . . . . . . . . . . . . . . . . . . . . . 60<br />
5 Conclusioni e sv<strong>il</strong>uppi futuri 63<br />
5.1 Riep<strong>il</strong>ogo del lavoro svolto . . . . . . . . . . . . . . . . . . . . . . . . 64<br />
5.1.1 Il <strong>modello</strong> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64<br />
5.1.2 L’implementazione . . . . . . . . . . . . . . . . . . . . . . . . 64<br />
5.2 Sv<strong>il</strong>uppi futuri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64<br />
5.2.1 L’uso dello stack nell’apprendimento delle relazioni binarie . 65<br />
5.2.2 <strong>Un</strong>a dimensione statistica <strong>per</strong> <strong>il</strong> <strong>modello</strong> . . . . . . . . . . . . 65<br />
5.2.3 Costruzione statica del <strong>modello</strong> . . . . . . . . . . . . . . . . . 66<br />
5.2.4 Applicazione del <strong>modello</strong> a sistemi virtualizzati . . . . . . . . 66<br />
Bibliografia 69
Elenco delle figure<br />
1.1 Codice semanticamente equivalente. . . . . . . . . . . . . . . . . . . 1<br />
1.2 Buffer over<strong>flow</strong>. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4<br />
2.1 Architettura generale di un sistema black-box. . . . . . . . . . . . . 9<br />
2.2 FSA d’esempio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />
2.3 Esempio riguardante la parte induttiva della definizione dell’EG. . . 15<br />
2.4 Rappresentazione grafica del significato di successore. . . . . . . . . . 16<br />
2.5 Time to check to time of use. . . . . . . . . . . . . . . . . . . . . . . 17<br />
2.6 Banale programma vulnerab<strong>il</strong>e ad un non-<strong>control</strong> <strong>data</strong> attack. . . . . 18<br />
2.7 Informazioni apprese a runtime dall’analisi <strong>data</strong> <strong>flow</strong>. . . . . . . . . . 21<br />
3.1 Programma vulnerab<strong>il</strong>e d’esempio. . . . . . . . . . . . . . . . . . . . 27<br />
3.2 FSA <strong>per</strong> <strong>il</strong> primo esempio. . . . . . . . . . . . . . . . . . . . . . . . . 27<br />
3.3 Execution graph <strong>per</strong> <strong>il</strong> primo esempio. . . . . . . . . . . . . . . . . . 28<br />
3.4 Passi dell’attacco al programma. . . . . . . . . . . . . . . . . . . . . 29<br />
3.5 Programma <strong>per</strong> <strong>il</strong> secondo esempio. . . . . . . . . . . . . . . . . . . . 32<br />
3.6 FSA <strong>per</strong> <strong>il</strong> secondo esempio. . . . . . . . . . . . . . . . . . . . . . . . 33<br />
3.7 Execution graph <strong>per</strong> <strong>il</strong> secondo esempio. . . . . . . . . . . . . . . . . 34<br />
3.8 Codice d’esempio <strong>per</strong> <strong>il</strong> terzo scenario. . . . . . . . . . . . . . . . . . 35<br />
3.9 Nuovo <strong>modello</strong> <strong>per</strong> l’esempio del primo scenario. . . . . . . . . . . . 36<br />
3.10 Nuovo <strong>modello</strong> <strong>per</strong> l’esempio del terzo scenario. . . . . . . . . . . . . 37<br />
3.11 Esecuzione dell’algoritmo su una traccia. . . . . . . . . . . . . . . . . 40<br />
3.12 Esecuzione dell’algoritmo su una traccia. . . . . . . . . . . . . . . . . 41<br />
4.1 Struttura generale del sistema. . . . . . . . . . . . . . . . . . . . . . 52<br />
4.2 Script di DTrace <strong>per</strong> monitorare lo stack. . . . . . . . . . . . . . . . 52<br />
4.3 Tempi di esecuzione di ls senza e con tracing. . . . . . . . . . . . . . 53<br />
4.4 Script di DTrace <strong>per</strong> monitorare lo stack. . . . . . . . . . . . . . . . 53<br />
4.5 Strutture dati usate da un consumer DTrace. . . . . . . . . . . . . . 54
viii Elenco delle figure<br />
4.6 Snippet di walk(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55<br />
4.7 Struttura dati di supporto all’implementazione di NewArgs(). . . . . 58<br />
5.1 Automa senza e con peso sugli archi. . . . . . . . . . . . . . . . . . . 66
1<br />
Introduzione<br />
Dai tempi della “dot-com bubble” stiamo assistendo ad una crescita enorme del-<br />
la diffusione di dispositivi informatici di ogni tipo e, a differenza di dieci anni fa,<br />
è abbastanza diffic<strong>il</strong>e pensare di poter vivere senza interagire ogni giorno con un<br />
computer potenzialmente connesso alla rete. Router casalinghi e telefonini montano<br />
CPU sufficientemente potenti da poter far girare un kernel <strong>Un</strong>ix, <strong>per</strong> non parlare di<br />
televisori e media-center. Se pensiamo a quanti di questi dispositivi ognuno di noi<br />
ha in casa e, soprattutto, alla quantità di dati <strong>per</strong>sonali che essi manipolano, la loro<br />
protezione da attacchi informatici diventa un obiettivo primario. Le minacce <strong>per</strong> un<br />
computer, comprendendo negli oggetti indicati con questa parola anche i dispositivi<br />
appena citati, sono dei tipi più svariati ed impensab<strong>il</strong>i e le tecniche di difesa sono<br />
letteralmente centinaia, ognuna volta a proteggere da determinati tipi di attacchi.<br />
Volendo prescindere dalla tipologia d’attacco possiamo distinguere due metodo-<br />
logie fondamentali nel r<strong>il</strong>evamento, la misuse detection, altrimenti detta signature-<br />
based detection e la anomaly detection. Gli antivirus in buona approssimazione fanno<br />
un lavoro di misuse detection: essi infatti, dato ad esempio un f<strong>il</strong>e eseguib<strong>il</strong>e, sono<br />
in grado di cercare al suo interno delle sequenze di byte corrispondenti a payload<br />
malevoli già noti. Il confronto è puramente sintattico e quindi una “versione B”<br />
del payload sintatticamente diversa ma semanticamente equivalente potrebbe non<br />
venire r<strong>il</strong>evata. Prendiamo i due frammenti di assembler x86 di Figura 1.1:<br />
jmp 0x01ab23cd<br />
(a)<br />
push 0x01ab23cd<br />
ret<br />
(b)<br />
Figura 1.1: Codice semanticamente equivalente.<br />
l’effetto netto del codice è esattamente lo stesso, quindi semanticamente sono identici
2 1. Introduzione<br />
ma nel caso (a) la signature è E9 C2 23 AB 01 mentre nel caso (b) è 68 CD 23 AB<br />
01 C3, quindi sintatticamente sono diversi. Se un programma antivirus conosce<br />
la prima signature ma non la seconda <strong>il</strong> payload passa inosservato e <strong>il</strong> virus può<br />
fare <strong>il</strong> suo lavoro. Naturalmente gli antivirus sono un po’ più furbi di così, ma<br />
anche chi scrive i virus è molto furbo e si serve di payload polimorfici, crittografati e<br />
quant’altro, molto diffic<strong>il</strong>i da r<strong>il</strong>evare (si veda ad esempio [23]). Al fine di contrastare<br />
questo tipo di payload sono stati proposte diverse soluzioni basate sulla semantica<br />
[19, 5, 20] ma, purché molto potenti, anche queste sono aggirab<strong>il</strong>i.<br />
<strong>Un</strong>’idea fondamentale da tenere in considerazione <strong>per</strong>ò è che <strong>per</strong> poter attaccare<br />
un sistema è necessario interagire con esso, ed è qui che entra in gioco l’anomaly<br />
detection. Per quanto l’affermazione precedente possa sembrare banale è abbastanza<br />
naturale chiedersi se le interazioni che avvengono sono lecite o meno. Quello che in<br />
genere succede è infatti che tramite interazioni non lecite un sistema viene spinto a<br />
comportarsi in un modo non previsto originariamente dai suoi progettisti creando<br />
quindi un’anomalia nel suo comportamento, una deviazione rispetto a quello che<br />
dovrebbe essere <strong>il</strong> comportamento corretto. R<strong>il</strong>evare un attacco dunque corrisponde<br />
all’accorgersi della presenza di questa anomalia. Sorgono alcune domande:<br />
• Quale è <strong>il</strong> comportamento corretto di un sistema?<br />
• Come può essere rappresentato?<br />
• Come si può r<strong>il</strong>evare un’anomalia?<br />
L’anomaly detection prevede l’esistenza di un <strong>modello</strong> che descriva quali sono i com-<br />
portamenti corretti del sistema. A runtime <strong>il</strong> sistema viene monitorato costantemen-<br />
te e viene verificata la conformità delle sue azioni rispetto al <strong>modello</strong>. Naturalmente<br />
più <strong>il</strong> <strong>modello</strong> è dettagliato più <strong>il</strong> r<strong>il</strong>evamento di anomalie sarà accurato e più <strong>il</strong> suo<br />
costo computazionale sarà elevato: la costruzione del <strong>modello</strong> è un punto critico. In<br />
letteratura è possib<strong>il</strong>e trovare moltissimi modelli <strong>per</strong> l’anomaly detection costruiti<br />
nei modi più svariati e con gli obiettivi più svariati. La distinzione fondamentale che<br />
<strong>per</strong>ò va fatta tra tutti i vari modelli esistenti è dovuta alla modalità con cui sono<br />
costruiti, ovvero se con tecniche black-box o con tecniche white-box. Le prime preve-<br />
dono soltanto l’osservazione di un processo a runtime, le seconde richiedono che sia<br />
fatta un’analisi sul codice sorgente o comunque sul codice binario del programma.<br />
La ricaduta fondamentale ovviamente è sull’applicab<strong>il</strong>ità. Il codice sorgente non<br />
sempre è disponib<strong>il</strong>e e le analisi sul binario sono tutt’altro che semplici. Nel caso
1.1. Anomaly detection e system call 3<br />
dell’architettura x86, attualmente la più diffusa in ambiente desktop e server, <strong>il</strong> pro-<br />
blema di disassemblare correttamente un binario è addirittura indecidib<strong>il</strong>e [25, 14].<br />
Certamente disassemblatori come IDAPro fanno un egregio lavoro ma <strong>il</strong> loro output,<br />
<strong>per</strong> quanto vicino al codice “vero”, è inerentemente non corretto. <strong>Un</strong>a conseguen-<br />
za di questo fatto è che la ricostruzione del <strong>control</strong> <strong>flow</strong> di un programma dato <strong>il</strong><br />
suo binario non è possib<strong>il</strong>e, se non in modo approssimato. <strong>Un</strong> interessante lavoro<br />
in questa direzione basato sull’interpretazione astratta è [11]. Le tecniche black-<br />
box di contro, pur essendo universalmente applicab<strong>il</strong>i, possono osservare soltanto le<br />
interazioni di un processo con l’ambiente (ad esempio col sistema o<strong>per</strong>ativo o con<br />
la rete). Potrebbe succedere <strong>per</strong>ò che, pur osservando <strong>per</strong> tempi molto lunghi un<br />
processo, non si riesca ad osservare tutte le interazioni ottenendo anche in questo<br />
caso un quadro incompleto. Le tecniche white-box tipicamente sono di tipo statico,<br />
mentre le black-box di tipo dinamico. Le prime quindi necessitano della capacità di<br />
riconoscere ed elaborare sintassi e semantica del programma che si vuole analizzare<br />
(quindi devono avere la capacità di leggere ed interpretare <strong>il</strong> codice sorgente o <strong>il</strong><br />
codice eseguib<strong>il</strong>e) e questo richiede <strong>il</strong> supporto di tool abbastanza complessi, come<br />
ad esempio <strong>il</strong> comp<strong>il</strong>atore, <strong>il</strong> che introduce un ulteriore livello di complessità. Le<br />
tecniche black-box al contrario richiedono una fase di apprendimento in cui tramite<br />
l’osservazione viene costruito <strong>il</strong> <strong>modello</strong>. La bontà di quest’ultimo è direttamente<br />
legata al training svolto. Come nel caso del testing del software, anche <strong>il</strong> training del<br />
<strong>modello</strong> deve essere fatto cercando di massimizzare la “co<strong>per</strong>tura” dei casi. Nel caso<br />
del testing <strong>per</strong>ò se la co<strong>per</strong>tura non è adeguata non si scoprono possib<strong>il</strong>i bug, nel<br />
caso dei modelli black-box invece si ha una quantità inaccettab<strong>il</strong>e di falsi positivi.<br />
1.1 Anomaly detection e system call<br />
<strong>Un</strong>a tecnica d’attacco notevolmente diffusa è quella di fornire ad un programma un<br />
input confezionato ad hoc <strong>per</strong> far si che esso esca dal suo comportamento previsto<br />
ed esegua delle azioni ut<strong>il</strong>i al malintenzionato. Nel 1996 Aleph One in [17] mostrava<br />
come, sfruttando una copia di stringhe fatta senza <strong>control</strong>lare i bound, fosse possib<strong>il</strong>e<br />
iniettare codice arbitrario in un programma e portarlo a lanciare una shell. Come<br />
si può immaginare, se <strong>il</strong> programma gira con priv<strong>il</strong>egi di su<strong>per</strong>utente, in questo<br />
modo è possib<strong>il</strong>e ottenere <strong>il</strong> <strong>control</strong>lo completo sul sistema. L’idea di questo tipo di<br />
attacco è molto semplice e in Figura 1.2 è riportato un programma vulnerab<strong>il</strong>e. Nel<br />
frammento di codice la stringa some other string viene copiata in buf, che è un<br />
vettore allocato sullo stack. Non essendoci alcun <strong>control</strong>lo sul numero di caratteri
4 1. Introduzione<br />
1 void g(void)<br />
2 {<br />
3 char buf[128];<br />
4 strcpy(buf, some_other_string);<br />
5 }<br />
6<br />
7 void f(void)<br />
8 {<br />
9 g();<br />
10 }<br />
Figura 1.2: Buffer over<strong>flow</strong>.<br />
copiati è possib<strong>il</strong>e scrivere oltre la fine del vettore, fino ad arrivare al record di<br />
attivazione della procedura g. Nel record di attivazione è memorizzato <strong>il</strong> punto<br />
del programma al quale restituire <strong>il</strong> <strong>control</strong>lo una volta che g è terminata: se lo<br />
sovrascriviamo con l’indirizzo di buf e in buf inseriamo del codice eseguib<strong>il</strong>e, esso<br />
verrà eseguito senza problemi. <strong>Un</strong>a buona prassi da seguire durante la stesura del<br />
codice consiste quindi nell’evitare di allocare vettori sullo stack e preferire l’ut<strong>il</strong>izzo di<br />
malloc(). Naturalmente questo non evita totalmente questo tipo di problemi, tant’è<br />
che dal buffer over<strong>flow</strong> (<strong>il</strong> tipo di attacco appena visto) si è passati ad altri attacchi<br />
più sofisticati che vanno sotto i nomi di jump to register, heap over<strong>flow</strong>, return into<br />
libc solo <strong>per</strong> citarne alcuni. Questo tipo di approccio ha avuto (ed ha) un successo<br />
tale che esistono dei framework (Metasploit) che <strong>per</strong>mettono la costruzione semi-<br />
automatica dell’attacco. Gli sforzi fatti <strong>per</strong> contrastare questo tipo di attacchi sono<br />
stati svariati e vanno da tecniche puramente software, tipo la Address Space Layout<br />
Randomization oppure lo Stack Protector di gcc a tecniche assistite dall’hardware,<br />
tipo <strong>il</strong> noto execute disable bit (XD) implementato nelle recenti CPU Intel e AMD.<br />
Nella stragrande maggioranza dei casi attaccare tramite l’iniezione di codice ma-<br />
levolo comporta l’esecuzione di chiamate di sistema estranee al normale flusso d’e-<br />
secuzione di un programma: <strong>per</strong> lanciare una shell ad esempio è necessario eseguire<br />
almeno una execve(). In Solaris è esistito un buffer over<strong>flow</strong> nel comando ping<br />
(CVE-1999-0056) che <strong>per</strong>metteva appunto l’esecuzione di codice arbitrario. Tale<br />
comando necessita di usare le raw socket, che <strong>per</strong>ò possono essere a<strong>per</strong>te solo dal<br />
su<strong>per</strong>utente: ping quindi era installato setuid root e dunque anche <strong>il</strong> codice inietta-<br />
to veniva eseguito a priv<strong>il</strong>egi elevati. Ora è chiaro che se ping esegue una execve,<br />
questo è un evento del tutto anomalo <strong>per</strong>ché <strong>per</strong> svolgere le sue funzioni <strong>il</strong> comando<br />
in questione non ha bisogno di lanciare in esecuzione alcun processo. Avendo questa
1.2. Obiettivo del lavoro 5<br />
informazione diventa quindi possib<strong>il</strong>e approntare un semplice sistema che osserva le<br />
chiamate di sistema e se non sono <strong>per</strong>tinenti prende provvedimenti quali impedirle<br />
o addirittura uccidere <strong>il</strong> processo dal quale sono state fatte.<br />
Già a metà degli anni ’90 ci si è resi conto che l’osservazione delle chiamate di<br />
sistema effettuate da un programma durante la sua esecuzione è un buon modo <strong>per</strong><br />
capire se <strong>il</strong> suo comportamento è normale o meno. In [9] ad esempio viene proposto<br />
un semplice metodo che, osservando le ultime tre chiamate di sistema effettuate da un<br />
processo, è in grado di stab<strong>il</strong>ire con buona approssimazione se <strong>il</strong> suo comportamento<br />
è anomalo. Negli anni successivi sono stati proposti svariati nuovi metodi basati<br />
sulle più disparate tecniche, sia statiche che dinamiche. Di queste ultime molte di<br />
esse sono di tipo probab<strong>il</strong>istico, molte di esse si basano su analisi più formali. Le<br />
più recenti tecniche dinamiche presenti in letteratura sono in grado di ricostruire<br />
una parte significativa del <strong>control</strong> <strong>flow</strong> di un programma. Successivamente queste<br />
tecniche sono state applicate con successo a sistemi virtualizzati, <strong>per</strong>mettendo di<br />
monitorare le attività dei processi in modo completamente invisib<strong>il</strong>e dall’interno<br />
della macchina virtuale [15].<br />
Presto ci si è resi conto che osservare soltanto qual’era la system call effettuata<br />
era limitativo e si è iniziato a prendere in considerazione anche i parametri. Anche in<br />
questo caso sono state proposte idee molto diverse, prevalentemente di tipo statistico.<br />
Recentemente <strong>per</strong>ò è stato proposto un <strong>modello</strong> in grado di apprendere <strong>il</strong> <strong>data</strong> <strong>flow</strong><br />
tra le chiamate di sistema.<br />
1.2 Obiettivo del lavoro<br />
Il lavoro presentato in questa tesi è centrato proprio sull’anomaly detection basata<br />
sull’osservazione delle system call. Tra i vari modelli che si possono costruire a<br />
questo scopo ne esistono alcuni basati su automi o su grafi, che si differenziano <strong>per</strong><br />
la loro precisione nel rappresentare <strong>il</strong> <strong>control</strong> <strong>flow</strong> del programma. Dal lato <strong>data</strong><br />
<strong>flow</strong> i modelli sono invece tutt’altro che numerosi. In questa tesi verranno prese in<br />
considerazione entrambe le tipologie di modelli, verranno studiate le loro possib<strong>il</strong>ità<br />
sia considerando i modelli presi singolarmente sia se integrati tra di loro (<strong>control</strong><br />
<strong>flow</strong> + <strong>data</strong> <strong>flow</strong>) e verranno evidenziate delle debolezze. Verrà quindi proposto un<br />
nuovo <strong>modello</strong> <strong>integrato</strong> che cerca di su<strong>per</strong>arle.
6 1. Introduzione<br />
1.3 Struttura della tesi<br />
La tesi si sv<strong>il</strong>uppa in 4 ulteriori capitoli oltre a quello presente.<br />
Nel secondo capitolo verranno esaminati alcuni modelli <strong>per</strong> <strong>il</strong> <strong>control</strong> <strong>flow</strong> presenti<br />
in letteratura, sia costruiti dinamicamente che staticamente. Verranno mostrate le<br />
tecniche necessarie alla loro costruzione e verranno discussi vantaggi e svantaggi dei<br />
modelli. Finita la discussione dei modelli <strong>control</strong> <strong>flow</strong> si osserverà come la protezione<br />
di quest’ultimo non sia sufficiente a impedire che un processo venga attaccato e verrà<br />
mostrato un semplice programma che è vulnerab<strong>il</strong>e ad un attacco che <strong>per</strong>mette di<br />
ottenere una shell senza che <strong>il</strong> suo <strong>control</strong> <strong>flow</strong> sia alterato. Si osserverà quindi<br />
che è necessario proteggere anche <strong>il</strong> <strong>data</strong>-<strong>flow</strong> e dopo una breve discussione verrà<br />
presentato un <strong>modello</strong> in grado di apprendere relazioni tra i parametri delle chiamate<br />
di sistema, anch’esso presente in letteratura.<br />
Il terzo capitolo si aprirà vedendo come anche unendo un execution graph (uno dei<br />
più potenti modelli <strong>control</strong> <strong>flow</strong>) con <strong>il</strong> <strong>modello</strong> <strong>data</strong> <strong>flow</strong> si possa comunque trovare<br />
dei casi in cui è possib<strong>il</strong>e attaccare <strong>il</strong> sistema senza essere sco<strong>per</strong>ti. Si osserverà<br />
quindi che questo è dovuto principalmente a due motivi che sono dati da un basso<br />
accoppiamento tra i due modelli e dalla relativa povertà delle informazioni <strong>data</strong> <strong>flow</strong><br />
raccolte. Il <strong>modello</strong> <strong>data</strong> <strong>flow</strong> può infatti trarre notevole vantaggio da informazioni<br />
già raccolte <strong>per</strong> la costruzione del <strong>modello</strong> <strong>control</strong> <strong>flow</strong>, inoltre verrà <strong>data</strong> la capacità<br />
al <strong>modello</strong> <strong>data</strong> <strong>flow</strong> di apprendere delle alternative. Senza <strong>il</strong> loro apprendimento vi<br />
sono casi in cui l’informazione raccolta è veramente povera. Si proporrà dunque un<br />
nuovo <strong>modello</strong> <strong>integrato</strong> <strong>per</strong> <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong> in grado di risolvere i problemi<br />
osservati, assieme a tutti gli algoritmi necessari a costruirlo.<br />
Il quarto capitolo tratterà l’implementazione. In una prima parte verrà analizza-<br />
to <strong>il</strong> framework DTrace che <strong>per</strong>metterà la raccolta dati necessaria alla costruzione del<br />
<strong>modello</strong>. Dopo aver visto come specificare quali dati si vuole raccogliere si passerà<br />
ad alcuni dettagli di libdtrace(3LIB), necessaria <strong>per</strong> l’interfacciamento low-level<br />
al framework e <strong>per</strong> l’estrazione dei dati raw. Verranno poi trattati alcuni dei dettagli<br />
implementativi salienti del sistema e infine, in modo del tutto informale, si discuterà<br />
sulla complessità computazionale del <strong>modello</strong>, sia dal punto di vista del training sia<br />
dal punto di vista della verifica online.<br />
Il quinto capitolo è dedicato alle conclusioni e alla descrizione dei possib<strong>il</strong>i sv<strong>il</strong>up-<br />
pi futuri di questo lavoro. In particolare saranno proposte tre possib<strong>il</strong>i estensioni.<br />
La prima cerca di “spremere” ulteriormente i dati già raccolti <strong>per</strong> la costruzione<br />
del <strong>modello</strong> <strong>per</strong> carpire più informazioni sulla struttura interna del programma. La
1.3. Struttura della tesi 7<br />
seconda parte dall’osservazione che questi modelli sono totalmente ciechi di fronte<br />
al denial of service e quindi la proposta in questo caso è di aggiungere una “dimen-<br />
sione” statistica al <strong>modello</strong> che vada in questa direzione. La terza idea è quella di<br />
cercare di costruire <strong>il</strong> <strong>modello</strong> <strong>control</strong> <strong>flow</strong> staticamente invece che dinamicamente,<br />
in modo che <strong>il</strong> comp<strong>il</strong>atore oltre a restituire l’eseguib<strong>il</strong>e restituisca un <strong>modello</strong> della<br />
sua struttura che verrà successivamente <strong>control</strong>lato a runtime.
8 1. Introduzione
2<br />
Modelli <strong>per</strong> l’anomaly detection<br />
In questa tesi si è interessati al r<strong>il</strong>evamento e alla conseguente segnalazione di com-<br />
portamenti anomali tenuti da parte di un processo in esecuzione su un elaboratore.<br />
Questo obiettivo prevede innanzitutto <strong>il</strong> possesso di un <strong>modello</strong> del comportamen-<br />
to del processo e successivamente la capacità di verificare che <strong>il</strong> processo, durante<br />
l’esecuzione, si comporti conformemente al <strong>modello</strong> (Figura 2.1).<br />
Processo<br />
Eventi<br />
Algoritmo di<br />
apprendimento<br />
Offline Online<br />
Modello<br />
Processo<br />
Eventi<br />
Motore di verifica<br />
del <strong>modello</strong><br />
Figura 2.1: Architettura generale di un sistema black-box.<br />
In letteratura si possono trovare decine di metodologie volte alla costruzione di<br />
modelli <strong>per</strong> l’anomaly detection basate su idee anche molto differenti tra di loro. La<br />
distinzione fondamentale <strong>per</strong>ò è forse quella tra metodologie white-box e black-box.<br />
Le prime prevedono di avere a disposizione <strong>il</strong> codice sorgente (o anche <strong>il</strong> binario)<br />
del programma in modo da poterlo analizzare staticamente e costruire un model-<br />
lo. Le seconde invece prevedono esclusivamente l’osservazione dell’esecuzione di un<br />
programma e, in base agli eventi generati, costruiscono un <strong>modello</strong>.
10 2. Modelli <strong>per</strong> l’anomaly detection<br />
Le tecniche white-box e black-box hanno entrambe vantaggi e svantaggi: se<br />
<strong>per</strong> esempio si è interessati alla struttura del programma è diffic<strong>il</strong>e ricostruirla solo<br />
guardandone varie esecuzioni. Se ci sono dei rami di codice morto <strong>per</strong> un sistema<br />
black-box è impossib<strong>il</strong>e scoprirli, mentre <strong>per</strong> un sistema white-box è immediato.<br />
Tuttavia, come vedremo, anche dalle sole osservazioni (a patto di eseguirle corretta-<br />
mente) è possib<strong>il</strong>e ottenere un’incredib<strong>il</strong>e quantità di informazioni. <strong>Un</strong>a delle molte<br />
situazioni in cui invece le tecniche black-box sono avvantaggiate è ad esempio quella<br />
in cui si sta osservando dove si trovano i f<strong>il</strong>e che una chiamata ad open() apre: se in<br />
un numero ragionevolmente grande di osservazioni si vede che i f<strong>il</strong>e stanno tutti in<br />
una <strong>data</strong> directory dir si può affermare che quella open() deve aprire solo f<strong>il</strong>e che<br />
stanno in dir. Questa, non conoscendo la struttura interna del programma osserva-<br />
to è sicuramente un’informazione tutt’altro che certa ma nonostante l’incertezza è<br />
comunque un’informazione che l’analisi statica nella maggioranza dei casi non può<br />
dare.<br />
In questa tesi si cercherà di modellare <strong>per</strong> via black-box sia <strong>il</strong> <strong>control</strong> <strong>flow</strong> del<br />
programma che <strong>il</strong> <strong>data</strong> <strong>flow</strong>. Verranno prese in considerazione diverse tecniche già<br />
note, le quali <strong>per</strong>ò in certi contesti presentano delle debolezze e dunque l’obiettivo<br />
è di migliorarle e di combinarle in modo da eliminare i problemi che presentano,<br />
ottenendo un <strong>modello</strong> in grado di r<strong>il</strong>evare un numero maggiore di attacchi.<br />
2.1 Modelli <strong>control</strong> <strong>flow</strong><br />
I modelli che analizzeremo in questa sezione sono basati sulla descrizione di proprietà<br />
che riguardano <strong>il</strong> flusso di <strong>control</strong>lo di un programma. Analizzando gli eventi che<br />
si osservano a runtime è possib<strong>il</strong>e costruire degli automi che rappresentano in modo<br />
più o meno fine le transizioni ammesse <strong>per</strong> un dato programma.<br />
2.1.1 Automi a stati finiti<br />
Il <strong>modello</strong> di automi a stati finiti sicuramente più interessante è stato proposto in<br />
[21]. Gli autori osservano come tutti i modelli precedenti abbiano problemi o limi-<br />
tazioni più o meno gravi, o di carattere computazionale [10, 18] o dovute al fatto<br />
che semplicemente è stata proposta una metodologia che poco si presta all’imple-<br />
mentazione [12] e propongono una tecnica molto veloce <strong>per</strong> apprendere un automa<br />
in grado di r<strong>il</strong>evare una consistente categoria di attacchi.<br />
L’automa viene costruito a partire da una o più tracce ottenute dall’osservazione<br />
del sistema. Ogni traccia è composta da un certo numero di eventi, rappresentab<strong>il</strong>i
2.1. Modelli <strong>control</strong> <strong>flow</strong> 11<br />
con una coppia (si, pi). Ogni evento contiene due informazioni e in particolare la<br />
chiamata di sistema che è stata eseguita e <strong>il</strong> punto del programma dal quale è stata<br />
eseguita.<br />
L’automa è rappresentab<strong>il</strong>e come un grafo G = (V ∪ {end}, E = V × V × L)<br />
e, dati due eventi consecutivi (si, pi) e (si+1, pi+1) di una traccia di lunghezza k, la<br />
costruzione avviene nel seguente modo:<br />
• <strong>per</strong> 0 ≤ i ≤ k: V = V ∪ {pi}<br />
• <strong>per</strong> 0 ≤ i ≤ k: E = E ∪ {(pi, pi+1, si)}<br />
• infine: E = E ∪ {(pk, end, sk)}<br />
L’idea dietro a questo automa è che <strong>per</strong> passare da uno stato ad un altro del pro-<br />
gramma deve avvenire una transizione causata da una chiamata di sistema. In-<br />
tuitivamente, questa costruzione porta quindi ad un automa in cui gli stati sono<br />
etichettati con <strong>il</strong> punto del programma dal quale viene eseguita la system call e gli<br />
archi con la chiamata di sistema coinvolta nella transizione. Vediamo un esempio.<br />
1 void f(int cond)<br />
2 {<br />
3 open();<br />
4 if (cond % 2)<br />
5 read();<br />
6 else<br />
7 write();<br />
8 close();<br />
9 }<br />
10<br />
11 int main(void)<br />
12 {<br />
13 int i = 3;<br />
14 wh<strong>il</strong>e (i--)<br />
15 f(i);<br />
16 }<br />
La traccia di una esecuzione del programma d’esempio sarebbe la seguente:<br />
(open, 3), (write, 7), (close, 8), (open, 3), (read, 5),<br />
(close, 8), (open, 3), (write, 7), (close, 8)
12 2. Modelli <strong>per</strong> l’anomaly detection<br />
3<br />
open<br />
open<br />
che da luogo agli insiemi<br />
5<br />
7<br />
close<br />
read<br />
write<br />
8<br />
Figura 2.2: FSA d’esempio.<br />
V ={3, 5, 7, 8, end}<br />
close<br />
E ={(3, 5, open), (3, 7, open), (5, 8, read),<br />
(7, 8, write), (8, 3, close), (8, end, close)}<br />
corrispondenti all’automa rappresentato in Figura 2.2.<br />
2.1.2 VtPath<br />
end<br />
VtPath [6] è un metodo che migliora gli FSA, andando a guardare l’intero user<br />
space stack del processo nel momento della chiamata di sistema invece di osservare<br />
soltanto <strong>il</strong> punto del programma in cui la chiamata è stata fatta. Questo sistema<br />
si basa sul concetto di virtual path, di seguito delineato. Siano A = {a1, . . . , an} e<br />
B = {b1, . . . , bm} gli stack osservati in due system call consecutive. Essi vengono<br />
confrontati finché non si trova un indice l tale che al = bl. A questo punto si definisce<br />
<strong>il</strong> path tra le due system call come:<br />
P = an → Exit; . . . ; al+1 → Exit; al → bl; Entry → bl+1; . . . ; Entry → bm<br />
Entry ed Exit sono dei nodi fittizi che rappresentano rispettivamente <strong>il</strong> punto<br />
d’ingresso e <strong>il</strong> punto d’uscita di una funzione. Questi path vengono appresi du-<br />
rante <strong>il</strong> training e, a training completato, vengono ut<strong>il</strong>izzati <strong>per</strong> verificare <strong>il</strong> cor-<br />
retto comportamento del programma. Possono generarsi differenti anomalie, in<br />
particolare:<br />
• Stack anomaly, se lo stack osservato non è tra quelli appresi durante <strong>il</strong> training<br />
• Return address anomaly, se uno qualunque dei return address sullo stack non<br />
è corretto rispetto a quelli osservati durante <strong>il</strong> training<br />
• System call anomaly, se la system call eseguita non è corretta
2.1. Modelli <strong>control</strong> <strong>flow</strong> 13<br />
• Virtual path anomaly, se non è possib<strong>il</strong>e trovare <strong>il</strong> <strong>per</strong>corso effettuato tra quelli<br />
appresi<br />
2.1.3 Execution graphs<br />
Gli execution graphs costituiscono forse <strong>il</strong> più potente <strong>modello</strong> <strong>control</strong> <strong>flow</strong> presente<br />
in letteratura ottenuto con tecniche black-box [7] ed è uno dei due componenti da<br />
cui si è partiti <strong>per</strong> sv<strong>il</strong>uppare <strong>il</strong> <strong>modello</strong> presentato in questa tesi.<br />
L’obiettivo dell’execution graph è quello di ottenere, osservando le esecuzioni di<br />
un programma, un <strong>modello</strong> che accetta le stesse sequenze di chiamate di sistema che<br />
sarebbero accettate da un <strong>modello</strong> basato sul <strong>control</strong> <strong>flow</strong> graph, quindi costruito<br />
staticamente. Naturalmente questo non è possib<strong>il</strong>e <strong>per</strong>ché nel codice potrebbero<br />
esserci dei rami morti, r<strong>il</strong>evab<strong>il</strong>i solo a tempo di comp<strong>il</strong>azione. Tuttavia ut<strong>il</strong>izzando<br />
solo tecniche black-box si riesce a costruire un execution graph con due proprietà<br />
molto importanti:<br />
• accetta solo sequenze di chiamate di sistema che sono consistenti con <strong>il</strong> <strong>control</strong><br />
<strong>flow</strong> graph del programma<br />
• <strong>il</strong> linguaggio accettato dall’execution graph è massimale rispetto ai dati ap-<br />
presi durante <strong>il</strong> training: in altre parole ogni estensione dell’execution graph<br />
potrebbe far passare inosservati degli attacchi<br />
Definizione 1 (Osservazione ed esecuzione) <strong>Un</strong>’osservazione è una n-pla di in-<br />
teri positivi 〈r1, r2, . . . , rk〉 con k > 1. <strong>Un</strong>’esecuzione è una sequenza di lunghezza<br />
arbitraria di osservazioni.<br />
In particolare in un’osservazione 〈r1, r2, . . . , rk〉, r1 è un indirizzo in main(), rk−1<br />
è <strong>il</strong> return address corrispondente all’istruzione che esegue la system call ed rk è <strong>il</strong><br />
numero corrispondente alla system call eseguita. In altre parole un’osservazione è<br />
una fotografia dello stack del processo al momento in cui viene eseguita una system<br />
call.<br />
Definizione 2 (Execution graph, foglia, crs<br />
→) <strong>Un</strong> execution graph <strong>per</strong> un insie-<br />
me di esecuzioni X è un grafo EG(X ) = (V, Ecall, Ecrs, Eret) in cui V è un insieme<br />
di nodi mentre Ecall, Ecrs, Eret ⊆ V × V sono insiemi di archi diretti, definiti come<br />
segue:
14 2. Modelli <strong>per</strong> l’anomaly detection<br />
• Per ogni esecuzione X ∈ X e ogni osservazione 〈r1, r2, . . . , rk〉 ∈ X, V contiene<br />
i nodi r1, . . . , rk. rk è una foglia dell’execution graph. Se l’osservazione a cui<br />
appartiene rk è la prima di un’esecuzione allora rk è detto anche nodo di<br />
ingresso, se è l’ultima è detto anche nodo di uscita.<br />
• Gli insiemi Ecall, Ecrs, Eret sono definiti induttivamente e contengono solo<br />
archi ottenuti dalle seguenti regole:<br />
– Caso base: Per ogni esecuzione X ∈ X e ogni coppia di osservazioni<br />
consecutive 〈r1, r2, . . . , rk〉 e 〈r ′<br />
1 , r′ 2<br />
dove: l =<br />
<br />
Se rk è un nodo d’ingresso<br />
Se rk è un nodo d’uscita<br />
, . . . , r′<br />
k ′ 〉 in X<br />
Ertn ← Ertn ∪ {(ri+1, ri)}l≤i
2.1. Modelli <strong>control</strong> <strong>flow</strong> 15<br />
1 int main() {<br />
2 int a, b;<br />
3 a = 1; b = 2;<br />
4 f(a);<br />
5 g();<br />
6 f(b);<br />
7 }<br />
8<br />
9 void f(int x) {<br />
10 syscall5();<br />
11 if (x == 1)<br />
12 syscall3();<br />
13 else if (x == 2)<br />
14 syscall4();<br />
15 }<br />
16<br />
17 void g() {<br />
18 syscall2();<br />
19 }<br />
f.10<br />
main.4 main.5<br />
f.12 f.14<br />
main.6<br />
g.18<br />
syscall5() syscall3() syscall4() syscall2()<br />
Figura 2.3: Esempio riguardante la parte induttiva della definizione dell’EG.<br />
ma non osservati potrebbero non essere sco<strong>per</strong>ti, in questo caso particolare quelli da<br />
f.14 a main.4 e da f.12 a main.6. Grazie alla parte induttiva si riescono a ricavare<br />
senza problemi.<br />
Definizione 3 ( call<br />
→, rtn<br />
→) Sia EG(X ) = (V, Ecall, Ecrs, Ertn) un execution graph.<br />
r call<br />
→ r ′<br />
se e solo se esiste un <strong>per</strong>corso da r a r ′<br />
Analogamente r rtn<br />
→ r ′<br />
archi in Ertn.<br />
se e solo se esiste un <strong>per</strong>corso da r a r ′<br />
costituito solo da archi in Ecall.<br />
costituito solo da<br />
Definizione 4 ( xcall<br />
→ , Execution stack) Sia EG(X ) = (V, Ecall, Ecrs, Ertn) un execution<br />
graph. r xcall<br />
→ r ′<br />
se e solo se:<br />
• (r, r ′<br />
) ∈ Ecall oppure<br />
• esiste r ′′<br />
∈ V tale che (r, r ′′<br />
′′ crs<br />
) ∈ Ecall e r → r ′<br />
Definizione 5 (Successore) <strong>Un</strong> execution stack s ′ = 〈r ′<br />
1<br />
, r′ 2<br />
, . . . , r′<br />
n ′ 〉 è un suc-<br />
cessore di s = 〈r1, r2, . . . , rn〉 in un execution graph se esiste un intero k tale che<br />
rn rtn<br />
→ rk, (rk, r ′<br />
k ) ∈ Ecrs, r ′<br />
k<br />
call<br />
→ r ′<br />
n ′ e ri = r ′<br />
i<br />
<strong>per</strong> 1 ≤ i < k.<br />
Intuitivamente quest’ultima definizione significa che affinché s ′ sia successore di s i<br />
due stack devono:
16 2. Modelli <strong>per</strong> l’anomaly detection<br />
• essere uguali nei livelli dal primo al k-esimo<br />
• nel caso di s ci devono essere tutti gli archi di return da rn a rk<br />
• nel caso di s ′ ci devono essere tutti gli archi di call da r ′<br />
k a r′<br />
n ′<br />
• deve esserci un arco da rk a r ′<br />
k in Ecrs<br />
rn<br />
...<br />
rk<br />
...<br />
r2<br />
r1<br />
rtn<br />
rtn<br />
crs<br />
Figura 2.4: Rappresentazione grafica del significato di successore.<br />
Questa definizione è forse la più importante di questa sezione: essa gioca un ruo-<br />
lo cruciale nell’implementazione del metodo degli execution graph <strong>per</strong>ché specifica<br />
esattamente ciò che deve fare <strong>il</strong> software. <strong>Un</strong>a volta eseguito <strong>il</strong> training l’IDS avrà<br />
a disposizione gli insiemi V, Ecall, Ecrs ed Ertn e <strong>per</strong> verificare che <strong>il</strong> programma<br />
stia eseguendo o<strong>per</strong>azioni conformi a quelle apprese durante <strong>il</strong> training è sufficiente<br />
verificare che valga la relazione di successore tra gli stack che man mano si osservano.<br />
2.1.4 Abstract stack, un <strong>modello</strong> costruito staticamente<br />
Per completezza riportiamo un <strong>modello</strong> costruito staticamente [24], denominato ab-<br />
stract stack model. L’idea in questo caso è quella di costruire un automa pushdown<br />
non deterministico che riconosce un linguaggio context-free. I simboli del linguaggio<br />
sono le chiamate di sistema.<br />
Supponiamo di avere <strong>il</strong> <strong>control</strong> <strong>flow</strong> graph G = 〈V, E〉 del programma, CFG che<br />
include gli archi interprocedurali. Si costruisce un NDPDA <strong>il</strong> quale ha un alfabeto<br />
che provoca o<strong>per</strong>azioni sullo stack V ∪ Σ, un alfabeto di input Σ e un insieme<br />
di transizioni che avvengono come spiegato di seguito. Inizialmente nello stack<br />
dell’automa è presente un simbolo v ∈ V :<br />
• se v è un nodo corrispondente ad una chiamata alla funzione f lo si toglie dallo<br />
stack, inserendo successivamente <strong>il</strong> nodo di ritorno v ′ e <strong>il</strong> punto di ingresso di<br />
f denotato con Entry(f)<br />
call<br />
call<br />
r'n'<br />
...<br />
r'k<br />
...<br />
r'2<br />
r'1
2.2. Modelli <strong>data</strong> <strong>flow</strong> 17<br />
• se v è Exit(f) semplicemente si rimuove v<br />
• se v non riguarda una chiamata di funzione ma una system call si toglie v e si<br />
inserisce s ∈ Σ e non-deterministicamente si sceglie w : (v, w) ∈ E e si inserisce<br />
w<br />
Se invece s si trova sulla cima dello stack si verifica che s = s ′ , dove s ′ è <strong>il</strong> simbolo<br />
corrente dell’input. Se l’uguaglianza è verificata si estrae s dallo stack e si procede,<br />
altrimenti si entra in uno stato d’errore che viene segnalato.<br />
2.2 Modelli <strong>data</strong> <strong>flow</strong><br />
I precedenti modelli visti focalizzano la loro attenzione esclusivamente sul flusso di<br />
<strong>control</strong>lo del programma, senza tenere in alcuna considerazione i dati (ovvero i pa-<br />
rametri) coinvolti nelle chiamate di sistema. Tuttavia monitorare anche i parametri<br />
si rivela di notevole importanza. Si prenda ad esempio un attacco che sfrutta una<br />
race condition del tipo TOCTTOU (Time of check to time of use) dove una risorsa<br />
riferita da un nome cambia tra <strong>il</strong> momento in cui viene fatto un test e <strong>il</strong> momento<br />
in cui viene usata (si veda l’esempio di Figura 2.5). In un attacco come questo le<br />
chiamate di sistema fatte da un programma sono sempre le stesse ma l’interpreta-<br />
zione dei loro parametri in un momento piuttosto che in un altro è completamente<br />
diversa.<br />
if (access("f<strong>il</strong>e", W_OK) != 0) {<br />
exit(1);<br />
}<br />
/* In questo esatto momento in un altro processo un<br />
attaccante esegue symlink("/etc/passwd", "f<strong>il</strong>e"); */<br />
fd = open("f<strong>il</strong>e", O_WRONLY);<br />
write(fd, buffer, sizeof(buffer));<br />
Figura 2.5: Time to check to time of use.<br />
<strong>Un</strong> altro scenario è quello delineato in [4]. Gli autori notano che chi attacca<br />
un sistema attualmente è concentrato su metodologie che portano <strong>il</strong> processore ad<br />
eseguire codice in qualche modo estraneo al programma (<strong>control</strong> <strong>data</strong> attacks: buffer<br />
over<strong>flow</strong>, heap over<strong>flow</strong>, return-to-libc,... ricadono in questa categoria). Da questa<br />
osservazione si chiedono se, nel momento in cui <strong>il</strong> <strong>control</strong> <strong>flow</strong> è protetto, diventa
18 2. Modelli <strong>per</strong> l’anomaly detection<br />
possib<strong>il</strong>e e realistico costruire degli attacchi (non-<strong>control</strong> <strong>data</strong> attacks) che non hanno<br />
bisogno di portare <strong>il</strong> sistema ad eseguire codice malevolo. Dal loro studio emerge che<br />
oltre ad essere <strong>per</strong>fettamente possib<strong>il</strong>e, la gravità degli attacchi non-<strong>control</strong> <strong>data</strong> è<br />
equivalente a quella dei più classici <strong>control</strong>-<strong>data</strong>. In [4] vengono identificate 4 classi<br />
di dati critici <strong>per</strong> la sicurezza del software:<br />
• dati di configurazione<br />
• input dell’utente<br />
• identità dell’utente<br />
• dati di decision-making<br />
1 #define STRSZ 32<br />
2<br />
3 int authenticate(char *user, char *password) {<br />
4 if ( (strncmp(user, "matteo", STRSZ) == 0) &&<br />
5 (strncmp(password, "pippo123", STRSZ) == 0) )<br />
6 return 1;<br />
7<br />
8 return 0;<br />
9 }<br />
10<br />
11 int main(void) {<br />
12 int authenticated = 0;<br />
13 char user[32], password[32];<br />
14<br />
15 gets(user);<br />
16 gets(password);<br />
17<br />
18 if ( authenticate(user, password) )<br />
19 authenticated = 1;<br />
20<br />
21 if (authenticated) system("/bin/sh");<br />
22 else printf("Not allowed\n");<br />
23 }<br />
Figura 2.6: Banale programma vulnerab<strong>il</strong>e ad un non-<strong>control</strong> <strong>data</strong> attack.<br />
Delle quattro l’ultima è sicuramente la più interessante: vi sono alcuni dati che so-<br />
no necessari affinché un programma decida che azione intraprendere e, riuscendo a<br />
corrom<strong>per</strong>li, è possib<strong>il</strong>e portare un programma a fare qualcosa di contrario rispetto<br />
a ciò che andrebbe fatto. Prendiamo ad esempio <strong>il</strong> programma di Figura 2.6: <strong>il</strong>
2.2. Modelli <strong>data</strong> <strong>flow</strong> 19<br />
suo scopo è autenticare un utente su un sistema e <strong>per</strong> farlo ut<strong>il</strong>izza una procedura<br />
authenticate() (che potrebbe essere di complessità arbitraria). Se l’autenticazione<br />
ha successo viene impostato un flag <strong>per</strong> ricordarlo ed <strong>il</strong> programma può procedere.<br />
L’uso non sicuro di gets() <strong>per</strong>mette <strong>per</strong>ò ad un utente malintenzionato di impostar-<br />
lo ad un valore diverso da zero e, anche se l’autenticazione non ha successo, l’accesso<br />
al sistema viene garantito. Al di là del fatto che gets() è qualcosa di talmente<br />
<strong>per</strong>icoloso che se viene usata viene generato un warning dal comp<strong>il</strong>atore, in un caso<br />
come questo un IDS potrebbe verificare i valori di ritorno delle read() provocate<br />
dalle gets() ed, essendo anomali, uccidere <strong>il</strong> processo.<br />
È notevole fino a dove ci si sia spinti con attacchi classificab<strong>il</strong>i come non-<strong>control</strong><br />
<strong>data</strong>: in [3], provocando fallimenti casuali all’hardware e dunque errori nelle com-<br />
putazioni, è stato mostrato come sia possib<strong>il</strong>e rendere del tutto vulnerab<strong>il</strong>i alcuni<br />
codici crittografici, RSA compreso.<br />
necessario proteggere anche <strong>il</strong> <strong>data</strong> <strong>flow</strong>.<br />
2.2.1 <strong>Un</strong> <strong>modello</strong> <strong>data</strong> <strong>flow</strong><br />
È dunque chiaro come, oltre al <strong>control</strong> <strong>flow</strong>, sia<br />
Nel tempo diversi sono stati i tentativi di ut<strong>il</strong>izzare anche i parametri nel r<strong>il</strong>evamento<br />
di comportamenti anomali [13, 22, 8] e gli approcci ut<strong>il</strong>izzati sono stati prevalen-<br />
temente statistici. <strong>Un</strong> <strong>modello</strong> notevole <strong>per</strong>ò è quello proposto in [2]. Gli autori<br />
mostrano una tecnica che <strong>per</strong>mette di apprendere delle relazioni tra i parametri del-<br />
le system call. Ad esempio, se viene eseguita una open() ed <strong>il</strong> f<strong>il</strong>e descriptor da<br />
essa restituito viene usato da una successiva read() e da una successiva close(), <strong>il</strong><br />
sistema è in grado di apprendere che <strong>il</strong> valore del f<strong>il</strong>e descriptor tra le tre chiamate<br />
deve essere uguale.<br />
Il sistema è in grado di apprendere sia relazioni unarie, ovvero che valgono pun-<br />
tualmente su un singolo argomento, sia binarie, ovvero che valgono tra due argomenti<br />
differenti.<br />
Algoritmo 1: Algoritmo del <strong>modello</strong> <strong>data</strong> <strong>flow</strong>.<br />
learnRelations(EvArg X, Value V);<br />
Y = lookup(V );<br />
CurRels[R][X] = CurRels[R][X] ∩ Y;<br />
Yn = Y ∩ NewArgs(X);<br />
CurRels[R][X] = CurRels[R][X] ∪ Yn;<br />
update(X, V );
20 2. Modelli <strong>per</strong> l’anomaly detection<br />
Nello pseudocodice R è la relazione che si vuole apprendere, che potrebbe essere<br />
equals, elementOf, inRange, ... La relazione appresa dipende sostanzialmente<br />
da cosa restituisce lookup() nel primo passo: questa chiamata infatti ha <strong>il</strong> compito<br />
di restituire tutti i nomi dei parametri precedentemente visti che avevano valore in<br />
relazione con V . Ad esempio:<br />
Esempio 1 (lookup) Si è osservata:<br />
• la traccia (X = 5); (Y = 6); (Z = 5); (W = 4); (Q = 5) e si sta processan-<br />
do l’evento P = 5. Si vuole imparare la relazione equals, quindi lookup(5)<br />
restituisce {X, Z, Q}.<br />
• la traccia (X = /a/b/c); (Y = /a/b); (Z = /zz); (W = /a/b/c/d) e si sta<br />
processando l’evento Q = /a. Si vuole apprendere la relazione isWithinDir,<br />
quindi lookup() dovrà restituire {X, Y, W }<br />
Prima di proseguire dobbiamo notare che in questo modo si apprende la R esclusi-<br />
vamente rispetto ad un preciso punto di una traccia. Quello che invece si vuole è<br />
apprendere qualcosa che sia valido lungo tutta la traccia, ovvero RT . Quindi, se R<br />
è la relazione che si vuole apprendere, è necessario definire <strong>il</strong> suo lifting RT su una<br />
traccia. Quello che l’algoritmo apprenderà sarà proprio RT (RT se applicato a più<br />
tracce).<br />
Definizione 6 (Lifting di una relazione R su una traccia) Sia <strong>data</strong> una rela-<br />
zione R tra due argomenti X e Y . Scriviamo X RT Y se e solo se <strong>per</strong> ogni oc-<br />
correnza di X e l’occorrenza di Y immediatamente precedente vale X R Y . Sia T<br />
l’insieme di tutte le tracce osservate: se ∀T ∈ T : X RT Y allora X RT Y .<br />
Nel secondo passo CurRels tiene traccia della RT finora appresa e, intersecando<br />
con <strong>il</strong> risultato di lookup() vengono scartati tutti quei parametri <strong>per</strong> i quali non vale<br />
più R. Nel terzo passo NewArgs(X) è una funzione che restituisce tutti gli eventi<br />
che compaiono <strong>per</strong> la prima volta dopo la precedente occorrenza di X e, intersecando<br />
<strong>il</strong> suo risultato con Y, si possono identificare i nuovi eventi che si trovano in relazione<br />
con X, <strong>per</strong> poi aggiungerli a CurRels al passo successivo.<br />
Esempio 2 (NewArgs) Sia <strong>data</strong> la sequenza di eventi<br />
A,B,C,D,B,E,C,F,D,A,...<br />
• NewArgs(B) è {A} <strong>per</strong> la prima occorrenza di B, è {C, D} <strong>per</strong> la seconda
2.3. Discussione dei modelli studiati 21<br />
• NewArgs(C) è {A, B} <strong>per</strong> la prima occorrenza di C, è {D, E} <strong>per</strong> la seconda<br />
In Figura 2.7 è riportato un piccolo programma insieme alle informazioni apprese<br />
dal <strong>modello</strong> <strong>data</strong> <strong>flow</strong>.<br />
1 int main(void)<br />
2 {<br />
3 int fd;<br />
4 fd = open("test.txt");<br />
5 write(fd,"Hello");<br />
6 close(fd);<br />
7 }<br />
fd@5 equals fd@4<br />
fd@6 equals fd@4<br />
fd@6 equals fd@5<br />
Figura 2.7: Informazioni apprese a runtime dall’analisi <strong>data</strong> <strong>flow</strong>.<br />
2.3 Discussione dei modelli studiati<br />
In questo capitolo sono stati descritti quattro modelli <strong>control</strong> <strong>flow</strong> ed uno <strong>data</strong> <strong>flow</strong>.<br />
Dei quattro modelli <strong>data</strong> <strong>flow</strong>, tre sono costruiti black-box, ovvero senza bisogno di<br />
aver accesso al codice sorgente: questo <strong>per</strong>mette una loro applicazione universale<br />
a differenza del quarto <strong>modello</strong> che, pur essendo potenzialmente più preciso, trova<br />
limitata applicab<strong>il</strong>ità. La costruzione di questo <strong>modello</strong>, dovendo essere effettuata<br />
staticamente, deve essere supportata da un tool in grado di riconoscere <strong>il</strong> linguaggio<br />
sorgente del programma o addirittura dal comp<strong>il</strong>atore stesso e questo ne accresce<br />
notevolmente la complessità implementativa. Inoltre, <strong>per</strong> gli execution graphs, in<br />
[7] viene dimostrata una proprietà molto importante, ovvero che <strong>il</strong> linguaggio rico-<br />
nosciuto da un execution graph è contenuto in quello riconosciuto dal <strong>control</strong> <strong>flow</strong><br />
graph del medesimo programma. Questo significa che, a patto di eseguire un trai-<br />
ning corretto, viene colta praticamente tutta la struttura del programma ut<strong>il</strong>e <strong>per</strong><br />
l’anomaly detection.<br />
I modelli sono stati presentati in ordine crescente di capacità di rivelare eventuali<br />
anomalie e le loro caratteristiche sono riassunte nella Tabella 2.1. Le relazioni ⊆ e<br />
⊇ in questo caso significano rispettivamente che le sequenze accettate dal <strong>modello</strong><br />
sono un sottoinsieme e un soprainsieme di quelle ammissib<strong>il</strong>i dal <strong>control</strong> <strong>flow</strong> graph.<br />
All’atto pratico questo si traduce nella presenza di falsi positivi e falsi negativi. In<br />
particolare <strong>il</strong> <strong>modello</strong> FSA può riconoscere come non valide transizioni che invece<br />
lo sono, generando falsi positivi (a causa di un training insufficiente) ma può anche<br />
riconoscere come valide transizioni che non lo sono, generando falsi negativi (ad
22 2. Modelli <strong>per</strong> l’anomaly detection<br />
esempio <strong>il</strong> problema dell’impossible path) e questo è causato dall’eccessiva semplicità<br />
del <strong>modello</strong>.<br />
L’execution graph invece può certamente generare falsi positivi, quindi riconosce-<br />
re come attacchi sequenze di stack che non lo sono (e questo è causato di nuovo dal<br />
training insufficiente), ma le successioni di stack che ammette sono tutte ammesse<br />
anche dal <strong>control</strong> <strong>flow</strong> graph.<br />
Modello Costruzione Relazione con CFG<br />
FSA Dinamica né ⊆ né ⊇<br />
VtPath Dinamica né ⊆ né ⊇<br />
Execution graph Dinamica Dimostrato formalmente ⊆<br />
Abs. Stack Statica = <strong>per</strong> costruzione<br />
Tabella 2.1: Caratteristiche dei modelli.<br />
Il <strong>modello</strong> FSA è estremamente semplice e se l’automa viene costruito con l’op-<br />
portuna cura si rivela efficace contro una discreta quantità di attacchi. <strong>Un</strong> buffer<br />
over<strong>flow</strong> <strong>per</strong> esempio, fatto nel modo standard [17], viene rivelato senza grossi pro-<br />
blemi. Anche l’implementazione di un IDS basato su questa tecnica non presenta<br />
difficoltà, se non quelle legate alla gestione di fork ed exec [21]. La semplicità del<br />
<strong>modello</strong> <strong>per</strong>ò è anche la sua debolezza ed è fac<strong>il</strong>e immaginare casi in cui l’automa<br />
non è in grado di osservare comportamenti anomali. <strong>Un</strong>o di questi casi è quello<br />
dell’impossible path. Supponiamo che in un programma ci sia una funzione f() e<br />
che <strong>il</strong> codice al suo interno sia vulnerab<strong>il</strong>e ad un buffer over<strong>flow</strong>: l’attaccante po-<br />
trebbe modificare <strong>il</strong> return address di f() in modo da farla ritornare in un punto<br />
diverso da quello di chiamata (<strong>il</strong> codice di Figura 2.3 potrebbe essere un esempio di<br />
questo scenario, si entra da riga 4 e si esce da riga 6). Questo <strong>modello</strong> è totalmente<br />
cieco a questo tipo di attacco e questa è una mancanza grave, in quanto <strong>il</strong> codice<br />
tra le due invocazioni di f() potrebbe essere ad esempio quello che verifica delle<br />
credenziali e consente o meno l’accesso al sistema.<br />
Il <strong>modello</strong> VtPath si avvicina all’execution graph e <strong>per</strong> come è costruito è ve-<br />
rosim<strong>il</strong>e che le stringhe di system call che accetta sono un sottoinsieme di quelle<br />
che sarebbero accettate dal <strong>control</strong> <strong>flow</strong> graph, questo fatto <strong>per</strong>ò non è provato<br />
formalmente dagli autori.<br />
Il <strong>modello</strong> dell’execution graph infine è sicuramente quello più interessante pro-<br />
prio <strong>per</strong> <strong>il</strong> fatto che è stato relazionato formalmente con <strong>il</strong> <strong>control</strong> <strong>flow</strong> graph, mo-<br />
strando che le stringhe riconosciute sono contenute in quest’ultimo. Questo significa<br />
che possono esserci casi in cui l’execution graph riconosce come <strong>il</strong>lecita un’azione
2.3. Discussione dei modelli studiati 23<br />
invece consentita (e questo succede a causa del training insufficiente) ma non ci<br />
sono casi in cui sequenze non contenute nel <strong>control</strong> <strong>flow</strong> graph passano inosservate<br />
all’execution graph.<br />
Si è poi visto che una volta che <strong>il</strong> <strong>control</strong> <strong>flow</strong> è protetto è comunque possib<strong>il</strong>e<br />
attaccare un programma con attacchi del tipo non-<strong>control</strong>-<strong>data</strong>. Non è quindi suffi-<br />
ciente proteggere solo <strong>il</strong> flusso di <strong>control</strong>lo ma è necessario proteggere anche quello<br />
dei dati. Si è allora studiato un <strong>modello</strong> <strong>data</strong> <strong>flow</strong> in grado di apprendere relazioni<br />
tra i parametri delle system call.<br />
L’obiettivo sarà, nel prossimo capitolo di questa tesi, integrare execution graph e<br />
<strong>data</strong> <strong>flow</strong> in un unico <strong>modello</strong> più potente e migliorarne la capacità di individuazione<br />
di comportamenti anomali.
24 2. Modelli <strong>per</strong> l’anomaly detection
3<br />
<strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong><br />
<strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
L’execution graph e <strong>il</strong> <strong>modello</strong> <strong>data</strong> <strong>flow</strong> visti, a patto di eseguire correttamente <strong>il</strong><br />
training, se uniti costituiscono uno strumento molto potente <strong>per</strong> <strong>il</strong> r<strong>il</strong>evamento di<br />
comportamenti anomali da parte del software che gira su un elaboratore. Il <strong>modello</strong><br />
costruito con questa tecnica rappresenta molto fedelmente e in modo molto sintetico<br />
quello che è concesso e quello che non è concesso fare ad un programma. Tuttavia,<br />
anche assumendo un training <strong>per</strong>fetto, vi sono situazioni in cui diventa impossib<strong>il</strong>e<br />
r<strong>il</strong>evare che <strong>il</strong> programma monitorato non sta facendo quello <strong>per</strong> cui è stato pensato.<br />
La debolezza sorge dal fatto che l’accoppiamento del <strong>modello</strong> <strong>data</strong> <strong>flow</strong> con <strong>il</strong> <strong>modello</strong><br />
<strong>control</strong> <strong>flow</strong> è relativamente basso.<br />
3.1 Debolezze dei modelli esistenti<br />
Il software, come è noto, è costruito “a strati” e tipicamente le parti più basse pos-<br />
sono essere richiamate da diverse parti che si collocano più in alto nell’architettura.<br />
Ad esempio, un web server avrà del codice dedicato alla gestione dei log, codice<br />
che viene richiamato dal codice che si occupa dell’autenticazione, da quello che si<br />
occupa dell’interazione con i client e così via. Pensando ad un’ipotetica funzione<br />
log event(), se questa viene chiamata dal codice di autenticazione provocherà la<br />
scrittura nel log-f<strong>il</strong>e che un dato utente, ad esempio, ha immesso la password errata,<br />
mentre se chiamata dal codice che interagisce con i client provocherà, ad esempio, la<br />
scrittura di un messaggio che informa l’amministratore che un dato client ha inviato<br />
una richiesta malformata. Le proprietà <strong>data</strong> <strong>flow</strong> apprese dal <strong>modello</strong> di [2] <strong>per</strong>ò<br />
non saranno in grado di dire nulla di più di qualcosa come
26 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
“la funzione log event() può scrivere nel log-f<strong>il</strong>e che un utente ha fallito<br />
l’autenticazione oppure che un client ha inviato una richiesta malforma-<br />
ta”.<br />
Questo <strong>per</strong>mette ad un ipotetico attaccante di costruire attacchi tali che <strong>il</strong> pro-<br />
gramma, pur rimanendo all’interno del <strong>control</strong> <strong>flow</strong> ammesso, riporti informazioni<br />
diverse da quelle che normalmente dovrebbe riportare. Nel contesto di un processo<br />
industriale critico, questo potrebbe rappresentare un notevole problema. Sarebbe<br />
ideale quindi riuscire a far si che le informazioni <strong>data</strong> <strong>flow</strong> che vengono apprese siano<br />
qualcosa di più dettagliato, qualcosa del tipo<br />
“la funzione log event(), se chiamata dal codice che si occupa dell’au-<br />
tenticazione può scrivere nel log-f<strong>il</strong>e che un utente ha inserito la password<br />
errata, mentre se chiamata dal codice di dialogo col client può scrivere<br />
che un client ha inviato una richiesta malformata”.<br />
L’osservazione fondamentale che consente questo miglioramento è che una chiama-<br />
ta di sistema è caratterizzata in modo molto più preciso se, invece di considerare<br />
soltanto la sua posizione assoluta nel codice, si considera anche <strong>il</strong> contesto in cui è<br />
eseguita, ovvero la sequenza di record di attivazione che la precedono sullo stack.<br />
Nei due casi si osserveranno sequenze differenti e questo consente di capire che si<br />
è arrivati a log event() da due strade diverse. L’informazione relativa allo stack<br />
viene già raccolta <strong>per</strong> la costruzione dell’execution graph e dunque vale la pena far<br />
si che ne benefici anche <strong>il</strong> <strong>modello</strong> <strong>data</strong> <strong>flow</strong>. In questo modo, <strong>per</strong> un ipotetico IDS<br />
costruito con queste tecniche, è immediato capire da dove proviene la richiesta di<br />
inserire un messaggio nel log-f<strong>il</strong>e e quindi capire se quel messaggio è lecito oppure<br />
no. Vediamo, nella pratica, come può presentarsi questo scenario.<br />
3.1.1 Primo scenario: vulnerab<strong>il</strong>ità nel codice<br />
Nel programma della Figura 3.1 è presente un evidente buffer over<strong>flow</strong>, dovuto al-<br />
l’uso non sicuro della funzione strcpy(). Il buffer over<strong>flow</strong> <strong>per</strong>mette di eseguire<br />
codice arbitrario e, nel caso <strong>il</strong> programma sia installato setuid root, <strong>per</strong>mette ad un<br />
attaccante di prendere <strong>il</strong> <strong>control</strong>lo completo del sistema. <strong>Un</strong> attacco tramite buffer<br />
over<strong>flow</strong> fatto nel modo standard tuttavia viene immediatamente r<strong>il</strong>evato anche dal<br />
più semplice FSA (Figura 3.2). Quello che <strong>per</strong>ò l’FSA non può r<strong>il</strong>evare è un attacco<br />
che, iniettando codice opportuno in buf, esegue un numero arbitrario di chiamate<br />
a put str() (e quindi a write) con parametri arbitrari. Le cose non migliorano di
3.1. Debolezze dei modelli esistenti 27<br />
1 #include <br />
2<br />
3 void put_msg(char *msg)<br />
4 {<br />
5 write(1, msg, strlen(msg));<br />
6 }<br />
7<br />
8 void g(void)<br />
9 {<br />
10 put_msg("Hello,");<br />
11 }<br />
12<br />
13 void f(char *path)<br />
14 {<br />
15 char buf[128]; /* Il programmatore non ha usato MAX_PATH! */<br />
16 strcpy(buf, path);<br />
17 /* ... qualcosa che non siano system calls ... */<br />
18 }<br />
19<br />
20 int main(void)<br />
21 {<br />
22 char *path = chiedi_nome_f<strong>il</strong>e(); /* read() */<br />
23 f(path);<br />
24 g();<br />
25 put_msg("world\n");<br />
26 }<br />
Figura 3.1: Programma vulnerab<strong>il</strong>e d’esempio.<br />
22<br />
read<br />
write<br />
5<br />
write<br />
end<br />
Figura 3.2: FSA <strong>per</strong> <strong>il</strong> primo esempio.
28 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
molto aggiungendo la <strong>data</strong> <strong>flow</strong> analysis standard al <strong>modello</strong> e, sempre sfruttando<br />
l’over<strong>flow</strong>, è possib<strong>il</strong>e condurre un attacco che sfugge anche ad un ipotetico IDS<br />
basato su execution graphs e <strong>data</strong> <strong>flow</strong>.<br />
chiedi_nome_f<strong>il</strong>e<br />
read main.22<br />
main.24 main.25<br />
g.10<br />
put_msg.5<br />
write<br />
Figura 3.3: Execution graph <strong>per</strong> <strong>il</strong> primo esempio.<br />
Supponiamo quindi di avere a disposizione un sistema IDS basato su execution<br />
graphs e <strong>data</strong> <strong>flow</strong> analisys. Quello che imparerebbe <strong>per</strong> questo programma è che i<br />
valori ammissib<strong>il</strong>i come secondo parametro della write sono Hello, e world\n:<br />
buf@5 elementOf {“Hello,”, “world\n” }<br />
A livello di <strong>control</strong> <strong>flow</strong> invece imparerebbe, nel caso di EG, un automa (Figura 3.3)<br />
che ammette soltanto la seguente sequenza di stack:<br />
[main.22|chiedi_nome_f<strong>il</strong>e|read]<br />
[main.24|g.10|put_msg.5|write]<br />
[main.25|put_msg.5|write]<br />
<strong>Un</strong> ipotetico attacco dunque, <strong>per</strong> non essere sco<strong>per</strong>to, dovrebbe rispettare i seguenti<br />
vincoli:<br />
• eseguire una read seguita da due write<br />
• eseguire le chiamate di sistema facendo in modo che lo stack, durante la loro<br />
esecuzione, rispetti una certa struttura<br />
• eseguire le write con i parametri Hello, oppure world\n
3.1. Debolezze dei modelli esistenti 29<br />
main<br />
main<br />
main.24<br />
main<br />
main.25<br />
main<br />
0x1234<br />
main<br />
1 2 3<br />
g.11<br />
msg<br />
main.25<br />
main<br />
4 5 6<br />
Figura 3.4: Passi dell’attacco al programma.<br />
Ci rimane quindi la possib<strong>il</strong>ità di attaccare <strong>il</strong> programma in modo da far assomigliare<br />
la prima write alla seconda, facendole scrivere lo stesso messaggio. I passi <strong>per</strong><br />
ottenere questo risultato sono <strong>il</strong>lustrati nella Figura 3.4 e descritti di seguito (si<br />
veda ad esempio [1] <strong>per</strong> informazioni sulle convenzioni di chiamata in <strong>Un</strong>ix/Intel):<br />
1. è stato letto l’input da chiedi nome f<strong>il</strong>e(), quindi la read() è stata eseguita.<br />
Lo stack contiene solo <strong>il</strong> frame di main()<br />
2. la f() parte e fa <strong>il</strong> setup del suo frame<br />
3. la strcpy() copia la stringa puntata da path in buf e sovrascrive <strong>il</strong> return<br />
address di f().<br />
conservato intatto<br />
È essenziale che <strong>il</strong> valore del frame pointer sullo stack sia<br />
4. la f() è ritornata, <strong>il</strong> suo frame è stato distrutto e sta <strong>per</strong> essere eseguito <strong>il</strong><br />
codice iniettato nel buffer<br />
5. lo shellcode fa una push del return address <strong>per</strong> g() ed esegue:<br />
push %ebp<br />
mov %esp, %ebp<br />
Il finto stack frame <strong>per</strong> g() è al suo posto<br />
6. lo shellcode fa la push dell’indirizzo di uno qualsiasi dei messaggi considerati<br />
validi <strong>per</strong> put msg() e del return address. Il punto di ritorno deve essere<br />
nell’ep<strong>il</strong>ogo di g() e precisamente a mov %ebp, %esp (o leave). Rimane da
30 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
eseguire:<br />
jmp put msg<br />
A questo punto avviene la write con l’esatto stack che l’IDS si aspetta e dunque<br />
non viene segnalata come non valida. Terminata la put msg() viene distrutto <strong>il</strong> suo<br />
stack frame e viene passato <strong>il</strong> <strong>control</strong>lo al codice dell’ep<strong>il</strong>ogo di g(), che distrugge<br />
<strong>il</strong> relativo frame. Il <strong>control</strong>lo infine torna al main(), dove verrà eseguita la seconda<br />
chiamata di put msg().<br />
Per poter ricostruire lo stack <strong>per</strong>ò è necessario ottenere alcune informazioni, in<br />
particolare i return address delle procedure coinvolte. Nel nostro esempio vogliamo<br />
simulare la prima write , quindi lo stack da ricostruire è:<br />
[main.22|g.10|put_msg.5|write]<br />
La prima informazione che ci serve è <strong>il</strong> punto in cui ritorna g quando è chiamata<br />
dalla riga 22 di main:<br />
(gdb) disas main<br />
Dump of assembler code for function main:<br />
0x00001fc7 : push %ebp<br />
0x00001fc8 : mov %esp,%ebp<br />
0x00001fca : push %ebx<br />
0x00001fcb : sub $0x14,%esp<br />
0x00001fce : call 0x1fd3 <br />
0x00001fd3 : pop %ebx<br />
0x00001fd4 : call 0x1fae <br />
0x00001fd9 : call 0x1f8d <br />
0x00001fde : lea 0x26(%ebx),%eax ;
3.1. Debolezze dei modelli esistenti 31<br />
(gdb) disas g<br />
Dump of assembler code for function g:<br />
0x00001f8d : push %ebp<br />
0x00001f8e : mov %esp,%ebp<br />
0x00001f90 : push %ebx<br />
0x00001f91 : sub $0x14,%esp<br />
0x00001f94 : call 0x1f99 <br />
0x00001f99 : pop %ebx<br />
0x00001f9a : lea 0x59(%ebx),%eax<br />
0x00001fa0 : mov %eax,(%esp)<br />
0x00001fa3 : call 0x1f4e <br />
0x00001fa8 : add $0x14,%esp ;
32 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
1 #include <br />
2 #define CONFIG "/etc/simpleserver.conf"<br />
3 #define BUFSZ 1024<br />
4<br />
5 read_f<strong>il</strong>e(char *name, char *buf) {<br />
6 fd = open(name, O_RDONLY);<br />
7 read(fd, buf);<br />
8 close(fd);<br />
9 }<br />
10<br />
11 write_f<strong>il</strong>e(char *name, char *buf) {<br />
12 fd = open(name, O_WRONLY);<br />
13 write(fd, buf, len);<br />
14 close(fd);<br />
15 }<br />
16<br />
17 server_loop(struct config *cfg)<br />
18 {<br />
19 wh<strong>il</strong>e (1) {<br />
20 fd = accept_client();<br />
21 read_req(fd, req);<br />
22<br />
23 if (req->method == GET) {<br />
24 read_f<strong>il</strong>e(req->f<strong>il</strong>e, tmpbuf);<br />
25 send(fd, tmpbuf);<br />
26 }<br />
27 else if (req->method == PUT) {<br />
28 recv(fd, tmpbuf);<br />
29 write_f<strong>il</strong>e(req->f<strong>il</strong>e, tmpbuf);<br />
30 break;<br />
31 }<br />
32 }<br />
33 }<br />
34<br />
35 main(void)<br />
36 {<br />
37 read_f<strong>il</strong>e(CONFIG, configbuf);<br />
38 cfg = parse_config(configbuf);<br />
39 if(cfg->admin_wants_chroot)<br />
40 chroot(cfg->chroot_ja<strong>il</strong>);<br />
41<br />
42 server_loop(cfg);<br />
43 }<br />
Figura 3.5: Programma <strong>per</strong> <strong>il</strong> secondo esempio.
3.1. Debolezze dei modelli esistenti 33<br />
Il mancato apprendimento di informazioni ut<strong>il</strong>i relative ai parametri della open è<br />
legato alla numerosità dei casi che potrebbero presentarsi: un client infatti potrebbe,<br />
del tutto legittimamente, eseguire la PUT di un f<strong>il</strong>e che ha un nome non incontra-<br />
to durante <strong>il</strong> training e poi farne la GET. Non è ovviamente possib<strong>il</strong>e segnalare<br />
un’intrusione ogni qualvolta si presenti questa situazione.<br />
Quello che l’IDS potrebbe <strong>per</strong>ò apprendere è dove i f<strong>il</strong>e richiesti sono ubicati.<br />
Supponendo che la root directory del server sia /srv, verrà appreso che <strong>il</strong> parametro<br />
di open deve essere un f<strong>il</strong>e nelle directory /srv oppure /etc. Questa informazione<br />
tuttavia non aiuta a capire se è stato tentato <strong>il</strong> download del f<strong>il</strong>e di configurazione.<br />
Se osserviamo <strong>il</strong> sorgente possiamo notare <strong>per</strong>ò che l’unico caso in cui è richiesto<br />
di aprire un f<strong>il</strong>e in /etc è quando <strong>il</strong> server viene lanciato, tramite la chiamata<br />
read f<strong>il</strong>e in main. In altre parole, se lo stack è<br />
[main.37|read_f<strong>il</strong>e.6|open]<br />
allora <strong>il</strong> f<strong>il</strong>e che viene passato alla open deve essere /etc/simpleserver.conf,<br />
mentre se lo stack è<br />
[main.42|server_loop.24|read_f<strong>il</strong>e.6|open]<br />
allora può essere a<strong>per</strong>to qualunque f<strong>il</strong>e stia in /srv. Se si costruisce <strong>il</strong> <strong>modello</strong> usando<br />
anche le informazioni sullo stack è molto semplice r<strong>il</strong>evare che queste condizioni<br />
valgano a runtime.<br />
28<br />
12<br />
read<br />
open<br />
read<br />
21<br />
accept<br />
write<br />
13 14<br />
read<br />
close<br />
open<br />
6 7<br />
20<br />
close<br />
8<br />
accept<br />
end<br />
Figura 3.6: FSA <strong>per</strong> <strong>il</strong> secondo esempio.<br />
read<br />
In questo caso naturalmente <strong>il</strong> comportamento <strong>il</strong>lecito è stato consentito non da<br />
un errore da parte di chi ha creato <strong>il</strong> software e dunque da un bug, ma da parte di
34 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
chi lo ha configurato e, riprendendo la terminologia usata nel precedente capitolo,<br />
si può classificare questo problema come un non-<strong>control</strong> <strong>data</strong> attack. Anche questa<br />
volta si può vedere come i modelli classici siano totalmente ciechi di fronte ad una<br />
sim<strong>il</strong>e eventualità mentre un <strong>modello</strong> <strong>data</strong> <strong>flow</strong> che tiene conto dello stack potrebbe<br />
venire in aiuto. L’automa a stati finiti (Figura 3.6) non può fare nulla in questa<br />
situazione <strong>per</strong>ché <strong>il</strong> <strong>control</strong> <strong>flow</strong> rimane totalmente lecito, stesso discorso vale <strong>per</strong><br />
l’execution graph (Figura 3.7). Se abbinati al <strong>modello</strong> <strong>data</strong> <strong>flow</strong> la situazione non<br />
cambia <strong>per</strong>ché <strong>il</strong> <strong>modello</strong>, come visto, non è sufficientemente potente.<br />
server_loop.29<br />
server_loop.20<br />
accept<br />
main.37<br />
read_f<strong>il</strong>e.6<br />
open<br />
write_f<strong>il</strong>e.12<br />
main.40 main.42<br />
chroot<br />
server_loop.21<br />
server_loop.24<br />
server_loop.25<br />
read_f<strong>il</strong>e.7<br />
write_f<strong>il</strong>e.13<br />
write<br />
read_f<strong>il</strong>e.8<br />
send read<br />
recv<br />
Figura 3.7: Execution graph <strong>per</strong> <strong>il</strong> secondo esempio.<br />
3.1.3 Terzo scenario: debolezza delle relazioni binarie<br />
write_f<strong>il</strong>e.14<br />
server_loop.28<br />
Finora abbiamo considerato unicamente le relazioni unarie e abbiamo visto, a livello<br />
intuitivo, quello che è necessario fare affinché vengano apprese in modo più preciso e<br />
dunque <strong>il</strong> r<strong>il</strong>evamento di comportamenti anomali sia più accurato. L’algoritmo <strong>data</strong><br />
<strong>flow</strong> tuttavia presenta delle debolezze anche <strong>per</strong> ciò che riguarda le relazioni binarie.<br />
Consideriamo l’esempio di Figura 3.8 e in particolare osserviamo le tracce di<br />
tutte le possib<strong>il</strong>i esecuzioni:<br />
Prima esecuzione: [fd@4 = 1 | fd@10 = 1 | fd@11 = 1]<br />
Seconda esecuzione: [fd@6 = 2 | fd@10 = 2 | fd@11 = 2]<br />
Ciò che impara l’algoritmo <strong>data</strong> <strong>flow</strong> da queste tracce è soltanto<br />
• fd@10 = fd@6<br />
• fd@11 = fd@6<br />
• fd@11 = fd@10<br />
close
3.2. Proposta 35<br />
1 int main(void)<br />
2 {<br />
3 if (...)<br />
4 fd = open(FILE_1);<br />
5 else<br />
6 fd = open(FILE_2);<br />
7<br />
8 [...]<br />
9<br />
10 write(fd, buf);<br />
11 close(fd);<br />
12 }<br />
Figura 3.8: Codice d’esempio <strong>per</strong> <strong>il</strong> terzo scenario.<br />
che, a prima vista, sembra addirittura sbagliato. Se <strong>per</strong> esempio, finito <strong>il</strong> training,<br />
si mette <strong>il</strong> programma in produzione e la prima traccia che arriva è<br />
[fd@4 = 1 | fd@10 = 1 | fd@11 = 1]<br />
cosa si deve fare, dato che fd@10 non è uguale a fd@6? Il punto è che la condizione<br />
sta parlando di qualcosa che non esiste, ovvero fd@6, <strong>per</strong>ché non si è mai passati da<br />
riga 6. Ma questo è davvero lecito? Il <strong>modello</strong> così com’è in questo caso è molto<br />
debole e se un attaccante riuscisse a sovrascrivere la variab<strong>il</strong>e fd, l’IDS non se ne<br />
accorgerebbe. Sarebbe ideale imparare:<br />
• fd@10 = fd@11<br />
• (fd@10 = fd@4) OR (fd@10 = fd@6)<br />
L’evoluzione dell’algoritmo proposta in questa tesi va a risolvere anche questo tipo<br />
di problema. L’idea di base è quella di imparare le relazioni in modo differenziato<br />
rispetto al valore degli argomenti. Così facendo, nel caso fd valga 1 viene appre-<br />
so che (fd@10 = fd@4) mentre nel caso valga 2 viene appreso (fd@10 = fd@6).<br />
Combinando queste due informazioni quello che si può dire è esattamente (fd@10 =<br />
fd@4) OR (fd@10 = fd@6).<br />
3.2 Proposta<br />
Nella precedente sezione si sono valutati alcuni dei modelli proposti in letteratura,<br />
sia di tipo <strong>control</strong> <strong>flow</strong> sia di tipo <strong>data</strong> <strong>flow</strong>. Nonostante, soprattutto se combinati,
36 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
la loro efficacia nel proteggere un’applicazione sia elevata, si sono identificati dei casi<br />
in cui degli attacchi passano inosservati. I falsi negativi sono dovuti al fatto che le<br />
informazioni raccolte sono troppo povere e prive di un adeguato contesto. In questa<br />
tesi si propone un <strong>modello</strong> <strong>integrato</strong> <strong>control</strong> <strong>flow</strong>/<strong>data</strong> <strong>flow</strong> in grado di risolvere<br />
questi problemi e di r<strong>il</strong>evare attacchi non visib<strong>il</strong>i dai modelli già esistenti.<br />
Per quanto riguarda la parte <strong>control</strong> <strong>flow</strong> si è adottato <strong>il</strong> <strong>modello</strong> dell’execution<br />
graph. La sua precisione è estremamente elevata e forse anche diffic<strong>il</strong>e da su<strong>per</strong>are.<br />
Per quanto riguarda la parte <strong>data</strong> <strong>flow</strong> invece si sono studiate delle estensioni al<br />
<strong>modello</strong> di [2] relative alle relazioni unarie e alle relazioni binarie, inoltre lo si è<br />
dotato della capacità di apprendere delle alternative.<br />
chiedi_nome_f<strong>il</strong>e<br />
read main.22<br />
main.24 main.25<br />
g.10<br />
put_msg.5<br />
write<br />
stack = [main.22 | g.10 | put_msg.5] implies buf@5 = “Hello,”<br />
stack = [main.25 | put_msg.5] implies buf@5 = “world\n”<br />
Figura 3.9: Nuovo <strong>modello</strong> <strong>per</strong> l’esempio del primo scenario.<br />
Il risultato può essere visto come un execution graph annotato su ogni nodo<br />
foglia con delle informazioni <strong>data</strong> <strong>flow</strong> che <strong>per</strong> ogni esecuzione valida del programma<br />
devono essere verificate. Se non lo sono si è di fronte ad un comportamento anomalo,<br />
verosim<strong>il</strong>mente un attacco.<br />
Durante <strong>il</strong> training ad ogni evento viene aggiunta informazione al <strong>modello</strong>. Ogni<br />
chiamata di sistema viene osservata assieme allo stack user-space e a tutti i suoi<br />
parametri. Lo stack viene ut<strong>il</strong>izzato <strong>per</strong> costruire l’execution graph e <strong>per</strong> contestua-<br />
lizzare le relazioni unarie, mentre i parametri vengono ut<strong>il</strong>izzati nell’apprendimento<br />
sia delle relazioni unarie che delle relazioni binarie.<br />
Nella sua forma più semplice l’idea <strong>per</strong> apprendere le relazioni unarie è quella<br />
di creare una tabella in cui le righe sono indicizzate dagli stack che si osservano<br />
mentre le colonne sono indicizzate dai nomi dei parametri. Ad ogni chiamata di
3.3. Costruzione del <strong>modello</strong> 37<br />
main.4<br />
open<br />
main.6<br />
main.10<br />
write<br />
fd@10=fd@4<br />
OR<br />
fd@10=fd@6<br />
fd@11=fd@10<br />
main.11<br />
close<br />
Figura 3.10: Nuovo <strong>modello</strong> <strong>per</strong> l’esempio del terzo scenario.<br />
sistema si individua la cesella che interessa e vi si aggiunge <strong>il</strong> valore del parametro<br />
osservato. Concluso <strong>il</strong> training si va ad osservare ogni casella: se contiene un valore<br />
soltanto l’informazione è che un dato parametro, in un dato contesto, può assumere<br />
solo quel valore. Se i valori invece sono molteplici c’è tutta una serie di possib<strong>il</strong>ità:<br />
andando a prenderli tutti si può costruire la relazione elementOf, mentre prendendo<br />
ad esempio <strong>il</strong> minimo ed <strong>il</strong> massimo si può costruire inRange.<br />
Riguardo all’apprendimento delle relazioni binarie, come si è detto si vuole poter<br />
apprendere delle alternative. Anche l’idea <strong>per</strong> ottenere questo risultato è molto<br />
semplice e prevede di apprendere relazioni differenziate in base al valore osservato<br />
del parametro. Concluso <strong>il</strong> training si passa ad una seconda fase, in cui si osserva se<br />
le relazioni apprese sono identiche <strong>per</strong> ogni valore del parametro o meno. Nel primo<br />
caso non si è appreso nulla di differente dal <strong>modello</strong> precedente ma nel secondo caso<br />
si sono apprese le alternative cercate.<br />
3.3 Costruzione del <strong>modello</strong><br />
Per costruire <strong>il</strong> <strong>modello</strong> proposto è necessario:<br />
• costruire l’execution graph<br />
• apprendere le relazioni unarie<br />
• apprendere le relazioni binarie<br />
3.3.1 Algoritmo di apprendimento delle relazioni unarie<br />
L’apprendimento delle relazioni unarie è molto semplice, almeno nel caso delle re-<br />
lazioni equals e elementOf. Inoltre non è molto complicato rendere l’algoritmo<br />
“furbo” e far si che impari anche relazioni come inRange o isWithinDir.
38 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
Algoritmo 2: Algoritmo di apprendimento delle relazioni unarie.<br />
learn<strong>Un</strong>ary(EvArg X, V alue V, Stack S)<br />
V als[S][X] = V als[S][X] ∪ {V }.<br />
La funzione learn<strong>Un</strong>ary viene chiamata <strong>per</strong> ogni evento su ogni traccia. Se dopo<br />
aver processato tutte le tracce di training si ha che |V als[S][X]| = 1 l’informazione<br />
appresa è che X, in un determinato stack S, è sempre uguale a V . Se invece<br />
|V als[S][X]| > 1 si è imparato che X, in un determinato stack S, può assumere solo<br />
i valori contenuti nell’insieme V als[S][X]. Nel caso del programma di Figura 3.1 si<br />
avrebbe, denotando con buf@5 <strong>il</strong> parametro della write:<br />
• V als[main.23|g.10|put msg.5][buf@5] = {“Hello, ”}<br />
• V als[main.24|put msg.5][buf@5] = {“world\n”}<br />
Se non si tenesse conto dello stack l’unica cosa imparata sarebbe<br />
• V als[buf@5] = {“Hello, ”, “world\n”}<br />
che è, come già detto, insufficiente <strong>per</strong> r<strong>il</strong>evare l’attacco.<br />
3.3.2 Algoritmo di apprendimento delle relazioni binarie<br />
L’apprendimento delle relazioni binarie avviene in due fasi: la prima non è molto<br />
dissim<strong>il</strong>e da quella originale mentre la seconda, del tutto nuova, è quella che <strong>per</strong>mette<br />
l’apprendimento delle alternative.<br />
Algoritmo 3: Algoritmo di apprendimento delle relazioni binarie, prima parte.<br />
lookup(V, R)<br />
lookup := {e : (V, val(e)) ∈ R}<br />
learnRelations(EvArg X, V alue V )<br />
Y = lookup(V );<br />
CurRels[R][X][V ] = CurRels[R][X][V ] ∩ Y;<br />
Yn = Y ∩ NewArgs(X, V );<br />
CurRels[R][X][V ] = CurRels[R][X][V ] ∪ Yn<br />
update(X, V )<br />
Notiamo che ora la tabella CurRels ha un nuovo indice: <strong>il</strong> valore del parametro<br />
preso in considerazione. Questo ci consente di costruire relazioni differenti <strong>per</strong> dif-<br />
ferenti valori della variab<strong>il</strong>e. Nulla infatti garantisce che le relazioni siano invarianti<br />
rispetto ai valori dei parametri delle chiamate di sistema.
3.3. Costruzione del <strong>modello</strong> 39<br />
<strong>Un</strong> altro cambiamento riguarda la funzione NewArgs(X, V ): essa, in questa<br />
nuova variante, restituisce gli eventi che compaiono <strong>per</strong> la prima volta dopo la pre-<br />
cedente occorrenza di X e tali che, se <strong>il</strong> loro valore era V ′ , vale che (V, V ′ ) ∈ R. Di<br />
seguito vengono mostrati due esempi di esecuzione dell’algoritmo.<br />
NOTA: Per brevità, nel seguito viene omesso [R], dando anche <strong>per</strong> sottinteso che<br />
R è la relazione di uguaglianza. Inoltre CurRels è abbreviato da CR.
40 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
Traccia: (fd@4 = 1), (fd@10 = 1), (fd@11 = 1), (fd@6 = 2), (fd@10 = 2), (fd@11 = 2)<br />
X =fd@4, V = 1<br />
Y = lookup(1) = ∅<br />
CR[fd@4][1] = CR[fd@4][1] ∩ Y = ∅ ∩ ∅ = ∅<br />
Yn = Y ∩ NewArgs(fd@4, 1) = ∅ ∩ ∅ = ∅<br />
CR[fd@4][1] = CR[fd@4][1] ∪ Yn = ∅ ∪ ∅ = ∅<br />
update(fd@4, 1)<br />
X =fd@10, V = 1<br />
Y = lookup(1) = {fd@4}<br />
CR[fd@10][1] = CR[fd@10][1] ∩ Y = ∅ ∩ {fd@4} = ∅<br />
Yn = Y ∩ NewArgs(fd@10, 1) = {fd@4} ∩ {fd@4} = {fd@4}<br />
CR[fd@10][1] = CR[fd@10][1] ∪ Yn = ∅ ∪ {fd@4} = {fd@4}<br />
update(fd@10, 1)<br />
X =fd@11, V = 1<br />
Y = lookup(1) = {fd@10, fd@4}<br />
CR[fd@11][1] = CR[fd@11][1] ∩ Y = ∅ ∩ {fd@10, fd@4} = ∅<br />
Yn = Y ∩ NewArgs(fd@11, 1) = {fd@10, fd@4} ∩ {fd@10, fd@4} = {fd@10, fd@4}<br />
CR[fd@11][1] = CR[fd@11][1] ∪ Yn = ∅ ∪ {fd@10, fd@4} = {fd@10, fd@4}<br />
update(fd@11, 1)<br />
X =fd@6, V = 2<br />
Y = lookup(2) = ∅<br />
CR[fd@6][2] = CR[fd@6][2] ∩ Y = ∅ ∩ ∅ = ∅<br />
Yn = Y ∩ NewArgs(fd@6, 2) = ∅ ∩ {fd@10, fd@11, fd@4} = ∅<br />
CR[fd@6][2] = CR[fd@6][2] ∪ Yn = ∅ ∪ ∅ = ∅<br />
update(fd@6, 2)<br />
X =fd@10, V = 2<br />
Y = lookup(2) = {fd@6}<br />
CR[fd@10][2] = CR[fd@10][2] ∩ Y = ∅ ∩ {fd@6} = ∅<br />
Yn = Y ∩ NewArgs(fd@10, 2) = {fd@6} ∩ {fd@10, fd@11, fd@4, fd@6} = {fd@6}<br />
CR[fd@10][2] = CR[fd@10][2] ∪ Yn = ∅ ∪ {fd@6} = {fd@6}<br />
update(fd@10, 2)<br />
X =fd@11, V = 2<br />
Y = lookup(2) = {fd@10, fd@6}<br />
CR[fd@11][2] = CR[fd@11][2] ∩ Y = ∅ ∩ {fd@10, fd@6} = ∅<br />
Yn = Y ∩ NewArgs(fd@11, 2) = {fd@10, fd@6} ∩ {fd@10, fd@11, fd@4, fd@6} = {fd@10, fd@6}<br />
CR[fd@11][2] = CR[fd@11][2] ∪ Yn = ∅ ∪ {fd@10, fd@6} = {fd@10, fd@6}<br />
update(fd@11, 2)<br />
Figura 3.11: Esecuzione dell’algoritmo su una traccia.
3.3. Costruzione del <strong>modello</strong> 41<br />
Traccia: (fd@4 = 1), (fd@10 = 1), (fd@11 = 1), (fd@6 = 2), (fd@10 = 2), (fd@11 = 1)<br />
X =fd@4, V = 1<br />
Y = lookup(1) = ∅<br />
CR[fd@4][1] = CR[fd@4][1] ∩ Y = ∅ ∩ ∅ = ∅<br />
Yn = Y ∩ NewArgs(fd@4, 1) = ∅ ∩ ∅ = ∅<br />
CR[fd@4][1] = CR[fd@4][1] ∪ Yn = ∅ ∪ ∅ = ∅<br />
update(fd@4, 1)<br />
X =fd@10, V = 1<br />
Y = lookup(1) = {fd@4}<br />
CR[fd@10][1] = CR[fd@10][1] ∩ Y = ∅ ∩ {fd@4} = ∅<br />
Yn = Y ∩ NewArgs(fd@10, 1) = {fd@4} ∩ {fd@4} = {fd@4}<br />
CR[fd@10][1] = CR[fd@10][1] ∪ Yn = ∅ ∪ {fd@4} = {fd@4}<br />
update(fd@10, 1)<br />
X =fd@11, V = 1<br />
Y = lookup(1) = {fd@10, fd@4}<br />
CR[fd@11][1] = CR[fd@11][1] ∩ Y = ∅ ∩ {fd@10, fd@4} = ∅<br />
Yn = Y ∩ NewArgs(fd@11, 1) = {fd@10, fd@4} ∩ {fd@10, fd@4} = {fd@10, fd@4}<br />
CR[fd@11][1] = CR[fd@11][1] ∪ Yn = ∅ ∪ {fd@10, fd@4} = {fd@10, fd@4}<br />
update(fd@11, 1)<br />
X =fd@6, V = 2<br />
Y = lookup(2) = ∅<br />
CR[fd@6][2] = CR[fd@6][2] ∩ Y = ∅ ∩ ∅ = ∅<br />
Yn = Y ∩ NewArgs(fd@6, 2) = ∅ ∩ {fd@10, fd@11, fd@4} = ∅<br />
CR[fd@6][2] = CR[fd@6][2] ∪ Yn = ∅ ∪ ∅ = ∅<br />
update(fd@6, 2)<br />
X =fd@10, V = 2<br />
Y = lookup(2) = {fd@6}<br />
CR[fd@10][2] = CR[fd@10][2] ∩ Y = ∅ ∩ {fd@6} = ∅<br />
Yn = Y ∩ NewArgs(fd@10, 2) = {fd@6} ∩ {fd@10, fd@11, fd@4, fd@6} = {fd@6}<br />
CR[fd@10][2] = CR[fd@10][2] ∪ Yn = ∅ ∪ {fd@6} = {fd@6}<br />
update(fd@10, 2)<br />
X =fd@11, V = 1<br />
Y = lookup(1) = {fd@11, fd@4}<br />
CR[fd@11][1] = CR[fd@11][1] ∩ Y = {fd@10, fd@4} ∩ {fd@11, fd@4} = {fd@4}<br />
Yn = Y ∩ NewArgs(fd@11, 1) = {fd@11, fd@4} ∩ {fd@10, fd@6} = ∅<br />
CR[fd@11][1] = CR[fd@11][1] ∪ Yn = {fd@4} ∪ ∅ = {fd@4}<br />
update(fd@11, 1)<br />
Figura 3.12: Esecuzione dell’algoritmo su una traccia.
42 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
Il risultato dell’algoritmo sulla traccia dell’esempio di Figura 3.11 è <strong>il</strong> seguente:<br />
CR[fd@4][1] = ∅<br />
CR[fd@6][2] = ∅<br />
CR[fd@10][1] = {fd@4} CR[fd@10][2] = {fd@6}<br />
CR[fd@11][1] = {fd@4, fd@10} CR[fd@11][2] = {fd@6, fd@10}<br />
Ora raccogliamo in un unico insieme tutti gli insiemi identificati <strong>per</strong> i vari valori<br />
osservati di una <strong>data</strong> variab<strong>il</strong>e:<br />
CR[fd@4] = {∅}<br />
CR[fd@6] = {∅}<br />
CR[fd@10] = {{fd@4}, {fd@6}}<br />
CR[fd@11] = {{fd@4, fd@10}, {fd@6, fd@10}}<br />
Relativamente a fd@4 e fd@6 <strong>il</strong> risultato è identico all’algoritmo di base. D’altronde<br />
non esistono osservazioni precedenti e dunque non devono essere in relazione con<br />
nessun altro argomento. Potrebbe comunque essere che informazioni relative a fd@4<br />
e fd@6 vengano catturate da relazioni unarie. Quello che cambia è ciò che riguarda<br />
fd@10 e fd@11: gli insiemi a loro associati contengono tutte le possib<strong>il</strong>i relazioni<br />
osservate, dalle quali si può dedurre:<br />
• fd@10 = fd@4 OR fd@10 = fd@6<br />
• fd@11 = fd@10 AND (fd@11 = fd@4 OR fd@11 = fd@6)<br />
Facciamo lo stesso <strong>per</strong> l’esempio di Figura 3.12:<br />
Raccogliamo:<br />
Da cui si ottiene<br />
CR[fd@4][1] = ∅<br />
CR[fd@6][2] = ∅<br />
CR[fd@10][1] = {fd@4} CR[fd@10][2] = {fd@6}<br />
CR[fd@11][1] = {fd@4}<br />
• fd@10 = fd@4 OR fd@10 = fd@6<br />
• fd@11 = fd@4<br />
CR[fd@4] = {∅}<br />
CR[fd@6] = {∅}<br />
CR[fd@10] = {{fd@4}, {fd@6}}<br />
CR[fd@11] = {{fd@4}}
3.3. Costruzione del <strong>modello</strong> 43<br />
Particolare attenzione va posta se uno degli insiemi contiene l’insieme vuoto: esso<br />
non va semplicemente scartato, al contrario esso causa la completa <strong>per</strong>dita di in-<br />
formazione <strong>per</strong> una <strong>data</strong> variab<strong>il</strong>e. Supponiamo si siano osservate le variab<strong>il</strong>i X, X ′<br />
entrambe con valore V e si sia osservata, in un momento differente X con valore<br />
V ′ : varrà su tutta la traccia che se <strong>il</strong> valore è V allora X equals X ′ mentre, se <strong>il</strong><br />
valore è V ′ , non si hanno relazioni con variab<strong>il</strong>i viste in passato. Nel momento in cui<br />
queste due informazioni vengono collassate assieme non si può far altro che dire che<br />
globalmente non esiste nessuna proprietà che valga indipendentemente dal valore.<br />
Questo è molto importante ed è indicativo del fatto che <strong>il</strong> processo di apprendimento<br />
non è monotono: quello che fa l’algoritmo è cercare di costruire una teoria basandosi<br />
su delle osservazioni, nel momento in cui arriva un’osservazione che contraddice la<br />
teoria, quest’ultima decade.<br />
Definizione 7 Siano:<br />
• se, <strong>per</strong> una <strong>data</strong> variab<strong>il</strong>e X si sono osservati i valori v1, . . . , vn si avrà che<br />
CurRels[R][X][v1] = P1, . . . , CurRels[R][X][vn] = Pn saranno definiti. Defi-<br />
niamo CurRels[R][X] = {P1, . . . , Pn}<br />
• V = {v1, . . . , vn} l’insieme di tutti i valori osservati. Definiamo l’insieme<br />
all[R][X] = <br />
v∈V (CurRels[R][X][v])<br />
• or[R][X] = {P \ all[R][X] : P ∈ CurRels[R][X]}<br />
Se all[R][X] = {a1, . . . , ak} allora <strong>per</strong> ogni traccia vale (X R a1) ∧ . . . ∧ (X R ak).<br />
Inoltre vale anche che se or[R][X] = {A1, . . . , Ah} e A1 = ∅, . . . , Ah = ∅ allora<br />
<br />
A∈or[R][X]<br />
<br />
<br />
(X R a)<br />
a∈A<br />
(3.1)<br />
Se ∅ ∈ or[R][X] si è nel caso in cui si sta scartando dell’informazione che non<br />
è universalmente valida. Se anche all[R][X] = ∅, tutte le informazioni raccolte<br />
relativamente ad X erano casi particolari non validi su tutte le tracce osservate.<br />
Quest’ultima definizione è quella che caratterizza la seconda fase dell’algoritmo<br />
di apprendimento, da eseguirsi una volta che sono state processate tutte le tracce.<br />
L’algoritmo completo diventa quindi <strong>il</strong> seguente:
44 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
Algoritmo 4: Algoritmo <strong>data</strong> <strong>flow</strong> completo.<br />
for T ∈ T do<br />
for e ∈ T do<br />
learn<strong>Un</strong>ary(arg(e), value(e), stack(e));<br />
learnBinary(arg(e), value(e));<br />
end<br />
end<br />
for R ∈ R do<br />
for X ∈ X do<br />
apply 3.1 to CurRels[R][X]<br />
end<br />
end<br />
L’algoritmo genera una formula logica <strong>per</strong> ogni argomento X osservato e <strong>per</strong> ogni<br />
relazione R che si vuole apprendere. Tali formule dovranno essere tutte e sempre<br />
verificate a runtime, in caso contrario si ha un’anomalia che potrebbe corrispondere<br />
ad un’intrusione.<br />
3.3.3 Descrizione dell’algoritmo<br />
Siano dati una traccia T , una relazione R e un valore V . Allora l’algoritmo apprende<br />
quali sono le coppie di argomenti {(xi, xj) : xi, xj ∈ T ∧ (xi, xj) ∈ R}<br />
Se la traccia è composta da un singolo evento (x = v), allora banalmente <strong>il</strong><br />
risultato è CurRels[R][x][v] = ∅<br />
Supponiamo ora che la traccia sia composta da più eventi {(x1 = v1), . . . , (xk−1 =<br />
vk−1)}. All’arrivo dell’evento (xk, vk) <strong>il</strong> valore vk viene cercato nella tabella di lookup<br />
e viene restituito l’insieme Y di tutti gli argomenti xi finora visti che hanno valore<br />
in relazione con vk. Allora si possono manifestare differenti casi:<br />
• CurRels[R][X][V ] = ∅ e Y = ∅: In questo caso non è stata osservata nessu-<br />
na relazione <strong>per</strong> l’argomento X quando ha valore V che sia valida su tutta<br />
la traccia finora processata. Inoltre non ci sono visti argomenti che hanno<br />
avuto come ultimo valore <strong>il</strong> valore V . Dunque l’argomento in analisi non ha<br />
nessuna relazione con argomenti passati e in questo caso l’algoritmo apprende<br />
correttamente CurRels[R][X][V ] = ∅<br />
• CurRels[R][X][V ] = ∅ e Y = ∅: Per quanto riguarda la prima parte vale <strong>il</strong><br />
discorso fatto al punto precedente. Per quanto riguarda la seconda parte, <strong>il</strong><br />
fatto che Y sia non vuoto significa che in passato sono stati osservati parametri<br />
<strong>il</strong> cui argomento aveva valore in relazione con <strong>il</strong> valore dell’argomento corrente.
3.3. Costruzione del <strong>modello</strong> 45<br />
Tuttavia questo non è sufficiente a dire che X è in relazione con i parametri<br />
in Y e quindi CurRels[R][X][V ] rimane vuoto. Prima di poter aggiungere<br />
qualcosa a CurRels[R][X][V ] è necessario escludere da Y i parametri che sono<br />
apparsi prima della precedente occorrenza di X con valore in relazione con<br />
V . Supponiamo di non farlo e di avere le occorrenze x1, x2, x3 dello stesso<br />
parametro: in questo caso verrebbero apprese delle relazioni che potrebbero<br />
valere <strong>per</strong> x1 e x3 ma non <strong>per</strong> x2 (vedi esempio). Se invece si prendono solo<br />
i parametri “nuovi”, cioè quelli in Yn si può essere certi che un’eventuale<br />
relazione appresa, almeno fino a quel punto della traccia, è valida. Quindi <strong>il</strong><br />
passo CurRels[R][X][V ] ∪ Yn è lecito<br />
• CurRels[R][X][V ] = ∅ e Y = ∅: Se Y = ∅ significa che <strong>il</strong> parametro che si sta<br />
analizzando non è più in relazione con nessun parametro visto in passato. In<br />
altre parole, i parametri con cui X era in relazione hanno cambiato valore, <strong>per</strong><br />
cui non è più possib<strong>il</strong>e stab<strong>il</strong>ire una relazione che valga globalmente sull’intera<br />
traccia. Quindi, tutte le relazioni finora imparate relativamente ad X vanno<br />
scartate e dunque CurRels[R][X][V ] = ∅<br />
• CurRels[R][X][V ] = ∅ e Y = ∅: Questo caso è semplicemente l’unione dei due<br />
precedenti: vengono conservate soltanto le relazioni che continuano a valere<br />
globalmente sulla traccia vista finora e vengono scartate tutte le altre<br />
I quattro casi appena visti riguardano la prima parte, prendiamo ora in considera-<br />
zione la seconda parte, ovvero quella delineata nella formula 3.1.<br />
Sia dato CurRels[R][X][V ] = {X1, . . . , Xn}. Questo risultato viene generato<br />
dall’algoritmo soltanto se su tutte le tracce osservate valgono X R X1, . . . , X R Xn,<br />
ovvero<br />
<br />
i∈{1,...,n}<br />
X R Xi<br />
(3.2)<br />
che è <strong>il</strong> vincolo a cui deve sottostare X quando ha valore V . Si supponga ora di<br />
aver osservato più valori <strong>per</strong> X, quindi {v1, . . . , vk}. Si avrà che <strong>per</strong> ogni 1 ≤ j ≤ n<br />
esiste un insieme Pj tale che CurRels[R][X][vj] = Pj e una corrispondente formula<br />
Fj del tipo della 3.2. I vari valori osservati sono possib<strong>il</strong>i alternative, quindi varrà<br />
banalmente che<br />
<br />
j∈{1,...,k}<br />
Fj<br />
(3.3)
46 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
Si noti che se uno qualunque dei Pj è un insieme vuoto significa che <strong>per</strong> quel valore<br />
di X non si è osservata alcuna relazione, che equivale a dire che deve sottostare al<br />
vincolo sempre vero ⊤. La 3.3 diventa quindi banalmente sempre vera e quindi non<br />
si può dire niente su X.<br />
3.4 L’algoritmo completo <strong>per</strong> la costruzione del <strong>modello</strong><br />
Algoritmo 5: Algoritmo <strong>per</strong> la costruzione del <strong>modello</strong>.<br />
for T ∈ T do<br />
for e ∈ T do<br />
(Ecall, Ecrs, Ertn) = egBaseCase(stack(e));<br />
learn<strong>Un</strong>ary(arg(e), value(e), stack(e));<br />
learnBinary(arg(e), value(e));<br />
end<br />
end<br />
egInduction(Ecall, Ecrs, Ertn);<br />
for R ∈ R do<br />
for X ∈ X do<br />
apply 3.1 to CurRels[R][X]<br />
end<br />
end<br />
In questa sezione viene dato l’algoritmo completo <strong>per</strong> la costruzione del <strong>modello</strong>.<br />
Esso consiste di due fasi, una “online” e una di post-processing. Durante la fase<br />
“online” le tracce vengono processate evento <strong>per</strong> evento. Ogni evento provoca l’in-<br />
serimento di nuovi archi nell’execution graph o l’apprendimento di nuove relazioni.<br />
La fase successiva di post-processing invece si occupa di lanciare la parte induttiva<br />
della costruzione dell’execution graph (Definizione 2) e di costruire le disgiunzioni<br />
che <strong>il</strong> nuovo algoritmo <strong>data</strong> <strong>flow</strong> è in grado di apprendere.<br />
3.4.1 Relazione con l’algoritmo originale rispetto ai falsi positivi<br />
Come già notato in precedenza la questione dei falsi positivi è molto delicata <strong>per</strong>ché<br />
è legata direttamente alla qualità del training svolto. <strong>Un</strong> training scadente provoca<br />
una elevata quantità di falsi positivi, qualunque sia <strong>il</strong> <strong>modello</strong> ut<strong>il</strong>izzato. Nel <strong>modello</strong><br />
presentato in questa tesi i falsi positivi del <strong>modello</strong> possono essere dovuti alla parte<br />
<strong>control</strong> <strong>flow</strong> e alla parte <strong>data</strong> <strong>flow</strong>. Siccome la parte <strong>control</strong> <strong>flow</strong> del <strong>modello</strong> è<br />
costituita dagli execution graphs senza modifiche, non c’è alcun peggioramento in<br />
falsi positivi da questo punto di vista.
3.5. L’algoritmo <strong>per</strong> la verifica delle tracce rispetto al <strong>modello</strong> 47<br />
Dal lato <strong>data</strong> <strong>flow</strong> succede che i vincoli a cui deve sottostare <strong>il</strong> programma con<br />
<strong>il</strong> nuovo <strong>modello</strong> sono più stretti, dunque sembrerebbe che questo possa portare ad<br />
un incremento dei falsi positivi. All’atto pratico questo <strong>per</strong>ò non succede <strong>per</strong>ché<br />
comunque, anche nel <strong>modello</strong> proposto, vengono apprese tutte e sole le relazioni che<br />
valgono su tutte le tracce osservate durante <strong>il</strong> training.<br />
È chiaro che se vengono<br />
osservate relazioni che durante <strong>il</strong> training non erano state apprese, queste saran-<br />
no segnalate. Purtroppo non c’è stato <strong>il</strong> tempo di condurre una s<strong>per</strong>imentazione<br />
estensiva su software reali relativa a questo aspetto.<br />
3.4.2 <strong>Un</strong>a possib<strong>il</strong>e variante<br />
<strong>Un</strong>a possib<strong>il</strong>e variante dell’algoritmo consiste nel tenere conto dello stack anche nel-<br />
l’apprendimento delle relazioni binarie: CurRels <strong>per</strong> questo avrebbe un ulteriore<br />
indice, da CurRels[R][X][V ] diventerebbe CurRels[R][S][X][V ]. La cosa è equiva-<br />
lente a giustapporre lo stack al nome degli eventi X, quindi differenziandoli rispetto<br />
al <strong>per</strong>corso che è stato seguito <strong>per</strong> giungere ad eseguire quella chiamata di sistema.<br />
Con questa variante diventerebbe possib<strong>il</strong>e capire da dove arrivano i dati che entra-<br />
no nelle procedure del programma, <strong>per</strong>mettendo quindi analisi potenzialmente più<br />
fini. Tuttavia non si è investigata questa possib<strong>il</strong>ità, quindi non è ben chiaro quanto<br />
possa essere ut<strong>il</strong>e e dove possa portare. In particolare è necessario capire a fondo se<br />
i benefici giustificano l’ulteriore dispendio di risorse di calcolo.<br />
3.5 L’algoritmo <strong>per</strong> la verifica delle tracce rispetto al<br />
<strong>modello</strong><br />
L’algoritmo <strong>per</strong> la verifica del <strong>modello</strong> è tutto sommato molto semplice. Esso deve<br />
<strong>control</strong>lare che valgano contemporaneamente le proprietà <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
apprese durante la fase di training. Come già accennato, <strong>il</strong> metodo <strong>per</strong> verificare<br />
le proprietà <strong>control</strong> <strong>flow</strong> deriva direttamente dalla Definizione 5: dati due stack<br />
s = 〈r1, r2, . . . , rn〉 e s ′ = 〈r ′<br />
1<br />
, r′ 2 , . . . , r′<br />
n ′ 〉 consecutivi e un k tale che ri = r ′<br />
i <strong>per</strong><br />
1 ≤ i ≤ k, deve essere sempre possib<strong>il</strong>e confermare che s ′ sia un successore di s, in<br />
caso contrario si deve segnalare la cosa. Affinché s ′ sia successore di s è necessario<br />
verificare che valgano rtn<br />
→ e call<br />
→, che si riduce a <strong>control</strong>lare l’effettiva presenza di<br />
alcuni archi negli insiemi appresi, inoltre è necessario <strong>control</strong>lare anche la presenza<br />
dell’arco (rk, r ′<br />
k ).<br />
Per quanto riguarda la parte <strong>data</strong> <strong>flow</strong> bisogna verificare sia le relazioni unarie<br />
sia quelle binarie. Per le relazioni unarie la questione è molto semplice: ad ogni
48 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong><br />
parametro P durante <strong>il</strong> training può venir associato un insieme di valori ammissib<strong>il</strong>i<br />
o un range. Durante la verifica non si deve far altro che accertarsi della presenza del<br />
valore attuale di P nell’insieme o nel range appresi. Per quanto riguarda le relazioni<br />
binarie, come si è visto l’algoritmo è in grado di apprendere delle formule logiche <strong>per</strong><br />
ognuno dei parametri monitorati. Si tratta quindi di fare <strong>il</strong> lookup di quella formula<br />
e di verificarne la verità. Questo implica la necessità di dover mantenere una tabella<br />
con tutti gli ultimi valori visti <strong>per</strong> i vari parametri.<br />
Algoritmo 6: Algoritmo di verifica.<br />
unaryOk(s, {p1, . . . , pn})<br />
begin<br />
for i ∈ {1, . . . , n} do<br />
if value(pi) ∈ V als[s][pi] then<br />
return false<br />
end<br />
end<br />
return true<br />
end<br />
binaryOk(s, {p1, . . . , pn})<br />
begin<br />
for i ∈ {1, . . . , n} do<br />
if 3.1 does not hold then<br />
return false<br />
end<br />
end<br />
return true<br />
end<br />
verify(Stack s, Stack s ′ , P arams {p ′ 1 , . . . , p′ n ′})<br />
begin<br />
if !successor(s, s ′ ) then<br />
alert(“Control <strong>flow</strong> anomaly”)<br />
end<br />
if !unaryOk(s ′ , {p ′ 1 , . . . , p′ n ′}) then<br />
alert(“Data <strong>flow</strong> anomaly on unary relations”)<br />
end<br />
if !binaryOk(s ′ , {p ′ 1 , . . . , p′ n ′}) then<br />
alert(“Data <strong>flow</strong> anomaly on binary relations”)<br />
end<br />
end
3.5. L’algoritmo <strong>per</strong> la verifica delle tracce rispetto al <strong>modello</strong> 49<br />
3.5.1 Gestione delle anomalie<br />
Il focus di questa tesi è centrato sulla costruzione di un <strong>modello</strong> tramite l’osservazione<br />
di eventi e, successivamente, sull’uso del <strong>modello</strong> appreso <strong>per</strong> monitorare dei processi.<br />
<strong>Un</strong>a volta r<strong>il</strong>evata un’anomalia <strong>per</strong>ò è necessario decidere che azione intraprendere<br />
<strong>per</strong> gestirla, tenendo presente che le varie anomalie che si possono riscontrare hanno<br />
“pesi” differenti, legati sia alla chiamata di sistema stessa [16], sia ai suoi parametri.<br />
Certamente, <strong>per</strong> fare un esempio, una open() di un f<strong>il</strong>e mai visto durante <strong>il</strong> training<br />
è meno sospetta di una exec() che ha come parametro /bin/sh e quindi si potrebbe<br />
decidere che nel primo caso si segnala semplicemente la cosa mentre nel secondo si<br />
uccide <strong>il</strong> processo o si impedisce la system call. Queste <strong>per</strong>ò sono decisioni fortemente<br />
dipendenti dal contesto in cui si colloca <strong>il</strong> sistema di cui si vuole monitorare i processi<br />
<strong>per</strong> cui, in un sistema reale, è verosim<strong>il</strong>e pensare alla presenza di un modulo tramite<br />
<strong>il</strong> quale sia possib<strong>il</strong>e inserire questo tipo di regole.
50 3. <strong>Un</strong> <strong>modello</strong> che integra <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong>
4<br />
L’implementazione<br />
L’implementazione del prototipo dell’IDS basato sul nuovo <strong>modello</strong> proposto è stata<br />
fatta sfruttando DTrace, <strong>il</strong> framework di tracing dinamico di Solaris. Creato inizial-<br />
mente da Sun Microsystems <strong>per</strong> <strong>il</strong> suo sistema o<strong>per</strong>ativo, in seguito è stato portato<br />
su altri sistemi <strong>Un</strong>ix, principalmente FreeBSD e Mac OS X. Sembra essere in corso<br />
un port anche verso Linux anche se su questa piattaforma c’è la concorrenza di un<br />
sistema analogo denominato SystemTap.<br />
DTrace <strong>per</strong>mette <strong>il</strong> monitoring di migliaia di parametri e di o<strong>per</strong>azioni del si-<br />
stema o<strong>per</strong>ativo, comprese le system calls, senza la necessità di scrivere programmi<br />
che girano in kernel space e che potrebbero quindi compromettere la stab<strong>il</strong>ità e la<br />
sicurezza del sistema. L’overhead imposto inoltre è più che accettab<strong>il</strong>e, <strong>il</strong> che disin-<br />
centiva ulteriormente a passare ad un’implementazione in kernel space, almeno <strong>per</strong><br />
quanto riguarda i prototipi. Interfacciandosi low-level a DTrace è possib<strong>il</strong>e ottenere<br />
tutte le informazioni necessarie alla costruzione del <strong>modello</strong>.<br />
I sistemi o<strong>per</strong>ativi che implementano DTrace infatti hanno inserite all’interno<br />
del kernel un gran numero di sonde (probes nel linguaggio di DTrace) e, tramite uno<br />
script apposito, è possib<strong>il</strong>e attivarle e collezionare i loro dati, o nel formato messo<br />
a disposizione dal framework o, intervenendo a basso livello, in formato grezzo.<br />
Le sonde che ci interessano ai fini di questa tesi sono quelle denominate entry e<br />
return messe a disposizione dal provider denominato syscall. Le sonde citate,<br />
come fac<strong>il</strong>mente intuib<strong>il</strong>e, si attivano appena si entra in una chiamata di sistema nel<br />
caso di entry e alla sua uscita (subito prima del ritorno in user space) nel caso di<br />
return. I dati da esse riportati sono cruciali <strong>per</strong> la costruzione del <strong>modello</strong>.<br />
4.1 Struttura generale del sistema<br />
Il sistema è composto da un programma di training e da un programma di anomaly<br />
detection. Il primo è responsab<strong>il</strong>e della costruzione del <strong>modello</strong>, <strong>il</strong> secondo della
52 4. L’implementazione<br />
Learning<br />
libdtrace(3LIB)<br />
DTrace<br />
DTrace entry<br />
probe<br />
int 0x80<br />
Processo in<br />
osservazione<br />
Syscall kernel code<br />
Training offline in<br />
ambiente sicuro<br />
Modello<br />
User space<br />
Kernel space<br />
DTrace return<br />
probe<br />
Anomaly<br />
Detection<br />
Engine<br />
libdtrace(3LIB)<br />
DTrace<br />
DTrace entry<br />
probe<br />
int 0x80<br />
Processo in<br />
osservazione<br />
Syscall kernel code<br />
Figura 4.1: Struttura generale del sistema.<br />
Alert<br />
Monitoring online<br />
User space<br />
Kernel space<br />
DTrace return<br />
probe<br />
sua verifica. Entrambi i programmi si appoggiano a libdtrace(3LIB) <strong>per</strong> ottenere<br />
i dati di cui necessitano. Di seguito verranno descritti alcuni dettagli di DTrace e<br />
successivamente verrà mostrato come sia possib<strong>il</strong>e interfacciarvisi tramite la libreria.<br />
4.2 Introduzione a DTrace<br />
Per monitorare un processo è sufficiente scrivere un programma nel linguaggio messo<br />
a disposizione da DTrace, che consente di specificare quali parametri monitorare e<br />
come.<br />
Volendo ottenere lo user space stack di un processo (ad esempio ls) ad ogni<br />
chiamata di sistema è sufficiente lanciare in esecuzione lo script di Figura 4.4.<br />
Naturalmente tracciare cosa succede durante l’esecuzione di un processo impone<br />
#!/usr/sbin/dtrace -s<br />
syscall:::entry<br />
/execname == "ls"/<br />
{<br />
ustack();<br />
}<br />
Figura 4.2: Script di DTrace <strong>per</strong> monitorare lo stack.
4.2. Introduzione a DTrace 53<br />
un certo overhead. <strong>Un</strong> banalissimo test con ls mostra che, in questo caso, è del<br />
tutto rispettab<strong>il</strong>e (circa <strong>il</strong> 6.8%). L’output di DTrace è estremamente informati-<br />
$ time ls -lR > /dev/null<br />
real 0m2.527s<br />
user 0m0.326s<br />
sys 0m2.174s<br />
$ time ls -lR > /dev/null<br />
real 0m2.680s<br />
user 0m0.329s<br />
sys 0m2.322s<br />
Figura 4.3: Tempi di esecuzione di ls senza e con tracing.<br />
vo ma è poco “machine readable”. Ai fini dell’implementazione dell’IDS questo è<br />
un problema: si vorrebbe evitare di aggiungere inut<strong>il</strong>e complessità <strong>per</strong> riformattare<br />
l’output, inoltre l’uso di tool come sed e awk probab<strong>il</strong>mente sarebbe inaccettab<strong>il</strong>e<br />
dal punto di vista computazionale, soprattutto nella fase di detection online. Per<br />
questo motivo si è scritto un consumer (nella terminologia di DTrace è <strong>il</strong> programma<br />
che si interfaccia low-level alla libreria) ad-hoc <strong>per</strong> prelevare direttamente i dati raw<br />
necessari all’IDS.<br />
4.2.1 Interfacciamento a DTrace tramite libdtrace(3LIB)<br />
<strong>Un</strong>a delle principali difficoltà con DTrace è quella dell’interfacciamento a basso li-<br />
vello. Le API sono private e la documentazione è scarsa, inoltre non sono stab<strong>il</strong>i<br />
e dunque potrebbero cambiare in una release futura del sistema o<strong>per</strong>ativo. Tutte<br />
le informazioni in merito sono state ottenute da corrispondenza con i membri della<br />
ma<strong>il</strong>ing list dtrace-discuss@dtrace.org, in particolare da Chad Mynhier.<br />
Per poter ottenere delle informazioni da DTrace in un custom consumer è neces-<br />
sario di nuovo impiegare uno script, questa volta leggermente differente da quello di<br />
Figura 4.4. Il nuovo script deve inserire i dati di interesse all’interno di un aggregato:<br />
#!/usr/sbin/dtrace -s<br />
syscall:::entry<br />
/execname == "ls"/<br />
{<br />
@[probefunc,ustack()];<br />
}<br />
Figura 4.4: Script di DTrace <strong>per</strong> monitorare lo stack.
54 4. L’implementazione<br />
L’aggregato poi verrà copiato in un buffer in user space, che potrà essere letto ed<br />
elaborato dai consumer. La sintassi <strong>per</strong> specificare un aggregato è @[record1, record2, . . .]<br />
e, nel nostro caso, è stato inserito <strong>il</strong> nome della chiamata di sistema (probefunc)<br />
e lo stack (ustack()). Procedendo in questo modo DTrace, ad ogni ingresso nella<br />
chiamata di sistema, costruisce una struttura dati apposita (Figura 4.5) alla quale si<br />
può accedere da un consumer scritto in C. L’intero aggregato è rappresentato da una<br />
struttura denominata dtrace agg<strong>data</strong> la quale contiene un array (dtrace aggdesc)<br />
che punta ai vari record. Ogni record ha un particolare tipo di dati, nel nostro caso<br />
probefunc è un char * mentre ustack() è un array di uint64 t.<br />
dtrace_agg<strong>data</strong><br />
dtada_desc<br />
dtada_<strong>data</strong><br />
dtada_<strong>per</strong>cpu[0]<br />
dtada_<strong>per</strong>cpu[1]<br />
dtrace_aggdesc<br />
dtagd_rec[1]<br />
dtagd_rec[2]<br />
probename<br />
ustack<br />
...<br />
dtrace_recdesc<br />
dtrd_offset<br />
dtrace_recdesc<br />
dtrd_offset<br />
Figura 4.5: Strutture dati usate da un consumer DTrace.<br />
<strong>Un</strong> consumer ha la funzione di prendere uno script DTrace, comp<strong>il</strong>arlo e passare<br />
l’oggetto risultante al framework DTrace. Questi dettagli sono gestiti con poche<br />
chiamate alla libreria. Fatto questo, si può entrare in un loop e iniziare a consumare<br />
i dati che arrivano dal kernel.<br />
All’interno di un loop infinito si chiama inizialmente la funzione dtrace work(),<br />
la quale si occupa di collezionare tutti i dati necessari. All’uscita da dtrace work()<br />
sono disponib<strong>il</strong>i gli aggregati che sono stati collezionati nell’ultimo ciclo. Questi<br />
vengono processati uno ad uno tramite una funzione walk() definita dall’utente che<br />
lavora sui singoli record. All’interno di walk(), <strong>per</strong> accedere ai valori probefunc e
4.2. Introduzione a DTrace 55<br />
1 static int<br />
2 walk(const dtrace_agg<strong>data</strong>_t *<strong>data</strong>, void *arg)<br />
3 {<br />
4 dtrace_aggdesc_t *aggdesc = <strong>data</strong>->dtada_desc;<br />
5 dtrace_recdesc_t *ustackrec, *<strong>data</strong>rec, *namerec, *argrec;<br />
6 uint64_t *ustack, *pc, timestamp;<br />
7 char *name;<br />
8<br />
9 /* [...] */<br />
10<br />
11 /* Nome system call */<br />
12 namerec = &aggdesc->dtagd_rec[1];<br />
13 name = <strong>data</strong>->dtada_<strong>data</strong> + namerec->dtrd_offset;<br />
14<br />
15 /* Stack */<br />
16 ustackrec = &aggdesc->dtagd_rec[2];<br />
17 if (ustackrec->dtrd_action != DTRACEACT_USTACK)<br />
18 fatal("aggregation key is not a ustack\n");<br />
19 ustack = (uint64_t *)(<strong>data</strong>->dtada_<strong>data</strong> + ustackrec->dtrd_offset);<br />
20<br />
21 /* [...] */<br />
22 }<br />
Figura 4.6: Snippet di walk().<br />
ustack(), si procederà come in Figura 4.6. A questo punto è possib<strong>il</strong>e processarli<br />
in qualsiasi modo si voglia.<br />
4.2.2 Note riguardo a DTrace<br />
I primi es<strong>per</strong>imenti con i consumer custom sono stati fatti in ambiente Mac OS X,<br />
fino a quando non è emerso che, <strong>per</strong> motivi ancora non del tutto chiari, la funzione<br />
ustack() “<strong>per</strong>deva <strong>per</strong> strada” dei frame. Non solo, i frame <strong>per</strong>si non erano gli<br />
stessi su piattaforma Intel e su piattaforma PowerPC. Inizialmente si è sospettato<br />
che questo accadesse a causa di ottimizzazioni introdotte dal comp<strong>il</strong>atore (del tipo<br />
di -fomit-frame-pointer), ma andando a guardare <strong>il</strong> codice macchina generato<br />
si è visto che non era questo <strong>il</strong> caso. Si è dunque spostato lo sv<strong>il</strong>uppo su Solaris<br />
(OpenIndiana 151a), dove nessun stack frame viene <strong>per</strong>so. Questo comportamento<br />
verrà discusso a breve con gli sv<strong>il</strong>uppatori di DTrace.
56 4. L’implementazione<br />
4.3 Implementazione del sistema<br />
I punti chiave dell’implementazione non sono molti e di seguito verranno descritti.<br />
<strong>Un</strong>o di essi è lo script <strong>per</strong> la collezione dei dati, l’altro è l’implementazione della<br />
funzione NewArgs().<br />
4.3.1 Lo script di <strong>data</strong> collection<br />
In base a quanto specificato nello script verranno raccolti più o meno dati che ver-<br />
ranno processati dal <strong>modello</strong>. I dati raccolti sono in ogni caso lo stack del processo<br />
al momento della system call e <strong>il</strong> suo nome. Eventualmente vengono raccolti anche<br />
uno o più parametri e <strong>il</strong> valore di ritorno. Lo script inizia con:<br />
string pname;<br />
BEGIN<br />
{<br />
pname = "gpserver";<br />
}<br />
In questa parte viene inizializzata la variab<strong>il</strong>e pname che consente di specificare<br />
<strong>il</strong> nome del processo da monitorare. Successivamente vengono inizializzate alcune<br />
variab<strong>il</strong>i che conterranno i valori collezionati.<br />
syscall:::entry<br />
/execname == pname/<br />
{<br />
self->arg0 = 0;<br />
self->arg1 = 0;<br />
self->arg2 = 0;<br />
self->arg3 = 0;<br />
self->arg4 = 0;<br />
self->arg5 = 0;<br />
}<br />
Si noti l’uso di self, necessario a memorizzare <strong>il</strong> valore localmente rispetto al thread.<br />
Le variab<strong>il</strong>i inizializzate conterranno fino a 5 parametri delle system call. In questa<br />
definizione si può notare anche l’espressione /execname == pname/, che fa attivare<br />
la sonda solo se <strong>il</strong> nome del processo è quello specificato. Infine prendiamo in esame<br />
la parte che effettivamente raccoglie i dati.
4.3. Implementazione del sistema 57<br />
syscall::accept:entry<br />
/execname == pname/<br />
{<br />
self->arg0 = arg0;<br />
}<br />
syscall::accept:return<br />
/execname == pname/<br />
{<br />
@accept[probefunc, arg0, self->arg0, ustack()] = max(timestamp);<br />
}<br />
L’esempio è relativo alla chiamata di sistema accept(). Nella sonda entry i valori<br />
dei parametri sono disponib<strong>il</strong>i nelle variab<strong>il</strong>i arg0, ..., argN. Nella sonda return<br />
invece in arg0 e in arg1 è disponib<strong>il</strong>e <strong>il</strong> valore di ritorno. Quando la sonda entry<br />
viene attivata <strong>il</strong> primo parametro di accept() (un f<strong>il</strong>e descriptor restituito prece-<br />
dentemente molto probab<strong>il</strong>mente da socket()) viene semplicemente salvato nella<br />
variab<strong>il</strong>e self->arg0. Quando viene attivata la sonda return invece viene costruito<br />
l’aggregato con:<br />
• Nome della system call<br />
• Valore di ritorno<br />
• Primo parametro, salvato dalla entry<br />
• Stack corrente del processo<br />
L’aggregato così composto viene messo in un buffer in user space a disposizione del<br />
consumer che, nel nostro caso, è l’implementazione del <strong>modello</strong> proposto.<br />
4.3.2 Implementazione di NewArgs()<br />
Nell’implementazione della fase di training del sistema è particolarmente importante<br />
avere una struttura dati che <strong>per</strong>metta a NewArgs() di restituire velocemente <strong>il</strong> suo<br />
risultato. Ricordiamo che <strong>data</strong> una traccia T formata dagli eventi e1, . . . , en, possi-<br />
b<strong>il</strong>mente ripetuti, e dato un evento ek la funzione NewArgs() deve restituire tutti gli<br />
eventi apparsi tra ek e la sua occorrenza immediatamente precedente, esclusi quelli<br />
che appaiono anche prima di essa. Nell’esempio di Figura 4.7 gli eventi registrati<br />
sono:<br />
(A = 1); (B = 1); (C = 1); (B = 2); (C = 2); (A = 3); (D = 1)
58 4. L’implementazione<br />
ed è appena arrivato l’evento B = 2. NewArgs() deve restituire {D}.<br />
A=1 B=1 C=1 B=2 C=2 A=3<br />
Figura 4.7: Struttura dati di supporto all’implementazione di NewArgs().<br />
Non è pensab<strong>il</strong>e scorrere la lista all’indietro fino alla precedente occorrenza di B<br />
e nel frattempo verificare, elemento <strong>per</strong> elemento, che esso non compaia prima. Nel<br />
peggiore dei casi se le due occorrenze di B sono distanti k e gli eventi processati sono<br />
stati finora n sarà necessario cercare k volte tra n elementi, <strong>per</strong> un costo di O(nk).<br />
La soluzione adottata è quindi stata quella di tenere traccia dell’ultima occorrenza di<br />
ogni evento. Inoltre ogni evento è marcato con un numero progressivo (non mostrato<br />
in figura) e punta al precedente. NewArgs(), sull’esempio di Figura 4.7, funziona nel<br />
seguente modo:<br />
• L’ultimo elemento della lista non è la precedente occorrenza di B, inoltre non<br />
ha predecessori, quindi necessariamente appare dopo la precedente occorrenza<br />
di B, indipendentemente dal fatto che tale occorrenza esista o meno. Viene<br />
aggiunto agli elementi che NewArgs() deve restituire.<br />
• A ha un predecessore, <strong>il</strong> cui numero progressivo è inferiore a quello dell’istanza<br />
precedente di B, quindi viene scartato.<br />
• La stessa cosa succede <strong>per</strong> C, che viene scartato.<br />
• Si è giunti alla precedenza occorrenza di B, NewArgs() termina restituendo<br />
{D}.<br />
Va notato che avrebbero potuto esserci più occorrenze di A o di C prima di B:<br />
<strong>il</strong> numero progressivo associato ad ogni evento <strong>per</strong>mette di sa<strong>per</strong>e immediatamen-<br />
te se occorre continuare a risalire all’indietro la catena dei precedenti o meno. La<br />
NewArgs() qui presentata è, <strong>per</strong> semplicità descrittiva, una possib<strong>il</strong>e implementa-<br />
zione della NewArgs() “originale” (in [2] non viene <strong>data</strong> alcuna implementazione),<br />
A<br />
B<br />
C<br />
D<br />
D=1<br />
B=2
4.4. Costo computazionale del <strong>modello</strong> 59<br />
ma la modifica <strong>per</strong> adattarla agli scopi di questa tesi è immediata. Assumendo di<br />
implementare la tabella delle ultime occorrenze con una hash table e assumendo<br />
(in modo sicuramente azzardato) anche di fermarsi dopo <strong>il</strong> primo passo nel risalire<br />
all’indietro le catene, si ha una complessità di O(k). Tale complessità, dato che <strong>il</strong><br />
training avviene offline, è un compromesso del tutto ragionevole tra semplicità e<br />
velocità.<br />
4.4 Costo computazionale del <strong>modello</strong><br />
Il <strong>modello</strong> presenta due tipologie di costi computazionali: <strong>il</strong> costo relativo alla co-<br />
struzione e <strong>il</strong> costo relativo alla verifica. In entrambi i casi i costi sono dovuti<br />
principalmente alle strutture dati ut<strong>il</strong>izzate. Diverse implementazioni potrebbero<br />
usare strutture dati differenti, che ottimizzano certe o<strong>per</strong>azioni a scapito di altre. Ci<br />
si limiterà quindi soltanto ad un’analisi sommaria della questione della complessità.<br />
4.4.1 Costo del learning<br />
Per quanto riguarda gli execution graphs e quindi la parte <strong>control</strong> <strong>flow</strong> del <strong>modello</strong>,<br />
durante <strong>il</strong> training devono essere inseriti degli archi all’interno di insiemi (Defini-<br />
zione 2, caso base). Gli insiemi usati sono quelli della libreria standard del C++ e<br />
l’o<strong>per</strong>azione di insert() ha complessità logaritmica nella dimensione dell’insieme.<br />
Alternativamente è possib<strong>il</strong>e ut<strong>il</strong>izzare una hash table che consente o<strong>per</strong>azioni molto<br />
più efficienti.<br />
Per quanto riguarda la parte <strong>data</strong> <strong>flow</strong> del <strong>modello</strong>, riguardo a learn<strong>Un</strong>ary si<br />
può dire che essa gira in tempo logaritmico in quanto:<br />
• si deve fare <strong>il</strong> lookup di V als[S], che costa O(log(|V als|))<br />
• si deve fare <strong>il</strong> lookup di V als[S][X], che costa O(log(|V als[S]|))<br />
• si devono fare l’unione e l’assegnamento, che collassano nell’o<strong>per</strong>azione insert()<br />
di complessità O(log(|V als[S][X]|))<br />
Complessivamente quindi <strong>il</strong> costo è logaritmico e la cardinalità maggiore è quel-<br />
la di V als, che contiene tutti gli stack visti. Questo vale nel caso del prototipo,<br />
ovviamente ut<strong>il</strong>izzando una hash table è possib<strong>il</strong>e fare molto meglio.<br />
Per quanto riguarda learnBinary si possono fare considerazioni sim<strong>il</strong>i, tenendo<br />
<strong>per</strong>ò presente <strong>il</strong> costo della chiamata a NewArgs. La complessità della parte di
60 4. L’implementazione<br />
apprendimento delle alternative infine è direttamente legata alla cardinalità degli<br />
insiemi su cui o<strong>per</strong>a.<br />
4.4.2 Costo della verifica<br />
Negli stessi insiemi creati con <strong>il</strong> training, durante la fase di verifica del <strong>modello</strong><br />
(e quindi online) è necessario cercare gli archi relativi agli stack che si osservano<br />
e anche l’o<strong>per</strong>azione find() ha complessità logaritmica. In un prototipo questa<br />
complessità è accettab<strong>il</strong>e ma in un sistema vero è più opportuno usare una hash<br />
table. Supponiamo, in riferimento alla Definizione 5, di dover verificare due stack di<br />
altezza n = n ′<br />
e con i primi k elementi a partire dal basso uguali: <strong>per</strong> verificare se vale<br />
rtn<br />
call<br />
→ serve O(n − k), come pure <strong>per</strong> verificare se vale →. Verificare se (rk, r ′<br />
k ) ∈ Ecrs<br />
ha complessità O(1) e verificare se ri = r ′<br />
i <strong>per</strong> 1 ≤ i < k richiede O(k). Per cui<br />
la verifica costa all’incirca O(n) <strong>per</strong> chiamata di sistema, costo valido se si usa una<br />
hash table.<br />
Per quanto riguarda la parte unaria del <strong>data</strong> <strong>flow</strong> del <strong>modello</strong> è necessario cer-<br />
care se un valore è presente in un insieme o se è compreso in un certo range. Nel<br />
primo caso l’o<strong>per</strong>azione ha costo logaritmico nella dimensione dell’insieme, mentre<br />
nel secondo caso ha costo costante. La parte binaria invece presenta <strong>il</strong> problema<br />
di dover cercare le formule apprese durante <strong>il</strong> training e verificare che valgano. In<br />
base a quanto ampie sono le relazioni di un dato parametro le formule avranno<br />
dimensioni più o meno grandi, quindi <strong>il</strong> costo della valutazione è legato alla loro<br />
dimensione. Naturalmente è possib<strong>il</strong>e valutarle con strategie furbe (short circuit) e<br />
questo migliora i tempi di esecuzione.<br />
4.4.3 Alcuni dati s<strong>per</strong>imentali<br />
Di seguito vengono riportate alcune misure effettuate su software reali relative alle<br />
dimensioni medie degli stack. Queste misure <strong>per</strong>mettono di avere un’idea dei dati<br />
che devono essere raccolti ad ogni chiamata di sistema. Supponendo di lavorare<br />
su una macchina a 64 bit e di avere un’altezza media di n, ad ogni system call si<br />
raccoglieranno 8n byte. Questo valore non considera i dati necessari alla parte <strong>data</strong><br />
<strong>flow</strong> del <strong>modello</strong>. Per costruire la parte <strong>control</strong> <strong>flow</strong> infatti è necessario registrare<br />
ogni chiamata di sistema, mentre <strong>per</strong> la costruzione del <strong>modello</strong> <strong>data</strong><strong>flow</strong> questo<br />
non è necessario, anzi, potrebbe essere controproducente: in base al programma da<br />
monitorare infatti certe system call hanno poco a che fare con la sua sicurezza e<br />
dunque è su<strong>per</strong>fluo monitorarle [16].
4.4. Costo computazionale del <strong>modello</strong> 61<br />
Server web Apache (httpd)<br />
Altezza stack Occorrenze Altezza stack Occorrenze<br />
7 2 (0.12%) 20 18 (1.1%)<br />
10 10 (0.6%) 22 1348 (81%)<br />
11 8 (0.48%) 23 132 (8%)<br />
14 4 (0.24%) 24 1 (0.06%)<br />
15 1 (0.06%) 25 5 (0.3%)<br />
16 3 (0.18%) 26 2 (0.12%)<br />
17 16 (0.96%) 28 18 (1.1%)<br />
18 8 (0.48%) 30 1 (0.06%)<br />
19 10 (0.6%) 31 73 (4.4%)<br />
I dati sono stati ottenuti monitorando <strong>per</strong> una decina di minuti un server web<br />
Apache con lieve carico. Sono stati osservati 1660 differenti stack, con altezza media<br />
di 22 elementi.<br />
MySQL (mysqld)<br />
Altezza stack Occorrenze Altezza stack Occorrenze<br />
3 1 (0.32%) 14 6 (1.9%)<br />
5 10 (3.2%) 15 6 (1.9%)<br />
6 9 (2.9%) 16 8 (2.6%)<br />
7 10 (3.2%) 17 27 (8.7%)<br />
8 14 (4.5%) 18 24 (7.7%)<br />
9 6 (1.9%) 19 21 (6.8%)<br />
10 4 (1.3%) 20 25 (8%)<br />
11 6 (1.9%) 22 120 (39%)<br />
12 3 (0.96%) 31 8 (2.6%)<br />
13 3 (0.96%)<br />
In questo caso i dati si riferiscono al server MySQL a cui si appoggia <strong>il</strong> web server<br />
dei dati precedenti. In questo caso <strong>il</strong> processo è stato monitorato <strong>per</strong> 10 minuti, nei<br />
quali si sono osservati 311 differenti stack con altezza media di 18 elementi.
62 4. L’implementazione<br />
Syslog server (syslogd)<br />
Altezza stack Occorrenze Altezza stack Occorrenze<br />
4 1 (1.5%) 11 8 (12%)<br />
5 1 (1.5%) 12 14 (21%)<br />
6 3 (4.5%) 13 9 (13%)<br />
7 3 (4.5%) 14 11 (16%)<br />
8 6 (9%) 15 3 (4.5%)<br />
9 3 (4.5%) 16 1 (1.5%)<br />
10 4 (6%)<br />
Il processo syslogd, <strong>per</strong> via della sua “tranqu<strong>il</strong>lità” legata alla bassa attività del<br />
sistema di test è stato monitorato <strong>per</strong> circa un’ora. Si sono osservati 67 differenti<br />
stack, con un’altezza media di 11 elementi.
5<br />
Conclusioni e sv<strong>il</strong>uppi futuri<br />
La sempre maggiore <strong>per</strong>vasività dei sistemi di calcolo, siano essi computer veri e<br />
propri o dispositivi embedded, impone una sempre maggiore necessità di garantire<br />
la loro sicurezza. Esistono diversi meccanismi e diverse tecniche <strong>per</strong> raggiungere<br />
questo scopo anche se in ogni caso dare un livello di sicurezza totale non è possib<strong>il</strong>e.<br />
Quello che <strong>per</strong>ò è possib<strong>il</strong>e è cercare di creare sistemi di difesa sempre più efficaci e<br />
ad ampio spettro.<br />
I sistemi che implementano le difese si possono classificare in due categorie, quelli<br />
che si occupano di misuse detection, ovvero tramite pattern matching verificano la<br />
presenza di signature di un attacco e quelli che fanno anomaly detection, ovvero che<br />
cercano di capire se sono in atto comportamenti che si discostano dalla norma.<br />
Il monitoraggio delle chiamate di sistema va sotto la classe dell’anomaly detec-<br />
tion: se un processo deve aprire un f<strong>il</strong>e e spedirlo via rete, nel momento in cui questo<br />
processo fa una chiamata ad execve() <strong>il</strong> comportamento esce dalla norma ed è so-<br />
spetto <strong>per</strong>ché ha poco senso che <strong>per</strong> svolgere suo <strong>il</strong> compito sia necessario lanciare<br />
un altro processo. L’osservazione delle system call come tecnica di r<strong>il</strong>evamento delle<br />
intrusioni è nota fin dagli anni ’90 e i modelli che sono stati costruiti sono i più<br />
svariati. In questa tesi sono stati considerati vari modelli esistenti in letteratura che<br />
solo osservando le system call e i loro parametri consentono di apprendere informa-<br />
zioni relative al <strong>control</strong> <strong>flow</strong> e al <strong>data</strong> <strong>flow</strong> del programma. Dopo aver discusso le<br />
loro caratteristiche sono state messe in luce alcune debolezze, che hanno portato allo<br />
sv<strong>il</strong>uppo di un nuovo <strong>modello</strong> che tratta in modo <strong>integrato</strong> <strong>control</strong> <strong>flow</strong> e <strong>data</strong> <strong>flow</strong>.<br />
Il nuovo <strong>modello</strong> risolve le debolezze dei precedenti apprendendo le informazioni in<br />
modo più fine.
64 5. Conclusioni e sv<strong>il</strong>uppi futuri<br />
5.1 Riep<strong>il</strong>ogo del lavoro svolto<br />
5.1.1 Il <strong>modello</strong><br />
Lo studio dei modelli esistenti ha messo in luce vari casi in cui essi sono ciechi<br />
a certi tipi di attacchi e partendo da tre scenari e da una semplice osservazione<br />
sono venute alla luce le possib<strong>il</strong>i migliorie. L’osservazione è che l’apprendimento del<br />
<strong>control</strong> <strong>flow</strong> richiede l’acquisizione di informazioni relative allo stack al momento<br />
della chiamata di sistema, informazioni di cui <strong>per</strong>ò può beneficiare anche <strong>il</strong> <strong>modello</strong><br />
<strong>data</strong> <strong>flow</strong>. Il modo in cui questo può beneficiarne appare dai primi due scenari<br />
presentati, che riguardano le relazioni unarie: si è visto che l’uso dello stack <strong>per</strong>mette<br />
di classificare i parametri delle system call in base al <strong>per</strong>corso che <strong>il</strong> <strong>control</strong> <strong>flow</strong> ha<br />
seguito <strong>per</strong> arrivare a fare una certa chiamata. Nel caso delle relazioni binarie invece<br />
l’ut<strong>il</strong>ità dell’uso dello stack rimane poco chiara e deve essere ancora investigata ma<br />
si è comunque proposta una miglioria, la cui motivazione appare nel terzo scenario<br />
analizzato. L’osservazione in questo caso è stata che non è assolutamente garantito<br />
che le relazioni che coinvolgono i parametri delle chiamate di sistema siano invarianti<br />
rispetto al loro valore e dunque si è proposto un nuovo schema di apprendimento<br />
che tiene conto di questo fatto.<br />
5.1.2 L’implementazione<br />
Nel quarto capitolo sono stati presentati alcuni dei dettagli dell’implementazione,<br />
in particolare è stato mostrato come DTrace abbia <strong>per</strong>messo di non scrivere nem-<br />
meno una riga di codice in kernel space. Certamente in un’implementazione reale<br />
questo è inevitab<strong>il</strong>e ma <strong>per</strong> un proof-of-concept l’overhead imposto da DTrace è di<br />
tutto rispetto. Inizialmente si è quindi vista la struttura di DTrace <strong>per</strong> poi passare<br />
all’interfacciamento al framework tramite l’apposita libreria. Successivamente si è<br />
visto come la <strong>data</strong> collection ruoti interamente attorno ad uno script nel linguaggio<br />
di DTrace. Infine, dopo aver presentato una possib<strong>il</strong>e struttura dati <strong>per</strong> l’esecuzione<br />
efficiente di un’o<strong>per</strong>azione necessaria al <strong>modello</strong>, è stata discussa la complessità in<br />
modo del tutto informale, essendo questa dettata prevalentemente dalle strutture<br />
dati ut<strong>il</strong>izzate.<br />
5.2 Sv<strong>il</strong>uppi futuri<br />
In questa sezione verranno presentate alcune idee che si vorrebbe investigare ed<br />
eventualmente integrare nel <strong>modello</strong>.
5.2. Sv<strong>il</strong>uppi futuri 65<br />
5.2.1 L’uso dello stack nell’apprendimento delle relazioni binarie<br />
Nel capitolo 3 è già stata accennata la possib<strong>il</strong>ità di considerare lo stack anche<br />
nell’apprendimento di relazioni binarie. Questo è equivalente a giustapporre lo stack<br />
al nome del parametro preso in considerazione, differenziandolo quindi rispetto al<br />
<strong>per</strong>corso che è stato seguito <strong>per</strong> eseguire una <strong>data</strong> chiamata di sistema. L’idea di<br />
usare lo stack in questo modo potrebbe <strong>per</strong>mettere di apprendere una sorta di “<strong>data</strong><br />
<strong>flow</strong> procedurale”: invece di guardare finemente <strong>il</strong> <strong>data</strong> <strong>flow</strong> tra una chiamata e<br />
l’altra in questo modo potrebbe diventare fattib<strong>il</strong>e di apprendere delle informazioni<br />
più high-level rispetto al flusso che seguono i dati. Tuttavia non è ancora ben<br />
chiaro quanto ut<strong>il</strong>e possa essere l’aggiunta di questo tipo di informazioni e la prima<br />
evoluzione che verrà investigata sarà proprio questa.<br />
5.2.2 <strong>Un</strong>a dimensione statistica <strong>per</strong> <strong>il</strong> <strong>modello</strong><br />
Control <strong>flow</strong> e <strong>data</strong> <strong>flow</strong> sono due dimensioni in qualche modo ortogonali e la loro<br />
osservazione combinata consente di raggiungere un buon livello di dettaglio nella<br />
comprensione del comportamento di un programma, che si traduce nella capacità di<br />
impedire un gran numero di attacchi. Tuttavia sembra <strong>per</strong>fettamente possib<strong>il</strong>e im-<br />
maginare dei casi in cui pur rimanendo esattamente all’interno del comportamento<br />
prescritto dal <strong>modello</strong> diventa possib<strong>il</strong>e un attacco denial of service. Immaginiamo<br />
un ciclo al cui interno viene eseguita una malloc() (ad esempio si sta allocando<br />
memoria <strong>per</strong> una matrice): ad ogni chiamata si osserverà lo stesso stack, quindi lo<br />
stack s sarà successore di se stesso. Se in qualche modo si riesce a modificare l’ese-<br />
cuzione del ciclo è possib<strong>il</strong>e eseguire un numero indefinito di malloc(), saturando<br />
la memoria. In questo caso si potrebbe pensare di far entrare in gioco un’ulteriore<br />
dimensione ortogonale alle precedenti due, cioè quella statistica. L’idea, che sarà<br />
mostrata su un FSA, è la seguente: durante <strong>il</strong> training si osserva le frequenze delle<br />
transizioni da uno stato all’altro, dando un peso agli archi. Finito <strong>il</strong> training e passa-<br />
ti alla verifica online si <strong>control</strong>la che <strong>il</strong> processo monitorato rispetti le frequenze delle<br />
transizioni. Nel caso non le rispetti si è di fronte ad un comportamento anomalo che<br />
va segnalato.<br />
In Figura 5.1 si può vedere un esempio estremamente semplificato dell’idea. Sup-<br />
poniamo che in un ciclo una venga eseguita una malloc() 10 volte e infine venga<br />
eseguita una close(): questo nei run di training verrà osservato e si otterrà che le<br />
due transizioni etichettate malloc e close avranno un peso rispettivamente di circa
66 5. Conclusioni e sv<strong>il</strong>uppi futuri<br />
open<br />
p1 p2<br />
open,w1<br />
p1 p2<br />
read<br />
(a)<br />
read,w2<br />
(b)<br />
malloc<br />
p3<br />
malloc,w3<br />
p3<br />
close<br />
close,w4<br />
Figura 5.1: Automa senza e con peso sugli archi.<br />
w3 = 91% e w4 = 9%. A runtime poi, nel momento in cui si arriva in p4, si deve<br />
verificare che questo valga.<br />
Quella presentata è soltanto un’idea senza la pretesa di essere corretta, ma sulla<br />
quale si vuole ragionare.<br />
sia affidab<strong>il</strong>e e che statisticamente sia valido.<br />
5.2.3 Costruzione statica del <strong>modello</strong><br />
È necessario infatti capire come costruire un <strong>modello</strong> che<br />
L’execution graph, come già visto, viene costruito dinamicamente osservando le<br />
esecuzioni di un programma. Si vorrebbe provare a far si che sia <strong>il</strong> comp<strong>il</strong>atore<br />
a costruire l’execution graph, magari sfruttando la notevole modularità e semplicità<br />
di interfacciamento di LLVM. L’idea è quella di far in modo che <strong>il</strong> comp<strong>il</strong>atore,<br />
oltre a produrre l’eseguib<strong>il</strong>e, produca anche un f<strong>il</strong>e contenente <strong>il</strong> <strong>modello</strong> <strong>control</strong><br />
<strong>flow</strong>. Al momento dell’esecuzione verrebbero caricati entrambi i f<strong>il</strong>e in modo che<br />
contestualmente al lancio del programma venga lanciata anche la verifica.<br />
5.2.4 Applicazione del <strong>modello</strong> a sistemi virtualizzati<br />
Analogamente a quanto fatto in [15] è possib<strong>il</strong>e applicare <strong>il</strong> <strong>modello</strong> presentato in<br />
questa tesi a sistemi virtualizzati, <strong>per</strong>mettendone <strong>il</strong> monitoraggio invisib<strong>il</strong>e. Il pro-<br />
blema principale da risolvere in questo caso è quello di riuscire, dal gestore di mac-<br />
chine virtuali, a risalire ai processi che stanno girando nelle istanze virtualizzate dei<br />
p4<br />
p4
5.2. Sv<strong>il</strong>uppi futuri 67<br />
sistemi. In altre parole è necessario, <strong>data</strong> una macchina virtuale, poter separare<br />
<strong>per</strong> PID le varie system call che si osservano. Questa tuttavia non dovrebbe essere<br />
un’o<strong>per</strong>azione che presenta particolari difficoltà.
68 5. Conclusioni e sv<strong>il</strong>uppi futuri
Bibliografia<br />
[1] System V Application Binary Interface - Intel386 Architechture Processor<br />
Supplement. 4th edition, 1997.<br />
[2] Sandeep Bhatak, Abhishek Chaturvedi, and R. Sekar. Data<strong>flow</strong> anomaly<br />
detection. 2006.<br />
[3] Dan Boneh, Richard A. DeM<strong>il</strong>lo, and Richard J. Lipton. On the importance of<br />
eliminating errors in cryptographic computations. 1997.<br />
[4] Shuo Chen, Jun Xu, Emre C. Sezer, Prachi Gauriar, and Ravishankar K. Iyer.<br />
Non <strong>control</strong>-<strong>data</strong> attacks are realistic threats.<br />
[5] Mihai Christodorescu, Somesh Jha, Sanjit A. Seshia, Dawn Song, and Randal E.<br />
Bryant. Semantics-aware malware detection.<br />
[6] Henry H. Feng, Oleg M. Kolesnikov, Prahlad Fogla, Wenke Lee, and Weibo<br />
Gong. Anomaly detection using call stack information. 2003.<br />
[7] Debin Gao, Michael K. Reiter, and Dawn Song. Gray-box extraction of<br />
execution graphs for anomaly detection. 2004.<br />
[8] Jin Han, Qiang Yan, Robert H. Deng, and Debin Gao. On detection of erratic<br />
arguments.<br />
[9] Steven A. Hofmeyr, Stephanie Forrest, and An<strong>il</strong> Somayaji. Intrusion detection<br />
using sequences of system calls. 1998.<br />
[10] M. Kearns and L. Valiant. Cryptographic limitations on learning boolean<br />
formulae and finite automata. ACM STOC, 1989.<br />
[11] Johannes Kinder, Florian Zuleger, and Helmut Veith. An abstract<br />
interpretation-based framework for <strong>control</strong> <strong>flow</strong> reconstruction from binaries.<br />
2009.<br />
[12] A. Kosoresow and S. Hofmeyr. Intrusion detection via system call traces. IEEE<br />
Software, 1997.
70 5. Bibliografia<br />
[13] Christopher Kruegel, Darren H. Mutz, Fredrik Valeur, and Giovanni Vigna. On<br />
the detection of anomalous system call arguments.<br />
[14] Cullen Linn and Saumya Debray. Obfuscation of executable code to improve<br />
resistance to static disassembly. 2003.<br />
[15] Carlo Maiero and Marino Miculan. <strong>Un</strong>observable intrusion detection based on<br />
call traces in paravirtualized systems.<br />
[16] Xu Ming, Chen Chun, and Ying Jing. Anomaly detection based on system call<br />
classification. 2003.<br />
[17] Aleph One. Smashing the stack for fun and profit. Phrack #49, 1996.<br />
[18] L. Pitt and M. Warmuth. The minimum consistency dfa problem cannot be<br />
approximated within any polynomial. ACM STOC, 1989.<br />
[19] M<strong>il</strong>a Dalla Preda, Mihai Christodorescu, Somesh Jha, and Saumya Debray. A<br />
semantics-based approach to malware detection. 2007.<br />
[20] M<strong>il</strong>a Dalla Preda, Matias Madou, Koen De Bosschere, and Roberto Giacobazzi.<br />
Opaque predicates detection by abstract interpretation.<br />
[21] R. Sekar, M. Bendre, D. Dhurjati, and P.Bollineni. A fast automaton-based<br />
method for detecting anomalous program behaviors. 2001.<br />
[22] Gaurav Tandon and Ph<strong>il</strong>ip Chan. Learning rules from system call arguments<br />
and sequences for anomaly detection.<br />
[23] theo detristan, tyll ulenspiegel, yann malcom, and mynheer su<strong>per</strong>bus von un-<br />
derduk. Polymorphic shellcode engine using spectrum analysis. Phrack #61,<br />
2003.<br />
[24] David Wagner and Drew Dean. Intrusion detection via static analysis.<br />
[25] Richard Wartell, Yan Zhou, Kevin W. Hamlen, Murat Kantarcioglu, and<br />
Bhavani Thuraisingham. Differentiating code from <strong>data</strong> in x86 binaries. 2011.