Преобразование кода — Архив WASM.RU

Все статьи

Преобразование кода — Архив WASM.RU

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

Для этого применяют следующие технологии: обфускация исходных кодов и обфускация бинарного кода. Обфускация исходных кодов часто применяется разработчиками программного обеспечения для затруднения анализа бинарного кода. Например, есть важная часть алгоритма, которая либо вычисляет что-то, либо генерирует ключ для активизации программы. Разработчик добавляет свой *.bat файл для перекомпиляции, который с помощью Perl’a обфусцирует функцию проверки ключа. После этого взломать данную версию программы по старому алгоритму будет значительно труднее. Для чего же используют разработчики вирусов эту технологию? Чаще всего, чтобы преобразовывать код в начале программы, потому что антивирусы очень часто определяют вирус именно по этому коду. Но что делают неопытные разработчики вирусов, а так же другого программного обеспечения, когда у них не хватает знаний и опыта? Правильно, обращаются к более опытным коллегам. Когда такой софт используется для легальных целей, эти конверты называются протекторами – Execryptor, Asprotect, Themida … Когда же этот софт используется для сокрытия от антивирусов, данные разработки называют крипторами или упаковщиками.

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

Вот исходные инструкции:

;------------------------------------------------------------------------------; 
	mov eax,12345678
	add ecx,333
;------------------------------------------------------------------------------;

После процесса пермутации они станут примерно такими:

;------------------------------------------------------------------------------; 
	mov eax,1
	add eax,12345677
	sub ecx,-333
;------------------------------------------------------------------------------;

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

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

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

Функция decrypto используется для преобразования кода между метками b(begin) и e(end), байт 0xc2 преобразуется в 0xc3 – это инструкция ret.

Если бы функция decrypto была использована в декрипторе вируса, то её легко можно было бы детектировать по сигнатуре. Например, так:

По выделенной последовательности байт можно было бы положить детект. Посмотрим на этот кусок кода в отладчике:

Всё достаточно примитивно и статично, сложностей в детекте не возникнет даже у малообразованного вирусного аналитика.

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

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

Permutation engine 0.2 beta

 (-) Нельзя статические смещения использовать
 (-) Не переделан ПГСЧ, потому сам себя движок не пермутирует 
 (-) Предназначен для мутации не больших кусков кода (около 200 инструкций x86)
 Преобразование инструкции mov r32,imm32

 1) push imm32 / pop r32
 2) (sub r32,r32) или (xor r32,32) или (mov r32,0) / (add r32,imm32) или
    (sub r32,-imm32)
 3) or r32,-1 / (add r32,imm32) или (sub r32,-imm32)
 4) mov r32,imm32-rnd / (add r32,rnd) или (sub r32,-rnd)
 6) mov r32,imm32 xor rnd / xor r32,rnd
 7) lea r32,[imm32]
 8) или оставить инструкцию без преобразования

 Преобразование инструкции mov r32,r32
 1) push r32 / pop r32
 2) lea r32,[r32]
 3) оставить оригинал

 Преобразование инструкции inc r32
 1) add r32,1
 2) lea r32,[r32+1]
 3) sub r32,-1
 4) оставить оригинал


 Преобразование инструкции dec r32
 1) add r32,-1
 2) lea r32,[r32-1]
 3) sub r32,1
 4) оставить оригинал

 Преобразование инструкции push r32
 1) sub esp,4 / mov [esp],r32
 2) оставить оригинал

 Преобразование инструкции pop r32
 1)  mov r32,[esp] / add esp,4
 2) оставить оригинал

 Преобразование инструкции push imm32 / imm8
 1) sub esp,4 / mov [esp],(imm32 / imm8)
 2) оставить оригинал

 Преобразование инструкции sub r32,imm32
 1) add r32,(neg imm32)
 2) lea r32,[r32-imm32]
 3) оставить оригинал

 Преобразование инструкции add r32,imm32
 1) sub r32,(neg imm32)
 2) lea r32,[r32+imm32]
 3) оставить оригинал

 Преобразование инструкции xor r32,r32 (обнуление)
 1) sub r32,r32
 2) mov r32,0
 3) оставить оригинал

 Преобразование инструкции sub r32,r32 (обнуление)
 1) xor r32,r32
 2) mov r32,0
 3) оставить оригинал

 Преобразование инструкции or r32,r32 (проверка на ноль)
 1) test r32,r32
 2) cmp r32,0
 3) оставить оригинал

 Преобразование инструкции test r32,r32 (проверка на ноль)
 1) or r32,r32
 2) cmp r32,0
 3) оставить оригинал

 Преобразование инструкции cmp r32,imm32
 1) push r32 / sub r32,imm32 / pop r32
 2) оставить оригинал

 Преобразование инструкции not r32
 1) xor r32,-1
 2) neg r32 / dec r32
 3) оставить оригинал

 Преобразование инструкции neg r32
 1) not r32 / inc r32
 2) оставить оригинал

Использование пермутиоующего движка:

 stdcall permutate,code_to_mutate,size_of_code_to_mutate,result_buffer

 где code_to_mutate указатель на код для мутации
 где size_of_code_to_mutate размер кода для мутации
 где result_buffer память в которую будет помещен мутировавшый код

 Результат:

 размер результирующего кода -> eax

Бинарный файл и библиотека будут прилагаться к статье.

Вернемся к нашему декриптору. Что произойдет с ним после разового пермутирующего преобразования?

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

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

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

А теперь давайте взглянем на функцию проверки ключа под отладчиком:

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

Из исходного кода видно, что было вызвано дважды пермутирующее преобразование. Если же включен DEP, то необходимо вызвать на выделенную память VirtualProtect. Взглянем в отладчике на результирующую функцию проверки ключа:

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

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

;------------------------------------------------------------------------------;
	sub	esp,4
	mov	[esp],ebp
;------------------------------------------------------------------------------;

После эмуляции первой инструкции анализатор замечает, что состояние esp изменилось на -4. После эмуляции второй инструкции анализатор видит, что в стек помещается регистр ebp. По внутреннему набору правил данные две инструкции сворачиваются в pseudo_push_ebp. И вот в таком духе эмулятор в связке с анализатором сворачивают инструкции. Если же попадается мусор, то его в список псевдоинструкций не добавляют. Таким образом, на выходе будет псевдоскелет расшифровщика вируса.

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

Итак, война меча и щита продолжается …

2002-2013 (c) wasm.ru