How linux distros protect you (parte 1)

Ultimo aggiornamento: 22-02-2016

Questo è il primo di una serie d’ articoli, dove voglio fare un resoconto delle difese ai buffer overflow, format string e pericoli vari presenti su linux.
Su linux (come su altri s.o.) ci sono protezioni sia a livello di compilazione che d’ esecuzione, potrei finire l’articolo postando il link al Hardening sul sito di Debian, però voglio approfondire con altri articoli trovati in giro per la rete e facendo esempi.
In questo primo articolo faccio una breve introduzione al formato ELF e al caricamento in memoria da parte del kernel dell’eseguibile, mostrando anche una prima protezione.
Per comprendere l’articolo ed i suoi link, si dovrebbero avere perlomeno le basi di programmazione C, della compilazione e linking con GCC, assembly INTEL ed anche una base di Sistemi Operativi non farebbe male :P.

ELF: Executable and Linking Format

Il formato ELF è formato da tre strutture principale: Elf Header, la Section Header Table (SHT) e Program Header Table (PHT) [1a1b] e quest’ultima contiene le informazioni utilizzate dal kernel per caricare in memoria l’eseguibile.
L’eseguibile se non è stato compilato con linking statico, richiederà l’uso delle “shared library” per poter essere eseguito correttamente: per vedere se l’elf ha bisogno di un linker dinamico per essere eseguito, basta vedere se ha una sezione “INTERP” nel PHT, che non contiene altro che una string al path del dinamic linker: “/lib/ld-linux.so”.

Le librerie possono essere:

  • linkate dinamicamente: la shared library viene linkata a tempo d’ esecuzione: è il loader di linux che effettua l’operazione;
  • caricate dinamicamente: la libreria viene caricata in memoria dal programma e poi viene chiamata una funzione [2] (lazy loading); come fa Windows con le DLL.

Col link dinamico c’è bisogno di due sezioni: la .PLT (Procedure Linkage Table) e .GOT (Global Offset Table).
Come vengono utilizzate? Faccio un esempio, compiliamo questo semplice programma (gcc file.c):

#include <stdio.h>
int main() {
printf("ciao");
return 0;
}

Disassemblando con objdump (objdump -d a.out), vediamo come viene chiamata la printf:

Disassembly of section .text:
080483e4 :
80483e4: push %ebp
80483e5: mov %esp,%ebp
80483e7: and $0xfffffff0,%esp
80483ea: sub $0x10,%esp
80483ed: mov $0x80484e0,%eax
80483f2: mov %eax,(%esp)
80483f5: call 8048300 <printf@plt> <-- la chiamata, nota l'indirizzo 0x8048300
...
Disassembly of section .plt:
...
08048300 <printf@plt>:
8048300: jmp *0x804a000 <-- salta a printf di libc
8048306: push $0x0
804830b: jmp 80482f0 <_init+0x3c>

dove quel jmp nella sezione .plt, corrisponde proprio a printf

~/temp$ readelf --relocs a.out
Relocation section '.rel.plt' at offset 0x29c contains 3 entries:
Offset   Info            Type     Sym.Value  Sym. Name
0804a000 00000107 R_386_JUMP_SLOT 00000000    printf

e l’indirizzo 804a000 è proprio nella sezione GOT.PLT
~/temp$ readelf -S a.out
Name        Type    Addr    Off
.got     PROGBITS 08049ff0 000ff0
.got.plt PROGBITS 08049ff4 000ff4 <-- QUI
.data    PROGBITS 0804a00c 00100c

Per semplificare, usando uno sketch up [3] , quando viene chiamata la prinf avviene ciò:

Chiamata a funzione

Oltre a questo meccanismo c’è quello del lazy binding (per approfondire invece: 1b e 4) : la prima volta che una funzione viene chiamata, viene eseguito il codice del suo stub (quello nella sezion .plt), viene eseguita la funzione, poi salva l’indirizzo della funzione  nel GOT, in modo tale che la prossima volta che si chiama la funzione viene eseguito direttamente il codice, senza passare dallo stub.

Nel caso che non venga generato codice Position Independent Code, può servire solo la sezione “.rel.dyn”, senza usare lo stab del PLT: vedi  [4].
Una volta caricate le librerie dinamiche, l’esecuzione torna nel “main” del programma, per poter essere eseguito

RELocation Read-Only

Come avete potuto vedere, per chiamare una fuzione utilizzando il link dinamico delle librerie, viene prima caricata la libreria in memoria,  viene  riempita la GOT  con l’indirizzo in memoria della funzione nella libreria ed infine, tramite il codice nel PLT, viene effettuata la chiamata alla funzione, lazy binding a parte, ovviamente.
Ma cosa capita se il GOT viene sovrascritto?
Nel caso più banale, come potete vedere dal  reference [5],  il programma potrebbe crashare, quindi seguiamo il consiglio, compiliamo usando il Full RelRo, usiamo i flag -z relro -z now, in modo da usare il linking dinamico (vedi sopra): una volta risolti i simboli, il GOT viene messo in sola lettura in memoria, risolvedo così questo problema: il programma dell’esempio del reference genererà un segment fault, però saremo al sicuro da altre cose, come da una chiamata ad un’altra funzione non desiderata.

Come fa a sapere il loader del kernel quale caricamento deve effettuare? Vede se nell’ELF, la sezione DYNAMIC ha questi flag:

~/temp$ readelf -d a.out
Dynamic section at offset 0xf28 contains 20 entries:
0x00000018 (BIND_NOW)
0x6ffffffb (FLAGS_1) Flags: NOW

Faccio notare: un programma compilato con -static, cioè che non richiede l’uso di alcuna  shared libray, non contiene la sezione dynamic ne interp.

Riferimenti:
[1a] The ELF format – How programs look from the inside
[1b] PLT redirection through shared object injection into a running process
[2] Anatomy of Linux dynamic libraries
[3] Dynamic Linking and Loading
[4] Redirecting functions in shared ELF libraries
[5] RELRO: RELocation Read-Only

Approfondimenti:

>>How distros linux protect you: parte 2<<