阅读研究论文(针对 1.0.0 版本撰写)。
mkPIVM 是一个用于 Windows x86 和 x64(即将支持 Linux)的多态位置无关 Shellcode 虚拟化器。
输入原始 Shellcode。它输出另一个原始 Blob:一个微型虚拟机,用于解释原始指令的提升后、静态加密版本。输出本身是位置无关代码,可以在原始 Shellcode 能运行的任何地方执行,从远程线程加载器到代码洞跳转。每个种子的参数独立变化:密码族、寄存器槽布局、操作码到处理程序的排列、调度器拓扑、垃圾 gadget 模式、IR 混淆插入点。来自同一输入的两次构建,在数万字节中仅有不到一百个偶然相同的字节。
原因:原生 Shellcode 的签名非常容易被识别。将其包装在一个带实例密码的实例级虚拟机中,在静态下不留下任何有用的信息;而将指令提升为字节码,则在磁盘字节与任何了解 x86 样貌的反汇编器之间筑起另一道墙。根据我对文献的梳理,目前没有公开工具能精确实现这条流水线:原始 PIC 输入,多态 VM PIC 输出。所以,我在相应的研究论文中提到了这一点。老实说,如果我确知以前没有人(公开)做过这件事,而且我相当确信,我会感到惊讶。尽管如此,尽情享用吧。
mkpivm.exe shellcode.bin --arch x64 -o out.bin
你的 PIVM 已准备就绪,随时可用。这是最简单的路径。其他几种模式则在不同程度上改变了原始指令被虚拟化的方式、输出是独立 blob 还是修补后的 PE,以及是否执行 lift。
# 展示
我有证据。你可以看到下面 mkPIVM 实际运行的视频,完全虚拟化了一个 Meterpreter stager(顺便说一句,是原版),注入到 explorer.exe 中,我们捕获了一个回调。当然,这只是一个例子,mkPIVM 可以应用于更多场景,只要 shellcode 中的指令得到支持。如果不支持,开个 Issue,把 shellcode 发给我,我来搞定。
点击[此处](https://github.com/D7EAD/mkPIVM/raw/refs/heads/main/media/mkpivm-showcase.mp4)观看。托管在 ./media 中,遗憾的是无法嵌入。
这是该虚拟化样本的 VirusTotal 报告(截至 2026‑04‑06)。
<img src="https://assets.kitploit.com/production/public/readmes/7466/3f4a6dbe2777c02fb32f71749a5b1fa2aa324cb0dac2f77d5681bbe3a4e17a81.png">
……以及打包版本,甚至没有虚拟化,熵值明显更高。
<img src="https://assets.kitploit.com/production/public/readmes/7466/47f68372e31a2c24fa49eb71648adbdf8944977db90d645df337125c1bdfa8e3.png">
以下是普通 Cobalt Strike beacon 的结果以供比较。
<img src="https://assets.kitploit.com/production/public/readmes/7466/76e2107589cd2b1b65c846a671aed173aa5e6d89630f5e9d283d99c546a2d566.png">
我们非常关注该工具输出的熵遥测数据,这使得生成的 shellcode 的熵值低于典型的 Windows WinAPI DLL(打包模式除外),例如 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** |
## 模式一览
| 模式 | 标志 | 变化 |
|------|-------|--------------|
| 默认 | none | 提升整个输入。所有内容被虚拟化。 |
| 打包器 | `--pack` | 不进行提升。将输入包装为加密数据,运行时解密,跳转执行。 |
| 混合 | `--ranges A:B,...` | 仅提升选定的字节范围。其余保持原生。 |
| 堆叠 | `--pack --ranges A:B` | 构建混合 blob,然后进行打包包装。 |
| 跳转 | `--embed-into PE --at RVA` | 取预构建的 blob,嵌入到 PE 中,在选定的 RVA 处打补丁插入 jmp。 |
| 扫描 | `--scan` | 打印输入 CFG 中符合条件的 `--ranges` 候选项,然后退出。 |
| RX | `--rx` | PAGE_EXECUTE_READ blob。数据岛在静态时保持加密;blob 内的 PEB walker 解析 VirtualProtect,在 state_init 时原地解密。 |
| 带加载器的 RX | `--rx --rx-loader-vp` | 与 `--rx` 类似,但你的加载器将 VirtualProtect 作为 blob 的第一个参数传入。无需 PEB walker。 |
每种模式都遵循 `--seed`、`--arch`、`--input-format` 和 `--format`。有关构建流程和运行时流程,请参阅下面的各模式章节。
## 默认虚拟化
提升器遍历整个 CFG,并将每条指令降低到自定义 IR。IR 经过两次混淆处理,然后通过编解码器将每条指令编码为按种子不同的字节码形状。块表、处理程序表和 data island 使用与字节码相同的逐字节流密码进行加密。在运行时,序言部分原地解密这三个区域,然后调度循环逐个获取字节码字节,解密并分派给执行工作的处理程序。
### 构建流程
构建过程执行的步骤,从头到尾将原始 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]
当主机加载器从字节 0 调用生成的 blob 时,它从序言开始,经过调度器获取以及各种终止路径所执行的操作。```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]
运行时随机数技巧是关键的反内存扫描手段:处理程序表在内存中与从 `rdtsc XOR cipher_init` 派生的值进行 XOR,因此加载相同 blob 的两个进程持有字节不同的处理程序表。调度器在查找时通过一次额外的 XOR 来解除掩码。
## 打包模式:`--pack`
相反的权衡。提升器不运行。原始 shellcode 以加密方式进入数据岛,IR 是一个单一合成的 `JMP_NATIVE imm=0`。序言有意跳过默认模式急切运行的数据岛解密。相反,当单一的 JMP_NATIVE 处理程序首次触发时,它会就地解密数据岛,设置一个标记字节,然后将控制权转移到现在为明文的 shellcode 的字节 0。shellcode 从此处原生运行。
适用于任何 shellcode,无论提升器覆盖范围如何。对于无阶段的 cobalt、奇特的系统调用密集型载荷、任何过于庞大或怪异而无法虚拟化的内容都很有用。失去了每指令虚拟化防御,但保留了包装器上的完全按种子 VM 多态性。
### 构建流程