sobota, 18 listopada 2017

Obfuskacja nazw w złośliwym oprogramowaniu

Poniżej opisujemy jedną z metod ukrywania ciągów znaków przez złośliwe oprogramowanie. Bazuje ona na generowaniu wartości hash i porównywaniu z własną bazą. Dzięki temu nigdy w pamięci procesu nie będą ujawniane wszystkie informacje o działaniu złośliwego kodu.
 
Jakiś czas temu otrzymaliśmy próbkę złośliwego oprogramowania, która sprawiała pewne problemy w analizie dynamicznej. Szybko okazało się, że przy uruchomionych narzędziach do dynamicznej analizy złośliwe oprogramowanie wyłączało je.

Funkcja terminująca proces jest poniżej:

void _killProcess (int arg0) {
esp = esp - 0x4 - 0x4 - 0x4 - 0x4;
esi = (*kernel32.OpenProcess)(0x1, 0x0, arg_1, esi, arg0);
  if (esi != 0x0) {
   (*kernel32.TerminateProcess)(esi, 0x0);
   stack[2041] = esi;
   esp = esp - 0x4 - 0x4 - 0x4;
   (*kernel32.CloseHandle)(stack[2041]);
  }
return;
}


Na poniższym zrzucie ekranu można zauważyć, że złośliwe oprogramowanie pobiera między innymi informacje o uruchomionych procesach i nazwach okien.

Są to jedne z prostszych metod wykrywania uruchomionych aplikacji (np. programów AV) na infekowanej stacji roboczej, niemniej w tym przypadku okazało się, że nigdzie w kodzie malware nie ma nazw z którymi porównywane są na przykład nazwy uruchomionych procesów. Twórcy malware w tym konkretnym przypadku zastosowali funkcje haszującą nazwy. W kodzie zapisali wyłącznie wartości hash.
Chcieliśmy poznać nazwy wszystkich procesów, które są wyłączone przez malware.
Stwierdziliśmy, że najprościej będzie napisać narzędzie: generujące wszystkie możliwe nazwy, tworzące hash a następnie porównujące wynik z wartościami hash znajdującymi się w złośliwym kodzie.
Funkcję generującą hash oraz funkcję porównującą wartości hash przekopiowaliśmy z kodu złośliwego oprogramowania.
Funkcja do generacji wartości hash jest poniżej. Przyjmuje dwa argumenty: wskaźnik do tablicy z ciągiem znaków oraz długość ciągu znaków. W celu zwrócenia wartości hash utworzyliśmy stałą x w funkcji. Wiedząc gdzie jest ona przechowywana na stosie podmieniliśmy wartość z 0 na wartość hash, która ma dokładnie długość 32 bitów. Return x zwraca nam zawartość utworzonego hash.

int hash(char *, int) {
int x = 0;
__asm {

   XOR EAX, EAX //zeruj rejestr
   MOV ECX, [EBP + 0xC] //ilość znaków – licznik dla instrukcji LOOP
   MOV EDX, [EBP + 8] //adres początku tablicy z nazwą

loop1:
   MOVZX EBX, [EDX] //wczytaj pierwszą literę
   AND BL, 0x0df //operacja logiczna AND na pierwszym znaku
   XOR AH, BL //operacja logiczna XOR
   ROL EAX, 8 //przesunięcie w lewo o 1 bajt
   XOR AL, AH //operacja logiczna
   INC EDX //przesuń na kolejną literę
   LOOP loop1
   MOV [EBP - 4], eax; //nadpisz wartość x
}

return x;
}


Podobnie zrobiliśmy z funkcją porównującą wartości hash. Funkcja przyjmuje dwie wartości: wskaźnik do tablicy (przekopiowanej z sekcji danych złośliwego kodu – są to wartości hash z nazw procesów które mają być wykrywane i wyłączane) oraz wartość hash (wynik zwracany przez naszą funkcje hash()).

unsigned char tablica[48] = { 0x05,0x1D,0x4A,0x0B,0x02,0x5C,0x19,0x19,0x1D,0x04,0x0E,0x1C,0x0B,0x5D,0x18,0x06,0x0A,0x12,0x07,0x1D,0x18,0x51,0x0B,0x06,0x0D,0x1E,0x0E,0x55,0x47,0x5C,0x56,0x51,0x14,0x4C,0x11,0x04,0x04,0x5C,0x4E,0x5F,0x12,0x5A,0x58,0x14,0x14,0x5C,0x5E,0x14 };

int compare(char *, int) {
int x = 0;
__asm {
  XOR ECX, ECX
   XOR EAX, EAX
   MOV ECX, [EBP + 8] //wczytanie adresu tablicy
   XOR EBX, EBX
   MOV EBX, [EBP + 0x10] //wczytanie wartości hash
loop2:
   CMP [ECX+EAX], EBX //porównanie wartości hash z tablicą
   JE exit2
   ADD EAX, 0x4 //przesuwamy się o 4 bajty
   CMP EAX, 0x30 //wielkość tablicy
   JB loop2
   MOV[EBP - 4], 0x00000030 //zapisujemy np. 0x30 gdy hash nie istnieje
   JMP exit1

exit2:
   MOV [EBP-4], 0x00000031 //inna wartośc np. 0x31 gdy hash istnieje
exit1:

}
return x;

};


Ostatnim elementem było napisanie funkcji generującej metodą brute-force nazwy procesów czy okien. Ograniczyliśmy długość nazw od 6 znaków do 20. Szybko się jednak okazało, że mamy bardzo dużo kolizji i generowanie wartości hash dla każdego wygenerowanego ciągu znaków jest bez sensu. Związane jest to z tym, że algorytm generujący wartości hash jest bardzo prosty. Dla przykładu ciągi znaków cycdy6.exe czy c8u59f.exe mają tą samą wartość hash równą 0x19195c02.
 
Stworzenie listy najbardziej popularnych narzędzi do analizy jest dużo lepszym pomysłem. I tak dla przykładu hash 0x19195c02 to proces procexp.exe a np. 0x6185d0b to procmon.exe

Analizowany malware ma następujące wartości MD5 i SHA1:
SHA1: BAB30F8093A10A933FC75555F496B6BCB9D031A7
MD5: 98c81e23b7e6fb6ce3525b2fc7821561



Brak komentarzy:

Prześlij komentarz