03.06.2013 Views

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 ...

SHOW MORE
SHOW LESS

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.

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!