Что нам стоит антивирь построить?! Часть 1

Такой штукой, как антивирус, сейчас никого не удивить. Каждый пользователь считает своим долгом иметь у себя на компьютере такую зверушку. И готов спорить: пока вы читаете данные строки, вашу систему постоянно мониторит какой-нибудь Касперский, а, быть может, Nod32... Но вы никогда не задумывались, как работают программы такого рода? И не появлялось ли желание собрать такую "штуку" самому? Если да, эта статья именно для вас. В ней я рассмотрел методику создания простейшего — повторяюсь: простейшего — антивирусного сканера (но вполне рабочего), способного обнаружить и уничтожить львиную долю вредоносного ПО.

Немного теории


Не буду заниматься разного рода "болтологией" на тему, что такое вирус, какие они бывают, и что случается, если они попадают на компьютер. Не маленькие — должны уже сами все это знать:). А если не знаете, то спросите Google или Yandex. Наш сканер будет искать вирусы, которые не умеют заражать другие файлы, а под эту категорию попадают: черви, трояны, логические бомбы, кейлоггеры. Вы спросите, почему? Дело в том, что для обнаружения файлового вируса в первую очередь необходимо нехилое знание assembler'a для того, чтобы узнать алгоритм работы вредоносной программы. Как мы будем бороться с вирусом, если не знаем, как он устроен? Во-вторых, нужно знание структуры PE-файла. А это дело непростое, особенно на языках высокого уровня — таких, как Delphi (в нашем случае мы обойдемся простым расчетом контрольной суммы). И, в-третьих, мы же являемся начинающими. Сразу попытаться взять быка за рога у нас не получится. Я не хочу сказать, что это невозможно — просто всегда надо с чего- то начинать, а, как правило, начинают с простейшего. Теперь мы разобрались, какой антивирус будем делать, и пора бы перейти к самому главному — к алгоритму его работы. Как вы знаете, у любого антивируса есть своя база, в которой он хранит сигнатуры известных ему вредоносных программ (по сути, это называется контрольной суммой). Так вот это и позволяет ему отличать вирус от обычной программы. Контрольная сумма (CRC) — некое число (оно может быть 16-, 32-, 64-битным... да хоть 512-битным — все зависит от фантазии разработчика), которое характерно для участка кода. То есть у каждого файла своя контрольная сумма. Не существует универсального способа подсчета CRC — на самом деле их очень много, но мы рассмотрим те, которые подходят для наших целей. Наш антивирус умеет считать контрольные суммы. Что еще ему нужно? Правильно: ему нужны файлы CRC, которые он будет сверять с сигнатурами из базы. А чтобы найти файлы для сканера, существуют очень удобные api-функции, о которых ты, наверно, не раз слышал: FindFirst, FindNext, FindClose. Они позволят нам устроить глобальный поиск по всему жесткому диску. Останется только навешать разного рода украшений — таких, как окошко, сигнализирующее о том, что мы нашли вирус, противный звук, предназначенный все для того же, статистику и отчет о сканировании, — и "домашний" антивирус готов.

От слов к делу: CRC

В нашем случае выбор алгоритма расчета контрольной суммы особой роли не сыграет. Если, конечно, мы не собираемся делать коммерческий продукт, ведь тогда придется разрабатывать свой вариант подсчета CRC (мы же не хотим отваливать проценты автору алгоритма). Если все же нашлись желающие попробовать написать платный антивирус, то советую почитать "Элементарное руководство по CRC-алгоритмам обнаружения ошибок", автор — Ross N. Williams. Причем наш вариант должен быть очень надежным. Но т.к. в этой статье просто рассматриваются основные методики создания простейшего антивирусного сканера, то мы не будем изобретать велосипед и воспользуемся уже готовыми разработками. К счастью, таких в интернете очень много. Следующая функция является простой и быстрой реализацией контрольной суммы:

function FastCheckSum(FileName: string): DWORD;
var
F: file of DWORD;
P: Pointer;
Fsize: DWORD;
Buffer: array[0..500] of DWORD; // можно, конечно, и побольше взять
begin
FileMode := 0;
AssignFile(F, FileName);
// ассоциируем файловую переменную с файлом, с которым будем работать
Reset(F);
// открываем файл для чтения/записи
Seek(F, FileSize(F) div 2);
// ставим положение считывания/записи на FileSize(F) div 2 байт — в середину
//файла
Fsize := FileSize(F) — 1 — FilePos(F);
// FilePos(F) определяет текущую позицию в файле
if Fsize > 500 then
Fsize := 500;
// Если решите изменить размер массива Buffer, то и здесь тоже нужно менять
BlockRead(F, Buffer, Fsize);
// считываем из файла Fsize байт
Close(F);
//закрываем наш файл
P := @Buffer;
asm
xor eax, eax // обнуляем eax
xor ecx, ecx // обнуляем ecx
mov edi, p // в регистр edi помещаем значение переменной p
@again: //наша метка
add eax, [edi + 4*ecx]
inc ecx
// увеличиваем значение регистра ecx на 1 — своеобразный счетчик
cmp ecx, fsize // сравниваем, и, если не равны, то
jl @again //перейдем обратно — к метке
mov @result, eax
// а если равны, то результату работы функции присваиваем значение регистра eax
end;
end;

Лично мне этот вариант очень нравится — он достаточно несложный, но при этом во время его тестирования серьезных багов замечено не было. Плюс ко всему цикл реализован при помощи asm-вставок, что позволяет увеличить скорость работы (конечно, это можно было реализовать на Delphi, но нам важна скорость). Вообще особых вопросов по коду возникнуть не должно. Но все же для полного понимания я закомментировал некоторые участки. Для людей, которые сомневаются в данном алгоритме, я припас юниты (находится в архиве с исходниками), способные рассчитать CRC32 и CRC64. Разумеется, с примерами. А для любителей "самопала" могу предоставить следующий код, написанный буквально за минуту:

function sampleCRC(filename: string): dword;
var
File_: hFile;
ReOpenBuff: OFSTRUCT;
buffer: array[0..1024] of byte;
counter, summa, read: cardinal;
begin
summa := 0;
File_ := OpenFile(PChar(filename), ReOpenBuff, OF_READ);
// Открываем файл для чтения
ReadFile(File_, buffer, 1024, read, nil);
// Читаем наш файл
CloseHandle(File_);
// Закрываем его
for counter := 0 to 1024 do
begin
summa := summa + buffer[counter];
// побайтно складываем — не слишком оригинально:)
end;
result := summa;
end;

Конечно, данный алгоритм надежным назвать сложно, но его с легкостью можно усовершенствовать. К примеру, можно увеличить количество считываемых байт, а еще лучше создать динамический массив — тем самым станет возможным считывать контрольную сумму всего файла (хотя контрольной суммой я бы это назвал с трудом). Идей на самом деле много, но, во-первых, надо и вам дать подумать, а во-вторых, статья не резиновая:).

Антивирусная база

Антивирусная база — это сердце любого антивируса. Благодаря ей он спасает нас от злых ][акеров, которые так и норовят угнать наши пароли от аськи, мыла... Я долго думал: как же "прикрутить" к нашему антивирусу базу? Хранить сигнатуры и имена вирусов в самом Exe'шнике? Нет, это слишком уж примитивно! Надо, чтобы пользователь при желании мог ее обновить. В общем, немного подумав, я пришел к такому выводу, что проще всего все это дело хранить в текстовом файле.Да-да, именно в текстовом файле. И сейчас вы поймете, почему. Идея состоит в следующем: у нас есть два текстовых файла: в одном хранятся сигнатуры, а в другом — имена вирусов.


Файлы антивирусной базы. В vb.vdb хранятся сигнатуры, а в vn.vdb — имена вирусов

Несложно догадаться, по какому принципу они там расположены. (см. рис.). Первой сигнатуре соответствует первое имя, второй — второе и т.д. Когда мы будем писать процедуру сканирования, вы поймете, зачем я сделал такой выбор, а пока сделаем простенькую утилиту, при помощи которой будем добавлять новые вирусы в базу. Для начала кинем на форму два компонента Edit, два компонента Button и один Open Dialog. В обработчике нажатия кнопки, которая будет добавлять сигнатуру в базу, напишем следующий код:

procedure TForm1.AddToBaseClick(Sender: TObject);
var
sum: Dword;
bas: TextFile;
VirusName: textFile;
begin
Sum := FastCheckSum(Edit1.Text); \\ читаем контрольную сумму
AssignFile(bas, 'Base\vb.bas'); \\ ассоциируемся с файлом базы(сигнатур)
Append(bas); \\ открываем для записи в конец
Writeln(bas, sum); \\ собственно пишем
closeFile(bas); \\закрываем файл
AssignFile(VirusName, 'base\vn.bas');
Append(VirusName); // тут
Writeln(VirusName, Edit2.text); // все
closefile(VirusName); //аналогично
end;

Как видите, ничего в этом сложного нет. Мы только что написали собственную утилиту, добавляющую новые записи в нашу антивирусную базу. Для большей конспирации мои файлы имеют расширение *.bas (возможно, такой ход не понравится любителям Бэйсика). Наверно, вас смущает "прозрачность" файлов, т.к. их можно просмотреть блокнотом. Ничего страшного — кто вам мешает все это дело защитить простеньким xor-шифрованием? Сразу хочу заметить, что мы могли бы работать с типизированными файлами — например, создать свою структуру наподобие:

type
TMyBase = record
VirusName : string[30];
Signature : LongInt;
end;

... // и добавлять вирус в базу уже так:
Procedure AddToBase(VirName:String; VirSignature:integer);
var
table: TMyBase;
AntiVirBase: file of TMyBase;
begin
AssignFile(AntiVirBase, 'base.bas'); //ассоциируем, ну, уже должны, знаете
Reset(AntiVirBase); // открываем файл для чтения/записи
table.VirusName := VirName; // получаем значения
table.Signature := VirSignature; // которые будем записывать в базу
Seek(AntiVirBase, FileSize(AntiVirBase)); //переходим в конец
write(AntiVirBase, table); // и только теперь пишем
closefile(AntiVirBase); //ну конечно, закрываем нашу базу
end;

Теперь нашу базу открыть блокнотом не получится. В смысле, открыть-то можно, но ничего не поймем. Вам осталось решить, какой из приведенных вариантов выбрать — лично я предпочел первый из-за его простоты, хотя вы можете со мной не согласиться и использовать второй (дальше я буду рассматривать только первый случай, но если вы чуть-чуть напряжете свои мозги, то сможете адаптировать наш антивирус под оба способа). Теперь разберемся с OpenDialog. Кто знает, для чего он нужен, могут пропустить этот абзац, а кто нет — читайте. Значит, так: по нажатию второй кнопки должно вылезти диалоговое окно с выбором файла — зловредного вируса. Конечно, в Edit можно вводить вручную, но это так нудно. В обработчике нажатия кнопки наберите следующий код:

procedure TForm1.Button2Click(Sender: TObject);
begin
if OpenDialog1.Execute then // в общем, тут комментировать нечего:)
begin
Edit1.Text := OpenDialog1.FileName;
end;
end;

В принципе, мы можем этим ограничиться, а можем чуть-чуть улучшить наше диалоговое окно. Например, установить фильтры только на выбор *.exe- и *.dll-файлов:

procedure TForm1.FormCreate(Sender: TObject);
begin
with OpenDialog1 do
begin
OpenDialog1.Filter := 'Exe файлы (*.exe)|*.exe|Dll файлы (*.dll)|*.dll';
// ставим фильтры
end;

Хм-м… А вы не задумались, почему мы фильтры ставим только на такие файлы? Ведь есть черви, написанные на скриптовых языках (таких, как VBScript или JScript). Что нам мешает добавить сигнатуру этих вирусов? Ответ, как всегда, прост. Достаточно добавить в скрипт пустую строчку, и CRC уже будет другой. Тут надо анализировать сам текст скрипта, но это уже совсем другая история…

Neon, SA-Security gr., q@sa-sec.org


Компьютерная газета. Статья была опубликована в номере 30 за 2008 год в рубрике безопасность

©1997-2024 Компьютерная газета