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