Основной задачей мусорных инструкций является скрытие/защита полезного кода (от аверов, зорких глаз реверсера и других любопытных). Однако, "неправильный" трэш может стать причиной обнаружения вирусного кода, сводя на нет все наши старания.
Этот текст о том, как улучшить качество генерируемого мусора.
Кто противник
Допустим, что происходит проверка файла, заражённого нашим вирусом. Антивирус может действовать так:
--------------------------------------------------------------------------------------------------------------
- запустит поверхностный анализ файла: его структуры и каких-то участков кода (сбор информации для начала работы эмулятора, кодо-анализатора, эвристика, может чего-то ещё);
--------------------------------------------------------------------------------------------------------------
- далее, запускается эмуль, в процессе работы которого могут быть вызваны: анализатор кода (сбор данных для (дальнейшей работы) эмулятора и эвристика), а также сигнатурный анализ (поиск известных сигнатур в уже проэмулированном коде);
--------------------------------------------------------------------------------------------------------------
- после отработки эмуля в дело вступает эвристический анализ собранных данных (сигнатур для нашего виря ещё не сделали=)), где происходит подсчёт баллов "опасности". Если полученный результат больше заданного предела - получите клеймо heur-virus'a.
--------------------------------------------------------------------------------------------------------------
От эмуля нам поможет рабочая антиэмуль, от сигнатур - генератор мусора, который (как оказывается 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
Трэшген может генерировать оба эти варианта, но более правдоподобно смотрится, если держаться одной тактики (неизвестно, какие извращения будут в новых версиях эвристиков).
Далее, помимо разных фичезов, которые вы встроите в трэшген, он также должен уметь генерировать "реалистичный" код (похожий на обычный код стандартных программ, написанных на ЯВУ), а именно:
--------------------------------------------------------------------------------------------------------------
+ "правильные" инструкции (опкоды и операнды - например, команда "mov eax, ecx" может быть закодирована с помощью двух разных опкодов: 0x8B 0xC1 и 0x89 0xC8 -> ms-компилер юзает первый вариант; некоторые команды с использованием регистра EAX, имеющие "оптимизированные" варианты опкодов; etc);
--------------------------------------------------------------------------------------------------------------
+ "правильные" конструкции (например, test/cmp без последующей инструкции jmp/jxx - очевидное палево);
--------------------------------------------------------------------------------------------------------------
+ множество различных команд (использующих регистры, адреса памяти и т.п.), функции (с прологами/резервированием стэка/эпилогами etc и команды с использованием локальных переменных, входящих параметров), winapi и другие;
--------------------------------------------------------------------------------------------------------------
+ "правильная" статистика частоты встречаемости опкодов (собираем стату в обычных прогах и используем её; для более точного результата можно собирать стату только в файлах, собранных выбранным ранее компилером);
--------------------------------------------------------------------------------------------------------------
+ нормальная энтропия (в битах ~ [5.5; 6.8]; кстати, энтропия будет примерно в заданном диапазоне, если генерить "правильный" код + использовать стату встречи опкодов (сюда можно добавить и логику команд));
--------------------------------------------------------------------------------------------------------------
+ только живой код (который может выполниться);
--------------------------------------------------------------------------------------------------------------
+ etc;
--------------------------------------------------------------------------------------------------------------
Но даже такой трэш-код, построенный с учётом данных пунктов, может легко ловиться эвристикой.
Полезный мусор
Основная засада в том, что мусорный код - это только мусор, набор бесполезных инструкций. В этом и кроются причины гнева эвристиков. А раз так, значит трэш должен стать полезным. Для этого надо реализовать ещё 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), который работает по следующей схеме:
Распишем подробно:
----------------------------------------------------------------------------------------------------------
I.вначале, конечно же, вызываем модуль генерации команд;
----------------------------------------------------------------------------------------------------------
II. генерируем "правильную" команду: правильные опкоды и остальные байты. Да, кстати, если разработанный двигатель логики (ДЛ) будет применяться к сторонним трэшгенам (или другим движкам), то ДЛ также должен проверять, правильно ли построена команда (aka проверка на уровне байтов);
----------------------------------------------------------------------------------------------------------
III.вызываем модуль логики, передавая в него адрес только что созданной команды;
----------------------------------------------------------------------------------------------------------
IV. за дело принимается парсер команд. Парсер может быть функцей, являющейся частью модуля логики, а может быть и отдельным самодостаточным движком (дизасм). Первый случай хорошо подходит, если модуль логики является частью генератора мусора. Тогда в функции парсера будут разобраны только те команды, которые может генерировать двигл. Второй случай хорошо подходит, если модуль логики является самостоятельным движком. И при этом мы не знаем, какие команды могут генерироваться.
Парсер выясняет, какая перед ним команда, и получает её параметры (операнды: регистры, адреса etc) - в соответствии с этим сохраняет в некоторую структуру данные параметры и выставляет определённые флаги. Заполненная структура будет использоваться анализатором команд (об этом ниже). Также, например, если встретилась команда mov ecx, dword ptr [403008h] и т.п., тогда парсер заменит адрес 403008h на другой, соответствующий ему адрес в выделенной памяти для корректной эмуляции команды;
----------------------------------------------------------------------------------------------------------
V.затем эмулируем (скорректированную) команду. Эмуль, по аналогии с парсером, может быть как встроенной функцией в модуле логики, так и полноценным движком-пирожком;
Эмуль получает адрес команды, подготавливает специальную среду, копирует туда команду и эмулирует. Причём эмуляция может быть как минимум 3-х видов: прямой запуск в специальной среде, полная имитация выполнения команды и сочетание этих двух методов (для большинства команд хватает 1-ого метода). Результат эмуляции (текущие значения параметров команды и др.) сохраняем в переменных: виртуальных регистрах и др.
Кстати, эмуль - козырная технология для вирей, с помощью которой можно творить очень интересные темы (для UEP'a, виртуальных машин, "logical trash" tech, морфинга и прочих вкусностей);
----------------------------------------------------------------------------------------------------------
VI. и после вызываем анализатор команд/корректор логики. Анализатор, по аналогии с парсером и эмулем, может быть как встроенной функцией в модуле логике, так и полноценным двиглом;
Анализатор, на основе данных от парсера (заполненная структура) и эмуля, решает, подходит ли команда по логике или нет.
Анализ команды проходит в 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. Проверка состояний параметров команды
Итак, если первый этап пройден, то это означает, что параметры годные. Продолжим.
Состояние - это некоторое сохранённое значение, которое принимал параметр. Состояния всех параметров хранятся в таблице состояний, которая представляет собой буфер определённого размера.
Значит, анализатор берёт текущее значение параметра, которое мы получили с помощью эмуляции (и сохранили, например, в виртуальном регистре), и сверяет его со всеми накопленными состояниями данного параметра.
Если совпадение найдено, тогда команду считаем мусорной, проверка не пройдена. В этом случае из таблицы состояний берём последнее сохранённое состояние данного параметра и делаем его текущим (сохраняем это состояние в виртуальном регистре); а также восстановим маски на предыдущие значения. Если совпадение не найдено - значит это новое состояние параметра, команда прошла проверку. Добавляем это значение в таблицу состояний;
----------------------------------------------------------------------------------------------------------
VII.переходим снова в модуль генерации команд. Смотрим, какое значение вернул нам модуль логики: если 0, тогда команда не подходит по логике - по её же адресу сгенерируем новую команду (перезапишем).
Прыгаем на пункт II. если 1, тогда команда подходит по логике. Прыгаем на пункт VIII.
----------------------------------------------------------------------------------------------------------
VIII. увеличиваем адрес (для генерации новой команды) на размер проверенной команды. И выясним: если мы сгенерировали нужное количество байтов, тогда прыгаем на пункт IX. Если не все, тогда на пункт II.
----------------------------------------------------------------------------------------------------------
IX. выходим;
----------------------------------------------------------------------------------------------------------
Примеры генерации простого мусора
------------------------------------------------------------------------------------------------------------------------
В примере 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
[ + ]
август, 2011
m1x
pr0mix@mail.ru
EOF
вирмэйкинг для себя...искусство вечно
Автор pr0mix
p.s. by Ar3s статья опубликована с просьбы автора. Это первая публикация у нас на форуме. Прошу отнестить с пониманием и поддержать материал обсуждениями. Спасибо.