Nymaim atakuje ponownie

cert.pl 7 lat temu

Wstęp

Nymaim nie jest nową rodziną złośliwego systemu – pierwszy raz został napotkany w 2013 roku. Wtedy był wykorzystywany jedynie jako dropper, używany głównie do dystrybucji TorrentLockera.

W lutym 2016 ponownie stał się popularny, po tym jak do jego kodu zostały dołączone fragmenty kodu ISFB, który wcześniej wyciekł. Zyskał wtedy przydomek „Goznym”. Ta inkarnacja Nymaima była dla nas szczególnie interesująca, ponieważ zyskała możliwości bankera i stała się poważnym zagrożeniem w Polsce. Z tego powodu przeprowadziliśmy dokładną analizę tego zagrożenia i byliśmy w stanie śledzić aktywność Nymaima od tamtego czasu.

Przez ostatnie dwa miesiące, wiele rzeczy się zmieniło. Przede wszystkim, sieć fast-flux nazywana „Avalanche” (wykorzystywana intensywnie przez Nymaima) została wyłączona na skutek skoordynowanych działań organów ścigania kilku krajów. Przez prawie dwa tygodnie Nymaim był zupełnie nieaktywny, a do dzisiaj jest cieniem tego czym był jeszcze niedawno. Mimo iż jest ciągle aktywny w Niemczech (z nowymi injectami), dopiero niedawno powrócił do Polski.

Obfuskacja kodu

Ten temat został już dobrze opisany przez innych badaczy, ale ciągle jest na tyle interesujący, iż warto o nim wspomnieć.

Kod Nymaima jest bardzo mocno zaciemniony dzięki autorskiego narzędzia – do tego stopnia, iż analiza jest prawie niemożliwa. Na przykład typowy kod wygląda tak:

Zostało tu użytych wiele technik utrudniających analizę, więc omówimy je po kolei:

Po pierwsze, rejestry procesora nie są umieszczane bezpośrednio na stosie, ale jest używana do tego pomocnicza funkcja push_cpu_register. Na przykład push_cpu_register(0x53) jest równoważna push ebx, a push_cpu_register(0x50) to push eax. Stałe odpowiadające rejestrom zmieniły się przynajmniej raz między wersjami, ale kolejność jest zawsze ta sama:

. register constant
0 eax 0x50
1 ecx 0x51
2 ebx 0x52
3 edx 0x53
4 esp 0x54
5 ebp 0x55
6 esi 0x56
7 edi 0x57

Dodatkowo, większość stałych jest obfuskowana. Na przykład mov eax, 25 może zostać zmienione na:

Stała użyta w przykładzie to 8CBFB5DA, ale to nie żadna reguła – to po prostu losowy dword, wygenerowany tylko na potrzeby zaciemnienia kilku stałych (zmienia się zawsze z wersji na wersję). Ważne jest tylko to, iż po tej operacji rejestr eax będzie miał wartość taką jaką chcemy.

Poza xor_eax_with_X są też analogiczne podobne funkcje: sub_*_from_eax and add_*_to_eax.

Ostatecznie, przepływ sterowania jest mocno zmodyfikowany. Jest bardzo dużo metod wykorzystanych do utrudnienia analizy, ale wszystkie sprowadzają się do prostej podmiany: call X i jmp X są transformowane do przynajmniej dwóch operacji push i skoku gdzieś. Ta metoda jest bardzo podobna do technik wykorzystywanych przy ukrywaniu zmiennych – np. zamiast użyć adresu 0x42424242, skok wykonywany jest do funkcji detour z parametrami 0x40404040 oraz 0x02020202. W asemblerze, zamiast:

będziemy widzieć coś w rodzaju:

Istnieje też sprytna wariacja tej metody, gdzie detour zamiast dwóch argumentów przyjmuje jeden – wtedy kod maszynowy za opkodem call jest wykorzystywany jako stała (inaczej mówiąc, detour używa adresu powrotu wrzuconego przez call jako wskaźnika do drugiej stałej).

Podsumowując, poprzednio wklejony przetworzony kod może być interpretowany w ten sposób:

Dysponując tą wiedzą, stworzyliśmy własny deobfuskator. Było to dość dawno temu i od tego czasu pojawiły się też inne rozwiązania. Nasze narzędzie niekoniecznie działa najlepiej, ale ma kilka unikalnych (z tego co wiemy) funkcji, których potrzebujemy, np. odzyskiwanie importów i deszyfrowanie zaszyfrowanych napisów.

Inne narzędzia to na przykład mynaim i ida-patchwork.

Tak czy inaczej, dzięki naszego narzędzia jesteśmy w stanie całkiem dobrze oczyścić kod:

Ale to nie wszystko co potrafi obfuskator Nymaima. Na przykład zewnętrzne funkcje nie są wywoływane bezpośrednio, zamiast tego używany jest skomplikowany wrapper:

Ten wrapper wrzuca hash z nazwy funkcji na stos i skacze do kolejnego (mimo iż użyty jest opkod call, nigdy nie wykonywany jest ret to tego adresu):

Drugi dispatcher wrzuca hash nazwy dll na stos i skacze do pomocniczej funkcji:

Ostatecznie wykonywany jest prawdziwy dispatcher:

Dodatkowo prawdziwy adres powrotu z API jest obfuskowany – wskazuje on na call ebx gdzieś w bibliotece ntdll, a prawdziwy adres powrotu znajduje się wtedy w ebx. Większość narzędzi zupełnie sobie z tym nie radzi. Bardzo utrudnia to śledzenie wykonania kodu.

Ale to nie wszystko. Tak jak widzieliśmy, krótkie stałe są obfuskowane przy pomocy prostych operacji matematycznych, ale co z dłuższymi stałymi, takimi jak np. napisy? Twórcy złośliwego systemu również na to mają odpowiedź. Prawie wszystkie napisy użyte w programie są przechowywane w specjalnej sekcji danych. Kiedy Nymaim potrzebuje jednej z nich, używa specjalnej funkcji, którą nazwaliśmy encrypted_memcpy. Jest dość prosta w swoim działaniu:

Samo memcpy_and_decrypt też nie jest skomplikowane. Nasza wersja tej funkcji w Pythonie ma jedynie kilka linijek kodu:

Potrzebujemy tylko wyciągnąć stałe użyte do deszyfrowania (różnią się między programami) – są ukryte w tych kawałkach kodu.

(Te funkcje nigdy nie są zaciemniane, więc możemy wyciągnąć te stałe dzięki prostego pattern matchingu).

Ale ukrywanie stałych to nie wszystko – okazjonalnie szyfrowany jest również kod. Nie zdarza się to często, ale kilka krytycznych funkcji jest przechowywanych w postaci zaszyfrowanej cały czas, poza chwilą ich wywołania. Dość interesujące podejście, trzeba przyznać.

Zostawiając temat obfuskacji za sobą, co więcej możemy powiedzieć o Nymaimie:

Statyczna konfiguracja

Po potraktowaniu deobfuskatorem kod jest znacznie prostszy do analizy i możemy zająć się bardziej interesującymi rzeczami. Po pierwsze, chcielibyśmy wyciągnąć statyczną konfigurację z próbek, szczególnie rzeczy takie jak:

    • adresy serwerów C&C
    • hashe DGA
    • klucze używane do szyfrowania
    • wersję malware
    • inne rzeczy potrzebne do nawiązania komunikacji

Okazuje się to być trudniejsze niż się wydaje – ponieważ te informacje nie są przechowywane podobnie jak zwykłe stałe, a wykorzystywany jest inny mechanizm. Na szczęście, tym razem algorytm szyfrowania jest prosty:

Musimy tylko wywołać funkcję nymaim_config_crypt na początku zaszyfrowanej statycznej konfiguracji.

Pozostaje ostatnia rzecz – jak znaleźć miejsce gdzie zaczynają się szukane dane? Spróbowaliśmy kilku metod (dopasowywanie kodu, charakterystyczne miejsca itp.), ale nie były wystarczająco niezawodne jak na nasze potrzeby. Dlatego użyliśmy najprostszego możliwego rozwiązania – próbujemy deszyfrować zaczynając od każdego możliwego miejsca w pamięci. Dodając do tego kilka trywialnych heurystyk (przewidywanie wielkości i zawartości wyniku), jest to dość szybkie rozwiązanie (poniżej 1s na typowym programie) i działa zawsze.

Co znajduje się w zdeszyfrowanej konfiguracji? Wynikowa struktura jest dość prosta w parsowaniu. Składa się z wielu następujących po sobie kawałków danych, z których każdy ma swój typ, długość i dane (poza brakiem paddingu jest to format zgodny z RIFF (Resource Interchange File Format), chociaż raczej nie był to cel twórców):

Graficznie wygląda to mniej więcej tak:

Kawałki te są położone jeden po drugim:

Więc możemy gwałtownie przejść przez nie wszystkie w kilku linijkach języka Python:

Fragment funkcji process_chunk (gdzie hash to typ)

Po wstępnym parsowaniu wynik wygląda tak:

(Swoją drogą, w tym artykule typy kawałków – chunków są reprezentowane w kolejności bajtów, czyli big endian)

W bardziej czytelnej dla człowieka postaci po interpretacji ciekawych fragmentów:

Przebieg infekcji

Jest więcej niż jeden gatunek Nymaimów. W tym momencie rozróżniamy trzy rodzaje:

    • dropper – pierwszy Nymaim, który jest wykonywany w systemie. To jedyny typ rozprowadzany bezpośrednio do ofiar (na przykład przez złośliwe załączniki).
    • payload – moduł odpowiedzialny za większość operacji – na przykład web injecty. Pobierany przez payload.
    • bot_peer – moduł odpowiedzialny za komunikację P2P. Próbuje też zostać supernodem w botnecie.

To wszysto jedna rodzina malware – wszystkie współdzielą ten sam kod, ten sam obfuskator, ten sam format konfiguracji, ten sam protokół sieciowy i te same metody szyfrowania. Różnią się tylko kilkoma specjalizowanymi funkcjami.

Rola droppera jest prosta. Robi najpierw kilka testów, na przykład:

    • Upewnia się, iż nie jest wirtualizowany/inkubowany
    • Porównuje obecną datę z „terminem ważności” zapisanym w konfiguracji
    • Sprawdza czy DNS działa poprawnie, odpytując serwery DNS o adresy microsoft.com i google.com

Jeśli coś jest nie tak, dropper zamyka się i proces infekcji komputera nie zachodzi.Szczególnie drugi test przeszkadza w analizie, ponieważ żeby zainfekować komputer trzeba mieć świeżą próbkę Nymaima – starsze programy nie zadziałają. choćby jeżeli dokona się patchowania tego sprawdzania w programie, jest to walidowane również po stronie serwera i payload nie będzie pobrany.

Żeby pobrać więcej danych od Nymaima, musimy znać adres IP peera albo C&C. Konfiguracja statyczna zawiera między innymi dwie przydatne tu informacje:

    • IP servera DNS (zawsze jest to 8.8.8.8 i 8.8.4.4).
    • Nazwa domeny serwera C&C (na przykład ejdqzkd.com albo sjzmvclevg.com).

Nymaim odpytuje o te domeny, ale zwrócone odpowiedzi nie są prawdziwymi adresami serwera C&C – są używane w kolejnym algorytmie żeby wyciągnąć faktyczne adresy IP. Nie będziemy reprodukować tutaj kodu, ale istnieje bardzo dobry artykuł na ten temat, opublikowany przez Talos. Kod samego DGA można znaleźć tutaj:

https://github.com/vrtadmin/goznym/blob/master/DGA_release.py

Kiedy dropper zdobywa adres serwera C&C, rozpoczyna prawdziwą komunikację. Pobiera kilka dodatkowych programów:

    • Payload – moduł odpowiedzialny za injecty. Łączy się z botnetem P2P, ale tylko pasywnie.
    • Opcjonalny bot (próbuje otwierać porty na routerze i stać się aktywnym elementem botnetu. jeżeli się to nie uda, usuwa się z systemu).
    • Kilka dodatkowych binarek (służących np. do wykradania haseł).

DGA

Payload bardzo różni się od droppera w kwestii komunikacji sieciowej:

    • Nie ma zapisanych na stałe domen
    • Ale ma zaimplementowane DGA
    • Oraz komunikację P2P

Algorytm DGA użyty tutaj jest prosty – znaki są generowane jeden po drugim, przy wykorzystaniu prostej pseudo-losowej funkcji (wariacji xorshifta). Początkowy stan DGA zależy wyłącznie od ziarna (ang. seed) przechowywanego w statycznej konfiguracji, więc możemy łatwo przewidzieć wartości DGA dla dowolnej próbki. Dodatkowo badacze z Talos wykonali przeszukiwanie metodą bruteforce prawidłowych seedów, upraszczając generowanie poprawnych domen jeszcze bardziej.

P2P

Po pierwsze, dlaczego podejrzewamy iż Nymaim faktycznie korzysta z komunikacji P2P?

Zauważyliśmy iż jedna z analizowanych binarek zachowuje się zauważalnie inaczej niż inne. Odpakowaliśmy ją wtedy i rozpoczęliśmy analizę. gwałtownie znaleźliśmy wiele rzeczy, które wyraźnie sugerowały komunikację P2P. Na przykład zaszyfrowane napisy, które typowo odpowiadają za dodawanie wyjątków do firewalla Windows:

Inne podejrzane zachowanie to otwieranie portów na routerze przy wykorzystaniu UPNP. W ten sposób pozwala zainfekowanym urządzeniom na całym świecie połączyć się do siebie.

Ostatecznie coś jeszcze bardziej wyróżniającego się. Zaobserwowaliśmy już wcześniej, iż malware prezentuje się jako nginx w nagłówku Server. Skąd pochodzi ten nagłówek? Okazuje się, iż prosto z samego Nymaima:

Zaimplementowaliśmy tracker botnetu, o którym napiszemy więcej za chwilę. Z informacji, które wyciągnęliśmy wynika, iż Nymaim to jeden botnet, ale z geolokalizowanymi injectami. Na przykład injecty pobrane z Polski i z USA różnią się, ale możemy stwierdzić iż napisane są przez tych samych aktorów. Rozkład adresów IP na świecie jest bardzo podobny do tego co ustalili inni badacze (poza tym iż znaleźliśmy więcej węzłów w Polsce, a mniej w USA niż inni, ale to prawdopodobnie dlatego iż nasze badania koncentrowały się na naszym obszarze działania).

49.9% (~7.5k) dostępnych publicznie węzłów, które znaleźliśmy znalazło się w Polsce, 30% (~4.5k) w Niemczech, a 15.7% (~2.2k) w USA.

Protokół sieciowy

Kolejną rzeczą do opisania jest protokół wykorzystywany przez Nymaim do komunikacji. To przykład typowego żądania sieciowego:

    • Wartośc nagłówka Host jest brana ze statycznej konfiguracji
    • Nazwa zmiennej POST i ścieżka w URL są randomizowane i nie mają znaczenia
    • Wartośc zmiennej POST to zaszyfrowane zapytanie (zakodowany dodatkowo dzięki base64)
    • User-Agent i reszta nagłówków jest generowana przez WinHTTP (więc nagłówki nie są zbyt unikalne i nie da się rozpoznawać łatwo Nymaima dzięki płytkiej analizy ruchu sieciowego).

A to typowa odpowiedź:

    • Tak naprawdę to oczywiście nie nginx, tylko zapisane na stałe nagłówki
    • Wszystko poza sekcją danych jest zapisane w programie
    • Dane to zaszyfrowany request

Zaszyfrowana wiadomość ma bardzo specyficzny format: Dolna połowa pierwszego bajtu jest równa długości soli, a dolna połowa drugiego bajtu jest równa długości paddingu. Wszystko pomiędzy solą a paddingiem to zaszyfrowana dzięki algorytmu RC4 wiadomość. Kluczem jest klucz sklejony z solą.

Po przeprowadzeniu analizy tego algorytmu możemy łatwo go odwrócić:

Po odszyfrowaniu wiadomości ponownie dostajemy format bardzo podobny do statycznej konfiguracji (tzn.sekwencja następujących po sobie kawałków danych – chunki):

Każdy kawałek ma swój typ, długość i adekwatne dane:

Możemy przetworzyć zaszyfrowaną wiadomość używając praktycznie takiego samego kodu jak ten do statycznej konfiguracji:

I to adekwatnie cały kod potrzebny do parsowania wiadomości. Każdy typ chunka ma inną zawartość i musi być przetwarzany w trochę inny sposób.

Co ciekawe, wiadomości trzeba parsować rekurencyjnie, ponieważ niektóre typy chunków zawierają kolejne, zagnieżdżone listy, które mogą z kolei zawierać kolejne listy itd. Niestety, żeby dostać się do najciekawszych danych, musimy pokonać jeszcze jedną warstwę – szyfrowanie i kompresję. Niektóre typy chunków są zaszyfrowane dzięki kolejnego mechanizmu. Takie chunki na końcu danych mają specjalny nagłówek, podpisany dzięki RSA. Po zdeszyfrowaniu (technicznie: po zdjęciu cyfrowego podpisu, bo robimy to kluczem publicznym) znajdujemy tam md5 i długość zaszyfrowanych danych – i przede wszystkim klucz użyty do zaszyfrowania adekwatnych danych:

Po zdeszyfrowaniu (za pomocą algorytmu Serpent), trafiamy na kolejną warstwę – dane są skompresowane przy pomocy APLIB32. Ta struktura jest bardzo podobna do tej używanej przez ISFB – najpierw mamy magiczny dword ‚ARCH’, później długość skompresowanych danych, dalej długość skompresowanych danych, a na końcu CRC32.

Ponownie z pomocą funkcji w języku Python możemy gwałtownie poradzić sobie z tym problemem:

Korzystając z tej funkcji, nareszcie udało nam się zdeszyfrować wszystkie interesujące rzeczy przekazywane z serwera, w szczególności dodatkowe binarki, filtry sieciowe, oraz webinjecty.

Komunikacja

Przykładowe żądanie, po parsowaniu, może wyglądać tak:

Jak widać, przekazywane jest wiele danych identyfikujących komputer i trochę informacji o aktualnym stanie. Odpowiedzi potrafią być bardzo długie, ale dla uproszczenia przedstawmy jedną z prostszych opcji:

Zainfekowana maszyna poznaje swój publiczny adres IP, porty i adresy IP swoich peerów oraz aktywną domenę C&C. Dodatkowo dostaje polecenie spania kilkadziesiąt sekund (minimalnie 90 sekund podczas gdy rozsyłane są aktualizacje, 280 sekund kiedy nic się nie dzieje).

Lista typów chunków które rozumiemy i jesteśmy w stanie parsować:

typ krótki opis
ffd5e56e fingerprint 1
014e2be0 fingerprint 2 + aktualny czas
f77006f9 fingerprint 3
22451ed7 CRC32 ostatnio otrzymanych chunków be8ec514 i 0282aa05
b873dfe0 flaga mówiąca czy serwer jest aktywny
0c526e8b zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
875c2fbf niezaszyfrowany program wykonywalny
08750ec5 zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
1f5e1840 web injecty (należy zdeszyfrować Serpentem, odpakować APLIB32, sparsować format injectów ISFB)
76daea91 handshake, którego używa dropper podczas nawiązywania połączenia
be8ec514 lista adresów IP peerów
138bee04 lista adresów IP peerów
1a701ad9 zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
30f01ee5 zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
3bbc6128 zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
39bc61ae zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
261dc56c zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
a01fc56c zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
76fbf55a padding
cae9ea25 zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
0282aa05 zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
d2bf6f4a informacje o stanie bota
41f2e735 filtry sieciowe
1ec0a948 filtry sieciowe
18c0a95e filtry sieciowe
3d717c2e filtry sieciowe
8de8f7e6 termin ważności? (nie jesteśmy pewni zastosowania, zawsze jest kilka dni naprzód od aktualnej)
3e5a221c lista dodatkowych binarek które zostały pobrane
5babb165 handshake używany przez payload podczas nawiązywania połączenia
b84216c7 publiczne IP zainfekowanej maszyny
cb0e30c4 liczba sekund, które ma spać zainfekowana maszyna
f31cc18f CRC32 dodatkowych zainfekowanych binarek
920f2f0c web injecty (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, sparsować format injectów ISFB)
930f2f0c web injecty (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, sparsować format injectów ISFB)

To już wydaje się być dużo, a i tak pomijaliśmy większość chunków których choćby nie próbowaliśmy analizować (np. zignorowaliśmy większość czterobajtowych bloków, albo tych które zawsze były równe zero).

Po ekstrakcji wszystkiego z komunikacji, możemy w końcu popatrzeć na injecty. Na przykład polskie:

(304 różne injecty, na dzień pisania tego artykułu)

Albo te z USA:

(393 różne injecty, na dzień pisania artykułu)

Inne zasoby

Regułki Yara:

Hashe (md5):

    • Payload 2016-10-20, 9d6cb537d65240bbe417815243e56461, wersja 90032
    • Dropper 2016-10-20, a395c8475ad51459aeaf01166e333179, wersja 80018
    • Payload 2016-10-05, 744d184bf8ea92270f77c6b2eea28896, wersja 90019
    • Payload 2016-10-04, 6b31500ddd7a55a8882ebac03d731a3e, wersja 90012
    • Dropper 2016-04-12, cb3d058a78196e5c80a8ec83a73c2a79, wersja 80017
    • Dropper 2016-04-09, 8a9ae9f4c96c2409137cc361fc5740e9, wersja 80016

Repozytorium z naszymi narzędziami: nymaim-tools

Materiały dodatkowe:

Idź do oryginalnego materiału