Mapa drogowa memory safety to plan z właścicielami i terminami. Możesz ją napisać w tym kwartale, nie dotykając większości swojego kodu w C i C++. Przepisywanie przyjdzie później i obejmie tylko te części, które tego wymagają. Na teraz potrzebujesz jasnego obrazu tego, gdzie leży niebezpieczny kod, sposobu na jego ograniczenie oraz zapisu podjętych decyzji.
Dokument CISA The Case for Memory Safe Roadmaps opisuje, jak producenci oprogramowania mogą przejść na języki bezpieczne pamięciowo (MSL), aby wyeliminować podatności związane z bezpieczeństwem pamięci. CISA prosi producentów o publikowanie map drogowych, dzięki którym klienci zobaczą, jak producent odpowiada za wyniki w obszarze bezpieczeństwa, stosuje radykalną przejrzystość i przyjmuje podejście odgórne. To zasady Secure by Design, a dokument jest adresowany zarówno do kadry zarządzającej, jak i do inżynierów. Ta druga grupa odbiorców ma znaczenie: mapa drogowa bez sponsora zostaje w wiki.
Co mówią źródła, a czego nie mówią
Strona z podsumowaniem CISA wspomina wyłącznie o przejściu na MSL oraz o stworzeniu i publikacji mapy drogowej. Nie pokazuje pełnej treści dokumentu, dlatego nie twierdzimy niczego o tym, co zawiera on na temat fuzzingu czy izolacji procesów. Te dwa kroki poniżej to nasza praktyka na lata między "mamy dziś C++" a "mamy bezpieczniejszy język w częściach, które mają znaczenie". Traktuj je jako zabezpieczenia tymczasowe, a nie zamiennik mapy drogowej.
Źródła nie podają też żadnych liczb mówiących, ile ryzyka usuwa dany krok. Nie będziemy ich zmyślać. Zmierz to samodzielnie, na podstawie własnej historii awarii i podatności.
Krok 1: zrób inwentaryzację niebezpiecznego kodu
Nie ustalisz priorytetów dla czegoś, czego nie wypisałeś. Przygotuj tabelę dla każdego komponentu, a nie dla każdego repozytorium, bo jedno repozytorium często łączy narzędzie, które spokojnie można zostawić, z parserem sieciowym.
| Pole | Dlaczego jest ważne |
|---|---|
| Język i standard | Pokazuje, co można, a czego nie można migrować |
| Osiągalny z zewnątrz? | Sieć, plik, USB, port szeregowy, kanał aktualizacji |
| Parsuje niezaufane dane wejściowe? | Decyduje o priorytecie fuzzingu i przepisania |
| Z jakimi uprawnieniami działa? | Decyduje o tym, ile da izolacja |
| Zewnętrzny czy nasz? | Decyduje o tym, kto może to naprawić |
| Właściciel i ostatnia istotna zmiana | Martwy kod często można usunąć |
Uwzględnij dołączone biblioteki zewnętrzne (vendored) i komponenty firmware'u. Zwłaszcza zespoły sprzętowe mają zwykle stary kod w C w bootloaderach, sterownikach i procedurach obsługi aktualizacji, do których nikt nie zaglądał od lat. Te komponenty zasługują na osobny wiersz.
Krok 2: izoluj to, czego nie możesz jeszcze zastąpić
Dla każdego komponentu oznaczonego jako osiągalny z zewnątrz i parsujący niezaufane dane zadaj pytanie: co się stanie, jeśli zostanie w pełni przejęty? Jeśli odpowiedź brzmi "wszystko", to jest twoja pierwsza zmiana w projekcie. Przenieś parser do osobnego procesu, odbierz mu uprawnienia i zablokuj dostęp do plików, gniazd i kluczy, których nie potrzebuje.
To nie naprawia błędu. Ogranicza natomiast to, ile ten błąd jest wart dla atakującego. Zgadza się to z celem opisanym w NIST Secure Software Development Framework (SP 800-218), czyli ograniczaniem potencjalnych skutków wykorzystania podatności, których jeszcze nie znalazłeś.
Krok 3: fuzzuj punkty wejścia
Weź z inwentarza komponenty parsujące niezaufane dane wejściowe i napisz dla każdego z nich harness do fuzzingu. Uruchamiaj harnessy w CI z limitem czasu i zachowuj korpus awarii. Zacznij od formatów plików, komunikatów protokołów i pakietów aktualizacji, bo to ścieżki, do których dotrze osoba z zewnątrz.
Buduj binaria do fuzzingu z włączonym w kompilatorze wykrywaniem błędów pamięci, aby ciche odczytanie poza zakresem (overread) stało się widoczną awarią. Każda przeanalizowana awaria daje ci coś, czego potrzebuje mapa drogowa: dowód, które komponenty zasługują na przepisanie. Jeśli harness nie znajduje niczego po długich przebiegach, to również jest użyteczne i pozwala obniżyć priorytet tego komponentu.
Krok 4: wybierz krótką listę do przepisania
Na podstawie trzech pierwszych kroków wybierz kandydatów. Dobry pierwszy kandydat do przepisania ma następujące cechy:
- Parsuje niezaufane dane wejściowe.
- Ma wąski interfejs, więc można go zastąpić za tym samym API.
- Powodował awarie podczas fuzzingu albo ma historię błędów.
- Jest na tyle mały, że jeden zespół zdąży go skończyć w jednym lub dwóch cyklach wydawniczych.
Stabilny, wewnętrzny kod, który nie jest wystawiony na zewnątrz, zostaw w C lub C++ i napisz o tym w mapie drogowej. Wpis "nie planujemy, izolowany, fuzzowany" przy komponencie jest uczciwy, a klienci potrafią z nim pracować. To nasza interpretacja radykalnej przejrzystości: przedstawiasz swoje stanowisko, a nie każdy wiersz musi kończyć się przepisaniem. Podsumowanie CISA nazywa tę zasadę, ale nie mówi, co oznacza ona dla komponentów, które zostają w C lub C++.
Nowe komponenty to najtańsze miejsce na start. Zdecyduj już teraz, w jakim języku powstanie nowy kod wystawiony do sieci, i zapisz to w standardach inżynierskich.
Krok 5: zapisz to i powiąż z procesem
NIST opisuje SSDF jako podstawowy zestaw ogólnych praktyk, które włączasz do dowolnego cyklu życia oprogramowania, jaki już stosujesz, aby wyeliminować przyczyny źródłowe i zapobiec nawrotom podatności. Wpisz swoje zasady memory safety w ten istniejący proces: wybór języka dla nowego kodu, fuzzing w CI, bramka przeglądu dla niebezpiecznych konstrukcji. SSDF daje też nabywcom i dostawcom wspólne słownictwo, co pomaga, gdy ankieta bezpieczeństwa klienta pyta o memory safety.
Jeśli szukasz list kontrolnych dotyczących kodowania, projekt OWASP Secure Coding Practices Quick Reference Guide jest zarchiwizowany. Jego listy kontrolne przeniesiono do OWASP Developer Guide, więc korzystaj z tej wersji, a nie z nieaktualnego pliku PDF.
Od czego zacząć
- Wyeksportuj listę wszystkich komponentów w C i C++, wraz z kodem vendored i firmware'em, i wypełnij powyższą tabelę inwentaryzacji. Ogranicz to do dwóch tygodni.
- Oznacz komponenty, które czytają niezaufane dane wejściowe, i wypisz, do czego każdy z nich ma dostęp w razie przejęcia.
- Umieść trzy najbardziej wystawione komponenty w osobnych procesach o niskich uprawnieniach albo zaplanuj tę zmianę z wyznaczonym właścicielem.
- Napisz harnessy do fuzzingu dla tych samych komponentów i uruchom je w CI. Przeglądaj awarie co tydzień.
- Wybierz jeden komponent do przepisania w języku bezpiecznym pamięciowo i ustal regułę językową dla całego nowego kodu wystawionego do sieci.
- Opublikuj jednostronicową wewnętrzną mapę drogową z właścicielami i terminami, a potem zdecyduj, co udostępnisz klientom. Jeśli chcesz niezależnej weryfikacji, nasza usługa bezpiecznego wytwarzania oprogramowania i testy penetracyjne mogą objąć przegląd inwentarza i testy izolowanych komponentów.
