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