Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
mkPIVM — Генерирует полиморфные позиционно-независимые виртуальные машины (PIVMs) из произвольного x86/x64 shellcode. | Kitploit
Инструменты/GitHubGitHub/d7ead/mkpivm
Генерация полезной нагрузкиЭксплуатацияОбратная инженерияШелл-кодАнализ вредоносных программRed Teaming
GitHubd7ead/mkpivm

mkPIVM

Генерирует полиморфные позиционно-независимые виртуальные машины (PIVMs) из произвольного x86/x64 shellcode.

Репозиторий
409176 дней назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться


Читать исследовательскую статью (написана для версии 1.0.0).

mkPIVM — это полиморфный виртуализатор оболочечного кода (shellcode) с позиционно-независимым кодом для Windows x86 и x64 (скоро Linux).

Передайте ему сырой шеллкод. Он выдаёт другой сырой блоб: небольшую виртуальную машину, которая интерпретирует поднятую (lifted), зашифрованную в состоянии покоя версию ваших исходных инструкций. Вывод сам по себе является позиционно-независимым кодом и выполняется везде, где выполнялся бы исходный шеллкод: от загрузчика удалённого потока до обходного кода в кодовой пещере (code cave detour). Каждая ручка настройки, зависящая от начального значения (seed), изменяется независимо: семейство шифров, компоновка слотов регистров, перестановка опкодов в обработчики, топология диспетчера, шаблон мусорных гаджетов, точки вставки обфускации промежуточного представления (IR). Две сборки из одного и того же входного кода имеют менее ста случайных совпадающих байт из десятков килобайт.

Почему: нативный шеллкод тривиален для сигнатурного анализа. Обёртывание его в экземплярную виртуальную машину с уникальным шифром не оставляет ничего полезного в состоянии покоя, а поднятие инструкций в байт-код ставит ещё одну стену между байтами на диске и любым дизассемблером, который знает, как выглядит x86. Насколько я могу судить по обзору литературы, ни один публичный инструмент не предоставляет именно этот конвейер: сырой PIC на входе, сырой полиморфный VM PIC на выходе. Поэтому я упомянул это в потребовавшейся исследовательской статье. Честно говоря, если я прав, что никто не делал этого (публично) раньше, а я довольно уверен, я удивлён. Тем не менее, наслаждайтесь.

Связанные работы и планы

  • Поддержка Linux будет добавлена в ближайшее время.

Быстрый старт```

mkpivm.exe shellcode.bin --arch x64 -o out.bin

root@kitploit:~
Ваша 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]

Поток выполнения во время выполнения

Что делает сгенерированный blob, когда загрузчик хоста вызывает его, начиная с байта 0, от пролога через выборки диспетчера и различные пути завершения.```mermaid flowchart TB ENTRY[blob entry at byte 0] --> PROL[prologue: push NV regs in seed-shuffled order, pushfq, allocate frame, lea state_ptr] PROL --> SI[emit_state_init body, gated by init_flag byte] SI --> ZERO[zero VM regs via seed-permuted xor source] ZERO --> CINIT[set cipher_state register, store cipher_init at cipher_extra+48] CINIT --> SBOX[copy sbox_inv table from blob to cipher_extra+256] SBOX --> NONCE[derive runtime nonce: rdtsc XOR cipher_init, store at cipher_extra+80] NONCE --> DDI[decrypt data island in place via emit_fetch_byte_dec loop] DDI --> DBT[decrypt block table in place] DBT --> DHT[decrypt handler table in place] DHT --> XHT[XOR each handler table entry with the runtime nonce so memory image is process-variant] XHT --> SET_INIT[set init_flag = 1 to gate subsequent vm_entry invocations] SET_INIT --> DISP[dispatcher tail] DISP --> FB[fetch one byte via emit_fetch_byte_dec, decrypts using cipher_state] FB --> HLU["load handler offset: mov scratch_b_32, [handler_base + op*4]"] HLU --> UXOR["xor scratch_b_32, [state_ptr + cipher_extra + runtime_nonce_off]"] UXOR --> SXD[movsxd to 64 bit, lea handler_addr = handler_base + offset, jmp handler] SXD --> H[handler body: fetch operands via stream cipher, perform op, advance state] H --> NEXT{terminator?} NEXT -- no --> DISP NEXT -- BR/BR_CC --> CRESET[cipher_reset to cipher_init, advance ip to target block] CRESET --> DISP NEXT -- CALL_VM --> CRESET NEXT -- JMP_NATIVE imm --> MARSH["marshal VM regs to host regs, rsp = VM_RSP, jmp [target_slot]"] NEXT -- CALL_NATIVE --> PUSHTR[push trampoline addr to VM_RSP, marshal, jmp target] NEXT -- RET_VM --> POPSS[pop shadow stack, jmp to popped trampoline addr] POPSS --> CRESET NEXT -- exit_handler reached --> EPI[restore NV regs, ret to caller of vm_entry]

root@kitploit:~
Трюк с одноразовым числом времени выполнения — ключевой приём против сканирования памяти: таблица обработчиков находится в памяти, зашифрованная XOR со значением, полученным из `rdtsc XOR cipher_init`, поэтому два процесса, загружающие один и тот же блоб, содержат таблицы обработчиков, отличающиеся побайтово. Диспетчер раскрывает их во время поиска с помощью одного дополнительного XOR.

## Режим упаковщика: `--pack`

Обратный компромисс. Лифтер не выполняется. Исходный шеллкод попадает в зашифрованный остров данных, а IR представляет собой единственную синтетическую `JMP_NATIVE imm=0`. Пролог намеренно пропускает расшифровку острова данных, которую обычный режим выполняет с готовностью. Вместо этого, когда в первый раз срабатывает единственный обработчик JMP_NATIVE, он расшифровывает остров данных на месте, устанавливает маркерный байт, а затем передаёт управление на байт 0 теперь уже открытого шеллкода. Шеллкод выполняется нативно оттуда.

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

### Конвейер сборки

Как `--pack` оборачивает исходный шеллкод в виде зашифрованного острова данных за одноинструкционной синтетической IR-программой.```mermaid
flowchart TB
    A[shellcode.bin] --> SKIP[skip CFG build, skip lift]
    SEED[seed u64] --> VMC[VMConfig polymorphism axes as in default mode]
    SKIP --> SYN[synthesize 1-insn IRProgram: one block with JMP_NATIVE Imm 0 Width Q]
    SYN --> SETF[VMConfig::set_pack_mode true, set_data_island_size shellcode.size]
    SETF --> ENC[BytecodeBuilder encodes the 1 insn into ~35 bytes]
    ENC --> CDATA[data island = entire shellcode bytes verbatim]
    CDATA --> ECIPH[encrypt shellcode bytes with cipher_init as the data island]
    ENC --> ECIPH2[encrypt bytecode + block table + handler table]
    VMC --> STUB[VMCodeGen::emit_full]
    STUB --> FLAG[data_island_init_flag byte starts at 0 because pack_mode true]
    STUB --> ASM[finalize layout]
    ECIPH --> ASM
    ECIPH2 --> ASM
    FLAG --> ASM
    ASM --> OUT[out.bin]

Поток выполнения во время работы

Что делает обёртка упаковщика при первом входе, включая управляемую ленивую расшифровку островка данных и единственный синтетический JMP_NATIVE, который передаёт управление теперь уже незашифрованному шелл-коду.```mermaid flowchart TB ENTRY[blob entry] --> PROL[prologue + state init] PROL --> SKIP_DI[skip data island decrypt because data_island_size gated on pack_mode false at build] SKIP_DI --> DBT[decrypt block table in place] DBT --> DHT[decrypt handler table in place + runtime nonce XOR] DHT --> DISP[dispatcher fetches first opcode = JMP_NATIVE] DISP --> NH[emit_native_handler entry] NH --> GATE{data_island_init_flag == 0?} GATE -- yes --> SAVE[save cipher_state and ip to kPreDecryptCsOff/IpOff slots] SAVE --> RESET[reset cipher_state to cipher_init] RESET --> LOOP[decrypt data island in place byte by byte] LOOP --> RESTORE[restore cipher_state and ip from saved slots] RESTORE --> SETFLAG[set data_island_init_flag = 1] GATE -- no --> CONT[skip the decrypt body] SETFLAG --> CONT CONT --> FETCH_TGT[fetch JMP_NATIVE operand: tag = 0, imm = 0] FETCH_TGT --> COMPUTE[target = data_island_base + imm = start of decrypted shellcode] COMPUTE --> MARSH[marshal all VM regs to host regs, rsp = VM_RSP seeded with exit_handler] MARSH --> JMP["jmp [target_slot]"] JMP --> NATIVE[original shellcode runs natively] NATIVE --> RET{shellcode does ret?} RET -- yes --> POPSS[ret pops exit_handler from VM shadow stack] POPSS --> EPI[exit_handler restores NV regs, rets to original caller] RET -- ExitProcess --> DEAD[process terminates]

root@kitploit:~
Отложенное дешифрование острова данных (data island) работает только в режиме упаковки. В стандартном и гибридном режимах поднятый код выполняет операции VM LOAD/STORE над байтами острова данных во время выполнения, и ему нужны открытые данные (plaintext) с самого начала, поэтому пролог обрабатывает это с нетерпением. В режиме упаковки есть ровно один потребитель острова данных — собственный выход (native escape) после синтетического JMP_NATIVE, поэтому дешифрование может подождать до этого момента.

## Гибридный режим: `--ranges A:B,C:D`

Целевая виртуализация. Выберите диапазоны байтов во входных данных, которые должны быть подняты (lifted). Всё остальное остаётся в виде исходных нативных байтов в выходных данных. В начале каждого диапазона подъемщик (lifter) вставляет 5-байтовый `jmp rel32` к заглушке `vm_entry_K`, добавляемой после области нативного шелл-кода. Внешний нативный код может повторно войти в поднятый диапазон только через изменённый начальный байт. Внутренние байты диапазона заполняются int3, поэтому любой нативный переход, целящийся во внутренний байт, отклоняется во время сканирования. Поднятый код, выходящий из диапазона на байты за его пределами, становится `JMP_NATIVE` или `CALL_NATIVE`.

Сначала используйте `--scan`, чтобы найти подходящие кандидаты. Сканирование классифицирует диапазоны с пропусками, с блоком, не завершающимся `ret`, с телом короче 5 байт, или с внешними нативными переходами во внутренние байты как «почти подходящие» (near-miss), а не как подходящие.

### Конвейер сборки

Как `--ranges` поднимает только выбранные диапазоны байтов и изменяет нативный шелл-код на месте, чтобы управление перенаправлялось в добавленные заглушки точек входа VM.```mermaid
flowchart TB
    A[shellcode.bin] --> RP[parse_ranges from --ranges flag]
    RP --> CFG[CFGBuilder with set_lifted_ranges restriction]
    CFG --> LIFT[lift_program: only blocks whose start_va is inside any range]
    LIFT --> XCG[branches/calls leaving the range become JMP_NATIVE/CALL_NATIVE]
    XCG --> OBF[IR obfuscation passes same as default]
    OBF --> ENC[encode bytecode]
    ENC --> BTAB[block table for RET_VM lookup]
    SEED[seed u64] --> VMC[VMConfig]
    VMC --> STUB[VMCodeGen::emit_range_mode: one vm_entry_K per range + dispatcher + handlers]
    STUB --> NSEC[start with verbatim copy of the native shellcode bytes]
    NSEC --> PATCH[at each range start, write 5-byte jmp rel32 to vm_entry_K]
    PATCH --> INT3[fill remaining bytes of the displaced run with int3]
    INT3 --> APPEND[append: VM stub + sbox_inv + bytecode + data island + block table]
    APPEND --> OUT[out.bin]

Поток выполнения

Управление потоком через смешанный native и VM blob, включая вход в VM dispatch, когда native код достигает начала patched range, и native-escape пути, используемые, когда lifted код ветвится обратно.```mermaid flowchart TB ENTRY[blob byte 0 = native shellcode prologue] --> NAT[native shellcode runs] NAT --> HIT{control reaches a patched range start?} HIT -- no --> NAT HIT -- yes --> JMP[jmp rel32 to vm_entry_K] JMP --> RPROL[range prologue: push NV regs, allocate frame, lea state_ptr] RPROL --> MARSHIN[marshal host volatile regs to VM slots, NV regs preserved by Win64 ABI] MARSHIN --> SI[state init gated by init_flag for first-time decrypts] SI --> DISP[dispatcher loop] DISP --> HND[handler] HND --> NXT{terminator?} NXT -- BR/BR_CC inside range --> DISP NXT -- range ret reached --> CLEANUP[restore NV regs, pop frame, ret pops native retaddr from host stack] NXT -- JMP_NATIVE to byte outside range --> MIDEXIT[mid-exec cleanup: overwrite caller retaddr slot with target, jmp to exit_handler] NXT -- CALL_NATIVE to API --> APITAIL[push trampoline addr to VM_RSP, jmp resolved API] CLEANUP --> NAT MIDEXIT --> EH[exit_handler unwinds NV pushes, ret lands at the target we wrote] EH --> NAT APITAIL --> APIRET[API rets to trampoline in stub which resumes VM dispatch] APIRET --> DISP

root@kitploit:~
Coroutine-style ranges, the ones with no `ret` that exit via a tail-jmp, are documented via `--coroutines` for `--scan` output. The flag does not change codegen at present. Range mode is best fit for self-contained leaf functions. Helper functions that depend on a specific caller-supplied register state cannot be lifted standalone; the API ends up called with garbage args.

## Stacked mode: `--pack --ranges A:B,C:D`

Run the hybrid build, then pack-wrap the result. The outer VM decrypts the inner hybrid blob in place and jumps to byte 0 of it. From there execution proceeds exactly like standalone hybrid mode, except the entire blob including the chosen-range bytecode is encrypted at rest. The two layers compose cleanly because the inner blob is a self-contained PIC region.

### Build pipeline

How stacked mode recursively invokes the packager: an inner `--ranges` build first, then an outer `--pack` wrap of that build.```mermaid
flowchart TB
    A[shellcode.bin] --> INNER[invoke package_shellcode recursively in range-only mode]
    INNER --> RBLOB[range-mode bytes in memory]
    RBLOB --> WRAP[invoke package_shellcode again in pack-only mode with RBLOB as input]
    WRAP --> OUT[out.bin: outer pack VM wrapping the inner range-mode blob]

Поток выполнения

Как внешняя VM пакета передает управление внутренней VM в режиме диапазона, каждая из которых работает в собственном независимом фрейме VMState.```mermaid flowchart TB E[blob entry] --> OPROL[outer pack prologue + state init] OPROL --> ODISP[outer dispatcher fetches synthetic JMP_NATIVE] ODISP --> LAZY[lazy decrypt of inner range-mode blob via deferred data-island decrypt] LAZY --> JN[outer JMP_NATIVE imm 0 sets target = inner blob byte 0] JN --> INAT[inner native shellcode runs] INAT --> IHIT{inner patched range start hit?} IHIT -- yes --> IVM[inner range vm_entry_K] IHIT -- no --> INAT IVM --> IRDISP[inner dispatcher loop, separate VMState frame] IRDISP --> INAT

root@kitploit:~
Внутренняя ВМ и внешняя ВМ не имеют общего состояния. Это две независимые ВМ, которые случайно находятся в одном блобе. Единственная задача внешней — ограничивать доступ к внутренней через слой расшифровки.

## Режим детура: `--embed-into target.exe --at RVA`

Форма отличается от остальных. На входе — сырой шелл-код, обычно это блоб ВМ, уже созданный mkPIVM в другом режиме, хотя подойдут любые байты PIC. На выходе — модифицированный PE.

Инструмент парсит `target.exe`, находит выбранный RVA в исполняемом разделе, дизассемблирует достаточно инструкций, чтобы покрыть 5 байт, отказывается, если перемещаемый фрагмент содержит RIP-относительную адресацию или относительное управление потоком, которое не выдержит переноса, затем добавляет новый раздел RWX, содержащий обёртку вместе с блобом ВМ. Обёртка сохраняет состояние вызывающего, передаёт управление блобу ВМ, восстанавливает состояние, выполняет смещённые исходные байты и переходит обратно на байт после патча. По выбранному RVA размещается 5-байтовый `jmp rel32`, указывающий на обёртку.

Два подрежима для передачи управления от обёртки к ВМ:

### Подрежим с нитями, по умолчанию

Далее следуют две диаграммы. Первая — конвейер модификации PE во время сборки, который вставляет раздел с обёрткой, исправляет ссылки и записывает 5-байтовый jmp по выбранному RVA. Вторая — поток управления во время выполнения, когда хост-процесс в конечном итоге достигает этого RVA.```mermaid
flowchart TB
    BLOB[vm_blob.bin pre-built from any other mode] --> READ[read target.exe bytes]
    TGT[target.exe] --> READ
    READ --> PEHDR[parse DOS header, NT headers, sections, OptionalHeader, BASERELOC dir]
    PEHDR --> ARCHCHK[arch from PE32/PE32+ magic must match blob arch]
    ARCHCHK --> LOC[locate RVA inside an executable section]
    LOC --> DA[Zydis disassemble at RVA, accumulate insns until total length >= 5]
    DA --> VALID{any displaced insn has RIP-relative mem operand or is a rel32 branch?}
    VALID -- yes --> FAIL[error, pick a different RVA]
    VALID -- no --> IATSCAN[scan IMAGE_DIRECTORY_ENTRY_IMPORT for kernel32!CreateThread]
    IATSCAN --> IATFOUND{found?}
    IATFOUND -- no --> FAIL2[error, suggest --detour-inline or different target]
    IATFOUND -- yes --> EMITW[emit threaded wrapper bytes]
    EMITW --> APPEND[concatenate wrapper + vm_blob into new section content]
    APPEND --> ARCHBR{arch?}
    ARCHBR -- x64 --> X64FIX[fix wrapper rel32s: lea r8 to vm_blob; call qword ptr rip+iat_disp32]
    ARCHBR -- x86 --> X86FIX[fix wrapper abs32s: push vm_blob_va; call dword ptr iat_va; then append combined IMAGE_BASE_RELOCATION table with new HIGHLOW entries for those abs32s]
    X64FIX --> RJMP[compute jmp_rel32 from wrapper-tail back to RVA + displaced_len]
    X86FIX --> RJMP
    RJMP --> SEC[allocate next aligned VA + raw offset, write IMAGE_SECTION_HEADER with RWX + CNT_CODE]
    SEC --> BUMP[bump NumberOfSections, SizeOfImage, zero CheckSum]
    BUMP --> X86RELOC{x86?}
    X86RELOC -- yes --> RDIR[update DataDirectory BASERELOC to new combined table]
    X86RELOC -- no --> WJ[write 5-byte jmp rel32 at RVA, NOP-fill remaining displaced bytes]
    RDIR --> WJ
    WJ --> WRITE[serialize patched bytes to output path]
    WRITE --> OUT[patched.exe]

Пример использования

Запуск инструмента с использованием Docker-контейнера с интерактивным терминалом

root@kitploit:~
docker run --rm -p 8081:8081 ghcr.io/cyberark/conjur-sdk-php:latest

Приведенная выше команда запустит контейнер с Conjur OSS Suite на http://localhost:8081 и откроет интерактивный терминал.

Для получения дополнительной информации об использовании Docker-контейнера обратитесь к официальной документации Docker.

Запуск инструмента с помощью файла docker-compose

  1. Сохраните следующий файл docker-compose.yml на своем компьютере:
root@kitploit:~
version: "3"
services:
  conjur:
    image: ghcr.io/cyberark/conjur-sdk-php:latest
    ports:
      - "8081:8081"
  1. Выполните docker-compose up -d, чтобы запустить контейнер.

Запуск инструмента с использованием PHP

  1. Установите PHP 8.1 и Composer на свой компьютер.
  2. Выполните composer require cyberark/conjur-sdk-php.
  3. В своем проекте PHP создайте экземпляр клиента и начните отправлять запросы:
root@kitploit:~
<?php

require_once 'vendor/autoload.php';

$configuration = new Conjur\Auth\Configuration();
$configuration->setAccount('myAccount');
$configuration->setApplianceUrl('http://localhost:8081');

$auth = new Conjur\Auth\ApiKeyAuth('username', 'api_key', $configuration);
$client = new Conjur\Client($auth);

$response = $client->getSecret('path/to/secret');
echo $response;

Для получения дополнительной информации о PHP SDK обратитесь к официальной документации PHP SDK.```mermaid flowchart TB HOST[host main reaches patched RVA] --> RJMP[jmp rel32 to wrapper in new section] RJMP --> PUSH[pushfq, push all volatile regs and rbp] PUSH --> SAVE_RSP[mov rbp, rsp; and rsp, -16; sub rsp, 0x38 for shadow + spill alignment] SAVE_RSP --> ARGS[xor ecx,ecx; xor edx,edx; lea r8, vm_blob_rel32; xor r9d,r9d; spill 0 at rsp+0x20, 0 at rsp+0x28] ARGS --> CT["call qword ptr [rip + CreateThread_iat_disp32]"] CT --> WTHREAD[worker thread starts running vm_blob] CT --> REST[mov rsp, rbp; pop volatiles; popfq] REST --> DISP[execute the displaced original bytes verbatim] DISP --> RJMP2[jmp rel32 back to RVA + N_displaced] RJMP2 --> HOST_CONT[host main continues normally] WTHREAD --> VMBODY[vm_blob runs concurrently: stager beacons, mbox shows dialog, whatever]

root@kitploit:~
Потоковый подрежим идеально подходит для стажеров и маяков. Главная нить хоста никогда не блокируется. Рабочий поток наследует жизненный цикл, необходимый полезной нагрузке. Если полезная нагрузка вызывает `ExitProcess`, весь процесс завершается, но для незавершающейся полезной нагрузки, такой как любой C2-маяк, хост работает постоянно параллельно с ней.

### Встроенный подрежим: `--detour-inline`

Та же структура обёртки, но обёртка выполняет прямой вызов `call vm_blob` вместо `CreateThread`. Главная нить хоста блокируется до тех пор, пока ВМ не вернёт управление. Полезно, когда в таблице IAT цели отсутствует `CreateThread`, или как запасной вариант, когда путь с базовой перелокацией в потоковом режиме не может быть применён к конкретной цели.```mermaid
flowchart TB
    HOST[host main reaches patched RVA] --> RJMP[jmp rel32 to wrapper]
    RJMP --> SAVE[save flags + volatile regs + align]
    SAVE --> CALL[call rel32 vm_blob synchronously]
    CALL --> BLOCK[host main thread blocks here for the duration]
    BLOCK --> RET{vm_blob returns?}
    RET -- via ret --> REST[restore rsp, pop volatiles, popfq]
    RET -- via ExitProcess in payload --> DEAD[process terminates, no further code runs]
    REST --> DISP[execute displaced original bytes]
    DISP --> RJMP2[jmp rel32 back to RVA + N_displaced]
    RJMP2 --> HOST_CONT[host continues]

Scan mode: --scan

Нет выходного файла. Строит CFG из входного шелл-кода и выводит кандидаты --ranges в stderr. Каждый кандидат классифицируется как допустимый, сопрограмма, почти подходящий или внутренний, где внутренний означает затенённый более крупным допустимым кандидатом, который уже его покрывает. Используйте это для выбора аргументов диапазона перед повторным вызовом инструмента в гибридном режиме.```mermaid flowchart TB INP[shellcode.bin] --> CFGB[CFGBuilder + recursive descent over the whole input] CFGB --> ITER[for each block that is a call_target or jmp_target] ITER --> BFS[BFS the reachable subgraph from this entry] BFS --> SPAN[compute min_va..max_va span] SPAN --> CONT_CHK{every byte in span belongs to a visited block or to a known insn boundary?} CONT_CHK -- yes --> RET_CHK{any visited block ends with ret?} CONT_CHK -- no --> NM1[near-miss: fragmented or mid-fn data] RET_CHK -- yes --> EXT_CHK{any non-visited block has a successor pointing inside the span?} RET_CHK -- no --> CORO[coroutine candidate, no ret terminator] EXT_CHK -- yes --> NM2[near-miss: native branches to mid-range bytes] EXT_CHK -- no --> SIZE_CHK{span >= 5 bytes for the entry patch?} SIZE_CHK -- yes --> ELIGIBLE[eligible: prints with --ranges hint] SIZE_CHK -- no --> NM3[near-miss: body too short] ELIGIBLE --> DEDUP[shadow dedupe: drop entries whose reachable set is fully contained in an earlier eligible's reachable set] CORO --> DEDUP NM1 --> DEDUP NM2 --> DEDUP NM3 --> DEDUP DEDUP --> PRINT[stderr table sorted by category] PRINT --> END[exit 0]

root@kitploit:~
## Оси полиморфизма для каждого сида

Перечислены примерно в порядке того, насколько сильно они влияют на статическую сигнатуру.

* Семейство шифров. Одно из ARX, LcgSub, SBoxAdd, FeistelByte. Определяет как шифрование байткода, используемое при сборке, так и встроенный декрипт, генерируемый в пути выборки диспетчера. Шифрование и дешифрование совпадают по построению.
* Схема размещения слотов регистров. VMState содержит слоты `reg_count`, размером от 24 до 32 на сид. 16 архитектурных GPR и 4 Tmp регистра получают новую перестановку индексов слотов для каждого сида.
* Отображение опкодов в обработчики. Для каждого семейства кодеков назначается случайный байт опкода на каждый сид. Таблица обработчиков из 256 записей индексируется по опкоду; закодированный байткод ссылается на сопоставленные опкоды.
* Топология диспетчера. Потоковая или центральная, выбирается для каждого сида.
* Стратегия самоопределения пролога. `call $+5; pop`, `lea reg, [rip+0]` или перемешивание jmp/call.
* Временные регистры обработчика. Обработчик BR_CC переставляет свои 6 временных ролей по временному пулу. Обработчик Store делает то же самое.
* Плотность мусорных гаджетов. От 0 до 3, контролирует выброс мусора между обработчиками.
* Решения этапов обфускации IR. Внедрение мертвого IR и непрозрачные предикаты используют свои собственные под-RNG, поэтому их решения детерминированы для каждого сида, не нарушая другие оси.
* Начальное состояние шифрования байткода. Случайное 64-битное значение для каждого сида.

## Протестированные полезные нагрузки

Этот инструмент был проверен на нескольких образцах Cobalt Strike, MSF, Sliver и множестве других shellcode.

## Сборка

Требуется Visual Studio 2022, CMake 3.21 или новее, и vcpkg. Проект CMake подтягивает Zydis и fmt через режим манифеста vcpkg.```
cmake -S . -B build
cmake --build build --config Release --target mkpivm

Известные ограничения

  • Режим диапазонов для кобальтовых стейджеров не работает как отдельный лист. Вспомогательные функции стейджера зависят от состояния регистров, предоставляемого вызывающей стороной, которое раннер не предоставляет. Полная виртуализация или --pack — это маршруты, которые действительно работают с маяком.
  • x86-тредовый детур требует, чтобы цель либо не имела ASLR, либо принимала дополнительные записи базовых релокаций, которые инструмент эмитирует при их наличии. Если каталог данных BASERELOC цели повреждён или отсутствует, инструмент переключается на инлайн-режим.
  • Лифтер в настоящее время не покрывает перемещения регистров SSE/AVX, атомарные операции, CMPXCHG, RDMSR или привилегированные инструкции. Режим упаковки является обходным путём для шеллкодов, использующих их.
  • Подписи Authenticode на выходных данных --embed-into становятся недействительными. Контрольная сумма PE обнуляется.
  • Выходные блобы требуют RWX во время выполнения, потому что дешифрование на месте записывает обратно в собственные страницы блоба. Большинство загрузчиков, выполняющих шеллкод, в любом случае выделяют RWX. Планов переходить на только RX нет, потому что такой компромисс даёт RX ценой обхода PEB и вызова VirtualAlloc, что, вероятно, является худшей сигнатурой, чем заменённая страница RWX.
  • Виртуализация полезных нагрузок без стейджеров через стандартный режим пока не поддерживается, так как они ужасно сложны для обёртывания в ВМ; предпочтите --pack + --ranges.

Примечания

  • Этот проект в значительной степени является исследовательским proof-of-concept. Если он будет хорошо принят, я расширю его по запросам и приветствую вклад. Однако для протестированных образцов он казался стабильным.
  • Если ваш шеллкод не работает, и вы не хотите создавать Issue, то, к сожалению, я ничем не могу помочь. Он был протестирован на Sliver, Cobalt Strike 4.12, MSF, Havoc и нескольких других нераскрытых образцах.
  • В моих тестах внедрение ВМ в живые процессы работало нормально. Однако, что касается встраивания в PE-файлы, это не тестировалось на коммерческом ПО, таком как MS Word, только на синтетических тестах, но, вероятно, работает. Если нет, исправлю.
  • По состоянию на 20.05.2026 (похоже, сейчас это закончилось) похоже, что репозиторий был атакован ботами. Это прекрасно. Пожалуйста, игнорируйте все пустые аккаунты GitHub.
  • Я видел какой-то пост, сгенерированный ИИ, о mkPIVM, в котором говорилось, что его недостатки:
    • 1). Отсутствие поддержки Linux — это справедливо, и я скоро добавлю её.
    • 2). Отсутствие графического интерфейса? Что?
    • Не знаю, мне показалось это забавным.

Вклад

Это крутая хрень, будьте реалистами. Если вы хотите предложить новые идеи, исправить свои баги, отправить Issues для меня, чтобы я исправил, и так далее — дерзайте.

Скачать инструмент