22:01
16/7/2010

Timing attacks (ataki czasowe) to ataki kryptograficzne, które mogą zostać wykorzystane do poznania haseł użytkowników portali internetowych korzystających z logowania po protokołach OpenID oraz OAuth. Tego typu logowanie wspiera m.in. Twitter, Digg czy Wikipedia.

Tick, Tack, Hack!

Luka umożliwiająca ataki czasowe znajduje się w kilkunastu open-source’owych bibliotekach, wykorzystywanych m.in. w projektach OpenID czy OAuth, umożliwiających “przyjemniejsze” zarządzanie tożsamością, a w szczególności rejestrację i logowanie do serwisów internetowych.

One Password To Rule Fail Them All!

Nate Lawson oraz Taylor Nelson przedstawią szczegóły ataku na zbliżającej się konferencji Black Hat.

Ataki czasowe (ang. Timing Attacks)

Ataki czasowe znane są od 15 lat, ale do tej pory uważano, że ich przeprowadzenie przez internet jest praktycznie niemożliwe z racji nieprzewidywalnych opóźnień, jakie dotykają pakiety IP przesyłane przez Internet — a ataki czasowe wymagają bardzo precyzyjnych pomiarów.

Tik, Tak, Hak?

Jak wygląda łamanie hasła poprzez atak czasowy? Atakujący loguje się do serwisu różnymi hasłami i mierzy czasy odpowiedzi serwera. Jeśli serwer weryfikuje hasło znak po znaku, to na podstawie różnić w czasach otrzymania komunikatu błędu “hasło nieprawidłowe” można wnioskować, czy któryś ze znaków w haśle był prawidłowy. Brzmi niewiarygodnie? A właśnie tak 3 lata temu złamano hasło do Xbox 360.

Zła chmura i zły python

Badacze wspominają, że łatwiej przeprowadza się ataki na webaplikacje, które zostały napisane w językach interpretowanych (np. Perl, Python), gdyż te “wolniej” przetwarzają dane niż języki kompilowane (np. C), a to zdecydowanie ułatwia przeprowadzenie ataku czasowego.

Podobnie łatwiej atakuje się webaplikacje hostowane w chmurze — bo atakujący może szybko (i tanio) wykupić swój serwer “w okolicy”, co znacznie ogranicza zakłócenia związane z przesyłaniem pakietów przez Internet.

Jak się chronić przed atakami czasowymi?

Bardzo prosto — zaprogramować webaplikacje tak, żeby weryfikacja hasła zawsze (albo nigdy) zajmowała tyle samo czasu. Może to wymagać wprowadzenia sztucznego opóźnienia w przetwarzaniu kodu (sleep random() niekoniecznie jest dobrym pomysłem), co przy okazji przyczyni się do spowolnienia ewentualnych ataków brute-force (pomagając inwoluntarnie podnieść bezpieczeństwo webaplikacji, które jeszcze nie korzystają np. CAPTCHY lub innych ograniczeń nieustannych prób logowania)

P.S. Ataki na formatki logowania i metody ochrony przed tego typu atakami poruszamy na naszych szkoleniach z bezpieczeństwa webaplikacji. Jest jeszcze 1 wolne miejsce — kto pierwszy ten lepszy! ;-)

Przeczytaj także:



28 komentarzy

Dodaj komentarz
  1. Takim sposobem właśnie, mierząc czas odpowiedzi, parę lat temu odzyskałem CD-key do Windows 2000 po zatarciu znaków na obudowie.

  2. “inwoluntarnie”

  3. @Tomasz: ile czasu ci to zajęło?

    Niesamowita historia. Ale: z tego co wiem(ekspertem broń Boże nie jestem) to nie sprawdza się hasła, tylko jego zakodowaną wersję: patrz: password_type. W takim przypadku czas potrzebny na sprawdzenie błędnego, częściowo poprawnego i poprawnego hasła powinien być identyczny. Nie wydaje mi się, żeby openid trzymało hasła w plaintexcie.

  4. A to hasła w bazie nie przechowuje sie jako hashe tylko porównuje sie plain text?
    ..wtedy można mierzyć czas.. hashowania ;-)

  5. Hmm… O ile zgadzam się z pomysłem, żeby weryfikacja zajmowała zawsze tyle samo czasu, to nie mogę się zgodzić z pomysłem dodania randoma ;)
    Wtedy dostajemy coś takiego:
    Total_Time = Check_Time + Random + Salt
    (Salt to stały odczekiwany fragment czasu, dodałem żeby od razu się i z tym rozprawić)
    I teraz tak… robimy N testów dla tego samego hasła, gdzie N to np. 1000 (do zrobienia).
    Jeśli Random będzie miał niewielki zasięg (a raczej będzie miał niewielki zasięg, w końcu nikt nie chce czekać 123 sekund, więc to pewnie będzie od 0 do 2 sec; rozdzielczości za dużej też bym się nie spodziewał, jako że sleep też ma pewną rozdzielczość której musi się trzymać), to po 1000 próbach możemy wziąć min(Random), i dostajemy:
    Total_Time = Check_Time + min(Random) + Salt
    Jeśli jest to Random od 0 do 2 sec np, to min(Random) z dużym prawdopodobieństwem będzie 0. Jeśli jest to random od N do 2, to redefiniujemy Salt jako Salt+N po prostu, co sprowadza nas do pierwszego przypadku. Więc dostajemy:
    Total_Time = Check_Time + Salt
    Ponieważ w tego typu atakach istotna jest różnica między czasem (to w zasadzie identyczna sprawa jak w przypadku blind sqli z wykorzystaniem sleep/delay), czyli pewna Delta,
    Delta = Total_Time2 – Total_Time1
    to Salt przestaje być istotne, bo jest stałe w obu przypadkach, czyli dostajemy
    Delta = Check_Time2 + Salt – Check_Time1 – Salt
    czyli
    Delta = Check_Time2 – Check_Time1
    A to sprowadza nas do problemu opisywanego w artykule ;p

    Podsumowując – “random” daje tyle, że zamiast 10 prób trzeba zrobić 1000. Ale ani 10, ani 1000 to nie jest dużo (ot takie 1mb pakietów).

    Więc jedynym słusznym imo pomysłem jest stały czas przetwarzania.

    • @Gynvael: good point, podlinkowałem Twój komentarz w tekście :)

  6. Fajne, można porównać do podsłuchiwania stetoskopem zamków w mechanicznym sejfie :).

  7. @anonim: W przypadku CD-keya chodziło o kilka znaków z dwóch ostatnich grup. Nie trwało to długo. Ok. 2 godzin. Ciekawe jest to, że sprawę przyśpieszyło…. spowolnienie pracy procesora. Dzięki temu różnice w czasie odpowiedzi były łatwiejsze do wychwycenia.

  8. Nie polecam dodawania jakichkolwiek sleepów w aplikacji webowej z tego względu, że jest to tykająca bomba w systemie.
    Przykładowo jeżeli każdy request generuje w systemie wiszący (uśpiony) wątek interpretera np. PHP to wyczerpanie limitu procesów jest b. łatwe.

    Przykładowy scenariusz:
    Klasa ACL odpowiedzialna za autoryzację użytkownika w przypadku niepoprawnych danych generuje sleepa na czas 2s. Wywołujemy X requestów do WWW tak aby wyczerpać limity w systemie dla ilości wątków apache/php. Scenariusz zależy oczywiście od konfiguracji i od tego z jakim modułem pracuje Apache czy jest to MPM Worker, Prefork itd.

    Potencjalny atak jest uzależniony od konfiguracji serwera ale 70% tak wadliwie napisanych skryptów jest w stanie skutecznie odciąć dostęp do swoich własnych zasobów.

    Sleepy w systemach webowych to nieporozumienie i jedna z wielu tych rzeczy, które powinno się sprawdzać przy audycie aplikacji.

  9. “do tej pory uważano, że ich przeprowadzenie przez internet jest praktycznie niemożliwe z racji nieprzewidywalnych opóźnień, jakie dotykają pakiety IP przesyłane przez Internet”

    Co się zmieniło?
    Chmury blisko zmniejszają lagi, ale jednak dalej pozostają one nieprzewidywalne(czy nie?)

  10. Ciekawa metoda, tylko gdy serwer znajduje się daleko, to opóźnienie (albo jego zmienność) może udaremnić ten atak. Chyba że jednocześnie śledzimy trasę pakietów i mierzymy czasy przeskoków między serwerami.

  11. “Lawson i Nelson opracowali jednak algorytmy, które pozwalają na odsianie sieciowego szumu i przeprowadzanie dokładnych pomiarów także w warunkach Internetu, intranetów i chmur obliczeniowych. Wyniki swoich prac przedstawią już wkrótce na konferencji Black Hat w Las Vegas – chcą by to było ostrzeżenie dla deweloperów, do tej pory przekonanych, że ataki czasowe nie grożą ich aplikacjom.” ciekawe czy opublikują ten algorytm

  12. @Radekk: Opisywany przez Ciebie przypadek można nieco uogólnić w tym sensie, że należy dążyć do tego, by czas wykonania skryptu był jak najkrótszy. Bo w sumie czy to 10 procesów wiszących na sleep, czy 10 procesów w pętli to nadal jest 10 procesów, w kontekście limitu (ilości procesów) różnicy nie ma. Dlatego też do DoS można wykorzystać choćby skrypty wyszukiwania, które zwracają za dużo rezultatów (kiedyś przy pomocy niepozornego or 1=1 — dostałem tak dużą ilość danych, że mi się przeglądarka i local proxy wysypało… Taki niespodziewany DoS na siebie samego :) ).

    Wracając do opóźnień w trakcie uwierzytelnienia – moim zdaniem trzeba popatrzeć na sprawę całościowo. Drobne opóźnienia przy uwierzytelnieniu mogą znacznie obniżyć skuteczność ataków typu brute-force, z drugiej strony co prawda podnieść ryzyko DoS, ale jeszcze utrzymać je w akceptowalnych granicach. Bo jeśli samo uwierzytelnienie trwa 0.04 sekundy, a generowanie dowolnej strony wewnątrz to już około 1 sekundy, to “sztuczne” zwiększenie opóźnienia przy uwierzytelnieniu do okolic tej 1 sekundy wielkim problemem nie będzie (dla uproszczenia zakładam, że serwis jest otwarty i KAŻDY, w tym atakujący, może sobie w nim założyć konto i w celu DoS jakąś wybitnie długo ładującą się stronę uporczywie wywoływać).

  13. według mnie ta metoda jest i tak jest nie do zrealizowania w rzeczywistości..
    Szybciej brutem lub dobrym słownikiem hasło się odzyska niż tym typem ataku :p

  14. Czasy trzymania haseł plain-textem (powinny) już dawno minąć ( tak tak – wiem że nadal
    ma to miejsce). Załadamy więc, że hasełko zaraz po wpisaniu/wysłaniu postem
    staje się np: MD5(pass+salt). Następnie czy to w bazie (select * from user where hash = $H and … ) czy to znak po znaku – przeprowadzenie timin-attack wydaje mi się niemożliwe. Dlaczemu?
    Bo przy zmianie 1 bitu w haśle zmnienić może się cały hash.

    Jeżeli chodzi o dobry substytut dla [ sleep random() ] do odpowiedź wydaje się prosta:
    jeżeli MUSIMY poruwnywać plain-texty (znak po znaku) – to porównujmy ich hashe.
    Moja odpowiedź na a kolizje brzmi: $H = md5(str+salt1)+shat(str+salt2). Powodzenia życzę fanom zderzeń

  15. anonim 2010.07.16 23:49
    …(ekspertem broń Boże nie jestem)…
    Jak wiele to mówi o twoim otoczeniu

  16. @Kedii: a zastanawiałeś się, jak wygląda porównanie s1 == s2? W sensie kiedy zwracane jest true lub false? :)

    A tak przy okazji, w tym przypadku chyba nie do końca chodzi o porównywanie haseł, tylko o weryfikację podpisu (tak wynika z: http://thenextweb.com/socialmedia/2010/07/17/oauth-and-openid-authentication-vulnerable-to-timing-attack/).

  17. Właśnie, jak jest “klepnięte” porównanie “==” w PHP albo “=” w MySQL ? Po kolei każdy bit ? Bo coś kiedyś słyszałem o porównywaniu na zmiane jednego bitu z początku i jednego bitu z końca, aż do środka… Że to w przypadku niektórych danych (np. porównanie id z auto_incrementa) będzie szybsze – coś w tym jest…

  18. @porównanie stringów
    Na pewno nie jest to porównanie typu “każdy bit po kolei” :)

    Ponieważ PHP i (jak sądzę) MySQL zapamiętują długość stringu, to pierwszym krokiem będzie zawsze porównanie długości.

    Krok drugi to porównanie danych (poszczególnych liter/bajtów/znaków/whatever stringu).
    To sprowadza się do porównania zawartości na poziomie języka C/C++, gdzie można to zrobić na kilka sposobów:
    1. bajt po bajcie w pętli for – tak się zazwyczaj nie robi, bo jest to niepotrzebnie wolne
    2. jeśli zna się długość stringu (a w przypadku PHP czy MySQL długość stringu jest zapisywana w klasie od napisów), to prawdopodobnie będzie użyta funkcja memcmp ze standardowej biblioteki C.

    Jeśli chodzi o memcmp, to jest zazwyczaj dobrze zoptymalizowany, więc porównania następują maksymalnymi blokami które opłaca i da się porównywać – zazwyczaj (w zależności od implementacji na danym systemie operacyjnym) zaczyna się od porównywania na raz 4rech bajtów (np. jeśli string ma 14 bajtów, to blogi 0-3, 4-7 i 8-11 będą porównane “na raz”), a pozostałą część porównuje bajtami (tj bajty 12, 13 i 14 w tym przykładzie). Oczywiście nie ma przeszkód żeby porównywać na raz 16 bajtów (SSE), a niedługo może i 32 bajty (AVX).

    Anyway, taki atak czasowy jest trudno do zrobienia jeśli porównania są robione blokami po kilka bajtów na raz. Więc stawiałbym, że w opisywanym przypadku chodziło raczej o porównanie bajt po bajcie.

    Jeśli chodzi o MySQL, to stawiałbym (to są moje przypuszczenia, nie sprawdzałem tego, ale rozsądne jest imo żeby tak to było zrobione), że dodatkowo jest porównanie hashowe, tj oprócz długości i danych jest również np. crc32 (no ok, wiem że crc32 to checksum a nie hash, ale w tym wypadku chodzi o dowolny skrót napisu który można porównać; ilość kolizji jest nieistotna, byle by porównanie hashów było w miarę atomową instrukcją; a crc32 ma 4ry bajty, więc można to na raz porównać, a dodatkowo w nowych prockach jest instrukcja assemblera CRC32 która własnie ten checksum liczy) stringu i porównanie idzie tak:
    1. czy długości się zgadzają?
    2. czy hash się zgadza?
    3. czy dane się zgadzają?
    Pytanie czy w tym wypadku atak czasowy jest możliwy (crc32 jest problemem, ale hmm… pytanie tez czy w pewnym momencie nie byłby ułatwieniem; do przemyślenia ;>).

    Ad PHP:
    case IS_STRING:
    Z_LVAL_P(result) = ((Z_STRLEN_P(op1) == Z_STRLEN_P(op2))
    && (!memcmp(Z_STRVAL_P(op1), Z_STRVAL_P(op2), Z_STRLEN_P(op1))));

    Czyli długość + memcmp, tak jak mówiłem :)

    Ad pomysł z porównaniem bitu z końca i z początku, czy nawet bajtu z końca i początku.
    Pomysł jest OK dla szczególnych przypadków, ale…
    1.możemy ostro po łapach oberwać od cache’u procesora za skakanie między różnymi liniami cache’u (choć ok, teoretycznie są tylko dwie linie).
    2. porównanie od końca albo pierwszy i ostatni bajt najpierw są sensowne jeśli mamy pewność że większość porównań będzie zachodziło na stringach o identycznym prefixie.
    Szczerze, to nie sądzę by twórcy MySQL lub PHP mogli poczynić takie założenie :)

    Ehm, mam nadzieje że to odpowiedziało na część pytań dot jak działa == czy = :)

  19. @siostra: nie mam zielonego pojęcia o co ci chodzi.

  20. @Gynvael: Mój szybki test empiryczny pokazał, że przynajmniej w przypadku Pythona (2.5) jest różnica w czasie porównania dwóch stringów tej samej długości, jeśli stringi różnią się na początku, w środku czy na końcu. To by sugerowało, że porównanie zwraca False przy napotkaniu pierwszej “nierówności”, co jest dość oczywiste, bo dalsze porównanie nie jest celowe, nie zmieni rezultatu całości. Nie sprawdzałem tego dokładnie, nie ukrywam też, że w celu zobaczenia różnicy musiałem robić chyba 1000000 porównań (ale użyłem kiepskiej rozdzielczości timera).

    Co do praktycznej przydatności tego zachowania, to zapewne jak zawsze – zależy. Kilka powiązań między usługami działa w ten sposób, że usługa A wysyła do usługi B coś takiego:
    dane+hmac(dane,klucz), gdzie klucz jest ustalony między usługą A i B.

    Chcąc przekonać usługę B, że otrzymała dane od usługi A trzeba “sfałszować” rezultat funkcji hmac. Ponieważ hmac będzie w reprezentacji hex, to będzie 16 dopuszczalnych znaków na każdej pozycji. Jeśli porównywalny blok miałby 4 bajty długości, dalej to “tylko” 65536 możliwości. Jeśli hmac jest liczony z sha1 to ma 40 znaków, więc prób maksymalnie należałoby wykonać 655360 (bo takich bloków będzie 10) co w porównaniu z 16**40 (gdyby zgadywać cały hmac bez “wskazówek”) robi “małą” różnicę :)

  21. […] ciekawych komentarzy pojawiło się pod wpisem Ataki czasowe na OpenID i OAuth. Szczególną uwagę chciałbym zwrócić na kilka z […]

  22. “pomagając inwoluntarnie podnieść bezpieczeństwo webaplikacji, które jeszcze nie korzystają np. CAPTCHY”

    No, w wiki to akurat CAPTCHA już włączona jest od dłuższego czasu. Do 6-8 kont w czasie sesji, dokładnie nie pamiętam, a nabijać licznika nie zamierzam. Ale takie nabijanki było swego czasu widać. Kolejne konta w krótkim czasie bez żadnych edycji.

    http://pl.wikipedia.org/wiki/Specjalna:Rejestr/newusers

  23. No to znamy już mniej więcej sposób atakowania, pozostaje tylko tajemniczy algorytm “odsiewania” szumu i innych zakłóceń wpływających na czas odpowiedzi serwera. Mam nadzieję, że na wspomnianej konferencji BlackHat chłopaki opowiedzą co nie co i informacje trafią szybko do sieci :-) Oczywiście liczę, że niezawodny niebezpiecznik szybko podlinkuje info :P

  24. Heh, a WP.pl wlasnie otworzyla serwer openid.wp.pl wykorzystujacy te wadliwa technologie…

  25. […] raportowane jako podatność. Przy okazji warto odgrzebać temat timing attacks na OpenID i OAuth: Ataki czasowe na OpenID i OAuth (Twitter, Digg i Wikipedia podatne na atak), oraz komentarz […]

  26. […] jednak słusznie przypomina Paweł Goleń, powyższy skrypt może się na niewiele zdać. Z kolei Krzysiek Kotowicz w dyskusji z Pawłem wskazuje na ciekawe opracowanie dot. ataków […]

  27. […] 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ść […]

Twój komentarz

Zamieszczając komentarz akceptujesz regulamin dodawania komentarzy. Przez moderację nie przejdą: wycieczki osobiste, komentarze nie na temat, wulgaryzmy.

RSS dla komentarzy: