Jak niebezpieczny jest dostęp do tablicy poza jej granicami (w C)? Czasami może się zdarzyć, że czytam spoza tablicy (teraz rozumiem, że wtedy uzyskuję dostęp do pamięci używanej przez niektóre inne części mojego programu lub nawet poza nią) lub próbuję ustawić wartość na indeks poza tablicą. Program czasami ulega awarii, ale czasami po prostu działa, dając tylko nieoczekiwane wyniki.
Teraz chciałbym wiedzieć, jak naprawdę jest to niebezpieczne? Jeśli uszkodzi mój program, nie jest taki zły. Jeśli z drugiej strony psuje coś poza moim programem, ponieważ jakoś udało mi się uzyskać dostęp do jakiejś całkowicie niepowiązanej pamięci, to jest to bardzo złe, jak sądzę. Czytam dużo „wszystko może się zdarzyć”, „segmentacja może być najmniejszym problemem” , „twój dysk twardy może zmienić kolor na różowy, a jednorożce śpiewają pod twoim oknem”, co jest miłe, ale co naprawdę jest niebezpieczne?
Moje pytania:
- Czy odczytywanie wartości spoza tablicy może uszkodzić coś oprócz mojego programu? Wyobrażam sobie, że samo patrzenie na rzeczy niczego nie zmienia, czy może na przykład zmieniłby atrybut „ostatnio otwierane” pliku, do którego dotarłem?
- Czy ustawienie wartości poza macierzą może uszkodzić coś oprócz mojego programu? Z tego pytania o przepełnienie stosu dowiaduję się, że można uzyskać dostęp do dowolnego miejsca w pamięci, że nie ma gwarancji bezpieczeństwa.
- Teraz uruchamiam moje małe programy z poziomu XCode. Czy zapewnia to dodatkową ochronę wokół mojego programu, w przypadku gdy nie może on dotrzeć poza własną pamięć? Czy może zaszkodzić XCode?
- Wszelkie zalecenia dotyczące bezpiecznego uruchamiania z natury mojego błędnego kodu?
Używam OSX 10.7, Xcode 4.6.
Odpowiedzi:
Jeśli chodzi o standard ISO C (oficjalna definicja języka), dostęp do tablicy poza jej granicami ma „ niezdefiniowane zachowanie ”. Dosłowne znaczenie tego:
Nota nienormatywna rozwija się na ten temat:
To jest teoria. Jaka jest rzeczywistość
W „najlepszym” przypadku uzyskasz dostęp do części pamięci, która jest własnością twojego aktualnie uruchomionego programu (co może spowodować, że Twój program źle się zachowuje) lub która nie jest własnością twojego aktualnie uruchomionego programu (co prawdopodobnie spowoduje, że Twój program awaria z czymś w rodzaju błędu segmentacji). Lub możesz spróbować zapisać w pamięci, którą posiada twój program, ale jest to oznaczone jako tylko do odczytu; prawdopodobnie spowoduje to również awarię programu.
Zakłada się, że Twój program działa w systemie operacyjnym, który próbuje chronić przed sobą równolegle działające procesy. Jeśli twój kod działa na „bare metal”, powiedzmy, że jest częścią jądra systemu operacyjnego lub systemu osadzonego, wówczas taka ochrona nie istnieje; Twój źle działający kod miał zapewnić tę ochronę. W takim przypadku możliwości uszkodzenia są znacznie większe, w tym, w niektórych przypadkach, fizyczne uszkodzenie sprzętu (lub przedmiotów lub osób w pobliżu).
Nawet w chronionym środowisku systemu operacyjnego zabezpieczenia nie zawsze są w 100%. Istnieją błędy systemu operacyjnego, które pozwalają nieuprzywilejowanym programom na przykład uzyskać dostęp do roota (administratora). Nawet przy zwykłych uprawnieniach użytkownika źle działający program może zużywać nadmierne zasoby (procesor, pamięć, dysk), co może doprowadzić do awarii całego systemu. Wiele złośliwych programów (wirusów itp.) Wykorzystuje przepełnienia bufora, aby uzyskać nieautoryzowany dostęp do systemu.
(Jeden historyczny przykład: słyszałem, że w niektórych starych systemach z pamięcią rdzeniową wielokrotne uzyskiwanie dostępu do pojedynczej lokalizacji pamięci w ciasnej pętli może dosłownie spowodować stopienie tej części pamięci. Inne możliwości obejmują zniszczenie wyświetlacza CRT i przesunięcie odczytu / napisz głowicę napędu dyskowego o częstotliwości harmonicznej szafy napędu, powodując, że przechodzi ona przez stół i spada na podłogę.)
I zawsze jest o co martwić Skynet .
Najważniejsze jest to: jeśli mógłbyś napisać program celowo robiąc coś złego , przynajmniej teoretycznie możliwe jest, że błędny program mógłby zrobić to samo przypadkowo .
W praktyce jest bardzo mało prawdopodobne, aby Twój błędny program działający na systemie MacOS X zrobił coś poważniejszego niż awaria. Ale nie można całkowicie uniemożliwić błędnemu kodowi robienia naprawdę złych rzeczy.
źródło
Ogólnie rzecz biorąc, dzisiejsze systemy operacyjne (i tak popularne) uruchamiają wszystkie aplikacje w chronionych regionach pamięci za pomocą wirtualnego menedżera pamięci. Okazuje się, że po prostu czytać lub pisać w lokalizacji, która istnieje w PRAWDZIWEJ przestrzeni poza regionem (regionami) przypisanymi / przydzielonymi do twojego procesu, nie jest strasznie ŁATWE (na powiedzenie).
Bezpośrednie odpowiedzi:
1) Czytanie prawie nigdy nie uszkodzi bezpośrednio innego procesu, jednak może pośrednio uszkodzić proces, jeśli zdarzy się odczytać wartość KLUCZOWĄ używaną do szyfrowania, deszyfrowania lub sprawdzania poprawności programu / procesu. Czytanie poza zakresem może mieć nieco negatywny / nieoczekiwany wpływ na kod, jeśli podejmujesz decyzje na podstawie czytanych danych
2) Jedynym sposobem, aby naprawdę ZNISZCZYĆ coś, pisząc do pętli dostępnej dla adresu pamięci, jest to, że ten adres pamięci, na który piszesz, jest w rzeczywistości rejestrem sprzętowym (miejsce, które w rzeczywistości nie służy do przechowywania danych, ale do kontrolowania jakiegoś elementu sprzętu), a nie lokalizacja pamięci RAM. W rzeczywistości nadal normalnie niczego nie uszkodzisz, chyba że napiszesz kiedyś programowalną lokalizację, której nie da się ponownie zapisać (lub coś takiego).
3) Generalnie uruchamiany z poziomu debugera uruchamia kod w trybie debugowania. Uruchomienie w trybie debugowania ma tendencję do (ale nie zawsze) szybszego zatrzymywania kodu, gdy zrobisz coś, co jest uważane za niepraktyczne lub wręcz nielegalne.
4) Nigdy nie używaj makr, używaj struktur danych, które mają już wbudowane sprawdzanie granic indeksów tablic itp.
DODATKOWE Powinienem dodać, że powyższe informacje są naprawdę tylko dla systemów korzystających z systemu operacyjnego z oknami ochrony pamięci. Jeśli piszesz kod dla systemu osadzonego, a nawet systemu wykorzystującego system operacyjny (w czasie rzeczywistym lub inny), który nie ma okien ochrony pamięci (lub wirtualnych okien adresowanych), należy zachować większą ostrożność podczas odczytywania i zapisywania w pamięci. Również w tych przypadkach należy zawsze stosować BEZPIECZNE i BEZPIECZNE praktyki kodowania, aby uniknąć problemów z bezpieczeństwem.
źródło
Brak sprawdzania granic może prowadzić do brzydkich efektów ubocznych, w tym dziur w zabezpieczeniach. Jednym z tych brzydkich jest wykonanie dowolnego kodu . W klasycznym przykładzie: jeśli masz tablicę o stałym rozmiarze i używasz
strcpy()
do umieszczenia tam łańcucha dostarczonego przez użytkownika, użytkownik może dać ci łańcuch, który przepełnia bufor i zastępuje inne lokalizacje pamięci, w tym adres kodu, do którego procesor powinien zwrócić, gdy funkcja kończy.Co oznacza, że użytkownik może wysłać ci ciąg, który spowoduje, że Twój program w zasadzie zadzwoni
exec("/bin/sh")
, co zamieni go w powłokę, wykonując wszystko, co zechce w twoim systemie, w tym zbierając wszystkie dane i zamieniając maszynę w węzeł botnetu.Widzieć Smashing the Stack dla zabawy i zysku, aby dowiedzieć się, jak to zrobić.
źródło
foo[0]
przezfoo[len-1]
po poprzednio używany sprawdzenielen
z długością tablicy albo wykonać lub pominąć kawałek kodu, kompilator powinien czuć się swobodnie uruchomić ten inny kod bezwarunkowo nawet jeśli aplikacja posiada pamięć poza tablicą, a skutki odczytu byłyby łagodne, ale efekt wywołania innego kodu nie byłby.Ty piszesz:
Ujmijmy to w ten sposób: załaduj broń. Skieruj go za okno bez żadnego szczególnego celu i ognia. Jakie jest niebezpieczeństwo?
Problem polega na tym, że nie wiesz. Jeśli twój kod zastąpi coś, co powoduje awarię programu, nic ci nie jest, bo zatrzyma go w określonym stanie. Jeśli jednak się nie zawiesi, problemy zaczną się pojawiać. Które zasoby są pod kontrolą Twojego programu i co może z nimi zrobić? Które zasoby mogą przejąć kontrolę nad twoim programem i co może z nimi zrobić? Znam co najmniej jeden poważny problem spowodowany takim przepełnieniem. Problem polegał na pozornie nieistotnej funkcji statystycznej, która pomieszała niepowiązaną tabelę konwersji dla produkcyjnej bazy danych. Rezultatem było potem bardzo drogie czyszczenie. W rzeczywistości byłoby to znacznie tańsze i łatwiejsze w obsłudze, gdyby ten problem sformatował dyski twarde ... innymi słowy: różowe jednorożce mogą być twoim najmniejszym problemem.
Idea, że Twój system operacyjny będzie cię chronić, jest optymistyczna. Jeśli to możliwe, staraj się unikać pisania poza granicami.
źródło
Brak uruchamiania programu jako użytkownik root lub inny uprzywilejowany użytkownik nie zaszkodzi żadnemu systemowi, więc ogólnie może to być dobry pomysł.
Zapisując dane w jakiejś przypadkowej lokalizacji w pamięci, nie uszkodzisz bezpośrednio żadnego innego programu uruchomionego na twoim komputerze, ponieważ każdy proces działa w swoim własnym obszarze pamięci.
Jeśli spróbujesz uzyskać dostęp do pamięci nieprzydzielonej procesowi, system operacyjny zatrzyma działanie programu z błędem segmentacji.
Tak więc bezpośrednio (bez uruchamiania jako root i bezpośredniego dostępu do plików takich jak / dev / mem) nie ma niebezpieczeństwa, że Twój program zakłóci działanie dowolnego innego programu działającego w twoim systemie operacyjnym.
Niemniej jednak - i prawdopodobnie o tym słyszałeś w kategoriach zagrożenia - przez przypadkowe zapisanie przypadkowych danych w losowych lokalizacjach pamięci przez przypadek, możesz z pewnością uszkodzić wszystko, co możesz uszkodzić.
Na przykład twój program może chcieć usunąć określony plik podany przez nazwę pliku zapisaną gdzieś w twoim programie. Jeśli przypadkowo zastąpisz lokalizację, w której jest przechowywana nazwa pliku, możesz zamiast tego usunąć zupełnie inny plik.
źródło
NSArray
W Objective-C przypisano określony blok pamięci. Przekroczenie granic tablicy oznacza, że miałbyś dostęp do pamięci, która nie jest przypisana do tablicy. To znaczy:Z punktu widzenia programu zawsze chcesz wiedzieć, kiedy kod przekracza granice tablicy. Może to spowodować zwrócenie nieznanych wartości, co spowoduje awarię aplikacji lub podanie nieprawidłowych danych.
źródło
NSArrays
mają wyjątki poza granicami. I te pytania wydają się dotyczyć tablicy C.Możesz spróbować użyć
memcheck
narzędzia w Valgrind podczas testowania kodu - nie wykryje naruszeń granic tablicy w ramce stosu, ale powinno wykryć wiele innych problemów z pamięcią, w tym powodujących subtelne, szersze problemy poza zakresem jednej funkcji.Z instrukcji:
ETA: Chociaż, jak mówi odpowiedź Kaz, nie jest to panaceum i nie zawsze daje najbardziej pomocne wyniki, szczególnie gdy używasz ekscytujących wzorców dostępu.
źródło
Jeśli kiedykolwiek wykonujesz programowanie na poziomie systemu lub programowanie w systemach wbudowanych, bardzo złe rzeczy mogą się zdarzyć, jeśli zapiszesz w losowych lokalizacjach pamięci. Starsze systemy i wiele mikrokontrolerów używają IO zamapowanego w pamięci, więc zapis w lokalizacji pamięci, która jest mapowana do rejestru peryferyjnego, może siać spustoszenie, szczególnie jeśli odbywa się to asynchronicznie.
Przykładem jest programowanie pamięci flash. Tryb programowania na układach pamięci jest włączony poprzez zapisanie określonej sekwencji wartości w określonych lokalizacjach w zakresie adresów układu. Gdyby inny proces zapisywał w dowolnym miejscu w układzie podczas tego procesu, spowodowałoby to błąd cyklu programowania.
W niektórych przypadkach sprzęt zawija adresy (najbardziej znaczące bity / bajty adresu są ignorowane), więc zapis na adres poza końcem fizycznej przestrzeni adresowej spowoduje, że dane będą zapisywane w samym środku rzeczy.
I wreszcie, starsze procesory, takie jak MC68000, mogą zostać zablokowane do tego stopnia, że tylko reset sprzętowy może je przywrócić. Nie pracowałem nad nimi przez kilka dziesięcioleci, ale myślę, że to wtedy wystąpił błąd magistrali (nieistniejąca pamięć) podczas próby obsługi wyjątku, po prostu zatrzymałby się, dopóki nie zostanie przywrócony reset sprzętowy.
Moim największym zaleceniem jest rażąca wtyczka do produktu, ale nie interesuję się nim osobiście i nie jestem z nim w żaden sposób związany - ale w oparciu o kilka dekad programowania C i systemów wbudowanych, w których niezawodność była krytyczna, komputer Gimpel Lint nie tylko wykryje tego rodzaju błędy, ale także sprawi, że stanie się lepszym programistą C / C ++, stale na ciebie napominając o złych nawykach.
Polecam również przeczytanie standardu kodowania MISRA C, jeśli możesz kogoś sfotografować. Nie widziałem żadnych ostatnich, ale w dawnych czasach dobrze wyjaśnili, dlaczego powinieneś / nie powinieneś robić rzeczy, które obejmują.
Nie wiem o tobie, ale około 2 lub 3 raz dostaję rdzeń lub zawieszenie z dowolnej aplikacji, moja opinia o każdej firmie, która go wyprodukowała, spada o połowę. Czwarty lub piąty raz i jakakolwiek jest paczka, staje się półką, a ja wbijam drewniany kołek przez środek paczki / dysku, który przyszedł, aby upewnić się, że nigdy nie wróci, by mnie prześladować.
źródło
Pracuję z kompilatorem układu DSP, który celowo generuje kod, który uzyskuje dostęp do końca tablicy poza kodem C, co nie!
Wynika to z faktu, że pętle są tak skonstruowane, że koniec iteracji poprzedza niektóre dane do następnej iteracji. Tak więc dane wstępnie wybrane na końcu ostatniej iteracji nigdy nie są faktycznie używane.
Pisanie takiego kodu C wywołuje niezdefiniowane zachowanie, ale jest to tylko formalność z dokumentu standardowego, który dotyczy samego siebie z maksymalną przenośnością.
Częściej nie, program, który uzyskuje dostęp poza granicami, nie jest sprytnie zoptymalizowany. To jest po prostu błąd. Kod pobiera pewną wartość śmieci i, w przeciwieństwie do zoptymalizowanych pętli wyżej wspomnianego kompilatora, kod następnie wykorzystuje tę wartość w kolejnych obliczeniach, powodując w ten sposób ich uszkodzenie.
Warto wychwytywać takie błędy, dlatego warto sprawić, by zachowanie było niezdefiniowane tylko z tego tylko powodu: aby czas działania mógł wygenerować komunikat diagnostyczny, taki jak „przepełnienie tablicy w linii 42 main.c”.
W systemach z pamięcią wirtualną może się zdarzyć, że tablica zostanie przydzielona w taki sposób, że następujący adres znajduje się w niezmapowanym obszarze pamięci wirtualnej. Dostęp wtedy bombarduje program.
Niemniej jednak dostęp do niezainicjowanych lub poza granicami wartości jest czasem ważną techniką optymalizacji, nawet jeśli nie jest maksymalnie przenośna. Jest to na przykład powód, dla którego narzędzie Valgrind nie zgłasza dostępu do niezainicjowanych danych, gdy te dostępy się zdarzają, ale tylko wtedy, gdy wartość jest później wykorzystywana w jakiś sposób, który mógłby wpłynąć na wynik programu. Otrzymujesz komunikat diagnostyczny, taki jak „gałąź warunkowa w xxx: nnn zależy od niezainicjowanej wartości” i czasami może być trudno wyśledzić, skąd pochodzi. Gdyby wszystkie takie dostępy zostały natychmiast uwięzione, pojawiłoby się wiele fałszywych alarmów wynikających z kodu zoptymalizowanego przez kompilator, a także kodu zoptymalizowanego ręcznie.
Mówiąc o tym, pracowałem z jakimś kodekiem od dostawcy, który dawał te błędy, gdy był przenoszony do Linuksa i działał pod Valgrind. Ale sprzedawca przekonał mnie, że tylko kilka bitówużyta wartość faktycznie pochodzi z niezainicjowanej pamięci, a logika ostrożnie uniknęła tych bitów. Użyto tylko dobrych bitów wartości, a Valgrind nie ma możliwości wyśledzenia pojedynczego bitu. Niezainicjowany materiał powstał po przeczytaniu słowa za końcem strumienia bitów zakodowanych danych, ale kod wie, ile bitów znajduje się w strumieniu i nie zużyje więcej bitów, niż jest w rzeczywistości. Ponieważ dostęp poza końcem tablicy strumienia bitów nie powoduje żadnej szkody w architekturze DSP (po tablicy nie ma pamięci wirtualnej, nie ma portów mapowanych w pamięci, a adres się nie zawija), jest to ważna technika optymalizacji.
„Niezdefiniowane zachowanie” nie znaczy tak naprawdę wiele, ponieważ według ISO C, po prostu włączenie nagłówka, który nie jest zdefiniowany w standardzie C, lub wywołanie funkcji, która nie jest zdefiniowana w samym programie lub standardzie C, są przykładami niezdefiniowanymi zachowanie. Niezdefiniowane zachowanie nie oznacza „niezdefiniowane przez nikogo na planecie”, tylko „niezdefiniowane przez normę ISO C”. Ale oczywiście czasami nieokreślone zachowanie nie jest absolutnie przez nikogo zdefiniowane.
źródło
Poza własnym programem, nie sądzę, że cokolwiek zepsujesz, w najgorszym przypadku spróbujesz odczytać lub napisać z adresu pamięci, który odpowiada stronie, której jądro nie przypisało twoim procesom, generując odpowiedni wyjątek i bycie zabitym (mam na myśli twój proces).
źródło