• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья VMProtect 2 - Детальный анализ архитектуры виртуальной машины

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Время на прочтение: 68 минут
Ссылка на скачивание: 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 - Инструкция виртуального вызова
  • Важные сигнатуры виртуальных машин - статический анализ
    • Поиск таблицы хэндлеров VM
    • Расшифровка записи в таблице хэндлеров виртуальной машины
    • Обработка трансформаций - шаблонные лямбды и map'ы
      • Извлечение преобразований - Продолжение статического анализа
    • Дилемма статического анализа - Заключение статического анализа
  • vmtracer - трассировка виртуальных инструкций
  • vmprofile-cli - Статический анализ с использованием трассировки времени выполнения
  • Отображение информации о трассировке - vmprofiler-qt
  • Поведение виртуальной машины
  • Demo - Осуществление и тестирование виртуальной тассировки
  • Изменение результатов виртуальных инструкций
  • Кодирование виртуальных инструкций - обратные преобразования
  • Заключение - статический и динамический анализ
Credit - Ссылки на существующие работы
Преамбула - Намерения и цели
Прежде чем погрузиться в этот пост, я хотел бы изложить несколько моментов в отношении существующей работы с 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.

1.png


Пример обфускации 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 запускается скользящий ключ дешифрования, который используется для дешифрования каждого операнда каждой отдельной виртуальной инструкции. Ключ скользящего дешифрования обновляется путем преобразования его в расшифрованное значение операнда.

2.png


Скользящее дешифрование
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, в частности, используется при расшифровке кода операции виртуальной инструкции.

3.png


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. Может быть второй операнд, который представляет собой непосредственное значение от одного до восьми байтов.

4.png


Все операнды зашифрованы и должны быть расшифрованы с помощью скользящего ключа дешифрования. Расшифровка выполняется внутри 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, однако формат преобразования одинаков для любых вторых операндов.

5.png


Обработчики виртуальных машин - Спецификации
Обработчики виртуальных машин содержат собственный код для выполнения виртуальных инструкций. Каждый двоичный файл VMProtect 2 имеет таблицу обработчика vm, которая представляет собой массив из 256 QWORD. Каждая запись содержит зашифрованный относительный виртуальный адрес соответствующего обработчика виртуальной машины. Существует множество вариантов виртуальных инструкций, таких как различные размеры непосредственных значений, а также знаковые и нулевые расширенные значения. В этом разделе будет рассмотрено несколько примеров виртуальных инструкций, а также некоторая ключевая информация, которую необходимо учитывать при попытке синтаксического анализа обработчиков виртуальных машин.

6.png


Обработчики виртуальных машин, которые обрабатывают немедленные значения, получают зашифрованное немедленное значение из 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
 
Важные сигнатуры виртуальных машин - статический анализ
Теперь, когда архитектура виртуальной машины VMProtect 2 задокументирована, мы можем задуматься о важных сигнатурах. Кроме того, обфускация, которую генерирует VMProtect 2, также может быть обработана довольно простыми методами. Это может сделать синтаксический анализ подпрограммы vm_entry тривиальным. vm_entry не имеет допустимых JCC, поэтому каждый раз, когда мы сталкиваемся с JCC, мы можем просто следовать за ним, удалять JCC из потока инструкций, а затем останавливаться, как только мы попадаем в JMP RCX / RDX. Мы можем удалить большую часть deadstore кода, проследив, как инструкция используется с Zydis, в частности, отслеживая зависимости чтения и записи от регистра назначения инструкции. Наконец, с очищенным vm_entry мы теперь можем перебирать все инструкции и находить обработчики vm, преобразования, необходимые для расшифровки записей таблицы обработчиков vm, и, наконец, преобразования, необходимые для расшифровки относительного виртуального адреса в виртуальные инструкции, помещенные в стек до переход к vm_entry.

Поиск таблицы хэндлеров VM
Одна из лучших и наиболее известных сигнатур - LEA r12, vm_handlers. Эта инструкция находится внутри фрагмента кода vm_entry и загружает линейный виртуальный адрес таблицы обработчика vm в R12. Используя Zydis, мы можем легко найти и проанализировать этот LEA, чтобы найти таблицу обработчиков vm самостоятельно.

C:
std::uintptr_t* vm::handler::table::get(const zydis_routine_t& vm_entry)
{
    const auto result = std::find_if(
        vm_entry.begin(), vm_entry.end(),
        [](const zydis_instr_t& instr_data) -> bool
        {
            const auto instr = &instr_data.instr;
            // lea r12, vm_handlers... (always r12)...
            if (instr->mnemonic == ZYDIS_MNEMONIC_LEA &&
                instr->operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
                instr->operands[0].reg.value == ZYDIS_REGISTER_R12 &&
                !instr->raw.sib.base) // no register used for the sib base...
                return true;

            return false;
        }
    );

    if (result == vm_entry.end())
        return nullptr;

    std::uintptr_t ptr = 0u;
    ZydisCalcAbsoluteAddress(&result->instr,
        &result->instr.operands[1], result->addr, &ptr);

    return reinterpret_cast<std::uintptr_t*>(ptr);
}

Вышеупомянутая процедура Zydis будет статически определять адрес таблицы обработчика виртуальной машины. Для этого требуется только вектор ZydisDecodedInstructions, по одному на каждую инструкцию в подпрограмме vm_entry. Моя реализация этого (vmprofiler) сначала деобфускирует vm_entry, а затем проходиться по этому вектору.

Расшифровка записи в таблице хэндлеров виртуальной машины
Вы можете легко программно определить, какое преобразование применяется к записям таблицы обработчиков виртуальных машин, сначала найдя инструкцию, которая выбирает записи из указанной таблицы. Эта инструкция задокументирована в разделе vm_entry, она состоит из инструкции SIB с RDX или RCX в качестве назначения, R12 в качестве базы, RAX в качестве индекса и восьмерки в качестве шкалы.

Код:
.vmp0:0000000140005A41 49 8B 14 C4            mov     rdx, [r12+rax*8]

Это легко найти с помощью Zydis. Все, что нужно сделать, это найти инструкцию mov SIB с RCX или RDX в качестве назначения, R12 в качестве базы, RAX в качестве индекса и, наконец, восемь в качестве индекса. Теперь, используя Zydis, мы можем найти следующую инструкцию с RDX или RCX в качестве пункта назначения, эта инструкция будет преобразованием, применяемым к записям таблицы обработчиков VM.

C:
bool vm::handler::table::get_transform(
    const zydis_routine_t& vm_entry, ZydisDecodedInstruction* transform_instr)
{
    ZydisRegister rcx_or_rdx = ZYDIS_REGISTER_NONE;

    auto handler_fetch = std::find_if(
        vm_entry.begin(), vm_entry.end(),
        [&](const zydis_instr_t& instr_data) -> bool
        {
            const auto instr = &instr_data.instr;
            if (instr->mnemonic == ZYDIS_MNEMONIC_MOV &&
                instr->operand_count == 2 &&
                instr->operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY &&
                instr->operands[1].mem.base == ZYDIS_REGISTER_R12 &&
                instr->operands[1].mem.index == ZYDIS_REGISTER_RAX &&
                instr->operands[1].mem.scale == 8 &&
                instr->operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
                (instr->operands[0].reg.value == ZYDIS_REGISTER_RDX ||
                    instr->operands[0].reg.value == ZYDIS_REGISTER_RCX))
            {
                rcx_or_rdx = instr->operands[0].reg.value;
                return true;
            }

            return false;
        }
    );

    // check to see if we found the fetch instruction and if the next instruction
    // is not the end of the vector...
    if (handler_fetch == vm_entry.end() || ++handler_fetch == vm_entry.end() ||
        // must be RCX or RDX... else something went wrong...
        (rcx_or_rdx != ZYDIS_REGISTER_RCX && rcx_or_rdx != ZYDIS_REGISTER_RDX))
        return false;

    // find the next instruction that writes to RCX or RDX...
    // the register is determined by the vm handler fetch above...
    auto handler_transform = std::find_if(
        handler_fetch, vm_entry.end(),
        [&](const zydis_instr_t& instr_data) -> bool
        {
            if (instr_data.instr.operands[0].reg.value == rcx_or_rdx &&
                instr_data.instr.operands[0].actions & ZYDIS_OPERAND_ACTION_WRITE)
                return true;
            return false;
        }
    );

    if (handler_transform == vm_entry.end())
        return false;

    *transform_instr = handler_transform->instr;
    return true;
}

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

Код:
.vmp0:0000000140005A41 49 8B 14 C4            mov     rdx, [r12+rax*8]
.vmp0:0000000140005A49 48 81 F2 49 21 3D 7F   xor     rdx, 7F3D2149h

Приведенный выше код эквивалентен приведенному ниже коду C ++. Он расшифрует записи обработчика vm. Чтобы зашифровать новые значения, необходимо выполнить обратную операцию. Для XOR это просто XOR.

C++:
vm::decrypt_handler _decrypt_handler =
    [](std::uint8_t idx) -> std::uint64_t
{
    return vm_handlers[idx] ^ 0x7F3D2149;
};

// this is not the best example as the inverse of XOR is XOR...
vm::encrypt_handler _encrypt_handler =
    [](std::uint8_t idx) -> std::uint64_t
{
    return vm_handlers[idx] ^ 0x7F3D2149;
};

Обработка трансформаций - шаблонные лямбды и map'ы
Вышеупомянутые обработчики дешифрования и шифрования могут быть динамически сгенерированы путем создания map'a c каждым типом преобразования и повторной реализации этой инструкции лямбда-выражением C++. Кроме того, может быть создана подпрограмма для обработки динамических значений, таких как размеры байтов. Это предотвращает switch case каждый раз, когда требуется преобразование.

C:
namespace transform
{
    // ...
    template <class T>
    inline std::map<ZydisMnemonic, transform_t<T>> transforms =
    {
        { ZYDIS_MNEMONIC_ADD, _add<T> },
        { ZYDIS_MNEMONIC_XOR, _xor<T> },
        { ZYDIS_MNEMONIC_BSWAP, _bswap<T> },
        // SUB, INC, DEC, OR, AND, ETC...
    };

    // max size of a and b is 64 bits, a and b is then converted to
    // the number of bits in bitsize, the transformation is applied,
    // finally the result is converted back to 64bits...
    inline auto apply(std::uint8_t bitsize, ZydisMnemonic op,
        std::uint64_t a, std::uint64_t b) -> std::uint64_t
    {
        switch (bitsize)
        {
        case 8:
            return transforms<std::uint8_t>[op](a, b);
        case 16:
            return transforms<std::uint16_t>[op](a, b);
        case 32:
            return transforms<std::uint32_t>[op](a, b);
        case 64:
            return transforms<std::uint64_t>[op](a, b);
        default:
            throw std::invalid_argument("invalid bit size...");
        }
    }
    // ...
}

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

C:
// here for your eyes - better understanding of the code :^)
using map_t = std::map<transform::type, ZydisDecodedInstruction>;

auto decrypt_operand(transform::map_t& transforms,
    std::uint64_t operand, std::uint64_t rolling_key) -> std::pair<std::uint64_t, std::uint64_t>
{
    const auto key_decrypt = &transforms[transform::type::rolling_key];
    const auto generic_decrypt_1 = &transforms[transform::type::generic1];
    const auto generic_decrypt_2 = &transforms[transform::type::generic2];
    const auto generic_decrypt_3 = &transforms[transform::type::generic3];
    const auto update_key = &transforms[transform::type::update_key];

    // apply transformation with rolling decrypt key...
    operand = transform::apply(key_decrypt->operands[0].size,
        key_decrypt->mnemonic, operand, rolling_key);

    // apply three generic transformations...
    {
        operand = transform::apply(
            generic_decrypt_1->operands[0].size,
            generic_decrypt_1->mnemonic, operand,
            // check to see if this instruction has an IMM...
            transform::has_imm(generic_decrypt_1) ?
                generic_decrypt_1->operands[1].imm.value.u : 0);

        operand = transform::apply(
            generic_decrypt_2->operands[0].size,
            generic_decrypt_2->mnemonic, operand,
            // check to see if this instruction has an IMM...
            transform::has_imm(generic_decrypt_2) ?
                generic_decrypt_2->operands[1].imm.value.u : 0);

        operand = transform::apply(
            generic_decrypt_3->operands[0].size,
            generic_decrypt_3->mnemonic, operand,
            // check to see if this instruction has an IMM...
            transform::has_imm(generic_decrypt_3) ?
                generic_decrypt_3->operands[1].imm.value.u : 0);
    }

    // update rolling key...
    rolling_key = transform::apply(key_decrypt->operands[0].size,
        key_decrypt->mnemonic, rolling_key, operand);

    return { operand, rolling_key };
}

Извлечение преобразований - Продолжение статического анализа
Возможность повторной реализации преобразований важна, однако возможность анализировать преобразования из обработчиков vm и calc_jmp - еще одна проблема, которая решит себя сама. Чтобы определить, где находятся преобразования, мы должны сначала определить, есть ли необходимость в преобразованиях. Преобразования применяются только к операндам виртуальных инструкций. Первый операнд виртуальной инструкции всегда преобразуется в одном и том же месте, этот код известен как calc_jmp, который я объяснил ранее. Второе место, где будут найдены преобразования, - это обработчики vm, которые обрабатывают немедленные значения. Другими словами, если виртуальная инструкция имеет непосредственное значение, для этого операнда будет уникальный набор преобразований. Немедленные значения считываются из VIP (RSI), поэтому мы можем использовать эту ключевую деталь, чтобы определить, есть ли немедленное значение, а также размер немедленного значения. Важно отметить, что немедленное значение, считываемое из VIP, не всегда равно размеру, выделенному для расшифрованного значения в стеке для таких инструкций, как LCONST. Это происходит из-за расширенных по знаку и нулевых виртуальных инструкций. Давайте рассмотрим пример виртуальной инструкции, которая имеет немедленное значение. Эта виртуальная инструкция называется LCONSTWSE, что означает «загрузить константу WORD, но знак расширен до DWORD». Деобфусцированный обработчик vm для этой виртуальной инструкции выглядит так:

Код:
.vmp0:0000000140004478 66 0F B7 06            movzx   ax, word ptr [rsi]
.vmp0:0000000140004412 66 29 D8               sub     ax, bx
.vmp0:0000000140004416 66 D1 C0               rol     ax, 1
.vmp0:0000000140004605 66 F7 D8               neg     ax
.vmp0:000000014000460A 66 35 AC 21            xor     ax, 21ACh
.vmp0:000000014000460F 66 29 C3               sub     bx, ax
.vmp0:0000000140004613 98                     cwde
.vmp0:0000000140004618 48 83 ED 04            sub     rbp, 4
.vmp0:0000000140006E4F 89 45 00               mov     [rbp+0], eax
.vmp0:0000000140007E2D 48 8D 76 02            lea     rsi, [rsi+2]

Как видите, из VIP считываются два байта. Это первая инструкция. Это то, что мы можем найти в zydis. Любые MOVZX, MOVSX или MOV, где RAX - место назначения, а RSI - источник, показывают, что существует немедленное значение, и, таким образом, мы знаем, что в потоке команд ожидается пять преобразований. Затем мы можем найти инструкцию, в которой RAX является местом назначения, а RBX - источником. Это будет первое преобразование. В приведенном выше примере первая инструкция вычитания - это то, что мы ищем.

Код:
.vmp0:0000000140004412 66 29 D8               sub     ax, bx

Затем мы можем найти три инструкции, которые имеют зависимость записи от RAX. Эти три инструкции будут общими преобразованиями, применяемыми к операнду.

Код:
.vmp0:0000000140004416 66 D1 C0               rol     ax, 1
.vmp0:0000000140004605 66 F7 D8               neg     ax
.vmp0:000000014000460A 66 35 AC 21            xor     ax, 21ACh

На этом этапе операнд полностью расшифрован. Единственное, что осталось, - это одно преобразование. Это последнее преобразование обновляет скользящий ключ дешифрования(RBX).

Код:
.vmp0:000000014000460F 66 29 C3               sub     bx, ax

Все эти инструкции преобразования теперь можно повторно реализовать с помощью лямбда-выражений C++ на лету. Использование std::find_if очень полезно для этих типов алгоритмов поиска, поскольку вы можете делать это шаг за шагом. Сначала найдите ключевые преобразования, затем найдите следующие три инструкции, которые пишут в RAX.

C++:
bool vm::handler::get_transforms(const zydis_routine_t& vm_handler, transform::map_t& transforms)
{
    auto imm_fetch = std::find_if(
        vm_handler.begin(), vm_handler.end(),
        [](const zydis_instr_t& instr_data) -> bool
        {
            // mov/movsx/movzx rax/eax/ax/al, [rsi]
            if (instr_data.instr.operand_count > 1 &&
                (instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOV ||
                    instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOVSX ||
                    instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOVZX) &&
                instr_data.instr.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
                util::reg::compare(instr_data.instr.operands[0].reg.value, ZYDIS_REGISTER_RAX) &&
                instr_data.instr.operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY &&
                instr_data.instr.operands[1].mem.base == ZYDIS_REGISTER_RSI)
                return true;
            return false;
        }
    );

    if (imm_fetch == vm_handler.end())
        return false;

    // this finds the first transformation which looks like:
    // transform rax, rbx <--- note these registers can be smaller so we to64 them...
    auto key_transform = std::find_if(imm_fetch, vm_handler.end(),
        [](const zydis_instr_t& instr_data) -> bool
        {
            if (util::reg::compare(instr_data.instr.operands[0].reg.value, ZYDIS_REGISTER_RAX) &&
                util::reg::compare(instr_data.instr.operands[1].reg.value, ZYDIS_REGISTER_RBX))
                return true;
            return false;
        }
    );

    // last transformation is the same as the first except src and dest are swapped...
    transforms[transform::type::rolling_key] = key_transform->instr;
    auto instr_copy = key_transform->instr;
    instr_copy.operands[0].reg.value = key_transform->instr.operands[1].reg.value;
    instr_copy.operands[1].reg.value = key_transform->instr.operands[0].reg.value;
    transforms[transform::type::update_key] = instr_copy;

    if (key_transform == vm_handler.end())
        return false;

    // three generic transformations...
    auto generic_transform = key_transform;

    for (auto idx = 0u; idx < 3; ++idx)
    {
        generic_transform = std::find_if(++generic_transform, vm_handler.end(),
            [](const zydis_instr_t& instr_data) -> bool
            {
                if (util::reg::compare(instr_data.instr.operands[0].reg.value, ZYDIS_REGISTER_RAX))
                    return true;

                return false;
            }
        );

        if (generic_transform == vm_handler.end())
            return false;

        transforms[(transform::type)(idx + 1)] = generic_transform->instr;
    }

    return true;
}

Как вы можете видеть выше, первое преобразование такое же, как последнее преобразование, за исключением того, что операнды источника и назначения меняются местами. VMProtect 2 допускает некоторые творческие вольности при применении последнего преобразования и иногда может помещать скользящий ключ дешифрования в стек, применять преобразование, а затем возвращать результат обратно в RBX. Это небольшое, но существенное неудобство можно устранить, просто поменяв местами регистры назначения и источника в переменной ZydisDecodedInstruction, как показано в приведенном выше коде.

Дилемма статического анализа - Заключение статического анализа
Дилемма при попытке статического анализа виртуальных инструкций заключается в том, что операции ветвления внутри виртуальной машины очень трудно обрабатывать. Чтобы вычислить, куда переходит виртуальный JMP, требуется эмуляция. Я займусь этим в ближайшем будущем.

vmtracer - трассировка виртуальных инструкций
Виртуальная трассировка инструкций легко достижима, исправляя каждую отдельную запись таблицы обработчика vm до зашифрованного значения, которое при расшифровке указывает на обработчик прерывания. Это позволит проверять регистры между инструкциями, а также изменять результат обработчика vm. Чтобы эффективно использовать эту функцию, важно понимать, какие регистры и какие значения содержат. Вы можете обратиться к разделу «Обзор - виртуальная машина VMProtect 2».

7.png


Первая и самая важная часть информации, которую нужно регистрировать при перехвате виртуальных инструкций, - это значение кода операции, которое находится в AL. Ведение журнала сообщит нам обо всех выполненных виртуальных инструкциях. Следующее значение, которое необходимо зарегистрировать, - это скользящее значение ключа дешифрования, которое находится в BL. Это позволит vmprofiler статически расшифровать операнды.

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

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

C++:
namespace vmp2
{
    enum class exec_type_t
    {
        forward,
        backward
    };

    enum class version_t
    {
        invalid,
        v1 = 0x101
    };

    struct file_header
    {
        u32 magic; // VMP2
        u64 epoch_time;
        u64 module_base;
        exec_type_t advancement;
        version_t version;
        u32 entry_count;
        u32 entry_offset;
    };

    struct entry_t
    {
        u8 handler_idx;
        u64 decrypt_key;
        u64 vip;

        union
        {
            struct
            {
                u64 r15;
                u64 r14;
                u64 r13;
                u64 r12;
                u64 r11;
                u64 r10;
                u64 r9;
                u64 r8;
                u64 rbp;
                u64 rdi;
                u64 rsi;
                u64 rdx;
                u64 rcx;
                u64 rbx;
                u64 rax;
                u64 rflags;
            };
            u64 raw[16];
        } regs;

        union
        {
            u64 qword[0x28];
            u8 raw[0x140];
        } vregs;

        union
        {
            u64 qword[0x20];
            u8 raw[0x100];
        } vsp;
    };
}

vmprofile-cli - Статический анализ с использованием трассировки времени выполнения
При наличии файла «vmp2» vmprofiler будет создавать псевдо-виртуальные инструкции, включая немедленные значения, а также затронутые временные регистры. Это ни в коем случае не девиртуализация и не дает представления о нескольких путях кода, однако дает очень полезную трассировку выполненных виртуальных инструкций. Vmprofiler также можно использовать для статического поиска таблицы обработчика vm и определения того, какое преобразование используется для расшифровки этих записей обработчика vm.

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

Код:
==========[vm handler LCONSTCBW, imm size = 8]=======
================[vm handler instructions]============
> 0x00007FF65BAE5C2E movzx eax, byte ptr [rsi]
> 0x00007FF65BAE5C82 add al, bl
> 0x00007FF65BAE5C85 add al, 0xD3
> 0x00007FF65BAE6FC7 not al
> 0x00007FF65BAE4D23 inc al
> 0x00007FF65BAE5633 add bl, al
> 0x00007FF65BAE53D5 sub rsi, 0xFFFFFFFFFFFFFFFF
> 0x00007FF65BAE5CD1 sub rbp, 0x02
> 0x00007FF65BAE62F8 mov [rbp], ax
=================[vm handler transforms]=============
add al, bl
add al, 0xD3
not al
inc al
add bl, al
=====================================================

Преобразования, если таковые имеются, также извлекаются из обработчика vm и могут выполняться динамически для расшифровки операндов.

Код:
> SREGQ 0x0000000000000088 (VSP[0] = 0x00007FF549600000) (VSP[1] = 0x0000000000000000)
> LCONSTDSX 0x000000007D361173 (VSP[0] = 0x0000000000000000) (VSP[1] = 0x0000000000000000)
> ADDQ (VSP[0] = 0x000000007D361173) (VSP[1] = 0x0000000000000000)
> SREGQ 0x0000000000000010 (VSP[0] = 0x0000000000000202) (VSP[1] = 0x000000007D361173)
> SREGQ 0x0000000000000048 (VSP[0] = 0x000000007D361173) (VSP[1] = 0x0000000000000000)
> SREGQ 0x0000000000000000 (VSP[0] = 0x0000000000000000) (VSP[1] = 0x0000000000000100)
> SREGQ 0x0000000000000038 (VSP[0] = 0x0000000000000100) (VSP[1] = 0x00000000000000B8)
> SREGQ 0x0000000000000028 (VSP[0] = 0x00000000000000B8) (VSP[1] = 0x0000000000000246)
> SREGQ 0x00000000000000B8 (VSP[0] = 0x0000000000000246) (VSP[1] = 0x0000000000000100)
> SREGQ 0x0000000000000010 (VSP[0] = 0x0000000000000100) (VSP[1] = 0x000000892D8FDA88)
> SREGQ 0x00000000000000B0 (VSP[0] = 0x000000892D8FDA88) (VSP[1] = 0x0000000000000000)
> SREGQ 0x0000000000000040 (VSP[0] = 0x0000000000000000) (VSP[1] = 0x0000000000000020)
> SREGQ 0x0000000000000030 (VSP[0] = 0x0000000000000020) (VSP[1] = 0x0000000000000000)
> SREGQ 0x0000000000000020 (VSP[0] = 0x0000000000000000) (VSP[1] = 0x2AAAAAAAAAAAAAAB)
// ...

Отображение информации о трассировке - vmprofiler-qt
Чтобы отобразить всю отслеживаемую информацию, такую как значения нативных регистров, значения временных регистров и значения виртуального стека, я создал очень небольшой проект Qt, который позволит вам выполнять трассировку. Я чувствовал, что консоль слишком ограничена, и мне также было трудно определить приоритеты того, что должно отображаться в консоли, поэтому возникла необходимость в графическом интерфейсе.

8.png


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

9.png


Другое поведение, которое демонстрирует архитектура виртуальной машины, заключается в том, что если нативная инструкция не реализована с помощью обработчиков vm, vmexit выполнит нативную инструкцию. В моей версии VMProtect 2 CPUID не реализован с обработчиками vm, поэтому происходит выход из виртуальной машины.

10.png


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

11.png


Demo - Осуществление и тестирование виртуальной тассировки
Для этой демонстрации я буду виртуализировать очень простой двоичный файл, который просто выполняет CPUID и возвращает true, если поддерживается AVX, иначе он возвращает false. Ассемблерный код примера показан ниже.

Код:
.text:00007FF776A01000 ; int __fastcall main()
.text:00007FF776A01000                 public main
.text:00007FF776A01000                 push    rbx
.text:00007FF776A01002                 sub     rsp, 10h
.text:00007FF776A01006                 xor     ecx, ecx
.text:00007FF776A01008                 mov     eax, 1
.text:00007FF776A0100D                 cpuid
.text:00007FF776A0100F                 shr     ecx, 1Ch
.text:00007FF776A01012                 and     ecx, 1
.text:00007FF776A01015                 mov     eax, ecx
.text:00007FF776A01017                 add     rsp, 10h
.text:00007FF776A0101B                 pop     rbx
.text:00007FF776A0101C                 retn
.text:00007FF776A0101C main            endp

Защищая этот код, я отказался от использования упаковки для простоты демонстрации. Я защитил двоичный файл настройками «Ультра», которые представляют собой просто обфускацию + виртуализацию. Глядя на PE-заголовок выходного файла, мы видим, что точка входа RVA - 0x1000, база изображения - 0x140000000. Теперь мы можем передать эту информацию vmprofiler-cli, и он должен предоставить нам таблицу обработчиков vm RVA, а также всю информацию об обработчиках vm.

Код:
> vmprofiler-cli.exe --vmpbin vmptest.vmp.exe --vmentry 0x1000 --imagebase 0x140000000

> 0x00007FF670F2822C push 0xFFFFFFFF890001FA
> 0x00007FF670F27FC9 push 0x45D3BF1F
> 0x00007FF670F248E4 push r13
> 0x00007FF670F24690 push rsi
> 0x00007FF670F24E53 push r14
> 0x00007FF670F274FB push rcx
> 0x00007FF670F2607C push rsp
> 0x00007FF670F24926 pushfq
> 0x00007FF670F24DC2 push rbp
> 0x00007FF670F25C8C push r12
> 0x00007FF670F252AC push r10
> 0x00007FF670F251A5 push r9
> 0x00007FF670F25189 push rdx
> 0x00007FF670F27D5F push r8
> 0x00007FF670F24505 push rdi
> 0x00007FF670F24745 push r11
> 0x00007FF670F2478B push rax
> 0x00007FF670F27A53 push rbx
> 0x00007FF670F2500D push r15
> 0x00007FF670F26030 push [0x00007FF670F27912]
> 0x00007FF670F2593A mov rax, 0x7FF530F20000
> 0x00007FF670F25955 mov r13, rax
> 0x00007FF670F25965 push rax
> 0x00007FF670F2596F mov esi, [rsp+0xA0]
> 0x00007FF670F25979 not esi
> 0x00007FF670F25985 neg esi
> 0x00007FF670F2598D ror esi, 0x1A
> 0x00007FF670F2599E mov rbp, rsp
> 0x00007FF670F259A8 sub rsp, 0x140
> 0x00007FF670F259B5 and rsp, 0xFFFFFFFFFFFFFFF0
> 0x00007FF670F259C1 mov rdi, rsp
> 0x00007FF670F259CB lea r12, [0x00007FF670F26473]
> 0x00007FF670F259DF mov rax, 0x100000000
> 0x00007FF670F259EC add rsi, rax
> 0x00007FF670F259F3 mov rbx, rsi
> 0x00007FF670F259FA add rsi, [rbp]
> 0x00007FF670F25A05 mov al, [rsi]
> 0x00007FF670F25A0A xor al, bl
> 0x00007FF670F25A11 neg al
> 0x00007FF670F25A19 rol al, 0x05
> 0x00007FF670F25A26 inc al
> 0x00007FF670F25A2F xor bl, al
> 0x00007FF670F25A34 movzx rax, al
> 0x00007FF670F25A41 mov rdx, [r12+rax*8]
> 0x00007FF670F25A49 xor rdx, 0x7F3D2149
> 0x00007FF670F25507 inc rsi
> 0x00007FF670F27951 add rdx, r13
> 0x00007FF670F27954 jmp rdx
> located vm handler table... at = 0x00007FF670F26473, rva = 0x0000000140006473

Мы видим, что vmprofiler-cli сгладил и деобфусцировал код vm_entry, а также обнаружил таблицу обработчиков vm. Мы также можем увидеть преобразование, выполненное для расшифровки сущностей обработчика vm, это XOR непосредственно после mov rdx, [r12 + rax * 8].

Код:
> 0x00007FF670F25A41 mov rdx, [r12+rax*8]
> 0x00007FF670F25A49 xor rdx, 0x7F3D2149

Мы также можем видеть, что VIP увеличивается, когда RSI увеличивается инструкцией INC.

Код:
> 0x00007FF670F25507 inc rsi

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

C++:
// lambdas to encrypt and decrypt vm handler entries
// you must extract this information from the flattened
// and deobfuscated view of vm_entry…

vm::decrypt_handler_t _decrypt_handler =
[](u64 val) -> u64
{

    return val ^ 0x7F3D2149;
};

vm::encrypt_handler_t _encrypt_handler =
[](u64 val) -> u64
{
    return val ^ 0x7F3D2149;
};

vm::handler::edit_entry_t _edit_entry =
[](u64* entry_ptr, u64 val) -> void
{
    DWORD old_prot;
    VirtualProtect(entry_ptr, sizeof val,
        PAGE_EXECUTE_READWRITE, &old_prot);

    *entry_ptr = val;
    VirtualProtect(entry_ptr, sizeof val,
        old_prot, &old_prot);
};

// create vm trace file header...
vmp2::file_header trace_header;
memcpy(&trace_header.magic, "VMP2", sizeof "VMP2" - 1);
trace_header.epoch_time = time(nullptr);
trace_header.entry_offset = sizeof trace_header;
trace_header.advancement = vmp2::exec_type_t::forward;
trace_header.version = vmp2::version_t::v1;
trace_header.module_base = module_base;

Я пропустил некоторые другие коды, такие как код ofstream и создание экземпляра класса vmtracer, вы можете найти этот код здесь. Основная цель этого отрывка кода - показать вам, как анализировать vm_entry и извлекать информацию, необходимую для создания трассировки.

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

C++:
// patch vm handler table...
tracer.start();

// call entry point...
auto result = reinterpret_cast<int (*)()>(
    NT_HEADER(module_base)->OptionalHeader.AddressOfEntryPoint + module_base)();

// unpatch vm handler table...
tracer.stop();

Теперь, когда файл трассировки создан, мы можем проверить трассировку с помощью vmprofiler-cli или vmprofiler-qt. Однако я бы посоветовал последнее, поскольку программа была явно создана для просмотра файлов трассировки.

При загрузке файла трассировки в vmprofiler-qt необходимо знать RVA vm_entry, а также базу изображения, содержащуюся в необязательном заголовке PE-файла. Учитывая всю эту информацию, а также исходный защищенный двоичный файл, vmprofiler-qt отобразит все виртуальные инструкции в файле трассировки и позволит вам «пошагово» выполнить его.

Давайте посмотрим на файл трассировки и посмотрим, сможем ли мы найти исходные инструкции, которые теперь были преобразованы в архитектуру RISC, основанную на стековой машине. Первый блок кода, который выполняется после vm_entry, похоже, не содержит кода, относящегося к исходному двоичному файлу. Это здесь просто для целей обфускации и предотвращения статического анализа виртуальных инструкций, так как для понимания того, где будет приземляться виртуальная инструкция JMP, потребуется эмуляция набора виртуальных инструкций. Этот первый блок перехода находится внутри каждого защищенного двоичного файла.

12.png


Следующий блок, следующий за виртуальной инструкцией JMP, выполняет несколько интересных математических операций, относящихся к стеку. Если вы присмотритесь, то увидите, что выполняется математическая операция:
Код:
sub(x, y) = ~((~(x) & ~(x)) + y) & ~((~(x) & ~(x)) + y); sub(VSP, 10).

Если мы упростим эту математическую операцию, мы увидим, что операция представляет собой вычитание, выполняемое для VSP. sub (х, у) = ~ ((~ х) + у). Это эквивалентно собственной операции sub rsp, 0x10. Если мы посмотрим на исходный двоичный файл, тот, который не виртуализирован, мы увидим, что эта инструкция действительно существует.

13.png


Показанный выше mov eax, 1 можно увидеть в виртуальных инструкциях сразу после вычитания, выполненного в VSP. MOV EAX, 1 выполняется через LCONSTBSX и SREGDW. Разрядность SREG соответствует ширине собственного регистра в 32 бита, а также загружаемому в него постоянному значению.

14.png


Далее мы видим, что происходит vmexit. Мы можем увидеть, где выполнение кода будет продолжаться за пределами виртуальной машины, перейдя к последнему ADDQ перед vmexit. Первые два значения в стеке должны быть базовым адресом модуля и 32-битным относительным виртуальным адресом подпрограммы, которая будет возвращена. В этой трассировке RVA - 0x140008236. Если мы проверим этот адрес в IDA, то увидим, что здесь находится инструкция «CPUID»

Код:
.vmp0:0000000140008236 0F A2                                         cpuid
.vmp0:0000000140008238 0F 81 88 FE FF FF                             jno     loc_1400080C6
.vmp0:000000014000823E 68 05 02 00 79                                push    79000205h
.vmp0:0000000140008243 E9 77 FD FF FF                                jmp     loc_140007FBF

Как видите, сразу после инструкции CPUID выполнение кода возвращается в виртуальную машину. Непосредственно после установки всех виртуальных рабочих регистров со значениями нативных регистров, расположенных в виртуальном стеке, в стек загружается константа со значением 0x1C. Результирующее значение CPUID затем сдвигается вправо на это постоянное значение.

15.png


Операция AND выполняется двумя операциями NAND. Первая NAND просто инвертирует результат SHR; invert(x) = ~ (x) & ~ (x). Это делается путем двойной загрузки значения DWORD в стек для создания одного QWORD.

16.png


Результат операции AND затем записывается в виртуальный рабочий регистр номер семь (SREGDW 0x38). Затем он перемещается в рабочий регистр 16. Если мы посмотрим на инструкцию vmexit и порядок, в котором выполняются LREGQ, мы увидим, что это действительно правильно.

17.png


Наконец, мы также можем увидеть инструкцию ADD и инструкцию LVSP, которая добавляет значение к VSP. Это ожидаемо, поскольку в исходном двоичном файле есть ADD RSP, 0x10.

18.png


Из приведенной выше информации мы можем восстановить следующие нативные инструкции:

Код:
sub rsp, 0x10
mov eax, 1
cpuid
shr ecx, 0x1C
and ecx, 1
mov eax, ecx ; from the LREGDW 0x38; SREGDW 0x80...
add rsp, 0x10
ret

Как видите, отсутствуют некоторые инструкции, в частности, push и pop RBX, а также XOR для обнуления содержимого ECX. Я предполагаю, что эти инструкции не преобразуются в виртуальные инструкции напрямую, а вместо этого реализуются окольными путями.

Изменение результатов выполнения виртуальных инструкций
Чтобы изменить виртуальные инструкции, нужно сначала полностью реализовать обработчик vm. Если обработчик vm расшифровывает второй операнд, необходимо помнить о важности достоверности ключа расшифровки. Таким образом, исходное непосредственное значение должно быть вычислено и применено к ключу дешифрования через исходное преобразование. Однако это значение может быть впоследствии отброшено после обновления ключа дешифрования. Примером этого может быть изменение постоянного значения от LCONST до SHR в приведенном выше разделе.

19.png


Эта виртуальная инструкция имеет два операнда, первый из которых представляет собой индекс обработчика vm для выполнения, а второй - непосредственное значение, которое в данном случае представляет собой один байт. Поскольку есть два операнда, внутри обработчика vm будет пять преобразований.

20.png


Мы можем перекодировать этот обработчик vm и сравнить расшифрованное немедленное значение с 0x1C, а затем перейти к подпрограмме, чтобы загрузить другое значение в стек. Это приведет к тому, что SHR вычислит другой результат. По сути, мы можем подделать результаты CPUID. Альтернативой этому могло бы быть воссоздание обработчика SHR, однако для простоты я просто перейду на установленный бит. В этом случае установлен бит 5 в ECX после CPUID, если поддерживается VMX и поскольку мой процессор поддерживает виртуализацию, этот бит будет высоким. Ниже представлен новый обработчик vm.

Код:
.data
    __mbase dq 0h
    public __mbase

.code
__lconstbzx proc
    mov al, [rsi]
    lea rsi, [rsi+1]
    xor al, bl
    dec al
    ror al, 1
    neg al
    xor bl, al

    pushfq            ; save flags...
    cmp ax, 01Ch
    je swap_val

                    ; the constant is not 0x1C
    popfq            ; restore flags...    
    sub rbp, 2
    mov [rbp], ax
    mov rax, __mbase
    add rax, 059FEh    ; calc jmp rva is 0x59FE...
    jmp rax

swap_val:            ; the constant is 0x1C
    popfq            ; restore flags...
    mov ax, 5        ; bit 5 is VMX in ECX after CPUID...
    sub rbp, 2
    mov [rbp], ax
    mov rax, __mbase
    add rax, 059FEh    ; calc jmp rva is 0x59FE...
    jmp rax
__lconstbzx endp
end

Если мы теперь снова запустим vm tracer с этим новым обработчиком vm, установленным на индекс 0x55, мы сможем увидеть изменение в LCONSTBZX. Чтобы облегчить этот хук, нужно установить виртуальный адрес нового обработчика vm в объект vm::handle::table_t

C++:
// change vm handler 0x55 (LCONSTBZX) to our implimentation of it…
auto _meta_data = handler_table.get_meta_data(0x55);
_meta_data.virt = reinterpret_cast<u64>(&__lconstbzx);
handler_table.set_meta_data(0x55, _meta_data);

Если мы запустим двоичный файл сейчас, он вернет 1. Вы можете увидеть это ниже.

21.png


Кодирование виртуальных инструкций - обратные преобразования
Поскольку VMProtect 2 генерирует виртуальную машину, которая выполняет виртуальные инструкции, закодированные в ее собственном байт-коде, можно запускать свои собственные виртуальные инструкции на виртуальной машине, если они могут их закодировать. Закодированные виртуальные инструкции также должны находиться в диапазоне адресного пространства 4 ГБ, хотя RVA для виртуальных инструкций имеет ширину 32 бита. В этом разделе я закодирую очень простой набор виртуальных инструкций, чтобы сложить два значения QWORD вместе и вернуть результат.

Для начала кодирование виртуальных инструкций требует, чтобы обработчики vm для указанных виртуальных инструкций находились внутри двоичного файла. Поиск этих обработчиков vm выполняется с помощью vmprofiler. Индекс обработчика vm - это первый код операции, а непосредственное значение, если оно есть, - второе. Объединение этих двух наборов операндов даст закодированную виртуальную инструкцию. Это первый этап сборки виртуальных инструкций, второй - шифрование операндов.

Когда у нас есть виртуальные инструкции, мы можем зашифровать их, используя обратные операции преобразования обработчика vm, а также обратные операции для calc_jmp. Важно отметить, что способ продвижения VIP должен приниматься во внимание при шифровании, поскольку порядок операндов и виртуальных инструкций зависит от направления продвижения.

22.png


Чтобы выполнить эти вновь собранные виртуальные инструкции, нужно поместить виртуальные инструкции в 32-битный диапазон адресов подпрограммы vm_entry, затем поместить зашифрованный rva в эти виртуальные инструкции в стек и, наконец, вызвать vm_entry. Я бы предложил использовать VirtualAllocEx для размещения страницы RW непосредственно под защищенным модулем. Ниже приведён пример выполнения виртуальных инструкций.

C++:
SIZE_T bytes_copied;
STARTUPINFOA info = { sizeof info };
PROCESS_INFORMATION proc_info;

// start the protected binary suspended...
// keep in mind this binary is not packed...
CreateProcessA("vmptest.vmp.exe", nullptr, nullptr,
    nullptr, false,
    CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
    nullptr, nullptr, &info, &proc_info);

// wait for the system to finish setting up...
WaitForInputIdle(proc_info.hProcess, INFINITE);
auto module_base = get_process_base(proc_info.hProcess);

// allocate space for the virtual instructions below the module...
auto virt_instrs = VirtualAllocEx(proc_info.hProcess,
    module_base + vmasm->header->offset,
    vmasm->header->size,
    MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

// write the virtual instructions...
WriteProcessMemory(proc_info.hProcess, virt_instrs,
    vmasm->data, vmasm->header->size, &bytes_copied);

// create a thread to run the virtual instructions...
auto thandle = CreateRemoteThread(proc_info.hProcess,
    nullptr, 0u,
    module_base + vm_entry_rva,
    nullptr, CREATE_SUSPENDED, &tid);

CONTEXT thread_ctx;
GetThreadContext(thandle, &thread_ctx);

// sub rsp, 8...
thread_ctx.Rsp -= 8;
thread_ctx.Rip = module_base + vm_entry_rva;

// write encrypted rva onto the stack...
WriteProcessMemory(proc_info.hProcess, thread_ctx.Rsp,
    &vmasm->header->encrypted_rva,
    sizeof vmasm->header->encrypted_rva, &bytes_copied);

// update thread context and resume execution...
SetThreadContext(thandle, &thread_ctx);
ResumeThread(thandle);

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

Хотя я задокументировал несколько виртуальных инструкций, гораздо больше я не задокументировал. Цель документирования виртуальных инструкций, которые у меня есть, - позволить читателю этой статьи получить представление о том, как должны выглядеть обработчики vm, а также как можно изменить результаты этих обработчиков vm. Документированные виртуальные инструкции в этой статье также являются наиболее распространенными. Эти виртуальные инструкции, скорее всего, будут внутри каждой виртуальной машины.

Я добавил несколько эталонных сборок в репозиторий, чтобы вы могли попробовать свои силы и заставить их вернуть 1, изменив обработчики vm. Также существует сборка, которая использует несколько виртуальных машин в одном двоичном файле.

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

От ТС
Это самый большой и самый комплексный перевод который я делал. Признаться, дался он мне не легко, я потратил много времени и сил.
Пришлось разделить на 2 поста, так как выходило больше 100к символов.
Если в тексте будут ошибки, и потребуются какие-то правки, не стесняйтесь мне писать.

В очередной раз спасибо admin, за предоставленый материал. И за редактор, отдельное, большое спасибо)

Также хотелось бы выразить уважение автору оригинала:
https://back.engineering/17/05/2021/
Он действительно проделал титаническую работу, анализируя VMProtect.

Перевод:
Azrv3l cпециально для xss.pro
 
Последнее редактирование:
Вышла вторая часть, очень ждём перевода.
 
Вышла вторая часть, очень ждём перевода.
Спасибо за наводку. Всё будет, как только освобожусь от дел
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх