Corso di Sicurezza Informatica Sicurezza del Software Ing. Giuseppe D Aquì
Sicurezza nell informatica Un computer sicuro è un computer spento (Kevin Mitnick)
Attacchi informatici Gli attacchi informatici, secondo Wikipedia vengono fatti tramite la rete internet, da parte di utenti chiamati dalla società cracker, che tramite l uso di software particolari, a volte creati da loro stessi, si intrufolano abusivamente all interno del sistema, riuscendo ad ottenere piena disponibilità della macchina, per gestire risorse e dati senza avere i giusti requisiti richiesti
Attacchi di massa Storicamente (fino a metà anni 80) i software venivano scritti appositamente per l utilizzo su specifiche macchine Le macchine stesse a loro volta erano molto diverse tra loro, come architettura e sistema operativo Un attacco, per riuscire, necessitava di una persona con grande esperienza e abilità
Attacchi di massa (2) Con la diffusione dell informatica di massa : Standardizzazione dell architettura Standardizzazione de facto dei sistemi operativi Accesso di rete tramite protocolli standard Si è passati ad un ambiente in cui ci sono centinaia di migliaia di macchine tutte uguali Se un attacco funziona su una, funziona potenzialmente su tutte pacchettizzazione degli attacchi e Script Kiddie
Vulnerabilità Una vulnerabilità è una debolezza del sistema che può essere sfruttata da un attacker Causata da un errore di implemetazione o di progettazione Un exploit è un insieme di istruzioni che sfruttano la vulnerabilità
Exploit Gli exploit vengono in genere pubblicati sui bollettini di sicurezza perché sono una prova tangibile dell esistenza di un bug Un exploit pubblicato ha ovviamente la conseguenza di aumentare gli attacchi da parte di Script Kiddie Ma, considerando che chi ha intenzioni malevole in qualche modo lo trova anche se non pubblicato, diffondere un exploit serve a difendere gli utenti visto che il produttore del software è costretto a rilasciare una correzione (patch)
Memory Leak Un memory leak è una scorretta gestione della memoria da parte di un software Per esempio, a seguito di un memory leak un programa può occupare più memoria di quanta gliene necessiterebbe Non sempre da un memory leak deriva una vulnerabilità
Errori di programmazione Errori che causano accesso non controllato ai dati. Errori che causano alterazione del flusso di esecuzione del programma. Mancanza di verifiche sui permessi di accesso alle funzioni oppure ai dati (controlli inadeguati o incompleti). Errori sulle condizioni limite (primo o ultimo caso). Altri errori logici.
Buffer Overflow Un buffer è un area di memoria temporanea che contiene dei dati Qualsiasi variabile può essere vista come un buffer
Struttura di un software Un software viene scritto con linguaggi di programmazione leggibili da umani e poi tradotto in un linguaggio leggibile dalla macchina I passi sono: Compilazione link
Richiami di architettura dei calcolatori Una CPU è composta da: Control Unit: unità che gestisce il flusso di esecuzione dei programmi Arithmethic Logic Unit Registri: aree di memoria interne, usate come supporto alle operazioni
Codice macchina Qualunque software per essere eseguito deve essere trasformato in codice macchina Sequenza di byte, composta da codice e dati Il codice è composto da numeri (opcode e parametri) che rappresentano istruzioni permettono di svolgere operazioni sui registri della CPU, sulla memoria, sulle periferiche ecc.
Uso della memoria Importante: osservando una locazione di memoria è impossibile sapere se il numero che contiene rappresenta una istruzione o un dato Il suo significato dipende dal flusso di esecuzione!
Mappa della memoria Un software caricato in memoria e pronto per l esecuzione si può suddividere in blocchi chiamati segmenti Blocchi che raggruppano locazioni di memoria che svolgono una funzione simile
Mappa della memoria Data BSS Heap Direzione di crescita Heap Stack Code Direzione di crescita Stack
Mappa della memoria Data: contiene le variabili globali e statiche inizializzate Es. static int pippo=321; BSS: contiene le variabili globali e statiche non inizializzate, o inizializzate a zero Es. static int pippo; static int pluto=0;
Mappa della memoria Heap: contiene le variabili allocate con malloc()/new durante l esecuzione del programma Stack: contiene le variabili locali semplici (int, char, short ) e informazioni ausiliarie per effettuare le chiamate di funzione
Struttura Stack Lo stack (pila) è una struttura dati LIFO Last In First Out L ultimo elemento ad entrare è il primo ad uscire Uno stack possiede due funzioni: Push: inserisce un dato in cima allo stack (top) Pop: rimuove un dato dalla cima dello stack
Heap e Stack L Heap e lo Stack rappresentano la memoria a disposizione di un programma e sono di dimensione variabile L Heap cresce spostando in avanti il puntatore alla cima (top) Lo Stack cresce spostando all indietro il puntatore alla cima (top) Se i due puntatori si incontrano Out of Memory
Mappa della memoria* Data BSS Heap Direzione di crescita Heap Stack Code Direzione di crescita Stack * la disposizione dei blocchi cambia a seconda dell architettura e del Sistema Operativo
Attacco Denial of Service (DoS) Conoscendo il funzionamento della memoria, un primo attacco che può venire in mente è riempire tutta la memoria a disposizione in modo che un programma smetta di funzionare Un attacco di questo tipo porta a negare il servizio agli altri utenti, perché il software è andato in crash
Attacco DoS (esempio) Un servizio erogato via internet (come web, email, etc) ha bisogno di una certa quantità di memoria per gestire ogni richiesta Al crescere delle richieste occuperà sempre più memoria Fino al punto in cui la memoria non basterà più
Attacco DoS Per evitare il fallimento dell esecuzione molti servizi accettano solo un certo numero di richieste, mettendo le altre in coda In questo modo il programma non va in crash Dal punto di vista degli utenti, però, non cambia niente: durante un attacco DoS si vedono negare il servizio
Esecuzione del codice macchina L esecuzione del codice macchina avviene in modo sequenziale Normalmente c è un registro (Program Counter (PC) o Instruction Pointer (IP)) memorizza l indirizzo dell istruzione corrente Viene incrementato per passare all istruzione successiva Esistono istruzioni di Jump che servono ad eseguire le condizioni (if else) e le chiamate a funzione
Chiamate di funzione Una chiamata di funzione è una cosa più complicata di come sembra Non è un semplice salto condizionale, perché ha queste caratteristiche: Può avere delle variabili come argomento Le istruzioni che la compongono non possono agire sulle variabili di altre funzioni e viceversa (visibilità) Alla sua conclusione, l esecuzione del programma deve riprendere da dove era stata interrotta
Chiamate di funzione Per garantire queste caratteristiche il compilatore traduce le chiamate di funzione in operazioni fatte sullo Stack Inserisce (push) sullo Stack: il Return Address: l indirizzo a cui ritornare una volta finita la funzione Il Frame Pointer, che rappresenta l indirizzo di riferimento per tutte le variabili locali Opzionalmente: Gli argomenti della funzione Variabili locali della funzione
Call Stack void funzione(int a){ int b; } b (4 byte) Cima dello Stack Frame Pointer (4 byte) Return Address (4 byte) Espansione dello Stack a (4 byte)
Call Stack void funzione2(int a){ char b[10]; } b (10 byte) Cima dello Stack Frame Pointer (4 byte) Return Address (4 byte) Espansione dello Stack a (4 byte)
Call Stack void funzione3(int a){ char b[8]; char c[16]; } c (16 bytes) Cima dello Stack b (8 bytes) Frame Pointer (4 bytes) Return Address (4 bytes) Espansione dello Stack a (4 bytes)
Stack Buffer Overflow Molte funzioni per l accesso alla memoria in linguaggi a basso/medio livello ( C/C++ ) non effettuano controlli sull accesso alle locazioni di memoria Questo per permettere la massima flessibilità di utilizzo ma da un grande potere derivano grandi responsabilità (cit.)
Stringhe Come si rappresenta una stringa in C? Una stringa è una sequenza di caratteri e si rappresenta come un array di char (interi a 8 bit) Un array è caratterizzato da: Dimensione: Una stringa in C è terminata da un carattere null string \0 Punto di inizio: L indirizzo iniziale della stringa è memorizzato in un puntatore (char*)
Stringhe char* saluto = Ciao ; saluto= 0x2345 0x2345 0x2346 0x2347 0x2348 0x2349 C i a o \0 Un char* è un puntatore ad un area di memoria (buffer) che memorizza la stringa
Copia di stringhe Come si copia una stringa? strcpy(char* dest, char* origine) Strcpy copia il contenuto di origine in dest Prende il primo carattere di origine e lo copia nella prima locazione di dest Prende il secondo carattere di origine e lo copia nella seconda locazione di dest E così via
Strcpy Strcpy ferma la copia solo quando ha esaurito tutti i caratteri di origine Se origine > dest, inizierà a scrivere i caratteri di origine al di fuori del buffer di dest (buffer overflow), potenzialmente distruggendo l esecuzione corretta del programma Se dest è una variabile memorizzata nello Stack allora avremmo un overflow che potenzialmente può distruggere lo Stack
Call Stack void funzione4(char* a){ char b[4]; char c[4]; strcpy(c, a); } c (4 bytes) Cima dello Stack b (4 bytes) Frame Pointer (4 bytes) Return Address (4 bytes) Espansione dello Stack a (4 bytes)
Call Stack void funzione4(char* a){ char b[4]; char c[4]; strcpy(c, a); } char* a = aaa ; a = 0x2345 c a a a \0 Cima dello Stack b FP Return Frame Pointer (4 bytes) Return Address (4 bytes) Espansione dello Stack a 0x2345
Call Stack void funzione4(char* a){ char b[4]; char c[4]; strcpy(c, a); } char* a = Saluti ; a = 0x2345!!!! c S a l u Cima dello Stack b t i \0 FP Return Frame Pointer (4 bytes) Return Address (4 bytes) Espansione dello Stack a 0x2345
Stack Buffer Overflow Vengono sovrascritte altre locazioni di memoria immediatamente successive nello Stack Si può anche arrivare a sovrascrivere il Return Address, modificando così l esecuzione del programma
Casi di accesso non-malizioso Se la variabile che viene sovrascritta non è pensata in modo malizioso, si avrà un Return Address che punta ad un area di memoria che non appartiene al programma In modalità protetta, questo significa generare un Segmentation fault e interromprere l esecuzione del programma
Attacco di Stack Buffer Overflow Per sfruttare in modo malizioso il bug, si possono fare due cose: Inserire nel buffer sotto attacco del codice eseguibile, che faccia qualcosa di malizioso oppure esegua componenti del sistema operativo (shell) Sovrascrivere il return address con l indirizzo del codice malizioso Al termine della funzione verrà eseguito in automatico il codice malizioso!
Proteggersi dallo Stack Buffer Overflow Usare funzioni sicure come strncpy(), che prevedono un controllo sulla dimensione massima del buffer Sfruttare funzionalità dei compilatori per produrre codice più difficile da attaccare Sfruttare funzionalità della CPU per marcare come non-eseguibili le aree di memoria dei dati
Canarino Il Canarino (canary) è un campanello d allarme per il buffer overflow, sfruttato alcuni compilatori È un numero, difficile da conoscere/scoprire, che viene inserito nello Stack subito dopo il Return Address
Canarino void funzione5(int a){ char b[8]; char c[16]; } c (16 bytes) b (8 bytes) Frame Pointer (4 bytes) Canarino Return Address (4 bytes) a (4 bytes) Cima dello Stack Espansione dello Stack
Canarino Il principio è che se qualcuno/qualcosa vuole sovrascrivere il Return Address, allora dovrà sovrascrivere anche il Canarino Prima di chiamare il Return Address, il compilatore inserisce codice che controlla se il Canarino corrisponde all originale Se è stato modificato, allora c è un tentativo di attacco in corso!
No-eXecute Il metodo del Canarino ha un problema: è possibile scoprirlo e ricostruirlo Per questo viene in aiuto l hardware I processori più recenti supportano un flag per le pagine di memoria chiamato NX No-eXecute Appena l esecuzione del programma entra in una pagina marcata come NX l esecuzione si ferma con un errore
Problemi del No-eXecute Il flag NX risolve l attacco Stack Buffer Overflow che abbiamo visto prima Non può impedire però la sovrascrittura del Return Address Ovvero si può sovrascrivere il Return Address per far saltare l esecuzione in qualsiasi punto del programma
Attacco return-to-libc In qualunque programma viene automaticamente aggiunta, in fase di link, la Libreria Standard C (libc) Pertanto si può sovrascrivere il Return Address facendolo puntare a una funzione della libreria C, eseguendola Una funzione come system() (presente nella Libreria C) permette di eseguire qualunque programma del sistema attaccato
Address Space Layout Randomization (ASLR) Per evitare anche questi ultimi attacchi si usa la disposizione casuale degli spazi di indirizzi I blocchi di memoria dedicati alle librerie, ai segmenti di dati e del codice vengono disposti in memoria in modo casuale In questo modo è molto difficile conoscere in anticipo l indirizzo da inserire al posto di Return Address
Heap Overflow Heap Buffer Overflow è simile alla versione Stack È più raro, perché raramente l heap contiene puntatori a funzione che possono essere sovrascritti Non per questo è meno pericoloso! Vedi vulnerabilità JPG Microsoft
Integer Overflow Un Integer Overflow si ha quando si superano i limiti di memorizzazione di un intero Unsigned Char 8 bit [0, 255] Char 8 bit [-128, 127] Unsigned Int -> 32 bit [0, 2 32-1] Int 32 bit [-2 31, 2 31-1]
Integer Overflow (Esempio) Un problema si ha con la conversione implicita da Signed a Unsigned A livello di memoria, non cambia la rappresentazione del dato ma solo la sua interpretazione
Tool per prevenzione e attacco
Analisi della memoria Valgrind/DRMemory
Analisi del codice compilato Disassembler e Debugger Ollydbg, IDA, gdb
Metasploit Piattaforma per la verifica di vulnerabilità
Riferimenti Mappa della memoria per differenti architetture: Notes on Assembly memory Smashing the Stack for fun and profit di Aleph One SecurityFocus