1.69k likes | 1.93k Views
Bezpieczeństwo aplikacji Windows. Krzysztof Sumowski Piotr Warzocha Rafał Zarębski Toruń, 07.01.2011. Plan prezentacji. Część I Krzysztof Sumowski, Rafał Zarębski Podstawowe zagadnienia Podstawowe błędy i sposoby ochrony Narzędzia testujące Część II Rafał Zarębski
E N D
Bezpieczeństwo aplikacji Windows Krzysztof Sumowski Piotr Warzocha Rafał Zarębski Toruń, 07.01.2011
Plan prezentacji Część I Krzysztof Sumowski, Rafał Zarębski • Podstawowe zagadnienia • Podstawowe błędy i sposoby ochrony • Narzędzia testująceCzęść II Rafał Zarębski • Bezpieczeństwo w WinApi - program Część III Piotr Warzocha • Różne metody zabezpieczania programów • Techniki kryptograficzne
1. Podstawowe zagadnienia • Podstawowe pojęcia • Znaczenie i rola bezpieczeństwa • Dlaczego i jak powinniśmy się bronić? • Oprogramowanie Open Source vs oprogramowanie komercyjne • Konsekwencje błędów bezpieczeństwa w kodzie
Podstawowe pojęcia Bezpieczny program to taki, który zachowuje narzucone granice bezpieczeństwa podczas przetwarzania danych, dostarczonych ze źródła objętego innymi uprawnieniami niż sam program. Bezpieczeństwo to zdolność do sprawowania nadzoru nad wykorzystywaniem przez innych naszych zasobów komputerowych, czyli zdolność do powiedzenia ludziom nie (lub tak) i umiejętność wsparcia tego odpowiednim działaniem. Bezpieczne programowanie polega w takim samym stopniu na wiedzy o tym, czego nie robić, jak i na wiedzy o tym, co zrobić.
Podstawowe pojęcia • Poufność • Dostępność • Integralność – informacja nie zmienia się bez naszej wiedzy • Kontrola dostępu – możliwość kontrolowania dostępu poprzez identyfikacje i uwierzytelnienia • Uwierzytelnianie – zapewnienie autentyczności informacji i osób • Nienaruszalność - zapewnia integralność komunikacji • Niezaprzeczalność – niemożność zaprzeczania faktowi wysyłania/odebrania informacji • Dyspozycyjność – ograniczanie skutków ataków
Znaczenie i rola bezpieczeństwa • Ogromna rola w dzisiejszym zinformatyzowanym świecie • Wiele czynności w naszym życiu uzależnionych od użycia komputera/Internetu. Sklepy internetowe, banki, dane poufne – konieczność obrony, zadbania o bezpieczeństwo. • Wyróżniamy bezpieczeństwo na poziomie:funkcjonalnym - silna autentykacja i uwierzytelnienie do zasobówkonfiguracji – odpowiednie opcje, wartości domyślne programu i ich interpretacjakodu – odporność kodu na specyficzne dane czy ataki złośliwych użytkowników
Znaczenie i rola bezpieczeństwa Kod programu i dane znajdują się w tym samym obszarze w pamięci – udogodnienie dla programistów jak i złośliwych użytkowników. Najważniejsze z punktu widzenia bezpieczeństwa jest wejście programu (przetwarzanie danych wejściowych). Brak lub niewystarczająca walidacja danych wejściowych jest główną przyczyną problemów z bezpieczeństwem aplikacji.
Dlaczego i jak powinniśmy się bronić? Złośliwi użytkownicy, atakujący: • dla przyjemności • dla zdobycia wiedzy, sprawdzenia się • dla pieniędzy Mimo iż spora część napastników używa prostych narzędzi, przed którymi łatwo się obronić, nie powinniśmy bagatelizować zagrożenia ze strony najgroźniejszych z nich.
Dlaczego i jak powinniśmy się bronić? Atak następuje, gdy zysk z niego jest większy bądź równy kosztom ataku. Powinniśmy dążyć do tego, aby koszt przeprowadzenia ataku stale wzrastał = przewyższał korzyści i zniechęcał atakującego. Nie istnieje całkowicie bezpieczny kod, możemy jedynie obniżać prawdopodobieństwo zajścia ataku.
Dlaczego i jak powinniśmy się bronić? • Fakt, iż nigdy nie zostaliśmy zaatakowani i nie przeczuwamy takiej sytuacji, nie powinien nas skłaniać do rezygnacji z dbałości o bezpieczeństwo naszych aplikacji. • Nie ma narzędzia, które kompleksowo ochroni nas przed wszystkimi typami ataków. Defence in depth obrona na wielu warstwach (firewall, obrona samej aplikacji). • Rewizja kodu naszego programu wtedy przynosi skutki, gdy jest dokonywana przez osoby, które go nie pisały (członek zespołu). • Narzędzia administracyjne (np. użytkownika root) –nigdy nie powinny ufać danym, na które mogą mieć wpływ nieautoryzowani użytkownicy. • Zawsze zakładamy możliwość podania złośliwych danych.
Dlaczego i jak powinniśmy się bronić? Projektowanie bezpiecznej aplikacji wymaga określenia wymagań bezpieczeństwa, stawianych projektowanej aplikacji. • Jakie jest środowisko bezpieczeństwa pracy programu? • Jakie są zagrożenia i na ile poważne? • Komu nie można ufać? • Jaka sieć/system operacyjny ? • Jaka jest polityka bezpieczeństwa firmy/organizacji? • Jakie dane będą chronione? • Jakie wymagania bezpieczeństwa stawiane są programowi?
Oprogramowanie Open Source vs oprogramowanie komercyjne Oprogramowanie, którego kod jest ogólnodostępny umożliwia lepsze jego uszczelnienie. Dostęp do kodu mają zarówno intruzi jak i potencjalne ofiary – każdy może zabezpieczyć się najlepiej jak potrafi. Oprogramowanie komercyjne – nie udostępnianie kodu/ nie informowanie o dziurach nie czyni go bezpieczniejszym. Stworzenie uaktualnienia nie jest takie łatwe. Fakt udostępnienia kodu źródłowego czyni oprogramowanie bezpieczniejszym.
Konsekwencje błędów bezpieczeństwa w kodzie • Niestabilne działanie systemu • Zmiana uprawnień dostępu do plików • Kradzież poufnych danych • Straty finansowe, intelektualne, moralne • Podmiana autorstwa • Załamanie aplikacji/systemu • Modyfikacja działania programu • Przejęcie kontroli nad systemem operacyjnym • Nieuprawnione użycie programu
Konsekwencje błędów bezpieczeństwa w kodzie Rodzaje ataków robaki ataki Dos, DDos exploit backdoor
2. Podstawowe błędy i sposoby ochrony • Dlaczego powstają dziury w programach? • Przepełnienie bufora. • Błąd ciągów formatujących. • Wycieki pamięci. • Błędy dostępu do pliku. • Podsumowanie.
Dlaczego powstają dziury w programach? • Brak odpowiedniej edukacji, mało dostępna literatura traktująca o pisaniu bezpiecznego kodu. • Używanie prostych, ale niezabezpieczonych funkcji (np. języka C operujących na łańcuchach znaków). • Programiści nie pamiętają, iż program pracuje w trybie „multiuser”, • Programiści zamiast pisać od razu bezpieczny kod, piszą go byle jak, wmawiając sobie, że poprawią go później. • Większość programistów nie potrafi myśleć jak intruz. • Aktualizacja napisanego już kodu, pod kątem zwiększenia bezpieczeństwa jest trudna. • Bezpieczne podejście wymaga większych nakładów czasowych, czyli większych nakładów finansowych.
Przepełnienie bufora Co to jest przepełnienie bufora? Bufor to blok pamięci, zazwyczaj w formie tablicy. Jeśli przy zapisywaniu do bufora nie zostanie sprawdzony rozmiar tablicy, możliwe jest zapisanie danych poza zaalokowanym obszarem pamięci (buforem). Zdarzenie, w którym dane zapisywane są pod adresem wyższym niż zaalokowany bufor, nazywa się przepełnieniem bufora (buffer overflow). Generalnie chodzi o sytuację w której program otrzymuje dane z zewnątrz. Gdy zaalokowaliśmy bufor o stałej długości, nie sprawdzamy, czy dane przesyłane z zewnątrz nie zajmują więcej miejsca niż na nie przeznaczyliśmy.
Przepełnienie bufora Przyczyny, umożliwiające włamanie się do programu: źle deklarowane wartości tablic, nieodpowiednio przekazywane parametry funkcji, nieprawidłowy znak, przepełnienie licznika, brak lub nieodpowiednia kontrola danych wejściowych. Szczególnie narażone programy napisane w C/C++ (które pozwalają na swobodę programiście)
Przepełnienie bufora Zagrożenia • Załamanie aplikacji • Atak Dos na aplikację • Wykonanie dowolnego kodu w systemie (z coraz większymi uprawnieniami uruchamianej aplikacji wiąże się większe zagrożenie) Przepełnienie stosu i sterty (trudniejsze do uzyskania i wykorzystania)
Przepełnienie bufora Budowa stosu w architekturze x86 Stos jest liniową strukturą danych, w której dane dokładane są na wierzch stosu i z wierzchołka stosu są pobierane (bufor typu LIFO, Last In, First Out; ostatni na wejściu, pierwszy na wyjściu). W architekturze x86 stos rośnie w dół, co oznacza, że nowsze dane są zapamiętywane pod adresami niższymi niż elementy wstawione na stos wcześniej.
Przepełnienie bufora Każde wywołanie funkcji powoduje utworzenie nowej ramki stosu o następującej budowie (należy pamiętać, że lista uporządkowana jest w kolejności malejących adresów): • parametry funkcji, • adres powrotu funkcji, • wskaźnik ramki, • ramka procedur obsługi wyjątków, • lokalnie zadeklarowane zmienne i bufory, • rejestry zachowane przez funkcję wywołaną.
Przepełnienie bufora Z budowy stosu widać, że przepełnienie bufora może nadpisać: inne zmienne zaalokowane przed buforem, ramkę obsługi wyjątków, wskaźnik ramki, adres powrotu oraz parametry funkcji. Aby przejąć kontrolę nad programem, wystarczy umieścić odpowiednią wartość w danych, które później zostaną załadowane do rejestru. Jedną z takich wartości jest adres powrotu funkcji. Typowe wykorzystanie przepełnienia bufora polega na nadpisaniu adresu powrotu funkcji i pozwoleniu instrukcjom powrotnym funkcji na załadowanie zmienionego adresu do rejestru.
Przepełnienie bufora Osoby, które wykorzystują tego typu luki działają w taki sposób, że podstawiają własny adres powrotu, który jest podawany do programu. Możliwe jest to dzięki prostej funkcji łańcuchowej, kopiującej wartości w stosie z jednego adresu do drugiego. Nie ma w trakcie tego automatycznego sprawdzenia, czy pod adresem docelowym jest wystarczająca ilość miejsca.
Przepełnienie bufora Przykłady. Stos wywołania funkcji FUN #include<stdio.h> void FUN(int a, int b, int c){ char bufor[5]; char bufor2[10]; int *ret; ret = &a - 1; (*ret) += 10; } void main(){ int x; x = 0; FUN(1,2,3); x = 1; printf("%d\n",x); }
Przepełnienie bufora Wykorzystanie Shellcodu Shellcode oznacza prosty, niskopoziomowy program prezentowany najczęściej w postaci kodu maszynowego, odpowiedzialny za wywołanie powłoki systemowej. Często wykorzystywany w ostatniej fazie wykorzystywania wielu błędów zabezpieczeń przez exploity. Dostarczany jest on zwykle wraz z innymi danymi wejściowymi użytkownika. Na skutek wykorzystania luki w atakowanej aplikacji, procesor rozpoczyna wykonywanie shellcode, pozwalając na uzyskanie nieautoryzowanego dostępu do systemu komputerowego lub eskalację uprawnień.
Przepełnienie bufora Shellcode składa się z instrukcji asemblera zapisanych już w formie binarnej, w której wszystkie adresy muszą być zakodowane na stałe. Aby uniknąć bezwzględnych odwołań do pamięci generujących błąd naruszenia segmentacji programu. Większość adresów uzyskuje się ze stosu a skoki są wykonywane nie do konkretnego miejsca w pamięci tylko o konkretną ilość instrukcji procesora w przód, bądź w tył.
Przepełnienie bufora Zapobieganie przepełnieniom bufora Aby zapobiegać przepełnieniom bufora, należy zwracać uwagę na funkcje, których używamy w naszych programach. Najczęstszą przyczyną problemów jest niewłaściwe stosowanie funkcji związanych z obsługą łańcuchów tekstowych. Dlatego też przyjrzymy się im bliżej, aby zobaczyć, jak można zabezpieczyć się przed niepożądanym działaniem.
Przepełnienie bufora Niebezpieczne konstrukcje języka C
Przepełnienie bufora Funkcja strcpy Wywołanie funkcji: char *strcpy( char *strDestination, const char *strSource ); Istnieje bardzo wiele sytuacji, w której działanie funkcji zakończy się z błędem. Najpopularniejsze z nich to między innymi: gdy bufor źródłowy i docelowy są puste, jeżeli bufor źródłowy nie jest zakończony znakiem null i największy z problemów – gdy rozmiar ciągu źródłowego jest większy niż bufor docelowy.
Przepełnienie bufora Funkcja strncpy Wywołanie funkcji: char *strncpy( char *strDest, const char *strSource, size_t count ); Jest bezpieczniejsza niż strcpy, ale i tutaj nadal mogą pojawiać się problemy, szczególnie w przypadku przekazywania wartości null jako ciągu źródłowego lub docelowego. Dodatkowo problemem może być zła wartość licznika. Jedna z różnic pomiędzy tą funkcją a poprzednią to fakt, że jeśli bufor źródłowy nie jest zakończony null, to funkcja nie zakończy się z błędem.
Przepełnienie bufora Funkcja sprintf Wywołanie funkcji: int snprintf( char *buffer, size _ t count, const char *format [, argument] ... ); Jest to jedna z bezpieczniejszych funkcji. Jedyna rzecz, na którą należy zwrócić uwagę to bufor docelowy jest zakończony znakiem null. Funkcja _snprintf Wywołanie funkcji: int _snprintf( char *buffer, size _ t count, const char *format [, argument] ... ); Jest to jedna z bezpieczniejszych funkcji. Jedyna rzecz, na którą należy zwrócić uwagę, to sprawdzenie, czy bufor docelowy jest zakończony znakiem null.
Przepełnienie bufora Address Space Layout Randomization (ASLR) implementacja w Windows Vista I Windows 7 rozmieszczenie bibliotek i aplikacji pod losowymi adresami szansa na powodzenie ataku maleje 256 razy może powodować problemy kompatybilnościowe Stack Defender – IPS - uniemożliwia uruchomienie kodu napastnika z obszaru pamięci stosu Data Execution Prevention (DEP) - uniemożliwia wykonanie kodu pochodzącego z niewykonywalnego obszaru pamięci - ochrona programowa - można skonfigurować ją dla systemu lub dla aplikacji - ochrona sprzętowa - wymagane „rozumienie” technologii przez procesor
Przepełnienie bufora Przepełnienie bufora to poważny problem. Każdy programista powinien mieć świadomość tego rodzaju zagrożenia. Zanim zacznie tworzyć kod, powinien wziąć pod uwagę podobne problemy i dużo wcześniej przemyśleć architekturę kodu. Z drugiej strony, programista powinien mieć pomoc ze strony narzędzi programistycznych – tak, aby to one mogły sprawdzić i rozwiązać takie problemy (przynajmniej częściowo). Po analizie tekstu możemy zadać następujące pytanie jak samemu wyrobić sobie nawyk nie popełniania takich błędów, jak przepełnienie bufora? O ile nigdy nie doświadczymy na własnej skórze problemu z włamaniem i nie stracimy przez to jakichś istotnych danych, to pewnie trudno nam będzie o tym pamiętać. A na poważnie – warto zrobić sobie listę wszystkich kroków, które musimy wykonać, i sprawdzić, czy wśród nich jest sprawdzenie możliwości wystąpienia przepełnienia bufora.
Błąd ciągów formatujących Format string attack - atak informatyczny, będący stosunkowo nową techniką wykorzystywania błędów programistycznych w aplikacjach. Błędnie napisana aplikacja może być przy wykorzystaniu tej techniki usunięta z listy procesów przez system operacyjny (tzw. crash) lub zmuszona do wykonania kodu dostarczonego przez napastnika.
Błąd ciągów formatujących Przez wiele lat niewłaściwe wykorzystanie funkcji operujących na ciągach formatujących było uważane za błąd, jednak nie brano uwagę, iż umożliwia on przejęcie kontroli nad aplikacją. W 2000 roku po raz pierwszy przedstawiono exploity wykorzystujące błędy tego typu w szeroko stosowanym serwerze usługi FTP. Kod źródłowy exploita pokazywał technikę umożliwiającą przejęcie kontroli nad aplikacją przy wykorzystaniu błędnego wywołania funkcji vsnprintf() wewnątrz własnej funkcji, odpowiedzialnej za formułowanie odpowiedzi (lreply()).
Błąd ciągów formatujących Atak wykorzystuje fakt, iż funkcje ze zmienną ilością argumentów takie jak printf() określają ilość tych argumentów na podstawie podanego ciągu znaków. Podczas jego interpretowania korzysta z dodatkowych parametrów podanych do funkcji, przedstawiając je w czytelnej dla człowieka formie. Podatność na atak ma miejsce gdy atakujący może dostarczyć do takiej funkcji ciąg formatujący. W ten sposób można zmienić zachowanie aplikacji, i przejąć nad nią kontrolę.
Błąd ciągów formatujących Najprostszy przykład gdzie podanie ciągu formatującego jest możliwe: int funkcja (char *nazwa) { printf (nazwa); } W języku C jest dużo funkcji formatujących. Na kolejnym slajdzie wymieniono niektóre z podstawowych funkcji, na których bazowane są inne bardziej złożone, zaś niektóre z nich nie należą do standardu, lecz są ogólnodostępne.
Błąd ciągów formatujących fprintf — drukuje do pliku printf — drukuje do strumienia standardowego wyjścia sprintf — drukuje do zmiennej typu string snprintf — drukuje do zmiennej typu string kontrolując długość vfprintf — drukuje do pliku ze struktury va_arg vprintf — drukuj from a va_arg structure vsprintf — drukuje do strumienia standardowego wyjścia ze struktury va_arg vsnprintf — drukuje do zmiennej typu string kontrolując długość ze struktury va_arg Pokrewne: setproctitle, syslog, inne err*, verr*, warn*, vwarn*
Błąd ciągów formatujących Aby zrozumieć skąd bierze się podatność na ataki wymienionych funkcji trzeba zbadać jaki jest cel funkcji formatujących. Zastosowania: • są używane do konwertowania prostych typów C do postaci ciągu znaków • pozwalają określić format reprezentacji danych • przetworzyć go (wyjście do stderr, stdout, syslog etc. )
Błąd ciągów formatujących Jak działa funkcja formatująca? • ciąg formatujący kontroluje zachowanie funkcji • określa typ danych jakie mają być wydrukowane • parametry są wepchnięte na stos • zapisane albo przez wartość albo przez referencję Podstawowe parametry określające typ danych: %d – liczba całkowita %u – liczba całkowita bez znaku %x – liczba w systemie szesnastkowym %s – ciąg znaków %n – do argumentu zapisywana jest liczba dotychczas zapisanych znaków
Błąd ciągów formatujących Przykład: printf ("liczba %d , liczba %d o adresie: %08x\n", a, b, &c); Z perspektywy prtintf stos wygląda następująco:
Błąd ciągów formatujących Funkcja przetwarza ciąg formatujący po jednym znaku. Jeśli znak ten nie jest '%' zostaje przekopiowany do wyjścia w przeciwnym wypadku następny znak/znaki określa typ zmiennej.
Błąd ciągów formatujących Zawieszenie programu Jest to najprostszy z ataków za pomocą ciągu formatującego. Bez problemu można wywołać błąd, próbując odczytać pamięć z niedozwolonego adresu. Dokonuje się tego za pomocą podania ciągu „%s%s%s%s%s%s%s”.
Błąd ciągów formatujących Odczytanie danych ze stosu Podając ciąg taki jak np: „%08x %08x %8x” każemy funkcji printf odczytać ze stosu 3 argumenty i wyświetlić je jako ośmiocyfrowe liczby w systemie szesnastkowym. Zależnie od wielkości bufora przeznaczonego na ciąg formatujący i na wielkość bufora wyjściowego, można odtworzyć mniejsze lub większe obszary pamięci. Można w ten sposób dowiedzieć się więcej o działaniu programu, odczytać zmienne lokalne, znaleźć przedziały pamięci, które chce się zaatakować. Dodatkowo umieszczając ręcznie adres na stosie można odczytać pamięć pod tym adresem poleceniem %s.
Błąd ciągów formatujących Pisanie do pamięci za pomocą %n Tak jak w przypadku czytania z dowolnego miejsca, tyle ze %n w tym wypadku wpisze liczbę całkowita do argumentu wskazywanego przez podstawiony adres. Używając tych metod można znaleźć i nadpisać kod powrotu funkcji tak aby wskazywał na spreparowany szkodliwy kod.
Błąd ciągów formatujących Jak się bronić: Zamiast printf (string); pisać: printf („%s”,str); Ostrożność przy używaniu funkcji typu fprintf (STDOUT,strFormat,arg1,arg2,arg3); Testowanie kodu: • Narzędzie RATS • Dedykowane narzędzie pscan (Cygwin)
Wycieki pamięci Zjawisko wycieku pamięci szczególnie znane jest osobom programującym w językach nie posiadających odśmiecania/śmieciarza (ang. Garbage Collector), takich jak C/C++. Na pozór, nazwa tego zjawiska niejako odnosi się do wadliwego działania pamięci, w rzeczywistości jednak winę ponosi niewłaściwie napisany kod. Aby zobrazować na czym polega cały problem, warto przeanalizować przykład z kolejnego slajdu.