Analiza bota Smoke Loader

Data publikacji: 18/07/2018, Michał Praszmo

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 że 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 nadal 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.


forum.png

Post reklamujący Smoke Loader na grabberz.com

Jedną z ciekawych rzeczy jest fakt, że 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.


layers.png

Kolejne etapy rozpakowywania się próbki

Spis treści

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:


pecompact.png

Użycie PECompact

Użycie upx

Warstwa II


function_first.png

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ę.


trash.png

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 za pomocą RC4 zanim zostaną przekazane do LoadLibraryA, a potem GetProcAddress.

Importy najpierw są odkładane na stos:

Potem deszyfrowane za pomocą 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śli połączymy oba, to dostaniemy plik będący następną warstwą.

Warstwa III

Zaraz po załadowaniu pliku widzimy:


woops.png

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.


arrows.png

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:


jumps.png

Częściowo zdeobfuskowana funkcja startu

Prawie od razu możemy zauważyć, że 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śli dwa bloki połączone są ze sobą za pomocą 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 za pomocą 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:


patched_jump.png

Nowo utworzona przerywana linia wskazuje na koniec instrukcji w danym bloku

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

Zdekompilowana funkcja startu po spatchowaniu instrukcji jn/jnz

Pomimo tego, że 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śli 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śli 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 za pomocą krótkiego skryptu wykorzystującego IDA API:

Sztuczki w języku assembly

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

Sztuczka I


string_call.png

    • 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


jump_return.png

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_next.png

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 za pomocą hashy djb2.

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


load_libraries.png

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


home_imports.png

Hashe nazw funkcji do zaimportowania


api_table.png

Skonstruowana tablica z adresami funkcji

Odpakowywanie

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

Warstwa IV (końcowa)

Szyfrowanie stringów

Wszystkie stringi są zaszyfrowane za pomocą RC4 oraz zapisanego klucza:

Funkcja odpowiedzialna za zwracanie zdeszyfrowanego stringa dla pobranego indeksu


string_packet.png

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:


cncs.png

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

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

c2_packet.png

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ż za pomocą RC4. Warto jednak zauważyć, że inny klucz jest użyty do deszyfrowania komunikacji wychodzącej i przychodzącej:


encrypt_packet.png

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


decrypt_packet.png

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ę, że 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.


communnication.png

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:


packet_sample.png

Yara:

Zebrane IOC

Konfiguracje statyczne:

Hashe:

Odniesienia

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

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

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

4 http://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/