Underground InformatioN Center [&articles] 
[network & security news] [RSS & Twitter] [articles, programing info] [books] [links, soft & more...] [soft archive][home]

Эмуляция программного кода

Содержание

 1.     Экскурс в историю
 2.     Технология кодо-эмулятора
 2.1.   Алгоритм
 2.1.1. Инициализация эмулятора
 2.1.2. "Предел терпения"
 2.1.3. Имитация исполнения инструкций
 2.1.4. Загрузка файлов
 2.1.5. Дизассемблер
 2.1.6. Запуск инструкций в специальной среде
 2.1.7. Полная имитация исполнения инструкций
 2.1.8. Твикс
 3.     Заключение

1. Экскурс в историю

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

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

  Первый движок назывался - MuTation Engine (MTE). После его появления антивирусные конторы стали хвататься за голову (особенно американские), некоторые чувствовали, что что-то подобное скоро выйдет, уже обдумывали универсальные методы по детектированию шифрованных вирусов.
  Кстати, автором MTE являлся программист из Болгарии, более известный под псевдонимом Dark Avenger. Он является автором "культовых" вирусов Eddie, Commander Bomber, Dir ... Именно этот человек двигал "прогресс" (в плане разработки новых вирусных технологий) в конце 80ых годов.

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

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

 Несмотря на геморрой, связанный с таким детектированием многие антивирусные компании решили выбрать этот сложный путь (например, взять ту же McAfee), то время, пока авторы вирусов только учились новой технологии полиморфизма, еще можно было использовать. Но в 1994 году, в Англии (уже не в Болгарии), появляется очень серьезный полиморфный движок SMEG (Simulated Metamorphic Encryption Generator). Определять "старым способом" декрипторы созданные по алгоритмам, использовавшимся в этом движке практически невозможно. Кроме определения, нужно еще и расшифровывать вирусное тело, что бы излечить инфицированный файл!
  Ходили слухи, что английская полиция летом 1995 года, арестовала автора известного под кличкой Black Baron, за создание вирусов Pathogen, Quueg и трех версий полиморфных движков SMEG.
  Но к этому моменту было уже достаточно программистов, способных писать гораздо более продвинутые полиморфики. Примерно в это же время, в Словакии, появляется Explosion's Mutation Machine (от "культовой" личности - Vyvojar, он же автор знаменитого вируса OneHalf). Немного позже в России появляется Zhengxi, от одноименного автора, который в своих вирусах использовал, усовершенствованную версию движка SMEG.

  Конечно, каждой вирусной технологии можно противопоставить обратную (антивирусную), но теперь факт заключался лишь в том, что хороший полиморфик написать гораздо проще, чем его обнаруживать. Так полиморфики и полиморфные движки стали появляться как грибы после дождя, в результате (к сожалению многих сторонников oldschool) пришлось завязать с алгоритмическим детектированием. Хотя был такой русский антивирус, который просто игнорировал появление полиморфных вирусов. Но это длилось до поры, до времени, пока в Россию не пришел вирус OneHalf, который стал распространяться с бешеной скоростью и "народный антивирус" (самый популярный по тем временам в exUSSR) оказался не в состоянии даже определить наличие данного вируса (тем более лечить инфицированные файлы или диски). Позже проект был закрыт, автор антивируса так и не смог (а может, просто не хотел) включить в свое "творение" эмулятор программного кода.

2. Технология кодо-эмулятора

  Процесс эмуляции кода, представляет собой разбор программного кода на инструкции и имитацию их исполнения. Данная технология используется главным образом в антивирусных программах для:

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

  Первый способ подразумевает собой обычную трассировку программы, т.е. ее загрузку в память и исполнение путем использования отладочного прерывания. Таким методом пользуется большинство отладчиков, но вся проблема в том, что даже безрукий человек может обломать процесс трассировки. В крайнем случае, есть множество статей на тему облома и TD и SoftIce. А если в полиморфном расшифровщике вставлены антиотладочные трюки, которые в случае обнаружения трассировки запускают какую-нибудь деструкцию. В результате антивирус сам запустит деструкцию в процессе эмуляции и только навредит. Кстати, для детектирования вирусов семейства SMEG, именно этот способ, использовал в своем антивирусе американец StormBringer.

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

 2.1. Алгоритм работы

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

  1. Тип исполняемого файла. Этот параметр является необходимым для универсальных эмуляторов, тех, которые способны работать с исполняемыми файлами различного типа (COM, EXE, PE и т.д.). Это связанно с тем, что как Вы (наверное) знаете, структура исполняемых файлов отличается друг от друга, следовательно, для нормальной работы эмулятору необходимо знать с каким типом файла он "имеет дело".
  2. Оригинальное расположение инструкции в памяти. В данном случае оригинальное расположение в памяти, точки входа в программу.
  3. Расположение инструкции в антивирусном буфере. В данном случае расположение точки входа в программу, в антивирусном буфере.
  4. Глубина анализа.
  5. Максимальное время работы эмулятора с программой.

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

 Теперь рассмотрим схему работы простейшего эмулятора:

  Пункты: 1 и "Инициализация анализатора кода" относятся к технологии "Анализаторов кода". Этой технологии "посвящена" отдельная статья, являющаяся продолжением этой.

 2.1.1. Инициализация эмулятора

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

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

  Теперь приведем небольшой пример:
; 
; regs   - буфер под значения 7ми виртуальных регистров (32bit)
; vstack - буфер под виртуальный стек
;
.data
regs                    db 7*4 dup (?)
vstack                  db 50000 dup (?)
vstack_small            db 10000 dup (?)
.code
                mov     edx,offset vstack+50000
                mov     dword ptr [regs+4*4],edx
                mov     edx,расположение точки входа в памяти
                mov     dword ptr [regs+0*4],eax

  Буфер vstack_small используется в качестве дополнительного буфера под виртуальный стек и предназначен специально на случай использования конструкций типа:

                ...
                add     esp,20
                push    00000000h
                pop     eax
                sub     esp,20
                add     eax,ключ необходимый для расшифровки кода
                ...

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

  Семь 32х битрых регистров хранятся в буфере regs, в последовательности:

       EAX = 000 или 0
       ECX = 001 или 1
       EDX = 010 или 2
       EBX = 011 или 3
       ESP = 100 или 4
       EBP = 101 или 5
       ESI = 110 или 6
       EDI = 111 или 7

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

 2.1.2. "Предел терпения"

  Эмулятор завершает свою работу в случае ошибки или по "служебной необходимости". Ошибки могут возникать по самым различным причинам, например:

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

  1. вирусах или троянских программах (так называемые эвристические маски)
  2. декрипторах создаваемых различными упаковывающими или шифрующими утилитами

  Правильная реализация работы эмулятора с "глубиной анализа", значительно экономит время, затраченное на "исследование" программ.

  Пытаясь обмануть эмуляторы, программисты "изобретают" анти-эмуляционные приемы - различные алгоритмы, которые эмуляторы выполняют неправильно. Если такие трюки успешно срабатывают, эмулятор не может правильно расшифровать вирусное тело или дойти до места, с которого начинается вирусная сигнатура. Зачастую эмулятор попадает в вечный цикл и в этом случае антивирусная программа просто зависает.
  Именно с целью предотвратить неожиданные зависания, на исполнение каждой программы отводится определенное время. После имитации выполнения каждой инструкции эмулятор проверяет, не прошло ли время, отведенное на "исследование" программы.
  Рассмотрим простейший метод использования таймера в эмуляторе:

.data
 emul_time              dd ?
.code
;
; Выполняется в процессе инициализации эмулятора.
;               
; a) Определим значение таймера (с момента запуска Windows (в миллисекундах)), на
;    момент запуска эмулятора. 
; b) Пусть допустимое время эмуляции исполнения программы составляет 3000
;    миллисекунд.
; c) В переменной emul_time запомним значение таймера, на котором процесс
;    эмуляции должен быть завершен.
;
 get_timer_val: call    GetTickCount
                cmp     eax,0FFFFFFFFh-3000d
                jc      set_timer_val
;
 skip_time:     call    GetTickCount
                or      eax,eax
                jnz     skip_time
;
 set_timer_val: add     eax,3000
                mov     dword ptr [emul_time],eax

 ...

;
; Выполняется после перевода указателя, на следующую инструкцию. 
;
 emulate_check: call    GetTickCount              ; если было привышено макс.
                cmp     eax,dword ptr [emul_time] ; время, отведенное на
                ja      выход из эмулятора        ; эмуляцию программы, выйдем
 ...

 2.1.3. Имитация исполнения инструкций

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

  Для того, что бы процесс эмуляции инструкции прошел правильно, необходима информация о ее структуре (полученная в результате разбора дизассемблером) и местоположении в памяти (EIP), которое она имеет:

  1. При загрузке в память программы, во время запуска.
  2. Когда программа находится в антивирусном буфере.

  Для хранения этих данных могут использоваться регистры или специальные переменные, начальное значение которых задается параметрами 2 и 3 и имеет свойство изменяться:

  1. В ходе процесса эмуляции инструкции, если ее "назначением" является изменение местоположения (например JUMP, CALL, RET).
  2. Во время "перевода указателя на следующую инструкцию" (значение обоих положений увеличивается на размер проэмулированной инструкции).

  Исходя из данных, полученных об инструкции, для имитации исполнения, используется один из способов:

  1. Разобранная инструкция (или участок кода) запускается "напрямую", в специальной среде.
  2. Исполнение инструкции имитируется полностью, без запуска.
  3. Используется комбинация из двух, описанных выше способов.

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

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

  1. Обращаются к различным смещениям.
  2. Изменяют местоположение (EIP) исследуемого кода.
  3. В случае неправильного использования, могут вызвать ошибку, которая приведет к зависани.

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

 2.1.4. Загрузка файлов

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

  Будем считать, что Вы владеете знаниями формата исполняемых файлов PortableExecutables. Если же нет, то советую отложить чтение данной статьи, до тех пор, пока Вы не проштудируете какое либо из описаний (очень советую руководство написанное Hard Wisdom).

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

  • В память загружаются данные нулевой секции, которая содержит в себе все данные, расположенные до начала первой секции (заголовки EXE, PE, данных о секциях файла ...). Ее размером (в памяти и файле), соответственно, будет являться местоположение первой секции, относительно начала файла, а начальным положением (в файле и памяти) - 0.
  • Затем грузятся все остальные секции. При загрузке, в качестве размера очень удобно использовать значение, полученное путем вычитания начала текущей секции в памяти, от начала следующей секции (если текущая секция является последней, то используется размер текущей секции в памяти). Можно так же использовать размер данных текущей секции в памяти, но может понадобиться выравнивание этого значения на границу, указанную в заголовке (смещение 38h, от начала заголовка PE).

  Конечно, все не так просто как может показаться на первый взгляд. Этим способом можно загружать файлы, размер которых в памяти (Image Size + размер данных нулевой секции) не будет превышать размер буфера, отведенного для исследования файлов. Но даже файлы очень маленьких размеров, в памяти могут занимать огромные размеры. Потому что в память, помимо кода, будут загружаться в полном объеме все переменные и буферы, не только содержащие данные, но и зарезервированные под их хранение. А первым же файлом, который невозможно будет загрузить, окажется Ваш антивирус.
  Таким образом, от этого способа придется отказаться и ограничится простым чтением файла в буфер. Но и в этом случае, нам так же не обойтись без трудностей, которые будут возникать в ходе эмуляции программы. Приведем несколько наглядных примеров, для файла с данными:

Image Base = 400000h

 Section name            : CODE
 Virtual Size            : 00002000H
 Virtual Address         : 00001000H
 Size of RAW data        : 00001C00H
 Pointer to RAW data     : 00000600H (X)

 ...                     : ...

 Section name            : DATA
 Virtual Size            : 009DE000H
 Virtual Address         : 00003000H (A)
 Size of RAW data        : 00000400H
 Pointer to RAW data     : 00002200H (B)

 ...                     : ...

  Размер данных секции "CODE" в файле составляет всего 1С00h байт, хотя в памяти ее размер на 400h байт больше. В файле, содержится 400h байт данных из секции "DATA", а в памяти размер этой секции на 9DDC00 больше. Теперь примеры конфликтных ситуаций:

  1. В секции CODE с позиции 401000h совершается безусловный переход (JMP) на смещение 403000h (секция DATA). Размер "прыжка" составляет 2000h байт, и если файл загружен в память, то переход осуществится правильно, в противном случае переход произойдет на позицию 403400h ( Image Base + A + (X + 2000h) - B), которая в файле расположена по адресу 2600 (X + 2000h).
  2. Инструкция, расположенная (в секции CODE) по смещению 401200h обладает огромным желанием воспользоваться некоторыми данными, расположенными по смещению 403400h (секция DATA). В файле, загруженном в память, по этому смещению располагаются именно те данные, которые "жизненно важны" этой инструкции. Если файл прочитан в буфер, то по этому смещению будет располагаться участок данных, который при загрузке программы в память, имеет положение 403600h (Image Base + A + (X + 2200h) - B), а в файле, он имеет положение 2A00h (X + 200h + 2200h).

  Для избежания подобного рода ошибок, должна быть написана специальная процедура, которая в процессе эмуляции будет проверять, и корректировать все:

  1. смещения, к которым происходит обращение эмулируемыми инструкциями
  2. изменения в местоположении (после имитации инструкций RET, JUMP ...) разбираемых инструкций

  Алгоритм работы таких процедур должен представлять собой:

  1. Поиск секции, в которую входит указанное смещение.
  2. Расчет местоположения данных, указанных смещением, относительно начала файла:
    • = Начальное положение данных секции в файле + (Начальное положение данных секции в памяти - смещение)

  Конечно, в большинстве вирусов все данные расположены в пределах одной секции, исключением являются вирусы, которые используют:

  • Генераторы полиморфных расшифровщиков использующие алгоритм разделения декриптора (а/ля OneHalf, CommanderBomber ...).
  • Переход из секции, содержащей точку входа в файл, на ту, в которой расположено вирусное тело (алгоритм передачи управления вирусу, его варианты, так же известны под названием Unknown Entry Point или Entry Point Obscuring).
  • Пермутирующие вирусы, которые разделяют свое тело на постоянное или произвольное количество частей и могут располагать их в различных секциях.

  Одну процедуру эмуляции, очень удобно использовать для "работы" с несколькими типами исполняемых файлов, имеющих, похожую структуру (COM, EXE, PE, NE). Учитывая, тот факт, что "начинка" исполняемых файлов, различных типов, может полностью совпадать (за исключением нескольких моментов) - разработка отдельных процедур, для каждого типа исполняемых файлов является лишней тратой времени. В таком случае, для того, что бы в процессе работы универсального эмулятора определять, какие действия в процессе исследования файла, нужно производить, а какие нельзя, используется параметр, полученный от антивируса - "тип исполняемого файла".

 2.1.5. Дизассемблер

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

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

  Результатом ошибочного определения размера инструкции, будет являться неправильная имитация работы инструкции. Что в свою очередь, может привести к самым разнообразным последствиям, самым безобидным из которых является обычное зависание программы. Именно по этому, процессу определения типа инструкции, должно уделяться особое внимание.
  Очень часто, инструкции одного типа имеют различный размер и инструкции различных типов имеют схожее строение, например:

 mov [reg], byte ptr [eREG]      - 8AHEX, 00regREG
 mov [reg], byte ptr [esp]       - 8AHEX, 00regESP, 00espESP
 mov [reg], byte ptr [ebp]       - 8AHEX, 01regEBP, 00000000

 mov [reg], byte ptr [eREG+byte] - 8AHEX, 01regREG, byte
 mov [reg], byte ptr [esp+byte]  - 8AHEX, 01regESP, 00espESP, byte

  Для упрощения и разработки универсального алгоритма полной имитации выполнения "схожих" инструкций, дизассемблером производится дополнительный разбор "родственных" инструкций, которые на время эмуляции объединяются в один тип.
  Я считаю, что очень удобно использовать общий алгоритм эмуляции всех инструкций, которые для указания смещений используют регистры. В этом случае дополнительными параметрами, которые эмулятору необходимо знать, является количество регистров, использующихся для указания смещения и номера этих регистров. Для указания смещения, кроме регистров (или в совокупности с ними) так же может использоваться число (любого размера). Оно так же является параметром, подлежащим разбору.

  Рассмотрим примеры инструкций, и "внешний вид" параметров, которые были получены дизассемблером:

 call [esi+edi]              : 2 (кол-во регистров), 6 (esi), 7 (edi), 0 (нет числа)
 xor  word ptr [ebx+100],ax  : 1 (кол-во регистров), 3 (ebx), 100 (число)
 mov  eax,dword ptr [ebp+15] : 1 (кол-во регистров), 5 (ebp), 15 (число)

  Далее стандартная информация о каждой инструкции (размер, требуемый способ эмуляции) и дополнительные параметры (если они есть) передаются в эмулятор, который производит процесс имитации выполнения, в соответствии с требуемым способом (пункты 2.1.6, 2.1.7 и 2.1.8).

 2.1.6. Запуск инструкций в специальной среде

  Эмулятор формирует участок кода, который представляет собой:

 instr_buf:    mov     dword ptr [res_stack+1],esp
               mov     esp,dword ptr [regs+4*4]
           ... разобранная инструкция ...
               mov     dword ptr [regs+4*4],esp
 res_stack:    mov     esp, ?
               ret

 где regs - буфер содержащий значения виртуальных регистров, 
 соответственно regs+4*4 - значение виртуального регистра ESP

  Сформированный участок:

  1. сохраняет значение оригинального стека
  2. производит настройку виртуального стека
  3. производит запуск разобранной инструкции
  4. восстанавливает оригинальное значение стека

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

  • Значения виртуальных регистров из переменных (буфера regs).
  • Значение флагов EFLAGS.

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

.data
 instr_buf              db 60 dup (?)    ; буфер под эмуляцию стека и разобранную инструкцию
 regs                   db 7*4 dup (?)   ; буфер для хранения значений виртуальных регистров
 temp                   dd ?             ; временная переменная для храения EFLAGS эмулятора
 flags                  dd ?             ; значение EFLAGS исследуемой программы
.code
;
; ecx - размер разобранной инструкции
; esi - смещение разобранной инструкции
;
 prep_to_run:   mov     edi,offset instr_buf
                mov     ax,2589h
                stosw                            ; перенесем "mov 4 ptr [?],esp"
;               
                mov     eax,offset instr_buf     ; рассчитаем смещение
                add     eax,ecx                  ; "res_stack+1" (см. выше) и
                add     eax,6+6+6+1              ; перенесем остаток инструкции
                stosd
;               
                mov     ax,258Bh                 ; перенесем инструкцию
                stosw                            ; "mov esp,4 ptr [regs+4*4]"
                mov     eax,offset [regs+4*4]
                stosd
;               
                rep     movsb                    ; перенесем разобранную
                                                 ; дизассемблером инструкцию
;               
                mov     ax,2589h                 ; перенесем инструкцию
                stosw                            ; "mov 4 ptr [regs+4*4],esp"
                mov     eax,offset [regs+4*4]
                stosd
;               
                mov     al,0bch                  ; перенесем инструкцию
                stosb                            ; "mov esp,?"
                stosd
                mov     al,0c3h                  ; "поставим последнюю точку"
                stosb                            ; перенесем "ret"
;                       
; "прямой запуск" разобранной инструкции в специальных условиях (карантине)
;
; запомним значение регистров программы-эмулятора в стеке
;
 run_instr:     pushad
;
; запомним значение флагов программы-эмулятора в стеке, перенесем
; в регистр eax, а из него в переменную "temp"
;
 save_orig_efl: pushfd
                mov     eax,dword ptr [esp]
                mov     dword ptr [temp],eax
                popfd
;
; значение флагов эмулируемого кода перенесем из переменной "flags" в регистр
; eax, а из него в стек использующийся программой-эмулятором
;
 set_emul_efl:  pushfd
                mov     eax,dword ptr [flags]
                mov     dword ptr [esp],eax
;
; установим значения регистров эмулируемого кода из переменной "regs"
; значение esp не устанавливается, иначе мы рискуем испортить значение флагов
; эмулируемого кода, стек будет перенаправлен в подпрограмме "instr_buf"
;
                mov     eax,dword ptr [regs+0*4]
                mov     ecx,dword ptr [regs+1*4]
                mov     edx,dword ptr [regs+2*4]
                mov     ebx,dword ptr [regs+3*4]
                mov     ebp,dword ptr [regs+5*4]
                mov     esi,dword ptr [regs+6*4]
                mov     edi,dword ptr [regs+7*4]
;
; инструкция "popfd" установит значение флагов эмулируемого кода, которое мы
; сохраняли выше
;
                popfd
                call    instr_buf  ; запустим на исполнение cформированный участок
;
; сохраним новые значения регистров эмулируемого кода в переменной "regs"
;
                mov     dword ptr [regs+0*4],eax
                mov     dword ptr [regs+1*4],ecx
                mov     dword ptr [regs+2*4],edx
                mov     dword ptr [regs+3*4],ebx
                mov     dword ptr [regs+5*4],ebp
                mov     dword ptr [regs+6*4],esi
                mov     dword ptr [regs+7*4],edi
;
; новое значение флагов эмулируемого кода запомним в стеке, затем перенесем
; в регистр eax и сохраним в переменной "flags"
;
 save_emul_efl: pushfd
                mov     eax,dword ptr [esp]
                mov     dword ptr [flags],eax
                popfd
;
; установим значение флагов программы-эмулятора из переменной "temp"
;
 set_orig_efl:  pushfd
                mov     eax,dword ptr [temp]
                mov     dword ptr [esp],eax
                popfd
;
; восстановим значение регистров программы-эмулятора
;
                popad
            ... перевод указателя на следующую инструкцию ...

 2.1.7. Полная имитация исполнения инструкций

  Существуют три основные причины, по которым возникает необходимость использования полной имитации выполнения инструкций:

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

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

  1. Происходит обращение к данным, расположенным в стеке.
  2. В программе используются процедуры для подсчета переменной, которая является разницей между текущим и оригинальным местоположением программы (delta value). Прибавив подсчитанную таким образом величину к любому смещению можно получить "текущее" местоположение необходимых данных в памяти.

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

Инструкции изменяют местоположение исследуемого кода.

  В эмуляторах для хранения местоположения используются регистры или переменные (см. выше), по этой причине изменение их значений должно происходить "вручную".
  Примером таких инструкций являются: CALL, JMP, LOOP, RET ...

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

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

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

;
; Пусть регистр esi содержит смещение разбираемой инструкции (EIP)
; в "антивирусном буфере"
;
.data
 regs                   db 7*4 dup (?)   ; буфер для хранения значений виртуальных
                                         ; регистров
.code
;
; * * * Имитации исполнения инструкции "ret" [0C3h]
;
 emul_ret               proc
;
                push    eax
                mov     esi,dword ptr [regs+4*4] ; расположение свободного места
                                                 ; в буфере виртуального стека
                lodsd                            ; прочтем в eax dword из стека
                add     dword ptr [regs+4*4],4   ; "уберем" значение из стека
                xchg    esi,eax                  ; изменим EIP на значение
                pop     eax
                ret
 endp
;                                                
; * * * Имитации исполнения инструкции "call dword" [0E8h, dword]
;
 emul_call              proc
;
                push    eax edi
                mov     eax,esi                  ; смещение инструкции call
                add     eax,5                    ; смещение инструкции следующей
                                                 ; за call
;
                mov     edi,dword ptr [regs+4*4] ; edi = местоположение в буфере
                                                 ; виртуального стека
                stosd                            ; перенесем смещение из eax в
                                                 ; буфер виртуального стека
                sub     dword ptr [regs+4*4],4   ; изменим указатель в буфере
                                                 ; (чтобы не затерли смещение)
;
                push    esi
                pop     edi                      ; edi = esi
                add     esi,dword ptr [edi+1]    ; изменим EIP: добавим dword
                                                 ; указанный в call'е
                pop     edi eax
                ret
 endp

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

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

  Процесс эмуляции таких инструкций, целесообразно производить в два этапа.
  Первый этап, является общим и не зависит от типа инструкции. Он представляет собой:

  1. цикл, который подсчитывает смещение, полученное в результате сложения значений регистров, использующихся в инструкции для указания смещения.
  2. проверку полученного смещения
.data
 dregs                  dd 9*4 dup (?)  ; буфер для хранения дополнительной
                                        ; информации о инструкции
 regs                   db 7*4 dup (?)  ; буфер для хранения значений виртуальных
                                        ; регистров
.code
; буфер dregs содержит примерно следующие данные:
;  dregs +  000    : количество регистров (допустим n) использующихся в инструкции
;  dregs +  004    : номер регистра 1
;  dregs +  008    : номер регистра 2
;  dregs +  n*4    : номер регистра n
;  dregs + (n+1)*4 : число использующеся для указания смещения, иначе = 0
;
                sub     ebx,ebx
                mov     esi,offset dregs
                lodsd
                xchg    eax,ecx
 count_sum_lp:  lodsd
                mov     edx,4
                mul     edx
                add     ebx,dword ptr [regs+eax]
                loop    count_sum_lp
                lodsd
                add     ebx,eax

  Имитируя исполнение инструкций, которые обращаются по смещениям, есть очень большой риск ошибки в расчете смещения. Эта ошибка может быть следствием:

  1. Недоработки (или ошибки) анализатора кода, который пропустил процедуру подсчета delta value или другую важную особенность кода.
  2. Неправильно проэмулированного участка кода, который предназначен для обмана эмуляторов антивирусных программ (так называемые анти-эвристические/отладочные трюки).
  3. Непредвиденной (разработчиками) ошибки в программе.

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

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

 2.1.8. Твикс

  Для имитации выполнения большинства инструкций используются предыдущие способы, но существуют и те, для которых "выгоднее" использовать комбинацию этих способов.
  В качестве примера рассмотрим инструкции "условного перехода" (jb, jnb, je, jne, jbe, ja, js, jns и т.д.). Конечно, для этих инструкций можно использовать полную имитацию. В этом случае, для каждой разновидности "перехода", придется писать свою процедуру, потому что в каждом типе используется проверка состояния разнообразных флагов. Используя же "комбинированный" метод, можно написать компактную и универсальную процедуру для всех типов переходов.

  Рассмотрим алгоритм работы такой процедуры:

  • Процесс запуска инструкции в специальных условиях:
    1. Сформируем участок кода следующего вида:
                 ... разобранная инструкция (без)условного перехода,
                     но не к оригинальной метке, а к _label ...
                     inc     eax
       _label:       ret
    2. Установим значение флагов эмулируемого кода и запустим на исполнение сформированный участок (перед запуском предварительно обнулив значение регистра eax).
    3. В том случае, если переход выполнится, значение eax будет равно нулю.
  • Процесс имитации выполнения:
    1. В том случае если переход выполнился, необходимо изменить смещение исследуемого кода. Для этого рассмотрим второй байт инструкции, который содержит данные, необходимые для вычисления нового смещения.

  Реализация алгоритма:

.data
 instr_buf              db 60 dup (?)
 regs                   db 7*4 dup (?)  ; буфер для хранения значений виртуальных
                                        ; регистров
 flags                  dd ?            ; значение EFLAGS исследуемой программы
.code
; * * * Имитация исполнения "условных" и "безусловных" переходов:
;
;       jb, jnb, je, jne, jbe, ja, js, jns, jmp _ byte
;
; регистр esi содержит смещение разбираемой инструкции (EIP) в "антивирусном
; буфере"
;
 emul_jmp_byte          proc
;
                push    esi
                pop     edi                     ; edi = esi
                mov     ah,1
                mov     al,byte ptr [edi]       ; ax: j?? $+2
                push    edi
                mov     edi,offset instr_buf
                stosw
                mov     ax,0c340h               ; ax: inc eax, ret;
                stosw
                pop     edi
;               
 jmp_ser_eflag: pushfd                          ; запомним оригинальное состояние
                                                ; флагов
                pushfd
                mov     eax,dword ptr [flags]   ; eax - состояние флагов
                                                ; эмулируемой программы
                mov     dword ptr [esp],eax     
                sub     eax,eax                 ; eax = 0
                popfd                           ; установим состояние флагов
                call    instr_buf               ; запустим сформированный
                                                ; участок
                popfd                           ; вспомним состояние флагов
                or      eax,eax
                jz      jmpb_goto               ; если инструкция выполнилась ...
                ret
;               
 jmpb_goto:     cmp     byte ptr [edi+1],07fh   ; если значение < 7F
                jna     jmpb_down               ; значит переход ниже
 jmpb_up:       sub     al,byte ptr [edi+1]     ; рассчитаем значение для перехода
                                                ; выше
                sub     esi,eax                 ; изменим EIP
                ret
 jmpb_down:     mov     al,byte ptr [edi+1]     ; рассчитаем значение для "спуска"
                add     esi,eax                 ; изменим EIP
            ... подсчет местоположения, при загрузке в 
                память операционной системой (если необходимо) ...
                ret
 endp

3. Заключение

  Первая (beta) версия данной статьи была опубликована на ресурсе wasm.ru. Вполне возможно появление новых, дополненных версий этой статьи. Я так же надеюсь на конструктивную критику и советы.

  Все рассуждения и примеры приводились на языке ассемблера, для исполняемых файлов PortableExecutables.

  В качестве примера для ознакомления рекомендуется Explosion Antivirus, части исходных текстов которого, были использованы в статье.

Автор: beauty on the fire: beautyonthefire (at) mail.ru

Статья написана специально для UInC (http://www.uinc.ru).

эмуляторы программного кода, кодоэмуляторы, антивирус, emulator, antivirus

Все документы и программы на этом сайте собраны ТОЛЬКО для образовательных целей, мы не отвечаем ни за какие последствия, которые имели место как следствие использования этих материалов\программ. Вы используете все вышеперечисленное на свой страх и риск.

Любые материалы с этого сайта не могут быть скопированы без разрешения автора или администрации.


[network & security news] [RSS & Twitter] [articles, programing info] [books] [links, soft & more...] [soft archive][home]
 Underground InformatioN Center [&articles] Choose style:
2000-2015 © uinC Team