Загрузка...

Virtualization as a method of code protection

Thread in Reversing / Assembler created by SaintHuman Aug 12, 2025. 621 view

  1. SaintHuman
    Привет!
    Сегодня мы заглянем в темный уголок нашего ремесла - виртуализацию. Если вы когда-нибудь, открыв очередного "пациента" в IDA Pro, вместо привычных ассемблерных инструкций видели нечто, похожее на кашу из байтов и непонятных переходов, то, скорее всего, вы столкнулись с ней, с "виртуалкой". Это не просто обфускация, это залупка со своими правилами, своей архитектурой и, конечно же, своими слабостями


    Что вообще такое виртуализация в контексте защиты софта? Забудьте про VMware или VirtualBox. Здесь идея похожа, но реализована на микроуровне. Вместо того чтобы эмулировать целую операционную систему, защитный механизм эмулирует свой собственный, выдуманный процессор. У этого "процессора" свой набор команд (виртуальные опкоды), свои регистры и своя логика работы. Разработчик защиты (например, создатели VMProtect или Themida) берет кусок оригинального, скомпилированного кода своей программы - скажем, критически важную функцию проверки лицензии - и "переводит" его с языка x86/x64 на язык этого выдуманного процессора

    В итоге, в исполняемом файле вместо CMP EAX, EBX и JE target_label появляется последовательность непонятных байтов. Это и есть наш виртуализированный код. А рядом с ним лежит "сердце" защиты - виртуальная машина, она же интерпретатор. Это довольно сложный кусок кода, который отвечает за чтение виртуальных опкодов один за другим и их исполнение

    Как эта залупка работает?
    Программа доходит до защищенного блока. Управление передается специальному коду-"диспетчеру" ВМ (виртуальная машина). Этот диспетчер инициализирует контекст нашей виртуальной машины: выставляет указатель на начало виртуального байткода (аналог EIP/RIP), подготавливает виртуальные регистры и так далее. А дальше начинается цикл, который в упрощенном виде можно представить так:
    1. Прочитать следующий виртуальный опкод из памяти
    2. Определить, что этот опкод значит. Обычно это делается через гигантский switch-case или таблицу указателей на обработчики (хендлеры)
    3. Выполнить соответствующий обработчик. Например, виртуальный опкод 0x01 может соответствовать обработчику, который эмулирует сложение двух чисел из виртуальных регистров
    4. Перейти к шагу 1

      И так до тех пор, пока не встретится специальный виртуальный опкод "выход из ВМ", который вернет управление обратно в обычный, нативный код

      Когда вы смотрите на это в дизассемблере, вы видите, как поток выполнения постоянно прыгает на разные обработчики внутри диспетчера. Логика оригинальной программы полностью размазана по десяткам, а то и сотням мелких функций-хендлеров. Пытаться понять, что происходит, просто глядя на граф вызовов в IDA, - это как пытаться собрать пазл из миллиона серых кусочков, смотря на них с расстояния в десять метров, короче пиздец

      Чё делать?

      Надо найти диспетчер. Это та самая точка, где крутится основной цикл "Fetch-Decode-Execute". То есть цикл команд, которому процессор следует с момента загрузки до завершения работы программы для обработки инструкций. Часто его можно опознать по характерной структуре, чтение байта по указателю, инкремент указателя, а затем большой switch или косвенный вызов через массив

      Как только мы нашли этот цикл, наша задача - "девиртуализировать" код. То есть перевести его обратно с языка ВМ на язык ассемблера x86 или хотя бы на высокоуровневый псевдокод. Это долгий и кропотливый процесс. Мы должны идентифицировать каждый обработчик, понять, какую именно нативную инструкцию он эмулирует, и присвоить ему осмысленное имя
      Например, мы заходим в отладчике в какой-нибудь handler_0x1A. Смотрим, что он делает: берет значение из одного виртуального регистра, берет другое из второго, складывает их с помощью нативной инструкции ADD и кладет результат в первый виртуальный регистр. Мы нашли обработчик виртуального сложения. Переименовываем его в IDA в v_ADD и идем дальше
      Постепенно, шаг за шагом, мы составляем карту соответствия виртуальных опкодов и их реальных действий. Это похоже на расшифровку языка: сначала мы находим пару символов, потом целые слова, а затем начинаем понимать грамматику. В конечном итоге, мы можем написать скрипт на Python с использованием фреймворков вроде Miasm

      Например, увидев последовательность байт 0x02, R1, 0x1234, 0x01, R1, R2, наш скрипт-девиртуализатор, зная, что 0x02 - это MOV_REG_IMM, а 0x01 - ADD_REG_REG, сможет сгенерировать что-то вроде:
    Code
    MOV R1, 0x1234
    ADD R1, R2
    Тем не менее, нет такой крепости, которую нельзя было бы взять. Терпение, внимательность и систематический подход приведут вас к результату. Процесс анализа виртуализированного кода - требует глубокого понимания как архитектуры процессора, так и принципов работы компиляторов и отладчиков. Но то чувство, когда после недель кропотливой работы перед тобой из непонятной мешанины байтов вдруг вырисовывается осмысленная логика оригинального алгоритма, бесценно)))

     
  2. cute
  3. becironnee
    :+rep: Завидую людям с такими знаниями низкой абстракции системного программирования. Усыновите меня пожалуйста.
     
Loading...