Техники выживания vx.exe в реальной среде


0. Вступление

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

Примечание: все примеры кода на asm; все ссылки на какие-либо упоминания в статье находятся в самом низу.


1. Начало

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

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

	---------------------------------------------------------------------------
		Invoke, OpenProcess ..
		Invoke, VirtualAllocEx ..
		Invoke, WriteProcessMemory ..
		Invoke, CreateRemoteThread ..
	---------------------------------------------------------------------------

* Вместо .. аргументы функций

Таблица импорта будет выглядеть примерно так:

		kernel32.dll
			- OpenProccess
			- Process32NextA
			- VirtualAllocEx
			- WriteProcessMemory
			- CreateRemoteThread
			- CreateToolhelp32Snapshot
			- ...
			- ExitProccess

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

Но, видимо, хороший человек с ником dx с http://kaimi.ru выложил на всеобщее обозрение макросы для вызова API не через IAT (Import Address Table), а другим хитро вы*банным и, опять же, довольно четким (по-пацански так) способом:

  • При помощи PEB - системной структуры окружения процесса, находим базу kernel32.dll в связном списке LDR.
  • * Парсим PE Header, в экспорте находим адрес GetProcAddress.
  • Используем ее, чтобы найти адрес LoadLibrary.

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

Теперь мы можем подгружать и юзать любые API, которые нам потребуются. Используем вышеупомянутый макрос для всех функций, которые могут выдать нашу основную задачу. Оставим, пожалуй, только ExitProcess для стандартного Invoke. Хоть единственная данная API в импорте, при наличии кода, не самое лучшие решение, поэтому попробуем сделать его более "реальным".


2. Генерация таблицы импорта

Есть 2 метода создания и использования:

  • 1) Генерация фиктивного импорта.
  • 2) Генерация псевдо-импорта.

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

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

О том, как собрать новую таблицу импорта, мы разговаривать не будем, т.к. инфу по этому можно найти в инете, особенно полно информации на tuts4you.com. Лучше поговорим о том, какие API мы будем использовать в нашем псевдо-импорте. Речь пойдет о так называемых "Безопасных API", о которых написал kaze в 2008 году, в своей статье "Stealth API-based decryptor", очень рекомендуется для чтения, т.к. имеет большое отношение к этой теме!! Само понятие Safe API означает, что если функция будет вызвана со случайными параметрами каждый раз, наша прога не упадет, а максимум вернет код ошибки.

Для поиска таких функций используется алгоритм (из перевода статьи на http://coru.ws):

  1. Получаем список всех API в системных DLL (kernel32, user32, advapi32, gdi32 и т.д.).
  2. Для каждой API находим количество параметров. Это легко можно узнать, вызвав API со, скажем, 20-ю параметрами и, проверив, сколько параметров извлеклось из стека. Это число является количеством параметров для API.
  3. Вызываем API много раз, каждый раз со случайными параметрами.
  4. Если не возникло исключительной ситуации, и если мы до сих пор не "упали", то эта API является безопасной.

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

Дальше, для использования этих функций мы можем поступить так:

  • Брать адреса функций прямо из IAT, свою базу берем из PEB.ImageBaseAddress, парсим PE.
  • Резервировать в нашей проге массив из ~25(количество Safe API) элементов DWORD и записывать туда виртуальное смещение наших API после генерации импорта. Прибавляем рандомный элемент массива к тому же PEB.ImageBaseAddress, чтобы получить адрес функи.

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

Вызов Safe API происходит по тому же способу, что и при поиске, только теперь мы точно знаем, что не упадем. Алгоритм генерации случайного числа для аргумента выбираем себе по душе.

        Mov _ESP, Esp	; сохраняем указатель на верхушку стека
	Mov Ecx, 16 	; количество аргументов
	@@:
		Call Get_Random_Dword
		push Eax
	loop @F
	Call SafeAPI;	; вызываем API
 
	Mov Eax, Esp	; текущий адрес верхушки стека в Eax
	Mov Ebx, _ESP	; прежний адрес в Ebx
	Sub Ebx, Eax	; выравниваем то, что осталось
	Add Esp, Ebx	; восстанавливаем

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


3. Изменение кода

Когда мы говорили о принципе действия макроса для вызова API не через IAT, обсуждался момент, что имя функции статично, и что это должно быть изменено. Хотя все это, конечно, пока полная ерунда, т.к. код макроса и наш код являются точно такими же. Это же простой сигнатурный рай для аверов!!

Существуют следующие техники для изменения кода:

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

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

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

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

  • Отсутствие логики. На эту тему есть уже давно заезженная статья от Pr0mix.
  • Много однотипных команд. Чересчур много. (В каждом участке кода тоже самое, отсюда повышение гистограммы определенных опкодов в секции кода, что, ИМХО, будет палевом для аверов).
  • Использования одного кривого полиморфного движка в разных проектах загубит все (Кэп).

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

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

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

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

	-----------------------------------------------------------------------------------------
		004016FB > $ EB 08          JMP SHORT Loader.00401705
		004016FD     EB             DB EB
		004016FE   > BE 10164000    MOV ESI,Loader.00401610
		00401703   . EB 03          JMP SHORT Loader.00401708
		00401705   >^EB F7          JMP SHORT Loader.004016FE
		00401707     74             DB 74                                    ;  CHAR 't'
		00401708   > EB 05          JMP SHORT Loader.0040170F
		0040170A     EB             DB EB
		0040170B   > 89F7           MOV EDI,ESI
		0040170D   . EB 03          JMP SHORT Loader.00401712
		0040170F   >^EB FA          JMP SHORT Loader.0040170B
		00401711     74             DB 74                                    ;  CHAR 't'
		00401712   > EB 05          JMP SHORT Loader.00401719
		00401714     75             DB 75                                    ;  CHAR 'u'
		00401715   > 29C0           SUB EAX,EAX
		00401717   . EB 04          JMP SHORT Loader.0040171D
		00401719   >^EB FA          JMP SHORT Loader.00401715
	-----------------------------------------------------------------------------------------

Что является просто говном, по-другому это никак не назвать!! Толку от этого не будет, обьясню почему:

  • Во-первых, очень много опкодов JMP на маленьком участке кода (50-60% точно) - это хорошо будет видно по гистограмме (детект).
  • Того не юзаем один jmp, а добавляем к нему еще все возможные операторы перехода: JE, JZ, JNE. Допустим, гистограмма снижается всетаки до оптимального значения, но здесь уже в силу вступает другая аверская техника, описанная z0mbie еще лет 10 назад. Заключается она в том, что копирует весь подозрительный участок кода в буфер и удаляет оттуда все операторы перехода с NOP, считает хеш, и выдает все тот же, злосчастный для нас, детект (а скорее для тех, кто юзает подобный говно-код).

И даже если добавить немного мусорного кода, менять команды на аналогичные, это все равно приведет к детекту. Так что такой вариант защитить нашу программу сразу выдаст вас с потрохами! Не задумывайтесь даже об этом.

Взглянем на эту технику координально с другой стороны, которая всетаки имеет право на жизнь! Начнем с того, что забудем про JMP и перестанем менять местами по одной кострукции. Возьмем лучше наш код (можно уже видоизмененный полиморфом) и поделим его на "кусочки" размером в 15-20 команд. Очень желательно, если в них будут использоваться API. Обязательным правилом будет, чтобы в конце каждого "кусочка" присутствовала команда "RET". Дальше, просто случайным образом располагаем наши куски кода в разном порядке и заполняем какую-нибудь табличку при компиляции. Можно в виде массива, в котором индекс будет играть роль порядкового номера, каким по очереди этот код выполнять, а значение - адрес, где расположен этот кусок. Дальше - все просто дело техники: переносим весь массив в стэк и передаем на него управление при помощи все того же "RET". Как бы мы не располагали наш код, он будет выполнен, ровно в той последовательности, что и нужно, причем само использование этой команды делает передачу управления неявным, и при грамотной подаче - заранее неизвестным.

Здесь может послужить один из самых простых трюков, который может быть использован для запутывания эмуля авера:

	call some_API		; вызываем любую API, которая возвратит заранее известное значение 
	Add/Xor [Esp], Eax	; модифицируем наш адрес в цепочке, на основе результата API
	ret			; передадим управление

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


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

Здесь я обсудил, к сожалению, еще далеко не всё, что могло бы помочь нам утирать нос аверам. В стороне осталось как самое простейшее: зашифровка кода и данных, так и темы: анти-эмуляция, анти-отладка, проверка целостности кода и т.п, что является также не менее интересным. Но здесь написано лишь то, что было у меня в голове на момент написания статьи, если бы меня спросили "Что бы я сейчас хотел рассказать начинающим?".
Так что, может быть, кто-нибудь продолжит идею и напишет статьи на оставшиеся темы. Удачи всем, во всех начинаниях и продолжениях!


Links:


______________________________
Fairhawk aka ProudPank
2013

Inception E-Zine