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
- 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 (0x004023AA – 0x23AA).
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/