NFsec Logo

Blokowanie b0f dzięki Libsafe

10/04/2005 w Bezpieczeństwo Brak komentarzy.  (artykuł nr 10, ilość słów: 2252)

B

łędy typu buffer overlow (b0f) należą do błędów implementacyjnych – ponieważ są one nieuchronne. Wynika to z faktu, że żadna aplikacja z reguły nie może zostać przetestowana w sposób wyczerpujący. Błędy b0f były wykorzystywane od 1988 roku, kiedy pojawił się pierwszy internetowy robak Morris Worm, atakujący na całym świecie serwery Uniksowe na platformie VAX i Sun, działające pod kontrolą systemu BSD. Robak wykorzystywał m.in. błąd w usłudze finger w celu nadpisania bufora, zawierającego nazwę pliku lokalnego klienta finger. Autorem owego robaka był wówczas 23 letni Robert Tappan Morris uczęszczający na studia doktoranckie na wydziale informatyki Uniwersytetu Cornell (posiadał stopień magistra Uniwersytetu Harward). Lubił on wykrywać błędy w systemach operacyjnych, dlatego na jesieni 1988 roku zaczął pracę nad programem mającym na celu ukazanie wykrytych błędów w zabezpieczeniach systemu 4 BSD Unix.

Program po “wpuszczeniu” w sieć miał wykazać możliwość uzyskania dostępu do dowolnego innego komputera i np. zainfekowania go wirusem. Robak (ang. worm) jak nazwano później program Morrisa, miał mniej niż 100 linii kodu. A jednak Robert pisząc ten program popełnił drobny błąd, który kosztował go bardzo wiele. Robak po uruchomieniu zajmował bardzo mało czasu procesora. Miał on pracować w ukryciu, być niezauważalny nawet dla administratora i nie powodować żadnych problemów. Morrisowi chodziło jedynie o udowodnienie, że może on się przenosić z komputera na komputer. Rozmnażanie robaka możliwe było poprzez skorzystanie z błędów w kilku elementach systemu UNIX oraz poprzez odkrywanie łatwych do odgadnięcia haseł użytkowników. Morris przewidział, że nieskończone rozmnażanie się robaka spowoduje zablokowanie komputerów. Dlatego robak “pytał się”, czy infekowany serwer zawiera już kopie programu, czy nie. To jednak mogłoby ułatwić administratorom pozbycie się programu, więc Robert zdecydował się, że mimo odpowiedzi “Tak” w jednym na siedem przypadków robak i tak się uruchamiał na komputerze jako nowy proces. Miało to ograniczyć prędkość rozmnażania się programu i uniknąć blokady systemu. Obliczenia Morrisa okazały się jednak błędne. Niecierpliwy informatyk zdecydował się wprowadzić program do Internetu 2 listopada ok. godz. 20:00, mimo iż wymagał on jeszcze kilku poprawek. Aby ukryć fakt, że program jest jego autorstwa, uruchomił go z jednego z serwerów na uniwersytecie Harvard. W ok. 2 godziny od uruchomienia programu do większości maszyn nie mogli się dostać nawet administratorzy. Nie pomagało ich ponowne uruchomienie, robak znów infekował system i powodował jego zawieszanie. Kiedy Morris zorientował się, co się dzieje, przy pomocy kolegi przygotował rozwiązanie problemu i rozesłał anonimowo po sieci. Niestety, było już za późno. Robak uniemożliwił w większości przypadków odebranie i przeczytanie listu. Nad zwalczeniem złośliwego programu pracowali specjaliści z tysięcy placówek w USA. Już w 12 godzin od infekcji informatycy z Uniwersytetu Kalifornijskiego w Berkeley i z Massachussetts Institute of Technology odkryli metodę likwidacji robaka. Ze względu na odłączenie wielu komputerów klasy Sun 3 i VAX. Straty w każdej lokalizacji sięgały nierzadko 50 tys. USD, a łącznie oceniono je na ok. 10 mln USD.

   Nawet w dzisiejszych czasach wiele ze znanych ataków przeciwko poprawnie skonfigurowanym systemom linuksowym powiodło się dzięki zastosowaniu metody przepełnienia bufora. Jej odkrycie umożliwiło gwałtowny rozwój na skalę globalną exploitów (ang. to exploit – wykorzystywać). W skrócie można powiedzieć, że ta technika polega na przepełnieniu dynamicznie zaalokowanego bufora (miejsce na dane przydzielane podczas wykonywania programu). Włamywacz po prostu atakuje program, który zawiera błąd uniemożliwiający poprawne ograniczenie ilości danych wejściowych w stosunku do przydzielonego bufora. Bufor ulega przepełnieniu wówczas, gdy umieszczone w nim dane nie mieszczą się w przydzielonym mu obszarze pamięci. Na takie gospodarowanie pamięcią, prowadzące do przepełnienia bufora, pozwala konstrukcja języków opartych na C. Cel ataku wybierany jest poprzez dokonywanie audytów bezpieczeństwa, czyli dogłębną analizę kodu źródłowego, użytkowanie oprogramowania lub użycia metody prób i błędów.

  Klasycznym błędem jest użycie procedury, która nie potrafi dopasować ilości danych do wielkości bufora. Wtedy zwykle program C deklaruje użycie automatycznych buforów. W tym celu stosowana jest domyślna klasa dla zmiennych zdeklarowanych wewnątrz funkcji. Takie zmienne są umieszczane na szczycie stosu (który służy między innymi do przekazywania parametrów do funkcji i do tworzenia zmiennych lokalnych funkcji). Powyżej lokalnego obszaru dla aktualnie wykonywanej funkcji znajduje się adres powrotny procedury, która ją wywołała. Agresor musi jedynie umieścić w buforze kilka bajtów, które stanowią rozkaz maszynowy umożliwiający przejęcie kontroli. Najczęściej jest to utworzenie powłoki set-UID lub ustawienie bitu set-UID dla innego programu. Aby to było możliwe należy wcześniej obliczyć liczbę bajtów znajdujących się pomiędzy końcem bieżącego bufora i adresem powrotnym, a następnie zapisać bajty pośrednie oraz adres początku bufora w słowie, które zawiera adres zwrotny dla funkcji wywołującej. Kiedy procedura spróbuje wykonać powrót, przejdzie do podstawionego kodu włamywacza i wykona wszystkie żądane przez niego operacje.

|--- bufor ---|--- dalsze elementy stosu ---|
|--- p r z e p e ł n i e n i e ---|
|             |      |
|             |      |- adres powrotu z funkcji
|             |           
|             |- koniec bufora
|
|- początek bufora

Mówiąc prościej: jeśli bufor nie jest w stanie pomieścić zbyt dużej ilości danych nadpisują one dane mieszczące się za buforem. W większości programów za buforem znajduje się adres odłożony przez stos przez wywołanie instrukcji procesora call (która umożliwia wywołanie funkcji i służy do przekazywania sterowania do innego fragmentu kodu z równoczesnym zapisaniem adresu powrotnego). Po nadpisaniu wartości na stosie odpowiednio dobranym adresem wskazującym na specjalnie spreparowany kod, program po zakończeniu aktualnie wykonywanej funkcji zaczyna wykonywać specjalnie przygotowany kod wywołujący np. set-UID shell. Jeżeli program, w którym został nadpisany bufor działał z uprawnieniami użytkownika root, włamywacz uzyska kontrolę nad systemem (dlatego zawsze powinno się dążyć do uruchamiania usług na serwerze z uprawnieniami zwykłego użytkownika). Należy podkreślić, że ataki typu b0f stanowią około połowy udanych włamań do systemów linuksowych.

  Poniżej zamieszczam przykładowy fragment kodu, który zawiera problem przepełnienia bufora. W tym przykładzie przedstawiono typowy sposób programowania, który nie pozwala zabezpieczyć programu przed błędnymi danymi. Nawet średniej klasy programista nie będzie miał żadnych trudności ze złamaniem takiego kodu:

void funkcja ( char *p) {
char buf [1024];
strcpy ( buf , p);
return ;
}

W powyższym przykładzie, tuż za buforem znajduje się zapisana wartość %EBP (rozszerzony wskaźnik bazy – rejestr niezmienny, pozostaje taki nawet podczas wykonywania funkcji, służy jako odnośnik do zmiennych alokowanych na stosie, praktycznie zawsze odnosi się do stosu bieżącej funkcji), a następnie zapisana wartość %EIP (rozszerzony wskaźnik instrukcji – zawartość tego rejestru wskazuje adres następnej instrukcji do wykonywania, jest składany na dnie stosu). Wystarczy więc nadpisać %EIP, tak aby przy powrocie, funkcja “wróciła” do kawałka kodu, który np. uruchomi spreparowaną powłokę. Nie każde wprowadzenie do bufora zbyt dużych danych spowoduje nadpisanie %EIP i adres powrotu. Aby precyzyjnie “trafić” w adres powrotu, należałoby znać dokładnie wielkość stosu. W naszym przypadku łańcuch będzie składać się z 1028 bajtów “śmieci” oraz 4 bajtów adresu, pod którym umieszczamy kod do wywołania. W innym przypadku napastnik zdany jest na strzelanie na chybił trafił. Może więc udać mu się nadpisanie innych zmiennych lokalnych (jeśli wprowadzone dane są tylko trochę większe niż rozmiar bufora), rejestru %EBP (jeśli są dużo większe) itd. W sumie napastnikowi zależy głównie na nadpisaniu %EIP i wykonaniu instrukcji procesora ret (służącej do powrotu z wywołanej funkcji i kontynuacji przetwarzania programu od instrukcji następującej bezpośrednio po odpowiedniej instrukcji call). Pobrana zostanie wtedy nadpisana wartość, a procesor wykona skok na nieprawidłowy adres.

     W Internecie można znaleźć wiele poprawek jądra Linuksa, które uniemożliwiają programom wykonywanie kodu znajdującego się w obszarze stosu (np. Openwall Solar Designer’a). Zastosowane takich poprawek jądra pozwoli ochronić się przed wieloma, lecz nie wszystkimi atakami przepełnienia bufora. Przed większością typowych ataków przepełnienia bufora można w prosty sposób zabezpieczyć się poprzez użycie Libsafe.

     Libsafe jest biblioteką powstałą jako projekt Avaya Labs. Biblioteka ta przeznaczona jest dla systemów Linuksowych (dostępna jest na zasadach licencji GNU Library General Public License). Jej innowacyjne rozwiązanie umożliwia wykrywanie i radzenie sobie z atakami przepełnienia bufora oraz tymi wykorzystującymi ciągi formatujące (które obecnie są najczęściej stosowanymi technikami). Jedną z największych zalet Libsafe jest wyjątkowa prostota instalacji i użycia. Dołączana jest do programów w sposób dynamiczny, dzięki czemu nie ma potrzeby ponownej kompilacji jakiegokolwiek programu w systemie (biblioteka nie wspiera programów linkowanych ze starszą wersją libc5).

Według autorów biblioteki Libsafe – badaczy zatrudnionych w laboratorium Murray Hill Bell Labs (to właśnie tu powstał UNIX) – połowa metod ataku zgłaszanych do CERT w latach od 1997 do 1999 była spowodowana problemami z przepełnieniem bufora. Libsafe pozwala powstrzymać większość takich ataków, które są spowodowane przepełnieniem bufora na stosie w przypadku dynamicznie konsolidowanych programów. W przeciwieństwie do wielu innych rozwiązań, Libsafe zadziała skutecznie nawet w przypadku nie znanych jeszcze problemów. Oznacza to, że administrator nie musi się martwić o to, które programy wymagają zabezpieczenia. Wpływ Libsafe na ogólną wydajność systemu jest dość niewielki, dzięki bardzo skutecznemu mechanizmowi działania. Zastosowana w Libsafe metoda działa w sposób w pełni transparentny dla aplikacji. Biblioteka jest wywoływana automatycznie przechwytując wywołania funkcji C, które powszechnie uznawane są za niebezpieczne [ jak np. gets(), strcpy(), getwd(), scanf() i sprintf() ]. Sama ustala, czy adres docelowy znajduje się na stosie, a także czy operacja zapisu nie spowoduje nadpisania danych znajdujących się poza bieżącą ramką stosu. W takich przypadkach operacja jest blokowana.

W celu ukazania jak Libsafe jest wprowadzane do użytku przez programy, przed instalacją możemy przeprowadzić pewien test. Za pomocą polecenia: ldd /bin/login sprawdzamy z jakie biblioteki są wykorzystywane do tego programu:

libcrypt.so.1 => /lib/libcrypt.so.1 (0x4001d000)
libc.so.6 => /lib/libc.so.6 (0x4004a000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 

Zapamiętując ten stan przechodzimy do instalacji Libsafe. W dystrybucji Slackware – Libsafe znajduje się w sekcji Extra. Dlatego jej instalacja poprzez wykorzystanie pakietu wymaga jedynie użycia polecenia: installpkg libsafe-2.0.16-i386-1.tgz. Reszta czynności jest wykonana przez skrypt znajdujący się w pakiecie: doinst.sh, który warto przestudiować w celu otrzymania większej porcji informacji. Jeśli chodzi o kompilację biblioteki ze źródeł to sprawdza się zwykle ona do wykonania poleceń:

gunzip libsafe-2.0.16.tgz
tar -xvf libsafe-2.0.16.tar
rm libsafe-2.0.16.tar
cd libsafe-2.0.16
make
make install

Po umieszczeniu pliku wynikowego w katalogu /lib (np. /lib/libsafe.so.2), należy zdefiniować go w zmiennej środowiskowej $LD_PRELOAD lub dodać odpowiedni wpis z pełną ścieżką dostępu w pliku /etc/ld.so.preload:

LD_PRELOAD=/lib/libsafe.so.2
export LD_PRELOAD
typeset -r LD_PRELOAD

lub

echo '/lib/libsafe.so.2' >> /etc/ld.so.preload

Pierwszy wpis możemy dodać do pliku /etc/profile, jednak drugi wpis daje nam pewność, że każdy uruchamiany program, będzie chroniony przez bibliotekę oraz zwykły użytkownik nie spowoduje jej przestawienia (w przeciwieństwie do zmiennych środowiskowych). Poniżej przedstawiam wywołanie tego samego polecenia: ldd /bin/login – już po zainstalowaniu Libsafe:

/lib/libsafe.so.2 => /lib/libsafe.so.2 (0x40017000)
libcrypt.so.1 => /lib/libcrypt.so.1 (0x40023000)
libc.so.6 => /lib/libc.so.6 (0x40050000)
libdl.so.2 => /lib/libdl.so.2 (0x4017f000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 

Użycie Libsafe nie może zostać uznane za panaceum na wszystkie możliwe metody ataku. Dobrze jednak, aby był to jeden z pierścieni zabezpieczeń. Alternatywnym rozwiązaniem jest użycie narzędzia StackGuard. Możliwe jest również skonfigurowanie stosu nie pozwalającego na wykonywanie kodu (Openwall). Choć te metody pozwolą przechwycić niektóre błędy, przed którymi nie chroni Libsafe, ich konfiguracja i użycie jest znacznie bardziej skomplikowane. Należy jednak pamiętać, że nawet kombinacja narzędzi tego typu nie powstrzyma wszystkich ataków przepełnienia bufora, gdyż takie całkowite zabezpieczenie po prostu nie jest możliwe.

Bonus:  Najlepszym sposobem zapobiegania problemom z przepełnieniem bufora i innym błędom w zabezpieczeniach jest zastosowanie się do zasad poprawnego programowania oraz dokładna kontrola kodu źródłowego przez doświadczone osoby. Nawet w dobrze napisanym kodzie może znaleźć się co najmniej jeden błąd na stronę kodu źródłowego. Wynika to z faktu, że język C oferuje wysoki stopień kontroli nad procesorem (wykonuje dokładnie o, czego chce programista), co okupione jest “szczątkową” kontrolą typów. Nie ma w tym języku zabezpieczeń przed niewłaściwym wykorzystaniem danych. Dowodem jest to, iż większość błędów przepełnienia bufora wynika ze złej obsługi typów łańcuchowych
danych:

  • gets() – pozwala na odczytanie wiersza danych wejściowych, ale nie jest wykonywana kontrola przepełnienia bufora (ponieważ ogromna liczba ataków przepełnienia bufora jest wykonywana właśnie poprzez funkcję gets(), kompilator GNU wyświetla ostrzeżenie w przypadku próby kompilacji programu z taką funkcją). Jej zamiennikiem powinna być funkcja fgets(),
  • fscanf() – odczytuje dane wejściowe ze wskaźnika do strumienia, w wielu wypadkach zamiast niej można użyć także funkcji fgets(),
  • realpath() – zamienia wszystkie dowiązania symboliczne i symbole /./, /../ oraz dodatkowe znaki / w podanym ciągu zakończonym bajtem zerowym,
  • scanf() – odczytuje dane wejściowe ze standardowego strumienia wejściowego, lepiej najpierw pobrać ciąg znaków funkcją fgets(), a potem wykonać na tym ciągu funkcję sscanf(),
  • sprintf() – zapisuje do ciągu znaków, ale nie sprawdza długości tego ciągu, zamiast tego lepiej użyć snprintf(),
  • strcat() – łączy dwa ciągi, ale nie sprawdza ich długości, warto wymienić ją na strncat(),
  • strcpy() – kopiuje ciąg do tablicy, ale nie sprawdza długości ciągu, zamiast niej należy użyć strncpy().

Innymi niebezpiecznymi funkcjami są: vsprintf(), getopt(), getpass(), streadd() i strtrns(). Twórcy programów powinni więc używać tzw. bezpiecznych funkcji języka C, które gwarantują sprawdzanie długości przetwarzanych łańcuchów. Należy także pamiętać o tym, że jakkolwiek byśmy ni próbowali, nigdy nie przewidzimy każdej możliwej kombinacji obejść zabezpieczeń w programie. Dobry włamywacz zawsze będzie próbował bardzo egzotycznych kombinacji obejścia, w ten sposób wyszukując luki w naszym programie. Aby zabezpieczyć się przynajmniej przed częścią możliwości najlepiej wykonać następujące kroki:

  • upewnić się, że wszystkie wykonywane procedury kontrolowane są pod kątem wystąpienia przepełnienia bufora. Jeśli tak nie jest, należy wprowadzić kod dokonujący takiego sprawdzenia,
  • upewnić się, że jednoznacznie określamy zmienne środowiskowe, katalogi początkowe i ścieżki,
  • skrupulatnie przetestować kod, przeprowadzając symulowane przepełnienie stosu, wstawianie poleceń do listy argumentów itd. jednym słowem, spróbujmy włamać się do własnego programu, lub pozwólmy na jego testy doświadczonych osób,
  • w skryptach przetwarzających dane uniemożliwić wprowadzanie metaznaków i dodać zasadę przyjmującą od użytkownika jedynie słowa składające się z dopuszczalnych znaków,
  • gdzie tylko to możliwe, używać pojedynczych cudzysłowów (wszystkie nazwane zmienne w cudzysłowach podwójnych zostaną zamienione na odpowiadające im wartości).

Więcej informacji: LibSafe, man libsafe, Openwall

Kategorie K a t e g o r i e : Bezpieczeństwo

Tagi T a g i : , ,

Komentowanie tego wpisu jest zablokowane.