Антивирусные технологии: эмуляция программного кода — Архив WASM.RU

Все статьи

Антивирусные технологии: эмуляция программного кода — Архив WASM.RU

Содержание

1. Введение
2. Экскурс в историю
3. Способы эмуляции
4. Пример использования технологии
4.1. Лирическое отступление
4.2. Имитация исполнения инструкций
4.2.1. Запуск инструкции в специальной среде
4.2.2. Полная имитация исполнения инструкции
4.2.3. Комбинация двух способов
4.3. Поворот не туда
4.3.1. Дизассемблер
4.3.2. Эмулятор
4.4. "Предел терпения" (Enough)
5. Пример использования технологии для детектирования вирусов
5.1. Кодо-анализатор
6. Заключение

1. Введение

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

 Это описание не является отличным или хорошим это всего лишь базис (основы) технологии, как я понимаю это, вполне возможно я понимаю ее неправильно, этого я не отрицаю.

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

 Тем, кому не интересны мои размышления о возможной истории появления технологии, могут пропустить «Экскурс в историю» и перейти непосредственно к описанию алгоритма технологии.

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

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

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

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

  Кстати автором MTE являлся программист из Болгарии, более известный под псевдонимом Dark Avenger. Он же автор культовых вирусов Eddie и если я не ошибаюсь 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, от одноименного автора. Но наилучшей разработкой под DOS (по моему мнению) стал Red Team Polymorphy от человека под псевдонимом SoulManager (который был выпущен в 1997 году, с тех пор так и остался «неконкурентоспособным» ;-) ).

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

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

3. Способы эмуляции

 Каждый уважающий себя антивирус должен содержать в себе эмулятор. Если мы исключим известные антивирусы и возьмем «кустарные», то я знаю только несколько программ, которые  содержали реализацию эмулятора (конечно кривые, убогие, но они работали и достаточно сносно). Это MultiScan (конечно, кустарным, его можно назвать с натяжкой), Lecar (содержал некоторое подобие) и мое творение the_Sweeper. Я смотрел исходные тексты некоторых иностранных программ, но там все детектировалось по плавающим сигнатурам или алгоритмами, иногда (но очень редко) использовали обычную трассировку.

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

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

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

4. Пример использования технологии

 В своем антивирусе “the_Sweeper” я использовал некоторую комбинацию этих двух способов, в результате получилось что-то совсем простенькое и кривое. Но антивирус мог ловить OneHalf, TMC, Pieck Примитивную реализацию алгоритма схожего с тем, что я использовал в антивирусе, представляю на Ваш суд.

 

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

 Программа состоит из:

  • Процедуры (рас)шифровки данных, которая шифрует и расшифровывает текстовую строку.

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

 Программа работает по алгоритму:

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

  • Эмулятор запускает код, который был перенесен в этот буфер на «псевдоисполнение».  В результате текстовая строка, которая также содержалась в этом буфере за процедурой (рас)шифровки будет зашифрована.

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

  • На экран выводится содержимое текстовой строки,содержащейся в буфере.

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

[emul.asm]

; name   : "code emulation" example
; author : andy [Most Needful Things]
; home   : http://amethyst.nm.ru
;
;  Пример программы умеющей выполнять "псевдо-эмуляцию" программного кода.
;
; компилировать: 
; tasm32 -ml -m5 emul.asm
; tlink32 -Tpe -c -x emul.obj ,,, import32
;
.386P
.model flat, stdcall
 jumps
;
 extrn                  lstrlenA:near            ; список needful апи
 extrn                 _wsprintfA:near
 extrn                  GetStdHandle:near
 extrn                  WriteConsoleA:near
 extrn                  ExitProcess:near
;                      
.data
_stack                 db 10000 dup (?)          ; буфер под стэк
 temp                   dd ?
 instr_buf:            db 30 dup (?)              ; буфер для запуска инструкции в "карантине"
 buffer                 db 10000 dup (?)          ; буфер под код
 flags                   dd ?                              ; значение флагового регистра EFLAGS эмулируемого кода
 regs                    dd 08 dup (?)                ; значения eax/ecx/edx/ebx/esp/ebp/esi/edi эмулируемого кода
;                                                                              
.code
 start:      mov     esi,offset decryptor            ; смещ. (рас)шифрощика и текста
               mov     ecx,(emulate-decryptor)/4 ; размер (рас)шифровщика и текста
               mov     edi,offset buffer                 ; перенесем данные в буфер
                rep      movsd
;               
                push    offset decryptor         ; старое значение ip (рас)шифровщика
                push    (emulate-decryptor)   ; размер кода для эмуляции
                push    offset buffer               ; расположение кода
                call      emulate                      ; эмулируем, т.е. зашифровываем
                                                               ; текст идущий после (рас)шифровщика
;               
                mov     edi,offset buffer         ; esi - смещ. буфера
                add       edi,(message-decryptor)
                push     edi
                call      decryptor_sze            ; расшифруем текст из буфера
                pop      esi
;
;  В данный момент ESI содержит смещение расшифрованного текста, теперь осталось вывести
; текст на экран. Поддержку консоли я убрал из исходных текстов, представленных в статье  (полные
; доступны в архиве).
;
                …
;               
 exit:        push    0
                call      ExitProcess
;               
; decryptor - простейший алгоритм (раз)шифроки текста идущего после ...
;
;  Эмулятор "не понимает" инструкцию nop, которая следует после цикла
; (рас)шифровки, по этому когда он дойдет до инструкции, то выйдет с "ошибкой".
; Но самое главное, что данные будут (за/рас)шифрованны ... в противном случае
; эмулятор бы просто завис в вечном цикле. ;-)
;
decryptor:mov    edi,offset message
                pushfd
                popfd
                call      aaa
                jmp     decryptor_sze
                           db 125 dup (?)
 aaa:         sub     edi,0ffffffh
                add     edi,0ffffffh-004h
                scasd
                ret
 decryptor_sze:
                mov    ecx,0
                add     cl,( (emulate-message) / 4 )
 decrypt_loop: 
                xor      dword ptr [edi],0ffffffh
                scasd
                loop    decrypt_loop
                nop
                ret 
 message             db 'Hey you, ugly face!',13,10
                           db 'Give me the bottle of tequilla!',13,10
                           db 'Hurry up, don''t make me mad!',13,10,0
;                                                                              
 include                inc\emulator.inc         ; эмуляция кода
 include                inc\disasm.inc            ; дизассемблер
;                                                                              
                end      start
[inc\emulator.inc]
.code
;                                                                              
; emulate - эмуляция участка кода
;
; on start :       ip - оригинальный ip блока
;            codesize - размер блока для эмуляции
;             codeloc - смещение на код для эмуляции
; on exit  :  - - - - -
;
 emulate        proc   codeloc: dword, codesize: dword, ip: dword
;                                                                              
; процесс инициализации эмулятора, перед работой с участком кода
;  а) сохранение значения регистров
;  б) установка параметров переданных в процедуру (ip/codesize/codeloc)
;  в) установка значения стека, для эмуляции кода
;
 emul_init:
               push    eax ecx edx ebx ebp esi edi
               mov     esi,offset _stack+10000    ; буфер под стек, для эмуляции
               mov     dword ptr [regs+016],esi  ; установим значение
                mov     esi,codeloc                       ; смещение на начало кода
                mov     ebx,esi
                add      ebx,codesize                    ; смещение на конец кода
;                                                
 emulate_loop:  
                push    esi                        ; включаем кодо-анализатор
                call     disasm                   ; помощью дизассемблера
                cmp     eax,-1                   ; если инструкция известна
                jnz      copy_instr             ; дизассемблеру, продолжим разбор
 emulate_error: 
                 jmp     emulate_ret         ; иначе (!) тихо выйдем
;                                                
; перенесем в буфер "instr_buf" инструкции для эмуляции стека и
; разобранную дизассемблером инструкцию размера ecx
;
; в результате получим в "instr_buf" примерно:
;
;               mov     dword ptr [res_stack+1],esp
;               mov     esp,dword ptr [regs+016]
;           ... разобранная инструкция ...
;               mov     dword ptr [regs+016],esp
; res_stack:    mov     esp, ?
;               ret
;
 copy_instr:    
                push     eax ecx                 ; запомним данные о инструкции, которую разбирал дизассемблер
                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+16]"
                mov     eax,offset regs+016
                stosd
;               
                rep     movsb                          ; перенесем разобранную, дизассемблером инструкцию
;               
                mov     ax,2589h                   ; перенесем инструкцию
                stosw                                     ; "mov 4 ptr [regs+16],esp"
                mov     eax,offset regs+016
                stosd
;               
                mov     al,0bch                     ; перенесем инструкцию
                stosb                                     ; "mov esp,?"
                stosd 
                mov     al,0c3h                     ; "поставим последнюю точку" - перенесем "ret"
                stosb                                     

;
                pop      ecx eax                    ; вспомним данные инструкции
;                                                                              
; работа с инструкциями, которые требуют специальной имитации исполнения
; первый этап - определение типа инструкции
;
                push    esi
                pop     edi                            ; edi - смещение инструкции
                sub      edi,ecx                     ;  которую разбирали
;                          инструкции размером 1 байт
 @one_byte:     
                cmp     cl,1
                jnz    @two_byte
                cmp     byte ptr [edi],0c3h   ;  
                jz     @found_ret             ; найден "ret" ?
;                          инструкции размером 2 байта
 @two_byte:     
                cmp  cl,2
                jc      run_instr                  ; если размер меньше или не равен
                jnz    @three_byte
 @check_xor:    cmp     byte ptr [edi],031h   ;  
                jz     @found_xor             ; (де)шифрующая инструкция xor?
 @check_loop:   cmp     byte ptr [edi],0e2h   ;  
                jz     @found_loop            ; найден "loop"?
                jmp     run_instr
;                         инструкции размером 3 байта
 @three_byte:   
                cmp   cl,3
                jc       run_instr                  ; если размер меньше или не равен
                jnz    @five_byte
 @check_xor2:
               cmp     byte ptr [edi],030h   ; найдена разновидность
                jz     @found_xor               ; (де)шифрующей инструкции xor?
                jmp     run_instr
;                         инструкции размером 5 байт
 @five_byte:    
                cmp  cl,5
                jc      run_instr                    ; если размер меньше или не равен
                jnz    @six_byte                 ; 5 байтам запустим в "карантине"
                cmp     byte ptr [edi],0e8h  ;  
                jz     @found_call               ; найден "call" ?
                cmp     byte ptr [edi],0e9h  ;  
                jz     @found_jmp               ; найден "jmp" ?
                jmp     run_instr
;                          инструкции размером 6 байт
 @six_byte:     
               cmp     cl,6
                jc        run_instr                      ; если размер меньше или не равен
                jnz       run_instr                     ; 6 байтам запустим в "карантине"
                cmp     byte ptr [edi],081h      ; "add/sub/xor [ereg], dword" ?
                jnz       run_instr                     ; нет, запустим в "карантине"
                cmp     byte ptr [edi+1],037h ; нам нужен только "xor", а
                jna    @found_xor                  ; остальные можно просто запустить
                jmp       run_instr                    ; на исполнение в "карантине"
;                                                                              
; работа с инструкциями, которые требуют специальной имитации исполнения
; второй этап - имитация исполнения инструкции
;
;                             инструкции размером 1 байт
; имитировать исполнение инструкции "ret" очень просто:
;  а) необходимо взять двойное слово из стека эмулируемого кода
;     смещение на это слово содержится в "regs+16"
;  б) это слово будет новым указателем на смещение разбираемой эмулятором
;     инструкции (значение регистра esi)
;  в) убрать из стека эмулируемого кода, взятое значение
;     add dword ptr [regs+16],4
;
 @found_ret:    
                mov     esi,dword ptr [regs+016]
                lodsd
                add     dword ptr [regs+016],4
                xchg    esi,eax
                jmp     emulate_check
;                            инструкции размером 2 байта
; Первая часть процесса имитации выполнения дешифрующей инструкции
;
; сейчас буфер "dregs" содержит примерно следующие данные:
;  dregs + 000 : ????? (количество регистров использующихся в инструкции)
;  dregs + 004 : номер регистра 1
;  dregs + 008 : номер регистра 2
;  dregs +  n  : номер регистра n / 4
;
;  Нам необходимо определить сумму значений регистров (в данном случае двух)
; использующихся в инструкции и передать ее во вторую часть процесса. В нашем
; случае это будет выглядеть примерно так:
;
;   a = [dregs+004] * 4
;   x = [regs+a]
;   a = [dregs+008] * 4
;   x = x + [regs+a]
;   и так далее ...
;
; где x является той самой суммой, которую необходимо узнать
;
 @found_xor:    
                push    ebx esi
                sub      edx,edx
                mov     dl,4
                sub      ebx,ebx
                mov     esi,offset dregs
                lodsd
                xchg    eax,ecx
 @fxor_load_lp: 
                push     edx
                lodsd
                mul      edx
                add      ebx,dword ptr [regs+eax]
                pop      edx
                loop   @fxor_load_lp
                xchg     ebx,ecx
                pop      esi ebx
;
; Вторая часть процесса имитации выполнения …
;
; допустим (только допустим!), что максимальный размер расшифровщика
; может составлять 200 байт
; значит (!) если сумма значений регистров (x) находится в промежутке
; от ip до ip+200,
; то менять значение не обязательно, вполне возможно что:
;  - декриптор содержит в себе процедуру определения ip
;  - значение регистра уже менялось раньше, т.е. инструкция выполняется
;    не первый раз
; если одно из вышеуказанных утверждений правда (true), то просто запускаем
; инструкцию на выполнение в специальных условиях
;
 @found_xor_ip: 
                mov     edx,ip
                cmp     ecx,edx
                jc         run_instr
                add      edx,200
                cmp     ecx,edx
                ja         run_instr
;
; Третья часть процесса имитации выполнения …
;
;  если значение x выпадает из промежутка, 
;  то: a) необходимо рассчитать разницу между старым ip и x (суммой
;         значений регистров)
;      б) добавить эту разницу к смещению начала буфера (codeloc), в который
;         мы копировали эмулируемый код
;      в) полученное смещение поместить в значение регистра "dreg+004"
;      г) значение остальных регистров использующихся в инструкции обнулить
;
                sub     ecx,ip
                add     ecx,codeloc
;
; новое смещение зашифрованного кода, полученное действиями, описанными
; в пунктах "а" и "б" (см. выше), положив в регистр номер "dreg+004"
;
                mov     eax,dword ptr [dregs+004]
                sub       edx,edx
                mov     dl,4
                push     edx
                mul      edx
                pop      edx
                mov     dword ptr [regs+eax],ecx
;
; цикл, для обнуления значений всех регистров начиная с "dreg+008"
;
                push    esi
                mov     esi,offset dregs
                lodsd
                cmp     al,02
                jc     @fxor_emu_ret
                xchg    eax,ecx
                dec     ecx
                lodsd
 @fxor_setz_lp: 
                push    edx
                lodsd
                mul      edx
                mov     dword ptr [regs+eax],0
                pop      edx
                loop   @fxor_setz_lp
 @fxor_emu_ret:
                pop     esi
                jmp     run_instr    ; можно запустить инструкцию на выполнение
;               
; имитируя исполнение инструкции "loop", мы должны уменьшить значение ecx
; эмулируемого блока на 1, если он не будет равен 0, то перейти к "началу цикла"
;
 @found_loop:
               dec      dword ptr [regs+004]     ; уменьшим значение регистра
                cmp     dword ptr [regs+004],0 ; "ecx" эмулируемого кода
                jz        emulate_check               ; если равно 0, выйдем из цикла
                sub      esi,ecx                           ; иначе рассчитаем смещение
                sub       esi,eax                           ; начала цикла
                jmp      emulate_check               ; перейдем к следующей инструкии
;                            инструкции размером 5 байт
; если перед нами инструкции "call" и "jmp" то необходимо изменить смещение
; (значение esi) инструкции, которая будет следующей исполняться эмулятором
; НО в случае если мы разбираем "call", нужно сохранить в стеке эмулируемого
; участка смещение команды следующей прямо за call'ом, что бы потом по встрече
; "ret" вернуться "на путь истинный"
;
 @found_call:   
                push    eax edi
                mov     eax,esi
                sub      dword ptr [regs+016],4
                mov     edi,dword ptr [regs+016]
                stosd
                pop      edi eax
 @found_jmp:    
                 add      esi,eax                  ; изменим esi
;               
; esi - указатель на инструкцию для разбора
; ebx - предел, за который выйти "непростительно"
;
 emulate_check: 
                cmp     esi,ebx                       ; если мы не вышли за пределы
                jc         emulate_loop             ; продолжим цикл эмуляции
 emulate_ret:
                pop     edi esi ebp ebx edx ecx eax
                ret
;                                                                              
; "прямой запуск" разобранной инструкции в специальных условиях (карантине)
;
; запомним значение регистров программы-эмулятора в стеке
;
 run_instr:push    eax ecx edx ebx ebp esi edi
;
; запомним значение флагов программы-эмулятора в стеке, затем перенесем
; в регистр 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+000]
                mov     ecx,dword ptr [regs+004]
                mov     edx,dword ptr [regs+008]
                mov     ebx,dword ptr [regs+012]
                mov     ebp,dword ptr [regs+020]
                mov     esi,dword ptr [regs+024]
                mov     edi,dword ptr [regs+028]
;
; инструкция "popfd" установит значение флагов эмулируемого кода, которое мы сохраняли выше
;
                popfd
                call       instr_buf                ; запустим на исполнение
;
; сохраним новые значения регистров эмулируемого кода в переменной "regs"
;
                mov     dword ptr [regs+000],eax
                mov     dword ptr [regs+004],ecx
                mov     dword ptr [regs+008],edx
                mov     dword ptr [regs+012],ebx
                mov     dword ptr [regs+020],ebp
                mov     dword ptr [regs+024],esi
                mov     dword ptr [regs+028],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
;
; восстановим значение регистров программы-эмулятора
;
                pop     edi esi ebp ebx edx ecx eax
                jmp     emulate_check
 endp
;
[inc\disasm.inc]
 .data
 dregs                 dd 010 dup (?)  ; буфер для хранения данных о регистрах использующихся в дешифрующих инструкциях
.code
;                                                                              
; disasm - дизассемблер, предназначается для "работы" с инструкциями
;
;  Определяет тип, размер и параметры инструкции (использующиеся регистры, значения и тд). В том
; случае, если данные присутствуют в базе.
;  Данные о дешифрующих инструкциях (количество регистров использующихся для указания
; смещения расшифровываемых данных и регистры) складываются в буффер "dregs".
;
; on start :  oins - смещение инструкции
; on exit  :  eax = -1 если указанная инструкция неизвестна процедуре
;                 ecx - размер инструкции
;                 eax, edx - параметры, использующиеся в инструкции
;
 disasm    proc   oins: dword
;                                                                              
                mov    dword ptr [dregs],0
                sub      ecx,ecx
                push    esi                         ; запомним значение
                mov    esi,oins                  ; esi - смещение инструкции
                lodsb                                ;  al - первый байт инструкции
;                                                
; по первому байту распознаем тип инструкции
;
 @add_al_byte:  
                cmp    al,004h
                jz    _@add_al_byte
 @add_eax_dd:
                cmp    al,005h
                jz    _@add_eax_dd
 @sub_ereg_ereg:
                cmp    al,02bh
                jz    _@sub_ereg_ereg
 @xor_2ereg_b:
                cmp    al,030h
                jz    _@xor_2ereg_b
 @xor_ereg_ereg:
               cmp    al,031h
                jz    _@xor_ereg_ereg
;               
 @inc_ereg:
                cmp    al,040h
                jc        unknown_instr
                cmp    al,047h
                jbe   _@inc_ereg
 @push_ereg:
                cmp    al,050h
                jc        unknown_instr
                cmp    al,057h
                jbe   _@push_ereg
 @pop_ereg:     cmp    al,058h
                jc     unknown_instr
                cmp    al,05Fh
                jbe   _@pop_ereg
 @pushad:       
                cmp    al,060h
                jz    _@one_byte
 @add_reg_byte: 
                cmp    al,080h
                jz    _@add_reg_byte
 @xas_ereg_dd:  
                cmp    al,081h                 ; может быть XOR/ADD/SUB [EREG],DD
                jz    _@xas_ereg             ; проверка в "_@xas_ereg"
 @xas_ereg_b:   
                cmp    al,083h
                jz    _@xas_ereg
 @lea_er_er_dd:
                cmp    al,08Dh
                jz    _@lea_er_er_dd
 @pushfd:
               cmp    al,09Ch
                jz    _@one_byte
 @popfd: 
                cmp    al,09Dh
                jz    _@one_byte
 @scasd:  cmp    al,0AFh
                jz    _@one_byte
 @mov_reg_byte: 
                cmp    al,0B0h
                jc     unknown_instr
                cmp    al,0B7h
                jbe   _@mov_reg_byte
 @mov_ereg_dd:  
                cmp    al,0B8h
                jc     unknown_instr
                cmp    al,0BFh
                jbe   _@mov_ereg_dd
 @ret_byte:
                cmp    al,0C3h
                jz    _@one_byte
 @loop_byte:
                cmp    al,0E2h
                jz    _@loop_byte
 @call_dword:
               cmp    al,0E8h                      ;   
                jz    _@cj_dword                 ; инструкции call и jmp
 @jmp_dword:                                    ;
                cmp    al,0E9h                      ; требуют одинакового разбора
                jz    _@cj_dword                 ;  
 @cld:      cmp    al,0FCh
                jz    _@one_byte
 unknown_instr: 
                sub    eax,eax                       ; инструкции нет в базе
                dec    eax                              ; eax = -1
 disasm_ret:    
                pop    esi                              ; вспомним оригинальное значение и …
                ret                                         ;  выйдем в ...
;                                                
; inc [ereg]             10000reg
;
_@inc_ereg:     
                xor    al,1000000b               ; eax - регистр "ereg"
                inc    ecx                              ; ecx - размер инструкции
                jmp    disasm_ret
;               
; push [ereg]            1010reg
;
_@push_ereg:    
                xor    al,1010000b               ; eax - регистр "ereg"
                inc    ecx                              ; ecx - размер инструкции
                jmp    disasm_ret
;               
; pop [ereg]             1011reg
;
_@pop_ereg:     
                xor    al,1011000b               ; eax - регистр "ereg"
                inc     ecx                             ; ecx - размер инструкции
                jmp    disasm_ret
;               
; sub [eregA],[eregB]    02BHEX, 11rgArgB
;
_@sub_ereg_ereg:
                lodsb
                xor    al,11000000b
                call   two_regs                  ; edx - "rgA" / eax - "rgB"
                inc    ecx
                inc    ecx                           ; ecx - размер инструкции
                jmp    disasm_ret
;               
; xor [eregA+eregB],reg   030HEX, 00reg100, 00rgArgB
;
_@xor_2ereg_b: 
                lodsb
                lodsb
                call     two_regs                 ; edx - "rgA" / eax - "rgB"
                mov    cl,3                         ; ecx - размер инструкции
                inc      dword ptr [dregs]
                mov    dword ptr [dregs+008],eax
                jmp  _@xor_2ereg_dr
;               
; xor [eregA],[eregB]    031HEX, 00rgArgB
;
_@xor_ereg_ereg:
                lodsb
                call   two_regs                  ; edx - "rgA" / eax - "rgB"
                inc    ecx
                inc    ecx                           ; ecx - размер инструкции
_@xor_2ereg_dr: 
                inc     dword ptr [dregs]
                mov   dword ptr [dregs+004],edx
                jmp    disasm_ret
;               
; add al,byte             004HEX, byte
;
_@add_al_byte:  
                mov   cl,2                          ; ecx - размер инструкции
                sub     eax,eax                    ; eax - значение рег. (al)
                jmp    disasm_ret
;               
; add eax,dword          005HEX, dword = 5 bytes
;
_@add_eax_dd:   
                mov    cl,5                          ; ecx - размер инструкции
                sub      eax,eax                   ; eax - значение рег. (eax)
                jmp     disasm_ret
;               
; add [reg],byte         080HEX, 11000reg, byte
;
_@add_reg_byte: 
                lodsb 
                cmp    al,0c0h
                jc       unknown_instr
                cmp    al,0c7h
                ja        unknown_instr
                xor      al,11000000b              ; eax - значение рег. (eax)
                mov    cl,3                              ; ecx - размер инструкции
                jmp     disasm_ret
;               
; xor [ereg],dword       081HEX, 00110reg, dword = 6 bytes
; add [ereg],dword       081HEX, 11000reg, dword = 6 bytes
; sub [ereg],dword       081HEX, 11101reg, dword = 6 bytes
; add [ereg],byte          083HEX, 11000reg, byte = 3 bytes
; sub [ereg],byte          081HEX, 11101reg, byte = 3 bytes
;
_@xas_ereg:     
                mov    cl,3
                cmp    al,83h
                jz    _@xas_ereg_nfo
                add     cl,3
_@xas_ereg_nfo: 
                lodsb                                 ; al - второй байт инструкции
                cmp    al,037h                   ; если байт больше 37HEX
                ja    _@xas_ereg_chk        ; то перед нами "ADD" или "SUB"
_@xor_ereg:                                      ; разбор инструкции "xor"
                xor     al,00110000b           ; используемый в инструкции
                inc     dword ptr [dregs]
                mov    dword ptr [dregs+004],eax
                jmp    disasm_ret                ; регистр кладем в EDX
_@xas_ereg_chk:
                cmp    al,0C7h                   ; если байт больше 37HEX
                ja    _@sub_ereg                 ; то перед нами "SUB"
_@add_ereg:     
                xor     al,11000000b
                jmp    disasm_ret                ; разбор инструкции "add"
_@sub_ereg:     
                xor     al,11101000b
                jmp    disasm_ret                ; разбор инструкции "sub"
;               
; lea ergA,ergB+dword    08DHEX,10rgArgB, dword
;
_@lea_er_er_dd: 
                lodsb
                xor     al,10000000b             ; оставим в al только регистры
                call     two_regs                    ; edx - "rgA" / eax - "rgB"
                mov    cl,6                            ; ecx - размер инструкции
                jmp     disasm_ret
;               
; mov [reg],byte         10110reg, byte
;
_@mov_reg_byte: 
                xor    al,10110000b              ; eax - регистр "reg"
                inc    ecx
                inc    ecx                              ; ecx - размер инструкции
                jmp    disasm_ret
;               
; mov [ereg],dword       10111reg, dword
;
_@mov_ereg_dd:  
                xor    al,10111000b              ; eax - регистр "ereg"
                mov    cl,5                            ; ecx - размер инструкции
                jmp    disasm_ret
;               
; loop byte              0E2HEX, FE-byte = num   ; jump to IP-num
;
_@loop_byte:    
                sub     eax,eax
                mov   al,0feh                       ; al = 0FE
                sub     al,byte ptr [esi]         ; al = 0FE-byte
                inc     ecx
                inc     ecx                             ; размер инструкции
                jmp    disasm_ret
;               
; call dword             0E8HEX, dword
; jmp  dword           0E9HEX, dword
;
_@cj_dword:     
                lodsd                                 ; eax - dword
                mov    cl,5                         ; ecx - размер инструкции
                jmp    disasm_ret
;               
; разбор простейших однобайтовых инструкций
;
_@one_byte:     
                sub    eax,eax
                inc     ecx
                jmp    disasm_ret
 endp
;                                                                              
; two_regs - если регистр al содержит 00rgArgB (где rgA, rgB два регистра)
;                   то на выходе edx - rgA, eax - rgB
;
 two_regs           proc
                push   eax
                shr     al,3                      ; al = regB
                pop    edx
                push   eax
                rol     al,3
                xor    dl,al                     ; dl = regA
                pop    eax
                ret
 endp
;

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

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

4.1. Лирическое отступление

 Как становится ясно принцип «создания» эмулятора похож на алгоритм создания полиморфных движков.

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

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

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

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

  1. Инструкция запускается «напрямую», но в специальной среде.

  2. Исполнение инструкции имитируется полностью, без запуска.

  3. Исполнение инструкции имитируется до или после запуска в специальной среде.

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

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

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

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

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

 После запуска инструкции запомнить новые значения и установить значения использовавшиеся эмулятором.

 В качестве примера, более подробно рассмотрим участок кода из программы примера:

.data  ; сегмент данных
 regs          dd 8 dup (?)     ; переменная под значения регистров эмулируемого блока
 flags         dd ?             ; переменная под значения флагов эмулируемого блока
 temp          dd ?             ; переменная под значение флагов программы-эмулятора
;
; "instr_buf" - переменная под исполняемую инструкцию и имитацию стека
;  должна содержать:
;               mov    dword ptr [set_esp+1],esp
;               mov    esp,dword ptr [regs+16]
;               исполняемая инструкция
;               mov     dword ptr [regs+16],esp
;  set_esp:mov    esp,?
;                ret
;
 instr_buf: db 30 dup (?)
.code ; сегмент кода
; "прямой запуск" инструкции в (карантине) специальных условиях
; запомним значение регистров программы-эмулятора в стеке
 run_instr: push    eax ecx edx ebx ebp esi edi
; запомним значение флагов программы-эмулятора в стеке, затем перенесем в регистр eax,
; а из него в переменную "temp"
                pushfd                           
                mov     eax,dword ptr [esp]
                mov     dword ptr [temp],eax
                popfd
; значение флагов эмулируемого кода перенесем из переменной "flags" в регистр eax, 
; а из него в стек использующийся программой-эмулятором
                pushfd
                mov     eax,dword ptr [flags]
                mov     dword ptr [esp],eax
; установим значения регистров эмулируемого кода из переменной "regs"
; значение esp не устанавливается, иначе мы рискуем испортить значение флагов эмулируемого кода
; стек будет перенаправлен в подпрограмме "instr_buf"
                mov     eax,dword ptr [regs+000] 
                mov     ecx,dword ptr [regs+004] 
                mov     edx,dword ptr [regs+008] 
                mov     ebx,dword ptr [regs+012]
                mov     ebp,dword ptr [regs+020]
                mov     esi,dword ptr [regs+024]
                mov     edi,dword ptr [regs+028]
; инструкция "popfd" установит значение флагов эмулируемого кода, которое мы сохраняли выше
                popfd
                call       instr_buf                                ; запустим на исполнение инструкцию
; сохраним новые значения регистров эмулируемого кода в переменной "regs" 
                mov     dword ptr [regs+000],eax
                mov     dword ptr [regs+004],ecx
                mov     dword ptr [regs+008],edx
                mov     dword ptr [regs+012],ebx
                mov     dword ptr [regs+020],ebp
                mov     dword ptr [regs+024],esi 
                mov     dword ptr [regs+028],edi
; новое значение флагов эмулируемого кода запомним в стеке, затем перенесем в регистр eax
; и сохраним в переменной "flags" 
                pushfd
                mov     eax,dword ptr [esp]
                mov     dword ptr [flags],eax
                popfd
; установим значение флагов программы-эмулятора из переменной "temp"

                pushfd
                mov     eax,dword ptr [temp]
                mov     dword ptr [esp],eax
                popfd                            
; восстановим значение регистров программы-эмулятора
                pop     edi esi ebp ebx edx ecx eax

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

.data ; сегмент данных
buf_stack   db 50000 dup (?)
.code ; сегмент кода
                mov     edx,offset buf_stack+50000
                mov     dword ptr [regs+16],edx

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

 Этот алгоритм применяется там, где нельзя обойтись запуском в специальной среде. В программе-эмуляторе из примера, я использовал регистр “esi”, что бы указывать на смещение исполняемой в «данный момент» инструкции. Обычно полностью имитируется (совершаются различные действия, которые и будут являться аналогом работы инструкции) выполнение инструкций, которые изменяют этот указатель, т.е. CALL, JUMP, LOOP, RET и т.д.

 Не будем ходить далеко и в качестве примера возьмем простейшую инструкцию LOOP:

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

    dec      dword ptr [regs+4]
    cmp      dword ptr [regs+4],0
    jz       проверка на остановку эмуляции (emulate_check в программе примере)
             изменяем местоположение регистра esi, используя данные инструкции LOOP
    jmp      проверка на остановку эмуляции

4.2.3. Комбинация двух способов

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

 В программе-примере присутствует один тип инструкции, которая имитируется этим способом:

xor dword ptr [erega],? и ее аналог xor dword ptr [erega],eregb

 Почему же эти инструкции требуют комбинированного способа имитации? Все очень просто и логично… При расшифровке данных, (в «декрипторах») используются регистры, указывающие на смещение расшифровываемых данных. Этот регистр может меняться в процессе расшифровки, таких регистров может быть несколько, все зависит от полиморфного алгоритма, который используется. Когда для эмуляции мы копируем в «свой» буфер код расшифровщика и зашифрованные данные, то соответственно смещение зашифрованного кода меняется, но в расшифровщике будет указанно предыдущее его расположение. Да, эту инструкцию можно запустить напрямую. Но, прежде всего, нужно исправить значение этого (в данном случае erega) регистра, на новое, которое будет указывать «правильное» местоположение зашифрованных данных в буфере.

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

  1. Дизассемблер разбирает инструкцию, и после его работы передает информацию о размере инструкции и регистрах, которые используются ней в программу-эмулятор. В нашем случае регистр edx будет содержать номер регистра erega, а eax номер eregb.

  2. Значения регистров эмулируемого кода содержится в буфере regs, под каждый регистр дается по 4 байта буфера. Последовательность регистров: eax, ecx, edx, ebx, esp, ebp, esi и edi.

  3. При вызове эмулятора, ему передается предыдущее расположение эмулируемого кода в памяти (переменная ip)!

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

            call      get_ip
get_ip:     pop       ebp
            sub       ebp, offset get_ip

 Если прибавить значение регистра ebp, к любому смещению получается «необходимое» нам, новое (смещение).

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

; не забыли, что а) edx содержит номер регистра erega, использующегося в инструкции
;                           б) под значение каждого регистра используется 4 байта, …
;                           
; из всего вышеуказанного следует, 
; что расположение значения регистра в буфере regs = 4 * edx
                mov     eax,4
                mul      edx
; допустим (только допустим!), что макс. размер расшифровщика может составлять 200 байт
; значит (!) если значение регистра "regs+eax" (erega) находится в промежутке от ip до ip+200,
; то менять значение не обязательно, вполне возможно что:
;  - декриптор содержит в себе процедуру определения ip
;  - значение регистра уже менялось раньше, т.е. инструкция выполняется не первый раз
; если одно из вышеуказанных утверждений правда (true), то просто запускаем инструкцию на
; выполнение, как было описано в 4.2.1
                mov     edx,ip                   
                cmp     dword ptr [regs+eax],edx 
                jc         run_instr
                add      edx,200
                cmp     dword ptr [regs+eax],edx
                ja         run_instr
;
;  если значение erega выпадает из промежутка, 
;  то: a) необходимо рассчитать разницу между старым ip и значением erega (индексного регистра)
;        б) добавить эту разницу к смещению начала буфера (codeloc), в который 
;            мы копировали эмулируемый код
;        в) полученное смещение поместить в значение регистра erega
                mov     edx,dword ptr [regs+eax]
                sub       edx,ip       ; edx = может быть размером "(де)криптора"
                add      edx,codeloc              
                mov     dword ptr [regs+eax],edx 

 Теперь установив в регистр новое значение, можно (смело) запускать инструкцию на выполнение.

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

4.3. Поворот не туда

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

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

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

xor byte ptr [erega+eregb],reg

 “reg” является любым 8-битным регистром, что это конкретно за регистр и каково его значение, нас вообще мало интересует. Это утверждение правдиво (истина), только в нашем случае, так как разработкой анализатора кода мы пока не занимаемся (точнее сказать не занимаемся на должном уровне). Больше интересует комбинация регистров erega и eregb, так как их неверное (в совокупности) значение может помешать процессу расшифровки идти в правильном направлении (смотри пункт 4.2.3)!

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

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

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

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

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

первый байт

второй байт

третий байт

00110000

00zzz100

00xxxyyy

определитель типа инструкции

zzz- reg

xxxrega, yyy - regb

 Нам необходимо получить следующие данные:

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

  2. Значения этих регистров.

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

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

.data
; первые 4 байта, количество регистров
; все последующие, регистры
dregs       dd 010 dup (?)
.code
; допустим, что мы разбираем нужную нам инструкцию.
; esi - содержит смещение разбираемой инструкции
;
_work:     lodsw         ; загрузим в ax - первые два байта инструкции
                lodsb          ; загрузим в al последний байт инструкции, 
				               ; необходимый для разбора
                push   eax
                shr    al,3    ; al = regb
                pop    edx
                push   eax
                rol    al,3
                xor    dl,al   ; dl = regb
                mov    edi,offset dregs
                sub    eax,eax
                mov    al,2
                stosd           ; установим число регистров, использующихся 
				                ; в инструкции (в нашем случае 2)
                pop    eax
                stosd           ; номер регистра regb в dregs+004
                xchg   eax,edx
                stosd           ; номер регистра rega в dregs+008

 Теперь Мы имеем всю информацию, необходимую эмулятору для имитации исполнения инструкции.

4.3.2. Эмулятор

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

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

; первая часть процесса имитации выполнения инструкции типа xor byte ptr [erega+eregb],reg
;
; сейчас буфер "dregs" содержит примерно следующие данные:
;  dregs + 000 : 002 (количество регистров использующихся в инструкции)
;  dregs + 004 : eregb (номер регистра)
;  dregs + 008 : erega (номер регистра)
;
; Нам необходимо определить сумму значений регистров (в данном случае двух) использующихся
; в инструкции и передать ее во вторую часть процесса. В нашем случае это будет выглядеть
; примерно так:
;   a = [dregs+004] * 4
;   x = [regs+a]
;   a = [dregs+008] * 4
;   x = x + [regs+a]
; где x является той самой суммой, которую необходимо узнать
;
fxor:   push    ebx esi
        sub      edx,edx
        mov      dl,4             ; edx = 4, для использования в цикле
        sub      ebx,ebx          ; ebx = 0, будет содержать сумму значений всех регистров
        lea      esi,offset dregs ; esi = данные и регистрах, полученные из дизассемблера
        lodsd                     ; eax = число регистров, использующихся в инструкции
        xchg     eax,ecx          ; ecx = eax
fxor_lp1: push    edx             ; запомним значение edx (=4), что бы востановить после порчи в
        lodsd                     ; eax = номер регистра использующегося в инструкции
        mul      edx              ; eax = eax * 4 = расположение значения регистра в "regs"
        add      ebx,dword ptr [regs+eax] ; добавим в ebx, знач. регистра использующегося в инструкции
        pop      edx              ; edx = 4
        loop     fxor_lp1         ; продолжим, составлять сумму значений регистров, если необходимо
        xchg     ebx,ecx          ; ecx - сумма значений регистров (x)
        pop      esi ebx
; вторая часть процесса имитации выполнения …
;
; допустим (только допустим!), что макс. размер расшифровщика может составлять 200 байт
; значит (!) если сумма значений регистров (x)  находится в промежутке от ip до ip+200,
; то менять значение не обязательно, вполне возможно что:
;  - декриптор содержит в себе процедуру определения ip
;  - значение регистра уже менялось раньше, т.е. инструкция выполняется не первый раз
; если одно из вышеуказанных утверждений правда (true), то просто запускаем инструкцию на
; выполнение, как было описано в 4.2.1
;
fxor_ip:        cmp     ecx,ip
                jc      run_instr
                cmp     ecx,ip+200
                ja      run_instr
;  третья часть процесса имитации выполнения …
;
;  если значение x выпадает из промежутка, 
;  то: a) необходимо рассчитать разницу между старым ip и x (суммой значений регистров)
;        б) добавить эту разницу к смещению начала буфера (codeloc), в который 
;            мы копировали эмулируемый код
;        в) полученное смещение поместить в значение регистра "dreg+004"
;        г) значение остальных регистров использующихся в инструкции обнулить
                sub      ecx,ip
                add      ecx,codeloc
; новое смещение зашифрованного кода, полученное действиями, описанными в пунктах а и б
; положив в регистр номер "dreg+004"
                mov     eax,dword ptr [dregs+004]
                sub      edx,edx
                mov     dl,4
                push    edx
                mul     edx
                pop     edx
                mov     dword ptr [regs+eax],ecx
;
; цикл, для обнуления значений всех регистров начиная с "dreg+008"
;
                push    esi
                mov     esi,offset dregs
                lodsd             ; eax - число регистров использующихся в инструкции
                cmp     al,02
                jc     fxor_eret  ; может быть инструкция использует только один регистр
                xchg    eax,ecx   ; ecx = eax - число регистров использующихся в инструкции
                dec     ecx       ; т.к. мы уже работали с "dreg+004", то уменьшим число регистров
                lodsd             ; eax - номер регистра "dreg+004", пропустим его
fxor_slp: push    edx
                lodsd             ; eax - номер регистра
                mul     edx       ; eax = eax * 4 = расположение значения регистра в "regs"
                mov     dword ptr [regs+eax],0 ; обнулим значение регистра
                pop     edx
                loop   fxor_slp
fxor_eret: pop     esi
                jmp     run_instr    ; теперь можно запустить инструкцию на выполнение

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

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

4.4. «Предел терпения» (Enough)

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

 Можно завершать работу:

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

 Я использовал способы 1 и 3, именно для этого я вставил в (рас)шифровщик инструкцию nop, которую эмулятор не понимает и соответственно на ней прекращает свою работу.

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

5. Пример использования технологии для детектирования вирусов

 Теперь рассмотрим, как можно использовать этот (представленный в примере) эмулятор, для детектирования вирусов.

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

 В качестве «жертвы», я взял один из простейших шифрованных вирусов – Win32.Ditto.1488. Исходные тексты этого вируса были, опубликованы в журнале Duke's Virus Labs номер 10. Они доступны в архиве, прилагающемся к этой статье, так же доступны исполняемые файлы, зараженные этим «вирусом».

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

 Программа детектирования состоит из:

  • Основной программы, которая определяет точку входа в указанном PE-файле. Читает данные, находящиеся в точке входа и запускает их на эмуляцию.

  • Эмуляторадизассемблера) использовавшегося в программе-примере, с минимальными отличиями (для совместной работы с анализатором).

  • Анализатора программного кода - процедуры детектирования вирусного кода, встроенной в эмулятор.

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

5.1. Кодо-анализатор

 Как я уже упоминал раньше, кодо-анализатор предназначен для учета особенностей кода исследуемого (эмулируемого) участка. Анализатор может быть отдельной частью антивируса, но вполне может, является частью эмулятора, работая в паре, они будут более «успешно действовать».

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

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

 Итак, преступим к описанию кодо-анализатора, который представлен в примере «антивируса» и позволяет определять наличие вируса в исполняемых файлах формата PE.

 Работает цикл имитации исполнения инструкций (он же эмулятор):

                …
 emulate_loop:
                push    8                           ; длина вирусной сигнатуры
                push    offset vir_sig         ; расположение вирусной сигнатуры
                push    esi                          ; инструкция, которая будет эмулироваться
                call      detect                    ; сравним сигнатуру, с участком кода с позиции esi
                or        eax,eax
                jz         emulate_dasm       ; если не совпала, эмулируем дальше
                inc       byte ptr [ill_mark] ; иначе устанавливаем флаг зараженности файла …
                jmp      emulate_ret           ; … и прекратим эмуляцию, файл заражен!
;               
 emulate_dasm:  
                push    esi
                call      disasm                   ; разберем инструкцию с помощью дизассемблера
                …                

 Процедура detect в нашем случае и является кодо-анализатором, она «анализирует» является ли код в позиции esi вирусным, т.е. совпадает с сигнатурой вируса Win32.Ditto.1488.

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

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

 Собственно текст процедуры «кодо-анализатора» представлен ниже:

; проверка участка на совпадение с сигнатурой "sloc", длины "slen"
;
; on start : slen - длина сигнатуры
;                sloc - расположение сигнатуры
;                cloc - расположение кода для сверки
; on exit  : eax = 0 - не совпадают, иначе сигнатура совпала
;
 detect      proc   cloc:dword, sloc: dword, slen: dword
;                                                                              
                push    ecx esi edi
                mov     ecx,slen
                mov     esi,sloc
                mov     edi,cloc
 det_lp:    lodsb
                cmp     byte ptr [edi],al
                jz         det_cnt
                sub      eax,eax
                jmp     det_ret
 det_cnt:  inc      edi
                loop    det_lp
                mov     al,1
 det_ret:   pop     edi esi ecx
                ret
 endp
;

 Флаг “ill_mark” представляет собой обычную переменную размером один байт. После завершения работы эмулятора, главная программа проверяет значение этой переменной, если оно равно единице, значит во время работы эмулятора, кодо-анализатор обнаружил вирусный код в файле. Иначе файл здоров.

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

 Если сигнатура состоит из 20 байт, и совпали только 18, или немного меньше … то вполне возможно, что Нам попалась модификация уже известного вируса. Можно (даже нужно) оповестить об этом пользователя.

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

 К статье прилагаются исходные тексты:

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

  • «Антивируса» предназначенного для детектирования вируса Win32.Ditto.1488 (по классификации AVP) в файлах формата PE.
  • Исходный код вируса Win32.Ditto.1488 (он жеWin32.Demo.1488) и PE-файлы инфицированные (зараженные) этим вирусом (расширение файлов было изменено, чтобы избежать вероятности случайного распространения вируса).

Скачать

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

 Если Вы заинтересовались разработкой антивирусов, очень советую почитать русские антивирусные журналы «Земский Фершал», в которых можно встретить множество интересной информации.

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

2002-2013 (c) wasm.ru