Tofsee, znany również pod nazwą Gheg, to kolejny analizowany przez nas botnet. Jego głównym celem jest rozsyłanie spamu, jednak może on wykonywać także inne zadania. Jest to możliwe dzięki modularnej budowie malware’u – składa się on z głównego pliku wykonywalnego (tego, którym infekuje się użytkownik), który później pobiera z serwera C2 kilkanaście dodatkowych bibliotek DLL rozszerzających działanie kodu poprzez nadpisywanie niektórych wywoływanych funkcji swoimi własnymi. Przykładem takiej DLL-ki może być moduł do rozprzestrzeniania się poprzez postowanie wiadomości na Facebooku i VKontakte (rosyjskim portalu społecznościowym).
Komunikacja bota z botmasterem odbywa się przy użyciu niestandardowego protokołu opartego na TCP. Pierwszą wiadomość zawsze wysyła serwer tuż po nawiązaniu połączenia – zawiera ona przede wszystkim jednorazowy 128-bajtowy klucz użyty do szyfrowania dalszych komunikatów. Nie da się zatem odkodować komunikacji, jeśli się jej nie słucha od początku.
W każdym momencie bot trzyma w pamięci listę zasobów. Początkowo jest ona niemal pusta i zawiera jedynie podstawowe informacje, jak identyfikator bota, ale szybko jest uzupełniana przez dane otrzymane od serwera w kolejnych wiadomościach. Zasoby przyjmują różne formy – może to być np. lista tematów do wykorzystania w mailu, ale i także biblioteki DLL rozbudowujące funkcje bota. Dodatkowo, jeden z zasobów – work_srv – zawiera listę adresów IP serwerów C2. Jest to jedna z pierwszych wiadomości wysyłanych przez serwer i, co ciekawe, może nie zawierać samego siebie (wówczas połączenie jest kończone i wybierany jest losowy serwer z listy). Tak też się zazwyczaj dzieje podczas połączenia z C2 zapisanym na stałe w próbce – pełni on zatem rolę „wskaźnika” na rzeczywiste serwery.
Wysyłane emaile są tworzone w sposób losowy – Tofsee wykorzystuje do tego celu specjalny język skryptowy – przykładowy plik znajduje się niżej, w analizie technicznej. Zawiera on wydzielone pola, które zostaną losowo zastąpione pewnymi ciągami znaków – np. %RND_SMILE zostanie zmienione na jedną z kilkunastu emotikon. Dzięki temu prostsze filtry spamowe mogą je przepuścić.
Analiza techniczna
Lista adresów IP serwerów C2 jest zapisana w samym pliku w postaci zaszyfrowanej. Algorytm szyfrowania jest bardzo prosty – opiera się na XOR-owaniu ze stałym kluczem.
Odszyfrowane dane to trzy stałe IP wraz z portem, który w analizowanej próbce był równy 443 dla wszystkich odkodowanych adresów. Prawdopodobnie więc bot próbuje uniknąć wykrycia poprzez używanie portu przeznaczonego dla ruchu SSL/TLS.
Protokół komunikacji
Po nawiązaniu połączenia TCP, pierwsza wiadomość wychodzi od serwera. Ma ona zawsze 200 bajtów długości. Wygląda jednak na to, że końcowe bajty są niewykorzystane i zarezerwowane – być może w celach późniejszej rozbudowy protokołu. Także w tym miejscu wykorzystana jest prosta obfuskacja zawartości poprzez operacje bitowe:
def greetingXor(data): | |
dec="" | |
res=198 | |
for c in data: | |
dec+=chr((res^(32*ord(c)|(ord(c)>>3)))&0xFF) | |
res=ord(c)^0xc6 | |
return dec, res |
Odszyfrowane dane składają się na nastepującą strukturę (nie poznaliśmy znaczenia wszystkich pól):
struct greeting{ | |
uint8_t key[128]; | |
uint8_t unk1[16]; | |
uint32_t bot_IP; | |
uint32_t srv_time; | |
uint8_t unk2[48]; | |
}; |
Od tego momentu, cała komunikacja (zarówno wiadomości przychodzące, jak i wychodzące) jest szyfrowana 128-bajtowym kluczem z pierwszej wiadomości. Klucz ten ulega modyfikacji przy każdym wysłanym/odebranym bajcie, zatem nie da się odszyfrować komunikacji, jeśli się jej nie słucha od początku. XOR-owanie jest użyte w taki sposób, że jedna funkcja służy zarówno do szyfrowania, jak i deszyfrowania:
def xorStream(data, key, main_key, it): | |
res="" | |
for c in data: | |
key[it%7]+=main_key[it%128] | |
key[it%7]&=0xFF | |
res+=chr(ord(c)^(key[it%7])) | |
it+=1 | |
return res |
Parametry:
- data – surowe dane
- key – krótki, 7-bajtowy klucz, inicjalizowany przed pierwszą wiadomością bajtami „abcdefg”
- main_key – 128-bajtowy klucz z powitania
- it – liczba już odebranych/wysłanych bajtów
Wszystkie wiadomości (poza powitalną) składają się z nagłówka oraz właściwych danych. Nagłówek zawiera następujące pola:
struct header{ | |
uint32_t size; | |
uint32_t size_decompressed; | |
uint32_t CRC32; | |
uint8_t flags; // flags&2!=0 -> compressed | |
uint32_t op; | |
uint32_t subop1; | |
uint32_t subop2; | |
}; |
Protokół wspiera kompresję danych, ale jest ona używana tylko dla większych wiadomości. Pola op, subop1 i subop2 to pewne stałe, definiujące typ wiadomości. W analizowanej próbce widać kod obsługujący wiele różnych typów, ale w praktyce jedynie nieliczne z nich są wykorzystywane.
Payload jest wysyłany zaraz po nagłówku. Jego dokładna struktura zależy od typu wiadomości – kilka z nich przedstawię poniżej.
Pierwsza wiadomość wysyłana przez bota ma typy {1,0,0} (kolejno op, subop1, subop2) i składa się na dość pokaźną strukturę:
struct botdata{ | |
uint32_t flags_upd; | |
uint64_t botID; | |
uint32_t unk1; | |
uint32_t net_type; | |
uint32_t net_flags; | |
uint32_t vm_flags; | |
uint32_t unk2; | |
uint32_t unk3; | |
uint32_t lid_file_upd; | |
uint32_t ticks; | |
uint32_t tick_delta; | |
uint32_t born_date; | |
uint32_t IP; | |
uint32_t unk4; | |
uint32_t unk5; | |
uint8_t unk6; | |
uint8_t OS; | |
uint8_t unk[46]; | |
}; |
Niektóre z nazw pól (np. lid_file_upd) dostaliśmy „za darmo”, ponieważ bot zapisywał je pod takimi właśnie indeksami do wewnętrznej struktury danych mapującej nazwy zmiennych na ich zawartość. Znaczenia innych musieliśmy się domyślić sami.
Odpowiedź serwera może mieć różną formę. Najprostsza z nich następuje gdy op=0 – oznacza to pustą odpowiedź (lub koniec transmisji składającej się z wielu wiadomości). Jeśli op=2, to serwer wysyła nam nowy zasób – payload wiadomości jest wówczas taką strukturą:
struct resource{ | |
uint32_t type; // Small integer. | |
char name[16]; | |
uint32_t unk; | |
uint32_t length; | |
uint8_t contents[]; // Size=length. | |
}; |
Zazwyczaj tuż po połączeniu się z C2 wpisanym na sztywno do próbki pierwszą wiadomością po powitalnej, jaką bot otrzymuje, jest pojedynczy zasób o nazwie work_srv. Znajduje się w nim lista kilku adresów IP oraz portów (już różnych niż 443), na których nasłuchują właściwe serwery C2. Wówczas następuje rozłączenie z dotychczasowym serwerem i po chwili bot rozpoczyna komunikację od nowa z losowym z świeżo otrzymanych serwerów C2.
Jeśli op=1, wiadomość ma różne znaczenie w zależności od subop2 oraz, dodatkowo, pierwszych czterech bajtów payloadu (które najwyraźniej w tym wypadku pełnią funkcję flag). Przykładowo, jeśli spełnione są następujące warunki: op=1, subop2&1=0, flags=4, to jest to żądanie C2, aby bot wysłał mu wszystkie posiadane zasoby. Odpowiedzią bota jest wówczas skonkatenowana lista zasobów o postaci podobnej do wyżej pokazanej, po czym serwer wysyła dziesiątki wiadomości typu 2 (zawierające zasób) – zasoby, których bot jeszcze nie ma.
Zasoby
Każdy zasób jest identyfikowany typem – niewielką liczbą (nie większą niż 40, chociaż większość jest nawet mniejsza niż 10) oraz krótką nazwą, np. „priority”. Z analizowanych przez nas typów najciekawsze to:
Typ 5
Zawiera pluginy w postaci bibliotek DLL. Ponieważ pojedyncze symbole będące pozostałościami po kompilacji zostały w nich zostawione, mogliśmy się łatwo domyślić, jakie zadania mają poszczególne pluginy. W czasie analizy Tofsee pobierał następujące pluginy:
Nazwa zasobu – numer | Nazwa biblioteki | Hash MD5 DLL-ki |
---|---|---|
1 | ddosR.dll | fbc7eebe4a56114e55989e50d8d19b5b |
2 | antibot.dll | a3ba755086b75e1b654532d1d097c549 |
3 | snrpR.dll | 385b09563350897f8c941b47fb199dcb |
4 | proxyR.dll | 4a174e770958be3eb5cc2c4a164038af |
5 | webmR.dll | 78ee41b097d402849474291214391d34 |
6 | protect.dll | 624c5469ba44c7eda33a293638260544 |
7 | locsR.dll | 2d28c116ca0783046732edf4d4079c77 |
10 | hostR.dll | c90224a3f8b0ab83fafbac6708b9f834 |
11 | text.dll | 48ace17c96ae8b30509efcb83a1218b4 |
12 | smtp.dll | 761e654fb2f47a39b69340c1de181ce0 |
13 | blist.dll | e77c0f921ef3ff1c4ef83ea6383b51b9 |
14 | miner.dll | 47405b40ef8603f24b0e4e2b59b74a8c |
15 | img.dll | e0b0448dc095738ab8eaa89539b66e47 |
16 | spread1.dll | 227ec327fe7544f04ce07023ebe816d5 |
17 | spread2.dll | 90a7f97c02d5f15801f7449cdf35cd2d |
18 | sys.dll | 70dbbaba56a58775658d74cdddc56d05 |
19 | webb.dll | 8a3d2ae32b894624b090ff7a36da2db4 |
20 | p2pR.dll | e0061dce024cca457457d217c9905358 |
Sądząc po nazwach, Tofsee poza spamowaniem ma także inne funkcje, jak koordynowany DDoS, czy kopanie kryptowalut (jak się okazuje, jednym z pobieranych zasobów jest właśnie koparka Litecoinów).
Typ 11
Zawiera okresowo uaktualniane skrypty w nietypowym języku, których zadaniem jest rozsyłanie spamu. Przykładowy skrypt:
From: "%NAME" <%FROM_EMAIL> | |
To: %TO_EMAIL | |
Subject: %SUBJ | |
Date: %DATE | |
MIME-Version: 1.0 | |
Content-Type: multipart/mixed; | |
boundary="%BOUNDARY1" | |
--%BOUNDARY1 | |
Content-Type: multipart/alternative; | |
boundary="%BOUNDARY2" | |
--%BOUNDARY2 | |
Content-Type: text/plain; | |
charset="%CHARSET" | |
Content-Transfer-Encoding: quoted-printable | |
{qp1-}%GI_SLAWIK{/qp} | |
--%BOUNDARY2 | |
Content-Type: text/html; | |
charset="%CHARSET" | |
Content-Transfer-Encoding: quoted-printable | |
{qp0+}%GI_SLAWIK{/qp} | |
--%BOUNDARY2-- | |
--%BOUNDARY1 | |
Content-Type: application/zip; | |
name="%ATTNAME1.zip" | |
Content-Transfer-Encoding: base64 | |
Content-Disposition: attachment; | |
filename="%ATTNAME1.zip" | |
%JS_EXPLOIT | |
--%BOUNDARY1-- | |
- GmMxSend | |
v SRV alt__M(%RND_NUM[1-4])__.gmail-smtp-in.l.google.com | |
U L_SKIP_5 5 __M(%RND_NUM[1-5])__ | |
v SRV gmail-smtp-in.l.google.com | |
L L_SKIP_5 | |
C __v(SRV)__:25 | |
R | |
S mx_smtp_01.txt | |
o ^2 | |
m %FROM_DOMAIN __A(4|__M(%HOSTS)__)__ | |
W """EHLO __A(3|__M(%{mail}{smtp}%RND_NUM[1-4].%FROM_DOMAIN)__)__\r\n""" | |
R | |
S mx_smtp_02.txt | |
o ^2 ^3 | |
L L_NEXT_BODY | |
v MI 0 | |
- m %FROM_EMAIL __M(%FROM_USER)__@__M(%FROM_DOMAIN)__ | |
W """MAIL From:<__M(%FROM_EMAIL)__>\r\n""" | |
R | |
S mx_smtp_03.txt | |
I L_QUIT ^421 | |
o ^2 ^3 | |
L L_NEXT_EMAIL | |
U L_NO_MORE_EMAILS @ __S(TO|__v(MI)__)__ | |
W """RCPT To:<__l(__S(TO|__v(MI)__)__)__>\r\n""" | |
R | |
S mx_smtp_04.txt | |
I L_OTLUP ^550 | |
I L_TOO_MANY_RECIP ^452 | |
o ^2 ^3 | |
v MI __A(1|__v(MI)__,+,1)__ | |
u L_NEXT_EMAIL 1 __A(1|__v(MI)__,<,1)__ L L_NO_MORE_EMAILS u L_NOEMAILS 0 __A(1|__v(MI)__,>,0)__ | |
W """DATA\r\n""" | |
R | |
S mx_smtp_05.txt | |
o ^2 ^3 | |
m %SS1970H __P(__t(126230445)__|16)__ | |
m %TO_EMAIL """<__l(__S(TO|0)__)__>""" | |
m %TO_NAME __S(TONAME|0)__ | |
W """__S(BODY)__\r\n.\r\n""" | |
R | |
S mx_smtp_06.txt | |
I L_SPAM ^550 | |
o ^2 ^3 | |
+ m | |
H TO -1 OK | |
J L_NEXT_BODY | |
L L_OTLUP | |
+ h | |
h """Delivery to the following recipients failed. __l(__S(TO|__v(MI)__)__)__""" | |
H TO __v(MI)__ HARD | |
J L_NEXT_EMAIL | |
L L_TOO_MANY_RECIP | |
H TO __v(MI)__ FREE | |
J L_NO_MORE_EMAILS | |
L L_QUIT | |
W """QUIT\r\n""" | |
R | |
S mx_smtp_07.txt | |
o ^2 ^3 | |
L L_NOEMAILS | |
E 1 | |
L L_SPAM | |
+ A | |
H TO -1 FREE | |
o ^2 ^3 |
Język ma składnię delikatnie podobną do assemblera – na przykład „J” na początku linii oznacza „jump”, a „L” – zdefiniowanie etykiety (ang. label). Wewnątrz skryptu zawarte są także makra, które w trakcie wykonywania zamieniane są na inny tekst – przykładem może być %ATTNAME1.
Typ 7
Zawiera makra ogólnego przeznaczenia. Nazwa tych zasobów jest taka sama, jak makro, które opisują, np. %DATE_RAN_SUB (przypuszczalnie skrót od angielskiego „DATE RANDOM SUBJECT”). Zawartość zasobu to lista różnych podstawień oddzielonych znakami nowej linii. Przykładowa zawartość:
%NAME posted something on your wall | |
Newsletter from %NAME | |
%NAME changed her status | |
User %NAME is available to chat | |
%NAME answered your message | |
New message from %NAME | |
%NAME requested to be your friends | |
User %NAME sent you a message | |
%NAME likes your status | |
Do you know %NAME? | |
%NAME sent you invitation | |
New friendship request from %NAME | |
%NAME is your friend now |
Ponieważ niektóre zmienne potrzebują skorzystać dosłownie ze znaku nowej linii, wprowadzone są także specjalne zmienne, jak %SYS_N służące właśnie do tego celu.
Typ 8
Zawiera makra lokalne. Ponieważ różne skrypty mogą chcieć korzystać ze zmiennych o tych samych nazwach, ale innej zawartości, niektóre makra są lokalne. Nazwy zasobów mają postać NUM%VAR, np. 1819%TO_NAME, gdzie 1819 to numer porządkowy skryptu będącego zakresem stosowania makra %TO_NAME.
Podstawienia zmiennych są rekurencyjne, co zresztą widać na wyżej wymienionym przykładzie %DATE_RAN_SUB – makra potrafią zawierać w swoim rozwinięciu inne makra. Ponadto język wspiera bardziej skomplikowane konstrukcje w rodzaju %RND_DIGIT[3], oznaczające trzy losowe cyfry (co jest często używane podczas generowania losowego koloru w postaci szesnastkowej), a także %{%RND_DEXL}{ %RND_SMILE}{}, oznaczający wybór jednego z %RND_DEXL, %RND_SMILE i pustego tekstu. Widać więc, że język jest dość elastyczny.
Reszta typów zawiera zaledwie pojedyncze zasoby i dlatego pominę ich opis w tym artykule.
Na zakończenie, załączam hash analizowanej próbki oraz reguły YARA pasujące do tej rodziny malware’u.
ae0d32e51f36ce6e6e8c5ccdc3d253a0 - analizowana próbka (przed rozpakowaniem)
Reguły YARY:
rule tofsee | |
{ | |
meta: | |
author="akrasuski1" | |
strings: | |
$decryptStr = {32 55 14 88 10 8A D1 02 55 18 F6 D9 00 55 14} | |
$xorGreet = {C1 EB 03 C0 E1 05 0A D9 32 DA 34 C6 88 1E} | |
$xorCrypt = {F7 FB 8A 44 0A 04 30 06 FF 41 0C} | |
$string_res1 = "loader_id" | |
$string_res2 = "born_date" | |
$string_res3 = "work_srv" | |
$string_res4 = "flags_upd" | |
$string_res5 = "lid_file_upd" | |
$string_res6 = "localcfg" | |
$string_var0 = "%RND_NUM" | |
$string_var1 = "%SYS_JR" | |
$string_var2 = "%SYS_N" | |
$string_var3 = "%SYS_RN" | |
$string_var4 = "%RND_SPACE" | |
$string_var5 = "%RND_DIGIT" | |
$string_var6 = "%RND_HEX" | |
$string_var7 = "%RND_hex" | |
$string_var8 = "%RND_char" | |
$string_var9 = "%RND_CHAR" | |
condition: | |
(7 of ($string_var*) and 4 of ($string_res*)) | |
or | |
(7 of ($string_var*) and 2 of ($decryptStr, $xorGreet, $xorCrypt)) | |
or | |
(4 of ($string_res*) and 2 of ($decryptStr, $xorGreet, $xorCrypt)) | |
} |