Sei sulla pagina 1di 57

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

Direzione di
Stack crescita Stack

Code
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

Direzione di
Stack crescita Stack

Code

* 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;

}

“Cima” dello Stack


b (4 byte)

Frame Pointer (4 byte)


Espansione
Return Address (4 byte) dello Stack

a (4 byte)
Call Stack
void funzione2(int a){
char b[10];

}

“Cima” dello Stack


b (10 byte)

Frame Pointer (4 byte)


Espansione
Return Address (4 byte) dello Stack

a (4 byte)
Call Stack
void funzione3(int a){
char b[8];
char c[16];

}

“Cima” dello Stack


c (16 bytes)

b (8 bytes)

Frame Pointer (4 bytes) Espansione


dello Stack
Return Address (4 bytes)

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 C
0x2346 i
0x2347 a
0x2348 o
0x2349 \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);
}

“Cima” dello Stack


c (4 bytes)

b (4 bytes)

Frame Pointer (4 bytes) Espansione


dello Stack
Return Address (4 bytes)

a (4 bytes)
Call Stack char* a = “aaa”;
a = 0x2345
void funzione4(char* a){
char b[4];
char c[4];
strcpy(c, a);
}

c “Cima” dello Stack


a a a \0
b
… … … …
FP Frame Pointer (4 bytes) Espansione
dello Stack
Return Return Address (4 bytes)
a 0x2345
Call Stack char* a = “Saluti”;
a = 0x2345
void funzione4(char* a){
char b[4];
char c[4];
strcpy(c, a);
}

c “Cima” dello Stack


!!!! S a l u
b
t i \0 …
FP Frame Pointer (4 bytes) Espansione
dello Stack
Return Return Address (4 bytes)
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) “Cima” dello Stack

b (8 bytes)
Frame Pointer (4 bytes)
Espansione
Canarino dello Stack

Return Address (4 bytes)


a (4 bytes)
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, 232-1]
 Int  32 bit  [-231, 231-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

Potrebbero piacerti anche