VMP Reverse engineering virtual machine protected binaries (RUS)

OSPFv3

Мудрец
Сообщения
236
Реакции
220
Автор: Souhail Hammou
Перевод: OSPFv3
September 29, 2015


В обфускации кода виртуальная машина - это механизм, используемый для выполнения набора команд, отличного от того, который используется машиной, выполняющей программу. Например, виртуальная машина может поддерживать выполнение набора команд ARM на 32-разрядной архитектуре x86. Виртуальные машины, используемые для обфускации кода, полностью отличаются от обычных виртуальных машин, способных запускать операционные системы (например, VMware ...), они очень специфичны для выполнения ограниченного набора инструкций для достижения своей цели, игнорируя все другие задачи, которые выполняют виртуальные машины

Задача ревёрсинга защиты виртуальной машины была бы простой, поскольку бы архитектура, которую она выполняет, нам была бы известна. Таким образом, поиск опкодов в справочнике набора инструкций архитектуры займет небольшое количество времени. К сожалению, большинство современных обфускаторов кода используют кастомный набор команд. Другими словами, каждой команде присваивается кастомный опкод (часто выбираемый случайным образом) и пользовательский формат, который обязывает реверс-инженера вручную декодировать каждую команду, а это утомительно. Для примера давайте рассмотрим разницу между 32-разрядным набором команд x86 и кастомным набором команд, над которым мы будем работать в этой статье.

1655383658297.png

Ясно, что каждая из этих инструкций перемещает байт из ячейки памяти, указанной вторым операндом, в регистр в первом операнде. Однако двоичное представление двух инструкций отличается в основном в части опкода, в которой 0x56 был совершенно случайным выбором. Второй байт в обеих инструкциях представляет два регистра, соответствующих опкоду, где бит (4 бита) указывает на регистр.

Прежде чем мы перейдем к ревёрсу примера, важно знать, как обфускация кода работает за кулисами. Виртуальная машина сначала запускается, устанавливая свое “адресное пространство” в виртуальном адресном пространстве выполняющегося процесса. Другими словами, он выделяет необходимое пространство для своей памяти, стека и регистров(Register File или вот(25-ая страница)), а затем начинает выполнять код. Выполнение кода происходит в рамках так называемого цикла виртуальной машины. Внутри этого цикла виртуальная машина выполняет роль процессора, анализируя каждый из своих предопределенных опкодов и их операндов, а затем используя изначальную архитектуру для выполнения инструкций. Итерация по циклу виртуальной машины будет продолжаться до тех пор, пока не будет достигнут специальный опкод выхода.

Переходим к разбору примера.

Для примера я потратил время на создание виртуальной машины с кастомным набором инструкций на языке Си. Полный исходный код на github доступен прямо в конце этой статьи. Как вы могли догадаться, виртуальная машина сама по себе не будет полезна для этой статьи без опкодов, которые она может выполнять, поэтому я написал небольшую программу(crackMe), которая запрашивает у пользователя пароль и выполняет его проверку.

Как упоминалось во введении, эта виртуальная машина использует кастомный набор команд и начинается с чтения опкодов из файла в “адресное пространство”, установленное виртуальной машиной после фазы инициализации.

Давайте дадим программе случайный пароль и посмотрим, что будет:
1655383924420.png
Выпала “гадина”! Поэтому наша цель - найти правильный пароль, чтобы программа выдала правильное сообщение. Давайте начнем с изучения файла opcodes (vm_file) с помощью HEX редактора или вот на гите:
1655383961713.png
Мы уже можем увидеть некоторые строки в файле vm_file, такие как “ Right pass!”, “ Wrong pass!” и “ Password:” Для того, чтобы определить, где находятся инструкции и данные, необходим ревёрс виртуальной машины. Так что давайте запустим IDA и начнем ревёрсить.

После открытия exe в IDA мы переходим непосредственно к циклу виртуальной машины, расположенному по виртуальному адресу: 0x00401334. Представление графика показывает нам функцию значительного размера, но мы можем крэкнуть нашу программу, если подойдем к ней правильно.
1655384043690.png
Давайте посмотрим, какие инструкции выполняются при входе в функцию:

Код:
push    ebp
push    edi
push    esi
push    ebx
sub     esp, 2Ch
mov     esi, [esp+3Ch+arg_0]
mov     ebx, [esp+3Ch+arg_4]
mov     ax, [ebx+0Ah]
lea     ebp, [esi+1200h]
loc_40134D: ; This is where the loop starts
movzx edx, ax
mov     cl, [esi+edx]
lea     edx, [eax+1]
mov     [ebx+0Ah], dx
sub     ecx, 10h
cmp     cl, 0E1h     ; switch 226 cases
jbe     short loc_40136C
Эта инструкция “mov cl, [esi+edx]” сообщает нам, что программа считывает байт и помещает его в CL. Ясно, что CL не содержит ничего, кроме опкода для выполнения. Для доступа к этому опкоду используются два регистра ESI и EDX. Мы можем ясно видеть, посмотрев на предыдущие инструкции, что EDX содержит word (16-разрядное значение), в то время как ESI содержит DWORD (32-разрядное значение). Таким образом, ESI фактически указывает на code section виртуальной машины, тогда как DX на самом деле является значением instruction pointer(IP) нашей виртуальной машины (индекс опкода в файле).

Сразу после считывания байта мы замечаем inc и перемещение регистра DX в [EBX+0Ah], тут виртуальная машина выделила место для своих регистров. К настоящему времени мы знаем, что EBX указывает, где находятся регистры, а ESI - где находятся данные файла в памяти.

Непосредственно перед сравнением мы можем заметить, что компилятор выполнил оптимизацию, и теперь код вычитает 0x10 из каждого значения опкода перед обращением к switch table.
Код:
loc_40136C:
movzx ecx, cl
jmp     ds:switchTable[ecx*4] ; switch jump
switch table довольно велика, и лучше выполнять программу динамически, чтобы получить вычисленный адрес. Вы можете сделать это, запустив программу под управлением IDA win32 debugger или под управлением OllyDbg, x32dbg.

Первая инструкция.

Первый switch jump приводит нас к этой небольшой рутине:
1655384216136.png

Итак, в основном мы сейчас находимся в “0x18 :”, поскольку компилятор решил провести некоторую оптимизацию и добавить операцию вычитания. Теперь, если вы вернетесь назад и проверите HEX вид vm_file, вы заметите, что самый первый байт равен 0x18. Этот опкод, по-видимому, нуждается в некоторых операндах, поэтому виртуальная машина считает еще один байт, используя регистр DX. Затем instruction pointer(IP) виртуальной машины на [EBX +0Ah] обновляется до EAX +2, это в основном означает, что IP переместился на следующий байт. После этого прочитанный байт сравнивается с 3, и если он превышает его, то программа выходит из цикла, вы можете увидеть это как исключение для виртуальной машины. В нашем примере мы в порядке, поскольку второй байт в файле равен 0x1. Таким образом, мы не будем совершать прыжок и приземлимся здесь:
1655384250138.png
Позвольте мне напомнить вам, что EBX - это указатель на память, в которой хранятся регистры, поэтому в нашем случае первая команда инициализирует [ebx+1*2] значением 0, которое является вторым регистром в массиве регистров.

Итак, теперь у нас есть достаточно информации, чтобы сделать вывод, что наша виртуальная машина содержит 4 регистра; давайте назовем их R0, R1, R2 и R3.

Затем остальная часть кода считывает 2 байта из данных, загруженных из файла (0x250 в big endian), и сохраняет их в регистре R1. В результате instruction pointer(IP) виртуальной машины увеличивается, указывая на следующую инструкцию, которая находится со смещением 0x4 файла (проверьте шестнадцатеричный дамп). Наконец, переход снова приведет нас к проверке цикла в loc_40134D.

До сих пор мы могли знать только то, что делает первая команда, и это всего лишь простая команда перемещения, которая перемещает абсолютное значение в регистр. Инструкция может быть написана следующим образом:
MOV R1,250h

Вторая инструкция
Давайте теперь посмотрим, что делает следующий опкод 0xFA:
1655384359799.png
Первый блок кода идентичен тому, что мы видели в инструкции MOV. По-видимому, этот опкод принимает регистр в качестве операнда, и в нашем случае этот регистр равен R1 (0x1). Что он делает дальше, так это обращается к регистру по адресу [EBX+0Ch]. Мы знаем, что этот регистр не является одним из R0, R1, R2 или R3, потому что R3 расположен по адресу EBX+6. Мы также знаем, что это не instruction pointer, поскольку IP находится в EBX+0Ah. Итак, чтобы выяснить, что на самом деле делает этот регистр, нам нужно вернуться назад и проверить инициализацию этих регистров в основной функции:

.text:00402703 mov word ptr [eax+0Ch], 256

Возвращаясь к нашей процедуре, мы замечаем, что регистр уменьшается, а затем проверяется на 0xFFFF(65,535). Поскольку он был инициализирован равным 256, этот регистр не будет принимать 0xFFFF(65,535) до тех пор, пока его значение перед декриментом не станет равным 0. Если это действительно так, то виртуальная машина выходит из цикла. Однако, поскольку мы впервые выполняем эту процедуру, мы уверены, что [EBX+0Ch] равно 255.
Следующие две инструкции после перехода считывают значение R1 (0x250) и сохраняют его в регистре DX. Теперь перейдем к интересной инструкции:
mov [esi+eax*2+1000h], dx

Если вы помните, ESI указывает на то, где хранятся код и данные. Кроме того, ESI+1000h находится на расстоянии 4 КБ от этого местоположения. В результате мы можем предположить, что ESI+1000h указывает на другой “section” выделенного “адресного пространства” виртуальной машины.

Если мы хотим посмотреть на код, выполненный выше, в виде псевдокода, то он будет выглядеть следующим образом:

Код:
    WORD section[256];
    […]
    section[ –reg ] = R1;
Это похоже на стек, в котором сначала уменьшается stack pointer(SP), а затем сохраняется значение. Мы можем с уверенностью предположить, что опкод 0xAF представляет собой команду PUSH. Таким образом, команда, которую только что выполнила виртуальная машина, может быть записана следующим образом: PUSH R1.

Теперь мы знаем, что регистр в [EBX+0Ch] является stack pointer’ом(SP) виртуальной машины, а раздел представляет собой не что иное, как стек размером 256*sizeof(uint16_t). Кроме того, если вы попытаетесь сравнить stack pointer(SP) виртуальной машины с указателем стека машины архитектуры x86, вы увидите, что указатель стека виртуальной машины на самом деле является индексом массива, тогда как SP машины x86 является адресом.

Третья инструкция.
Следующий опкод - 0xC2, давайте посмотрим, что он делает:
1655384548937.png
Этот опкод, по-видимому, обращается к стеку и считывает его верхний WORD, но перед этим проверяет, пуст ли стек. Если он действительно пуст, он выходит из цикла виртуальной машины (исключение). Поскольку значение уже было введено, мы знаем, что стек не пуст! После того, как WORD был прочитан в DX, указатель стека увеличивается. Мы знаем, что значение DX равно 0x250, и это часть раздела, содержащего код и данные. Следовательно, “адрес” (offset) не должен превышать 0x1000, иначе виртуальная машина будет выполнять чтение / запись в пространство стека. Следующее, что делается, - это использование ESI + DX в качестве указателя на строку, строка впоследствии печатается с помощью printf. В нашем примере строка, хранящаяся со смещением 0x250 в файле vm_file, имеет вид: “Password:”, и это то, что printf покажет на экране.

Мы можем сделать вывод, что опкод 0xC2 ожидает, что offset строки будет запушен в стек, извлекает его, а затем печатает.

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

Ищем рабочий пароль.

Вот краткое пошаговое руководство о том, как получить правильный пароль.

При открытии vm_file в HEX редакторе байты со смещением от 0x80 до 0x17F представляют собой массив из 256 случайно сгенерированных байт. Давайте назовем это рандомом. Каждый байт из пароля, введенного пользователем, фиксируется случайным [байтом], а затем сравнивается с предопределенным массивом байтов, хранящихся со смещением 0x240.

Я закодил сценарий решения на C, который вы можете найти в приведенных ниже ссылках. Скрипт выполняет поиск и выводит действительный пароль для программы:

Заключение.
Задача ревёрса кода виртуальной машины - непростая задача, особенно если обфускация основана на кастомном наборе команд. Пример виртуальной машины, над которым мы работали, реализует больше функций, таких как сравнение (использует выделенные биты флага: ZF и CF), условные и безусловные переходы, доступ к памяти... и т.д. Поэтому я предоставляю вам самим открывать их для себя. Чтобы сделать это, вы можете просто получить доступ к исходному коду виртуальной машины (ссылка в ссылках ниже), выполнить поиск HEX кода инструкции, и вы найдете мнемонику инструкции с примером в комментарии чуть выше инструкции case.

Доп. информация.
 

Sp1n

Мудрец
Сообщения
116
Реакции
72
Не закончено, обрывается на середине:

> мы добрались только до инструкции, где печатается “Password :”

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

Семпла(бинаря) нет, толку от сурков вирты никакого..
 

OSPFv3

Мудрец
Сообщения
236
Реакции
220
Не закончено, обрывается на середине:

> мы добрались только до инструкции, где печатается “Password :”

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

Семпла(бинаря) нет, толку от сурков вирты никакого..
Что было в той статье, то я и предоставил.
 

plutos

_Вечный_Студент_
Мудрец
Сообщения
195
Реакции
714
Не закончено, обрывается на середине:
переводчик тут совершенно ни причем.
Он сделал, кстати, довольно неплохой перевод из того, что было под рукой.
Автор статьи из марокко и его english весьма условный и местами туманный.
А в переводе на русский читается неплохо, в чем заслуга переводчика.
Так что все претензии нужно адресовать мароканцу, но его посту уже 7 лет и надежды на ответ очень мало.:mad:
 

OSPFv3

Мудрец
Сообщения
236
Реакции
220
переводчик тут совершенно ни причем.
Он сделал, кстати, довольно неплохой перевод из того, что было под рукой.
Автор статьи из марокко и его english весьма условный и местами туманный.
А в переводе на русский читается неплохо, в чем заслуга переводчика.
Так что все претензии нужно адресовать мароканцу, но его посту уже 7 лет и надежды на ответ очень мало.:mad:
Надо мне будет чутка позже вернуться к IDA PRO BOOK))
Мне там канал передали, так что могу делать, что хочу.

Там уже 7 глав готово, думаю также продолжить с книгой, чтобы не перезаписывать 7 глав, а позже тогда уже новый плейлист запустить, но попробовать там всё делать как делает Яша, но только с GHIDRA и без тараканов.
 
Верх Низ