При изучении вредоносных программ динамического анализа в изолированных системах-песочницах в большинстве случаев достаточно для того, чтобы выявить природу той или иной угрозы и на основе полученной информации пополнить вирусную базу антивируса. Однако иногда в вирусную лабораторию попадают образцы, требующие глубокого понимания непосредственного алгоритма их работы. И тогда без статического анализа уже не обойтись. Например, для бэкдоров может понадобиться разбор протокола общения с C&C-сервером или алгоритма генерации доменных имен. А при анализе шифровальщиков необходимо выяснять, каким алгоритмом и какими ключами шифровались файлы. При этом часто такие троянские приложения запакованы или обфусцированы, и потому статический анализ может быть осложнен. Пример разбора такой сложной угрозы мы сегодня и рассмотрим.
Итак, перед нами некая вредоносная программа для платформы Windows. Первым делом загрузим ее в IDA Pro Disassembler и попытаемся выполнить автоанализ. Как ни странно, это нам не удается, поскольку тот спотыкается уже в самом начале — на функции Main. Это значит, что для разбора образца нам потребуется приложить определенные усилия.
Анализ ассемблерного листинга расставит все на свои места
Переоткроем файл в IDA с выключенным анализатором и приступим к разметке инструкций вручную. Перед нами предстает обфускация с перепрыгиванием между мусорными участками кода — так называемая jmp-вермишель. Рассмотрим алгоритм этой обфускации. Для наглядности фрагмент соответствующего кода показан на изображении ниже.
Итак, мы видим, что регистр ESP уменьшается на заданное значение, затем следуют 2 или 3 инструкции, задающие некие значения регистрам с последующим их сравнением. Далее идет условный переход, который на самом деле является безусловным, поскольку значения сравниваемых величин не вычисляются, а прописаны непосредственно в коде. Вслед за этим переходом регистр ESP возвращается к исходному значению. Затем инструкция CALL записывает в стек адрес возврата (в данном случае это адрес 4142D1), а вызванная функция увеличивает это значение. После исполнения инструкции RET указатель инструкций имеет значение 414306. И по адресу 414306 мы видим начало перехода на точно такой же шаблон перепрыгивания. Подобными кусками бессмысленного кода наполнен весь файл. Поэтому пока мы не избавимся от этого мусора, о статическом анализе можно забыть.
Убираем jmp-вермишель
Чтобы разобраться с обфускацией, мы воспользуемся IDAPython — встроенным в IDA Pro интерпретатором Python. С его помощью напишем специальный скрипт поиска выявленного нами шаблона обфускации и пропатчим образец так, чтобы «нейтрализовать» jmp-вермишель.
Назовем один из методов созданного нами скрипта SizeOfObfuscatedCode. В этом методе с помощью idaapi и функций по декодированию инструкций реализуем поиск нужного нам шаблона. На входе каждая инструкция будет брать предполагаемый адрес мусорного блока и число инструкций, которые будут идти после первого регистра SUB ESP. Напомним, что в мусорном блоке задействуется 2 или 3 инструкции по записи неких значений регистров и их сравнению.
Результатом работы данной поисковой функции станет значение размера мусорного блока, то есть число байт, которые можно превратить в инструкции NOP, если мусорный шаблон найден по конкретному адресу.
Другой метод (назовем его CleanCode) будет брать диапазон адресов исполняемой секции, где фактически расположен весь код программы, и выполнит поиск в нем байтовой последовательности 83 EC. Эта последовательность означает инструкцию SUB ESP и, собственно, начало мусорного блока. Для найденного адреса будет вызываться метод SizeOfObfuscatedCode. Если в результате исполнения возвратится ненулевой результат (обнаруживается целевой шаблон), мусорный блок заполняется байтами 0x90, то есть инструкциями NOP.
Запускаем наш скрипт, ждем окончания его работы и сохраняем пропатченный файл, в котором фрагменты кода с обфускацией фактически обнулились. Если теперь открыть образец в 16-ричном редакторе, в коде перед нами предстанут последовательности из NOP‑инструкций. Мы успешно избавились от мусора.
Открываем в IDA PRO уже пропатченный файл, но теперь снова со включенным автоанализатором. Как мы видим, секция кода корректно размечена именно как код, и с декомпилированными функциями вполне можно работать. Нам все еще могут попасться фрагменты мусорного кода, как показано на изображении ниже, однако на дальнейший анализ это уже не повлияет.
При беглом осмотре декомпилированных функций находим секцию, наиболее вероятно отвечающую за получение адресов WinAPI‑функций. Здесь одна и та же функция для различных DWORD (контрольных сумм) возвращает определенные значения. Запускаем отладку файла и обнаруживаем, что полученная таблица импорта не содержит никаких адресов. Копаем немного глубже и понимаем, что значения из этой таблицы проходят через функцию sub_40DEA0 непосредственно перед вызовом каждой функции из таблицы. Фактически sub_40DEA0 используется для раскодирования реального адреса вызова. Ей отдается кодированное значение целевой WinAPI‑функции, она возвращает ее реальный адрес, после чего функция вызывается по этому адресу.
Убеждаемся в том, что имеем дело с таблицей импорта, изучив работающие с адресами функции. Проблема в том, что на понимание алгоритма их работы может уйти слишком много времени. Поэтому лучше всего сделать так, чтобы вредоносная программа выполнила работу за нас и сама раскодировала значения соответствующих адресов.
Восстанавливаем таблицу импорта
Для восстановления таблицы импорта напишем функцию. На вход она будет брать адрес функции, используемой трояном для вычисления реального адреса, а также указатели на начало и конец блока в оперативной памяти, содержащего закодированные адреса. Нам необходимо пройтись по указанному диапазону, получить контрольные суммы DWORD, вызвать для них функцию во вредоносной программе и полученный результат записать на то же самое место в памяти.
При этом обратите внимание, что в найденной нами таблице адресов значение адреса одной из функций, как ни странно, оказалось реальным. Можно предположить, что троян использует его без предварительного раскодирования. Соответственно, при восстановлении таблицы импорта этот адрес следует пропустить.
После того, как наша функция готова, компилируем ее и берем двоичный код объектного файла. Выделяем в адресном пространстве отлаживаемого вредоносного приложения область памяти и внедряем туда полученный ранее двоичный код.
Выставляем точку прерывания (breakpoint) на начале нашей функции. Далее создаем для нее поток, и после ручной правки аргументов на стеке возобновляем ее работу. В результате мы наконец-то получаем таблицу импорта с корректными адресами.
Теперь при помощи утилиты Scylla собираем целевые адреса из отлаживаемого образца, создаем дамп процесса и на основе этих данных реконструируем таблицу импорта.
Успешно восстановленная таблица представлена на скриншоте ниже.
На основании информации о полученных адресах мы можем предположить, что перед нами вредоносная программа-вымогатель, а именно — шифровальщик. Так, здесь присутствуют функции FindFirst* и FindNext* для обхода дисков и дерикторий, MoveFile, которую энкодеры обычно используют для переименования зашифрованных файлов, а также функции BCrypt* для генерации криптографических ключей и непосредственно выполнения шифрования.
В пропатченном нами файле остаются теперь уже лишние вызовы функции sub_DEA0:
Однако необходимости в ее вызове больше нет, ведь мы получили реальные адреса. Вновь воспользуемся IDAPython. Напишем небольшой скрипт, который для заданного адреса пропатчит программу так, чтобы MOV ECX поменялся на MOV EAX, а вызов sub_136DEA0 вообще исчез. Для этого нам необходимо встать на начало этой ненужной функции, с помощью метода CodeRefsTo получить все адреса, откуда данная функция вызывается, и передать их в метод PatchCall скрипта.
Наш скрипт:
А это — результат его работы:
Как мы видим, вызовы пропатчены, и теперь IDA Pro понимает, что именно вызывается при выполнении инструкции CALL EAX.
В итоге после финальной декомпиляции файла в IDA Pro в функциях можно отчетливо увидеть все признаки трояна-шифровальщика:
Обход директорий.
Сбрасывание флага Read-only у файла, подлежащего шифрованию.
Создание потока для шифрования целевого файла.
Использование криптографических функций для генерации ключей шифрования.
Наконец-то мы можем приступить непосредственно к анализу алгоритма работы шифровальщика.