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