Jump to content
Ganav

[RST] Protejarea executabilelor impotriva atacurilor de tip stack buffer overflow

Recommended Posts

In acest tutorial voi ilustra cum pot fi prevenite atacurile de tip stack buffer overflow si heap buffer overflow pe platformele LINUX x86-64 fara editarea fisierelor sursa(presupunem ca toate aplicatiile pe care dorim sa le securizam sunt open source).

Cuprins:

  1. Tipuri de fisiere obiect(binare) in LINUX
  2. Structura fisierelor ELF
  3. Imaginea stivei in memorie
  4. Ce sunt atacurile de tip stackbuffer overflow?(breviar teoretic)
  5. Mijloace curente de prevenire a acestora. Setari implicite(default)
  6. Recompilarea aplicatiilor tinta

1. Tipuri de fisiere obiect(binare) in LINUX

In LINUX sunt trei tipuri de fisiere obiect:

  1. Un fisier relocabil(relocatable) contine cod si date pentru legarea(linking) cu alte fisiere obiect pentru a crea fisiere partajate(shared object) sau fisiere executabile
  2. Fisiere executabile. Acestea contin programe(in cod masina) care pot fi rulate direct. Fisierul specifica cum exec creaza imaginea procesului
  3. Un fisier obiect partajat(shared object file) contine cod si date ce pot fi legate(linked) in doua contexte. In primul editorul ld il poate procesa impreuna cu un numar de fisiere relocabile si fisiere de tip obiecte partajate(shared object files) pentru a crea un alt fisiere de tip obiect partajat. In celalalt context, linker-ul dinamic(dynamic linker) il combina impreuna cu un fisier executabil si alte fisiere de tip obiecte partajate pentru a crea imaginea unui proces.

Diferenta intre fisierele de tip obiect partajat dinamic(dynamic shared object, aceastea se termina cu extensia .so) si cele de tip

obiect partajat static(static shared object, acestea se termina cu extensia .a) este ca in primul caz functiile care se gasesc in fisierul .so nu sunt incluse in executabilul final si acestea sunt rezolvate la executie(runtime) in felul urmator:

  • La inceputul imaginii executabilului din memorie se gaseste un fragment de memorie numit procedure linking table(sau PLT). Imaginea executabilului este obtinuta prin incarcarea in memorie de catre sistemul de operare(prin apelul catre functia exec) a fisierului binar de pe hard disk. Imaginea depinde de antetul fisierului ELF despre care vom vorbi in sectiunile urmatoare.
    Tabela PLT contine intrari(instructiuni de tip jump care modifica pointer-ul de instructiuni(adica punctul in care suntem la un moment dat in executia programului) sa arate catre intrari din GOT) catre proceduri/functii ale caror adrese nu sunt cunoscute in momentul in care se face link-area(prin link-are se intelege legarea definitiei unei proceduri/functii de implementarea sa. In cazul fisierelor obiect partajat dinamic(dynamic shared object) aceasta se gaseste intr-un fisier diferit fata de cel din care a fost chemata). Link-area se realizeaza de catre linker-ul dinamic in momentul executiei(run-time).
    GOT este o abreviere pentru Global Offset Table si este folosit, de asemenea, pentru rezolvarea adreselor procedurilor in timpul rularii.
    Mai exact sa presupunem ca avem urmatoarele linii de cod:

    #include <stdio.h>

    void main(void) {
    int a = 1, b = 2;
    printf("%d %d", a, ;
    }


    Implementarea functiei printf nu se gaseste in programul de mai sus. Se obtine in timpul rularii prin link-area dinamic cu libc(libc este un fisiere .so care este incarcat in memorie la boot-are) in felul urmator. Pointer-ul de instructiuni(acesta este memorat intr-un registru(eip(pe platforme x86-64)) si indica catre instructiunea care se executa la un moment dat) va arata catre o instructiune din zona .text(codul programelor si anume al instructiunilor se gaseste aici) care apeleaza printf in felul urmator:


    call printf # acesta este cod masina(assembly) si reprezinta apelul catre functia printf din programul nostru


    In cazul nostru nu cunoastem adresa functiei printf in memorie. Acum call printf face ca pointer-ul de instructiuni sa sara catre o intrare din procedure linking table(PLT). Inca o data pointer-ul de instructiuni ne arata instructiunea care se executa in momentul de fata din programul nostru(practic este un reper in cadrul programului; ne arata unde suntem; ca si o analogie pointer-ul poate fi comparat cu un vehicul care "merge" prin program. Instructiunile deja executate reprezinta drumul parcurs)
    Acum suntem in PLT la o adresa care corespunde functiei printf. Aici intalnim urmatorul set de instructiuni:


    jmp [printf] # aceasta linie "sare", muta pointer-ul de instructiuni, in GOT. Acum obtinem adresa din GOT. De ex. 0x7fff400
    push 0 # aceasta linie urca un argument pe stiva necesar pentru dlresolv
    jmp dlresolv # aceasta linie apeleaza utilitarul dlresolv care mapeaza adresa din GOT cu adresa functiei printf din libc


    dlresolve obtine adresa functiei printf din fisierul partajat obiect libc dupa care o plaseaza in GOT. Acum programul nostru stie unde se gaseste implementarea functiei printf in memorie. O data ce se realizeaza aceasta corespondenta(functie - adresa implementare) dlresolve nu mai trebuie apelat ulterior pentru aceiasi functie. Adica daca am mai avea un apel la printf l-am gasi direct in GOT de aceasta data.
    Un exemplu grafic se gaseste mai jos:

2. Structura fisierelor ELF

Fisierele de tip ELF contin urmatoarele sectiuni:

.text: aceasta contine totalitatea instructiunilor programului

.symtab: tabela de simboluri(importata si exportata)

.rel*: adrese de relocare(unde sunt simbolurile folosite)

.data and .data1: date initializate

.rodata and .rodata1: date initializate de tip read only(valoarea acestora nu poate fi modificata)

.debug: informatii simbolice de depanare

Mai jos se gasesc cateva ilustrari grafice:

Tabela GOT se afla in zona de inceput a executabilului:

Modul in care se identifica si mapeaza adresa functiilor din fisierele dinamice partajate se poate observa mai jos:

Detalii despre antetul fisierelor ELF se gasesc aici:

Executable and Linkable Format - Wikipedia, the free encyclopedia

3. Imaginea stivei in memorie

Inainte de a vorbi despre structura stivei voi explica intai rolul acesteia. Initial programele erau scrise in totalitate in limbaj de asamblare. Apelurile catre functii se realizau populand regsitrii apriori cu argumente necesare acestora. In timp, o data cu cresterea complexitatii aplicatiilor software, numarul de registrii disponibili nu mai era suficient pentru apelarea functiilor complexe ce presupuneau un numar ridicat de paramentri(argumente). In consecinta s-a gandit ca o zona de memorie structurata ar putea deservi acestui scop. Astfel, s-a nascut stiva. Aceasta este o structura de tip FIFO(first in first out)

pe care sunt stocate, in aceasta ordine, parametrii functiei, adresa de revenire(pointer-ul de instructiuni catre urmatoarea instructiune de apelat dupa functie) si variabile locale functiilor.

Stiva functioneaza in felul urmator: la apelul unei functii se salveaza pointer-ul de baza(acesta arata catre fundul stivei), dupa aceea salvam valoarea varfului stivei in pointer-ul de baza iar in pasul final alocam memorie varaiabilelor locale urcand varful stivei cu un anumit numar de bytes.


pushl %ebp # salvam baza stivei [B]ebp[/B]
movl %esp, %ebp # initializam valoarea bazei cu valoarea varfului(ponter-ului de varf [B]esp[/B])
subl $n,%esp # alocam spatiu pentru variabile locale(urcam valoarea pointer-ului stivei)

La sfarsitul apelului eliberam stiva:


movl %ebp, %esp # salvam valoarea pointer-ului de baza in pointer-ul de varf(coboram in stiva)
popl %ebp # scoatem valoarea intiala a bazei
ret # rezumam executia cu instructiunea imediat urmatoare apelului functiei

4. ]Ce sunt atacurile de tip stackbuffer overflow?(breviar teoretic)

Atacurile de tip buffer overflow se bazeaza, in principiu, pe lipsa de validare a lungimii vectorilor in limbaje precum C si C++. Cu alte cuvinte un atacator ar putea trimite un sir de octeti(bytes) care sa suprascrie adresa de revenire din functie(acel ret de mai sus) cu o adresa care arata catre o alta functie/instructiune decat cea asteptata in mod normal. Astfel, firul executiei este modificat. De exemplu, in trecut, un atacator incerca sa obtina un apel:


system("/bin/bash");

care crea un shell pe statia respectiva prin intermediul caruia puteau fi rulate comenzi primite de la atacator.

5. Mijloace curente de prevenire a acestora. Setari implicite(default)

In prezent sunt prezente mijloace de prevenire a executiei de cod in procese prin intermediul suprascrierii adresei de revenire. Mijloacele folosite astazi sunt:

  1. NX memory. Cu alte cuvinte stiva nu mai este executabila. Chiar daca un atacator reuseste sa urce un shellcode acesta va genera o exceptie(si in cele mai frecvente cazuri o esuare(crash) a procesului respectiv).
  2. ASLR care este o abreviere pentru address space layout randomization. Aceasta tehnica se bazeaza pe arbitrarea zonelor de memorie in care este incarcat programul si a adreselor functiilor/procedurilor acestuia. La fiecare repornire a programului acestea pot fi diferite in functie de implementare
  3. Canarries. Acestea sunt secvente de bytes situate pe stiva intre instructiunile functiei si variabilele locale(acestea includ si buffer-ele). Astfel, daca un atacator modifica valoarea canarries-urilor se va genera o exceptie iar programul este oprit. La repornire valoarea acestora poate fi modificata sau nu depinzand de implementare.
  4. PIE care este o abreviere pentru Position Independent Executables. Acesta tehnica se bazeaza pe plasarea programelor la adrese arbitrare in memorie(ASLR depinde de PIE)

In mod implicit PIE nu este folosita.

6. Recompilarea aplicatiilor tinta

Am petrecut ieri putin timp pentru a recompila firefox cu optiunea -pie setata(in gcc). Tehnica urmatoare se poate aplica tuturor

aplicatiilor de tip opensource(cele care se compileaza apeland configure, make si (sudo)make install.

Descarcam intai fisierele sursa. Acestea se gasesc aici:

ftp://ftp.mozilla.org/pub/mozilla.org/firefox/releases/

Pentru a dezarhiva sursele rulam:


tar -xjf <source-file.tar.bz2>

Acum navigam in directorul creat dupa dezarhivare si cream un fisier .mozconf In el includem urmatoarele:


# Start .mozconfig
mk_add_options MOZ_OBJDIR=../obj
# Pe cate core-uri are loc compilarea
mk_add_options MOZ_MAKE_FLAGS="-j6"
ac_add_options --disable-debug
ac_add_options --disable-tests
ac_add_options --enable-strip
ac_add_options --disable-installer
ac_add_options --enable-optimize="-march=corei7 -mavx -mpclmul -mfpmath=sse+387 -pie -fPIC"


mk_add_options MOZ_OBJDIR=../obj

ne spune unde se va crea executabilul.

Linia:


ac_add_options --enable-optimize="-march=corei7 -mavx -mpclmul -mfpmath=sse+387 -pie -fPIC"

contine parametrii de optimizare. Observam aici si argumentul -pie. -march ne da arhitectura pentru care dorim sa optimizam browser-ul. Folositi -march=native daca nu sunteti siguri de arhitectura pe care o detineti.

Aceasta se poate obtine cu:


uname -m

Acum avem toate fanioanele pentru optimizare setate si de asemenea compilarea se va face cu PIE. Pentru a porni procesul de compilare rulam:


make -f client.mk build MOZ_CURRENT_PROJECT=browser

Daca apar erori sau doriti sa compilati cu alte optiuni stergeti tot ce se gaseste in directorul destinatie(in acest caz acesta este ../obj astfel:


rm -a ../obj/* # argumentul a sterge toate fisierele(si symlink-urile)

si rulati din nou comanda de compilare. La sfarsit va veti putea bucura de un browser mai sigur si mai rapid. Tehnica este aplicabila oricarei aplicatii scrise in C/C++(asta include si kernel-ul).

  • Upvote 2
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...