Содержание статьи
Наш сегодняшний разбор будет посвящен полиморфному генератору инструкций, применяемому в известном вирусе Virus.Win32.Sality.aa. Он позволяет получать различный обфусцированный код с применением FakeAPI при каждом его использовании.
Но и это еще не все. Если честно, на эту статью у нас большие планы, ведь в ней мы рассмотрим тонкости формирования x86-инструкций – префиксы, опокды, поля ModRM, SIB, и опишем признаки, по которым можно отличить обфусцированный и потенциально зловредный код от стандартного, сгенерированного обычным компилятором. Также не останутся без внимания общие схемы работы обфускатора и непосредственно генератора кода.
Для начала, рассмотрим сам вирус. Самая первая модификация Virus.Win32.Sality – «a» – появилась еще в 2003 году, а одна из последних, модификация «aa» – в июле 2008 года. В течение пяти лет было выпущено немало версий, но в них было мало качественных изменений и они не получили широкого распространения. А вот экземпляр Virus.Win32.Sality.aa получился на редкость «удачным» – он отлично распространяется и обходит антивирусы. С момента своего выпуска он занял лидирующие позиции в рейтингах вирусной активности и остается в них вплоть до настоящего времени. Основной его функционал – заражение PE-файлов, распространение с помощью съемных носителей, скачивание и установка BackDoor’а.
Именно этот зловред является отличным примером для разбора. Он в полной мере полиморфен, код его сильно и качественно обфусцирован, а также снабжен антиэмуляцией на основе вызова API-функций. Полиморфизм отлично проявляется при заражении файлов – каждый раз генерируется совершенно разный код, но, в целом, он обладает идентичной функциональностью, при этом в файле присутствует статичная область кода, которая зашифрована, опять-таки, всегда по-разному.
Он в полной мере полиморфен, код его сильно и качественно обфусцирован, а также снабжен антиэмуляцией на основе вызова API–функций
Обфускация реализована практически идеально – то, что можно исполнить с помощью четырех–шести инструкций, растянуто на 0x300–0x400 байт кода. Статический анализ дизассемблерного листинга сопряжен, по понятным причинам, со значительными трудностями, а применение FakeApi позволяет легко обойти некоторые эмуляторы. Конечно, существуют и другие вирусы с аналогичной «начинкой», но большинство из них являются так называемыми PoC (Proof of Concept) и не получили широко распространения. Отличным примером такой модели является EPO (Entry Point Obscuring) – вирус Virus.Win32.Zombie.
Ликбез: полиморфизм и обфускация
Полиморфизм – возможность объектов с одинаковой спецификацией иметь различную реализацию. Гружено? Согласен! Приведем простой пример. Допустим, нам нужно создать код, который заполняет регистр EAX значением «0». Возможно множество реализаций:
PUSH 0; POP EAX;
XOR EAX, EAX;
MOV EAX, 0;
AND EAX, 0;
В этом и заключается полиморфизм – сделать одно и то же, только разными способами.
Обфускация – приведение исходного текста или исполняемого кода программы к виду, сохраняющему ее функциональность, но затрудняющему анализ, понимание алгоритмов работы и модификацию при декомпиляции. Конкретный пример:
PUSHAD; NOP; NOP; NOP; NOP; POPAD;
ADD EAX, 0xFFEEFFEE; INC EAX; OR EAX, EAX; SUB EAX, 0xFFEEFFEF;
PUSH EBX; PUSH ECX; POP ECX; POP EBX; LEA EAX, [EAX]; MOV EDX, EDX;
Все три последовательности инструкций не несут никакой полезной нагрузки, но вполне могут быть использованы, чтобы «замусорить» полезный код.
Теперь о самой технике заражения. Весь процесс показан на схеме. Sality заменяет оригинальный код приложения, расположенный по точке входа, на свой собственный, сгенерированный «на лету». Подробнее о нем будет написано ниже. Также в начальный файл добавляется дополнительная секция с полиморфным декриптором, который во время исполнения расшифровывает и исполняет «полезную нагрузку» вируса. Генератор в обоих случаях используется одинаковый, вследствие чего при беглом просмотре код получается практически идентичным. Расположение самой точки входа остается неизменным. Атрибуты последней секции задаются таким образом, чтобы в ней можно было читать, писать и исполнять. Оверлей исходного файла «отодвигается» в конец. Таким образом, если инсталлятор с данными в оверлее подвергнется заражению, то его функциональность сохранится.
Схема заражения PE-файла вирусом Virus.Win32.Sality.aa
Перейдем непосредственно к генератору. В его задачу входит формирование кода с заданными свойствами, определенного размера и выполняющего заданный псевдокод. Так каким же образом работает генератор? В общем случае алгоритм выглядит следующим образом:
Схема, отражающая алгоритм работы полиморфного генератора
Поясним приведенную схему. На самом начальном этапе генератор получает на вход так называемый псевдокод, который содержит логику исполняемого фрагмента. Псевдокод представляет собой совокупность команд, отражающих определенную функциональность и понятных для человека. Далее происходит компиляция, причем полученный машинный код получается каждый раз разными способами. В этом и заключается полиморфизм. После этого машинный код подвергается обфускации, опять-таки с использованием генератора случайных чисел. И начатое дело завершается добавлением одной или нескольких фэйковых API-функций в код. Каждый этап по-своему интересен и будет рассмотрен довольно подробно прямо сейчас.
Выше было написано, что в Sality формируется две области, содержащие сгенерированный код. Они располагаются по точке входа и в начале новой секции, добавленной вирусом. Вся работа генератора будет пояснена на первом участке. Второй обладает богатой функциональностью, в отличие от первого, и будет слишком сложным для разбора. Итак, на схеме этот фрагмент называется «стартовый код вируса». Псевдокод стартового участка вируса довольно прост. Вот его логика:
- Сохранить оригинальные значения регистров и флагов;
- Получить VA текущего;
- Вычислить VA перехода;
- Выполнить переход по вычисленному VA.
Третий и четвертый этапы, в отличие от первого и второго, реализуются в Sality множеством способов. В последних используются соответственно инструкции PUSHAD и CALL. Вызов PUSHAD нужен, чтобы сохранить исходное состояние процессора, и чтобы перед тем, как передать управление оригинальной программе, можно было вызвать инструкцию POPAD, восстанавливающую оригинальные значения регистров. А вызов команды CALL, помимо выполнения перехода, кладет в стек виртуальный адрес (VA) следующей инструкции. Для вычисления адреса перехода используется значение, полученное на втором этапе. Способов вычисления очень много. Приведем некоторые из них (будем считать, что адрес, полученный на втором этапе, располагается на вершине стека):
POP REG;
SUB REG, IMM;
MOV REG, [ESP];
ADD REG, IMM;
ADD [ESP], IMM;
POP REG;
В приведенных выше примерах и далее по тексту под REG подразумевается любой 32-битный регистр общего назначения, а под IMM – числовое значение любой размерности. Самый последний этап (четвертый) также может быть выполнен по-разному (считаем, что в REG находится необходимый Sality адрес назначения):
JMP REG;
CALL REG;
PUSH REG;
RETN;
Совмещая все возможные варианты каждой из операций, можно получить довольно большое количество исполнений такого простого алгоритма. Однако без применения обфускации назначение полученного кода весьма очевидно при просмотре дизассемблерного листинга, чего не скажешь об «обработанном» коде. Приведен полный участок стартового кода вируса, начиная с точки входа зараженного файла:
Стартовый код Virus.Win32.Sality.aa в зараженном файле
Теперь перейдем к обзору обфускации. Как видно из дизассемблерных листингов, представленных на иллюстрациях, большинство инструкций не несут полезной нагрузки, а добавлены только для того, чтобы усложнить анализ. Красными овалами выделены инструкции, соответствующие всем этапам псевдокода. В данном файле вычисление адреса для перехода реализуется аж семью командами.
Сам обфускатор работает в несколько этапов, получая на входе определенные параметры, задающие возможный вид получаемой команды, а на выходе – готовую инструкцию:
- генерация опкода из списка (одно- или двух- байтовый опкод);
- генерация байта ModRM;
- добавление произвольного числового операнда;
- добавление префикса к полученной инструкции.
Этапы 2 – 4 опираются на результат первого. Так, не для всех опкодов существует байт ModRM и числовой операнд. Не к каждой инструкции можно безболезненно добавить префикс. Генератор работает не полностью случайно. У него есть определенный список опкодов, которые он может использовать и, соответственно, возможные варианты байта ModRM. Генератор, например, не устанавливает поле Mod равным 3 для инструкции LEA. Для того, чтобы объяснить назначение всех упомянутых полей и само устройство элементарной инструкции процессора x86, обратимся к первоисточнику – документации Intel, а именно – «IA-32 Intel® Architecture Software Developer’s Manual», часть 2А.
Формат инструкции архитектуры x86
Итак, каждая инструкция обязана включать в себя опкод. Он может быть как однобайтовый, так и двух- и трехбайтовый. Если первый байт опкода равен 0Fh, то он является двухбайтовым. Для трехбайтовых возможно несколько вариантов, но это не будет затронуто в статье. Каждый опкод обладает определенными свойствами. Например, 0B8h – MOV eAX, lz, использует следующие четыре байта, добавляя их к инструкции. Таким образом получается, что последовательность байт B8 FF FF FF FF эквивалентна инструкции MOV EAX, 0FFFFFFFFh. Существуют опкоды, которые сами по себе образуют инструкцию, например, PUSH EBP – 55h, PUSHAD – 60h. А есть такие опкоды, в которых следующий байт несет определенный смысл и называется ModRM.
Этот байт состоит из трех полей, показанных на схеме: Mod, Reg/Opcode, R/M. С помощью Mod’а определяется тип операнда, используемого инструкцией – память или регистр. Если его значение равно 3, то это регистр, а остальные возможные – память. Основное назначение R/M и Reg/Opcode – указывать на операнды инструкции. А для некоторых опкодов, например, 0x80, 0x81, 0xC1, поле Reg/Opcode указывает на саму операцию, выполняемую инструкцией. Если в инструкции поле Mod не равно 3, и в то же время поле R/M равно 4, то появляется еще один байт – SIB. Он также показан на схеме. Он используется для формирования инструкций, работающих с составными операндами памяти. Например: ADD EAX, [EAX + ECX*4 + 600].
Еще одним необязательным элементом, образующим инструкцию является префикс. Каждый префикс занимает один байт. Но у команды может быть сразу несколько префиксов. Они используются для придания инструкции определенных свойств. Всего их одиннадцать:
F0 – Lock; F2 – REPNE; F3 – REP; 2E – CS segment override; 36 – SS - // - // -; 3E – DS - // - // -; 26 – ES - // - // -; 64 – FS - // - // -; 65 – GS - // - // -; 66 – Operand – Size; 67 – Address Size;
Согласно документации Intel каждый префикс может использоваться только в определенных целях, хотя, безусловно, их можно вручную добавить к произвольной команде. Все сегментные префиксы используются совместно с инструкциями, работающими с памятью, для обращения к определенному сегменту. А байты REPNE/REP добавляются только к инструкциям, работающим со строками или с массивом байт, к примеру – SCASB, MOVSD, LODSW. Префиксы размеров операнда и адреса изменяют соответственно их размеры на меньший или больший, в зависимости от режима работы.
Так что же использует генератор Sality? Во-первых, он использует однобайтовые опкоды, целиком образующие инструкцию. К ним относятся все PUSH’ы и POP’ы. Во–вторых, во всех генерируемых инструкциях поле Mod равно 3, чтобы избежать взаимодействия с памятью, а оперировать только с другими регистрами или мгновенными значениями. Исключение составляет команда LEA, в которой Mod всегда равен 2, а использование 3 приведет к генерации исключения. В-третьих, используется множество двухбайтовых опкодов, которые опять-таки не оперируют с памятью: SHLD, BSF, BTS, BTC, XADD и др. И напоследок – Sality добавляет байт 66h, а также сегментные и REPxx префиксы к произвольным инструкциям. С помощью всех этих методов получается качественный обфусцированный код, который не взаимодействует с памятью, но при этом значительно усложняет анализ кода.
А теперь можно обсудить и те моменты, которые позволяют отличить сгенерированный код от кода, полученного компилятором. Начнем с префиксов. Генератор в Sality добавляет их к совершенно произвольным инструкциям. А в документации Intel написано следующее:
«Repeat prefixes (F2H, F3H) cause an instruction to be repeated for each element of a string. Use these prefixes only with string instructions (MOVS, CMPS, SCAS, LODS, STOS, INS, and OUTS). Use of repeat prefixes and/or undefined opcodes with other IA-32 instructions is reserved; such use may cause unpredictable behavior».
Таким образом, использование префиксов F2 и F2 возможно лишь с ограниченным списком инструкций, а в приведенном выше примере кода эти байты находятся перед командами CALL, SHL, TEST и прочими. Аналогичная ситуация и с сегментными префиксами. Их использование не по назначению может носить неопределенный характер. А в Sality байт 64h может без проблем предварять инструкцию CALL. Компилятор при генерации кода никогда не будет добавлять префиксы к тем командам, к которым это запрещено делать!
На что еще следует обратить внимание при просмотре кода? Например, на практически полное отсутствие работы с памятью, а значит, и отсутствие локальных и глобальных переменных. Это весьма странно, так как компилятор может использовать только регистры в коротких функциях, и то при определенных условиях. Еще одна особенность полученного кода – совершенно произвольные числовые операнды. Такое, конечно, иногда бывает, когда реализуется какая–то функция из криптоалгоритма, но даже в ней нет такого обилия случайных чисел.
Переходим к самому последнему этапу генерации кода – добавление FakeAPI. Этот метод используется для обхода простых эмуляторов или виртуальных машин. А заключается он в вызове произвольной API – функции, в надежде на то, что эмулятор не поддерживает работу с таблицей импортов или не может реализовать корректное исполнение. В представленном дизассемблерном листинге в таком качестве используются две функции – FindClose и GetModuleHandleA. В представленном случае очень легко отличить фэйковые запуски от реальных вызовов. Во-первых, результат их работы – регистр EAX или его составные части, впоследствии просто стираются и не используются, что довольно странно для нормальной программы. Во-вторых, функции FindClose должен передаваться корректный хэндл, полученный ранее с помощью FindFirstFile. А в приведенном файле происходит исполнение FindClose (0) прямо с точки входа, что явно не может иметь место в нормальном файле. Генератор, при добавлении FakeApi в конечный код, пользуется определенным списком, в котором перечислены API-функции, которые получают на вход только один параметр и не вносят никаких значительных изменений в систему.
Заключение
Итак, в этой статье мы рассмотрели все этапы работы полиморфного генератора. Как оказалось, в Sality очень эффективно используется полиморфизм и обфускация. В нем присутствует большое количество инструкций, которые не несут полезной нагрузки, значительно усложняют статический анализ и практически полностью «размывают» полезную нагрузку вируса. Добавление FakeAPI также добавляет исследователю головной боли. Но, в то же время, стоит отметить и тот факт, что применение всех этих «примочек» значительно упрощает определение вредоносного кода. Так, при должном опыте, достаточно только одного взгляда, чтобы определить, что перед тобой находится зловред.