Дык понятно что там далеко не все возможные команды. Имел в виду, что часть перечисленных в твоем файле команд (развернутых в опкодах вм) у меня отсутствовала.
Пришло время подвести некоторые итоги по декомпиляции ВМ. Для начала определимся с терминологией и устройством интерпретатора ВМ. 1. Что же представляет из себя пикод, поступающий на вход ВМ? Пикод состоит из последовательного набора примитивов, которые выполняются ВМ. Примитив представляет часть завиртуализованной и обфускированной команды нативного ассемблерного кода. Примитив состоит из поля кода (byte) и может иметь поле данных определенного размера (byte, word, dword), которое отводится под константу. Весь пикод хэширован определенным образом, ключом хэширования является адрес размещения пикода в памяти. Поля кода всех примитивов хэшируются по одному алгоритму, а поля данных каждого типа примитивов хэшируются своим алгоритмом. На точках условных и безусловных переходов, а также метках переходов осуществляется разрыв хэширования. 2. Устройство ВМ. ВМ состоит из интерпретатора примитивов и блока регистров. Интерпретатор в общем цикле декодирует примитив пикода и исполняет его. Нативный код интерпретатора обфускирован, причем алгоритм обфускации кода интерпретатора отличается от алгоритма обфускации нативного кода программы, реализованного в примитивах. Кол-во примитивов ВМ – 168. Большинство примитивов представляют одну значимую команду ассемблера, но имеются примитивы, состоящие из двух и трёх значимых команд. Значимая команда – команда ассемблерного кода выполняющая полезную работу, к которой можно преобразовать код выполнения примитива. Блок регистров ВМ состоит из 17 регистров. Все регистры имеют строго определенное назначение. Восемь регистров используются для хранения базовых регистров нативного кода, остальные регистры используются для хранения состояний ВМ. Весь пикод можно условно разбить на блоки нескольких типов: - ENTRY – блок входа в ВМ, используется для сохранения параметров нативного кода в регистрах ВМ. - RESTORE – блок выхода из ВМ, используется для восстановления параметров нативного кода из регистров ВМ. - CNGFLAG – блок возможного изменения регистра флагов процессора. - JMP и JCC – блоки безусловных и условных переходов. - CALL – блок вызова процедуры - ASSIGN – блоки присвоения четырех типов: памяти, регистров ВМ, нативных регистров и стековых переменных. В точках вызова процедур выполняется разрыв выполнения пикода следующим образом: RESTORE, CALL, ENTRY. 3. Реализации ВМ и их различия. Если процедура вызванная из пикода имеет защиту, реализованную на ВМ, то на каждый уровень вложенности процедур создается своя реализация ВМ. Принципы работы и структура всех ВМ одной программы одинаковы. Различия: - алгоритм хэширования примитивов - опкоды примитивов - алгоритм обфускации интерпретатора ВМ На первый раз хватит, прошу поправить, если я в чем-то ошибаюсь, или дополнить. В следующей статейке начну разбирать принципы декомпиляции ВМ (восстановление нативного кода программы).
Что начал, то и заканчиваю, а по большому счету я не вижу разницы в алгоритмах декодирования ВМ, используемых для защиты кода. Да, конкретная реализация ВМ может отличаться, но общие принципы её работы и декодирования остаются одинаковыми. И что интересно, для декомпилятора ВМ как нельзя лучше подошел Декомпилятор С++ в урезанном и доработанном виде, общие же принципы анализа и реструктуиризации кода остались прежними. Почему Code Virtualizer, а не VMProtect? Просто мне сейчас более актуальна первая защита, а здесь можно посмотреть их сравнение, если кому-то это интересно.
Продолжение… В дополнение к ранее сказанному прилагаю парочку листингов. Выбор базового инструмента, на основе которого будем строить декомпилятор ВМ. Сначала определим цель – восстановление первоначального кода программы. Именно рабочего кода, а не листингов по изучению работы ВМ и сокрытию исходного кода. Так как посредством ВМ произведена реструктуризация нативного кода, а нам необходимо его восстановить, то необходимо будет выполнять следующие операции над кодом – дизассемблирование, ассемблирование, динамическую трассировку и отладку. Необходимость предпоследней операции будет обоснована ниже. Отладка пригодится для проверки как промежуточных, так и окончательных результатов декомпиляции. Конечно, все эти процессы назвать декомпиляцией можно с большой натяжкой, по сути из ассемблерного кода мы получим ассемблерный код, функционально идентичный, но измененный до неузнаваемости, но общий процесс восстановления нативного кода будем называть именно так. Для выполнения нужных нам операций над кодом как нельзя лучше подходит универсальный отладчик OllyDbg с возможностью создания плагинов. В качестве второго альтернативного инструмента можно рассмотреть IDA, но с одной стороны его возможности будут избыточны (дизассемблирование с возможностью восстановления исходных типов), а с другой недостаточны (трассировка, отладка и изменение кода). Выбираем OllyDbg и создаем плагин декомпиляции ВМ. Именно плагин, так как возможности скриптового языка OllyDbg для наших целей будет недостаточно. База декомпилятора. Первоначально необходимо выполнить ручную работу по изучению интерпретатора ВМ и разбору кода исполнения примитивов для любой реализации ВМ. Результат – построение списка значимых команд для примитивов и основного цикла ВМ с созданием сигнатурного шаблона каждого примитива. В приведенных листингах это можно увидеть. Шаблон нам понадобится для определения декомпилятором опкода примитива конкретной реализации ВМ (решение второго и третьего различия в реализации ВМ). Сложнее обстоит дело с определением алгоритма хеширования некоторых примитивов, имеющих поля данных, и хеширования опкода примитива основным циклом ВМ (решение первого различия в реализации ВМ). Пока эта часть в программе не реализована, т.е. работает всего один прототип ВМ без автоматической возможности выбора нужной реализации ВМ. К решению этой задачи мы вернемся позже. Поехали… Для исключения лишних вопросов скажу, что все дальнейшее, о чем будет идти речь, уже реализовано и работает. Запускаем OllyDbg с исследуемой программой, предварительно с программы должна быть снята запаковка, находим нужную защищенную функцию, ставим точку останова на вход в ВМ, доходим до неё и вызываем декомпилятор. Всю дальнейшую работу он делает самостоятельно. Продолжение следует…
Vam Насколько я понял у CV контект ВМ лежит в статическом массиве? Если это так, то как интересно народ умудряется использовать ЭТО в многопоточных приложениях. ИМХО это сразу все загнется.
Да, в том же сегменте исполняемого кода, в котором находится и сама ВМ. Сначала идут 17 регистров, затем таблица переходов на тела примитивов, видно отсюда (опкод начального примитива 0х11): Код (Text): /* исполнение примитива */ jmp dword ptr [edi+eax*4] а далее точка входа в ВМ (VM_Entry. Повторю вопрос TSS, почему загнется? А палка то о двух концах..., один раз сломали, рассказали, нашли хилые моменты -> а автор родил новую защиту, уже без этих недостатков. Правда, клиенты пострадали, кто уже приобрел и использует... Прогресс, а иначе никак...
TSS Если на этой ВМ будет работать сразу 2 потока, то тот кто сохранил свой контекст последним - тот и папа. Соотвественно первый поток скорее всего выпадет с AV, т.к. будет работать не со своим контекстом.
Vam Обычно защита как рза пишется с учетом слабых сторон, инструментов с помощью которых она взламывается. В данном случае идет о декомпиляторе - сейчас узкое место это как раз идентификация примитивов (выражаясь терминами Vam-а): Вот вокруг этого как раз все и крутится. На начальном этапе достаточно будет усилить мутацию исполнителя ВМ, а на сладкое - рожать более сложные примитивы исходя из полученного пикода (макрокоманды). Определение того что же делает такой примитив - задача из области эвристики.
Хорошие исходники вм протектора уже есть , публично опубликованы будут после сравнения различных версий защиты. Цель проследить логику автора c предположительным дальнейшим развитием. Скорее всего будет сделан общий анализатор всех версий.
Узкое, потому-что оно не реализовано? Но это не значит, что оно узкое, просто руки пока не дошли, получим исходный код от одной реализации ВМ и вернемся к этому куску. Это ничего не даст, уже сейчас каждая реализация ВМ имеет разную мутацию. Мутация создается автоматически программой защиты, как же автоматически она и может убираться декомпилятором. А вот это называется виртуализацией и программа защиты пока не в силах сама родить новые методы виртуализации без участия человека. Конечно, можно настругать человеку несколько разных примитивов с одинаковой функциональностью, а программа автоматом будет их вставлять в разные реализации ВМ. Но, их число конечно и есть такая удивительная операция OR, посредством которой на один примитив можно будет применить конечное число созданных сигнатурных шаблонов. Весь вопрос будет в том, с чего создать этот шаблон, если нужный нам примитив будет появляться в защищаемой программе раз в год, то вероятность, что он будет реализован в декомпиляторе равняется нулю, но тогда аналогичная вероятность и у пользователя защиты, что его программу не взломают. Но выход есть и из этого положения, создавать сигнатурный шаблон примитива не внутри декомпилятора, а вне его по определенным правилам, хотя это на сегодняшний день и не нужно.