Czy używanie przestarzałego kompilatora C stanowi zagrożenie bezpieczeństwa?

139

Mamy w produkcji kilka systemów kompilacji, o które nikt nie dba, a te maszyny działają na starożytnych wersjach GCC, takich jak GCC 3 lub GCC 2.

I nie mogę przekonać kierownictwa, aby zaktualizował go do nowszego: mówią, „jeśli nie jest zepsuty, nie naprawiaj go”.

Ponieważ utrzymujemy bardzo stary kod (napisany w latach 80-tych), ten kod C89 dobrze kompiluje się na tych kompilatorach.

Ale nie jestem pewien, czy warto używać tych starych rzeczy.

Moje pytanie brzmi:

Czy użycie starego kompilatora C może zagrozić bezpieczeństwu skompilowanego programu?

AKTUALIZACJA:

Ten sam kod jest budowany przez Visual Studio 2008 dla celów Windows, a MSVC nie obsługuje jeszcze C99 ani C11 (nie wiem, czy nowszy MSVC to robi) i mogę go zbudować na moim Linuksie przy użyciu najnowszego GCC. Więc gdybyśmy po prostu wpadli w nowszą GCC, prawdopodobnie zbudowałby tak dobrze, jak poprzednio.

Calmarius
źródło
5
Ciekawe pytanie - to również może być warte szybkiego przeczytania - developers.slashdot.org/story/13/10/29/2150211/… .. więc nowsze kompilatory mogą również naruszyć bezpieczeństwo podczas optymalizacji.
Neil
6
Czy te stare wersje gcc obsługują kompilację do PIC / PIE dla ASLR? Czy obsługują kanarki stosowe? W ^ X (NX)? Jeśli nie, brak środków łagodzących luki w zabezpieczeniach jest dobrym powodem do aktualizacji.
EOF
12
Samo spojrzenie na ostrzeżenia z gcc 4.x może natychmiast ujawnić cały ładunek istniejących luk w zabezpieczeniach, o których nie wiedziałeś.
OrangeDog
7
@OrangeDog: Dlaczego gcc 4.x? gcc6 to seria aktualnych wydań, a gcc 5 istnieje już od jakiegoś czasu. Ale tak, naprawienie wszelkich problemów zidentyfikowanych przez -O3 -Wall -Wextra -fsanitize=undefinednowoczesne gcc i clang powinno pomóc.
Peter Cordes
4
@OrangeDog GCC przeszedł do numerów wersji marketingowych. GCC 5 zasługiwało na poważną zmianę wersji, ponieważ zmieniło domyślne standardy C i C ++ oraz libstdc ++ ABI. GCC 6 powinno być nazwane 5.1.
zwolniony

Odpowiedzi:

102

Właściwie argumentowałbym odwrotnie.

Jest wiele przypadków, w których zachowanie jest niezdefiniowane przez standard C, ale jest oczywiste, co by się stało z „głupim kompilatorem” na danej platformie. Przypadki takie jak zezwolenie na przepełnienie liczby całkowitej ze znakiem lub dostęp do tej samej pamięci za pomocą zmiennych dwóch różnych typów.

Najnowsze wersje gcc (i clang) zaczęły traktować te przypadki jako możliwości optymalizacji, które nie dbają o to, czy zmieniają zachowanie pliku binarnego w stanie „niezdefiniowane zachowanie”. Jest to bardzo złe, jeśli twój kod został napisany przez ludzi, którzy traktowali C jak „przenośny asembler”. W miarę upływu czasu optymizatorzy zaczęli przyglądać się coraz większym fragmentom kodu, wykonując te optymalizacje, zwiększając prawdopodobieństwo, że plik binarny skończy robić coś innego niż „to, co zrobiłby plik binarny zbudowany przez głupi kompilator”.

Istnieją przełączniki kompilatora, które przywracają „tradycyjne” zachowanie (-fwrapv i -fno-strict-aliasing dla dwóch, o których wspomniałem powyżej), ale najpierw musisz o nich wiedzieć.

Podczas gdy w zasadzie błąd kompilatora może zmienić zgodny kod w lukę w zabezpieczeniach, uważam, że ryzyko tego jest pomijalne w ogólnym rozrachunku.

plugwash
źródło
13
Ale ten argument działa w obie strony. Jeśli kompilator ma przewidywalne „niezdefiniowane zachowanie”, prawdopodobnie łatwiej będzie go złośliwie wykorzystać ...
André
18
@Andre Skompilowany kod i tak ma przewidywalne niezdefiniowane zachowanie. Oznacza to, że po skompilowaniu kodu każde nieprzewidywalne zachowanie jest teraz przewidywalne w tej konkretnej skompilowanej wersji.
user253751
6
people who treated C like a "portable assembler"czy to nie jest to, czym jest C?
Max
10
@Max Ta odpowiedź ostrzega właśnie przed faktem, że pojęcie „przenośnego asemblera” jest w praktyce co najmniej przestarzałe, ze względu na nowoczesne optymalizatory. I argumentowałbym, że początkowo nigdy nie było to poprawne koncepcyjnie.
Theodoros Chatzigiannakis
6
Brak współczucia dla tych, którzy polegają na niezdefiniowanym zachowaniu, a później zaczynają zbierać to, co zasiali. Nie oznacza to, że nowsze kompilatory są z natury mniej bezpieczne - oznacza to, że niezgodny kod był bombą zegarową. Winę należy odpowiednio rozłożyć.
underscore_d
52

W obu przypadkach istnieje ryzyko.


Starsze kompilatory mają przewagę w postaci dojrzałości i cokolwiek zostało w nich zepsute, prawdopodobnie zostało (ale nie ma gwarancji) obejść się pomyślnie.

W tym przypadku nowy kompilator jest potencjalnym źródłem nowych błędów.


Z drugiej strony nowsze kompilatory mają dodatkowe narzędzia :

  • GCC i Clang są teraz wyposażone w środki odkażające, które mogą instrumentować środowisko wykonawcze do wykrywania nieokreślonych zachowań różnego rodzaju (Chandler Carruth z zespołu Google Compiler stwierdził w zeszłym roku, że spodziewa się, że osiągną one pełne pokrycie)
  • Szczęk przynajmniej wyposażony utwardzanie , na przykład sterowania przepływem integralności jest informacja o wykryciu HI-wyczkę kontroli przepływu, są także hartowania przyrządów do ochrony przed atakiem ochrony stosu (oddzielając część sterowania przepływu stosu z części danych) ; funkcje hartowania są generalnie niskie (<1% obciążenia procesora)
  • Clang / LLVM pracuje również nad libFuzzer , narzędziem do tworzenia instrumentalnych testów jednostkowych fuzzing, które inteligentnie eksplorują przestrzeń wejściową testowanej funkcji (poprzez modyfikację danych wejściowych, aby obrać jeszcze nie zbadane ścieżki wykonywania)

Oprzyrządowanie pliku binarnego za pomocą środków dezynfekujących (Address Sanitizer, Memory Sanitizer lub Undefined Behavior Sanitizer), a następnie fuzzowanie go ( na przykład za pomocą American Fuzzy Lop ) ujawniło luki w wielu popularnych programach, zobacz na przykład ten artykuł LWN.net .

Te nowe narzędzia i wszystkie przyszłe narzędzia będą niedostępne, chyba że zaktualizujesz kompilator.

Pozostając na słabo wydajnym kompilatorze, kładziesz głowę w piasek i trzymasz kciuki, aby nie znaleziono żadnej luki. Jeśli Twój produkt jest celem o dużej wartości, zachęcam do ponownego rozważenia.


Uwaga: nawet jeśli NIE uaktualniasz kompilatora produkcyjnego, możesz i tak chcieć użyć nowego kompilatora, aby sprawdzić podatność; pamiętaj, że ponieważ są to różne kompilatory, gwarancje są jednak zmniejszone.

Matthieu M.
źródło
1
+1 za kłopotliwe wzmianki o przypadkach, w których nowe kompilatory mogą być bezpieczniejsze, zamiast gromadzić modę „b-ale mój stary dobry UB” innych odpowiedzi. jest to dodatek do wielu innych ulepszeń, które oferują, które nie są bezpośrednio związane z bezpieczeństwem, ale dają jeszcze większy impuls, aby być rozsądnym nowoczesnym.
underscore_d
Chociaż wydaje się, że opowiada się za „bezpieczeństwem poprzez zapomnienie”; błędy, które dotyczą starych kompilatorów, są znane i publiczne. Chociaż zgadzam się, że nowe kompilatory będą wprowadzać błędy, te błędy nie są jeszcze publiczne, jak te z poprzednich wersji, co jest pewnym zabezpieczeniem, jeśli często aktualizujesz aplikację.
The6P4C
Chandler Carruth jest taki uroczy i mówi o takich wspaniałych rzeczach. Ożeniłbym się z nim, gdybym mógł.
Daniel Kamil Kozar
46

Twój skompilowany kod zawiera błędy, które można wykorzystać. Błędy pochodzą z trzech źródeł: błędy w kodzie źródłowym, błędy w kompilatorze i bibliotekach oraz niezdefiniowane zachowanie w kodzie źródłowym, które kompilator zamienia w błąd. (Niezdefiniowane zachowanie to błąd, ale jeszcze nie błąd w skompilowanym kodzie. Na przykład i = i ++; w C lub C ++ to błąd, ale w twoim skompilowanym kodzie może zwiększyć i o 1 i być OK lub ustawić i do jakiegoś śmieci i być błędem).

Odsetek błędów w skompilowanym kodzie jest przypuszczalnie niski ze względu na testowanie i naprawianie błędów wynikających z raportów klientów. Więc początkowo mogło być wiele błędów, ale liczba ta spadła.

Jeśli dokonasz aktualizacji do nowszego kompilatora, możesz stracić błędy wprowadzone przez błędy kompilatora. Ale te wszystkie błędy byłyby błędami, których według twojej wiedzy nikt nie znalazł i nikt nie wykorzystał. Ale nowy kompilator może mieć same błędy, a co ważne, nowsze kompilatory mają silniejszą tendencję do przekształcania niezdefiniowanego zachowania w błędy w skompilowanym kodzie.

Będziesz miał więc wiele nowych błędów w skompilowanym kodzie; wszystkie błędy, które hakerzy mogą znaleźć i wykorzystać. A jeśli nie wykonasz wielu testów i nie zostawisz swojego kodu klientom w celu znalezienia błędów przez długi czas, będzie on mniej bezpieczny.

gnasher729
źródło
6
Innymi słowy ... nie ma łatwego sposobu, aby stwierdzić, jakie problemy wprowadza kompilator, a przełączając wszystko, co robisz, to otrzymujesz inny zestaw nieznanych problemów?
Jeremy Kato
1
@JeremyKato: dobrze, istnieją pewne przypadki, w których jesteś również uzyskiwanie innego zestawu znanych problemów. Nie jestem pewien, jakie znane luki w zabezpieczeniach są w samym kompilatorze, ale ze względu na konkretny przykład przypuśćmy, że aktualizacja do nowego kompilatora oznacza również możliwość pobrania najnowszej biblioteki libc (używanie starego oznacza niemożność aby to zrobić), to wiesz, że naprawiasz tę usterkę w getaddrinfo(): access.redhat.com/articles/2161461 . Ten przykład w rzeczywistości nie jest luką w zabezpieczeniach kompilatora, ale ponad 10 lat musi istnieć kilka znanych naprawionych błędów.
Steve Jessop
2
Heh, właściwie ta wada została wprowadzona dopiero w 2008 roku, więc pytający może być przed nią bezpieczny. Ale nie chodzi mi o ten konkretny przykład, chodzi o to, że istnieją znane błędy, które stary łańcuch narzędzi umieści w twoim kodzie. Więc kiedy aktualizujesz, to prawda, że ​​wprowadzasz nowy zestaw niewiadomych, ale to nie wszystko, co robisz . Po prostu musisz zgadnąć, czy jesteś „bezpieczniejszy”, pozostawiając znaną krytyczną lukę, którą naprawia najnowszy toolchain, czy też biorąc nieznane konsekwencje ponownego rzucenia kostką na wszystkie niezdefiniowane zachowanie w twoim własnym kodzie.
Steve Jessop
19

Jeśli się nie zepsuł, nie naprawiaj tego

Twój szef ma rację, mówiąc to, jednak ważniejszym czynnikiem jest ochrona wejść, wyjść, przepełnienia bufora. Ich brak jest niezmiennie najsłabszym ogniwem w łańcuchu z tego punktu widzenia, niezależnie od używanego kompilatora.

Jeśli jednak baza kodu jest stara i podjęto prace mające na celu złagodzenie słabych punktów używanego K&R C, takich jak brak bezpieczeństwa typów, niezabezpieczone elementy itp., Rozważ pytanie „ Czy uaktualnienie kompilatora do nowocześniejszego C99 / C11 wszystko psują?

Zakładając, że istnieje jasna ścieżka migracji do nowszych standardów C, co może wywołać efekty uboczne, najlepiej będzie spróbować rozwidlić stary kod, ocenić go i wprowadzić dodatkowe kontrole typu, testy poprawności i określić, czy aktualizacja do nowszy kompilator ma jakikolwiek wpływ na zbiory danych wejściowych / wyjściowych.

Następnie możesz pokazać go swojemu szefowi: „ Oto zaktualizowana baza kodu, refaktoryzowana, bardziej zgodna z przyjętymi w branży standardami C99 / C11… ”.

To jest ryzyko, które należałoby rozważyć, bardzo ostrożnie , opór przed zmianą mógłby pojawić się w tym środowisku i może odmówić dotknięcia nowszych rzeczy.

EDYTOWAĆ

Po prostu usiadłem na kilka minut i zdałem sobie sprawę, że kod wygenerowany przez K&R może działać na platformie 16-bitowej, są szanse, że aktualizacja do nowocześniejszego kompilatora może faktycznie złamać bazę kodu, myślę w kategoriach architektury, wygenerowany zostanie kod 32-bitowy , może to mieć zabawne skutki uboczne dla struktur używanych w zestawach danych wejściowych / wyjściowych, to kolejny ważny czynnik, który należy dokładnie rozważyć.

Ponadto, ponieważ OP wspomniał o używaniu Visual Studio 2008 do budowania bazy kodu, użycie gcc może spowodować wprowadzenie do środowiska MinGW lub Cygwin, co może mieć wpływ na środowisko, chyba że celem jest Linux, wtedy byłoby to warto spróbować, być może trzeba będzie dołączyć dodatkowe przełączniki do kompilatora, aby zminimalizować szum na starej bazie kodu K&R, inną ważną rzeczą jest przeprowadzenie wielu testów, aby upewnić się, że żadna funkcjonalność nie zostanie zepsuta, może okazać się bolesnym ćwiczeniem.

t0mm13b
źródło
Ten sam kod jest budowany przez Visual Studio 2008 dla celów Windows, a MSVC nie obsługuje jeszcze C99 ani C11 (nie wiem, czy nowszy MSVC to robi) i mogę go zbudować na moim Linuksie przy użyciu najnowszego GCC. Więc gdybyśmy po prostu wpadli w nowszą GCC, prawdopodobnie zbudowałby tak dobrze, jak poprzednio.
Calmarius
@Calmarius dzięki za ostrzeżenia, może najlepiej byłoby zmodyfikować swoje pytanie, aby zawierało komentarz, To ważne :) I powinno tam być; D
t0mm13b
@Calmarius zredagował moją odpowiedź, czyli myślę o nowo zaktualizowanym pytaniu.
t0mm13b
2
„Może działać na platformie 16-bitowej, są szanse, że aktualizacja do nowocześniejszego kompilatora może w rzeczywistości złamać bazę kodu, myślę w kategoriach architektury, kodu 32-bitowego” Nie sądzę, aby pytanie dotyczyło przeniesienia kodu do nowej implementacji parametry.
Pascal Cuoq,
Zgoda. Jest możliwe , że luka wykonawcze mogą być tworzone przez błąd kompilatora. Ale jest o wiele bardziej prawdopodobne, że kod zawiera luki w czasie wykonywania spowodowane takimi rzeczami, jak przepełnienia bufora i stosu. Tak więc, gdy inwestujesz czas w zwiększenie bezpieczeństwa tej bazy kodu, powinieneś zainwestować go w takie rzeczy, jak sprawdzanie długości ciągów wejściowych, aby upewnić się, że nie przekraczają one ograniczeń twojego programu. Uzyskanie nowszego kompilatora niewiele pomoże. Przepisanie kodu od podstaw w języku z natywnymi obiektami typu string i tablice bardzo pomoże. Ale twój szef za to nie zapłaci.
O. Jones
9

Czy użycie starego kompilatora C może zagrozić bezpieczeństwu skompilowanego programu?

Oczywiście, jeśli stary kompilator zawiera znane błędy, o których wiesz, że mogą wpłynąć na Twój program.

Pytanie brzmi, prawda? Aby mieć pewność, musiałbyś przeczytać cały dziennik zmian od swojej wersji do obecnej daty i sprawdzić każdy pojedynczy błąd naprawiony na przestrzeni lat.

Jeśli nie znajdziesz dowodów na błędy kompilatora, które mogłyby wpłynąć na twój program, aktualizacja GCC tylko ze względu na to wydaje się nieco paranoiczna. Należy pamiętać, że nowsze wersje mogą zawierać nowe błędy, które nie zostały jeszcze odkryte. Ostatnio wprowadzono wiele zmian dzięki obsłudze GCC 5 i C11.

To powiedziawszy, kod napisany w latach 80. najprawdopodobniej jest już wypełniony po brzegi lukami w zabezpieczeniach i polega na słabo zdefiniowanym zachowaniu, niezależnie od kompilatora. Mówimy tutaj o standardowym C.

Lundin
źródło
6
Nie sądzę, że to paranoja; Myślę, że OP próbuje wymyślić powody, aby przekonać swojego szefa. Prawdopodobnie OP faktycznie chce nowego kompilatora, ponieważ tworzy lepszy asm (w tym optymalizację między plikami za pomocą LTO), ma bardziej użyteczną diagnostykę / ostrzeżenia i pozwala na nowoczesne funkcje językowe i składnię. (np. C11 stdatomic).
Peter Cordes
9

Istnieje zagrożenie bezpieczeństwa polegające na tym, że złośliwy programista może przemycić tylne drzwi przez błąd kompilatora. W zależności od ilości znanych błędów w używanym kompilatorze, backdoor może wyglądać mniej lub bardziej niepozornie (w każdym przypadku chodzi o to, że kod jest poprawny, nawet jeśli jest zagmatwany, na poziomie źródłowym. Przeglądy i testy kodu źródłowego przy użyciu kompilator bez błędów nie znajdzie backdoora, ponieważ backdoor nie istnieje w tych warunkach). Aby uzyskać dodatkowe punkty zaprzeczenia, złośliwy programista może również samodzielnie szukać wcześniej nieznanych błędów kompilatora. Ponownie, jakość kamuflażu będzie zależeć od wyboru znalezionych błędów kompilatora.

Ten atak został zilustrowany w programie sudo w tym artykule . bcrypt napisał świetną kontynuację dla minifier Javascript .

Oprócz tego niepokoju, ewolucja kompilatory C było wykorzystać niezdefiniowanej zachowanie bardziej i bardziej i bardziej agresywnie, tak stary kod C, który został napisany w dobrej wierze, by rzeczywiście być bardziej bezpieczne skompilowane z kompilatora C od czasu, lub zestawiane w -O0 (ale w nowych wersjach kompilatorów wprowadzono kilka nowych, przełamujących programy optymalizacje wykorzystujące UB, nawet przy -O0 ).

Pascal Cuoq
źródło
7

Starsze kompilatory mogą nie mieć ochrony przed znanymi atakami hakerskimi. Na przykład ochrona przed zniszczeniem stosu została wprowadzona dopiero w GCC 4.1 . Więc tak, kod skompilowany za pomocą starszych kompilatorów może być podatny na zagrożenia, przed którymi chronią nowsze kompilatory.

DrMcCleod
źródło
6

Kolejnym aspektem, o który należy się martwić, jest rozwój nowego kodu .

Starsze kompilatory mogą mieć inne zachowanie dla niektórych funkcji języka niż to, co jest ustandaryzowane i oczekiwane przez programistę. Ta niezgodność może spowolnić rozwój i wprowadzić subtelne błędy, które można wykorzystać.

Starsze kompilatory oferują mniej funkcji (w tym funkcje językowe!) I nie są również optymalizowane. Programiści będą omijać te niedociągnięcia - np. Ponownie wdrażając brakujące funkcje lub pisząc sprytny kod, który jest niejasny, ale działa szybciej - tworząc nowe możliwości tworzenia subtelnych błędów.


źródło
5

nie

Powód jest prosty, stary kompilator może mieć stare błędy i exploity, ale nowy kompilator będzie miał nowe błędy i exploity.

Nie "naprawiasz" żadnych błędów poprzez aktualizację do nowego kompilatora. Twoje przełączanie starych błędów i exploitów na nowe błędy i exploity.

coteyr
źródło
3
Wydaje się to bardzo uproszczone: nowy kompilator może mieć swoje słabości, ale spodziewałbym się, że będzie ich mniej niż w starym kompilatorze i prawdopodobnie wykryje kilka luk w kodzie, które stały się znane od tego czasu.
PJTraill
Ale nowy kompilator może mieć nieznane nowe słabości. Sam kompilator nie stanowi zagrożenia bezpieczeństwa, które wymaga aktualizacji. Nie zmniejszasz swojej powierzchni. Handlujesz znanym zestawem problemów dla nieznanego zestawu.
coteyr
Narzędzia pomagające w znajdowaniu błędów znacznie się poprawiły od wczesnych dni GCC, a narzędzia te (analiza statyczna, instrumentalna analiza dynamiczna / dezynfekcja kodu, fuzzery itp.) Zostały również zastosowane w kodzie kompilatora, aby poprawić jakość. Znacznie trudniej było znaleźć wszystkie klasy błędów w erze GCC 2. Porównanie błędów kompilatora z wersjami - patrz strona 7: cs.utah.edu/~regehr/papers/pldi11-preprint.pdf GCC 4.5 i LLVM 2.8 (ostatnie w momencie publikacji) mają najmniej błędów z fuzzingu.
Jetski typu S
2

Cóż, istnieje większe prawdopodobieństwo, że wszelkie błędy w starym kompilatorze są dobrze znane i udokumentowane, w przeciwieństwie do używania nowego kompilatora, więc można podjąć działania w celu uniknięcia tych błędów, kodując je wokół nich. Więc w pewnym sensie to nie wystarczy jako argument za aktualizacją. Prowadzimy te same dyskusje, w których ja pracuję, używamy GCC 4.6.1 na bazie kodu dla oprogramowania wbudowanego i istnieje duża niechęć (wśród kierownictwa) do aktualizacji do najnowszego kompilatora z powodu obawy o nowe, nieudokumentowane błędy.

AndersK
źródło
0

Twoje pytanie dzieli się na dwie części:

  • Jawne: „Czy istnieje większe ryzyko w korzystaniu ze starszego kompilatora” (mniej więcej tak, jak w tytule)
  • Domniemane: „Jak przekonać kierownictwo do uaktualnienia”

Być może możesz odpowiedzieć na jedno i drugie, znajdując możliwą do wykorzystania lukę w istniejącej bazie kodu i pokazując, że nowszy kompilator wykryłby ją. Oczywiście kierownictwo może powiedzieć „znalazłeś to w starym kompilatorze”, ale możesz zauważyć, że kosztowało to sporo wysiłku. Lub uruchomisz go za pomocą nowego kompilatora, aby znaleźć lukę, a następnie wykorzystaj ją, jeśli możesz / masz pozwolenie na skompilowanie kodu za pomocą nowego kompilatora. Możesz potrzebować pomocy przyjaznego hakera, ale zależy to od zaufania mu i możliwości / pozwolenia na pokazanie mu kodu (i użycie nowego kompilatora).

Ale jeśli twój system nie jest narażony na ataki hakerów, być może powinieneś być bardziej zainteresowany tym, czy aktualizacja kompilatora zwiększy twoją skuteczność: Analiza kodu MSVS 2013 dość często znajduje potencjalne błędy znacznie wcześniej niż MSVS 2010 i mniej więcej obsługuje C99 / C11 - nie jestem pewien, czy tak jest oficjalnie, ale deklaracje mogą następować po instrukcjach i możesz deklarować zmienne w for-loops.

PJTraill
źródło