15—17 декабря 2012 года прошли отборочные соревнования под названием PHDays CTF Quals. Более 300 команд боролись за право участия в конкурсе PHDays III CTF, который состоится в мае 2013 года в рамках международного форума PHDays III.
Исполняемый файл представляет собой MBR-буткит, использующий технологию аппаратной виртуализации (Intel VT-x). В связи с этим мы решили сразу предупреждать пользователей, что приложение может нанести вред системе, и что его нужно запускать на виртуальной машине или эмуляторе.
Рисунок 1. Предупреждение и лицензионное соглашение
Модуль использует несколько приемов, усложняющих его анализ. Во-первых, он написан на С++ с использованием STL, ООП и виртуальных функций. Поэтому все вызовы функций являются неявными и используют таблицы виртуальных методов.
Рисунок 2. Вызовы виртуальных методов в IDA Pro
Второй подход, который усложняет анализ данного файла, – чтение и запись файлов. Все операции с жестким диском проводятся напрямую через SCSI-контроллер. Вместо вызовов стандартных ReadFile/WriteFile мы использовали функцию DeviceIoControl с контрольным кодом SCSI_PASS_THROUGH_DIRECT, которая позволяет взаимодействовать с жестким диском на более низком уровне.
Также все файлы, находящиеся в ресурсах, зашифрованы с использованием 256-битного ключа.
Теперь перейдем к описанию скрытой файловой системы. Ее структура достаточно проста. Рост системы происходит с конца и записывается за 2 сектора до конца жесткого диска. Первый DWORD содержит количество файлов в файловой системе, сложенное операцией XOR с константой 0x8FC54ED2. Далее идет директория с описанием файлов:
struct MiniFsFileEntry
{
DWORD fileIndex;
DWORD fileOffset;
DWORD fileSize;
};
Индекс файла – это просто некоторая константа, представляющая данный файл в файловой системе (вместо имени). Смещение до файла измеряется в байтах относительно начала файловой системы.
Рисунок 3. Структура файловой системы MiniFs
Выяснив, какими инструментами мы можем отлаживать данный код, принимаемся за дело. Первая часть MBR очень проста, и ее анализ не должен вызвать никаких проблем. Единственное, что она делает, – читает вторую часть нашей MBR (Extended MBR) с жесткого диска вызовом функции 0х42 обработчика прерывания 0х13 BIOS и записывает ее по адресу 0x7e00 (сразу после первой части загрузчика). Это действие необходимо, поскольку BIOS копирует в память всего 512 байт загрузчика, а наш код превышает этот размер.
Попав на расширенную часть загрузчика, опытный специалист сразу заподозрит неладное, а именно то, что загрузчик обфусцирован.
Рисунок 4. Сравнение исходников загрузчика с результатом анализа IDA Pro
Вся сложность обфускации состоит в вызовах функций, так как они происходят неявно. С самого начала в регистр AX заносится адрес функции, которая ищет вызываемую функцию в специальной таблице, сопоставляющей индекс функции (двухбайтовая константа) с ее смещением относительно этого поля таблицы. Данная функция берет адрес возврата и читает WORD, который отвечает за индекс функции. Далее производится поиск смещения в таблице и передается управление на эту функцию. В самом конце происходит возврат управления за константу с индексом вызываемой функции (адрес возврата + 2).
Рисунок 5. Таблица функций в MBR
Рисунок 6. Схематичное представление алгоритма обфускации MBR
Сам код загрузочного сектора достаточно простой:
Стоит упомянуть, что для шифрования загрузчика и тела гипервизора использовался набор байт из BIOS эмулятора Bochs. Таким образом, задание получилось заточенным под данный эмулятор. Это было сделано по нескольким причинам. Во-первых, отладка аппаратной виртуализации Intel VT-x возможна только на реальной машине или на Bochs (начиная с версии 2.4.5. Первый способ в рамках PHDays CTF Quals очень затруднителен. Таким образом, мы с самого начала были привязаны к использованию данного эмулятора. Во-вторых, нам не хотелось, чтобы данное задание можно было исследовать только при помощи статического анализа, поэтому было решено, что шифрование с использованием ключа из BIOS будет принуждать игроков применять динамический анализ. В-третьих, мы решили подстраховаться: в случае случайного запуска программы на реальной машине загрузчик не был бы расшифрован, и управление передалось бы на оригинальную загрузочную запись, что не позволило бы повредить систему.
Для облегчения работы игроков мы заранее выложили информацию о том, что для решения одного из заданий им понадобится сборка Bochs, а также рабочий образ операционной системы.
Как упоминалось ранее, отладка приложения, использующего аппаратную виртуализацию Intel VT-x, может быть осуществлена только на реальной машине или на эмуляторе Bochs (начиная с версии 2.4.5), но на этом проблемы не заканчиваются. Стандартная сборка эмулятора не поддерживает аппаратную виртуализацию. Именно поэтому нами была скомпилирована специальная сборка Bochs, ссылку на которую мы дали в первой подсказке к заданию.
Основная задача загрузчика гипервизора — перенести тело гипервизора выше первого мегабайта и передать ему управление. Однако по пути он делает несколько нетривиальных операций, о которых стоит рассказать отдельно.
На вход загрузчик гипервизора принимает несколько параметров. Среди них адрес, на который его загрузили. Этот адрес используется в качестве базы сегмента кода, которая устанавливается дальним переходом (far jump).
Далее инструкцией CPUID проверяется, что код исполняется на Intel-системе (функция 0), и что данная система поддерживает аппаратную виртуализацию (функция 1). Проверка происходит следующим образом: если при вызове CPUID в регистре EAX лежит значение 1, то на выходе в бите 5 регистра ECX будет находиться флаг VMX. Если флаг взведен, то виртуализация поддерживается. Чтобы проверить, заблокирована ли виртуализация на ранних этапах загрузки (BIOS), необходимо прочитать регистр MSR с номером 0x3A. Если на выходе инструкции RDMSR в регистре EAX установлен бит 0 и сброшен бит 2, то виртуализация заблокирована.
На следующем этапе вызывается функция чтения карты памяти системы. Это достигается путем вызова в цикле прерывания 0x15 с параметром 0xE820 в регистре EAX. При этом в буфере сохраняется набор записей, описывающих области памяти: база, длина, тип, дополнительный тип (если поддерживается BIOS). Дальше полученная карта изучается на наличие участка свободной памяти для тела монитора. Сам монитор на данный момент занимает около 20КБ пространства, но сохраняется 2МБ (для удобства работы с памятью). Если такая память найдена, то она помечается как занятая.
Для того чтобы перенести тело монитора выше первого мегабайта, необходимо перейти из реального режима работы в защищенный или длинный. Поскольку в дальнейшем монитор должен работать в длинном режиме (в документации говорится, что монитор может оставаться и в защищенном режиме, но большого смысла в этом нет), происходит переход в длинный режим. Для этого необходимо выполнить несколько условий: подготовить страничные структуры (PML4, PDPT, некоторое количество PD для страниц размером 2МБ), взвести бит PAE в регистре CR4, записать в регистр CR3 адрес таблицы PML4, установить GDTR с дескрипторами сегментов длинного режима, взвести бит LMA в регистре MSR EFER, взвести биты PG и PE в регистре CR0. Если после этого выполнить инструкцию дальнего перехода, то произойдет переключение из реального режима в длинный.
Было замечено, что дизассемблер IDA 6.1 неправильно работает с эмулятором Bochs и после перехода в длинный режим начинает выдавать странные значения (в IDA 6.3 данная ошибка исправлена). Возможно, он самостоятельно вычисляет значения регистров и не обращается к Bochs за соответствующими сервисами. При этом он не способен правильно обработать прямое переключение из реального режима в длинный.
Далее гипервизор копируется на адрес назначения, и ему передается управление.
Главная задача, которая стоит перед исследователем этого кода, — найти адрес, с которого запускаются обработчики выходов из гостевой системы.
Из кода обработчика легко увидеть, что если был совершен выход по инструкции CPUID, а регистр EIP равен определенному значению, то начинается обработка некоторого события. Из значений регистров EAX, ECX, EDX, EBX, ESI, EDI, ESP и EBP происходит заполнение вектора (32 байта), а далее этот вектор проверяется на валидность. Проверка заключается в подстановке вектора (x_0,…,x_31 ) в систему уравнений следующего вида:
Если равенство выполняется, то введенный вектор верен и используется в качестве ключа для расшифровки буфера. Таким образом, игроку нужно решить систему уравнений из 32 уравнений с 32 неизвестными и получить ключ шифрования. Единственное усложнение данной проверки заключается в том, что она написана с использованием инструкций математического сопроцессора (FPU).
В зашифрованном буфере находится еще одна MBR, в которой в открытом виде лежит флаг. Такой загрузчик прописывается на место оригинального, и вызывается принудительная перезагрузка, после чего MBR выводит флаг на экран.
Рисунок 7. Пример выведенного на экран флага
Рисунок 8. Пример тестовой программы
Искренне надеемся, что у нас получилось заинтересовать как участников, так и тех, кто читал данный обзор!