кто-нибудь ломал VMProtect

Тема в разделе "WASM.RESEARCH", создана пользователем avraam, 27 апр 2009.

  1. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    domino
    Да видели мы уже ваши "хорошие" исходники. Оставьте их себе :))
    Vam
    Нет - можно пойти другим путем. Каким - я пока не буду говорить :))
     
  2. reversecode

    reversecode Guest

    Публикаций:
    0
    ждем новой версии VMP
    он уже есть но его не выкладывают =)))?
     
  3. kioresk

    kioresk New Member

    Публикаций:
    0
    Регистрация:
    28 май 2009
    Сообщения:
    9
    Адрес:
    REVENGE Crew
    avraam,
    Какая версия протектора тебя интересует и какие инструкции (конвертированные в опкоды вм) тебе нужны?

    Vam
    Плагин для отладчика конечно вариант неплохой, но опять же повторюсь — подобный разбор надо делать в статике. Что если в программе десятки виртуализованных участков, часть из которых не обязательно будет выполняться, т.е. на которых не будут срабатывать бряки?

    dermotolog
    В виртуальной машине ореановских продуктов реализована поддержка многопоточности. Во-первых, разные куски виртуализуемого кода могут выполняться на различных ВМ, но даже если 2 куска кода (выполняющиеся в разных потоках) используют одну и туже ВМ, то проблем не будет, т.к. в начале обработчика проверяется не занят ли он. Т.е. если 1-й поток использует текущий обработчик, 2-й будет ждать пока обработчик не освободится.

    Кстати, если мне не изменяет память, в их продуктах реализация нескольких ВМ появилась раньше, чем в VMProtect'е, поэтому не стоит думать, что она у них плохо реализована. :derisive:
     
  4. dermatolog

    dermatolog Member

    Публикаций:
    0
    Регистрация:
    3 фев 2005
    Сообщения:
    406
    Адрес:
    Екатеринбург
    kioresk
    Возможно ты прав - я особо не разбирался в структуре CV-ной ВМ, поэтому сужу по архитектуре из выложенного Vam-ом кода. В любом случае насколько я знаю CV не поддерживает исключения и даже при такой архитектуре с поиском незанятого места под контекст - освободить контекст при эксепшине скорее будет некому и свободные хендлеры могут "быстро" кончится. Вобщем это все очень ненадежно.
     
  5. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Я же написал
    , неужели подождать сложно, а то меня так и тянет прямо сейчас обосновать динамику, а не в нужном месте... :)
     
  6. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Продолжим…
    Шаг 1
    Проверяем, что мы действительно стоим на точке входа, вернее не на самой VM_Entry, а несколько выше:
    Код (Text):
    1. //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    2. /* Защищенная функция (один из вариантов) */
    3. //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    4. xxx:
    5.         prolog
    6.         …
    7.         jmp     segVM_PI_xxx    /* здесь точка останова */
    8.         "мусор"
    9.         ...
    10.         "мусор"
    11. returnVM_ххх:
    12.         epilog
    13.         retn
    14. //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    15.         ...
    16. segVM_PI_xxx:
    17.         /* адрес таблицы пикода */
    18.         push        PIcode_xxx
    19.         jmp     VM_Entry
    20.  
    21. //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    Проводится анализ тела ВМ на предмет обрабатываемости данной ВМ декомпилятором. Далее должен идти автоматический выбор конкретной реализации ВМ с составлением таблицы опкодов примитивов и алгоритма хеширования, но как говорил ранее, этот кусок пока не реализован, поэтому после проверки ВМ сразу входим в реализованный прототип ВМ.
    Далее принимается за работу декодировщик/трассировщик пикода. Он работает в статическо-динамическом режиме. Его функции:
    1. Декодирование опкода примитива.
    2. «Исполнение примитива», т.е. декодирование поля данных примитива, если оно имеется и статическая трассировка всех нативных регистров, регистров ВМ, переменных в памяти и стека.
    3. Создание из значимых команд примитивов промежуточного рабочего ассемблерного кода, заменяющего интерпретатор ВМ.
    4. Создание полного листинга «дизассемблирования» пикода.
    Особенности работы его работы:
    - Трассировка стека.
    Для правильной трассировки стека необходимо отслеживать все моменты помещения и изъятия из него данных. Статическая трассировка с этим справляется, кроме одного случая, ей неизвестно кол-во аргументов вызванной функции передаваемое через стек. Как мы знаем большинство функций при завершении своей работы «чистит» за собой стек, т.е. удаляет переданные ей через стек аргументы. В этом случае, чтобы узнать, что «почистила» функция необходимо заглянуть в неё, но в некоторых случаях при косвенных вызовах функции (виртуальные, интерфейсные, динамически-табличные и др.) это сделать статической трассировкой не представляется возможным. Решим эту задачу следующим образом, ставим точку останова на вход вызываемой функции, доходим до неё (не забываем, что мы работаем в Olly), запоминаем вершину стека, для листинга берем из Olly аргументы функции, ставим точку останова на вход в ВМ (здесь осуществляется разрыв выполнения пикода, как отмечал ранее), доходим до неё, по разнице вершин стека определяем и запоминаем кол-во аргументов вызванной функции. Далее продолжаем статический анализ. Вы можете сказать, как быть в том случае, если пикод ветвится и нужная функция не будет из него вызвана? Ниже рассмотрим и этот вариант.
    - Трассировка условных и безусловных переходов.
    Адрес любого перехода и место перехода запоминаются, с созданием «слепка» состояния трассировки. Начнем наиболее с простых безусловных переходов. Декодировщик такой переход «разгибает», то есть следует по нему, если переход идет в уже декодиронный блок (back), то в нем ищется первый условный переход за пределы этого блока, восстанавливается слепок трассировки и продолжается статический анализ после перехода. Условные переходы статический трассировщик «игнорирует», т.е. идет всегда прямо, но ставит точку останова на него, если переход не обратный. Далее, если после перехода встречается вызов функции, запускается динамический отладчик и срабатывает точка останова на условном переходе, анализируем флаги, определяем нужный нам путь, и если необходимо то «переводим стрелку» изменяя флаги и направляя отладчик на нужную нам функцию. В итоге мы имеем разобранным весь пикод защищенной функции.
    По завершению работы декодировщика/трассировщика выполняем ещё несколько нужных моментов:
    - если необходимо меняем местами блоки кода в порядке возрастания адресов.
    - осуществляем релокейшен всем переходам.
    - заменяем переход segVM_PI_xxx на переход на наш созданный промежуточный рабочий код, точка останова на нем уже стоит, поэтому просто рестартуем Olly вместе с программой и попадаем в это место.
    Итог шага: ВМ исключена из работы и заменена декодированным промежуточным рабочим кодом. Этот код является «грязным», т.к. имеет большую степень обфускации, вот его чисткой мы и займемся далее.

    Во вложении находится полный листинг «дизассемблированного» пикода, а в посте #190 находится полный листинг промежуточного рабочего кода.

    Продолжение следует…
     
  7. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Продолжение…
    Шаг 2
    Дизассемблируем и конвертируем полученный промежуточный код в структуры инструкций (insn_t) и операндов (op_t), позаимствованные у IDA. На это есть две причины. Первое – структуры IDA, описывающие команды ассемблера очень упорядоченные, по сравнению со структурами Olly, в которых, мягко говоря, полный бардак. Второе – дальнейший обработчик кода позаимствован из Декомпилятора С++, который работает с IDA’шными структурами. Создаем карту всех переходов с сохранением/восстановлением состояний трассировки кода в точках возможного разрыва линейного анализа. Например, блоки if … else. Файл карты xRefs.stat есть во вложении. Инициализируем структуры всех регистров, стека и памяти. В этой точке мы готовы к статической трассировке промежуточного кода.
    Трассировку проводим в два прохода. Первый проход линейный, разрывов потоков данных в точках присвоения (их мы ещё и не выявили) не происходит, в местах взаимо исключаемых переходов осуществляется сохранение-восстановление всех данных.
    На первом проходе:
    - строятся массивы точек изменения состояний нативных регистров (EMPTY, UNKNOWN, INIT, CHANGE, USE)
    - строятся массивы всех присвоений ячейкам памяти и изменений значений в памяти
    - создаются логические блоки следующих типов: CNGFLAG, JMP и JCC, CALL и осуществляется юстировка их границ, если требуется
    - определяются границы блоков входа и выхода из ВМ (ENTRY/RESTORE)
    Состояние после первого прохода приведено в логе, секция 000.
    После первого прохода создаем:
    - Блоки ENTRY и RESTORE
    - Блоки всех присвоений и изменений для всех ячеек памяти - ASSIGN (MEM)
    - Блоки всех присвоений и изменений для всех регистров ВМ - ASSIGN (REGVM), это та же ячейка памяти, только находится по специальному адресу.
    - Блоки некоторых присвоений и изменений для некоторых нативных регистров - ASSIGN (REG) по специальному алгоритму
    - Блоки всех присвоений (помещение в стек) для всех аргументов функций - ASSIGN (ARG)
    Состояние приведено в логе, секция 001.
    Далее следуют несколько обработчиков созданных блоков, которые вызываются последовательно, в нашем случае, первый обработчик юстирует начальные границы блоков, когда переход идет внутрь блока или на пустое место, которое не принадлежит ни одному блоку, в этом месте создается блок «пустышка» DUMMY. Второй обработчик корректирует начальные границы блоков для исключения разрывов. Состояние приведено в логе, секция 003.
    Для данного примера, декодируемая функция, этого набора блоков (8) и их обработчиков (2) достаточно, в дальнейшем, при декомпилировании других функций этот набор может быть расширен без изменения общей структуры программы. Например, в Декомпиляторе С++ кол-во создаваемых блоков равно 25, не считая их разновидности, а кол-во обработчиков – 29.
    Второй проход трассировки так же линейный, но выполняются разрывы данных в местах их присвоения или изменения. Выполняется он следующим образом: так как блоки перекрывают весь декодируемый код трассировка кода выполняется в контексте конкретного блока, здесь создаются логические структуры с параметрами присвоений, переходов, анализа флагов, вызова функций и т.д. Данные при перемещении или изменении из/в регистров, ячеек памяти и стека не затирают друг друга, а выстраиваются в логические цепочки выражений. Далее эти структуры выстраиваются в строки (не текстовые), которые составят двусвязный список текста. Результат этого прохода виден в логе, секция а00. Как видно большая часть обфускации кода после этих операций исчезла.
    Итог шага: Закончена обработка ассемблерного кода. Мы перешли на другой уровень представления информации. Вся дальнейшая обработка будет производиться над строками и текстом.

    Продолжение следует…
     
  8. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Подскажите, наткнулся на небольшую проблемку, полученный исходный код не лезет в отведенное ему место в функции, т.к. он на несколько байт длиннее. Все переходы ассемблировались в 6 байт длиной, хотя short переходы могли бы быть и по 2 байта.
    С теми переходами, которые вверх - всё просто, метка уже есть в ассемблированном коде и он легко делается коротким, если до метки близко. А как быть с переходами вниз, во время создания перехода код с меткой ещё отсутствует. Как ассемблер может точно определить переход вниз, короткий он или длинный?
     
  9. Y_Mur

    Y_Mur Active Member

    Публикаций:
    0
    Регистрация:
    6 сен 2006
    Сообщения:
    2.494
    Только через многопроходность. Или резервировать место всегда под длинный переход, а потом заполнять излишки nop-ами, если ничего не путаю tasm так в однопроходном режиме поступал.
     
  10. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Так и делаю, сначала длинный переход, на старую метку, затем когда весь код построен, переходы корректируются под новые метки, но здесь они могут быть и короткими, но созданный код сдвигать уже нельзя, метки съедут. А места нехватает всего несколько байт.
    И сколько проходов делать, пока метки не перестанут смещаться?
     
  11. diamond

    diamond New Member

    Публикаций:
    0
    Регистрация:
    21 май 2004
    Сообщения:
    507
    Адрес:
    Russia
    Для этого придумали специальный модификатор short:
    Код (Text):
    1. jmp short l1
    2. ...
    3. l1:
    Если l1 близко, то выделяется всего два байта. Если далеко, то выдаётся ошибка компиляции.
     
  12. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Это не то: short пишется когда точно уверен, что переход короткий. На первом проходе асм не может знать длину перехода, т.к. метка ещё не пройдена, поэтому ставит длинный переход и создает две таблицы меток и переходов. После ассемблирования из таблицы преходов берется переход, ищется для него метка, и корректируется переход в коде, но здесь выявляется, что переход мог бы быть коротким, но под него уже зарезервировано 6 байт и чтобы сделать его коротким, нужно переассемблировать весь код от перехода до конца текста. И повторить это энное кол-во раз со всеми переходами уходящими вниз, даже уже откорректированными, т.к. первоначально длинный переход может превратиться в короткий. В общем этот процесс выглядит не так тривиально, как кажется. Эффект оптимизации переходов будет достигнут тогда, когда после очередного ассемблирования при коррекции переходов ни один из них не изменится с длинного на короткий.
    Конечно, процесс оптимизации переходов можно упростить, если при самом первом проходе учитывать максимально возможное смещение от перехода до неопределенной метки = кол-во команд * мах длину команды (7 в общем случае), тогда, уже первоначально переход может быть коротким, в длинный в дальнейшем он уже никак не превратится.
     
  13. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Вопрос с оптимизацией асм кода по длине команд решен. Некоторая статистика:
    Место под код в функции - 304 байта. До оптимизации кода не хватало места на 47 байт.
    После оптимизации размера кода команд ассемблера не хватало 2 байта. После оптимизации переходов осталось свободными 39 байт.
    В итоге оптимизации размер кода уменьшен на 86 байт (28%), сами же ассемблерные команды (текстовое представление) остались без изменения.
     
  14. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Продолжение…
    Шаг 3 (заключительный)
    Удаляем незначимые присвоения типа: ххх = ххх.
    Поглощаем регистры состояний ВМ. Лог, секция а01.
    Поглощаем промежуточную стековую переменную __$esp. Лог, секция а02.
    Поглощаем прямые регистровые переменные __$edx и другие. Лог, секция а03.
    Производим деобфускацию констант и корректируем стековые смещения. Лог, секция а04.
    Устраняем вынужденные присвоения операторов изменения. Лог, секция а05.
    Заменяем регистры ВМ нативными регистрами. Лог, секция а06.
    Ассемблируем полученный листинг, оптимизируем ассемблерный код по размеру и вставляем в функцию. Конечный вариант DecodeFunc.txt
    Всё – задача выполнена, восстановлен исходный код защищенной функции.
     
  15. je_

    je_ New Member

    Публикаций:
    0
    Регистрация:
    27 янв 2004
    Сообщения:
    143
    молодеЦЪ
     
  16. TSS

    TSS New Member

    Публикаций:
    0
    Регистрация:
    13 апр 2009
    Сообщения:
    494
    Vam
    Блок CNGFLAG - это что?
     
  17. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Это любая команда ассемблера модифицирующая флаги.
     
  18. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Извиняюсь за неточный ответ. Этот блок создается в том случае, если примитив ВМ изменяет регистр флагов и записывет его в стек, чтобы в дальнейшем ВМ могла поместить флаги с свой регистр (образ EFL процессора) для возможности дальнейшего анализа.
    Например (из листинга ВМ):
    011BE6EC AC pop eax ;00000072 ;0x72
    pop ecx ;00000072 ;0x72
    test eax, ecx
    push efl ;00200206
    в примитиве AC меняются флаги и записываются в стек, значит на команде test будет создан блок CNGFLAG.

    Вопрос: Можнл ли в OllyDbg создать секцию кода и привязать её к выполняемой программе, чтобы эта секция была доступна из кода программы? Сейчас "промежуточный" рабочий код я создаю в секции импорта, но это неправильно, т.к. после воосстановления кода функции его нужно сохранить в файл, при этом сохраняются все изменения, но так как секция импорта "сломана", то такой файл уже не запускается.
     
  19. TSS

    TSS New Member

    Публикаций:
    0
    Регистрация:
    13 апр 2009
    Сообщения:
    494
    Vam
    В плагине можно выделить память для промежуточного кода, затем после обработки на место завиртуализированного кода скопировать восстановленный код из выделенной памяти. Создать секцию в ольке не получиться.
     
  20. Vam

    Vam New Member

    Публикаций:
    0
    Регистрация:
    16 июл 2008
    Сообщения:
    149
    Я так и делаю, только промежуточный код большой и в функцию он не лезет, нужно отдельное место для проверки его работоспособности. Ладно, выход найден, т.к. записать изменения функции в файл из плагина не получается (функции патчевания в SDK отсутствуют и горячих клавиш на них нет), то запись изменений в файл будет производиться вручную с выделением только тела восстановленной функции, следовательно, импорт в этом случае перезаписан не будет.