Вторая статья небольшой серии о защите, которая используется для сокрытия алгоритма приложения. В прошлой статье мы разобрали основные части защиты и собрали тестовое приложение. Здесь познакомимся со структурой обработчиков команд и попробуем поотлаживать и раскодировать исполняемый файл.
Описание работы VMProtect
Рассматриваемая защита имеет ряд функций, которые здорово портят жизнь реверс-инженеру. Например:
Создание полиморфных обработчиков для одной и той же программы;
Однонаправленное кодирование команд процессора;
Чтобы продемонстрировать эти проблемы, запакуем еще раз файл, который использовали для первой части статьи.
Дизассемблированный граф выглядит так:
В этот раз нам повезло, процессинг файла прошел успешно и все обработчики команд виртуальной машины VMP были обработаны корректно для построения графа. В первой части статьи исполняемый граф был разбит на отдельные команды, которые не могли быть использованы для графического представления.
Чтобы проанализировать приложение, нужно понимать, как строится код VMP и как он обрабатывает различные команды. Приведем в качестве шаблона следующий псевдокод.
pusha ; сохранить все регистры
push 0 ; установка начального значения
mov esi, [esp+x+var] ; esi = указатель на VM байт код, адрес может меняться за счет x и var
mov ebp, esp ; так как VMProtect использует стековую виртуальную машину, то ebp = VM "stack" указатель
sub esp, 0C0h
mov edi, esp ; edi = область памяти, где находятся регистры общего назначения
Сместить обработчик:
add esi, [ebp+0]
Выбрать следующую команду:
mov al, [esi]; читаем байт байткода в EIP виртуальной машины
movzx eax, al
sub esi, -1; смещаем значение EIP виртуальной машины
jmp ds:VMHandlers[eax*4] ; выполняем обработчик команды
Теперь тоже самое, но на графе:
Финальная часть, которая решает, какой будет следующая команда и каким обработчиком эту команду выполнять:
Теперь, когда мы разобрались с основным конвейером, можно начинать отладку.
Отладка VM
Для отладки будем использовать x64dbg, на сегодняшний день это самый активно развивающийся отладчик для ОС Windows (помимо WinDBG). Вообще можно пользоваться любым отладчиком, лишь бы вы могли удобно видеть регистры и память, с которой работает приложение. Загрузим приложение в отладчик и встанем на первую команду протектора:
Выделим основные точки для наблюдения, чтобы в дальнейшем можно было восстановить оригинальный алгоритм приложения. Если опустить все подробности работы виртуальной машины, то нам необходимо сейчас определить, где находится в коде так называемый main_loop. Его достаточно просто найти, если несколько раз протрассировать приложение:
На рисунке пунктирной красной рамкой выделен main loop. Это основной цикл, который работает с байткодом, расположенным по адресу из регистра ESI. EIP для виртуальной машины является AX регистр. В него помещаются идентификаторы обработчиков. В итоге, чтобы получить развернутый листинг приложения, нам нужно собрать номера вызываемых обработчиков. Сделать это можно либо используя приложение, которое будет самостоятельно парсить сегмент упакованного исполняемого файла, либо нужно установить условный breakpoint, который будет регистрировать заданные данные. Будем использовать второй метод и запишем в лог отладчика все номера вызванных хэндлеров.
Для начала определим точку, где индекс хэндлера можно получить уже после вычислений:
Теперь откроем меню "breakpoints" и установим вот такие значения:
Настройка достаточно проста, поле "Log Text" используется для занесения данных в лог отладчика. Формат строки, которая заполняется из регистров или локальных переменных отладчика выглядит так: {формат данных:объект откуда взять данные}
. В нашем случае мы просматриваем регистр eax
и локальную переменную $breakpointcounter
.
Поле Command Text
позволяет выполнять операции автоматически по достижению адреса точки останова. В нашем случае мы ничего не делаем, поэтому команда просто продолжает выполнение приложения.
Посмотрим на результат:
Итак, у нас есть индексы обработчиков и их количество — 669. Согласитесь, разбирать такое количество обработчиков достаточно трудоемкий процесс, однако уникальных индексов может быть гораздо меньше. Попробуем это выяснить. Для фильтрации будем использовать notepad++ и его функцию замены текста:
Для замены использовалась следующая регулярка: ^(.*?)$\s+?^(?=.*^\1$)
Итого у нас 53 уникальных индекса для хэндлеров. Уже лучше, перейдем к следующему этапу.
Сбор команд алгоритма
К сожалению, на этом этапе придется попрощаться на время с отладчиком и заняться программированием. Основная наша задача будет получение общего листинга всех обработчиков приложения, которые вызываются последовательно из main loop. Зачем это нужно? Чтобы собрать алгоритм воедино и попробовать его оптимизировать для разбора.
Скрипт, который нам поможет собрать обработчики в единую последовательность команд:
import pefile
# загружаем файл
pe = pefile.PE(filePath)
# помещаем в память
image = pe.get_memory_mapped_image()
# смещение до таблицы хэндлеров
baseOffset = 0xB400
# Всего 255 хэндлеров в исполняемом файла
handlers = []
for i in range(255):
offset+=4
handlers.append(image[offset])
# собираем байткод хэндлеров
for h in handlers:
md = Cs(CS_ARCH_X86, CS_MODE_32)
for i in md.disasm(h, 0x1000):
print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))
Фрагмент получаемого листинга:
Следующий этап — оптимизация кода и удаление заведомо избыточного кода. Выполняется это либо вручную, либо с привлечением рекомпиляции через промежуточный язык программирования. Для этих действий можно использовать вот этот проект. Предлагаем читателям попробовать сделать это самостоятельно.
Автор статьи — Александр Колесников.
Статья приурочена к курсу "Reverse-Engineering. Basic".
Приглашаем также посетить открытый вебинар на тему «Эксплуатация уязвимостей в драйвере. Часть 2»: разберём уязвимость переполнения пула памяти и уязвимость типа type confusion; напишем эксплойт.