How linux distros protect you: parte 2

Ultimo aggiornamento: 22-02-2016

Dopo aver parlato di elf hardening nella prima parte, introduciamo questa seconda parte con un esempio di buffer overflow: lo stack overflow, per parlare dei meccanismi di protezione degli eseguibili che il mondo linux ci offre.
Questo articolo è l’ultimo della serie: volevo farne un terzo in cui si parlasse di Macchine Virtuali (partendo dal comando chroot, passando per i container di linux o le jails di BSD), però basta girare per il web per trovare roba interessante.

Introduzione

Nell’articolo precedente viene spiegato il formato degli eseguibili di linux: l’ELF. Ora per continuare con la spiegazione aggiungo solo che i segmenti degli ELF caricati in memoria (data, codice o text, stack, heap), come molti di voi sapranno già, hanno degli indirizzi prestabiliti in cui vanno allocati.

linux_mem_manag
Esempio di processo su architettuta a 32 bit

Questa caratteristica era utile nei primi anni della storia dell’informatica, in cui il programmatore doveva sapere dove il loader del sistema operativo caricasse il proprio programma; ora non è più certo utile: nella maggior parte dei casi, al programmatore non serve sapere dove i segmenti del codice vengono allocati, inoltre l’allocamento in posizioni pre-fissate viene anche sfruttata per scopi non certo buoni, come si vedrà ora.

Buffer Overflow

Chi programma in C\C++ saprà che quando si instanzia un array in una funzione, lo spazio di memoria utilizzato è lo stack (se non si dichiara staticamente) e chi conosce diversi linguaggi assembly, in primis Intel x64, ma anche Mips e ARM (si pensi a diverse chiamate a funzioni, una dentro l’altra) saprà che nello stack viene messo anche l’indirizzo di ritorno della funzione chiamante, oltre ai parametri passati:

   | function arg1|  ;indirizzo: 1000d
   | function arg2| 
   | return addr  |
   | int variable |
   | array [4-7]  |
   | array [0-3]  |  ;indirizzo:  976d 

Immaginiamo che questa sia la situazione dello stack per una chiamata ad una funzione che usa memcpy:

function copia(char arg1[], int arg2) {
   int variable;
   char array[8];
   memcpy(arg1, array, sizeof(char)*arg2); //arg2 è la lunghezza dell'array arg1
   return 
}

Se il parametro arg2 fosse un valore maggiore di 8, cosa potrebbe succedere una volta eseguito il memcpy? Potremmo passare in arg1 anche del codice macchina da eseguire: uno shellcode e fare in modo di riscrivere l’indirizzo di ritorno con l’indirizzo del nostro codice malevolo, dato che potremmo conoscere gli indirizzi in memoria, tramite debugging:

   
   | codice assembly         |
   | codice assembly         |  ; inizio del codice malevolo
   | 0x0                     |  ;indirizzo: 1000d
   | 0x0                     | 
   | return codice_malevolo  |
   | 0x0                     |
   | 0x0                     |
   | 0x0                     |
   | 0x0                     |  ;indirizzo:  976d 

C’è da dire che nel SDK di Windows, ci sono funzioni come memcpy_s, alle quali si deve passare anche la dimensione del buffer in cui si va a copiare.

Preambolo importante: Execstack
“execstack is a program which sets, clears, or queries executable stack flag of ELF binaries and shared libraries. Linux has in the past allowed execution of instructions on the stack and there are lots of binaries and shared libraries assuming this behaviour.” (dal man)

Quindi da come si può leggere dal man, eseguire del codice nello stack non è sempre un’ azione malevola, almeno per il mondo linux; però dato che molto spesso lo è, si sono inventate diverse contromisure, alcune inserite direttamente nel codice dei programmi dal compilatore, altre messe nel kernel o addirittura a livello hardware.

Controlli del compilatore:i canarini

Per ovviare ai buffer overflow, nei compilatori è stato introdotto lo stack smashing protection tramite l’uso del canarino (anno 2005): il quale è semplicemente un valore di controllo messo nello stack, che viene controllato ogni volta che si fa un return. Se il valore non è quello corretto viene mostrato un messaggio d’errore (un approfondimento). Ovviamente ad ogni esecuzione del programma il valore del canarino deve cambiare, altrimenti è tutto inutile.
Per il compilatore gcc si può inserire questo controllo tramite le opzioni:

  • -fstack-protector: protegge solo le funzioni che il compilatore ritiene vulnerabili: quelle che chiamano alloca e che hanno un buffer maggiore di 8 byte.
  • -fstack-protector-strong: come -fstack-protector ma protegge tutte le funzioni che allocano un buffer locale.
  • -fstack-protector-all: protegge tutte le funzioni.

Se invece si vuol disabilitare tale opzione per fare degli esprimenti, si può usare con: -fno-stack-protector. Diverse distribuzioni, come Ubuntu, lo settano di default quando si compila. Da vedere anche RenewSSP utile per evitare gli attacchi a brute force contro il canarino.

Controlli nel kernel

No eXecute

NX(No eXecute) è un bit della tabella delle pagine del processo, che per i processori Intel è nell’hardware (si veda PAE), che indica se la memoria è eseguibile o meno.
Questo bit è stato introdotto da AMD nel 2003 per la sua architettura desktop a 64bit, ma già nel 2001 Intel la mise sui processori Itanium e nel fine 2004 anche sui processori server di fascia media Xeon, per non parlare della miriade di altri processori che già l’aveva: si veda la pagina di wiki.
Prima di NX veniva indicato al sistema operativo se il segmento poteva essere eseguibile o meno (si veda la documentazione di mprotect o VirtualProtect per Windows), però ora è proprio la CPU che si rifiuta di eseguire codice nella memoria che ha questo bit settato: infatti sono proprio lo stack e la memoria heap che lo hanno di default.

Anti NX: ROP

Per sfuggire a questa protezione gli hacker si sono inventati la tecnica Return Oriented Programming: invece di mettere codice da eseguire nello stack, si trova nel programma (e nelle sue librerie) dei pezzi di codice da utilizzare, che terminano tutti con un return; così basta che il nostro  “return addr” punti agli indirizzi dei codici scelti, e che ad ogni return si vada ad un indirizzo indicato da noi ed il gioco è fatto (Introduction to return oriented programming (ROP)).

Address Space Layout Randomization

Quando si lancia un eseguibile, ci sono dei segmenti (data, codice, stack, heap) che hanno degli indirizzi prestabiliti in memoria: la tecnologia ASLR (Address Space Layout Randomization) permette a questi segmenti di avere degli indirizzi random, ad ogni lancio.
Tornando all’esempio dello stack overflow di sopra o alla tecnica ROP: se nel caso il nostro codice malevolo non fosse ad un indirizzo fisso, l’esecuzione di quest’ultimo non sarebbe affatto facile.
Affinche un programma possa sfruttare a pieno i vantaggi di ASLR deve essere compilato come “Position Independent Executables“, passando a gcc: “-fPIE -pie”.
PIE vuol dire che il programma può essere allocato ovunque, perchè il codice compilato è indipendente dalla posizione che può avere in memoria.
Ci sono vari tipi di ASLR (si veda Security Feature di Ubuntu):

  • Stack ASLR: ad ogni esecuzione il programma ha un diverso stack memory space layout.
  • Libs/mmap ASLR:ad ogni esecuzione il programma ha un diverso mmap memory space layout, utile quando si carica una nuova libreria e per evitare il “return-to-libc“.
  • Exec ASLR: ad ogni esecuzione un programma compilato con “-fPIE -pie” sarà caricato in una differente memory location.
  • Brk ASLR: il brk è il memory manager che alloca memoria nel segmento dati, di solito invocato da malloc.  “brk ASLR adjusts the memory locations relative between the exec memory area and the brk memory area”.
  • VDSO ASLR: ad ogni esecuzione il programma ha un diverso vdso location

Sempre dalla pagine di Ubuntu, si dice che su un’architettura con pochi registri general purpose (tipo Intel a 32bit), ASLR ha un impatto del 5-10% sulle performance, però io consiglierei sempre di settarla di default.
Per settare ASLR si deve modificare il valore di “randomize_va_space” (in Ubuntu è nel file /proc/sys/kernel/randomize_va_space), tramite il comando sysctl:

/proc/sys/kernel$ sudo cat randomize_va_space 
2

/proc/sys/kernel$ sudo sysctl -w kernel.randomize_va_space=1
kernel.randomize_va_space = 1
/proc/sys/kernel$ sudo cat randomize_va_space 
1

/proc/sys/kernel$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2
/proc/sys/kernel$ sudo cat randomize_va_space 
2

Ma cosa indicano questi valori? Da come riporta la guida: “Documentation for /proc/sys/kernel/*“:

  • 0: Si disabilita ASLR
  • 1: Le pagine di mmap, stack e VDSO sono randomizzate. Anche le shared libraries sono caricate con indirizzamento casuale, così come il segmento relativo al codice degli eseguibili compilati con “-fPIE -pie”
  • 2: Viene randomizzato anche l’indirizzamento dell’heap

Anti ASLR: Offset2lib

Alla fine del Novembre del 2014, è stata presentato l’attacco Offset2lib: che sfruttando l’implementazione di ASLR, sui sistemi linux, è capace di trovare l’indirizzo base delle shared library (si pensi al return-to-libc). Sul link è presente anche una demo di un exploit che bypassa: il canarino, il bit NX e la tecnologia ASLR.

Intel MPX:

Intel introdurrà a breve, dal momento in cui scrivo l’articolo, sui suoi processori la tecnologia: Memory Protection Extensions (MPX). Da come dice il nome MPX è un meccanismo atto a difendere la memoria da bug quali il buffer overflow, introducendo nella CPU nuovi registri, tra i quali i bound registers e nuove istruzione atte a controllare questi registri. I bound register contengono l’indirizzo base di un puntatore ed il suo limite superiore. Quando un puntatore è usato, vengono controllati questi bound, così da avere un controllo simile ai linguaggi Java o .NET sull’accesso alla memoria.Per approfondire consiglio: Intel MPX support in GCC.
Questo è un controllo simile a quello che si può avere usando, però quando si sta sviluppando, Intel Pointer Checker o  AddressSanitizer. Quest’ultima è una libreria utile per scoprire i memory error quali heap, stack e global buffer overflow che può avere un programma. Inoltre sul sito del progetto vi è una pagina dedicata a MPX dove vengono mostrate anche le sue debolezze.

Altri bug

Chi è appassionato di programmazione e di sicurezza, sa che che non si vive di soli buffer overflow: format string attacks, heap overflow. Ma ci sono anche problemi non relativi agli eseguibili in se come le politiche di accesso ai file, socket, ecc.
Per il primo tipo di problemi basta seguire una delle possibile guide sul Hardening/Security delle distro linux (si vedano i link in fondo).
Per il secondo tipo di problemi RedHat con NSA (è doveroso dirlo) hanno introdotto SELinux (Security-Enhanced Linux), ormai presente in tutte le distribuzioni linux, però ce ne sono anche altri di Mandatory Access Control: come AppArmor.

Exec-Shield

Un’altra security feature inventata da RedHat è Exec-Schield (simile a W^X dei BSD), anch’essa introdotta intorno al 2005, che: setta di default la tecnologia ASLR, utilizza il bit NX e se non è presente nel processore lo virtualizza, aggiunge check a glibc, per il format string e altro ancora.

Grsecurity

Grsecurity è l’unione delle caratteristiche di ExecSchield e di SELinux. Include PAX, che offre le feature di ExecShield, più meccanismi di Roled Based Access Control (si pensi a SELinux) e altro ancora.

Riferimenti

Corso di Security II presso Unimi
Debian Hardening
Fedora Security Features
HardenedBSD
Ubuntu Security Features

Link interessanti

Blink ROP – 2015
Hacker Hut – 2003
Heap Overflow illustrato – 2007
Return-to-libc
Secure Programming with GCC and GLibc – 2008
Dive into ROP – a quick introduction to Return Oriented Programming – 2013
Buffer overflow su Windows – 2014
64-bit Linux Return-Oriented Programming

 

Smashing the stack: 1996, 2010, 2013