Depășirea bufferului stivei - Stack buffer overflow

În software, o depășire a bufferului stivei sau depășirea bufferului stivei apare atunci când un program scrie pe o adresă de memorie din stiva de apel a programului în afara structurii de date intenționate, care este de obicei un buffer cu lungime fixă . Bug-urile de depășire a bufferului stivei sunt cauzate atunci când un program scrie mai multe date într-un buffer situat pe stivă decât ceea ce este de fapt alocat pentru acel buffer. Acest lucru duce aproape întotdeauna la corupția datelor adiacente din stivă și, în cazurile în care deversarea a fost declanșată din greșeală, va provoca deseori programul să se blocheze sau să funcționeze incorect. Stack buffer overflow este un tip de defecțiune de programare mai generală cunoscută sub numele de buffer overflow (sau buffer overrun). Supraumplerea unui tampon pe stivă este mai probabil să deraieze execuția programului decât supraumplerea unui tampon pe heap, deoarece stiva conține adresele de retur pentru toate apelurile de funcții active.

O depășire a bufferului stivei poate fi cauzată în mod deliberat ca parte a unui atac cunoscut sub numele de stricare a stivei . Dacă programul afectat rulează cu privilegii speciale sau acceptă date de la gazde de rețea neacredibile (de exemplu, un server web ), atunci eroarea este o vulnerabilitate de securitate potențială. Dacă bufferul stivei este umplut cu date furnizate de la un utilizator care nu are încredere, atunci acel utilizator poate corupe stiva în așa fel încât să injecteze cod executabil în programul care rulează și să preia controlul procesului. Aceasta este una dintre cele mai vechi și mai fiabile metode pentru atacatori de a obține acces neautorizat la un computer.

Exploatarea depășirilor tamponului stivei

Metoda canonică pentru exploatarea unui overflow de tampon bazat pe stivă este de a suprascrie adresa de returnare a funcției cu un pointer la date controlate de atacator (de obicei pe stiva în sine). Acest lucru este ilustrat cu strcpy()următorul exemplu:

#include <string.h>

void foo(char *bar)
{
   char c[12];

   strcpy(c, bar);  // no bounds checking
}

int main(int argc, char **argv)
{
   foo(argv[1]);
   return 0;
}

Acest cod preia un argument din linia de comandă și îl copiază într-o variabilă de stivă locală c. Acest lucru funcționează bine pentru argumentele din linia de comandă mai mici de 12 caractere (după cum puteți vedea în figura B de mai jos). Orice argumente mai mari de 11 caractere vor duce la corupția stivei. (Numărul maxim de caractere care este sigur este cu unul mai mic decât dimensiunea bufferului de aici, deoarece în limbajul de programare C, șirurile sunt terminate de un caracter de octet nul. O intrare de douăsprezece caractere necesită astfel stocarea a treisprezece octeți, intrarea urmată de către octetul zero santinelă. Octetul zero ajunge apoi să suprascrie o locație de memorie care este cu un octet dincolo de sfârșitul bufferului.)

Programul se stochează foo()cu diverse intrări:

Image
A. - Înainte de copierea datelor.
Image
B. - „salut” este primul argument din linia de comandă.
Image
C. - „AAAAAAAAAAAAAAAAAAAA \ x08 \ x35 \ xC0 \ x80” este primul argument din linia de comandă.

Observați în figura C de mai sus, când un argument mai mare de 11 octeți este furnizat pe linia de comandă foo()suprascrie datele stivei locale, indicatorul cadrului salvat și, cel mai important, adresa de returnare. Când foo()revine, apare adresa de returnare de pe stivă și sare la acea adresă (adică începe să execute instrucțiuni de la acea adresă). Astfel, atacatorul a suprascris adresa de returnare cu un pointer în memoria tampon de stivă char c[12], care conține acum date furnizate de atacator. Într-o stivă reală, buffer overflow exploatează șirul de „A” ar fi în schimb shellcode adecvat platformei și funcției dorite. Dacă acest program avea privilegii speciale (de ex. Bitul SUID setat să ruleze ca superutilizator ), atunci atacatorul ar putea folosi această vulnerabilitate pentru a obține privilegii de superutilizator pe mașina afectată.

Atacatorul poate modifica, de asemenea, valorile variabilei interne pentru a exploata unele bug-uri. Cu acest exemplu:

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

void foo(char *bar)
{
   float My_Float = 10.5; // Addr = 0x0023FF4C
   char  c[28];           // Addr = 0x0023FF30

   // Will print 10.500000
   printf("My Float value = %f\n", My_Float);

    /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       Memory map:
       @ : c allocated memory
       # : My_Float allocated memory

           *c                      *My_Float
       0x0023FF30                  0x0023FF4C
           |                           |
           @@@@@@@@@@@@@@@@@@@@@@@@@@@@#####
      foo("my string is too long !!!!! XXXXX");

   memcpy will put 0x1010C042 (little endian) in My_Float value.
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

   memcpy(c, bar, strlen(bar));  // no bounds checking...

   // Will print 96.031372
   printf("My Float value = %f\n", My_Float);
}

int main(int argc, char **argv)
{
   foo("my string is too long !!!!! \x10\x10\xc0\x42");
   return 0;
}

Diferențe legate de platformă

O serie de platforme au diferențe subtile în implementarea stivei de apeluri care pot afecta modul în care va funcționa un exploatare de depășire a bufferului stivei. Unele arhitecturi de mașini stochează adresa de returnare de nivel superior a stivei de apeluri într-un registru. Aceasta înseamnă că orice adresă de returnare suprascrisă nu va fi utilizată decât după o derulare ulterioară a stivei de apeluri. Un alt exemplu de detaliu specific mașinii care poate afecta alegerea tehnicilor de exploatare este faptul că majoritatea arhitecturilor de mașini în stil RISC nu vor permite accesul nealiniat la memorie. Combinată cu o lungime fixă ​​pentru opcodes-ul mașinii, această limitare a mașinii poate face saltul la tehnica ESP aproape imposibil de implementat (cu o singură excepție fiind atunci când programul conține de fapt codul improbabil pentru a trece în mod explicit la registrul stivei).

Stive care cresc

În cadrul depășirii bufferului stivei, o arhitectură adesea discutată, dar rar văzută, este una în care stiva crește în direcția opusă. Această modificare în arhitectură este sugerată frecvent ca o soluție la problema de depășire a tamponului de stivă, deoarece orice depășire a unui tampon de stivă care apare în același cadru de stivă nu poate suprascrie indicatorul de întoarcere. O investigație suplimentară a acestei protecții revendicate constată că este cel mai bine o soluție naivă. Orice depășire care apare într-un buffer dintr-un cadru de stivă anterior va suprascrie în continuare un indicator de returnare și va permite exploatarea rău intenționată a bug-ului. De exemplu, în exemplul de mai sus, indicatorul de întoarcere pentru foonu va fi suprascris, deoarece deversarea are loc de fapt în cadrul stivei pentru memcpy. Cu toate acestea, deoarece bufferul care se revarsă în timpul apelului se memcpyaflă într-un cadru de stivă anterior, indicatorul de întoarcere pentru memcpyva avea o adresă de memorie numeric mai mare decât bufferul. Aceasta înseamnă că, în loc să foofie suprascris indicatorul de întoarcere, indicatorul de întoarcere pentru memcpyva fi suprascris. Cel mult, aceasta înseamnă că creșterea stivei în direcția opusă va schimba unele detalii despre modul în care sunt exploatabile depășirile de tampon de stivă, dar nu va reduce semnificativ numărul de bug-uri exploatabile.

Scheme de protecție

De-a lungul anilor, au fost dezvoltate o serie de scheme de integritate a fluxului de control pentru a inhiba exploatarea rău intenționată a depășirii bufferului stivei. Acestea pot fi de obicei clasificate în trei categorii:

  • Detectați că s-a produs o depășire a bufferului stivei și astfel împiedicați redirecționarea indicatorului de instrucțiuni către codul rău intenționat.
  • Preveniți executarea codului rău intenționat din stivă fără a detecta direct depășirea bufferului stivei.
  • Randomizați spațiul de memorie astfel încât găsirea unui cod executabil să devină nesigur.

Stivați canarii

Canarii de stivă, denumiți pentru analogia lor cu un canar dintr-o mină de cărbune , sunt folosiți pentru a detecta un exces de tampon de stivă înainte de a se produce executarea unui cod rău intenționat. Această metodă funcționează prin plasarea unui număr întreg mic, a cărui valoare este aleasă la întâmplare la începutul programului, în memorie chiar înainte de indicatorul de returnare a stivei. Majoritatea depășirilor de tampon suprascriu memoria de la adresele de memorie mai mici la cele mai mari, astfel încât, pentru a suprascrie indicatorul de întoarcere (și astfel să preia controlul procesului), valoarea canarului trebuie de asemenea suprascrisă. Această valoare este verificată pentru a vă asigura că nu s-a schimbat înainte ca o rutină să utilizeze indicatorul de întoarcere din stivă. Această tehnică poate crește foarte mult dificultatea de a exploata o depășire a bufferului stivei, deoarece forțează atacatorul să obțină controlul indicatorului de instrucțiuni prin unele mijloace netradiționale, cum ar fi coruperea altor variabile importante din stivă.

Stivă neexecutabilă

O altă abordare pentru prevenirea exploatării depășirii bufferului stivei este aplicarea unei politici de memorie în regiunea de memorie a stivei care interzice executarea din stivă ( W ^ X , "Write XOR Execute"). Aceasta înseamnă că, pentru a executa shellcode din stivă, un atacator trebuie fie să găsească o modalitate de a dezactiva protecția de execuție din memorie, fie să găsească o modalitate de a pune sarcina utilă a shellcode-ului într-o regiune de memorie neprotejată. Această metodă devine din ce în ce mai populară acum, deoarece suportul hardware pentru semnalizarea fără executare este disponibil în majoritatea procesoarelor desktop.

În timp ce această metodă face cu siguranță că abordarea canonică a exploatării depășirii bufferului eșuează, nu este lipsită de probleme. În primul rând, este obișnuit să găsiți modalități de stocare a codului de shell în regiuni de memorie neprotejate, cum ar fi heap-ul, și, prin urmare, foarte puțin trebuie să se schimbe modul de exploatare.

Chiar dacă nu ar fi așa, există și alte căi. Cea mai blestematoare este așa-numita metodă return to libc pentru crearea shellcode-ului. În acest atac, sarcina utilă rău intenționată va încărca stiva nu cu shellcode, ci cu o stivă de apeluri corespunzătoare, astfel încât execuția să fie vectorizată într-un lanț de apeluri de bibliotecă standard, de obicei cu efectul de a dezactiva protecția executării memoriei și a permite shellcode-ului să ruleze normal. Acest lucru funcționează deoarece execuția nu vectorează de fapt niciodată stiva.

O variantă a return-to-libc este programarea orientată pe returnare (ROP), care stabilește o serie de adrese de retur, fiecare dintre care execută o mică secvență de instrucțiuni de mașină culese în cadrul programului sau bibliotecilor de sistem existente, secvență care se încheie cu o întoarcere. Aceste așa-numite gadgeturi realizează fiecare o manipulare simplă a registrului sau o execuție similară înainte de a se întoarce, iar înșirarea lor atinge scopurile atacatorului. Este chiar posibil să se utilizeze programarea orientată pe returnare „returnless” prin exploatarea instrucțiunilor sau a grupurilor de instrucțiuni care se comportă la fel ca o instrucțiune return.

Randomizare

În loc să separe codul de date, o altă tehnică de atenuare este introducerea randomizării în spațiul de memorie al programului de executare. Deoarece atacatorul trebuie să stabilească unde se află codul executabil care poate fi utilizat, fie este furnizată o sarcină utilă executabilă (cu o stivă executabilă), fie una este construită folosind reutilizarea codului, cum ar fi ret2libc sau programare orientată pe retur (ROP). Randomizarea aspectului de memorie, ca concept, va împiedica atacatorul să știe unde este orice cod. Cu toate acestea, implementările de obicei nu vor randomiza totul; de obicei executabilul în sine este încărcat la o adresă fixă ​​și, prin urmare, chiar și atunci când ASLR (randomization layout layout randomization) este combinat cu o stivă neexecutabilă, atacatorul poate folosi această regiune fixă ​​de memorie. Prin urmare, toate programele ar trebui să fie compilate cu PIE (executabile independente de poziție) astfel încât chiar și această regiune a memoriei să fie randomizată. Entropia randomizării este diferită de la implementare la implementare și o entropie suficient de scăzută poate fi în sine o problemă în ceea ce privește forțarea brută a spațiului de memorie care este randomizat.

Exemple notabile

  • Viermele Morris în 1988 răspândit în parte prin exploatarea unui buffer overflow stivă în Unix degetul server. [1]
  • Viermele Slammer în 2003 răspândirea prin exploatarea unui buffer overflow stivă în Microsoft serverului SQL. [2]
  • Blaster Worm în 2003 răspândirea prin exploatarea unui buffer overflow stivă în Microsoft DCOM serviciu.
  • Viermele Witty în 2004 răspândirea prin exploatarea unui buffer overflow stivă în Internet Security Systems BlackICE Agent Desktop. [3]
  • Există câteva exemple de Wii care permit codul arbitrar să fie rulat pe un sistem nemodificat. „Twilight hack”, care implică acordarea unui nume lung calului personajului principal în The Legend of Zelda: Twilight Princess și „Smash Stack” pentru Super Smash Bros. Brawl, care implică utilizarea unui card SD pentru a încărca un fișier special pregătit în editor la nivel de joc. Deși ambele pot fi utilizate pentru a executa orice cod arbitrar, acesta din urmă este adesea folosit pentru a reîncărca Brawl în sine cu modificările aplicate.

Vezi si

Referințe