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

       

Ступень 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 до (слева) и после (справа) его заражения перкутирующим вирусом


Содержание раздела