
Genera máquinas virtuales polimórficas e independientes de posición (PIVMs) a partir de shellcode x86/x64 arbitrario.
Leer el artículo de investigación (escrito para la versión 1.0.0).
mkPIVM es un virtualizador de shellcode polimórfico e independiente de posición para Windows x86 y x64 (Linux próximamente).
Pásale shellcode en bruto. Emite otro blob en bruto: una pequeña máquina virtual que interpreta una versión elevada y cifrada en reposo de tus instrucciones originales. La salida es en sí misma código independiente de posición y se ejecuta en cualquier lugar donde lo haría el shellcode original, desde un cargador de hilo remoto hasta un desvío de code cave. Cada parámetro por semilla varía de forma independiente: familia de cifrado, disposición de ranuras de registro, permutación opcode-manejador, topología del despachador, patrón de gadgets basura, puntos de inserción de ofuscación IR. Dos compilaciones de la misma entrada comparten menos de un centenar de bytes coincidentes de entre decenas de kilobytes.
Por qué: el shellcode nativo es trivial de detectar por firmas. Envolverlo en una VM por instancia con un cifrado por instancia no deja nada útil en reposo, y elevar las instrucciones a bytecode pone otro muro entre los bytes del disco y cualquier desensamblador que sepa cómo se ve x86. Hasta donde puedo decir tras un barrido de literatura, ninguna herramienta pública ofrece exactamente este pipeline: PIC crudo de entrada, PIC de VM polimórfica crudo de salida. Así que lo mencioné en el artículo de investigación que lo exigía. Para ser honesto, si estoy en lo cierto acerca de que nadie ha hecho esto (públicamente) antes, y estoy bastante seguro, me sorprende. No obstante, disfruta.
mkpivm.exe shellcode.bin --arch x64 -o out.bin
Tu PIVM está lista y funcionando. Ese es el camino más sencillo. Varios otros modos varían la agresividad con la que se virtualizan las instrucciones originales, si la salida es un blob independiente o un PE parcheado, y si el lift se ejecuta o no.
# Showcase
Tengo los recibos. Puedes ver un vídeo de mkPIVM en acción a continuación, virtualizando por completo un stager de Meterpreter (vanilla, por cierto), inyectándose en explorer.exe, y nosotros capturando un callback. Por supuesto, esto es solo un ejemplo, y mkPIVM se puede aplicar a mucho más, siempre que las instrucciones del shellcode sean compatibles. Si no lo son, abre un Issue, envíame el shellcode, yo te cubro.
Míralo [aquí](https://github.com/D7EAD/mkPIVM/raw/refs/heads/main/media/mkpivm-showcase.mp4). Alojado en ./media, lamentablemente no se puede incrustar.
Este es el informe de VirusTotal para esa muestra virtualizada exacta (a fecha de 06/04/2026).
<img src="https://assets.kitploit.com/production/public/readmes/7466/3f4a6dbe2777c02fb32f71749a5b1fa2aa324cb0dac2f77d5681bbe3a4e17a81.png">
...y la versión empaquetada, ni siquiera virtualizada, con una entropía notablemente mayor.
<img src="https://assets.kitploit.com/production/public/readmes/7466/47f68372e31a2c24fa49eb71648adbdf8944977db90d645df337125c1bdfa8e3.png">
Aquí están los resultados de un beacon normal de Cobalt Strike para comparar.
<img src="https://assets.kitploit.com/production/public/readmes/7466/76e2107589cd2b1b65c846a671aed173aa5e6d89630f5e9d283d99c546a2d566.png">
Se prestó especial atención a la telemetría de entropía de la salida de esta herramienta, lo que da como resultado un shellcode con una entropía menor que la de las DLL WinAPI típicas de Windows (fuera del modo de empaquetado), como ntdll.dll o kernel32.dll. La comparación de entropía es aproximadamente...
| Archivo | Bytes | Entropía |
|------|-------|---------|
| `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** |
## Modos de un vistazo
| Modo | Flags | Qué cambia |
|------|-------|--------------|
| Default | ninguno | Levanta toda la entrada. Todo virtualizado. |
| Packer | `--pack` | No levantar. Envuelve la entrada como datos cifrados, los descifra en tiempo de ejecución y salta dentro. |
| Hybrid | `--ranges A:B,...` | Levanta solo los rangos de bytes elegidos. El resto permanece nativo. |
| Stacked | `--pack --ranges A:B` | Construye el blob híbrido y luego lo envuelve con pack. |
| Detour | `--embed-into PE --at RVA` | Toma un blob preconstruido, lo incrusta en un PE y parchea un jmp en la RVA elegida. |
| Scan | `--scan` | Imprime los candidatos elegibles de `--ranges` desde el CFG de la entrada y luego sale. |
| RX | `--rx` | Blob PAGE_EXECUTE_READ. La isla de datos permanece cifrada en reposo; el walker de PEB dentro del blob resuelve VirtualProtect y descifra en el lugar en state_init. |
| RX w/ Loader | `--rx --rx-loader-vp` | Como `--rx`, pero tu loader pasa VirtualProtect como primer argumento del blob. Sin walker de PEB. |
Cada modo respeta `--seed`, `--arch`, `--input-format` y `--format`. Consulta las secciones por modo a continuación para ver el pipeline de construcción y el flujo en tiempo de ejecución.
## Virtualización por defecto
El lifter recorre todo el CFG y reduce cada instrucción a un IR personalizado. El IR pasa por dos pasadas de ofuscación y luego por codecs que codifican cada insn en la forma de bytecode específica de la semilla. La tabla de bloques, la tabla de handlers y la isla de datos se cifran con el mismo cifrado de flujo por byte que el bytecode. En tiempo de ejecución, el prólogo descifra esas tres regiones en el lugar y el bucle del dispatcher obtiene los bytes del bytecode de uno en uno, descifrándolos y despachándolos a un handler que hace el trabajo.
### Pipeline de construcción
Pasos que realiza la construcción para convertir el shellcode sin procesar en el blob virtualizado emitido, de principio a fin. _Todos los gráficos a continuación se aplican a la versión 1.0.0; estos han cambiado desde entonces, pero la idea es la misma._```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]