Cyberbezpieczeństwo okiem programisty

SY0-601: Metody łamania haseł

S

Ten artykuł powstał na bazie moich notatek, które sporządzam na bieżąco, przygotowując się do egzaminu CompTIA Security+ SY0-601. Notatki są w formie opisu zagadnień obowiązujących na egzaminie, zawartych na oficjalnej liście: CompTIA Security+ Certification Exam Objectives. Główne materiały, z których korzystam przygotowując się do egzaminu:

Proszę mieć na uwadze, że artykuł nie wyczerpuje wszystkich opisanych tutaj tematów, ale oprócz podstawowych informacji wymaganych na egzaminie, staram się delikatnie rozszerzać zakres opisywanych tutaj zagadnień o dodatkową wiedzę.


W tym odcinku serii artykułów opisujących zagadnienia SY0-601 przyjrzymy się metodom przechowywania haseł statycznych oraz popularnym technikom łamania haseł. Artykuł trochę różni się od poprzednich, ponieważ temat jest dla mnie na tyle interesujący, że postanowiłem go rozszerzyć o informacje ponadprogramowe. Dlatego mocno posiłkowałem się książką Bezpieczeństwo aplikacji webowych wydaną przez firmę Securitum (patron serwisu Sekurak), którą generalnie polecam.

Innym materiałem godnym polecenia jest seria artykułów Kompendium bezpieczeństwa haseł – atak i obrona (część I; część II oraz część III).

Przechowywanie haseł

Zanim przejdziemy do omawiania różnych technik łamania haseł, warto sobie wyjaśnić na wstępie czym jest proces uwierzytelniania (ang. authentication). W najprostszych słowach, jest to potwierdzenie, że na pewno jesteś osobą, za którą się podajesz. Innym często używanym zwrotem jest autentykacja, choć formalnie jest on uznawany za niepoprawny.

Przy okazji uwierzytelniania, warto moim zdaniem wspomnieć jeszcze o takim terminie jak autoryzacja (ang. authorization), który bywa czasami używany zamiennie z uwierzytelnianiem, co również jest błędem. Proces autoryzacji polega na przyznawaniu użytkownikowi dostępu do określonych zasobów w zależności od jego uprawnień. Innymi słowy, proces uwierzytelniania weryfikuje czy jesteś osobą za którą się podajesz, a proces autoryzacji sprawdza do czego masz dostęp. Żeby jeszcze bardziej to rozjaśnić, posłużę się analogią na przykładzie wsiadania zwyczajnego pasażera do samolotu pasażerskiego na lotnisku. Przed samym wejściem na pokład sprawdzany jest dowód osobisty lub paszport pasażera wraz z kartą pokładową, żeby potwierdzić jego tożsamość (uwierzytelnienie). Kiedy zweryfikowany pasażer jest już na pokładzie może usiąść na swoim przydzielonym miejscu, ale nie może wejść do kabiny pilotów, bo nie ma odpowiednich uprawnień (nie przeszedł pomyślnie procesu autoryzacji).

W dzisiejszych czasach mamy do wyboru wiele wyrafinowanych technik uwierzytelniania (np. skanery biometryczne), ale ciągle najpopularniejszym sposobem zabezpieczania tajnych informacji jest hasło statyczne, czyli tekstowy ciąg znaków znany wyłącznie osobie, która chce się uwierzytelnić (a przynajmniej takie jest założenie). Popularność tej metody wynika z prostoty jej użytkowania i implementacji oraz niewielkich kosztów. Poza tym jest to technologia, która została powszechnie zaakceptowana przez społeczeństwo. Pomimo stałego wzrostu popularności skanerów biometrycznych (w tym czytników linii papilarnych), ludzie ciągle obawiają się, że rozpowszechnienie tej metody zabezpieczeń doprowadzi do masowego ucinania palców lub innych niepokojących sytuacji. Teoretycznie skanery powinny być odporne na taki spoofing (np. czytnik linii papilarnych powinien rozpoznać, że palec jest martwy), ale napastnik może o tym nie wiedzieć, dlatego niekoniecznie jest to nieuzasadniona obawa :).

https://www.youtube.com/watch?t=87&v=3bt7u5rV__Y&feature=youtu.be
Fragment filmu Fast & Furious presents: Hobbs & Shaw (2019).

Wracając do haseł statycznych, kiedy użytkownik chce dostać się do systemu, jest zobligowany do podania swojego identyfikatora (np. nazwa użytkownika lub adres email) oraz hasła. Moduł systemu odpowiedzialny za proces uwierzytelniania sprawdza więc czy podany użytkownik istnieje oraz porównuje podane hasło z tym zapisanym w bazie danych. Jeśli wszystko się zgadza, użytkownik otrzymuje dostęp do systemu. Oznacza to, że hasła wszystkich użytkowników muszą być bezpieczne przechowywane gdzieś w systemie i ten obowiązek spoczywa na twórcach i administratorach danego systemu.

Dawno temu, bo w 1979, pojawiła się publikacja Roberta Morrisa oraz Kena Thompsona zatytułowana Password Security: A Case History. Panowie opisali w niej kilka koncepcji dotyczących bezpiecznego przechowywania haseł statycznych w systemach Unix. Wiele z nich wykorzystuje się do dziś i to nie tylko w systemach uniksowych. Wspomniane koncepcje to m.in. przechowywanie skrótów w osobnym, chronionym pliku; wymuszenie na użytkownikach ustawiania bardziej skomplikowanych haseł; celowe spowolnienie operacji kryptograficznych oraz tzw. solenie haseł.

Plaintext/unencrypted

Chyba najcięższym dzisiaj grzechem jest przechowywanie haseł użytkowników w postaci jawnego tekstu (ang. plaintext), który jest niezaszyfrowany (ang. unencrypted). W teorii nikt poza osobami uprawnionymi nie powinien mieć dostępu do bazy danych zawierającej hasła, ale doskonale wiemy, że nasz świat nie jest idealny. W każdej chwili może dojść do wycieku danych; ktoś może włamać się na serwer i wykraść wszystkie informacje, łącznie z naszymi hasłami; nawet uprawniona osoba, która ma wgląd do naszych danych może okazać się nie do końca uczciwa. Jak widać zagrożeń jest bardzo wiele. Sytuację pogarsza fakt, że sporo osób używa identycznego hasła do kilku serwisów, ze względu na wygodę. Co jest jeszcze gorsze, to samo hasło bywa używane zarówno do konta na Pudelku, jak i do bankowości elektronicznej.

Bardzo często zdarza się, że dane, które zostały wykradzione lub wyciekły przez pomyłkę, stają się po jakimś czasie ogólnodostępne w Internecie. Na domiar złego, jeśli hasła użytkowników nie zostały w jakiś sposób zaszyfrowane, a ci z przyzwyczajenia używają tych samych haseł do wielu kont, to cyberprzestępcy nawet nie muszą się starać. Wystarczy, że natrafią na jakiś duży wyciek i dowiedzą się, gdzie jeszcze dany użytkownik ma konto (oprócz skompromitowanego serwisu). Istnieją nawet specjalne narzędzia, które automatycznie wyszukują serwisy z kontami zarejestrowanymi pod daną nazwą użytkownika, na przykład:

Ciekawostka: proces wyszukiwania kont użytkowników jest elementem tzw. białego wywiadu (OSINT = Open Source Intelligence).

W związku z zagrożeniami opisanymi powyżej, mam nadzieję, że czasy, w których aplikacje składują hasła swoich użytkowników w formie jawnego tekstu (ang. plaintext), minęły bezpowrotnie. A nawet jeśli to się jeszcze zdarza, to mam nadzieję, że są to odosobnione przypadki, tym bardziej, że dzisiejsze narzędzia do wytwarzania oprogramowania znacznie ułatwiają zabezpieczanie haseł. Jeśli więc, jako użytkownicy, trafimy na system bądź aplikację, w której procedura przypomnienia hasła polega na otrzymaniu identycznego hasła, jakiego użyliśmy wcześniej (zamiast np. możliwości ustawienia nowego hasła po wcześniejszym zweryfikowaniu tożsamości), to możemy być pewni, że nasze tajne informacje nie są w żaden sposób chronione i powinniśmy natychmiast zaprzestać korzystania z takiej aplikacji. Trudno bowiem zaufać twórcom, którzy robią takie rzeczy w dzisiejszych czasach i nawet hak-atak wyprzedzający tutaj nie pomoże :).

Można jeszcze spotkać się z techniką tzw. zaciemniania (ang. obfuscation), czyli np. przechowywania haseł w nietypowych i najmniej spodziewanych miejscach (np. bardzo głęboko ukryta ścieżka w katalogu systemowym) lub dodanie do haseł szumu w postaci losowych znaków. Jest ona jednak traktowana na równi z przechowywaniem haseł w postaci jawnej, ponieważ jest to tylko pozorna “ohrona”.

Ciphertext (szyfrogram)

Innym sposobem przechowywania haseł może być szyfrogram (ang. ciphertext), czyli po prostu hasła zaszyfrowane wybranym algorytmem szyfrującym. Jest to podejście odrobinę lepsze niż przechowywanie wrażliwych danych w postaci jawnego tekstu, ale wbrew pozorom bardzo dalekie od ideału. Kłopotem z takim podejściem jest fakt, że szyfrogram można najzwyczajniej w świecie odszyfrować posiadając odpowiedni klucz. Jeśli taki klucz wpadnie w niepowołane ręce, to tak jakbyśmy w ogóle nie szyfrowali haseł – dochodzi więc kolejny problem bezpiecznego przechowywania klucza.

Starsi czytelnicy (tacy jak ja :)) na pewno pamiętają nasz rodzimy komunikator Gadu-Gadu. A czy pamiętacie jak przez pewien czas hasło do konta, w postaci szyfrogramu, było przechowywane w lokalnym pliku konfiguracyjnym aplikacji config.dat? Niestety, użyty wówczas algorytm szyfrujący nie należał do najmocniejszych, więc odzyskanie hasła z tego pliku nie było dużym wyzwaniem, o czym świadczy chociażby ten tekst: Dekoder Haseł Gadu-Gadu.

Skoro tekst jawny jest nie do przyjęcia, a szyfrogramy nie są wcale lepsze, to co nam zostaje? Potrzebny jest nam mechanizm, który spowoduje, że hasło zostanie zakodowane w sposób nieodwracalny i tutaj z pomocą przychodzi nam matematyka, cała na biało ;). Z każdego hasła można bowiem wyliczyć tzw. skrót (ang. hash), który jest praktycznie nieodwracalny.

Hash (skrót hasła)

Czym jest hash?

Skrót (ang. hash lub hash-code) jest to nieuporządkowany ciąg znaków o stałej długości, wygenerowany za pomocą specjalnej funkcji matematycznej na podstawie wejściowego ciągu znaków (w tym kontekście, hasła użytkownika). Skrót charakteryzuje się tym, że odzyskanie hasła na jego podstawie jest praktycznie niemożliwe (w przeciwieństwie do tekstu zaszyfrowanego kluczem) oraz tym, że jest unikalny dla różnych danych wejściowych (dwa różne hasła nie mogą wygenerować identycznego skrótu, przynajmniej w założeniu).

W teorii wszystko wygląda świetnie i nawet kiedy baza danych z naszymi hasłami wycieknie, to i tak nikt nie będzie w stanie odczytać oryginalnego hasła, prawda? Niestety, rzeczywistość jest często rozczarowująca i to czy nasze hasła są bezpieczne, zależy zarówno od jakości wspomnianej funkcji matematycznej, jak i od siły naszego hasła, o czym przekonamy się w dalszej części tego artykułu.

Zobaczmy jak może wyglądać taki skrót, wygenerowany przez specjalną funkcję o nazwie SHA-256. Weźmy dla przykładu hasło o poziomie skomplikowania charakterystycznym dla większości polskich polityków, czyli admin123. Hash dla tego hasła, wyliczony przez funkcję SHA-256, ma następującą postać: 240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9. Co ciekawe, kiedy zmienimy choćby jeden znak w oryginalnym haśle to wyliczony skrót będzie wyglądał zupełnie inaczej. Na przykład, dla hasła Admin123 (zmiana wielkości jednej litery) skrót SHA-256 wygląda już tak: 3b612c75a7b5048a435fb6ec81e52ff92d6d795a8b5a9c17070f6a63c97a53b2. Jeśli chcemy sobie poeksperymentować z tą funkcją (i nie tylko), możemy to zrobić za pomocą narzędzia dostępnego online.

W tym momencie warto również sobie wyjaśnić, że funkcja SHA-256 zawdzięcza swoją nazwę m.in. temu, że zawsze zwraca skrót o długości 256 bitów. Jeśli policzycie sobie znaki w powyższym przykładzie, to zauważycie, że ten wyjściowy skrót wygląda jak ciąg tekstu składający się z 64 znaków. Wiedząc, że jeden znak zajmuje przynajmniej 1 bajt (8 bitów), to już coś nam się nie zgadza – przecież 64 znaki to minimum 64 bajty, czyli 64 * 8 = 512 bitów. O co więc tutaj chodzi? Okazuje się, że sprawa jest prosta: hasha w takiej postaci nie powinniśmy traktować jako tekstowego ciągu znaków, ale jako zbiór cyfr heksadecymalnych. Oznacza to, że tak naprawdę mamy tutaj 32 liczby szesnastkowe (3b, 61, 2c i tak dalej), gdzie każda z nich zajmuje 8 bitów (32 liczby szesnastkowe * 8 bitów = 256 bitów na wyjściu).

Widzimy już, że wyliczony hash znacząco różni się od oryginalnego tekstu i nawet drobna zmiana w danych wejściowych powoduje wygenerowanie zupełnie innego ciągu znaków. Być może teraz nasuwa się pytanie: skoro w bazie przechowywane są tylko wyliczone skróty haseł, to skąd system wie, że podano prawidłowe hasło podczas logowania? Odpowiedź jest prosta: za każdym razem kiedy podajemy hasło podczas procesu logowania, liczony jest dla niego skrót, który jest porównywany z tym zapisanym w bazie danych. Oznacza to, że ani system, ani osoby nim zarządzające nie mają pojęcia jakie jest Twoje oryginalne hasło i co za tym idzie, proces odzyskiwania konta (jeśli zapomnieliśmy danych uwierzytelniających) polega zwyczajnie na wygenerowaniu nowego hasła.

W tym artykule omawiamy skróty w kontekście bezpiecznego przechowywania haseł, ale ze względu na swoje właściwości, hashe mają również inne zastosowania:

  • Łatwe znakowanie plików w bazach danych – np. jeśli chcemy szybko porównać tekst z zawartością pliku tekstowego, to zamiast jego całej zawartości, w bazie możemy przechowywać tylko skrót wyliczony na podstawie tejże zawartości. Wiedząc, że nawet niewielka zmiana powoduje wygenerowanie zupełnie innego skrótu, możemy wykorzystać tę właściwość do szybkiego porównywania i zidentyfikowania ewentualnych duplikatów.
  • Gwarancja niezmienności danych – to samo co wyżej, czyli wykorzystanie unikatowego charakteru skrótu do wygenerowania sygnatury dla określonych danych. Jeśli nie mamy pewności, czy ściągnięty plik nie został zmodyfikowany gdzieś po drodze, możemy dla pewności porównać sygnaturę ściągniętego pliku (hash) z tą udostępnioną dla oryginalnego pliku.
  • Podpisy elektroniczne – dzięki nim wiemy, że dane zostały przesłane przez zaufanego nadawcę. Hash pełni tutaj funkcję pomocniczą, jako sygnatura oryginalnych danych, która później jest jeszcze szyfrowana za pomocą algorytmu szyfrowania asymetrycznego. Dzięki temu sam podpis elektroniczny nie generuje ogromnego narzutu dodatkowy bajtów do wysłania.
  • Technologia blockchain – w telegraficznym skrócie i bardzo dużym uproszczeniu: blockchain to nic innego jak rozproszona baza danych z zapisanym łańcuchem transakcji, do którego stale dodawane są nowe transakcje. Transakcją może być np. informacja o tym, że ktoś zakupił bądź sprzedał określoną ilość Bitcoinów. Zastosowanie skrótów pozwala na zweryfikowanie czy nowa transakcja jest prawdziwa i dozwolona, zanim zostanie dodana do łańcucha.

Obliczanie skrótu

Generowanie skrótów jest możliwe dzięki specjalnym funkcjom, nazywanym funkcjami skrótu (ang. hash function) bądź kryptograficznymi funkcjami mieszającymi (lub haszującymi). Jeśli chcemy dogłębniej zrozumieć naturę takich funkcji, musimy sobie najpierw wyjaśnić czym są funkcje mieszające.

Funkcja mieszająca

Najprostszą i najkrótszą definicją funkcji mieszającej (haszującej) jest funkcja matematyczna, która generuje na wyjściu ciąg znaków o stałej i określonej długości, na podstawie wejściowego ciągu znaków o dowolnej długości. Z technicznego punktu widzenia, można powiedzieć, że zadaniem takiej funkcji jest utworzenie krótkiej i unikalnej sygnatury dla dowolnie dużego zbioru danych. Algorytm funkcji haszującej jest deterministyczny, czyli w prostych słowach: to samo wejście da zawsze identyczne wyjście.

W książce Bezpieczeństwo aplikacji webowych, możemy trafić na informację, że w polskojęzycznych materiałach często terminu funkcja skrótu używa się zamiennie z terminem funkcja mieszająca, co wg autora jest błędem. Pomyłka wynika zapewne z faktu, że zarówno funkcja mieszająca, jak i funkcja skrótu mają identyczne nazwy w języku angielskim tj. hash function, ale z kryptograficznego punktu widzenia funkcja skrótu jest tak naprawdę bardziej wyspecjalizowaną wersją funkcji mieszającej (w prostych słowach: każda funkcja skrótu jest funkcją mieszającą, ale nie każda funkcja mieszająca jest funkcją skrótu), dlatego często określa się ją mianem kryptograficznej funkcji mieszającej bądź haszującej.

Każda funkcja mieszająca (nazwijmy ją h(x)) ma dwie charakterystyczne właściwości:

  • Kompresja (ang. compression) – dla ciągu bitów x o pewnej skończonej długości, przypisany jest na wyjściu ciąg bitów `h(x) o stałej długości n. Oznacza to tyle, że nieważne jak długi ciąg znaków podamy na wejściu, na wyjściu zawsze otrzymamy ciąg o stałej długości, która zależy od funkcji mieszającej (np. w przypadku funkcji mieszającej zwracającej ciąg o długości 32 znaków, nie ma znaczenia czy tekst na wejściu będzie składał się z 500 czy z 100 znaków – na wyjściu zawsze będzie 32).
  • Łatwość/szybkość obliczeń (ang. ease of computation) – ta właściwość raczej nie wymaga specjalnych wyjaśnień, ale chodzi o to, aby obliczenia wykonane przez funkcje były na tyle szybkie, żeby można było przetworzyć duże ilości danych w krótkim czasie.

Kryptograficzna funkcja mieszająca (funkcja skrótu)

Funkcja skrótu jest rozwinięciem funkcji mieszającej. To znaczy, że pełni de facto identyczną rolę, czyli generowanie ciągu znaków stałej długości z danych wejściowych o różnej długości, ale oprócz dwóch cech wymienionych powyżej, musi spełniać dodatkowe kryteria. Są one niezbędne, żeby daną funkcję uznać za bezpieczną i kwalifikującą się do zastosowań w kryptografii. Funkcję skrótu również oznaczmy jako h(x) i przyjrzyjmy się tym kryteriom:

  • Nieodwracalność (ang. preimage resistance) – polega na tym, że odtworzenie danych wejściowych na podstawie danych wyjściowych jest mocno problematyczne. Matematycznie można to określić w ten sposób, że jeśli funkcję skrótu oznaczymy jako h(x) = y, to znalezienie x (przeciwobraz funkcji; ang. preimage) dla danego y (obraz funkcji; ang. image) jest obliczeniowo trudne.
    • Funkcje, których nie da się odwrócić nazywamy funkcjami jednokierunkowymi (ang. one-way function). Szkopuł w tym, że funkcje prawdziwie jednokierunkowe istnieją tylko teoretycznie, ponieważ do tej pory nie udowodniono matematycznie ich istnienia. Istnieją jednak funkcje, które są dobrymi kandydatami na miano funkcji jednokierunkowej, ponieważ są łatwe do wyliczenia, ale obliczeniowo trudne do odwrócenia. Bardzo prostym przykładem może być mnożenie dużych liczb pierwszych – o ile arytmetyczna operacja mnożenia jest prosta do przeprowadzenia, to już znalezienie tych liczb tylko na podstawie iloczynu (wyniku mnożenia) może sprawiać problem ze względu na złożoność wymaganych obliczeń.
  • Słaba odporność na kolizje (ang. second preimage resistance) – charakteryzuje się tym, że znalezienie dwóch różnych wejściowych ciągów znaków, które dadzą w wyniku identyczny skrót, jest trudne obliczeniowo. Formalnie można to zapisać w ten sposób: dla danego x1, znalezienie x2 takiego, że x1 ≠ x2 oraz h(x1) ≠ h(x2), jest obliczeniowo trudne.
    • Z powyższej definicji wynika, że kolizja (ang. collision), w kontekście funkcji skrótu, występuje wtedy gdy różne dane wejściowe dadzą na wyjściu identyczny hash. Zapewne dostrzegasz już, dlaczego jest to problem: kryptograficzne funkcje mieszające ze względu na swoje zastosowania, czyli generowanie unikatowych sygnatur trudnych do odwrócenia, nie powinny generować identycznych skrótów dla różnych danych wejściowych (bardzo jaskrawy przykład: jeśli ciągi znaków Bolek oraz Lolek spowodują wygenerowanie identycznego hasha, to do systemu przechowującego hasła w postaci skrótów, można będzie dostać się stosując zarówno jedno, jak i drugie hasło).
    • Możemy więc przyjąć, że najlepsza funkcja skrótu to taka, która nigdy nie wygeneruje identycznego hasha dla dwóch różnych ciągów wejściowych. Zastanówmy się jednak, czy jest to w ogóle możliwe. Wiemy już z definicji podanych powyżej, że funkcja mieszająca zawsze zwraca ciąg znaków o stałej długości. Oznacza to, że zakres wartości tej funkcji będzie zawsze mniejszy wielkość danych wejściowych, ergo prędzej czy później do kolizji dojdzie. Na szczęście istnieją algorytmy, dla których zakres danych wyjściowych jest na tyle duży, że jeszcze nie odnaleziono dla nich kolizji – przykładem jest wspomniany wcześniej algorytm SHA-256 (chyba, że w NSA już znaleźli i się nie chwalą ;)).
  • Silna odporność na kolizje (ang. collision resistance) – polega na tym, że znalezienie JAKIEJKOLWIEK kolizji jest trudne obliczeniowo, w odróżnieniu od słabej odporności, która dotyczy znalezienia kolizji dla konkretnego skrótu. Sformalizujmy sobie teraz ten warunek, tak jak czyniliśmy to wcześniej: znalezienie jakiejkolwiek pary x1 oraz x2 takiej, że x1 ≠ x2 oraz h(x1) = h(x2) jest obliczeniowo trudne. Warto również wspomnieć, że silna odporność na kolizję implikuje słabą odporność (jeśli funkcja jest silnie odporna na kolizje to automatycznie spełnia warunek słabej odporności).
    • Wspomniałem już przy okazji omawiania słabej odporności, że prawdopodobieństwo znalezienia kolizji jest nieuniknione z matematycznego punktu widzenia (z jednej strony nieograniczona liczba danych wejściowych, z drugiej ograniczona liczba danych wyjściowych ze względu na ich stałą wielkość). Jeśli jednak znalezienie jakiejkolwiek kolizji wymaga milionów lat obliczeń to uznajemy, że funkcja charakteryzuje się silną bezkolizyjnością (np. wspomniany już wcześniej algorytm SHA-256).
Właściwości kryptograficznych funkcji mieszających.

Przykłady funkcji skrótu

Skoro znamy już charakterystykę kryptograficznych funkcji mieszających, przyjrzyjmy się ich najpopularniejszym implementacjom:

  • Message Digest (MD) jest rodziną algorytmów opracowaną przez Ronalda Rivesta, której najpopularniejszym członkiem jest obecnie MD5, utworzony w 1991 roku i dokładnie opisany w dokumencie RFC-1321. Funkcja MD5 generuje na wyjściu 128-bitowy skrót, co niestety nie jest już wystarczającą długością w dzisiejszych czasach. Złożoność ataku siłowego na taki skrót wynosi 264, co nie jest już wyzwaniem dla aktualnej technologii. Dodatkowo, kryptoanaliza (wnikliwe badanie systemów kryptograficznych celem znalezienia ich słabości) wykazała w 2004 roku, że funkcja MD5 nie jest silnie odporna na kolizje. Jednym z najwydajniejszych obecnie ataków na funkcję MD5 jest tzw. MD5 Tunneling, który pod pewnymi warunkami (jedynie dla dużych łańcuchów/plików binarnych) pozwala na znalezienie kolizji w około minutę.
    • Chociaż nie opracowano jeszcze skutecznego ataku przeciwko słabej odporności na kolizje dla tej funkcji, to stanowczo odradza się jej stosowania w systemach kryptograficznych na rzecz bezpieczniejszych funkcji. Jednakże pomimo swoich niedoskonałości, funkcja dalej bardzo dobrze sprawdza się przy mechanizmach badania sumy kontrolnej plików (potwierdzenie, że ściągnęliśmy właściwy plik).
  • Secure Hash Algorithm (SHA) – rodzina funkcji skrótu opracowana przez NSA (National Security Agency), stanowiąca największą konkurencję dla algorytmów z rodziny MD. Również w tym przypadku możemy wyróżnić kilka wariantów:
    • SHA-0 – oznaczenie pierwszej wersji algorytmu z 1993 roku, który generował 160-bitowy skrót. Wycofany z użytku ze względu na zidentyfikowane słabości (opracowano atak, który umożliwiał znalezienie kolizji w czasie kilkunastu minut).
    • SHA-1 – Opracowany przez NSA następca poprzedniej wersji, który również zwraca 160-bitowy skrót, jednak różnił się implementacją. Aktualnie uważany za niedostatecznie bezpieczny, po tym jak Google w 2017 roku zaprezentował sposób na wygenerowanie kolizji. Dodatkowo, w 2019 roku zaprezentowano względnie wydajną odmianę ataku o nazwie Chosen-Prefix Collision Attack.
    • SHA-2aktualnie najpopularniejszy wariant, dalej uznawany za bezpieczny (oficjalnie jeszcze nie opracowano sposobu na wydajne wyszukiwanie kolizji). W tym wariancie możemy wyróżnić jeszcze 4 odmiany, które różnią się głównie długością generowanego skrótu, tj. SHA-224; SHA-256; SHA-384 oraz SHA-512.
    • SHA-3 – funkcja skrótu wyłoniona w 2012 roku w ramach konkursu przeprowadzonego przez NIST (National Institute of Standards and Technology). Algorytm jest aktualnie uznawany za bezpieczny i ma bardzo dobre opinie, jednak nie jest jeszcze powszechnie stosowany ze względu na niegasnącą popularność poprzednika (SHA-2).

Podsumowując, w chwili pisania tego artykułu (lipiec 2022) zaleca się używanie funkcji z rodziny SHA-2 oraz SHA-3.

Dodatkowe zabezpieczenia

Wiemy już jak działają funkcje skrótu i może teraz nam się wydawać, że jeśli hasła przechowujemy w postaci skrótów wygenerowanych przez bezpieczne i sprawdzone funkcje, to są one zabezpieczone w stu procentach. Wyobraźmy sobie więc następujący scenariusz: baza danych razem z hasłami zostaje wykradziona, a część nie do końca świadomych użytkowników używa hasła typu abc123. Jak myślisz, ile czasu zajmie atakującemu przeprowadzenie ataku siłowego (ang. brute force) w tym przypadku, polegającego na wygenerowaniu skrótów dla wszystkich możliwych kombinacji 6-znakowego hasła? Raczej niewiele.

Z tego względu często stosuje się dodatkowe zabezpieczenia, które mają za zadanie wzmocnić ochronę haseł przechowywanych w postaci hashy. Oto najpopularniejsze z nich:

  • Sól (ang. salt) – pewna wartość (przeważnie losowa), która jest dodawana do hasła użytkownika jeszcze przed wyliczeniem skrótu, przez co w bazie jest zapisany skrót, który nijak nie koresponduje z oryginalnym hasłem użytkownika. Np. jeśli użytkownik wybrał bardzo trywialne hasło, takie jak 12345, a wartości soli to 3edg63jfdh to wynikowy skrót będzie liczony dla hasła 123453edg63jfd. Oczywiście istnieje ryzyko, że wartość soli, która też jest przechowywana w bazie, może również zostać wykradziona – w takim przypadku atak siłowy ciągle jest możliwy, ale utrudniony. Jeśli wartość soli dodanej do każdego hasła jest inna i wygenerowana losowo, to mówimy, że jest to sól dynamiczna (ang. dynamic salt) lub sól losowa (ang. random salt).
  • Pieprz (ang. pepper) – jest to stała wartość, która jest dodawana do wszystkich haseł, podobnie jak wspomniana wcześniej sól. Jednak w odróżnieniu do soli, nie musi być różna dla każdego użytkownika, ale powinna zostać zapisana w innych chronionym miejscu, żeby w przypadku ewentualnego wycieku z bazy danych, atakujący nie miał pojęcia o tej wartości. Innymi określeniami opisującymi ten mechanizm, z jakimi możemy się jeszcze spotkać w literaturze, są sól statyczna (ang. static salt) oraz tajna sól (ang. secret salt). Pierwszy termin dotyczy identycznej wartości dla haseł wszystkich użytkowników, natomiast drugi termin jest bardziej generyczny.
  • Key stretching – jest to celowe wydłużenie czasu obliczeń wykonywanych przez funkcję skrótu. Z jednej strony wymaga się szybkości od funkcji kryptograficznych, ale z drugiej strony funkcja błyskawicznie obliczająca skrót hasła jest pożądana przez atakujących (dzięki temu możliwe jest szybsze przeprowadzanie ataków siłowych). Postanowiono więc pójść na kompromis: użytkownikowi logującemu się do systemu nie robi tak naprawdę różnicy, czy weryfikacja jego danych potrwa 5 czy 500 milisekund, ale dla atakującego, który jest zmuszony sprawdzić miliony kombinacji, jest to już bardzo znaczące utrudnienie, bo całkowity czas potrzebny na złamanie hasła mocno się rozciąga. Implementacja tej techniki polega zazwyczaj na kilkukrotnym wywołaniu funkcji haszującej odpornej na kolizje – czyli z hasła jest liczony skrót, potem z tego skrótu liczony jest kolejny skrót i tak dalej. Okazuje się bowiem, że w przypadku funkcji silnie odpornych na kolizje, wielokrotne wyliczanie skrótu nie zwiększa prawdopodobieństwa znalezienia kolizji.

Klucz PBKDF

Czwarty sposób przechowywania haseł jest najbezpieczniejszym i aktualnie najbardziej zalecanym podejściem, ponieważ stanowi kombinację kryptograficznych funkcji mieszających z dodatkowymi zabezpieczeniami opisanymi powyżej.

Funkcje PBKDF (Password-Based Key Derivation Function) są specjalnymi funkcjami dedykowanymi dla systemów kryptograficznych, które łączą w sobie zalety funkcji skrótu wraz z dodatkowymi technikami zabezpieczeń, takimi jak np. key stretching czy wspomniane powyżej posolenie haseł. W tym przypadku, oprócz typowego skrótu, w bazie zapisywane są dodatkowe informacje charakterystyczne dla tego rodzaju funkcji.

Generowanie skrótu polega na tym, że na wejściu podajemy hasło użytkownika oraz tzw. koszt obliczeń (jeśli zdecydujemy się na wyższy koszt to obliczenie skrótu zajmie więcej czasu, ale też czas ataku siłowego znacznie się wydłuży). Sól możemy dostarczyć sami lub zlecić jej wygenerowanie funkcji za sprawą wewnętrznego generatora liczb pseudolosowych. Wynikiem jest specjalny klucz (ang. master key), który jest następnie przechowywany w bazie.

Kiedy użytkownik chce się później uwierzytelnić za pomocą swojego hasła to odpowiedni algorytm wyodrębnia z wcześniej zapisanego klucza sól oraz podaną złożoność obliczeniową, a następnie wykorzystuje te wartości, żeby obliczyć nowy skrót z właśnie podanym hasłem. Jeśli nowy skrót zgadza się z tym co zostało zapisane wcześniej, użytkownik jest uwierzytelniony i ma dostęp do systemu.

Miejmy na uwadze fakt, że akronim PBKDF nie jest konkretną implementacją, ale określa rodzinę specjalnych funkcji kryptograficznych. Przykłady konkretnych implementacji możemy natomiast znaleźć poniżej:

  • bcrypt – funkcja opracowana w 1999 r. jako element systemu OpenBSD przez Neilsa Probosa oraz Davida Mazieresa. Wykorzystuje pod spodem szyfr blokowy Blowfish, który jest uznawany za niezwykle bezpieczny algorytm szyfrujący.
    • Przykład klucza wygenerowanego przez funkcję bcrypt wygląda następująco: $<version>$<rounds>$<saltaddon>$<pwhash>. Gdzie:
      • <version> – wersja algorytmu.
      • <rounds> – koszt obliczeń (ang. work factor), czyli omówiona wcześniej technika key stretching.
      • <saltaddon> – sól dodana do hasła.
      • <pwhash> – wygenerowany skrót.
    • Każde zwiększenie wartości work factor o 1 podwaja czas obliczeń. Aktualnie zalecaną wartością współczynnika złożoności obliczeniowej jest 12-15: w zależności od mocy obliczeniowej maszyny, czas potrzebny na wygenerowanie skrótu może wahać się od milisekund do kilku sekund.
  • PBKDF2 – funkcja wykorzystywana na szeroką skalę ze względu na silne poparcie środowisk naukowych. Wykorzystywana aktualnie do zabezpieczanie sieci Wi-Fi (WPA/WPA2) oraz używana do przechowywania haseł w systemach Apple.
  • scrypt – funkcja podobna do pozostałych, ale wyróżnia ją jedna cecha: oprócz parametru określającego złożoność obliczeniową, pozwala również określić wymagania pamięciowe (przepustowość pamięci oraz zrównoleglenie algorytmu). W związku z tym wymaga sporych nakładów sprzętowych do złamania. Uważana jest za jeden z najbezpieczniejszych algorytmów PBKDF, ale ma dosyć skomplikowaną konfigurację (zbyt małe wartości nic nie dadzą, a zbyt duże mogą znacząco obniżyć wydajność systemu).
  • Argon2 – jest to stosunkowo świeża funkcja, bo pojawiła się na scenie w 2015 roku jako zwycięzca konkursu Password Hashing Competition.
    • Charakteryzuje się dobrą odpornością na ataki siłowe w wersji równoległej oraz rozproszonej, a także dobrą odpornością na tzw. atak side-channel (atak przeprowadzany na podstawie informacji zebranych z implementacji systemu komputerowego, zamiast z błędów w samym algorytmie – np. sama implementacja algorytmu może być bezbłędna, ale to jak algorytm działa na procesorze może powodować nieprzewidziane problemy).
    • Funkcja ta jest prosta w konfiguracji i aktualnie używana w menadżerze haseł KeePass2 oraz w PHP 7.2, jako wsparcie. Jednak z racji na jej młody wiek oraz ciągle przeprowadzane kryptoanalizy, zalecana jest ostrożność.

Password attacks

Wiemy już, że hasło stanowi pewnego rodzaju przepustkę do tajnych i często cennych informacji. Nic więc dziwnego, że znajdą się osoby, które będą chciały je za wszelką cenę poznać i wykorzystać. To czy cyberprzestępcy poznają nasze hasła zależy zarówno od nas samych, jak i od poziomu zabezpieczeń stosowanego w systemach, z których korzystamy.

W dalszej części artykułu zauważymy, że większość omawianych tutaj sposobów łamania haseł to tzw. ataki wyczerpujące, które bazują na przeprowadzeniu wielu prób odgadnięcia prawidłowego hasła. Do ataków wyczerpujących zaliczamy m.in. ataki siłowe (ang. brute force) polegające na sprawdzeniu każdej możliwej kombinacji znaków; ataki słownikowe (ang. dictonary attack) z wykorzystaniem listy najbardziej prawdopodobnych haseł czy też ataki hybrydowe (ang. hybrid attack), które łączą w sobie różne metody.

Inną istotną klasyfikacją jest podział na ataki zdalne (ang. online) oraz ataki lokalne (ang. offline). Powiemy sobie o nich trochę więcej w dalszej części tego rozdziału, ale na razie wystarczy wiedzieć, że ataki zdalne polegają obejściu modułu uwierzytelniającego systemu dostępnego przez sieć (m.in. przez odgadnięcie hasła istniejącego użytkownika), a ataki lokalne skupiają się głównie na łamaniu skrótów, do których mamy bezpośredni dostęp (np. wykradziona baza danych z hasłami, którą trzymamy na naszym dysku).

Główne czynniki wpływające na skuteczność wspomnianych ataków to:

  • Złożoność oraz długość używanego hasła – im hasło jest dłuższe i mniej przewidywalne (tj. składające się z losowych znaków alfanumerycznych z użyciem znaków specjalnych), tym więcej czasu potrzeba na jego złamanie. Warto również wspomnieć, że sama długość hasła nie jest wystarczającym zabezpieczeniem, jeśli użyty wyraz ma jakieś znaczenie. Przykładowo, wyraz Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz jest nazwą niemieckiej ustawy dotyczącej podziału kompetencji w procesie nadzoru nad etykietowaniem wołowiny (serio :)). Wystarczy, że atakujący będzie znał nasze upodobania do stosowania długich i egzotycznych wyrazów posiadających jakieś znaczenie oraz stworzy z nich odpowiedni słownik. Może się wtedy okazać, że nasze hasło zostanie odgadnięte w przeciągu kilkunastu minut.
  • Jakość oraz szybkość bezpiecznej funkcji haszującej – funkcje skrótu omówiliśmy już w poprzedniej części artykułu, więc teraz sobie jedynie krótko przypomnimy, że nawet jeśli funkcja generuje unikatowe i niemożliwe do odwrócenia skróty, to nie powinna działać zbyt szybko. Dłuższy czas liczenia skrótu to dłuższy czas potrzebny na złamanie hasła.
  • Moc obliczeniowa użyta do ataku – jest jej coraz więcej i staje się coraz tańsza, dlatego tak ważne jest podążanie za najnowszymi standardami bezpieczeństwa. Oprócz obliczeń przeprowadzanych przez coraz szybsze procesory (CPU = Central Processing Unit), mamy również możliwość wykorzystania jeszcze szybszych procesorów graficznych (GPU = Graphics Processing Unit). Dodatkowo, dużą część obliczeń można zrównoleglić (tj. przetestować większą liczbę haseł w tym samym czasie). Jakby tego było mało, wprowadzenie komputerów kwantowych (które wprawdzie są jeszcze w powijakach) do powszechnego użytku może spowodować, że wszystkie dzisiejsze techniki kryptograficzne staną się bezużyteczne i będą potrzebne nowe metody wykorzystujące obliczenia kwantowe.

Brute force

Atak siłowy (ang. brute force) jest najprostszą, ale też najmniej skuteczną techniką, która polega na sprawdzeniu każdej możliwej kombinacji znaków takich jak litery (duże i małe), cyfry oraz znaki specjalne (np. # zwany płotkiem lub krzyżykiem). Skuteczność tej metody zależy przede wszystkim od poziomu złożoności hasła (długość i zakres użytych znaków) oraz od mocy obliczeniowej, którą dysponuje napastnik. Liczba kombinacji, które musi sprawdzić atakujący chcący odgadnąć hasło to LM, gdzie L jest liczbą wszystkich znaków w danym alfabecie, a M oznacza długość hasła. Przykładowo, jeśli do zbudowania hasła o długości 10 znaków użyjemy jedynie liter z alfabetu łacińskiego, składającego się obecnie z 26 znaków, to liczba kombinacji do sprawdzenia wynosi 2610 = 141 167 095 653 376. Kiedy postanowimy użyć małych i dużych liter to liczba dostępnych znaków w alfabecie nam się podwoi (26 liter małych i 26 liter dużych): 5210 = 144 555 105 949 057 024. Możemy więc wywnioskować, że im hasło jest krótsze i mniej skomplikowane, tym większa jest szansa na powodzenie ataku. Biorąc pod uwagę fakt, że dzisiaj większość haseł jest przechowywana w postaci omówionych wcześniej skrótów, czas wymagany do sprawdzenie każdej kombinacji jest zależny od czasu potrzebnego na wyliczenie pojedynczego skrótu przez użytą funkcję.

W przypadku ataku brute force, zarówno w wersji zdalnej jak i lokalnej, wszystko sprowadza się do sprawdzenia każdej możliwej kombinacji znaków, aż do momentu odgadnięcia właściwego hasła lub znalezienia kolizji. Pamiętamy, że kolizja pojawia się wtedy gdy 2 różne hasła generują ten sam skrót, a w przypadku siłowego ataku online wszystkie możliwe hasła, które wpiszemy, są pod spodem przeliczane przez system na odpowiedni skrót.

Dictionary

Wiemy już, że pospolity atak siłowy może zająć bardzo wiele czasu, nawet jeśli przeprowadzamy go lokalnie (ang. offline) posiadając dostęp do dużej mocy obliczeniowej. A co jeśli nie musielibyśmy sprawdzać każdej możliwej kombinacji? Okazuje się, że ludzki umysł jest raczej wygodny, dlatego ludzie bardzo często wymyślają hasła, które mają dla nich jakieś znaczenie i które łatwo zapamiętać. Dlaczego by tego nie wykorzystać?

Atak słownikowy (ang. dictionary attack) polega na wykorzystaniu zbioru określonych słów (słownika), które mogą okazać się prawidłowym hasłem z pewną dozą prawdopodobieństwa. Słownik może zawierać listę najpopularniejszych haseł stworzoną w oparciu o dostępne w Internecie wycieki bądź składać się z listy słów, których mogła użyć potencjalna ofiara, opracowaną na podstawie skrupulatnie zebranego wywiadu (niezwykle pomocne okazują się tutaj portale społecznościowe). Jeśli wiemy, że nasza ofiara ma związek z branżą medyczną to jest spore prawdopodobieństwo, że użyje w swoim haśle wyrażeń z żargonu medycznego, szczególnie pracując na komputerze służbowym. Dodatkowo istnieje możliwość wyciągnięcia z ofiary bardzo osobistych informacji, takich jak imiona dzieci/małżonka, daty urodzin czy imiona zwierząt domowych. To również są cenne dane, które mogą się przydać do budowy skutecznego słownika.

Oczywiście sam zbiór samodzielnych wyrazów o określonym znaczeniu nie będzie wystarczający. Dzisiaj bardzo wiele systemów wymusza na swoich użytkownikach zwiększony poziom złożoności hasła. Przykładem niech będzie wymaganie, że hasło powinno zawierać przynajmniej jedną dużą literę; jedną cyfrę i jeden znak specjalny. W związku z tym dobre słowniki powinny zawierać też różne kombinacje (np. imiona dzieci z datami urodzin czy też litery podmienione na cyfry: a->4; e->3; o->0). Atakujący nie musi robić tego samodzielnie, ponieważ są programy, które automatycznie generują różne wariacje z podanego wyrazu. Kiedy bierzemy hasła dostępne w słownikach i dynamicznie je modyfikujemy, tworząc wiele dodatkowych kombinacji (np. dodając cyfry z określonego zakresu na koniec wyrazu; zamieniając określone litery na cyfry lub zamieniając małe i duże litery) mówimy, że mamy do czynienia z atakiem hybrydowym.

CUPP (Common User Password Profiler) jest narzędziem, które pomaga generować odpowiednio dopasowane słowniki. Po uruchomieniu program zada kilka pytań dotyczących ofiary, a następnie za pomocą zaimplementowanych algorytmów odpowiednio wymiesza dane i przygotuje listę potencjalnych haseł.

Spraying

W aplikacji, systemie czy też bazie danych, hasła są przypisane do nazw użytkowników, które z kolei mogą być przechowywane w postaci jawnej (w przeciwieństwie do haseł). Niby sama nazwa użytkownika nie wystarczy, żeby uzyskać dostęp do chronionych zasobów, ale znając te dane, atakujący zamiast ataków siłowych na pojedynczego użytkownika, może wykorzystać tzw. spraying.

Jeśli korzystamy z bankowości elektronicznej to być może zdarzyło nam się zablokować dostęp do naszego konta internetowego przez kilkukrotne podanie błędnych danych logowania. Później musieliśmy kontaktować się z bankiem celem odblokowania naszego konta i/lub przejść przez dodatkowe procedury weryfikujące naszą tożsamość. O ile banki z wiadomych względów mają dosyć restrykcyjne metody ochrony przed nieuprawnionym dostępem (np. maksymalnie 3 próby logowania zanim konto zostanie zablokowane lub dopuszczona jedynie określona liczba pomyłek, nawet jeśli nie wystąpiły one pod rząd), to wiele aplikacji również zabezpiecza się przed wielokrotnymi próbami odgadnięcia hasła. Przede wszystkim praktycznie uniemożliwia to odgadnięcie hasła atakowanego użytkownika, ale też chroni serwer aplikacji przed zmasowanymi próbami odgadnięcia hasła przez boty, co może prowadzić do problemów z wydajnością, a nawet do całkowitej niedostępności usługi (DoS = Denial of Service).

Spraying jest techniką ataku, która polega na przeprowadzeniu jedynie kilku prób zalogowania się na pojedyncze konto użytkownika, z użyciem krótkiej listy najczęściej używanych haseł. Kiedy próby zalogowania się nie powiodą, atakujący przechodzi do następnego konta, żeby nie doprowadzić do zablokowania żadnego z kont i nie zwrócić na siebie uwagi. Przykładowo, jeśli atakujący wie, że w danym serwisie po 5 nieudanych próbach konto użytkownika zostanie na jakiś czas zablokowane, to będzie on próbował użyć 4 haseł ze swojej listy (np. pierwsze cztery z listy najpopularniejszych haseł) na pojedynczym koncie, zanim przejdzie do kolejnego użytkownika. Jeśli uda mu się trafić prawidłowe hasło, będzie próbował dalszych ataków z użyciem skompromitowanego konta. Jeśli nie trafi, przejdzie do następnego konta – i tak aż do wyczerpania listy użytkowników. Poprzez próbowanie bardzo ograniczonej liczby haseł dla pojedynczego konta, atakujący jest w stanie uniknąć wykrycia i ewentualnej blokady konta.

Najskuteczniejszą obroną w tym przypadku jest stosowanie mechanizmów blokujących automatyczne próby logowania (CAPTCHA) oraz upewnienie się, że nazw użytkowników (loginów) danego systemu nie da się w łatwy sposób przewidzieć i enumerować.

Przykład pytania z książki CompTIA Security+ Practice Tests:

Angela reviews the authentication logs for her website and sees attempts from many different accounts using the same set of passwords. What is this attack technique called?

A. Brute forcing.
[X] B. Password spraying.
C. Limited login attacks.
D. Account spinning.

O ile można uznać spraying za odmianę metody siłowej to pamiętajmy, żeby na egzaminie wybierać taką odpowiedź, która najlepiej pasuje do opisanej sytuacji, a nie tylko technicznie poprawną.

Rainbow table

Zanim przejdziemy do sedna, omówmy sobie czym jest tzw. atak pamięciowy. Kiedy wykonujemy ataki wyczerpujące opisane powyżej (siłowe, słownikowe czy hybrydowe) przeciwko różnym użytkownikom, to przy każdej iteracji musimy na nowo obliczać skróty dla tych samych haseł, przez co tracimy cenny czas. Dlatego chcąc usprawnić proces możemy zapisywać już obliczone hashe na przyszłość (zakładając oczywiście, że później atakujemy skróty wygenerowane przez tę samą funkcję).

Pamięciowy wariant ataku wyczerpującego polega na przechowywaniu listy już wyliczonych skrótów w pamięci celem szybszego przeszukiwania bazy i tym samym poprawienia skuteczności ataku. Jednak ze względu na bardzo dużą złożoność pamięciową (musielibyśmy przechowywać miliony par hasło-skrót w pamięci) nie zawsze jesteśmy w stanie użyć tego wariantu dla wszystkich kombinacji, które chcemy przetestować. W związku z tym, już jakiś czas temu, pan Philippe Oechslin zaproponował kompromis czasowo-pamięciowy i tak narodziła się koncepcja tęczowych tablic.

Tęczowa tablica (ang. rainbow table) jest specjalnie przygotowaną bazą haseł i ich wstępnie obliczonych skrótów, która może znacząco przyspieszyć atak wyczerpujący. Taka baza przypomina trochę skompresowaną strukturę lookup table, gdzie możemy szybko odnaleźć interesującą nas parę hash-hasło (zajmuje mniej miejsca niż klasyczna lookup table z powodu wspomnianej kompresji). Rozmiar tęczowej tablicy jest parametryzowany i może wynosić od kilku gigabajtów do nawet kilkuset gigabajtów, w zależności od zasobów jakimi dysponuje atakujący. Gigantyczna tablica znacząco skraca czas obliczeń, ale kosztem ogromnego zapotrzebowania na pamięć. Z drugiej strony mniejsza tablica wymaga mniej pamięci, ale wtedy wydłuża się czas obliczeń (dlatego znalezienie złotego środka nosi nazwę kompromisu czasowo-pamięciowego). Podczas przeprowadzania ataku tablica może być przechowywana w pliku lub w pamięci operacyjnej. Oczywiście czas dostępu do danych w pamięci RAM jest krótszy niż w przypadku operacji plikowych, ale pamiętajmy, że rozmiar tablicy może przekroczyć ilość dostępnej pamięci operacyjnej.

Tyle w zasadzie wystarczy, żeby poradzić sobie z tym zagadnieniem na egzaminie. Poniżej znajduje się przykładowe pytanie egzaminacyjne, ale jeśli ktoś jest zainteresowany głębszym zrozumieniem jak działają tęczowe tablice, zachęcam do przeczytania dalszej części tego rozdziału.

What tool/technique of password cracking consists of searching tables of precomputed hashes in order to speed up a brute-force attack?

A. Dictionary attack.
B. Spraying.
[X] C. Rainbow tables.
D. Whaling
.

Utworzenie tęczowej tablicy wymaga czasochłonnych obliczeń na początku, ale kolejne wyszukiwania skrótu za jej pomocą odbywają się już dużo szybciej. Jeśli nie chcemy budować jej samodzielnie możemy użyć jednej z gotowych tablic dostępnych w Internecie (np. w serwisie Free Rainbow Tables). Pamiętajmy też, że nie ma jednej uniwersalnej tablicy do wszystkich rodzajów skrótów, dlatego potrzebujemy różnych tablic w zależności od użytej funkcji haszującej.

Przyjrzyjmy się teraz zasadom działania tęczowych tablic od strony technicznej.
Składa się ona z m łańcuchów (ang. chains) o długości n, gdzie wartości m oraz n są parametryzowane. Żeby zrozumieć strukturę pojedynczego łańcucha, przeanalizujmy sobie najpierw algorytm budowania tęczowej tablicy:

  1. Budowę pierwszego łańcucha zaczynamy od wybrania hasła początkowego w dowolny sposób – może to być jakikolwiek wpis ze słownika lub ciąg znaków wygenerowany losowo. Jest to nasz element początkowy pierwszego łańcucha, więc oznaczmy go sobie jako p11, gdzie pierwsza jedynka to numer łańcucha, a druga to indeks jego ogniwa (indeksujemy od 1).
  2. Obliczamy skrót dla elementu początkowego łańcucha za pomocą odpowiedniej funkcji haszującej H. Tak wyliczony skrót możemy oznaczyć jako h11 = H(p11).
  3. Następnie wynik (skrót) przekazujemy to tzw. funkcji redukującej R. Jest to funkcja, która w jakiś ustalony sposób przekształca hash otrzymany na wejściu na kolejne hipotetyczne hasło (w postaci jawnej), które jest zgodne z założeniami tablicy (najprostszym przykładem jest konwersja heksadecymalnego skrótu na alfanumeryczne znaki ASCII). Wynik funkcji redukującej jest potencjalnym kandydatem na kolejne hasło w danym łańcuchu, więc oznaczamy go jako p12 = R(h11) i dla niego również obliczamy skrót h12 = H(p12).
    1. Cały cykl powtarzamy n razy (n jest długością pojedynczego łańcucha). I teraz najlepsze: w tablicy zapisujemy jedynie ostatni skrót h1n pomijając te wszystkie pary pośrednie. Oznacza to, że pojedynczy łańcuch składa się tak naprawdę z 3 elementów: hasła początkowego p11; skrótu końcowego h1n oraz z informacji o długości łańcucha n (możemy się jeszcze spotkać z tablicami gdzie na końcu zachowany jest wynik ostatniej redukcji zamiast hasha, ale wtedy podczas przeszukiwania tablicy musielibyśmy najpierw zredukować szukany skrót).
    2. Kiedy bieżący łańcuch jest już gotowy, przechodzimy do budowy kolejnego (krok 4.).
  4. Znowu musimy wybrać wartość początkową dla łańcucha i możemy tak jak w punkcie 1. wybrać kolejne hasło ze słownika lub ostatni element poprzedniego łańcucha. Teraz powtarzamy czynności z punktu 3. m razy, gdzie m oznacza liczbę łańcuchów w tablicy.

Gotowe, mamy zbudowaną tęczową tablicę. Teraz weźmy na warsztat proces przeszukiwania:

  1. Bierzemy hash, który chcemy złamać (oznaczmy go jako h1) i szukamy go w utworzonej tablicy, próbując dopasować do zapisanych na końcu skrótów.
    1. PUDŁO: Nie udało nam się znaleźć skrótu h1 w tablicy, więc go redukujemy za pomocą odpowiedniej funkcji r1 = R(h1) i dla tej wartości obliczamy hash h2 = H(r1). Teraz próbujemy znaleźć w tablicy skrót h2 i jeśli go nie znajdziemy to powtarzamy ten krok aż do momentu znalezienia dopasowania lub przez określoną liczbę iteracji n (liczba par w łańcuchu, czyli jego długość). Jeśli po n iteracjach nie znajdziemy dopasowania to uznajemy, że danego hasha h1 nie ma w tablicy i go nie złamiemy – możemy więc próbować z kolejnymi skrótami na naszej liście.
    2. TRAFIENIE: Bingo! Trafiliśmy na hash hx w jednym z łańcuchów, więc teraz wykorzystamy go do znalezienia przeciwobrazu dla pierwotnej wartości h1. Skoro wiemy, że skrót na pewno znajduje się w danym łańcuchu (oznaczmy go px), musimy jedynie go odtworzyć , więc zaczynając od pierwszego elementu px1 powtarzamy operację haszowania i redukcji, aż do momentu otrzymania oryginalnego skrótu h1 oraz powiązanego z nim hasła w postaci jawnej.

Myślę, że widzicie już na czym polega wspomniany wcześniej kompromis. Z jednej strony mamy zapisane jedynie początkowe wartości oraz ostatnie skróty łańcucha, więc nie przechowujemy wszystkich obliczonych par hasło-skrót, znacząco ograniczając rozmiar tablicy. Z drugiej strony, kiedy znajdziemy już odpowiedni łańcuch musimy wykonać obliczenia, żeby go odtworzyć i trafić na szukaną wartość, ale ilość tych obliczeń będzie znacznie mniejsza niż przy zwykłym ataku siłowym.

Na zakończenie możemy się zastanowić, skąd w ogóle wziął się przymiotnik tęczowa. Otóż okazuje się, że niedoskonałość funkcji skrótu polegająca na tym, że mogą one sporadycznie wygenerować kolizje, może poważnie zakłócić proces przeszukiwania tęczowej tablicy. Aby temu zapobiec stosuje się różne funkcje redukujące R na długości pojedynczego łańcucha (każdy łańcuch ma identyczny zestaw funkcji redukujących). Gdyby każdą funkcję R oznaczyć innym kolorem, to przy wizualizacji pełnych łańcuchów otrzymalibyśmy wielobarwną tęczę. Więcej na temat funkcji redukujących można przeczytać w artykule: Tęczowe tablice – przyśpiesz atak brute-force.

Pomimo tego, że na pierwszy rzut oka wykorzystanie tęczowych tablicy wydaje się być atakiem ostatecznym to istnieje bardzo prosty sposób, żeby mu zapobiec. Najskuteczniejszą metodą obrony przed tęczowymi tablicami jest stosowanie soli w hasłach. Wtedy nawet jak znajdziemy posolony skrót w tablicy to powiązane hasło nie będzie tym właściwym.

Kiedy całą noc generowałeś tęczowe tablice, ale okazało się, że hasła zostały posolone ;). Fragment filmu Kiler-ów 2-óch (1999).

Online

Jak zostało już wcześniej wspomniane, ataki zdalne (ang. online) są przeprowadzane za pośrednictwem sieci, a ich celem jest mechanizm uwierzytelniający zdalnego systemu bądź aplikacji. Skuteczność takiego ataku zależy w dużej mierze od dostępnej przepustowości sieci oraz od zastosowanych technik obrony, takich jak mechanizm CAPTCHA blokujący automatyczne zapytania bądź blokada konta użytkownika po kilku nieudanych próbach logowania.

Przedstawione rozwiązania obronne nie tylko chronią użytkowników przed przejęciem konta, ale również zabezpieczają przed potencjalnymi atakami typu Denial of Service (DoS/DDoS) na cały system. Atakujący, który chce złamać hasło będzie próbował wysłać mnóstwo żądań w bardzo krótkim czasie co może przecież zakłócić stabilność działania systemu. Pamiętajmy jednak, żeby stosując blokadę na atakowane konto nie doprowadzić do sytuacji, w której prawdziwy użytkownik w ogóle nie będzie mógł się zalogować. Atakujący może wykorzystać nieprzemyślaną technikę obrony i specjalnie blokować konta rzeczywistym użytkownikom.

Ze względu na opóźnienia sieciowe (ang. latency) oraz dodatkowe zabezpieczenia wspomniane w poprzednich akapitach, zwyczajne ataki siłowe nie mają wiele sensu. Lepiej sprawdzają się w tym przypadku dopasowane ataki słownikowe lub spraying, dlatego atakujący musi się do nich dobrze przygotować. Przykładowy scenariusz może wyglądać następująco:

  1. Zebranie informacji – dobrze zebrany wywiad na temat ofiary umożliwia zbudowanie skutecznych słowników. Na tym etapie należy jeszcze sprawdzić czy da się łatwo przewidzieć nazwy kont użytkowników (np. często firmy stosują politykę przydzielana nazw użytkowników typu imię.nazwisko).
  2. Poznanie technologii – sprawdzenie w jaki sposób przebiega proces uwierzytelnienia w atakowanym systemie lub usłudze, czyli: jakie warunki musi spełniać nazwa użytkownika i hasło; liczba dozwolonych prób zanim konto zostanie zablokowane; w jaki sposób przesyłane są dane uwierzytelniające (czy aby nie jawnym tekstem :)); jak wygląda odpowiedź z serwera w przypadku danych prawidłowych i nieprawidłowych itd. W zebraniu potrzebnych informacji może przydać się narzędzie WhatWeb, które pozwala zidentyfikować technologię użytą do zbudowania testowanej usługi webowej.
  3. Przygotowanie dobrych słowników na podstawie zebranych informacji.
  4. Przygotowanie odpowiednich narzędzi – oprócz słowników przydają się wszelkiego rodzaju skrypty i programy, które są w stanie wiele rzeczy zautomatyzować. Przykładem jest potężne narzędzie THC-Hydra, które zostało zaprojektowane z myślą o przeprowadzaniu ataków online. Jego atutem jest możliwość testowania wielu usług sieciowych, takich jak SSH, FTP, HTTP i inne.
  5. Przemyślane testowanie – należy stosować tylko te techniki, które mają jakąkolwiek szanse powodzenia unikając jednocześnie wykrycia. Poza tym, należy się zastanowić czy nie lepiej jest przeprowadzić atak na kilka usług sieciowych danego systemu równolegle.

OFFLINE

Zwyczajny atak siłowy przeprowadzany zdalnie jest mocno utrudniony przez dodatkowy narzut czasowy związany z przesyłaniem danych przez sieć oraz przez zabezpieczenia, które są w stanie wykryć taki atak i go skutecznie zablokować. Z tego względu, metody siłowe lepiej sprawdzają się w środowisku lokalnym, kiedy jesteśmy w posiadaniu skrótów haseł, które chcemy złamać. Podsumowując, ataki wyczerpujące lokalne (ang. offline) to ataki siłowe bądź kryptograficzne, których celem jest odzyskanie oryginalnego hasła na podstawie wykradzionego skrótu (ang. hash). Są one dużo szybsze niż ataki zdalne, dlatego o prawdziwej sile systemu kryptograficznego przekonujemy się dopiero wtedy gdy napastnik ma dostęp do bazy danych ze wszystkimi skrótami haseł.

Większość ataków wyczerpujących opisanych powyżej można spotkać zarówno w wersji zdalnej jaki lokalnej, choć zdecydowanie lepiej sprawdzają się kiedy nie mamy dodatkowego narzutu związanego z komunikacją. Oprócz nich do ataków offline możemy zaliczyć również:

  • Ataki kryptograficzne, które mają więcej sensu w wersji lokalnej i skupiają się głównie na znalezieniu oraz wykorzystaniu niedoskonałości funkcji skrótu. Na przykład poprzez wyszukiwanie kolizji.
  • Atak z fizycznym dostępem odbiega delikatnie od omawianych tutaj taktyk łamania haseł, ale również może stanowić poważne zagrożenie. Wyobraźmy sobie sytuację, w której zapomnieliśmy zablokować nasz system po odejściu od komputera na dłuższą chwilę. Ktoś nam nieprzyjazny może to wykorzystać – ma przecież dostęp do systemu z pełnymi uprawnieniami zalogowanego użytkownika (konkretnie naszymi). Możliwości jest wiele: może zrobić zrzut pamięci (ang. memory dump) i później wyciągnąć z niego wrażliwe dane (pamiętajmy, że hasła załadowane do pamięci są przeważnie w formie jawnej); może zainstalować złośliwe oprogramowanie, a nawet uzyskać dostęp do naszego menadżera haseł (np. plik KeePass z hasłami jeśli był akurat otwarty).

Do popularnych narzędzi wspomagających przeprowadzanie ataków lokalnych możemy zaliczyć Hashcat oraz John The Ripper.

Obrona i profilaktyka

W poniższym rozdziale znajduje się lista wskazówek, do których należy się zastosować, jeśli chcemy aby nasze hasła i konta były bezpieczne. W tym konkretnym przypadku bezpieczeństwo zależy nie tylko od samych użytkowników haseł, ale też od systemów czy aplikacji, z których korzystają. Z tego względu część poniższych porad została zaczerpnięta z dokumentu OWASP Application Security Verification Standard 4.0 i jest skierowana do twórców/administratorów systemów i aplikacji oraz do audytorów bezpieczeństwa.

Jeśli więc jesteś odpowiedzialna/odpowiedzialny za stworzenie oraz utrzymanie systemu bądź aplikacji:

  • Nie implementuj systemu kryptograficznego na własną rękę, tylko korzystaj z gotowych i sprawdzonych rozwiązań! Kryptologia jest skomplikowanym działem nauki i jak zdążyliśmy już zauważyć, nawet bardzo tęgie głowy, które poświęciły dużą część życia temu zagadnieniu, nie zawsze są w stanie ustrzec się od błędów. Nie próbujmy więc wynajdować koła od nowa, bo po pierwsze szkoda na to czasu, a po drugie prawdopodobnie nawet nie będziemy w stanie stworzyć rozwiązania, które jest rzeczywiście bezpieczne – nawet znane i popularne algorytmy okazywały się wadliwe dopiero po kilku latach intensywnych badań i testów.
  • Przechowuj hasła w postaci skrótów wygenerowanych przez bezpieczną funkcję haszującą. Dzisiaj najbardziej zalecane są funkcje z rodziny PBKDF, które oprócz generowania bezpiecznych skrótów domyślnie zapewniają dodatkową ochronę (solenie haseł i key stretching). Dopuszczalne jest również stosowanie funkcji z rodziny SHA-2 oraz SHA-3, ale generacje SHA-0 i SHA-1 powinniśmy już sobie odpuścić, podobnie jak MD5.
    • Stosowana sól powinna mieć długość przynajmniej 32-bitów i mieć inną wartość dla każdego hasła.
    • Jeśli stosujesz funkcję PBKDF2 to wybrana liczba iteracji (parametr wymagany przez tę funkcję) powinna być tak duża, jak tylko pozwala na to wydajność serwera – zazwyczaj jest to około 100 000.
    • W przypadku funkcji bcrypt, koszt obliczeń (ang. work factor) powinien być tak duży, na ile pozwala serwer – przeważnie jest to przynajmniej 13.
  • Pamiętaj, że baza danych to nie jedyne miejsce gdzie należy chronić hasła – musisz upewnić się, że nie ma wycieków w logach, dziennikach zdarzeń, komunikatach błędów czy zrzutach pamięci.
  • Przetestuj wydajność systemu kryptograficznego zanim zostanie wdrożony. Miejmy na uwadze fakt, że zbyt szybkie funkcje skrótu nie są pożądane ze względu na potencjalne ataki siłowe, ale jeśli nasz system będzie działał zbyt wolno to stanie się problematyczny dla naszych użytkowników.
  • Udostępnij możliwość korzystania z mechanizmów wieloskładnikowego uwierzytelnienia (MFA = Multi-Factor Authentication). Oprócz podania prawidłowego hasła, użytkownik powinien jeszcze dodatkowo potwierdzić swoją tożsamość, np. za pomocą jednorazowego kodu SMS wysłanego na numer telefonu. Dzięki temu, nawet jeśli hasło zostanie wykradzione, konto pozostanie bezpieczne.
  • Zadbaj o odpowiednią politykę stosowania haseł przez użytkowników:
    • Wymuszaj na swoich użytkownikach stosowanie silnych haseł. Na przykład za pomocą polityki, która nakazuje użycie przynajmniej jednej litery, cyfry i znaku specjalnego. Możesz dodatkowo wyświetlić wskaźnik określający siłę podanego hasła.
    • Nie blokuj możliwości wklejania haseł ze schowka lub za pomocą menadżera haseł. Jeśli użytkownik będzie zmuszony zawsze wpisywać hasło ręcznie, prawdopodobnie użyje takiego, które jest wygodne do wpisania, ale niekoniecznie bezpieczne.
    • Możesz zweryfikować czy hasło wybrane przez użytkownika nie znajduje się na liście popularnych haseł, które gdzieś kiedyś wyciekły. Można to zrobić lokalnie lub korzystając z odpowiedniego zewnętrznego API, ale w tym przypadku musielibyśmy zadbać również o bezpieczne przesyłanie wrażliwych danych. Ja osobiście nie jestem zwolennikiem wysyłania haseł do zewnętrznych serwisów, nawet jeśli są one uznawane za bezpieczne.
    • Pozornie kontrowersyjna porada z dokumentu OWASP ASVS 4 (2.1.10): nie wymuszaj na użytkownikach okresowej zmiany hasła i nie stosuj ograniczeń związanych z historią haseł. Na pierwszy rzut oka może to być furtka dla włamywacza, ale gdy naprawdę przyłożymy się do wdrożenia bezpiecznego i wygodnego systemu kryptograficznego to taka polityka okaże się zbędna. Poza tym firmy (w tym NIST i Microsoft) zauważyły, że okresowe zmiany hasła w dłuższej perspektywy prowadziły do obniżenia ich złożoności.
    • Upewnij się, że prawidłowo interpretujesz znaki specjalne czy inne znaki Unicode. W przypadku haseł nie powinniśmy kodować znaków typu < czy >, bo później może okazać się, że zamiast rzeczywiście podanego hasła: I <3 you, w swojej bazie przechowujemy hash hasła: I%20%3C3%20you. Wprawdzie, jeśli użytkownik zawsze będzie używał tego samego interfejsu do logowania to nie będzie to stanowiło dużego problemu, ale sytuacja może się zmienić gdy trzeba będzie się zalogować z pominięciem UI (np. poprzez API). Co do braku kodowania znaków, nie bójmy się w tym przypadku ataków XSS (Cross Site Scripting), ponieważ hasło użytkownika nigdy i nigdzie nie powinno być eksponowane.
    • Umożliwiaj zmianę hasła użytkownikom, ale tylko wtedy gdy ten potwierdzi swoją tożsamość. Na przykład wpisując swoje stare hasło lub, gdy go nie pamięta, potwierdzając linkiem/kodem wysłanym na jego adres email/numer telefonu.
  • Jeśli użytkownicy mogą się logować zdalnie (online) do Twojego systemu to:
    • Upewnij się, że dane uwierzytelniające są przesyłane bezpiecznym protokołem, który zapewnia szyfrowanie: HTTPS, FTPS, SSH itp. W przeciwnym razie wrażliwe dane mogą wyciec w przypadku przechwycenia ruchu sieciowego.
    • Stosuj mechanizmy CAPTCHA, który blokuje automatyczne żądania (ang. requests) wysyłane przez boty lub narzędzia skanujące. Oprócz ochrony kont użytkowników może to również zapobiec atakom DoS/DDoS (ang. Denial of Service/Distributed Denial of Service).
    • Wykrywaj podejrzane próby wielokrotnego logowania się na konta użytkowników (np. bardzo wiele prób w krótkim czasie) i zastosuj odpowiednią blokadę, w zależności od potrzeb (pamiętaj jednak, żeby taka blokada nie została wykorzystana przeciwko Twoim użytkownikom). Przykładem mogą być różne serwisy, które wykrywają próby połączenia się z usługą z innego urządzenia niż dotychczas – w takim przypadku nawet rzeczywisty użytkownik jest proszony o dodatkowe potwierdzenie swojej tożsamości poprzez MFA (Multi-Factor Authentication), na przykład wpisując jednorazowy kod otrzymany SMS-em.
    • Nie stosuj loginów, które można łatwo przewidzieć i enumerować. Dzięki temu ataki online na wiele kont (np. spraying) zostaną utrudnione.
    • Zadbaj o utwardzenie swoich usług sieciowych. Proces utwardzania (ang. hardening) to modyfikacja domyślnej konfiguracji, zarówno sprzętu jak i oprogramowania, celem zwiększenia ich bezpieczeństwa. Najbardziej podstawowym przykładem jest zmiana domyślnych haseł w urządzeniach (np. router Wi-Fi) lub oprogramowaniu, które zostały dopiero co zainstalowane – takie domyślne hasła są zazwyczaj ogólnodostępne.

Użytkownicy nie mają raczej wpływu na bezpieczeństwo przechowywanych haseł, ale mogą zadbać o to, żeby nawet w razie wycieku danych ich hasła dalej pozostały tajemnicą. Poniżej znajduje się garść porad jak tego dokonać:

  • Stosuj długie (minimum 12 znaków) i skomplikowane hasła z dużym współczynnikiem losowości. Często mamy tendencję do stosowania haseł, które łatwo zapamiętać i jest to naturalny odruch, ale niekoniecznie pożądany z punktu widzenia bezpieczeństwa. W związku z tym warto zastosować się do kolejnej wskazówki.
  • Używaj menadżera haseł. Są to aplikacje przechowujące Twoje hasła w postaci zaszyfrowanej, do których masz wygodny dostęp (musisz zapamiętać tylko jedno hasło do klucza). Dzięki temu nie musisz pamiętać wszystkich swoich haseł, co znacznie ułatwia stosowanie się do przedstawionych tutaj porad. Przykłady takich menadżerów: KeePass (działa w trybie offline), BitWarden (działa w chmurze). Oprócz tego polecam zapoznać się z dosyć obszernym opracowaniem tego tematu: Menadżer haseł – dlaczego warto go używać.
  • Nie stosuj identycznych haseł do różnych usług, w szczególności tak newralgicznych jak bankowość elektroniczna. W przypadku wycieku hasła z jednej usługi, Twoje pozostałe konta nie zostaną skompromitowane.
  • Włącz MFA (Multi-Factor Authentication) gdzie tylko się da. Jeśli ktoś uzyska hasło do Twojego konta to i tak będzie miał duży problem, żeby się zalogować, a Ty szybko dowiesz się, że hasło nie jest już bezpieczne i należy je jak najszybciej zmienić.
  • Zmień hasło w danym serwisie od razu jak tylko dowiesz się o ewentualnym wycieku. Jeśli do tej pory używałaś/używałeś tego samego hasła w innych serwisach to je również należy zmienić. Rzetelni usługodawcy nie wstydzą się przyznać do błędu i od razu informują swoich użytkowników o podobnych incydentach (pamiętajmy, że jest to aktualnie obowiązek wymuszony przez prawo), a następnie robią wszystko, żeby problem się nie powtórzył.
    • Pwned Passwords – ogólnodostępny serwis, w którym możemy sprawdzić czy nasze hasło przypadkiem już kiedyś nie wyciekło (i zostało złamane). Przyznam szczerze, że mam pewne obawy przed wpisywaniem haseł, z których aktualnie korzystam, do tego typu wyszukiwarek. W tym przypadku są one prawdopodobnie nieuzasadnione, ponieważ jest to dosyć popularny serwis, ale ja akurat jestem lekkim paranoikiem :).
  • Ostatnia, ale równie ważna porada: nie eksponuj swoich haseł! Tutaj chodzi nie tylko o nie zapisywanie swoich haseł w zwykłych plikach tekstowych czy na karteczkach przyklejonych do monitora, ale również o zachowanie czujności podczas pracy w miejscu publicznym.

O autorze

Łukasz Mieczkowski

Programista, który zainteresował się cyberbezpieczeństwem.

2 komentarze

Skomentuj Łukasz Mieczkowski Anuluj odpowiedź

Cyberbezpieczeństwo okiem programisty

Łukasz Mieczkowski

Programista, który zainteresował się cyberbezpieczeństwem.

Kontakt

Zapraszam do kontaktu za pośrednictwem mediów społecznościowych.