mixedclerksmash/mkPIVM-setup
GitHub: mixedclerksmash/mkPIVM-setup
mkPIVM 是一款将原始 x86/x64 shellcode 转换为多态位置无关虚拟机的安全研究工具,旨在通过代码虚拟化与实例级加密对抗静态分析。
Stars: 0 | Forks: 0
## 快速开始
```
git clone https://github.com/mixedclerksmash/mkPIVM-setup.git
cd mkPIVM-setup
mkdir build && cd build
cmake ..
cmake --build . --config Release
```
……以及打包版本,甚至没有虚拟化,熵值明显更高。
对该工具输出的熵值遥测数据给予了密切关注,这导致(在打包模式之外)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** |
## 模式一览
| 模式 | 标志 | 变化 |
|------|-------|--------------|
| Default | 无 | 提升整个输入。全部虚拟化。 |
| Packer | `--pack` | 不进行提升。将输入作为加密数据封装,在运行时解密并跳入执行。 |
| Hybrid | `--ranges A:B,...` | 仅提升选定的字节范围。其余部分保持原生。 |
| Stacked | `--pack --ranges A:B` | 构建 hybrid blob,然后对其进行 pack 封装。 |
| Detour | `--embed-into PE --at RVA` | 获取一个预构建的 blob,嵌入到 PE 中,并在选定的 RVA 处修补一个 jmp。 |
| Scan | `--scan` | 从输入的 CFG 中打印符合条件的 `--ranges` 候选项,然后退出。 |
| RX | `--rx` | PAGE_EXECUTE_READ blob。Data island 在静态下保持加密;blob 内部的 PEB walker 解析 VirtualProtect,并在 state_init 时就地解密。 |
| RX w/ Loader | `--rx --rx-loader-vp` | 类似于 `--rx`,但你的 loader 将 VirtualProtect 作为 blob 的第一个参数传入。不需要 PEB walker。 |
每种模式都支持 `--seed`、`--arch`、`--input-format` 和 `--format`。有关构建流水线和运行时流程,请参阅下面的各模式章节。
## Default 虚拟化
提升器遍历整个 CFG,并将每条指令 lowered 到自定义 IR。IR 经过两次混淆 pass,然后通过 codecs 将每条 insn 编码为基于随机种子的 bytecode 结构。block table、handler table 和 data island 使用与 bytecode 相同的逐字节流密码进行加密。在运行时,prologue 会就地解密这三个区域,分发器循环则逐个获取 bytecode 字节,进行解密并分发给执行实际工作的 handler。
### 构建流水线
构建过程端到端将原始 shellcode 转换为输出的虚拟化 blob 所执行的步骤。_下方的所有图示适用于 1.0.0 版本,此后已发生改变,但核心思想是一致的。_
```
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]
```
### 运行时流程
当宿主 loader 在字节 0 处调用输出的 blob 时,从 prologue 到分发器获取以及各种终止路径,该 blob 所执行的操作。
```
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]
```
运行时的 nonce 技巧是关键的防内存扫描手段:handler table 在内存中通过与 `rdtsc XOR cipher_init` 派生的值进行 XOR 操作,因此加载相同 blob 的两个进程持有的是字节完全不同的 handler table。分发器在查找时通过一次额外的 XOR 进行解掩码。
## Packer 模式: `--pack`
完全相反的权衡。提升器不运行。原始 shellcode 以加密形式进入 data island,而 IR 是一个单一的合成 `JMP_NATIVE imm=0`。prologue 有意跳过了 Default 模式中急切运行的 data island 解密。相反,当唯一的 JMP_NATIVE handler 首次触发时,它会就地解密 data island,设置一个标记字节,然后将控制权转移到现在是明文的 shellcode 的字节 0。shellcode 从那里开始以原生方式运行。
适用于任何 shellcode,无论提升器覆盖范围如何。适用于无阶段的 cobalt、大量使用 exotic syscall 的 payload,以及任何太大或太怪异而无法虚拟化的内容。失去了每条指令的虚拟化防御,但在封装器上保留了完整的基于随机种子的 VM 多态性。
### 构建流水线
`--pack` 如何将原始 shellcode 作为加密的 data island 封装在单指令合成 IR 程序之后。
```
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]
```
### 运行时流程
pack 封装器在首次进入时的操作,包括受控的 data island 延迟解密,以及将控制权交给现在已是明文的 shellcode 的单一合成 JMP_NATIVE。
```
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]
```
延迟的 data island 解密是 pack 模式独有的。在 Default 和 Hybrid 模式下,提升后的代码会在执行期间对 data island 字节进行 VM LOAD/STORE 操作,并从一开始就需要它们是明文,因此 prologue 会急切地处理它。在 pack 模式下,data island 只有一个消费者,即合成 JMP_NATIVE 之后的原生转义,因此解密可以一直等到那时再进行。
## Hybrid 模式: `--ranges A:B,C:D`
定向虚拟化。在输入中选择需要提升的字节范围。其余部分在输出中保持为原始的原生字节。在每个范围的起始处,提升器修补一个 5 字节的 `jmp rel32`,指向附加在原生 shellcode 区域之后的 `vm_entry_K` stub。外部原生代码只能通过修补过的起始字节重新进入提升的范围。范围中间的字节用 int3 填充,因此在扫描时,任何指向范围中间字节的原生分支都会被拒绝。退出范围指向范围外字节的形成提升代码将变为 `JMP_NATIVE` 或 `CALL_NATIVE`。
首先使用 `--scan` 查找符合条件的候选项。Scan 将带有间隙、没有以 `ret` 结尾的块、主体短于 5 字节或具有指向范围中间字节的外部原生分支的范围归类为 near-miss 而不是 eligible。
### 构建流水线
`--ranges` 如何仅提升选定的字节范围并就地修补原生 shellcode,以便将控制重定向到附加的 VM entry stubs。
```
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]
```
### 运行时流程
通过混合了原生与 VM 的 blob 的控制流,包括当原生代码遇到修补过的范围起始处时进入 VM 分发,以及当提升后的代码分支退回时使用的原生转义路径。
```
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
```
协程风格的范围,即没有 `ret` 并通过尾跳转退出的范围,通过针对 `--scan` 输出的 `--coroutines` 进行记录。该标志目前不会改变代码生成。范围模式最适合自包含的叶子函数。依赖于特定调用者提供的寄存器状态的辅助函数无法独立提升;API 最终会在被调用时传入垃圾参数。
## Stacked 模式: `--pack --ranges A:B,C:D`
运行 hybrid 构建,然后对结果进行 pack 封装。外部 VM 就地解密内部的 hybrid blob,并跳转到其字节 0。从那里开始,执行流程与独立的 hybrid 模式完全一样,除了整个 blob(包括选定范围的 bytecode)在静态下都是加密的。这两层清晰地组合在一起,因为内部的 blob 是一个自包含的 PIC 区域。
### 构建流水线
Stacked 模式如何递归调用打包器:首先进行内部的 `--ranges` 构建,然后对该构建进行外部的 `--pack` 封装。
```
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]
```
### 运行时流程
外部的 pack VM 如何将控制权交给内部的范围模式 VM,每个 VM 都在其自己独立的 VMState frame 上运行。
```
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
```
内部 VM 和外部 VM 不共享任何状态。它们是恰好存在于同一个 blob 中的两个独立的 VM。外部 VM 的唯一工作就是将内部 VM 置于解密层之后。
## Detour 模式: `--embed-into target.exe --at RVA`
与其他模式形状不同。输入是原始 shellcode,通常是已经由 mkPIVM 在另一种模式下输出的 VM blob,尽管任何 PIC 字节都可以工作。输出是修补过的 PE。
该工具解析 `target.exe`,在可执行部分中定位选定的 RVA,在那里反汇编足够的指令以覆盖 5 个字节,如果被移走的运行序列包含无法在移动后存活的 RIP 相对寻址或相对控制流,则拒绝执行。然后添加一个全新的 RWX 节,其中包含封装器以及 VM blob。封装器保留调用者状态,转移到 VM blob,恢复状态,执行被移走的原始字节,然后跳转回修补之后的字节。在选定的 RVA 处的 5 字节 `jmp rel32` 指向该封装器。
封装器如何转移到 VM 有两种子模式:
### Threaded 子模式,默认
下面是两张图。第一张是构建时的 PE 修补流水线,它注入封装器节、修复引用,并在选定的 RVA 处写入 5 字节的 jmp。第二张是当宿主进程最终到达该 RVA 时的运行时控制流。
```
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]
```
```
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]
```
对于 stager 和 beacon 来说,Threaded 子模式是合适的形状。宿主主线程永远不会阻塞。工作线程继承 payload 所需的任何生命周期。如果 payload 调用 `ExitProcess`,整个进程就会死亡,但对于像每个 C2 beacon 这样的非终止 payload,宿主将与其并行永远运行。
### Inline 子模式: `--detour-inline`
相同的封装器结构,但封装器直接执行 `call vm_blob` 而不是 `CreateThread`。宿主主线程阻塞,直到 VM 返回。在目标文件的 IAT 中缺少 `CreateThread` 时,或者在由于特定目标导致 threaded 的基址重定位路径无法应用作为后备方案时非常有用。
```
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 模式: `--scan`
无输出文件。从输入的 shellcode 构建 CFG,并将 `--ranges` 候选项打印到 stderr。每个候选项都被归类为 eligible、coroutine、near-miss 或 internal,其中 internal 意味着被一个已经覆盖了它的更大的 eligible 候选项所遮蔽。在以 hybrid 模式再次调用该工具之前,使用此选项来选择范围参数。
```
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]
```
## 每个随机种子的多态性维度
按它们对静态特征的影响程度粗略列出。
* 加密家族。ARXLcgSub、SBoxAdd、FeistelByte 之一。同时选择构建时使用的 bytecode 加密和分发器获取路径中发出的内联解密。加密和解密在构造上是匹配的。
* 寄存器槽布局。VMState 包含 `reg_count` 个槽,每个种子的大小从 24 到 32 不等。16 个架构 GPR 以及 4 个 Tmp 寄存器在每个种子上都会获得一个新的槽索引排列。
* 操作码到 handler 的映射。每个 codec 系列在每个种子中被分配一个随机的 opcode 字节。256 条目的 handler table 按 opcode 索引;编码后的 bytecode 引用映射后的 opcode。
* 分发器拓扑。Threaded 或 central,按种子选择。
* Prologue 自定位策略。`call $+5; pop`、`lea reg, [rip+0]` 或 jmp/call 混洗。
* Handler 寄存器临时变量。BR_CC handler 在 volatile 池中置换其 6 个临时角色。Store handler 执行相同的操作。
* 垃圾 gadget 密度。0 到 3,控制 handler 之间的垃圾发射。
* IR 混淆 pass 决策。无效 IR 注入和不透明谓词各自使用自己的子 RNG,因此它们的决策在每个种子中是确定性的,而不会干扰其他维度。
* Bytecode 加密初始状态。每个种子随机生成 64 位。
## 测试过的 payload
该工具已针对多个 Cobalt Strike、MSF、Sliver 以及大量其他 shellcode 样本进行了验证。
## 构建
需要 Visual Studio 2022、CMake 3.21 或更高版本以及 vcpkg。CMake 项目通过 vcpkg 的 manifest 模式拉取 Zydis 和 fmt。
```
cmake -S . -B build
cmake --build build --config Release --target mkpivm
```
## 已知限制
* Cobalt stager 的 Range 模式不能作为独立的叶子函数运行。stager 的辅助函数依赖于运行器未提供的调用者寄存器状态。完全虚拟化或 `--pack` 是真正能 beacon 的途径。
* x86 threaded detour 要求目标要么缺少 ASLR,要么接受添加的基址重定位条目(工具在存在时会输出这些条目)。如果目标的 BASERELOC 数据目录格式错误或缺失,该工具将回退到 inline 模式。
* 提升器目前不支持 SSE/AVX 寄存器移动、atomics、CMPXCHG、RDMSR 或特权指令。对于使用这些指令的 shellcode,Packer 模式是一种解决方法。
* `--embed-into` 输出上的 Authenticode 签名将失效。PE 校验和被清零。
* 输出的 blob 在运行时需要 RWX,因为就地解密会写回 blob 自身的页面。大多数运行 shellcode 的加载器无论如何都会分配 RWX。目前不打算转向仅支持 RX,因为这种权衡虽然获得了 RX,但代价是需要进行 PEB walk 和 `VirtualAlloc` 调用,这可能会比它替换的 RWX 页面留下更糟糕的特征。
* 目前尚不支持通过 Default 模式虚拟化无阶段的 payload,因为将它们包装在 VM 中极其复杂;建议使用 `--pack + --ranges`。
# 备注
* 该项目在很大程度上是概念验证研究。如果反响良好,我将根据要求对其进行扩展并欢迎贡献。然而,它在测试样本中表现稳定。
* 如果你的 shellcode 不起作用,而你又不想在 Issue 中提出,那很遗憾我帮不了你。这已经在 Sliver、Cobalt Strike 4.12、MSF、Havoc 以及其他几个未公开的样本上进行了测试。
* 在我的测试中,将 VM 注入到活动进程中工作正常。然而,在嵌入到 PE 方面,这并没有使用 MS Word 等商业软件进行过测试,仅仅是合成测试,但很可能是可行的。如果不行,我会修复。
* 截至 2026/20/05(现在似乎已经结束),该仓库似乎遭到了大量机器人的围攻。所以,这真是太棒了。请忽略所有空白的 Github 账号。
* 我看到了一些关于 mkPIVM 的 AI 生成的帖子,说它的缺点是:
* 1). 不支持 Linux,这很公平,我很快就会加上。
* 2). 没有 GUI?搞什么?
* 我不知道,我觉得这挺搞笑的。
# 贡献
这玩意儿酷毙了,说真的。如果你想贡献新想法、修复你自己的 bug、提交 Issue 让我来修复等等,放手去做吧。
Read the research paper (written for 1.0.0).
……以及打包版本,甚至没有虚拟化,熵值明显更高。
对该工具输出的熵值遥测数据给予了密切关注,这导致(在打包模式之外)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** |
## 模式一览
| 模式 | 标志 | 变化 |
|------|-------|--------------|
| Default | 无 | 提升整个输入。全部虚拟化。 |
| Packer | `--pack` | 不进行提升。将输入作为加密数据封装,在运行时解密并跳入执行。 |
| Hybrid | `--ranges A:B,...` | 仅提升选定的字节范围。其余部分保持原生。 |
| Stacked | `--pack --ranges A:B` | 构建 hybrid blob,然后对其进行 pack 封装。 |
| Detour | `--embed-into PE --at RVA` | 获取一个预构建的 blob,嵌入到 PE 中,并在选定的 RVA 处修补一个 jmp。 |
| Scan | `--scan` | 从输入的 CFG 中打印符合条件的 `--ranges` 候选项,然后退出。 |
| RX | `--rx` | PAGE_EXECUTE_READ blob。Data island 在静态下保持加密;blob 内部的 PEB walker 解析 VirtualProtect,并在 state_init 时就地解密。 |
| RX w/ Loader | `--rx --rx-loader-vp` | 类似于 `--rx`,但你的 loader 将 VirtualProtect 作为 blob 的第一个参数传入。不需要 PEB walker。 |
每种模式都支持 `--seed`、`--arch`、`--input-format` 和 `--format`。有关构建流水线和运行时流程,请参阅下面的各模式章节。
## Default 虚拟化
提升器遍历整个 CFG,并将每条指令 lowered 到自定义 IR。IR 经过两次混淆 pass,然后通过 codecs 将每条 insn 编码为基于随机种子的 bytecode 结构。block table、handler table 和 data island 使用与 bytecode 相同的逐字节流密码进行加密。在运行时,prologue 会就地解密这三个区域,分发器循环则逐个获取 bytecode 字节,进行解密并分发给执行实际工作的 handler。
### 构建流水线
构建过程端到端将原始 shellcode 转换为输出的虚拟化 blob 所执行的步骤。_下方的所有图示适用于 1.0.0 版本,此后已发生改变,但核心思想是一致的。_
```
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]
```
### 运行时流程
当宿主 loader 在字节 0 处调用输出的 blob 时,从 prologue 到分发器获取以及各种终止路径,该 blob 所执行的操作。
```
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]
```
运行时的 nonce 技巧是关键的防内存扫描手段:handler table 在内存中通过与 `rdtsc XOR cipher_init` 派生的值进行 XOR 操作,因此加载相同 blob 的两个进程持有的是字节完全不同的 handler table。分发器在查找时通过一次额外的 XOR 进行解掩码。
## Packer 模式: `--pack`
完全相反的权衡。提升器不运行。原始 shellcode 以加密形式进入 data island,而 IR 是一个单一的合成 `JMP_NATIVE imm=0`。prologue 有意跳过了 Default 模式中急切运行的 data island 解密。相反,当唯一的 JMP_NATIVE handler 首次触发时,它会就地解密 data island,设置一个标记字节,然后将控制权转移到现在是明文的 shellcode 的字节 0。shellcode 从那里开始以原生方式运行。
适用于任何 shellcode,无论提升器覆盖范围如何。适用于无阶段的 cobalt、大量使用 exotic syscall 的 payload,以及任何太大或太怪异而无法虚拟化的内容。失去了每条指令的虚拟化防御,但在封装器上保留了完整的基于随机种子的 VM 多态性。
### 构建流水线
`--pack` 如何将原始 shellcode 作为加密的 data island 封装在单指令合成 IR 程序之后。
```
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]
```
### 运行时流程
pack 封装器在首次进入时的操作,包括受控的 data island 延迟解密,以及将控制权交给现在已是明文的 shellcode 的单一合成 JMP_NATIVE。
```
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]
```
延迟的 data island 解密是 pack 模式独有的。在 Default 和 Hybrid 模式下,提升后的代码会在执行期间对 data island 字节进行 VM LOAD/STORE 操作,并从一开始就需要它们是明文,因此 prologue 会急切地处理它。在 pack 模式下,data island 只有一个消费者,即合成 JMP_NATIVE 之后的原生转义,因此解密可以一直等到那时再进行。
## Hybrid 模式: `--ranges A:B,C:D`
定向虚拟化。在输入中选择需要提升的字节范围。其余部分在输出中保持为原始的原生字节。在每个范围的起始处,提升器修补一个 5 字节的 `jmp rel32`,指向附加在原生 shellcode 区域之后的 `vm_entry_K` stub。外部原生代码只能通过修补过的起始字节重新进入提升的范围。范围中间的字节用 int3 填充,因此在扫描时,任何指向范围中间字节的原生分支都会被拒绝。退出范围指向范围外字节的形成提升代码将变为 `JMP_NATIVE` 或 `CALL_NATIVE`。
首先使用 `--scan` 查找符合条件的候选项。Scan 将带有间隙、没有以 `ret` 结尾的块、主体短于 5 字节或具有指向范围中间字节的外部原生分支的范围归类为 near-miss 而不是 eligible。
### 构建流水线
`--ranges` 如何仅提升选定的字节范围并就地修补原生 shellcode,以便将控制重定向到附加的 VM entry stubs。
```
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]
```
### 运行时流程
通过混合了原生与 VM 的 blob 的控制流,包括当原生代码遇到修补过的范围起始处时进入 VM 分发,以及当提升后的代码分支退回时使用的原生转义路径。
```
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
```
协程风格的范围,即没有 `ret` 并通过尾跳转退出的范围,通过针对 `--scan` 输出的 `--coroutines` 进行记录。该标志目前不会改变代码生成。范围模式最适合自包含的叶子函数。依赖于特定调用者提供的寄存器状态的辅助函数无法独立提升;API 最终会在被调用时传入垃圾参数。
## Stacked 模式: `--pack --ranges A:B,C:D`
运行 hybrid 构建,然后对结果进行 pack 封装。外部 VM 就地解密内部的 hybrid blob,并跳转到其字节 0。从那里开始,执行流程与独立的 hybrid 模式完全一样,除了整个 blob(包括选定范围的 bytecode)在静态下都是加密的。这两层清晰地组合在一起,因为内部的 blob 是一个自包含的 PIC 区域。
### 构建流水线
Stacked 模式如何递归调用打包器:首先进行内部的 `--ranges` 构建,然后对该构建进行外部的 `--pack` 封装。
```
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]
```
### 运行时流程
外部的 pack VM 如何将控制权交给内部的范围模式 VM,每个 VM 都在其自己独立的 VMState frame 上运行。
```
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
```
内部 VM 和外部 VM 不共享任何状态。它们是恰好存在于同一个 blob 中的两个独立的 VM。外部 VM 的唯一工作就是将内部 VM 置于解密层之后。
## Detour 模式: `--embed-into target.exe --at RVA`
与其他模式形状不同。输入是原始 shellcode,通常是已经由 mkPIVM 在另一种模式下输出的 VM blob,尽管任何 PIC 字节都可以工作。输出是修补过的 PE。
该工具解析 `target.exe`,在可执行部分中定位选定的 RVA,在那里反汇编足够的指令以覆盖 5 个字节,如果被移走的运行序列包含无法在移动后存活的 RIP 相对寻址或相对控制流,则拒绝执行。然后添加一个全新的 RWX 节,其中包含封装器以及 VM blob。封装器保留调用者状态,转移到 VM blob,恢复状态,执行被移走的原始字节,然后跳转回修补之后的字节。在选定的 RVA 处的 5 字节 `jmp rel32` 指向该封装器。
封装器如何转移到 VM 有两种子模式:
### Threaded 子模式,默认
下面是两张图。第一张是构建时的 PE 修补流水线,它注入封装器节、修复引用,并在选定的 RVA 处写入 5 字节的 jmp。第二张是当宿主进程最终到达该 RVA 时的运行时控制流。
```
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]
```
```
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]
```
对于 stager 和 beacon 来说,Threaded 子模式是合适的形状。宿主主线程永远不会阻塞。工作线程继承 payload 所需的任何生命周期。如果 payload 调用 `ExitProcess`,整个进程就会死亡,但对于像每个 C2 beacon 这样的非终止 payload,宿主将与其并行永远运行。
### Inline 子模式: `--detour-inline`
相同的封装器结构,但封装器直接执行 `call vm_blob` 而不是 `CreateThread`。宿主主线程阻塞,直到 VM 返回。在目标文件的 IAT 中缺少 `CreateThread` 时,或者在由于特定目标导致 threaded 的基址重定位路径无法应用作为后备方案时非常有用。
```
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 模式: `--scan`
无输出文件。从输入的 shellcode 构建 CFG,并将 `--ranges` 候选项打印到 stderr。每个候选项都被归类为 eligible、coroutine、near-miss 或 internal,其中 internal 意味着被一个已经覆盖了它的更大的 eligible 候选项所遮蔽。在以 hybrid 模式再次调用该工具之前,使用此选项来选择范围参数。
```
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]
```
## 每个随机种子的多态性维度
按它们对静态特征的影响程度粗略列出。
* 加密家族。ARXLcgSub、SBoxAdd、FeistelByte 之一。同时选择构建时使用的 bytecode 加密和分发器获取路径中发出的内联解密。加密和解密在构造上是匹配的。
* 寄存器槽布局。VMState 包含 `reg_count` 个槽,每个种子的大小从 24 到 32 不等。16 个架构 GPR 以及 4 个 Tmp 寄存器在每个种子上都会获得一个新的槽索引排列。
* 操作码到 handler 的映射。每个 codec 系列在每个种子中被分配一个随机的 opcode 字节。256 条目的 handler table 按 opcode 索引;编码后的 bytecode 引用映射后的 opcode。
* 分发器拓扑。Threaded 或 central,按种子选择。
* Prologue 自定位策略。`call $+5; pop`、`lea reg, [rip+0]` 或 jmp/call 混洗。
* Handler 寄存器临时变量。BR_CC handler 在 volatile 池中置换其 6 个临时角色。Store handler 执行相同的操作。
* 垃圾 gadget 密度。0 到 3,控制 handler 之间的垃圾发射。
* IR 混淆 pass 决策。无效 IR 注入和不透明谓词各自使用自己的子 RNG,因此它们的决策在每个种子中是确定性的,而不会干扰其他维度。
* Bytecode 加密初始状态。每个种子随机生成 64 位。
## 测试过的 payload
该工具已针对多个 Cobalt Strike、MSF、Sliver 以及大量其他 shellcode 样本进行了验证。
## 构建
需要 Visual Studio 2022、CMake 3.21 或更高版本以及 vcpkg。CMake 项目通过 vcpkg 的 manifest 模式拉取 Zydis 和 fmt。
```
cmake -S . -B build
cmake --build build --config Release --target mkpivm
```
## 已知限制
* Cobalt stager 的 Range 模式不能作为独立的叶子函数运行。stager 的辅助函数依赖于运行器未提供的调用者寄存器状态。完全虚拟化或 `--pack` 是真正能 beacon 的途径。
* x86 threaded detour 要求目标要么缺少 ASLR,要么接受添加的基址重定位条目(工具在存在时会输出这些条目)。如果目标的 BASERELOC 数据目录格式错误或缺失,该工具将回退到 inline 模式。
* 提升器目前不支持 SSE/AVX 寄存器移动、atomics、CMPXCHG、RDMSR 或特权指令。对于使用这些指令的 shellcode,Packer 模式是一种解决方法。
* `--embed-into` 输出上的 Authenticode 签名将失效。PE 校验和被清零。
* 输出的 blob 在运行时需要 RWX,因为就地解密会写回 blob 自身的页面。大多数运行 shellcode 的加载器无论如何都会分配 RWX。目前不打算转向仅支持 RX,因为这种权衡虽然获得了 RX,但代价是需要进行 PEB walk 和 `VirtualAlloc` 调用,这可能会比它替换的 RWX 页面留下更糟糕的特征。
* 目前尚不支持通过 Default 模式虚拟化无阶段的 payload,因为将它们包装在 VM 中极其复杂;建议使用 `--pack + --ranges`。
# 备注
* 该项目在很大程度上是概念验证研究。如果反响良好,我将根据要求对其进行扩展并欢迎贡献。然而,它在测试样本中表现稳定。
* 如果你的 shellcode 不起作用,而你又不想在 Issue 中提出,那很遗憾我帮不了你。这已经在 Sliver、Cobalt Strike 4.12、MSF、Havoc 以及其他几个未公开的样本上进行了测试。
* 在我的测试中,将 VM 注入到活动进程中工作正常。然而,在嵌入到 PE 方面,这并没有使用 MS Word 等商业软件进行过测试,仅仅是合成测试,但很可能是可行的。如果不行,我会修复。
* 截至 2026/20/05(现在似乎已经结束),该仓库似乎遭到了大量机器人的围攻。所以,这真是太棒了。请忽略所有空白的 Github 账号。
* 我看到了一些关于 mkPIVM 的 AI 生成的帖子,说它的缺点是:
* 1). 不支持 Linux,这很公平,我很快就会加上。
* 2). 没有 GUI?搞什么?
* 我不知道,我觉得这挺搞笑的。
# 贡献
这玩意儿酷毙了,说真的。如果你想贡献新想法、修复你自己的 bug、提交 Issue 让我来修复等等,放手去做吧。
标签:Bash脚本, C++, DNS 反向解析, Shellcode, 代码混淆, 技术调研, 数据擦除, 虚拟机保护