Что нам стоит антивирь построить?! Часть 2
Сканируем файл
В этой части статьи мы коснемся, пожалуй, самого сложного, но в то же время интересного момента создания "домашнего" антивируса. Мы займемся написанием процедуры, которая как раз и будет отличать обычный файл от вируса. Но не спешите открывать Delphi — как всегда, придется сначала почитать теорию — а что делать: без теории никуда:). Для начала нашей процедуре нужно передать имя файла (разумеется, не только имя, но и полный путь до него), который мы будем проверять "на вшивость". Затем рассчитаем его контрольную сумму. Открываем базу, в которой хранятся сигнатуры вирусов, и начинаем сравнивать с полученной. Если они не равны, то переходим к следующей, и т.д. до тех пор, пока все сигнатуры не кончатся. Если ни одна сигнатура не подошла — значит, файл чист, можем переходить к другому (если речь идет о проверки всего HDD). Если же контрольные суммы сошлись, то считываем номер строки сошедшейся сигнатуры, переходим на такою же строку, только уже в базе с именами. Считываем ее значение. И, наконец, выдаем пользователю мессагу, что найден вирус такой-то, файл такой-то. Удалить или нет? (Ну, разумеется, всю информацию забиваем в отчет.) С первого взгляда все не так просто, как я обещал. Но не так страшен черт, как его малюют. Свидетельством этого является код, приведенный ниже:
procedure CheckFile(FileCrc: string);
var count: integer;
baza: textFile;
VirCRC, VirName: string;
Perexod: string;
begin
Perexod := #10 + #13; // можно оформить как константу
Assignfile(baza, ApplicationPath + '\base\vb.bas'); // уже должно быть знакомо
reset(baza); // открываем для чтения с первой строки
count := 0; // обнуляем счетчик, отвечающий за номер строки
while not eof(baza) do // читаем весь файл
begin
inc(count); // то же самое, что и count:=count+1
readln(baza, VirCRC); //читаем строку
if (count mod 1000) = 0 then
begin
if canceled then break;
end;
if VirCRC = FileCrc then // Если контрольные суммы сошлись, то...
begin
v := v + 1; // количество найденных вирусов увеличиваем на 1
Form1.naideno.Caption := inttostr(v);
// отображаем количество найденных вирусов
VirName := GrabLine2(ApplicationPath + '\base\Vn.bas', count);
// узнаем имя вируса
if Application.MessageBox(Pchar('Обнаружен вирус : ' + VirName + perexod +
form1.Label3.Caption + perexod + ' Рекомендуется его удалить! Удалить файл?'),
'Внимание! ',MB_ICONWARNING + MB_YESNO) = IDYES
// выводим мессагу с информацией и запросом на удаление файла
then
if deleteFile(Form1.Label3.Caption) then // если файл успешно удален, то
with Form1.report.Items.Add do
begin
Caption := string(VirName); // забьем всю инфу
SubItems.Add(Form1.Label3.Caption);
SubItems.Add('Удален'); // все OK!
end
else
// а если нет, то
with Form1.report.Items.Add do
begin
Caption := string(VirName); // делаем то же самое
SubItems.Add(Form1.Label3.Caption);
SubItems.Add('Не удален');
// только пишем, что файл не удалось удалить :(
end;
end;
end;
CloseFile(baza); // ну и как всегда закроем файл
end;
Ну что? Разве сложно? Это и есть ВСЯ процедура проверки. По большому счету, основной алгоритм можно уместить в десять строчек кода (и даже меньше). Остальное — это разные украшательства — ну как же без них? Теперь-то вы должны понять, почему в качестве базы использовались текстовики. Но, как я уже ранее говорил, вы можете использовать абсолютно любой формат базы: можете мой, можете придумать свой — главное — чтобы вам с ним было удобно работать!
Примечание:
1. Переменная canceled(глобальная) была введена для того, чтобы можно было прервать процесс сканирования. Это необходимо в том случае, если программа зависнет на проверке файла. Если честно, у меня такой ситуации пока еще не возникало, но подстраховаться не мешает.
2. Удаление вируса производилось за счет стандартной функции окошек DeleteFile. Разумеется, если файл занят, то ничего удалить у нас не получится. Да и вообще не стоит доверять Windows:). Поэтому я бы рекомендовал написать что-нибудь свое, способное удалить запущенное приложение (например, искать процесс с таким же именем, как у вируса, а потом его закрывать, и только после этого пытаться удалять).
3. Для красоты я выводил имя файла, которое сканируется в данный момент, поэтому, как вы уже могли заметить, в самой процедуре путь до вируса мы узнаем через label3. Если вам это не нравится, вы можете создать глобальную переменную и работать уже с ней.
4. А вот две функции, необходимые для работы нашей процедуры:
function ApplicationPath: string; // определяем директорию программы
begin
Result := ExtractFilePath(ParamStr(0));
end;
и
function GrabLine(const baza: string; ALine: Integer): string;
// функция для чтения заданной строки в текстовом файле
var
sl: TStringList;
begin
sndPlaySound(ApplicationPath+'Alarm.WAV', SND_ASYNC);
// противный звук, сигнализирующий о наличии вируса
sl := TStringList.Create;
try
sl.LoadFromFile(baza);
Result := sl[ALine — 1];
finally
sl.Free;
end;
end;
Лирическое отступление:
Сразу хочу оговориться, чтобы потом в мой адрес не было никаких упреков. Данная процедура и все вышесказанное являются только моим видением данного процесса и никоим образом не являются каким-либо стандартом. Не надо высказывать свое мнение насчет того, что код не оптимизирован. Я не ставил задачу написать сверхбыстрый антивирус, а лишь пытался донести до читателя свои мысли и соображения по этому поводу.
Глобальный поиск
Для полного счастья нам осталось научить наш антивирус проверять весь компьютер на наличие заразы. Как всегда, начнем с того, что нам нужно. Если вы хоть раз работали с FindFirst, то должны знать, что она ищет файлы в строго заданном каталоге. Отсюда следует, что сначала нам необходимо получить все локальные диски, а потом подставлять их в процедуру поиска. Ну что, не буду тянуть и сразу предлагаю разобрать вот эту простенькую процедуру:
procedure GetHDD;
var
Drive: Char; //Буква диска
n: byte; // счетчик дисков
lst: TStringList; // переменная, в которой будет храниться список дисков
const
pref = ':\';
begin
lst := TStringList.Create;
lst.Clear;
for Drive := 'A' to 'Z' do // перебираем буквы дисков
if GetDriveType(PChar(Drive + pref)) = DRIVE_FIXED then
// и если диск несъемный, то...
lst.Add(Drive + pref);
//добавим в список дисков, которые нужно сканировать
for n := 0 to (lst.Count — 1) do // по очереди начинаем сканировать
ScanDir(lst.Strings[n]); // ScanDir — наша процедуру поиска файлов
lst.Free;
end;
Я думаю, не стоит подробно останавливаться на этой процедуре, т.к. даже новичок сможет с ней разобраться. И, наконец, переходим к последнему этапу создания антивирусного сканера. Т.к. я люблю объяснять не в теории, а на практике, то сразу приведу код процедуры с комментариями. Если же вам нужна более подробная информация по функциям FindFirst, FindNext, FindClose, то в интернете ее навалом. Не поленитесь поискать.
procedure ScanDir(Dir: string);
var
SearchRec: TSearchRec;
ras: string;
sum: Dword;
begin
if Dir <> '' then if Dir[length(Dir)] <> '\' then Dir := Dir + '\';
// Осуществляем поиск по всем вложенным папкам
if FindFirst(Dir + '*.*', faAnyFile, SearchRec) = 0 then
//Ищем файлы с любым расширением
repeat // пошел цикл
//если имеет название "." или "..", тогда продолжаем
if (SearchRec.name = '.') or (SearchRec.name = '..') then Continue;
// если найден каталог, то...
if (SearchRec.Attr and faDirectory) <> 0 then
ScanDir(Dir + SearchRec.name) //то проверим и его
Else // иначе мы поймали файл
begin
Form1.label3.caption := dir + SearchRec.Name;
// Выводим найденный файл(как я уже говорил, можете использовать глобальную
// переменную)
ras := AnsiLowerCase(ExtractFileExt(dir + SearchRec.name));
// Узнаем расширение найденного файла
if (ras = '.exe') or (ras = '.dll') then
// И если оно либо *.exe, либо *.dll, то...
begin
sum := FastCheckSum(dir + SearchRec.name);
// Как вы уже успели понять, dir + SearchRec.name — это и есть "подозреваемый"
CheckFile(IntToStr(Sum));
// Начинаем проверять файл
end;
end;
Application.ProcessMessages;
if stopScan then // если stopScan=true, то ...
begin
Exit; // останавливаем поиск
end;
until FindNext(SearchRec) <> 0;
//Продолжаем поиск до тех пор, пока результат работы функции не равен нулю
FindClose(SearchRec);
// завершаем поиск, освобождаем память, выделенную системой под него
end;
Вот и все. Наш грозный антивирус готов к бою. Теперь он обладает самыми примитивными функциями для борьбы с вредоносными программами. Я же говорил, что это не так сложно, как кажется на первый взгляд. Теперь все только в твоих руках. Можешь воспользоваться этой статьей чисто в образовательных целях, а можешь заняться разработкой "Антивируса нового поколения"!!! И помни: не бойся ошибаться, постоянно пробуй, и в конце концов все труды увенчаются успехом.
Постскриптум
Минусом данного сканера является отсутствие монитора, способного в реальном времени обнаружить и обезвредить вирус. В дальнейшем (если аудитория этого захочет) я постараюсь осветить и этот вопрос. А пока придется довольствоваться тем, что есть, или не ждать моей статьи, а придумать все самому. Еще одним серьезным недостатком является то, что большинство вирусов в природе обработаны упаковщиками и разными крипторами. Если один и тот же вирус был упакован разными упаковщиками, то наш сканер обломается. Поэтому тем, кому захотелось устранить данный недостаток, придется прочитать не один десяток крэкерских статей, повествующих о таком сложном процессе, как распаковка.
Нелирическое отступление:
Хотелось бы сказать по поводу распаковки. Ясен пень, что у каждого пакера\криптрора\протектора свой алгоритм сжатия\защиты. Поэтому, если мы хотим, чтобы наш антивирус мог действительно искать вирусы, то придется писать распаковщик каждому пакеру\криптрору\протектору… Согласитесь, работа не из приятных! А во-вторых, нужно в своей базе иметь сигнатуры наших пакеров — если вирус будет упакован неизвестной "защитой", то наш антивирус, опять же, идет лесом (именно так работают все современные антивирусы — очень печально). И как же быть в таком случае? Ответ прост: Сделать универсальный распаковщик:) (именно так я и поступил). Почему бы не использовать технологию крэкеров (в смысле, не печенья, а людей, которые ломают софт). В общем, суть такова: находим Оригинальную Точку Входа (кто не знает, что это такое, goto cracklab.ru), снимаем дамп упакованной проги, правим его — вот и все. Знающие люди скажут, что еще надо восстановить таблицу импорта, но зачем нужен лишний геморрой? Мы просто будем сканировать дамп в районе точки входа, и отсутствие таблицы импорта нам никак не помешает. В этом процессе самое сложное — найти оригинальную точку входа. В общем, я не стал выпендриваться (придумывать велосипед) и взял плагин к одной известной крэкерской программе, которая, собственно, этим и занимается. А упакованность файла я проверял такой методикой, как энтропия (опять же, если не знаете, что это такое, goto cracklab.ru or wasm.ru). В итоге получился вполне путный распаковщик, который снимает почти все пакеры и справляется с некоторыми протекторами. И, опять же, это тема отдельной статьи. И если тебя, уважаемый читатель, увлекла данная тема, и ты хочешь увидеть продолжение статьи, то присылай все свои вопросы и пожелания мне на ящик. А мне остается только откланяться на этой позитивной ноте и пожелать вам удачи в этом нелегком труде…
Neon, SA-Security gr., q@sa-sec.org
В этой части статьи мы коснемся, пожалуй, самого сложного, но в то же время интересного момента создания "домашнего" антивируса. Мы займемся написанием процедуры, которая как раз и будет отличать обычный файл от вируса. Но не спешите открывать Delphi — как всегда, придется сначала почитать теорию — а что делать: без теории никуда:). Для начала нашей процедуре нужно передать имя файла (разумеется, не только имя, но и полный путь до него), который мы будем проверять "на вшивость". Затем рассчитаем его контрольную сумму. Открываем базу, в которой хранятся сигнатуры вирусов, и начинаем сравнивать с полученной. Если они не равны, то переходим к следующей, и т.д. до тех пор, пока все сигнатуры не кончатся. Если ни одна сигнатура не подошла — значит, файл чист, можем переходить к другому (если речь идет о проверки всего HDD). Если же контрольные суммы сошлись, то считываем номер строки сошедшейся сигнатуры, переходим на такою же строку, только уже в базе с именами. Считываем ее значение. И, наконец, выдаем пользователю мессагу, что найден вирус такой-то, файл такой-то. Удалить или нет? (Ну, разумеется, всю информацию забиваем в отчет.) С первого взгляда все не так просто, как я обещал. Но не так страшен черт, как его малюют. Свидетельством этого является код, приведенный ниже:
procedure CheckFile(FileCrc: string);
var count: integer;
baza: textFile;
VirCRC, VirName: string;
Perexod: string;
begin
Perexod := #10 + #13; // можно оформить как константу
Assignfile(baza, ApplicationPath + '\base\vb.bas'); // уже должно быть знакомо
reset(baza); // открываем для чтения с первой строки
count := 0; // обнуляем счетчик, отвечающий за номер строки
while not eof(baza) do // читаем весь файл
begin
inc(count); // то же самое, что и count:=count+1
readln(baza, VirCRC); //читаем строку
if (count mod 1000) = 0 then
begin
if canceled then break;
end;
if VirCRC = FileCrc then // Если контрольные суммы сошлись, то...
begin
v := v + 1; // количество найденных вирусов увеличиваем на 1
Form1.naideno.Caption := inttostr(v);
// отображаем количество найденных вирусов
VirName := GrabLine2(ApplicationPath + '\base\Vn.bas', count);
// узнаем имя вируса
if Application.MessageBox(Pchar('Обнаружен вирус : ' + VirName + perexod +
form1.Label3.Caption + perexod + ' Рекомендуется его удалить! Удалить файл?'),
'Внимание! ',MB_ICONWARNING + MB_YESNO) = IDYES
// выводим мессагу с информацией и запросом на удаление файла
then
if deleteFile(Form1.Label3.Caption) then // если файл успешно удален, то
with Form1.report.Items.Add do
begin
Caption := string(VirName); // забьем всю инфу
SubItems.Add(Form1.Label3.Caption);
SubItems.Add('Удален'); // все OK!
end
else
// а если нет, то
with Form1.report.Items.Add do
begin
Caption := string(VirName); // делаем то же самое
SubItems.Add(Form1.Label3.Caption);
SubItems.Add('Не удален');
// только пишем, что файл не удалось удалить :(
end;
end;
end;
CloseFile(baza); // ну и как всегда закроем файл
end;
Ну что? Разве сложно? Это и есть ВСЯ процедура проверки. По большому счету, основной алгоритм можно уместить в десять строчек кода (и даже меньше). Остальное — это разные украшательства — ну как же без них? Теперь-то вы должны понять, почему в качестве базы использовались текстовики. Но, как я уже ранее говорил, вы можете использовать абсолютно любой формат базы: можете мой, можете придумать свой — главное — чтобы вам с ним было удобно работать!
Примечание:
1. Переменная canceled(глобальная) была введена для того, чтобы можно было прервать процесс сканирования. Это необходимо в том случае, если программа зависнет на проверке файла. Если честно, у меня такой ситуации пока еще не возникало, но подстраховаться не мешает.
2. Удаление вируса производилось за счет стандартной функции окошек DeleteFile. Разумеется, если файл занят, то ничего удалить у нас не получится. Да и вообще не стоит доверять Windows:). Поэтому я бы рекомендовал написать что-нибудь свое, способное удалить запущенное приложение (например, искать процесс с таким же именем, как у вируса, а потом его закрывать, и только после этого пытаться удалять).
3. Для красоты я выводил имя файла, которое сканируется в данный момент, поэтому, как вы уже могли заметить, в самой процедуре путь до вируса мы узнаем через label3. Если вам это не нравится, вы можете создать глобальную переменную и работать уже с ней.
4. А вот две функции, необходимые для работы нашей процедуры:
function ApplicationPath: string; // определяем директорию программы
begin
Result := ExtractFilePath(ParamStr(0));
end;
и
function GrabLine(const baza: string; ALine: Integer): string;
// функция для чтения заданной строки в текстовом файле
var
sl: TStringList;
begin
sndPlaySound(ApplicationPath+'Alarm.WAV', SND_ASYNC);
// противный звук, сигнализирующий о наличии вируса
sl := TStringList.Create;
try
sl.LoadFromFile(baza);
Result := sl[ALine — 1];
finally
sl.Free;
end;
end;
Лирическое отступление:
Сразу хочу оговориться, чтобы потом в мой адрес не было никаких упреков. Данная процедура и все вышесказанное являются только моим видением данного процесса и никоим образом не являются каким-либо стандартом. Не надо высказывать свое мнение насчет того, что код не оптимизирован. Я не ставил задачу написать сверхбыстрый антивирус, а лишь пытался донести до читателя свои мысли и соображения по этому поводу.
Глобальный поиск
Для полного счастья нам осталось научить наш антивирус проверять весь компьютер на наличие заразы. Как всегда, начнем с того, что нам нужно. Если вы хоть раз работали с FindFirst, то должны знать, что она ищет файлы в строго заданном каталоге. Отсюда следует, что сначала нам необходимо получить все локальные диски, а потом подставлять их в процедуру поиска. Ну что, не буду тянуть и сразу предлагаю разобрать вот эту простенькую процедуру:
procedure GetHDD;
var
Drive: Char; //Буква диска
n: byte; // счетчик дисков
lst: TStringList; // переменная, в которой будет храниться список дисков
const
pref = ':\';
begin
lst := TStringList.Create;
lst.Clear;
for Drive := 'A' to 'Z' do // перебираем буквы дисков
if GetDriveType(PChar(Drive + pref)) = DRIVE_FIXED then
// и если диск несъемный, то...
lst.Add(Drive + pref);
//добавим в список дисков, которые нужно сканировать
for n := 0 to (lst.Count — 1) do // по очереди начинаем сканировать
ScanDir(lst.Strings[n]); // ScanDir — наша процедуру поиска файлов
lst.Free;
end;
Я думаю, не стоит подробно останавливаться на этой процедуре, т.к. даже новичок сможет с ней разобраться. И, наконец, переходим к последнему этапу создания антивирусного сканера. Т.к. я люблю объяснять не в теории, а на практике, то сразу приведу код процедуры с комментариями. Если же вам нужна более подробная информация по функциям FindFirst, FindNext, FindClose, то в интернете ее навалом. Не поленитесь поискать.
procedure ScanDir(Dir: string);
var
SearchRec: TSearchRec;
ras: string;
sum: Dword;
begin
if Dir <> '' then if Dir[length(Dir)] <> '\' then Dir := Dir + '\';
// Осуществляем поиск по всем вложенным папкам
if FindFirst(Dir + '*.*', faAnyFile, SearchRec) = 0 then
//Ищем файлы с любым расширением
repeat // пошел цикл
//если имеет название "." или "..", тогда продолжаем
if (SearchRec.name = '.') or (SearchRec.name = '..') then Continue;
// если найден каталог, то...
if (SearchRec.Attr and faDirectory) <> 0 then
ScanDir(Dir + SearchRec.name) //то проверим и его
Else // иначе мы поймали файл
begin
Form1.label3.caption := dir + SearchRec.Name;
// Выводим найденный файл(как я уже говорил, можете использовать глобальную
// переменную)
ras := AnsiLowerCase(ExtractFileExt(dir + SearchRec.name));
// Узнаем расширение найденного файла
if (ras = '.exe') or (ras = '.dll') then
// И если оно либо *.exe, либо *.dll, то...
begin
sum := FastCheckSum(dir + SearchRec.name);
// Как вы уже успели понять, dir + SearchRec.name — это и есть "подозреваемый"
CheckFile(IntToStr(Sum));
// Начинаем проверять файл
end;
end;
Application.ProcessMessages;
if stopScan then // если stopScan=true, то ...
begin
Exit; // останавливаем поиск
end;
until FindNext(SearchRec) <> 0;
//Продолжаем поиск до тех пор, пока результат работы функции не равен нулю
FindClose(SearchRec);
// завершаем поиск, освобождаем память, выделенную системой под него
end;
Вот и все. Наш грозный антивирус готов к бою. Теперь он обладает самыми примитивными функциями для борьбы с вредоносными программами. Я же говорил, что это не так сложно, как кажется на первый взгляд. Теперь все только в твоих руках. Можешь воспользоваться этой статьей чисто в образовательных целях, а можешь заняться разработкой "Антивируса нового поколения"!!! И помни: не бойся ошибаться, постоянно пробуй, и в конце концов все труды увенчаются успехом.
Постскриптум
Минусом данного сканера является отсутствие монитора, способного в реальном времени обнаружить и обезвредить вирус. В дальнейшем (если аудитория этого захочет) я постараюсь осветить и этот вопрос. А пока придется довольствоваться тем, что есть, или не ждать моей статьи, а придумать все самому. Еще одним серьезным недостатком является то, что большинство вирусов в природе обработаны упаковщиками и разными крипторами. Если один и тот же вирус был упакован разными упаковщиками, то наш сканер обломается. Поэтому тем, кому захотелось устранить данный недостаток, придется прочитать не один десяток крэкерских статей, повествующих о таком сложном процессе, как распаковка.
Нелирическое отступление:
Хотелось бы сказать по поводу распаковки. Ясен пень, что у каждого пакера\криптрора\протектора свой алгоритм сжатия\защиты. Поэтому, если мы хотим, чтобы наш антивирус мог действительно искать вирусы, то придется писать распаковщик каждому пакеру\криптрору\протектору… Согласитесь, работа не из приятных! А во-вторых, нужно в своей базе иметь сигнатуры наших пакеров — если вирус будет упакован неизвестной "защитой", то наш антивирус, опять же, идет лесом (именно так работают все современные антивирусы — очень печально). И как же быть в таком случае? Ответ прост: Сделать универсальный распаковщик:) (именно так я и поступил). Почему бы не использовать технологию крэкеров (в смысле, не печенья, а людей, которые ломают софт). В общем, суть такова: находим Оригинальную Точку Входа (кто не знает, что это такое, goto cracklab.ru), снимаем дамп упакованной проги, правим его — вот и все. Знающие люди скажут, что еще надо восстановить таблицу импорта, но зачем нужен лишний геморрой? Мы просто будем сканировать дамп в районе точки входа, и отсутствие таблицы импорта нам никак не помешает. В этом процессе самое сложное — найти оригинальную точку входа. В общем, я не стал выпендриваться (придумывать велосипед) и взял плагин к одной известной крэкерской программе, которая, собственно, этим и занимается. А упакованность файла я проверял такой методикой, как энтропия (опять же, если не знаете, что это такое, goto cracklab.ru or wasm.ru). В итоге получился вполне путный распаковщик, который снимает почти все пакеры и справляется с некоторыми протекторами. И, опять же, это тема отдельной статьи. И если тебя, уважаемый читатель, увлекла данная тема, и ты хочешь увидеть продолжение статьи, то присылай все свои вопросы и пожелания мне на ящик. А мне остается только откланяться на этой позитивной ноте и пожелать вам удачи в этом нелегком труде…
Neon, SA-Security gr., q@sa-sec.org
Компьютерная газета. Статья была опубликована в номере 31 за 2008 год в рубрике безопасность