+-----------------------------------------------------------------------------------------------------------------------------------------------[CP #2]----+
|
|
|
|
|
|
|
  _|_| _|_|         _|  _|    _|_|_|
_|     _|  _|     _|_|_|_|_|  _|  _|
_| ODE _|_|         _|  _|      _|  
_|     _| IMPS    _|_|_|_|_|  _|    
    _|_| _|           _|  _|    _|_|_|  
|
|
|
|
|
|
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------+
+----------------------[2x0a Полиморфные алгоритмы 0x00: Концепция (/home/cp2/programming/poly)]--------------------+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

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

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

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

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

Предлагаю рассмотреть несколько простейших инвариантных примеров:
...   ...   ...
mov AX,3; xor AX,AX;  xor AX,AX;
add AX,5; mov AX,8;   add AX,8;
...     ...   ...
Все 3 этих примера (на самом деле, даже для такого простого участка кода, число инвариантных примеров гораздо больше) выполняют одно и то же действие - mov AX,8. Но 2й и 3й примеры имеют длину скомпилированного кода (машинного кода) 5 байт, а 1й - 6 байт.

1й пример:
B8 03 00 05 05 00

2й пример:
33 C0 B8 08 00

3й пример:
33 C0 05 08 00

Т.о. сразу же можно безболезненно использовать вместо 2го фрагмента кода 3й и наоборот. 1й фрагмент можно менять на 2й и 3й, не забыв дописать команду NOP (90h). Обратная замена без коррекции всех меняющихся адресов перехода невоможна. Так же стоит помнить, что B8 может встречаться в тексте программы как mov и как байт со значением B8 (то же самое и для всех остальных операций). Т.е. мы должны научиться различать операции и операнды.

Самая простая реализация задачи распознавания заключается в однопроходном просмотре преобразуемого кода. Для еще большего упрощения поставленной задачи мы будем оперировать над com-файлом. В таком случае первые n байт всегда обозначает операцию (n зависит от типа операции, например 0F 00 означает LTR, а 24 - AND AL,imm8), а далее, по заранее составленной таблице, узнаем количество байт-операндов. Естественно, такой метод применим только для несжатых незашифрованных исполняемых файлов.

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

В итоге мы имеем таблицу:
 _______________________
| addr 1 (program in)   |
| addr 2 (in or out)    |
| ...                   |
| ...                   |
| addr n (program out)  |
|_______________________|
Между указателями на "in"-"in" и "in"-"out" и находится код, который нужно преобразовать. Понятно, что между "out"-"in" может находиться какая-то константа, а вариант "out"-"out" можно считать запрещенным, т.к. ни один из компиляторов языков высокого уровня не должен допустить подобного, при условии грамотно написанных разработчиком среды процедур оптимизации кода.

Теоретически вариантов замены для конкретного фрагмента кода может быть бесконечно много. Это можно доказать так: mov ax,3 можно заменить на mov ax,3; sub ax; add ax; и повторять последние 2 операции произвольное количество раз. Но можно указать разумные пределы разрастания или сжатия кода. Поэтому будем считать, что каждый фрагмент кода можно удлиннить не более, чем на 2 байта. Все эти допущения нам пригодятся, когда мы будем писать полиморфный преобразователь исполняемых файлов.

Все необходимые приготовления сделаны. Дело осталось за реальным кодом, который будем писать и оптимизировать в следующих статьях.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+-----[content]-----------------------------------------------------------------------------------------------------------------------------[mail us]-----+