Оглавление


Введение

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



Распаковка

Характерные особенности Obsidium

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

MD5

dd6f96fb094521deec916ba841f4fc74

SHA1

9fa9355ef207a3be73d34bd8424b0439adcef5bf

SHA256

8d6caffb18f4abe4f096881221856ce21fcbf50ad385983bc710423c811991d7

File name

ksc.exe

File size

241.00 KB (246784 bytes)

Первым делом открываем файл в дизассемблере и видим entrypoint, который встречает нас полезными инструкциям, разбросанными по памяти. Об этой обфускации мы писали в предыдущей статье. Если же мы обратим внимание на секции, то увидим, что их названия отличаются от привычных .text, .rdata и подобных. Их названия сформированы по следующему шаблону: sect_<num>, где num — последовательный номер секции. Помимо нестандартных названий видим секции, физический размер которых равен 0, при том, что виртуальные размеры похожи на реальные. Также флаги всех секций, кроме последней, не выделяются и выглядят так, как если бы упаковки не было. Все эти особенности свойственны пакеру Obsidium, и они представлены на картинке ниже:

Характерные особенности пакера Obsidium
Характерные особенности пакера Obsidium

Шифрование

Но прежде чем начать наш путь в распаковке, пройдемся по использованным алгоритмам шифрования. Упаковщик использует два алгоритма:

  • Extended Tiny Encryption Algorithm (XTEA);
  • AES в режиме счетчика.

Tiny Encryption используется для расшифровки внутренних функций, ответственных за распаковку полезной нагрузки. Ключ состоит из четырех чисел размером 32 байта. Хранится в зашифрованном виде. Для расшифровки используется результат алгоритма контрольной суммы Adler-32 на байтах как самой функции расшифровки, так и зашифрованных данных. Для получения ключа результат контрольной суммы “ксорится” с каждым зашифрованным значением. Интересной особенностью является то, что сумма считается чанками по 5550 байт. После суммирования каждого чанка применяется модуль 0xfff1 (65521).

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

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


Начало работы. Первые шаги

Переходим к распаковке. Помимо инструкций файл не содержит никакой полезной информации в открытом виде - вся зашифрована в несколько итераций. А для достижения хоть чего-то интересного, сначала необходимо пройти три одинаковых итерации с расшифровкой последующего кода. Каждая итерация представляет собой комбинацию алгоритмов adler32 и XTEA. Результат суммы Adler32 используется для ксора с зашифрованным ключом для XTEA. На картинке ниже представлен код, отвечающий за подсчет контрольной суммы. Здесь обфускация уже снята, однако в оригинале код выглядит так, как это показано на второй картинке. Для удобства восприятия на скриншотах далее будет представлена версия без множественных инструкций jmp,что позволит сосредоточиться лишь на полезных опкодах.

Adler32 – первый цикл на нашем пути (деобфусцированный вид)
Adler32 – первый цикл на нашем пути (деобфусцированный вид)
Adler32 “вживую”
Adler32 “вживую”

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

XTEA и выход из цикла (деобфусцированный вид)
XTEA и выход из цикла (деобфусцированный вид)

Для обхода такой итерации здесь следует воспользоваться аппаратными точками останова, так как они не изменяют код, байты которого используются для формирования ключа расшифровки. После прохождения трех таких итераций (Adler32 + XTEA), которые отличаются друг от друга крайне небольшими изменениями, мы встретим первую за это время инструкцию call, которой также предшествует множество pop инструкций. Вызываемая функция расшифровывает первую часть функций и данных, необходимых для дальнейшей распаковки исполняемого файла: AES, crc32, aplib и другие. Также здесь встречаются функции обнаружения отладчика, которые вызываются сразу при расшифровке. Здесь можно выделить то, что на эти функции не распространяется обфускация. Таким образом еще в отладке можно отличить , находитесь ли вы в коде, который реализует основную логику распаковки или вы перешли во внутренние вспомогательные функции. Позже мы повстречаем еще одну похожую рутину с расшифровкой большого количества функций, где нам встретятся и дополнительные меры по обнаружению отладчика, и дополнительная функциональность, нужная для распаковки итогового файла. Пропускаем эту функцию.

Далее происходит еще два цикла того же самого шифрования XTEA, которому предшествует один цикл с Adler32. Небольшое изменение здесь касается лишь того, что первое выполнение XTEA вынесено в отдельную функцию, к которой идет обращение через инструкцию call. Второй проход также завершается множеством инструкций pop, после которых наблюдаем уже интересный для нас вызов call.


Встречаем полезные инструкции

На картинке ниже изображен код, который мы встречаем после прохождения вышеописанного пути. Главной задачей первой из этих двух функций – это выполнить распаковку исполняемого файла. Для удобства назовем эту функцию "unpack" (как и помечено на изображении). Опишем её подробнее.

Вызовы более полезных для нас функций (деобфусцированный вид)
Вызовы более полезных для нас функций (деобфусцированный вид)

Функция Unpack

Первая особенность касается вызова API-функций. Для вызова импортируемых функций существует структура, которая передается через регистр r12. В ней нам интересны два поля, содержащие указатели на функции. Первый указатель помогает отыскать адрес WinAPI-функции, второй содержит адрес найденной функции. Для поиска адреса необходимой функции используется контрольная сумма (crc32) от имени нужной импортируемой функции.

Пример вызова WinAPI-функций (вывод hex rays от деобфуcцированного кода)
Пример вызова WinAPI-функций (вывод hex rays от деобфуcцированного кода)

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

Интересные инструкции call (деобфусцированый вид)
Интересные инструкции call (деобфусцированый вид)

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

 
import construct as C 
section_header = C.Struct(
  "offset" / C.Int32ul,
  "vaddr" / C.Int32ul,
  "psize" / C.Int32ul,
  "size" / C.Int32ul,
  "rights" / C.Int32ul,
  "flags" / C.Int32ul,
)

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

Секция лежит в открытом виде:
---------------------------------
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
---------------------------------

Секция зашифрована и запакована:
---------------------------------
| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---------------------------------

Секция только зашифрована:
---------------------------------
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---------------------------------

После расшифровки всех секций они записываются по порядку в уже существующие секции, которые называются в формате "sect_$i".

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

Пример распакованной нагрузки без секции импортов
Пример распакованной нагрузки без секции импортов

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

Информация об импортируемых запакованным исполняемым файлом функциях хранится в отдельном чанке данных вместе с кодом для его парсинга. Структура хранения импортируемых функций отчасти похожа на таблицу импортов в PE-заголовке. Главной особенностью является то, что вместо имени функции хранится ее CRC32 сумма. Кусочек данных, где происходит обработка функций импорта, можно опознать по названиям dll, которые находятся в конце блока распакованных данных как на картинке ниже.

 Названия библиотек в конце кусочка расшифрованных данныхНазвания библиотек в конце кусочка расшифрованных данных
Названия библиотек в конце кусочка расшифрованных данных

Структура хранения импортируемых функций выглядит так:

 
import construct as C 
func_st = C.Struct(
  "type" / C.Int8ul,
  "b_1" / C.Int8ul,
  C.Padding(2),
  "w_4" / C.Int16ul,
  "func_name_crc" / C.Int32ul,
  "offset_e_table" / C.Int16ul,
)
lib_st = C.Struct(
  "func_count" / C.Int32ul,
  "dw_4" / C.Int32ul,
  "dw_8" / C.Int32ul,
  "dw_c" / C.Int32ul,
  "dw_10" / C.Int32ul,
  "lib_name_offset" / C.Int32ul,
  "func_st_offset" / C.Int32ul,
  "funcs" / C.Pointer(C.this.func_st_offset,
    C.Array(C.this.func_count, func_st)),
  "libname" / C.Pointer(C.this.lib_name_offset,
    C.RepeatUntil(C.obj_ == 0, C.Byte)),
)
libs_st = C.Struct(
   "count" / C.Int32ul,
   "libs" / C.Array(C.this.count, lib_st),
)

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


Завершаем отладку

Вернувшись из функции unpack, проходим вторую инструкцию call, затем еще раз знакомый паттерн расшифровки XTEA. После этого встретим инструкцию jmp, у которой в качестве операнда будет регистр - в нашем случае rax. Это и будет адрес точки входа. Также после выхода из функции unpack можно установить точку останова на область памяти с кодом и просто нажать кнопку ”Выполнить”. Так программа остановится на истинной точке входа.



Способы антиотладки и особенности ее реализации

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

Начнем с проверки запущенных процессов по имени исполняемых файлов. Obsidium ищет следующий список процессов:

  • win64_remotex64.exe
  • idaq64.exe
  • idag64.exe
  • msvsmon.exe
  • fdbg.exe
  • x64_dbg.exe
  • windbg.exe

Из любопытного здесь можно найти два процесса: msvsmon.exe и fdbg.exe. Первый является стандартным отладчиком в Visual Studio, однако нечасто применяется ИБ-специалистами и хакерами для анализа бинарных файлов. Второй исполняемый файл – довольно старый отладчик FDBG. Его последнее обновление вышло в 2013 году.

Кроме процессов пытается искать открытые окна:

  • WinDbg
  • OllyDbg
  • x64_dbg
  • "IDA "

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

Завершая поиск артефактов конкретных отладчиков, добавим TitanHide, который на уровне ядра скрывает отладчик, перехватывая определенные системные вызовы. При своей работе он создает устройство "\\.\TitanHide", и для обнаружения пакер пытается открыть это устройство.

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

Вызов DbgBreakPoint необходим для возвращения в отладчик ядра. Однако пакер в начало вызова функции ставит инструкцию ret, игнорируя вызов. Таким образом он старается исключить возможность использования отладчика на уровне ядра.

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

1. Ограничение по количеству: четырех точек в архитектуре Intel x86 крайне мало для отладки сложного пакера;

2. Obsidium также может отловить использование аппаратных точек останова и завершить работу после этого.

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



Полезные советы для ручной распаковки

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

1. Включаем в Scylla Hide профиль Obsidium.

2. Для достижения первой инструкции call нам необходимо пройти три блока расшифровки данных. Это можно отследить по множественным инструкциям pop, идущим следом друг за другом. А чтобы пропустить циклы, пользуемся аппаратными точками останова. Сразу после третьего блока произойдет вызов функции, которая расшифровывает большое количество функций. Пропускаем ее и идем дальше до функции unpack, которой предшествуют множество pop инструкций.

Инструкция unpack
Инструкция unpack

3. Пропускаем ее с помощью программной точки останова, предварительно отключив все аппаратные. Это нужно для того, чтобы обойти антиотладку.

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

5. Чтобы добраться до точки входа, ставим на секцию с кодом точку останова и запускаем. Отладчик остановится именно на entrypoint.

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