Artykuł jest częścią serii opracowań zagadnień obowiązujących na egzaminie CompTIA Security+ i stanowi wprowadzenie do ataków wykorzystujących niewłaściwe zarządzanie pamięcią w aplikacjach.
Artykuł jest również dostępny w wersji PDF. Jeśli nie chcesz przegapić kolejnych materiałów, to zachęcam do zapisania się na mój newsletter:
Na czym polega przepełnienie bufora?
Swego czasu, bardzo popularne było wykorzystywanie podatności związanych z niewłaściwym zarządzaniem pamięci poprzez tzw. przepełnienie bufora (ang. buffer overflow). Nie wdając się w szczegóły techniczne, atakujący był w stanie wprowadzić więcej danych do aplikacji (input) niż przewidział to jej twórca. Jeśli program otrzymał na wejściu większą liczbę bajtów niż zarezerwowany obszar pamięci (bufor), to mówiąc w przenośni, nadmiarowe bajty wylewały się poza zarezerwowany bufor, nadpisując sąsiadujące komórki pamięci.
W większości przypadków, powodowało to błąd naruszenia pamięci (segmentation fault) i zakończenie działania programu. Jednakże okazało się, że w niektórych przypadkach można przepełnić bufor i nadpisać pamięć w taki sposób, żeby program, zamiast się wykrzaczyć, wykonał przekazany na wejściu, specjalnie spreparowany kod. Przeważnie był to tzw. kod wywołania powłoki (shellcode), dający atakującemu dostęp do skompromitowanego systemu (zakładając, że zaatakowany program działał z uprawnieniami administratora systemu).
Żeby lepiej zrozumieć to zagadnienie, zróbmy sobie błyskawiczne i mocno uproszczone wprowadzenie do zarządzania pamięcią w systemach operacyjnych. Uruchomiony program (proces) potrzebuje określonej ilości pamięci do prawidłowego działania. Im bardziej złożona jest aplikacja, tym więcej pamięci operacyjnej potrzebuje (innymi słowy, im więcej RAM-u, tym gry chodzą płynniej ;)).
Aby ułatwić aplikacjom dostęp do fizycznej pamięci operacyjnej RAM (Random Access Memory) oraz do pamięci trwałej (nośniki danych), został opracowany mechanizm pamięci wirtualnej (ang. virtual memory). Dzięki niemu procesy (uruchomione aplikacje) mają do swojej dyspozycji ciągły i spójny obszar pamięci, który jest zmapowany na często pofragmentowaną pamięć fizyczną (przydzielone obszary są porozrzucane w różnych miejscach):
Przestrzeń adresowa procesu, widoczna na powyższej grafice, to po prostu fragment pamięci przydzielony uruchomionej aplikacji. Ta przestrzeń również ma swoją określoną strukturę i składa się m.in. z segmentu danych (ang. data segment), segmentu kodu (ang. code segment), a także miejsca zawierającego tzw. adres powrotu (ang. return address), wskazującego na następną instrukcję, którą program ma wykonać. W tej przestrzeni są oczywiście przechowywane dane wykorzystywane przez program, włącznie z informacjami przekazanymi przez użytkownika.
Spróbujmy sobie teraz zwizualizować na czym polega przepełnienie bufora. Wyobraźmy sobie, że mamy program wymagający od użytkownika podania imienia i nazwiska, które są później zapisywane w pamięci aplikacji. Przyjmijmy również, że zmienna first_name
jest w stanie pomieścić tylko 8 znaków (dla uproszczenia przyjmijmy również, że jeden znak to jeden bajt) i program nie sprawdza w żaden sposób długości wprowadzanego imienia. Co się stanie, gdy użytkownik poda imię Aleksandra?
Widzimy, że zmienna (bufor) nie była w stanie zmieścić podanej informacji, co spowodowało nadpisanie bufora zarezerwowanego dla zmiennej last_name
– nastąpiło przepełnienie (ang. overflow).
Pamiętajmy, że jest to bardzo mocno uproszczona wizualizacja, której celem jest przedstawienie zagadnienia w sposób intuicyjny. W rzeczywistości atakujący musiałby przygotować ładunek (ang. payload), który zmodyfikowałby pamięć w taki sposób, żeby nadpisać wspomniany wcześniej adres powrotu wartością wskazującą na wstrzyknięty kod. Jest to szalenie trudne do wykonania, a jeszcze trudniejsze do powtórzenia, szczególnie przy dzisiejszych zabezpieczeniach. Jeśli jesteś zainteresowany(-a) tematem, to zachęcam do zapoznania się z bardziej szczegółowym opracowaniem: Buffer overflow explained.
Podsumowując, atak polega na wprowadzeniu większej ilości danych do zarezerwowanego obszaru pamięci programu, niż ten obszar jest w stanie pomieścić. Prowadzi to do nadpisania sąsiadujących komórek pamięci, co w większości przypadków spowoduje awarię działającej aplikacji bądź systemu. Jednak w określonych okolicznościach umożliwia atakującemu wykonanie złośliwego kodu, wstrzykniętego do pamięci skompromitowanego programu.
Ochrona pamięci
Opisana powyżej nie powinna w ogóle nastąpić, jeśli programista czy programistka zadba o sprawdzenie, czy dane przekazane do aplikacji nie przekraczają wartości granicznych (ang. boundary values).
Na szczęście tego typu podatności nie są już dzisiaj tak powszechne, głównie za sprawą języków programowania wysokiego poziomu, działających w ramach dedykowanego środowiska uruchomieniowego (np. Java, .NET), które samo dba o odpowiednie zarządzanie pamięcią, odciążając tym samym programistów. Oczywiście to nie znaczy, że błędy umożliwiające przepełnienie bufora zniknęły na dobre – po prostu dużo trudniej jest je dzisiaj znaleźć, a jeszcze trudniej wykorzystać.
Oprócz narzędzi programistycznych, które przyczyniły się do lepszego zarządzania pamięcią w tworzonych aplikacjach, również systemy operacyjne wprowadziły szereg zabezpieczeń, które praktycznie uniemożliwiają wykorzystanie błędów typu buffer overflow:
- DEP (Data Execution Prevention) – mechanizm uniemożliwiający wykonanie kodu znajdującego się w segmencie danych pamięci. Jak już wcześniej wspomnieliśmy, program załadowany do pamięci operacyjnej składa się m.in. z segmentu danych (ang. data segment), gdzie przechowywane są wszelkie dane przetwarzane przez aplikację (np. te wprowadzone przez użytkownika) oraz segmentu kodu (ang. code segment), zawierającego instrukcje programu w postaci kodu maszynowego. Opisany powyżej atak przepełnienia bufora umożliwiał wstrzyknięcie złośliwego kodu do pamięci programu, ale jedynie do segmentu danych. Technologia DEP jest kojarzona głównie z Windowsem, ale inne systemy operacyjne również stosują te zabezpieczenie. Możemy się jednak spotkać z bardziej generyczną nazwą: Executable Space Protection.
- ASLR (Address Space Layout Randomization) – czyli organizacja przestrzeni adresowej procesu w sposób losowy. Dzięki temu bardzo trudno jest zlokalizować atakującemu miejsce w pamięci, które trzeba nadpisać, żeby atak się powiódł. Nawet jeśli jakimś cudem by się to jednak udało, to powtórzenie całej akcji byłoby niemożliwe, ponieważ przy kolejnym uruchomieniu programu, jego przestrzeń adresowa byłaby już zorganizowana w zupełnie inny sposób.