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

Статья VMProtect 2 - Часть вторая, полный статический анализ

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Содержание
  • Цель
  • Намерения
  • Определения
  • VMProtect 2 - Обзор проекта
    • VMHook - Обзор
      • VMHook - Пример, um-hook
    • VMProfiler - Обзор
      • VMProfiler - Профилирование обработчика виртуальных машин
      • VMProfiler - алгоритм обнаружения виртуальных ветвей
    • VMProfiler Qt - Обзор
    • VMProfiler CLI - Обзор
    • VMEmu - Обзор
      • VMEmu - Unicorn Engine, статическая расшифровка кодов операций
      • VMEmu - виртуальное ветвление
    • VMAssembler - Обзор
      • VMAssembler - Этапы
      • VMAssembler - Этап первый, лексический анализ и парсинг
      • VMAssembler - второй этап, кодирование виртуальных инструкций
      • VMAssembler - третий этап, шифрование виртуальных инструкций
      • VMAssembler - четвертый этап, создание заголовка C++
      • VMAssembler - Пример
  • VTIL - Начало
    • VTIL - Базовый блок
    • VTIL - VMProfiler лифтинг
  • Заключение
    • Заключение - дальнейшая работа
Цель
Цель этой статьи - подробно рассказать о предыдущей работе, описанной в последней статье под названием «VMProtect 2 - Подробный анализ архитектуры виртуальной машины», а также исправить несколько ошибок. Кроме того, в этом посте основное внимание будет уделено созданию инструментов статического анализа с использованием знаний, раскрытых в предыдущем посте, и предоставлению подробной, но неофициальной документации VTIL. В этой статье также будут представлены все проекты на githacks.org/vmp2, однако эти проекты могут быть изменены.
*Перевод прошлой части уже есть на форуме

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

Определения

Блок кода(Code block): блок виртуальных инструкций или блок кода - это последовательность виртуальных инструкций, которые содержатся между инструкциями виртуального ветвления. Примером этого могут быть любые инструкции, следующие за инструкцией JMP и следующей инструкцией JMP или VMEXIT. Блок кода представлен в C++ как структура (vm::instrs::code_block_t), содержащая вектор виртуальных инструкций вместе с начальным адресом блока кода, содержащимся в самой структуре. Другие метаданные о данном блоке кода также содержатся внутри этой структуры, например, если блок кода разветвляется на два других блока кода, разветвляется только на один блок кода или выходит из виртуальной машины.

VMProtect 2 IL: средний уровень представления или языка. Считайте закодированные и зашифрованные виртуальные инструкции удобной, нативной формой виртуальных инструкций. Тогда IL будет представлением более высокого уровня, обычно представление IL относится к представлению кода, используемому компиляторами и ассемблерами. Примером VMProtect 2 IL является то, что VMAssembler выполняет лексический анализ, или файл, содержащий IL, чтобы быть более конкретным.

VMProtect 2 - Обзор проекта

1.png


Хотя может показаться, что на githacks.org/vmp2 расположено довольно много проектов, на самом деле существует только один большой библиотечный проект и более мелкие проекты, которые наследуют эту библиотеку. VMProfiler - это базовая библиотека для VMProfiler Qt, VMProfiler CLI, VMEmu и VMAssembler. Каждый из этих проектов основан на статическом анализе, поэтому VMHook и um-hook не наследуют VMProfiler.

VMHook - Обзор

2.png


VMHook - это очень небольшой C++ фреймворк для подключения к виртуальным машинам VMProtect 2, um-hook наследует эту структуру и демонстрирует, как ее использовать. VMHook не используется для раскрытия виртуальных инструкций и их функций, а для их изменения.

VMHook - Пример, um-hook
Код:
.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

um-hook - это проект, наследующий VMHook, он демонстрирует перехват виртуальной инструкции LCONSTBZX и подмену ее непосредственного значения. Это впоследствии влияет на результат более поздних функций виртуального сдвига, что в конечном итоге приводит к тому, что виртуальная процедура возвращает true вместо false.

VMProfiler - Обзор
VMProfiler - это библиотека C++, которая используется для статического анализа двоичных файлов VMProtect 2. Это базовый проект для VMProfiler Qt, VMProfiler CLI, VMEmu и VMAssembler. VMProfiler также наследует VTIL и содержит профили обработчиков виртуальных машин и лифтеры.

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

Код:
0:  36 48 8b 00             mov    rax,QWORD PTR ss:[rax]
4:  48 8b 00                mov    rax,QWORD PTR [rax]
0:  36 48 8b 04 05 00 00    mov    rax,QWORD PTR ss:[rax*1+0x0]
7:  00 00

Чтобы справиться с такими случаями, была разработана и реализована новая итерация алгоритма профилирования. Это новое представление по-прежнему совпадает с шаблоном, однако для каждой инструкции обработчика виртуальной машины определена лямбда. Эта лямбда принимает параметр ZydisDecodedInstruction по ссылке и возвращает логическое значение. Результат будет истинным, если данная декодированная инструкция соответствует всем случаям сравнения. Использование zydis для этой цели позволяет сравнивать операнды на более тонком уровне. Например, второй операнд обеих инструкций на рисунке выше имеет тип ZYDIS_OPERAND_TYPE_MEMORY. Кроме того, основой этого операнда памяти для обеих инструкций является RAX. Мнемоника обеих инструкций одинакова. На этом минималистском сравнительном мышлении и основана эта интерпретация алгоритма профилирования.

C++:
vm::handler::profile_t readq = {
    // MOV RAX, [RAX]
    // MOV [RBP], RAX
    "READQ",
    READQ,
    NULL,
    { { // MOV RAX, [RAX]
        []( const zydis_decoded_instr_t &instr ) -> bool {
            return instr.mnemonic == ZYDIS_MNEMONIC_MOV &&
                   instr.operands[ 0 ].type == ZYDIS_OPERAND_TYPE_REGISTER &&
                   instr.operands[ 0 ].reg.value == ZYDIS_REGISTER_RAX &&
                   instr.operands[ 1 ].type == ZYDIS_OPERAND_TYPE_MEMORY &&
                   instr.operands[ 1 ].mem.base == ZYDIS_REGISTER_RAX;
        },
        // MOV [RBP], RAX
        []( const zydis_decoded_instr_t &instr ) -> bool {
            return instr.mnemonic == ZYDIS_MNEMONIC_MOV &&
                   instr.operands[ 0 ].type == ZYDIS_OPERAND_TYPE_MEMORY &&
                   instr.operands[ 0 ].mem.base == ZYDIS_REGISTER_RBP &&
                   instr.operands[ 1 ].type == ZYDIS_OPERAND_TYPE_REGISTER &&
                   instr.operands[ 1 ].reg.value == ZYDIS_REGISTER_RAX;
        } } } };

На рисунке выше показан профиль READQ. Обратите внимание, что не каждая инструкция для обработчика виртуальной машины должна иметь для нее лямбду zydis. Достаточно только для создания уникального профиля. Фактически существуют дополнительные встроенные инструкции для READQ, которые не учитываются лямбда-выражениями zydis для сравнения.

VMProfiler - алгоритм обнаружения виртуальных ветвей
Самая очевидная последовательность в виртуальном филиале - это использование PUSHVSP. Эта виртуальная инструкция выполняется, когда два зашифрованных значения находятся в стеке в VSP + 0 и VSP + 8. Эти зашифрованные значения дешифруются с использованием последнего значения LCONSTDW данного блока. Таким образом, на основе этих двух согласований может быть создан тривиально маленький алгоритм. Первая часть алгоритма просто использует std::find_if с обратными итераторами, чтобы найти последний LCONSTDW в заданном блоке кода. Это значение DWORD будет интерпретироваться как ключ XOR, используемый для дешифрования зашифрованных относительных виртуальных адресов обеих ветвей. Теперь выполняется второй std::find_if, чтобы определить местонахождение виртуальной инструкции PUSHVSP, при выполнении которой в стеке будут располагаться два зашифрованных относительных виртуальных адреса. Алгоритм интерпретирует два верхних значения стека каждой инструкции PUSHVSP как зашифрованные относительные виртуальные адреса и применяет операцию XOR с последним значением LCONSTDW.

C++:
std::optional< jcc_data > get_jcc_data( vm::ctx_t &vmctx, code_block_t &code_block )
{
    // there is no branch for this as this is a vmexit...
    if ( code_block.vinstrs.back().mnemonic_t == vm::handler::VMEXIT )
        return {};

    // find the last LCONSTDW... the imm value is the JMP xor decrypt key...
    // we loop backwards here (using rbegin and rend)...
    auto result = std::find_if( code_block.vinstrs.rbegin(), code_block.vinstrs.rend(),
                                []( const vm::instrs::virt_instr_t &vinstr ) -> bool {
                                    auto profile = vm::handler::get_profile( vinstr.mnemonic_t );
                                    return profile && profile->mnemonic == vm::handler::LCONSTDW;
                                } );

    jcc_data jcc;
    const auto xor_key = static_cast< std::uint32_t >( result->operand.imm.u );
    const auto &last_trace = code_block.vinstrs.back().trace_data;

    // since result is already a variable and is a reverse itr
    // i'm going to be using rbegin and rend here again...
    //
    // look for PUSHVSP virtual instructions with two encrypted virtual
    // instruction rva's ontop of the virtual stack...
    result = std::find_if(
        code_block.vinstrs.rbegin(), code_block.vinstrs.rend(),
        [ & ]( const vm::instrs::virt_instr_t &vinstr ) -> bool {
            if ( auto profile = vm::handler::get_profile( vinstr.mnemonic_t );
                 profile && profile->mnemonic == vm::handler::PUSHVSP )
            {
                const auto possible_block_1 = code_block_addr( vmctx,
                        vinstr.trace_data.vsp.qword[ 0 ] ^ xor_key ),
                           possible_block_2 = code_block_addr( vmctx,
                        vinstr.trace_data.vsp.qword[ 1 ] ^ xor_key );

                // if this returns too many false positives we might have to get
                // our hands dirty and look into trying to emulate each branch
                // to see if the first instruction is an SREGQ...
                return possible_block_1 > vmctx.module_base &&
                       possible_block_1 < vmctx.module_base + vmctx.image_size &&
                       possible_block_2 > vmctx.module_base &&
                       possible_block_2 < vmctx.module_base + vmctx.image_size;
            }
            return false;
        } );

    // if there are not two branches...
    if ( result == code_block.vinstrs.rend() )
    {
        jcc.block_addr[ 0 ] = code_block_addr( vmctx, last_trace );
        jcc.has_jcc = false;
        jcc.type = jcc_type::absolute;
    }
    // else there are two branches...
    else
    {
        jcc.block_addr[ 0 ] = code_block_addr( vmctx,
            result->trace_data.vsp.qword[ 0 ] ^ xor_key );
        jcc.block_addr[ 1 ] = code_block_addr( vmctx,
            result->trace_data.vsp.qword[ 1 ] ^ xor_key );

        jcc.has_jcc = true;
        jcc.type = jcc_type::branching;
    }

    return jcc;
}

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

VMProfiler Qt - Обзор

3.png


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

VMProfiler CLI - Обзор
VMProfiler CLI - это проект командной строки, который используется для демонстрации всех функций VMProfiler. Этот проект состоит только из одного файла (main.cpp), однако это хороший справочник для тех, кто хочет унаследовать VMProfiler в качестве базы кода.

Код:
Usage: vmprofiler-cli [options...]
Options:
    --bin, --vmpbin        unpacked binary protected with VMProtect 2
    --vmentry, --entry     rva to push prior to a vm_entry
    --showhandlers         show all vm handlers...
    --showhandler          show a specific vm handler given its index...
    --vmp2file             path to .vmp2 file...
    --showblockinstrs      show the virtual instructions of a specific code block...
    --showallblocks        shows all information for all code blocks...
    --devirt               lift to VTIL IR and apply optimizations, then display the output...
    -h, --help             Shows this page

VMEmu - Обзор

VMEmu - это проект, основанный на unicorn-engine, который эмулирует обработчики виртуальных машин для последующего дешифрования операндов виртуальных команд. VMEmu наследует VMProfiler, который помогает определить, есть ли в данном блоке кода виртуальный JCC. VMEmu в настоящее время не поддерживает выгружаемые модули, так как «выгруженные модули» могут принимать разные формы. Не существует единого стандартного формата файла для выгружаемого модуля, поэтому поддержка выгружаемых модулей будет идти вместе с другим проектом, основанным на unicorn-engine, для создания стандартного формата дампа.

Код:
Usage: vmemu [options...]
Options:
    --vmentry              relative virtual address to a vm entry... (Required)
    --vmpbin               path to unpacked virtualized binary... (Required)
    --out                  output file name for trace file... (Required)
    -h, --help             Shows this page

VMEmu - Unicorn Engine, статическая расшифровка кодов операций
Чтобы статически расшифровать операнды виртуальных инструкций, нужно сначала понять, как эти операнды зашифрованы в первую очередь. Алгоритм, который VMProtect 2 использует для шифрования операндов виртуальных команд, можно представить в виде математической формулы.

4.png


Учитывая приведенный выше рисунок, расшифровка операндов - это просто обратная функция функции FFF. Эта инверсия генерируется в собственных инструкциях x86_64 и встраивается в каждый обработчик виртуальной машины, а также в calc_jmp. Можно просто эмулировать эти инструкции путем их повторной реализации на C / C ++, однако моя реализация таких инструкций предназначена только для шифрования, а не для дешифрования. Вместо этого в этой ситуации предпочтительнее использовать unicorn-engine, поскольку путем простой эмуляции этих обработчиков виртуальных машин будут созданы расшифрованные операнды.

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

Повторюсь, unicorn-engine используется для вычисления F (e, o) и G (e, o), где e принимает форму собственного регистра RBX, o принимает форму собственного регистра RAX, а TmFn принимает форму преобразования mth.

Кроме того, с помощью unicorn-engine можно не только получить дешифрованные операнды, но и сделать снимки представлений виртуального стека для каждой отдельной виртуальной инструкции. Это позволяет алгоритмам использовать значения, находящиеся в стеке. Вызовы собственных WinAPI выполняются вне виртуальной машины, за исключением редких случаев, таких как обработчик виртуальной машины упаковщика VMProtect 2, который вызывает LoadLibrary с указателем на строку «NTDLL.DLL» в RCX.

VMEmu - виртуальное ветвление
Чрезвычайно важно видеть все пути кода. Рассмотрим самую простую ситуацию, когда параметр проверяется на предмет наличия nullptr.
Код:
auto demo(int* a)
{
    if (!a)
        return {};

    // more code down here
}

Анализ приведенного выше кода без возможности увидеть все пути кода приведет к чему-то бесполезному. Таким образом, видеть все ветки внутри виртуальной машины было главным приоритетом. В этом разделе я подробно расскажу, как работает виртуальное ветвление внутри виртуальной машины VMProtect 2, а также об алгоритмах, которые я разработал для распознавания и анализа всех путей.

Начнем с того, что не все блоки кода заканчиваются виртуальной инструкцией ветвления. Некоторые заканчиваются выходом из виртуальной машины или абсолютным скачком. Таким образом, потребность в алгоритме, который может определить, будет ли данный блок виртуальных команд ветвиться или нет. Для создания такого алгоритма необходимо доскональное знание механизма ветвления виртуальной машины, в частности того, как нативные JCC транслируются в виртуальные инструкции.

Рассмотрим возможные затронутые биты флага нативной инструкции ADD. Все флаги OF, SF, ZF, AF, CF и PF могут быть затронуты в зависимости от вычислений. Собственное ветвление выполняется с помощью инструкций JCC, которые зависят от состояния конкретного флага или флагов.

Код:
test rax, rax
jz branch_1

Рассмотрим пример выше, поймите, что нативная инструкция JZ перейдет к «branch_1», если установлен флаг ZF. Можно было бы переопределить пример выше таким образом, чтобы можно было использовать только нативную инструкцию JMP и несколько других математических операций и операций со стеком. Уменьшение количества инструкций ветвления до одной нативная инструкции JMP.

Учтите, что нативная команда TEST выполняет побитовое И для обоих операндов, соответственно устанавливает флаги и игнорирует результат И. Можно просто заменить нативную инструкцию TEST несколькими операциями со стеком и инструкцией AND.

Код:
0:  50                      push   rax
1:  48 21 c0                and    rax,rax
4:  9c                      pushf
5:  48 83 24 24 40          and    QWORD PTR [rsp],0x40
a:  48 c1 2c 24 03          shr    QWORD PTR [rsp],0x3
f:  58                      pop    rax
10: ff 34 25 00 00 00 00    push   branch_1
17: ff 34 25 00 00 00 00    push   branch_2
1e: 48 8b 04 04             mov    rax,QWORD PTR [rsp+rax*1]
22: 48 83 c4 10             add    rsp,0x10
26: 48 89 44 24 f8          mov    QWORD PTR [rsp-0x8],rax
2b: 58                      pop    rax
2c: ff 64 24 f0             jmp    QWORD PTR [rsp-0x10]

Хотя может показаться, что преобразование одной инструкции в несколько может быть контрпродуктивным и в конечном итоге потребовать дополнительной работы, это не так, поскольку эти инструкции будут повторно использоваться в других ориентациях. Повторная реализация всех инструкций JCC может быть выполнена довольно просто с использованием вышеуказанного шаблона кода сборки. Даже такие инструкции ветвления, как JRCXZ, JECXZ и JCXZ, могут быть реализованы путем простой замены RAX на RCX/EAX/CX в приведенном выше примере.

На примере выше, хотя и в нативном x86_64, представлен убедительный пример того, как VMProtect 2 выполняет ветвление внутри виртуальной машины. Однако VMProtect 2 добавляет дополнительную обфускацию с помощью математической обфускации. Во-первых, оба адреса, помещенные в стек, являются зашифрованными относительными виртуальными адресами. Эти адреса дешифруются с помощью XOR. Хотя сами операции XOR, SUB и другие математические операции запутываются в операции NAND.

Код:
; push encrypted relative virtual addresses onto the stack...
LCONSTQ 0x19edc194
LCONSTQ 0x19ed8382
PUSHVSP

; calculate which branch will be executed, then read its encrypted address on the stack...
LCONSTBZXW 0x3
LCONSTBSXQ 0xbf
LREGQ 0x80
NANDQ
SREGQ 0x68
SHRQ
SREGQ 0x70
ADDQ
SREGQ 0x48
READQ

; clear the stack of encrypted addresses...
SREGQ 0x68
SREGQ 0x70
SREGQ 0x90

; put the selected branch encrypted address back onto the stack...
LREGQ 0x68
LREGQ 0x68

; xor value on top of the stack with 59f6cb36
LCONSTDW 0xa60934c9
NANDDW
SREGQ 0x48
LCONSTDW 0x59f6cb36
LREGDW 0x68
NANDDW
SREGQ 0x48
NANDDW
SREGQ 0x90
SREGQ 0x70

; removed virtual instructions...
; …

; load the decrypted relative virtual address and jmp...
LREGQ 0x70
JMP

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

C++:
#define FIRST_CONSTANT a60934c9
#define SECOND_CONSTANT 59f6cb36

unsigned int jcc_decrypt(unsigned int encrypted_rva)
{
    unsigned int result = ~encrypted_rva & ~encrypted_rva;
    result = ~result & ~FIRST_CONSTANT;
    result = ~(~encrypted_rva & ~SECOND_CONSTANT) & ~result;
    return result;
}

Примечание: обратите внимание, что FIRST_CONSTANT и SECOND_CONSTANT инвертируют друг друга.

VMAssembler - Обзор
VMAssembler - это проект ассемблера виртуальных инструкций, изначально задуманный как шутка. Независимо от его значимости для чего-либо, это увлекательный проект, который позволяет человеку лучше познакомиться с функциями VMProtect 2. VMAssembler использует LEX и YACC для анализа текстовых файлов на предмет меток и виртуальных токенов инструкций. Затем он кодирует и шифрует эти виртуальные инструкции на основе конкретной виртуальной машины, указанной в командной строке. Наконец, создается файл заголовка C++, который содержит собранные виртуальные инструкции, а также исходный двоичный файл VMProtect.

VMAssembler - Этапы

5.png


VMAssembler использует LEX и YACC для анализа текстовых файлов на предмет имен виртуальных инструкций и немедленных значений. В VMAssembler есть четыре основных этапа: лексический анализ и синтаксический анализ, кодирование виртуальных инструкций, шифрование виртуальных инструкций и, наконец, генерация кода C++.

VMAssembler - Этап первый, лексический анализ и парсинг

6.png


Лексический анализ и синтаксический анализ токенов сами по себе являются двумя этапами, однако я буду называть эти этапы одним, так как результатом этого являются структуры данных, которыми управляет C ++.

Первый этап VMAssembler почти полностью обрабатывается LEX и YACC. Текст преобразуется в структуры C++, представляющие виртуальные инструкции. Эти структуры называются _vinstr_meta и _vlable_meta. Эти структуры затем используются на втором этапе для проверки существования виртуальных инструкций, а также для кодирования этих представлений виртуальных инструкций более высокого уровня в дешифрованные виртуальные операнды.

VMAssembler - второй этап, кодирование виртуальных инструкций

7.png


Этап кодирования виртуальных инструкций при сборке также проверяет существование всех виртуальных инструкций для каждой виртуальной метки. Это делается путем сравнения профилированных имен обработчиков vm с токеном имени виртуальной инструкции. Если виртуальная инструкция не существует, сборка прекратится.

C++:
if ( !parse_t::get_instance()->for_each( [ & ]( _vlabel_meta *label_data ) -> bool {
         std::printf( "> checking label %s for invalid instructions... number of instructions = %d\n",
                      label_data->label_name.c_str(), label_data->vinstrs.size() );

         const auto result = std::find_if(
             label_data->vinstrs.begin(), label_data->vinstrs.end(),
             [ & ]( const _vinstr_meta &vinstr ) -> bool {
                 std::printf( "> vinstr name = %s, has imm = %d, imm = 0x%p\n", vinstr.name.c_str(),
                              vinstr.has_imm, vinstr.imm );

                 for ( auto &vm_handler : vmctx->vm_handlers )
                     if ( vm_handler.profile && vm_handler.profile->name == vinstr.name )
                         return false;

                 std::printf( "[!] this vm protected file does not have the vm handler for: %s...\n",
                              vinstr.name.c_str() );

                 return true;
             } );

         return result == label_data->vinstrs.end();
     } ) )
{
    std::printf( "[!] binary does not have the required vm handlers...\n" );
    exit( -1 );
}

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

VMAssembler - третий этап, шифрование виртуальных инструкций

8.png


Как и на втором этапе сборки, на третьем этапе необходимо также учитывать, в каком направлении движется указатель виртуальной инструкции. Это связано с тем, что операнды должны быть зашифрованы в порядке, основанном на направлении продвижения VIP. Ключ шифрования, полученный в результате шифрования последних операндов, используется в качестве начального ключа шифрования для следующего, как подробно описано в “VMEmu - Unicorn Engine, Static Decryption Of Opcodes”.

На этом этапе выполняется F ^ {- 1} (e, o) и G ^ {- 1} (e, o) для каждого операнда виртуальной инструкции каждой метки. Наконец, относительный виртуальный адрес от vm_entry до первого операнда первой виртуальной инструкции вычисляется и затем шифруется с использованием обратных преобразований, используемых для дешифрования относительного виртуального адреса в самих виртуальных инструкциях. Вы можете найти более подробную информацию об этих преобразованиях в разделе vm_entry последней статьи.

VMAssembler - четвертый этап, создание заголовка C++

9.png


Четвертый этап - заключительный этап сборки виртуальной инструкции. На этом этапе генерируется код C ++. Код полностью автономен и не зависит от среды. Однако в текущей реализации есть несколько ограничений. Наиболее очевидной является необходимость в разделе RWX (чтение, запись и исполняемый файл). Если бы кто-то использовал этот сгенерированный код C ++ в драйвере ядра Windows, тогда драйвер не поддерживал бы системы HVCI. Кроме того, с 19.06.2021 MSVC не может скомпилировать сгенерированный заголовок, поскольку по какой-либо причине статический инициализатор для необработанного модуля приводит к зависанию компилятора. Вы должны использовать clang-cl, если хотите скомпилировать сгенерированный файл заголовка из VMAssembler.

VMAssembler - Пример
После того, как заголовок C++ был сгенерирован с помощью VMAssembler, вы теперь можете включить его в свой проект и скомпилировать с использованием любого компилятора, отличного от MSVC, поскольку компилятор MSVC по какой-то причине не может обрабатывать такой большой статический инициализатор, в котором содержится защищенный двоичный файл, clang- cl, однако, справляется с этим. Каждая определенная вами метка будет вставлена в перечисление vm::calls. Значение для каждой записи перечисления - это зашифрованный относительный виртуальный адрес виртуальных инструкций метки.

C++:
namespace vm
{
    enum class calls : u32
    {
        get_hello = 0xbffd6fa5,
        get_world = 0xbffd6f49,
    };
    
    //
    // ...
    //
    
    template < calls e_call, class T, class... Ts > auto call( const Ts... args ) -> T
    {
        static auto __init_result = gen_data.init();

        __vmcall_t vmcall = nullptr;
        for ( auto idx = 0u; idx < sizeof( call_map ) / sizeof( _pair_t< u8, calls > ); ++idx )
            if ( call_map[ idx ].second == e_call )
                vmcall = reinterpret_cast< __vmcall_t >( &gen_data.__vmcall_shell_code[ idx ] );

        return reinterpret_cast< T >( vmcall( args... ) );
    }
}

Теперь вы можете вызвать любую метку из вашего кода C ++, просто указав запись vm::calls enum и тип возвращаемого значения меток в качестве шаблонных параметров.

C++:
#include <iostream>
#include "test.hpp"

int main()
{
    const auto hello = vm::call< vm::calls::get_hello, vm::u64 >();
    const auto world = vm::call< vm::calls::get_world, vm::u64 >();
    std::printf( "> %s %s\n", ( char * )&hello, (char*)&world );
}

Вывод
Код:
> hello world

VTIL - Начало
Проект VTIL в том виде, в каком он сейчас находится на github, имеет некоторые невыразимые требования и зависимости, которые не являются субмодульными. Я создал форк VTIL, краеугольный камень и вершину подмодуля, а также описывает конфигурации Visual Studios, которые необходимо применить к проекту, наследующему VTIL. VTIL использует функции C ++ 2020, такие как ключевое слово concept, поэтому необходимо использовать последнюю версию Visual Studios (2019), vs2017 не поддерживается. Если вы компилируете в среде, отличной от windows / non-visual studio, вы можете проигнорировать последнее предложение.
Код:
git clone --recursive https://githacks.org/_xeroxz/vtil.git

Примечание: возможно, это станет веткой в VTIL-Core, в таком случае вам следует обратиться к официальному репозиторию VTIL-Core, если / когда это произойдет.

Еще одно требование для компиляции VTIL заключается в том, что вы должны определить макрос NOMINMAX до любого включения Windows.h, поскольку std::numeric_limits имеет статические функции-члены (max и min). Эти имена статических функций-членов обрабатываются как макросы min / max и поэтому вызывают ошибки компиляции.
Код:
#define NOMAXMIN
#include <Windows.h>

Последнее требование связано с динамическими инициализаторами, вызывающими переполнение стека. Чтобы ваш скомпилированный исполняемый файл, содержащий VTIL, не аварийно завершил работу, вы должны увеличить начальный размер стека. Я установил размер 4 МБ на всякий случай, так как у меня есть большое количество динамических инициализаторов в VMProfiler.
Код:
Linker->System->Stack Reserve Size/Stack Commit Size, set both to 4194304

VTIL - Базовый блок
vtil::optimizer::apply_all работает с объектом vtil::basic_block, который можно создать, вызвав vtil::basic_block::begin. Vtil::basic_block содержит список инструкций VTIL, который заканчивается инструкцией ветвления или vexit. Чтобы добавить новый базовый блок, связанный с существующим базовым блоком, вы можете вызвать vtil::basic_block::fork.
C++:
// Creates a new block connected to this block at the given vip, if already explored returns nullptr,
// should still be called if the caller knowns it is explored since this function creates the linkage.
//
basic_block* basic_block::fork( vip_t entry_vip )
{
    // Block cannot be forked before a branching instruction is hit.
    //
    fassert( is_complete() );

    // Caller must provide a valid virtual instruction pointer.
    //
    fassert( entry_vip != invalid_vip );

    // Invoke create block.
    //
    auto [blk, inserted] = owner->create_block( entry_vip, this );
    return inserted ? blk : nullptr;
}

Примечание: vtil::basic_block::fork будет утверждать is_complete, поэтому убедитесь, что ваши базовые блоки заканчиваются инструкцией ветвления перед разветвлением.

После создания базового блока можно приступить к добавлению инструкций VTIL, задокументированных на https://docs.vtil.org/, к объекту базового блока. Для каждой определенной инструкции VTIL создается шаблонная функция с использованием макроса «WRAP_LAZY». Теперь вы можете с легкостью «emplace_back» любой инструкции VTIL в ваших подъемниках обработчика виртуальной машины.

Код:
        // Generate lazy wrappers for every instruction.
        //
#define WRAP_LAZY(x)                                                     \
        template<typename... Tx>                                         \
        basic_block* x( Tx&&... operands )                               \
        {                                                                \
            emplace_back( &ins:: x, std::forward<Tx>( operands )... );   \
            return this;                                                 \
        }
        WRAP_LAZY( mov );    WRAP_LAZY( movsx );    WRAP_LAZY( str );    WRAP_LAZY( ldd );
        WRAP_LAZY( ifs );    WRAP_LAZY( neg );      WRAP_LAZY( add );    WRAP_LAZY( sub );
        WRAP_LAZY( div );    WRAP_LAZY( idiv );     WRAP_LAZY( mul );    WRAP_LAZY( imul );
        WRAP_LAZY( mulhi );  WRAP_LAZY( imulhi );   WRAP_LAZY( rem );    WRAP_LAZY( irem );
        WRAP_LAZY( popcnt ); WRAP_LAZY( bsf );      WRAP_LAZY( bsr );    WRAP_LAZY( bnot );   
        WRAP_LAZY( bshr );   WRAP_LAZY( bshl );     WRAP_LAZY( bxor );   WRAP_LAZY( bor );   
        WRAP_LAZY( band );   WRAP_LAZY( bror );     WRAP_LAZY( brol );   WRAP_LAZY( tg );     
        WRAP_LAZY( tge );    WRAP_LAZY( te );       WRAP_LAZY( tne );    WRAP_LAZY( tle );   
        WRAP_LAZY( tl );     WRAP_LAZY( tug );      WRAP_LAZY( tuge );   WRAP_LAZY( tule );   
        WRAP_LAZY( tul );    WRAP_LAZY( js );       WRAP_LAZY( jmp );    WRAP_LAZY( vexit ); 
        WRAP_LAZY( vemit );  WRAP_LAZY( vxcall );   WRAP_LAZY( nop );    WRAP_LAZY( sfence );
        WRAP_LAZY( lfence ); WRAP_LAZY( vpinr );    WRAP_LAZY( vpinw );  WRAP_LAZY( vpinrm );   
        WRAP_LAZY( vpinwm );
#undef WRAP_LAZY

VTIL - VMProfiler лифтинг
Возьмем, к примеру, подъемник лифтер виртуальных машин LCONSTQ. Лифтер просто добавляет инструкцию VTIL push, которая помещает в стек 64-битное значение. Обратите внимание на использование vtil::operand для создания 64-битного операнда непосредственного значения.
C++:
vm::lifters::lifter_t lconstq = {
    // push imm<N>
    vm::handler::LCONSTQ,
    []( vtil::basic_block *blk, vm::instrs::virt_instr_t *vinstr, vmp2::v3::code_block_t *code_blk ) {
        blk->push( vtil::operand( vinstr->operand.imm.u, 64 ) );
    } };

VMProfiler просто перебирает все виртуальные инструкции для данного блока и применяет лифтеры. Когда все блоки кода исчерпаны, вызывается vtil::optimizer::apply_all. Это кульминация VTIL в настоящее время, поскольку некоторые из этапов оптимизации нацелены на обфускацию на основе обработки стека. Цель подмоделирования VTIL в vmprofiler - для этих оптимизаций, поскольку их программирование потребовало бы месяцев исследований. Оптимизация компилятора - это отдельная область, интересная, но у меня нет времени заниматься ею в данный момент, так что VTIL будет достаточно.

Заключение
Хотя я проделал большую работу над VMProtect 2, главным успехом моих усилий было статическое обнаружение всех виртуальных ветвей и создание читабельного IL. Кроме того, все это делается в хорошо документированной библиотеке C++ с открытым исходным кодом, которая может быть унаследована другими исследователями. Я бы не стал считать свою работу чем-то похожим на «готовый продукт» или что-то, что может быть представлено как таковое, это просто шаг в правильном направлении для девиртуализации. Последнее слово последнего предложения подводит меня к следующему пункту.

Девиртуализации избегали во всей моей документации и статьях, касающихся моей работы с VMProtect 2, поскольку для меня это всегда выходило за рамки проекта. Учитывая, что я одинокий исследователь, существует множество аспектов архитектуры виртуальных машин, которые не могут быть рассмотрены одним человеком за значительный промежуток времени. Например, если инструкция не виртуализирована VMProtect 2, происходит vmexit, и исходная инструкция выполняется вне виртуальной машины. Это означает, что если бы я хотел увидеть ВСЮ подпрограмму, мне потребовалось бы, чтобы я следил за выполнением кода обратно из виртуальной машины, и, таким образом, VMEmu потребовалось бы еще много месяцев разработки для поддержки такой вещи. Способ, которым я запрограммировал эти проекты, позволяет нескольким инженерам работать над базой кода в определенный момент, за исключением того, что, похоже, нет никакого интереса к разработке этих инструментов с открытым исходным кодом, даже с такой подробной документацией, которую каждый хочет «сделать своё собственное решение», что понятно, но в долгосрочной перспективе непродуктивно.

Кроме того, девиртуализация требует обратного преобразования в нативный x86_64. Для этого каждый отдельный обработчик виртуальной машины должен быть профилирован, для каждого отдельного обработчика виртуальной машины должен быть определен подъемник VTIL, и каждая отдельная инструкция VTIL должна быть сопоставлена с собственной инструкцией. По крайней мере, это то, что, кажется, требуется при том уровне знаний, который у меня в настоящее время есть, вполне может быть гораздо более элегантный способ сделать это, о котором я просто не замечаю в настоящее время. Таким образом, мой вывод о девиртуализации: это не работа для одного человека, поэтому целью моего проекта (ов) никогда не была девиртуализация, это всегда был IL-вид виртуальных инструкций с VTIL, обеспечивающим псевдокод деобфускации. Одного только IL достаточно, чтобы преданный человек начал исследование, псевдокод VTIL упрощает задачу для всех нас. VMProfiler Qt в сочетании с IDA Pro в том виде, в каком он существует в настоящее время, может использоваться для анализа двоичных файлов, защищенных с помощью VMProtect 2. Возможно, это не подходящее решение для новичков, но, на мой взгляд, этого будет достаточно.

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

Заключение - дальнейшая работа
Наконец, во время моего исследования VMProtect 2 возникло тонкое побуждение самостоятельно реализовать некоторые функции обфускации и виртуальных машин с открытым исходным кодом, чтобы лучше передать функции VMProtect 2. Однако после долгих размышлений было бы лучше. продуктивно для создания среды обфускации, которая позволила бы относительно легко создавать эти идеи. Фреймворк, который будет обрабатывать анализ кода, а также синтаксический анализ, деконструкцию и реконструкцию формата файлов. Что-то более низкое, чем проход оптимизации LLVM, но достаточно высокий уровень, чтобы программисту, использующему эту структуру, нужно было бы только написать сами алгоритмы обфускации и даже не знать базовый формат файла. Эта структура будет поддерживать только один ISA, которым будет x86. Подробности, выходящие за рамки этого пункта, все еще рассматриваются по адресу: https://githacks.org/llo/

От ТС
Эта статья является переводом, а оригинал доступен тут
Кроме того, перевод первой части уже сделан - https://xss.pro/threads/52487/

Спасибо AlexDev, за то что напомнил о выходе 2 части статьи.

Перевод:
Azrv3l cпециально для xss.pro
 


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