Привет, Хабр! Меня зовут Георгий Кучерин, я — Security Researcher в Глобальном центре исследования и анализа угроз (GReAT) «Лаборатории Касперского», где мы занимаемся изучением APT-атак, кампаний кибершпионажа и тенденций в международной киберпреступности. Да-да, тот самый GReAT, который раскрыл кампанию «Операция Триангуляция» и множество других сложных атак :)



В нашем арсенале есть собственный плагин hrtng для IDA Pro (известная утилита для реверс-инжиниринга), который упрощает реверсинг вредоносного ПО. Недавно мы опубликовали код этого плагина в открытом доступе под лицензией GPLv3 — и хотим наглядно показать, как именно он может облегчить реверсеру жизнь. В этой статье мы проанализируем с помощью hrtng образец известного трояна FinSpy, а в процессе анализа дадим немного рекомендаций по работе с IDA в целом.


А что это за плагин вообще?


hrtng с 2016 года разрабатывал наш коллега по GReAT Сергей Белов; а сама разработка берет начало от плагина hexrays tools, который написал исследователь Милан Бохачек. Постепенно плагин улучшался, и в него добавлялось множество функций — от дешифрования строк до декомпиляции обфусцированного кода. В первую очередь это был функционал, которого нам остро не хватало в самой IDA Pro. Порой нужные возможности были реализованы в уже существующих, зачастую заброшенных плагинах. В этом случае получалось вдохновляться имеющимся кодом, который дополнялся и обновлялся для совместимости с последними версиями часто меняющегося IDA SDK.

Приступаем к анализу


Мы будем анализировать вредоносное ПО FinSpy: это сложный коммерческий троян-шпион, о котором можно подробнее почитать здесь.

Если мы откроем наш образец в HEX-редакторе, то увидим, что первые его два байта — 4D 5A, что соответствует сигнатуре исполняемого файла на Windows. Однако IDA при загрузке этого файла не опознает его как EXE-файл. Тогда попробуем загрузить его как бинарный файл, выбрав в окне соответствующую опцию.




В таком случае IDA отобразит байты загруженного файла. Если их дизассемблировать, мы увидим, что 4D 5A — это вовсе не заголовок EXE-файла, а часть вот такого шелл-кода:



Рассмотрим подробнее, что здесь происходит. Его первые инструкции (выделены желтым) преимущественно нужны, чтобы замаскировать начало шелл-кода под заголовок EXE-файла. В выделенной оранжевым части кода мы видим присваивание двух с виду интересных констант: 0x2C7B2 и 0xF6E4BB5E.

Далее в выделенной голубым части есть две инструкции fldz и fstenv. Эту комбинацию инструкций стоит запомнить, так как она часто встречается во вредоносном ПО для получения значения регистра EIP: он содержит адрес, по которому расположен исполняемый вредоносный код. Этот адрес, увеличенный на число 0x1D, сохраняется в регистре EDX при помощи инструкции LEA.

Но как именно инструкции fldz и fstenv помогают получить этот адрес?
Поищем описание инструкций fldz и fstenv в документации к процессору Intel. Для инструкции fldz в ней (п. 8.3.4) написано, что эта инструкция сохраняет константу 0 на стек математического сопроцессора (FPU). В свою очередь, инструкция fstenv (п. 8.1.10) получает текущее состояние математического сопроцессора, которое сохраняется в следующем формате:



Чтобы понять, как шелл-код использует получаемое им состояние сопроцессора, создадим в IDA структуру состояния, которая будет иметь следующий вид:

struct sFPUstate
{
  int cw;
  int sw;
  int tw;
  int fip;
  int fis;
  int fdp;
  int fds;
};

Применив эту структуру к переменной, в которую сохраняется состояние, видим, что в коде используется поле структуры fip:



Как видно по названию поля, FPU Instruction Pointer Offset, в нем хранится счетчик команд математического сопроцессора: в нашем случае это адрес инструкции fldz.


После того как шелл-код получает собственный адрес, он входит в цикл (выделен зеленым на скриншоте). Его тело состоит из двух инструкций — XOR и ROL. Как видно из скриншота, в операции XOR участвует регистр EDX — чуть выше я писал, что в нем хранится адрес внутри шелл-кода. Таким образом, шелл-код применяет операцию XOR к своим собственным байтам — расшифровывает сам себя. При этом ключ шифрования хранится в регистре EBX (его значение 0xF6E4BB5E присваивается в оранжевой части), а счетчик цикла, количество байт, которые нужно расшифровать, — в регистре ECX (в оранжевой части ему присваивается значение 0x2C7B2).

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

Мы же сделаем это при помощи нашего плагина hrtng. Если кратко, то для этого можно при помощи комбинации клавиш Alt + L применить к нему операцию Decrypt Data. В появившемся окне можно задать алгоритм и ключ шифрования — в наш плагин уже добавлены наиболее часто встречающиеся криптографические алгоритмы, такие как XOR, RC4 или AES. Мы же выберем алгоритм FinSpy и зададим ключ 0xF6E4BB5E. После этого шелл-код успешно расшифруется — и мы сможем продолжить его анализ. Получилось довольно удобно, так как всю работу мы сделали за пару кликов в IDA.



Как добавить в плагин собственный алгоритм шифрования?
Для этого необходимо открыть файл decr.cpp и в функции decr_init добавить название алгоритма:



Сам алгоритм шифрования нужно реализовать на языке C++ и вставить в функцию decr_code по аналогии с тем, как это сделано на скриншоте ниже:



После перекомпиляции плагина алгоритм отобразится в окне Decrypt String — и им можно будет пользоваться!

После выполнения цикла исполняется часть кода, выделенная фиолетовым: она передает управление на расшифрованный код. Если мы посмотрим на то, что расположено ниже фиолетовой части, то увидим нулевые байты, а после них (по смещению 0x108) — вот это:



Последовательность байтов на скриншоте выше начинается с байтов 50 45 (PE) — это сигнатура, характерная для PE-файлов. В то же время наш шелл-код начинается с байтов 4D 5A (MZ) — это еще одна сигнатура PE-файла. Получается, наш шелл-код расшифровал себя в PE-файл.


Теперь мы можем сохранить расшифрованное содержимое в файл на диск, используя функцию нашего плагина Create DEC file:





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



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



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

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



Одна из первых вещей, которую делает декомпилированная функция, — это вызов функции sub_A9D8. Она вызывается несколько раз, и каждый раз с довольно большим числом в качестве аргумента (0x5C2D1A97, 0xE0762FEB и так далее). В свою очередь, в коде функции sub_A9D8 есть вызов другой функции — sub_A924. В ее коде можем заметить константу 0xEDB88320 — она используется в алгоритме хеширования CRC32.



Хеширование в помощь!


Во вредоносном коде хеш-функции наподобие CRC32 очень часто применяются для реализации техники Dynamic API Resolution, предназначенной для обхода защитных средств. Она позволяет при помощи хешей получать указатели на функции Windows API. За счет использования хешей удается избежать упоминания в коде имен «подозрительных» API-функций — это делает вредоносное ПО более незаметным для защитных решений.

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

С нашим плагином сделать это можно очень просто — при помощи функции Turn on APIHashes Scan, которая позволяет автоматически искать в дизассемблированном и декомпилированном коде значения хешей, соответствующие именам API-функций. Обнаружив такой хеш, она не только добавляет в код комментарий с именем соответствующей хешу функции, но и присваивает переменной с указателем на функцию корректный тип данных и осмысленное имя.



Чтобы воспользоваться этой функциональностью, надо сначала импортировать в IDA библиотеки типов Windows (mssdk_win10 и ntapi_win10) при помощи окна Type Libraries (горячая клавиша Shift + F11) и далее активировать поиск констант API-хешей в плагине.



Теперь, когда мы восстановили имена функций, замаскированные при помощи API-хеширования, мы можем проанализировать код, расположенный дальше:



Код выше исполняет цикл, в котором он ищет две сигнатуры: 4D 5A (MZ) и 50 45 (PE). Это, как мы уже упомянули, сигнатуры, находящиеся в заголовках EXE-файлов. В частности, в PE-файлах с байтов 50 45 начинается структура IMAGE_NT_HEADERS. Попробуем применить ее к нашему коду:




Из видео выше мы можем заметить, что структура наложилась корректно: значения ее полей соответствуют спецификации PE-файлов. Например, в поле FileHeader.Machine указана константа 0x14C (IMAGE_FILE_MACHINE_I386), а значение OptionalHeader.Magic — 0x10B (PE32).

Получив содержимое структуры IMAGE_NT_HEADERS, шелл-код его обрабатывает — такое часто происходит, чтобы загружать PE-файлы в память. Как раз внутри этой структуры содержится смещение директории импортов (хранящееся в поле OptionalHeader.DataDirectory[1].VirtualAddress и равное 0x1240C).

Директория импортов — что это такое?


Директория импортов — массив структур IMAGE_IMPORT_DESCRIPTOR. Чтобы применить эти структуры к директории импортов, надо сначала импортировать определение структуры IMAGE_IMPORT_DESCRIPTOR в IDA, а затем наложить эту структуру на сами байты:



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

Однако если мы посмотрим на наши структуры IMAGE_IMPORT_DESCRIPTOR, то увидим, что смещение этого массива в них равно нулю!



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

Второе по счету поле — Name — содержит имя библиотеки, из которой импортируются функции. С этими именами все в порядке (например, по смещению 0x12768 содержится строка msvcrt.dll), чего нельзя сказать про поле FirstThunk: в этих именах содержатся довольно странно выглядящие массивы. Однако если мы начнем определять элементы этих массивов как 4-байтовые числа, то hrtng будет определять их как CRC32-хеши имен API-функций. Это позволяет очень легко понять, какие API-функции используются во вредоносном коде. Кроме того, плагин автоматически определяет количество аргументов этих функций и их типы данных:



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

Разобравшись с импортами, можем дальше продолжить анализ файла. Переместим его на корректный базовый адрес (0x400000, он указан в поле заголовка OptionalHeader.ImageBase) и посмотрим на его точку входа (0x407FB8, поле OptionalHeader.AddressOfEntryPoint).

Видим такой код:



В этом коде значения регистров ESI и ECX сохраняются на стек. Затем с регистром ESI производятся различного рода арифметические действия: сложение, умножение, XOR и так далее. После всех этих действий в регистры ESI и ECX восстанавливаются значения со стека, и тем самым результат этих арифметических операций теряется. Получается, все арифметические операции являются лишними и запутывают работу дизассемблера — в таком случае их надо убрать. Наш плагин позволяет легко это сделать: можно выделить участок кода и применить для него функцию Fill with NOPs:



Далее наша функция при помощи инструкции jz передает управление на инструкцию по адресу 0x402E40. По ней находится обфусцированный еще одной техникой код: две противоположные друг другу инструкции ja (перейти, если больше) и jbe (перейти, если меньше или равно) переходят на одну и ту же инструкцию. Иными словами, два условных перехода ведут себя как один безусловный, и это мешает IDA произвести анализ функции.



Чтобы противодействовать подобным обфускациям, в которых используются инструкции условных и безусловных переходов, в нашем плагине есть специальная функция Decompiled Obfuscated Code, которую можно активировать нажатием клавиш Alt + F5. Она справляется и с нашей обфускацией, с легкостью декомпилируя ее код:



Если теперь поискать, где ранее встречался код, который получился в ходе работы декомпилятора, то можно увидеть, что он представляет собой движок обфускатора — виртуальной машины FinSpy VM. Деобфускация виртуальных машин — в целом задача сложная, поэтому в этой статье мы не будем рассматривать, как это делать: продвинутым реверс-инженерам, кому это интересно, можно почитать эти статьи: [вставить ссылки: мы, ESET, Rolf]. В случае с FinSpy VM есть уже готовый скрипт для деобфускации — им мы и воспользуемся!

Однако чтобы этот скрипт работал, ему нужно расположить функции движка в правильном порядке, чтобы получить верные номера опкодов виртуальной машины. Так как в каждом образце FinSpy свой, случайный порядок опкодов, для определения порядка нужно, чтобы в IDB функции движка были правильно названы.

В реверс-инжиниринге для того, чтобы распознавать в бинарных файлах уже известные функции, используются сигнатуры. Например, в IDA встроена технология FLIRT, позволяющая распознавать функции при помощи сигнатур, накладываемых на бинарный код. Однако обфускации, которые присутствуют в нашем файле, не позволяют воспользоваться этой технологией (например, из-за наличия рандомных условных переходов). У нас в плагине реализована более стойкая к обфускациям альтернатива FLIRT — за счет того, что она использует не бинарный код, а оптимизированный микрокод декомпилятора. Сигнатуры для микрокода можно импортировать при помощи меню File -> Load file -> [hrt] MSIG file.



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



Что мы поняли?


Мы изучили, как работает находящийся в начале нашего образца шелл-код, а затем стали смотреть видоизмененный PE-файл, содержащийся внутри шелл-кода. Мы рассмотрели, как устроены структуры PE-файлов, и смогли преодолеть различные техники обфускации: API-хеширование, добавление мусорного кода и виртуализацию. При этом плагин hrtng позволил нам сделать всю эту работу очень быстро: если бы не он, пришлось бы писать скрипты для IDA из большого количества строк. Очень надеемся, что наш плагин будет полезен всем в этом непростом деле — реверс-инжиниринге!

Если вам интересно участвовать в подобных изысканиях, приходите к нам в «Лабораторию Касперского» в подразделение Threat Research, а также в команды пентестеров или Defensive Security — в зависимости от того, с какой стороны вы хотите смотреть на инструменты кибербезопасности. Будем вместе расследовать уже случившиеся инциденты и предупреждать будущие.

Комментарии (1)


  1. Timick
    15.12.2024 23:23

    Fill with NOPs было бы автоматом, ценная штука была бы.