Kryptografia jest ważna, ale to nie jest magic dust, którym wystarczy posypać system, by stał się on automatycznie bezpieczny. Z kryptografii należy korzystać, ale trzeba znać jej ograniczenia. Niestety, w trakcie testów często spotykam się z sytuacją, w której kryptografia jest użyta w sposób nieprawidłowy. Dziś trochę na ten temat.
Trochę o kryptografii
Szyfrowanie nie gwarantuje integralności i autentyczności danych
Wspominałem o tym problemie kilkukrotnie. Wykorzystałem go między innymi w ramach tego przykładu w bootcamp (patrz też: Bootcamp IIa: rozwiązanie (prawie)), ale błąd ten jest na tyle powszechny, że napiszę to jeszcze raz: szyfrowanie nie chroni integralności danych, a sam fakt poprawnego zaszyfrowania danych nie jest potwierdzeniem źródła ich pochodzenia.
W wielu aplikacjach spotkałem się z próbą wykorzystania kryptografii do uniemożliwienia atakującemu manipulacji pewnymi parametrami, które do tej aplikacji są przekazywane. Ewentualnie jeśli aplikacja składa się z różnych rozłącznych modułów, a dane między nimi przekazywane są za pośrednictwem klienta (np. przez przekierowanie z modułu A do modułu B), to fakt przekazania prawidłowych, zaszyfrowanych danych na wejściu modułu B jest uznawany za potwierdzenie tego, że pochodzą one z modułu A. Problem w tym, że takie założenie jest kompletnie nieuzasadnione.
Pierwszą możliwą ścieżką ataku jest sprawdzenie przez atakującego wszystkich możliwych wartości zaszyfrowanego parametru. Atakujący może liczyć na to, że uda mu się przekazać takie losowe dane, które po rozszyfrowaniu będą sensowne. Umówmy się jednak, że prawdopodobieństwo takiego zdarzenia jest znikome (zakładam, że użyty został właściwy algorytm szyfrowania i został on użyty w sposób prawidłowy).
Ciekawą ścieżką ataku na takie "rozwiązanie" jest również atak padding oracle. Jeśli moduł B będzie informował atakującego (w jakikolwiek sposób), czy dane rozszyfrowały się prawidłowo, czy też przy rozszyfrowaniu napotkano nieprawidłowy padding, atakujący może być w stanie nie tylko rozszyfrować dane przekazywane między modułami, ale również zaszyfrować dowolne, wybrane przez siebie dane i przekazać je do modułu B, który w końcu prawidłowo je rozszyfruje i potraktuje z pełnym zaufaniem, bo naiwnie wierzy, że pochodzą z modułu A.
Można oczywiście mieć nadzieję, że takich błędów nie będzie. Lepiej jednak oprócz szyfrowania zastosować mechanizmy, które integralność i autentyczność danych potwierdzą.
Hash nie jest potwierdzeniem integralności danych
Skoro hash nie jest potwierdzeniem integralności danych, to dlaczego jest do tego wykorzystywany? Dlatego, że hash może służyć do potwierdzenia integralności danych, ale tylko w pewnych specyficznych scenariuszach. Nadaje się on do sprawdzenia, czy plik pobrał się prawidłowo, lub czy nie został on zmodyfikowany od czasu wyliczenia sumy kontrolnej (hasha), w omawianym przypadku jest on jednak niewystarczający.
Wyobraźmy sobie, że mamy do czynienia z ową hipotetyczną aplikacją składającą się z modułów A i B. Moduł A w tej chwili przekazuje do modułu B dane postaci:
sha1(enc(msg))||enc(msg)
Czyli:
- hash zaszyfrowanej wiadomości (w sensie hash z ciphertext tej wiadomości),
- zaszyfrowaną wiadomość,
Problem w tym, że to nic nie daje. Moduł B może co prawda sprawdzić, czy hash zaszyfrowana wiadomość odpowiada przesłanemu z nią hashowi, ale atakujący nie ma żadnego problemu w wyliczeniu prawidłowej wartości sha1(enc(msg)). Hash w tym scenariuszu użycia w żaden sposób nie gwarantuje tego, że przesłane między modułem A i B dane nie zostały zmodyfikowane przez atakującego. Ups...
Kiedy hashe mogą być potwierdzeniem integralności? Wtedy, gdy atakujący nie ma możliwości modyfikacji wyliczonych hashy, jeśli na przykład istnieje jakieś publiczne repozytorium informacji o hashach. Wówczas każdy może weryfikować integralność (na przykład integralność plików, czy pakietów oprogramowania), ale jedynie uprawnione osoby mają prawo dodawać/zmieniać elementy w tym repozytorium. To jest jednak inna sytuacja, niż przesłanie danych i hasha mającego potwierdzać ich integralność przez niezaufane środowisko (co pokazywał inny przykład w moim bootcamp).
Jeśli potrzebujesz integralności i autentyczności, użyj MAC
Dobrym rozwiązaniem wspominanego tutaj problemu jest użycie MAC (patrz: Message Authentication Code). W tym przypadku moduł A do modułu B może przesłać komunikat następującej postaci:
MAC(enc(msg))||enc(msg)
Do wyliczenia wartości MAC potrzebny jest oczywiście współdzielony klucz, który musi znać zarówno moduł A, jak i moduł B. Nie zna go natomiast atakujący i o ile nie ma dodatkowych błędów w implementacji (Ataki czasowe na OpenID i OAuth (Twitter, Digg i Wikipedia podatne na atak)), jedyne co może zrobić, to próbować odgadnąć właściwą wartość MAC.
Spotykam się również czasem z następującą konstrukcją:
sha1(enc(msg)||klucz)||enc(msg)
W tym przypadku teoretycznie atakujący nie jest w stanie uzyskać prawidłowej wartości pierwszej części komunikatu, bo nie zna właściwej wartości klucza. Ta konstrukcja nie jest jednak prawidłowa, zamiast niej należy stosować na przykład HMAC (Hash-based Message Authentication Code). HMAC oczywiście nie jest jedynym dostępnym algorytmem (tu więcej informacji), jego niewątpliwą zaletą jest natomiast to, że zwykle jego implementacja jest dostępna w ramach frameworku, z którego użyciem aplikacja jest tworzona.
I w tym miejscu, skoro znamy mechanizm, który potwierdza integralność i autentyczność danych, warto się zastanowić, czy aby na pewno potrzebujemy je jeszcze szyfrować.
Oczywiście do potwierdzenia autentyczności tych danych można wykorzystać również podpisy cyfrowe, nie zawsze jest to jednak niezbędne. A jeśli już używasz podpisów cyfrowych, upewnij się, czy prawidłowo je weryfikujesz i czy przypadkiem nie posyłasz wyników tej weryfikacji do /dev/null...
No cóż za zbieg okoliczności. Pojawił się dobry artykuł mówiący o tym, dlaczego należy używać HMAC. Ostatnio o tym wspominałem. Przy okazji akurat dzisiaj na WhiteHat Security Blog wygasł certyfikat. I z tego wszystkiego aż dodałem kolejne zadanie do m
Przesłany: Mar 31, 16:49