PINy, hasła – less is more

Będzie o PINach, a ogólniej o hasłach, bo PIN to specyficzny rodzaj hasła. Less is more to w tym przypadku mniej ograniczeń przekładających się na większe bezpieczeństwo. Zaczęło się od wpisu na Twitterze gdzie pokazano ograniczenia nakładane na PIN w pewnej aplikacji.

Zastanowiło mnie, czy da się policzyć o ile takie ograniczenia zmniejszą bezpieczeństwo, rozumiane jako przestrzeń możliwych poprawnych kombinacji PINu.

Na wstępie widać, że pierwsza cyfra PINu musi być inna niż 0 oraz 1. Już sam ten warunek zmniejsza przestrzeń poprawnych PINów o 20%. I to niezależnie od długości PINu.

Tu moja matematyka wysiada i sięgam po symulacje. Skrypt w Pythonie, który sprawdza wszystkie czterocyfrowe kombinacje i podaje, czy PIN jest poprawny (True) czy błędny (False) może wyglądać tak:

def is_valid(a, b, c, d):
    if a < 2:
        return False
    if ((a - b) == (b - c) or (b - c) == (c - d)):
        return False
    if (a == b) or (b == c) or (c == d):
        return False
    if ((b - a > 0) and (c - b > 0) and ((d - c > 0)) or (b - a < 0) and (c - b < 0) and (d - c < 0)):
        return False
    return True


for a1 in range(0, 10):
    for a2 in range(0, 10):
        for a3 in range(0, 10):
            for a4 in range(0, 10):
                result = is_valid(a1, a2, a3, a4)
                print(a1, a2, a3, a4, result)
python piny.py | grep -c True
5120

Jak widać, dla PINów o czterech cyfrach, wprowadzone ograniczenia zmniejszają przestrzeń dozwolonych PINów aż o niemal połowę.

Po co te ograniczenia i jak to zrobić poprawnie?

I teraz o motywacji. Jestem pewnien, że była ona szczytna i chodziło o podniesienie bezpieczeństwa. Jestem prawie pewien, że twórcy chodziło o wyeliminowanie prostych, schematycznych PINów. 0000, 1111, 1234 itp. Jak widać, w praktyce konsekwencje były dalej idące.

Najlepiej biorąc pod uwagę statystyki najczęściej występujących PINów i tworząc krótką listę PINów zabronionych. Wg statystyk PIN 1234 odpowiada za ponad 10% wystąpień wśród używanych PINów. 20 najczęściej używanych PINów jest używanych w 26% przypadków. Taka lista podniesie bezpieczeństwo, rozumiane jako „wymuszenie mało popularnego PINu” równie skutecznie, co proponowane ograniczenia. Zaś bezpieczeństwo rozumiane jako „ilość możliwych kombinacji” zostanie zmniejszone jedynie minimalnie.

Przypadki brzegowe

Jeśli przypuszczamy, że nasi klienci mają znacząco różne preferencje niż te z powyższych statystyk, robi się trudniej. W ogólności najlepiej walidować hasła na obecność zabronionych ciągów już w momencie ustalania hasła przez użytkownika. Zabronić można nazwy firmy, zasobu do którego jest chroniony dostęp, roku, czy popularnych ciągów znaków.

Jeśli jednak tego nie zrobiliśmy zawczasu, nadal nie wszystko stracone, choć robi się nieco ślisko. Nie znamy przecież PINów, co najwyżej mamy dostęp do hashy, więc ciężko będzie określić, których używają nasi klienci. Na ratunek przychodzi hashcat. Niezależnie od użytej funkcji hashującej, brute force krótkich PINów trwa moment. Jeśli korzystamy z bezpiecznych, wolno liczonych hashy, a użytkowników jest wielu, nie ma potrzeby robienia brute force wszystkich. Wystarczy analiza próbki statystycznej. Oczywiście taki audyt to operacja bardzo delikatna, więc nie zabieramy się za to bez stosownych umocowań i zachowania właściwej higieny.

UPDATE: Wersja uogólniona skryptu poniżej. Ilość cyfr w PIN regulowana ilością powtórzeń alfabetu w funkcji product. Dla dłuższych PINów warto skorzystać z pypy, które potrafi być szybsze.

import itertools
alphabet = list(range(0, 10))

def is_valid(i):
    if i[0] < 2:
        return False

    for n in range(0, len(i)-2):
        if i[n] - i[n+1] == i[n+1] - i[n+2]:
            return False

    for n in range(0, len(i)-1):
        if i[n] == i[n+1]:
            return False

    monotonic = True
    for n in range(0, len(i)-2):
        if (i[n] - i[n+1]) * (i[n+1] - i[n+2]) < 0:
            return True
    if monotonic:
        return False

    return True

for i in itertools.product(alphabet, alphabet, alphabet, alphabet):
    result = is_valid(i)
    print(i, result)

Phishing w Polsce

Phishing w Polsce ma się dobrze. Niedawno na z3s.pl pojawił się opis kolejnej kampanii phishingowej. Tak się złożyło, że równolegle widziałem opis jednej z ofiar. No i zbiegło się to w czasie z wpisem na nfsec.pl o tworzeniu RPZ dla unbound. Przypomniałem sobie o inicjatywie CERT Polska, która w założeniu może pomóc zmniejszyć liczbę ofiar tego typu ataków. Powróciło pytanie, które chodziło mi od początku po głowie, odkąd usłyszałem o projekcie hole.cert.pl[1]. Kto z tego będzie korzystał?

Phishing a hole.cert.pl

Pomysł jest prosty. Ludzie zgłaszają domeny phishingowe, CERT Polska je weryfikuje i udostępnia publicznie do wykorzystania wszystkim zainteresowanym. Zainteresowani, czyli polscy ISP, przekierowują żądania do domen wykorzystywanych przy phishingu na serwery ostrzegające użytkownika o zagrożeniu.

Pewne kontrowersje budzi analogia do rozwiązania blokującego dostęp do serwisów w ramach „ustawy antyhazardowej”. Zgadza się, w obu projektach wykorzystywany jest ten sam mechanizm. Ale są to projekty niezależne. Wszystko rozbija się tak naprawdę o „wsad”, czyli to, jakie domeny są blokowane. Jak pisałem niedawno, to samo narzędzie (tu: mechanizm) może być wykorzystane do różnych celów.

Więc tak, dla purystów wolnościowych jest to cenzura, zło i naruszenie wolności. Ale praktycznie rzecz biorąc, jest to jedyny sposób by skutecznie blokować phishingi. Szczególnie na urządzeniach mobilnych, gdzie często nie można łatwo sprawdzić domeny przed kliknięciem. Albo nie jest ona dobrze, w całości, widoczna. O ile sam raczej łatwo nie nabiorę się na phishing na desktopie, bo go zauważę[2], to w przypadku telefonu nie mam do siebie takiego zaufania. Nawet po zwróceniu uwagi i zachowaniu ostrożności.

Więc ostatecznie IMO bardzo dobra inicjatywa. Nie wymaga żadnych działań po stronie użytkowników, czyli rozwiązanie jest dostępne dla użytkowników o dowolnym poziomie wiedzy o komputerach czy bezpieczeństwie. Skutecznie blokuje dostęp do zasobu systemom korzystających z serwera DNS z wdrożonym rozwiązaniem. Jest łatwe do wdrożenia przez ISP – mają już potrzebną infrastrukturę i korzystają z niej w analogicznym projekcie. Oczywiście nie eliminuje phishingu w Polsce w zupełności, ale zmniejsza jego skuteczność.

Pomysł

Wpadłem zatem na pomysł, żeby zebrać dane, czy ISP – poza wymienionymi w porozumieniu – korzystają z tego rozwiązania. Przy okazji trochę wzrośnie świadomość, że hole.cert.pl w ogóle istnieje. Szczególnie, jeśli portale piszące o bezpieczeństwie pokuszą się o interpretację danych.

Jednak przede wszystkim ludzie dostaną argument w rozmowach ze swoimi ISP, czemu ochrona nie jest włączona. Tym bardziej, że po stronie koszt ISP praktycznie żaden. I tak już utrzymują mechanizm w związku z ustawą antyhazardową, wystarczy dodać obsługę kolejnego źródła danych.

Po krótkim namyśle stwierdziłem, że dane powinny być zbierane publicznie. Tak, aby każdy mógł sprawdzić, skąd się wzięły i ew. samodzielnie zweryfikować ich poprawność. Zresztą nie mam ani dostępu do wszystkich ISP, ani czasu na ich samodzielne testowanie.

Szybko zrobiłem repo projektu badającego adopcję hole.cert.pl na GitHubie, w którym zapisuję dane. Plus prosty skrypt w bashu, który ma ułatwić zbieranie danych.

Trudności

Niestety, nawet znając adresy IP serwerów DNS polskich ISP nie da się samodzielnie sprawdzić jak resolvują daną domenę. Ze względu na ataki DDoS wykorzystujące open resolvery DNS do amplifikacji, większość serwerów limituje dostęp do usługi. Zresztą ISP nie mają obowiązku świadczenia usługi nie swoim abonentom, więc rozumiem. To akurat wziąłem pod uwagę od początku, stąd m.in. użycie GitHub i nadzieja na pomoc innych osób.

Kolejna sprawa: nawet jeśli ktoś korzysta z danego providera, to niekoniecznie korzysta z jego serwerów DNS. I niekoniecznie ma je podane bezpośrednio w konfiguracji komputera. Dlatego trzeba podawać je jawnie. A wcześniej ustalić. Nic trudnego, ale jest to ręczna robota – albo poszukanie na stronie ISP, albo wyciągnięcie z konfiguracji.

To co mnie zaskoczyło najbardziej: Android nie daje łatwej możliwości sprawdzenia aktualnie wykorzystywanych serwerów DNS. Szczególnie przy wykorzystaniu połączenia GSM nie znalazłem tej możliwości w ogóle. Dla WiFi są programy, które to podają[3].

W zasadzie należałoby nie tylko sprawdzać, czy ISP korzysta z tych danych, ale jak często je aktualizuje. Kampanie phishingowe są coraz bardziej dynamiczne, domeny żyją nawet tylko po kilka(naście) godzin. Czas reakcji jest więc kluczowy dla rozwiązania. Synchronizacja raz dziennie byłaby słaba… Niemniej, CERT również opiera się na ręcznie dostarczanych danych i podlegają one ręcznej weryfikacji, a to potrafi chwilę trwać[4]. Wymaga to trochę innego podejścia, w tym cyklicznego uruchamiania skryptu, więc na początek odpuszczam.

Co dalej?

Planuję zebrać dane samodzielnie i z pomocą znajomych dla największych polskich ISP, którzy z nich blokują phishing. Dopracować skrypty i metody zbierania danych, szczególnie dla urządzeń mobilnych. Ustalić docelowy format plików.

Jeśli pomysł „chwyci”, postaram się dorobić badanie opóźnienia między pojawieniem się domeny na hole.cert.pl, a propagacją danych do systemów DNS danego ISP. Jeśli ktoś jest zainteresowany, zachęcam do pomocy. Raczej na GitHub, niż w komentarzach, ale nie będę wybrzydzał.

UPDATE Dobrzy ludzie przypomnieli, że istnieje coś takiego jak RIPE Atlas probe i doładowali kredyty, używane przy requestach API. Wygląda, że nada się idealnie, więc nieco zmodyfikuję podejście. Tym bardziej, że będzie łatwa możliwość zrobienia samodzielnych, cyklicznych zapytań i mierzenia czasu propagacji.

UPDATE 2 Wygląda, że RIPE Atlas probe nie potrafi zwrócić pełnego wyniku resolvowania domeny (tu: rekordów A) z danej sondy, wykonanego z danej sondy na konkretnym serwerze DNS. Będę jeszcze badał temat czy nie można jakoś tego obejść, ale w API nie znalazłem. Dobrzy ludzie też nie, więc to nie moja ślepota.

UPDATE 3 Jednak zwraca, choć w mniej oczywisty sposób. Wyjaśnienie w kolejnym wpisie.

[1] Akurat ta domena nie była jeszcze blokowana na hole.cert.pl, została zgłoszona jako phishing i… nadal jej nie widzę w blokowanych. Zdarza się.

[2] Tak pewnie myśli każdy z niezerowym pojęciem o komputerach i bezpieczeństwie IT, prawda?

[3] Tylko co mi po nich, jeśli wtedy jest to IP routera, ew. ISP stacjonarnego?

[4] Nawet robiłem jakiś benchmark, ale próbka zdecydowanie za mała, żeby wyciągać wiążące wnioski. Nie było źle.

Dlaczego k-anonimowość nie jest dobra przy hasłach?

Na z3s.pl pojawił się artykuł o tym, czym jest k-anonimowość. Jest to dobry artykuł i warto go przeczytać przed lekturą tego wpisu. Nie zgadzam się jedynie z tezą, że w przypadku haseł jest to bezpieczna metoda sprawdzania. Napisałem komentarz, ale pewnie nie wszyscy czytelnicy bloga tam trafią. Ponieważ bawię się z bazą hashy z HIBP i planuję wkrótce wpis na ten temat, uznałem, że jest dobra okazja do wstępu.

Moja teza jest taka, że w przypadku haseł k-anonimowość wcale nie jest taka bezpieczna, jak jest to przedstawiane. Zgodnie z artykułem obecnie dla hashy z bazy HIBP pierwsze 5 znaków występuje od 381 do 584. Czyli podczas sprawdzenia strona trzecia nie poznaje ani hasła, ani jego pełnego hasha. Przekazywane jest jedynie pierwszych 5 znaków hasha, czyli – tu moja interpretacja – ma jedynie 1/381 do 1/584 prawdopodobieństwo, że zna właściwy hash.

Gdyby przyjąć, że strona trzecia jest złośliwa, warto też przyjąć, że jest inteligentna. Czyli zamiast prawdopodobieństwa zwykłego użyje prawdopodobieństwa ważonego, uwzględniając ilość wystąpień danego hasha. Dla przykładu z artykułu na z3s.pl i hasła P@ssw0rd mamy zwracanych 543 różnych hashy:

curl -s https://api.pwnedpasswords.com/range/21BD1 | wc -l

Natomiast suma wystąpień wszystkich hashy w momencie pisania tego wpisu wynosi 60808.

curl -s https://api.pwnedpasswords.com/range/21BD1 | awk -F ":" '{sum += $2} END {print sum}'

Nasz hash wystąpił 52579 razy. Znając zwyczaje ludzi dotyczące haseł i stosując prawdopodobieństwo ważone uzyskujemy 86% szansę na to, że chodzi o hash należący do hasła P@ssw0rd. Pewności nie ma, ale z 1/543 czyli z ~0,18% robi się 86%, czyli jakieś 467 razy więcej. Ups!

Oczywiście nie znamy tu samego hasła. Znamy jedynie – a i to jedynie ze sporym prawdopodobieństwem – jego hash. O tym, że to niekoniecznie jest problem, może będzie w którymś kolejnym wpisie.

W każdym razie gdybym był serwisem, to bałbym się odpytywać serwis trzeci o hashe haseł moich użytkowników. Użytkowników podejrzewam o proste, słownikowe hasła, jakiś serwis trzeci. Zwłaszcza jeśli ten serwis ma/może mieć także inne informacje, które pozwalają mu ustalić kto pyta o hasło. Tak właśnie może być w przypadku Cloudflare, który może dostawać część ruchu od użytkownika w ramach CDN, DNS lub DoH. Prosta korelacja czasowa może w tym przypadku prowadzić do powiązania hasha hasła z IP użytkownika. Jeśli chcemy sprawdzać hasła, to lepszym rozwiązaniem jest stworzenie lokalnej kopii bazy którą pobierzemy z HIBP.

Co nie znaczy oczywiście, że k-anonimowość ogólnie nie spełnia swojego zadania. Po prostu mam wrażenie, że akurat w przypadku hashy hasła i tej konkretnej implementacji nie jest tak bezpieczna, jak jest to przedstawiane.

Warto też zauważyć, że hasło z którym mamy tu do czynienia jest proste/populare. Dla innych pięcioznakowych początków hashy wystąpienia mogą rozkładać się inaczej, bez tak silnego wskazania na konkretny hash.

UPDATE Tak naprawdę nie ma potrzeby używania całej bazy hashy i ilości ich wystąpień z HIBP (>20GB). Najczęściej występujące 100 tys. hashy to raptem 3,2 MB. Najczęstszy milion – 32 MB.