Dlaczego tak trudno jest uczynić C mniej podatnym na przepełnienia bufora?

23

Robię kurs na studiach, gdzie jednym z laboratoriów jest przeprowadzanie exploitów związanych z przepełnieniem buforu na dostarczanym przez nas kodzie. Dotyczy to zarówno prostych exploitów, jak zmiana adresu zwrotnego funkcji na stosie, aby powrócić do innej funkcji, aż do kodu zmieniającego rejestr / stan pamięci programów, ale następnie powraca do funkcji, którą wywołałeś, co oznacza, że wywołana funkcja jest całkowicie nieświadoma tego exploita.

Zrobiłem trochę badań nad tym, a tego rodzaju exploity są używane prawie wszędzie, nawet teraz, w takich rzeczach jak uruchamianie homebrew na Wii i niezwiązany jailbreak dla iOS 4.3.1

Moje pytanie brzmi: dlaczego tak trudno jest rozwiązać ten problem? To oczywiste, że jest to jeden z głównych exploitów używanych do hakowania setek rzeczy, ale wydaje się, że byłoby to dość łatwe do naprawienia po prostu obcinając dane wejściowe poza dozwoloną długość i po prostu odkażając wszystkie dane, które pobierasz.

EDYCJA: Kolejna perspektywa, którą chciałbym wziąć pod uwagę odpowiedzi - dlaczego twórcy C nie naprawiają tych problemów poprzez ponowne wdrożenie bibliotek?

Ankit Soni
źródło

Odpowiedzi:

35

Naprawili biblioteki.

Wszelkie nowoczesny C biblioteka standardowa zawiera bezpieczniejsze warianty strcpy, strcat, sprintf, i tak dalej.

W systemach C99 - czyli w większości Uniksów - znajdziesz je z nazwami takimi jak strncati snprintf, „n” oznacza, że ​​potrzeba argumentu o rozmiarze bufora lub maksymalnej liczby elementów do skopiowania.

Tych funkcji można używać do bezpieczniejszego wykonywania wielu operacji, ale z perspektywy czasu ich użyteczność nie jest świetna. Na przykład niektóre snprintfimplementacje nie gwarantują, że bufor zostanie zakończony zerem. strncatkopiuje wiele elementów, ale wiele osób błędnie podaje rozmiar bufora docelowego.

W systemie Windows, jeden często znajdzie strcat_s, sprintf_sThe "_s" przyrostek wskazujący "bezpieczne". Te również znalazły drogę do standardowej biblioteki C w C11 i zapewniają większą kontrolę nad tym, co dzieje się w przypadku przepełnienia (na przykład obcięcie vs. aser).

Wielu dostawców zapewnia jeszcze więcej niestandardowych alternatyw, takich jak asprintfGNU libc, które automatycznie przydzielą bufor o odpowiednim rozmiarze.

Pomysł, że można „po prostu naprawić C”, jest nieporozumieniem. Naprawienie C nie jest problemem - i zostało już zrobione. Problem polega na naprawianiu dekad kodu C napisanego przez nieświadomych, zmęczonych lub spieszących się programistów lub kodu przeniesionego z kontekstów, w których bezpieczeństwo nie miało znaczenia, do kontekstów, w których bezpieczeństwo ma znaczenie. Żadne zmiany w standardowej bibliotece nie mogą naprawić tego kodu, chociaż migracja do nowszych kompilatorów i standardowych bibliotek może często pomóc w automatycznej identyfikacji problemów.


źródło
11
+1 za skierowanie problemu na programistów, a nie język.
Nicol Bolas,
8
@Nicol: Mówienie „problem [jest] programistami” jest niesprawiedliwie redukcjonistyczne. Problem polega na tym, że przez lata (dziesięciolecia) C ułatwiał pisanie niebezpiecznego kodu niż bezpiecznego, zwłaszcza że nasza definicja „bezpiecznego” ewoluowała szybciej niż jakikolwiek standard językowy i ten kod wciąż istnieje. Jeśli chcesz spróbować sprowadzić to do pojedynczego rzeczownika, problemem jest „1970-1999 libc”, a nie „programiści”.
1
Programiści nadal muszą korzystać z narzędzi, które mają teraz, aby rozwiązać te problemy. Poświęć około pół dnia i przeszukuj kod źródłowy tych rzeczy.
Nicol Bolas,
1
@Nicol: Chociaż banalne jest wykrycie potencjalnego przepełnienia bufora, często nie jest trywialne mieć pewność, że jest to realne zagrożenie, a mniej trywialne jest ustalenie, co powinno się stać, jeśli bufor zostanie kiedykolwiek przepełniony. Obsługa błędów jest / często nie była brana pod uwagę, nie jest możliwe „szybkie” wdrożenie ulepszenia, ponieważ można zmienić zachowanie modułu w nieoczekiwany sposób. Zrobiliśmy to właśnie w wielomilionowej bazie kodu starszego typu, i chociaż ćwiczenie to było czasochłonne, kosztowało dużo czasu (i pieniędzy).
mattnz
4
@NicolBolas: Nie wiesz, jaki rodzaj sklepu ty pracujesz, ale ostatnie miejsce pisałem C do użytku produkcyjnego wymagane zmieniającej szczegółowy projekt dokumentu, przeglądania, zmiany kodu, zmieniające plan testów, przegląd planu testów, wykonując kompletne test systemu, przegląd wyników testu, a następnie ponowna certyfikacja systemu u klienta. Dotyczy to systemu telekomunikacyjnego na innym kontynencie napisanego dla firmy, która już nie istnieje. Ostatnio wiedziałem, że źródło znajduje się w archiwum RCS na taśmie QIC, która nadal powinna być czytelna, jeśli można znaleźć odpowiedni napęd taśmowy.
TMN
19

Stwierdzenie, że C jest „podatne na błędy” z założenia , wcale nie jest niedokładne . Oprócz poważnych błędów, takich jak getsjęzyk C, tak naprawdę nie może być inaczej, bez utraty głównej funkcji, która przede wszystkim przyciąga ludzi do C.

C został zaprojektowany jako język systemowy, który działa jako rodzaj „przenośnego zestawu”. Główną cechą języka C jest to, że w przeciwieństwie do języków wyższego poziomu, kod C często bardzo ściśle odwzorowuje na rzeczywisty kod maszynowy. Innymi słowy, ++izwykle jest to tylko incinstrukcja i często można uzyskać ogólne pojęcie o tym, co procesor będzie robił w czasie wykonywania, patrząc na kod C.

Ale dodanie kontroli niejawnych granic wiąże się z dodatkowym obciążeniem - narzut, o który programista nie prosił i może nie chciał. Ten narzut wykracza znacznie poza dodatkowe miejsce do przechowywania wymaganej długości każdej tablicy lub dodatkowe instrukcje sprawdzania granic tablicy przy każdym dostępie do tablicy. Co z arytmetyką wskaźników? A co jeśli masz funkcję, która przyjmuje wskaźnik? Środowisko wykonawcze nie ma możliwości sprawdzenia, czy wskaźnik ten mieści się w granicach prawidłowo przydzielonego bloku pamięci. Aby to śledzić, potrzebna byłaby poważna architektura środowiska wykonawczego, która może sprawdzić każdy wskaźnik względem tabeli aktualnie przydzielonych bloków pamięci, w tym momencie już wkraczamy na terytorium wykonawcze zarządzane w stylu Java / C #.

Charles Salvia
źródło
12
Szczerze mówiąc, kiedy ludzie pytają, dlaczego C nie jest „bezpieczny”, zastanawiam się, czy narzekaliby, że montaż nie jest „bezpieczny”.
Ben Brocka
5
Język C jest bardzo podobny do przenośnego zestawu na maszynie PDP-11 firmy Digital Equipment Corporation. Jednocześnie maszyny Burroughs miał granice tablicy sprawdzanie w CPU, więc oni naprawdę łatwo dostać się w programy kontroli tablicy w życiu sprzętowych na sprzętowo Rockwell Collins. (Głównie stosowanych w lotnictwie.)
Tim Williscroft
15

Myślę, że prawdziwym problemem nie jest to, że tego typu błędy są trudne do ustalenia, ale są one tak łatwe do wykonania: Jeśli używasz strcpy, sprintfi przyjaciół w (pozornie) najprostszy sposób, że praca może, to prawdopodobnie otworzył drzwi do przepełnienia bufora. I nikt tego nie zauważy, dopóki ktoś go nie wykorzysta (chyba że masz bardzo dobre recenzje kodu). Teraz należy dodać fakt, że istnieje wiele przeciętnych programistów i że są one pod presją czasu większość czasu - i masz przepis na kodzie, który jest tak podziurawiony z przepełnienia bufora, że to będzie trudno naprawić je wszystkie po prostu dlatego, że nie ma tylu z nich i tak dobrze się ukrywają.

nikie
źródło
3
Naprawdę nie potrzebujesz „bardzo dobrych recenzji kodu”. Musisz tylko zablokować sprintf lub ponownie # zdefiniować sprintf do czegoś, co używa sizeof () i błędów w rozmiarze wskaźnika, itp. Nie potrzebujesz nawet recenzji kodu, możesz robić takie rzeczy za pomocą SCM commit haki i grep.
1
@JoeWreschnig: ogólnie sizeof(ptr)wynosi 4 lub 8. To kolejne ograniczenie C: nie ma sposobu, aby określić długość tablicy, biorąc pod uwagę tylko wskaźnik do niej.
MSalters
@MSalters: Tak, tablica int [1] lub char [4] lub cokolwiek, co może być fałszywie pozytywne, ale w praktyce nigdy nie obsługujesz buforów tej wielkości za pomocą tych funkcji. (Nie mówię tu teoretycznie - pracowałem nad dużą bazą kodu C przez cztery lata, która korzystała z tego podejścia. Nigdy nie dotarłem do ograniczenia sprintu do znaku [4].)
5
@BlackJack: Większość programistów nie jest głupia - jeśli zmusisz ich do przekroczenia rozmiaru, przejdą właściwy. To po prostu większość nie przekroczy rozmiaru, chyba że jest do tego zmuszony. Możesz napisać makro, które zwróci długość tablicy, jeśli ma rozmiar statyczny lub automatyczny, ale błędy, jeśli otrzyma wskaźnik. Następnie # definiujesz sprintf, aby wywołać snprintf z tym makrem podającym rozmiar. Masz teraz wersję sprintf, która działa tylko na tablicach o znanych rozmiarach i zmusza programistę do wywołania snprintf o ręcznie określonym rozmiarze w innym przypadku.
1
Jednym prostym przykładem takiego makra byłoby #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))wywołanie podziału przez zero w czasie kompilacji. Kolejnym sprytnym, który po raz pierwszy zobaczyłem w Chromium, jest #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0]))wymiana garstki fałszywych trafień na niektóre fałszywe negatywy - niestety jest bezużyteczna dla char []. Możesz użyć różnych rozszerzeń kompilatora, aby uczynić go jeszcze bardziej niezawodnym, np . Blogs.msdn.com/b/ce_base/archive/2007/05/08/… .
7

Naprawienie przepełnienia bufora jest trudne, ponieważ C nie zapewnia praktycznie żadnych przydatnych narzędzi do rozwiązania problemu. Jest to podstawowa wada językowa polegająca na tym, że natywne bufory nie zapewniają ochrony i praktycznie, jeśli nie całkowicie, niemożliwe jest zastąpienie ich lepszym produktem, takim jak C ++ z std::vectori std::array, i trudno jest nawet w trybie debugowania znaleźć przepełnienia bufora.

DeadMG
źródło
13
„Wada językowa” to strasznie stronnicze twierdzenie. To, że biblioteki nie zapewniały sprawdzania granic, było wadą; że język nie był świadomym wyborem, aby uniknąć kosztów ogólnych. Ten wybór jest częścią tego, co pozwala std::vectorna wydajne wdrażanie konstrukcji wyższego poziomu . I vector::operator[]sprawia, że sam wybór dla prędkości nad bezpieczeństwem. Bezpieczeństwo vectorpolega na tym, że łatwiej jest przewozić wokół wielkości, co jest tym samym podejściem, jakie stosują współczesne biblioteki C.
1
@Charles: „C po prostu nie zapewnia żadnego dynamicznie rozwijającego się bufora jako części standardowej biblioteki”. Nie, to nie ma z tym nic wspólnego. Po pierwsze, C zapewnia je przez realloc(C99 pozwala również na sortowanie tablic stosów przy użyciu ustalonego w czasie wykonywania, ale stałego rozmiaru za pomocą dowolnej zmiennej automatycznej, prawie zawsze preferowanej char buf[1024]). Po drugie, problem nie ma nic wspólnego z rozszerzaniem buforów, ma związek z tym, czy bufory niosą ze sobą rozmiar i sprawdzają ten rozmiar, kiedy uzyskujesz do nich dostęp.
5
@Joe: Problem nie polega na tym, że rodzime tablice są zepsute. Chodzi o to, że nie można ich wymienić. Na początek vector::operator[]wykonuje sprawdzanie granic w trybie debugowania - coś, co nie może zrobić natywne tablice - i po drugie, w C nie ma możliwości zamiany natywnego typu tablicy na taki, który może sprawdzać granice, ponieważ nie ma szablonów ani operatora przeciążenie. W C ++, jeśli chcesz przejść od T[]do std::array, można praktycznie tylko zamienić się typedef. W C nie ma sposobu na osiągnięcie tego i nie ma sposobu na napisanie klasy o równoważnej funkcjonalności, nie mówiąc już o interfejsie.
DeadMG
3
@Joe: Tyle że nigdy nie można go statycznie określić i nigdy nie można go uczynić ogólnym. To niemożliwe, aby napisać dowolnej biblioteki C, która realizuje tę samą rolę, że std::vector<T>i std::array<T, N>zrobić w C ++. Nie byłoby sposobu zaprojektowania i określenia żadnej biblioteki, nawet standardowej, która mogłaby to zrobić.
DeadMG
1
Nie jestem pewien, co rozumiesz przez „nigdy nie można go statycznie zmierzyć”. Jak użyłbym tego terminu, std::vectornigdy też nie można go statycznie określić. Jeśli chodzi o rodzajowe, możesz uczynić go tak ogólnym, jak potrzebuje tego dobry C - niewielka liczba podstawowych operacji na void * (dodawanie, usuwanie, zmiana rozmiaru) i wszystko inne napisane specjalnie. Jeśli zamierzasz narzekać, że C nie ma generycznych stylów w C ++, jest to znacznie poza zakresem bezpiecznej obsługi bufora.
7

Problem nie jest z C języku .

IMO, główną przeszkodą do pokonania jest to, że C jest po prostu źle nauczany . Dziesięciolecia złych praktyk i złych informacji zostały zinstytucjonalizowane w podręcznikach i notatkach z wykładów, co od samego początku zatruwało umysły każdego nowego pokolenia programistów. Studenci mają krótki opis z „Easy” funkcji I / O jak gets1 lub scanfa następnie w lewo do własnych urządzeń. Nie powiedziano im, gdzie i jak te narzędzia mogą zawieść, ani jak zapobiegać tym awariom. Nie powiedziano im o używaniu fgetsistrtol/strtodponieważ są one uważane za „zaawansowane” narzędzia. Następnie zostają uwolnieni w świecie zawodowym, aby siać spustoszenie. Nie, że wielu bardziej doświadczonych programistów wie lepiej, ponieważ otrzymali taką samą edukację uszkodzoną przez mózg. To szaleje. Widzę tak wiele pytań tutaj i na Stack Overflow oraz na innych stronach, gdzie jest jasne, że osoba zadająca pytanie jest nauczana przez kogoś, kto po prostu nie wie, o czym mówi , i oczywiście nie można po prostu powiedzieć „twój profesor się myli”, ponieważ jest profesorem, a ty jesteś tylko facetem w Internecie.

A potem masz tłum, który gardzi każdą odpowiedzią zaczynającą się od „cóż, zgodnie ze standardem językowym ...”, ponieważ pracują w prawdziwym świecie i według nich standard nie ma zastosowania do prawdziwego świata . Mogę poradzić sobie z kimś, kto ma po prostu złe wykształcenie, ale każdy, kto nalega na bycie ignorantem, jest po prostu zarazy dla branży.

Nie byłoby problemów z przepełnieniem bufora, gdyby język był nauczany poprawnie, z naciskiem na pisanie bezpiecznego kodu. To nie jest „trudne”, nie jest „zaawansowane”, to po prostu ostrożność.

Tak, to był rant.


1 Które, na szczęście, zostało w końcu wyrwane ze specyfikacji językowej, chociaż na zawsze pozostanie w posiadaniu 40-letniego kodu.

John Bode
źródło
1
Chociaż w większości się z tobą zgadzam, myślę, że nadal jesteś trochę niesprawiedliwy. To, co uważamy za „bezpieczne”, jest także funkcją czasu (i widzę, że jesteś profesjonalnym programistą znacznie dłużej ode mnie, więc jestem pewien, że znasz się na tym). Za dziesięć lat ktoś będzie miał tę samą rozmowę na temat tego, dlaczego, do cholery, wszyscy w 2012 roku korzystali z implementacji tabeli skrótów obsługującej DoS, czy nie wiedzieliśmy nic o bezpieczeństwie? Jeśli istnieje problem w nauczaniu, jest to problem, który zbytnio koncentrujemy na nauczaniu „najlepszej” praktyki, a nie, że sama najlepsza praktyka ewoluuje.
1
I bądźmy szczerzy. Ty mógł pisać bezpieczny kod z właśnie sprintf, ale to nie znaczy, że język nie była błędna. C był wadliwy i jest wadliwy - jak każdy język - i ważne jest, abyśmy przyznali się do tych wad, abyśmy mogli je nadal naprawiać.
@JoeWreschnig - Chociaż zgadzam się z większym punktem, myślę, że istnieje jakościowa różnica między implementacjami tabeli skrótów obsługującymi DoS a przepełnieniem bufora. Pierwszy z nich można przypisać okolicznościom, które się wokół ciebie zmieniają, ale drugi nie ma wymówek; przepełnienia bufora to błędy kodowania, kropka. Tak, C nie ma osłon ostrzy i cię cię, jeśli będziesz nieostrożny; możemy spierać się o to, czy jest to wada w języku, czy nie. To jest prostopadła do faktu, że bardzo niewiele studenci otrzymują żadnej instrukcji bezpieczeństwa, kiedy uczysz się języka.
John Bode
5

Problem dotyczy zarówno krótkowzroczności kierowniczej, jak i niekompetencji programisty. Pamiętaj, że aplikacja 90 000 linii potrzebuje tylko jednej niepewnej operacji, aby być całkowicie niepewną. Jest prawie niemożliwe, że każda aplikacja napisana na podstawie zasadniczo niepewnej obsługi łańcucha będzie w 100% doskonała - co oznacza, że ​​będzie niepewna.

Problem polega na tym, że koszty związane z niepewnością albo nie są naliczane właściwemu adresatowi (firma sprzedająca aplikację prawie nigdy nie będzie musiała zwracać ceny zakupu), albo nie są wyraźnie widoczne w momencie podejmowania decyzji („Musimy wysłać w marcu bez względu na wszystko! ”). Jestem całkiem pewien, że gdybyś uwzględnił długoterminowe koszty i koszty dla swoich użytkowników, a nie dla zysków Twojej firmy, pisanie w C lub językach pokrewnych byłoby znacznie droższe, prawdopodobnie tak drogie, że w wielu przypadkach jest to zły wybór dziedziny, w których obecnie konwencjonalna mądrość mówi, że jest to konieczność. Ale to się nie zmieni, chyba że wprowadzona zostanie znacznie surowsza odpowiedzialność za oprogramowanie - czego nikt w branży nie chce.

Kilian Foth
źródło
-1: Obwinianie zarządzania jako źródła wszelkiego zła nie jest szczególnie konstruktywne. Trochę mniej ignorując historię. Odpowiedź jest prawie wykorzystana w ostatnim zdaniu.
mattnz
Bardziej rygorystyczna odpowiedzialność za oprogramowanie może zostać wprowadzona przez użytkowników zainteresowanych bezpieczeństwem i gotowych za to zapłacić. Prawdopodobnie można go wprowadzić, nakładając surowe kary za naruszenia bezpieczeństwa. Rozwiązanie rynkowe działałoby, gdyby użytkownicy byli gotowi zapłacić za bezpieczeństwo, ale tak nie jest.
David Thornley,
4

Jedną z wielkich mocy używania C jest to, że pozwala on manipulować pamięcią w dowolny sposób, jaki uznasz za stosowny.

Jedną z największych słabości używania C jest to, że pozwala on manipulować pamięcią w dowolny sposób, jaki uznasz za odpowiedni.

Istnieją bezpieczne wersje wszelkich niebezpiecznych funkcji. Jednak programiści i kompilator nie wymuszają ściśle ich użycia.

Sardathrion - Przywróć Monikę
źródło
2

dlaczego twórcy C nie naprawiają tych problemów poprzez ponowne wdrożenie bibliotek?

Prawdopodobnie dlatego, że C ++ już to zrobił i jest wstecznie kompatybilny z kodem C. Więc jeśli chcesz mieć bezpieczny typ łańcucha w swoim kodzie C, po prostu użyj std :: string i napisz swój kod C za pomocą kompilatora C ++.

Podsystem pamięci podstawowej może pomóc w zapobieganiu przepełnieniu bufora, wprowadzając bloki ochronne i sprawdzanie ich poprawności - tak więc wszystkie alokacje mają dodane 4 bajty „fefefefe”, gdy te bloki są zapisywane, system może wyrzucić wobler. Nie ma gwarancji, że zapobiegnie zapisowi w pamięci, ale pokaże, że coś poszło nie tak i musi zostać naprawione.

Myślę, że problem polega na tym, że stare procedury strcpy itp. Są nadal obecne. Gdyby zostały usunięte na rzecz strncpy itp., To by pomogło.

gbjbaanb
źródło
1
Całkowite usunięcie strcpy itp. Spowodowałoby, że przyrostowe ścieżki aktualizacji byłyby jeszcze trudniejsze, co z kolei spowodowałoby, że ludzie wcale się nie aktualizują. W ten sposób możesz teraz przejść na kompilator C11, następnie zacząć używać wariantów _s, następnie zablokować warianty inne niż _s, a następnie naprawić istniejące użycie, niezależnie od okresu, który jest praktycznie wykonalny.
-2

Łatwo jest zrozumieć, dlaczego problem przepełnienia nie został rozwiązany. C był wadliwy w kilku obszarach. W tym czasie te wady były postrzegane jako tolerowane, a nawet jako cecha. Teraz dekady później tych wad nie da się naprawić.

Niektóre części społeczności programistów nie chcą, aby te dziury zostały zatkane. Wystarczy spojrzeć na wszystkie wojny płomieni, które zaczynają się od ciągów znaków, tablic, wskaźników, wywozu śmieci ...

mhoran_psprep
źródło
5
LOL, okropna i błędna odpowiedź.
Heath Hunnicutt
1
Aby wyjaśnić, dlaczego jest to zła odpowiedź: C rzeczywiście ma wiele wad, ale zezwolenie na przepełnienie bufora itp. Ma bardzo niewiele wspólnego z nimi, ale z podstawowymi wymaganiami językowymi. Nie byłoby możliwe zaprojektowanie języka do wykonania zadania języka C i niedopuszczenie do przepełnienia bufora. Części społeczności nie chcą rezygnować z możliwości, na jakie pozwala im C, często z uzasadnionego powodu. Istnieją również spory dotyczące sposobu uniknięcia niektórych z tych problemów, co pokazuje, że nie mamy pełnego zrozumienia projektowania języka programowania, nic więcej.
David Thornley,
1
@DavidThornley: Można zaprojektować język do zrobienia pracę c, ale zrobić to tak, że normalne idiomatyczne sposoby robienia rzeczy, które przynajmniej pozwolić kompilator sprawdzić przepełnienia bufora dość sprawnie, powinna kompilator wybrać, aby to zrobić. Istnieje ogromna różnica między posiadaniem memcpy()a posiadaniem go jako standardowego sposobu efektywnego kopiowania segmentu macierzy.
supercat