Полиморфный генератор — своим руками

         

Классификация полиморфных генераторов


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



Полиморфный генератор— своим руками


крис касперски ака мыщъх, no e-mail

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



Ступень 0: permutating или простейшие перестановки


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

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

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

base_table:

       block_1       DD     offset block_1

       block_2       DD     offset block_2

       ...

       block_N       DD     offset block_N

Листинг 1 таблица косвенных вызовов с базовыми адресами всех блоков

А вызов блока может выглядеть так:

       SHR    ESI,2                      ; умножаем номер блока на 4



       ADD    ESI, offset base_table + 4 ; переводим в смещение (4 понадобилось

                                         ; затем, что блоки нумеруются с 1)

       LODSD                             ; считываем адрес блока

       ADD    EAX,EBX                           ; добавляем смещение функции


       CALL   EAX                         ; вызываем блок

Листинг 2  косвенный межблочный вызов, номер блока передается в регистре ESI, а смещение функции от начала блока — в регистре EBX, аргументы функции можно передавать через стек

Как вариант, можно разместить перед функцией ASCIIZ-строку с ее именем (например, "my_func"), а затем осуществлять его хэш-поиск, что позволяет обстрагивается от смещений, а, значит, упростить кодирование, но в этом случае содержимое всех блоков должно быть зашифровано, чтобы текстовые строки не сразу бросались в глаза. Впрочем, шифруй - не шифруй, антивирус все равно сможет нас обнаружить, а обнаружив — поиметь. Или отыметь? А! Не важно!

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

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

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

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


С программами, откомпилированными на величину выравнивания от 08h до 10h байт, все намного проще и они вполне пригодны для внедрения.

Ниже в качестве примера приведен фрагмент одного из таких вирусов

.text:08000BD9             xor    eax, eax

.text:08000BDB             xor    ebx, ebx

.text:08000BDD             call loc_8000C01



.text:08000C01 loc_8000C01:                     ; CODE XREF: .text:0800BDD^j

.text:08000C01             mov    ebx, esp

.text:08000C03             mov    eax, 90h

.text:08000C08             int    80h                  ; LINUX - sys_msync

.text:08000C0A             add    esp, 18h

.text:08000C0D             call loc_8000D18



.text:08000D18 loc_8000D18:                     ; CODE XREF: .text:08000C0D^j

.text:08000D18             dec    eax

.text:08000D19             call   short loc_8000D53

.text:08000D1B             call   short loc_8000D2B



.text:08000D53 loc_8000D53:                     ; CODE XREF: .text:08000D19^j

.text:08000D53             inc    eax

.text:08000D54             mov    [ebp+8000466h], eax

.text:08000D5A             mov    edx, eax

.text:08000D5C             call   short loc_8000D6C

Листинг 3 фрагмент файла, зараженного пермутирующим вирусом "размазывающим" себя по кодовой секции

Естественно, фрагменты вируса не обязательно должны следовать линейно друг за другом. Напротив, если только создатель вируса не даун, CALL'ы будут блохой скакать по всему файлу, используя "левые" эпилоги и прологи для слияния с окружающими функциями.

В машинном представлении CALL target является относительным адресом. Как правильно вычислить относительный адрес перехода? Определяем смещение команды перехода от физического начала секции, добавляем к нему три или пять байт (в зависимости от длины команды). Полученную величину складываем в виртуальным адресом секции и кладем полученный результат в переменную a1. Затем определяем смещение следующей цепочки, отсчитываемое от начала той секции, к которой она принадлежит и складываем его с виртуальным адресом, записывая полученный результат в переменную a2.


Разность a2 и a1 и представляет собой операнд инструкции CALL.

Теперь необходимо как-то запомнить начальные адреса, длины и исходное содержимое всех цепочек. Если этого не сделать, тогда вирус не сможет извлечь свое тело из файла для внедрения в остальные файлы. Вот поэтому-то для перехода между блоками мы использовали команду CALL, а не JMP! При каждом переходе на стек забрасывается адрес возврата, представляющий собой смещение конца текущего блока. Как нетрудно сообразить, совокупность адресов возврата представляет собой локализацию "хвостов" всех используемых цепочек, а адреса "голов" хранятся… в операнде команды CALL! Извлекаем очередной адрес возврата, уменьшаем его на четыре и – относительный стартовый адрес следующей цепочки перед нами! Так что "сборка" вирусного тела не будет большой проблемой!



Рисунок 1 так выглядел файл cat до (слева) и после (справа) его заражения перкутирующим вирусом


Ступень 3: самый мусорный


Идея: внедряем в расшифровщик мусорные команды, не имеющие никакого побочного эффекта, но преображающие код до неузнаваемости. Чаще всего используются различные вариации NOP, которых насчитываются десятки (например, XCHG EBX, EBX/MOV ECX,ECX/Jx$+2/XCHG EAX, EBX; XCHG EBX, EAX и т.д. — главное фантазию иметь).

Обработанный расшифровщик из листинга 4 после "экзекуции" может выглядеть, например, так:

       XCHG   ECX, ECX

       MOV    ESI, offset body_begin

       JO     $+2

       MOV    EDI, ESI

       MOV    EAX, EAX

       MOV    ECX, offset body_end - body_begin

       NOP

my_begin:

       LODSB

       JNZ    $+2

       XOR    AL, 66h

       MOV    EDI,EDI

       STOSB

       JZ     $+2

LOOP my_begin

Листинг 9 расшифровщик, разбавленный ничего не значащими мусорными командами

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

А вот вторая проблема. Чтобы добавить мусорную инструкцию между двумя другими, необходимо как-то определить где кончается одна и начинается другая. Вот для этого нам и нужен дизассемблер длин. Дизассемблер длин представляет собой до предела упрощенный x86 дизассемблер, декодирующий инструкции и определяющий их границы. Это достаточно сложная задача, которая не имеет простых и красивых решений. Приходится зарываться в формат кодирования машинных команд, а это целый исторический пласт с кучей наслоений. Когда-то мы дойдем и до него, но нельзя ли пока обойтись методом грубой силы? Можно! Некоторые вирусы определяют границы команд с помощью трассировки, так сказать, руками самого процессора. Но это все равно утомительно. Лучше (и быстрее!) определить границы вручную и занести в специальную таблицу. Уродливо, зато эффективно.

Как антивирусы справляются с такими файлами? Да очень просто! Вычищают весь мусор и натягивают на отстоявшийся "осадок" старую добрую сигнатуру. Мусорные команды легко генерировать, но легко и распознавать! Так что данная методика крайне ненадежна и годится разве что для тренировки.



Ступень 4: еще более мусорный


Идея: усложнить генерацию мусорных инструкций, сделав ее менее очевидной. Например, если мы видим: MOV EAX,EBX, то перед этим в EAX можно писать все, что угодно. Все равно он будет заново проинициализирован.

Более сложная задача: отслеживать обращения к регистрам, выявлять неиспользуемые регистры (как локально, так и глобально), и пихать в них всякую глупость. Для этого нужен не только дизассемблер длин, но и полноценный дизассемблер команд, определяющий, что это именно MOV EAX,EBX, а не что-то другое. Причем, необходимо специальным образом обрабатывать флаги — встретив команду, зависимую от флагов (например, Jxx), необходимо найти ближайшую к ней инструкцию, воздействующую на этот флаг и между ними вставлять только тот "мусор", который не оказывает на флаги никакого влияния. Разумеется, это сложно. Очень сложно. Зато такой эффект!

       MOV    ESI, EAX

       SUB    ESI, ECX

       XCHG   EDX, EBP

       LEA    EBX, [ESI+EDI]

       ROR    AL,8

       MOV    ESI, offset body_begin

       XOR    ECX, ECX

       SUB    EDI, EAX

       ADD    EBP, ESI

       NOT    EDI

       MOV    EDI, ESI

       MOV    ECX, offset body_end - body_begin

       CALL   $+5

       SUB    EAX, ECX

       NOT    EBP

       POP    EBX

       XOR    EAX, EAX

       PUSH   EBX

       SUB    EBP,ECX

       LODSB

       MOV    EAX, EAX

       ADD    EDX, ESI

       XCHG   EBX, EBX

       XOR    AL, 66h

       XOR    EDX, EDX

       ADD    EAX, EDX

       STOSB

       MOV    EBP, EAX

       DEC    ECX

       XCHG   EBX,EBX

       MOV    EBP,ESI

       JNZ    $+4

       JMP    EBX

body_begin:

; // тело вируса со всеми остальными шифровщиками

body_end:

Листинг 10 расшифровщик, замусоренный значащими командами

Антивирусу, чтобы распознать этот код потребуется реализовать сложную систему графов, анализирующих зависимости по данным и отбрасывающих инструкции, замыкающие граф. Например, MOV EAX, EBX --> ADD EAX, ECX –> MOV EAX, ECX.
Последняя команда перекрывает результат деятельности первых двух, выдавая их галимую мусорную природу. Реализовать полиморфный генератор четвертого уровня намного проще чем разработать "лекарство", так что антивирусники тут находятся в проигрыше.

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

MOV $R1, offset body_begin

MOV $R3, offset body_end - body_begin

MOV $R3, $IP

XOR [$R1], 66h

ADD $R1, 1

SUB $R2, 1

JNZ $R3

Листинг 11 псеводокод простейшего шифровщика

Тогда наша задача сведется к написанию кодогенератора для x86, что намного проще. Наш псведокод может быть построен по самым демократичным принципам и иметь фиксированные длины инструкций. В псевдокоде допустимо напрямую адресовать регистр EIP, переложив заботу на кодогенератор, который, кстати говоря, можно выдрать из любого Open Source компилятора. Это тем более замечательно, что кодогенератор имеет множество различных опций, порождающих различный код (например, код для 8088 и 386 процессоров). Антивирусы идут в глухой отруб!


Ступень1 выбор случайного расшифровщика из списка


Идея: создаем некоторое количество шифровщиков/расшифровщиков, выбираем случайный шифровщик и зашифровываем тело вируса вместе с остальными шифровщиками/расшифровщиками. В каждом последующем поклонении расшифровщик расшифровывает вирус, передает управление основному телу, а перед внедрением выбирает случайный шифровщик, которым и зашифровывает его. Код вируса меняется на 100% и "визуально" опознать зараженный файл уже не представляется возможным.

Правда, поскольку количество расшифровщиков всегда конечно (даже если оно очень-очень велико), сигнатурный поиск остается достаточно эффективной мерой противодействия. Антивирус просто заносит в базу "фотороботы" всех расшифровщиков и… хана вирусу. Правда, тут есть одно небольшое "но". Расшифровщики различных вирусов (и даже честных программ с навесной защитой) обычно похожи как две капли воды и потому в их детектировании нет никакой пользы. Антивирус вынужден привлекать эмулятор, имитирующий выполнение расшифровщика и раскриптовывающий основной вирусный код. Если распаковщик содержит антиотладочные команды или использует машинные инструкции, не поддерживаемые эмулятором, антивирус обломается по полной программе. Здесь, правда, появляется проблема типа "грабли", на которые наступают те, кто борется с эмулятором. Чем больше антиотладочных приемов содержит расшифровщик, тем выше его уникальность и, следовательно, надежнее детектирование. Строго говоря, первая ступень — это еще не полиморфизм. Такие вирусы называют псевдо-полиморфными или олигоморфиками, но все-таки это уже большой шаг вперед.

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

Свойством "симметрии" обладают операции NOT; XOR X, любая константа; ROL/ROR X, 4 и некоторые другие арифметическо-логические операции, например, ADD byte,100h/2. Простейший симметричный шифровщик может выглядеть так (предварительно необходимо открыть сегмент кода на запись, что можно сделать функцией VirtualProtect):

       MOV    ESI, offset body_begin

       MOV    EDI, ESI

       MOV    ECX, offset body_end - body_begin

my_begin:

       LODSB

       XOR    AL, 66h

       STOSB

LOOP my_begin

body_begin:

; // тело вируса со всеми остальными шифровщиками

body_end:

Листинг 4 симметричный расшифровщик на основе XOR

А вот другой расшифровщик:

       LEA EAX, body_end

my_begin:

       NOT    byte ptr DS:[EAX]

       SUB    EAX, offset body_begin +1

       PUSHF

       ADD    EAX, offset body_begin

       POPF

       JNZ my_begin

body_begin:

; // тело вируса со всеми остальными шифровщиками

body_end:

Листинг 5 симметричный расшифровщик на основе NOT

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


Ступень2: расшифровщик из кирпичиков


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

MOV ESI,offset body_begin à LEA ESI,body_begin;MOV ESI,offset body_begin-1/INC ESI;

MOV EDI, ESI à PUSH ESI/POP EDI; XOR EDI,EDI/ADD EDI,ESI;

LODSB à MOV AL,[ESI]/INC ESI; SUB AL,AL/SUB AL,[ESI]/NOT AL/INC ESI/ADD AL,1

XOR AL,66h à XOR AL, 6/XOR AL, 60h

Листинг 6 инструкции и их синонимы

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

       LEA ESI, body_begin

       PUSH ESI

       POP EDI

       MOV ECX, offset body_end - body_begin + 2

       DEC ECX

       DEC ECX

my_begin:

       MOV AL,[ESI]

       INC ESI

       XOR AL, 6

       XOR AL, 66

       MOV [EDI], AL

       SUB EDI,-1

       ADD ECX,-1

       JNZ my_begin

Листинг 7 случайно сгенерированный расшифровщик

Поскольку, "синонимы" команд подготавливаются вручную еще на этапе ассемблирования, автоматически определять их длины совсем необязательно! Реализация получается простая как два пальца. Единственную проблему представляют метки. Как видно, и абсолютное, и относительное смещение my_begin изменилось. Мы не можем, не имеет права на этапе ассемблирования подставлять в команду JNZ смещение my_begin, поскольку оно будет указывать в космос. Что делать? Вот одно из решений проблемы. Вместо метки подставляем команду сохранения текущего EIP в один из свободных регистров общего назначения, а затем прыгаем на него:

my_begin:

       CALL

$+5      ; прыгаем на следующую команду, занося EIP


в стек

       POP EBX              ; выталкиваем EIP из стека

       PUSH EBX      ; ( как вариант, можно дать INC EBX)

       ...

       JNZ

$+4              ; выход из расшифровщика

       JMP EBX              ; переход на my_begin с динамич

Листинг 8 автоматическое определение смещений меток

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

Не стоит забывать и про эмулятор. Расшифровав основной код вируса, разработчики запросто отождествят его по сигнатуре. Поэтому, антиотладочные приемы должны быть! Здесь за уникальность можно уже не опасаться (команды-синонимы делают свое), оттянувшись на полную катушку со всем набором SSE и прочих векторных команд, до которых антивирусным эмуляторам еще ползти и ползти…


Ступень5: дальше только небо


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

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

Более продвинутые полиморфики вообще отказываются от расшифровщика и модифицируют непосредственно само вирусное тело. Это архисложная задача, которую в полном виде до сих пор никто не решил. Усилий тут требуется просто море. Зато какой куш нас ждет! Адекватных методик детектирования подобных вирусов до сих пор не разработано и вряд ли они появятся в дальнейшем. Программистам здесь делать пока нечего. Сначала тут должны поработать математики. Необходимо научить компьютер распознавать "смысл" совокупности машинных команд, а это уже из области AI, работы над котором были свернуты еще в конце семидесятых. Автоматическая декомпиляция невозможна! А вот компиляция — очень даже вполне! Так что появление очередных полиморфных монстров уже не за горами.



Заключение или прокалывая небо


Вот уже статья подошла к своему концу, который как ни оттягивай, а он все равно наступает, а готовых примеров полиморфных генераторов в ней не наблюдалось. Почему? Да потому, что даже самый простейший пседвополиморфный вирус в статью просто не влезет. Даже если сосредоточиться на ключевых фрагментах генератора, нам потребуется стопка листов формата A1. Лучше взять готовый исходник (благо недостатка в них не наблюдается, сходите хотя бы на vx.netlux.org и скачайте все вирусные журналы) и проанализировать его. А эта статья поможет понять назначение отдельных вирусных частей, определив степень его полиморфизма и выбранный алгоритм.

Что ж! Как говориться, дорогу осилит идущий. Кстати говоря, что-то до сих пор не слышно про вирусы под платформу x86-64 (она же AMD-64). Неправильно это! В следующей статье мы покажем какие преимущества дает переход на новые процессоры от AMD и как начать писать ассемблерные программы под нее. Как-никак, это последнее радикальное изменение архитектуры со времен 386, у которого есть неплохие шансы потеснить Intel и занять свое место под солнцем. Немного забегая вперед скажем, что иметь для этого 64-битный процессор хоть и желательно, но необязательно. На первых порах можно воспользоваться и эмулятором, а потом уже решать — нужна ли нам эта технология или нет.