Zgłoś incydent
Zgłoś incydent

Tofsee – modularny spambot
16 września 2016 | Adam Krasuski | #analiza, #malware, #tofsee

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.

decryptstr
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
view raw greetingXor.py hosted with ❤ by GitHub

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];
};
view raw greeting.c hosted with ❤ by GitHub

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
view raw xorStream.py hosted with ❤ by GitHub

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;
};
view raw header.c hosted with ❤ by GitHub

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];
};
view raw botdata.c hosted with ❤ by GitHub

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.
};
view raw resource.c hosted with ❤ by GitHub

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
view raw script hosted with ❤ by GitHub

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
view raw %DATE_RAN_SUB hosted with ❤ by GitHub

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.

Hash:
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))
}
view raw tofsee.yar hosted with ❤ by GitHub

Udostępnij: