
Генерирует полиморфные позиционно-независимые виртуальные машины (PIVMs) из произвольного x86/x64 shellcode.
Читать исследовательскую статью (написана для версии 1.0.0).
mkPIVM — это полиморфный виртуализатор оболочечного кода (shellcode) с позиционно-независимым кодом для Windows x86 и x64 (скоро Linux).
Передайте ему сырой шеллкод. Он выдаёт другой сырой блоб: небольшую виртуальную машину, которая интерпретирует поднятую (lifted), зашифрованную в состоянии покоя версию ваших исходных инструкций. Вывод сам по себе является позиционно-независимым кодом и выполняется везде, где выполнялся бы исходный шеллкод: от загрузчика удалённого потока до обходного кода в кодовой пещере (code cave detour). Каждая ручка настройки, зависящая от начального значения (seed), изменяется независимо: семейство шифров, компоновка слотов регистров, перестановка опкодов в обработчики, топология диспетчера, шаблон мусорных гаджетов, точки вставки обфускации промежуточного представления (IR). Две сборки из одного и того же входного кода имеют менее ста случайных совпадающих байт из десятков килобайт.
Почему: нативный шеллкод тривиален для сигнатурного анализа. Обёртывание его в экземплярную виртуальную машину с уникальным шифром не оставляет ничего полезного в состоянии покоя, а поднятие инструкций в байт-код ставит ещё одну стену между байтами на диске и любым дизассемблером, который знает, как выглядит x86. Насколько я могу судить по обзору литературы, ни один публичный инструмент не предоставляет именно этот конвейер: сырой PIC на входе, сырой полиморфный VM PIC на выходе. Поэтому я упомянул это в потребовавшейся исследовательской статье. Честно говоря, если я прав, что никто не делал этого (публично) раньше, а я довольно уверен, я удивлён. Тем не менее, наслаждайтесь.
mkpivm.exe shellcode.bin --arch x64 -o out.bin
Ваша PIVM готова и ждет. Это самый простой путь. Несколько других режимов различаются тем, насколько агрессивно виртуализируются исходные инструкции, является ли вывод автономным blob-объектом или пропатченным PE, и выполняется ли лифт вообще.
# Демонстрация
У меня есть доказательства. Ниже вы можете увидеть видео mkPIVM в действии, полностью виртуализирующее стейджер Meterpreter (кстати, ванильный), внедряющееся в explorer.exe, и мы получаем обратный вызов. Конечно, это всего лишь пример, и mkPIVM можно применять к гораздо большему, при условии, что инструкции в shellcode поддерживаются. Если нет, создайте Issue, пришлите мне shellcode, я вас выручу.
Смотрите [здесь](https://github.com/D7EAD/mkPIVM/raw/refs/heads/main/media/mkpivm-showcase.mp4). Хранится в ./media, к сожалению, не встраивается.
Вот отчет VirusTotal для этого конкретного виртуализированного сэмпла (по состоянию на 06.04.2026).
<img src="https://assets.kitploit.com/production/public/readmes/7466/3f4a6dbe2777c02fb32f71749a5b1fa2aa324cb0dac2f77d5681bbe3a4e17a81.png">
...и упакованная версия, даже не виртуализированная, заметно более высокая энтропия.
<img src="https://assets.kitploit.com/production/public/readmes/7466/47f68372e31a2c24fa49eb71648adbdf8944977db90d645df337125c1bdfa8e3.png">
Вот результаты обычного beacon Cobalt Strike для сравнения.
<img src="https://assets.kitploit.com/production/public/readmes/7466/76e2107589cd2b1b65c846a671aed173aa5e6d89630f5e9d283d99c546a2d566.png">
Было уделено тщательное внимание телеметрии энтропии вывода этого инструмента, что приводит к shellcode с энтропией ниже, чем у типичных DLL Windows WinAPI (вне режима упаковки), таких как ntdll.dll или kernel32.dll. Сравнение энтропии выглядит примерно так...
| Файл | Байты | Энтропия |
|------|-------|---------|
| `p_m64.bin` | 3,969 | **7.1181** |
| `msvcrt.dll` | 699,888 | 6.5319 |
| `wininet.dll` | 2,724,528 | 6.4934 |
| `shell32.dll` | 7,839,992 | 6.3639 |
| `kernel32.dll` | 836,232 | 6.3597 |
| `crypt32.dll` | 1,538,632 | 6.3010 |
| `rpcrt4.dll` | 1,162,672 | 6.2405 |
| `ntdll.dll` | 2,522,104 | 6.1934 |
| `v.bin` | 29,229 | **6.0442** |
## Режимы с первого взгляда
| Режим | Флаги | Что меняется |
|------|-------|--------------|
| Default | none | Лифт всего ввода. Все виртуализировано. |
| Packer | `--pack` | Не лифтить. Оборачивать ввод как зашифрованные данные, дешифровать во время выполнения, перейти. |
| Hybrid | `--ranges A:B,...` | Лифтить только выбранные диапазоны байт. Остальное остается нативным. |
| Stacked | `--pack --ranges A:B` | Создать гибридный blob, затем упаковать его. |
| Detour | `--embed-into PE --at RVA` | Взять предварительно созданный blob, встроить в PE, пропатчить jmp по заданному RVA. |
| Scan | `--scan` | Вывести подходящие кандидаты `--ranges` из CFG ввода, затем выйти. |
| RX | `--rx` | Blob PAGE_EXECUTE_READ. Остров данных остается зашифрованным в состоянии покоя; PEB-ходок в blob разрешает VirtualProtect, дешифрует на месте при state_init. |
| RX w/ Loader | `--rx --rx-loader-vp` | Как `--rx`, но ваш загрузчик передает VirtualProtect первому аргументу blob. Без PEB-ходока. |
Каждый режим учитывает `--seed`, `--arch`, `--input-format` и `--format`. Смотрите разделы каждого режима ниже для конвейера сборки и потока выполнения.
## Виртуализация по умолчанию
Лифтер проходит по всему CFG и понижает каждую инструкцию до пользовательского IR. IR проходит через два этапа обфускации, затем через кодеки, которые кодируют каждую инструкцию в форму байткода, зависящую от сида. Таблица блоков, таблица обработчиков и остров данных шифруются тем же поточным шифром, что и байткод. Во время выполнения пролог дешифрует эти три области на месте, а цикл диспетчера извлекает байты байткода по одному, дешифрует их и направляет к обработчику, который выполняет работу.
### Конвейер сборки
Шаги, которые выполняет сборка для преобразования сырого shellcode в выводимый виртуализированный blob, от начала до конца. _Все графики ниже относятся к версии 1.0.0, с тех пор они изменились, но идея та же._```mermaid
flowchart TB
A[shellcode.bin] --> CFG[CFGBuilder: identify blocks via Zydis disasm + recursive descent]
SEED[seed u64] --> VMC[VMConfig: pick cipher kind, reg perm, opcode map, dispatcher topology]
CFG --> LIFT[LifterRegistry: lower each block to IR]
LIFT --> RBT[resolve_branch_targets: link BR_CC/BR/CALL_VM/LOOP_DEC to target_block_id; synthesize JMP_NATIVE block for any out-of-range jcc]
RBT --> OBF1[obfuscate_ir_dead_inject: 20% per insn-gap, IMM Tmp2/Tmp3 random]
OBF1 --> OBF2[obfuscate_ir_opaque_predicates: 25% per block, split block with IMM Tmp3=0 + TEST + BR_CC NZ random_block, never taken]
OBF2 --> ENC[BytecodeBuilder: each codec emits its variant for this seed]
ENC --> CDATA[compact data island: bytes not covered by any CFG block]
CDATA --> PROMO[promote LEA-fixup target VAs back into data island if CFG put them in code]
ENC --> BTAB[build block table: va_off to bytecode_off pairs]
PROMO --> ECIPH[encrypt data island with cipher_init]
BTAB --> ECIPH2[encrypt block table with cipher_init]
ENC --> ECIPH3[encrypt bytecode per-block with cipher_init reset at each block start]
VMC --> STUB[VMCodeGen::emit_full: prologue, state init, dispatcher tail, handlers, sbox_inv, exit handler]
STUB --> HTAB[handler table: 256 entries, each a 32-bit offset from handler_base; encrypted at rest]
ECIPH --> ASM[finalize: stub + trampolines + sbox_inv + bytecode + data island + block table]
ECIPH2 --> ASM
ECIPH3 --> ASM
HTAB --> ASM
ASM --> OUT[out.bin]