Этим отчетом мы начинаем серию публикаций «Заметки исследователя», в которых собираемся делиться технической информацией по исследуемым нами атакам, полученной из первых рук.

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

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

Аннотация

Для автоматизации деобфускации у нас было 2 дизассемблера, 75 библиотек для анализа кода, 5 IDE, полкуска функции из Obsidium для примера и множество идей по алгоритмизации всех сортов и расцветок, а также Unicorn, Flare-emu, Capstone, Triton и Python. Не то чтобы это был необходимый запас для написания скрипта, но, раз уж начали заниматься деобфускацией, трудно остановиться.

Введение

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

Начнем с того, что такое Obsidium. Это коммерческий продукт, который защищает программное обеспечение от динамического и статического анализа, а также позволяет настроить систему лицензирования ПО. Разработан немецкой компанией Obsidium Software. Все, что будет указано в этой и следующих статьях, не направлено на компрометацию ПО для защиты от анализа. Мы рассматриваем только ту версию, которая была получена в результате расследования инцидента ИБ и использовалась для сокрытия вредоносного ПО.

Постановка проблемы

Обфускация JMP

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

пример обфусцированного кода
Пример обфусцированного кода

В начале обозначен «прыжок» с entrypoint в далекую область памяти. Также видно, что поток управления когда-то возвращается. Заметим, что есть область, где полезные инструкции следуют одна за другой, разделенные случайными байтами. Именно таким способом и обфусцирован весь код пакера: есть блоки кода, разделенные случайными байтами и расположенные в случайных местах памяти. Исключением для обфускации являются отдельные функции, о которых мы расскажем в следующих статьях. Исследовать код при такой обфускации при линейном потоке не составляет большого труда, учитывая, что блоки в основном идут друг за другом. Но в случае с циклами и условными переходами становится труднее понять, куда будет происходить переход и что находится в каждой из веток этого перехода. Общая схема обфускации представлена ниже. Также, учитывая большой объем для анализа, было решено написать скрипт для деобфускации функций пакера.

общая схема обфускации
Общая схема обфускации

Обфускация неявных предикатов

В процессе реализации скрипта мы встретили условные переходы, у которых одна из веток возможного потока управления вела на нерабочие инструкции. Это оказалось одной из реализаций обфускации типа неявных предикатов (ее общая схема представлена ниже). То есть на месте потока выполнения встраивается условный переход и по необходимости генерируется еще одна ветка исполнения. Она может быть обфусцированной версией исходной ветки. В таких случаях условный переход может зависеть от каких-либо входных данных. Если же выбран другой метод генерации ветки, то условный переход всегда будет вести себя одинаково вне зависимости от состояния программы. Затруднение анализа при таком типе обфускации заключается в следующем:

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

Если вернуться к случаю с пакером Obsidium, то здесь указанный вид трансформации кода выполнен на бинарном уровне. Это позволило разработчикам пакера не генерировать отдельно новую ветку исполнения, а просто совершать «прыжок» на случайный отступ. Также данная трансформация хорошо встраивается в общую канву обфускации. Так как каждая полезная инструкция отделена случайным набором байтов, можно просто добавлять условные переходы вместо безусловных. Благодаря этому легко определить и полезные условные переходы, и те, что добавлены обфускатором. И если бы мы на это обратили внимание в начале написания скрипта, о котором далее пойдет речь, то, скорее всего, мы бы потратили меньше времени на разработку логики и отладку.

Ниже приведен реальный пример обфускации подобного рода. Как мы видим, сначала идет арифметическая инструкция xor, после которой следует условный переход в виде инструкции jge. Если рассмотреть подобное сочетание инструкций, то увидим, что xor всегда сбрасывает флаги CF и OF, а инструкция jge совершает переход тогда и только тогда, когда флаг SF равен флагу OF. Таким образом, мы понимаем, что переход будет совершаться всегда.

реальный пример обфускации из пакера
Реальный пример обфускации из пакера

О выбранном способе деобфускации

Вернемся к началу. Для автоматизации деобфускации у нас было 2 дизассемблера, 75 библиотек для анализа кода, 5 IDE, полкуска функции для примера и множество идей по алгоритмизации всех сортов и расцветок, а также Unicorn, Flare-emu, Capstone, Triton и Python. Не то чтобы это был необходимый запас для написания скрипта, но, раз уж начали заниматься деобфускацией, трудно остановиться.

Именно поэтому мы решили не останавливаться на одном варианте. Мы поделимся своими идеями и объясним, почему одни прижились, а другие нет.

Начало

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

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

Эти особенности привели нас к решению использовать эмулятор. Поэтому наш первый рабочий прототип по деобфускации функции заключался в следующем. При помощи Flare-emu и Rizin мы собираем трассировку кода. Далее строим граф из полученных инструкций. Удаляем из него инструкции jmp, собираем код обратно в бинарном виде и сохраняем в исполняемом файле, который уже можно загрузить в дизассемблер. Данный подход имеет ряд преимуществ:

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

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

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

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

Вторая попытка

Использование Capstone требовало решения следующих вопросов:

  • необходимо было придумать способ обработки неявных предикатов;
  • требовалось самостоятельно выполнять расшифровку данных.

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

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

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

triton общая схема работы алгоритма

Обработка условных и безусловных переходов

Отдельно расскажем про самый сложный этап – удаление лишних инструкций условного и безусловного перехода. Сложность проявляется при встрече условных переходов и циклов.

Алгоритм хотелось сделать простым – без обработки большого количество крайних случаев. Мы не претендовали на математическое обоснование работоспособности, а пошли чисто эмпирическим путем. Насмотренность графов в IDA Pro и воображение подсказали нам различные способы организации циклов и условных переходов. Ниже приведены все возможные виды графов, которые нам по итогу встретились. О последнем мы догадались, так как посчитали, что он избыточен и может реализоваться с помощью третьего типа графа. Таким образом, команда вывела следующую закономерность: инструкция jmp оказывалась полезной тогда, когда указывала на узел (инструкцию), у которого больше одного предка. И эта закономерность сработала для большинства случаев. Граф, похожий на последний, все-таки встретился. Однако его оказалось легко превратить в третий тип.

примеры графов циклов и условных переходов
Примеры графов циклов и условных переходов

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

Особенности отладки скрипта

Каждый процесс написания программного кода сопровождается отладкой. Написание деобфускатора не стало исключением.

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

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

пример графа для генерации ключа
Пример графа для генерации ключа.

Исходя из своего опыта, все участники команды Threat Intelligence самыми проблемными местами посчитали следующие этапы:

  • деобфускация кода, а именно отслеживание неявных предикатов и удаление jmp-инструкций;
  • сборка полученного кода в исполняемый файл в виде отдельной функции.

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