Analiza bota Smoke Loader

cert.pl 6 lat temu

Smoke Loader (znany także jako Dofoil) jest względnie małym, modularnym botem używanym do instalowania różnych rodzin złośliwego oprogramowania.

Mimo iż został zaprojektowany głównie z myślą o pobieraniu oprogramowania, to posiada parę funkcji, które czynią go bardziej trojanem niż zwykłym dropperem.

Nie jest nowym zagrożeniem, ale przez cały czas jest rozwijany i aktywny. W ostatnich miesiącach zaobserwowaliśmy jego udział w kampaniach malspamowych i RigEK.

W artykule pokażemy jak Smoke Loader rozpakowuje się i jak wygląda jego komunikacja z serwerem C2.

Smoke Loader pierwszy raz ujrzał światło dzienne w czerwcu 2011, kiedy to użytkownik SmokeLdr umieścił reklamę swojego produktu na forach grabberz.com1 oraz xaker.name2.


Post reklamujący Smoke Loader na grabberz.com

Jedną z ciekawych rzeczy jest fakt, iż oprogramowanie jest sprzedawane wyłącznie osobom posługującym się językiem rosyjskim3.

Ponieważ wszystkie jego możliwości zostały opisane w postach na wspomnianym forum, nie będziemy ich tutaj rozważać.

Próbka, którą będziemy analizować to d32834d4b087ead2e7a2817db67ba8ca.


Kolejne etapy rozpakowywania się próbki

Spis treści

    • Warstwa I
    • Warstwa II
      • Sprawdzanie obecności debuggera
      • Niepotrzebny kod
      • Importy zaszyfrowane RC4
      • Odpakowywanie
    • Warstwa III
      • Obfuskacja skokami
        • Deobfuskacja
          • Próba I
          • Próba II
      • Sprawdzanie obecności debuggera
      • Sprawdzanie wirtualizacji
      • Szyfrowanie kodu
      • Sztuczki w języku assembly
        • Sztuczka I
        • Sztuczka II
        • Sztuczka III
      • Własne importy
      • Odpakowywanie
    • Warstwa IV (końcowa)
      • Szyfrowanie stringów
      • Urle C2
      • Sturktura pakietów
      • Działanie programu
    • Ogólne IOC
    • Zebrane IOC
    • Odniesienia

Warstwa I

Pierwszą rzeczą, którą napotykamy jest kompresja narzędziem PECompact2 albo UPX.

Oba jednak możemy dosyć prosto dekompresować używając publicznie dostępnych narzędzi:


Użycie PECompact

Użycie upx

Warstwa II


Funkcja startowa, która odpowiada za kontrolowanie metody sprawdzającej obecność debuggera, zawiera również parę niepotrzebnych odwołań do API w celu obfuskacji

Sprawdzanie obecności debuggera

Struktura PEB jest sprawdzana pod kątem obecności debuggera:

Niepotrzebny kod

Prawie każda funkcja ma wstrzyknięte nic nie wnoszące instrukcje, które utrudniają analizę.


Kawałek funkcji szyfrowania RC4, która zawiera sporo bezużytecznego kodu.

Importy zaszyfrowane RC4

W tej warstwie prawie wszystkie importy oraz nazwy bibliotek są deszyfrowane dzięki RC4 zanim zostaną przekazane do LoadLibraryA, a potem GetProcAddress.

Importy najpierw są odkładane na stos:

Potem deszyfrowane dzięki RC4 z zapisanym kluczem:

Następnie nazwa biblioteki jest podawana do LoadLibrary, a potem nazwa funkcji wraz z uzyskanym uchwytem przekazywane są do GetProcAddress:

Tablica z importami jest w ten sposób wypełniania i używana w dalszej części programu.

Odpakowywanie

Tworzony jest nowy proces i dwa razy wywołana jest funkcja WriteProcessMemory:

Zapisy do pamięci są dosyć charakterystyczne i łatwo widoczne w raporcie Cuckoo

Jedno z wywołań zapisuje nagłówek MZ, a drugi resztę pliku binarnego. jeżeli połączymy oba, to dostaniemy plik będący następną warstwą.

Warstwa III

Zaraz po załadowaniu pliku widzimy:



Kod w adresie startowym

To co obserwujemy jest rezultatem paru obfuskacji i sztuczek. Zaprezentujemy każdą z nich i sprawdzimy jak działa.

Obfuskacja skokami

Prawie wszystkie początkowe funkcje wykorzystują obfuskację skokami.

Zamiast ułożenia instrukcji w normalny, liniowy sposób, są one pomieszane z sobą nawzajem i połączone instrukcjami skokowymi.


Przykład obfuskacji skokami

Jeśli napisalibyśmy skrypt, który podąża za wykonaniem programu i przedstawia wynik w postaci grafu, to dostalibyśmy coś podobnego do:


Częściowo zdeobfuskowana funkcja startu

Prawie od razu możemy zauważyć, iż większość instrukcji jest używana tylko po to, żeby utrudnić analizę.

Deobfuskacja

Próba I

Spróbowaliśmy napisać skrypt, który przegląda wszystkie bloki instrukcji w danej funkcji i próbuje je łączyć w jeden ciąg. Robi to tylko wtedy jeżeli dwa bloki połączone są ze sobą dzięki skoku w liczności 1:1 (skok z jednego możliwego miejsca do jednego możliwego miejsca).

Autor obfuskacji prawdopodobnie wziął to pod uwagę i zaimplementował skoki jmp dzięki sąsiadujących instrukcji jnz i jz. To jednak nie skomplikowało naszego rozwiązania za bardzo.

Prosty skrypt implementujący nasze rozwiązanie

Jeśli teraz uruchomimy go na funkcji startowej i pozbędziemy się wszystkich instrukcji skoku dostaniemy:

Kod wygląda teraz o wiele lepiej, możemy jednak ulepszyć nasze rozwiązanie korzystając z mocy programu IDA.

Próba II

Tak naprawdę jedyna rzecz, która powstrzymuje IDA przed rozpoznaniem obfuskowanych bloków instrukcji jako poprawnych funkcji są występujące po sobie skoki warunkowe.

Podczas gdy instrukcje jmp są oznaczane jako koniec kodu bloku, dwie sąsiadujące instrukcje jz/jnz nie są. Dlatego muszą one zostać spatchowane na instrukcję jmp:


Nowo utworzona przerywana linia wskazuje na koniec instrukcji w danym bloku

Ta mała zmiana pozwala IDA na rozpoznanie funkcji i choćby próbę dekompilacji:

Zdekompilowana funkcja startu po spatchowaniu instrukcji jn/jnz

Pomimo tego, iż dekompilacja nie jest w 100% poprawna, daje nam dobry obraz tego, co dana funkcja robi.

Dla przykładu, powyższa funkcja wczytuje strukturę PEB i sprawdza wartości pól OSMajorVersion i BeingDebugged.

Sprawdzanie obecności debuggera

W tej warstwie zaoberwowaliśmy dwa takie zjawiska, znajdują się one na samym początku działania programu. Są identyczne do tych z poprzedniej warstwy, jednak różnią się lekko w wykonaniu.

Wartości sprawdzanych pól są użyte do wyliczenia adresu następnych funkcji:

Czytanie pola BeingDebugged ze struktury PEB

Czytanie pola NtGlobalFlag ze struktury PEB

Jeśli jedno z pól BeingDebugged lub NtGlobalFlag nie jest zerem, to program skacze w losowe miejsce w pamięci, co skutkuje gwałtownym zakończeniem procesu.

Sprawdzanie wirtualizacji

Binarium próbuje uzyskać uchwyt do biblioteki „sbiedll”, która jest używana przez Sandboxie do sandboxowania procesów. jeżeli operacja się powiedzie, a co za tym, idzie Sandboxie jest zainstalowane na systemie, to program kończy działanie.

Wartość w rejestrze System\CurrentControlSet\Services\Disk\Enum jest czytana, i jeżeli którakolwiek z poniższych wartości występuje w kluczu, to program również kończy działanie.

    • qemu
    • virtio
    • vmware
    • vbox
    • xen

Szyfrowanie kodu

Znaczna większość kodu funkcji jest zaszyfrowana:

Funkcja z zaszyfrowaną częścią kodu

Po deobfuskacji funkcji szyfrującej, ta okazuje się dosyć prosta:

Zdekompilowana funkcja szyfrująca

Funkcja pobiera adres oraz liczbę bajtów w rejestrach eax oraz ecx i xoruje wszystkie bajty w danym przedziale ze stałą wartością.

Co ciekawe, w danej chwili program próbuje utrzymywać jak najmniej deszyfrowanego kodu:

Przykład utrzymywania kodu zaszyfrowanego

Możemy zdeszyfrować cały tak zaszyfrowany kod dzięki krótkiego skryptu wykorzystującego IDA API:

Sztuczki w języku assembly

Ta warstwa zawiera parę ciekawych sztuczek w języku assembly.

Sztuczka I



    • call loc_4024A7 umieszcza adres następnej instrukcji (w tym przypadku adres stringa „kernel32”) na stosie i skacze ponad dane do dalszych instrukcji
    • pop esi ładuje adres do rejestru esi
    • cmp byte ptr [esi], 0 wskaźnik może być teraz użyty jak normalny string

Sztuczka II



Zamiast wykonania jmp eax, program najpierw umieszcza rejestr eax na stosie, a następnie wykonuje instrukcję retn, która zbiera adres ze stosu i do niego skacze.

Sztuczka III



call $+5 skacze do następnej instrukcji (ponieważ instrukcja call $+5 ma 5 bajtów), ale ponieważ jest to instrukcja call, to dodatkowo umieszcza aktualny adres na górze stosu.

W tym wypadku sztuczka ta jest użyta do wyliczenia adresu bazowego programu (0x004023AA0x23AA).

Własne importy

Ta warstwa tworzy własną tablicę importów dzięki hashy djb2.

Najpierw iteruje po zapisanych nazwach bilbiotek, ładuje każdą z nich i zapisuje uchwyt:



Następnie iteruje po odpowiadających tablicach zawierających hashe nazw funkcji. jeżeli hash zostanie dopasowany, to odczytuje adres funkcji z biblioteki i umieszcza ją w tablicy importów trzymanej na stosie.


Hashe nazw funkcji do zaimportowania


Skonstruowana tablica z adresami funkcji

Odpakowywanie

Program ostatecznie wywołuje RtlDecompressBuffer z parametrem COMPRESSION_FORMAT_LZNT1, aby zdekompresować bufor i wstrzyknąć go dzięki techniki PROPagate injection4.

Warstwa IV (końcowa)

Szyfrowanie stringów

Wszystkie stringi są zaszyfrowane dzięki RC4 oraz zapisanego klucza:

Funkcja odpowiedzialna za zwracanie zdeszyfrowanego stringa dla pobranego indeksu


Struktura zaszyfrowanych stringów

W tej próbce zaszyfrowane stringi to:

Adresy serwerów C2

Adresy serwerów C2 są zapisane w postaci zaszyfrowanej w sekcji z danymi:


Część sekcji .data zawierająca adresy C2

Strukturę zaszyfrowanego adresu można przedstawić za pomocą:

Adresy szyfrowane są za operacji xor wykorzystując klucz utworzony ze zmiennej:

Zdekompilowana funkcja odpowiedzialna za deszyfrowanie adresów C2

Możemy ją zapisać w Pythonie jako:

Przykład deszyfrowania

Struktura pakietów

Zdekompilowana funkcja odpowiedzialna za pakowanie i wysyłanie pakietów z komendami

Strukturę pakietów możemy zaprezentować jako następującą strukturę w języku C:

Szyfrowanie pakietów odbywa się również dzięki RC4. Warto jednak zauważyć, iż inny klucz jest użyty do deszyfrowania komunikacji wychodzącej i przychodzącej:


Część funkcji szyfrującej pakiety przed wysłaniem ich do serwera C2


Część funkcji deszyfrującej pakiety przed sparsowaniem ich

Działanie programu

    • Program zaczyna od pozyskania User Agenta dla aktualnej wersji IE poprzez zapytanie rejestru Software\Microsoft\Internet Explorer i wartości svcVersion oraz Version
    • Następnie próbuje do skutku połączyć się z http://www.msftncsi.com/ncsi.txt, tym sposobem upewnia się, iż system ma dostęp do internetu.
    • Ostateczine, Smoke Loader nawiązuje komunikację z serwerem C2 wysyłając pakiet z komendą 10001. W odpowiedzi otrzymuje listę pluginów do zainstalowania oraz liczbę zadań do pobrania.
    • Program iteruje po zadaniach i próbuje pobrać każde z nich przy pomocy pakietu 10002 z numerem zadania jako argument.
    • Pliki często nie są hostowane bezpośrednio na serwerze C2 tylko na innym hoście, w takim wypadku serwer zwraca poprawny adres URL w nagłówku HTTP Location.
    • Po wykonaniu zadania, wysyłany jest pakiet z komendą 10003 z argumentem arg_1 oznaczającym numer zadania oraz arg_2 mówiącym o sukcesie zadania.


Komunikacja między botem a serwerem C2

Ogólne IOC

    • Program kopiuje się do %APPDATA%\Microsoft\Windows\[a-z]{8}\[a-z]{8}.exe
    • Program tworzy skrót do samego siebie w %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\[a-z]{8}.lnk
    • Czyta wartość rejestru System\CurrentControlSet\Services\Disk\Enum\0
    • Zapytania GET do http://www.msftncsi.com/ncsi.txt
    • Zapytania POST z odpowiedzią HTTP 404 i danymi

Przykładowe zapytanie i odpowiedź do serwera C2:



Yara:

Zebrane IOC

Konfiguracje statyczne:

Hashe:

Odniesienia

1https://grabberz.com/showthread.php?t=29680

2https://web.archive.org/web/20160419010008/http://xaker.name/threads/22008/

3http://stopmalvertising.com/rootkits/analysis-of-smoke-loader.html

4http://www.hexacorn.com/blog/2017/10/26/propagate-a-new-code-injection-trick/

https://blog.malwarebytes.com/threat-analysis/2016/08/smoke-loader-downloader-with-a-smokescreen-still-alive/

Idź do oryginalnego materiału