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/

 

czwartek, 8 stycznia 2026

Raport z analizy technicznej: SlimAgent

SlimAgent to zaawansowane oprogramowanie szpiegujące w formie 64-bitowej biblioteki DLL, przypisywane grupie APT28. Ze względu na posiadaną funkcjonalność typową dla oprogramowania szpiegującego (zrzuty ekranu, kopiowanie zawartości schowka czy odczytywanie dane z klawiatury) jest wykorzystywane jako jeden z etapów ataku (post-exploitation).

0x1 Charakterystyka pliku

  • Nazwa: SlimAgent
  • Format: DLL (x64)
  • MD5: 889B83D375A0FB00670AF5276816080E
  • SHA1: 5603e99151f8803c13d48d83b8a64d071542f01b
  • Entry Point (RVA): 0x921C
  • Ilość eksportowanych funkcji: 9
  • Biblioteka nie jest podpisana cyfrowo
Nagłówek pliku DLL:


Biblioteka eksportuje 9 funkcji. Pierwsza funkcja pozwala na bezpośrednie wywołanie złośliwego kodu zaimplementowanego w bibliotece (np. z użyciem rundll32.exe). Pozostałe 8 funkcji jak i entry point są wykorzystywanie do implementacji techniki DLL Proxying i następnie wywołania złośliwego kodu.

Tablica eksportu:


0x2 Metody uruchomienia złośliwego kodu


Biblioteka może być uruchomiona w jednym z dwóch trybów:
  • Tryb automatycznego uruchamiana w którym Windows ładuje DLL automatycznie - wtedy wywoływana jest funkcja DLLMain.
  • Tryb z linii komend w którym wskazana jest konkretna funkcja - w tym przypadku #1.

Tryb 1 - DLL Proxying

W trybie automatycznego ładowania (przez explorer.exe), malware podszywa się pod systemową bibliotekę eapphost.dll.
  • Malware pobiera ścieżkę do oryginalnej biblioteki %SystemRoot%\System32\eapphost.dll.
  • Za pomocą LoadLibraryW() ładuje autentyczny moduł i dynamicznie buduje tablicę adresów funkcji eksportowanych (GetProcAddress).

Wszystkie legalne wywołania systemowe są przekierowywane (proxowane) do oryginalnej biblioteki, co zapobiega awariom systemu i ukrywa obecność złośliwego kodu. Te technika nazywa się DLL Proxying.

Poniżej zrzut z programu Process Explorer. Możemy zauważyć, że załadowane są dwie biblioteki o tej samej nazwie (malware i oryginalna biblioteka).




Podczas inicjalizacji w tym trybie kluczową rolę odgrywa procedura wywoływana przez dllmain_dispatch(), do której prowadzi punkt wejścia (Entry Point) biblioteki. Poniższy listing dekompilacji przedstawia fragment kodu rozpoczynający się pod offsetem 0x13C0 (przy założeniu domyślnego adresu bazowego ImageBase równego 0x180000000). Analiza kodu potwierdza implementację techniki DLL Proxying: malware ładuje autentyczny moduł eapphost.dll za pomocą funkcji LoadLibraryW(), a następnie w pętli iteracyjnej (zakres 0x40 bajtów z krokiem 8) dynamicznie rozwiązuje adresy ośmiu oryginalnych eksportów przy użyciu GetProcAddress. Pobrane wskaźniki są zapisywane w lokalnej tablicy adresów, co umożliwia transparentne przekierowanie wywołań systemowych.

// Fragment procedury inicjalizującej DLL Proxying (Offset: 0x13C0)


// Alokacja pamięci i przygotowanie ścieżki do oryginalnej biblioteki systemowej

pWVar1 = (LPWSTR)FUN_malloc_base(uVar2);

ExpandEnvironmentStringsW(L"%SystemRoot%\\System32\\eapphost.dll", pWVar1, nSize);


// Ładowanie autentycznego modułu do przestrzeni adresowej procesu

DAT_1800562f0 = LoadLibraryW(pWVar1);

thunk_FUN_180009a4c();


// Pętla dynamicznego rozwiązywania eksportów

uVar2 = 0;

do {

    // Pobieranie adresu funkcji z oryginalnej biblioteki na podstawie tablicy nazw

    pFVar3 = GetProcAddress(DAT_1800562f0, 

                            *(LPCSTR *)((longlong)&PTR_s_OnSessionChange_1800484f0 + uVar2));

    

    // Zapisywanie wskaźnika do lokalnej tablicy skoków

    *(FARPROC *)((longlong)&DAT_180056300 + uVar2) = pFVar3;

    

    uVar2 = uVar2 + 8; // Krok 8 bajtów (architektura x64, wskaźniki 64-bitowe)

} while (uVar2 < 0x40); // Przetworzenie 8 eksportowanych funkcji (8 * 8 = 64 bajty)


// Weryfikacja kontekstu procesu wykonawczego

pWVar1 = (LPWSTR)FUN_malloc_base(0x208);

GetModuleFileNameW((HMODULE)0x0, pWVar1, 0x104);


// Sprawdzenie, czy biblioteka została załadowana przez proces explorer.exe

pWVar1 = StrStrIW(pWVar1, L"explorer.exe");

if (pWVar1 != (LPWSTR)0x0) {

    // Inicjalizacja głównego wątku złośliwego oprogramowania

    CreateThread((LPSECURITY_ATTRIBUTES)0x0, 0, FUN_main, (LPVOID)0x0, 0, (LPDWORD)0x0);

    return 1;

}


Efekt końcowy mechanizmu DLL Proxying:

Fragment przestrzeni pamięci malware (tablica z adresami oryginalnych funkcji):


Uzupełniona tablica eksportu z malware:



Instrukcja skoku do oryginalnej funkcji - w tym przykładzie OnSessionChange():




Ostatnim etapem w przedstawionym powyżej kodzie jest sprawdzenie, czy biblioteka uruchomiona jest w kontekście procesu explorer.exe. W przypadku pozytywnej weryfikacji, inicjuje główny wątek roboczy za pomocą CreateThread(). Główna funkcja złośliwego kodu znajduje się pod offsetem 0x1230.

Tryb 2 - Wywołanie bezpośrednie

Pozwala na szybkie uruchomienie bez konieczności konfiguracji plików systemowych. Ta komenda też może być dodana do autostart rundll32.exe eapphost.dll,#1


Poniżej fragment wywoływanego kodu i funkcja CreateThread() wywołująca główny kod malware (ponownie trafiamy pod offset 0x1230):

// Eksport #1: Inicjalizacja bezpośrednia (Offset: 0x1300)

undefined8 Ordinal_1(void)
{
    int iVar1;
    DWORD DVar2;
    HANDLE hHandle;
    undefined1 auStackY_78[32];
    tagMSG local_48;
    ulonglong local_18;

    // Uruchomienie głównego modułu złośliwego (FUN_main) w nowym wątku
    hHandle = CreateThread((LPSECURITY_ATTRIBUTES)0x0, 0, FUN_main, (LPVOID)0x0, 0, (LPDWORD)0x0);

    // Inicjalizacja pętli obsługi komunikatów
    // Zapobiega to natychmiastowemu zakończeniu procesu rundll32.exe
    iVar1 = GetMessageW(&local_48, (HWND)0x0, 0, 0);
    
    while (true) 
    {
        if (iVar1 == 0) {
            return 0; // Wyjście po otrzymaniu komunikatu WM_QUIT
        }

        TranslateMessage(&local_48);
        DispatchMessageW(&local_48);

        // Monitorowanie stanu wątku roboczego (timeout 500ms)
        DVar2 = WaitForSingleObject(hHandle, 500);
        
        // Sprawdzenie, czy wątek roboczy nadal pracuje (WAIT_TIMEOUT = 0x102)
        // Jeśli wątek zakończył działanie (DVar2 != WAIT_TIMEOUT), pętla zostaje przerwana
        if (0xfffffffd < DVar2 - 1) break;

        iVar1 = GetMessageW(&local_48, (HWND)0x0, 0, 0);
    }
    
    return 0;
}


0x3 Funkcjonalność


Główny moduł operacyjny realizuje następujące zadania:
  • Keylogger: Rejestracja sekwencji uderzeń klawiszy przy użyciu GetKeyboardState() wraz z kontekstem aktywnego okna.
  • Screen Capture: Wykonywanie zrzutów ekranu i zapisywanie jako jpg.
  • Clipboard Monitor: Monitorowanie i przechwytywanie zawartości schowka systemowego.
Kluczowym elementem logiki malware jest integracja keyloggera z modułem graficznym. System monitoruje każde zdarzenie wejścia, a przy wykryciu określonych aktywności inicjuje procedurę zrzutu ekranu w celu wizualnego potwierdzenia kradzionych informacji.

Główna pętla operacyjna inicjuje się od wywołania GetKeyboardState(). Poniższy fragment kodu przedstawia mechanizm warunkowego wyzwalania funkcji zrzutu ekranu (FUN_zrzuty_ekranu) w zależności od flag ustawionych w strukturze parametrów:

// Fragment weryfikujący warunki wykonania zrzutu ekranu
if ((3 < *(ulonglong *)(param_1 + 0xe8)) && (param_1[0x158] == 1)) { 
    // Wywołanie procedury przechwytywania obrazu
    plVar6 = FUN_zrzuty_ekranu(pbVar8, (longlong *)&local_a0, -1); 
}

Interesującym aspektem analizowanej próbki jest mechanizm automatycznego generowania raportów w formacie HTML. Zamiast przechowywać zrzuty ekranu jako oddzielne pliki graficzne, SlimAgent agreguje przechwycone dane (tekst z klawiatury, zawartość schowka) i obrazy wewnątrz struktury dokumentu HTML. Przykładowy format:

<img src="data:image/jpeg;base64, [DŁUGI_CIĄG_ZNAKÓW]"/><br>

Zrzut z pamięci z fragmentem raportu:



Wygenerowany raport stanowi chronologiczny zapis aktywności. Zastosowany format pozwala na pełną rekonstrukcję zdarzeń dzięki naprzemiennemu umieszczaniu danych tekstowych i graficznych.
  • Logi systemowe i telemetryczne: Wyróżnione w raporcie (kolor czerwony) są komunikaty statusowe generowane przez malware, metadane okien procesów oraz surowe dane przechwycone z klawiatury i schowka systemowego.
  • Wizualne potwierdzenie: Pomiędzy wpisami tekstowymi umieszczane są zrzuty ekranu zakodowane w formacie Base64. Dzięki temu każda operacja tekstowa jest bezpośrednio powiązana z kontekstem wizualnym widocznym na pulpicie użytkownika.
Poniższa ilustracja przedstawia fragment raportu w oknie przeglądarki. Widoczna jest ścisła korelacja czasowa między aktywnością w schowku a przechwyconym obrazem pulpitu:


0x4 Kryptografia


W analizowanej bibliotece zaszyty jest klucz publiczny RSA o długości 2048 bitów atakującego. Poniżej fragment pamięci z widocznym nagłówkiem zaczynającym się od ciągu RSA1 a zaraz po nim podana jest długość klucza.


Adres pamięci gdzie znajduje sie klucz publiczny jest jednym parametrem przekazywanym do głównej funkcji złośliwego kodu (zmienna local_38).


// Główna procedura operacyjna (Offset: 0x1230)

undefined8 FUN_main(void)
{
    ...
    // Utworzenie unikalnego mutex w celu uniknięcia wielokrotnej infekcji
    CreateMutexA((LPSECURITY_ATTRIBUTES)0x0, 1, "hey4kmr8oj46n45n3p");

    // Weryfikacja, czy instancja malware już działa w systemie (0xb7 = ERROR_ALREADY_EXISTS)
    DVar1 = GetLastError();
    if (DVar1 != 0xb7) {
        // Inicjalizacja struktur danych dla klucza RSA
        local_28 = 0;
        local_20 = 0xf;
        local_38 = '\0';

        // Kopiowanie zaszytego klucza publicznego RSA z sekcji danych (.data) pod adres lokalny
        // Offset klucza: 0x180052c80, Długość: 0x114 (276 bajtów)
        FUN_Kopiowanie_klucza_RSA((longlong *)&local_38, (undefined8 *)&DAT_180052c80, 0x114);

        // Wywołanie głównego kodu złośliwego oprogramowania z przekazaniem klucza RSA jako parametru
        FUN_main_malware(&local_38);
    }
    // [ ... ]
}

Klucz publiczny RSA jest wykorzystywany wyłącznie do szyfrowania klucza sesji (unikalnego klucza AES 256 bit). Poniżej fragment kodu, który odpowiada za import klucza publicznego RSA, generowanie klucza AES oraz szyfrowanie klucza AES za pomocą klucza publicznego RSA.

// Implementacja kryptografii (RSA 2048 + AES 256)

// 1. Import klucza publicznego RSA atakującego
BVar7 = CryptImportKey(local_88, param_3, *pDVar1, 0, 0, &local_80);

if (BVar7 != 0) {
    ...
    // 2. Generowanie unikalnego klucza symetrycznego AES256 dla bieżącej sesji
    // Stała 0x6610 odpowiada identyfikatorowi CALG_AES_256
    BVar7 = CryptGenKey(local_88, 0x6610, 1, &local_78);

    if (BVar7 != 0) {
        // 3. Eksport klucza AES zaszyfrowanego kluczem publicznym RSA
        // Wywołanie służy do określenia wymaganej wielkości bufora
        BVar7 = CryptExportKey(local_78, local_80, SIMPLEBLOB, 0, (BYTE *)0x0, local_a8);

        if (BVar7 != 0) {
            // Alokacja bufora pod zaszyfrowany klucz sesyjny
            if ((ulonglong)local_a8[0] != 0) {
                FUN_180007430((longlong *)&local_a0, (ulonglong)local_a8[0], local_b8);
                pBVar14 = local_a0;
                lVar15 = lStack_98;
            }

            // Właściwy eksport zaszyfrowanego klucza AES do bufora pBVar14
            CryptExportKey(local_78, local_80, SIMPLEBLOB, 0, pBVar14, local_a8);
            
            local_58 = local_90;
            local_68 = pBVar14;
            lStack_60 = lVar15;
        }
    }

    // 4. Szyfrowanie właściwego ładunku za pomocą wygenerowanego klucza AES
    // Funkcja FUN_1800072b0 odpowiada za symetryczne szyfrowanie zebranych danych
    uVar8 = FUN_1800072b0((longlong)&local_88, param_2, *puVar9, (longlong *)&local_a0);
}

Zastosowanie stałej 0x6610 w wywołaniu funkcji CryptGenKey() potwierdza wykorzystanie algorytmu AES z 256-bitowym kluczem. Kod generuje unikalny, klucz symetryczny dla każdej sesji. Wykorzystanie szyfrowania symetrycznego do zabezpieczania wolumenu danych jest standardową praktyką, zapewniającą wysoką wydajność.

Kluczowym etapem ochrony danych jest asymetryczne szyfrowanie klucza sesji. Wygenerowany klucz AES zostaje zaszyfrowany przy użyciu klucza publicznego RSA atakującego. Proces ten tworzy bezpieczny obiekt, który może zostać odszyfrowany wyłącznie przez atakującego posiadającego odpowiadający mu klucz prywatny.

Właściwa procedura szyfrowania zebranych artefaktów (implementowana w FUN_1800072b0) operuje na wcześniej przygotowanym kluczu AES. Wykorzystanie natywnego interfejsu Windows CryptoAPI pozwala złośliwemu oprogramowaniu na uniknięcie implementacji własnych bibliotek kryptograficznych.

0x5 Zarządzanie plikami


Po zaszyfrowaniu otrzymujemy pakiet którego struktura wygląda tak:
  • Nagłówek: Rozmiar całości.
  • Zaszyfrowany klucz AES: (Zaszyfrowany przez RSA).
  • Zaszyfrowane Dane: (Zaszyfrowane przez AES).
Implementacja w funkcji pod adresem 0x1F60 odpowiada za dynamiczne tworzenie unikalnych plików. Proces ten przebiega w trzech etapach:
  1. Wykorzystując funkcję ExpandEnvironmentStringsW(), malware dynamicznie pobiera lokalizację katalogu tymczasowego użytkownika z prefiksem systemowym: %TEMP%\Desktop_. Pliki są umieszczane w lokalizacji C:\Users\<user>\AppData\Local\Temp\.
  2. W celu zapewnienia unikalności plików malware pobiera czas systemowy za pomocą _Xtime_get_ticks oraz _localtime64. Dane te są formatowane do ciągu znaków według wzorca: %d-%m-%Y_%H-%M-%S (Dzień-Miesiąc-Rok_Godzina-Minuta-Sekunda).
  3. Kod składa końcową nazwę pliku, łącząc prefix, sformatowaną datę oraz specyficzne rozszerzenie .svc. Ten konkretny wzór to ostatecznie: C:\Users\<user>\AppData\Local\Temp\Desktop_[DATA]_[GODZINA].svc
Finalna ścieżka może wyglądać tak: C:\Users\Jan.Kowalski\AppData\Local\Temp\Desktop_03-01-2026_10-30-05.svc

0x6 Podsumowanie


Analiza wykazała brak zaimplementowanych mechanizmów komunikacji sieciowej (C2). SlimAgent pełni rolę wyłącznie systemu zbierającego dane, a eksfiltracja zgromadzonych plików .svc realizowana jest przez inne narzędzie.

Dobra implementacja modelu szyfrowania, zaawansowana obsługa błędów, wbudowane logowanie zdarzeń samego malware czy mechanizmy zbierania danych poufnych wskazują na dojrzały komponent  wykorzystywany do ataków przez grupę APT.

0x7 Źródła


[1] https://blog.sekoia.io/apt28-operation-phantom-net-voxel/

[2] https://cert.gov.ua/article/6284080