Ricerca tra la vecchia roba

Exploit: prevenire NON è meglio che curare

Posted: Settembre 11th, 2011 | Author: | Filed under: Guide | Tags: , , , | Commenti disabilitati su Exploit: prevenire NON è meglio che curare

Eccoci alla seconda puntata di come capire il funzionamento degli exploit; l’ultima volta siamo rimasti alla potenziale modifica del flusso di un programma scritto male a nostro uso e consumo, oggi non andremo di molto avanti in quanto cercheremo di capire assieme come viene gestita la memoria relativa ad un processo in un OS moderno. Partire subito con esempi di exploit non sarebbe utile perché gli esempi semplici praticamente non esistono: il primo paper di Aleph1 risale al 1996 e da allora i sistemi informatici si sono evoluti con contromisure proprio atte a evitare il succedersi di intrusioni alzando sempre di più l’asticella della difficoltà insita nel deviare il $pc tanto agognato.

C’è da dire che benché molti sforzi siano stati fatti, la genialità degli exploiter è cresciuta altrettanto portando questa pratica ad uno stato quasi magico. Ma bando alle ciance e iniziamo ad analizzare come è strutturato un programma (mi limiterò al formato eseguibile ELF of course).

Teoria

Nella prima parte abbiamo visto come un programma funziona a livello di istruzioni, ma per eseguire istruzioni un programma ha prima di tutto bisogno di essere caricato in memoria; bisogna tenere conto che ci saranno zone di memoria apposite per ogni tipo di necessità, elenchiamole:

  • Stack: visto sempre nella puntata precedente, contiene le variabili locali del contesto di esecuzione più le variabili della funzione attualmente in esecuzione
  • File mappings: esiste la possibilità di mappare direttamente file in memoria ed è anche il metodo con cui vengono caricate le shared library (ne parlerò un po’ meglio dopo)
  • Heap: zone di memoria allocate dinamicamente (in pratica malloc() et familia)
  • BSS: variabili statiche non inizializzate
  • Data: variabili statiche inizializzate
  • Text: contiene il codice eseguibile

Ognuna di queste sezioni ha ovviamente delle proprietà sue proprie, in particolare può essere scrivili,leggibile ed eseguibile. Per avere un esempio pratico eseguiamo

$ cat /proc/self/maps
08048000-08052000 r-xp 00000000 08:21 8421442    /bin/cat
08052000-08053000 rw-p 0000a000 08:21 8421442    /bin/cat
0875a000-0877b000 rw-p 00000000 00:00 0          [heap]
b74bc000-b76bc000 r--p 00000000 08:21 1035297    /usr/lib/locale/locale-archive
b76bc000-b76bd000 rw-p 00000000 00:00 0
b76bd000-b7810000 r-xp 00000000 08:21 29540827   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7810000-b7811000 ---p 00153000 08:21 29540827   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7811000-b7813000 r--p 00153000 08:21 29540827   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7813000-b7814000 rw-p 00155000 08:21 29540827   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7814000-b7817000 rw-p 00000000 00:00 0
b784a000-b784c000 rw-p 00000000 00:00 0
b784c000-b784d000 r-xp 00000000 00:00 0          [vdso]
b784d000-b7868000 r-xp 00000000 08:21 29540776   /lib/i386-linux-gnu/ld-2.13.so
b7868000-b7869000 r--p 0001b000 08:21 29540776   /lib/i386-linux-gnu/ld-2.13.so
b7869000-b786a000 rw-p 0001c000 08:21 29540776   /lib/i386-linux-gnu/ld-2.13.so
bfb95000-bfbb6000 rw-p 00000000 00:00 0          [stack]

Come potete vedere vengono visualizzati i vari mappings che il programma ha nella sua esecuzione; per capire cosa significhi ‘sta roba ecco a voi la pagina di man

/proc/[pid]/maps
 A file containing the currently mapped memory regions and their access permissions.

 The format is:

 address           perms offset  dev   inode   pathname
 08048000-08056000 r-xp 00000000 03:0c 64593   /usr/sbin/gpm
 08056000-08058000 rw-p 0000d000 03:0c 64593   /usr/sbin/gpm
 08058000-0805b000 rwxp 00000000 00:00 0
 40000000-40013000 r-xp 00000000 03:0c 4165    /lib/ld-2.2.4.so
 40013000-40015000 rw-p 00012000 03:0c 4165    /lib/ld-2.2.4.so
 4001f000-40135000 r-xp 00000000 03:0c 45494   /lib/libc-2.2.4.so
 40135000-4013e000 rw-p 00115000 03:0c 45494   /lib/libc-2.2.4.so
 4013e000-40142000 rw-p 00000000 00:00 0
 bffff000-c0000000 rwxp 00000000 00:00 0

 where "address" is the address space in the process that it occupies, "perms" is a set of permissions:

 r = read
 w = write
 x = execute
 s = shared
 p = private (copy on write)

 "offset"  is  the  offset  into  the  file/whatever,  "dev" is the device (major:minor), and "inode" is the inode on that
 device.  0 indicates that no inode is associated with the memory region, as the case would  be  with  BSS  (uninitialized
 data).

 Under Linux 2.0 there is no field giving pathname.

Piccolo appunto: se controllate i mappings in vari programmi eseguiti contemporanemente potreste chiedervi come mai c’è un overlap fra le varie regioni di memoria fra programmi diversi e la spiegazione è semplice: ogni programma ha a disposizione virtualmente 3GB di memoria flat, è il sistema operativo che si preoccupa di eseguire i mappings virtuale/fisico in maniera corretta.

Tornando all’ouput di cui sopra potete vedere che tutte le zone di memoria elencate sono presenti con in più una zona chiamata vdso (Virtual Dynamic Shared Object) che si preoccupa di fare da trampolino per le syscall (invenzione di linus che dichiara “I’m a disgusting pig, and proud of it to boot.“)

Prendiamo il seguente programma

#include<stdio.h>
#include<string.h>

int main(int argc, char* argv[]) {
 char* unitialized;
 static char* s_unitialized;
 char initialized[] = "porcamadonna";
 static char s_initialized[] = "porcamadonna";

 printf("process %d \n"
  "\t       uninitialzed: 0x%08x\n"
  "\tstatic uninitialzed: 0x%08x\n"
  "\t       initialized:  0x%08x\n"
  "\tstatic initialized:  0x%08x\n",
  getpid(), &unitialized, &s_unitialized, &initialized, &s_initialized);

 int iMustWait = !(argc > 1 && ! strcmp(argv[1], "-go"));
 if (iMustWait)
  getchar();

 return 0;
}

e compiliamolo

$ gcc segment.c -o segment

Una volta lanciato si bloccherà mostrandoci gli indirizzi di memoria assegnati alle variabili

$ ./segment
process 11663
        uninitialzed: 0xbf9e3ad8
 static uninitialzed: 0x08049804
        initialized:  0xbf9e3acb
 static initialized:  0x080497ec

Se controlliamo il map del programma in esecuzione

$ cat /proc/11663/maps
08048000-08049000 r-xp 00000000 08:13 3556546    /home/packz/Programmazione/Ci/Exploit/Data segment/segment
08049000-0804a000 rw-p 00000000 08:13 3556546    /home/packz/Programmazione/Ci/Exploit/Data segment/segment
b76ad000-b76ae000 rw-p 00000000 00:00 0
b76ae000-b7801000 r-xp 00000000 08:21 29540770   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7801000-b7802000 ---p 00153000 08:21 29540770   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7802000-b7804000 r--p 00153000 08:21 29540770   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7804000-b7805000 rw-p 00155000 08:21 29540770   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b7805000-b7808000 rw-p 00000000 00:00 0
b7839000-b783d000 rw-p 00000000 00:00 0
b783d000-b783e000 r-xp 00000000 00:00 0          [vdso]
b783e000-b7859000 r-xp 00000000 08:21 29540856   /lib/i386-linux-gnu/ld-2.13.so
b7859000-b785a000 r--p 0001b000 08:21 29540856   /lib/i386-linux-gnu/ld-2.13.so
b785a000-b785b000 rw-p 0001c000 08:21 29540856   /lib/i386-linux-gnu/ld-2.13.so
bf9c4000-bf9e5000 rw-p 00000000 00:00 0          [stack]

possiamo vedere che le variabili statiche (cioé globali all’interno del flusso del programma) sono nello spazio di memoria indicato nella seconda riga. Le altre sono nello spazio dello stack (ultima riga).

Tuttavia fra le due variabili statiche c’è una leggera differenza: se andiamo a vedere con readelf i section headers scopriamo che la variabile inizializzata punta alla sezione .data, mentre quella non inizializzata alla sezione .bss

$ readelf -S segment
There are 31 section headers, starting at offset 0x934:

Section Headers:
 [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
 [ 0]                   NULL            00000000 000000 000000 00      0   0  0
...
 [25] .data             PROGBITS        080497e4 0007e4 000018 00  WA  0   0  4
 [26] .bss              NOBITS          080497fc 0007fc 00000c 00  WA  0   0  4
...
 [30] .strtab           STRTAB          00000000 00126c 00024a 00      0   0  1
Key to Flags:
 W (write), A (alloc), X (execute), M (merge), S (strings)
 I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
 O (extra OS processing required) o (OS specific), p (processor specific)

Un ulteriore segmento di memoria è quello denominato heap che è associato con l’allocazione dinamica di memoria (si pensi alle varie malloc(), calloc() e free()). A livello implementativo sono variazioni della chiamata di sistema brk() che per esempio nelle libc è eseguita tramite la Doug Lea’s malloc; se avrò tempo un giorno potrei scrivere come eseguire exploit dall’heap, pratica molto diffusa nei browser.

L’argomento che rimane da discutere è il caricamento delle librerie dinamiche che necessità l’eseguibile e ciò avviene tramite l’uso dell’interpreter (nei sistemi linux rappresentato da /lib/linux-so.2). In realtà quando un file eseguibile viene lanciato (funzione load_elf_binary in fs/binfmt.c), il kernel si preoccupa di leggere dall’apposita sezione del file ELF proprio quale è il suo interpreter  e dopo aver impostato opportunamente le zone di memoria carica le librerie necessarie al funzionamento del programma tramite esso. Se interessa il codice di ld.so si trova nella directory elf/ del tree delle libc; il suo punto di ingresso è la funzione dl_main in rtld.c.

Siccome le librerie vengono caricate a run-time esiste un processo chiamato di relocation che avviene all’interno di un processo ELF quando si necessità di eseguire una routine appartenente ad una libreria esterna: se disassembliamo il programma segment, possiamo vedere che alla riga in cui viene eseguita la call alla printf, la jump salta alla sezione .plt del nostro eseguibile

$gdb segment
Reading symbols from /home/packz/Programmazione/Ci/Exploit/Data segment/segment...(no debugging symbols found)...done.
(gdb)disassemble main
...
0x080484ad <+89>:    call   0x8048350 <printf@plt>
...

Se disassembliamo quella sezione otteniamo

$ objdump -j .plt -d segment

segment:     file format elf32-i386

Disassembly of section .plt:

08048340 <printf@plt-0x10>:
 8048340:       ff 35 c8 97 04 08       pushl  0x80497c8
 8048346:       ff 25 cc 97 04 08       jmp    *0x80497cc
 804834c:       00 00                   add    %al,(%eax)
 ...

08048350 <printf@plt>:
 8048350:       ff 25 d0 97 04 08       jmp    *0x80497d0
 8048356:       68 00 00 00 00          push   $0x0
 804835b:       e9 e0 ff ff ff          jmp    8048340 <_init+0x38>

Se in gdb andiamo a vedere che indirizzo c’è scritto alla cella di memoria 0x80497d0

(gdb) x/a 0x80497d0
0x80497d0 <printf@got.plt>:    0x8048356 <printf@plt+6>

con stupore scopriamo che punta alla istruzione successiva. Seguendo il flusso delle istruzioni vediamo che viene pushato un numero (l’indice della funzione da relocare) e avviene una jump all’inizio della sezione .plt; viene pushato un indirizzo (?) e si salta alla routine (call *0x80497cc) che andrà a risolvere la routine printf. L’indirizzo di quest’ultima jump è riempito dal linker all’avvio del programma.

Finita la routine all’indirizzo 0x80497d0 si ritroverà direttamente l’indirizzo della funzione in questione così da non aver più necessità di rieseguire tutto la relocazione alla prossima invocazione della printf. La Procedure Linkage Table e la Global Offset Table hanno un ruolo molto importante nella scrittura degli exploit avanzati, spero di poterlo spiegare in futuro.

Dopo tutta la teoria

Dopo tutto questo viaggio sulle varie zone di memoria possiamo passare al collegare questo con la sicurezza di un eseguibile: per prima cosa c’è da notare che se andiamo ad eseguire lo stesso programma due volte e controlliamo gli indirizzi assegnati alle varie aree di memoria scopriamo che tutti gli indirizzi tranne quelli per il codice e dei dati globali sono cambiati, in particolare sono randomizzati.

Questa è una delle prime contromisure sviluppate nel corso degli anni per prevenire i buffer overflow: se gli indirizzi cambiano ogni volta non è così facile scrivere un exploit che funzioni correttamente.

Per sapere come è implementata la randomizzazione leggetevi questo post: https://xorl.wordpress.com/2011/01/16/linux-kernel-aslr-implementation/.

Secondo step di protezione è lo stack non eseguibile (NX): siccome il codice per l’exploit viene posto (solitamente) nello stack e si cerca di eseguirlo da lì, rimuovendo dalle pagine di memoria dello stack il bit eseguibile provoca un segmentation fault quando l’istruction pointer viene dirottato in quelle zone. Come si nota l’unica zona eseguibile è quella text e le librerie condivise.

Terzo step di protezione è l’anteporre al frame dello stack dei valori random che vengono chiamati canary values da controllare al termine di una routine per la loro corruzione: siccome un buffer overflow dovrà per forza passare attraverso questi valori corrompendoli, la protezione dovrebbe essere effettiva.

L’ultimo livello di protezione è la cosiddetta ASCII-Armor, cioé caricare librerie  con indirizzi contenenti un byte nullo in maniera da rendere impossibile utilizzare tali indirizzi in un payload.

Purtroppo(?) tutte queste contromisure sono (per la maggior parte dei casi)  inutili, un eseguibile ha dentro di se abbastanza spazio di azione da rendere un overflow exploitabile nella maggioranza dei casi; nei post futuri spero di riuscire a spiegare come.