Błąd Buffer Overflow... w szczegółach

Author
Krzysztof Cabaj
blog_date_icon
11 lipca 2010

Chyba każda osoba interesująca się sieciami komputerowymi i ich bezpieczeństwem zetknęła się z atakiem typu „Buffer overflow”. Idea działania większości też jest znana, jednak kojarzy się z jakimiś niesamowitymi sztuczkami. W tym artykule chcę przybliżyć, jak w szczegółach wygląda podatny kod oraz jak trzeba spreparować specjalny ciąg wejściowy, aby doprowadzić to ataku.

Rozpatrzmy prostą funkcję zaprezentowaną poniżej:

void VulnFunc(char *ptr)
{
  char buffer[32];
  int i;

  for(i=0;(*ptr)!='&';i++)
    buffer[i]=*(ptr++);

  //dalsze instrukcje działające na buforze, nie zmieniające jego
  //zawartości, przykładowo liczące wartość skrótu MD5 z zawartości
  //bufora, nieistotne jeśli chodzi o atak buffer overflow
}

Możemy łatwo wyobrazić sobie i podać zadanie tej funkcji, która pobiera pewne dane, kopiuje je do bufora oraz wykonuje z jego pomocą pewne operacje. Łatwo możemy wskazać widoczny od razu błąd: dane kopiujemy do bufora bez sprawdzania, czy go nie przepełnimy. Warunkiem zakończenia kopiowania jest natrafienie na znak &. Właśnie taki błąd, o ile możemy podać dowolne dane wejściowe, może zostać wykorzystany.

Nim przejdziemy do preparowania odpowiedniego ciągu znaków będących exploitem obejrzyjmy stos podczas normalnego wywołania funkcji, zaprezentowany na rysunku nr 1.

rys1.webp

W lewej części rysunku przedstawiona jest zawartość pamięci wykorzystywanej na stos. Wyróżnione na czerwono obszary to zmienna i oraz bufor, z przepisaną zawartością, w tym przypadku tekstem „Ala”. Wyróżniony na niebiesko fragment to wskaźnik ptr przekazywany jako parametr. Wszystkie te informacje można skonfrontować z zawartością zmiennych podczas wykonania programu zaprezentowanych po prawej stronie rysunku.

Wróćmy na chwilę do wyróżnionego na niebiesko adresu, będącego parametrem. W takim przypadku nasze zainteresowanie powinny dotyczyć 32 bitowy adres znajdujący się bezpośrednio przed nim. Adres ten, jest adresem powrotu z funkcji. Jeśli uda nam się go zmienić, w momencie powrotu z funkcji … wykonamy inny kod, nieprzewidziany przez programistę.

Aby prześledzić taki przypadek, rozpatrzmy sytuację, kiedy do funkcji zostanie podany ciąg znaków postaci (liczby hexadecymalne):

0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x83,0xec,0x80,0xbe, 0xe6,0x10,0x41,0x00,0x68,0x41,0x6c,0x61,0x00,0x8b,0xdc,0x6a,0x40,0x53, 0x53,0x33,0xc0,0x50,0xff,0xd6,0xff,0x15,0xe6,0x6c,0x41,0x00,0x68, 0xfd,0x12,0x00,0x26

... i podobnie jak w poprzednim wywołaniu funkcji prześledźmy zawartość stosu.

rys2.webp

Na zaprezentowanym rysunku możemy zaobserwować, że do bufora zostało przepisane dużo więcej danych niż poprzednio. Dokładnie 44 bajty (wartość hexadecymalna 2c w obszarze pamięci zmiennej i). Wartość ta jest większa niż zadeklarowany na 32 bajty bufor. Jednak co najważniejsze, czy - powinniśmy powiedzieć - najgroźniejsze, został nadpisany adres powrotu z funkcji (fragment wyróżniony na niebiesko), gdzie teraz znajdują się instrukcje, które zostaną wykonane przez program. Pamiętając, iż zmienne na maszynach z procesorem Intel są przechowywane „od końca”, możemy odkodować adres powrotu. Skok nastąpi do instrukcji znajdującej się pod adresem 0x0012fd68. Jeśli przyjrzymy się dokładnie obszarowi pamięci stosu, możemy zauważyć, że skoczymy w zaprezentowany na rysunku obszar pamięci. Dokładniej wykonanie nowego kodu zaczniemy od instrukcji o kodzie 0x90. Dalej możemy zaobserwować jeszcze kilka instrukcji o tym samym kodzie. Taka sekwencja jest często spotykana w exploitach. Kod 0x90 ma instrukcja NOP – no operation. Instrukcja, która nic nie robi, ale ma długość jednego bajta. Taka sekwencją zwana poduszeczką NOP ( jest przydatna, jeśli okazałoby się, iż nie trafimy dokładnie w początek kodu exploita). Dalszą analizę tego kodu zostawmy na później … i zobaczmy co się stanie, jak wykonamy ten kod. Wyniki przedstawia rysunek.

rys3.webp

Co robi kod na stosie ... trochę assemblera

Znaczenie instrukcji o kodzie 0x90 zostało już wyjaśnione wcześniej, teraz przyjrzyjmy się reszcie kodu. . . .

0x90nop
0x83,0xec, 0x80,sub esp,0x80//zrobienie miejsca na stosie
0xbe,0xe6, 0x10, 0x41, 0x00mov esi, <adres>
0x68,0x41, 0x6c, 0x61, 0x00push <dane>
0x8b,0xdcmov ebx,esp
0x6a,0x40push 0x40
0x53push ebx
0x53push ebx
0x33,0xc0xor eax, eax
0x50push eax
0xff,0xd6call esi
0xff,0x15, 0xe6, 0x6c, 0x41, 0x00call <adres>
0x68,0xfd, 0x12,0x00adres nadpisujacy adres powrotu
0x26znak &
Co robi ten kod? Pierwsza instrukcja odejmuje 128 od rejestru esp – stack pointer, robiąc miejsce na stosie dla dalszego kodu. W dalszej części przypisujemy do rejestru esi adres funkcji MessageBox. Później odkładamy 5 wartości na stos, najpierw pewne dane, które już wcześniej widzieliśmy na stosie (co dokładnie … zostawiam czytelnikowi do zgadnięcia). Nim odłożymy kolejne 4 wartości zapamiętujemy zawartość rejestru esp w rejestrze ebx. Adres ten odpowiada początkowi stosu, czyli aktualnie danym załadowanym na stos przed chwilą. W dalszej części odkładamy na stos liczbę 0x40, dwa razy wskaźnik wskazujący na pewne dane znajdujące się na stosie oraz wartość 0. Potem dokonujemy wywołania funkcji MessageBoxA. Co robiliśmy wcześniej na stosie? – odkładaliśmy parametry – handle do okna (0 – NULL, czyli okno bez powiązania z innym oknem), dalej dwa razy wskaźnik na zawartość okna i jego tytuł (czy już wiadomo jakie dane były wkładane na stos? ;) ) i na końcu typ okna, w naszym przypadku okno informacyjne. Na uwagę zasługuje jeszcze ostatnia wartość – 0x26 czyli w reprezentacji ASCII znak & - warunek stopu pętli kopiującej dane do bufora.

Oczywiście zaprezentowany w tym przykładzie kod nie robi nic groźnego, jedynie wyświetla okienko. Jednak mając więcej miejsca na stosie, można spreparować naprawdę groźną funkcję. Ale to już temat na kolejny wpis ... ;)

Kilka wyjaśnień

Wykorzystywane w przykładowym kodzie exploita adresy zostały wcześniej odkryte poprzez analizę kodu działającego programu za pomocą debuggera. Architektura systemu Windows (no może z wyjątkiem Visty i dalszych) powoduje, że w tym samym środowisku (te same zainstalowane programy) wykonujący się program zawsze będzie posiadał te same adresy funkcji oraz stosu. Dzięki temu w środowisku testowym można je poznać … i wykorzystywać do ataku na inne identyczne lub podobne (patrz poduszeczkę NOP) systemy. Analogicznie ma się sprawa z adresem wykorzystywanej funkcji MessageBoxA.