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

Не секрет что 9 из 10 файлов являются запакованными той или иной программой. Но данная концепция вовсе не претендует на то, что бы написать файловый вирус, который вызовет всеобщую эпидемию. Напротив, если мы заразим некоторые системные файлы: DLL и т.п. - то вирус может оставаться в системе незамеченным (если все реализовано правильно) бесконечно долго. Т.е. мы как бы пускаем корни в этот компьютер. Все было бы не столь хорошо, если бы наш вирус не являлся так же и сетевым червем. И оппа! Глобальная эпидемия!

Со временем идет смена парадигмы недетектируемого вируса. Раньше эти лавры пожинали так называемые FullMorph'ы. Но чем больше времени проходило, тем более люди понимали, что таким путем вряд ли кто-то куда-то придет.

Выделяется две принципиально разные модели недетектируемого вируса:

  1. Делаем все тело вируса полиморфным, что в результате должно(?) привести к недетектируемости.
  2. Лишь небольшая часть вируса является полиморфной, сам же вирус зашифрован. Эта модель сводится к тому, чтобы вычислить переход на этот вирус было маловероятно, а поиск его в файле не чем бы ни увенчался.
  3. Эта модель состоит из объединения первой и второй модели.

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


Основная ошибка модели FullMorph'ов

Чиста теоритически эта модель выглядит идеально. На деле же она терпит полный крах, и дело совсем не в плохой реализации этой модели.


Начнем с малого
===============

Остановимся на "кривой" реализацией этой модели. Для более понятного объяснения приведем примеры такой реализации.

 mov eax, 401000h -> xor ebx, ebx      ->  push 401000h 
 jmp eax             add ebx, 401000h      ret
                     jmp ebx

Как видим константа 401000h присутствует во всех вариантах, что, конечно же, не прибавляет плюсов тому, кто делает подобным образом.

Что мы имеем? А вот что, мы старательно хотели избавиться от сигнатур, но в то же время их напихали в нашу реализацию.

Конечно, эта ошибка искореняется не столь сложно.

 mov eax, 401000h xor 606060h
 xor eax, 606060h
 jmp eax 

Но ошибка кроется еще ближе, чем вы думаете, и мало кто ее видел.


Вгрызаемся в пласты проблемы
============================

В общем случае так называемые FullMorph'ы имеют определенные алгоритмы, которые от одной мутации к другой не изменяются. Меняются операции, константы и регистры, но алгоритмы не изменны!

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

Например, мы можем генерировать следующие однобайтовые конструкции cld, std, cmc, nop. Авир в свою очередь это предусмотрит, и его произведение будет раздевать наш FullMorph и в конце концов примет логической решение о том что это именно данный вирус, данный движок.

Потому что зараннее обговорены все варианты. В этом коде, например, может не быть какой-то операции, которая есть в обычных программах и оппа!

Решение было бы то, что вирус может предложить очень большое множество вариантов генерирования кода и несколько алгоритмов реализации той или иной вещи.

Вообще-то мы получаем одну матрешку в другой, и так до бесконечности. Лично мне не видится выхода из сложившейся ситуации.


Вторая модель и абсолютный UEP метод

Необратимый алгоритм
====================

Например, имеем алгоритм, который пишет в файл числа 1,2,... и так далее. Конечно, мы можем по результатам сказать, что это работа того или иного алгоритма. Здесь связь видна сразу, между числами в файле.

Теперь берем необратимый алгоритм. Имеем числа 5,0,90. О нем мы не можем сказать ничего! Но если у нас будет таких чисел очень много, то мы можем найти минимальное и максимальное число, а так же среднее. И в результате использовать теорию вероятностей для исследования этого закона.

Поэтому нужно следовать правилу, что чем меньше, тем лучше.

Вся прелесть этого алгоритма в том что строя на его основе(в простейшем случаи это генератор случайных чисел) программу мы как бы делаем ее немного не уязвимой.

Если вы пишете полиморфный движок, то пусть генерируемый код будет небольшим и будет несколько алгоритмов расшифровщика (не тупой набор из операция типа not,xor,rol,ror,add,sub...).

Например:

1. mov esi, 401000h   
   mov edi, esi       
   mov ebx, KEY       
   mov ecx, length/4  
   cld                
 @@1:                 
   lodsd              
   xor eax, ebx       
   stosd              
   loop @@1           
   ...                     


2. mov ebx, 401000h 
   mov ecx, length/4                     
   mov edx, key                          
 @@1:                                    
   mov eax, [ebx]                        
   xor eax, edx                          
   mov [ebx], eax                        
   add ebx, 4                            
   dec ecx                               
   test ecx, ecx                         
   jnz @@2                               
   ...                                   
   jmp @@3                               
 @@2:                                     
   ...                                   
   jmp @@1                               
   ...                                   
 @@3:                                     


3. mov ebx, 401000h
   mov ecx, (length/4)*4
   mov edx, key    
 @@3:               
   call @@1        
   call @@2        
   add  ebx, 4     
   sub  ecx, 4     
   test ecx, ecx   
   jnz  @@3        
   ...             
   jmp  @@4        
 @@1:               
   ...             
   mov eax, [ebx]  
   ...             
   ret             
 @@2:                
   ...             
   xor eax, edx    
   ...             
   mov [ebx], eax  
   ...             
   ret             
 @@4:                

В этом примере я лишь показал примерные алгоритмы, все зависит от вашей фантазии.

Т.е. чем больше вариантов алгоритма расшифровки и больше вариантов того или иного действия, а так же чем меньше декриптор тем лучше.

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


О заголовке файла
=================

К сожалению, вирус является весьма сложной программой, которую замаскировать ни так уж и легко. Но в этом и состоит весь азарт!

Главное что ты должен помнить, дорогой читатель, так это то, что вирус не должен находиться по конкретному адресу (в конце, в начале, вместо настраиваемых элементов). Также не должно быть прямых ссылок на код вируса, таких как точка входа.

Затем нужно следить за полями PE файла. Например, у некоторых файлов VirtualSize > PhisicalSize, у других VirtualSize = PhisicalSize, а у третьих VirtualSize = 0. Чтобы правильно заразить, ты должен помнить об этом. Так же, лучше всего, нужно сохранять эту зависимость после заражения. Если у файла имеются настраиваемые элементы, так пусть они присутствуют после его заражения. Идеально было бы вообще не менять в заголовке какие-либо значения, а если и менять то оставлять те зависимости, которые наблюдались.

В качестве метки заражение можно использовать полиморфные метки, смотреть аналогичные статьи. К примеру, статья RedArc "Концепция недетектируемого вируса (FullMorph)".


Некоторые детали в UEP методе
=============================

Допустим, что имеем вирус в произвольной части, с минимальным размером декриптора, и все правила и закономерности в заголовке выполнены. Точки входа на вирус нету, т.е. старый добрый UEP.

Что хочется в первую очередь заметить, так это то, что антивирусу не будет опоры и придется проверять абсолютно весь файл! Конечно это тормозно. Так еще нужно учитывать, что в ходе эмулирования кода не всегда возможно попасть на код вируса.

Я бы сделал на этом месте следующее: взял бы случайно смещение и производил эмулирование, ждем пока попадем на код вируса. Если не попали, то выбираем опять случайное смещение и все повторяем, конечно, это очень тормозно.

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

Т.е. мы имеем количество итераций около N^N (демонический смех).

Какая подлянка нас в свою очередь поджидает? Она в том, что нужно сделать UEP так что бы управление мы получили, но в то же время путем эмулирования кода в эту точку попасть было бы очень сложно. Т.е. нужно свести сценарий к тому, что чтобы эмулировать код нужно эмулировать всю операционную систему.


Основные критерии
=================

Главные правила при написании недетектируемого вируса по второму методу следующие:

  1. Там где по правилам должно быть конкретное значение, его не следует изменять ( например не нужно всегда обнулять контрольную сумму или все время ее считать (там где она 0-ая). Или все время удалять настроечные элементы).
  2. Вирус не должен располагаться в определенных местах, выбирайте произвольные.
  3. В нем должны быть использованы команды, которые чаще всего встречаются или абсолютно весь набор команд ( это касается в первую очередь полиморфных вирусов ). И чем меньше размер команды тем лучше.
  4. Чем вирус меньше, тем лучше (и полиморфный декриптор).
Модель вируса и антивируса

Представим программу в машинном коде. Она состоит из совокупности команд. Причем, как правило, заранние не известна длинна той или иной команды. Ее длина вычисляется в зависимости от ее составляющих. Что бы использовать математический аппарат введем следующие обозначения.

 {X[i]} - множество команд, программа на машинном коде   X[i] - i-ая по счету команда

В силу аппаратных ограничений {X[i]} имеет конечное число элементов.

Мы должны ввести определение процесса, которого собираемся изучать. В противном случаи мы получим абсурдную ситуацию.

Определение: Имеется исходная комбинация {X[i]}, в результате преобразований этой комбинации получается {Y[j]}, причем {X[i]} =/ {Y[j]}. Данная комбинация интерпретируется определенным и однозначным способом. Если комбинация {X[i]} функционально ничем или мало чем отличается от {Y[j]}, причем полученная комбинация помимо выбранных свойств {X[i]} обладает специфическими, не характерными ей свойствами, то такая операция называется инфицированием {X[i]}.

Если имеется оператор переводящий {X[i]} -> {Y[j]}, то он называется инфектором. Эта операция обозначается следующим способом P {X[i]} = {Y[j]}.

Команда X[i] = {x1,x2,x3,...}. Где x1,x2,... - элементы команды. Здесь каждый элемент команды может принимать значения 1 или 0.

Определение: Совокупность элементов команды, не влияющие на длину команды, называют командно-независимыми. В противном случаи совокупность элементов называется командно-зависимыми.

Пример: Имеется команда x86 процессора в 16 системе: 0xB8,0x00,0x00,0x00,0x00. В этой команде совокупность битов числа 0xB8 является командно-зависимыми. Причем 3 последних бита числа 0xB8 будут командно-независимыми, они образуют имя регистра, они никак не влияют на длину команды, так же как нулевые байты.

Будем считать, что в результате операции P {X[i]} = {Y[j]}, {Y[j]} состоит из измененного {X[i]} и P. Т.е. {Y[j]} тоже может выступать в роли инфектора. Это предположение сильно упрощает нашу работу. Инфектор удовлетворяющий вышесказанного назовем вирусом.

Введем операцию сканирования, которая определяет инфектор имея в качестве аргумента только конечную комбинацию.

Эту операцию можно примерно показать как S {Y[j]} = {X[i]}. Эта запись лишь поясняет функционирования сканирования, она не претендует на ее обозначение.

Далеко не всегда возможно получить инфектор, поэтому расширим операцию сканирования.

 S {Y[j]} (Sign P) = Boolean.

Где Sign P -- сигнатура данного инфектора, наиболее важная часть по которой можно выявить изменение.

Процедура сканирования будет иметь булевское значение истина или ложь. То есть сканирование однозначно определяет изменена ли данная комбинация данным инфектором или нет.

Сигнатура инфектора может сильно варьироваться в зависимости от типа оператора сканирования.

Рассмотрим типы сигнатур:

  1. В качестве сигнатуры используется весь инфектор. в таком случаи правильность определения инфектора будет стремиться к 100%. Из-за громоздкости инфектора это не представляется возможным.
  2. Используется часть инфектора. Такой способ не дает 100% вероятности правильного обнаружения инфектора, но он компактен и при соблюдении некоторых правил можно добиться максимально правильной точности сканирования.
  3. В качестве сигнатуры используется измененная часть инфектора. например операция вида Crc {X1,X2,...} = Z. Она из множества элементов инфектора получает универсальное число, параметр. Этот способ наиболее компактен, но он так же дает менее точное определение.
  4. Используется часть инфектора, а так же измененная часть инфектора. Такой способ дает более точное определение, чем пункты 2 и 3. При этом он обладает достаточной компактностью.

Из-за недостатков первый метод мы отбросим, как не удовлетворяющий нашей цели.

Если предположить что все команды являются равновероятными, то минимально возможная длина сигнатуры(2 тип) будет равняться log 2 (Max FSize).

Пример: Самый большой файл равняется 2^32, тогда минимально возможная длина сигнатуры будет равняться 32 битам.

Если размер вируса будет меньше 32 то его определение будет не возможным, это утверждение не всегда является истинным в силу наших предположений.

05.07.02