Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
mkPIVM — Genera máquinas virtuales polimórficas e independientes de posición (PIVMs) a partir de shellcode x86/x64 arbitrario. | Kitploit
Herramientas/GitHubGitHub/d7ead/mkpivm
Generación de PayloadsExplotaciónIngeniería InversaShellcodeAnálisis de MalwareRed Teaming
GitHubd7ead/mkpivm

mkPIVM

Genera máquinas virtuales polimórficas e independientes de posición (PIVMs) a partir de shellcode x86/x64 arbitrario.

Ver Repositorio
40917hace 6 díasRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir


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.

Trabajo relacionado y planes

  • El soporte para Linux se añadirá próximamente.

Inicio rápido```

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

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

Flujo de ejecución

Lo que hace el blob generado cuando el cargador del host lo invoca en el byte 0, desde el prólogo hasta las captaciones del despachador y las distintas rutas de terminación.```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:~
El truco del nonce en tiempo de ejecución es la clave de la jugada anti-escaneo de memoria: la tabla de manejadores reside en memoria sometida a XOR con un valor derivado de `rdtsc XOR cipher_init`, por lo que dos procesos que cargan el mismo blob tienen tablas de manejadores con bytes distintos. El despachador la desenmascara en el momento de la búsqueda con un XOR adicional.

## Modo empaquetado: `--pack`

La compensación inversa. El lifter no se ejecuta. El shellcode original se guarda en la isla de datos cifrado, y el IR es un único `JMP_NATIVE imm=0` sintético. El prólogo omite intencionadamente el descifrado de la isla de datos que el modo predeterminado ejecuta de forma anticipada. En su lugar, la primera vez que se dispara el único manejador de JMP_NATIVE, descifra la isla de datos en su lugar, establece un byte marcador y luego transfiere el control al byte 0 del shellcode ya en texto plano. El shellcode se ejecuta de forma nativa a partir de ahí.

Funciona con cualquier shellcode, independientemente de la cobertura del lifter. Útil para Cobalt stageless, payloads exóticos con muchas syscalls, cualquier cosa demasiado grande o extraña para virtualizar. Pierde la defensa de virtualización por instrucción, pero conserva el polimorfismo completo de VM por semilla en el envoltorio.

### Pipeline de compilación

Cómo `--pack` envuelve el shellcode original como una isla de datos cifrada detrás de un programa IR sintético de una sola instrucción.```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]

Flujo de ejecución

Lo que hace el pack wrapper en la primera entrada, incluido el descifrado diferido controlado de la data island y el único JMP_NATIVE sintético que cede el control al shellcode ahora en texto claro.```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:~
El descifrado diferido de la isla de datos solo está disponible en modo pack. En el modo predeterminado y en el modo híbrido, el código elevado ejecuta VM LOAD/STORE sobre los bytes de la isla de datos a mitad de ejecución y los necesita en texto plano desde el inicio, por lo que el prólogo lo maneja de forma inmediata. En modo pack hay exactamente un consumidor de la isla de datos, la salida nativa después del `JMP_NATIVE` sintético, por lo que el descifrado puede esperar hasta ese momento.

## Modo híbrido: `--ranges A:B,C:D`

Virtualización dirigida. Selecciona rangos de bytes en la entrada que deben elevarse. Todo lo demás permanece como los bytes nativos originales en la salida. Al inicio de cada rango, el elevador parchea un `jmp rel32` de 5 bytes hacia un stub `vm_entry_K` añadido después de la región nativa del shellcode. El código nativo externo solo puede volver a entrar en un rango elevado a través del byte inicial parcheado. Los bytes intermedios del rango se rellenan con int3, por lo que cualquier rama nativa que tenga como destino un byte intermedio del rango se rechaza en el momento del escaneo. El código elevado que sale de un rango hacia bytes fuera de él se convierte en `JMP_NATIVE` o `CALL_NATIVE`.

Usa `--scan` primero para encontrar candidatos elegibles. El escaneo clasifica como casi coincidencia, en lugar de elegible, los rangos con huecos, sin bloque terminado en `ret`, con cuerpo más corto de 5 bytes, o con ramas nativas externas hacia bytes intermedios del rango.

### Canalización de construcción

Cómo `--ranges` eleva solo los rangos de bytes elegidos y parchea el shellcode nativo en su lugar para que el control se redirija hacia los stubs de entrada de VM añadidos.```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]

Flujo de ejecución

Flujo de control a través de un blob mixto nativo y de VM, incluyendo la entrada al despacho de VM cuando el código nativo alcanza el inicio de un rango parcheado y las rutas de escape nativas utilizadas cuando el código elevado se bifurca de vuelta hacia afuera.```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:~
Los rangos de estilo corrutina, los que no tienen `ret` y salen mediante un tail-jmp, se documentan mediante `--coroutines` para la salida de `--scan`. El indicador no cambia la generación de código en la actualidad. El modo de rangos es el más adecuado para funciones hoja autocontenidas. Las funciones auxiliares que dependen de un estado de registros específico proporcionado por el llamador no pueden extraerse de forma independiente; la API acaba llamándose con argumentos basura.

## Modo apilado: `--pack --ranges A:B:C,D`

Ejecuta la compilación híbrida y luego envuelve el resultado con pack. La VM externa descifra el blob híbrido interno en su sitio y salta al byte 0 del mismo. A partir de ahí, la ejecución continúa exactamente igual que en el modo híbrido autónomo, excepto que todo el blob, incluido el bytecode del rango elegido, está cifrado en reposo. Las dos capas se combinan limpiamente porque el blob interno es una región PIC autocontenida.

### Canalización de compilación

Así es como el modo apilado invoca recursivamente al empaquetador: primero una compilación interna con `--ranges` y luego una envoltura externa con `--pack` de esa compilación.```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]

Flujo de ejecución

Cómo la VM del paquete externo transfiere el control a la VM interna en modo rango, cada una ejecutándose en su propio marco VMState independiente.```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:~
La VM interna y la VM externa no comparten estado. Son dos VMs independientes que casualmente viven en el mismo blob. El único trabajo de la externa es poner la interna detrás de una capa de descifrado.

## Detour mode: `--embed-into target.exe --at RVA`

Forma distinta de las demás. La entrada es shellcode crudo, normalmente un blob de VM ya generado por mkPIVM en otro modo, aunque cualquier byte PIC funcionará. La salida es un PE parcheado.

La herramienta analiza `target.exe`, localiza el RVA elegido en una sección ejecutable, desensambla allí las instrucciones suficientes para cubrir 5 bytes, rechaza si el tramo desplazado contiene direccionamiento relativo a RIP o flujo de control relativo que no pueda sobrevivir al ser movido, y luego añade una nueva sección RWX que contiene un wrapper además del blob de VM. El wrapper preserva el estado del llamador, transfiere el control al blob de VM, restaura el estado, ejecuta los bytes originales desplazados y salta de vuelta al byte posterior al parche. Un `jmp rel32` de 5 bytes en el RVA elegido apunta al wrapper.

Dos submodos para la manera en que el wrapper transfiere el control al VM:

### Submodo enhebrado, el predeterminado

A continuación se presentan dos diagramas. El primero es la canalización de parcheo del PE en tiempo de compilación que inyecta la sección del wrapper, corrige las referencias y escribe el jmp de 5 bytes en el RVA elegido. El segundo es el flujo de control en tiempo de ejecución cuando el proceso anfitrión alcanza finalmente ese 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]

Las herramientas incluyen:

  • Forense de Memoria
  • Forense de Red
  • Forense de Disco
  • Análisis de Registros
  • Análisis de Archivos
  • Forense de Correo Electrónico
  • Forense de Bases de Datos
  • Forense de Navegadores
  • Forense Móvil
  • Forense en Vivo
  • Forense de Malware
  • Forense en la Nube
  • Forense de IoT
  • Herramientas de Recuperación de Datos
  • Herramientas de Análisis
  • Herramientas de Cronología
  • Herramientas de Reporte```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:~
El submodo con hilos es la forma adecuada para stagers y beacons. El hilo principal del host nunca se bloquea. El hilo de trabajo hereda el ciclo de vida que el payload necesite. Si el payload llama a `ExitProcess`, todo el proceso muere, pero para un payload que no termina, como cualquier beacon de C2, el host se ejecuta para siempre en paralelo con él.

### Submodo en línea: `--detour-inline`

La misma estructura de wrapper, pero el wrapper hace una llamada directa a `call vm_blob` en lugar de `CreateThread`. El hilo principal del host se bloquea hasta que la VM retorna. Útil cuando el objetivo carece de `CreateThread` en su IAT, o como respaldo cuando la ruta de reubicación base con hilos no puede aplicarse contra un objetivo concreto.```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]

Modo de escaneo: --scan

Sin archivo de salida. Construye el CFG a partir del shellcode de entrada e imprime los candidatos de --ranges a stderr. Cada candidato se clasifica como elegible, corrutina, casi-acierto o interno, donde interno significa que está sombreado por un candidato elegible más grande que ya lo cubre. Usa esto para elegir los argumentos de rango antes de invocar la herramienta nuevamente en modo híbrido.```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:~
## Ejes de polimorfismo por semilla

Enumerados aproximadamente en orden de cuánto afectan a la firma estática.

* Familia de cifrado. Una de ARX, LcgSub, SBoxAdd, FeistelByte. Elige tanto el cifrado del bytecode usado en tiempo de compilación como el descifrado en línea emitido en la ruta de obtención del dispatcher. El cifrado y el descifrado coinciden por construcción.
* Distribución de ranuras de registros. El VMState contiene ranuras `reg_count`, con un tamaño de 24 a 32 por semilla. Los 16 GPR arquitectónicos más los 4 registros Tmp obtienen una nueva permutación de índices de ranura en cada semilla.
* Mapeo de opcode a manejador. A cada familia de codec se le asigna un byte de opcode aleatorio por semilla. La tabla de manejadores de 256 entradas se indexa por opcode; el bytecode codificado hace referencia a los opcodes mapeados.
* Topología del dispatcher. Enhebrado o central, elegida por semilla.
* Estrategia de autoubicación del prólogo. `call $+5; pop`, `lea reg, [rip+0]` o una mezcla de jmp/call.
* Temporales de registro del manejador. El manejador BR_CC permuta sus 6 roles temporales entre el grupo de registros volátiles. El manejador Store hace lo mismo.
* Densidad de gadgets basura. De 0 a 3, controla la emisión de basura entre manejadores.
* Decisiones del paso de ofuscación de IR. La inyección de IR muerto y los predicados opacos utilizan cada uno su propio sub-RNG, de modo que sus decisiones son deterministas por semilla sin perturbar otros ejes.
* Estado inicial del cifrado del bytecode. 64 bits aleatorios por semilla.

## Payloads probados

Esta herramienta fue validada contra varias muestras de shellcode de Cobalt Strike, MSF, Sliver y numerosas otras.

## Compilación

Requiere Visual Studio 2022, CMake 3.21 o superior y vcpkg. El proyecto CMake obtiene Zydis y fmt a través del modo manifiesto de vcpkg.```
cmake -S . -B build
cmake --build build --config Release --target mkpivm

Known limits

  • El modo de rangos para stagers de Cobalt no funciona como una hoja independiente. Las funciones auxiliares del stager dependen del estado de registros proporcionado por el llamador que el runner no proporciona. La virtualización completa o --pack son las rutas que realmente emiten beacon.
  • El x86 threaded detour requiere que el objetivo carezca de ASLR o acepte entradas de reubicación base añadidas, que la herramienta emite cuando están presentes. Si el directorio de datos BASERELOC del objetivo está malformado o ausente, la herramienta recurre al modo inline.
  • El lifter actualmente no cubre movimientos de registros SSE/AVX, operaciones atómicas, CMPXCHG, RDMSR ni instrucciones privilegiadas. El modo pack es la solución para los shellcodes que las utilizan.
  • Las firmas Authenticode en la salida de --embed-into quedan invalidadas. El checksum del PE se pone a cero.
  • Los blobs de salida requieren RWX en tiempo de ejecución porque el descifrado in situ escribe de vuelta en las propias páginas del blob. La mayoría de los loaders que ejecutan shellcode asignan RWX de todos modos. No hay planes de pasar a solo-RX porque la compensación compra RX a costa de un recorrido del PEB y una llamada a VirtualAlloc, lo que probablemente sea una firma peor que la página RWX que reemplaza.
  • La virtualización de payloads sin estado (stageless) mediante el modo predeterminado aún no es compatible, ya que son horriblemente complicados de envolver en la VM; prefiere --pack + --ranges.

Notas

  • Este proyecto es en gran parte investigación de prueba de concepto. Si tiene buena acogida, lo ampliaré según lo solicitado y agradeceré las contribuciones. Sin embargo, parecía estable para las muestras probadas.
  • Si tu shellcode no funciona y no quieres publicarlo en un Issue, entonces lamentablemente no puedo ayudarte. Esto se probó en Sliver, Cobalt Strike 4.12, MSF, Havoc y algunas otras muestras no reveladas.
  • En mis pruebas, la inyección de la VM en procesos en vivo funcionó bien. Sin embargo, en cuanto a la incrustación en PE, esto no se probó con software comercial como MS Word, solo con pruebas sintéticas, pero probablemente funcione. Si no, lo arreglaré.
  • A fecha de 5/20/2026 (parece que ya ha terminado), parece que el repositorio estaba siendo invadido por bots. Así que, es maravilloso. Por favor, ignora todas las cuentas de Github en blanco.
  • Vi una publicación generada por IA sobre mkPIVM, que decía que sus desventajas eran:
    • 1). No tener soporte para Linux, lo cual es justo, y lo añadiré pronto.
    • 2). ¿No tener GUI? ¿Qué?
    • No sé, me pareció gracioso.

Contribuciones

  • Esto es algo genial, en serio. Si quieres contribuir con nuevas ideas, corregir tus propios errores, enviar Issues para que yo los arregle, y así sucesivamente, adelante.
Descargar herramienta