(x) 1997-2000 Z0MBiE
http://z0mbie.host.sk
Почему я взялся за написание этой статьи? Потому, что разобрался с пермутацией и на этом пути узнал много нового, а такие знания представляют наибольшую ценность, и, следовательно, достойны опубликования. Сегодня пермутация распространена скорее в виде идеи, чем на практике. Я за то, чтобы положить начало широкому практическому применению. Но обо всем по порядку. Возможности программиста ограничиваются в первую очередь его воображением, и только потом умением и опытом. В какой-то момент вы подумаете - это все теория, фантазии. Поэтому хочу предупредить такие действия, сказав, что все здесь описанное, реализовано мной на практике, и это реально существует и работает.
Нас, вирмэйкеров, объединяет процесс. Цель у каждого своя. Кто-то
получает удовольствие, кто-то знания, кто-то -- и то и другое.
Удовольствия я оставлю себе, а знания предлагаю вам.
Слово "пермутация" пришло хрен знает откуда. В словаре написано, что
это: перестановка, подстановка, размещение, перемена, изменение,
перемещение и т.п. Мы будем понимать под пермутацией процесс изменения
вируса на уровне ассемблерных инструкций. Еще более детально, пермутация
-- это создание новой копии вируса состоящей из перемещенных в другое
место и, возможно, измененных инструкций, используя в качестве источника
имеющуюся копию.
Существует еще одна интересная фича, не имеющая к пермутации никакого
отношения. Заключается эта фича вот в чем: вирус кодируется некоторым
хитрым образом, а в процессе создания каждой новой полиморфной копии
вируса на основе этих закодированных данных и генерируется ассемблерный
код. Так поступают вирусы TMC, Lexotan и теперь уже порядком других
появилось. Прообразом таких действий мне представляется генератор
полиморфных расшифровщиков, где расшифровщик вырос до размеров вируса.
Минус тут один -- это сложно. Плюсы в том, что пермутирующий вирус
сложнее дизассемблировать и изучать, и, следовательно при возникновении
эпидемии вирус имеет больше времени на распространение. К этому вопросу
стоит отнестись достаточно серьезно хотя бы потому, что если началось
нечто вроде "цепной реакции" распространения вируса, и за каждый следующий
час число копий значительно увеличивается, то любая задержка в написании
антивирусного кода играет значительную роль.
Кроме того, с пермутацией вносятся сложности в процесс детектирования
вируса в файле, замедление которого весьма нас порадует.
Поскольку пермутацию мы будем применять к своему собственному коду, то
мы наложим на него некоторые ограничения, что позволит сильно упростить
алгоритм движка и сделает возможными разные интересные вещи.
Здесь уже должно быть ясно, что внутри буфера с исходным кодом (т.е. с
вирусом) не должно быть никаких данных -- они были бы просто потеряны
после генерации нового буфера. Таким образом мы избавляемся от
необходимости анализа данных. Взамен этого неиспользуемое пространство в
выходном буфере может быть заполнено, например, случайными значениями.
Все команды условного перехода должны идти непосредственно после
команд, изменяющих соответствующие флаги -- это позволит нам менять
местами воздействующие на флаги инструкции.
Рассмотрим вопрос о хранении данных внутри пермутирующего вируса.
Учитывая то, что измененные инструкции могут быть другой длины,
вирус есть буфер в котором содержится только код; этот
буфер будет перестраиваться с каждой новой копией вируса.
В таком случае возможны два варианта:
Мне ближе всего второй вариант. Он обладает следующими свойствами: в
вирусе присутствует только буфер с кодом; данные разбиты на части, каждая
из которых генерируется по мере необходимости. Недостаток тут такой: код,
генерирующий данные, занимает чуть больше места, чем сами данные.
Решением этой проблемы являются макросы, генерирующие код из текстовых
строк.
Сгенеренный этими макросами код может выглядеть, например, так:
На вход пермутирующему движку подается исходный буфер с кодом. Метки,
на которые из этого буфера идет передача управления командами типа
JMP/CALL/JCC (с относительным к EIP аргументом), могут быть как внутри так
и вне буфера. Последние мы будем называть внешними метками. От
пермутирующего движка будет требоваться сохранить работоспособными все
внешние вызовы.
Практика показала, что если не идеально, то близко к совершенству
вынесение дизассемблирования инструкций в отдельную задачу. При этом
дизассемблирование имеется в виду не полное, а только на предмет длины
инструкции. Поэтому мы отдельно пишем и отлаживаем дизассемблер длин, и
применяем где только придется, включая и нашу задачу.
Дизассемблер длин может быть, конечно, расчитан только на те
инструкции, которые используются в конкретном вирусе. Это упрощает
дизассемблер в разы, лишая его универсальности. То есть такой дизассемблер
может быть применен только к одному вирусу и в следующий раз придется его
переделывать. Написав несколько штук таких весьма похожих подпрограмм, я
остановился на универсальном дизассемблере длин LDE32, чего и вам желаю.
Итак, в дальнейшем мы знаем длину любой команды, и исходя из этого
можем разбить весь вирус на минимальные его составляющие - инструкции.
Для работы с инструкциями существует несколько препятствий. Например
то, что инструкции переменной длины -- это заставляет встраивать в
пермутирующий движок дизассемблер длин. (в вирусах Ply этот вопрос решен
тем, что все команды дополнены до одинаковой длины NOP-ами). Также нам
мешает то, что инструкции могут быть связаны командами типа JMP и CALL --
это не позволяет просто так убрать какую-нибудь инструкцию из программы
или вставить в программу лишнюю инструкцию. Значит требуется
преобразование ассемблерного кода в некоторую промежуточную сущность, кояя
позволит легко оперировать инструкциями. Таким объектом является список
инструкций. Можно обойтись и без списка инструкций. Так я поступил в ZCME.
Движок работал за один проход, в котором одновременно происходило
дизассемблирование старого кода и ассемблирование нового. В результате в
один момент времени я мог рассматривать только одну ассемблерную команду,
но не несколько. Список же позволяет не только единовременно оперировать
несколькими инструкциями, но и производить над ними такие действия,
которые без него весьма трудоемки.
Рассмотрим одну запись списка, соответствующую одной ассемблерной
команде.
Во-первых, должен существовать указатель на следующую запись списка,
коий равняется NULL для последней записи.
Следует отметить, что этот указатель связывает только записи списка,
но не команды. То есть команда в следующей записи списка не является
ассемблируемой сразу после команды из предыдущей записи.
Теперь информация о команде.
В качестве дополнительной информации в записи может хранится еще и
текущее смещение команды, оно используется во время
дизассемблирования&обработки списка, а также во время
ассемблирования&линковки.
Итак, с учетом списка инструкций имеем три основных этапа:
(*) дизассемблирование предполагает наличие последующего этапа, то
есть некоторую обработку списка уже после дизассемблирования всего
исходного кода. Эта обработка позволит сделать команды независимыми от их
исходных смещений, то есть заменить ссылки на смещения (на метки) ссылками
на записи списка.
Кроме всего прочего, обработка списка позволяет выявить метки, то есть
команды, на которые происходит передача управления. (эта информация
потребуется при последующей мутации)
(**) Ассемблирование предполагает наличие последующей линковки. Это
происходит потому, что существует ситуация, когда уже была ассемблирована
первая команда с указателем на вторую, но еще не была ассемблирована
вторая команда, то есть адрес второй команды еще не известен сейчас, а
будет известен только позже.
Итак, дизассемблирование у нас есть разбор куска кода в список инструкций.
Происходит это по следующему алгоритму:
Ниже приведен кусок из движка RPME, дизассемблирующая часть:
Собственно этот пункт не обязателен, он нужен для упрощения задачи.
Здесь мы проводим две операции:
В принципе этот этап не обязателен если вы планируете использовать
JMPы как постоянные команды. Я же использую JMPы в качестве мусора,
поэтому здесь они удаляются, а при ассемблировании списка будут
добавляться случайным образом.
На этом шаге у нас есть список уже измененных инструкций из которого
нужно сделать ассемблерный код.
Ассемблирование списка происходит по следующему алгоритму:
Вот как это выглядит на C++:
Линковка здесь требуется не для абсолютных смещений, которые мы вовсе
не рассматриваем, а для относительных к EIP 4х-байтных смещений, коии
присутствуют после опкодов jmp,call,jcc.
Относительный адрес перехода можно пофиксить зная,
на которую запись списка указывает относительное смещение и зная
оффсеты текущей команды и команды-метки
(эти оффсеты становтся известны при ассемблировании).
Совершенно охуительным качеством любого движка (равно как и вирусного
конструктора) является добавление к нему новых возможностей. Причем сии
дополнения должны быть намного проще чем написание всего движка и,
желательно, приводить к таким изменениям в работе движка, чтобы
антивируснику приходилось заново разбираться с кодом, произведенным в
результате изменений. Именно по этому мутация, то есть -- мутация списка
инструкций, производимая между дизассемблированием и ассемблированием,
должна быть вынесена во внешнюю процедуру, которая может быть легко
изменена и/или дополнена пользователем.
Очевидны три основных действия, применяемых к инструкциям: замена,
перестановка и мусор (т.е. добавление мусора). Действие может быть
ОБРАТИМЫМ и НЕОБРАТИМЫМ, в зависимости от того, возможно ли после него
вернуться к исходному состоянию или нет. Необратимые действия как правило
приводят к увеличению кода. Замена и перестановка являются операциями
обратимыми. Добавление мусора может быть операцией как обратимой, так и
необратимой, в зависимости от того, насколько наши возможности совпадают
со сложностью алгоритма удаления мусора из кода. Комбинация замены и
мусора увеличивает вероятность необратимых изменений в коде.
Возможны два варианта мутации, с использованием необратимых изменений
и без них.
Очевидно, сказать здесь почти нечего -- все определяется практической
реализацией... (что называется -- смотри код)
Рассмотрим например такую команду, как условный переход (Jcc).
Находим Jcc, меняем на JNcc (инвертируем условие, попросту -
XORим опкод на 1). Меняем местами указатели на следующую и
условно-следующую записи списка. Готово.
Теперь при ассемблировании две ветви алгоритма (после Jcc) поменяются
местами.
Пример:
Еще пара вариантов замены: XOR r, r <--> SUB r, r, TEST r, r <--> OR r,r
и т.п.
Учитывая одно из наложенных на код ограничений -- условные jmp-ы идут
только сразу за командами, устанавливающими соответствующие флаги,
рассмотрим 2 команды, следующая за которыми - не jcc.
Обязательным условием будет также отсутствие флага XREF на
каждой из команд, то есть они не должны быть метками.
Кроме того, из двух команд только одна может использовать стэк.
А если какая-либо из команд использует
ESP, то обе не должны использовать стэк.
если какой-либо один из аргументов отсутствует,
условия с ним не проверяются.
Применяя таким образом перестановку для всего списка инструкций легко
добиваемся перемешивания некоторых команд друг с другом.
Процедуры перестановки команд и добавления мусора, а точнее их
сложность, находятся в зависимости от ограничений на код, то есть от
дополнительных правил, которые мы накладываем на ассемблерные инструкции.
Чем сложнее мусор, добавляемый в код, тем сложнее его генерировать и
удалять, и тем сложнее оставить программу работоспособной. С другой
стороны, чем проще мусор - тем легче его генерировать и удалять, в идеале
это NOP, не влияющий ни на флаги, ни на регистры, ни на стэк. При всем
этом мусор не обладает практически никакой эффективностью, то есть не
делает программу сложнее, хотя при этом сильно увеличивает результирующий
код. Другими словами, если детектирующий алгоритм будут писать таким,
каким я себе это представляю, то один хуй, убирать ли из вируса NOPы или
более сложные инструкции - результат почти тот же самый.
О результатах расскажем и даже покажем кодом.
Итак, в середине пермутации движок вызывает процедуру обработки списка
инструкций. (callback)
Проход по всем инструкциям вируса выглядит так:
Для того, чтобы во всем вирусе изменить все, например, NOPы на XCHG EBX,EBX
в обработку инструкции достаточно добавить такой код:
Для того, чтобы удалить инструкцию из списка и, соответственно,
из вируса, делаем так:
или так:
В первом случае могут остаться ссылки на эту запись (то есть она реально
не удалится) (проверить сие можно так: test [ebx].h_flags, CM_XREF)
Для того, чтобы инвертировать все jcc на jncc с сохранением
работоспособности кода, делаем так:
Мне кажется, что легкость приведенных операций и все возможныеТЕРМИНЫ
ПЕРМУТАЦИЯ
МЕТАМОРФИЗМ (ГЕНЕРАЦИЯ КОДА)
ПЛЮСЫ И МИНУСЫ ПЕРМУТАЦИИ
ОГРАНИЧЕНИЯ НА ИСХОДНЫЙ КОД
ХРАНЕНИЕ ДАННЫХ ВНУТРИ КОДА
lea edi, temparea x_push ecx, C:\WINDOWS\*.EXE~
x_stosd C:\WINDOWS\*.EXE~ nop
x_pop
BF00200010 mov edi,0xxxxxxxx 33C9 xor ecx,ecx
33C0 xor eax,eax 81E900868687 sub ecx,087868600
2DBDC5A3A8 sub eax,0A8A3C5BD 51 push ecx
AB stosd 81F12E3F213D xor ecx,03D213F2E
350A741818 xor eax,01818740A 51 push ecx
AB stosd 81C1290E04E5 add ecx,0E5040E29
050E0518DB add eax,0DB18050E 51 push ecx
AB stosd 81F11E1D1865 xor ecx,065181D1E
357916046F xor eax,06F041679 51 push ecx
AB stosd 81E90614E8F7 sub ecx,0F7E81406
2D2ECD0111 sub eax,01101CD2E 51 push ecx
AB stosd 90 nop
8D642414 lea esp,[esp+00014]
ВНЕШНИЕ ВЫЗОВЫ
ДИЗАССЕМБЛИРОВАНИЕ ИНСТРУКЦИЙ
СПИСОК ИНСТРУКЦИЙ
ФОРМАТ ЗАПИСИ СПИСКА
#define CM_STOP 1 // JMP/RET-alike instruction
#define CM_HAVEREL 2 // have relative argument (JMP,JCC,CALL,etc.)
#define CM_EXTREL 4 // rel. arg points to external label
#define CM_ASSEMBLED 8 // alredy assembled
#define CM_XREF 16 // label, i.e. have XREF
struct hooy // instruction list entry structure
{
BYTE cmd[MAXCMDLEN]; // opcode
BYTE* ofs; // pointer to current location (temporary)
DWORD len; // length of command
DWORD flags; // CM_xxx
hooy* rel; // CM_HAVEREL: 0=NULL 1= CM_EXTREL: 0=hooy* 1=BYTE*
hooy* nxt; // CM_STOP: 1=NULL 0=hooy*
hooy* next; // next entry or NULL
};
ЭТАПЫ ПЕРМУТАЦИИ
b) обработка списка (*)
b) линковка (**)
ДИЗАССЕМБЛИРОВАНИЕ
пометить все точки входа для последующей обработки
while (есть помеченные для-последующей-обработки инструкции)
{
найти оффсет инструкции для-последующей-обработки
for (;;)
{
вычислить длину команды (используя дизассемблер длин)
пометить инструкцию как код
добавить инструкцию в список
если опкод JMP/CALL/JCC то обозначить метку на которую они показывают
как 'для-последующей-обработки'
если опкод RET или JMP то выход из цикла
перейти к следующему опкоду
если опкод уже обработан, выход из цикла
}
}
imap[ientry] = C_NEXT; // mark entrypoint(s)
for (hooy**h = &root;;) { // main disasm cycle
DWORD ip; // current address (relative)
for (ip=0; ip
ПОДГОТОВКА СПИСКА К МУТАЦИИ
АССЕМБЛИРОВАНИЕ СПИСКА
while (есть не ассемблированные команды в списке)
{
выбрать случайную инструкцию из списка
for (;;)
{
с некоторй вероятностью: выбрать случайное место в выходном буфере
копировать команду в буфер
сохранить смещение команды в буфере в запись списка
перейти к следующей команде
если команда уже была добавлена в список, выход
}
}
DWORD ip=0;
if (poentry) *poentry=ip;
printf("entrypoint at %08X\n", ip);
if (ofiller!=NONE) for (DWORD i=0; i
ЛИНКОВКА
for (hooy* h=root; h; h=h->next) // for each entry
if (h->flags&CM_HAVEREL) { // have relative argument?
*(DWORD*)&h->ofs[h->len-4]=
(h->flags&CM_EXTREL ? ((DWORD)h->rel+extrelfix) : (DWORD)h->rel->ofs)
- (DWORD)h->ofs - h->len;
}
ДОПОЛНЕНИЕ АЛГОРИТМА (PLUGIN-Ы) И ВНЕШНЯЯ МУТАЦИЯ
ИЗМЕНЕНИЕ СПИСКА (МУТАЦИЯ)
ЗАМЕНА
; JNZ (было) JZ (стало)
... ...
cmp eax, 'MZ' cmp eax, 'MZ'
jnz skip jz exe
exe: call infect skip: nop
skip: nop ...
... exe: call infect
jmp skip ; auto-generated jmp
ПЕРЕСТАНОВКА
ttt [r1] [,r2]
ttt [r3] [,r4]
условие допустимости перестановки двух команд:
(r1 != r4) && (r2 != r3) && (r1 != r3)
МУСОР
О РЕЗУЛЬТАТАХ
mov ebx, _root
__cycle:
; обработка текущей инструкции
mov ebx, [ebx].h_next
or ebx, ebx
jnz __cycle
cmp [ebx].h_opcode, 90h
jne __skip
mov word ptr [ebx].h_opcode, 0DB87h
mov [ebx].h_len, 2
__skip:
mov eax, [ebx].h_next
mov eax, [eax].h_next
mov [ebx].h_next, eax
mov [ebx].h_len, 0
mov [ebx].h_flags, 0
mov ax, word ptr [ebx].h_opcode
and ax, 0F0FFh
cmp ax, 0800Fh ; near jcc: 0F 8x nn nn nn nn
jne __skip
xor [ebx].h_opcode, 1
mov eax, [ebx].h_nxt
xchg eax, [ebx].h_rel
mov [ebx].h_nxt, eax
__skip: