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

Дата публикации 27 янв 2004

Антивирусные технологии: эмуляция программного кода — Архив 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]

Код (Text):
  1.  
  2. ; name   : "code emulation" example
  3. ; author : andy [Most Needful Things]
  4. ; home   : http://amethyst.nm.ru
  5. ;
  6. ;  Пример программы умеющей выполнять "псевдо-эмуляцию" программного кода.
  7. ;
  8. ; компилировать:
  9. ; tasm32 -ml -m5 emul.asm
  10. ; tlink32 -Tpe -c -x emul.obj ,,, import32
  11. ;
  12. .386P
  13. .model flat, stdcall
  14.  jumps
  15. ;
  16.  extrn                  lstrlenA:near            ; список needful апи
  17.  extrn                 _wsprintfA:near
  18.  extrn                  GetStdHandle:near
  19.  extrn                  WriteConsoleA:near
  20.  extrn                  ExitProcess:near
  21. ;                      
  22. .data
  23. _stack                 db 10000 dup (?)          ; буфер под стэк
  24.  temp                   dd ?
  25.  instr_buf:            db 30 dup (?)              ; буфер для запуска инструкции в "карантине"
  26.  buffer                 db 10000 dup (?)          ; буфер под код
  27.  flags                   dd ?                              ; значение флагового регистра EFLAGS эмулируемого кода
  28.  regs                    dd 08 dup (?)                ; значения eax/ecx/edx/ebx/esp/ebp/esi/edi эмулируемого кода
  29. ;                                                                              
  30. .code
  31.  start:      mov     esi,offset decryptor            ; смещ. (рас)шифрощика и текста
  32.                mov     ecx,(emulate-decryptor)/4 ; размер (рас)шифровщика и текста
  33.                mov     edi,offset buffer                 ; перенесем данные в буфер
  34.                 rep      movsd
  35. ;              
  36.                 push    offset decryptor         ; старое значение ip (рас)шифровщика
  37.                 push    (emulate-decryptor)   ; размер кода для эмуляции
  38.                 push    offset buffer               ; расположение кода
  39.                 call      emulate                      ; эмулируем, т.е. зашифровываем
  40.                                                                ; текст идущий после (рас)шифровщика
  41. ;              
  42.                 mov     edi,offset buffer         ; esi - смещ. буфера
  43.                 add       edi,(message-decryptor)
  44.                 push     edi
  45.                 call      decryptor_sze            ; расшифруем текст из буфера
  46.                 pop      esi
  47. ;
  48. ;  В данный момент ESI содержит смещение расшифрованного текста, теперь осталось вывести
  49. ; текст на экран. Поддержку консоли я убрал из исходных текстов, представленных в статье  (полные
  50. ; доступны в архиве).
  51. ;
  52.                 …
  53. ;              
  54.  exit:        push    0
  55.                 call      ExitProcess
  56. ;              
  57. ; decryptor - простейший алгоритм (раз)шифроки текста идущего после ...
  58. ;
  59. ;  Эмулятор "не понимает" инструкцию nop, которая следует после цикла
  60. ; (рас)шифровки, по этому когда он дойдет до инструкции, то выйдет с "ошибкой".
  61. ; Но самое главное, что данные будут (за/рас)шифрованны ... в противном случае
  62. ; эмулятор бы просто завис в вечном цикле. ;-)
  63. ;
  64. decryptor:mov    edi,offset message
  65.                 pushfd
  66.                 popfd
  67.                 call      aaa
  68.                 jmp     decryptor_sze
  69.                            db 125 dup (?)
  70.  aaa:         sub     edi,0ffffffh
  71.                 add     edi,0ffffffh-004h
  72.                 scasd
  73.                 ret
  74.  decryptor_sze:
  75.                 mov    ecx,0
  76.                 add     cl,( (emulate-message) / 4 )
  77.  decrypt_loop:
  78.                 xor      dword ptr [edi],0ffffffh
  79.                 scasd
  80.                 loop    decrypt_loop
  81.                 nop
  82.                 ret
  83.  message             db 'Hey you, ugly face!',13,10
  84.                            db 'Give me the bottle of tequilla!',13,10
  85.                            db 'Hurry up, don''t make me mad!',13,10,0
  86. ;                                                                              
  87.  include                inc\emulator.inc         ; эмуляция кода
  88.  include                inc\disasm.inc            ; дизассемблер
  89. ;                                                                              
  90.                 end      start
  91. [inc\emulator.inc]
  92. .code
  93. ;                                                                              
  94. ; emulate - эмуляция участка кода
  95. ;
  96. ; on start :       ip - оригинальный ip блока
  97. ;            codesize - размер блока для эмуляции
  98. ;             codeloc - смещение на код для эмуляции
  99. ; on exit  :  - - - - -
  100. ;
  101.  emulate        proc   codeloc: dword, codesize: dword, ip: dword
  102. ;                                                                              
  103. ; процесс инициализации эмулятора, перед работой с участком кода
  104. ;  а) сохранение значения регистров
  105. ;  б) установка параметров переданных в процедуру (ip/codesize/codeloc)
  106. ;  в) установка значения стека, для эмуляции кода
  107. ;
  108.  emul_init:
  109.                push    eax ecx edx ebx ebp esi edi
  110.                mov     esi,offset _stack+10000    ; буфер под стек, для эмуляции
  111.                mov     dword ptr [regs+016],esi  ; установим значение
  112.                 mov     esi,codeloc                       ; смещение на начало кода
  113.                 mov     ebx,esi
  114.                 add      ebx,codesize                    ; смещение на конец кода
  115. ;                                                
  116.  emulate_loop:  
  117.                 push    esi                        ; включаем кодо-анализатор
  118.                 call     disasm                   ; помощью дизассемблера
  119.                 cmp     eax,-1                   ; если инструкция известна
  120.                 jnz      copy_instr             ; дизассемблеру, продолжим разбор
  121.  emulate_error:
  122.                  jmp     emulate_ret         ; иначе (!) тихо выйдем
  123. ;                                                
  124. ; перенесем в буфер "instr_buf" инструкции для эмуляции стека и
  125. ; разобранную дизассемблером инструкцию размера ecx
  126. ;
  127. ; в результате получим в "instr_buf" примерно:
  128. ;
  129. ;               mov     dword ptr [res_stack+1],esp
  130. ;               mov     esp,dword ptr [regs+016]
  131. ;           ... разобранная инструкция ...
  132. ;               mov     dword ptr [regs+016],esp
  133. ; res_stack:    mov     esp, ?
  134. ;               ret
  135. ;
  136.  copy_instr:    
  137.                 push     eax ecx                 ; запомним данные о инструкции, которую разбирал дизассемблер
  138.                 mov     edi,offset instr_buf
  139.                 mov     ax,2589h
  140.                 stosw                                ; перенесем "mov 4 ptr [?],esp"
  141. ;              
  142.                 mov     eax,offset instr_buf    ; рассчитаем смещение
  143.                 add      eax,ecx                       ; "res_stack+1" (см. выше) и
  144.                 add       eax,6+6+6+1             ; перенесем остаток инструкции
  145.                 stosd
  146. ;              
  147.                 mov     ax,258Bh                   ; перенесем инструкцию
  148.                 stosw                                      ; "mov esp,4 ptr [regs+16]"
  149.                 mov     eax,offset regs+016
  150.                 stosd
  151. ;              
  152.                 rep     movsb                          ; перенесем разобранную, дизассемблером инструкцию
  153. ;              
  154.                 mov     ax,2589h                   ; перенесем инструкцию
  155.                 stosw                                     ; "mov 4 ptr [regs+16],esp"
  156.                 mov     eax,offset regs+016
  157.                 stosd
  158. ;              
  159.                 mov     al,0bch                     ; перенесем инструкцию
  160.                 stosb                                     ; "mov esp,?"
  161.                 stosd
  162.                 mov     al,0c3h                     ; "поставим последнюю точку" - перенесем "ret"
  163.                 stosb                                    
  164.  
  165. ;
  166.                 pop      ecx eax                    ; вспомним данные инструкции
  167. ;                                                                              
  168. ; работа с инструкциями, которые требуют специальной имитации исполнения
  169. ; первый этап - определение типа инструкции
  170. ;
  171.                 push    esi
  172.                 pop     edi                            ; edi - смещение инструкции
  173.                 sub      edi,ecx                     ;  которую разбирали
  174. ;                          инструкции размером 1 байт
  175.  @one_byte:    
  176.                 cmp     cl,1
  177.                 jnz    @two_byte
  178.                 cmp     byte ptr [edi],0c3h   ;  
  179.                 jz     @found_ret             ; найден "ret" ?
  180. ;                          инструкции размером 2 байта
  181.  @two_byte:    
  182.                 cmp  cl,2
  183.                 jc      run_instr                  ; если размер меньше или не равен
  184.                 jnz    @three_byte
  185.  @check_xor:    cmp     byte ptr [edi],031h   ;  
  186.                 jz     @found_xor             ; (де)шифрующая инструкция xor?
  187.  @check_loop:   cmp     byte ptr [edi],0e2h   ;  
  188.                 jz     @found_loop            ; найден "loop"?
  189.                 jmp     run_instr
  190. ;                         инструкции размером 3 байта
  191.  @three_byte:  
  192.                 cmp   cl,3
  193.                 jc       run_instr                  ; если размер меньше или не равен
  194.                 jnz    @five_byte
  195.  @check_xor2:
  196.                cmp     byte ptr [edi],030h   ; найдена разновидность
  197.                 jz     @found_xor               ; (де)шифрующей инструкции xor?
  198.                 jmp     run_instr
  199. ;                         инструкции размером 5 байт
  200.  @five_byte:    
  201.                 cmp  cl,5
  202.                 jc      run_instr                    ; если размер меньше или не равен
  203.                 jnz    @six_byte                 ; 5 байтам запустим в "карантине"
  204.                 cmp     byte ptr [edi],0e8h  ;  
  205.                 jz     @found_call               ; найден "call" ?
  206.                 cmp     byte ptr [edi],0e9h  ;  
  207.                 jz     @found_jmp               ; найден "jmp" ?
  208.                 jmp     run_instr
  209. ;                          инструкции размером 6 байт
  210.  @six_byte:    
  211.                cmp     cl,6
  212.                 jc        run_instr                      ; если размер меньше или не равен
  213.                 jnz       run_instr                     ; 6 байтам запустим в "карантине"
  214.                 cmp     byte ptr [edi],081h      ; "add/sub/xor [ereg], dword" ?
  215.                 jnz       run_instr                     ; нет, запустим в "карантине"
  216.                 cmp     byte ptr [edi+1],037h ; нам нужен только "xor", а
  217.                 jna    @found_xor                  ; остальные можно просто запустить
  218.                 jmp       run_instr                    ; на исполнение в "карантине"
  219. ;                                                                              
  220. ; работа с инструкциями, которые требуют специальной имитации исполнения
  221. ; второй этап - имитация исполнения инструкции
  222. ;
  223. ;                             инструкции размером 1 байт
  224. ; имитировать исполнение инструкции "ret" очень просто:
  225. ;  а) необходимо взять двойное слово из стека эмулируемого кода
  226. ;     смещение на это слово содержится в "regs+16"
  227. ;  б) это слово будет новым указателем на смещение разбираемой эмулятором
  228. ;     инструкции (значение регистра esi)
  229. ;  в) убрать из стека эмулируемого кода, взятое значение
  230. ;     add dword ptr [regs+16],4
  231. ;
  232.  @found_ret:    
  233.                 mov     esi,dword ptr [regs+016]
  234.                 lodsd
  235.                 add     dword ptr [regs+016],4
  236.                 xchg    esi,eax
  237.                 jmp     emulate_check
  238. ;                            инструкции размером 2 байта
  239. ; Первая часть процесса имитации выполнения дешифрующей инструкции
  240. ;
  241. ; сейчас буфер "dregs" содержит примерно следующие данные:
  242. ;  dregs + 000 : ????? (количество регистров использующихся в инструкции)
  243. ;  dregs + 004 : номер регистра 1
  244. ;  dregs + 008 : номер регистра 2
  245. ;  dregs +  n  : номер регистра n / 4
  246. ;
  247. ;  Нам необходимо определить сумму значений регистров (в данном случае двух)
  248. ; использующихся в инструкции и передать ее во вторую часть процесса. В нашем
  249. ; случае это будет выглядеть примерно так:
  250. ;
  251. ;   a = [dregs+004] * 4
  252. ;   x = [regs+a]
  253. ;   a = [dregs+008] * 4
  254. ;   x = x + [regs+a]
  255. ;   и так далее ...
  256. ;
  257. ; где x является той самой суммой, которую необходимо узнать
  258. ;
  259.  @found_xor:    
  260.                 push    ebx esi
  261.                 sub      edx,edx
  262.                 mov     dl,4
  263.                 sub      ebx,ebx
  264.                 mov     esi,offset dregs
  265.                 lodsd
  266.                 xchg    eax,ecx
  267.  @fxor_load_lp:
  268.                 push     edx
  269.                 lodsd
  270.                 mul      edx
  271.                 add      ebx,dword ptr [regs+eax]
  272.                 pop      edx
  273.                 loop   @fxor_load_lp
  274.                 xchg     ebx,ecx
  275.                 pop      esi ebx
  276. ;
  277. ; Вторая часть процесса имитации выполнения …
  278. ;
  279. ; допустим (только допустим!), что максимальный размер расшифровщика
  280. ; может составлять 200 байт
  281. ; значит (!) если сумма значений регистров (x) находится в промежутке
  282. ; от ip до ip+200,
  283. ; то менять значение не обязательно, вполне возможно что:
  284. ;  - декриптор содержит в себе процедуру определения ip
  285. ;  - значение регистра уже менялось раньше, т.е. инструкция выполняется
  286. ;    не первый раз
  287. ; если одно из вышеуказанных утверждений правда (true), то просто запускаем
  288. ; инструкцию на выполнение в специальных условиях
  289. ;
  290.  @found_xor_ip:
  291.                 mov     edx,ip
  292.                 cmp     ecx,edx
  293.                 jc         run_instr
  294.                 add      edx,200
  295.                 cmp     ecx,edx
  296.                 ja         run_instr
  297. ;
  298. ; Третья часть процесса имитации выполнения …
  299. ;
  300. ;  если значение x выпадает из промежутка,
  301. ;  то: a) необходимо рассчитать разницу между старым ip и x (суммой
  302. ;         значений регистров)
  303. ;      б) добавить эту разницу к смещению начала буфера (codeloc), в который
  304. ;         мы копировали эмулируемый код
  305. ;      в) полученное смещение поместить в значение регистра "dreg+004"
  306. ;      г) значение остальных регистров использующихся в инструкции обнулить
  307. ;
  308.                 sub     ecx,ip
  309.                 add     ecx,codeloc
  310. ;
  311. ; новое смещение зашифрованного кода, полученное действиями, описанными
  312. ; в пунктах "а" и "б" (см. выше), положив в регистр номер "dreg+004"
  313. ;
  314.                 mov     eax,dword ptr [dregs+004]
  315.                 sub       edx,edx
  316.                 mov     dl,4
  317.                 push     edx
  318.                 mul      edx
  319.                 pop      edx
  320.                 mov     dword ptr [regs+eax],ecx
  321. ;
  322. ; цикл, для обнуления значений всех регистров начиная с "dreg+008"
  323. ;
  324.                 push    esi
  325.                 mov     esi,offset dregs
  326.                 lodsd
  327.                 cmp     al,02
  328.                 jc     @fxor_emu_ret
  329.                 xchg    eax,ecx
  330.                 dec     ecx
  331.                 lodsd
  332.  @fxor_setz_lp:
  333.                 push    edx
  334.                 lodsd
  335.                 mul      edx
  336.                 mov     dword ptr [regs+eax],0
  337.                 pop      edx
  338.                 loop   @fxor_setz_lp
  339.  @fxor_emu_ret:
  340.                 pop     esi
  341.                 jmp     run_instr    ; можно запустить инструкцию на выполнение
  342. ;              
  343. ; имитируя исполнение инструкции "loop", мы должны уменьшить значение ecx
  344. ; эмулируемого блока на 1, если он не будет равен 0, то перейти к "началу цикла"
  345. ;
  346.  @found_loop:
  347.                dec      dword ptr [regs+004]     ; уменьшим значение регистра
  348.                 cmp     dword ptr [regs+004],0 ; "ecx" эмулируемого кода
  349.                 jz        emulate_check               ; если равно 0, выйдем из цикла
  350.                 sub      esi,ecx                           ; иначе рассчитаем смещение
  351.                 sub       esi,eax                           ; начала цикла
  352.                 jmp      emulate_check               ; перейдем к следующей инструкии
  353. ;                            инструкции размером 5 байт
  354. ; если перед нами инструкции "call" и "jmp" то необходимо изменить смещение
  355. ; (значение esi) инструкции, которая будет следующей исполняться эмулятором
  356. ; НО в случае если мы разбираем "call", нужно сохранить в стеке эмулируемого
  357. ; участка смещение команды следующей прямо за call'ом, что бы потом по встрече
  358. ; "ret" вернуться "на путь истинный"
  359. ;
  360.  @found_call:  
  361.                 push    eax edi
  362.                 mov     eax,esi
  363.                 sub      dword ptr [regs+016],4
  364.                 mov     edi,dword ptr [regs+016]
  365.                 stosd
  366.                 pop      edi eax
  367.  @found_jmp:    
  368.                  add      esi,eax                  ; изменим esi
  369. ;              
  370. ; esi - указатель на инструкцию для разбора
  371. ; ebx - предел, за который выйти "непростительно"
  372. ;
  373.  emulate_check:
  374.                 cmp     esi,ebx                       ; если мы не вышли за пределы
  375.                 jc         emulate_loop             ; продолжим цикл эмуляции
  376.  emulate_ret:
  377.                 pop     edi esi ebp ebx edx ecx eax
  378.                 ret
  379. ;                                                                              
  380. ; "прямой запуск" разобранной инструкции в специальных условиях (карантине)
  381. ;
  382. ; запомним значение регистров программы-эмулятора в стеке
  383. ;
  384.  run_instr:push    eax ecx edx ebx ebp esi edi
  385. ;
  386. ; запомним значение флагов программы-эмулятора в стеке, затем перенесем
  387. ; в регистр eax, а из него в переменную "temp"
  388. ;
  389.  save_orig_efl:
  390.                 pushfd
  391.                 mov     eax,dword ptr [esp]
  392.                 mov     dword ptr [temp],eax
  393.                 popfd
  394. ;
  395. ; значение флагов эмулируемого кода перенесем из переменной "flags" в регистр
  396. ; eax, а из него в стек использующийся программой-эмулятором
  397. ;
  398.  set_emul_efl:
  399.                 pushfd
  400.                 mov     eax,dword ptr [flags]
  401.                 mov     dword ptr [esp],eax
  402. ;
  403. ; установим значения регистров эмулируемого кода из переменной "regs"
  404. ; значение esp не устанавливается, иначе мы рискуем испортить значение флагов
  405. ; эмулируемого кода, стек будет перенаправлен в подпрограмме "instr_buf"
  406. ;
  407.                 mov     eax,dword ptr [regs+000]
  408.                 mov     ecx,dword ptr [regs+004]
  409.                 mov     edx,dword ptr [regs+008]
  410.                 mov     ebx,dword ptr [regs+012]
  411.                 mov     ebp,dword ptr [regs+020]
  412.                 mov     esi,dword ptr [regs+024]
  413.                 mov     edi,dword ptr [regs+028]
  414. ;
  415. ; инструкция "popfd" установит значение флагов эмулируемого кода, которое мы сохраняли выше
  416. ;
  417.                 popfd
  418.                 call       instr_buf                ; запустим на исполнение
  419. ;
  420. ; сохраним новые значения регистров эмулируемого кода в переменной "regs"
  421. ;
  422.                 mov     dword ptr [regs+000],eax
  423.                 mov     dword ptr [regs+004],ecx
  424.                 mov     dword ptr [regs+008],edx
  425.                 mov     dword ptr [regs+012],ebx
  426.                 mov     dword ptr [regs+020],ebp
  427.                 mov     dword ptr [regs+024],esi
  428.                 mov     dword ptr [regs+028],edi
  429. ;
  430. ; новое значение флагов эмулируемого кода запомним в стеке, затем перенесем
  431. ; в регистр eax и сохраним в переменной "flags"
  432. ;
  433.  save_emul_efl:
  434.                 pushfd
  435.                 mov     eax,dword ptr [esp]
  436.                 mov     dword ptr [flags],eax
  437.                 popfd
  438. ;
  439. ; установим значение флагов программы-эмулятора из переменной "temp"
  440. ;
  441.  set_orig_efl:
  442.                 pushfd
  443.                 mov     eax,dword ptr [temp]
  444.                 mov     dword ptr [esp],eax
  445.                 popfd
  446. ;
  447. ; восстановим значение регистров программы-эмулятора
  448. ;
  449.                 pop     edi esi ebp ebx edx ecx eax
  450.                 jmp     emulate_check
  451.  endp
  452. ;
  453. [inc\disasm.inc]
  454.  .data
  455.  dregs                 dd 010 dup (?)  ; буфер для хранения данных о регистрах использующихся в дешифрующих инструкциях
  456. .code
  457. ;                                                                              
  458. ; disasm - дизассемблер, предназначается для "работы" с инструкциями
  459. ;
  460. ;  Определяет тип, размер и параметры инструкции (использующиеся регистры, значения и тд). В том
  461. ; случае, если данные присутствуют в базе.
  462. ;  Данные о дешифрующих инструкциях (количество регистров использующихся для указания
  463. ; смещения расшифровываемых данных и регистры) складываются в буффер "dregs".
  464. ;
  465. ; on start :  oins - смещение инструкции
  466. ; on exit  :  eax = -1 если указанная инструкция неизвестна процедуре
  467. ;                 ecx - размер инструкции
  468. ;                 eax, edx - параметры, использующиеся в инструкции
  469. ;
  470.  disasm    proc   oins: dword
  471. ;                                                                              
  472.                 mov    dword ptr [dregs],0
  473.                 sub      ecx,ecx
  474.                 push    esi                         ; запомним значение
  475.                 mov    esi,oins                  ; esi - смещение инструкции
  476.                 lodsb                                ;  al - первый байт инструкции
  477. ;                                                
  478. ; по первому байту распознаем тип инструкции
  479. ;
  480.  @add_al_byte:  
  481.                 cmp    al,004h
  482.                 jz    _@add_al_byte
  483.  @add_eax_dd:
  484.                 cmp    al,005h
  485.                 jz    _@add_eax_dd
  486.  @sub_ereg_ereg:
  487.                 cmp    al,02bh
  488.                 jz    _@sub_ereg_ereg
  489.  @xor_2ereg_b:
  490.                 cmp    al,030h
  491.                 jz    _@xor_2ereg_b
  492.  @xor_ereg_ereg:
  493.                cmp    al,031h
  494.                 jz    _@xor_ereg_ereg
  495. ;              
  496.  @inc_ereg:
  497.                 cmp    al,040h
  498.                 jc        unknown_instr
  499.                 cmp    al,047h
  500.                 jbe   _@inc_ereg
  501.  @push_ereg:
  502.                 cmp    al,050h
  503.                 jc        unknown_instr
  504.                 cmp    al,057h
  505.                 jbe   _@push_ereg
  506.  @pop_ereg:     cmp    al,058h
  507.                 jc     unknown_instr
  508.                 cmp    al,05Fh
  509.                 jbe   _@pop_ereg
  510.  @pushad:      
  511.                 cmp    al,060h
  512.                 jz    _@one_byte
  513.  @add_reg_byte:
  514.                 cmp    al,080h
  515.                 jz    _@add_reg_byte
  516.  @xas_ereg_dd:  
  517.                 cmp    al,081h                 ; может быть XOR/ADD/SUB [EREG],DD
  518.                 jz    _@xas_ereg             ; проверка в "_@xas_ereg"
  519.  @xas_ereg_b:  
  520.                 cmp    al,083h
  521.                 jz    _@xas_ereg
  522.  @lea_er_er_dd:
  523.                 cmp    al,08Dh
  524.                 jz    _@lea_er_er_dd
  525.  @pushfd:
  526.                cmp    al,09Ch
  527.                 jz    _@one_byte
  528.  @popfd:
  529.                 cmp    al,09Dh
  530.                 jz    _@one_byte
  531.  @scasd:  cmp    al,0AFh
  532.                 jz    _@one_byte
  533.  @mov_reg_byte:
  534.                 cmp    al,0B0h
  535.                 jc     unknown_instr
  536.                 cmp    al,0B7h
  537.                 jbe   _@mov_reg_byte
  538.  @mov_ereg_dd:  
  539.                 cmp    al,0B8h
  540.                 jc     unknown_instr
  541.                 cmp    al,0BFh
  542.                 jbe   _@mov_ereg_dd
  543.  @ret_byte:
  544.                 cmp    al,0C3h
  545.                 jz    _@one_byte
  546.  @loop_byte:
  547.                 cmp    al,0E2h
  548.                 jz    _@loop_byte
  549.  @call_dword:
  550.                cmp    al,0E8h                      ;  
  551.                 jz    _@cj_dword                 ; инструкции call и jmp
  552.  @jmp_dword:                                    ;
  553.                 cmp    al,0E9h                      ; требуют одинакового разбора
  554.                 jz    _@cj_dword                 ;  
  555.  @cld:      cmp    al,0FCh
  556.                 jz    _@one_byte
  557.  unknown_instr:
  558.                 sub    eax,eax                       ; инструкции нет в базе
  559.                 dec    eax                              ; eax = -1
  560.  disasm_ret:    
  561.                 pop    esi                              ; вспомним оригинальное значение и …
  562.                 ret                                         ;  выйдем в ...
  563. ;                                                
  564. ; inc [ereg]             10000reg
  565. ;
  566. _@inc_ereg:    
  567.                 xor    al,1000000b               ; eax - регистр "ereg"
  568.                 inc    ecx                              ; ecx - размер инструкции
  569.                 jmp    disasm_ret
  570. ;              
  571. ; push [ereg]            1010reg
  572. ;
  573. _@push_ereg:    
  574.                 xor    al,1010000b               ; eax - регистр "ereg"
  575.                 inc    ecx                              ; ecx - размер инструкции
  576.                 jmp    disasm_ret
  577. ;              
  578. ; pop [ereg]             1011reg
  579. ;
  580. _@pop_ereg:    
  581.                 xor    al,1011000b               ; eax - регистр "ereg"
  582.                 inc     ecx                             ; ecx - размер инструкции
  583.                 jmp    disasm_ret
  584. ;              
  585. ; sub [eregA],[eregB]    02BHEX, 11rgArgB
  586. ;
  587. _@sub_ereg_ereg:
  588.                 lodsb
  589.                 xor    al,11000000b
  590.                 call   two_regs                  ; edx - "rgA" / eax - "rgB"
  591.                 inc    ecx
  592.                 inc    ecx                           ; ecx - размер инструкции
  593.                 jmp    disasm_ret
  594. ;              
  595. ; xor [eregA+eregB],reg   030HEX, 00reg100, 00rgArgB
  596. ;
  597. _@xor_2ereg_b:
  598.                 lodsb
  599.                 lodsb
  600.                 call     two_regs                 ; edx - "rgA" / eax - "rgB"
  601.                 mov    cl,3                         ; ecx - размер инструкции
  602.                 inc      dword ptr [dregs]
  603.                 mov    dword ptr [dregs+008],eax
  604.                 jmp  _@xor_2ereg_dr
  605. ;              
  606. ; xor [eregA],[eregB]    031HEX, 00rgArgB
  607. ;
  608. _@xor_ereg_ereg:
  609.                 lodsb
  610.                 call   two_regs                  ; edx - "rgA" / eax - "rgB"
  611.                 inc    ecx
  612.                 inc    ecx                           ; ecx - размер инструкции
  613. _@xor_2ereg_dr:
  614.                 inc     dword ptr [dregs]
  615.                 mov   dword ptr [dregs+004],edx
  616.                 jmp    disasm_ret
  617. ;              
  618. ; add al,byte             004HEX, byte
  619. ;
  620. _@add_al_byte:  
  621.                 mov   cl,2                          ; ecx - размер инструкции
  622.                 sub     eax,eax                    ; eax - значение рег. (al)
  623.                 jmp    disasm_ret
  624. ;              
  625. ; add eax,dword          005HEX, dword = 5 bytes
  626. ;
  627. _@add_eax_dd:  
  628.                 mov    cl,5                          ; ecx - размер инструкции
  629.                 sub      eax,eax                   ; eax - значение рег. (eax)
  630.                 jmp     disasm_ret
  631. ;              
  632. ; add [reg],byte         080HEX, 11000reg, byte
  633. ;
  634. _@add_reg_byte:
  635.                 lodsb
  636.                 cmp    al,0c0h
  637.                 jc       unknown_instr
  638.                 cmp    al,0c7h
  639.                 ja        unknown_instr
  640.                 xor      al,11000000b              ; eax - значение рег. (eax)
  641.                 mov    cl,3                              ; ecx - размер инструкции
  642.                 jmp     disasm_ret
  643. ;              
  644. ; xor [ereg],dword       081HEX, 00110reg, dword = 6 bytes
  645. ; add [ereg],dword       081HEX, 11000reg, dword = 6 bytes
  646. ; sub [ereg],dword       081HEX, 11101reg, dword = 6 bytes
  647. ; add [ereg],byte          083HEX, 11000reg, byte = 3 bytes
  648. ; sub [ereg],byte          081HEX, 11101reg, byte = 3 bytes
  649. ;
  650. _@xas_ereg:    
  651.                 mov    cl,3
  652.                 cmp    al,83h
  653.                 jz    _@xas_ereg_nfo
  654.                 add     cl,3
  655. _@xas_ereg_nfo:
  656.                 lodsb                                 ; al - второй байт инструкции
  657.                 cmp    al,037h                   ; если байт больше 37HEX
  658.                 ja    _@xas_ereg_chk        ; то перед нами "ADD" или "SUB"
  659. _@xor_ereg:                                      ; разбор инструкции "xor"
  660.                 xor     al,00110000b           ; используемый в инструкции
  661.                 inc     dword ptr [dregs]
  662.                 mov    dword ptr [dregs+004],eax
  663.                 jmp    disasm_ret                ; регистр кладем в EDX
  664. _@xas_ereg_chk:
  665.                 cmp    al,0C7h                   ; если байт больше 37HEX
  666.                 ja    _@sub_ereg                 ; то перед нами "SUB"
  667. _@add_ereg:    
  668.                 xor     al,11000000b
  669.                 jmp    disasm_ret                ; разбор инструкции "add"
  670. _@sub_ereg:    
  671.                 xor     al,11101000b
  672.                 jmp    disasm_ret                ; разбор инструкции "sub"
  673. ;              
  674. ; lea ergA,ergB+dword    08DHEX,10rgArgB, dword
  675. ;
  676. _@lea_er_er_dd:
  677.                 lodsb
  678.                 xor     al,10000000b             ; оставим в al только регистры
  679.                 call     two_regs                    ; edx - "rgA" / eax - "rgB"
  680.                 mov    cl,6                            ; ecx - размер инструкции
  681.                 jmp     disasm_ret
  682. ;              
  683. ; mov [reg],byte         10110reg, byte
  684. ;
  685. _@mov_reg_byte:
  686.                 xor    al,10110000b              ; eax - регистр "reg"
  687.                 inc    ecx
  688.                 inc    ecx                              ; ecx - размер инструкции
  689.                 jmp    disasm_ret
  690. ;              
  691. ; mov [ereg],dword       10111reg, dword
  692. ;
  693. _@mov_ereg_dd:  
  694.                 xor    al,10111000b              ; eax - регистр "ereg"
  695.                 mov    cl,5                            ; ecx - размер инструкции
  696.                 jmp    disasm_ret
  697. ;              
  698. ; loop byte              0E2HEX, FE-byte = num   ; jump to IP-num
  699. ;
  700. _@loop_byte:    
  701.                 sub     eax,eax
  702.                 mov   al,0feh                       ; al = 0FE
  703.                 sub     al,byte ptr [esi]         ; al = 0FE-byte
  704.                 inc     ecx
  705.                 inc     ecx                             ; размер инструкции
  706.                 jmp    disasm_ret
  707. ;              
  708. ; call dword             0E8HEX, dword
  709. ; jmp  dword           0E9HEX, dword
  710. ;
  711. _@cj_dword:    
  712.                 lodsd                                 ; eax - dword
  713.                 mov    cl,5                         ; ecx - размер инструкции
  714.                 jmp    disasm_ret
  715. ;              
  716. ; разбор простейших однобайтовых инструкций
  717. ;
  718. _@one_byte:    
  719.                 sub    eax,eax
  720.                 inc     ecx
  721.                 jmp    disasm_ret
  722.  endp
  723. ;                                                                              
  724. ; two_regs - если регистр al содержит 00rgArgB (где rgA, rgB два регистра)
  725. ;                   то на выходе edx - rgA, eax - rgB
  726. ;
  727.  two_regs           proc
  728.                 push   eax
  729.                 shr     al,3                      ; al = regB
  730.                 pop    edx
  731.                 push   eax
  732.                 rol     al,3
  733.                 xor    dl,al                     ; dl = regA
  734.                 pop    eax
  735.                 ret
  736.  endp
  737. ;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Код (Text):
  1.  
  2. .data  ; сегмент данных
  3.  regs          dd 8 dup (?)     ; переменная под значения регистров эмулируемого блока
  4.  flags         dd ?             ; переменная под значения флагов эмулируемого блока
  5.  temp          dd ?             ; переменная под значение флагов программы-эмулятора
  6. ;
  7. ; "instr_buf" - переменная под исполняемую инструкцию и имитацию стека
  8. ;  должна содержать:
  9. ;               <strong>mov    dword ptr [set_esp+1],esp</strong>
  10. ;               <strong>mov    esp,dword ptr [regs+16]</strong>
  11. ;               <em>исполняемая инструкция</em>
  12. ;               mov     dword ptr [regs+16],esp
  13. ;  <strong>set_esp:mov    esp,?</strong>
  14. ;                <strong>ret</strong>
  15. ;
  16.  instr_buf: db 30 dup (?)
  17. <strong>.code</strong> ; сегмент кода
  18. <strong>; "прямой запуск" инструкции в (карантине) специальных условиях</strong>
  19. ; запомним значение регистров программы-эмулятора в стеке
  20.  run_instr: push    eax ecx edx ebx ebp esi edi
  21. ; запомним значение флагов программы-эмулятора в стеке, затем перенесем в регистр eax,
  22. ; а из него в переменную "temp"
  23.                 pushfd                          
  24.                 mov     eax,dword ptr [esp]
  25.                 mov     dword ptr [temp],eax
  26.                 popfd
  27. ; значение флагов эмулируемого кода перенесем из переменной "flags" в регистр eax,
  28. ; а из него в стек использующийся программой-эмулятором
  29.                 pushfd
  30.                 mov     eax,dword ptr [flags]
  31.                 mov     dword ptr [esp],eax
  32. ; установим значения регистров эмулируемого кода из переменной "regs"
  33. ; значение esp не устанавливается, иначе мы рискуем испортить значение флагов эмулируемого кода
  34. ; стек будет перенаправлен в подпрограмме "instr_buf"
  35.                 mov     eax,dword ptr [regs+000]
  36.                 mov     ecx,dword ptr [regs+004]
  37.                 mov     edx,dword ptr [regs+008]
  38.                 mov     ebx,dword ptr [regs+012]
  39.                 mov     ebp,dword ptr [regs+020]
  40.                 mov     esi,dword ptr [regs+024]
  41.                 mov     edi,dword ptr [regs+028]
  42. ; инструкция "popfd" установит значение флагов эмулируемого кода, которое мы сохраняли выше
  43.                 popfd
  44.                 call       instr_buf                                ; запустим на исполнение инструкцию
  45. ; сохраним новые значения регистров эмулируемого кода в переменной "regs"
  46.                 mov     dword ptr [regs+000],eax
  47.                 mov     dword ptr [regs+004],ecx
  48.                 mov     dword ptr [regs+008],edx
  49.                 mov     dword ptr [regs+012],ebx
  50.                 mov     dword ptr [regs+020],ebp
  51.                 mov     dword ptr [regs+024],esi
  52.                 mov     dword ptr [regs+028],edi
  53. ; новое значение флагов эмулируемого кода запомним в стеке, затем перенесем в регистр eax
  54. ; и сохраним в переменной "flags"
  55.                 pushfd
  56.                 mov     eax,dword ptr [esp]
  57.                 mov     dword ptr [flags],eax
  58.                 popfd
  59. ; установим значение флагов программы-эмулятора из переменной "temp"
  60.  
  61.                 pushfd
  62.                 mov     eax,dword ptr [temp]
  63.                 mov     dword ptr [esp],eax
  64.                 popfd                            
  65. ; восстановим значение регистров программы-эмулятора
  66.                 pop     edi esi ebp ebx edx ecx eax

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

Код (Text):
  1.  
  2. .data ; сегмент данных
  3. buf_stack   db 50000 dup (?)
  4. .code ; сегмент кода
  5.                 mov     edx,offset buf_stack+50000
  6.                 mov     dword ptr [regs+16],edx

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

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

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

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

Код (Text):
  1.  
  2.     dec      dword ptr [regs+4]
  3.     cmp      dword ptr [regs+4],0
  4.     jz       <strong>проверка на остановку эмуляции</strong> (<em>emulate_check</em> в программе примере)
  5.              изменяем местоположение регистра esi, используя данные инструкции LOOP
  6.     jmp      <strong>проверка на остановку эмуляции</strong>

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. Процедура, которая  предназначена для этих целей, выглядит примерно так:

Код (Text):
  1.  
  2.             call      get_ip
  3. get_ip:     pop       ebp
  4.             sub       ebp, offset get_ip

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

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

Код (Text):
  1.  
  2. ; не забыли, что а) edx содержит номер регистра erega, использующегося в инструкции
  3. ;                           б) под значение каждого регистра используется 4 байта, …
  4. ;                          
  5. ; из всего вышеуказанного следует,
  6. ; что расположение значения регистра в буфере regs = 4 * edx
  7.                 mov     eax,4
  8.                 mul      edx
  9. ; допустим (только допустим!), что макс. размер расшифровщика может составлять 200 байт
  10. ; значит (!) если значение регистра "regs+eax" (erega) находится в промежутке от ip до ip+200,
  11. ; то менять значение не обязательно, вполне возможно что:
  12. ;  - декриптор содержит в себе процедуру определения ip
  13. ;  - значение регистра уже менялось раньше, т.е. инструкция выполняется не первый раз
  14. ; если одно из вышеуказанных утверждений правда (true), то просто запускаем инструкцию на
  15. ; выполнение, как было описано в 4.2.1
  16.                 mov     edx,ip                  
  17.                 cmp     dword ptr [regs+eax],edx
  18.                 jc         run_instr
  19.                 add      edx,200
  20.                 cmp     dword ptr [regs+eax],edx
  21.                 ja         run_instr
  22. ;
  23. ;  если значение erega выпадает из промежутка,
  24. ;  то: a) необходимо рассчитать разницу между старым ip и значением erega (индексного регистра)
  25. ;        б) добавить эту разницу к смещению начала буфера (codeloc), в который
  26. ;            мы копировали эмулируемый код
  27. ;        в) полученное смещение поместить в значение регистра erega
  28.                 mov     edx,dword ptr [regs+eax]
  29.                 sub       edx,ip       ; edx = может быть размером "(де)криптора"
  30.                 add      edx,codeloc              
  31.                 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. Значения этих регистров.

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

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

Код (Text):
  1.  
  2. <strong>.data</strong>
  3. ; первые 4 байта, количество регистров
  4. ; все последующие, регистры
  5. dregs       dd 010 dup (?)
  6. <strong>.code</strong>
  7. ; <strong>допустим</strong>, что мы разбираем нужную нам инструкцию.
  8. ; esi - содержит смещение разбираемой инструкции
  9. ;
  10. _work:     lodsw         ; загрузим в ax - первые два байта инструкции
  11.                 lodsb          ; загрузим в al последний байт инструкции,
  12.                                ; необходимый для разбора
  13.                 push   eax
  14.                 shr    al,3    ; al = <strong><u>regb</u></strong>
  15.                 pop    edx
  16.                 push   eax
  17.                 rol    al,3
  18.                 xor    dl,al   ; dl = <strong><u>regb</u></strong>
  19.                 mov    edi,offset dregs
  20.                 sub    eax,eax
  21.                 mov    al,2
  22.                 stosd           ; установим число регистров, использующихся
  23.                                 ; в инструкции (в нашем случае 2)
  24.                 pop    eax
  25.                 stosd           ; номер регистра <strong><u>regb</u></strong> в dregs+004
  26.                 xchg   eax,edx
  27.                 stosd           ; номер регистра <strong><u>rega</u></strong> в dregs+008

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

4.3.2. Эмулятор

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

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

Код (Text):
  1.  
  2. ; <strong>первая часть</strong> процесса имитации выполнения инструкции типа xor byte ptr [<strong><u>erega</u></strong>+<strong><u>eregb</u></strong>],<strong><u>reg</u></strong>
  3. ;
  4. ; сейчас буфер "<strong>dregs</strong>" содержит примерно следующие данные:
  5. ;  dregs + 000 : <strong>002</strong> (количество регистров использующихся в инструкции)
  6. ;  dregs + 004 : <strong>eregb</strong> (номер регистра)
  7. ;  dregs + 008 : <strong>erega</strong> (номер регистра)
  8. ;
  9. ; Нам необходимо определить сумму значений регистров (в данном случае двух) использующихся
  10. ; в инструкции и передать ее во <strong>вторую часть</strong> процесса. В нашем случае это будет выглядеть
  11. ; примерно так:
  12. ;   a = [dregs+004] * 4
  13. ;   x = [regs+a]
  14. ;   a = [dregs+008] * 4
  15. ;   x = x + [regs+a]
  16. ; где x является той самой суммой, которую необходимо узнать
  17. ;
  18. fxor:   push    ebx esi
  19.         sub      edx,edx
  20.         mov      dl,4             ; edx = 4, для использования в цикле
  21.         sub      ebx,ebx          ; ebx = 0, будет содержать сумму значений всех регистров
  22.         lea      esi,offset dregs ; esi = данные и регистрах, полученные из <strong>дизассемблера</strong>
  23.         lodsd                     ; eax = число регистров, использующихся в инструкции
  24.         xchg     eax,ecx          ; ecx = eax
  25. fxor_lp1: push    edx             ; запомним значение edx (=4), что бы востановить после порчи в
  26.         lodsd                     ; eax = номер регистра использующегося в инструкции
  27.         mul      edx              ; eax = eax * 4 = расположение значения регистра в "regs"
  28.         add      ebx,dword ptr [regs+eax] ; добавим в ebx, знач. регистра использующегося в инструкции
  29.         pop      edx              ; edx = 4
  30.         loop     fxor_lp1         ; продолжим, составлять сумму значений регистров, если необходимо
  31.         xchg     ebx,ecx          ; ecx - сумма значений регистров (x)
  32.         pop      esi ebx
  33. ; <strong>вторая часть</strong> процесса имитации выполнения …
  34. ;
  35. ; <strong>допустим</strong> (только допустим!), что макс. размер расшифровщика может составлять 200 байт
  36. ; <strong>значит</strong> (!) если <em>сумма значений регистров</em> (<strong>x</strong>)  находится в промежутке от <strong>ip</strong> до <strong>ip</strong>+200,
  37. ; <strong>то</strong> менять значение не обязательно, вполне возможно что:
  38. ;  - декриптор содержит в себе <strong>процедуру определения ip</strong>
  39. ;  - значение регистра уже менялось раньше, т.е. инструкция выполняется не первый раз
  40. ; <strong>если</strong> одно из вышеуказанных утверждений <strong>правда</strong> (<strong>true</strong>), то просто запускаем инструкцию на
  41. ; выполнение, как было описано в <strong>4.2.1</strong>
  42. ;
  43. fxor_ip:        cmp     <strong>ecx</strong>,<strong>ip</strong>
  44.                 jc      run_instr
  45.                 cmp     ecx,<strong>ip</strong>+<strong>200</strong>
  46.                 ja      run_instr
  47. ;  <strong>третья часть</strong> процесса имитации выполнения …
  48. ;
  49. ;  <strong>если</strong> значение <strong>x</strong> выпадает из промежутка,
  50. ;  <strong>то:</strong> a) необходимо рассчитать разницу между старым <strong>ip</strong> и <strong>x</strong> (<em>суммой значений регистров</em>)
  51. ;        б) добавить эту разницу к смещению начала буфера (<strong>codeloc</strong>), в который
  52. ;            мы копировали эмулируемый код
  53. ;        в) полученное смещение поместить в значение регистра "<strong>dreg+004</strong>"
  54. ;        г) значение остальных регистров использующихся в инструкции обнулить
  55.                 sub      ecx,<strong>ip</strong>
  56.                 add      ecx,<strong>codeloc</strong>
  57. ; новое смещение зашифрованного кода, полученное действиями, описанными в пунктах а и б
  58. ; положив в регистр номер "<strong>dreg+004</strong>"
  59.                 mov     eax,dword ptr [dregs+004]
  60.                 sub      edx,edx
  61.                 mov     dl,4
  62.                 push    edx
  63.                 mul     edx
  64.                 pop     edx
  65.                 mov     dword ptr [regs+eax],ecx
  66. ;
  67. ; цикл, для обнуления значений всех регистров начиная с "<strong>dreg+008</strong>"
  68. ;
  69.                 push    esi
  70.                 mov     esi,offset dregs
  71.                 lodsd             ; eax - число регистров использующихся в инструкции
  72.                 cmp     al,02
  73.                 jc     fxor_eret  ; может быть инструкция использует только один регистр
  74.                 xchg    eax,ecx   ; ecx = eax - число регистров использующихся в инструкции
  75.                 dec     ecx       ; т.к. мы уже работали с "<strong>dreg+004</strong>", то уменьшим число регистров
  76.                 lodsd             ; eax - номер регистра "<strong>dreg+004</strong>", пропустим его
  77. fxor_slp: push    edx
  78.                 lodsd             ; eax - номер регистра
  79.                 mul     edx       ; eax = eax * 4 = расположение значения регистра в "regs"
  80.                 mov     dword ptr [regs+eax],0 ; обнулим значение регистра
  81.                 pop     edx
  82.                 loop   fxor_slp
  83. fxor_eret: pop     esi
  84.                 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.

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

Код (Text):
  1.  
  2.                 …
  3.  emulate_loop:
  4.                 push    8                           ; длина вирусной сигнатуры
  5.                 push    offset vir_sig         ; расположение вирусной сигнатуры
  6.                 push    esi                          ; инструкция, которая будет эмулироваться
  7.                 call      detect                    ; сравним сигнатуру, с участком кода с позиции esi
  8.                 or        eax,eax
  9.                 jz         emulate_dasm       ; если не совпала, эмулируем дальше
  10.                 inc       byte ptr [ill_mark] ; иначе устанавливаем флаг зараженности файла …
  11.                 jmp      emulate_ret           ; … и прекратим эмуляцию, файл заражен!
  12. ;              
  13.  emulate_dasm:  
  14.                 push    esi
  15.                 call      disasm                   ; разберем инструкцию с помощью дизассемблера
  16.                 …                

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

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

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

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

Код (Text):
  1.  
  2. ; проверка участка на совпадение с сигнатурой "sloc", длины "slen"
  3. ;
  4. ; on start : slen - длина сигнатуры
  5. ;                sloc - расположение сигнатуры
  6. ;                cloc - расположение кода для сверки
  7. ; on exit  : eax = 0 - не совпадают, иначе сигнатура совпала
  8. ;
  9.  detect      proc   cloc:dword, sloc: dword, slen: dword
  10. ;                                                                              
  11.                 push    ecx esi edi
  12.                 mov     ecx,slen
  13.                 mov     esi,sloc
  14.                 mov     edi,cloc
  15.  det_lp:    lodsb
  16.                 cmp     byte ptr [edi],al
  17.                 jz         det_cnt
  18.                 sub      eax,eax
  19.                 jmp     det_ret
  20.  det_cnt:  inc      edi
  21.                 loop    det_lp
  22.                 mov     al,1
  23.  det_ret:   pop     edi esi ecx
  24.                 ret
  25.  endp
  26. ;

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

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

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

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

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

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

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

Скачать

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

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

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


0 11.345
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532