Настольная книга компьютерного исследователя


Вирусные алгоритмы: обратное проектирование (часть 1)

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

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

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

Классический пример активной защиты — стелс-технология, используемая в компьютерных вирусах. Все то время, пока такой вирус активен, он скрывает приращение длины инфицированных файлов, или даже во время операций с этими файлами подставляет их оригинальное (не-инфицированное) тело.

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

Например многие емыльные сервера уже проверяют наличие почтовых червей.

Кроме того, чем сложнее вирус, тем больше времени требуется на его изучение, детектирование и лечение; и, следовательно, при возникновении эпидемии, этот вирус будет иметь больше времени для распространения.

Поэтому мы будем говорить о пассивной защите от изучения. И здесь существуют следующие направления:
- Шифровка тела вируса. Используется в крипт- и полиморфных вирусах.
- Автоматическая генерация (или изменение) тела вируса на уровне ассемблерных инструкций. Используется в метаморфных и пермутирующих вирусах.

Простейший крипт-вирус содержит всего одну процедуру ассемблерного кода — так называемый расшифровщик, который расшифровывает основное тело вируса и передает ему управление.

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

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

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

Кроме этого, используется техника «неизвестной точки входа» (UEP, Unknown Entry Point): при инфицировании, вирус шифрует свое тело, а команду перехода на cебя вставляет в случайное место программы. В таком случае, при изучении, программа выглядит практически так же, как и до заражения; а вирус будет активирован при непредсказуемых заранее условиях.

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

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

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

Задача упрощается тем, что в случае такого автоматического троянизирования файла, дизассемблировать его код нужно только до уровня длин инструкций. Значение же большинства составляющих файл инструкций не требуется вовсе.

Такие действия мы будем называть реверсированием, а библиотеку кода, выполняющую эти действия — реверсером.

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

Элементами списка могут являться ассемблерные инструкции, блоки данных, а также различные объекты, относящиеся к структуре исполняемого файла, например фиксапы.

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

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

Теперь о проблемах, которые возникают при реверсировании.

Основная проблема — это время, которое необходимо затратить на реверсинг файла. Для файлов меньше 512k это от нескольких секунд до нескольких минут.

Следующая проблема — это собственно дизассемблирование. Дело в том, что не всегда возможно с полной уверенностью отличить в файле код от данных. Такая ошибка будет фатальна для работоспособности программы (но не червя). Поскольку большинство таких двойственных ситуаций удается выявить, достаточно большой процент (~80%) существующих файлов реверсингу не подлежит.

Кроме того, реверсинг требует большое количество памяти, у меня получалось в 30-50 раз больше собственно длины файла.

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

Дальше приводится статья-описание существующего реверсера.

Автоматизация обратного проектирования
(технология недетектируемого вируса)

 Наши усилия направлены на то, чтобы научиться модифицировать исполняемые программы (в формате PE) так, чтобы поиск внесенных изменений занимал максимум времени.

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

Исходя из этого, задача разбивается на 3 части, или же три глобальных вопроса: ЧТО?, КУДА? и КАК?.
ЧТО — это вирусные инструкции, которыми будет дополнен инфицируемый PE файл. О том, какими могут быть эти инструкции, рассказано в статье про метаморфизм и показано в кодегенераторе.
КУДА — это вопрос о том, в какие места программы вставлять инструкции расшифровщика, и как определять эти места. В принципе, это относительно просто; более того, как раз эта часть возлагается на пользователя движка.

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

Теория

Нашей задачей является вставить между инструкциями PE файла свои собственные. Но из-за того, что код и данные в файле могут быть по-всякому связаны друг с другом, изменение кода повлечет за собой изменение всех связей. Однако, изменение одних связей влечет за собой изменение других, и так далее, пока значимая часть файла не будет изменена.

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

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

Исходя из сказанного, необходимо разобрать весь файл в некоторую легко изменяемую сущность, изменить ее, и собрать файл заново.

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

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

 Проблемы дизассемблирования

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

Ошибка влечет за собой следующее: код, принятый за данные, не будет пофиксен, так что когда он получит управление, сразу произойдет глюк. Ибо фиксить в этом коде надо jxx, call и т.п. Если же, наоборот, данные будут приняты за код, и в них подобная инструкция будет пофиксена, то это тоже чревато глюком.

Поэтому необходимо научиться безошибочно отличать данные от кода. Некоторые библиотечные процедуры можно было бы найти по сигнатурам, но у нас нет таких ресурсов. Можно было бы рассматривать jmptable’ы, но это помогает лишь частично. Можно было бы работать только с файлами определенного вида, а именно, такими, в которых в кодовой секции находится только код и ничего больше. Но это как раз то, чего не хотелось бы делать. Ведь нам надо, чтобы антивирусы проверяли КАЖДЫЙ файл по пол-часа. Да и мало таких файлов.  Короче говоря, все ошибки в рекомпиляции файла возникают из-за неправильного дизассемблирования, а именно — из-за описанной выше проблемы. Выхода два: ограничить множество обрабатываемых файлов, либо насколько это возможно улучшить дизассемблер и надеяться на удачу.



©2015 Журнал Хакера Entries (RSS) and Comments (RSS)