RapidFuzz i CharMap jako pomoc przy sprawdzaniu phishingu

nfsec.pl 1 dzień temu

R

apidFuzz jest cenioną za uniwersalność i szybkość biblioteką języka Python, która oferuje zestaw funkcji pomagających w obliczaniu różnic między ciągami znaków oraz dopasowywania rozmytego (ang. fuzzy matching). W swojej implementacji posiada algorytmy obliczające odległość (miarę różnicy między dwoma ciągami znaków) takie jak: Levenshteina, Damerau-Levenshteina, Hamminga, czy Jaro-Winklera oraz algorytmy obliczające współczynnik podobieństwa ciągów znaków (np. porównujące liczby pasujących do siebie znaków na tle całkowitej liczby znaków w obu ciągach). Możemy wykorzystać ją w swoim procesie przetwarzania danych, aby automatycznie identyfikować: dopasowania, duplikaty, literówki; usprawniać oczyszczanie danych lub szukać w innych bazach danych podobnych wzorców.

Jako przykład użycia posłużymy się tym ostatnim przypadkiem. Na postawie listy ostrzeżeń CERT Polska, w której zawarte są głównie domeny wykorzystywane do phishingu będziemy sprawdzać, czy inne domeny pojawiające się w przestrzeni internetowej pasują przynajmniej w 90% do już znanych wzorców. jeżeli nie – będziemy sprawdzać czy istnieją takie same wzorce dzięki algorytmu, który nazwałem “mapą znaków”. Bardzo często dzieje się tak, iż prowadzona kampania wymierzona w daną markę posiada wiele domen o podobnym schemacie np.:

00lx-shopping.2323256.xyz 00lx-shopping.3122121.xyz 00lx-shopping.385430.xyz delivery-dpd.64684512.xyz delivery-dpd.64781200.xyz delivery-dpd.6560000.xyz kup-online33212.pl kup-online33882.pl kup-online55003.pl

Dlatego wyłapywanie ich dzięki algorytmów dostępnych w i poza RapidFuzz może znacznie przyśpieszyć proces automatycznego ich zgłaszania do wielu zainteresowanych odbiorców. Pierwszą funkcją jaką użyjemy w celach demonstracyjnych jest WRatio (ang. weighted ratio) – oblicza ona wyważony współczynnik na podstawie innych algorytmów dostępnych w bibliotece. Do “przemielenia” wielu domen użyjemy modułu procesowania, który porównuje ciągi znaków z listami innych ciągów. Jest to generalnie bardziej wydajne niż używanie bezpośrednio algorytmów punktujących w pętli.

def find_similar_score(entries: list[str]) -> bool: report: list[tuple[str, float, int]] = process.extract( query=fqdn, choices=entries, limit=999999, score_cutoff=90 )

Na razie będą interesowały nas tylko wyniki, które są równe lub powyżej 90% (score_cutoff) i chcemy przeszukiwać całą bazę (limit), która aktualnie liczy 257.646 wpisów. Nie musimy definiować algorytmu punktującego, ponieważ w standardzie jest on ustawiony na WRatio; nie ustawiamy też żadnego procesora usuwającego znaki specjalne oraz przeprowadzającego konwersję wszystkich znaków na małe litery. Kilka przykładów użycia:

agresor@darkstar:~$ ./phisherfisher allegrolokalnie.pl62985oferta-iphone.pl INFO: Domain score found 1 similar phishing domain(s). Example: allegrolokalnie.pl19293oferta-iphone.pl with score 92.30. agresor@darkstar:~$ ./phisherfisher o1x.99415487.xyz INFO: Domain score found 4 similar phishing domain(s). Example: o1x.99415487.xyz with score 100.0. agresor@darkstar:~$ ./phisherfisher www.inpost.polska24-dostawa24.shop INFO: Domain score found 4 similar phishing domain(s). Example: www.i-inpost.polska24-dostawa24.shop with score 97.14. agresor@darkstar:~$ ./phisherfisher allegrolokalnie.p24-v09a091.shop INFO: Domain score found 1 similar phishing domain(s). Example: allegrolokalnie.p24-v9a01.pl with score 90.0.

W bardzo wielu przypadkach RapidFuzz spisuje się wzorowo, ale faktem jest, iż porównujemy domeny, które nie zawsze muszą być semantycznie poprawne i mogą być tak różne od siebie pod względem użytych znaków w całym ciągu, iż trudno mu wskazać jednoznacznie podobieństwo mimo, iż po wzrokowej analizie widać, iż to ten sam lub powracający schemat. Na przykład:

agresor@darkstar:~$ ./phisherfisher o1x.79715482.xyz INFO: Based on score, no similar phishing domain(s) found.

Oczywiście możemy obniżyć próg detekcji np. do 80%:

agresor@darkstar:~$ ./phisherfisher o1x.79715482.xyz INFO: Domain score found 12 similar phishing domain(s). Example: o1x.539715481.xyz with score 84.85.

Jednak ciągłe obniżanie progu spowoduje, iż dojdziemy do ściany tworząc niskiej jakości dopasowania. I tutaj wkracza mapa znaków aka CharM(ap), czyli prosty konwerter ciągu znaków na rodzaje znaków użyte w owym ciągu:

def create_map(item: str) -> str: return ''.join('a' if c.isalpha() else 'd' if c.isdigit() else c for c in item)

Dzięki temu domena, z którą RapidFuzz sobie “nie poradził” zamienia się w:

o1x.79715482.xyz = ada.dddddddd.aaa

i zostaje porównana do innych domen, które mają taką samą mapę:

agresor@darkstar:~$ ./phisherfisher o1x.79715482.xyz INFO: Based on score, no similar phishing domain(s) found. INFO: Subdomain charm found 84 similar phishing domain(s). Example: o1x.05412458.xyz. o1x.79715482.xyz = ada.dddddddd.aaa o1x.05412458.xyz = ada.dddddddd.aaa

W pierwszym kroku nie robimy tego na wszystkich domenach, tylko na zbiorze o takich samych subdomenach:

def find_similar_subdomain_charm(entries: list[str]) -> bool: report = [entry for entry in entries if re.search(f'^{subdomain}\.', entry) and create_map(fqdn) == create_map(entry)]

Patrząc na charakterystykę phishingu puszczanego na różne polskie serwisy można dojść do wniosku, iż subdomeny są stałym elementem w ramach różnych kampanii. Dlatego zamiast przeszukiwać cały zbiór od razu możemy na początku skupić się na subdomenie, która podwyższa wiarygodność detekcji:

agresor@darkstar:~$ ./phisherfisher vimted.99415483.xyz INFO: Based on score, no similar phishing domain(s) found. INFO: Subdomain charm found 16 similar phishing domain(s). Example: vimted.07865460.xyz. agresor@darkstar:~$ ./phisherfisher ddpd.98941248.xyz INFO: Based on score, no similar phishing domain(s) found. INFO: Subdomain charm found 18 similar phishing domain(s). Example: ddpd.06591548.xyz. agresor@darkstar:~$ ./phisherfisher impost.68941248.xyz INFO: Based on score, no similar phishing domain(s) found. INFO: Subdomain charm found 53 similar phishing domain(s). Example: impost.05184122.xyz.

Algorytmem “ostatniego ratunku” jest hybryda RapidFuzz oraz CharM(ap). Zamiast przefiltrować wszystkie domeny tylko samym CharM(ap) pozwalamy RapidFuzz na przeszukanie również ich lekkich modyfikacji. Dzięki temu, gdy dopasowanie 1 = 1 (100%) zawiedzie w CharM(ap) będziemy mogli spojrzeć na bliskie alternatywy obniżając dopasowanie do 95%, ale tylko jeżeli ich TLD są identyczne:

def find_similar_domain_charm(entries: list[str]) -> bool: report = [(entry, score) for entry in entries if (score := fuzz.WRatio(create_map(fqdn), create_map(entry), score_cutoff=95)) > 95 and re.search(f'\.{tld}$', entry)]

Przykłady:

agresor@darkstar:~$ ./phisherfisher allegr00lokalnie.47906408.xyz INFO: Based on score, no similar phishing domain(s) found. INFO: Based on subdomain charm, no similar phishing domain(s) found INFO: Domain charm found 70 similar phishing domain(s). Example: all-egr0lokalnie.52312226.xyz with score 96.55. agresor@darkstar ./phisherfisher allegr0loka1nie.pl-ogloszenie-firmowe-736832.icu INFO: Based on score, no similar phishing domain(s) found. INFO: Based on subdomain charm, no similar phishing domain(s) found INFO: Domain charm found 1 similar phishing domain(s). Example: allegrolokalnie.pl-ogloszenie-firmowe-521092.icu with score 95.83.

Cały kod skryptu znajduje się na GitHub.

Więcej informacji: RapidFuzz, Lista Ostrzeżeń przed niebezpiecznymi stronami

Idź do oryginalnego materiału