Путеводитель идиота по написанию полиморфных движков

Дата публикации 26 авг 2002

Путеводитель идиота по написанию полиморфных движков — Архив WASM.RU

1. Введение

Полиморфизм, по-моему мнению, очень сложная тема, чтобы ее можно было хорошо изложить в пределах одного маленького туториала. Вы можете найти туториал, в котором будет рассказано многое, но лично я еще не видел ни одного туториала, где бы излагалсь все аспекты (написания) полимофных движков. Почему? Потому что это очень большая тема, а самое главное, есть много путей и способов, которым можно добиться требуемого. Многие туториалы объясняют и дают много примеров того, как могут выглядеть сгенерированные декрипторы, и почему компоновка (layout) декрипторов случайным образом так необходима. Я попытаюсь объяснить больше, чем это делается в среднем туториале, но, тем не менее, это тоже будут только основы полиморфизма, чтобы вы могли получить хорошее представление, что это такое.Также вам следует решить, действительно ли вы хотите сделать свой полиморфный движок. Обартите внимание, что это "Путеводитель идиота по написанию полиморфных движков", то есть излагаются основы их написания и проблемы, с которыми вы сможете столкнуться во время этого. Я дам вам примеры конкретного кода, не только, потому что он эффективен, но и для того, чтобы научивать писать свой. Для этой цели полный исходный код не нужен и поэтому не был включен. Возможно это расстроит кого-то из читателей, но я не верю, что код, готовый к использованию, поощеряет творческое начало.

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

2. Почему полиморфизм?

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

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

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

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

Неплохо, но действительно ли это стоит того (особенно учитывая, что большинство движков больше самих вирусов)? Как правило: да. Во-первых, полиморфный вирус достаточно неприятен для людей из AV-индустрии, и если движок достаточно сложен, им придется спроектирвать процедуру обнаружения специально под ваш движок. Это займет время, которое ваш вирус может использовать для свего собственного распространения. Во-вторых, создание процедуры обнаружения, которая будет ловить 100% из любых возможных поколений очень сложно. Чем сложнее движок, тем труднее это будет сделать. Если ваш полиморф не 100% обнаруживаем, у вируса остается шанс на выживание, даже если антивирусники утверждают, что могут его обнаружить! Здесь я согласен с Бонтчевым: любой процент обнаружения меньше 100% практически бесполезен, так как один пропущенный зараженный файл может заразить всю систему или сеть.

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

3. Как вызывать движок

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

Код (Text):
  1.  
  2. Входные параметры:
  3.        DS:SI = код, который должен быть зашифрован (тело вируса)
  4.        ES:DI = место, где должен быть помещен декриптор и зашифрованный код
  5.        CX    = длина код
  6.        AX    = смещение в памяти стартового кода вируса
  7.  
  8. Возвращает:
  9.        CX    = длина декриптора + зашифрованный код
  10.        DS:DX = смещение декриптора и зашифрованный код

Этот движок шифрует кусок кода (вирус) с помощью определенного алгоритма и помещает вначале соответствующий декриптор. Как вы можете видеть, дружественный к пользователю движок возвращает параметры в формате i21/40: после того, как движок был вызван, вирусу нужно только установить AH=40 и вызвать Int21, предполагая, что хэндл находится в BX. Обратите внимание, что ES:DI на входе и DS:DX на выходе равны.

Таким образом, вам нужно сделать следующее:

  • Правильно установите входной параметр, указывающий на тело вируса (в данном случае DS:SI):
  • Правильно установите входной параметр, указывающий на свободную память, которой будет достаточно для вмещения декриптора и зашифрованного тела вируса (не забудьте включить движок в размер; это часть вируса!)
  • Правильно установитель входной параметр размера вируса (повторюсь, не забудьте включить в размер движок, так как это часть вируса)
  • Установите входной параметр смещения в памяти в правильное значение. Это смещение (в памяти, куда загружен файл), по которому будет начинаться декриптор. Пример: при заражении 200-байтового COM-файла, смещение, по которому начнется декриптор (начало вируса) будет равно 100h + 200d = 1С8h. Обратите внимание, что смещение в файле будет 200d = C8h, но COM-файл грузится в памяти после PSP, т.е. начинается со смещения 100h.
  • Мы почти готовы к вызову движка. Последнее, что мы должны проверить, это разрушает ли движок значения регистров, которые не использует как параметры. Это может избавить вас от лишней отладки. :smile3:

4. Пример движка

Полиморфный движок создает (из определенного количества вариантов) случайный декриптор, который соответсвует определенному методу шифровки (также случайный) при каждом заражении. Как он знает как сделать это. Программист движка задает пару декрипторных схем, из которых движок выбирает один, а затем случайным образом изменяет определенные байты. Это требует некоторого пояснения: давайте представим (примитивный) движок, в котором есть только одна декрипторная схема. Пусть она выглядит примерно так:

Код (Text):
  1.  
  2. [1]    MOV     <reg16> , смещение_первого_зашифрованного_слова
  3.     :decrypt_next_byte:
  4. [2]    <crypt> [<reg16>] , случайное_значение
  5. [3]    <add2>  <reg16>
  6. [4]    CMP     <reg16> , смещение_последнего_зашифрованного_слова
  7. [5]    JB     расшифровываем_следующий_байт
  8.  
  9. Легенда: <reg16>        = случайный 16-ти битный регистр (AX, BX, SI, BP, etc.)
  10.          [<reg16>]      = указатель на содержимое по смещению, сохраненному в
  11.                           <reg16>
  12.                           eg: BX=1234, [BX]=что по смещению 1234
  13.          <crypt>        = шифрующая инструкция, eg: XOR, ADD, SUB
  14.          <add2>         = добавляем 2 к <reg16> (добавляем 1 при побайтовой
  15.                           шифровке)

Все изменяющиеся значения движок изменит следующим образом:

  • Он выбирает случайный регистр, с котором будет работать. В данном примере движок сохранит инструкцию MOV, закодированную с правильным регистром (об этом ниже), после чего сохраняет смещение первого зашифрованного байта (или слова) вируса.
  • После этого он выбирает (из таблицы, предоставленной программистом) расшифровочную операцию. Обратите внимание, что движок должен запомнить, какая операция была выбрана, потому что в дальнейшем вирус должен быть зашифрован так, чтобы сгенерированный ранее метод расшифровки корректно работал. Все верно, метод шифрования наследуется от метода расшифровки. Движок сохраняет шифрующие операции, а затем генерирует случайное значение, идущее вместе с шифрующей операцией. Это значение тоже необходимо запомнить, иначе впоследствии вирус не сможет быть зашифрован так, чтобы работал декриптор. Например, если случайное значение равно 1234, шифрующая операция равна XOR, а регистр - BX, то шифрующая инструкция будет 'XOR [BX], 1234'. Движок сохраняет значение и повторяет пункт [3] определенное (случайное) количество раз, так как одна инструкция обеспечивает достаточно слабую защиту.
  • Добавляем 2 к <reg16>. Продвинутые движки умеют делать выбор побайтовым и пословным шифрованием, сейчас я предполагаю, что этот движок использует пословное шифрование. Значение регистра необходимо увеличить на два, чтобы в следующем проходе цикла декритор расшифровал следующее слово (и зачем я это объясняю... :smile3: )
  • Сравниваем регистр с EOV (конец вируса). Говорит само за себя (я надеюсь :smile3: ).
  • Если мы еще не достигли EOV, делаем переход и расшифровываем следующий байт/слово.

4.1 Кодирование регистров

В данном примере движок хочет выбрать регистр. Чтобы понять то, что он делает, мы должны рассмотреть, как в 80x86 кодируются инструкции. Когда я работал над своим собственным движком, у меня не было никаких технических документов, поэтому я взял SoftIce и гексого-двоичный калькулятор и начал исследование. Здесь представлена его часть относительно 'MOV reg16, reg16':

Код (Text):
  1.  
  2.  INSTRUCTION       HEX          BINARY
  3.  
  4. MOV     AX,AX      8BC0    10001011 11000000
  5. MOV     AX,BX      8BC3    10001011 11000011
  6. MOV     AX,CX      8BC1    10001011 11000001
  7. MOV     AX,DX      8BC2    10001011 11000010
  8.  
  9. MOV     BX,AX      8BD8    10001011 11011000
  10. MOV     BX,BX      8BDB    10001011 11011011
  11. MOV     BX,CX      8BD9    10001011 11011001
  12. MOV     BX,DX      8BDA    10001011 11011010
  13.  
  14. MOV     CX,AX      8BC8    10001011 11001000
  15. MOV     CX,BX      8BCB    10001011 11001011
  16. MOV     CX,CX      8BC9    10001011 11001001
  17. MOV     CX,DX      8BCA    10001011 11001010
  18.  
  19. (...)

Посмотрите на двоичные значение. Вы что-нибудь замечаете? Очевидно, что первый байт инструкции 'MOV reg16,reg16' всегда равен 10001011b или 8Bh. Второй байт меняется только того, когда меняем регистры. При более внимательном рассмотрении оказывается, что биты 5-7 всегда остаются равными 110.

ОБРАТИТЕ ВНИМАНИЕ: Для тех из вас, кто ничего не знает, самый правый бит называется 'бит 0', а самый левый (последний) - битом 7. Байт состоит из 7 битов, а не из 8? Нет, так как бит 0 - первый бит, то бит 7 - восьмой.

О чем я говорил? Ах, да, последний бит всегда остается 110. Теперь, похоже, что остальные биты постоянно меняются. Я слышу крик внимательно читателя: но что насчет 5-ого и 3-его битов?! В данном примере я не использовал 16-ти битные регистры SI, DI, SP и BP. Они также должны быть закодированны, для них и требуется тот самый третий бит.

Код (Text):
  1.  
  2. Заключение:             10001011 11...... = MOV reg16,reg16

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

Внимательный читатель, вероятно, заметит, что я рассказал только об инструкции 'MOV reg16, reg16', а в движке-примере требуется не два регистра, а только ОДИН регистр и число (стартовое смещение). Смотрите:

Код (Text):
  1.  
  2.   INSTRUCTION       HEX                 BINARY
  3.  
  4. MOV     AX,1234    B83412       10111000 00110100 00010010
  5. MOV     CX,1234    B93412       10111001 00110100 00010010
  6. MOV     DX,1234    BA3412       10111010 00110100 00010010
  7. MOV     BX,1234    BB3412       10111011 00110100 00010010
  8. MOV     SP,1234    BC3412       10111100 00110100 00010010
  9. MOV     BP,1234    BD3412       10111101 00110100 00010010
  10. MOV     SI,1234    BE3412       10111110 00110100 00010010
  11. MOV     DI,1234    BF3412       10111111 00110100 00010010

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

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

Код (Text):
  1.  
  2. Заключение:             10111... 00110100 00010010  =  MOV &lt;reg16&gt;, 1234

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

Код (Text):
  1.  
  2.                 AX = 000b = 0h
  3.                 CX = 001b = 1h
  4.                 DX = 010b = 2h
  5.                 BX = 011b = 3h
  6.                 SP = 100b = 4h
  7.                 BP = 101b = 5h
  8.                 SI = 110b = 6h
  9.                 DI = 111b = 7h

Все используемые регистры перечислены здесь (IP не в счет), и мы можем заметить, что они как раз влезают в доступные 3 бита. Что за совпадение :smile3:. Запишите этот список, он вам понадобится.

Теперь вопрос состоит в том, как мы скажем движку сделать это? Очень просто: ребята в Интеле изобрели инструкцию 'OR'. Я думаю, многие начинающие вирмейкеры не знают точного назначения этой инструкции. Я думаю, что вам следует об этом знать:

Код (Text):
  1.  
  2.         0 OR 0 = 0
  3.         0 OR 1 = 1
  4.         1 OR 0 = 1
  5.         1 OR 1 = 1

Но вы, наверное, удивляетесь: для чего я могу использовать эту инструкцию. Ок, что она реально делает, это дает нам уверенность, что в байте-цели все биты будут установлены (=1), если они были установлены в первом или втором операнде. И это очень полезно для кодирования регистров: помните наши изменяющиеся биты для 'MOV <reg16>, <reg16>'? Нет? Это были 10001011 11...... = MOV <reg16>,<reg16>, с первыми тремя точка для reg16-назначения и вторые три для исходного reg16. Скажем, если мы заполним точки нулями для reg16-назначения, а затем проORим их с соответствующим кодируемым регистрам значениями. Скажем, нам нужен 'MOV BX, CX'. Сначла нам нужно поместить в регистр (например, AX) базовое для 'MOV <reg16>,<reg16>' значение (10001011 11000000):

Код (Text):
  1.  
  2.         MOV AX, 8BC0h   (используйте калькулятор с функцией bin2hex)

Затем нам нужно закодировать значения регистра-назначения в битах 3-5 и регистра-источника в битах 0-2, потому что инструкция MOV требует от нас этого. BX = 011b и CX = 001b, поэтому когда мы соединим их, мы получим 011001b. В итоге, чтобы проORить это значением с AX (которое содержит значение инструкции MOV): OR AX, 19 (снова используйте калькулятор с bin2hex).

Код (Text):
  1.  
  2. AX = 8BC0 = 10001011 11000000
  3.        19 = 00000000 00011001  OR
  4.             -----------------
  5.             10001011 11011001      что есть 8BD9, то есть MOV BX,CX!

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

4.2. Выбор регистра

Одна из первых вещей, которую должен сделать наш движок - это выбрать регистр, с которым он будет работать. В этом примере мы используем регистры, чтобы они адресовали к какому-либо участку в памяти, например XOR [BX], 1234. В силу архитектуры 80x86 мы ограничены четырьмя регистрами, а именно BX, SI, DI и BP. Поэтому 'XOR [AX], 1234' будет нелегальной инструкцией. Только эти четыре регистра поддерживают данный режим адресации (уже начиная с 386? процессора Интел сняла эти ограничения, поэтому вышеприведенная инструкция будет прекрасно работать практически на всех используемых машинах - прим.пер.).

Правда, с BP есть одна проблема. Давайте я покажу вам кое-что:

Код (Text):
  1.  
  2. 81 37 34 12             XOR  WORD PTR [BX],1234
  3. 81 77 01 34 12          XOR  WORD PTR [BX+1],1234

В первой инструкции мы адресуем с помощью регистра без относительного смещения. Во второй инструкции мы делаем относительное смещение и оно кодируется в дополнительном байте. Обратите внимание, что второй байт меняется с 37 на 77, когда мы добавляем относительное смещение. На двоичном уровне 37 и 77 отличаются только одним битом, а именно шестым. Это, конечно, делается для того, чтобы дать регистру знать, будет ли относительное смещение или нет.

Код (Text):
  1.  
  2. 81 34 34 12             XOR  WORD PTR [SI],1234
  3. 81 74 01 34 12          XOR  WORD PTR [SI+1],1234

То же самое верно и для SI. Если относительного смещения нет, инструкция выглядит по-другому. Это касается и DI, но не BP!

Код (Text):
  1.  
  2. 81 76 00 34 12          XOR  WORD PTR [BP+00],1234
  3. 81 76 01 34 12          XOR  WORD PTR [BP+1],1234

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

Какое это значение имеет для движка? В движке, который мы рассматривали в примере, мы адресовали без относительного смещения. Для SI, DI и BX дополнительный байт не нужен, достаточно установитель соответствующий бит в инструкции. Если мы хотим, чтобы движок мог работать и с BP, мы должны написать процедуру, которая будет проверять, не выбрал ли движок BP, и если так, добавлять дополнительный нулевой байт. Конечно, для написания как можно более случайных декрипторов это хорошая идея. Тем не менее, это улучшение лучше добавить, когда вы будут заканчивать уже работающий движок. Я дам вам хороший совет: не пытайтесь сразу создать сложный движок, сначала начните с простой, но работающей "оболочки", которую вы в дальнейшем сможете расширять. Если вы начнете добавлять все возможные примочки, вы найдете себя постоянно отлаживающимся.

Вернемся обратно к примеру. Я предполагаю, что мы используем только BX, SI и DI, поэтому нам не нужно думать об относительном смещении. Брр.. Что я объяснял ранее? Просто поместите закодированные значения BX, SI и DI в таблицу и позвольте движку выбирать из них.

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

4.3 Перемещение

После прочтения 4.1 понять то, что я говорю, будет нетрудно :smile3:. В движке, который мы рассматривали, нам нужно сделать 'MOV <reg16>, imm16>, что будет выглядеть так:

Код (Text):
  1.  
  2.              10111... 00110100 00010010  =  MOV &lt;reg16&gt;, 1234

О 1234 мы можем пока забыть. Для нас важен первый байт инструкции 10111..., где кодируемое значение регистра указано последними тремя точками.

Пример: как нам сделать так, что движок генерировал 'MOV SI, 0120' в декрипторе? Сначала мы должны заполнить точки нулями, а потом проORить получившееся значение с корректным опкодом. Я предполагаю, что точки заполнены нулями. Окей, базовое значение для MOV <reg16>, imm16 - B8h (сконвертированное в шестнадцатиричное число), давайте поместим это значение в AX. Теперь мы должны проORить AX с кодируемым значением регистра. Нам нужен SI, чье значение равно 6h (110b), поэтом мы выполним AX, 6h.

4.4 Расшифровка

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

Сейчас мы хотим сделать декриптор, и нам нужно сделать так, чтобы он был как можно более случайным. Поэтому мы должны дать движку как можно большее количество операций шифрования, из которых он сможет выбирать. XOR, ADD и SUB - хорошие примеры, потому что у них одинаковая конструкция: они работают с двумя операндами, для наших целей нам нужен будет ссылающийся на ячейку памяти регистр и случайное 16-ти битное значение, то есть, например, SUB [BX], 1234. Неплохой идеей является добавление других операций шифрования, таких как NEG, NOT, INC, DEC, но их недостатком является то, что они кодируются по другому, и нам потребуются дополнительные процедуры. Если вы решите добавить эти и другие инструкции в ваш движок, перечисленные выше четыре инструкции могут стать неплохим выбором, потому что они требуют одного операнда, а в нашем примера движке-примере это будет регистр-указатель, например NOT [SI]. На данный момент давайте договоримся, что наш движок использует только XOR, ADD и SUB.

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

Код (Text):
  1.  
  2. XOR:    81 30 xx xx
  3. ADD:    81 00 xx xx
  4. SUB:    81 28 xx xx

Пожалуйста, обратите внимание, что эти значения действительно, когда есть указывающий регистр! С помощью этих значений вы можете генерировать такие инструкции как XOR [SI], 1234, но не XOR SI, 1234!

Другое важно замечание: первый байт - это 81, вне зависимости от того, какую инструкцию выбирает движок! Это не очень хорошо, потому что идея полиморфного движка состоит в том, чтобы не было постоянных байтов в декрипторе. Впрочем, пока мы будем игнорировать это обстоятельство и попытаемся создать рабочий движок.

Окей, возвращаемся обратно к нашей программе. Давайте предположим, что движок выбрал ADD в качестве шифрующей операции. Мы просто сохраняем 81h, а затем кодируем правильный регистр. Ранее мы кодировали в инструкции просто регистры, теперь мы должны закодировать регистры-указатели. Вот список:

Код (Text):
  1.  
  2.                 000b = 0h = [BX+SI]
  3.                 001b = 1h = [BX+DI]
  4.                 010b = 2h = [BP+SI]
  5.                 011b = 3h = [BP+DI]
  6.                 100b = 4h = [SI]
  7.                 101b = 5h = [DI]
  8.                 110b = 6h = [immediate(число)]
  9.                 111b = 7h = [BX]

Вы можете видеть возможные улучшения: адресация, используя два регистра. Например, установите BX в ноль, а затем сделайте SI регистром указателем, но вместо просто [SI] используйте [BX+SI], указывающие на нужный байт/слово. Больше случайности!

Окей, я думаю, что это довольно простой. Примените 'OR 30h' на конкретном регистре, сохраните байт и случайное 16-ти битное число. Не забудьте запомнить, какую операцию и число использовал движок. Большинство движков создают криптор вместе с декриптором, чтобы запустить первый, когда будет сгенерирован последний.

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

4.5 Увеличение

Теперь настало время декриптору увеличить значение регистра. Так как мы используем шфирование по словам, регистр необходимо увеличить на два. Очевидно, что мы можем использовать: два INC, 'ADD reg, 2' или даже 'SUB reg, -2'. Но также подумайте о строковых операциях, таких как SCAS, CMPS, STOS, LODS и MOVS. Они выполняют определенную операцию, а затем увеличивают значение SI, DI или обоих. Мы можем использовать это свойство, но будьте внимательны с этими инструкциями, так как, например, MOVS может привести к нежелательным результатам. Я думаю, что вы сможете найти нужные кодируемые значения этих инструкций. Чтобы не слишком удлинять данный туториал, я объясню только как использовать два INC'а, так как остальное вы сможете выяснить самостоятельно.

Базовое шестнадцатиричное значение INC равно 40h, вам следует кодировать его с обычными кодируемыми значениями регистров (не значениями регистров-указателей), потому что это будет шифрующая операция! (Сравните INC BX и INC [BX]) ), чтобы получить необходимый результат. Например 'INC SI' будет кодироваться с помощью 'OR 40, 6', где 6 - это кодируемое значение SI. Надеюсь, теперь все понятно :smile3:.

4.6 Сравнение

Конечно, декриптор должен знать, какое смещение он должен расшифровывать. В этом примере единственная возможно сделать это - инструкция CMP, что не очень хорошо, так как вводит в декриптор стабильный байт. Другим путем может стать использование SUB, но во время вычитания из регистра, но тогда вы не сможете использовать его в следующий проход цикла. Правда, вы можете помещать конечное смещение в подсобный регистр во время каждого прохода и вычитать его из основного регистра (используемого для расшифровки). Это всего лишь идея.

Для данного примера мы выберем CMP. Базовое значение 'CMP reg16, imm16' равно 81F8h. Мы сохраняем 81h, ORим F8h с кодируемым значением регистра (не со значением регистра как указателя и сохраняем результат.

После этого мы должны сохранить imm16. Это последний байт/слово, который нужно расшифровать. Как мы можем вычислить это значение? Довольно просто: стартовое смещение + длина кода. Последняя передается движку через CX, а стартовое смещение можно посчитать так: смещение в памяти + длина декриптора. Смещение в памяти опять-таки передавалось движку в качестве параметра через AX, а посчитать длину декриптора, думаю, будет не очень сложно. Если ваши декрипторы всегда одной длины, это легко, но если они изменяются в длине раз от раза, вам следует отслеживать, сколько байтов вы сохранили. Поэтому:

Код (Text):
  1.  
  2.                  AX                   длина                  CX
  3.  /---------------+---------------\ /----+------\ /-----------+-----------\|
  4. ---------------------------------------------------------------------------
  5. і          Носитель               і  Декриптор  і      Тело вируса        і
  6. ---------------------------------------------------------------------------
  7.                                                последний байт/слово ------/
  8.  
  9.         AX + длина декриптора + CX = CMP operand
  10.  
  11.         Достаточно легко, хех. Сложите значения и сохраните их как операнд.

4.7 Переход

После CMP идет условный переход. Очевидным вариантом представляется JB, но это снова даст нам стабильный байт, так как у нас нет для него замены. Да, есть JNA, но проблема в том, что у них одинаковые опкоды, что не слишком удивительно, учитывая, что делают они одно и то же. Что насчет JNZ? Интересно, но только если бы мы добавляли к операнду CMP один/два! В противном случае последний байт/слово не будет расшифрован! Я предполагаю, что мы используем только JB (обратите внимание, что если вы выбрали использовали использование инструкции JNZ, следующее объяснение тоже некоторым образом относится к ней.

Если условие выполнено (условие = мы ниже последнего байта/слова), должен быть выполнен переход обратно на первую декриптующую операцию. Как мы можем закодировать это? Во-первых, гексовое значение JB равно 72h, поэтому мы должны его сохранить. Как вы знаете из заражения COM, у таких переходов байт с относительным смещением идет прямо за опкодом инструкции. В этом байте находится количество байтов, на которое CPU должен сдвинуть IP, чтобы началось выполнение требуемой инструкции. Вот пример:

Код (Text):
  1.  
  2. 0BF9:03B6   81 37 64 23         XOR [BX],2364
  3. 0BF9:03BA   (...)               (...)
  4. 0BF9:03C6   81 FB A4 06         CMP BX,06A4
  5. 0BF9:03CA   72 EA               JB 03B6
  6. 0BF9:03CC   (...)               (...)

В этом примере первая расшифровывающая операция начинается со смещения 3B6. Следующая инструкция после перехода идет начинается с 03CC. Поэтому правильное относительное смещение, которое нужно задать в инструкции JB можно высчитать следующим образом:

Код (Text):
  1.  
  2.       Новое смещение  -  Смещение следующей инструкции  =  Относительное смещ.
  3.           3B6         -           3CC                   =     FFEA

Как вы можете видеть, в качестве относительного смещения мы получили FFEAh. Мы можем использовать только один байт, поэтому это будет EAh. Как правило, вы будете исходить из того, что EAh = 234d, но также можно сказать, что EAh = -16d, и это как раз то количнство, на которое должен сдвинуть IP CPU. Обратите внимание, что вам не надо заботиться о таких вещах, как смещение в памяти, потому что относительное смещение будет всегда однима и тем же, если не меняется количество байт, на которое нужно сместить IP.

5. Шифрование

Здесь мы заканчиваем с циклом расшифровки. Я говорил вам, что вам движок должен "запоминать", что он делает. Звучит хорошо, но вам, вероятно, интересно, как это реализовать. Во-первых, будет мудро включить в таблицу операций обратные расшифровывающим. Т.е. SUB для ADD, ADD для SUB и XOR для XOR. Таким образом мы сможем сохранять расшифровывающую операцию в декрипторе и шифрующую операцию в крипторе. Обратите внимание, что обратная операция первой инструкции декриптора - это последняя инструкция криптора.

6. Рандомизация

В этом туториале я уже говорил, как важно, чтобы ваш движок умел случайным образом генерировать различные элементы декриптора. Большинство языков высокого уровня имеют функцию, которая возвратит нам случайное значение, но, как это обычно и бывает в ассемблере, в нашем случае мы должны полагаться сами на себя (небольшая поправка: при программировании под Windows можно использовать msvcrt.dll, которая входит во все Windows, начиная с 95; в данной библиотеке содержаться функции, входящие в стандарт C, среди них есть и функции для получения псевдослучайных значений - прим.пер.).

Так как компьютер не может возвратить полностью случайное значение (так как он не может этого сделать), мы должны написать процедуру, которая будет давать как можно более случайное значение. Наиболее часто используемый путь получить случайное значение - это читать из порта 40h. Так как это значение не является по-настоящему случайным, будет мудрым подвергнуть его нескольким математическим операциями, чтобы рандомизировать его еще больше. Другим путем может являться использование для получения случайного значения системного времени или даты. Между прочим, если вы грамотно используете их, вы получите форму медленного полиморфизма.

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

Как только вы получили случайное значение и хотети использовать его, чтобы, например, решить, какую операцию использвоать, вы должны проANDить его со смещение последнего значения такблицы (AND гарантирует, что в результате будут установлены только те биты, которые были уставлены в обоих операндах). Таким образом, если у вас выбор из 5 операций, ANDите случайное значение с 4, чтобы добавить получившийся результат к началу таблицу. Не ANDите с 5, так как начальное смещение + 5 находится за пределами таблицы (она начинается с 0). Поэтому, если у вас есть X значений, ANDите случайное значение с X-1.

Здесь есть одна проблема, на которую часто не обращают внимание. Возьмите ваш калькулятор с функцие hex2bin и начните ANDить случайные значения с 4h. Скажем, если вы сделаете это 100 раз, в качестве результата вы будете получать только 4 или 0. Вы никогда не получите 1, 2 или 3. Хмм... странно... Давайте попробуем 5h. Выполните 'AND xx, 5' со 100 значениями, и вы получите 0, 1, 4 и 5. Но никогда 2 или 3. Чтобы понять в чем заключается проблема, мы должны рассмотреть значения, над которыми мы выполняем AND в двоичной системе (на уровне битов):

Код (Text):
  1.  
  2.         0h = 00000000b
  3.         1h = 00000001b
  4.         2h = 00000010b
  5.         3h = 00000011b
  6.         4h = 00000100b
  7.         5h = 00000101b
  8.         6h = 00000110b
  9.         7h = 00000111b

Во время ANDинга значение биты в результирующем байте могут быть равны 1, только если они равны 1 в операнде, с которым мы ANDим. Скажем, вы ANDите с 5h и хотите получить 3h в качестве результат. 3h - это 10b, что означает что в операнде, который мы используем, должен быть установлен бит 1. Поэтому если мы ANDим с 5h (101b), бит 1 никогда не будет установлен, а значит вы никогда не получите 3h в качестве результата. Снова замечу, так как это важно, что если вы выберете игнорирование данной проблемы, некоторые фичи вашего движка останутся неиспользованными!

Я надеюсь, что вы уже поняли, что нужно ANDить со значениями, в которых установлены все (необходимые) биты, например, с 11b, 111b, 1111b и так далее.

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

7. Заметки на полях

Еще одно важное дополнение. При работе над собственным движком, вы вероятно будете планировать добавить несколько хороших фич. И когда вы включите в ваш движок поддержку дополнительных инструкций, вам понадобятся их опкоды. Если у вас в фоновом режиме не запущен Softice, использование DOS DEBUG будет, вероятно, довольно утомительным. Никогда не делайте этого... Есть кое-что очень странное в инструкциях 80x86, для чего у меня нет объяснения: некоторые инструкции могут кодироваться по-разному. Например, инструкция "OR AX, AX" кодируется как "09C0", но "0BC0" даст нам ту же самую инструкцию.

Все 'официальные' ассемблеры, такие как Borland'овский TASM, Mircosoft'овский MASM и все остальные выбирают один и тот же варианты, условно называемый "наивысшим" (например, "0BC0" при "OR AX, AX"). Другими словами, если определенная инструкция кодируется так, как она никогда бы не была закодирвана обычными ассемблерами, то очень похоже, что это работа полиморфного движка. Некоторые (или большинство?) антивирусных программ знают об этом и выдают предупреждение, если находят подобную инструкцию. Как бы то ни было, я рекомендую использовать SoftIce (хотя в его ассемблере есть кое-какие ошибки), но если вы в силу какой-то странной причины не можете сделать этого, используйте Borland'овский TurboDebugger, чтобы узнавать соответствующие инструкциям опкоды. Я думаю, что для этого он тоже подойдет.

Я хочу заметить, что популярный до сих пор (боюсь, что ко времени перевода данной статьи это утверждение уже не актуально - прим. пер.) условно-бесплатный ассемблер A86 имеет неприятный побочный эффект. Иногда он выбирает "неверный" из двух возможных опкодов инструкции, из-за чего некоторые антивирусы бьют тревогу. Это можно избежать, используя TASM.

8. Мусор

Мусорные инструкции, включаемые в декриптор, не выполняют никаких "реальных" действий и не оказывают никакого влияния на выполнение декриптора. Их единственной задачей является скрыть назначение декриптора. Без мусора декриптор выглядит как декриптор, что делает очень простой задачей обнаружить его эвричтически. Более того, мусорные инструкции позволят "настоящим" инструкциям находиться в случайных местах (в постоянном порядке, разумеется). В одном поколении операции inc и dec находятся непосредственно рядом друг с другм, в то время как в другом поколении между ними окажется какая-то (ничего не делающая) инструкция.

Классическая концепция мусорных инструкций предусматривает включение в декриптор однобайтовых инструкций, таких как NOP, CLC, STC, INT 3 (единственная однобайтовое прерывание (CCh)). В наши дни использование таких инструкций тривиально, почти все AV-программы просто игнорируют такие инструкции, когда встречают цикл расшифровки.

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

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

К счастью, есть другие методы генерирования "мусорного кода". Главная идея, лежащая в их основе, заключается в генерации кода, который мог бы быть необходимым для выполнения программы кодом. Хороший пример состоит в вызове какого-нибудь функции int 21h, вроде получения версии и тому подобного. Здесь есть много возможностей. Каждый раз принимайте во внимание две вещи:

  • Какой бы мусор вы не использовали, убедитесь, что он не влияет на выполнение декриптора, а выглядит при этом как полезный код.
  • При использовании больших мусорных конструкций (например, подготовка к вызову int21), убедитесь, что у вас есть достаточное количество вариантов, иначе AV сможет сделать из них сканстроку прямо из вашего мусора!

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

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

Код (Text):
  1.  
  2. start:
  3.         call    maybe_insert_garbage
  4.         call    set_up_registers
  5.         call    maybe_insert_garbage
  6.         call    perform_move_instruction
  7.         call    maybe_insert_garbage
  8.         call    do_first_crypt_operation
  9.         call    maybe_insert_garbage
  10.         (...)

9. Множественные декрипторы

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

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

В данном туториале я попытался объяснить, как избежать постоянных значений, используя вариации на уровне инструкций. Если вы хотите усовершенствовать ваш движок вариациями на уровне процедур, вы должны использовать хорошо продуманный код. Движок должен решить, какую декрипторную схему (процедуру) он будет генерировать. Будет мудрым сделать некоторые функции не зависящими от схемы (например, функцию повышения значения регистра).

10. Заключение

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

Наконец, я хочу поблагодарить Cse/IRG за объяснение некоторых вещей, когда я только начал заниматься полиморфизмом, и PM, который был рад протестировать статью.

Trigger / SLAM '97 © Trigger/SLAM, пер. Aquila


0 1.048
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532