Przepełnienie bufora: przyczyny, skuteczne rozwiązania i wymagana ochrona

Wszyscy programiści są świadomi potencjalnego zagrożenia związanego z przepełnieniem bufora w ich programach. Istnieje wiele zagrożeń z nim związanych zarówno w nowym, jak i starym oprogramowaniu, niezależnie od liczby wprowadzonych poprawek. Atakujący mogą wykorzystać ten błąd wstrzykując kod specjalnie zaprojektowany do spowodowania przepełnienia początkowej części zestawu danych, a następnie zapisując resztę pod adresem pamięci sąsiadującym z przepełnionym.

Dane mogą zawierać kod wykonywalny, który umożliwia atakującym uruchamianie większych, bardziej złożonych programów lub daje im dostęp do systemu. Błędy są bardzo trudne do znalezienia i poprawienia, ponieważ kod oprogramowania składa się z milionów linii. Poprawianie tych błędów jest dość trudne, a z kolei podatne na błędy utrudniające rozwiązanie.

Wykrywanie przepełnienia bufora

Wykrywanie przepełnienia bufora

Przed szukaniem przelewu, trzeba mieć świadomość, co reprezentuje. Jak sama nazwa wskazuje, luki te są związane z buforami lub alokacją pamięci w językach zapewniających bezpośredni dostęp do odczytu i zapisu na niskim poziomie.

Podczas używania języków C i Assembler, czytanie lub pisanie takich dystrybucji nie sprawdza automatycznie granic. Dlatego w przypadku wykrycia przepełnienia bufora stosu w danej aplikacji nie ma sprawdzania, czy w danym buforze można umieścić liczbę bajtów. W takich przypadkach program może "przepełnić" swoje możliwości. Powoduje to, że dane zapisane po wypełnieniu nadpisują zawartość kolejnych adresów na stosie i odczytują dodatkowe. Przepełnienia mogą wystąpić nieumyślnie z powodu błędów użytkownika.

Czasami jest to spowodowane przez złośliwy podmiot wysyłający starannie spreparowane złośliwe dane wejściowe do programu, który następnie próbuje zapisać je w niewystarczającym buforze. W przypadku wykrycia przepełnienia bufora stosu w danej aplikacji, nadmiar danych jest zapisywany do sąsiedniego bufora, gdzie nadpisuje wszystkie istniejące dane.

Zawierają one zazwyczaj wskaźnik powrotny do wykorzystywanej funkcji - adres, pod który proces powinien przeskoczyć w następnej kolejności. Atakujący mógłby ustawić nowe wartości tak, aby wskazywały na wybrany przez siebie adres. Atakujący zwykle ustawia nowe wartości, aby wskazać, gdzie znajduje się payload. Zmienia to ścieżkę wykonania procesu i natychmiast przekazuje kontrolę do złośliwego kodu.

Wykorzystanie przepełnienia bufora pozwala atakującemu na kontrolowanie lub zakończenie procesu lub zmianę jego wewnętrznych zmiennych. To naruszenie jest jednym z 25 najbardziej niebezpiecznych błędów programistycznych na świecie (2009 CWE/SANS Top 25 Most Dangerous Programming Errors) i jest oznaczone jako CWE-120 w Słowniku Podatności Systemowych. Chociaż są one dobrze rozumiane, nadal nękają popularne programy.

Prosty wektor wykorzystania buforów

Podczas pracy z kodem źródłowym należy zwrócić szczególną uwagę na to, gdzie bufory są używane i modyfikowane. Szczególną uwagę należy zwrócić na funkcje związane z danymi wejściowymi dostarczanymi przez użytkownika lub inne źródło zewnętrzne, ponieważ stanowią one prosty wektor do wykorzystania w przypadku wykrycia przepełnienia bufora stosu. Na przykład, gdy użytkownik zadaje pytanie "tak" lub "nie", właściwe jest przechowywanie danych ciągu użytkownika w małym buforze dla ciągu "tak", jak pokazano w poniższym przykładzie.

Prosty wektor wykorzystania buforów

Patrząc na kod, można zauważyć, że nie jest wykonywane sprawdzanie granic. Jeśli użytkownik wpisze "może", program raczej się zawiesi niż poprosi o odpowiedź, która jest zapisywana do bufora niezależnie od jego długości. W tym przykładzie, ponieważ odpowiedź użytkownika jest jedyną zadeklarowaną zmienną, następnymi wartościami na stosie będą wartości adresu zwrotnego lub miejsca w pamięci, do którego program powróci po wykonaniu funkcji ask question.

Oznacza to, że jeśli użytkownik wprowadzi cztery bajty danych, co wystarczy do przepełnienia bufora poleceń klienta, nastąpi prawidłowy adres zwrotny, który zostanie zmieniony. Spowoduje to wyjście programu z funkcji w innym punkcie kodu niż pierwotnie zamierzony i może spowodować, że oprogramowanie zachowa się w niebezpieczny i niezamierzony sposób.

Jeśli pierwszym krokiem do wykrycia przepełnień bufora w kodzie źródłowym jest zrozumienie, jak działają, drugim krokiem jest poznanie zewnętrznego wejścia i manipulacji buforem, trzecim krokiem jest poznanie, które funkcje są podatne na tę podatność i które mogą działać jako "czerwone flagi". Funkcja gets jest świetna do pisania poza buforem, który jej dostarczono. W rzeczywistości ta cecha dotyczy całej rodziny funkcji połączonych, w tym strcpy, strcmp i printf/sprintf, wszędzie tam, gdzie wykorzystywana jest jedna z tych luk przepełnienia.

Usunięcie z bazy kodów

Jeśli w kodzie źródłowym zostanie wykryty przepełnienie bufora stosu, będzie on musiał zostać usunięty z bazy w spójny sposób. Musisz być zaznajomiony z bezpiecznymi praktykami. Najprostszym sposobem zapobiegania tym podatnościom jest używanie języka, który nie. Język C ma te luki z powodu bezpośredniego dostępu do pamięci i braku ścisłego typowania obiektów. Języki, które nie posiadają tych aspektów, są zazwyczaj odporne na ataki. Są to Java, Python i .NET, wraz z innymi językami i platformami, które nie wymagają specjalnych kontroli lub zmian.

Oczywiście nie zawsze możliwa jest całkowita zmiana języka programowania. W tym przypadku stosowane są bezpieczne metody do pracy przepełnienie bufora komendy. W przypadku funkcji przetwarzania ciągów, było wiele dyskusji na temat tego, które metody są dostępne, które są bezpieczne w użyciu i których należy unikać. Funkcje strcpy i strcat kopiują łańcuch do bufora i dodają zawartość jednego do drugiego. Te dwie metody pokazują niebezpieczne zachowanie, ponieważ nie sprawdzają granicy bufora docelowego i będą zapisywać poza granicę, jeśli jest wystarczająco dużo bajtów, aby to zrobić.

alternatywna ochrona

Jedną z często sugerowanych alternatyw są wersje bound, które zapisują do maksymalnego rozmiaru bufora docelowego. Na pierwszy rzut oka wygląda to na idealne rozwiązanie. Niestety, te funkcje mają mały niuans, który powoduje problemy. Po osiągnięciu limitu, jeśli znak kończący nie mieści się w ostatnim bajcie, następuje poważny ulega awarii, gdy odczyt bufora.

Alternatywne zabezpieczenia

Ten uproszczony przykład pokazuje niebezpieczeństwa związane z ciągami nie kończącymi się na zero. Kiedy foo jest umieszczony w normalnym buforze, kończy się na zero, ponieważ ma dodatkowe miejsce. To jest najlepszy scenariusz. Jeśli bajty w buforze przepełnienia na stosie znajdują się w innym buforze znakowym lub innym łańcuchu wydruku, funkcja print będzie kontynuować czytanie aż do osiągnięcia ostatniego znaku tego łańcucha.

Wadą jest to, że język C nie zapewnia standardowej, bezpiecznej alternatywy dla tych funkcji. Jest jednak pozytywna strona tego stanu rzeczy - dostępnych jest kilka implementacji specyficznych dla danej platformy. OpenBSD dostarcza strlcpy i strlcat, które działają podobnie do funkcji strn, z tą różnicą, że obcinają łańcuch o jeden znak wcześniej, aby uwolnić przestrzeń dla terminator zerowy.

Podobnie Microsoft dostarcza własne bezpieczne implementacje powszechnie używanych funkcji obsługi łańcuchów: strcpy_s, strcat_s i sprintf_s.

Preferowane jest stosowanie bezpiecznych alternatyw wymienionych powyżej. Gdy nie jest to możliwe, wykonaj ręczne sprawdzanie granic i zerowe zakończenie dla buforów łańcuchowych.

Luki w kompilacji

Luki w kompilacji

W przypadku, gdy niebezpieczna funkcja pozostawia otwartą możliwość przepełnienia bufora C, wszystko nie jest stracone. Podczas uruchamiania programu kompilatory często tworzą losowe wartości, zwane kanarkami, i umieszczają je na stosie, więc istnieje niebezpieczeństwo. Sprawdzenie wartości kanarka w stosunku do jego oryginalnej wartości może określić, czy wystąpił przepełnienie bufora systemu Windows. Jeśli wartość została zmieniona, program zostanie zamknięty lub przejdzie do stanu błędu zamiast potencjalnie zmienionego adresu zwrotnego.

Niektóre nowoczesne systemy operacyjne zapewniają dodatkową ochronę przed przepełnieniem bufora w postaci niewykonywalnych stosów i randomizacji alokacji przestrzeni adresowej (ASLR). Niewykonalne stosy - Data Execution Prevention (DEP) - oznacza stos, a w niektórych przypadkach także inne struktury, jako obszary, w których kod nie będzie wykonywany. Oznacza to, że atakujący nie może wstrzyknąć kodu exploita na stos i oczekiwać jego pomyślnego wykonania.

Przed naprawą przepełnienia bufora należy rozpakować ASLR na komputerze. Został zaprojektowany do obrony przed programowaniem zorientowanym na powrót jako obejście niewykonywalnych stosów, gdzie istniejące fragmenty kodu są łączone razem na podstawie ich offsetów adresowych.

Działa poprzez randomizację regionów pamięci struktur, dzięki czemu ich przesunięcia są trudniejsze do wykrycia. Gdyby ta ochrona istniała pod koniec lat 80-tych, można by uniknąć robaka Morrisa. Funkcjonował on bowiem częściowo poprzez wypełnienie bufora w protokole UNIX finger kodem exploita, a następnie przepełnienie go w celu zmiany adresu zwrotnego i wskazania na wypełniony bufor.

ASLR i DEP utrudniają wskazanie adresu, na który ma być wskazany, czyniąc lokalizację pamięci całkowicie nieoperacyjną. Czasami luka prześlizguje się przez szczeliny otwarte na atak przepełnienia bufora, pomimo obecności kontroli na poziomie rozwoju, kompilatora lub systemu operacyjnego.

Statyczna analiza zasięgu

W sytuacji przepełnienia są dwa decydujące zadania. Po pierwsze, podatność musi zostać zidentyfikowana, a baza kodu dla rozwiązywanie problemów. Po drugie, upewnij się, że wszystkie wersje kodu podatności na przepełnienie bufora zostały zastąpione. Idealnie byłoby zacząć od automatycznych aktualizacji wszystkich systemów podłączonych do Internetu.

Nie można założyć, że taka aktualizacja zapewni wystarczające pokrycie. Organizacje lub osoby prywatne mogą używać oprogramowania w systemach z ograniczonym dostępem do Internetu, które wymagają ręcznej aktualizacji. Oznacza to, że wiadomość o aktualizacji powinna zostać przekazana wszystkim administratorom, którzy mogą korzystać z oprogramowania, a łatka powinna być łatwo dostępna do pobrania. Tworzenie i dystrybucja łatek odbywa się jak najbliżej wykrycia luki, co zapewnia zminimalizowanie czasu jej występowania.

Dzięki zastosowaniu bezpiecznych funkcji obsługi buforów i odpowiednich funkcji bezpieczeństwa kompilatora oraz system operacyjny możliwe jest stworzenie solidnego zabezpieczenia przed przepełnieniem. Mając na uwadze powyższe kroki, konsekwentna identyfikacja błędów jest kluczowym krokiem do zapobiegania exploitom.

Łączenie linii kodu źródłowego w poszukiwaniu potencjalnych zagrożeń może być żmudne. Ponadto zawsze istnieje możliwość, że ludzkie oko może przeoczyć coś ważnego. Narzędzia analizy statycznej służą do zapewnienia jakości kodu, zostały opracowane specjalnie w celu wykrywania luk w zabezpieczeniach podczas tworzenia kodu.

Analiza pokrycia statycznego ustala "czerwone znaki" dla potencjalnych przepełnień. Są one następnie przetwarzane i łatane osobno, aby nie były ręcznie przeszukiwane. Narzędzia te, w połączeniu z regularnymi kontrolami i wiedzą o tym, jak naprawiać przepełnienia, pozwalają na zidentyfikowanie i naprawienie zdecydowanej większości błędów przed zakończeniem tworzenia oprogramowania.

Przeprowadzenie ataku przez root`a

Błędy w kodowaniu są zazwyczaj przyczyną przepełnienia bufora. Częste błędy w tworzeniu aplikacji, które mogą do niego doprowadzić, to m.in. nieprzydzielanie wystarczająco dużych buforów i brak mechanizmu sprawdzania tych problemów. Takie błędy są szczególnie problematyczne w językach C/C++, które nie mają wbudowanej ochrony przed przepełnieniem i często są celem ataków typu buffer overflow.

W niektórych przypadkach atakujący wstrzykuje złośliwy kod do pamięci, która została uszkodzona przez przepełnienie bufora stosu. W innych przypadkach, po prostu wykorzystując uszkodzenie pamięci sąsiedniej. Na przykład program, który pyta o hasło użytkownika, aby przyznać mu dostęp do systemu. W poniższym kodzie poprawne hasło zapewnia uprawnienia roota. Jeśli hasło jest nieprawidłowe, program nie nada użytkownikowi uprawnień.

Program nie nadaje użytkownikowi uprawnień

W tym przykładzie program nadaje użytkownikowi uprawnienia roota, nawet jeśli podał on nieprawidłowe hasło. W tym przypadku atakujący dostarcza dane wejściowe, które są dłuższe niż bufor może pomieścić, tworząc przepełnienie, które nadpisuje pamięć przepustu integer. Dlatego też, pomimo nieprawidłowego hasła, pass staje się wartością niezerową i atakujący uzyskuje uprawnienia roota.

Atak na tymczasowy obszar przechowywania danych

Bufor to tymczasowy obszar do przechowywania danych. Kiedy program lub proces systemowy umieszcza więcej danych niż było pierwotnie przeznaczone do przechowywania, dodatkowe dane przepełniają się. To powoduje, że część z nich wycieka do innych procesów, uszkadzając lub nadpisując dane.

W ataku typu flood dodatkowe dane zawierają specjalne instrukcje dotyczące działań zamierzonych przez hakera lub złośliwego użytkownika, np. wywołują reakcję, która powoduje uszkodzenie plików, zmianę danych lub ujawnienie informacji osobistych.

Napastnik wykorzystuje exploit przepełnienia, aby wykorzystać program oczekujący na wejście użytkownika. Istnieją dwa rodzaje przepełnienia bufora: oparte na stosie i oparte na stercie. Te oparte na stercie są trudne do wykonania i najrzadziej spotykane, natomiast atakują aplikację poprzez zapełnienie przestrzeni zarezerwowanej dla programu.

Stos to przestrzeń pamięci używana Aby zapisać dane użytkownika wejście. Ten typ przepełnienia jest bardziej prawdopodobny do napotkania przez atakujących wykorzystujących aplikacje.

Nowoczesne kompilatory zazwyczaj zapewniają możliwość sprawdzania przepełnień w czasie kompilacji, ale w czasie runtime jest dość trudno sprawdzić ten problem bez jakiegoś dodatkowego mechanizmu ochrony obsługi wyjątków.

Przeprowadzenie ataku przez root`a

Opcje programu:

  1. Wejście: 12345678 (8 bajtów), program działa bez awarii.
  2. Wpisz: 123456789 (9 bajtów), pojawia się komunikat "Segmentation error", program kończy pracę.

Luka występuje z powodu przepełnienia, jeśli dane wejściowe użytkownika argv przekraczają 8 bajtów. Dla systemu 32-bitowego (4 bajty) wypełniasz pamięć podwójnym słowem (32 bity). Rozmiar znaku to 1 bajt, więc jeśli zażądasz bufora o wielkości 5 bajtów, system przydzieli 2 podwójne słowa (8 bajtów). Dlatego wprowadzenie więcej niż 8 bajtów spowoduje przepełnienie Bufora.

Istnieją podobne standardowe funkcje, które są technicznie mniej podatne na zagrożenia. Na przykład strncpy (), strncat () i memcpy (). Problem z tymi funkcjami polega na tym, że, co stanowi zobowiązanie To do programisty, a nie do kompilatora, należy określenie wielkości bufora.

Każdy programista C/C++ powinien znać ten problem, zanim zacznie kodować. Wiele generowanych problemów może w większości przypadków być chronionych przed przepełnieniem.

Niebezpieczeństwa w C/C++

http://blogs.grammatech.com/eliminating-the-danger-of-uninitialized-variables

Użytkownicy C powinni unikać używania niebezpiecznych funkcji, które nie sprawdzają limitów, chyba że są pewni, że limity nie zostaną przekroczone. Funkcje, których należy unikać w większości przypadków, aby zapewnić ochronę, obejmują strcpy. Powinny one zostać zastąpione przez funkcje takie jak strncpy. Użycie strlen powinno być unikane, jeśli użytkownik jest pewien, że zostanie znaleziony końcowy znak nil. Rodzina scanf (): scanf (3), fscanf (3), sscanf (3), vscanf (3), vsscanf (3) i vfscanf (3) jest niebezpieczna w użyciu i nie powinna być używana do wysyłania danych do łańcucha bez kontroli maksymalnej długości, "format% s" jest szczególnie częstą usterką.

Oficjalnie snprintf () nie jest standardową funkcją C w klasyfikacji ISO 1990. Systemy te nie chronią przed przepełnieniem bufora, po prostu wywołują sprintf bezpośrednio. Obecna wersja linuksowego snprintf jest znana z tego, że działa poprawnie, to znaczy, że faktycznie respektuje limit ustawiony. Zmienia się również wartość zwrotna snprintf ().

Wersja 2 specyfikacji Unix (SUS) i standard C99 różnią się tym, że zwracają snprintf (). Niektóre wersje snprintf nie gwarantują, że łańcuch zakończy się NIL, a jeśli łańcuch jest zbyt długi, nie będzie zawierał NIL w ogóle. Biblioteka glib ma g_snprintf () ze spójną semantyką powrotu, zawsze kończy się NIL i, co najważniejsze, zawsze respektuje długość bufora.

Przepełnienie bufora portu komunikacyjnego

Przepełnienie bufora portu komunikacyjnego

Czasami port szeregowy zgłasza przepełnienie bufora. Problem ten może być spowodowany przez kilka czynników. Obejmują one szybkość komputera, szybkość transmisji używanych danych, rozmiar FIFO portu szeregowego oraz rozmiar FIFO urządzenia, które wysyła dane do portu szeregowego.

Kontrola przepływu będzie czekać, aż określona liczba bajtów znajdzie się w buforze, zanim procesor wyśle komunikat lub sygnał do drugiego urządzenia, aby zatrzymać transmisję. Przy wyższych szybkościach transmisji port szeregowy otrzyma kilka bajtów od momentu osiągnięcia poziomu kontroli nici bufora i przestanie nadawać.

Te dodatkowe bajty będą większe, jeśli proces o wysokim priorytecie kontroluje procesor docelowy w czasie rzeczywistym. Ponieważ proces przepełnienia bufora portu komunikacyjnego ma wyższy priorytet niż przerwanie VISA, procesor nie podejmie żadnej akcji do czasu zakończenia takiego procesu w czasie rzeczywistym.

Domyślnym ustawieniem VISA i Windows dla 16-bajtowego FIFO jest 14 bajtów, co pozostawia 2 bajty w FIFO, gdy urządzenie próbuje wysłać wiadomość ze źródła. Przy większych szybkościach transmisji możliwe jest, że wolniejsze komputery otrzymają więcej niż 4 bajty w czasie, gdy port szeregowy jest odpytywany przez procesor, wysyłając sygnał końca transmisji.

Aby rozwiązać problem po wykryciu przepełnienia bufora stosu w systemie Windows 10, należy otworzyć Menedżera urządzeń. Następnie znajdź port COM, dla którego zmieniasz ustawienia i otwórz jego właściwości. Następnie klikamy na zakładkę "Advanced", pojawi się suwak, który zmienia wielkość przepełnienia bufora, aby UART mógł szybciej włączyć kontrolę przepływu.

Wartość domyślna jest wystarczająca w większości przypadków. Jeśli jednak wystąpi błąd przepełnienia, należy zmniejszyć. Spowoduje to wysłanie większej ilości przerwań do procesora przy spowolnionych bajtach w UART.

Bezpieczne praktyki rozwojowe

Bezpieczne metody rozwoju

Metody bezpiecznego rozwoju obejmują regularne testowanie w celu wykrycia i skorygowania przepełnień. Najbardziej wiarygodny Sposobem na uniknięcie lub zapobieżenie temu jest zastosowanie automatycznej ochrony na poziomie języka. Inną poprawką jest runtime boundary check, która zapobiega przepełnieniom poprzez automatyczne sprawdzanie, czy dane zapisywane do bufora mieszczą się w dopuszczalnych granicach.

Usługa w chmurze Veracode wykrywa luki w kodzie, takie jak przepełnienia bufora, dzięki czemu programiści naprawiają je zanim zostaną wykorzystane. Unikalna w branży, opatentowana technologia binarnego statycznego testowania bezpieczeństwa aplikacji (SAST) analizuje je, w tym komponenty open-source i innych firm, bez konieczności uzyskiwania do nich dostępu.

SAST uzupełnia modelowanie zagrożeń i przeglądy kodu wykonywane przez programistów poprzez wykrywanie błędów i pominięć w kodzie szybciej i taniej dzięki automatyzacji. Jest on zazwyczaj uruchamiany na początku cyklu życia oprogramowania, ponieważ łatwiej i mniej kosztownie jest naprawić problemy przed wdrożeniem produkcyjnym.

SAST identyfikuje krytyczne luki, takie jak SQL injection, cross-site scripting (XSS), błąd przepełnienia bufora, surowe warunki błędów i potencjalne zakamarki. Dodatkowo, binarna technologia SAST dostarcza użytecznych informacji, które nadają priorytety w zależności od stopnia ciężkości i zapewniają szczegółowe instrukcje w sprawie łatania.

Podatność na przepełnienie istnieje od prawie 3 dekad, ale wciąż jest uciążliwa. Hakerzy na całym świecie nadal uważają to za swoją domyślną taktykę ze względu na samą liczbę podatnych aplikacji internetowych. Deweloperzy i programiści poświęcają wiele wysiłku, aby zwalczyć tę informatyczną plagę, wymyślając coraz więcej sposobów.

Podstawową ideą tego ostatniego podejścia jest zaimplementowanie narzędzia naprawczego, które wykonuje wiele kopii adresu zwrotnego na stosie, a następnie randomizuje położenie wszystkich kopii oprócz liczby. Wszystkie duplikaty są aktualizowane i sprawdzane równolegle, tak że każda niezgodność między nimi wskazuje na możliwą próbę ataku i wywołuje wyjątek.

Artykuły na ten temat