Połam to jeszcze raz

Niedawno sekurak.pl zorganizował kolejny konkurs z łamaniem hashy. Tym razem dowiedziałem się o nim przypadkiem – ot, wpadło powiadomienie z Discorda, które akurat zauważyłem. Rzuciłem okiem, jest link do wpisu, są hashe, więc warto spróbować, bo do wygrania był kolejny dostęp do Akademii Sekuraka 2024.

Miałem w zasadzie gotowca na podstawie poprzedniego wpisu i writeupu, więc liczyłem, że kilka sztuk wpadnie. I faktycznie, nie zawiodłem się. Opisane wcześniej metody pozwalały złamać 7 do 9 hashy. Skąd rozbieżność? Zależy jak liczyć i jaki się utworzyło dedykowany słownik.

Zajmijmy się trzema hashami, z którymi mógł być problem. Pierwszy to db13ca089eb4860896567399a30558f2c1fc69e7:sekurak.academy.
Przyznaję, że zwyczajnie miałem szczęście. W słowniku zagościł bowiem ciąg sekurakacademy, a OneRuleToRuleThemAll zrobiło resztę. Aż się zdziwiłem, czemu to wpadło, więc sprawdziłem.

Sprawdzenie, która reguła spowodowała dopasowanie robi się zgodnie z opisem z dokumentacji hashcata poprzez dodanie parametrów

--debug-mode=1 --debug-file=matched.rule

Okazuje się, że OneRuleToRuleThemAll ma takie cudo jak

l i7.

Przekładając na bardziej zrozumiały język: zamień wszystkie litery na małe i wstaw kropkę po siódmym znaku. Chwilę po godzinie dziewiętnastej odesłałem 8 złamanych hashy.

Kolejne hasło, które mogło sprawić trudność to złożenie nie dwóch, ale trzech wyrazów. Czułem, że będzie, bo tego typu pojawiały się w poprzednich edycjach. Schemat był zawsze ten sam – dwa wyrazy oddzielone krótkim spójnikiem. Sprawdziłem, ile jest krótkich spójników w moim słowniku. Samych dwuznakowych było aż 428. Uznałem, że to zbyt wiele. W dodatku większość się nie nadawała – zz czy yy po prostu mi nie pasowały.

Postanowiłem spróbować z dwuliterowymi. Ręcznie przejrzałem wszystkie dwuliterowe słowa, wybrałem 25, które wydały mi się najbardziej prawdopodobne, zapisałem do pliku. Dorzuciłem jeszcze 3 jednoliterowe spójniki: i, o, a. Na koniec dodałem 19 wyrazów trzyznakowych. Tak powstał plik laczniki.txt.

Stwierdziłem też, że dla pełnego słownika języka polskiego i tak wyszłoby wiele kombinacji. Hasła z poprzednich edycji były umiarkowanej długości. Postanowiłem wziąć na warsztat tylko wyrazy od 2 do 9 znaków. Nazwałem go 29_slownik_all_nopl.txt. Liczył 845 tys. wierszy.

Krótkim skryptem w Pythonie utworzyłem słownik z wszystkimi kombinacjami wyrazów z 29_slownik_all_nopl.txt oraz 57 łączników. Wynikiem był spory, ale nadal akceptowalny słownik zawierający 48 mln wierszy.

Potem już standardowo – operacja na dwóch słownikach. Pierwszym był 29_slownik_all_nopl_laczniki.txt zaś drugim 29_slownik_all_nopl.txt. Tak udało się złamać 61d54fe02ce6edcde2f5762f2677b3c83d876417:trudnotozgadnac
i kwadrans po dwudziestej pierwszego dnia odesłałem 9 haseł.

Wersja z tworzeniem słownika na dysku nie jest może najefektywniejszym podejściem do haseł zbudowanych z trzech słów, ale jak widać wystarczyła.

Warto w tym miejscu odnotować, że słowo to pojawiało się wcześniej i gdyby człowiek pomyślał, to mógł przyjąć, że będzie i tym razem. Cóż, po fakcie każdy mądry.

Ostatnie hasło jest ciekawą i pouczającą sprawą. Na discordowym kanale pojawiła się informacja, że już dwie osoby mają 9 haseł, w dodatku gdyby połączyć ich odpowiedzi, to będzie komplet, czyli wszystkie hasła z konkursu zostały złamane. Dopuszczałem możliwość, że coś przeoczyłem, bo jak wspominałem do konkursu przystąpiłem w pośpiechu. Na innym kanale Patryk (jego writeup z łamania haseł znajdziecie na tutaj – polecam) też pochwalił się złamaniem 9 hashy.

Po krótkiej rozmowie czego komu brakuje i ustaleniu, że faktycznie chodzi o nas, Patryk rzucił podpowiedzią, że brakujący hash jest podobny do już złamanego, ale po polsku. Cóż, wielkiego wyboru nie było i wkrótce, po dosłownie czterech próbach wykonanych ręcznie, do złamanych dołączył:
283d5cb401e9de6a2e56f97166a639479fb86aee:akademiasekuraka
Komplet haseł odesłałem o 20:23 drugiego dnia konkursu.

Zarówno słowo akademia jak i sekurak oczywiście miałem w słowniku. Zabrakło wersji z deklinacją. Mam zatem pewne wnioski dotyczące słownika na przyszłość…

UPDATE: Artykuł z oficjalnym rozwiązaniem konkursu i statystykami rozwiązań.

Podobieństwo i porównywanie zbiorów

Przy okazji projekciku[1] trafiłem na interesujący problem. Chodzi o porównywanie zbiorów, a dokładnie liczenie ich podobieństwa. Marzyło mi się, by wyrazić je w zakresie 0 do 1, gdzie 0 to zbiory rozłączne, a 1 zbiory identyczne[2].

Popełniłem następujący kawałek kodu, liczący, na ile set2 jest podobny do set1:

def count_similarity_percent(set1, set2):
    base_count = len(set1)
    if base_count == 0:
        base_count = 1
    matching = 0
    for element in set1:
        if element in set2:
            matching += 1
    perc = 100 * matching / base_count
    return perc

Niby działa, ale… Spójrzmy na przykłady.
Przykład 1:

A = [1, 2, 3, 4]
B = [3, 4, 5, 6]

Podobieństwo A do B wynosi 50% (2 z 4 elementów ze zbioru A występują w B).
Podobieństwo B do A także wynosi 50% (2 z 4 elementów ze zbioru B występują w A).
Wszystko fajnie. Podobieństwa są równe, niezależnie z której strony patrzeć.

Niestety, jeśli zbiory będą miały różną ilość elementów, to sprawa się komplikuje i podobieństwo A do B przestaje być równe podobieństwu B do A. Możemy to zobaczyć w kolejnym przykładzie.
Przykład 2:

A = [1, 2, 3, 4]
B = [3, 4]

Podobieństwo B do A to nadal 50%.
Podobieństwo A do B wynosi 100% – 2 z 2 elementów A występują w B.

Nie spodobało mi się to i zastanawiałem się, co można z tym zrobić. Z jednej strony chciałem, by wartości były przechodnie, tj. równe, niezależnie z które strony porównuję.

Przez głowę przeleciało mi szybko liczenie średniej z podobieństw i zapamiętywanie w obu przypadkach tego wyniku. Podobnie mógłbym zapamiętywać w obu wynikach większą – lub mniejszą – z wartości podobieństwa. Z drugiej strony kołatało mi się, że fajnie nie byłoby liczyć dwa razy.

Poszukałem, popytałem i okazało się, że są do tego miary. W tym przypadku miarą podobieństwa zbiorów jest indeks Jaccarda[3] . Jest prosta i „przechodnia”, tj. nie ma znaczenia, który zbiór z którym się porównuje.

Znalazłem taki wpis o indeksie Jaccarda dla Pythona, ale przyznaję, że nie spodobała mi się implementacja. Po pierwsze, bez sensu importowane jest numpy, które w tym przypadku niczego nie robi. Po drugie, implementacja jest poprawna, ale nieco zakręcona. Lepiej implementować na setach, nie listach. I jak najbardziej można skorzystać z wbudowanego union, a następnie liczyć wprost, wg definicji.

def jaccard_index_percent(set1, set2):
    intersection_count = len(set1.intersection(set2))
    union_count = len(set1.union(set2))
    if union_count == 0:
        union_count = 1
    return 100 * intersection_count / union_count

Zapewne użyję ww. wersji do porównywania zbiorów. Chyba, że pokuszę się jeszcze kiedyś o optymalizację pod kątem prędkości działania. O ile zauważę taką potrzebę.

[1] Jeśli coś się z tego wykluje sensownego, to niebawem opiszę dokładniej.
[2] Od 0 do 1, od 0% do 100%, bez znaczenia, wartość jest taka sama. Choć IMO ludzie lepiej rozumieją procenty, a łatwiej kodować na float. Ale to wszystko nieistotny detal.
[3] Linkuję we wpisie polską wersję, która jest lakoniczna, ale prostsza. Jeśli ktoś jest bardziej zainteresowany tematem, to polecam wersję angielską, wraz z see also.

Advent of Code – po latach

Całkiem niedawno M. w mailu zapytał, czy znam Advent of Code. Ano znam. Nawet miałem o tym notkę. Odkąd poznałem, to bawię się regularnie. Z przerwą w 2019 z powodów, których nie pamiętam.

Ponieważ z tego co pamiętam nie robiłem jeszcze zadań „poza konkursem”, to dobry moment na podsumowanie. 2018 – 10 gwiazdek. 2020 – 33 gwiazdki, 2021 – 29 gwiazdek. W tym roku póki co 21 gwiazdek. Oczywiście na ilość gwiazdek rzutuje wiele czynników, z których istotnym jest czas, który mogę poświęcić na zabawę.

Niemniej staram się siąść codziennie rano o szóstej do komputera i porywalizować. Głównie ze znajomymi z pracy. Wspólny dashboard z wynikami to dobry motywator. I AoC ma to rozwiązane bardzo fajnie. W sumie tego najbardziej zabrakło mi w opisywanym rok temu Advent of Cyber.

Czy coś się zmieniło? Wydaje mi się, że tak. Dorobiłem się schematu rozwiązywania zadań. Po pierwsze, wszystko rozwiązuję w Pythonie. Po drugie, standardu dorobiły się struktura katalogów, nazewnictwo plików, nazewnictwo plików z wejściem, czytanie wejścia. Parsowanie wejścia, i debug podczas pisania też. Niekoniecznie zawsze jest przez to wszystko szybciej, ale mam wrażenie, że czas rozwiązywania jest stabilniejszy.

Wydaje mi się, że lepiej radzę sobie ze strukturami danych i podziałem na funkcje. Staram się pisać bardziej pythonowo, ale różnie to wychodzi. Z innych obserwacji – do rozwiązywania niektórych zadań (np. części drugiej dziś) przydaje się zabawa w CTFy.

Ogólnie Advent of Code bardzo mi się spodobał. Na tyle, że wykupuję co roku AoC+. Warto, bo to prawdziwe pay what you want.