Zgłoś incydent
Zgłoś incydent

Nymaim atakuje ponownie

logo

Wstęp

Nymaim nie jest nową rodziną złośliwego oprogramowania – pierwszy raz został napotkany w 2013 roku. Wtedy był wykorzystywany jedynie jako dropper, używany głównie do dystrybucji TorrentLockera.

W lutym 2016 ponownie stał się popularny, po tym jak do jego kodu zostały dołączone fragmenty kodu ISFB, który wcześniej wyciekł. Zyskał wtedy przydomek „Goznym”. Ta inkarnacja Nymaima była dla nas szczególnie interesująca, ponieważ zyskała możliwości bankera i stała się poważnym zagrożeniem w Polsce. Z tego powodu przeprowadziliśmy dokładną analizę tego zagrożenia i byliśmy w stanie śledzić aktywność Nymaima od tamtego czasu.

Przez ostatnie dwa miesiące, wiele rzeczy się zmieniło. Przede wszystkim, sieć fast-flux nazywana „Avalanche” (wykorzystywana intensywnie przez Nymaima) została wyłączona na skutek skoordynowanych działań organów ścigania kilku krajów. Przez prawie dwa tygodnie Nymaim był zupełnie nieaktywny, a do dzisiaj jest cieniem tego czym był jeszcze niedawno. Mimo że jest ciągle aktywny w Niemczech (z nowymi injectami), dopiero niedawno powrócił do Polski.

Obfuskacja kodu

Ten temat został już dobrze opisany przez innych badaczy, ale ciągle jest na tyle interesujący, że warto o nim wspomnieć.

Kod Nymaima jest bardzo mocno zaciemniony za pomocą autorskiego narzędzia – do tego stopnia, że analiza jest prawie niemożliwa. Na przykład typowy kod wygląda tak:

jz loc_4381B4
xchg eac, [ebp-0Ch]
push 053h
call sub_408D02
push 050h
call sub_408D02
push edx
push 8AB4BF9EH
push 754A35C1H
call sub_41CF77
mov eax, 8CBFB5FFh
call sub_43AFBD
mov ecx, [ebp-0Ch]
cmp [ecx], ax
jnz loc_4381B4

Zostało tu użytych wiele technik utrudniających analizę, więc omówimy je po kolei:

Po pierwsze, rejestry procesora nie są umieszczane bezpośrednio na stosie, ale jest używana do tego pomocnicza funkcja push_cpu_register. Na przykład push_cpu_register(0x53) jest równoważna push ebx, a push_cpu_register(0x50) to push eax. Stałe odpowiadające rejestrom zmieniły się przynajmniej raz między wersjami, ale kolejność jest zawsze ta sama:

. register constant
0 eax 0x50
1 ecx 0x51
2 ebx 0x52
3 edx 0x53
4 esp 0x54
5 ebp 0x55
6 esi 0x56
7 edi 0x57

Dodatkowo, większość stałych jest obfuskowana. Na przykład mov eax, 25 może zostać zmienione na:

mov eax, 0x8CBFB5FF
call xor_eax_with_8CBFB5DA

Stała użyta w przykładzie to 8CBFB5DA, ale to nie żadna reguła – to po prostu losowy dword, wygenerowany tylko na potrzeby zaciemnienia kilku stałych (zmienia się zawsze z wersji na wersję). Ważne jest tylko to, że po tej operacji rejestr eax będzie miał wartość taką jaką chcemy.

Poza xor_eax_with_X są też analogiczne podobne funkcje: sub_*_from_eax and add_*_to_eax.

Ostatecznie, przepływ sterowania jest mocno zmodyfikowany. Jest bardzo dużo metod wykorzystanych do utrudnienia analizy, ale wszystkie sprowadzają się do prostej podmiany: call X i jmp X są transformowane do przynajmniej dwóch operacji push i skoku gdzieś. Ta metoda jest bardzo podobna do technik wykorzystywanych przy ukrywaniu zmiennych – np. zamiast użyć adresu 0x42424242, skok wykonywany jest do funkcji detour z parametrami 0x40404040 oraz 0x02020202. W asemblerze, zamiast:

będziemy widzieć coś w rodzaju:

push 0x40404040
push 0x02020202
jmp detour
detour:
pop eax ; (uproszczenie, prawdziwy detour nie zmienia wartości żadnego rejestru)
pop ebx
add eax, ebx ; może tu być xor, sub albo add
jmp eax

Istnieje też sprytna wariacja tej metody, gdzie detour zamiast dwóch argumentów przyjmuje jeden – wtedy kod maszynowy za opkodem call jest wykorzystywany jako stała (inaczej mówiąc, detour używa adresu powrotu wrzuconego przez call jako wskaźnika do drugiej stałej).

Podsumowując, poprzednio wklejony przetworzony kod może być interpretowany w ten sposób:

jz loc_4381B4
xchg eac, [ebp-0Ch]
push 053h
call push_cpu_register ; push ebx
push 050h
call push_cpu_register ; push eax
push edx
push 8AB4BF9Eh
push 754A35C1h
call detour_1 ; call f(8AB4BF9Eh, 754A35C1h)
mov eax, 8CBFB5FFh
call xor_eax_const_4 ; eax ^= 8CBFB5DAh
mov ecx, [ebp-0Ch]
cmp [ecx], ax
jnz loc_4381B4

Dysponując tą wiedzą, stworzyliśmy własny deobfuskator. Było to dość dawno temu i od tego czasu pojawiły się też inne rozwiązania. Nasze narzędzie niekoniecznie działa najlepiej, ale ma kilka unikalnych (z tego co wiemy) funkcji, których potrzebujemy, np. odzyskiwanie importów i deszyfrowanie zaszyfrowanych napisów.

Inne narzędzia to na przykład mynaim i ida-patchwork.

Tak czy inaczej, za pomocą naszego narzędzia jesteśmy w stanie całkiem dobrze oczyścić kod:

jz loc_4381B4
xchg eac, [ebp-0Ch]
; nops
push ebx
; nops
push eax
call sub_428b51
; nops
mov eax, 25h
mov ecx, [ebp-0Ch]
cmp [ecx], ax
jnz loc_4381B4

Ale to nie wszystko co potrafi obfuskator Nymaima. Na przykład zewnętrzne funkcje nie są wywoływane bezpośrednio, zamiast tego używany jest skomplikowany wrapper:

Ten wrapper wrzuca hash z nazwy funkcji na stos i skacze do kolejnego (mimo że użyty jest opkod call, nigdy nie wykonywany jest ret to tego adresu):

Drugi dispatcher wrzuca hash nazwy dll na stos i skacze do pomocniczej funkcji:

Ostatecznie wykonywany jest prawdziwy dispatcher:

Dodatkowo prawdziwy adres powrotu z API jest obfuskowany – wskazuje on na call ebx gdzieś w bibliotece ntdll, a prawdziwy adres powrotu znajduje się wtedy w ebx. Większość narzędzi zupełnie sobie z tym nie radzi. Bardzo utrudnia to śledzenie wykonania kodu.

Ale to nie wszystko. Tak jak widzieliśmy, krótkie stałe są obfuskowane przy pomocy prostych operacji matematycznych, ale co z dłuższymi stałymi, takimi jak np. napisy? Twórcy złośliwego oprogramowania również na to mają odpowiedź. Prawie wszystkie napisy użyte w programie są przechowywane w specjalnej sekcji danych. Kiedy Nymaim potrzebuje jednej z nich, używa specjalnej funkcji, którą nazwaliśmy encrypted_memcpy. Jest dość prosta w swoim działaniu:

void encrypted_memcpy(char *to, char *from, int len) {
if (is_in_encrypted_section(to)) {
if (is_in_encrypted_section(from)) {
memcpy(to, from, len);
} else {
memcpy_and_encrypt(to, from, len);
}
} else {
if (is_in_encrypted_section(from)) {
memcpy_and_decrypt(to, from, len);
} else {
memcpy(to, from, len);
}
}
}

Samo memcpy_and_decrypt też nie jest skomplikowane. Nasza wersja tej funkcji w Pythonie ma jedynie kilka linijek kodu:

def nymaim_decrypt(self, raw, from_raw, length):
from_va = from_raw + self.image_base
xsize = from_va - self.off
cur_key = self.key
if xsize < 0:
raise RuntimeError("raw too small - min is " + hex(self.off - self.image_base))
for _ in range(xsize / 4):
cur_key = (cur_key + self.xstep) & 0xffffffff
r = ''
length = min(length, len(raw) - from_raw)
for i in range(length):
r += chr(raw[from_raw + i] ^ (ror(cur_key, (xsize & 3) * 8) & 0xff))
xsize += 1
if xsize % 4 == 0:
cur_key = (cur_key + self.xstep) & 0xffffffff
return r

Potrzebujemy tylko wyciągnąć stałe użyte do deszyfrowania (różnią się między programami) – są ukryte w tych kawałkach kodu.

(Te funkcje nigdy nie są zaciemniane, więc możemy wyciągnąć te stałe za pomocą prostego pattern matchingu).

Ale ukrywanie stałych to nie wszystko – okazjonalnie szyfrowany jest również kod. Nie zdarza się to często, ale kilka krytycznych funkcji jest przechowywanych w postaci zaszyfrowanej cały czas, poza chwilą ich wywołania. Dość ciekawe podejście, trzeba przyznać.

Zostawiając temat obfuskacji za sobą, co więcej możemy powiedzieć o Nymaimie:

Statyczna konfiguracja

Po potraktowaniu deobfuskatorem kod jest znacznie prostszy do analizy i możemy zająć się bardziej interesującymi rzeczami. Po pierwsze, chcielibyśmy wyciągnąć statyczną konfigurację z próbek, szczególnie rzeczy takie jak:

    • adresy serwerów C&C
    • hashe DGA
    • klucze używane do szyfrowania
    • wersję malware
    • inne rzeczy potrzebne do nawiązania komunikacji

Okazuje się to być trudniejsze niż się wydaje – ponieważ te informacje nie są przechowywane podobnie jak zwykłe stałe, a wykorzystywany jest inny mechanizm. Na szczęście, tym razem algorytm szyfrowania jest prosty:

def nymaim_config_crypt(self, mem, ndx):
"""decrypt final config (read keys and length and decrypt raw data)"""
key0 = mem.dword(ndx)
key1 = mem.dword(ndx+4)
len = mem.dword(ndx+8)
raw = mem.read(ndx + 12, len)
prev_chr = 0
result = ''
for i, c in enumerate(raw):
bl = ((key0 & 0x000000FF) + prev_chr) & 0xFF
key0 = (key0 & 0xFFFFFF00) + bl
prev_chr = ord(c) ^ bl
result += chr(prev_chr)
key0 = (key0 + key1) & 0xFFFFFFFF
key0 = ((key0 & 0x00FFFFFF) << 8) + ((key0 & 0xFF000000) >> 24)
return result

Musimy tylko wywołać funkcję nymaim_config_crypt na początku zaszyfrowanej statycznej konfiguracji.

Pozostaje ostatnia rzecz – jak znaleźć miejsce gdzie zaczynają się szukane dane? Spróbowaliśmy kilku metod (dopasowywanie kodu, charakterystyczne miejsca itp.), ale nie były wystarczająco niezawodne jak na nasze potrzeby. Dlatego użyliśmy najprostszego możliwego rozwiązania – próbujemy deszyfrować zaczynając od każdego możliwego miejsca w pamięci. Dodając do tego kilka trywialnych heurystyk (przewidywanie wielkości i zawartości wyniku), jest to dość szybkie rozwiązanie (poniżej 1s na typowym programie) i działa zawsze.

Co znajduje się w zdeszyfrowanej konfiguracji? Wynikowa struktura jest dość prosta w parsowaniu. Składa się z wielu następujących po sobie kawałków danych, z których każdy ma swój typ, długość i dane (poza brakiem paddingu jest to format zgodny z RIFF (Resource Interchange File Format), chociaż raczej nie był to cel twórców):

struct chunk {
uint32_t type;
uint32_t length;
char data[chunk_length];
}

Graficznie wygląda to mniej więcej tak:

Kawałki te są położone jeden po drugim:

Więc możemy szybko przejść przez nie wszystkie w kilku linijkach języka Python:

def parse_static_config(blob):
i = 0
while i < len(blob):
chunk_type = blob[i:i+4] # chunk type, also called "hash" or "chunk hash" in this article
chunk_len = from_uint32(blob[i+4:i+8])
chunk_content = blob[i+8:i+8+chunk_len]
process_chunk(chunk_type, chunk_content) # this function should process every type of chunk
i += 8 + chunk_len

Fragment funkcji process_chunk (gdzie hash to typ)

if hash == self.CFG_URL: # '48c2026b':
parsed['urls'] += [{'url': append_http(x)} for x in filter(None, map(get_domainc, raw.split(';')))]
elif hash == self.CFG_DGA_HASH: # 'd9aea02a':
parsed['dga_hash'] = [uint32(h) for h in chunks(raw, 4)]
elif hash == self.CFG_DOMAINS: # '095d4b1d':
parsed['domains'] += map(lambda x: {'cnc': x}, filter(None, map(get_domainc, raw.split(';'))))
elif hash == self.CFG_ENC_KEY: # '510be622':
parsed['encryption_key'] = raw
...

Po wstępnym parsowaniu wynik wygląda tak:

static config example

(Swoją drogą, w tym artykule typy kawałków – chunków są reprezentowane w kolejności bajtów, czyli big endian)

W bardziej czytelnej dla człowieka postaci po interpretacji ciekawych fragmentów:

static config example

Przebieg infekcji

Jest więcej niż jeden gatunek Nymaimów. W tym momencie rozróżniamy trzy rodzaje:

    • dropper – pierwszy Nymaim, który jest wykonywany w systemie. To jedyny typ rozprowadzany bezpośrednio do ofiar (na przykład przez złośliwe załączniki).
    • payload – moduł odpowiedzialny za większość operacji – na przykład web injecty. Pobierany przez payload.
    • bot_peer – moduł odpowiedzialny za komunikację P2P. Próbuje też zostać supernodem w botnecie.

To wszysto jedna rodzina malware – wszystkie współdzielą ten sam kod, ten sam obfuskator, ten sam format konfiguracji, ten sam protokół sieciowy i te same metody szyfrowania. Różnią się tylko kilkoma specjalizowanymi funkcjami.

Rola droppera jest prosta. Robi najpierw kilka testów, na przykład:

    • Upewnia się, że nie jest wirtualizowany/inkubowany
    • Porównuje obecną datę z „terminem ważności” zapisanym w konfiguracji
    • Sprawdza czy DNS działa poprawnie, odpytując serwery DNS o adresy microsoft.com i google.com

Jeśli coś jest nie tak, dropper zamyka się i proces infekcji komputera nie zachodzi.Szczególnie drugi test przeszkadza w analizie, ponieważ żeby zainfekować komputer trzeba mieć świeżą próbkę Nymaima – starsze programy nie zadziałają. Nawet jeśli dokona się patchowania tego sprawdzania w programie, jest to walidowane również po stronie serwera i payload nie będzie pobrany.

Żeby pobrać więcej danych od Nymaima, musimy znać adres IP peera albo C&C. Konfiguracja statyczna zawiera między innymi dwie przydatne tu informacje:

    • IP servera DNS (zawsze jest to 8.8.8.8 i 8.8.4.4).
    • Nazwa domeny serwera C&C (na przykład ejdqzkd.com albo sjzmvclevg.com).

Nymaim odpytuje o te domeny, ale zwrócone odpowiedzi nie są prawdziwymi adresami serwera C&C – są używane w kolejnym algorytmie żeby wyciągnąć faktyczne adresy IP. Nie będziemy reprodukować tutaj kodu, ale istnieje bardzo dobry artykuł na ten temat, opublikowany przez Talos. Kod samego DGA można znaleźć tutaj:

https://github.com/vrtadmin/goznym/blob/master/DGA_release.py

Kiedy dropper zdobywa adres serwera C&C, rozpoczyna prawdziwą komunikację. Pobiera kilka dodatkowych programów:

    • Payload – moduł odpowiedzialny za injecty. Łączy się z botnetem P2P, ale tylko pasywnie.
    • Opcjonalny bot (próbuje otwierać porty na routerze i stać się aktywnym elementem botnetu. Jeśli się to nie uda, usuwa się z systemu).
    • Kilka dodatkowych binarek (służących np. do wykradania haseł).

DGA

Payload bardzo różni się od droppera w kwestii komunikacji sieciowej:

    • Nie ma zapisanych na stałe domen
    • Ale ma zaimplementowane DGA
    • Oraz komunikację P2P

Algorytm DGA użyty tutaj jest prosty – znaki są generowane jeden po drugim, przy wykorzystaniu prostej pseudo-losowej funkcji (wariacji xorshifta). Początkowy stan DGA zależy wyłącznie od ziarna (ang. seed) przechowywanego w statycznej konfiguracji, więc możemy łatwo przewidzieć wartości DGA dla dowolnej próbki. Dodatkowo badacze z Talos wykonali przeszukiwanie metodą bruteforce prawidłowych seedów, upraszczając generowanie poprawnych domen jeszcze bardziej.

def dga_single(self, state):
name = ''
len = self.getbyte(state, 8) + 5
for i in range(len):
r = self.getbyte(state, 0xFFFFFFFF)
c = self.getbyte(state, 26) + 0x61
name += chr(c)
n = 0
while n == 0:
n = self.getbyte(state, 5)
name += '.' + [0, 'net', 'com', 'in', 'pw'][n]
return name
def getbyte(self, state, param):
temp0 = ((state[0] << 11) ^ state[0]) & 0xFFFFFFFF
temp2 = state[2]
state[0] = (state[0] + state[1]) & 0xFFFFFFFF
state[1] = (state[1] + state[2]) & 0xFFFFFFFF
state[2] = (state[2] + state[3]) & 0xFFFFFFFF
state[3] = ((state[3] >> 19) ^ state[3] ^ temp0 ^ (temp0 >> 8)) & 0xFFFFFFFF
return (((state[3] + temp2) & 0xFFFFFFFF) % (param * 100)) / 100
def __init__(self, seed, date):
arg8 = seed + date.day + (date.year << 9) + (date.month << 5)
state = [0] * 4
state[0] = (arg8 + seed) & 0xFFFFFFFF
state[1] = ror(state[0] * 2, 4)
state[2] = ror(bswap(state[1]), 0xE) + seed
state[3] = ror(state[2] + state[1], 0x12)
for i in range(16):
next_byte = self.getbyte(state, 0xFFFFFFFF)
dword_ndx, byte_ndx = i / 4, i % 4
byte_mask = 0xFF << (byte_ndx * 8)
state[dword_ndx] = (state[dword_ndx] & ~byte_mask) |
((next_byte & 0xFF) << (byte_ndx * 8))
self.state = state

P2P

Po pierwsze, dlaczego podejrzewamy że Nymaim faktycznie korzysta z komunikacji P2P?

Zauważyliśmy że jedna z analizowanych binarek zachowuje się zauważalnie inaczej niż inne. Odpakowaliśmy ją wtedy i rozpoczęliśmy analizę. Szybko znaleźliśmy wiele rzeczy, które wyraźnie sugerowały komunikację P2P. Na przykład zaszyfrowane napisy, które typowo odpowiadają za dodawanie wyjątków do firewalla Windows:

╰─$ strings decrypted_nymaim | grep -E "#!#|Firewall"
#!#*|Action=Allow|#*
#!#*|Action=Block|#*
#!#*|Active=TRUE|#*
#!#*|Active=FALSE|#*
#!#*|Dir=In|#*
#!#*|Dir=Out|#*
#!#*|Profile=Private|#*
#!#*|Profile=Public|#*
#!#*|LPort=#*
#!#*|RPort=#*
\Registry\Machine\SYSTEM\ControlSet001\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\AuthorizedApplications\List
\Registry\Machine\SYSTEM\ControlSet001\Services\SharedAccess\Parameters\FirewallPolicy\FirewallRules

Inne podejrzane zachowanie to otwieranie portów na routerze przy wykorzystaniu UPNP. W ten sposób pozwala zainfekowanym urządzeniom na całym świecie połączyć się do siebie.

╰─$ strings decrypted_nymaim | grep -E "PortMap|upnp"
DeletePortMapping
urn:schemas-upnp-org:service:WANPPPConnection:1
urn:schemas-upnp-org:device:InternetGatewayDevice:1
GetSpecificPortMappingEntry
upnp:rootdevice
AddPortMapping
AddAnyPortMapping
urn:schemas-upnp-org:service:WANIPConnection:1
NewPortMappingDescription

Ostatecznie coś jeszcze bardziej wyróżniającego się. Zaobserwowaliśmy już wcześniej, że malware prezentuje się jako nginx w nagłówku Server. Skąd pochodzi ten nagłówek? Okazuje się, że prosto z samego Nymaima:

╰─$ strings decrypted_nymaim | grep -E "nginx" -B 4
HTTP/1.1 200 OK
Connection: close
Content-Length: %u
Content-Type: application/octet-stream
Server: nginx/1.9.4

Zaimplementowaliśmy tracker botnetu, o którym napiszemy więcej za chwilę. Z informacji, które wyciągnęliśmy wynika, że Nymaim to jeden botnet, ale z geolokalizowanymi injectami. Na przykład injecty pobrane z Polski i z USA różnią się, ale możemy stwierdzić że napisane są przez tych samych aktorów. Rozkład adresów IP na świecie jest bardzo podobny do tego co ustalili inni badacze (poza tym że znaleźliśmy więcej węzłów w Polsce, a mniej w USA niż inni, ale to prawdopodobnie dlatego że nasze badania koncentrowały się na naszym obszarze działania).

49.9% (~7.5k) dostępnych publicznie węzłów, które znaleźliśmy znalazło się w Polsce, 30% (~4.5k) w Niemczech, a 15.7% (~2.2k) w USA.

Protokół sieciowy

Kolejną rzeczą do opisania jest protokół wykorzystywany przez Nymaim do komunikacji. To przykład typowego żądania sieciowego:

    • Wartośc nagłówka Host jest brana ze statycznej konfiguracji
    • Nazwa zmiennej POST i ścieżka w URL są randomizowane i nie mają znaczenia
    • Wartośc zmiennej POST to zaszyfrowane zapytanie (zakodowany dodatkowo za pomocą base64)
    • User-Agent i reszta nagłówków jest generowana przez WinHTTP (więc nagłówki nie są zbyt unikalne i nie da się rozpoznawać łatwo Nymaima za pomocą płytkiej analizy ruchu sieciowego).

A to typowa odpowiedź:

    • Tak naprawdę to oczywiście nie nginx, tylko zapisane na stałe nagłówki
    • Wszystko poza sekcją danych jest zapisane w programie
    • Dane to zaszyfrowany request

Zaszyfrowana wiadomość ma bardzo specyficzny format: Dolna połowa pierwszego bajtu jest równa długości soli, a dolna połowa drugiego bajtu jest równa długości paddingu. Wszystko pomiędzy solą a paddingiem to zaszyfrowana za pomocą algorytmu RC4 wiadomość. Kluczem jest klucz sklejony z solą.

Po przeprowadzeniu analizy tego algorytmu możemy łatwo go odwrócić:

def nymaim_decrypt(key, raw_bytes):
nibble0 = raw_bytes[0] & 0xF
nibble1 = raw_bytes[1] & 0xF
salt = raw_bytes[2:2+nibble0]
password = key + salt
data = raw_bytes[2+nibble0:len(raw_bytes)-nibble1]
decrypted = rc4_decrypt(password, body)
decrypted_len = struct.unpack('&lt;I', decrypted[:4])[0]
assert decrypted_len == len(decrypted - 4)
return decrypted

Po odszyfrowaniu wiadomości ponownie dostajemy format bardzo podobny do statycznej konfiguracji (tzn.sekwencja następujących po sobie kawałków danych – chunki):

Każdy kawałek ma swój typ, długość i właściwe dane:

Możemy przetworzyć zaszyfrowaną wiadomość używając praktycznie takiego samego kodu jak ten do statycznej konfiguracji:

def parse_message(blob):
i = 0
while i < len(blob):
chunk_type = blob[i:i+4]
chunk_len = from_uint32(blob[i+4:i+8])
chunk_content = blob[i+8:i+8+chunk_len]
process_chunk(chunk_type, chunk_content)
i += 8 + chunk_len

I to właściwie cały kod potrzebny do parsowania wiadomości. Każdy typ chunka ma inną zawartość i musi być przetwarzany w trochę inny sposób.

Co ciekawe, wiadomości trzeba parsować rekurencyjnie, ponieważ niektóre typy chunków zawierają kolejne, zagnieżdżone listy, które mogą z kolei zawierać kolejne listy itd. Niestety, żeby dostać się do najciekawszych danych, musimy pokonać jeszcze jedną warstwę – szyfrowanie i kompresję. Niektóre typy chunków są zaszyfrowane za pomocą kolejnego mechanizmu. Takie chunki na końcu danych mają specjalny nagłówek, podpisany za pomocą RSA. Po zdeszyfrowaniu (technicznie: po zdjęciu cyfrowego podpisu, bo robimy to kluczem publicznym) znajdujemy tam md5 i długość zaszyfrowanych danych – i przede wszystkim klucz użyty do zaszyfrowania właściwych danych:

Po zdeszyfrowaniu (za pomocą algorytmu Serpent), trafiamy na kolejną warstwę – dane są skompresowane przy pomocy APLIB32. Ta struktura jest bardzo podobna do tej używanej przez ISFB – najpierw mamy magiczny dword ‚ARCH’, później długość skompresowanych danych, dalej długość skompresowanych danych, a na końcu CRC32.

Ponownie z pomocą funkcji w języku Python możemy szybko poradzić sobie z tym problemem:

def inner_decrypt(raw, rsa_key):
encrypted_header, encrypted_data = raw[-0x40:], raw[:-0x40]
decrypted_data = rsa_decrypt(encrypted_header, rsa_key)
md5 = decrypted_data[0:16]
blob = decrypted_data[16:32]
length = from_uint32(decrypted_data[32:36])
Serpent_decrypted = crypto.s_decrypt(encrypted_data, blob)[:length]
assert md5 == hashlib.md5(Serpent_decrypted).digest()
return Serpent_decrypted

Korzystając z tej funkcji, nareszcie udało nam się zdeszyfrować wszystkie interesujące rzeczy przekazywane z serwera, w szczególności dodatkowe binarki, filtry sieciowe, oraz webinjecty.

Komunikacja

Przykładowe żądanie, po parsowaniu, może wyglądać tak:

<d2bf6f4a> >>> [+] [ 62 bytes]:
state information:
data field 0: 0x263
data field 1: 0x23426908
data field 2: 0x0
data field 3: 0x0 <- injects version
data field 4: 0x0
data field 5: 0x0
data field 6: 0x0 <- webfilters version
data field 7: 0x0
data field 8: 0x0
body: 846372/573,0,0,0,0/0/0/0/2 <- versions of downloaded binaries
<ffd5e56e> >>> [+] [ 48 bytes]:
const_30: 30
const_90012: 90030
const_from_memory1: 0x1
const_from_memory2: 0x1
hash_of_machine_guid: 0x61fa3a8c
hash_of_computer_name: 0x9ddad832
cpuid xor (eax^edx^ecx): 0xbfa81e83
hash_of_user_name: 0x1a776b
hash_of_default_user_name: 0x1a776b
CreateTime: 0xb330815e
crc_of_rsa_key: 0x2c3a27c2
ProcessId (TEB[32]): 3196
<014e2be0> >>> [+] [ 48 bytes]:
OS Build Number: 0x1001db1
OS Major Version: 0x6
OS Minor Version: 0x1
Is64BitProcess * 32 + 32: 0x20
bitmask_of_running_processes: 0x0
ProcSidSubauthority[0]: 0x2000
IsAdmin: 0x1
SystemTimeAsFileTime/10^7: 1467890012
SystemTimeOfDayInformation/10^7: 1467888755
SystemDefaultUILanguage ID: 2009596937
GetSystemDefaultLCID: 1033
zero: 0
<f77006f9> >>> [+] [ 12 bytes]:
volume seral number: 0xd49f44a8
crc32(computer name): 0x33898496
crc32(volume name name): 0x0
<22451ed7> >>> [+] [ 8 bytes]:
crc32 from be8ec514: 0xab8c0ad6
crc32 from 0282aa05: 0xa12e7929
<76fbf55a> >>> [+] [ 314 bytes]:
76fbf55a chunk is null, with length 314

Jak widać, przekazywane jest wiele danych identyfikujących komputer i trochę informacji o aktualnym stanie. Odpowiedzi potrafią być bardzo długie, ale dla uproszczenia przedstawmy jedną z prostszych opcji:

<b84216c7> <<< [+] [ 4 bytes]:
client ip 195.187.xxx.xxx
<be8ec514> <<< [+] [ 257 bytes]:
uri found: 66.168.203.239:34352
uri found: 98.109.148.2:34352
uri found: 80.147.180.254:34352
uri found: 162.222.25.250:34352
uri found: 78.28.51.22:34352
uri found: 75.166.109.79:34352
uri found: 69.132.170.172:34352
uri found: 95.91.6.149:34352
uri found: 76.115.190.186:34352
uri found: 173.14.184.9:34352
uri found: 72.199.113.123:34352
uri found: 73.150.46.222:34352
uri found: ~[mungyshlde.com]
<cb0e30c4> <<< [+] [ 4 bytes]:
number of seconds client should sleep: 280
<d2bf6f4a> <<< [+] [ 73 bytes]:
state information:
data field 0: 0x1fd
data field 1: 0xd8798c3e
data field 2: 0x0
data field 3: 0x0
data field 4: 0x0
data field 5: 0x0
data field 6: 0x595ef998
data field 7: 0x0
data field 8: 0x0
body: 846372/194,68,48,48,0/329188118/3/0/2
<76fbf55a> <<< [+] [ 268 bytes]:
padding: I%S07h6LjRjN&5*sozG4t!5D%7Zk&FVQelCONWlgKRnuOKZ6HALQxaq73zophDS3#zYYnT*B*al&CZi9o9b5KQOfdwI37A%t@O*Kvb&q#gUPC@jDuLMfnsJXZa5*lyrSwgWjGkL1T*iqn*5taHZK!@R21zN0hQhS8IBs%65sdrOclF2dC5hEoPwvkRxP3gqAct&wjE#!T40bvBa*mi1D6nxFPVH&&oe#zxU07T1L%LSft*RiBO#yMWIjikH80dTcmUWWEP6TAmIu

Zainfekowana maszyna poznaje swój publiczny adres IP, porty i adresy IP swoich peerów oraz aktywną domenę C&C. Dodatkowo dostaje polecenie spania kilkadziesiąt sekund (minimalnie 90 sekund podczas gdy rozsyłane są aktualizacje, 280 sekund kiedy nic się nie dzieje).

Lista typów chunków które rozumiemy i jesteśmy w stanie parsować:

typ krótki opis
ffd5e56e fingerprint 1
014e2be0 fingerprint 2 + aktualny czas
f77006f9 fingerprint 3
22451ed7 CRC32 ostatnio otrzymanych chunków be8ec514 i 0282aa05
b873dfe0 flaga mówiąca czy serwer jest aktywny
0c526e8b zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
875c2fbf niezaszyfrowany program wykonywalny
08750ec5 zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
1f5e1840 web injecty (należy zdeszyfrować Serpentem, odpakować APLIB32, sparsować format injectów ISFB)
76daea91 handshake, którego używa dropper podczas nawiązywania połączenia
be8ec514 lista adresów IP peerów
138bee04 lista adresów IP peerów
1a701ad9 zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
30f01ee5 zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
3bbc6128 zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
39bc61ae zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
261dc56c zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
a01fc56c zaszyfrowana binarka (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, zapisać)
76fbf55a padding
cae9ea25 zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
0282aa05 zagnieżdżona lista chunków (należy zdeszyfrować używając nymaim_config_crypt, odpakować APLIB32 i sparsować)
d2bf6f4a informacje o stanie bota
41f2e735 filtry sieciowe
1ec0a948 filtry sieciowe
18c0a95e filtry sieciowe
3d717c2e filtry sieciowe
8de8f7e6 termin ważności? (nie jesteśmy pewni zastosowania, zawsze jest kilka dni naprzód od aktualnej)
3e5a221c lista dodatkowych binarek które zostały pobrane
5babb165 handshake używany przez payload podczas nawiązywania połączenia
b84216c7 publiczne IP zainfekowanej maszyny
cb0e30c4 liczba sekund, które ma spać zainfekowana maszyna
f31cc18f CRC32 dodatkowych zainfekowanych binarek
920f2f0c web injecty (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, sparsować format injectów ISFB)
930f2f0c web injecty (należy zdeszyfrować algorytmem Serpent, odpakować APLIB32, sparsować format injectów ISFB)

To już wydaje się być dużo, a i tak pomijaliśmy większość chunków których nawet nie próbowaliśmy analizować (np. zignorowaliśmy większość czterobajtowych bloków, albo tych które zawsze były równe zero).

Po ekstrakcji wszystkiego z komunikacji, możemy w końcu popatrzeć na injecty. Na przykład polskie:

(304 różne injecty, na dzień pisania tego artykułu)

Albo te z USA:

(393 różne injecty, na dzień pisania artykułu)

Inne zasoby

Regułki Yara:

rule nymaim: trojan
{
meta:
author = "mak"
strings:
$call_obfu_xor = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 33 ?? 08 E9 }
$call_obfu_add = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 03 ?? 08 E9 }
$call_obfu_sub = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 2b ?? 08 E9 }
$nym_get_cnc = {E8 [4] C7 45 ?? [4] C7 45 ?? [4] 83 ??}//3D[4] 01 74 4E E8}
$nym_get_cnc2 ={E8 [4] C7 45 ?? [4] 89 [5] 89 [5] C7 45 ?? [4] 83 ??}
$nym_check_unp = {C7 45 ?? [4] 83 3D [3] 00 01 74 }
$set_cfg_addr = {FF 75 ?? 8F 05 [4] FF 75 08 8F 05 [4] 68 [4] 5? 68 [4] 68 [4] E8}
condition:
(
/* orig */
(2 of ($call_obfu*)) and (
/* old versions */
$nym_check_unp or $nym_get_cnc2 or $nym_get_cnc or
/* new version */
$set_cfg_addr
)
)
}

Hashe (md5):

    • Payload 2016-10-20, 9d6cb537d65240bbe417815243e56461, wersja 90032
    • Dropper 2016-10-20, a395c8475ad51459aeaf01166e333179, wersja 80018
    • Payload 2016-10-05, 744d184bf8ea92270f77c6b2eea28896, wersja 90019
    • Payload 2016-10-04, 6b31500ddd7a55a8882ebac03d731a3e, wersja 90012
    • Dropper 2016-04-12, cb3d058a78196e5c80a8ec83a73c2a79, wersja 80017
    • Dropper 2016-04-09, 8a9ae9f4c96c2409137cc361fc5740e9, wersja 80016

Repozytorium z naszymi narzędziami: nymaim-tools

Materiały dodatkowe:

Udostępnij: