Czy techniki weryfikacji programu mogą zapobiec występowaniu błędów w gatunku Heartbleed?


9

W sprawie błędu Heartbleed Bruce Schneier napisał w swoim Crypto-Gram z 15 kwietnia: „Katastroficzne” to właściwe słowo. W skali od 1 do 10 jest to 11. ” Czytałem kilka lat temu, że jądro określonego systemu operacyjnego zostało rygorystycznie zweryfikowane za pomocą nowoczesnego systemu weryfikacji programów. Czy w ten sposób można zapobiec występowaniu błędów w gatunku Heartbleed, stosując dziś techniki weryfikacji programu, czy jest to nierealne, a nawet zasadniczo niemożliwe?


2
Oto interesująca analiza tego pytania autorstwa J. Regehra.
Martin Berger,

Odpowiedzi:


6

Aby odpowiedzieć na twoje pytanie w najbardziej zwięzły sposób - tak, ten błąd mógł zostać potencjalnie wykryty przez narzędzia formalnej weryfikacji. Rzeczywiście, właściwość „nigdy nie wysyłaj bloku, który jest większy niż rozmiar wysłanego impulsu” jest dość łatwa do sformalizowania w większości języków specyfikacji (np. LTL).

Problem (który jest powszechną krytyką metod formalnych) polega na tym, że specyfikacje, których używasz, są pisane przez ludzi. Rzeczywiście, metody formalne zmieniają jedynie wyzwanie polegające na wyszukiwaniu błędów od znajdowania błędów do definiowania, jakie są błędy. To trudne zadanie.

Ponadto formalna weryfikacja oprogramowania jest niezwykle trudna ze względu na problem eksplozji stanu. W tym przypadku jest to szczególnie istotne, ponieważ wiele razy, aby uniknąć eksplozji państwa, odrywamy granice. Na przykład, gdy chcemy powiedzieć „po każdym żądaniu następuje przyznanie dotacji, w ciągu 100 000 kroków”, potrzebujemy bardzo długiej formuły, więc wyodrębniamy ją do wzoru „po każdym żądaniu następuje przyznanie”.

Tak więc w przypadku serca, nawet podczas próby sformalizowania wymagań, przedmiotowe ograniczenie mogło zostać usunięte, co skutkowałoby takim samym zachowaniem.

Podsumowując, potencjalnie tego błędu można było uniknąć za pomocą metod formalnych, ale musiałby istnieć człowiek, który wcześniej określi tę właściwość.


5

Programy sprawdzające programy komercyjne, takie jak Klocwork czy Coverity, mogły znaleźć Heartbleed, ponieważ jest to stosunkowo proste „zapomnienie o błędzie sprawdzania granic”, co jest jednym z głównych problemów, które mają sprawdzić. Ale jest o wiele prostszy sposób: użyj nieprzejrzystych abstrakcyjnych typów danych, które są dobrze przetestowane pod kątem braku przepełnienia bufora.

Istnieje wiele abstrakcyjnych typów danych „bezpieczny ciąg” dostępnych do programowania w języku C. Najbardziej znany mi jest Vstr . Autor, James Antill, ma wielką dyskusję na temat, dlaczego potrzebny jest ciąg abstrakcyjny typ danych z własnych konstruktorów / metod fabrycznych , a także listę innych ciągów abstrakcyjnych typów danych dla C .


2
Coverity nie znajduje Heartbleed, zobacz tę analizę John Regehr.
Martin Berger,

Niezły link! Pokazuje prawdziwy morał tej historii: weryfikacja programu nie może zrekompensować źle zaprojektowanych (lub nieistniejących) abstrakcji.
Wandering Logic

2
To zależy od tego, co rozumiesz przez weryfikację programu. Jeśli chodzi o analizę statyczną, to tak, to zawsze jest przybliżenie, jako bezpośrednia konsekwencja twierdzenia Rice'a. Jeśli zweryfikujesz pełne zachowanie w interaktywnym module do twierdzenia, otrzymasz żeliwną gwarancję, że program spełnia jego specyfikacje, ale jest to niezwykle pracochłonne. Nadal masz problem z tym, że twoje specyfikacje mogą być niepoprawne (patrz np. Eksplozja Ariane 5).
Martin Berger,

1
@MartinBerger: Coverity znajduje to teraz .
Przywróć Monikę - M. Schröder

4

Jeśli liczysz jako „  technikę weryfikacji programu  ” połączenie sprawdzania granic czasu wykonywania i fuzzingu, tak, ten konkretny błąd mógł zostać złapany .

Prawidłowe rozmycie sprawi, że niesławny będzie teraz memcpy(bp, pl, payload);czytał ponad limit bloku pamięci pl. Sprawdzanie ograniczeń w czasie wykonywania może w zasadzie przechwycić taki dostęp, aw praktyce w tym konkretnym przypadku nawet debugowana wersja, mallocktóra dba o sprawdzenie parametrów memcpy, wykonałaby zadanie (nie trzeba tutaj zadzierać z MMU) . Problem polega na tym, że przeprowadzanie testów fuzzingu dla każdego rodzaju pakietu sieciowego wymaga wysiłku.


1
Chociaż ogólnie rzecz biorąc, IIRC, w przypadku OpenSSL autorzy wdrożyli własne zarządzanie pamięcią wewnętrzną, tak że znacznie rzadziej trafiało memcpysię na prawdziwą granicę (dużego) regionu pierwotnie wymaganego od systemu malloc.
William Price

Tak, w przypadku OpenSSL, tak jak to było w czasie błędu, memcpy(bp, pl, payload)musiałby sprawdzić granice używane przez malloczastąpienie OpenSSL , a nie system malloc. Wyklucza to automatyczne sprawdzanie granic na poziomie binarnym (przynajmniej bez głębokiej wiedzy na temat malloczamiany). Konieczna jest ponowna kompilacja za pomocą kreatora na poziomie źródła, przy użyciu np. Makr C zastępujących token malloclub dowolnych zastępczych używanych OpenSSL; i wydaje się, że potrzebujemy tego samego z memcpywyjątkiem bardzo sprytnych sztuczek MMU.
fgrieu

4

Używanie ściślejszego języka nie tylko przesuwa słupki celów od poprawnej implementacji do poprawnej specyfikacji. Trudno jest stworzyć coś bardzo złego, ale logicznie spójnego; dlatego kompilatory wyłapują tyle błędów.

Arytmetyka wskaźnika w takiej postaci, w jakiej jest normalnie sformułowana, jest niesłyszalna, ponieważ system typów w rzeczywistości nie oznacza, co powinien oznaczać. Możesz całkowicie uniknąć tego problemu, pracując w języku śmieci (normalne podejście, które powoduje, że płacisz również za abstrakcję). Możesz też sprecyzować, jakiego rodzaju wskaźników używasz, aby kompilator mógł odrzucić wszystko, co jest niespójne lub po prostu nie może być udowodnione, jak napisano. Takie jest podejście niektórych języków, takich jak Rust.

Skonstruowane typy są równoważne dowodom, więc jeśli napiszesz system typów, który o tym zapomina, wtedy wszystkie rzeczy się psują. Załóżmy przez chwilę, że kiedy deklarujemy typ, mamy na myśli, że potwierdzamy prawdę o tym, co znajduje się w zmiennej.

  • int * x; // Fałszywe twierdzenie. x istnieje i nie wskazuje na liczbę całkowitą
  • int * y = z; // Prawda tylko wtedy, gdy udowodniono, że z wskazuje na int
  • * (x + 3) = 5; // Prawda tylko wtedy, gdy (x + 3) wskazuje int w tej samej tablicy co x
  • int c = a / b; // Prawda tylko wtedy, gdy b jest niezerowe, na przykład: "nonzero int b = ...;"
  • nullable int * z = NULL; // nullable int * to nie to samo co int *
  • int d = * z; // Fałszywe twierdzenie, ponieważ z jest zerowalne
  • if (z! = NULL) {int * e = z; } // Ok, ponieważ z nie ma wartości null
  • wolny (y); int w = * y; // Fałszywe twierdzenie, ponieważ y już nie istnieje w w

W tym świecie wskaźniki nie mogą być zerowe. Dereferencje NullPointer nie istnieją, a wskaźniki nie muszą być nigdzie sprawdzane pod kątem nieważności. Zamiast tego „nullable int *” to inny typ, którego wartość można wyodrębnić do wartości null lub do wskaźnika. Oznacza to, że w miejscu, w którym zaczyna się założenie niepuste , albo przechodzisz rejestrowanie wyjątku, albo schodzisz do gałęzi zerowej.

W tym świecie nie występują również błędy poza zakresem. Jeśli kompilator nie może udowodnić, że jest w granicach, spróbuj przepisać, aby kompilator mógł to udowodnić. Jeśli nie może, będziesz musiał ręcznie założyć Wniebowzięcie w tym miejscu; kompilator może później znaleźć sprzeczność.

Ponadto, jeśli nie możesz mieć wskaźnika, który nie został zainicjowany, nie będziesz mieć wskaźników do niezainicjowanej pamięci. Jeśli masz wskaźnik zwolnionej pamięci, kompilator powinien go odrzucić. W Rust istnieją różne typy wskaźników, aby uzasadnić tego rodzaju dowody. Istnieją wyłącznie posiadane wskaźniki (tj .: brak aliasów), wskaźniki do głęboko niezmiennych struktur. Domyślny typ pamięci jest niezmienny itp.

Istnieje również kwestia egzekwowania rzeczywistej, dobrze zdefiniowanej gramatyki protokołów (która obejmuje elementy interfejsu), aby ograniczyć pole powierzchni wejściowej dokładnie do tego, czego się spodziewano. Rzecz „poprawności” polega na: 1) Pozbyciu się wszystkich niezdefiniowanych stanów 2) Zapewnieniu logicznej spójności . Trudność dotarcia tam ma wiele wspólnego ze stosowaniem bardzo złego oprzyrządowania (z punktu widzenia poprawności).

Właśnie dlatego dwie najgorsze praktyki to zmienne globalne i gotos. Te rzeczy uniemożliwiają ustawienie warunków przed / po / niezmiennych wokół wszystkiego. Właśnie dlatego typy są tak skuteczne. Gdy typy stają się silniejsze (ostatecznie wykorzystując typy zależne do uwzględnienia rzeczywistej wartości), stają się konstruktywnymi dowodami poprawności same w sobie; powodowanie, że niespójne programy kończą się niepowodzeniem.

Pamiętaj, że nie chodzi tylko o głupie błędy. Chodzi również o obronę bazy kodu przed sprytnymi infiltratorami. Będą przypadki, w których musisz odrzucić zgłoszenie bez przekonującego, wygenerowanego maszynowo dowodu ważnych właściwości, takich jak „postępuje zgodnie z formalnie określonym protokołem”.



1

automatyczna / formalna weryfikacja oprogramowania jest przydatna i może w niektórych przypadkach pomóc, ale jak zauważyli inni, nie jest to srebrna kula. można zauważyć, że OpenSSL jest podatny na ataki ze względu na to, że jest open source, a mimo to jest używany komercyjnie i w całej branży, szeroko stosowany i nie musi być poddawany szczegółowej recenzji przed wydaniem (zastanawia się, czy w projekcie są jeszcze płatni programiści). wada została odkryta w zasadzie poprzez przegląd kodu po wydaniu, a kod został najwyraźniej sprawdzony przed wydaniem (zauważ jednak, że prawdopodobnie nie ma sposobu na śledzenie, kto dokonał przeglądu wewnętrznego kodu). „możliwy do nauczenia się moment” z sercem serca (między innymi) to zasadniczo lepsze przeglądanie kodu, najlepiej przed wydaniem esp bardzo wrażliwego kodu, być może lepiej śledzone. być może OpenSSL będzie teraz podlegał większej kontroli.

więcej bkg z mediów szczegółowo opisujących jego pochodzenie:

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.