Advent of Cyber

Jakiś czas temu pisałem o Advent of Code. Uspokajam, że bawię się co roku. W zeszłym nawet z pewnymi sukcesami, przynajmniej według mojej subiektywnej oceny. Jednak nie samym programowaniem człowiek żyje. Jeśli ktoś interesuje się – albo zamierza interesować się – bezpieczeństwem komputerowym, to informuję, że jest coś takiego jak Advent of Cyber.

Wydarzenie ma za zadanie naukę podstaw IT security poprzez codzienne, proste zadania. Czyli nieco podobnie jak w przypadku Advent of Code. Na tym jednak podobieństwa się kończą. Zacząłem korzystać wczoraj, więc mogę nie mieć pełnego oglądu, ale pozwolę sobie na podsumowanie dotychczasowych spostrzeżeń.

Advent of Cyber ma trochę przyległości[1]. W pierwszym zadaniu jest spore parcie na obserwowanie firmy w social media. Co prawda tylko deklaratywne, ale jest. Całość też jest zauważalnie związana z platformą organizatora. Za to, w przeciwieństwie do Advent of Code, są nagrody rzeczowe losowane wśród uczestników.

Kolejna różnica, to zapewnienie pełnych materiałów edukacyjnych. Try Hack Me, czyli organizator to serwis oferujący szkolenia security i nie jest tu inaczej. Do pierwszego zadania było podane pełne wytłumaczenie podatności. Czyli w zasadzie zero wyzwania. Dla przypomnienia w Advent of Code dostajemy tylko treść zadania i trzeba kombinować samodzielnie od początku do końca.

Teoretycznie wymagane jest zestawienie tunelu VPN, ale organizator zapewnia maszynę wirtualną dostępną przez przeglądarkę. Jedyne ograniczenie to czas dostępności dla darmowych kont – jest to jedna godzina dziennie. Wydaje mi się, że powinno w zupełności wystarczyć na zadania z eventu.

Wydarzenie zaczęło się wczoraj, ale nie jest to żadna przeszkoda, by dołączyć. Można traktować je jako wprowadzenie do CTFów. Formuła podobna – zadania do zrobienia, flagi do zdobycia. Planuję się pobawić, trochę dla odświeżenia wiedzy, trochę dla bliższego zapoznania z platformą.

UPDATE: Po pierwszych paru dniach mogę potwierdzić spostrzeżenie po pierwszym dniu. To wydarzenie raczej edukacyjne, z gotowcami lub praktycznie gotowcami, niż wyzwanie. Nie jest to zarzut, a stwierdzenie faktu.

[1] Strings attached byłoby tu idealnym określeniem, nie znam polskiego odpowiednika.

Certstream HOWTO

Do czego można wykorzystać dane pochodzące z certstream można zobaczyć choćby na prezentacji z konferencji z BSides Warsaw z 2019 roku. Ponieważ każdy ma certstream, mam i ja. Nie jest to zbyt zasobożerne, a można popatrzeć pod kątem security i phishingu, co w sieci piszczy.

Problem

Niestety, zauważyłem korzystanie z certstream jak w przykładach, z użyciem biblioteki Pythona nie jest idealne. Problem objawia się tym, że co jakiś czas następuje zerwanie połączenia z serwerem widoczne jako:

[ERROR:certstream] 2021-11-22 17:18:01,171 - Error connecting to CertStream - Connection to remote host was lost. - Sleeping for a few seconds and trying again…
[INFO:certstream] 2021-11-22 17:18:06,564 - Connection established to CertStream! Listening for events…
[ERROR:certstream] 2021-11-22 17:18:51,893 - Error connecting to CertStream - Connection to remote host was lost. - Sleeping for a few seconds and trying again…
[INFO:certstream] 2021-11-22 17:18:57,213 - Connection established to CertStream! Listening for events…

Początkowo nie wydawało się to dramatem. Kilka sekund przerwy, co parę minut nie powinno mocno wpływać na wyniki. Niestety, pominięte certyfikaty okazały się zauważalne, więc zacząłem szukać przyczyny i rozwiązania tego problemu.

Debug

Na pierwszy ogień poszła wyszukiwarka. Okazało się, że ludzie mają ten problem w różnych warunkach. Jedni sugerowali, że zależne jest to od wersji Pythona. Inni nie potwierdzali i sugerowali problem z serwerem certstream.calidog.io. Podejrzewałem także zbyt niską wydajność serwera VPS na którym działa skrypt, ale ten pomysł szybko odrzuciłem – na mocniejszym VPS nie było wielkiej różnicy. Problem nie był palący i przeleżał trochę czasu. Dopiero na którejś konferencji zaczepiłem Adama L., który również korzysta z certstream i zdarza mu się wspominać w prezentacjach o tym. Zasugerował problem z biblioteką, jej debug i zrobienie po swojemu, na wzór.

Zainspirowało mnie to do bliższych oględzin. Nie jest to trudne – biblioteka jest niewielka i prosta. Na pierwszy ogień poszła złożoność operacji wykonywanych w skrypcie podczas sprawdzania domen. Szybko jednak okazało się, że nawet nie wykonując jakichkolwiek operacji, nawet nie wyświetlając domen, nadal obserwuję rozłączenia. Postanowiłem spróbować skorzystać z innego serwera certstream, tym bardziej, że biblioteka wprost przewiduje zmianę URLa. Tyle, że miałem problem ze znalezieniem takowego.

Na szczęście CaliDog udostępnia także kod źródłowy serwera napisany w… Eliksirze. Nie ukrywam, że była to dla mnie nowość. Instalacja na Debianie była pewnym wyzwaniem. Dla leniwych jest dostępny obraz dockera, ale z pewnych względów stwierdziłem, że wolę zainstalować w systemie.

Instalacja serwera certstream na Debianie

Instalacja na Debianie Bullseye jest w zasadzie zgodna z tym, co napisano w instrukcji repo i na stronie Elixir. Czyli nie jest zgodna. Pierwsza sprawa, to brak pakietów dla Bullseye[1]. Bez problemu można jednak skorzystać z pakietów przygotowanych dla starszej wersji, czyli Buster. W pliku /etc/apt/sources.list.d/erlang-solutions.list powinno pojawić się zatem:

deb http://binaries.erlang-solutions.com/debian buster contrib

Kolejna rzecz powodująca problemy z uruchomieniem serwera certstream to konieczność doinstalowania pakietu erlang-dev:

apt-get install erlang-dev

Po tych zabiegach serwer powinien się bez problemu uruchomić. Domyślnie działa bez szyfrowania na porcie 4000, więc jeśli nie postawimy frontu z certyfikatem SSL, to URL serwera certstream zmieni się z domyślnego wss://certstream.calidog.io na ws://NASZ.ADRES:4000 – trzeba podać port i usunąć jedno s.

Po uruchomieniu własnego serwera i podłączeniu tego samego skryptu, z tego samego VPSa, okazało się, że rozłączenia prawie nie występują. Zaś porównując ilość zgubionych domen okazało się, że jest ich znacznie więcej, niż przypuszczałem. Nawet średnie kilkadziesiąt procent.

Serwis

Do pełni szczęścia brakuje tylko, aby wszystko działało automatycznie. Można to uzyskać tworząc serwis. Można np. utworzyć plik:

/etc/systemd/system/certstream-server.service

o zawartości

[Unit]
Description=Certstream server service
After=network.target
StartLimitIntervalSec=3


[Service]
Type=simple
Restart=always
RestartSec=1
User=certstream
WorkingDirectory=/opt/certstream-server
ExecStart=/usr/bin/mix run --no-halt

[Install]
WantedBy=multi-user.target

Następnie należy zarejestrować serwis i można cieszyć się serwisem uruchamiającym się automatycznie podczas startu systemu. A także po ew. awarii. Ścieżki i user do dokonfigurowania we własnym zakresie.

[1] UPDATE: Część o braku pakietów dla Bullseye jest nieaktualna. Ze sporym opóźnieniem, ale się pojawiły. Warto zatem korzystać z wersji dedykowanej dla używanej dystrybucji.

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)