W artykule omówimy poszczególne etapy tworzenia prostego skanera antywirusowego.
Zadaniem artykułu jest wyjaśnienie podstawowych zasad pracy programów antywirusowych, wykorzystujących sygnatury dla rozpoznania szkodliwego kodu.
Oprócz skanera napiszemy również program do tworzenia bazy sygnatur.
Ze względu na prostotę algorytmu sprawdzania, nasz skaner może wykrywać tylko szkodliwe programy, które rozprzestrzeniają się całymi plikami, tzn. takie, które nie zarażają innych plików i nie zmieniają swoich właściwości podczas działania, jak wirusy polimorficzne.
Poza tym, odnosi się to do większości wirusów, robaków i praktycznie wszystkich trojanów, dlatego stworzony przez nas skaner okaże się niezwykle pożytecznym narzędziem.
Sygnatura to nic innego jak unikalna część pliku (bajt).
Jednak ta część powinna maksymalnie jednoznacznie wyróżniać plik spośród mnóstwa innych plików.
Oznacza to, że wybrana sekwencja powinna odpowiadać tylko jednemu (i żadnemu innemu) plikowi, do którego należy.
W praktyce oprócz samej sekwencji stosowane są także dodatkowe parametry, pozwalające maksymalnie jednoznacznie zestawić sygnaturę z plikiem.
Wprowadzenie dodatkowych parametrów jest także skierowane na przyspieszenie wyszukiwania sygnatury w pliku.
Takimi parametrami może być np. rozmiar pliku, zastąpienie sekwencji bajtów, typ pliku, specjalne maski (wyobrażenie jak powinien wyglądać przykładowy plik, aby mogła się w nim zawierać wymagana sygnatura) i wiele innych.
W naszym skanerze, w charakterze dodatkowego parametru, będziemy wykorzystywać przeniesie sekwencji w pliku, w odniesieniu do początku.
Ta uniwersalna metoda znajduje zastosowanie dla dowolnych plików niezależnie od typu.
Jednak wykorzystywanie przeniesienia sekwencji posiada jeden znaczący minus: aby "oszukać" skaner, wystarczy delikatnie przesunąć sekwencję bajtów w pliku, czyli zmienić przeniesienie sekwencji (np. ponownie kompilować wirus lub dodać symbol w przypadku skryptowego wirusa).
Dla zaoszczędzenia pamięci i podwyższenia szybkości wykrywania, w praktyce zwykle wykorzystuje się liczbę kontrolną (hash) sekwencji.
W ten sposób przed dodaniem sygnatury do bazy określana jest liczba kontrolna wybranego odcinka pliku. Pomaga to także w niewykrywaniu szkodliwego kodu we własnych bazach.
Baza antywirusowa to jeden lub kilka plików, posiadających zapis o wszystkich wirusach znanych skanerowi.
Każdy zapis obowiązkowo zawiera nazwę wirusa (w końcu użytkownik powinien wiedzieć, co za intruz pojawił się w jego systemie). W zapisie powinna występować też sygnatura, poprzez którą wykonywane jest wyszukiwanie.
Oprócz nazwy i sygnatury, zapis może zawierać dodatkowe informacje, np. instrukcję dotyczącą usunięcia skutków pojawienia się wirusa.
Algorytm pracy skanera, wykorzystującego sygnatury, można sprowadzić do kilku punktów:
1. Pobranie bazy sygnatur
2. Otworzenie sprawdzanego pliku
3. Wyszukiwanie sygnatury w otworzonym pliku
4. Po odnalezieniu sygnatury
- podjęcie stosownych działań
5. Jeśli żadna z bazowych sygnatur nie zostanie odnaleziona
- zamknięcie pliku i przejście do sprawdzenia następnego.
Jak widać, ogólna zasady działania skanera jest całkiem prosta.
Teorii już wystarczy. Przejdźmy do części praktycznej.
Zanim zaczniemy pisać kod, warto ustalić wszystkie części składowe skanera i to, w jaki sposób będą ze sobą współpracować.
W celu wykrycia szkodliwych plików niezbędny jest nam sam skaner.
Dla działania skanera niezbędne są sygnatury, które chronione są w bazie antywirusowej.
Specjalny program tworzy i uzupełnia bazę.
W rezultacie zawiązuje się następująca zależność:
Program tworzący bazę -> Baza -> Skaner
Jednak zanim stworzymy bazę, konieczne jest ustalenie jej formatu, który zależy z kolei od przechowywanych informacji.
Informacja sygnatury
Sygnatura będzie się składać z:
- przesunięcia (modyfikacji) sekwencji w pliku
- rozmiaru sekwencji
- hasha sekwencji
W celu "hashowania" będziemy posługiwać się algorytmem MD5.
Każdy MD5-hash składa się z 16 bajtów lub 4 podwójnych słów.
Aby zachować zmiany i rozmiar sekwencji zarezerwujemy po 4 bajty.
W ten sposób sygnaturę możemy opisać w nastepujący sposób:
[Offset * 4 ]
[Lenght * 4 ]
[Hash * 16 ]
Zapis bazy antywirusowej
Zapis będzie zawierał:
- Sygnaturę
- Rozmiar nazwy pliku
- Nazwę pliku
Dla rozmiaru nazwy pliku zostawimy 1 bajt, co w zupełności wystarczy.
Nazwa pliku może mieć dowolny rozmiar, nie przekraczający 255 znaków.
Otrzymujemy następującą strukturę:
[Signature]
[NameLen * 1 ]
[Name ... ]
Po odkryciu struktury sygnatury otrzymujemy zapis:
Właśnie w ten sposób - w pliku, jeden za drugim, będą znajdować się zapisy dla wszystkich, znanych skanerowi, szkodników.
Oprócz samych zapisów w pliku bazy powinien znajdować się nagłówek, który będzie zawierać liczbę rekordów w bazie i sygnaturę pliku "AVB". Wyznaczenie sygnatury - będzie przekonywać, że jest to właśnie plik bazy.
W ten sposób plik bazy będzie mieć strukturę:
[Sign * 3 ]
[RecordCount * 4 ]
[Records]
Przejdźmy zatem do napisania kodu.
Nie jest ich dużo, bo dwie.
Dane struktury będą wykorzystywane zarówno przez skaner jak i program, który stworzył bazę antywirusową.
Po pierwsze, musimy koniecznie przywołać wszystkie niezbędne nam struktury.
Pierwszą z nich będzie struktura sygnatury SAVSignature.
Kolejną będzie struktura zapisu SAVRecord, obejmująca sygnaturę z nazwą.
Dla wygody struktura będzie zawierać funkcję wydzielenia pamięci na nazwę szkodnika (allocName).
Wszystkie struktury znajdują się w pliku avrecord.h
//! Struktura zapisu o szkodniku typedefstruct SAVRecord{
SAVRecord(){
this->Name =NULL;
this->NameLen =0; }
~SAVRecord(){ if(this->Name !=NULL) this->Name; } //! Wyznaczenie pamięci na nazwę void allocName(BYTE NameLen){ if(this->Name ==NULL){
this->NameLen = NameLen;
this->Name =newCHAR[this->NameLen +1]; memset(this->Name, 0, this->NameLen +1); } }
PSTR Name; // - Nazwa
BYTE NameLen; // - Rozmiar nazwy
SAVSignature Signature; // - Sygnatura
}* PSAVRecord;
#endif
Kolejny etap to napisanie klasy dla pracy z plikiem bazy antywirusowej.
Klas będzie kilka:
- Bazowa klasa pliku "CAVBFile"
- Klasa odczytu pliku "CAVBFileReader"
- Klasa dodawania zapisu "CAVBFileWriter"
Informacje wszystkich klas znajdują się w pliku CAVBFile.h
Przejdźmy teraz do realizacji przedstawionych klas.
Ich realizacja będzie odbywać się w pliku AVBFile.cpp
Pamiętajmy, że musimy dołączyć plik AVBFile.h
W niektórych funkcjach będzie nam potrzebne sprawdzenie czy plik istnieje.
To najszybszy sposób sprawdzenia czy plik istnieje. Wykorzystywany jest w większości przykładów w MSDN, więc może być traktowany jako standard dla Windows.
Funkcja GetFileAttributes przywraca atrybuty pliku lub 0xfffffff w przypadku, kiedy plik nie zostanie odnaleziony.
Następnie realizujemy funkcję klasy dla zapisu pliku
Listing :
// // - CAVBFileWriter //
//! Otworzenie pliku bool CAVBFileWriter::open(PCSTR FileName){ if(FileName ==NULL)returnfalse; // - Jeśli plik nie zostanie znaleziony, tworzymy jego prototyp if(!isFileExist(FileName)){
hFile.open(FileName, ios::out| ios::binary); if(!hFile.is_open())returnfalse;
hFile.write("AVB", 3); // - Sygnatura pliku
hFile.write((PCSTR)&this->RecordCount, sizeof(DWORD)); // - Ilość zapisów // - Inaczej otwieramy i sprawdzamy ważność }else{
hFile.open(FileName, ios::in| ios::out| ios::binary); if(!hFile.is_open())returnfalse; // - Sprawdzenie sygnatury CHAR Sign[3];
hFile.read((PSTR)Sign, 3); if(memcmp(Sign, "AVB", 3)){
hFile.close(); // - To obcy plik returnfalse; } // - Odczytujemy ilość zapisów
hFile.read((PSTR)&this->RecordCount, sizeof(DWORD)); } returntrue; }
bool CAVBFileWriter::addRecord(PSAVRecord Record){ if(Record ==NULL||!hFile.is_open())returnfalse; // - Przenosimy się na koniec pliku
hFile.seekp(0, ios::end); // - Dodajemy zapis
hFile.write((PSTR)&Record->Signature.Offset, sizeof(DWORD)); // - Modyfikacja sygnatury
hFile.write((PSTR)&Record->Signature.Lenght, sizeof(DWORD)); // - Rozmiar sygnatury
hFile.write((PSTR)&Record->Signature.Hash, 4*sizeof(DWORD)); // - Suma kontrolna
hFile.write((PSTR)&Record->NameLen, sizeof(BYTE)); // - Rozmiar nazwy
hFile.write((PSTR)Record->Name, Record->NameLen); // - Nazwa // - Przesuwamy się do ilości zapisów
hFile.seekp(3, ios::beg); // - Zwiększamy licznik zapisów
this->RecordCount++;
hFile.write((PSTR)&this->RecordCount, sizeof(DWORD));
returntrue; }
Przy otworzeniu pliku, jeśli plik nie został znaleziony, tworzy się nowy plik i w nim zapisywana jest sygnatura i liczba wpisów).
Jeśli plik istnieje, przeprowadzane jest sprawdzenie sygnatury pliku i odczyt liczby wpisów.
Funkcja addRecord przyjmuje w charakterze parametru odnośnik do struktury dodanego zapisu.
Najpierw przenosimy się na koniec pliku (nowy wpis jest tam notowany).
Następnie ma miejsce zapis danych w pliku zgodnie z ustalonym formatem.
Po dokonaniu zapisu następuje zwiększenie licznika zapisów.
Listing :
// // - CAVBFileReader // bool CAVBFileReader::open(PCSTR FileName){ if(FileName ==NULL)returnfalse; // - Jeśli plik nie został znaleziony, to wykonujemy jego prototyp if(isFileExist(FileName)){
hFile.open(FileName, ios::in| ios::out| ios::binary); if(!hFile.is_open())returnfalse; // - Sprawdzenie sygnatury CHAR Sign[3];
hFile.read((PSTR)Sign, 3); if(memcmp(Sign, "AVB", 3)){
hFile.close(); // - To obcy plik returnfalse; } // - Odczytujemy liczbę zapisów
hFile.read((PSTR)&this->RecordCount, sizeof(DWORD)); }else{returnfalse; } returntrue; }
W tym przypadku, jeśli podczas próby otworzenia pliku, okazuje się że plik nie istnieje, funkcja zwróci wartość false, świadczącą o błędzie.
Odczyt zapisu dokonuje się stopniowo i zabezpieczony jest funkcją readNextRecord, która w charakterze parametru przyjmuje odnośnik do struktury zapisu, w której będą odczytywane dane z pliku.
Na tym skończymy pisanie ogólnego kodu.
Pora przejść do realizacji programu tworzącego wpisy i skanera.
Jak już wcześniej wspomniano, skaner bez sygnatur nie ma sensu. Właśnie dlatego pierwszą rzeczą, którą wykonamy będzie program tworzący bazy.
W charakterze parametrów, program przyjmie ścieżkę do pliku szkodnika, ścieżkę do pliku bazy, zastąpienie sekwencji w pliku szkodnika, rozmiar sekwencji i nazwę szkodnika.
Argumenty przekazywane są w formacie -A[Value], gdzie A to odpowiadający klucz, a Value - wartość.
Oznaczymy wszystkie argumenty:
-s = ścieżka do pliku szkodnika
-d = ścieżka do pliku bazy
-o = modyfikacja sekwencji
-l = rozmiar sekwencji
-n = nazwa pliku
Algorytm pracy programu:
1. Otworzyć plik szkodnika
2. Przejść stosując się do zmian
3. Obliczyć MD5-hash sekwencji bajtów
4. Dodać zapis do bazy
Wykonania algorytmu nie będziemy przytaczać. Nie odnosi się do artykułu, ale można go znaleźć w pliku md5hash.cpp
W tym miejscu, w prosty sposób przedstawimy funkcję getMD5, która obejmuje wskaźnik na dane, ich rozmiar i wskaźnik na bufor z 16 bajtów oraz gdzie będzie zapisany hash.
Kod programu znajduje się w pliku avrec.cpp
Najpierw połączymy wszystkie niezbędne pliki i określimy funkcję MD5, a także napiszemy funkcję wspomagającą, która będzie potrzebna przy analizie argumentów programu.
Jeśli podano zbyt mało argumentów, to pojawi się informacja na temat wykorzystania programu. W przeciwnym razie ma miejsce ich "identyfikacja".
Funkcja copyArg kopiuje we wskazane miejsce wartość argumentu bez klucza.
Po tym, jak dane o zapisie zostały otrzymane, można przystąpić do obliczeń sumy kontrolnej sygnatury.
Program gotowy! Można kompilować. :)
A póki kompiluje, przejdźmy do napisania samego skanera!
W końcu dotarliśmy do sedna sprawy.
Na razie skaner będzie w prosty sposób sprawdzać, czy dany plik jest szkodliwy czy nie.
Leczenie, usuwanie, kwarantannę odłóżmy na później.
Plik z bazą i skaner powinny znajdować się we wspólnym folderze. Plik powinien nosić nazwę avbase.avb.
Program przyjmuje jeden jedyny parametr - ścieżkę do folderu, w którym należy przeprowadzić sprawdzenie.
Skaner będzie zawierał więcej kodu, ale całość generalnie nie przysporzy problemów
Algorytm pracy:
1. Pobranie pliku bazy
2. Pobranie listy plików w podanym folderze.
3. Jeśli jest to plik - sprawdzamy. Jeśli folder - przechodzimy do punktu 2.
Pobranie pliku bazy będzie odbywać się w specjalnej strukturze SAVRecordCollection.
Sprawdzenie pliku sprowadza się do prostej analizy wszytkich sygnatur.
Jeśli sygnatura jest obecna, to informujemy, że z plikiem jest coś nie tak. W przeciwnym razie - że wszystko w porządku.
// - Otrzymanie ścieżki do pliku i bazy CHAR AVBPath[MAX_PATH]; // - Ścieżka do folderu z programem memset(AVBPath, 0, MAX_PATH);
PCHAR NamePtr;
GetFullPathNameA(argv[0], MAX_PATH, AVBPath, &NamePtr); *NamePtr = 0x00;
strcat_s(AVBPath, MAX_PATH, "avbase.avb");
Funkcja GetFullPathName w trzecim parametrze zwraca wskaźnik do nazwy pliku wykonawczego, na początku którego wpisujemy "\0" (bez cudzysłowu). W ten sposób odrzucając go zostawia tylko ścieżkę do folderu, w którym znajduje się plik wykonawczy.
Otwieramy plik, wyznaczamy pamięć na wpisy, następnie odczytujemy informację z pliku.
Jeśli wszystko przebiegło pomyślnie, to pojawi się funkcja processPath, która wykonuje rekursywne sprawdzenie na ukazanej ścieżce.
// - Jeśli folder, skanujemy rekursywnie if((FindData.dwFileAttributes& FILE_ATTRIBUTE_DIRECTORY)){
processPath(File.c_str()); // - Inaczej sprawdzamy wirusy }else{
checkFile(File.c_str()); }
}while(FindNextFileA(hFind, &FindData));
}
Otrzymujemy listę plików i folderów (poza folderami "." i ".."), przy czym jeśli dostaliśmy folder, to wykonujemy rekursywny przegląd. A jeśli otrzymaliśmy plik, spradzamy jego funkcję checkFile.
Omówimy ją bardziej szczegółowo.
Po pierwsze, w funkcji zamiast odczytu pliku wykorzystano obraz pliku w pamięci, przy którym plik zmienia się w adresową przestrzeń procesu, i dla dostępu do pliku nie wymaga przeprowadzenia operacji odczytu lub zapisu.
Ta metoda została wybrana, ponieważ przy sprawdzaniu sygnatur wymaga ciągłego przemieszczania sie w obrębie pliku zgodnie ze zmianą sygnatur.
Przemieszczenie na tablicy odbywa się o wiele szybciej niż w pliku. Także dla obliczeń hasha wystarczy przekazać wskaźnik na początek sekwencji.
Przy standardowym podejściu, za każdym razem byłoby konieczne odczytywanie informacji z pliku, co jest nie tylko wolne, ale po prostu niewygodne.
Funkcja MapViewOfFile zwraca adres, począwszy od tego, z którego był otworzony plik.
Adres ten jest początkiem pliku, lub jeśli przedstawiać plik jako tablicę, to dany adres będzie jej początkiem.
Wyszukiwanie sygnatury wykonujemy w następujący sposób:
Wszystkie wpisy ze zbioru przeglądane są w cyklu.
Jeśli dla wpisu suma zmiany sygnatury i jej rozmiar są mniejsze od rozmiaru pliku (sygnatura mieści się w pliku), to zachodzi hashowanie sekwencji danych.
Następnie porównuje się otrzymany hash z hash'em z sygnatury.
Jeśli są zgodne, to plik będzie rozpoznany jako niebezpieczny (lub fałszywy alarm)
3G - 3G (skrót terminu "trzecia generacja") to ogólne pojęcie oznaczające technologie i standardy, które łączą szybki dostęp mobilny z usługami opartymi na IP. 3G usprawnia usługi bezprzewodowe,...