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.
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