Антивирусные технологии: эмуляция программного кода — Архив 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):
; 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. Имитация исполнения инструкций
Как Вы уже поняли, для имитации исполнения инструкций эмулятор использует три способа:
- Инструкция запускается «напрямую», но в специальной среде.
- Исполнение инструкции имитируется полностью, без запуска.
- Исполнение инструкции имитируется до или после запуска в специальной среде.
Я постараюсь рассказать обо всех трех способах, описав их и представив в качестве примера немного измененные участки выше представленной программы.
4.2.1. Запуск инструкции в специальной среде
Многие инструкции гораздо проще запустить напрямую, специальная имитация может обойтись «дороже». Т.е. будет потеря в скорости работы эмулятора (а это очень щекотливый вопрос), если каждую инструкцию имитировать, то можно писать только эмуляцию инструкций до пенсии. А зачем заниматься ерундой, если можно просто запустить инструкцию и зафиксировать изменения, которые произошли после ее исполнения (в регистрах, стеке, флагах … ).
Для этого необходимо хранить параметры эмулируемого кода в (специальных) переменных, как поступил я (в вышеуказанной программе). Перед запуском инструкции запомнить значения параметров программы-эмулятора и установить параметры эмулируемого кода:
- Значения регистров и флагов из переменных.
- Изменить указатель на буфер стека, что бы стек эмулируемого блока не использовался в ходе работы эмулятора, а только во время исполнения или имитации инструкций блока. В противном случае стек будет портиться.
После запуска инструкции запомнить новые значения и установить значения использовавшиеся эмулятором.
В качестве примера, более подробно рассмотрим участок кода из программы примера:
Код (Text):
.data ; сегмент данных regs dd 8 dup (?) ; переменная под значения регистров эмулируемого блока flags dd ? ; переменная под значения флагов эмулируемого блока temp dd ? ; переменная под значение флагов программы-эмулятора ; ; "instr_buf" - переменная под исполняемую инструкцию и имитацию стека ; должна содержать: ; <strong>mov dword ptr [set_esp+1],esp</strong> ; <strong>mov esp,dword ptr [regs+16]</strong> ; <em>исполняемая инструкция</em> ; mov dword ptr [regs+16],esp ; <strong>set_esp:mov esp,?</strong> ; <strong>ret</strong> ; instr_buf: db 30 dup (?) <strong>.code</strong> ; сегмент кода <strong>; "прямой запуск" инструкции в (карантине) специальных условиях</strong> ; запомним значение регистров программы-эмулятора в стеке 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 эмулируемого блока на конец специально отведенного для этих целей буфера. Например:
Код (Text):
.data ; сегмент данных buf_stack db 50000 dup (?) .code ; сегмент кода mov edx,offset buf_stack+50000 mov dword ptr [regs+16],edx4.2.2. Полная имитация исполнения инструкции
Этот алгоритм применяется там, где нельзя обойтись запуском в специальной среде. В программе-эмуляторе из примера, я использовал регистр “esi”, что бы указывать на смещение исполняемой в «данный момент» инструкции. Обычно полностью имитируется (совершаются различные действия, которые и будут являться аналогом работы инструкции) выполнение инструкций, которые изменяют этот указатель, т.е. CALL, JUMP, LOOP, RET и т.д.
Не будем ходить далеко и в качестве примера возьмем простейшую инструкцию LOOP:
Инструкция LOOP уменьшает значение регистра ECX на единицу, если после этого значение ECX не равняется нулю, то переводит указатель на указанную позицию.
Код (Text):
dec dword ptr [regs+4] cmp dword ptr [regs+4],0 jz <strong>проверка на остановку эмуляции</strong> (<em>emulate_check</em> в программе примере) изменяем местоположение регистра esi, используя данные инструкции LOOP jmp <strong>проверка на остановку эмуляции</strong>4.2.3. Комбинация двух способов
Существуют такие, инструкции, которые просто выполнить в «карантине» нельзя, да и полностью имитировать их совсем не обязательно (точнее сказать, выйдет «дороже»). Для работы с такими инструкциями используется комбинация из двух вышеописанных способов.
В программе-примере присутствует один тип инструкции, которая имитируется этим способом:
xor dword ptr [erega],? и ее аналог xor dword ptr [erega],eregb
Почему же эти инструкции требуют комбинированного способа имитации? Все очень просто и логично… При расшифровке данных, (в «декрипторах») используются регистры, указывающие на смещение расшифровываемых данных. Этот регистр может меняться в процессе расшифровки, таких регистров может быть несколько, все зависит от полиморфного алгоритма, который используется. Когда для эмуляции мы копируем в «свой» буфер код расшифровщика и зашифрованные данные, то соответственно смещение зашифрованного кода меняется, но в расшифровщике будет указанно предыдущее его расположение. Да, эту инструкцию можно запустить напрямую. Но, прежде всего, нужно исправить значение этого (в данном случае erega) регистра, на новое, которое будет указывать «правильное» местоположение зашифрованных данных в буфере.
Для процесса расчета нового местоположения зашифрованного кода в буфере, я использовал следующий подход. Прежде всего, необходимо знать, что в нашем случае:
- Дизассемблер разбирает инструкцию, и после его работы передает информацию о размере инструкции и регистрах, которые используются ней в программу-эмулятор. В нашем случае регистр edx будет содержать номер регистра erega, а eax номер eregb.
- Значения регистров эмулируемого кода содержится в буфере regs, под каждый регистр дается по 4 байта буфера. Последовательность регистров: eax, ecx, edx, ebx, esp, ebp, esi и edi.
- При вызове эмулятора, ему передается предыдущее расположение эмулируемого кода в памяти (переменная ip)!
Замена может быть лишней, если декриптор содержит в себе процедуру определения ip. Процедура, которая предназначена для этих целей, выглядит примерно так:
Код (Text):
call get_ip get_ip: pop ebp sub ebp, offset get_ipЕсли прибавить значение регистра ebp, к любому смещению получается «необходимое» нам, новое (смещение).
Теперь рассмотрим участок кода, который производит замену значения регистра использующегося в инструкции для указания смещения (если, конечно же, замена необходима):
Код (Text):
; не забыли, что а) 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
xxx – rega, yyy - regb
Нам необходимо получить следующие данные:
- Количество регистров, использующихся в дешифрующей инструкции для указания положения расшифровываемых данных.
- Значения этих регистров.
Для этого объявим переменную, которая будет содержать эти данные после работы дизассемблера. Данными этой переменной и будет пользоваться эмулятор, имитируя исполнения этой инструкции.
Процесс разбора инструкции может выглядеть примерно так:
Код (Text):
<strong>.data</strong> ; первые 4 байта, количество регистров ; все последующие, регистры dregs dd 010 dup (?) <strong>.code</strong> ; <strong>допустим</strong>, что мы разбираем нужную нам инструкцию. ; esi - содержит смещение разбираемой инструкции ; _work: lodsw ; загрузим в ax - первые два байта инструкции lodsb ; загрузим в al последний байт инструкции, ; необходимый для разбора push eax shr al,3 ; al = <strong><u>regb</u></strong> pop edx push eax rol al,3 xor dl,al ; dl = <strong><u>regb</u></strong> mov edi,offset dregs sub eax,eax mov al,2 stosd ; установим число регистров, использующихся ; в инструкции (в нашем случае 2) pop eax stosd ; номер регистра <strong><u>regb</u></strong> в dregs+004 xchg eax,edx stosd ; номер регистра <strong><u>rega</u></strong> в dregs+008Теперь Мы имеем всю информацию, необходимую эмулятору для имитации исполнения инструкции.
4.3.2. Эмулятор
Основы имитации выполнения инструкций подобного типа были описаны в 4.2.3, по этому повторяться я не буду.
В данном случае процесс имитации будет состоять из трех частей и выглядеть примерно так:
Код (Text):
; <strong>первая часть</strong> процесса имитации выполнения инструкции типа xor byte ptr [<strong><u>erega</u></strong>+<strong><u>eregb</u></strong>],<strong><u>reg</u></strong> ; ; сейчас буфер "<strong>dregs</strong>" содержит примерно следующие данные: ; dregs + 000 : <strong>002</strong> (количество регистров использующихся в инструкции) ; dregs + 004 : <strong>eregb</strong> (номер регистра) ; dregs + 008 : <strong>erega</strong> (номер регистра) ; ; Нам необходимо определить сумму значений регистров (в данном случае двух) использующихся ; в инструкции и передать ее во <strong>вторую часть</strong> процесса. В нашем случае это будет выглядеть ; примерно так: ; 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 = данные и регистрах, полученные из <strong>дизассемблера</strong> 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 ; <strong>вторая часть</strong> процесса имитации выполнения … ; ; <strong>допустим</strong> (только допустим!), что макс. размер расшифровщика может составлять 200 байт ; <strong>значит</strong> (!) если <em>сумма значений регистров</em> (<strong>x</strong>) находится в промежутке от <strong>ip</strong> до <strong>ip</strong>+200, ; <strong>то</strong> менять значение не обязательно, вполне возможно что: ; - декриптор содержит в себе <strong>процедуру определения ip</strong> ; - значение регистра уже менялось раньше, т.е. инструкция выполняется не первый раз ; <strong>если</strong> одно из вышеуказанных утверждений <strong>правда</strong> (<strong>true</strong>), то просто запускаем инструкцию на ; выполнение, как было описано в <strong>4.2.1</strong> ; fxor_ip: cmp <strong>ecx</strong>,<strong>ip</strong> jc run_instr cmp ecx,<strong>ip</strong>+<strong>200</strong> ja run_instr ; <strong>третья часть</strong> процесса имитации выполнения … ; ; <strong>если</strong> значение <strong>x</strong> выпадает из промежутка, ; <strong>то:</strong> a) необходимо рассчитать разницу между старым <strong>ip</strong> и <strong>x</strong> (<em>суммой значений регистров</em>) ; б) добавить эту разницу к смещению начала буфера (<strong>codeloc</strong>), в который ; мы копировали эмулируемый код ; в) полученное смещение поместить в значение регистра "<strong>dreg+004</strong>" ; г) значение остальных регистров использующихся в инструкции обнулить sub ecx,<strong>ip</strong> add ecx,<strong>codeloc</strong> ; новое смещение зашифрованного кода, полученное действиями, описанными в пунктах а и б ; положив в регистр номер "<strong>dreg+004</strong>" mov eax,dword ptr [dregs+004] sub edx,edx mov dl,4 push edx mul edx pop edx mov dword ptr [regs+eax],ecx ; ; цикл, для обнуления значений всех регистров начиная с "<strong>dreg+008</strong>" ; push esi mov esi,offset dregs lodsd ; eax - число регистров использующихся в инструкции cmp al,02 jc fxor_eret ; может быть инструкция использует только один регистр xchg eax,ecx ; ecx = eax - число регистров использующихся в инструкции dec ecx ; т.к. мы уже работали с "<strong>dreg+004</strong>", то уменьшим число регистров lodsd ; eax - номер регистра "<strong>dreg+004</strong>", пропустим его 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 и 3, именно для этого я вставил в (рас)шифровщик инструкцию nop, которую эмулятор не понимает и соответственно на ней прекращает свою работу.
Именно эти способы удобны для написания маленьких эмуляторов, под конкретную цель, но если разрабатывать серьезный (универсальный) проект, то следует использовать комбинацию из всех (или нескольких) вышеописанных способов или придумывать что-то свое, новое.
5. Пример использования технологии для детектирования вирусов
Теперь рассмотрим, как можно использовать этот (представленный в примере) эмулятор, для детектирования вирусов.
Программа пример, которая представлена выше, не приспособлена для детектирования вирусов, так как в ней отсутствует анализатор кода, который используется для детектирования известных (антивирусной программе) вирусов или вирусов нового типа.
В качестве «жертвы», я взял один из простейших шифрованных вирусов – Win32.Ditto.1488. Исходные тексты этого вируса были, опубликованы в журнале Duke's Virus Labs номер 10. Они доступны в архиве, прилагающемся к этой статье, так же доступны исполняемые файлы, зараженные этим «вирусом».
Чем же отличается программа для детектирования вируса, от программы примера? Практически ничем, она лишь содержит процедуры для разбора файлов, формата PE и процедуру для определения, наличия вируса в файле (примитивнейшую реализацию анализатора программного кода).
Программа детектирования состоит из:
- Основной программы, которая определяет точку входа в указанном PE-файле. Читает данные, находящиеся в точке входа и запускает их на эмуляцию.
- Эмулятора (и дизассемблера) использовавшегося в программе-примере, с минимальными отличиями (для совместной работы с анализатором).
- Анализатора программного кода - процедуры детектирования вирусного кода, встроенной в эмулятор.
В статье я не буду приводить исходные тексты программы, они так же доступны в архиве, прилагающемся к статье. Так как я считаю, что делать это (приводить полный исходный текст программы, составные части которой почти полностью совпадают с программой-примером, представленным выше) бессмысленно, по этому приведу участки кода, которые используются для детектирования вирусного кода в ходе эмуляции.
5.1. Кодо-анализатор
Как я уже упоминал раньше, кодо-анализатор предназначен для учета особенностей кода исследуемого (эмулируемого) участка. Анализатор может быть отдельной частью антивируса, но вполне может, является частью эмулятора, работая в паре, они будут более «успешно действовать».
Наиболее низкой реализацией этой технологии является анализ сигнатур, т.е. участков кода и их сравнение с известными вирусными сигнатурами. Для детектирования известного программе вируса достаточно именно обычного сравнения участка кода с конкретной (вирусной) сигнатурой.
Вообще алгоритмы технологии кодо-анализатора заслуживают отдельной статьи, именно по этому я выполнил ее на самом низком уровне, однако как не странно этот процесс (сравнения сигнатур) входит в технологию антивирусного анализа кода.
Итак, преступим к описанию кодо-анализатора, который представлен в примере «антивируса» и позволяет определять наличие вируса в исполняемых файлах формата PE.
Работает цикл имитации исполнения инструкций (он же эмулятор):
Код (Text):
… 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 байт) расположенного в начале расшифрованного вирусного тела, то можно принять его за сигнатуру.
- Когда кодо-анализатор (во время работы эмулятора) встретит участок кода полностью схожий с сигнатурой, можно считать что программа содержит известный вирус.
Собственно текст процедуры «кодо-анализатора» представлен ниже:
Код (Text):
; проверка участка на совпадение с сигнатурой "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 раз повторюсь, что все представленное выше всего лишь моим представлением о технологии эмуляции программного кода. Вполне возможно я заблуждаюсь в некоторых моментах технологии или вообще во всем, что касается ее. Я ничего не утверждаю, просто предполагаю.
Если Вы заинтересовались разработкой антивирусов, очень советую почитать русские антивирусные журналы «Земский Фершал», в которых можно встретить множество интересной информации.
Вполне возможно, что будут появляться мои разработки на эту тему, но не в виде статей, а в виде исходных текстов.
Антивирусные технологии: эмуляция программного кода
Дата публикации 27 янв 2004