Pytanie:
Dlaczego programy napisane w C i C ++ są tak często podatne na ataki przepełnienia?
Nzall
2016-02-23 20:37:56 UTC
view on stackexchange narkive permalink

Kiedy patrzę na exploity z ostatnich kilku lat związane z implementacjami, widzę, że całkiem sporo z nich pochodzi z C lub C ++, a wiele z nich to ataki przepełnienia.

  • Heartbleed było przepełnieniem bufora w OpenSSL;
  • Niedawno znaleziono błąd w glibc, który zezwalał na przepełnienia bufora podczas rozwiązywania DNS;

to tylko te, o których myślę teraz wyłączone, ale wątpię, czy były to jedyne, które A) dotyczą oprogramowania napisanego w C lub C ++ i B) są oparte na przepełnieniu bufora.

Szczególnie jeśli chodzi o błąd glibc, przeczytałem komentarz, który stwierdza, że ​​gdyby stało się to w JavaScript zamiast w C, nie byłoby problemu. Nawet gdyby kod był tylko skompilowany do Javascript, nie stanowiłoby to problemu.

Dlaczego C i C ++ są tak podatne na ataki przepełnienia?

Z dużą mocą przychodzi duża odpowiedzialność
[Ta odpowiedź] (http://security.stackexchange.com/questions/95245/security-implications-of-neglecting-the-extra-byte-for-null-termination-in-cc/95248#95248) i [this answer] (http://security.stackexchange.com/questions/82750/why-are-buffer-overflows-executed-in-the-direction-they-are/82846#82846) mogą być ciekawymi lekturami. Zasadniczo sprowadza się to do projektu języka i poziomu, w jakim został zaimplementowany.
@RoraΖ istnieją jednak narzędzia do kompilacji C do javascript, takie jak emscripten. http://dankaminsky.com/2016/02/20/skeleton/, mam na myśli u dołu.
Twoje pytanie jest w rodzaju „Dlaczego tylko komputery z systemem Windows otrzymują wirusy Windows?”. Ponieważ wirusy Windows są możliwe tylko na komputerach z systemem Windows. C i C ++ uzyskują luki w przepełnieniu bufora z ich zdolności do wykonywania niezaznaczonych arytmetyki wskaźników. Większość innych języków nie ma tej możliwości, a zatem nie może mieć przepełnienia bufora. Twoje pytanie również nie uwzględnia popularności tych języków. (Być może inne języki są BARDZIEJ problematyczne, ale nie są używane tak często, więc mają mniej całkowitych luk w zabezpieczeniach).
Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została [przeniesiona do czatu] (http://chat.stackexchange.com/rooms/36311/discussion-on-question-by-nate-kerkhofs-why-are-programs-written-in-c-and- c-tak).
W C ++ jednym z powodów przepełnienia bufora jest niepowodzenie we współczesnym C ++ i ignorowanie bardziej bezpiecznych koncepcji, takich jak STL. Jeśli używasz C ++ tak jak C, dostaniesz to, na co zasługujesz.
Wcześniej był genialny komentarz, który najwyraźniej został usunięty: „To dlatego, że skalpel tnie więcej niż nożyczki bezpieczeństwa”
C / C ++ jest również najczęściej używany w przypadku oprogramowania, które jest narażone na największe ryzyko i ataki.
Heartbleed był dla mnie wynikiem dwóch złych praktyk w programowaniu: 1. Użycie instrukcji goto i 2. brak standardu programowania typu „Zabroń używania instrukcji„ if ”bez nawiasów klamrowych”.W takim przypadku to się nie stanie.Brak testów jednostkowych jest prawdopodobnie kolejnym powodem ... Zgadzam się, że C i C ++ są bardziej podatne na ten atak, ponieważ są językiem dość niskiego poziomu i często to programista jest odpowiedzialny za zapobieganie złemu użyciu.
Osiem odpowiedzi:
Thomas Pornin
2016-02-23 20:48:25 UTC
view on stackexchange narkive permalink

C i C ++, w przeciwieństwie do większości innych języków, tradycyjnie nie sprawdzają przepełnień. Jeśli kod źródłowy mówi, aby umieścić 120 bajtów w 85-bajtowym buforze, procesor z radością to zrobi. Jest to związane z faktem, że podczas gdy C i C ++ mają pojęcie tablica , to pojęcie to dotyczy tylko czasu kompilacji. W czasie wykonywania dostępne są tylko wskaźniki, więc nie ma metody sprawdzania dostępu do tablicy w odniesieniu do koncepcyjnej długości tej tablicy.

W przeciwieństwie do tego, w większości innych języków istnieje pojęcie tablicy, które przetrwa w czasie wykonywania, więc wszystkie dostępy do tablicy mogą być systematycznie sprawdzane przez system wykonawczy. Nie eliminuje to przepełnień: jeśli kod źródłowy prosi o coś bezsensownego, jak zapis 120 bajtów w tablicy o długości 85, nadal nie ma to sensu. Jednak powoduje to automatyczne wywołanie stanu błędu wewnętrznego (często „wyjątku”, np. Wyjątku ArrayIndexOutOfBoundException w języku Java), który przerywa normalne wykonywanie i uniemożliwia kontynuowanie kodu. To zakłóca wykonanie i często implikuje zaprzestanie pełnego przetwarzania (wątek umiera), ale zwykle zapobiega eksploatacji wykraczającej poza zwykłą odmowę usługi.

Zasadniczo, exploity przepełnienia bufora wymagają od kodu przepełnienie (czytanie lub pisanie poza granicami bufora, do którego uzyskano dostęp) i aby kontynuować działania poza tym przepełnieniem. Większość współczesnych języków, w przeciwieństwie do C i C ++ (i kilku innych, takich jak Forth lub Assembly), nie pozwala na wystąpienie przepełnienia i zamiast tego strzela do sprawcy. Z punktu widzenia bezpieczeństwa jest to znacznie lepsze.

* „Z punktu widzenia bezpieczeństwa jest to o wiele lepsze.” * Chociaż jest to z pewnością prawdą, znacznie utrudnia to również niektóre rodzaje programowania - w szczególności programowanie systemu operacyjnego. Pamiętaj, że dziedzictwo C wywodzi się z bycia językiem programowania zaprojektowanym do implementacji Uniksa w przenośny sposób; nie bez powodu C jest czasami określane jako ** przenośny asembler **.
Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została [przeniesiona do czatu] (http://chat.stackexchange.com/rooms/36312/discussion-on-answer-by-thomas-pornin-why-are-programs-written-in-c-and- c-so-f).
@MichaelKjörling To prawda, ale z drugiej strony istnieje wiele systemów operacyjnych Microsoft Research, które opierają się na całkowicie zarządzanym (iw ten sposób całkowicie bezpiecznym) kodzie, w tym na weryfikacji statycznej. Microsoft wydaje dużo pieniędzy na systematyczne naprawianie tego problemu, zamiast czekać, aż ludzie się rozbudzą. Jak zawsze: D Wydajność jest zawsze trudna, ale z drugiej strony masz mnóstwo możliwości optymalizacji za pomocą zarządzanego i odzwierciedlającego kodu kodu niż kiedykolwiek w przypadku asemblacji - w przypadku wielu programów serwerowych udało im się nawet uzyskać znaczny wzrost wydajności dzięki że.
@Luaan Wątpię, czy główna różnica polegała na przejściu od „zestawu” do „kodu zarządzanego”. Jeśli już, wydaje się, że bardziej prawdopodobne jest, że było to spowodowane przejściem z kodu nie-JITed do kodu JITed. W przypadku optymalizacji czasu kompilacji musisz wybrać najniższą linię bazową, którą chcesz obsługiwać. Dzięki kodowi JITed możesz zoptymalizować pod kątem konkretnej maszyny, na której pracujesz. Zasadniczo prawdopodobnie mógłbyś napisać kod JIT w C; Nie jestem pewien, czy ktoś tego próbował, chociaż ...
@MichaelKjörling W rzeczywistości wiele z nich nie jest skompilowanych w JIT. Jednak nadal są kompilowane dla określonej konfiguracji sprzętowej. Ale aby uzyskać jakikolwiek efekt w kompilacji JITted, potrzebujesz wielu dodatkowych informacji i wielu ograniczeń - C jest po prostu zbyt swobodny, aby umożliwić wiele znaczących optymalizacji nawet z kodu źródłowego, a tym bardziej skompilowanego kodu. Nawet coś tak prostego, jak sprawdzanie tych granic - nie ma sposobu, aby kompilator C sprawdzał granice za Ciebie, ponieważ manipulujesz tylko przypadkowymi wskaźnikami, o ile kompilator wie. To samo dotyczy bezpiecznego ich pominięcia.
DevSolar
2016-02-23 22:12:13 UTC
view on stackexchange narkive permalink

Zwróć uwagę, że istnieje pewna liczba argumentów cyklicznych: kwestie bezpieczeństwa są często powiązane z C i C ++. Ale ile z tego wynika z nieodłącznych słabości tych języków, a ile z tego, że są to po prostu języki, w których jest napisana większość infrastruktury komputerowej?


C ma być „o jeden krok wyżej od asemblera”. Nie ma żadnych ograniczeń sprawdzających poza tym, co sam zaimplementowałeś, aby wycisnąć ostatni cykl zegara z twojego systemu.

C ++ oferuje różne ulepszenia w stosunku do C, z których najbardziej istotne dla bezpieczeństwa są jego klasy kontenerów (np. <vector> i <string> ), a od C ++ 11, inteligentne wskaźniki, które pozwalają na obsługę danych bez konieczności ręcznej obsługi pamięci. Jednak ze względu na to, że jest ewolucją języka C zamiast całkowicie nowego języka, nadal także zapewnia mechanikę ręcznego zarządzania pamięcią w C, więc jeśli nalegasz na strzelanie do siebie stopę, C ++ nie robi nic, co by cię przed tym powstrzymało.


Dlaczego więc takie rzeczy jak SSL, bind lub jądra systemu operacyjnego są nadal pisane w tych językach?

Ponieważ te języki mogą modyfikować pamięć bezpośrednio, dzięki czemu są wyjątkowo dostosowane do określonego typu wysokowydajnych aplikacji niskiego poziomu (takich jak szyfrowanie, wyszukiwanie w tabelach DNS, sterowniki sprzętowe ... lub maszyny wirtualne Java, jeśli o to chodzi ;-)).

Tak więc, jeśli oprogramowanie związane z bezpieczeństwem zostanie naruszone, szansa , że zostanie ono napisane w C lub C ++ jest wysoka, po prostu dlatego, że większość programów związanych z bezpieczeństwem jest napisany w C lub C ++, zwykle ze względów historycznych i / lub wydajnościowych, a jeśli jest napisany w C / C ++, głównym wektorem ataku jest przepełnienie bufora.

Jeśli tak byłby innym językiem, byłby to inny kierunek ataku r, ale jestem pewien, że równie dobrze byłyby naruszenia bezpieczeństwa.


Wykorzystywanie oprogramowania C / C ++ jest łatwiejsze niż, powiedzmy, oprogramowanie Java. W ten sam sposób, w jaki eksploatacja systemu Windows jest łatwiejsza niż eksploatacja systemu Linux: ta pierwsza jest wszechobecna, dobrze zrozumiana (tj. Dobrze znane wektory ataku, jak je znaleźć i jak je wykorzystać), a wiele osób szuka exploitów, w których stosunek nagród do wysiłku jest wysoki.

Nie oznacza to, że ta ostatnia jest z natury bezpieczna (bezpieczna er , być może, ale nie bezpieczny ). Oznacza to, że - będąc trudniejszym celem z niższymi korzyściami - Bad Boys nie marnują jeszcze na to tyle czasu.

gnasher729
2016-02-23 21:46:35 UTC
view on stackexchange narkive permalink

Właściwie „zawodzenie” nie było przepełnieniem bufora. Aby uczynić rzeczy bardziej „wydajnymi”, umieścili wiele mniejszych buforów w jednym dużym buforze. Duży bufor zawierał dane od różnych klientów. Błąd odczytał bajty, których nie powinien odczytywać, ale w rzeczywistości nie czytał danych poza tym dużym buforem. Język, który sprawdzał przepełnienia bufora, nie zapobiegłby temu, ponieważ ktoś zszedł mu z drogi lub uniemożliwił takie sprawdzenie w celu znalezienia problemu.

IIRC, alokacja pamięci BSD * mogłaby * zapobiec niezauważeniu tego błędu, ale implementatorzy aktywnie omijali ten system, ponieważ uważali go za „zbyt wolny”. W pewnym sensie taki właśnie wybór dotyczy C / C ++, tyle że tym razem była to * naprawdę * zła decyzja. ;-)
W rzeczy samej. Jeśli zrobiłeś to w C #, możesz łatwo wprowadzić równoważny atak.
Czy profilowali swój kod przed podjęciem decyzji projektowej? Naprawdę trudno mi uwierzyć, że blokowali „malloc (3)”.
Alokacje pamięci @Kevin są stosunkowo powolnymi operacjami, szczególnie w porównaniu z jednorazowym przydzieleniem bufora i ponownym użyciem. Jeśli piszesz szybki kod (a rzeczy, których serwery internetowe muszą być szybkie, ponieważ ludzie narzekają), to tak, po usunięciu wszystkich innych wąskich gardeł może to łatwo stanowić wąskie gardło! Jest to bardzo prawdą, jeśli przydzielasz wiele małych buforów.
@Kevin: Użycie funkcji malloc () w odpowiedzi na dane otrzymane z niezaufanych źródeł sprawi, że kod będzie podatny na atakującego, który wyzwala wzorce alokacji / wydania, które spowodują fragmentację. Kod, który używa i odtwarza pule pamięci, może chronić przed takimi problemami w sposób, którego nie potrafi kod wykorzystujący malloc ().
@gbjbaanb: to naprawdę zależy od tego, co porównujemy. Pamiętam, jak czytałem, że "złota ścieżka" jemalloc miała na przykład około ~ 25 cykli. Ponieważ mówimy o bibliotece kryptograficznej, a krypto nie jest szczególnie szybkie (chyba że jest wspomagane sprzętem), myślę, że warto byłoby profilować. To powiedziawszy, wiele się zmieniło od czasu napisania tego kodu i wydaje mi się, że narzekali, że określone platformy są wolne (ale wprowadzili bufor dla wszystkich platform).
@MatthieuM.consider EASTL, implementacja STL dla gier, ponieważ zwykły STL ma system alokacji, który nie jest wystarczająco zoptymalizowany do użytku w grach. Jest jeden przykład z prawdziwego świata, gdzie alokacja pamięci była wąskim gardłem, więc nie jest tak „trudno w to uwierzyć”, jak sądził Kevin. OpenSSL może mieć takie same wymagania „tak szybko, jak to możliwe” lub może być źle zaprojektowanymi alokacjami pamięci WRT.
Viktor Toth
2016-02-24 07:49:51 UTC
view on stackexchange narkive permalink

Po pierwsze, jak wspominali inni, C / C ++ jest czasami określany jako gloryfikowany asembler makr: ma być „bliski żelazka”, jako język programowania na poziomie systemu.

Na przykład język pozwala mi zadeklarować tablicę o zerowej długości jako symbol zastępczy, podczas gdy w rzeczywistości może ona reprezentować sekcję o zmiennej długości w pakiecie danych lub początek regionu o zmiennej długości w pamięci, który jest używany do komunikować się z częścią sprzętu.

Niestety oznacza to również, że C / C ++ jest niebezpieczny w niepowołanych rękach; jeśli programista zadeklaruje tablicę 10 elementów, a następnie zapisze do elementu 101, kompilator z radością ją skompiluje, kod szczęśliwie wykona, usuwając wszystko, co znajduje się w tej lokalizacji pamięci (kod, dane, stos, kto wie). / p>

Po drugie, C / C ++ jest idiosynkratyczne. Dobrym przykładem są łańcuchy, które są w zasadzie tablicami znaków. Ale każda stała łańcuchowa ma dodatkowy, niewidoczny znak kończący. Stało się to przyczyną niezliczonych błędów, ponieważ (zwłaszcza, ale nie wyłącznie) początkujący programiści często nie przydzielają dodatkowego bajtu potrzebnego do kończenia wartości null.

Po trzecie, C / C ++ jest właściwie dość stary. Język powstał w czasie, gdy zewnętrzne ataki na system oprogramowania w zasadzie nie istniały. Od użytkowników oczekiwano zaufania i współpracy, a nie wrogości, ponieważ ich celem było sprawienie, aby program działał, a nie jego awaria.

Z tego powodu standardowa biblioteka C / C ++ zawiera wiele funkcji, które są z natury niebezpieczne. Weźmy na przykład strcpy (). Z przyjemnością skopiuje wszystko, aż do kończącego znaku null. Jeśli nie znajdzie kończącego znaku null, będzie kopiował dalej, aż piekło zamarznie, lub bardziej prawdopodobne, dopóki nie nadpisze czegoś ważnego i program się zawiesi. Nie stanowiło to problemu w starych dobrych czasach, kiedy od użytkownika nie oczekiwano, że wejdzie w pole zarezerwowane dla, powiedzmy, kodu pocztowego, 16000 śmieci, po których następuje specjalnie skonstruowany zestaw bajtów, które miały zostać wykonane po usunięciu stosu i wznowieniu wykonywania przez procesor pod niewłaściwym adresem.

Dla pewności, C / C ++ nie jest jedynym językiem idiosynkratycznym. Inne systemy mają inne idiosynkratyczne zachowanie, ale może być równie złe. Weźmy języki programowania zaplecza, takie jak PHP, i to, jak łatwo jest napisać kod, który pozwala na wstrzykiwanie SQL.

Ostatecznie, jeśli damy programistom potężne narzędzia, których potrzebują do wykonywania swojej pracy odpowiednie szkolenie i świadomość środowiska bezpieczeństwa, złe rzeczy zdarzają się bez względu na używany język programowania.

Potężne narzędzia potrzebne do * wydajnego * programowania. Ogólnie rzecz biorąc, bezpośredni dostęp do pamięci nie jest * konieczny *; zobacz prawie każdy inny język wysokiego poziomu.
„Ostatecznie, jeśli damy programistom potężne narzędzia, których potrzebują do wykonywania swojej pracy, ale bez odpowiedniego przeszkolenia i znajomości środowiska bezpieczeństwa, złe rzeczy zdarzają się bez względu na używany język programowania”. Złe rzeczy mogą się zdarzyć, gdy używany jest dowolny język programowania. Ale z powodów takich jak te, które wspaniale opisujesz w odniesieniu do C i C ++, mają one również tendencję (* tendencję *) do łatwiejszego i częstszego występowania, gdy niektóre są używane, a inne.
Co gorsza, standardy C w rzeczywistości nie pozwalają programistom pisać kodu „blisko metalu”. Jeśli kompilator może ustalić, że pewna kombinacja danych wejściowych doprowadziłaby do sytuacji, w których standard nie nakładałby żadnych wymagań, może pominąć kod, który w innym przypadku obsługiwałby takie dane wejściowe, nawet jeśli nie ma prawie żadnych innych prawdopodobnych konsekwencji dla Nieokreślonego zachowania, które być w przybliżeniu tak złe, jak pominięcie kodu w oparciu o wnioski dotyczące możliwych danych wejściowych.
Nie ma czegoś takiego jak „C / C ++”. Większość tego, o czym tutaj mówisz, dotyczy C.
** Wszystko ** jest specyficzne dla C, a nawet dla określonych implementacji. W C nie ma reguły, która mówi, że kompilator musi nawet ** zaakceptować ** próbę dostępu do 101-tego elementu tablicy 10-elementowej. Może przerwać kompilację, jeśli nie da się uniknąć tak zwanego niezdefiniowanego zachowania. Bardziej realistycznie, może po prostu założyć, że odpowiedni kod jest nieosiągalny z argumentu „main” i po prostu pominąć całą niewłaściwą funkcję.
@MSalters Nie jest specyficzne dla C, ponieważ dotyczy również C ++. Nie mogę zacząć rozumieć, jak myślisz, że kod C ++ nie może mieć przepełnienia bufora. Nawet `std :: vector :: operator []` nie sprawdza granic.
idiosynkratyczne - _adj._ Hack upon hack upon hack i hojna pomoc dla legacy: D
@immibis: W przeciwieństwie do tablic C, std :: vector zawsze ma swój własny rozmiar. _Może_ sprawdzać granice i faktycznie robi to z `.at (i)`. Jednak właśnie dlatego, że rozmiar jest tak wygodnie dostępny, sprawdzanie granic zwykle nie ma sensu.
@MSalters "To * może * sprawdzać granice" - Tak, a `operator []` (najbardziej naturalny sposób indeksowania wektora) nie.
@supercat, Przeczytałem twój komentarz trzy razy i nadal go nie rozumiem. Masz ochotę wyjaśnić mniej niż dziesięć oddzielnych zdań w jednym zdaniu? :RE
@Wildcard: Jeśli pewne dane wejściowe spowodowałyby wywołanie przez program niezdefiniowanego zachowania, standard nie nakłada żadnych ograniczeń dotyczących tego, co może zrobić wygenerowany kod, jeśli otrzyma takie dane wejściowe. Na przykład, biorąc pod uwagę `int * p, * q`, jeśli program testuje` if (p> q) ... `, kompilator byłby uprawniony do wnioskowania, że ​​kod nigdy nie otrzyma danych wejściowych, które spowodowałyby wykonanie tego testu chyba że „p” i „q” są częścią tego samego obiektu. Nawet jeśli instrukcje, których platforma użyłaby do normalnych porównań wskaźników, definiowałyby globalnie spójny ranking dla wszystkich wskaźników, nie ma sposobu zdefiniowanego przez Standard ...
... aby program mógł to wykorzystać. W 1990 r. Wielu kompilatorów zapewniłoby spójne i użyteczne zachowania w wielu okolicznościach, w których norma nie nakładała żadnych wymagań; mimo że norma nigdy nie uznawała takich zachowań, programiści nie widzieli potrzeby posiadania takiego upoważnienia, że ​​kompilatory muszą robić to, co już robili. Niestety, dziwaczna forma historycznego rewizjonizmu zaatakowała rozwój kompilatora C, promując przekonanie, że zachowania, których kompilatory nie dokumentują wyraźnie, ponieważ były tak powszechne, że nie warto o nich wspominać ...
... nigdy nie były naprawdę ważne. Zwolennicy tej filozofii sugerują, że gdyby istniała potrzeba dyrektyw zezwalających wskaźnikom na aliasy rzeczy różnych typów, byłby popyt na to, ignorując fakt, że programiści piszą kod wymagający aliasingu, a kompilatory akceptują taki kod i uruchamiają go prawidłowo. Pomysł, że nie ma popytu na rzeczy, które programiści i kompilatory rutynowo robią / robią, jest dziwny.
C. M.
2016-02-26 05:53:14 UTC
view on stackexchange narkive permalink

Prawdopodobnie poruszyłem kilka kwestii, na które niektóre z innych odpowiedzi już się udzieliły ... ale ... samo pytanie jest błędne i „wrażliwe”.

Zgodnie z pytaniem, pytanie zakłada bez zrozumienia podstawowych problemów. C / C ++ nie są bardziej podatne na ataki niż inne języki. Raczej oddają większość mocy urządzeń komputerowych i odpowiedzialność za ich używanie bezpośrednio w ręce programisty. Tak więc rzeczywistość jest taka, że ​​wielu programistów pisze kod podatny na eksploatację, a ponieważ C / C ++ nie robi zbyt wiele, aby chronić programistę przed sobą, tak jak robią to niektóre języki, ich kod jest bardziej podatny na ataki. To nie jest problem C / C ++, ponieważ na przykład programy napisane w asemblerze miałyby te same problemy.

Powód, dla którego takie niskopoziomowe programowanie może być tak podatne na ataki Dzieje się tak, ponieważ wykonywanie takich czynności jak sprawdzanie granic tablicy / bufora może być kosztowne obliczeniowo i bardzo często jest niepotrzebne podczas programowania defensywnego. Wyobraź sobie na przykład, że piszesz kod dla jakiejś dużej wyszukiwarki, która musi przetwarzać biliony rekordów bazy danych w mgnieniu oka, aby użytkownik końcowy nie był znudzony ani sfrustrowany podczas „ładowania strony ...” jest wyświetlany. Nie chcesz, aby Twój kod sprawdzał granice tablicy / bufora za każdym razem w pętli; Chociaż wykonanie takiej kontroli może zająć nanosekund, co jest trywialne, jeśli przetwarzasz tylko dziesięć rekordów, może dodać do wielu sekund lub minut, gdy przeglądasz miliardy lub biliony rekordów.

Zamiast tego „ufasz”, że źródło danych (na przykład „bot internetowy”, który skanuje witryny internetowe i umieszcza dane w bazie danych) już sprawdziło dane. Nie powinno to być nieracjonalne założenie; W przypadku typowego programu chcesz sprawdzić dane przy wejściu , aby kod przetwarzający dane mógł działać z maksymalną prędkością. Wiele bibliotek kodu również stosuje to podejście. Niektórzy nawet dokumentują, że spodziewają się, że programista sprawdził już dane przed wywołaniem funkcji bibliotecznych w celu wykonania danych.

Niestety, wielu programistów nie programuje defensywnie i po prostu zakłada, że ​​dane muszą być ważne i mieścić się w bezpiecznych granicach / parametrach. I to jest właśnie to, co jest wykorzystywane przez atakujących.

Niektóre języki programowania są zaprojektowane w taki sposób, że próbują chronić programistę przed takimi kiepskimi praktykami programistycznymi, automatycznie wprowadzając dodatkowe sprawdzenia do wygenerowanego programu, czego programista nie zrobił jawnie pisać do swojego kodu. Ponownie, jest to w porządku, gdy zamierzasz przeglądać kod tylko kilkaset razy lub mniej. Ale kiedy przechodzisz przez miliardy lub biliony iteracji, sumuje się to do długich opóźnień w przetwarzaniu danych, które mogą stać się nie do zaakceptowania. Jest to więc kompromis przy wyborze języka, który ma być używany dla określonego fragmentu kodu oraz jak często i gdzie sprawdzasz potencjalnie niebezpieczne / możliwe do wykorzystania warunki w danych.

tl; dr: Istnieje kompromis między potencjalnie niepotrzebnymi kontrolami bezpieczeństwa a prędkością.
„Ale kiedy przechodzisz przez miliardy lub tryliony iteracji, sumuje się to do” - kiedy iterujesz przez tablicę, sumuje się do dokładnie * jednej * pojedynczej kontroli przed pętlą, ponieważ współczesne kompilatory są dość sprytne. Jedynym momentem, w którym płacisz za sprawdzenie granic, jest to, że kompilator nie może dowiedzieć się, czy jest bezpieczny, co ogólnie oznacza, że ​​jest to dostęp losowy. W takim przypadku płacisz o 1 cykl więcej, co tak, w niektórych sytuacjach może się sumować (np. Operacje na macierzach), ale dla 99,9% całego kodu jest to całkowicie pomijalne.
To niekoniecznie prawda. Tak, nowoczesne kompilatory są rzeczywiście bardzo sprytne i potrafią zoptymalizować wiele kodu. Ale to jest * nadal tylko program komputerowy *, a nie inteligentna istota, która może spojrzeć na twój kod i wiedzieć z całkowitą pewnością, co dokładnie zamierzał zrobić programista. Nadal istnieją przypadki, w których kompilator nie może dokonać „doskonałego wyboru” optymalizacji i wraca do bardziej bezpiecznych, które mogą być zbyt wolne do określonego celu, programiści je wyłączają. Tendencja ludzi do polegania na „inteligentnych kompilatorach” wykonujących swoją pracę za nich jest jednym z powodów, dla których ten rodzaj problemu się utrzymuje.
Aby dodać do tego, poczyniono kilka założeń. Najpierw „99,9%” - skąd się wzięła ta liczba? Wydaje mi się, że „80% statystyk tworzy się na miejscu”. Po drugie, przetwarzane dane znajdują się w ładnej, schludnej tablicy… co nie zawsze jest prawdą. Faktycznie, koncepcja dostosowania danych do "bezpiecznych" operacji jest częścią kosztownej obliczeniowo manipulacji danymi, której programiści starają się uniknąć i po prostu zakładają, że albo dane są "bezpieczne", albo że kompilator "naprawi to tak jest." I tak dalej.
Bing Bang
2016-02-23 23:37:48 UTC
view on stackexchange narkive permalink

Zasadniczo programiści to leniwi ludzie (łącznie ze mną). Robią takie rzeczy, jak używanie gets () zamiast fgets () i definiowanie buforów we / wy na stosie i nie szukają wystarczająco dużo sposobów na niezamierzone nadpisanie pamięci (dobrze nieumyślnie dla programisty, celowo dla hakera:).

Posiadanie bufora I / O na stosie nie jest złe, po prostu nie wywołuj z nim polecenia „gets”.
Trudno sobie wyobrazić programistę używającego C / C ++ z ** lenistwa **!
@DmitryGrigoryev sprawili, że nauczyłem się C / C ++ w szkole i jestem zbyt leniwy, żeby uczyć się innych języków :)
@JOW Z mojego doświadczenia wynika, że ​​kodowanie dość małej nakładki (kilka przycisków, żądania HTTP, parsowanie XML, plik IO) w C # bez wcześniejszej znajomości tego języka było nadal szybsze niż się spodziewałem, że kodowanie tej samej aplikacji w C ++ / MFC będzie .
@JOW Oczywiście nie robisz tego rodzaju programowania, co ja. Mam około 10000 linii intensywnego kodu C w produkcji ...
@BingBang Czułem się nieswojo, czytając to.
@Gusdor Bez odwagi, bez chwały, stary.
Yakk
2016-02-27 03:41:50 UTC
view on stackexchange narkive permalink

Istnieje duża ilość istniejącego kodu C, który zapisuje do buforów niesprawdzone. Część tego znajduje się w bibliotekach. Ten kod jest potencjalnie niebezpieczny, jeśli jakikolwiek stan zewnętrzny może zmienić zapisaną długość, i tylko bardzo niebezpieczny w przeciwnym razie.

Istnieje większa ilość istniejącego kodu C, który ogranicza zapis do buforów. Jeśli użytkownik wspomnianego kodu popełnia błąd matematyczny i pozwala napisać więcej niż powinien, jest to tak samo możliwe do wykorzystania, jak powyżej. Nie ma gwarancji w czasie kompilacji, że matematyka została wykonana poprawnie.

Istnieje również duża ilość istniejącego kodu C, który czyta na podstawie przesunięć w pamięci. Jeśli przesunięcie nie zostanie sprawdzone jako poprawne, może to spowodować wyciek informacji.

Kod C ++ jest często używany jako język wysokiego poziomu do współpracy z C, więc przestrzeganych jest wiele pomysłów C i błędów związanych z komunikacją z C Interfejsy API są powszechne.

Style programowania w C ++, które zapobiegają takim przepełnieniom, istnieją, ale wystarczy jeden błąd, aby je zaistnieć.

Ponadto problem wiszących wskaźników, gdzie pamięć zasoby są odzyskiwane, a wskaźnik wskazuje teraz na pamięć o innym okresie życia / strukturze niż pierwotnie, zezwala na niektóre rodzaje exploitów i wycieków informacji.

Tego rodzaju błędy - błędy „słupka ogrodzenia”, wskaźnik "błędy - są tak powszechne i tak trudne do całkowitego wyeliminowania, że ​​wiele języków zostało opracowanych za pomocą systemów zaprojektowanych jawnie , aby im zapobiec.

Nic dziwnego, że w językach Zaprojektowany w celu wyeliminowania tych błędów, błędy te nie występują tak często. Nadal czasami występują: albo silnik, na którym działa język, ma problem lub jest konfigurowana sytuacja ręczna, która pasuje do środowiska przypadku C / C ++ (ponowne użycie obiektów w puli, użycie wspólnego dużego wspólnego bufora podzielonego na konsumenta itp. ). Ale ponieważ te zastosowania są rzadsze, problem występuje rzadziej.

Każda alokacja dynamiczna, każde użycie bufora w C / C ++ stwarza takie ryzyko. A bycie doskonałym jest nieosiągalne.

Rich
2016-02-27 05:07:40 UTC
view on stackexchange narkive permalink

Najczęściej używane języki (na przykład Java i Ruby) są kompilowane do kodu działającego na maszynie wirtualnej. Maszyna wirtualna jest przeznaczona do segregowania kodu maszynowego, danych i zwykle stosu. Oznacza to, że zwykłe operacje językowe nie mogą zmienić kodu ani przekierować przepływu sterowania (czasami istnieją specjalne interfejsy API, które mogą to zrobić, np. Do debugowania).

C i C ++ są zwykle kompilowane bezpośrednio do natywny język maszynowy procesora - daje to korzyści związane z wydajnością i elastycznością, ale oznacza, że ​​błędny kod może nadpisać pamięć programu lub stos, a tym samym wykonywać instrukcje, których nie ma w oryginalnym programie.

Zwykle ma to miejsce, gdy bufor jest (być może celowo) przepełniony w C ++. Z kolei w Javie lub Ruby przepełnienie buforu natychmiast spowoduje wyjątek i nie może (z wyjątkiem błędów maszyny wirtualnej) nadpisać kodu ani zmienić przepływu sterowania.

Nie ma to nic wspólnego z uruchomieniem na maszynie wirtualnej lub nie. Możesz mieć maszynę wirtualną z takim samym zachowaniem jak c, tak jak możesz mieć programy, które kompilują się bezpośrednio do kodu maszynowego, które są tak bezpieczne, jak powiedzmy java (np. ADA)
W teorii W prawie wszystkich praktycznych przypadkach Java działa na maszynie wirtualnej, która zapobiega nadpisywaniu kodu, a C / C ++ działa na czystej maszynie, która tego nie robi.
Tak. A ADA nie działa na maszynie wirtualnej, a także zapobiega większości tych exploitów, podobnie jak Java. Posiadanie maszyny wirtualnej, czy nie, jest dla tego całkowicie nieistotne (jak myślisz, co jest wyjątkowego w przypadku maszyny wirtualnej, czego nie można zrobić inaczej? Do diabła, w rzeczywistości wprowadza to tylko możliwą lukę w zabezpieczeniach, ponieważ JIT potrzebuje zapisywalnej i wykonywalnej pamięci!)


To pytanie i odpowiedź zostało automatycznie przetłumaczone z języka angielskiego.Oryginalna treść jest dostępna na stackexchange, za co dziękujemy za licencję cc by-sa 3.0, w ramach której jest rozpowszechniana.
Loading...