
Générez des machines virtuelles polymorphes et indépendantes de la position (PIVM) à partir d'un shellcode x86/x64 arbitraire.
Lire l'article de recherche (écrit pour 1.0.0).
mkPIVM est un virtualiseur de shellcode polymorphe indépendant de la position pour Windows x86 et x64 (Linux bientôt).
Fournissez-lui du shellcode brut. Il émet un autre blob brut : une petite machine virtuelle qui interprète une version surélevée et chiffrée au repos de vos instructions d'origine. La sortie est elle-même du code indépendant de la position et s'exécute partout où le shellcode d'origine le ferait, d'un chargeur de thread distant à un détour de code cave. Chaque paramètre par graine varie indépendamment : famille de chiffrement, disposition des emplacements de registres, permutation opcode-vers-gestionnaire, topologie du répartiteur, motif de gadgets de remplissage, points d'insertion d'obfuscation RI. Deux constructions à partir de la même entrée partagent moins d'une centaine d'octets fortuits sur des dizaines de kilo-octets.
Pourquoi : le shellcode natif est trivial à signer. L'envelopper dans une VM par instance avec un chiffrement par instance ne laisse rien d'utile au repos, et élever les instructions en bytecode met un autre mur entre les octets du disque et tout désassembleur qui connaît l'apparence de x86. Pour autant que je puisse en juger par un balayage de la littérature, aucun outil public ne fournit exactement ce pipeline : PIC brut en entrée, PIC de VM polymorphe brute en sortie. Donc, je l'ai mentionné dans l'article de recherche qu'il exigeait. Pour être honnête, si j'ai raison sur le fait que personne n'a fait cela (publiquement) auparavant, et j'en suis assez confiant, je suis surpris. Néanmoins, profitez-en.
mkpivm.exe shellcode.bin --arch x64 -o out.bin
Votre PIVM est chaud et prêt. C'est le chemin le plus simple. Plusieurs autres modes varient selon l'agressivité avec laquelle les instructions originales sont virtualisées, selon que la sortie est un blob autonome ou un PE patché, et selon que le lift s'exécute ou non.
# Démonstration
J'ai les preuves. Vous pouvez voir ci-dessous une vidéo de mkPIVM en action, virtualisant entièrement un stager Meterpreter (vanilla, soit dit en passant), s'injectant dans explorer.exe, et nous capturant un callback. Bien sûr, ce n'est qu'un exemple, et mkPIVM peut être appliqué à bien plus, à condition que les instructions du shellcode soient prises en charge. Si ce n'est pas le cas, ouvrez une Issue, envoyez-moi le shellcode, je m'en occupe.
Voyez-le [ici](https://github.com/D7EAD/mkPIVM/raw/refs/heads/main/media/mkpivm-showcase.mp4). Hébergé dans ./media, impossible à intégrer malheureusement.
Voici le rapport VirusTotal pour cet échantillon virtualisé exact (au 06/04/2026).
<img src="https://assets.kitploit.com/production/public/readmes/7466/3f4a6dbe2777c02fb32f71749a5b1fa2aa324cb0dac2f77d5681bbe3a4e17a81.png">
...et la version packée, même pas virtualisée, avec une entropie nettement plus élevée.
<img src="https://assets.kitploit.com/production/public/readmes/7466/47f68372e31a2c24fa49eb71648adbdf8944977db90d645df337125c1bdfa8e3.png">
Voici les résultats d'un beacon Cobalt Strike normal pour comparaison.
<img src="https://assets.kitploit.com/production/public/readmes/7466/76e2107589cd2b1b65c846a671aed173aa5e6d89630f5e9d283d99c546a2d566.png">
Une attention particulière a été portée à la télémétrie d'entropie de la sortie de cet outil, ce qui donne un shellcode d'entropie inférieure aux DLL Windows WinAPI typiques (hors mode d'empaquetage), telles que ntdll.dll ou kernel32.dll. Voici la comparaison d'entropie...
| Fichier | Octets | Entropie |
|------|-------|---------|
| `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 en un coup d'œil
| Mode | Options | Changements |
|------|-------|--------------|
| Default | aucun | Lift toute l'entrée. Tout est virtualisé. |
| Packer | `--pack` | Pas de lift. Enveloppe l'entrée en données chiffrées, déchiffre à l'exécution, puis saute dedans. |
| Hybrid | `--ranges A:B,...` | Lift uniquement les plages d'octets choisies. Le reste reste natif. |
| Stacked | `--pack --ranges A:B` | Construit le blob hybride, puis l'encapsule. |
| Detour | `--embed-into PE --at RVA` | Prend un blob pré-construit, l'intègre dans un PE, patche un jmp à la RVA choisie. |
| Scan | `--scan` | Affiche les candidats `--ranges` éligibles depuis le CFG d'entrée, puis se termine. |
| RX | `--rx` | Blob PAGE_EXECUTE_READ. L'île de données reste chiffrée au repos ; le walker PEB dans le blob résout VirtualProtect, déchiffre sur place à state_init. |
| RX w/ Loader | `--rx --rx-loader-vp` | Comme `--rx` mais votre loader passe VirtualProtect en tant que premier argument du blob. Pas de walker PEB. |
Chaque mode respecte `--seed`, `--arch`, `--input-format`, et `--format`. Voir les sections par mode ci-dessous pour le pipeline de construction et le flux d'exécution.
## Virtualisation par défaut
Le lifter parcourt tout le CFG et convertit chaque instruction en un IR personnalisé. L'IR passe par deux passes d'obfuscation, puis par des codecs qui encodent chaque insn dans la forme de bytecode propre à chaque seed. La table de blocs, la table des handlers et l'île de données sont chiffrées avec le même chiffrement de flux par octet que le bytecode. À l'exécution, le prologue déchiffre ces trois régions sur place et la boucle du dispatcher récupère les octets du bytecode un par un, les déchiffre et les envoie à un handler qui fait le travail.
### Pipeline de construction
Étapes que la construction effectue pour transformer le shellcode brut en blob virtualisé émis, de bout en bout. _Tous les graphiques ci-dessous s'appliquent à la version 1.0.0, ils ont changé depuis, mais l'idée est la même._```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]