Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

Умный мусор: построение логики

pr0mix
Electrical Ordered Freedom #3
Август 2011

1
[Вернуться к списку] [Комментарии]

Введение

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

Этот текст о том, как улучшить качество генерируемого мусора.

Кто противник

Допустим, что происходит проверка файла, заражённого нашим вирусом. Антивирус может действовать так:

От эмуля нам поможет рабочая антиэмуль, от сигнатур - генератор мусора, который (как оказывается xD) использует наш вирь. Но если созданный трэш-код окажется слабым, то нас накроет эвристика.

План наступления

Итак, для построения более качественного трэш-кода, вначале я предлагаю выбрать, под генерируемый код какого компилятора мы будем "косить": ms, borland etc. После того, как выбран компилер (например, ms), можно ещё определить, под какой режим генерации/оптимизации мы будем подстраиваться ("min size"/"max speed"). Это всё, конечно же, не обязательно, но желательно. Так как под разными режимами код генерируется по-другому. Например, для ms-компилера, команда занесения единицы в регистр в режиме "max speed" (в основном) будет такая:

        mov     eax, 1          ;0xB8 0x01 0x00 0x00 0x00
 

А для "min size"

        xor     eax, eax        ;0x33 0xC0
        inc     eax             ;0x40
 

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

Далее, помимо разных фичезов, которые вы встроите в трэшген, он также должен уметь генерировать "реалистичный" код (похожий на обычный код стандартных программ, написанных на ЯВУ), а именно:

Но даже такой трэш-код, построенный с учётом данных пунктов, может легко ловиться эвристикой.

Полезный мусор

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

  1. полезный код должен использовать результат работы трэш-кода (или наоборот, трэш-код должен как-либо повлиять на работу полезного кода) aka "псевдо-цель";
  2. "LOGICAL TRASH" technique;

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

        mov     eax, 100
        mov     ecx, eax
        sub     ecx, 95
 

После его выполнения ECX = 5. И данное значение можно добавить к ключу для расшифровки вирусного кода (применений куча). Однако, сгенерированный трэш-код может быть и таким:

        mov     eax, 100        ;1
        mov     ecx, eax        ;2
        mov     ecx, eax        ;3
        mov     ecx, eax        ;4
        sub     ecx, 95         ;5
 

После его выполения ECX также равно 5. Но команды 2 и 3 выдают себя с потрохами, за что будут наказаны эвристикой. Решение состоит в построении "логичного" мусора.

"LOGICAL TRASH" technique

Идея заключается в том, чтобы мусорный код сделать логичным, подобно логике кода обычных программ. Нормальный код вначале инициализирует параметры (регистры, локальные переменные etc); затем выполняет команды, использующие и/или как-либо влияющие на эти параметры. Причём команды являются одним целым - выполняют общую задачу, и среди них нет лишних - мусорных. Нет повторных инициализаций, использования и обращения к (значениям) неинициализированным параметрам. Все инструкции связаны друг с другом, каждая влияет на дальнейший ход выполнения кода.

Примерно такую логику я реализовал в новой версии своего движка xTG (v2.0.0), который работает по следующей схеме:

	 ________
	|	 |
	| начало |
	|________|
	    |
	    |
	    |
	    |					+--------------------------------------------+
	    |					|					     |
	    |					|					     |
	    |	 ________________________	|		 ________________________    |
	    |	|			 |	|		|			 |   |
	    |	| модуль генерации	 |	|		| модуль логики		 |   |
	    |	| команд		 |	|		|			 |   |
	    |	|			 |	|		|			 |   |
	    |	|			 |	|		|			 |   |
	    |	| ---------------------- | 	|		| ---------------------- |   |
	    +-->| генерация 		 |<-----+	   +--->| парсер команд	      	 |   |
	+------>| правильно		 |		   |    |			 |   |
	|	| построенной		 | адрес команды   |	| ---------------------- |   |
	|	| команды		 |-----------------+ 	| эмулятор команд	 |   |
	|	| 			 |  			| 			 |   |
	|	| ---------------------- |			| ---------------------- | 0 |
	|	| корректировка адреса	 |<----------------+	| анализатор команд/     |---+
	|     1	| для генерации новой	 |		   |  1 | корректор логики       |
	+-------| команды		 | 0		   +----| команд		 |
		|			 |------+		|			 |
		| ---------------------- |	|		| ---------------------- |
		|________________________|	|		|________________________|
						|
						|
	   					|			
	 ________				|
	|	 |				|
	| конец	 |<-----------------------------+
	|________|

Распишем подробно:

  1. вначале, конечно же, вызываем модуль генерации команд;
  2. генерируем "правильную" команду: правильные опкоды и остальные байты. Да, кстати, если разработанный двигатель логики (ДЛ) будет применяться к сторонним трэшгенам (или другим движкам), то ДЛ также должен проверять, правильно ли построена команда (aka проверка на уровне байтов);
  3. вызываем модуль логики, передавая в него адрес только что созданной команды;
  4. за дело принимается парсер команд. Парсер может быть функцей, являющейся частью модуля логики, а может быть и отдельным самодостаточным движком (дизасм). Первый случай хорошо подходит, если модуль логики является частью генератора мусора. Тогда в функции парсера будут разобраны только те команды, которые может генерировать двигл. Второй случай хорошо подходит, если модуль логики является самостоятельным движком. И при этом мы не знаем, какие команды могут генерироваться.

    Парсер выясняет, какая перед ним команда, и получает её параметры (операнды: регистры, адреса etc) - в соответствии с этим сохраняет в некоторую структуру данные параметры и выставляет определённые флаги. Заполненная структура будет использоваться анализатором команд (об этом ниже). Также, например, если встретилась команда mov ecx, dword ptr [403008h] и т.п., тогда парсер заменит адрес 403008h на другой, соответствующий ему адрес в выделенной памяти для корректной эмуляции команды;

  5. затем эмулируем (скорректированную) команду. Эмуль, по аналогии с парсером, может быть как встроенной функцией в модуле логики, так и полноценным движком-пирожком;

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

    Кстати, эмуль - козырная технология для вирей, с помощью которой можно творить очень интересные темы (для UEP'a, виртуальных машин, "logical trash" tech, морфинга и прочих вкусностей);

  6. и после вызываем анализатор команд/корректор логики. Анализатор, по аналогии с парсером и эмулем, может быть как встроенной функцией в модуле логике, так и полноценным двиглом;

    Анализатор, на основе данных от парсера (заполненная структура) и эмуля, решает, подходит ли команда по логике или нет.

    Анализ команды проходит в 2 этапа:

    1. Проверка параметров команды.

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

      LGC_INSTR_INIT  equ     00000000000000000000000000000001b       ;команда инициализации параметров;
      LGC_INSTR_CHG   equ     00000000000000000000000000000010b       ;команда изменения параметров;
      LGC_P1_DST      equ     00000000000000000000000000000100b       ;первый парам - приёмник
      LGC_P1_SRC      equ     00000000000000000000000000001000b       ;первый парам - источник
      LGC_P2_DST      equ     00000000000000000000000000010000b       ;второй парам - приёмник
      LGC_P2_SRC      equ     00000000000000000000000000100000b       ;второй парам - источник
      LGC_P1_REG      equ     00000000000000000000000001000000b       ;первый парам - регистр
      LGC_P1_ADDR     equ     00000000000000000000000010000000b       ;первый парам - адрес
      LGC_P1_NUM      equ     00000000000000000000000100000000b       ;первый парам - число
      LGC_P2_REG      equ     00000000000000000000001000000000b       ;второй парам - регистр
      LGC_P2_ADDR     equ     00000000000000000000010000000000b       ;второй парам - адрес
      LGC_P2_NUM      equ     00000000000000000000100000000000b       ;второй парам - число
       

      Этими флагами можно описать почти все инструкции (некоторые флаги можно убрать). Если инструкция содержит больше 2 параметров, тогда остальные хранятся в отдельном поле.

      Далее, по флагам определяется, что и как чекать: проверки на возможность инициализации параметров, на изменение их значений, на использование их в других командах и многое другое. Результат каждой проверки заносится в маски. Их 2: regs_init & regs_used. Грубо говоря, это 2 dword'a, где каждый бит соответствует определённому параметру (например, какому-то регистру). Причём, биты в regs_init показывают, можно ли инициализировать параметр или нет (защита от повторной инициализации). А по битам в regs_used узнаём, можно ли вообще использовать параметр в командах или нет.

    2. Проверка состояний параметров команды

      Итак, если первый этап пройден, то это означает, что параметры годные. Продолжим.

      Состояние - это некоторое сохранённое значение, которое принимал параметр. Состояния всех параметров хранятся в таблице состояний, которая представляет собой буфер определённого размера.

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

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

  7. переходим снова в модуль генерации команд. Смотрим, какое значение вернул нам модуль логики: если 0, тогда команда не подходит по логике - по её же адресу сгенерируем новую команду (перезапишем). Прыгаем на пункт II. если 1, тогда команда подходит по логике. Прыгаем на пункт VIII.
  8. увеличиваем адрес (для генерации новой команды) на размер проверенной команды. И выясним: если мы сгенерировали нужное количество байтов, тогда прыгаем на пункт IX. Если не все, тогда на пункт II.
  9. выходим;

Примеры генерации простого мусора

; ---------------------------------------        ------------------------------------
;| такой код не пройдёт проверку логики, |       | такой код пройдёт проверку логики, |
;| а значит не сгенерируется              |       | а значит сгенерируется         |
; ---------------------------------------        ------------------------------------

        ;1.1                                    ;2.1
        mov     edi, 1000                       mov     edi, 1000
        mov     esi, edi                        mov     esi, edi
        mov     edi, esi                        mov     edi, 2000

        ;1.2                                    ;2.2
        mov     eax, 1000                       mov     eax, 1000
        mov     ecx, 2000                       mov     ecx, 2000
        xchg    eax, ecx                        xchg    eax, ecx
        xchg    eax, ecx                        inc     ecx
                                                xchg    eax, ecx

        ;1.3                                    ;2.3
        xor     edi, edi                        xor     edi, edi
        xor     ebx, ebx                        xor     ebx, ebx
        add     edi, ebx                        inc     ebx
                                                add     edi, ebx

        ;1.4                                    ;2.4
        mov     edx, 1000                       mov     edx, 1000
        mov     eax, edx                        mov     eax, edx
        dec     edx                             mov     edx, 2000
        mov     edx, 2000                              

        ;1.5                                    ;2.5
        mov     eax, 1000                       mov     eax, 1000
        mov     ecx, 1001                       mov     ecx, 1001
        sub     ecx, eax                        sub     ecx, eax
        sub     eax, ecx                        sub     eax, ecx
        inc     eax                             dec     eax
 

В примере 1.1 первые 2 команды нормальные, а третья - мусорная. Регистр EDI инициализировать можно, но он примет такое же значение, какое имеет сейчас - ненужная инициализация. Пример 2.1 (и все остальные в дальнейшем) показывает правильный вариант инициализации.

В примере 1.2 первые 3 команды правильные, а 4-ая - мусорная. Регистры EAX & ECX снова примут значения, которые уже имели (проверка состояний параметров).

В примере 1.3 первые 2 команды правильные, а 3-я - мусорная. EDI += 0 (проверка состояний);

в примере 1.4 первые 3 команды правильные, а 4-я - мусорная. Регистр EDX нельзя инициализировать, если он прежде не повлиял на значение другого параметра (проверка параметров);

в примере 1.5 первые 4 команды правильные, а 5-ая - мусорная. После выполнения первых 4-x команд состояния регистра EAX будут такие: 1000, 999. А после выполнения 5-ой команды EAX = 1000. Такое состояние уже было (проверка состояний параметров).

Позитив =)

Реализацию техники "логичного мусора", наглядные примеры генерации трэш-кода, а также более полное понимание задумки - всё это ты найдешь в сорцах xTG v2.0.0.

Следует сказать, что в xTG наиболее лучшее качество логики получается при генерации линейного трэш-кода без winapi-функций. В остальных случаях логика будет нечёткой, но будет: на ветвлениях и с винапишками. Это связано с модулем логики - чем он мощнее, тем качественней выхлоп.

Если же не устраивает логика каких-либо инструкций, достаточно просто установить другие флаги.

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

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

Используемая инфа

  1. beauty on the fire "Эмуляция программного кода", 2004, http://uinc.ru/articles/47/
  2. beauty on the fire "Анализаторы кода в антивирусах", 2004, http://uinc.ru/articles/45/
  3. Sl0n "Полиморфизм. Новые техники", 2004, http://vx.netlux.org/lib/vsl05.html
m1x
[email protected]
EOF
вирмэйкинг для себя...искусство вечно
[Вернуться к списку] [Комментарии]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! vxheaven.org aka vx.netlux.org
deenesitfrplruua