ZF

                 ИСПОЛНЯЕМЫЙ КОД В АНТИВИРУСНОЙ БАЗЕ
                               by Redarc
Введение

    В данной    работе   предлагается   рассмотреть   один   из   путей
использования исполняемого кода во внешней  антивирусной  базе.  Пример
выполнен на языке Borland Pascal 7.0 for DOS. Статья рассматривает лишь
конкретный пример и не претендует на полноту исследований  и отсутствие
ошибок.
    Антивирусные программы-полифаги   используют   антивирусные    базы
данных.  В этих базах данных хранится информация,  которая используется
для детектирования  и,  возможно,  лечения  вирусов.  Базы  могут  быть
внутренними  или внешними.  Внутренние антивирусные базы корректируются
(обновляются)   непосредственно   вместе   с    антивирусом.    Внешние
антивирусные  базы  могут  корректироваться (обновляться) без изменения
кода  антивируса.  Алгоритм   использования   внешних   баз   позволяет
подключать  к  антивирусу  небольшие  дополнительные базы - ежедневные,
еженедельные и ежемесячные дополнения.  Примером полифага с  внутренней
базой  данных  может  являться  AIDSTEST  Д.  Н.  Лозинского.  Примером
полифага с внешней базой данных может являться AVP Е.  В.  Касперского.
Алгоритм  использования внутренней антивирусной базы можно посмотреть в
книге П.  Л.  Хижняка <Пишем вирус:  и антивирус>.  Мы же в  дальнейшем
будем рассматривать в основном внешние базы данных.
    Обычно антивирусные   базы   состоят   из    данных,    позволяющих
обнаруживать  вирусы (сигнатуры,  контрольные суммы постоянных участков
вируса и т.д.) и данных,  необходимых для лечения обнаруженного  вируса
(метод внедрения в жертву, местоположение оригинальных байт жертвы, тип
криптозащиты и т.д.). Однако в дополнениях к антивирусной базе возможно
(даже необходимо) использование исполняемого кода. Зачем этот код нужен
и как его использовать мы и поговорим более подробно.
    Исполняемый код  полифага  обычно содержит сервисные и библиотечные
процедуры.   Сервисные   процедуры   используются   для    диалога    с
пользователем, настройки функций антивируса, загрузки антивирусных баз,
поиска  и  тестирования  возможных   объектов   заражения,   алгоритмов
детектирования   и   лечения   вирусов   и  т.д.  Библиотечные  функции
используются  в  алгоритмах  детектирования  и   лечения   вирусов.   В
дальнейшем  будем  рассматривать  исполняемый  код в антивирусных базах
относительно к алгоритмам лечения.

Внешние антивирусные базы

    Целые группы вирусов можно лечить по одному и  тому  же  алгоритму.
Отличие  обычно  состоит  лишь  в данных (смещение точки входа в вирус,
смещение оригинальных  байт  программы,  количество  оригинальных  байт
программы  и  т.д.),  характерных  для  конкретного  вируса  или группы
вирусов.  Данные из антивирусных баз используются для выбора  алгоритма
лечения   (выбора  алгоритма  использования  библиотечных  процедур)  и
сообщения библиотечным процедурам характерной  информации  о  найденном
вирусе.
    В настоящий момент известны десятки тысяч  вирусов.  Десятки  новых
вирусов появляются ежедневно. Размеры баз современных антивирусов стали
настолько  большими,  что  еженедельная  или  (тем  более)   ежедневная
рассылка   пользователям  полной  обновленной  антивирусной  базы,  как
реакция  на  появление  в  <дикой   природе>   нового   вируса,   стала
нецелесообразной.  Да  и  зачем  пользователю  постоянно  присылать  те
антивирусные записи (антивирусная запись - элемент  антивирусной  базы,
позволяющей   обнаруживать  и  лечить  один  вирус  или  группу  схожих
вирусов),  которые уже имеются на его компьютере?  Дополнения позволяют
оперативно   обновлять   (дополнять)   антивирусные  базы  лишь  новыми
антивирусными записями.  Это более логично и более удобно.  Кроме того,
при  обнаружении ошибки в дополнении,  пользователю необходимо заменить
лишь <неправильное> дополнение,  а не  огромную  основную  антивирусную
базу и тем более не весь антивирусный пакет (в случае внутренней базы).
Для уменьшения  количества  внешних  файлов  с  антивирусными  записями
возможно   использование  кумулятивных  обновлений.  Суть  кумулятивных
обновлений  антивирусных  баз  заключается   в   присоединении   набора
обновлений к основной антивирусной базе данных. При этом также возможна
некоторая  оптимизация  данных.  Обычно  кумулятивные  обновления   баз
проводят  совместно  с  кумулятивным  обновлением  исполняемых  модулей
антивирусного  пакета  (поставка  пользователю  улучшенных  модулей  и,
возможно, изменение формата антивирусной базы данных).
    Однако эпизодически появляются новые  вирусы  (обычно  это  сложные
полиморфные вирусы или вирусы,  использующие новые методы проникновения
в систему и внедрения в жертву),  которые невозможно излечить имеющимся
набором  библиотечных  процедур.  Решить  эту  проблему  можно одним из
следующих способов:
    * модернизировать    код    антивируса   (добавить   новую
библиотечную функцию)  и  поставить   пользователям   модернизированный
исполняемый модуль вместе с новой антивирусной записью в базе;
    * создать  <временную>  антивирусную  запись  с  указанием
неизлечимости (временной  неизлечимости)  данного  вируса  (вирус будет
удаляться совместно с кодом жертвы);
    * передать антивирусу новую библиотечную процедуру.
    Первый способ наиболее  удобен  для  разработчиков  антивируса,  но
менее  удобен  для  пользователей.  Заплатить  за обновление антивируса
через Internet или  e-mail  пользователь  может  гораздо  меньше,  если
разработчики будут использовать второй или третий способ. Второй способ
идеален для детекторов (детектор - антивирус без  возможности лечения),
но  нарушает идеологию полифага (если вирус можно вылечить - значит его
нужно вылечить). Кроме того, пользователь не всегда может согласиться с
удалением    инфицированного    объекта,    что    грозит    дальнейшим
распространением вирусного кода.  Третий способ более рациональный, так
как   и   антивирус   обновится   (следующие  дополнения  смогут  также
использовать  новую  библиотечную  процедуру)  и   пользователь   будет
доволен.
    Передать новую библиотечную процедуру можно отдельно  от обновления
(в виде DLL,  Overlay или PlugIn) или вместе с обновлением (исполняемый
код внутри  обновления).  При  передачи  новой  процедуры  отдельно  от
обновления   антивирусу   необходим   дополнительный  код,  проверяющий
подлинность  и  сохранность   получаемого   внешнего   кода   (проверка
подлинности  гарантирует отсутствие ложных деструктивных дополнений,  а
проверка сохранности гарантирует отсутствие ошибок в  работе антивируса
из-за  порчи  дополнения).  Однако  в  антивирусе  должен  уже  иметься
подобный код,  использующийся при проверке  подлинности  и  сохранности
данных  в  дополнениях.  Было  бы  разумным использовать этот код и для
проверки  новых  процедур.  Поэтому  чаще  всего   новые   библиотечные
процедуры  поставляются  внутри  антивирусного дополнения.  Собственно,
такое дополнение и будет являться  плагином  (Plug  In).  При  поставке
пользователям кумулятивного обновления данных и кода или,  при поставке
новой версии антивирусного пакета,  данные из такого  обновления  будут
перенесены в основную антивирусную базу, а новая библиотечная процедура
(новые библиотечные процедуры) будут перенесены в  основную  библиотеку
процедур. Принципы создания, использования и защиты плагинов мы и будем
рассматривать в дальнейшем.

    Разработка и экспорт дополнения с внутренним исполняемым кодом

    Сразу хочу предупредить,  что описываемый дальше способ подключения
внешних   процедур   может   не   использоваться  (и  скорее  всего  не
используется) ни одним известным антивирусом.  Дело в  том,  что  здесь
будет  описано  подключение не просто подпрограммы,  а целой программы,
втиснутой в рамки процедуры.  То есть,  такая  процедура  может  вполне
самостоятельно  производить  все  необходимые  ей  действия  с экраном,
дисками,  файлами,  памятью и т.д.  Однако в качестве иллюстрации  этот
пример вполне годится.
    Внешняя процедура  может  быть  написана  на  низком  уровне   (при
условии,   что  она  будет  выполняться,  а  не  интерпретироваться)  и
использовать  все  необходимые  ей  функции   BIOS,   OS   и   Hardware
самостоятельно.  Однако  в  этом  случае  она  скорее  всего  не сможет
использовать готовый  набор  библиотечных  процедур,  что  не  является
рациональным решением.
    Внешняя процедура  может  быть  создана  в  некотором  p-коде  (или
написана  на  скриптязыке) и интерпретироваться антивирусом.  Для этого
необходимо разработать свой скрипт-язык с необходимой функциональностью
и   написать  собственный  интерпретатор.  Использование  VBA  (VBS)  в
антивирусе, как мне кажется, мало приемлемо из-за огромной избыточности
(антивирусу  столько  функций в дополнении не нужно),  большого размера
интерпретатора и доступности VBA (VBS) широкой общественности хакеров и
вирмейкеров   (что  явно  упрощает  написание  фальшивых  дополнений  с
деструктивными функциями).  Кроме того,  за использование коммерческого
скрипт-языка   необходимо   платить   деньги.   Написание   же   своего
скрипт-языка и  интерпретатора,  а  также  поддержание  его  в  рабочем
состоянии  (внесение  необходимых  дополнений  и  исправлений)  требует
некоторых усилий как в умственном,  так и в материальном плане J. Хотя,
крупные антивирусные фирмы вполне могут это себе позволить.
    Внешняя процедура может быть написана на некотором  внутреннем  для
антивируса  языке  и скомпилирована в p-код.  Сложности здесь такие же,
как и в предыдущем случае. К тому же добавляются сложности с написанием
встроенного языка и компилятора к нему.
    Внешняя процедура может быть  написана  на  том  же  языке,  что  и
антивирус  и  передана  ему as is.  В таком случае все дело упрощается.
Некоторая  сложность  возникает  разве  что  в  использовании   внешней
процедурой библиотечных (в том числе и системных) подпрограмм,  а также
нелокальных для этой  процедуры  данных.  Однако  это  довольно  просто
обходится, что мы и увидим ниже.
    Первое, с  чего  следует  начать  -  это  с   создания   библиотеки
подпрограмм,  которыми  будет  манипулировать наша процедура,  да и сам
антивирус тоже.  Допустим,  что  для  этого  святого  дела  мы  создали
отдельный  модуль  (ProcLib.Pas),  который  может  выглядеть  следующим
образом:

Unit ProcLib;

Interface

procedure LibProc1;
procedure LibProc2;

Implementation

procedure LibProc1;
begin
:
end;

procedure LibProc2;
begin
:
end;

end.

    Дальше следует  описать  все  структуры  данных,  к  которым  будем
относить следующее:
    * заголовок файла с дополнением;
    * структуру локальных процедур (структурирование  данных необходимо
в данном случае);
    * данные для поиска вируса (сигнатура, название вируса и т.д.);
    * структуру   формальных   параметров  внешней  процедуре;
структуры дополнительных данных; :
    Следует учитывать,  что  внешняя  процедура  не   должна   вызывать
какие-либо   системные   или  библиотечные  подпрограммы  и  переменные
напрямую (по фиксированному имени или  адресу),  так  как  в  менеджере
плагинов  (антивирусе)  адреса  этих  подпрограмм  и  переменных  могут
оказаться несколько другими.  Разумеется, что можно вручную настраивать
каждый  раз все офсеты,  но зачем это нужно?  Все можно сделать гораздо
проще.  Для этого можно использовать массив указателей на  системные  и
библиотечные  (pProcLib)  подпрограммы и массив указателей на системные
(pVarLib) переменные.  Можно так же использовать глобальные  переменные
для  программы  и  всех  плагинов,  но  описать их лучше в виде массива
структур (GlobVar).  Пример такого описания можно посмотреть  в  модуле
DataProc.Pas.
    Элементам массивов  указателей  на  подпрограммы  (InitProcLib)   и
переменные   (InitVarLib)   следует  присвоить  адреса  соответствующих
подпрограмм  и  переменных.  Указатели  на  эти   массивы   (чтобы   не
перегружать   лишний   раз  стек)  и  указатель  на  массив  глобальных
переменных (GlobVar) очень легко можно передать во  внешнюю  процедуру.
При этом мы отвязываемся от конкретных адресов и их настроек во внешней
процедуре.  Также можно передать в качестве VARпараметра  (в  принципе,
тот   же   указатель  на  данные)  -  собственные  данные  (DataRecord)
файладополнения.  В  этих  данных  можно  хранить  необходимые  внешней
процедуре  константы  и  использовать  эти структурированные данные для
локальных нужд.
    Теперь перейдём   к   непосредственному   созданию   экспортируемой
процедуры (EICAR.PAS).  Для  этой  процедуры  следует  установить  ключ
компиляции Force far calls,  чтобы компилятор генерировал полные адреса
вызовов.  Также придется описать структуры, для разыменования ссылок на
библиотечные  процедуры  и  переменные  (что  не обязательно,  в случае
передачи уже разыменованных ссылок, как показано в приложении), а также
ввести указатели на эти структуры и на адреса библиотечных процедур для
последующего  разыменования  ссылок.  Кроме  того,  необходимо  описать
специальные параметры-процедуры, которые позволят передавать формальные
параметры  библиотечным  подпрограммам.  В  общем  случае   это   может
выглядеть так:

{$F+}
    function EICARProc (pP,  pV,  pGV : Pointer; var DR : DataRecord) :
Byte;
TYPE
    pPA = ^PA;
    PA = array [1..MaxExtPro] of Pointer;
    pVA = ^VA;
    VA = array [1..MaxExtVar] of Pointer;
    pGA = ^GA;
    GA = array [1..10] of LocalVarType;
VAR
   pProcArray : pPA;
   pVarArray  : pVA;
   pLocVarArr : pGA;
   pLibProc : Pointer;

   I : Integer;
   W, W1 : Word;
   P, P1 : Pointer;
   F : File;

   LibProc : procedure;
   fProc0  : procedure (var F : File);
   fProc1  : procedure (var F :  File;  FileName :  String);
   fProc2  : procedure (var F :  File; var Buff; Size : Word);
   fProc3  : procedure (var P  :  Pointer;  Size  :  Word);
   fProc4  : function  (MemAddr, BlockAddr : Pointer;
                        MemSize, BlockSize :  Word)  :  Word;
   fProc5  : procedure (S :  String);
   fProc6  : procedure (var S : String);
   fProc7  : function (A, B : Byte) : Byte;
   fProc8  : function (A, B : Integer):  Integer;
   fProc9  : function (A,  B :  Word) :  Word;
   fProc10 : function (Param :  Byte) : Byte;
   fProc11 : function (Param : Word) : Word;
   fProc12 : procedure (OldName,  NewName :  String);
   fProc13 : function (FileName :  String) : Boolean;
   fProc14 : function (var F : File)  :  LongInt;
   fProc15 : procedure  (var F :  File;  NewPos : LongInt);
   fProc16 : function (Drive :  Byte) :  String;
   fProc17 : procedure  (Drive  :  Byte);
   fProc18 : procedure  (var  Drives  : DrivesSetType);
   fProc19 : function (P : Pointer) : Word;
   fProc20 : function (W : Word) : Byte;
   fProc21 : procedure (Path: String; Attr: Word;  var F:  SearchRec);
   fProc22 : procedure (var F : File; Size : Word);
   fProc23 : procedure (var SR : SearchRec);

BEGIN
     {Init}
     pProcArray := pP;
     pVarArray  := pV;
     pLocVarArr := pGV;
END;
{$F-}

    А теперь  самое  интересное  - описание алгоритма работы процедуры.
Как уже говорилось,  придется отказаться от прямых вызовов библиотечных
и  системных  процедур  и  переменных.  При наличии некоторой фантазии,
описание алгоритма становится даже несколько интересным занятием:

     {FindFirst}
     pLibProc := pProcArray^[C_FindFirst];
     @fProc21 := Addr(pLibProc^);
     fProc21(DR.LocalVar[1].TString,Archive,
     DR.LocalVar[1].TSearchRec); {I := DosError} pLibProc := pVarArray^
     [c_DosError]; I := Integer(pLibProc^); while I = 0 do begin
           {FindNext (SR)}
           pLibProc := pProcArray^[C_FindNext];
           @fProc23 := Addr(pLibProc^);
           fProc23(DR.LocalVar[1].TSearchRec);
           {I := DosError}
           pLibProc := pVarArray^[c_DosError];
           I := Integer(pLibProc^);
     end;

    Полученную процедуру можно проверить на работоспособность,  а затем
экспортировать во внешний бинарный файл. Например:

     {Test}
     InitProcLib;
     InitVarLib;
     EICARProc (@pProcLib, @pVarLib, @GlobalVar, fr.DR);
     {Write}
     Assign (f, 'CODE.BIN');
     ReWrite (f, 1);
     P1 := @EICARProc;
     P2 := @Block;
     MySize := _SignOffset (P1^, P2^, 65535, 4) + 3;
     BlockWrite (f, P1^, MySize, CW);
     Close (f);

    Самым сложным    является    определение   размера   экспортируемой
подпрограммы.  Однако я решил эту проблему следующим  образом.  Сначала
записываю   в   бинарный   файл  65535  байт,  а  затем  в  Hiew'е  ищу
последовательность команд:  leave / retf xxxx  (эта  последовательность
байт  характерна для описания подпрограммы,  поэтому все подпрограммы с
одинаковым  форматом  формальных  параметров  будут  иметь   одинаковую
последовательность байт в начале и в конце).  Для пущей надежности беру
первые четыре байта 0C9h,  0Cah,  0xxh,  0yyh и записываю их в байтовый
массив   Block.   В   программе,  создающей  экспортируемую  процедуру,
использую поиск этой комбинации  байт  с  помощью  функции  В.  Лохова.
Возвращаемое   значение  как  раз  и  является  размером  процедуры  (к
возвращаемому   значению   необходимо   добавить   SizeOf   (Block)-1).
Собственно  говоря - на этом можно было бы и закончить.  Однако остался
нерешенным вопрос с данными.
    Создаем переменную типа FileRecord и заполняем её поля:

     with fr do begin
          with HR do begin
               RecordSize := 0;
               DataSize   := SizeOf (DataRecord);
               CodeSize   := 0;
               VersionPIN := 1;
               CRCRecord  := 0;
          end;
          with DR do begin
               with Signature do begin
                    SigName := 'EICAR Test File';
                    CouSigByte := 7;
                    SigCode[1] := $37;
                    SigCode[2] := $7D;
                    SigCode[3] := $24;
                    SigCode[4] := $45;
                    SigCode[5] := $49;
                    SigCode[6] := $43;
                    SigCode[7] := $41;
               end;
               CouLocVar := 2;
               with LocalVar[1] do begin
                    TypeVar := _TVString;
                    TString := '*.com';
               end;
               with LocalVar[2] do begin
                    TypeVar := _TVString;
                    TString := 'Е<усббу?nе уЯСу Я  м<уд?  2001  ъ<с<?';
               end;
          end;
     end;

    Данные запишем  отдельно  от  заголовка.  Это  нужно для дальнейшей
коррекции заголовка.

     Assign (f, 'DATA.BIN');
     ReWrite (f, 1);
     BlockWrite (f, fr.DR, SizeOf (DataRecord), CW);
     Close (f);

    Корректируем заголовок:

     Assign (f, 'CODE.BIN');
     ReSet (f, 1);
     fr.HR.CodeSize := FileSize (f);
     fr.HR.RecordSize := SizeOf (HeaderRecord) + SizeOf  (DataRecord) +
                         fr.HR.CodeSize;
     Close (f);

    Записываем сам заголовок:

     Assign (f, 'HEADER.BIN');
     ReWrite (f, 1);
     BlockWrite (f, fr.HR, SizeOf (HeaderRecord), CW);
     Close (f);

    По большому счету можно сразу записать весь файл дополнения. Однако
для контроля лучше сделать это всё раздельно.  Теперь можно записать  и
полный файл дополнения (заголовок + данные + код).

     Assign (f, 'EICAR.BIN');
     ReWrite (f, 1);
     BlockWrite (f, fr.HR, SizeOf (HeaderRecord), CW);
     BlockWrite (f, fr.DR, SizeOf (DataRecord), CW);
     P1 := @EICARProc;
     BlockWrite (f, P1^, MySize, CW);
     Close (f);

    Вот и   всё.   Процедура  и  данные  готовы  к  передачи  менеджеру
(антивирусу).  Остальные блоки кода,  которые мы  еще  не  рассмотрели,
созданы  для  защиты  дополнения  от изменений (проверка подлинности) и
контроля от файловых ошибок при хранении и копировании  дополнения.  Об
этом мы поговорим чуть позже.

    Импорт дополнения с внутренним исполняемым кодом

    Подключение такого дополнения - вещь весьма тривиальная. Для начала
нужно найти и проверить все дополнения (Manager.Pas):

     InitProcLib;
     InitVarLib;
     FindFirst ('*.sig', Archive, SR);
     while DosError = 0 do begin
           Write (SR.Name, ' - ');
           if TestSig (SR.Name) then begin
           end else begin
           end;
           FindNext (SR);
     end;

    Больше всего  кода  придется  написать  на  проверку  подлинности и
качества дополнения.  Однако это оправданно.  Вы же  не  хотите,  чтобы
антивирус  повис при исполнении <битой> процедуры,  или чтобы некоторые
<добрые> люди подсунули бы вашим пользователям дополнение  с  троянской
начинкой? Вот и замечательно.

function TestSig (FileName : String) : Boolean;
begin
     Assign (f, FileName);
     ReSet (f, 1);
     if IOResult <> 0 then begin
        GError := 0;
        TestSig := False;
        Exit;
     end;
     Close (f);
     if FileSize (f) < SizeOf (FileRecord) then begin
        Close (f);
        Erase (f);
        GError := 1;
        TestSig := False;
        Exit;
     end;
     BlockRead (f, fr, SizeOf(FileRecord), CR);
     if fr.HR.RecordSize <> FileSize (f) then begin
        Close (f);
        Erase (f);
        GError := 3;
        TestSig := False;
        Exit;
     end;
     if fr.HR.DataSize  <>  SizeOf(FileRecord)  -  SizeOf(HeaderRecord)
        then begin Close (f); Erase (f); GError := 3; TestSig := False;
        Exit;
     end;
     if fr.HR.CodeSize <> FileSize (f) - SizeOf(FileRecord)  then begin
        Close (f); Erase (f); GError := 3; TestSig := False; Exit;
     end;
     Close (f);
     TestSig := True;
end;

    Назначение остального   кода   будет   пояснено   ниже.  Сейчас  же
рассмотрим подключение импортируемой  процедуры  в  случае  возвращения
проверкой значения <истина>:

           if TestSig (SR.Name) then begin
              GetMem (P, fr.HR.CodeSize);
              Assign (f, SR.Name);
              ReSet (f, 1);
              Seek (f, SizeOf (FileRecord));
              BlockRead (f, P^, fr.HR.CodeSize, CR);
              if (fr.HR.CodeSize  <> CR) or (CR = 0) then begin WriteLn
                 ('файл испорчен'); FreeMem (P, fr.HR.CodeSize);
              end else begin
                 Inc (CouPlugIn);
                 pExtern[CouPlugIn].T := _teFunc1;
                 pExtern[CouPlugIn].P := Addr(P^);
                 pExtern[CouPlugIn].S := fr.HR.CodeSize;
                 pExtern[CouPlugIn].F := fr;
                 @ExternFunc[CouPlugIn] := Addr (P^);
                 WriteLn ('подключен');
              end;
              Close (f);
              Erase (f);
              if CouPlugIn = MaxPlugIn then Break;
           end else begin
               case GError of
                    0 : WriteLn ('ошибка чтения файла');
                    1 : WriteLn ('ошибка в структуре файла');
                    3 : WriteLn ('файл испорчен');
               end;
           end;

    Всё готово для запуска. Стартуем!

     if CouPlugIn = 0 then begin
        WriteLn ('Плагины не подключены');
        Exit;
     end;
     for B := 1 to CouPlugIn do begin
         S := pExtern[B].F.DR.Signature.SigName;
         Write ('Стартует "', S, '" - ');
         ExternFunc[B] (@pProcLib,        @pVarLib,         @GlobalVar,
         pExtern[B].F.DR); WriteLn ('выполнен');
     end;

    Главное - после использования внешних процедур не забыть освободить
память J.

     for B := CouPlugIn downto 1 do begin
         P := Addr (pExtern[B].P^);
         CR := pExtern[CouPlugIn].S;
         FreeMem (P, CR);
     end;

    Вот и   вся   хитрость.  В  реальном  антивирусе  возможно  следует
продумать  алгоритм  подключения  плагинов.  Здесь  можно  выбрать  два
основных  алгоритма:  подключение  плагинов  по  мере  необходимости  и
выгрузка их из  памяти  после  использования,  а  также  загрузка  всех
плагинов в память перед началом работы программы.
    В первом случае экономится место в ОЗУ.  Антивирус  загружает  лишь
данные, позволяющие идентифицировать вирусы. По мере необходимости (при
нахождении соответствующего вируса) подгружаются данные для  лечения  и
импортируются процедуры, обеспечивающие это лечение.
    Во втором случае можно <собирать> антивирус целиком из плагинов (не
только  базы,  но  и  весь код антивируса может быть представлен в виде
набора плагинов). Однако это отдельная большая и очень интересная тема,
о которой мы сейчас не будем вести разговор.

    Защита дополнения от изменений

    Теперь следует  поговорить  о защите нашего дополнения от <внешнего
воздействия>  в  виде  хакера  с  длинными  ручками  или   <умирающего>
винчестера   с   <осыпавшейся>   дискеткой.   Проверка   подлинности  и
сохранности дополнения будет производиться с помощью контрольной суммы.
Это  наиболее  просто  и  довольно надежно.  Так как подобрать алгоритм
формирования контрольной суммы дело довольно  безнадежное.  В  качестве
контрольной   суммы   будем   использовать   табличные  значения  CRC32
(CRC32.Pas). Вернемся к модулю EICAR.PAS:

     Sum := 0;
     B := 0;
     Assign (f, 'DATA.BIN');
     ReSet (f, 1);
     repeat
           BlockRead (f, B, 1, CW);
           if CW = 0 then Break;
           Sum := crc_32 (B, Sum);
     until False;
     Close (f);
     Assign (f, 'CODE.BIN');
     ReSet (f, 1);
     repeat
           BlockRead (f, B, 1, CW);
           if CW = 0 then Break;
           Sum := crc_32 (B, Sum);
     until False;
     Close (f);
     Sum := crcend(Sum);
     fr.HR.CRCRecord := Sum;

    Для уменьшения размера дополнения и укрытия его от <подозрительных>
глаз проще всего использовать архиватор (LZH. PAS):

main ('E', 'EICAR.BIN', 'EICAR.SIG');

    Проверка контрольной суммы и разархивирование в  модуле Manager.Pas
также тривиальны:

     FSplit (FileName, Dir, Nam, Ext);
     S := Dir + Nam + '.bin';
     main ('D', FileName, S);
:
     ReSet (f, 1);
     Seek (f, SizeOf(HeaderRecord));
     Sum := 0;
     B := 0;
     repeat
           BlockRead (f, B, 1, CR);
           if CR = 0 then Break;
           Sum := crc_32 (B, Sum);
     until False;
     Sum := crcend(Sum);
     if fr.HR.CRCRecord <> Sum then begin
        Close (f);
        EraseFile (f);
        GError := 2;
        TestSig := False;
        Exit;
     end;

    Разумеется, что  простое  удаление  разархивированного  файла очень
ослабит защиту архиватором,  так как удаленный файл легко восстановить.
Поэтому важно сделать <хитрое> удаление разархивированных файлов, типа:

procedure EraseFile (var f : file);
var
   i : integer;
begin
     for i := 5 to 400 do
         ArrByteWrite[i] := 55 + Random (200);
     ArrByteWrite[1] := Byte (Ord ('K'));
     ArrByteWrite[2] := Byte (Ord ('i'));
     ArrByteWrite[3] := Byte (Ord ('l'));
     ArrByteWrite[4] := Byte (Ord ('l'));
     ReWrite (f, 1);
     BlockWrite (f,  ArrByteWrite,  SizeOf (ArrByteWrite),  CR);  Close
     (f); Erase (f);
end;

    Есть еще  одна  хитрость.  Это   перестановка   элементов   массива
указателей  (например,  перестановка  элементов  записи  Tenvironment в
приложении NewPlugIn) на библиотечные и системные функции и процедуры в
каждой  новой  версии  антивируса  и  дополнений к нему.  При этом даже
созданное кем-то ложное дополнение не сможет выполнить  свое  троянское
предназначение,  а  удачливому  хакеру  придется  свою  работу начинать
сначала.  Разработчику же антивируса такая смена  адресов  обойдется  в
несколько минут.  К примеру,  можно лишь пронумеровать элементы массива
не явным способом, а с помощью констант:

CONST
     C_AssignFile    = 1;
     C_OpenFileRead  = 2;
     C_OpenFileWrite = 3;
     C_ReadFromFile  = 4;
     C_WriteToFile   = 5;
     C_GetMemProc    = 6;
     C_FreeMemProc   = 7;
     C_SignOffset    = 8;
     C_ClrScr        = 9;
     :

procedure InitProcLib;
var
   i : integer;
begin
     for i := 1 to MaxExtPro do
         pProcLib[i] := nil;
     pProcLib[C_AssignFile]  := @_AssignFile;
     pProcLib[C_OpenFileRead]  := @_OpenFileRead;
     pProcLib[C_OpenFileWrite]  := @_OpenFileWrite;
     pProcLib[C_ReadFromFile]  := @_ReadFromFile;
     pProcLib[C_WriteToFile]  := @_WriteToFile;
     pProcLib[C_GetMemProc]  := @_GetMemProc;
     pProcLib[C_FreeMemProc]  := @_FreeMemProc;
     pProcLib[C_SignOffset]  := @_SignOffset;
:

    Однако, чтобы собственные дополнения работали  исправно, необходимо
иметь  признак  версии  антивируса  (обычно  - минимально и максимально
допустимые версии), с которым оно не будет конфликтовать в адресах. Для
этого  в  HeaderReg  нужно включить следующую переменную:  VersionPIN :
Word; {версия плагина} и проверять её в Manager.Pas

     if fr.HR.VersionPIN <> PlugInVersion then begin
        Close (f);
        EraseFile (f);
        GError := 4;
        TestSig := False;
        Exit;
     end;

    Вот и всё.  Разумеется,  что для настоящей защиты своих  дополнений
вирусологи  используют  более хитрые приёмы.  Но с основными принципами
защиты мы всё же  познакомились.  Для  любительского  антивируса  этого
вполне достаточно.


Выводы и заключение

    Приведенные в данной работе идеи и алгоритмы подкреплены работающим
примером.   Пример   заключается   в   изменении  выводимой  надписи  в
стандартном файле EICAR-TEST-FILE,  который  используется  вирусологами
для демонстрации работоспособности антивирусов.
    С помощью  приведенных  здесь  примеров   и   алгоритмов   возможно
написание   собственного   антивируса   с   <горячими>  и  не  очень  J
дополнениями,  готовыми восстанавливать программы даже  от  неизвестной
ранее антивирусу заразы, к которой он, естественно, еще не готов.
    Вариантов передачи  импортируемой   подпрограмме   библиотечных   и
системных  процедур  может  быть  довольно  много.  Пример  еще  одного
варианта работы с плагинами приведен в архиве NewPlagIn. В этом примере
показано  создание  записи типа TEnvironment,  в котором все глобальные
процедуры имеют аналогичный стандартному вид.  Это значительно упрощает
написание плагина, но несколько увеличивает его бинарный код.
    Следует также обратить внимание на тот факт, что плагины и менеджер
должны быть откомпилированы с одинаковыми прагмами компилятора, так как
полученный в разных ситуациях код может давать разный  эффект  за  счет
использования различных регистров.
    Необходимо заострить внимание на обработке ошибок  в  импортируемом
коде.  Возможно  возникновение  таких  ситуаций,  как  нехватка памяти,
отсутствие доступа к файлу,  деление на ноль  и  т.д.  Поэтому  следует
писать  плагины с минимальными требованиями к окружению,  а в менеджере
предусмотреть  контроль  возникновения  ошибочных  ситуаций  (например,
обработка Int 0h, Int 4h, Int 24h собственными процедурами).
    И ещё.  Принцип подключения  плагинов  делает  возможным  написание
программного   обеспечения  различного  типа,  где  требуется  гибкость
конфигурации.  Например,  при написании игр можно подключать персонажей
(спрайты,  алгоритмы поведения и прочее) в виде плагинов. При написании
вьюверов файлов с графическими форматами можно использовать плагины для
подключения   обработки   нового  формата.  Более  подробно  технология
импортирования кода и данных рассматривается в следующей статье (автор:
Sassa) на примере антивируса, написанного на языке Java.
    Хочу отметить ещё один тонкий момент  -  эта  технология  позволяет
писать  полиморфные вирусы на языках высокого уровня.  Однако я надеюсь
на  благоразумее  наших  читателей,  которые  не   будут   использовать
результаты наших исследований во вред компьютерному сообществу.

Ваш RedArc


(C) NF, 1998-2004