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?
źródło
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
gets
ję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,
++i
zwykle jest to tylkoinc
instrukcja 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 #.
źródło
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
,sprintf
i 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ą.źródło
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.#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/… .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::vector
istd::array
, i trudno jest nawet w trybie debugowania znaleźć przepełnienia bufora.źródło
std::vector
na wydajne wdrażanie konstrukcji wyższego poziomu . Ivector::operator[]
sprawia, że sam wybór dla prędkości nad bezpieczeństwem. Bezpieczeństwovector
polega na tym, że łatwiej jest przewozić wokół wielkości, co jest tym samym podejściem, jakie stosują współczesne biblioteki C.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 preferowanejchar 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.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ść odT[]
dostd::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.std::vector<T>
istd::array<T, N>
zrobić w C ++. Nie byłoby sposobu zaprojektowania i określenia żadnej biblioteki, nawet standardowej, która mogłaby to zrobić.std::vector
nigdy 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.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
gets
1 lubscanf
a 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żywaniufgets
istrtol/strtod
ponieważ 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.
źródło
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ć.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.
źródło
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.
źródło
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.
źródło
Ł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 ...
źródło
memcpy()
a posiadaniem go jako standardowego sposobu efektywnego kopiowania segmentu macierzy.