Pokazywanie postów oznaczonych etykietą APT. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą APT. Pokaż wszystkie posty

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 );
}


sobota, 13 stycznia 2018

An example of static analysis of an APT malware


This article describes how to perform static analysis of suspicious file with use of a few tools like Oletools, IDA and RetDec.

Few weeks ago, an email with suspicious attachment had been sent to one of government agency in Poland. The attached file name is BG Presidency Draft Calendar-Final.doc.

SHA1 of the doc file is: 601367EED1DDC8473F99CAAA4E2673C13E5D30D7

The first step is to verify the content of the doc file. One of the tool which we usually use at Prevenity is Oletools. It is a package of python tools to analyse Microsoft OLE2 files.


The RTFOBJ tool extracted for us ole objects. One of it (#2) contains command cMD /c rundll32 “%temp%\osk.exe”,Start. Now, we know that the entry point is the Start function from the osk.exe file (dll file).
We can also notice that class name for this object is Equation.3. The part of dumped object is presented below.


The exploit (CVE-2017-11882) used in this doc file is related to the executable module EQNDT32.EXE (Microsoft Equation Editor). 

Note: We can easily confirm that Equation editor was used by analysing output from process monitor.


Let's come back to static analysis. The another OLE object (#0) is executable file, but it is the dll – not the exe file.

SHA1 of extracted dll file: D9276ED0D9370CE08970F869591E93185D3D022C

A few weeks ago Avast did great job by sharing the source code of the RetDec decompiler. Let’s use it to perform the initial analysis of our PE binary file. 

The first interesting notice is that the malware tries to hide communication with C&C by using COM technology. The COM allows malware developers to start and perform http communication with use of iexplore.exe process where the session is hidden. The malware  is using a few interesting functions: CoInitialize() and CoCreateInstance(). The first is used to initialize the COM library. The second function is used to create an object of the class associated with a specified CLSID.


The registry value confirms that malware creates an instance of Internet Explorer.
The malware collects and then sends in HTTP GET to C&C server some basic information about an infected host like hostname, username and operating system type. This data are encrypted by using simple formula. Subtract 0x41 from current letter and XOR with counter. 



Then such encrypted data are coded by Base64. The RetDec in comment provided information about identified base64 function.


The response from C&C server contains in body message a value parameter. The value of this parameter contains encrypted data (commands to execute) which must be decrypted by below code.


In next step malware tries to find the following characters „~~”. In this way commands from C&C server are extracted. Next few instructions are responsible for checking if command id contains only numbers between 0-9 and space as a separator.


We can identify few conditions which redirect code to execute commands, read files, delete files. All content sent to C&C server is encrypted by session symetric key.


One of the feature is executing commands at compromised machines. The results are stores in fw1ei.tmp file. 


As we can see wsprintfA() is used to prepare a final command. The first parameter of wsprintfA() is located a few lines above:

*(char *)(v3 | 2) = *(char *)((int32_t)"Cm" + 2);

By concatenating „Cm”  with „D” malware builds command cmd and add destination executable file name received from C&C server.

Depending on command the result is sent to one of below URLs:
  • http://%s/view.aspx?li=%s
  • http://%s/post.aspx?fs=%s
  • http://%s/li.aspx?id=%s
  • http://%s/query.aspx?q=%s

Some IoC are presented below:

C&C server: maps.fakemediavis.com
IP address: 115.144.238.67
Created files:
  • C:\Users\<username>\AppData\Roaming\mozilla\mozlib.dll
  • C:\Users\<username>\AppDadta\Local\Temp\Microsoft Office Update.lnk with content: C:\Windows\system32\rundll32.exe C:\Users\IEUser\AppData\Roaming\mozilla\mozlib.dll,Start
SHA1 Hash values:
  • 601367EED1DDC8473F99CAAA4E2673C13E5D30D7
  • D9276ED0D9370CE08970F869591E93185D3D022C

External links: