MCPcopy Create free account
hub / github.com/D7EAD/mkPIVM

github.com/D7EAD/mkPIVM @2.0.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release 2.0.0 ↗ · + Follow
553 symbols 1,730 edges 31 files 144 documented · 26% updated 38d ago2.0.0 · 2026-06-04★ 447

Browse by type

Functions 458 Types & classes 95
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Working on polymorphism improvements, will push sooner or later

Read the research paper (written for 1.0.0).

mkPIVM is a polymorphic position-independent shellcode virtualizer for Windows x86 and x64 (Linux soon).

Feed it raw shellcode. It emits another raw blob: a small virtual machine that interprets a lifted, encrypted-at-rest version of your original instructions. The output is itself position-independent code and runs anywhere the original shellcode would, from a remote-thread loader to a code cave detour. Every per-seed knob varies independently: cipher family, register slot layout, opcode-to-handler permutation, dispatcher topology, junk-gadget pattern, IR obfuscation insertion points. Two builds from the same input share fewer than a hundred coincidental bytes out of tens of kilobytes.

Why: native shellcode is signature-trivial. Wrapping it in a per-instance VM with a per-instance cipher leaves nothing useful at rest, and lifting the instructions to bytecode puts another wall between disk bytes and any disassembler that knows what x86 looks like. As far as I can tell from a literature sweep, no public tool ships exactly this pipeline: raw PIC in, raw polymorphic VM PIC out. So, I mentioned that in the research paper it demanded. To be honest, if I am right about no one having done this (publicly) before, and I am pretty confident, I am surprised. Nonetheless, enjoy.

Related Work & Plans

  • Linux support will be added soon.

Quick start

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

Your PIVM is hot and ready. That's the simplest path. Several other modes vary how aggressively the original instructions get virtualized, whether the output is a standalone blob or a patched PE, and whether the lift runs at all.

Showcase

I got the receipts. You can see a video of mkPIVM in action below, fully virtualizing a Meterpreter stager (vanilla btw), injecting into explorer.exe, and us capturing a callback. Of course this is just an example, and mkPIVM can be applied to much more, assuming the instructions in the shellcode are supported. If they aren't, make an Issue, send me the shellcode, I got you.

See it here. Hosted in ./media, can't embed sadly.

Here is the VirusTotal report for that exact virtualized sample (as of 05/27/2026).

...and the packed version, not even virtualized, notably higher entropy.

There was careful attention paid to the entropy telemetry of the output of this tool, which results in shellcode of entropy less than typical Windows WinAPI DLLs (outside of packing mode), such as ntdll.dll or kernel32.dll. The entropy comparison is about...

File Bytes Entropy
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

Modes at a glance

Mode Flags What changes
Default none Lift the whole input. Everything virtualized.
Packer --pack Don't lift. Wrap input as encrypted data, decrypt at runtime, jump in.
Hybrid --ranges A:B,... Lift only the chosen byte ranges. Rest stays native.
Stacked --pack --ranges A:B Build the hybrid blob, then pack-wrap it.
Detour --embed-into PE --at RVA Take a pre-built blob, embed into a PE, patch a jmp at the chosen RVA.
Scan --scan Print eligible --ranges candidates from the input's CFG, then exit.

Every mode honors --seed, --arch, --input-format, and --format. See the per-mode sections below for the build pipeline and the runtime flow.

Default virtualization

The lifter walks the whole CFG and lowers every instruction to a custom IR. The IR goes through two obfuscation passes, then through codecs that encode each insn into the per-seed bytecode shape. The block table, handler table, and data island are encrypted with the same per-byte stream cipher as the bytecode. At runtime the prologue decrypts those three regions in place and the dispatcher loop fetches bytecode bytes one at a time, decrypting and dispatching to a handler that does the work.

Build pipeline

Steps the build performs to turn raw shellcode into the emitted virtualized blob, end to end.

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]

Runtime flow

What the emitted blob does when the host loader calls into it at byte 0, from prologue through dispatcher fetches and the various termination paths.

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]

The runtime nonce trick is the key anti-memory-scan move: the handler table sits in memory XOR'd with a value derived from rdtsc XOR cipher_init, so two processes loading the same blob hold byte-different handler tables. The dispatcher unmasks at lookup time with one extra XOR.

Packer mode: --pack

The opposite tradeoff. The lifter does not run. The original shellcode goes into the data island encrypted, and the IR is a single synthetic JMP_NATIVE imm=0. The prologue intentionally skips the data-island decrypt that default mode runs eagerly. Instead, the first time the lone JMP_NATIVE handler fires, it decrypts the data island in place, sets a marker byte, then transfers control to byte 0 of the now-plaintext shellcode. The shellcode runs natively from there.

Works on any shellcode, regardless of lifter coverage. Useful for stageless cobalt, exotic syscall-heavy payloads, anything too large or weird to virtualize. Loses the per-instruction virtualization defense but keeps the full per-seed VM polymorphism on the wrapper.

Build pipeline

How --pack wraps the original shellcode as an encrypted data island behind a one-instruction synthetic IR program.

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]

Runtime flow

What the pack wrapper does on first entry, including the gated lazy decrypt of the data island and the single synthetic JMP_NATIVE that hands control to the now-plaintext shellcode.

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]

The deferred data-island decrypt is pack-mode-only. In default and hybrid mode the lifted code does VM LOAD/STORE on data-island bytes mid-execution and needs them plaintext from the start, so the prologue handles it eagerly. In pack mode there is exactly one consumer of the data island, the native escape after the synthetic JMP_NATIVE, so the decrypt can wait until then.

Hybrid mode: --ranges A:B,C:D

Targeted virtualization. Pick byte ranges in the input that should be lifted. Everything else stays as the original native bytes in the output. At each range start the lifter patches a 5-byte jmp rel32 to a vm_entry_K stub appended after the native shellcode region. External native code may only re-enter a lifted range through the patched start byte. Mid-range bytes are int3-filled, so any native branch that targets a mid-range byte is rejected at scan time. Lifted code that exits a range to bytes outside it becomes JMP_NATIVE or CALL_NATIVE.

Use --scan first to find eligible candidates. Scan classifies ranges with gaps, with no ret-terminated block, with body shorter than 5 bytes, or with external native branches into mid-range bytes as near-miss rather than eligible.

Build pipeline

How --ranges lifts only the chosen byte ranges and patches the native shellcode in place so control redirects into the appended VM entry stubs.

```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 verbat

Core symbols most depended-on inside this repo

browse all functions →

Shape

Method 325
Function 133
Class 80
Enum 15

Languages

C++100%
C1%

Modules by API surface

src/x64_emit.cpp88 symbols
src/x86_emit.cpp87 symbols
include/mkpivm/lifter.h59 symbols
include/mkpivm/vm_isa.h39 symbols
src/codec.cpp31 symbols
src/main.cpp25 symbols
src/lifter.cpp23 symbols
src/pe_embed.cpp22 symbols
include/mkpivm/ir.h22 symbols
src/codec_x86.cpp21 symbols
include/mkpivm/bytecode.h19 symbols
src/vm_codegen_x86.cpp11 symbols

For agents

$ claude mcp add mkPIVM \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page