Время на прочтение: 68 минут
Ссылка на скачивание: https://githacks.org/vmp2
Содержание
Прежде чем погрузиться в этот пост, я хотел бы изложить несколько моментов в отношении существующей работы с VMProtect 2, цели этой статьи и моих намерений, поскольку они, кажется, время от времени неправильно истолковываются или искажаются.
Намерения
Несмотря на то, что по VMProtect 2 уже было проведено много исследований, я чувствую, что все еще есть информация, которая не обсуждалась публично, и не было раскрыто достаточное количество исходного кода. Информация, которую я раскрываю в этой статье, направлена на то, чтобы выйти за рамки общего архитектурного анализа, погрузиться гораздо глубже. Уровень, на котором можно было прописывать свои собственные инструкции виртуальной машины с помощью двоичного файла, защищенного VMProtect, а также с легкостью перехватывать и изменять результаты виртуальных инструкций. Динамический анализ, обсуждаемый в этой статье, основан на существующей работе Самуэля Чевета, мое исследование динамического анализа и проект vmtracer - это просто расширение его работы, продемонстрированной в его презентации «Inside VMProtect».
Цели
Этот пост не предназначен для того, чтобы озвучить какие-либо негативные мнения о VMProtect 2, создателях указанного ПО или всех, кто его использует. Я восхищаюсь создателями, которые явно обладают впечатляющими навыками создания такого продукта.
Этот пост также был создан под впечатлением, что все, что здесь обсуждается, скорее всего, было обнаружено частными лицами, и что я не первый, кто обнаружил или задокументировал такие вещи об архитектуре VMProtect 2. Я не собираюсь представлять эту информацию, как будто она является новаторской или ещё что-то, как раз наоборот. Это просто набор существующей информации, дополненной моими собственными исследованиями.
Смиренно представляю вам «VMProtect 2 - Детальный анализ архитектуры виртуальной машины».
Терминология
VIP - Virtual Instruction Pointer, это эквивалент регистра RIP x86-64, который содержит адрес следующей инструкции, которая должна быть выполнена. VMProtect 2 использует регистр RSI для хранения адреса следующего указателя виртуальной инструкции. Таким образом, RSI эквивалентен VIP.
VSP - Virtual Stack Pointer, это эквивалент регистру x86-64 RSP, который содержит адрес стека. VMProtect 2 использует регистр RBP для хранения адреса указателя виртуального стека. Таким образом, RBP эквивалентно VSP.
VM Handler - Подпрограмма, содержащая собственный код для выполнения виртуальной инструкции. Например, инструкция VADD64 складывает два значения в стек вместе и сохраняет результат, а также RFLAGS в стеке.
Virtual Instruction - Также известный как «виртуальный байт-код» - это байты, интерпретируемые виртуальной машиной и впоследствии выполняемые. Каждая виртуальная инструкция состоит как минимум из одного или нескольких операндов. Первый операнд содержит код операции для инструкции.
Virtual Opcode - Первый операнд каждой виртуальной инструкции. Это индекс обработчика vm. Размер кода операции VMProtect 2 всегда составляет один байт.
IMM / Immediate Value (немедленные значения) - Значение, закодированное в виртуальную инструкцию, с помощью которой должны выполняться операции, такие как загрузка указанного значения в стек или в виртуальный регистр. Виртуальные инструкции, такие как LREG, SREG и LCONST, имеют IMM.
Transformations - Термин «преобразование», используемый в этом посте, относится конкретно к операциям, выполняемым для расшифровки операндов виртуальных инструкций и записей таблицы обработчиков vm. Эти преобразования состоят из add, sub, inc, dec, not, neg, shl, shr, ror, rol и, наконец, BSWAP. Преобразования выполняются с размером 1, 2, 4 и 8 байт. Преобразования также могут иметь связанные с ними немедленные/постоянные значения, такие как «xor rax, 0x123456» или «add rax, 0x123456».
Вступление
VMProtect 2 - это обфускатор x86 на основе виртуальной машины, который преобразует инструкции x86 в RISC, стековую машину, набор инструкций. Каждый защищенный двоичный файл имеет уникальный набор зашифрованных инструкций виртуальной машины с уникальной обфускацией. Этот проект направлен на выявление очень важных сигнатур, которые есть в каждом бинарном файле VMProtect 2, с целью помочь в дальнейших исследованиях. В этой статье также кратко обсуждаются различные типы обфускации VMProtect 2. Все методы деобфускации специально адаптированы к процедурам виртуальных машин и не будут работать с обычно запутанными подпрограммами, особенно с подпрограммами, в которых есть настоящие JCC.
Обфускация - Deadstore, Непрозрачное ветвление
VMProtect 2 по большей части использует два типа обфускации: первый - это deadstore, а второй - непрозрачное ветвление. В запутанных подпрограммах вы можете увидеть несколько инструкций, за которыми следует JCC, затем еще один набор инструкций, за которым следует еще один JCC. Другая часть непрозрачного ветвления - это случайные инструкции, которые влияют на регистр FLAGS. Вы можете увидеть этих маленьких *бездельников повсюду. В основном это инструкции битового тестирования, бесполезные сравнения, а также инструкции по установке/очистке флагов.
*Примечание переводчика - bugger дословно переводится как бездельник, но может в контексте быть интерпретированно как мужеложец или содомит
Пример непрозрачной обфускации ветвления
В этом примере обфускации непрозрачного ветвления я рассмотрю то, как выглядит непрозрачное ветвление VMProtect 2, другие факторы, такие как состояние rflags, и, что наиболее важно, как определить, смотрите ли вы на непрозрачную ветвь или на легетимный JCC.
Рассмотрим приведенный выше обфусцированный код. Обратите внимание на ветку JNO. Если вы проследуете в эту ветвь в ida и сравните инструкции с инструкциями после JNO, вы увидите, что ветвь бесполезна, поскольку оба пути выполняют одни и те же по значению инструкции.
Если вы посмотрите достаточно внимательно, то увидите, что есть несколько одинаковых инструкций в обеих ветках. Может быть сложно определить, какой код является deadstore, а какой код действительно нужен, однако, если вы выберете регистр в ida и посмотрите все места, в которых он написан до инструкции, которую вы просматриваете, вы можете удалить все эти другие написание инструкций до тех пор, пока не будет прочитан указанный регистр. Теперь вернемся к примеру. В этом случае важны следующие инструкции:
Генерация этих непрозрачных ветвей приводит к дублированию инструкций. Для каждого пути кода есть больше обфускации deadstore, а также непрозрачные условия и другие инструкции, которые влияют на RFLAGS.
Пример обфускации Deadstore
Примечание переводчика - Deadstore, я решил не переводить, потому, что мертвое хранилище звучит ужасно
Deadstore обфускация VMProtect 2 добавляет больше всего мусора в поток инструкций, помимо непрозрачных битовых тестов и сравнений. Эти инструкции бесполезны, их можно легко обнаружить и удалить вручную.
Взгляните сюда:
Давайте начнем сверху, по одной инструкции за раз. Первая инструкция по адресу
Следующая инструкция - POP RAX по адресу
Переходим к следующей инструкции, SHLD - сдвиг с двойной точностью влево, зависимость записи от R11, зависимость чтения от BX.
Следующая инструкция, которая ссылается на R11, - это POP R11 по адресу
Теперь просто повторите процесс для каждой инструкции. Конечный результат должен выглядеть примерно так:
Этот метод не идеален для удаления deadstore обфускации, поскольку в приведенном выше результате отсутствует второй POP RCX. Инструкции POP и PUSH - это особые случаи, которые не должны выводиться из потока инструкций, поскольку эти инструкции также изменяют RSP. Этот метод удаления deadstore также применяется только к обработчикам vm_entry и vm. Он не может быть применён к обычным запутанным подпрограммам как есть. Опять же, этот метод НЕ будет работать с какой-либо запутанной подпрограммой, он специально разработан для обработчиков vm_entry и vm, поскольку в этих подпрограммах нет легитимных JCC.
Обзор - виртуальная машина VMProtect 2
Виртуальные инструкции дешифруются и интерпретируются обработчиками виртуальных инструкций, называемыми «обработчиками vm». Виртуальная машина представляет собой стековую машину на основе RISC с рабочими регистрами. Перед vm-записями зашифрованный RVA (относительный виртуальный адрес) для виртуальных инструкций помещается в стек, и все регистры общего назначения, а также флаги помещаются в стек. VIP расшифровывается, вычисляется и загружается в RSI. Затем в RBX запускается скользящий ключ дешифрования, который используется для дешифрования каждого операнда каждой отдельной виртуальной инструкции. Ключ скользящего дешифрования обновляется путем преобразования его в расшифрованное значение операнда.
Скользящее дешифрование
VMProtect 2 использует скользящий ключ дешифрования. Этот ключ используется для дешифрования операндов виртуальных команд, что впоследствии предотвращает любой вид перехвата, как если бы какие-либо виртуальные инструкции выполнялись не по порядку, скользящий ключ дешифрования станет недействительным, что приведет к недействительности дальнейшего дешифрования виртуальных операндов.
Использование нативных регистров
Во время выполнения внутри виртуальной машины некоторые нативные регистры выделяются для механизмов самой виртуальной машины, таких как указатель виртуальной инструкции и виртуальный стек. В этом разделе я буду обсуждать эти нативные регистры и их использование виртуальной машиной.
Постоянные регистры - регистры специального назначения
Для начала RSI всегда используется для указателя виртуальной инструкции. Операнды выбираются из адреса, хранящегося в RSI. Начальное значение, загружаемое в RSI, выполняется vm_entry.
RBP используется для указателя виртуального стека, адрес, хранящийся в RBP, на самом деле является собственной памятью стека. RBP загружается с RSP до выделения временных регистров.
Это подводит нас к RDI, который содержит временные регистры. Адрес в RDI также инициализируется в vm_entry и устанавливается на адрес, находящийся внутри собственного стека.
В R12 загружается линейный виртуальный адрес таблицы обработчика vm. Это делается внутри vm_entry, и на протяжении всего времени выполнения внутри виртуальной машины R12 будет содержать этот адрес.
R13 загружается с линейным виртуальным адресом базового адреса модуля внутри vm_entry и не изменяется во время выполнения внутри виртуальной машины.
RBX это особый регистр, который содержит скользящий ключ дешифрования. После каждой расшифровки каждого операнда каждой виртуальной инструкции RBX обновляется путем применения к нему преобразования со значением расшифрованного операнда.
Непостоянные регистры - временные регистры
RAX, RCX и RDX используются как временные регистры внутри виртуальной машины, однако RAX используется для очень специфических временных операций над другими регистрами. RAX используется для расшифровки операндов виртуальных инструкций, AL, в частности, используется при расшифровке кода операции виртуальной инструкции.
vm_entry - Вход в виртуальную машину
vm_entry - очень важный компонент архитектуры виртуальной машины. Перед входом в виртуальную машину зашифрованный RVA для виртуальных инструкций помещается в стек. Этот RVA представляет собой четырехбайтовое значение.
После того, как это значение помещается в стек, выполняется jmp, чтобы начать выполнение vm_entry.
vm_entry подвергается обфускации, о чем я подробно объяснил выше. Сглаживая, а затем удаляя deadstore код, мы можем получить хороший чистый вид vm_entry.
Как и ожидалось, все регистры, а также RFLAGS помещаются в стек. Последний push помещает в стек восемь байтов нулей, а не перемещение, как я ожидал вначале. Порядок, в котором происходят push, уникален для каждой сборки, однако последние восемь нулей всегда одинаковы для всех двоичных файлов. Это очень стабильная сигнатура, позволяющая определить, когда закончится общий регистр. Ниже приведены точные последовательности инструкций, которые я имею в виду в этом абзаце.
После того, как все регистры и RFLAGS помещены в стек, базовый адрес модуля загружается в R13. Это происходит в каждом отдельном двоичном файле, R13 всегда содержит базовый адрес модуля во время выполнения виртуальной машины. Базовый адрес модуля также помещается в стек.
Затем расшифровывается относительный виртуальный адрес желаемых виртуальных инструкций, которые должны быть выполнены. Это делается путем загрузки 32-битного RVA в ESI из RSP + 0xA0. Это очень важная сигнатура, и ее можно легко найти. Затем к ESI применяются три преобразования, чтобы получить расшифрованный RVA виртуальных инструкций. Эти три преобразования уникальны для каждого бинарного файла. Однако их всегда три.
Кроме того, следующая важная операция, которая происходит, - это пространство, выделенное в стеке для временных регистров. RSP всегда перемещается в RBP, затем RSP вычитается на 0x140. Затем выравнивается по 16 байтам. После этого адрес переносится в RDI. Во время выполнения VM RDI всегда содержит указатель на временные регистры.
Следующая примечательная операция - загрузка адреса таблицы обработчика vm в R12. Это делается для каждого двоичного файла VMProtect 2. R12 всегда содержит линейный виртуальный адрес таблицы обработчика vm. Это еще одна важная сигнатура, с помощью которой можно легко найти расположение таблицы обработчиков vm.
Затем с RSI выполняется другая операция для расчета VIP. Внутри PE-заголовков есть заголовок, который называется «необязательный заголовок». В нем содержится разнообразная информация. Одно из полей называется «ImageBase». Если в этом поле есть какие-либо биты выше 32, эти биты затем добавляются к RSI. Например, поле vmptest.vmp.exe ImageBase содержит значение 0x140000000. Таким образом, 0x100000000 добавляется к RSI как часть расчета. Если поле ImageBase содержит менее 32-битного значения, к RSI добавляется ноль.
После того, как это добавление сделано в RSI, выполняется небольшая и несколько незначительная инструкция. Эта инструкция загружает линейный виртуальный адрес виртуальных инструкций в RBX. Теперь у RBX есть очень специальное назначение, он содержит ключ «скользящего дешифрования». Как видите, первое значение, загруженное в RBX, будет адресом самих виртуальных инструкций! Не линейный виртуальный адрес, а просто RVA, включая верхние 32 бита поля ImageBase.
Затем базовый адрес модуля vmp добавляется к RSI, вычисляя полный линейный виртуальный адрес виртуальных инструкций. Помните, что RBP содержит адрес RSP до выделения временного пространства. В этот момент базовый адрес модуля находится на вершине стека.
На этом подробности о vm_entry заканчиваются. Следующая часть этой процедуры фактически называется «calc_vm_handler» и выполняется после каждой отдельной виртуальной инструкции, кроме инструкции vm_exit.
calc_jmp - Расшифровка индекса обработчика Vm
calc_jmp является частью подпрограммы vm_entry, однако на нее ссылается не только подпрограмма vm_entry. Каждый обработчик vm в конечном итоге перейдет к calc_jmp (кроме vm_exit). Этот фрагмент кода отвечает за расшифровку кода операции каждой виртуальной инструкции, а также за индексацию в таблице обработчика vm, расшифровку записи таблицы обработчика vm и переход к полученному обработчику vm.
Первая инструкция этого фрагмента кода считывает один байт из RSI, который, как вы знаете, является VIP. Этот байт представляет собой зашифрованный код операции. Другими словами, это зашифрованный индекс в таблице обработчика vm. Всего сделано 5 преобразований. Первое преобразование всегда применяется к зашифрованному коду операции и значению в RBX в качестве источника. Это «rolling encryption». Важно отметить, что первое значение, загружаемое в RBX, - это RVA для виртуальных инструкций. Таким образом, BL будет содержать последний байт этого RVA.
Затем три преобразования применяются непосредственно к AL. Эти преобразования могут иметь немедленные значения, однако в эти преобразования никогда не добавляется ценность другого регистра.
Последнее преобразование применяется к скользящему ключу шифрования, хранящемуся в RBX. Это преобразование такое же, как и первое. Однако регистры меняются местами. Конечным результатом является расшифрованный индекс обработчика vm. Затем значение AL обнуляется и распространяется на остальную часть RAX.
Теперь, когда индекс в таблице обработчиков vm был расшифрован, необходимо извлечь и расшифровать саму запись обработчика vm. К этим записям таблицы обработчиков vm применяется только одно преобразование. В этих преобразованиях никогда не используются значения регистров. Регистр, в который загружается зашифрованное значение записи таблицы vm, всегда является RCX или RDX.
VIP теперь расширен. VIP можно продвигать вперед или назад, а сама операция продвижения может быть инструкцией LEA, INC, DEC, ADD или SUB.
Наконец, базовый адрес модуля добавляется к расшифрованному обработчику vm RVA, и затем выполняется JMP, чтобы начать выполнение этой подпрограммы обработчика vm. Опять же, RDX или RCX всегда используются для этого ADD и JMP. Это еще одна важная сигнатура виртуальной машины.
На этом завершаются спецификации фрагмента кода calc_jmp. Как видите, есть несколько очень важных сигнатур, которые можно легко найти с помощью Zydis. Особенно дешифрование записей таблицы обработчиков vm и получение этих зашифрованных значений.
vm_exit - Выход из виртуальной машины
В отличие от vm_entry, vm_exit - довольно простая процедура. Эта процедура просто возвращает все регистры на место, включая RFLAGS. Есть несколько избыточных POP, которые используются для удаления базы модуля, заполнения, а также RSP из стека, поскольку они не нужны. Порядок, в котором выполняются pop, является обратным порядку, в котором выполняется push в vm_entry. Адрес возврата вычисляется и загружается в стек перед процедурой vm_exit.
check_vsp - Перемещает временные регистры
Обработчики VM, которые помещают любые новые значения в стек, будут иметь проверку стека после выполнения обработчика vm. Эта процедура проверяет, не вторгается ли стек в рабочие регистры.
Обратите внимание на использование «movsb», которое используется для копирования содержимого временных регистров.
Виртуальные инструкции - коды операций, операнды, спецификации
Виртуальные инструкции состоят из двух или более операндов. Первый операнд - это код операции виртуальной инструкции. Коды операций представляют собой 8-битные значения без знака, которые при расшифровке являются индексом в таблице обработчика vm. Может быть второй операнд, который представляет собой непосредственное значение от одного до восьми байтов.
Все операнды зашифрованы и должны быть расшифрованы с помощью скользящего ключа дешифрования. Расшифровка выполняется внутри calc_jmp, а также в самих обработчиках vm. Обработчики виртуальных машин, выполняющие дешифрование, будут работать только с непосредственными значениями, а не с кодом операции.
Расшифровка операнда - трансформация
VMProtect 2 шифрует свои виртуальные инструкции с помощью скользящего ключа дешифрования. Этот ключ находится в RBX и изначально установлен на адрес виртуальных инструкций. Преобразования, выполняемые для дешифрования операндов, состоят из XOR, NEG, NOT, AND, ROR, ROL, SHL, SHR, ADD, SUB, INC, DEC и BSWAP. Когда операнд дешифруется, первое преобразование, применяемое к операнду, включает в себя скользящий ключ дешифрования. Таким образом, только XOR, AND, ROR, ROL, ADD и SUB будут первым преобразованием, применяемым к операнду. Затем к операнду всегда применяются три преобразования. На этом этапе операнд полностью расшифровывается, и значение в RAX будет содержать расшифрованное значение операнда. Наконец, скользящий ключ дешифрования обновляется путем преобразования скользящего ключа в полностью расшифрованное значение операнда. Пример выглядит так:
Фрагмент кода выше расшифровывает первый операнд, который всегда является кодом операции инструкции. Этот код является частью процедуры calc_jmp, однако формат преобразования одинаков для любых вторых операндов.
Обработчики виртуальных машин - Спецификации
Обработчики виртуальных машин содержат собственный код для выполнения виртуальных инструкций. Каждый двоичный файл VMProtect 2 имеет таблицу обработчика vm, которая представляет собой массив из 256 QWORD. Каждая запись содержит зашифрованный относительный виртуальный адрес соответствующего обработчика виртуальной машины. Существует множество вариантов виртуальных инструкций, таких как различные размеры непосредственных значений, а также знаковые и нулевые расширенные значения. В этом разделе будет рассмотрено несколько примеров виртуальных инструкций, а также некоторая ключевая информация, которую необходимо учитывать при попытке синтаксического анализа обработчиков виртуальных машин.
Обработчики виртуальных машин, которые обрабатывают немедленные значения, получают зашифрованное немедленное значение из RSI. Затем к этому зашифрованному немедленному значению применяются пять традиционных преобразований. Формат преобразования такой же, как и у преобразований calc_jmp. Первое преобразование применяется к зашифрованному немедленному значению, при этом скользящий ключ дешифрования является источником операции. Затем к зашифрованному немедленному значению применяются три преобразования, при этом значение полностью расшифровывается. Наконец, скользящий ключ дешифрования обновляется путем выполнения первого преобразования, за исключением того, что операнды назначения и источника меняются местами.
Также обратите внимание, что обработчики vm подвергаются непрозрачному ветвлению, а также deadstore обфускации.
LCONST - положить константу на стек
Одна из самых знаковых инструкций виртуальных машин - LCONST. Эта виртуальная инструкция загружает в стек постоянное значение из второго операнда виртуальной инструкции.
LCONSTQ - положить константный QWORD
Это деобфусцированное представление обработчика виртуальной машины LCONSTQ. Как видите, этот обработчик виртуальной машины считывает второй операнд виртуальной инструкции из VIP (RSI). Затем он расшифровывает это непосредственное значение и продвигает VIP. Затем расшифрованное немедленное значение помещается в VSP.
LCONSTCDQE - положить константный DWORD, расширенную до QWORD
Эта виртуальная инструкция загружает операнд размера DWORD из RSI, расшифровывает его и расширяет до QWORD, наконец помещая его на виртуальный стек.
Обратите внимание, что этот обработчик vm обновляет скользящий ключ дешифрования, помещая значение в стек и применяя преобразование. Это то, что может вызвать серьезные проблемы при синтаксическом анализе этих обработчиков виртуальных машин. К счастью, есть очень простой способ справиться с этим, всегда помните, что преобразование, применяемое к подвижному ключу, такое же преобразование, как и первое. В приведенном выше случае это простой XOR.
LCONSTCBW - положить константный Byte, преобразовать в DWORD
LCONSTCBW загружает константу Byte из RSI, расшифровывает его, а ноль расширяет результат как значение WORD. Это расшифрованное значение затем помещается в виртуальный стек.
LCONSTCWDE - положить константный WORD, преобразовать в DWORD
LCONSTCWDE загружает константу WORD из RSI, расшифровывает его, и расширяет до DWORD. Наконец, полученное значение помещается на виртуальный стек.
LCONSTDW - положить константный DWORD
LCONSTDW загружает константу DWORD из RSI, расшифровывает его и, наконец, помещает результат на виртуальный стек. Также обратите внимание, что VIP перемещается назад в приведенном ниже примере. Вы можете увидеть это в выборке операнда как его вычитание из RSI перед разыменованием.
LREG - положить значение временного регистра на стек
Давайте посмотрим на другой обработчик виртуальной машины, на этот раз под названием LREG. Как и в случае с LCONST, существует множество вариантов этой инструкции, особенно для разных размеров. LREG также будет присутствовать в каждом отдельном двоичном файле, поскольку он используется внутри виртуальной машины для загрузки значений регистров в временные регистры. Подробнее об этом позже.
LREGQ - положить временный регистр как QWORD
LREGQ имеет непосредственное однобайтовое значение. Это индекс рабочего регистра. Указатель на временные регистры всегда загружается в RDI. Как неоднократно описывалось выше, к непосредственному значению применяются пять полных преобразований для его расшифровки. Первое преобразование применяется из скользящего ключа дешифрования, за которым следуют три преобразования, применяемые непосредственно к непосредственному значению, которое полностью его расшифровывает. Наконец, скользящий ключ дешифрования обновляется путем применения к нему первого преобразования с расшифрованным непосредственным значением в качестве источника.
LREGDW - положить временный регистр как DWORD
LREGDW - это вариант LREG, который загружает DWORD из временного регистра в стек. Он имеет два операнда, второй из которых представляет собой один байт, представляющий индекс временного регистра. Приведенный ниже фрагмент кода представляет собой деобфусцированное представление LREGDW.
SREG - Установить значение временного регистра
Еще одна знаковая виртуальная инструкция, которая есть в каждом отдельном двоичном файле, - это SREG. У этой инструкции есть много вариантов, которые устанавливают временные регистры на определенные значения. Эта виртуальная инструкция имеет два операнда, второй из которых представляет собой непосредственное однобайтовое значение, содержащее индекс временного регистра.
SREGQ - Установить значение временного регистра как QWORD
SREGQ устанавливает виртуальный рабочий регистр со значением QWORD, забирая заначение сверху виртуального стека. Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
SREGDW - Установить значение временного регистра как DWORD
SREGDW устанавливает виртуальный рабочий регистр со значением DWORD, забирая заначение сверху виртуального стека. . Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
SREGW - Установить значение временного регистра как WORD
SREGW устанавливает виртуальный рабочий регистр со значением WORD сверху виртуального стека. Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
SREGB - Установить значения временного регистра как Byte
SREGB устанавливает виртуальный рабочий регистр со значением Byte сверху виртуального стека. Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
ADD - сложить два значения
Виртуальная инструкция ADD складывает два значения на стеке вместе и сохраняет результат во второй позиции значения в стеке. RFLAGS затем помещается на стек, поскольку инструкция ADD изменяет RFLAGS.
ADDQ - сложить два значения QWORD
ADDQ складывает два значения QWORD, хранящиеся в верхней части виртуального стека. RFLAGS также помещается в стек, поскольку инструкция ADD изменяет флаги.
ADDW - сложить два значения WORD
ADDW складывает два значения WORD, хранящиеся поверх виртуального стека. RFLAGS также помещается в стек, поскольку собственная инструкция ADD изменяет флаги.
ADDB - сложить два значения Byte
ADDB складывает два значения Byte, хранящиеся поверх виртуального стека. RFLAGS также помещается в стек, поскольку собственная инструкция ADD изменяет флаги.
MUL - Беззнаковое умножение
Виртуальная инструкция MUL умножает два значения, хранящихся на стеке. Эти обработчики vm используют нативную инструкцию MUL, кроме того RFLAGS помещается в стек. Наконец, это инструкция с одним операндом, что означает, что с этой инструкцией не связано никакого немедленного значения.
MULQ - Беззнаковое умножение QWORD
MULQ умножает два значения QWORD, результат сохраняется на стеке по адресу VSP+24, дополнительно RFLAGS помещается в стек.
DIV - Беззнаковое деление
Виртуальная инструкция DIV использует нативную инструкцию DIV, верхние операнды, используемые при делении, расположены на вершине виртуального стека. Это виртуальная инструкция с одним операндом, поэтому немедленного значения нет. RFLAGS также помещается на стек.
DIVQ - Беззнаковое деление QWORD
DIVQ делит два значения QWORD, расположенных в виртуальном стеке. Помещает RFLAGS на стек.
READ - чтение памяти
Инструкция READ читает память разного размера. Есть вариант этой инструкции для чтения одного, двух, четырех и восьми байтов.
READQ - прочитать QWORD
READQ считывает значение QWORD из адреса, хранящегося в верхней части стека. Кажется, что к этой виртуальной инструкции иногда добавляется сегмент. Однако не все обработчики виртуальных машин READQ связаны с этим
READDW - прочитать DWORD
READDW считывает значение DWORD из адреса, хранящегося в верхней части виртуального стека. Затем значение DWORD помещается поверх виртуального стека. Ниже приведены два примера READDW, один из которых использует этот синтаксис индекса сегмента, а другой - не использует.
Обратите внимание на использование смещения сегмента
READW - прочитать Word
READW считывает значение WORD из адреса, хранящегося в верхней части виртуального стека. Затем значение WORD помещается поверх виртуального стека. Ниже приведен пример этого обработчика vm, использующего синтаксис индекса сегмента, однако имейте в виду, что существуют обработчики vm без этого индекса сегмента.
WRITE - Запись в память
Виртуальная инструкция WRITE записывает до восьми байтов по данному ей адресу. Есть четыре варианта этой виртуальной инструкции, по одному для каждой степени от двух до восьми включительно. Существуют также версии каждого обработчика vm, которые используют кодировку инструкций типа смещения сегмента. Однако в длинном режиме базовые адреса некоторых сегментов равны нулю. Сегмент, который, кажется, всегда используется, - это сегмент SS с нулевой базой, поэтому база сегмента здесь не влияет, это просто немного затрудняет синтаксический анализ этих обработчиков vm.
WRITEQ - Записать QWORD
WRITEQ записывает значение QWORD по адресу, расположенному в верхней части виртуального стека. Стек увеличивается на 16 байтов.
WRITEDW - Записать DWORD
WRITEDW записывает значение DWORD по адресу, расположенному в верхней части виртуального стека. Стек увеличивается на 12 байтов.
Обратите внимание на использование смещения сегмента
WRITEW - Записать WORD
Виртуальная инструкция WRITEW записывает значение WORD по адресу, расположенному в верхней части виртуального стека. Затем стек увеличивается на десять байтов.
WRITEB - Записать Byte
Виртуальная инструкция WRITEB записывает значение BYTE по адресу, расположенному на вершине виртуального стека. Затем стек увеличивается на десять байтов.
SHL - Сдвиг влево
Обработчик SHL vm сдвигает значение, расположенное вверху стека, влево на определенное количество бит. Число битов для сдвига сохраняется над значением сдвига в стеке. Затем результат помещается в стек вместе с RFLAGS.
SHLCBW - Сдвиг влево, преобразование результата в WORD
SHLCBW сдвигает значение Byte влево, а ноль расширяет результат до WORD. RFLAGS помещается в стек.
SHLW - Сдвиг влево WORD
SHLW сдвигает значение WORD влево. RFLAGS помещается в виртуальный стек.
SHLDW - сдвиг влево DWORD
SHLDW сдвигает DWORD влево. RFLAGS помещается на виртуальный стек.
SHLQ - сдвиг влево QWORD
SHLQ сдвигает QWORD влево. RFLAGS помещается в виртуальный стек.
SHLD - Cдвиг влево с двойной точностью
Виртуальная инструкция SHLD сдвигает значение влево с помощью собственной инструкции SHLD. Затем результат помещается в стек вместе с RFLAGS. Существует вариант этой инструкции для сдвигов в один, два, четыре и восемь байтов.
SHLDQ - Cдвиг влево с двойной точностью QWORD
SHLDQ сдвигает QWORD влево с двойной точностью. Затем результат помещается в виртуальный стек, а RFLAGS помещается в виртуальный стек.
SHLDDW - Cдвиг влево с двойной точностью DWORD
Виртуальная инструкция SHLDDW сдвигает значение DWORD влево с двойной точностью. Результат помещается в виртуальный стек, а также в RFLAGS.
SHR - Cдвиг вправо
Инструкция SHR является дополнением к SHL, эта виртуальная инструкция изменяет RFLAGS, и, таким образом, значение RFLAGS будет наверху стека после выполнения этой виртуальной инструкции.
SHRQ - Cдвиг вправо QWORD
SHRQ сдвигает значение QWORD вправо. Результат помещается в виртуальный стек, какже как и RFLAGS.
SHRD - Cдвиг вправо с двойной точностью
Виртуальная инструкция SHRD сдвигает значение вправо с двойной точностью. Существует вариант этой инструкции для сдвигов в один, два, четыре и восемь байтов. Виртуальная инструкция завершается помещением RFLAGS на виртуальный стек.
SHRDQ - Cдвиг с двойной точностью вправо QWORD
SHRDQ сдвигает значение QWORD вправо с двойной точностью. Результат помещается на виртуальный стек. Затем RFLAGS помещается на виртуальный стек.
SHRDDW - Cдвиг с двойной точностью вправо DWORD
SHRDDW сдвигает значение DWORD вправо с двойной точностью. Результат помещается в виртуальный стек. RFLAGS затем помещается в виртуальный стек.
NAND - И-НЕ
*Примечание переводчика - решил перевести Not Then And как И-НЕ
Инструкция NAND состоит из применения нативной NOT к значениям в верхней части стека, за которым следует результат. Инструкция и изменяет RFLAGS, поэтому RFLAGS будет помещен в виртуальный стек.
NANDW - И-НЕ для WORD
NANDW применяет NOT к двум значениям WORD, затем побитовое AND объединяет их. Затем RFLAG помещаются в виртуальный стек.
READCR3 - Чтение третьего регистра управления
Виртуальная инструкция READCR3 - это обработчик vm-оболочки вокруг нативного
WRITECR3 - Записать третий регистр управления
Виртуальная инструкция WRITECR3 - это обработчик vm-оболочки вокруг нативного
PUSHVSP - Положить на стек указатель виртуального стека
Виртуальная инструкция PUSHVSP помещает значение, содержащееся в нативном регистре RBP, в стек виртуального стека. Есть вариант этой инструкции для одного, двух, четырех и восьми байтов.
PUSHVSPQ - Положить на стек указатель виртуального стека QWORD
PUSHVSPQ помещает все значение указателя виртуального стека в виртуальный стек.
PUSHVSPDW - Положить на стек указатель виртуального стека DWORD
PUSHVSPDW помещает нижние четыре байта указателя виртуального стека в виртуальный стек.
PUSHVSPW - Положить на стек указатель виртуального стека WORD
PUSVSPW помещает нижнее значение WORD указателя виртуального стека в виртуальный стек.
LVSP - Загрузить указатель виртуального стека
Эта виртуальная инструкция загружает регистр указателя виртуального стека значением наверху стека.
LVSPW - Загрузить указателя виртуального стека WORD
Эта виртуальная инструкция загружает регистр указателя виртуального стека значением WORD наверху стека.
LVSPDW - Загрузить указателя виртуального стека DWORD
Эта виртуальная инструкция загружает регистр указателя виртуального стека значением DWORD наверху стека.
LRFLAGS - Загрузить RFLAGS
Эта виртуальная инструкция загружает в нативный регистр флагов значение QWORD с верхней части стека.
JMP - Инструкция виртуального прыжка
Виртуальная инструкция JMP изменяет регистр RSI, чтобы указать на новый набор виртуальных инструкций. Значение наверху стека - это нижние 32 бита RVA от базы модуля до виртуальных инструкций. Затем это значение добавляется к верхним 32 бита базового значения изображения, содержащегося в необязательном заголовке PE-файла. Затем к этому значению добавляется базовый адрес.
CALL - Инструкция виртуального вызова
Инструкция виртуального вызова берет адрес вершины виртуального стека и затем вызывает его. RDX используется для хранения адреса, поэтому вы можете действительно вызывать функции только с одним параметром.
Ссылка на скачивание: https://githacks.org/vmp2
Содержание
- Credit - Ссылки на существующие работы
- Преамбула - Намерения и цели
- Намерения
- Цели
- Терминология
- Вступление
- Обфускация - Deadstore, Непрозрачное ветвление
- Пример непрозрачной обфускации ветвления
- Пример обфускации Deadstore
- Обзор - виртуальная машина VMProtect 2
- Скользящее дешифрование
- Использование нативных регистров
- Постоянные регистры - регистры специального назначения
- Непостоянные регистры - временные регистры
- vm_entry - Вход в виртуальную машину
- calc_jmp - Расшифровка индекса обработчика Vm
- vm_exit - Выход из виртуальной машины
- check_vsp - Перемещает временные регистры
- Виртуальные инструкции - коды операций, операнды, спецификации
- Расшифровка операнда - трансформация
- Обработчики виртуальных машин - Спецификации
- LCONST - положить константу на стек
- LCONSTQ - положить константный QWORD
- LCONSTCDQE - положить константный DWORD, расширенную до QWORD
- LCONSTCBW - положить константный Byte, преобразовать в DWORD
- LCONSTCWDE - положить константный WORD, преобразовать в DWORD
- LCONSTDW - положить константный DWORD
- LREG - положить значение временного регистра на стек
- LREGQ - положить временный регистр как QWORD
- LREGDW - положить временный регистр как DWORD
- SREG - Установить значение временного регистра
- SREGQ - Установить значение временного регистра как QWORD
- SREGDW - Установить значение временного регистра как DWORD
- SREGW - Установить значение временного регистра как WORD
- SREGB - Установить значения временного регистра как Byte
- ADD - сложить два значения
- ADDQ - сложить два значения QWORD
- ADDW - сложить два значения WORD
- ADDB - сложить два значения Byte
- MUL - Беззнаковое умножение
- MULQ - Беззнаковое умножение QWORD
- DIV - Беззнаковое деление
- DIVQ - Беззнаковое деление QWORD
- READ - чтение памяти
- READQ - прочитать QWORD
- READDW - прочитать DWORD
- READW - прочитать Word
- WRITE - Запись в память
- WRITEQ - Записать QWORD
- WRITEDW - Записать DWORD
- WRITEW - Записать WORD
- WRITEB - Записать Byte
- SHL - Сдвиг влево
- SHLCBW - Сдвиг влево, преобразование результата в WORD
- SHLW - Сдвиг влево WORD
- SHLDW - сдвиг влево DWORD
- SHLQ - сдвиг влево QWORD
- SHLD - Cдвиг влево с двойной точностью
- SHLDQ - Cдвиг влево с двойной точностью QWORD
- SHLDDW - Cдвиг влево с двойной точностью DWORD
- SHR - Cдвиг вправо
- SHRQ - Cдвиг вправо QWORD
- SHRD - Cдвиг вправо с двойной точностью
- SHRDQ - Cдвиг с двойной точностью вправо QWORD
- SHRDDW - Cдвиг с двойной точностью вправо DWORD
- NAND - И-НЕ
- NANDW - И-НЕ для WORD
- READCR3 - Чтение третьего регистра управления
- WRITECR3 - Записать третий регистр управления
- PUSHVSP - Положить на стек указатель виртуального стека
- PUSHVSPQ - Положить на стек указатель виртуального стека QWORD
- PUSHVSPDW - Положить на стек указатель виртуального стека DWORD
- PUSHVSPW - Положить на стек указатель виртуального стека WORD
- LVSP - Загрузить указатель виртуального стека
- LVSPW - Загрузить указателя виртуального стека WORD
- LVSPDW - Загрузить указателя виртуального стека DWORD
- LRFLAGS - Загрузить RFLAGS
- JMP - Инструкция виртуального прыжка
- CALL - Инструкция виртуального вызова
- LCONST - положить константу на стек
- Важные сигнатуры виртуальных машин - статический анализ
- Поиск таблицы хэндлеров VM
- Расшифровка записи в таблице хэндлеров виртуальной машины
- Обработка трансформаций - шаблонные лямбды и map'ы
- Извлечение преобразований - Продолжение статического анализа
- Дилемма статического анализа - Заключение статического анализа
- Поиск таблицы хэндлеров VM
- vmtracer - трассировка виртуальных инструкций
- vmprofile-cli - Статический анализ с использованием трассировки времени выполнения
- Отображение информации о трассировке - vmprofiler-qt
- Поведение виртуальной машины
- Demo - Осуществление и тестирование виртуальной тассировки
- Изменение результатов виртуальных инструкций
- Кодирование виртуальных инструкций - обратные преобразования
- Заключение - статический и динамический анализ
- Samuel Chevet
- Rolf Rolles
- Anatoli Kalysch
- Can Bölük
- Katy Hearthstone
- IRQL0
- Helped created vmprofiler v1.0, and helped with general analysis of vm handlers.
- BTBD
- Providing an algorithm to handle deadstore removal with Zydis.
Прежде чем погрузиться в этот пост, я хотел бы изложить несколько моментов в отношении существующей работы с VMProtect 2, цели этой статьи и моих намерений, поскольку они, кажется, время от времени неправильно истолковываются или искажаются.
Намерения
Несмотря на то, что по VMProtect 2 уже было проведено много исследований, я чувствую, что все еще есть информация, которая не обсуждалась публично, и не было раскрыто достаточное количество исходного кода. Информация, которую я раскрываю в этой статье, направлена на то, чтобы выйти за рамки общего архитектурного анализа, погрузиться гораздо глубже. Уровень, на котором можно было прописывать свои собственные инструкции виртуальной машины с помощью двоичного файла, защищенного VMProtect, а также с легкостью перехватывать и изменять результаты виртуальных инструкций. Динамический анализ, обсуждаемый в этой статье, основан на существующей работе Самуэля Чевета, мое исследование динамического анализа и проект vmtracer - это просто расширение его работы, продемонстрированной в его презентации «Inside VMProtect».
Цели
Этот пост не предназначен для того, чтобы озвучить какие-либо негативные мнения о VMProtect 2, создателях указанного ПО или всех, кто его использует. Я восхищаюсь создателями, которые явно обладают впечатляющими навыками создания такого продукта.
Этот пост также был создан под впечатлением, что все, что здесь обсуждается, скорее всего, было обнаружено частными лицами, и что я не первый, кто обнаружил или задокументировал такие вещи об архитектуре VMProtect 2. Я не собираюсь представлять эту информацию, как будто она является новаторской или ещё что-то, как раз наоборот. Это просто набор существующей информации, дополненной моими собственными исследованиями.
Смиренно представляю вам «VMProtect 2 - Детальный анализ архитектуры виртуальной машины».
Терминология
VIP - Virtual Instruction Pointer, это эквивалент регистра RIP x86-64, который содержит адрес следующей инструкции, которая должна быть выполнена. VMProtect 2 использует регистр RSI для хранения адреса следующего указателя виртуальной инструкции. Таким образом, RSI эквивалентен VIP.
VSP - Virtual Stack Pointer, это эквивалент регистру x86-64 RSP, который содержит адрес стека. VMProtect 2 использует регистр RBP для хранения адреса указателя виртуального стека. Таким образом, RBP эквивалентно VSP.
VM Handler - Подпрограмма, содержащая собственный код для выполнения виртуальной инструкции. Например, инструкция VADD64 складывает два значения в стек вместе и сохраняет результат, а также RFLAGS в стеке.
Virtual Instruction - Также известный как «виртуальный байт-код» - это байты, интерпретируемые виртуальной машиной и впоследствии выполняемые. Каждая виртуальная инструкция состоит как минимум из одного или нескольких операндов. Первый операнд содержит код операции для инструкции.
Virtual Opcode - Первый операнд каждой виртуальной инструкции. Это индекс обработчика vm. Размер кода операции VMProtect 2 всегда составляет один байт.
IMM / Immediate Value (немедленные значения) - Значение, закодированное в виртуальную инструкцию, с помощью которой должны выполняться операции, такие как загрузка указанного значения в стек или в виртуальный регистр. Виртуальные инструкции, такие как LREG, SREG и LCONST, имеют IMM.
Transformations - Термин «преобразование», используемый в этом посте, относится конкретно к операциям, выполняемым для расшифровки операндов виртуальных инструкций и записей таблицы обработчиков vm. Эти преобразования состоят из add, sub, inc, dec, not, neg, shl, shr, ror, rol и, наконец, BSWAP. Преобразования выполняются с размером 1, 2, 4 и 8 байт. Преобразования также могут иметь связанные с ними немедленные/постоянные значения, такие как «xor rax, 0x123456» или «add rax, 0x123456».
Вступление
VMProtect 2 - это обфускатор x86 на основе виртуальной машины, который преобразует инструкции x86 в RISC, стековую машину, набор инструкций. Каждый защищенный двоичный файл имеет уникальный набор зашифрованных инструкций виртуальной машины с уникальной обфускацией. Этот проект направлен на выявление очень важных сигнатур, которые есть в каждом бинарном файле VMProtect 2, с целью помочь в дальнейших исследованиях. В этой статье также кратко обсуждаются различные типы обфускации VMProtect 2. Все методы деобфускации специально адаптированы к процедурам виртуальных машин и не будут работать с обычно запутанными подпрограммами, особенно с подпрограммами, в которых есть настоящие JCC.
Обфускация - Deadstore, Непрозрачное ветвление
VMProtect 2 по большей части использует два типа обфускации: первый - это deadstore, а второй - непрозрачное ветвление. В запутанных подпрограммах вы можете увидеть несколько инструкций, за которыми следует JCC, затем еще один набор инструкций, за которым следует еще один JCC. Другая часть непрозрачного ветвления - это случайные инструкции, которые влияют на регистр FLAGS. Вы можете увидеть этих маленьких *бездельников повсюду. В основном это инструкции битового тестирования, бесполезные сравнения, а также инструкции по установке/очистке флагов.
*Примечание переводчика - bugger дословно переводится как бездельник, но может в контексте быть интерпретированно как мужеложец или содомит
Пример непрозрачной обфускации ветвления
В этом примере обфускации непрозрачного ветвления я рассмотрю то, как выглядит непрозрачное ветвление VMProtect 2, другие факторы, такие как состояние rflags, и, что наиболее важно, как определить, смотрите ли вы на непрозрачную ветвь или на легетимный JCC.
Код:
.vmp0:00000001400073B4 D0 C8 ror al, 1
.vmp0:00000001400073B6 0F CA bswap edx
.vmp0:00000001400073B8 66 0F CA bswap dx
.vmp0:00000001400073BB 66 0F BE D2 movsx dx, dl
.vmp0:00000001400073BF 48 FF C6 inc rsi
.vmp0:00000001400073C2 48 0F BA FA 0F btc rdx, 0Fh
.vmp0:00000001400073C7 F6 D8 neg al
.vmp0:00000001400073C9 0F 81 6F D0 FF FF jno loc_14000443E
.vmp0:00000001400073CF 66 C1 FA 04 sar dx, 4
.vmp0:00000001400073D3 81 EA EC 94 CD 47 sub edx, 47CD94ECh
.vmp0:00000001400073D9 28 C3 sub bl, al
.vmp0:00000001400073DB D2 F6 sal dh, cl
.vmp0:00000001400073DD 66 0F BA F2 0E btr dx, 0Eh
.vmp0:00000001400073E2 8B 14 38 mov edx, [rax+rdi]
Рассмотрим приведенный выше обфусцированный код. Обратите внимание на ветку JNO. Если вы проследуете в эту ветвь в ida и сравните инструкции с инструкциями после JNO, вы увидите, что ветвь бесполезна, поскольку оба пути выполняют одни и те же по значению инструкции.
Код:
loc_14000443E:
.vmp0:000000014000443E F5 cmc
.vmp0:000000014000443F 0F B3 CA btr edx, ecx
.vmp0:0000000140004442 0F BE D3 movsx edx, bl
.vmp0:0000000140004445 66 21 F2 and dx, si
.vmp0:0000000140004448 28 C3 sub bl, al
.vmp0:000000014000444A 48 81 FA 38 04 AA 4E cmp rdx, 4EAA0438h
.vmp0:0000000140004451 48 8D 90 90 50 F5 BB lea rdx, [rax-440AAF70h]
.vmp0:0000000140004458 D2 F2 sal dl, cl
.vmp0:000000014000445A D2 C2 rol dl, cl
.vmp0:000000014000445C 8B 14 38 mov edx, [rax+rdi]
Если вы посмотрите достаточно внимательно, то увидите, что есть несколько одинаковых инструкций в обеих ветках. Может быть сложно определить, какой код является deadstore, а какой код действительно нужен, однако, если вы выберете регистр в ida и посмотрите все места, в которых он написан до инструкции, которую вы просматриваете, вы можете удалить все эти другие написание инструкций до тех пор, пока не будет прочитан указанный регистр. Теперь вернемся к примеру. В этом случае важны следующие инструкции:
Код:
.vmp0:0000000140004448 28 C3 sub bl, al
.vmp0:000000014000445C 8B 14 38 mov edx, [rax+rdi]
Генерация этих непрозрачных ветвей приводит к дублированию инструкций. Для каждого пути кода есть больше обфускации deadstore, а также непрозрачные условия и другие инструкции, которые влияют на RFLAGS.
Пример обфускации Deadstore
Примечание переводчика - Deadstore, я решил не переводить, потому, что мертвое хранилище звучит ужасно
Deadstore обфускация VMProtect 2 добавляет больше всего мусора в поток инструкций, помимо непрозрачных битовых тестов и сравнений. Эти инструкции бесполезны, их можно легко обнаружить и удалить вручную.
Взгляните сюда:
Код:
.vmp0:0000000140004149 66 D3 D7 rcl di, cl
.vmp0:000000014000414C 58 pop rax
.vmp0:000000014000414D 66 41 0F A4 DB 01 shld r11w, bx, 1
.vmp0:0000000140004153 41 5B pop r11
.vmp0:0000000140004155 80 E6 CA and dh, 0CAh
.vmp0:0000000140004158 66 F7 D7 not di
.vmp0:000000014000415B 5F pop rdi
.vmp0:000000014000415C 66 41 C1 C1 0C rol r9w, 0Ch
.vmp0:0000000140004161 F9 stc
.vmp0:0000000140004162 41 58 pop r8
.vmp0:0000000140004164 F5 cmc
.vmp0:0000000140004165 F8 clc
.vmp0:0000000140004166 66 41 C1 E1 0B shl r9w, 0Bh
.vmp0:000000014000416B 5A pop rdx
.vmp0:000000014000416C 66 81 F9 EB D2 cmp cx, 0D2EBh
.vmp0:0000000140004171 48 0F A3 F1 bt rcx, rsi
.vmp0:0000000140004175 41 59 pop r9
.vmp0:0000000140004177 66 41 21 E2 and r10w, sp
.vmp0:000000014000417B 41 C1 D2 10 rcl r10d, 10h
.vmp0:000000014000417F 41 5A pop r10
.vmp0:0000000140004181 66 0F BA F9 0C btc cx, 0Ch
.vmp0:0000000140004186 49 0F CC bswap r12
.vmp0:0000000140004189 48 3D 97 74 7D C7 cmp rax, 0FFFFFFFFC77D7497h
.vmp0:000000014000418F 41 5C pop r12
.vmp0:0000000140004191 66 D3 C1 rol cx, cl
.vmp0:0000000140004194 F5 cmc
.vmp0:0000000140004195 66 0F BA F5 01 btr bp, 1
.vmp0:000000014000419A 66 41 D3 FE sar r14w, cl
.vmp0:000000014000419E 5D pop rbp
.vmp0:000000014000419F 66 41 29 F6 sub r14w, si
.vmp0:00000001400041A3 66 09 F6 or si, si
.vmp0:00000001400041A6 01 C6 add esi, eax
.vmp0:00000001400041A8 66 0F C1 CE xadd si, cx
.vmp0:00000001400041AC 9D popfq
.vmp0:00000001400041AD 0F 9F C1 setnle cl
.vmp0:00000001400041B0 0F 9E C1 setle cl
.vmp0:00000001400041B3 4C 0F BE F0 movsx r14, al
.vmp0:00000001400041B7 59 pop rcx
.vmp0:00000001400041B8 F7 D1 not ecx
.vmp0:00000001400041BA 59 pop rcx
.vmp0:00000001400041BB 4C 8D A8 ED 19 28 C9 lea r13, [rax-36D7E613h]
.vmp0:00000001400041C2 66 F7 D6 not si
.vmp0:00000001400041CB 41 5E pop r14
.vmp0:00000001400041CD 66 F7 D6 not si
.vmp0:00000001400041D0 66 44 0F BE EA movsx r13w, dl
.vmp0:00000001400041D5 41 BD B2 6B 48 B7 mov r13d, 0B7486BB2h
.vmp0:00000001400041DB 5E pop rsi
.vmp0:00000001400041DC 66 41 BD CA 44 mov r13w, 44CAh
.vmp0:0000000140007AEA 4C 8D AB 31 11 63 14 lea r13, [rbx+14631131h]
.vmp0:0000000140007AF1 41 0F CD bswap r13d
.vmp0:0000000140007AF4 41 5D pop r13
.vmp0:0000000140007AF6 C3 retn
Давайте начнем сверху, по одной инструкции за раз. Первая инструкция по адресу
0x140004149 - «RCL - Rotate Left Carry». Эта инструкция влияет на регистр FLAGS, а также на DI. Давайте посмотрим, когда в следующий раз будет ссылка на DI. Это чтение или запись? Следующая ссылка на DI - это инструкция NOT по адресу 0x140004158. NOT читает и записывает DI, пока действительны обе инструкции. Следующая инструкция, которая ссылается на DI, - это инструкции POP. Это очень важно, поскольку все записи в RDI до этого POP могут быть удалены из потока инструкций.
Код:
.vmp0:000000014000414C 58 pop rax
.vmp0:000000014000414D 66 41 0F A4 DB 01 shld r11w, bx, 1
.vmp0:0000000140004153 41 5B pop r11
.vmp0:0000000140004155 80 E6 CA and dh, 0CAh
.vmp0:000000014000415B 5F pop rdi
Следующая инструкция - POP RAX по адресу
0x14000414C. RAX никогда не записывается во всем потоке инструкций, он только читается. Поскольку у нее есть зависимость чтения, эта инструкция не может быть удалена. Переходим к следующей инструкции, SHLD - сдвиг с двойной точностью влево, зависимость записи от R11, зависимость чтения от BX.
Следующая инструкция, которая ссылается на R11, - это POP R11 по адресу
0x140004153. Мы можем удалить инструкцию SHLD как deadstore.
Код:
.vmp0:000000014000414C 58 pop rax
.vmp0:0000000140004153 41 5B pop r11
.vmp0:0000000140004155 80 E6 CA and dh, 0CAh
.vmp0:000000014000415B 5F pop rdi
Теперь просто повторите процесс для каждой инструкции. Конечный результат должен выглядеть примерно так:
Код:
.vmp0:000000014000414C 58 pop rax
.vmp0:0000000140004153 41 5B pop r11
.vmp0:000000014000415B 5F pop rdi
.vmp0:0000000140004162 41 58 pop r8
.vmp0:000000014000416B 5A pop rdx
.vmp0:0000000140004175 41 59 pop r9
.vmp0:000000014000417F 41 5A pop r10
.vmp0:000000014000418F 41 5C pop r12
.vmp0:000000014000419E 5D pop rbp
.vmp0:00000001400041AC 9D popfq
.vmp0:00000001400041B7 59 pop rcx
.vmp0:00000001400041B7 59 pop rcx
.vmp0:00000001400041CB 41 5E pop r14
.vmp0:00000001400041DB 5E pop rsi
.vmp0:0000000140007AF4 41 5D pop r13
.vmp0:0000000140007AF6 C3 retn
Этот метод не идеален для удаления deadstore обфускации, поскольку в приведенном выше результате отсутствует второй POP RCX. Инструкции POP и PUSH - это особые случаи, которые не должны выводиться из потока инструкций, поскольку эти инструкции также изменяют RSP. Этот метод удаления deadstore также применяется только к обработчикам vm_entry и vm. Он не может быть применён к обычным запутанным подпрограммам как есть. Опять же, этот метод НЕ будет работать с какой-либо запутанной подпрограммой, он специально разработан для обработчиков vm_entry и vm, поскольку в этих подпрограммах нет легитимных JCC.
Обзор - виртуальная машина VMProtect 2
Виртуальные инструкции дешифруются и интерпретируются обработчиками виртуальных инструкций, называемыми «обработчиками vm». Виртуальная машина представляет собой стековую машину на основе RISC с рабочими регистрами. Перед vm-записями зашифрованный RVA (относительный виртуальный адрес) для виртуальных инструкций помещается в стек, и все регистры общего назначения, а также флаги помещаются в стек. VIP расшифровывается, вычисляется и загружается в RSI. Затем в RBX запускается скользящий ключ дешифрования, который используется для дешифрования каждого операнда каждой отдельной виртуальной инструкции. Ключ скользящего дешифрования обновляется путем преобразования его в расшифрованное значение операнда.
Скользящее дешифрование
VMProtect 2 использует скользящий ключ дешифрования. Этот ключ используется для дешифрования операндов виртуальных команд, что впоследствии предотвращает любой вид перехвата, как если бы какие-либо виртуальные инструкции выполнялись не по порядку, скользящий ключ дешифрования станет недействительным, что приведет к недействительности дальнейшего дешифрования виртуальных операндов.
Использование нативных регистров
Во время выполнения внутри виртуальной машины некоторые нативные регистры выделяются для механизмов самой виртуальной машины, таких как указатель виртуальной инструкции и виртуальный стек. В этом разделе я буду обсуждать эти нативные регистры и их использование виртуальной машиной.
Постоянные регистры - регистры специального назначения
Для начала RSI всегда используется для указателя виртуальной инструкции. Операнды выбираются из адреса, хранящегося в RSI. Начальное значение, загружаемое в RSI, выполняется vm_entry.
RBP используется для указателя виртуального стека, адрес, хранящийся в RBP, на самом деле является собственной памятью стека. RBP загружается с RSP до выделения временных регистров.
Это подводит нас к RDI, который содержит временные регистры. Адрес в RDI также инициализируется в vm_entry и устанавливается на адрес, находящийся внутри собственного стека.
В R12 загружается линейный виртуальный адрес таблицы обработчика vm. Это делается внутри vm_entry, и на протяжении всего времени выполнения внутри виртуальной машины R12 будет содержать этот адрес.
R13 загружается с линейным виртуальным адресом базового адреса модуля внутри vm_entry и не изменяется во время выполнения внутри виртуальной машины.
RBX это особый регистр, который содержит скользящий ключ дешифрования. После каждой расшифровки каждого операнда каждой виртуальной инструкции RBX обновляется путем применения к нему преобразования со значением расшифрованного операнда.
Непостоянные регистры - временные регистры
RAX, RCX и RDX используются как временные регистры внутри виртуальной машины, однако RAX используется для очень специфических временных операций над другими регистрами. RAX используется для расшифровки операндов виртуальных инструкций, AL, в частности, используется при расшифровке кода операции виртуальной инструкции.
vm_entry - Вход в виртуальную машину
vm_entry - очень важный компонент архитектуры виртуальной машины. Перед входом в виртуальную машину зашифрованный RVA для виртуальных инструкций помещается в стек. Этот RVA представляет собой четырехбайтовое значение.
Код:
.vmp0:000000014000822C 68 FA 01 00 89 push 0FFFFFFFF890001FAh
После того, как это значение помещается в стек, выполняется jmp, чтобы начать выполнение vm_entry.
vm_entry подвергается обфускации, о чем я подробно объяснил выше. Сглаживая, а затем удаляя deadstore код, мы можем получить хороший чистый вид vm_entry.
Код:
> 0x822c : push 0xFFFFFFFF890001FA
> 0x7fc9 : push 0x45D3BF1F
> 0x48e4 : push r13
> 0x4690 : push rsi
> 0x4e53 : push r14
> 0x74fb : push rcx
> 0x607c : push rsp
> 0x4926 : pushfq
> 0x4dc2 : push rbp
> 0x5c8c : push r12
> 0x52ac : push r10
> 0x51a5 : push r9
> 0x5189 : push rdx
> 0x7d5f : push r8
> 0x4505 : push rdi
> 0x4745 : push r11
> 0x478b : push rax
> 0x7a53 : push rbx
> 0x500d : push r15
> 0x6030 : push [0x00000000000018E2]
> 0x593a : mov rax, 0x7FF634270000
> 0x5955 : mov r13, rax
> 0x5965 : push rax
> 0x596f : mov esi, [rsp+0xA0]
> 0x5979 : not esi
> 0x5985 : neg esi
> 0x598d : ror esi, 0x1A
> 0x599e : mov rbp, rsp
> 0x59a8 : sub rsp, 0x140
> 0x59b5 : and rsp, 0xFFFFFFFFFFFFFFF0
> 0x59c1 : mov rdi, rsp
> 0x59cb : lea r12, [0x0000000000000AA8]
> 0x59df : mov rax, 0x100000000
> 0x59ec : add rsi, rax
> 0x59f3 : mov rbx, rsi
> 0x59fa : add rsi, [rbp]
> 0x5a05 : mov al, [rsi]
> 0x5a0a : xor al, bl
> 0x5a11 : neg al
> 0x5a19 : rol al, 0x05
> 0x5a26 : inc al
> 0x5a2f : xor bl, al
> 0x5a34 : movzx rax, al
> 0x5a41 : mov rdx, [r12+rax*8]
> 0x5a49 : xor rdx, 0x7F3D2149
> 0x5507 : inc rsi
> 0x7951 : add rdx, r13
> 0x7954 : jmp rdx
Как и ожидалось, все регистры, а также RFLAGS помещаются в стек. Последний push помещает в стек восемь байтов нулей, а не перемещение, как я ожидал вначале. Порядок, в котором происходят push, уникален для каждой сборки, однако последние восемь нулей всегда одинаковы для всех двоичных файлов. Это очень стабильная сигнатура, позволяющая определить, когда закончится общий регистр. Ниже приведены точные последовательности инструкций, которые я имею в виду в этом абзаце.
Код:
> 0x48e4 : push r13
> 0x4690 : push rsi
> 0x4e53 : push r14
> 0x74fb : push rcx
> 0x607c : push rsp
> 0x4926 : pushfq
> 0x4dc2 : push rbp
> 0x5c8c : push r12
> 0x52ac : push r10
> 0x51a5 : push r9
> 0x5189 : push rdx
> 0x7d5f : push r8
> 0x4505 : push rdi
> 0x4745 : push r11
> 0x478b : push rax
> 0x7a53 : push rbx
> 0x500d : push r15
> 0x6030 : push [0x00000000000018E2] ; pushes 0’s
После того, как все регистры и RFLAGS помещены в стек, базовый адрес модуля загружается в R13. Это происходит в каждом отдельном двоичном файле, R13 всегда содержит базовый адрес модуля во время выполнения виртуальной машины. Базовый адрес модуля также помещается в стек.
Код:
> 0x593a : mov rax, 0x7FF634270000
> 0x5955 : mov r13, rax
> 0x5965 : push rax
Затем расшифровывается относительный виртуальный адрес желаемых виртуальных инструкций, которые должны быть выполнены. Это делается путем загрузки 32-битного RVA в ESI из RSP + 0xA0. Это очень важная сигнатура, и ее можно легко найти. Затем к ESI применяются три преобразования, чтобы получить расшифрованный RVA виртуальных инструкций. Эти три преобразования уникальны для каждого бинарного файла. Однако их всегда три.
Код:
> 0x596f : mov esi, [rsp+0xA0]
> 0x5979 : not esi
> 0x5985 : neg esi
> 0x598d : ror esi, 0x1A
Кроме того, следующая важная операция, которая происходит, - это пространство, выделенное в стеке для временных регистров. RSP всегда перемещается в RBP, затем RSP вычитается на 0x140. Затем выравнивается по 16 байтам. После этого адрес переносится в RDI. Во время выполнения VM RDI всегда содержит указатель на временные регистры.
Код:
> 0x599e : mov rbp, rsp
> 0x59a8 : sub rsp, 0x140
> 0x59b5 : and rsp, 0xFFFFFFFFFFFFFFF0
> 0x59c1 : mov rdi, rsp
Следующая примечательная операция - загрузка адреса таблицы обработчика vm в R12. Это делается для каждого двоичного файла VMProtect 2. R12 всегда содержит линейный виртуальный адрес таблицы обработчика vm. Это еще одна важная сигнатура, с помощью которой можно легко найти расположение таблицы обработчиков vm.
Код:
> 0x59cb : lea r12, [0x0000000000000AA8]
Затем с RSI выполняется другая операция для расчета VIP. Внутри PE-заголовков есть заголовок, который называется «необязательный заголовок». В нем содержится разнообразная информация. Одно из полей называется «ImageBase». Если в этом поле есть какие-либо биты выше 32, эти биты затем добавляются к RSI. Например, поле vmptest.vmp.exe ImageBase содержит значение 0x140000000. Таким образом, 0x100000000 добавляется к RSI как часть расчета. Если поле ImageBase содержит менее 32-битного значения, к RSI добавляется ноль.
Код:
> 0x59df : mov rax, 0x100000000
> 0x59ec : add rsi, rax
После того, как это добавление сделано в RSI, выполняется небольшая и несколько незначительная инструкция. Эта инструкция загружает линейный виртуальный адрес виртуальных инструкций в RBX. Теперь у RBX есть очень специальное назначение, он содержит ключ «скользящего дешифрования». Как видите, первое значение, загруженное в RBX, будет адресом самих виртуальных инструкций! Не линейный виртуальный адрес, а просто RVA, включая верхние 32 бита поля ImageBase.
Код:
> 0x59f3 : mov rbx, rsi
Затем базовый адрес модуля vmp добавляется к RSI, вычисляя полный линейный виртуальный адрес виртуальных инструкций. Помните, что RBP содержит адрес RSP до выделения временного пространства. В этот момент базовый адрес модуля находится на вершине стека.
Код:
> 0x59fa : add rsi, [rbp]
На этом подробности о vm_entry заканчиваются. Следующая часть этой процедуры фактически называется «calc_vm_handler» и выполняется после каждой отдельной виртуальной инструкции, кроме инструкции vm_exit.
calc_jmp - Расшифровка индекса обработчика Vm
calc_jmp является частью подпрограммы vm_entry, однако на нее ссылается не только подпрограмма vm_entry. Каждый обработчик vm в конечном итоге перейдет к calc_jmp (кроме vm_exit). Этот фрагмент кода отвечает за расшифровку кода операции каждой виртуальной инструкции, а также за индексацию в таблице обработчика vm, расшифровку записи таблицы обработчика vm и переход к полученному обработчику vm.
Код:
> 0x5a05 : mov al, [rsi]
> 0x5a0a : xor al, bl
> 0x5a11 : neg al
> 0x5a19 : rol al, 0x05
> 0x5a26 : inc al
> 0x5a2f : xor bl, al
> 0x5a34 : movzx rax, al
> 0x5a41 : mov rdx, [r12+rax*8]
> 0x5a49 : xor rdx, 0x7F3D2149
> 0x5507 : inc rsi
> 0x7951 : add rdx, r13
> 0x7954 : jmp rdx
Первая инструкция этого фрагмента кода считывает один байт из RSI, который, как вы знаете, является VIP. Этот байт представляет собой зашифрованный код операции. Другими словами, это зашифрованный индекс в таблице обработчика vm. Всего сделано 5 преобразований. Первое преобразование всегда применяется к зашифрованному коду операции и значению в RBX в качестве источника. Это «rolling encryption». Важно отметить, что первое значение, загружаемое в RBX, - это RVA для виртуальных инструкций. Таким образом, BL будет содержать последний байт этого RVA.
Код:
> 0x5a05 : mov al, [rsi]
> 0x5a2f : xor bl, al ; transformation is unique to each build
Затем три преобразования применяются непосредственно к AL. Эти преобразования могут иметь немедленные значения, однако в эти преобразования никогда не добавляется ценность другого регистра.
Код:
> 0x5a11 : neg al
> 0x5a19 : rol al, 0x05
> 0x5a26 : inc al
Последнее преобразование применяется к скользящему ключу шифрования, хранящемуся в RBX. Это преобразование такое же, как и первое. Однако регистры меняются местами. Конечным результатом является расшифрованный индекс обработчика vm. Затем значение AL обнуляется и распространяется на остальную часть RAX.
Код:
> 0x5a2f : xor bl, al
> 0x5a34 : movzx rax, al
Теперь, когда индекс в таблице обработчиков vm был расшифрован, необходимо извлечь и расшифровать саму запись обработчика vm. К этим записям таблицы обработчиков vm применяется только одно преобразование. В этих преобразованиях никогда не используются значения регистров. Регистр, в который загружается зашифрованное значение записи таблицы vm, всегда является RCX или RDX.
Код:
> 0x5a41 : mov rdx, [r12+rax*8]
> 0x5a49 : xor rdx, 0x7F3D2149
VIP теперь расширен. VIP можно продвигать вперед или назад, а сама операция продвижения может быть инструкцией LEA, INC, DEC, ADD или SUB.
Код:
> 0x5507 : inc rsi
Наконец, базовый адрес модуля добавляется к расшифрованному обработчику vm RVA, и затем выполняется JMP, чтобы начать выполнение этой подпрограммы обработчика vm. Опять же, RDX или RCX всегда используются для этого ADD и JMP. Это еще одна важная сигнатура виртуальной машины.
Код:
> 0x7951 : add rdx, r13
> 0x7954 : jmp rdx
На этом завершаются спецификации фрагмента кода calc_jmp. Как видите, есть несколько очень важных сигнатур, которые можно легко найти с помощью Zydis. Особенно дешифрование записей таблицы обработчиков vm и получение этих зашифрованных значений.
vm_exit - Выход из виртуальной машины
В отличие от vm_entry, vm_exit - довольно простая процедура. Эта процедура просто возвращает все регистры на место, включая RFLAGS. Есть несколько избыточных POP, которые используются для удаления базы модуля, заполнения, а также RSP из стека, поскольку они не нужны. Порядок, в котором выполняются pop, является обратным порядку, в котором выполняется push в vm_entry. Адрес возврата вычисляется и загружается в стек перед процедурой vm_exit.
Код:
.vmp0:000000014000635F 48 89 EC mov rsp, rbp
.vmp0:0000000140006371 58 pop rax ; pop module base of the stack
.vmp0:000000014000637F 5B pop rbx ; pop zero’s off the stack
.vmp0:0000000140006387 41 5F pop r15
.vmp0:0000000140006393 5B pop rbx
.vmp0:000000014000414C 58 pop rax
.vmp0:0000000140004153 41 5B pop r11
.vmp0:000000014000415B 5F pop rdi
.vmp0:0000000140004162 41 58 pop r8
.vmp0:000000014000416B 5A pop rdx
.vmp0:0000000140004175 41 59 pop r9
.vmp0:000000014000417F 41 5A pop r10
.vmp0:000000014000418F 41 5C pop r12
.vmp0:000000014000419E 5D pop rbp
.vmp0:00000001400041AC 9D popfq
.vmp0:00000001400041B7 59 pop rcx ; pop RSP off the stack.
.vmp0:00000001400041BA 59 pop rcx
.vmp0:00000001400041CB 41 5E pop r14
.vmp0:00000001400041DB 5E pop rsi
.vmp0:0000000140007AF4 41 5D pop r13
.vmp0:0000000140007AF6 C3 retn
check_vsp - Перемещает временные регистры
Обработчики VM, которые помещают любые новые значения в стек, будут иметь проверку стека после выполнения обработчика vm. Эта процедура проверяет, не вторгается ли стек в рабочие регистры.
Код:
.vmp0:00000001400044AA 48 8D 87 E0 00 00 00 lea rax, [rdi+0E0h]
.vmp0:00000001400044B2 48 39 C5 cmp rbp, rax
.vmp0:000000014000429D 0F 87 5B 17 00 00 ja calc_jmp
.vmp0:00000001400042AC 48 89 E2 mov rdx, rsp
.vmp0:0000000140005E5F 48 8D 8F C0 00 00 00 lea rcx, [rdi+0C0h]
.vmp0:0000000140005E75 48 29 D1 sub rcx, rdx
.vmp0:000000014000464C 48 8D 45 80 lea rax, [rbp-80h]
.vmp0:0000000140004655 24 F0 and al, 0F0h
.vmp0:000000014000465F 48 29 C8 sub rax, rcx
.vmp0:000000014000466B 48 89 C4 mov rsp, rax
.vmp0:0000000140004672 9C pushfq
.vmp0:000000014000467C 56 push rsi
.vmp0:0000000140004685 48 89 D6 mov rsi, rdx
.vmp0:00000001400057D6 48 8D BC 01 40 FF FF FF lea rdi, [rcx+rax-0C0h]
.vmp0:00000001400051FC 57 push rdi
.vmp0:000000014000520C 48 89 C7 mov rdi, rax
.vmp0:0000000140004A34 F3 A4 rep movsb
.vmp0:0000000140004A3E 5F pop rdi
.vmp0:0000000140004A42 5E pop rsi
.vmp0:0000000140004A48 9D popfq
.vmp0:0000000140004A49 E9 B0 0F 00 00 jmp calc_jmp
Обратите внимание на использование «movsb», которое используется для копирования содержимого временных регистров.
Виртуальные инструкции - коды операций, операнды, спецификации
Виртуальные инструкции состоят из двух или более операндов. Первый операнд - это код операции виртуальной инструкции. Коды операций представляют собой 8-битные значения без знака, которые при расшифровке являются индексом в таблице обработчика vm. Может быть второй операнд, который представляет собой непосредственное значение от одного до восьми байтов.
Все операнды зашифрованы и должны быть расшифрованы с помощью скользящего ключа дешифрования. Расшифровка выполняется внутри calc_jmp, а также в самих обработчиках vm. Обработчики виртуальных машин, выполняющие дешифрование, будут работать только с непосредственными значениями, а не с кодом операции.
Расшифровка операнда - трансформация
VMProtect 2 шифрует свои виртуальные инструкции с помощью скользящего ключа дешифрования. Этот ключ находится в RBX и изначально установлен на адрес виртуальных инструкций. Преобразования, выполняемые для дешифрования операндов, состоят из XOR, NEG, NOT, AND, ROR, ROL, SHL, SHR, ADD, SUB, INC, DEC и BSWAP. Когда операнд дешифруется, первое преобразование, применяемое к операнду, включает в себя скользящий ключ дешифрования. Таким образом, только XOR, AND, ROR, ROL, ADD и SUB будут первым преобразованием, применяемым к операнду. Затем к операнду всегда применяются три преобразования. На этом этапе операнд полностью расшифровывается, и значение в RAX будет содержать расшифрованное значение операнда. Наконец, скользящий ключ дешифрования обновляется путем преобразования скользящего ключа в полностью расшифрованное значение операнда. Пример выглядит так:
Код:
.vmp0:0000000140005A0A 30 D8 xor al, bl ; decrypt using rolling key...
.vmp0:0000000140005A11 F6 D8 neg al ; 1/3 transformations...
.vmp0:0000000140005A19 C0 C0 05 rol al, 5 ; 2/3 transformations...
.vmp0:0000000140005A26 FE C0 inc al 3/3 transformations...
.vmp0:0000000140005A2F 30 C3 xor bl, al ; update rolling key...
Фрагмент кода выше расшифровывает первый операнд, который всегда является кодом операции инструкции. Этот код является частью процедуры calc_jmp, однако формат преобразования одинаков для любых вторых операндов.
Обработчики виртуальных машин - Спецификации
Обработчики виртуальных машин содержат собственный код для выполнения виртуальных инструкций. Каждый двоичный файл VMProtect 2 имеет таблицу обработчика vm, которая представляет собой массив из 256 QWORD. Каждая запись содержит зашифрованный относительный виртуальный адрес соответствующего обработчика виртуальной машины. Существует множество вариантов виртуальных инструкций, таких как различные размеры непосредственных значений, а также знаковые и нулевые расширенные значения. В этом разделе будет рассмотрено несколько примеров виртуальных инструкций, а также некоторая ключевая информация, которую необходимо учитывать при попытке синтаксического анализа обработчиков виртуальных машин.
Обработчики виртуальных машин, которые обрабатывают немедленные значения, получают зашифрованное немедленное значение из RSI. Затем к этому зашифрованному немедленному значению применяются пять традиционных преобразований. Формат преобразования такой же, как и у преобразований calc_jmp. Первое преобразование применяется к зашифрованному немедленному значению, при этом скользящий ключ дешифрования является источником операции. Затем к зашифрованному немедленному значению применяются три преобразования, при этом значение полностью расшифровывается. Наконец, скользящий ключ дешифрования обновляется путем выполнения первого преобразования, за исключением того, что операнды назначения и источника меняются местами.
Код:
.vmp0:00000001400076D2 48 8B 06 mov rax, [rsi] ; fetch immediate value...
.vmp0:00000001400076D9 48 31 D8 xor rax, rbx ; rolling key transformation...
.vmp0:00000001400076DE 48 C1 C0 1D rol rax, 1Dh ; 1/3 transformations...
.vmp0:0000000140007700 48 0F C8 bswap rax ; 2/3 transformations...
.vmp0:000000014000770F 48 C1 C0 30 rol rax, 30h ; 3/3 transformations...
.vmp0:0000000140007714 48 31 C3 xor rbx, rax ; update rolling key...
Также обратите внимание, что обработчики vm подвергаются непрозрачному ветвлению, а также deadstore обфускации.
LCONST - положить константу на стек
Одна из самых знаковых инструкций виртуальных машин - LCONST. Эта виртуальная инструкция загружает в стек постоянное значение из второго операнда виртуальной инструкции.
LCONSTQ - положить константный QWORD
Это деобфусцированное представление обработчика виртуальной машины LCONSTQ. Как видите, этот обработчик виртуальной машины считывает второй операнд виртуальной инструкции из VIP (RSI). Затем он расшифровывает это непосредственное значение и продвигает VIP. Затем расшифрованное немедленное значение помещается в VSP.
Код:
mov rax, [rsi]
xor rax, rbx ; transformation
bswap rax ; transformation
lea rsi, [rsi+8] ; advance VIP…
rol rax, 0Ch ; transformation
inc rax ; transformation
xor rbx, rax ; transformation (update rolling decrypt key)
sub rbp, 8
mov [rbp+0], rax
LCONSTCDQE - положить константный DWORD, расширенную до QWORD
Эта виртуальная инструкция загружает операнд размера DWORD из RSI, расшифровывает его и расширяет до QWORD, наконец помещая его на виртуальный стек.
Код:
mov eax, [rsi]
xor eax, ebx
xor eax, 32B63802h
dec eax
lea rsi, [rsi+4] ; advance VIP
xor eax, 7E4087EEh
; look below for details on this...
push rbx
xor [rsp], eax
pop rbx
cdqe ; sign extend EAX to RAX…
sub rbp, 8
mov [rbp+0], rax
Обратите внимание, что этот обработчик vm обновляет скользящий ключ дешифрования, помещая значение в стек и применяя преобразование. Это то, что может вызвать серьезные проблемы при синтаксическом анализе этих обработчиков виртуальных машин. К счастью, есть очень простой способ справиться с этим, всегда помните, что преобразование, применяемое к подвижному ключу, такое же преобразование, как и первое. В приведенном выше случае это простой XOR.
LCONSTCBW - положить константный Byte, преобразовать в DWORD
LCONSTCBW загружает константу Byte из RSI, расшифровывает его, а ноль расширяет результат как значение WORD. Это расшифрованное значение затем помещается в виртуальный стек.
Код:
movzx eax, byte ptr [rsi]
add al, bl
inc al
neg al
ror al, 0x06
add bl, al
mov ax, [rax+rdi*1]
sub rbp, 0x02
inc rsi
mov [rbp], ax
LCONSTCWDE - положить константный WORD, преобразовать в DWORD
LCONSTCWDE загружает константу WORD из RSI, расшифровывает его, и расширяет до DWORD. Наконец, полученное значение помещается на виртуальный стек.
Код:
mov ax, [rsi]
add rsi, 0x02
xor ax, bx
rol ax, 0x0E
xor ax, 0xA808
neg ax
xor bx, ax
cwde
sub rbp, 0x04
mov [rbp], eax
LCONSTDW - положить константный DWORD
LCONSTDW загружает константу DWORD из RSI, расшифровывает его и, наконец, помещает результат на виртуальный стек. Также обратите внимание, что VIP перемещается назад в приведенном ниже примере. Вы можете увидеть это в выборке операнда как его вычитание из RSI перед разыменованием.
Код:
mov eax, [rsi-0x04]
bswap eax
add eax, ebx
dec eax
neg eax
xor eax, 0x2FFD187C
push rbx
add [rsp], eax
pop rbx
sub rbp, 0x04
mov [rbp], eax
add rsi, 0xFFFFFFFFFFFFFFFC
LREG - положить значение временного регистра на стек
Давайте посмотрим на другой обработчик виртуальной машины, на этот раз под названием LREG. Как и в случае с LCONST, существует множество вариантов этой инструкции, особенно для разных размеров. LREG также будет присутствовать в каждом отдельном двоичном файле, поскольку он используется внутри виртуальной машины для загрузки значений регистров в временные регистры. Подробнее об этом позже.
LREGQ - положить временный регистр как QWORD
LREGQ имеет непосредственное однобайтовое значение. Это индекс рабочего регистра. Указатель на временные регистры всегда загружается в RDI. Как неоднократно описывалось выше, к непосредственному значению применяются пять полных преобразований для его расшифровки. Первое преобразование применяется из скользящего ключа дешифрования, за которым следуют три преобразования, применяемые непосредственно к непосредственному значению, которое полностью его расшифровывает. Наконец, скользящий ключ дешифрования обновляется путем применения к нему первого преобразования с расшифрованным непосредственным значением в качестве источника.
Код:
mov al, [rsi]
sub al, bl
ror al, 2
not al
inc al
sub bl, al
mov rdx, [rax+rdi]
sub rbp, 8
mov [rbp+0], rdx
inc rsi
LREGDW - положить временный регистр как DWORD
LREGDW - это вариант LREG, который загружает DWORD из временного регистра в стек. Он имеет два операнда, второй из которых представляет собой один байт, представляющий индекс временного регистра. Приведенный ниже фрагмент кода представляет собой деобфусцированное представление LREGDW.
Код:
mov al, [rsi]
sub al, bl
add al, 97h
ror al, 1
neg al
sub bl, al
mov edx, [rax+rdi]
sub rbp, 4
mov [rbp+0], edx
SREG - Установить значение временного регистра
Еще одна знаковая виртуальная инструкция, которая есть в каждом отдельном двоичном файле, - это SREG. У этой инструкции есть много вариантов, которые устанавливают временные регистры на определенные значения. Эта виртуальная инструкция имеет два операнда, второй из которых представляет собой непосредственное однобайтовое значение, содержащее индекс временного регистра.
SREGQ - Установить значение временного регистра как QWORD
SREGQ устанавливает виртуальный рабочий регистр со значением QWORD, забирая заначение сверху виртуального стека. Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
Код:
movzx eax, byte ptr [rsi]
sub al, bl
ror al, 2
not al
inc al
sub bl, al
mov rdx, [rbp+0]
add rbp, 8
mov [rax+rdi], rdx
SREGDW - Установить значение временного регистра как DWORD
SREGDW устанавливает виртуальный рабочий регистр со значением DWORD, забирая заначение сверху виртуального стека. . Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
Код:
movzx eax, byte ptr [rsi-0x01]
xor al, bl
inc al
ror al, 0x02
add al, 0xDE
xor bl, al
lea rsi, [rsi-0x01]
mov dx, [rbp]
add rbp, 0x02
mov [rax+rdi*1], dx
SREGW - Установить значение временного регистра как WORD
SREGW устанавливает виртуальный рабочий регистр со значением WORD сверху виртуального стека. Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
Код:
movzx eax, byte ptr [rsi-0x01]
sub al, bl
ror al, 0x06
neg al
rol al, 0x02
sub bl, al
mov edx, [rbp]
add rbp, 0x04
dec rsi
mov [rax+rdi*1], edx
SREGB - Установить значения временного регистра как Byte
SREGB устанавливает виртуальный рабочий регистр со значением Byte сверху виртуального стека. Эта виртуальная инструкция состоит из двух операндов, второй из которых представляет собой один байт, представляющий виртуальный регистр.
Код:
mov al, [rsi-0x01]
xor al, bl
not al
xor al, 0x10
neg al
xor bl, al
sub rsi, 0x01
mov dx, [rbp]
add rbp, 0x02
mov [rax+rdi*1], dl
ADD - сложить два значения
Виртуальная инструкция ADD складывает два значения на стеке вместе и сохраняет результат во второй позиции значения в стеке. RFLAGS затем помещается на стек, поскольку инструкция ADD изменяет RFLAGS.
ADDQ - сложить два значения QWORD
ADDQ складывает два значения QWORD, хранящиеся в верхней части виртуального стека. RFLAGS также помещается в стек, поскольку инструкция ADD изменяет флаги.
Код:
mov rax, [rbp+0]
add [rbp+8], rax
pushfq
pop qword ptr [rbp+0]
ADDW - сложить два значения WORD
ADDW складывает два значения WORD, хранящиеся поверх виртуального стека. RFLAGS также помещается в стек, поскольку собственная инструкция ADD изменяет флаги.
Код:
mov ax, [rbp]
sub rbp, 0x06
add [rbp+0x08], ax
pushfq
pop [rbp]
ADDB - сложить два значения Byte
ADDB складывает два значения Byte, хранящиеся поверх виртуального стека. RFLAGS также помещается в стек, поскольку собственная инструкция ADD изменяет флаги.
Код:
mov al, [rbp]
sub rbp, 0x06
add [rbp+0x08], al
pushfq
pop [rbp]
MUL - Беззнаковое умножение
Виртуальная инструкция MUL умножает два значения, хранящихся на стеке. Эти обработчики vm используют нативную инструкцию MUL, кроме того RFLAGS помещается в стек. Наконец, это инструкция с одним операндом, что означает, что с этой инструкцией не связано никакого немедленного значения.
MULQ - Беззнаковое умножение QWORD
MULQ умножает два значения QWORD, результат сохраняется на стеке по адресу VSP+24, дополнительно RFLAGS помещается в стек.
Код:
mov rax, [rbp+0x08]
sub rbp, 0x08
mul rdx
mov [rbp+0x08], rdx
mov [rbp+0x10], rax
pushfq
pop [rbp]
DIV - Беззнаковое деление
Виртуальная инструкция DIV использует нативную инструкцию DIV, верхние операнды, используемые при делении, расположены на вершине виртуального стека. Это виртуальная инструкция с одним операндом, поэтому немедленного значения нет. RFLAGS также помещается на стек.
DIVQ - Беззнаковое деление QWORD
DIVQ делит два значения QWORD, расположенных в виртуальном стеке. Помещает RFLAGS на стек.
Код:
mov rdx, [rbp]
mov rax, [rbp+0x08]
div [rbp+0x10]
mov [rbp+0x08], rdx
mov [rbp+0x10], rax
pushfq
pop [rbp]
READ - чтение памяти
Инструкция READ читает память разного размера. Есть вариант этой инструкции для чтения одного, двух, четырех и восьми байтов.
READQ - прочитать QWORD
READQ считывает значение QWORD из адреса, хранящегося в верхней части стека. Кажется, что к этой виртуальной инструкции иногда добавляется сегмент. Однако не все обработчики виртуальных машин READQ связаны с этим
ss. Значение QWORD теперь хранится поверх виртуального стека.
Код:
mov rax, [rbp]
mov rax, ss:[rax]
mov [rbp], rax
READDW - прочитать DWORD
READDW считывает значение DWORD из адреса, хранящегося в верхней части виртуального стека. Затем значение DWORD помещается поверх виртуального стека. Ниже приведены два примера READDW, один из которых использует этот синтаксис индекса сегмента, а другой - не использует.
Код:
mov rax, [rbp]
add rbp, 0x04
mov eax, [rax]
mov [rbp], eax
Обратите внимание на использование смещения сегмента
ss…
Код:
mov rax, [rbp]
add rbp, 0x04
mov eax, ss:[rax]
mov [rbp], eax
READW - прочитать Word
READW считывает значение WORD из адреса, хранящегося в верхней части виртуального стека. Затем значение WORD помещается поверх виртуального стека. Ниже приведен пример этого обработчика vm, использующего синтаксис индекса сегмента, однако имейте в виду, что существуют обработчики vm без этого индекса сегмента.
Код:
mov rax, [rbp]
add rbp, 0x06
mov ax, ss:[rax]
mov [rbp], ax
WRITE - Запись в память
Виртуальная инструкция WRITE записывает до восьми байтов по данному ей адресу. Есть четыре варианта этой виртуальной инструкции, по одному для каждой степени от двух до восьми включительно. Существуют также версии каждого обработчика vm, которые используют кодировку инструкций типа смещения сегмента. Однако в длинном режиме базовые адреса некоторых сегментов равны нулю. Сегмент, который, кажется, всегда используется, - это сегмент SS с нулевой базой, поэтому база сегмента здесь не влияет, это просто немного затрудняет синтаксический анализ этих обработчиков vm.
WRITEQ - Записать QWORD
WRITEQ записывает значение QWORD по адресу, расположенному в верхней части виртуального стека. Стек увеличивается на 16 байтов.
Код:
.vmp0:0000000140005A74 48 8B 45 00 mov rax, [rbp+0]
.vmp0:0000000140005A82 48 8B 55 08 mov rdx, [rbp+8]
.vmp0:0000000140005A8A 48 83 C5 10 add rbp, 10h
.vmp0:00000001400075CF 48 89 10 mov [rax], rdx
WRITEDW - Записать DWORD
WRITEDW записывает значение DWORD по адресу, расположенному в верхней части виртуального стека. Стек увеличивается на 12 байтов.
Код:
mov rax, [rbp]
mov edx, [rbp+0x08]
add rbp, 0x0C
mov [rax], edx
Обратите внимание на использование смещения сегмента
ss ниже…
Код:
mov rax, [rbp]
mov edx, [rbp+0x08]
add rbp, 0x0C
mov ss:[rax], edx ; note the SS usage here...
WRITEW - Записать WORD
Виртуальная инструкция WRITEW записывает значение WORD по адресу, расположенному в верхней части виртуального стека. Затем стек увеличивается на десять байтов.
Код:
mov rax, [rbp]
mov dx, [rbp+0x08]
add rbp, 0x0A
mov ss:[rax], dx
WRITEB - Записать Byte
Виртуальная инструкция WRITEB записывает значение BYTE по адресу, расположенному на вершине виртуального стека. Затем стек увеличивается на десять байтов.
Код:
mov rax, [rbp]
mov dl, [rbp+0x08]
add rbp, 0x0A
mov ss:[rax], dl
SHL - Сдвиг влево
Обработчик SHL vm сдвигает значение, расположенное вверху стека, влево на определенное количество бит. Число битов для сдвига сохраняется над значением сдвига в стеке. Затем результат помещается в стек вместе с RFLAGS.
SHLCBW - Сдвиг влево, преобразование результата в WORD
SHLCBW сдвигает значение Byte влево, а ноль расширяет результат до WORD. RFLAGS помещается в стек.
Код:
mov al, [rbp+0]
mov cl, [rbp+2]
sub rbp, 6
shl al, cl
mov [rbp+8], ax
pushfq
pop qword ptr [rbp+0]
SHLW - Сдвиг влево WORD
SHLW сдвигает значение WORD влево. RFLAGS помещается в виртуальный стек.
Код:
mov ax, [rbp]
mov cl, [rbp+0x02]
sub rbp, 0x06
shl ax, cl
mov [rbp+0x08], ax
pushfq
pop [rbp]
SHLDW - сдвиг влево DWORD
SHLDW сдвигает DWORD влево. RFLAGS помещается на виртуальный стек.
Код:
mov eax, [rbp]
mov cl, [rbp+0x04]
sub rbp, 0x06
shl eax, cl
mov [rbp+0x08], eax
pushfq
pop [rbp]
SHLQ - сдвиг влево QWORD
SHLQ сдвигает QWORD влево. RFLAGS помещается в виртуальный стек.
Код:
mov rax, [rbp]
mov cl, [rbp+0x08]
sub rbp, 0x06
shl rax, cl
mov [rbp+0x08], rax
pushfq
pop [rbp]
SHLD - Cдвиг влево с двойной точностью
Виртуальная инструкция SHLD сдвигает значение влево с помощью собственной инструкции SHLD. Затем результат помещается в стек вместе с RFLAGS. Существует вариант этой инструкции для сдвигов в один, два, четыре и восемь байтов.
SHLDQ - Cдвиг влево с двойной точностью QWORD
SHLDQ сдвигает QWORD влево с двойной точностью. Затем результат помещается в виртуальный стек, а RFLAGS помещается в виртуальный стек.
Код:
mov rax, [rbp]
mov rdx, [rbp+0x08]
mov cl, [rbp+0x10]
add rbp, 0x02
shld rax, rdx, cl
mov [rbp+0x08], rax
pushfq
pop [rbp]
SHLDDW - Cдвиг влево с двойной точностью DWORD
Виртуальная инструкция SHLDDW сдвигает значение DWORD влево с двойной точностью. Результат помещается в виртуальный стек, а также в RFLAGS.
Код:
mov eax, [rbp]
mov edx, [rbp+0x04]
mov cl, [rbp+0x08]
sub rbp, 0x02
shld eax, edx, cl
mov [rbp+0x08], eax
pushfq
pop [rbp]
SHR - Cдвиг вправо
Инструкция SHR является дополнением к SHL, эта виртуальная инструкция изменяет RFLAGS, и, таким образом, значение RFLAGS будет наверху стека после выполнения этой виртуальной инструкции.
SHRQ - Cдвиг вправо QWORD
SHRQ сдвигает значение QWORD вправо. Результат помещается в виртуальный стек, какже как и RFLAGS.
Код:
mov rax, [rbp]
mov cl, [rbp+0x08]
sub rbp, 0x06
shr rax, cl
mov [rbp+0x08], rax
pushfq
pop [rbp]
SHRD - Cдвиг вправо с двойной точностью
Виртуальная инструкция SHRD сдвигает значение вправо с двойной точностью. Существует вариант этой инструкции для сдвигов в один, два, четыре и восемь байтов. Виртуальная инструкция завершается помещением RFLAGS на виртуальный стек.
SHRDQ - Cдвиг с двойной точностью вправо QWORD
SHRDQ сдвигает значение QWORD вправо с двойной точностью. Результат помещается на виртуальный стек. Затем RFLAGS помещается на виртуальный стек.
Код:
mov rax, [rbp]
mov rdx, [rbp+0x08]
mov cl, [rbp+0x10]
add rbp, 0x02
shrd rax, rdx, cl
mov [rbp+0x08], rax
pushfq
pop [rbp]
SHRDDW - Cдвиг с двойной точностью вправо DWORD
SHRDDW сдвигает значение DWORD вправо с двойной точностью. Результат помещается в виртуальный стек. RFLAGS затем помещается в виртуальный стек.
Код:
mov eax, [rbp]
mov edx, [rbp+0x04]
mov cl, [rbp+0x08]
sub rbp, 0x02
shrd eax, edx, cl
mov [rbp+0x08], eax
pushfq
pop [rbp]
NAND - И-НЕ
*Примечание переводчика - решил перевести Not Then And как И-НЕ
Инструкция NAND состоит из применения нативной NOT к значениям в верхней части стека, за которым следует результат. Инструкция и изменяет RFLAGS, поэтому RFLAGS будет помещен в виртуальный стек.
NANDW - И-НЕ для WORD
NANDW применяет NOT к двум значениям WORD, затем побитовое AND объединяет их. Затем RFLAG помещаются в виртуальный стек.
Код:
not dword ptr [rbp]
mov ax, [rbp]
sub rbp, 0x06
and [rbp+0x08], ax
pushfq
pop [rbp]
READCR3 - Чтение третьего регистра управления
Виртуальная инструкция READCR3 - это обработчик vm-оболочки вокруг нативного
mov reg, cr3. Эта инструкция поместит значение CR3 на виртуальный стек.
Код:
mov rax, cr3
sub rbp, 0x08
mov [rbp], rax
WRITECR3 - Записать третий регистр управления
Виртуальная инструкция WRITECR3 - это обработчик vm-оболочки вокруг нативного
mov cr3, reg. Эта инструкция поместит значение в CR3.
Код:
mov rax, [rbp]
add rbp, 0x08
mov cr3, rax
PUSHVSP - Положить на стек указатель виртуального стека
Виртуальная инструкция PUSHVSP помещает значение, содержащееся в нативном регистре RBP, в стек виртуального стека. Есть вариант этой инструкции для одного, двух, четырех и восьми байтов.
PUSHVSPQ - Положить на стек указатель виртуального стека QWORD
PUSHVSPQ помещает все значение указателя виртуального стека в виртуальный стек.
Код:
mov rax, rbp
sub rbp, 0x08
mov [rbp], rax
PUSHVSPDW - Положить на стек указатель виртуального стека DWORD
PUSHVSPDW помещает нижние четыре байта указателя виртуального стека в виртуальный стек.
Код:
mov eax, ebp
sub rbp, 0x04
mov [rbp], eax
PUSHVSPW - Положить на стек указатель виртуального стека WORD
PUSVSPW помещает нижнее значение WORD указателя виртуального стека в виртуальный стек.
Код:
mov eax, ebp
sub rbp, 0x02
mov [rbp], ax
LVSP - Загрузить указатель виртуального стека
Эта виртуальная инструкция загружает регистр указателя виртуального стека значением наверху стека.
Код:
mov rbp, [rbp]
LVSPW - Загрузить указателя виртуального стека WORD
Эта виртуальная инструкция загружает регистр указателя виртуального стека значением WORD наверху стека.
Код:
mov bp, [rbp]
LVSPDW - Загрузить указателя виртуального стека DWORD
Эта виртуальная инструкция загружает регистр указателя виртуального стека значением DWORD наверху стека.
Код:
mov ebp, [rbp]
LRFLAGS - Загрузить RFLAGS
Эта виртуальная инструкция загружает в нативный регистр флагов значение QWORD с верхней части стека.
Код:
push [rbp]
add rbp, 0x08
popfq
JMP - Инструкция виртуального прыжка
Виртуальная инструкция JMP изменяет регистр RSI, чтобы указать на новый набор виртуальных инструкций. Значение наверху стека - это нижние 32 бита RVA от базы модуля до виртуальных инструкций. Затем это значение добавляется к верхним 32 бита базового значения изображения, содержащегося в необязательном заголовке PE-файла. Затем к этому значению добавляется базовый адрес.
Код:
mov esi, [rbp]
add rbp, 0x08
lea r12, [0x0000000000048F29]
mov rax, 0x00 ; image base bytes above 32bits...
add rsi, rax
mov rbx, rsi ; update decrypt key
add rsi, [rbp] ; add module base address
CALL - Инструкция виртуального вызова
Инструкция виртуального вызова берет адрес вершины виртуального стека и затем вызывает его. RDX используется для хранения адреса, поэтому вы можете действительно вызывать функции только с одним параметром.
Код:
mov rdx, [rbp]
add rbp, 0x08
call rdx