ZF

        Детектирование вредоносных программ: продвинутый подход
                               by DrMAD

                                            -... Твоим колпачком только
                                            ловить головастиков...

                                            А.Толстой. Золотой ключик

    Введение
    1. Основные принципы
    2. Синтаксический анализ
    3. Интерпретация эмулятором
    4. Анализ семантики
    5. Эвристическое распознавание
    5.1. Основные определения
    5.2. Выделение характерных признаков
    5.3. Проблема прерывности потока управления
    5.4. Логические методы
    5.5. Синтаксический подход
    5.6. Статистический подход
    5.7. Нейросети
    Заключение
    Литература
    Приложение. Пример.

                               Введение

     Сразу признаюсь,  что  все  написанное ниже не основано ни на чем,
кроме  как  на  здравом  смысле.  Нет   никакой   гарантии,   что   оно
соответствует   действительности,   имеющей   место   внутри   реальных
антивирусов.  Но по конференциям,  эхам и журнальным  страницам  годами
бродят  совершенно  чудовищные  (и  при  этом  абсолютно  неконкретные)
домыслы на  эту  тему,  а  никакой  достоверной  информации  нет  и  не
предвидится.
     В статье  нет  ничего  нового.  Идеи,  описанные  в  ней,  активно
развивались  лет  10-12  назад,  когда  по  миру ползали сотни и тысячи
примитивных "хижняков" и  прочих  вирусов-однодневок,  не  отличающихся
друг от друга ничем,  кроме как порядком расположения некоторых команд.
А математические основы этих идей вообще известны,  наверное,  лет  50,
если  не  больше.  Подозреваю,  что  за  прошедшие  годы с точки зрения
применяемых ИДЕЙ ничего нового так и не появилось. Навороченные червяки
и спамерское мыло сегодня детектируются в точности так же, как и 10 лет
назад детектировались "хижняки".
    Очень хочется,  чтобы на статью реагировали как на набор ИДЕЙ, а не
как  на  коллекцию  готовых  решений.  Если  какой-нибудь   современный
"кулхацкер" презрительно скривится, посчитав, что "если упоминается MS-
DOS,  то это прошлый век и поэтому никому  давно  не  нужно"...  скорее
всего, он просто не дорос до этой статьи. J
    Короче, эта   статья   -   призыв   подумать   и    _конструктивно_
покритиковать.

                         1. Основные принципы

     Как распознавать ИЗВЕСТНЫЕ вирусы см. в другой статье. Наша задача
распознать НЕИЗВЕСТНЫЙ вирус.
     Итак, перед нами программный файл.  Требуется попытаться правильно
ответить на вопрос: вирус это или не вирус?
     Решение задачи складывается из ряда шагов.

     Шаг 1.  Синтаксический  анализ.  Необходимо  корректно  РАСПОЗНАТЬ
команды и,  возможно, преобразовать их к какому-то более удобному виду.
     Шаг 2.  Интерпретация.  Неплохо бы ВЫПОЛНИТЬ распознанные команды,
поскольку  детектированию  подлежит поток тесно связанных друг с другом
команд,  т.е.  программа.  Но это  не  всегда  возможно,  и  не  всегда
необходимо.
     Шаг 3.  Семантический  анализ.  Необходимо  понять СМЫСЛ отдельных
команд и их групп.
     Шаг 4.   Прагматический  анализ.  Необходимо,  опираясь  на  смысл
отдельных команд и их групп, попытаться понять НАЗНАЧЕНИЕ алгоритма.

    Решение задачи   распознавания  естественным  образом  вытекает  из
результатов,  полученных на последнем шаге.  "В народе", как правило, с
удовольствием  обсуждаются  лишь  первые  полтора  шага,  да  и то - их
КОНКРЕТНЫЕ РЕАЛИЗАЦИИ.  Мы заглянем глубже и пойдем  дальше,  мы  будем
рассматривать ИДЕИ и МЕТОДЫ.

                       2. Синтаксический анализ

    Не стоит  сводить понятие синтаксического анализа только к проблеме
дизассемблирования инструкций i80x86 (или,  как сейчас чаще выражаются,
IA-16/32).  Ведь  декомпилироваться  могут программы на WordBasic-e или
VBA  (или  на  одном  из  их  промежуточных   кодов),   программы   для
микроконтроллеров мобильных телефонов и пр.  И вообще, в этой статье мы
попытаемся показать,  что  синтаксический  подход  при   детектировании
вирусов является одним из основных.
     Понятие "синтаксический  анализ"  всегда  подразумевает   проверку
соответствия каких-либо "фраз" какому-либо "языку".  Поэтому необходимо
более-менее формально ввести эти понятия.
     Произвольное множество  каких-либо элементов (букв,  цифр,  битов,
etc.) назовем "алфавитом",  а сами эти элементы  "символами  алфавита".
Комбинируя  элементы,  составляя  из  них  различные последовательности
(цепочки),  мы получаем различные "фразы". Некоторое четко ограниченное
подмножество   фраз   является   "языком",  остальные  фразы  считаются
неправильными с точки зрения этого "языка".
    Некоторые языки  можно  задать  формально  (язык инструкций i80x86,
язык Java,  язык собачих команд -  апорт,  тубо,  фас),  другие  -  нет
(естественные языки).  Если язык можно задать формально,  то его обычно
задают  в  виде  "грамматики".  Есть  несколько  нотаций  для   задания
грамматик,   например:   нотация   Ахо-Ульмана,  нотация  Бэкуса-Наура,
графическая нотация Вирта и пр.  Вот пример нотации Бэкуса-Наура (БНФ -
Бэкус-Наурова   форма)  для  задания  грамматики,  описывающей  правила
оформления целых чисел:

     <знак>  ::= + | - | <пусто>
     <цифра> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
     <цифры> ::= <цифра> [<цифра>]
     <число> ::= <знак> <цифры>

    Синтаксически проанализировать  фразу  означает - проверить цепочку
символов на соответствие грамматике.  Обычно  это  делается  средствами
конечных  автоматов.  "Автомат"  - это некоторый объект,  который может
находиться во множестве различных состояний S1,  S2...  SN, и в котором
происходят различные события E1,  E2... EM. Если с автоматом происходит
какое-то событие,  то  он  переходит  в  какое-то  состояние,  и  новое
состояние  зависит  от того,  1) в каком состоянии автомат находился до
прихода события,  и  2)  какое  именно  событие  произошло.  "Конечным"
автомат  называют   потому,   что   рано  или  поздно  он  переходит  в
"завершающее"  состояние  и  заканчивает  работу.  Для  синтаксического
анализатора события являются фактами поступления на вход автомата новых
символов алфавита.  Автомат удобно задавать в виде таблицы, в клеточках
которой  записываются  новые  состояния.  Вот пример для распознавателя
целых чисел (начальное состояние S1, завершающие S4 и S5):

        E1     E2      E3      E4
      <знак> <цифра> <иначе> <конец>
     -------------------------------
  S1 |  S2     S3      S5      S5
  S2 |  S5     S3      S5      S5
  S3 |  S5     S3      S5      S4
  S4 |  Это целое число
  S5 |  Ошибка

     Программируется автомат   очень   легко.  Можно  заполнить  массив
числами (номерами состояний) и, как любит выражаться Крис Касперски, по
нему  "скакать  блохой".  А  небольшой  автомат  можно  оформить в виде
нескольких вложенных "case" или "if"  (в  Приложении  приведен  пример,
использующий именно второй подход).
     Язык инструкций  i80x86  довольно  простой и жесткий,  поэтому для
него удобней всего  использовать  именно  такой  способ.  Но  если  вам
необходимо  декомпилировать текст на каком-нибудь высокоуровневом языке
(например,  на VBA),  то  самостоятельно  строить  автомат  (да  еще  и
программировать его на ассемблере) совсем не кРРРуто, а весьма гЛЛЛупо.
Не стоит пилить бревно перочинным ножом и точить  карандаш  бензопилой.
Для  этого  есть  специальные  пакеты,  например,  CocoL/R  или сладкая
парочка Lex/Yacc.  Что это  такое,  рассмотрим  на  том  же  примере  -
на распознавателе целых чисел.
     Для текстовых  языков  задачу проверки фразы на соответствие языку
обычно разделяют на два этапа:
     1) лексический  анализ,  во  время которого выделяются структурные
элементы, кирпичики текста - "лексемы" (или "токены"), например, <знак>
или <цифра>  (а  иногда  -  более  крупные  элементы,  типа <число> или
<ключевое слово>);
     2) собственно   синтаксический   анализ,   во    время    которого
проверяется, в правильном ли порядке эти лексемы расположены.
    Для создания   лексического   анализатора  служит  Lex.  Пусть  нам
необходимо  распознавать  вещественные  числа.  Из  каких  лексем   они
состоят?  Из  цифр  (назовем  эту лексему DIGITS) и знаков (назовем эту
лексему SIGNS).

     DIGITS [0-9]+
     SIGNS  [\+\-]*
     %%

    Операторный знак "+" означает "1 или больше",  знак "*" означает "0
или  больше",  а  "0-9"  и  "\+\-"  определяют  класс символов для этой
лексемы.  Запись  "\-"  вместо  просто  "+"  используется,   чтобы   не
перепутать  минус и плюс,  рассматриваемые как символы,  с операторными
знаками.  "%%"  -  символ-разделитель   секции   описаний   от   секции
лексических правил.
    Теперь определим,  как реагировать, если в исходном тексте появятся
эти  лексемы.  А реагировать надо просто:
    1) сохранить кусочек  строки,  удовлетворившей  этому  правилу  (по
умолчанию он уже живет в переменной yytext);
    2) сгенерировать номер лексемы (Yacc/Lex  нумеруют пользовательские
лексемы, начиная с 257).
    Делается это вставкой Си-шной команды в нужную строку грамматики:

    {SIGNS}{DIGITS}"."{DIGITS}    {save=strdup(yytext);return(257);}

    Далее эти 4 строки пропускаются через Lex и превращаются в ИСХОДНЫЙ
ТЕКСТ на языке Си для процедуры yylex(), умеющей распознавать описанные
лексемы  и  возвращать  значение  257,  если  встретилась  фраза   типа
"-123.4567".  Текст  процедуры  yylex()  выглядит  достаточно  дико для
человеческого глаза, он содержит числовой массив с описанием автомата и
строки  скакания  по этому масиву.  Но он соответствует самому древнему
стандарту на язык Си, и поэтому будет без проблем компилироваться любым
компилятром  в  любой  операционной системе.  Ясный перец,  описывать и
программировать этот автомат вручную пришлось бы несколько дольше.
    Для создания   синтаксического   анализатора   используется   Yacc.
Идеология использования в точности  такая  же,  как  и  в  Lex,  только
входной   язык   немножко   другой.   Пусть   нам   надо   распознавать
арифметические операции над вещественными числами (уже  выделенными при
помощи Lex). Вот описание грамматики:

    %{
    #include <stdio.h>
    float r;
    extern char *save;
    %}

    %start begin
    %token REALNUM
    %%
    begin: arifmop "=" {printf("%f",result);} ;
    arifmop:
      REALNUM {r=atof(save);} "+" REALNUM  {result=r+atof(save);}
    | REALNUM {r=atof(save);} "-" REALNUM  {result=r+atof(save);}
    | REALNUM {r=atof(save);} "*" REALNUM  {result=r+atof(save);}
    | REALNUM {r=atof(save);} "/" REALNUM  {result=r+atof(save);}
    ;
    %%

    Между "%{"  и "%}" размещаются те Си-шные строки,  которые хотелось
бы добавить к программе.  Ключевое слово "%start"  описывает  стартовое
синтаксическое   правило  грамматики  (в  данном  случае  оно "begin").
Ключевое слово "%token" описывает лексемы,  которые могут встретиться в
грамматике, и автоматически нумерация их идет, начиная с 257, что нам и
надо.  Знак "|"  разделяет  варианты  синтаксического  правила,  а  ";"
отмечает его конец.  Если прогнать эти строки через Yacc,  то на выходе
получится  Си-шный  исходник  процедуры  yyparse(),   которая   активно
обращается    к    процедуре    yylex().   В   процессе   интерпретации
синтаксического автомата в нужных местах  выполняются  Си-шные  строки,
которые  мы  вставили  в  грамматику,  а именно - строки,  производящие
вычисления.
    Осталось дописать модуль с main(),  скомпилировать  и  скомпоновать
все это вместе. И транслятор выражений типа "3.14*2.71=" готов!
     Крайне несложно модифицировать эту  грамматику,  чтобы  она  умела
транслировать  сложные арифметические выражения со скобками и т.п.,  на
это уйдет минут 15. Чтобы составить и отладить грамматику для WordBasic
-а,  уйдет ну,  например,  один рабочий день. Или два. А вручную писать
этот автомат можно неделями!
    Важно: еще раз замечу,  что декомпиляция с  языка  машинных  команд
i80x86 (дизассемблирование) - это тот крайне редкий случай, когда проще
написать собственный автомат,  чем использовать Lex/Yacc.  Но, как было
выше сказано и будет ниже продемонстрировано, синтаксический разбор при
детектировании вирусов встречается  не  только  при  дизассемблировании
команд Intel-овских процессоров.

                      3. Интерпретация эмулятором

     Собственно говоря,  мы  уже  частично  вышли  за   пределы   чисто
синтаксического анализа.  Связано это с тем,  что в клетках построенных
нами при помощи Yacc/Lex автоматов появились  не  только  номера  новых
состояний,  но и некоторые содержательные действия (Си-шные команды). В
простейшем   случае   это   может   быть   простой   вывод   на   экран
декомпилированной  команды,  т.е.  результат  перевода  входного  языка
в какой-то другой, - это действие называется "компиляцией".
     Но у  нас  встретился  более продвинутый случай - мы дополнительно
еще и ВЫПОЛНИЛИ некие команды,  заданные во входном тексте. Фактически,
мы   просто   реализовали   другой   тип  транслятора:  мы  реализовали
"интерпретатор".
     Нужна ли  при  распознавании  вирусов  "компиляция",  и  нужна  ли
"интерпретация". Как говорят истинные лондонцы: "хум хау". J
     Компиляция в  форме  перевода  двоичного языка в визуальный,  т.е.
отображение декомпилированных  команд,  нужна,  например,  для  отладки
антивируса. Также   это   очень   эффектная   вещь,   направленная   на
популяризацию антивируса.  В  частности,   отображать   декомпилируемые
команды умел MultiScan Валентина Колесникова.

 +-------------------------------------------------------------------+
 |            Х== 5==== Internal disassembler [+,-,.,.,.,] ===С[x]ё  |
 |            | AX 6246 BX 0000 CX 5BAD DX 7F8B SI 0000 DI 0000| VK| |
 |      Обнару| SP 3112 BP 0000 CS D222 SS 1A5D DS 1A5D ES 1A5D| AV| |
 |            +------------------------------------------------+---+ |
 |     Х=M21==|04C5 80FC0B         CMP    AH,0B                |O=0| |
 |     |      |04C8 7203           JC     04CD                 |D=0| |
 |     |      |04CA 50             PUSH   AX                   |I=0| |
 |     | Верси|04CB EB1B           JMP    04E8                 |T=0| |
 |     |Автор:|04CD 50             PUSH   AX                   |S=0| |
 |     |  Укра|04CE 53             PUSH   BX                   |Z=0| |
 |     |      |04CF 8ADC           MOV    BL,AH                |A=0| |
 |     +------|04D1 B700           MOV    BH,00                |P=0| |
 |      ******|04D3 2E8A872204     MOV    AL,CS:[BX+0422]      |C=0| |
 |            |04D8 B400           MOV    AH,00                |   | |
 |D:\HOWCAT\MS|04DA 5B             POP    BX                   |   | |
 |D:\HOWCAT\MS|04DB 05F404         ADD    AX,04F4              |   | |
 |D:\HOWCAT\MSФ================================================П===ѕ |
 |                                                                   |
 +-------------------------------------------------------------------+

     Нужно ли исполнение команд? Видимо, да.
     Первая причина  заключается в том,  что программа - не статический
объект, и  распознавать  ее  в  общем  случае   статическими   методами
невозможно. Самый  простой  пример  -  самомодифицирующиеся  программы,
полиморфные вирусы.  Статичная сигнатура появляется в них только  после
выполнения определенного количества команд, не ранее.
     Вторая причина заключается в том,  что только таким образом  можно
легко распознать смысл  команд. Но об этом речь пойдет дальше.
     Итак, выполнение команд.  Как нетрудно  догадаться,  возможно  оно
двумя способами.
     Первый заключается   в  том,  чтобы  честно  выпонять  команду  за
командой,  как это делают трассирующие отладчики, повесившись на Int 1.
Это возможно и в DOS, и в Windows. Другой вариант - повеситься на Int 3
и засовывать байт CCh в начало каждой очередной команды.  Это  немножко
разные технологии,  но суть остается одной и той же: в любом случае это
ни что иное,  как аппаратная трассировка РЕАЛЬНЫХ КОМАНД программы. При
работе отладчика за экраном следит человек,  который направляет процесс
трассировки и  останавливает  его  при  необходимости.  Если  программа
заполненая  РЕАЛЬНЫМИ  обращениями  к  внешней среде,  т.е.  к РЕАЛЬНЫМ
физическим устройствам ЭВМ,  к РЕАЛЬНЫМ областям памяти  и  т.  п.,  то
только   человек   может  определить,  позволительно  ли  трассировщику
выполнять  соответствующие  команды.  А  если  человека  нет,  а   есть
антивирус?  Запускать  под  автоматической  (без человека) трассировкой
FORMAT.COM, чтобы проверить, не вирус ли это, было бы рискованным. J
     Второй подход   заключается   в   том,   чтобы  выполнять  команды
"понарошку",  по мере возможности моделируя внешнюю среду - устройства,
память и пр. Этот процесс получил название эмуляции кода. Реальный диск
при этом реально отформатировать вряд ли получится (и это слава Богу!),
но кодоэмуляторы втыкаются в противоположную проблему - в необходимость
адекватно моделировать ВЕСЬ компьютер (и это черт побери!).  Видимо,  в
полной  мере  это  сделать  невозможно.  Не  случайно  самым  простым и
популярным способом обмана  антивируса  является  обращение  вируса  ко
внешнему устройству. Например, это делается так:

Subr:
     in al, 40h  ; Младший байт счетчика таймера
     mov ah, al
More:
     in al, 40h  ; Старший байт счетчика, он нам не нужен
     in al, 40h  ; Опять младший байт
     cmp al, ah
     jnz More
     sub ah, al
     ret

     Вопрос на засыпку: что появится в ah после вычитания? Ответ: число
2.  (Мне не повезло - на моем медленном компьютере получается 4 или 6).
Можно   ли   создать   программную  модель  таймера,  каждые  1.19  мкс
вычитающую из   счетчика    двойку?    IMHO,    нет.    Засуньте    эту
последовательность в цикл расшифровки:

     mov esi, offset Decrypt
     mov ecx, CryptLen
     call Subr
Repeat:
     xor  [esi], ah
     inc esi
     loop Repeat

    и можете  быть уверены,  что антивирус не проэмулирует работу этого
расшифровщика.  (Т.е.  не выполнит этот  цикл  в  "неизвестном"  вирусе
автоматически и не опознает в нем потенциальную заразу,  но после того,
как вирус попадет  в  руки  вирусологу,  тот  использует  в  своем  а/в
процедуру, которая элементарно расшифровывает код безо всяких обращений
к таймеру).
     Только один   таймер  позволяет  создать  и  использовать  ДЕСЯТКИ
различных трюков, а ведь кроме таймера есть еще часы реального времени,
последовательный  и параллельный порты,  клавиатура,  контроллеры HDD и
FDD, видеоадаптер, звуковая карта...
     Ну ладно,  поглумились над аверами и  хватит.  Давайте  рассмотрим
проблему   последовательно.   Сначала  предположим,  что  мы  эмулируем
DOS-овскую программу.
     Как эмулировать  работу  с  регистрами  процессора?   Элементарно.
Завести структуру,  аналогичную типу Registers в Паскале или Си,  и при
распознании строки типа

                             MOV AX,1234h

смело в своем  Паскалевом эмуляторе выполнять

                             R.AX:=$1234.

     Как эмулировать  работу с памятью?  Простейший (но очень медленный
способ):  выделить на диске файл размером  1Мб,  скопировать  туда  всю
оперативку и, встретив что-то типа

                             MOV [21h*4], 1234h

поступать в Си-шном эмуляторе так:

                             tmp = 0x1234;
                             lseek(f, 0x84);
                             write(f, &tmp, 2);

     Но тут  тоже  не   все   просто,   например,   в   ячейке   0:46Сh
подсчитывается число таймерных прерываний.  Фактически, фрагмент памяти
с сегментом 40h - компонент связи с внешними устройствами,  а с ними не
все так просто, и о них речь пойдет дальше.
    Для скорости можно выделить 1Мб в  расширенной  памяти  и  работать
известными  методами (EMS/XMS и т.п.) с этим фрагментом.  Так,  видимо,
поступает утилита AVPDOS32.  Кстати,  она до сих пор прекрасно понимает
современные  KAV-овские  базы  и  работает с ними без проблем!  Вообще,
отказ от поддержки этой утилиты в пользу многомегабайтных GUI-мамонтов,
IMHO,  чисто  маркетинговое  решение  и любые доводы "за GUI" и "против
консоли" - полное фуфло. Фи, господин Касперский! Позор и ганьба! K
    Как эмулировать  работу  с  внешними устройствами?  В общем случае,
никак. K Как уже отмечалось, для этого надо как минимум знать полную и
адекватную  модель  внешнего  устройства,  каковой не знает,  наверное,
никто. В частности, работа того же таймера может зависеть от его режима
(вы знаете, что микросхема таймера может работать в 6 режимах, и только
по умолчанию работает в режиме номер 3?),  от BIOS-а (а вы знаете,  что
на   некоторых   ноутбуках   по   умолчанию  некоторые  каналы  таймера
инициализируются в режим номер  2?),  от  чипсета  (а  вы  знаете,  что
некоторые  чипсеты  поддерживают работу двух независимых таймеров?),  и
пр.  Еще хуже обстоит дело с COM-портом,  а про  CMOS-память  я  вообще
молчу.  Не надо забывать еще,  что внешние устройства предназначены для
работы с внешними миром, и поэтому их свойства и состояние определяется
не только изнутри компьютера,  но и извне его. Короче, полная труба. K
В лучшем случае можно составить список из  нескольких  десятков  портов
ввода-вывода,  назначение которых жестко определено и поведение которых
предсказуемо. В любом случае, это капля в море, потому что регистров, с
которыми невозможно так поступить - сотни. (Если бы это было не так, то
в составе  Windows  давно   существовал  бы  один  общий  универсальный
драйвер для всех моделей видеоадаптеров, звуковых и сетевых карт, etc.)
Тем не менее,  IMHO,  в  уважающем  себя  антивирусе  по  крайней  мере
минимальный набор регистров длжен быть проэмулирован.
     Как эмулировать  системные   вызовы?   Тоже   практически   никак,
особенно,  если  учесть,  что  большинство  из  них  взаимодействуют со
внешними устройствами.  Можно проэмулировать только  небольшой  процент
их, те, которые ни с чем не взаимодействуют и ничего не видоизменяют, а
только возвращают какую-нибудь информацию. Ну, например, в DOS-е это ah
=52h/Int  21h  -  запрос,  который возвращает адрес системной структуры
List Of Lists,  или Ah=30h/Int 21h - запрос,  который возвращает  номер
версии DOS.  Кроме того,  такие вызовы, наверное, не стоит моделировать
(ведь при  этом  пришлось  бы  воспроизводить  операционку!),  а  можно
реально  выполнить  и  передать  в  модель  результат выполнения.  Ну и
вообще,  поскольку системные вызовы,  как правило, тесно связаны друг с
другом  (бессмысленно,  например,  разрешать  открытие файла на запись,
если раньше в той же  программе  было  запрещено  открытие  его  же  на
чтение)  то,  списочек  таких  эмулируемых  сервисов получится ну очень
маленький. K
     И вот  тут  имеет  смысл  выплеснуть  в  полученную  бочку   дегтя
ма-а-аленькую  ложечку очень сладкого меда.  J Дело в том,  что есть и
еще один  подход  к  эмулированию  работы  с  внешними  устройствами  и
системных  вызовов.  И  он  мне  представляется  хотя и более сложным и
рискованным,  но  зато  очень  полезным.  Невозможность  проэмулировать
некоторые   операции   заключается  в  том,  что  после  их  исполнения
становятся неопределенными значения  некоторых  регисторов  и  областей
памяти. Например, после

         dw 310Fh ; код команды RDTSC

     накопленное с момента включения компьютера количество процессорных
тактов  помещается  в EDX:EAX.  Это значение нельзя никак вычислить или
предсказать средствами  виртуальной  машины.  Ну  и  не  надо!  В  этой
ситуации  надо  просто  пометить,  что  команда RDTSC якобы выполнена и
взвести у регистров EDX и EAX флажки "значение неопределено", продолжив
выполнение  программы с неопределенными значениями регистров.  Понятно,
что эти значения  рано  или  поздно  где-то  в  программе  используются
(например, в вычислениях) и неопределенными станут также значения каких
-нибудь других регистров и областей памяти.  Ничего страшного,  ведь  в
программе  существует  и  обратный  процесс  - замещение неопределенных
какими-то конкретными значениями,  так  что  не  исключено,  что  через
несколько  шагов  неопределенность  исчезнет.  Так  что рано пугаться и
прерывать эмуляцию программы.  Такой "храбрый" способ эмуляции принесет
нам  немало  пользы  при дальнейшем рассмотрении методов детектирования
вирусов.

    ...Важное замечание:    все,    что    выше    сказано,    касается
"нормального"  антивируса, работающего в "своих" условиях.
    Например, так обстоит дело с антивирусом,  работающим в MS-DOS, для
поиска DOS-оских вирусов (например,  для старых версий АВП и ДрВеб). Но
переходя в защищенный режим, антивирус имеет в своем распоряжении режим
V86 - т.е. возможность организовать для тестируемой программы отдельное
адресное пространство с моделями таблицы вектров прерываний,  BIOS-а  и
т.п.  -  и,  как следствие,  возможность мониторинга доступа к портам и
организации вирутальных  устройств.  Имеется  ли  это  в  популярных  и
распространенных   антивирусах  (КАВ,  ДрВеб,  НАВ  и  т.п.)?  Судя  по
проведенным тестам - нет.  Просто потому что эра MS-DOS  уже  прошла  и
тратить время на довольно сложные эмуляторы бессмысленно.
    То же самое можно сказать про антивирус для Windows, работающий под
Windows.  Наверное, можно крепко напрячься, и написать некую надстройку
над  Windows,  этакую  операционную  метасистему,  типа  VMWare,  но...
Представьте себе,  что антивирусу,  работающему в Windows 95, подсунули
вирус, ориентированный на файловые потоки NTFS - он что, должен Windows
NT  эмулировать?.  Представьте  себе,  что  антивирусу,  работающему на
процессоре  Intel,  предложили  вирус,  ориентированный  на  расширения
процессора  AMD  -  он  что,  должен все известные в природе процессоры
полностью эмулировать?  Особенно  с  учетом  того,  насколько  "охотно"
Microsoft   и   Intel   делится   своей   "кухней",  100%-ная  эмуляция
представляется НЕРЕАЛИЗУМОЙ.
     Кроме того,  весьма  вероятно,  что  полная эмуляция невозможна по
очень простой причине - это только  Мюнхгаузен  способен  сам  вытащить
себя из болота за волосы, а компьютер сам себя смоделировать не может в
принципе.  Компьютер может смоделировать только некотрое _подмножество_
самого себя, при этом затрачивая часть ресурсов, недоступных модели, на
поддержку этой самой модели. 
    Разработчики антивирусов  являются  горячими  энтузиастами  идеи  о
возможности осуществления 100%-ной эмуляции,  но ни в КАВ, ни в НАВ, ни
в ДрВеб,  ни в других популярных антивирусах этого не  наблюдается.  По
крайней   мере,   эти  антивирусы  не  умеют  в  режиме  эмуляции  даже
раскручивать  криптовщики   типа   UPX   или   ASProtect,   напичканные
антиотладочными трюками,  - эта раскрутка осуществляется более простыми
и надежными средствами (просто,  опознав упаковщик, антивирус "снимает"
его  специальной процедурой по жесткому алгоритму).  А будь реализована
полная виртуальная модель компьютера -  разве  это понадобилось бы?!...

     Рассмотрим также  вопрос  "глубины  эмуляции",   т.е.   количества
эмулируемых  команд  в  программе.  Речь идет о том,  когда и при каких
условиях следует завершать эмуляцию.
     Ну, во-первых,  это  определяется  целью эмуляции.  Эмуляция - это
ведь подчиненный механизм,  направленный на решение конкретной  задачи.
Например, если антивирус удостоверился,  что перед ним 100%-ная зараза,
эмуляцию можно прекращать.  Поэтому обычно эмулируется  лишь  маленькая
часть подозрительной программы.
     Во-вторых, эмуляцию  стоит  (не  всегда!)  прекратить  в ситуации,
когда эмулятор вступает в область непредсказуемости.  Например, когда в
программе   пошли   INT-ы   и   IN/OUT-ы.   Для   вирмеров   это  повод
позлорадствовать и использовать на всю  катушку,  для  вирусологов  это
повод поскрипеть зубами и засучить рукава.
     В-третьих, эмуляцию надо прекращать в ситуации,  когда она слишком
уж затянулась  и  дальнешее  ее  выполнение   бессмысленно.   Например,
однозначно надо рано или поздно отрубать эмулятор в ситуации:

                           Label: jmp Label

     Также имеет смысл прекратить эмуляцию,  если продолжительное время
не происходит  ничего подозрительного - что характерно для "нормальных"
программ. Как все это делается? Два варианта.
     Первый вариант   -   считать   общее  количество  шагов  эмуляции.
Например,  завершать эмуляцию, выполнив 65535 шагов и выполнив такое же
количество _разных_ команд.  Важно,  что _разных_ ! В самом деле, вирус
ведь может спрятаться за 64К NOP-ов или за 64К холостых циклов.
     Второй вариант,  более  с  этой  точки зрения предпочтительный,  -
отводить на  эмуляцию  определенный  интервал  времени.  Повеситься  на
таймер  и принудительно отрубать кодоэмулятор через 1 секунду.  "Кончил
или не кончил дело, все равно - слезай с тела" J
     Наконец, эмуляцию стоит выключить, если эмулируемая программа сама
завершилась естественным образом.
    Все, сказанное   про  DOS,  практически  полностью  относится  и  к
Windows,  за исключением того,  что тут,  пожалуй, потеряна возможность
эмулировать  память,  чужую по отношению к процессу,  а также системные
структуры (TIB/TEB/PEB,  таблицы дескрипторов и т.п.).  Нельзя сказать,
что она потеряна совсем уж полностью, и что-то частично можно было бы и
промоделировать.  Например,  снабдить модель  копией  TEB/PEB,  но  где
гарантия,  что  программа  (вирус) через нее не полезет искать в памяти
KERNEL32,  который в этом случае тоже придется  моделировать?...  Кроме
того, формат и правила доступа к различным фрагментам памяти зависят от
типа операционки - в Windows 95 они одни,  а в Windows NT -  другие,  а
программа и антивирус и там, и сям одни и те же.  Т.е.,  в принципе все
это возможно...  но потребовало бы неимоверных усилий от  разработчиков
антивируса, да и жуткого напряжения всех ресурсов компьютера (например,
самого критичного - ПРОЦЕССОРНОГО ВРЕМЕНИ). А кому это надо?
     Анализ работы современных антивирусов показывает,  что именно  эти
проблемы  и  стоят  перед  их  авторами,  и  решаются  не всегда лучшим
образом.

                          4. Анализ семантики

    Работающая программа   выполняет  множество  различных  действий  -
изменяет  значения  регистров,   флагов  процессора,  областей памяти и
т.п.  Но  все  ли  они  должны  учитываться  при распознавании вирусов?
Конечно  же  нет!  При  распознавании   используются   так   называемые
"дискретно-событийные"  модели вирусов,  и далеко не каждое программное
действие в рамках такой модели получает статус "события".  Но  об  этом
пойдет  речь  в следующем разделе,  а наша задача сейчас - разобраться,
как именно распознавать смысл отдельных команд и их комбинаций.
    Это возможно двумя способами - динамическим и статическим.
    Динамический. Проще всего смысл программных фрагментов распознается
в режиме пошагового выполнения программы (аппаратным трассировщиком или
кодоэмулятором).  Фактически,  для этого достаточно знать два состояния
объекта  -  ДО  выполнения фрагмента,  и ПОСЛЕ него.  Например,  Z0mbIE
насчитал 21  способ  обнулить  регистр  (на  самом  деле  их  гооораздо
больше).  Пусть  для  определенности  это  будет регистр AX.  Тогда для
распознания события "обнуление AX" достаточно проверить два условия: 1)
сначала было AX<>0; 2) а потом стало AX=0. И абсолютно без разницы, как
именно это было достигнуто:  <PUSH 0/POP AX>,  или <MOV AX,0>, или <XOR
AX,AX>, или еще как-нибудь.
    Если же динамический такой подход по какой-либо причине недоступен,
то  остается  только одно - статически распознавать действия по заранее
известным жестким "шаблонам", описывающим типичные последовательности и
форматы   команд.  Фактически,  речь  идет  о  разработке  для  каждого
отдельного  случая  отдельной  грамматики,   о   реализации   для   нее
распознающего  автомата и о применении его к коду программы.  Например,
грамматика для события "обнуление AX" может выглядеть так:

    <Обнуление AX> ::= XOR AX,AX | SUB AX,AX | MOV AX,0

    Можно до безобразия усложнить ее, добавив <PUSH 0/POP AX>, <AND AX,
0> и другие варианты,  в любом случае список окажется неполным. Утешает
одно:   в   99%   случаев   для  обнуления  регистра  AX  действительно
используются всего 3-4 варианта.
    Надеюсь, понятно,  что речь в этом примере идет не о текстах,  а  о
машинных  кодах  вирусов.  Поэтому  выражения  типа  <Регистр>  следует
воспринимать именно как лексемы,  как структурные  единицы  метатекста,
как  битовые  поля,  выделяемые лексическим анализатором в байтах кода.
Ну, если непонятно, вот пример из американского патента (принадлежащего
фирме Symantec): обобщенная маска для распознавания записи в файл.

                  assembly language          machine code

                  MOV DX, ????               BA ?? ??
                  MOV CX, ????               B9 ?? ??
                  MOV AX, 4000               B8 00 40
                  INT 21                     CD 21

    В этом  примере лексеме <MOV DX,> соответствует конечно же не текст
'MOV DX,', а битовое поле 0xBA. А "??" обозначают переменный байт.
    В итоге получится  довольно  сложная,  негибкая  и  малоэффективная
программа,  которая,  тем не менее,  все-таки будет способна худо-бедно
выполнять свою задачу.  И такие программы были и есть!  Если судить  по
отдельным   репликам   в   древних  СофтПанорамах,  именно  с  подобных
экспериментов начинались 15 лет назад ранние версии TBAV,  Lie Detector
и пр. (Кстати, автор последнего - Е.Сусликов aka SEH, родитель великого
HIEW-а).  Элементы подобного подхода в середине 90-х годов предлагались
в  дипломе  П.Семьянова и использовались в AVSP А.  Борисова.  Наконец,
американские разработчики НАВ  просто-напросто  запатентовали  подобную
технологию  (как часть более общей технологии эмуляции и эвристического
анализа)! Не стали бы они патентовать полную лажу, верно?
    Вообще, сконструировать  и   заставить   работать   такую   систему
распознавания - очень нетривиальная теоретическая и практическая задача.
Любители удалять гланды через задний проход, - вперед!
    Впрочем, при распознавании вируса по тексту (например,  когда  речь
идет о макровирусах) такой подход более чем приемлим,  например, как ни
крути,  а  события  .Import/.Expоrt   и   MacroCopy   распознаются   (в
незашифрованном вирусе!) безо всякой эмуляции.

                    5. Эвристическое распознавание

     В общем  и  целом  есть  два подхода к распознаванию чего бы то ни
было:
     1) структурный;
     2) статистический.

    Структурный подход подразумевает, что человеку заранее известно, из
каких элементов состоит распознаваемый объект, и в каких отношениях они
находятся.  Человек  целенаправленно формирует "систему знаний" об этом
объекте,  которую в дальнейшем использует для  распознавания.  Качество
распознавания зависит прежде всего от того, насколько адекватно человек
знаком со структурой объекта, и насколько корректно оформил эти знания.
Например,  при распознавании символов человек заранее знает,  что буква
"А" - это две наклонные палочки,  соединенные вершинками,  и поперечная
перекладинка  между  ними.  Человек описывает эту систему знаний в виде
уравнений векторной алгебры и получает отличную  систему распознавания,
которая   уверенно   распознает   самые   разнообразные,  искривленные,
искаженные и полустертые варианты буквы "А". Хорошо? Да не совсем. Дело
в  том,  что  в  точности то же самое векторное описание имеет "квантор
общности",  только он  перевернут  по  отношению  к  букве  "А"  кверху
тормашками.  Человек  указал  взаимную  ориентацию  векторов,  но забыл
указать общую ориентацию буквы.  Пришлось дописывать систему уравнений.
Теперь нормально?  Нет,  вот другая проблема: в славянской азбуке буква
"А" выглядит по  другому.  Для  распознавания  двух  сортов  буквы  "А"
человеку необходимо полностью переписывать всю систему правил, и т.д.
    Статистический подход  подразумевает,   что   распознающая   модель
строится   в   процессе  "обучения".  Берется  исходная  модель,  и  ей
предъявляется  большое  количество   типичных   образцов,   воспринимая
которые,  модель  видоизменяется,  подстраивается  под  типичный  образ
объекта.  Например, берется сетка 8х8 и через нее рассматриваются сотни
и  тысячи  различных вариантов буквы "А".  Постепенно формируется образ
буквы "А" в виде частот встречаемости  темной  точки  в  той  или  иной
клетке  или  в  той  или иной группе клеток.  К статистическому подходу
относятся  и  так  называемые  методы  "кластеризации",  когда   модель
_самообучается_ в процессе сравнения отдельных образцов друг с другом и
раскладывания их в "кучки" по признаку "похожести".  И в том, и в другом
случае   система   знаний   формируется   сама   в   процессе  обучения
(самообучения).  Участие в этом  процессе  человека  чисто  формальное.
Исключается  субъективный  фактор  (т.е.  влияние  человека на качество
распознавания), остается голая статистика.
    В разных  антивирусах в разные времена использовались, используются
и будут использоваться и тот, и другой подход.
    Эти подходы  в  той  или  иной  степени  напоминают  путь,  который
проходит человеческая  мысль  при  решении  новой  для  него  проблемы.
Поэтому  методы  анализа  объектов,  основанные  на  подобных подходах,
иногда  называют  "эвристическими"  от   греческого   слова   heurisco,
обозначающего "познание".

                       5.1. Основные определения

    Введем ряд определений.
    Состояние -   фиксированный   набор   значений  атрибутов  объекта.
Атрибуты программы  -  это  значения  регистров,   флагов   процессора,
переменных памяти, содержимое стека и т.п.
    Событие - смена состояния объекта,  т.е.  смена  значения  хотя  бы
одного атрибута  объекта.  Например,  MOV  AX,AX  -  не  событие (хотя,
конечно, значение IP все-таки меняется),  а NEG  AX  -  однозначно  да.
    События можно классифицировать, например, так:
    - "особые"  события,-  те,  которые  важны  для  нас с точки зрения
распознавания;
    - "связные"  события,-  те,  которые  образуют группу и для которых
важен порядок расположения в этой группе;
    - "несвязные" события,- те, которые важны сами по себе, без связи с
другими событиями.
    Модель сложного объекта (программы),  заданная в терминах событий и
состояний - дискретно-событийная модель.

                 5.2. Выделение характерных признаков

    Прежде, чем  заниматься распознавнием,  необходимо выбрать перечень
признаков,  по которым это распознавание будет  производиться.  Простой
пример:  событие  "встретилось  обнуление  AX"  никак  не  поможет  нам
распознать вирус,  а событие  "встретилось  дельта-смещение"  -  совсем
наоборот, один из самых главных симптомов заразы.
    Можно взять карандашик,  наморщить лобик,  и акуратно  выписать  на
бумажечку несколько десятков таких симптомов. Но при этом что-то важное
можно и пропустить,  а что-то ерундовое посчитать характерным и важным.
    Можно взять  большое  количество  вирусов  разных  типов и здоровых
программ и прогнать их через семантический анализатор  команд,  попутно
подсчитывая число встретившихся событий. Наиболее часто встречающиеся в
вирусах,  и не встречающиеся в здоровых  программах,  события  и  будут
искомыми  признаками.  Но  где  гарантия,  что среди этих признаков нет
"лишних"?  Например,  среди  признаков  гриппа  зафиксировано:  высокая
температура,  ломота в костях,  насморк,  розовый цвети кожи,  головная
боль.  Среди них лишние - "головная боль" и "розовый цвет кожи", потому
что  они  оба  -  следствие  "высокой температуры".  Это так называемые
"коррелированные" (т.е.  статистически связанные) признаки, от них надо
избавляться, и не всегда это можно сделать "на глазок".
    Для того,  чтобы  отделить   важные   симптомы   от   неважных,   и
обязательные   от   лишних,   существует   специальная   математическая
дисциплина -  "факторный  анализ".  В  этой  очень  сложной  дисциплине
рассматриваются несколько различных методов (метод главных компонентов,
центроидный метод и т.п.),  я немножко  занимался  этими  вопросами,  и
признаю  -  рассматривать,  хотя  бы  поверхностно,  факторный анализ в
данной статье было бы безумием.  Но все равно,  предварительный  анализ
выделенных  вирусных  признаков сделать крайне желательно.  Хотя бы для
того,  чтобы существенно оптимизировать антивирус по  быстродействию  и
размеру кода.
    Вообще, тут  тоже  нет  готовых  рецептов,  заведомо  приводящих  к
хорошему   результату.   Формально   и  четко  определив  математически
оптимальный   перечень   признаков,   можно   впороться   в   то,   что
сооответствующие  им события будут очень тяжело распознаваться на этапе
семантического анализа.  По-хорошему,  среди симптомов  надо  оставлять
только те, которые связаны с системными вызовами:
    - для DOS-вирусов с Int 21h/Int 20h/Int 27h и пр.;
    - для BOOT-вирусов с Int 13h;
    - для макровирусов с MacroCopy/.Import/.Export и пр.;
    - для Win32-вирусов с обращениями к KERNEL32.DLL;
    - для  сетевых  червяков  с  обращениями  к  MPR.DLL,   MAPI32.DLL,
WSOCK32.DLL и пр.
    Этот список,  конечно,  может быть более коротким или длинным.  Вот
DrWEB дополнительно учитывает,  например, такие "слабые сигнатуры", как
флажок разрешения записи на секции .text. В американском Патенте 6.357.
008 предлагается учитывать такие сложносоставные признаки, как

    ...присутствие операции   перехода   на   конец  файла  (SEEK),
    следующей за записью (WRITE) в файл инструкции  JMP,  где  SEEK
    позволяет определить размер файла в байтах,  а JMP производится
    на равное или большее расстояние...

    При детектировании  сетевой  заразы,  видимо,  наличие  "косвенных"
признаков    (упакованность   одновременно   UPX-ом   и   ASProtect-ом,
прописываемость в ключ реестра "HKLM \...\Run" и  т.п.)  должно  играть
еще большую роль.  Т.е.  математика -математикой,  советы-советами,  но
главное - собственный опыт, знания и здравый смысл.
    Ну и,  наконец,  для иллюстрации сказанного - вот на какие  события
умел реагировать антивирус MultiScan Валентина Колесникова:

    01 Программа открыла некоторый файл
    02 Было прочитано из файла 24-28 байт
    03 Было прочитано из файла менее 24 байт
    04 Программа записала в файл 24-28 байт
    05 Программа записала в файл менее 24 байт
    06 Было записано от 29 до 10000 байт
    07 Программа закрыла некоторый файл
    08 Было установлено новое значение int 21
    09 Было установлено новое значение int 13
    10 Программа считала вектор int 21
    11 Программа считала вектор int 13
    12 Была вызвана функция ____ некоторого прерывания
    13 Была вызвана функция ____ некоторого прерывания
    14 Произведена установка времени создания некоторого файла
    15 Программа узнала время создания некоторого файла
    16 Программа установила атрибуты для некоторого файла
    17 Программа считала атрибуты некоторого файла
    18 Был перемещен указатель некоторого файла в конец файла
    19 Был перемещен указатель некоторого файла в начало файла
    20 Программа искала все файлы в некотором каталоге
    21 Программа искала .COM файлы в некотором каталоге
    22 Программа искала .EXE файлы в некотором каталоге
    23 Программа искала следующий файл
    24 Программа считала загрузочный сектор некоторого диска
    25 Вызов функции записи в загрузочный сектор некоторого диска
    26 Возможная запись нескольких секторов на диск
    27 Программа считала более 32000 байт из некоторого файла
    28 Программа выделила блок памяти
    29 Было установлено новое значение int 24
    30 Программа записала некоторое число байт в ранее открытый файл
    31 Был закрыт ранее открытый файл

              5.3. Проблема прерывности потока управления

     Изучая программу при помощи последовательной эмуляции  команд,  мы
рано  или  поздно  столкнемся  с  тем,  что  в некоторые ветви кода наш
эмулятор  никогда  не  попадет.   Вот   типичный   фрагмент   какого-то
резидентного вируса:

;
; Главная программа -- ветвь #1
;
Begin:

     ...

     push cs
     pop  ds
     mov  si, Begin        ; Адресуемся на свой код

     push NewSeg
     pop  es
     mov  di, NewBegin     ; Адресуемся на "откусанную" память

     mov  cx, VirLen
     cld
     rep  movsb      ; Копирование кода в "откусанную" память

     ...

     mov  ax, 2521h
     mov  dx, offset Int21
     int  21h        ; Установка нового обработчика Int21

     ...

     push 100h       ; Возврат управления оригинальной программе
     retn            ; Конец вируса и конец эмуляции

     ...
;
; Вирусный обработчик Int21 -- ветвь #2
;
Int21:

     cmp ah, 4Bh   ; Это запус проги?
     jne Next1                         ; Подветвь 2.1
     ...
     iret

Next1:                                 ; Подветвь 2.2
     cmp ah, DEDAh ; Это пароль?
     jne Next2
     mov ah, BABAh ; Отзыв
     iret

Next2:                                 ; Подветвь 2.3
     ...

     Если не  предпринимать  никаких  дополнительных  мер,  то эмулятор
неминуемо  прекратит  работу  на  команде  RETN,  не  встретив  никаких
подозрительных действий со стороны вируса.  Даже если предположить, что
он каким-то образом попал в обработчик Int21,  возникнет проблема -  по
какой ветке пойти?
     В процессе своей работы эмулятор  должен  сохранять  (например,  в
стеке),   все  (или  не  все,  а  только  связанные  с  неопределенными
регистрами) точки перехода на иные ветви кода, и по завершении эмуляции
одной  ветви,  переходить на эмуляцию другой.  В нашем случае эмулятору
придется  дополнительно  просканировать  ветвь,  начинающуюся  с  метки
Int21,  а  попав  туда,  просканировать  "основной поток" и переходы на
метки Next1  и  Next2.  Короче  говоря,  нужен  рекурсивный обход  всех
ветвей дерева управления.
     Так поступают,  например,  последние версии дизассемблера IDA  при
построении дерева программы, так же должен поступать и антивирус.

                        5.4. Логические методы

    Эта группа методов получила свое наименование потому,  что основана
на исчислении  высказываний  или  исчислении  предикатов.  Несмотря  на
заумную терминологию, идея всего этого очень проста. Итог распознавания
вычисляется в результате проверки истинности  или  ложности  составного
предиката (условия), например:

    ЕСЛИ
     (
     "Встретился Int 27h"          ИЛИ
     "встретился Int 21h/ah=31h"   ИЛИ
     "модифицирован заголовок MCB" ИЛИ
     "модифицировано поле PSP:[2]"
     )
       И
     (
     "Открыт программный файл"   И
     "Запись в программный файл" И
     "Закрыт программный файл"
     )
      ТО
     Программа = Резидентный Вирус.

     Фактически, речь  идет  о  том,  чтобы  поставить  в  соответствие
каждому событию некоторые  логические  флажки  и,  сканируя  программу,
установить или  сбросить некоторые из них,  а потом произвести над ними
некоторые логические преобразования и прийти к некоторому выводу.
    Это наиболее  простой  и  наиболее часто используемый в антивирусах
метод. Но у него есть и недостатки.
    Во-первых, он  не  позволяет проверить последовательность появления
событий.  В  частности,  он  отреагирует  как  на  вирус,  на  бредовую
програму,  в которой файл сначала закрыт,  потом записан, потом открыт.
Особенно  подобными  "эффектами"  страдает   подсистема   распознавания
антивируса DrWeb.
    Во-вторых, этот  метод  оперирует  двоичной  логикой  и всегда дает
ответ либо "да",  либо "нет".  Благодаря этому принципиально невозможно
избежать (хотя бы и в небольшом количестве):
     - "ошибок   первого  рода",  когда  здоровая  программа  считается
больной;
    - "ошибок   второго   рода",   когда   вирус  считается  нормальной
программой.
     Наконец, на  качество  его работы очень сильно влияет субъективный
фактор.  Оно зависит от правильности  составленных  предикатов,  а  при
каждом  изменении  условий  распознавания систему предикатов приходится
переписывать заново.

                      5.5. Синтаксический подход

     Этот подход позволяет,  не меняя ничего принципиально в предыдущем
методе,  избавиться от некоторых его недостатков. Обратим внимание, что
расположенные  в  определенной  последовательности события - это и есть
фразы определенного языка!
     Простой примерчик  - в длинной цепочке событий проверить не только
присуствие,  но и правильный порядок расположения событий O -  "открыть
файл",  W - "записать в файл",  C - "закрыть файл".  Строим примитивный
автомат:

           O     W     C  <else> <eof>
        ------------------------------
     S1   S2    S1    S1    S1    S5
     S2   S2    S3    S1    S2    S5
     S3   S2    S3    S4    S3    S5
     S4   Встретилась цепочка O-W-C
     S5   Ошибка

     Если я не обмишурился при составлении этого автомата (субъективный
фактор,  блин),  то правильными будут, например, считаться цепочки вида
"O-O-W-W-W-C" и неправильными - "O-W-O-C", что нам и надо.
     "Синтаксический" подход  не  отменяет  "логического",  а  довольно
удачно дополняет его.  Есть все основания предполагать, что большинство
современных коммерческих антивирусов  используют  именно  эту  "сладкую
парочку",  и  она  обеспечивает процент эвристического распознавания на
уровне 95-98%.  Но потенциала для дальнейшего развития у  этой  системы
методов, судя по всему, больше нет.

                      5.6. Статистический подход

     Статистическому подходу   соответствует  большая  группа  методов,
основанная на формальном учете частоты  встречаемости классификационных
признаков.  Этих  методов  и  их  модификаций - огромное количество,  а
подчас один и тот же метод имеет несколько различных наименований.
     Эти методы объективны,  и в общем случае  работают  независимо  от
знаний, умений  и  аккуратности  человека.  Кроме  того,  они позволяют
сделать нечеткий,  вероятностный вывод о результатах  распознавания.  В
этом их недостатки, в этом же их  достоинства.
     Вот цитата из Касперского:

     "Во время    своей   работы   сканер   анализирует   частоту
     использования    команд    процессора,    строит     таблицу
     встречающихся  команд  процессора (опкодов) и на основе этой
     информации делает вывод о заражении  файла  вирусом.  Данный
     метод эффективен для поиска некоторых полиморфных вирусов т.
     к.  эти  вирусы  используют  ограниченный  набор  команд   в
     декрипторе,  тогда  как "чистые" файлы используют совершенно
     другие команды с другой частотой.  Например,  все  программы
     для  MS DOS часто используют прерывание 21h (опкод CDh 21h),
     однако в декрипторе  полиморфных  DOS  вирусов  эта  команда
     практически не встречается".

    Это пример  типичного  статистического подхода.  Кстати,  далеко не
лучший пример,  хотя бы потому,  что он не универсален и не годится для
распознавния   других   типов   вирусов.   Его  основное  назначение  -
обнаруживать  "Random  Push  Generator"-ы   и   прочие   шифровальщики,
изготавливающие  длинный,  но однообразный код (см.,  например,  вирусы
семейства Nostardamus).  Впрочем,  этой цитате уже лет 10,  и вряд ли в
современных  версиях KAV используется столь примитивный подсчет частоты
опкодов.
     Как альтернативу рассмотрим очень известный метод,  основанный  на
формуле Байеса из теории вероятностей.

     P(Di|Sj) = (P(Di)*P(Sj|Di))/(P(D1)*P(Sj|D1)+P(D2)*P(Sj|D2)+...),

    где S - симптомы;  D - диагнозы; P(Di) - априорная вероятность i-го
диагноза; P(Si|Dj) - частота появления i-го симптома при j-ом диагнозе;
P(Di|Sj)   -   условная   вероятность   истинности  i-го  диагноза  при
обнаружении j-го симптома.
    Сначала берется  большая  обучающая  выборка  вирусов  и нормальных
программ (причем количественные пропорции их должны быть примерно такие
же,  как в жизни) и по ней семантическим сканером определяются исходные
значения  P(Di).  Потом   берется   коллекция   из   вирусов,   заранее
раскласифицированных  по  типам  (резидентный/нерезидентный,  search  в
каталогах,  search по path и т.п.) и по ней определяются  P(Si|Dj).  На
этом   "обучение"  Байесовского  распознавателя  завершено.  Полученные
данные зашиваются в антивирус.
    Теперь начинается   "работа".   Допустим,   имеется  подозрительная
программа.  Антивирус сканирует ее и  находит  в  ней  некоторый  набор
из k симптомов S={s1,s2...sk}.  Предположим, необходимо оценить, какова
вероятность того,  что программе можно поставить  диагноз  Di.  В  этом
случае по  формуле Байеса вычисляются вероятности P(Di|sj) для всех sj,
входящих в  набор   S.   Далее   предполагается,   что   эти   симптомы
статистически независимы  (хотя это не всегда так;  впрочем,  вирусолог
сам виноват - надо было выбирать некоррелированные признаки), и поэтому

                P(Di|S)=P(Di|s1)*P(Di|s2)*...*P(Di|sk).

    Далее, при  желании  можно  посчитать  эти  вероятности  для   всех
диагнозов, и  выбрать  из  них  максимальную  - это и будет результатом
класификации.
    Кстати, еще  есть  разновидность  метода Байеса,  которая позволяет
автоматическое дообучение распознавателя без участия человека.  В самом
деле,  если  вирус  уверенно  распознан,  значит  чуть-чуть  изменились
априорные вероятности диагноза, условные вероятности симптомов и т.п. -
имеет смысл их пересчитать и заменить новыми значениями.
    При распознавании вирусов in the wild это,  наверное, будет неплохо
работать.  Но  это  не работает при распознавании спама,  ведь спаммеры
имеют  постоянный  контроль  над  содержимым  своих  писем,   и   могут
целенаправленно    разбавлять    характерные    признаки   присутствием
нехарактерных, размывая тем самым зашитые в распознаватель вероятности.
    Главный недостаток  Байесовского  метода:  в  нем  не   учитывается
порядок  расположения  признаков.

                            5.7. Нейросети

    Это семейство страшно,  жутко,  кошмарно популярных и модных сейчас
методов,  имитирующих работу мозга живых существ. Кстати, есть и другие
современные "интеллектуальные" технологии,  которые часто упоминаются в
контексте антивирусной борьбы (например,  "генетические алгоритмы"), но
я  считаю,  что  дальше  пустой  болтовни речь пока не идет и пойдет не
скоро. А вот нейростети можно попробовать использовать.
    Есть много  разновидностей  нейросетей  (сети  Кохонена,  Хопфилда,
рекуррентные,  etc.),  но мы  кратко  рассмотрим  самую  старую,  самую
известную  и  самую  изученную  разновидность - многослойный нелинейный
перцептрон Розенблатта с обратным распространением ошибок.
    Идея очень  простая.  Сеть проще всего представить себе как граф из
большого  количества  узлов.  Есть  несколько  признаков   (симптомов),
каждому  из  них  ставится  в  соответствие узел "входного" слоя.  Есть
несколько  возможных   результатов   распознавания,   им   ставятся   в
соответствие  узлы "выходного" слоя.  По-разному:
     - может быть, каждому логическому результату - один узел;
     - а   может   быть,   один  узел  -  каждому  разряду  в  двоичном
представлении целочисленного результата;
     - а может быть, один узел с вещественным выходом на все результаты
сразу.
     И есть  несколько  промежуточных  слоев.  В типичном случае каждый
узел N одного слоя соединен со всеми узлами соседних слоев:

                            N1  w12 N2
                  x1=y1 ->  *-------*   y2
                             \     / \
                           w14\   /   \ w25
                               \ /     \
                                \       * N5 -> Y
                               / \     /
                           w32/   \   / w45
                             /  w34\ /
                  x2=y3 ->  *-------*   y4
                            N3      N4

                      Входной   Пром.  Выходной
                      слой      слой   слой
                      N1,N3     N2,N4  N5

     По ребрам распространяются числовые значения.  Кроме того, каждому
ребру графа приписано некоторое переменное число W, "вес" ребра (еще их
называют "синапсами" по аналогии с похожими элементами  в биологических
нейронах).  Распознающие  свойства  нейронной сети заключаются именно в
фиксированных значениях весов W.
    Предположим, что имеются числовые признаки x1 и x2, они подаются на
соответствующие  узлы  входного слоя.  Эти значения распространяются по
связям и собираются вместе в узлах промежуточного (и  всех  дальнейших)
слоя, пересчитываются и движутся дальше по правилам:

                        y2 = F(x1*w12 + x2*w32)
                        y4 = F(x1*w14 + x2*w34)
                                  ...
                        Y  = F(y2*w25 + y4*w45)

    Функция F - общая для всех узлов.  Используется несколько вариантов
этой функции, чаще всего - "логистическая":

                          F(x) = A/(B+exp(-C*x)).

                     F(x) ^
                          |-------------------
                          |        ШШШ
                          |      ШШ
                          |     Ш
                          |     Ш
                          |   ШШ
                          |ШШШ
                        0 +------------------> x

     На выходе  получается  некоторое значение Y (или вектор значений).
Если синапсы W правильно подобраны,  то  Y  -  это  и  есть  правильный
"ответ". Таков принцип "работы" нейросети.
     Но сначала,  прежде чем "работать", сеть нуждается в "обучении", в
настройке  синапсов  W.  Берется  большое количество обучающих образцов
(наример, коллекция вирусов) и для каждого из них выполняется несколько
(иногда довольно много - сотни и тысячи!  - итераций).  Во время первой
итерации на выходе получается  некоторое  значение  Y,  которое  скорее
всего   сильно  отличается  от  требуемого  результата  Z,  причем  для
логистической функции величина ошибки рассчитывается как:

                           D=(Z-E)*E*(1-E).

    Эта ошибка  D  начинает обратное распространение по сети,  при этом
пересчитываются значения весов W:

                           E5 = Y5*(1-Y5)*(Z-Y)
                           E2 = (1-Y2)*E5*W25
                           E4 = Y4*(1-Y4)*E5*W45

                           w25 = W25 + n*Y2*E5
                           w45 = W45 + n*Y4*E5
                           w12 = W12 + n*Y1*E2
                           w34 = W34 + n*Y3*E4

    Здесь n - маленькая "норма" обучения, например,  n=0.1.  Чем меньше
это  число,  тем больше итераций потребуется,  но тем более точно будут
вычислены правильные значения весов W.  Дойдя до входного  слоя,  волна
преобразований   опять   движется   по  прямому  направлению  с  учетом
пересчитанных весовых коэффициентов,  и так далее.  Иногда  для  одного
обучающего  образца  приходится выполнять десятки,  сотни и даже тысячи
итераций.  Критерий завершения - близость полученного  и  "правильного"
ответов.  Количество  же  обучающих  образцов иногда достигает десятков
тысяч. Т.е. обучение - довольно длительный процесс.
    И этот  процесс  не  всегда приводит к хорошему результату.  Дело в
том,  что нет формальных критериев,  позволивших бы сразу  использовать
правильную  топологию сети - количество слоев и количество узлов в них.
Есть общее представление,  что  количество  слоев  должно  совпадать  с
количеством   многомерных  гиперповерхностей,  которыми  предполагается
разделять классифицируемые объекты.  А количество узлов в слоях  должно
совпадать  с  количеством  "изгибов" этих поверхностей.  Короче говоря,
поскольку все это невозможно увидеть глазами и даже вообразить,  то  на
практике оптимальная архитектура сети подбирается в результате десятков
и сотен экспериментов.
    И только когда  правильно  выбрана  архитектура  сети  и  правильно
настроены синапсы,  можно  зашить  сеть  в антивирус и использовать для
детектирования новых вирусов.
    Надо упомянуть  еще  одну  проблему.  Дело  в том,  что перцептроны
имитируют распознавание человеком ЗРИТЕЛЬНЫХ образов, т.е. образов, для
которых  число  их  элементов КОНЕЧНО,  и принадлежность того или иного
элемента  к   классу   характерных   признаков   зависит   от   частоты
встречаемости  элемента в какой-нибудь точке изображения с определенной
КООРДИНАТОЙ.  Очевидно, вирусы не могут быть представлены в виде такого
изображения.  Ведь  количество  признаков,  которые могут встретиться в
вирусе, и порядок их появления заранее не определены.
     Как же быть?
     Вообще-то, есть  хороший  нейросетевой  подход,  который позволяет
решить  эту  проблему  -  применение  так   называемых   "рекуррентных"
нейросетевых  конструкций.  Это  нейронные  сети,  в которых имеется не
только прямая связь входа с выходом,  но и обратная - выхода со входом,
например:

                                  +-
                                  | \
                                  v  \
                               N1 *---* N2
                                   \ /
                                    \
                                   / \
                               N3 *---* N4
                                  ^  /
                                  | /
                                  +-

    Но это тема для еще более серьезной статьи,  и я не хотел бы  здесь
ее   обсуждать.  Вместо  этого  рассмотрим  другой  подход,  в  котором
применяется скользящее "окно".  Он позволяет использовать рассмотренный
выше   многослойный   перцептрон.   Идея   такова:  пусть  у  нас  есть
последовательность  встречающихся  в  программах   (в   вирусах   и   в
"невирусах") всевозможных признаков (например, O - Open, W - Write, R -
Read,  C - Close, всего их N=4). Выберем достаточно короткое (например,
L=3)   "окно",  в  него  одновременно  могут  попасть  только  3  рядом
расположенных признака.  Общее число возможных троек есть M=N^L=4^3=64,
таким же будет и число узлов во входном слое перцептрона.  Каждому узлу
будет поставлен в соответствие уникальный набор из 3-х признаков.
    Обучение сети: берем большое количество вирусов и "невирусов" и для
каждой  такой  программы  скользим  по  цепочкам  встречающихся  в  них
признаков окном длиной L=3.  В него будут попадать всевозможные  тройки
рядом  лежащих  признаков.  Если  признак встретился в программе,  то к
соответствующей входной переменной  сети  добавляем  1.  Например,  для
цепочки O-O-O-O-W-O-O-W получим тройки O-O-O, O-O-O, O-O-W, O-W-O, W-O-
O и O-O-W. Финальный набор для этой цепочки будет:

    O-O-O : 2
    O-O-W : 2
    O-W-O : 1
    W-O-O : 1,

    остальные 60  входных  переменных  равны  0.  Поскольку  все тройки
признаков "сцеплены" с соседями своими  "боками",  то  подобный  вектор
хранит информацию не только о наличии или отсутствии признаков, но и об
их взаимном расположении.  Конечно,  он делает это  хуже,  чем  автомат
синтаксического  анализатора,  но все равно информация сохраняется (это
можно доказать).  Используем этот  вектор  в  качестве  обучающего  для
перцептрона,  так же поступим и с остальными учебными образцами. Дальше
работаем с этой сетью,  как с самым обычным многослойным  перцептроном.
Распознаваться  также  должны программы,  представленные в виде вектора
троек признаков.
    Разумеется, вместо   "троек"   признаков   можно   использовать  их
"четверки",  "пятерки" и т.д.  Распознаватель будет работать лучше,  но
сеть многократно усложнится.  В любом случае, этот распознаватель слабо
будет учитывать взаимное отношение  признаков,  расположенных  друг  от
друга дальше, чем длина окна - и это существенный недостаток.
    Поэтому еще раз повторюсь,  я считаю, что этот метод будет работать
вполне удовлетворительно,  но похуже,  чем основанный  на  рекуррентных
сетях.  Я  использовал  его  в  этой статье только для иллюстрации того
факта, что возможности нейронных сетей зависят не только от архитектуры
самой сети, но и от способа подготовки исходных данных для нее.
    А в   общем  и  целом,  распознающие  возможности  нейронных  сетей
потенциально  огромны,  наверное,  99%  и  более.  Другое   дело,   что
эффективные  нейронные  структуры  в мозге живых существ складывались в
ходе естественного отбора в течение десятков и сотен милионов лет, а мы
только  начинаем познавать их закономерности.  Поэтому ПОКА очень редко
можно встретить РЕАЛЬНО РАБОТАЮЩИЕ  системы,  основанные  на  нейронных
сетях. Чаще встречаются экспериментальные образцы, и типичные цифры для
них - 70-80 %  удачно распознанных вирусов.  Насколько  я  в  курсе,  в
коммерческих  антивирусах  удачные  попытки  использования нейросетевых
методов уникальны.  Пожалуй,  ЕДИНСТВЕННЫМ пока примером может  служить
подсистема  распознавания загрузочных вирусов,  использованная в Нортон
Антивирусе и основанная на более ранних разработках фирмы IBM.

                              Заключение

     Итак, в  статье  автором  была  выполнена  попытка  собрать вместе
некоторые знания и умения,  замесить их на  основе  здравого  смысла  и
вылепить  несколько  похожих  на правду и практически реализуемых ИДЕЙ,
любая из которых может быть  положена  в  основу  реально  действующего
современного антивируса. Удачно ли получилось - решать читателю.
    Надеюсь, прочитав статью,  можно составить  определенное  мнение  о
способах  эвристического  анализа  вирусов,  об  их  сильных  и  слабых
сторонах,  об  их  нынешних  возможностях  и  перспективах  дальнейшего
развития.
     Ошибок и неточностей, наверное, много, да это и к лучшему. Значит,
есть повод  еще  глубже залезть в тему,  и получить на основе описанных
идей более качественный продукт.
     Короче, читайте и критикуйте.

                            Благодарности

Валентину Колесникову - за обсуждение принципов эвристического анализа.
Алексу Гостеву - за критику.
Александру Отенко - за критику и корректуру. 

                              Литература

     1. Огарок  А.,  Комашинский  Д.,  Школьников  Д.,  Мартыненко   В.
Виртуальные войны.  Искусственный  интеллект  на  защите  от  вирусов и
программных закладок. // Конфидент, 2003 - #2 (50). - С. 64-69, 97.
     http://stocona.ru/technology/antivirus/virtWar.html
     2. Льюис   Ф.,  Розенкранц  Д.,  Стирнз  Р.  Теоретические  основы
проектирования компиляторов. - М.: Мир, 1979. - 654 с.
     3. Гайдышев И.  Анализ и обработка данных. Специальный справочник.
- СПб: Питер, 2001. - 752 с.
     4. Каллан   Р.   Основные   концепции   нейронных   сетей.  -  М.:
Издательский дом "Вильямс", 2003. - 288 с.
     5. Колесников В. Эвристические анализаторы кода // Земский Фершал,
#2.
     6. http://www.research.ibm.com/antivirus/SciPapers/Tesauro/NeuralNets.html
     7. http://www.decision-support.ru/rffi/results.htm
     8. US Patent 6.357.008.  Dynamic  heuristic  method  for  detecting
computer viruses using decryption exploration and evaluation phases.


               Приложение. Пример антивируса "БУРАТИНО"

     В качестве  примера  предлагается  простая программа,  реализующая
некоторые рассмотренные выше  идеи.  Фактически,  это  огрызок  другой,
более сложной программы,  поэтому в нем можно найти кое-что лишнее, что
я просто не стал удалять.  Антивирусом это назвать сложно,  скорее  это
просто   иллюстративный  материал  к  идеям,  рассмотренным  в  статье;
доказательство,  что эти идеи вполне  реализуемы,  и  что  если  к  ним
подойти серьезно (а не как я),  то получится что-нибудь работоспособное
и полезное.
     Этот "кукольный" антивирус умеет:
     - сканировать  ДОС-овские  COM-программы  (с  учетом "прерывности"
потока   управления),   состоящие   из    команд    всего    нескольких
разновидностей;
     - декомпилировать и отображать эти команды;
     - "понарошку"  выполнять   эти   программы,   изменяя   содержимое
регистров;
     - анализировать  события,  связанные   с   некоторыми   системными
вызовами;
     - принимать решение о "вирусности" сканируемой программы.

/**********************************************************************
 BURATINO - Примитивный демонстрационный сканер программ.
(с) Климентьев К.Е., Самара 1999, 2005.
**********************************************************************/
#include <stdio.h>
#include <dos.h>
#include <mem.h>
#include <io.h>
#include <fcntl.h>

/*
Распознаваемые команды и их машинная кодировка:
 MOV R,N : 1011 XRRR NNNNNNNN/NNNNNNNN NNNNNNNN
 MOV R,R : 1000 101X 11RRRRRR
 JMP A   : 1110 1011 AAAAAAAA
 MOV A,N : 1100 111X NNNNNNNN/NNNNNNNN NNNNNNNN
 MOV A,R : 1000 100X 00RRR110 AAAAAAAA
 MOV R,A : 1000 101X 00RRR110 AAAAAAAA
 INT N   : 1100 1101 NNNNNNNN
 RET     : 1100 0011
 IRET    : 1100 FFFF

Коды регистров:
 RRR  X=1/0
 ----------
 000  AX/AL
 001  CX/CL
 010  DX/DL
 011  BX/BL
 100  SP/AH
 101  BP/CH
 110  SI/DH
 111  DI/BH

 Критерий вирусности: наличие цепочки вида OPEN-WRITE-CLOSE
 Проверяется автоматом:

       O     W     C
    ------------------
 S1   S2    S1    S1
 S2   S2    S3    S1
 S3   S2    S3    S4
 S4   Вирус!

*/

/* Тип данных для хранения значений регистров */
struct X {unsigned int ax, bx, cx, dx, si, di, bp, sp, ip; };
struct H {unsigned char al, ah, bl, bh, cl, ch, dl, dh; };
union RRR {
 struct X x;
 struct H h;
 unsigned char r8[18];
 unsigned int r16[9];
};

#define TRUE -1
#define FALSE 0

/* Коды распознаваемых событий */
#define CODE_OF 1
#define CODE_RF 2
#define CODE_WF 3
#define CODE_CF 4
#define CODE_WD 5
#define CODE_RD 6
#define CODE_SI 7
#define CODE_EX 8
#define CODE_SR 9
#define CODE_SF 10
#define CODE_FD 11
#define CODE_GI 12
#define CODE_NO 0

/* Коды состояний эвристического автомата */
#define S1 1
#define S2 2
#define S3 3
#define S4 4

#define NO_BRANCH 0xFFFF

int f;                         /* Хэндл сканируемого файла */
int Finish=FALSE;              /* Признак конца сканируемой ветви */
size_t new_branch=NO_BRANCH;   /* Флаг наличия новых ветвей */
int Result = FALSE;            /* Результат детектирования */
unsigned char c , c1;
unsigned int  n1, n2;
unsigned int  m1, m2;
unsigned int  r1, r2;
int e = CODE_NO;               /* Событие */
int l = 0;                     /* Уровень вложенности ветвей */
unsigned int n = 0;            /* Счетчик шагов эмуляции */
int i, j, k;

/* Печать декомпилированной команды/события */
pr(union RRR *r) {
 printf(" %04X %04X %04X %04X %04X %04X %04X %04X",
         r->x.ax, r->x.bx, r->x.cx, r->x.dx,
         r->x.sp, r->x.bp, r->x.si, r->x.di);
 switch (e)
  {
   case CODE_OF: printf(" OPEN FILE"); break;
   case CODE_RF: printf(" READ FILE"); break;
   case CODE_WF: printf(" WRITE FILE"); break;
   case CODE_CF: printf(" CLOSE FILE"); break;
   case CODE_WD: printf(" WRITE SECTOR"); break;
   case CODE_RD: printf(" READ SECTOR"); break;
   case CODE_SI: printf(" SET INTERRUPT"); break;
   case CODE_EX: printf(" EXIT PROGRAM"); break;
   case CODE_SR: printf(" STAY RESIDENT"); break;
   case CODE_SF: printf(" SEARCH FILE"); break;
   case CODE_FD: printf(" FORMAT TRACK"); break;
   case CODE_GI: printf(" GET INTERRUPT"); break;
  }
 printf("\n");
}

/* Чтение байта */
unsigned char fg(int st) {
 unsigned char _tmp;
 read( st, &_tmp, 1);
 n++;
 if (eof(st)||(n==0xFFFF)) Finish=TRUE;
 return _tmp;
}

/* Выделение из байта битового поля */
int _b(unsigned char n, int l, int r) {
 int i, tmp, mask=0;
 if (l>r) { tmp=l; l=r; r=tmp; }
 for (i=l;i<=r;i++) {
  mask |= (1<<i);
 }
 return (n&mask)>>l;
}

/* Печать 8-битового регистра */
int _r8(int r) {
  int rn;
  switch (r)
   {
    case 0  : printf("AL"); rn=0; break;
    case 1  : printf("CL"); rn=4; break;
    case 2  : printf("DL"); rn=6; break;
    case 3  : printf("BL"); rn=2; break;
    case 4  : printf("AH"); rn=1; break;
    case 5  : printf("CH"); rn=5; break;
    case 6  : printf("DH"); rn=7; break;
    case 7  : printf("BH"); rn=3; break;
    default : printf("?");  rn=-1;
   };
  return rn;
}

/* Печать 16-битового регистра */
int _r16(int r) {
  int rn;
  switch (r)
   {
    case 0  : printf("AX"); rn=0; break;
    case 1  : printf("CX"); rn=2; break;
    case 2  : printf("DX"); rn=3; break;
    case 3  : printf("BX"); rn=1; break;
    case 4  : printf("SP"); rn=7; break;
    case 5  : printf("BP"); rn=6; break;
    case 6  : printf("SI"); rn=4; break;
    case 7  : printf("DI"); rn=5; break;
    default : printf("?");  rn=-1;
   };
  return rn;
}

  /* Эвристический автомат */
  autom(int *s)
   {
    switch (*s)
    {
     case S1:

        switch (e)
         {
          case CODE_OF : *s=S2; break;
          case CODE_WF : *s=S1; break;
          case CODE_CF : *s=S1; break;
         } break;

     case S2:

        switch (e)
         {
          case CODE_OF : *s=S2; break;
          case CODE_WF : *s=S3; break;
          case CODE_CF : *s=S1; break;
         } break;

     case S3:

        switch (e)
         {
          case CODE_OF : *s=S2; break;
          case CODE_WF : *s=S3; break;
          case CODE_CF : *s=S4; break;
         }
    }
   }

  /* Формирование кода события, связанного с INT21 */
  code21h(union RRR *r)
  {
   switch ((int)r->h.ah)
    {
     case 0x00: case 0x4C:  { e=CODE_EX; Finish=TRUE; break; }
     case 0x31: { e=CODE_SR; Finish=TRUE; break; }
     case 0x4E: case 0x4F: { e=CODE_SF; break; }
     case 0x3D: { e=CODE_OF; break; }
     case 0x3E: { e=CODE_CF; break; }
     case 0x3F: { e=CODE_RF; break; }
     case 0x40: { e=CODE_WF; break; }
     case 0x25: { new_branch = (size_t) (r->x.dx-0x100); e=CODE_SI; break; }
     case 0x35: { e=CODE_GI; break; }
    }
  }

  /* Формирование кода события, связанного с INT13 */
  code13h(union RRR *r)
  {
   switch ( (int)r->h.ah )
    {
     case 0x2: { e=CODE_RD; break;  }
     case 0x3: { e=CODE_WD; break; }
     case 0x5: { e=CODE_FD; break; }
    }
  }

  /* Анализ семантики системных вызовов */
  seman(union RRR *r)
  {
   switch ((int) c1)
   {
    case 0x20: e=CODE_EX; Finish=TRUE; break;
    case 0x21: code21h(r); break;
    case 0x13: code13h(r); break;
    case 0x25: e=CODE_RD; break;
    case 0x26: e=CODE_WD; break;
    case 0x27: e=CODE_SR; Finish=TRUE; break;
   }
  }

  /* Синтаксический анализ команды, начинающейся с байта 0xС */
  codeC(union RRR *r)  {
   switch ( _b(c,0,3) )  {
     case 0xD : c1=fg(f);
                printf("INT 0x%X     ",c1);
                r->x.ip+=2;
                seman(r);             // Вызов семантического анализатора
                break;
     case 0x3 : printf("RETN         "); r->x.ip++; Finish=TRUE; break;
     case 0xF : printf("IRET         "); r->x.ip++; Finish=TRUE; break;
     default  : printf("?");
    }
  }

  /* Синтаксический анализ команды, начинающейся с байта 0xB */
  codeB(union RRR *r) {
   n1 = _b(c,0,2); printf("MOV ");   // Печать команды (декомпиляция)
   switch ( _b(c,3,3) )  {           // Декодирование опкода (декомпиляция)
    case 0 : r1=_r8(n1);             // Печать имени регистра (декомпиляция)
                                     // и вычисление его индекса (эмуляция)
             m1=fg(f);               // Чтение константы (декомпиляция)
             r->r8[r1]=m1;           // Заполнение регистра константой (эмуляция)
             r->x.ip+=2;             // Инкремент счетчика команд (эмуляция)
             printf(",0x%02X  ",m1); // Печать константы (декомпиляция)
             break;
    case 1 : r1=_r16(n1); m1=fg(f)+256*(int)fg(f); r->r16[r1]=m1;
             printf(","); printf("0x%04X", m1); r->x.ip+=3; break;
    }
  }

  /* Синтаксический анализ команды, начинающейся с байта 0x8 */
  code8(union RRR *r) {
   c1 = fg(f);
   if (_b(c1,6,7)==3) {
    n1 = _b(c1,3,5); n2 = _b(c1,0,2); printf("MOV ");
    switch ( _b(c,0,3) ) {
     case 0xA : r1=_r8(n1); printf(","); r2=_r8(n2);
                r->r8[r1]=r->r8[r2]; r->x.ip+=2; break;
     case 0xB : r1=_r16(n1); printf(","); r2=_r16(n2);
                r->r16[r1]=r->r16[r2]; r->x.ip+=2; break;
     default  : printf("??,??");
    }
    printf("    ");
   }
   else printf("?");
  }

  /* Обработка команды */
  decompile(union RRR *r)
  {
  for (i=0;i<l;i++) printf(" "); // Пробелы в начале строки
  printf("%04X: ", r->x.ip);
  switch( _b(c,4,7) )
   {
      case 0xC : codeC(r); break;
      case 0xB : codeB(r); break;
      case 0x8 : code8(r); break;
      default  : printf("?");
   }
   pr(r);
   if (new_branch!=NO_BRANCH)
    {
     l++; // Уровень вложенности ветви
     detect(new_branch);
     l--;
     new_branch=NO_BRANCH;
    }
  }

  /* Обработка программной ветви */
  detect(size_t a)
  {
   size_t a_tmp;
   union RRR r;
   int s=S1; // Состояние автомата внутри ветви

   new_branch = NO_BRANCH;
   setmem(&(r.r8[0]), sizeof(r), 0);
   printf("\nBEGIN BRANCH 0x%X\n", a);
   a_tmp = tell(f);
   lseek(f, a, SEEK_SET);
   Finish=FALSE;
   r.x.ip = a+0x100;
   while (!Finish)
    {
     c=fg(f);
     e=CODE_NO;
     if (!Finish) decompile(&r); // Рекурсия !!!
     autom(&s);
     if (s==S4) Result=TRUE;
    }
   Finish=FALSE;
   lseek(f, a_tmp, SEEK_SET);
   printf("END BRANCH\n\n");
  }

/* Головной модуль */
main(int argn, char *argv[] )
{
 if (argn>1)
 {
  printf("  IP  MOV   COMMAND   AX   BX   CX   DX   SP   BP   SI   DI\n");
  printf("-----------------------------------------------------------\n");
  n=0;
  f = open(argv[1], O_BINARY|O_RDWR);
  detect(0);
  close(f);
  if (Result) printf("IT IS VIRUS !!!");
 }


  Пример работы:

  IP  MOV   COMMAND   AX   BX   CX   DX   SP   BP   SI   DI
-----------------------------------------------------------

BEGIN BRANCH 0x0
0100: MOV AX,0x2521 2521 0000 0000 0000 0000 0000 0000 0000
0103: MOV DX,0x010A 2521 0000 0000 010A 0000 0000 0000 0000
0106: INT 0x21      2521 0000 0000 010A 0000 0000 0000 0000 SET INTERRUPT

BEGIN BRANCH 0xA
 010A: MOV AH,0x3D   3D00 0000 0000 0000 0000 0000 0000 0000
 010C: INT 0x21      3D00 0000 0000 0000 0000 0000 0000 0000 OPEN FILE
 010E: MOV AH,0x40   4000 0000 0000 0000 0000 0000 0000 0000
 0110: INT 0x21      4000 0000 0000 0000 0000 0000 0000 0000 WRITE FILE
 0112: MOV AH,0x3D   3D00 0000 0000 0000 0000 0000 0000 0000
 0114: INT 0x21      3D00 0000 0000 0000 0000 0000 0000 0000 OPEN FILE
 0116: MOV AH,0x40   4000 0000 0000 0000 0000 0000 0000 0000
 0118: INT 0x21      4000 0000 0000 0000 0000 0000 0000 0000 WRITE FILE
 011A: MOV AH,0x40   4000 0000 0000 0000 0000 0000 0000 0000
 011C: INT 0x21      4000 0000 0000 0000 0000 0000 0000 0000 WRITE FILE
 011E: MOV AH,0x3E   3E00 0000 0000 0000 0000 0000 0000 0000
 0120: INT 0x21      3E00 0000 0000 0000 0000 0000 0000 0000 CLOSE FILE
END BRANCH

0108: INT 0x27      2521 0000 0000 010A 0000 0000 0000 0000 STAY RESIDENT
END BRANCH

IT IS VIRUS




(C) NF, 1998-2004