Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
mkPIVM — Générez des machines virtuelles polymorphes et indépendantes de la position (PIVM) à partir d'un shellcode x86/x64 arbitraire. | Kitploit
Outils/GitHubGitHub/d7ead/mkpivm
Génération de PayloadsExploitationRétro-ingénierieShellcodeAnalyse de MalwareRed Teaming
GitHubd7ead/mkpivm

mkPIVM

Générez des machines virtuelles polymorphes et indépendantes de la position (PIVM) à partir d'un shellcode x86/x64 arbitraire.

Voir le dépôt
40917il y a 6 joursVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager


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.

Travaux connexes et plans

  • La prise en charge de Linux sera bientôt ajoutée.

Démarrage rapide```

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

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

Flux d'exécution

Ce que fait le blob émis lorsque le chargeur hôte l'appelle à l'octet 0, du prologue aux récupérations du dispatcher et aux différents chemins de terminaison.```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:~
Le truc du nonce à l'exécution est la parade clé contre l'analyse mémoire : la table des handlers réside en mémoire XORée avec une valeur dérivée de `rdtsc XOR cipher_init`, de sorte que deux processus chargeant le même blob contiennent des tables de handlers différentes octet par octet. Le dispatcher la démasque au moment de la consultation avec un XOR supplémentaire.

## Mode packer : `--pack`

Le compromis inverse. Le lifter ne s'exécute pas. Le shellcode d'origine est placé chiffré dans l'îlot de données, et l'IR est un unique `JMP_NATIVE imm=0` synthétique. Le prologue ignore délibérément le déchiffrement de l'îlot de données que le mode par défaut exécute de manière anticipée. À la place, la première fois que le handler du JMP_NATIVE isolé se déclenche, il déchiffre l'îlot de données sur place, définit un octet marqueur, puis transfère le contrôle à l'octet 0 du shellcode désormais en clair. Le shellcode s'exécute ensuite nativement à partir de là.

Fonctionne sur n'importe quel shellcode, quelle que soit la couverture du lifter. Utile pour le cobalt sans stageless, les payloads exotiques chargés en appels système, tout ce qui est trop volumineux ou trop bizarre pour être virtualisé. On perd la défense de virtualisation instruction par instruction mais on conserve la pleine polymorphie VM par graine sur le wrapper.

### Pipeline de construction

Comment `--pack` enveloppe le shellcode d'origine sous forme d'îlot de données chiffré derrière un programme IR synthétique à une seule instruction.```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]

Flux d'exécution

Ce que fait le wrapper du packer lors de la première entrée, y compris le déchiffrement paresseux contrôlé de l'îlot de données et le JMP_NATIVE synthétique unique qui cède le contrôle au shellcode désormais en clair.```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:~
Le déchiffrement différé de l'îlot de données est réservé au mode pack. En mode par défaut et en mode hybride, le code lifté effectue des VM LOAD/STORE sur les octets de l'îlot de données en cours d'exécution et en a besoin en clair dès le début, donc le prologue le traite de manière anticipée. En mode pack, il n'y a exactement qu'un consommateur de l'îlot de données, l'échappée native après le JMP_NATIVE synthétique, donc le déchiffrement peut attendre jusqu'à ce moment-là.

## Mode hybride : `--ranges A:B,C:D`

Virtualisation ciblée. Choisissez les plages d'octets dans l'entrée qui doivent être liftées. Tout le reste reste sous forme d'octets natifs d'origine dans la sortie. Au début de chaque plage, le lifter pose un `jmp rel32` de 5 octets vers un stub `vm_entry_K` ajouté après la région de shellcode natif. Le code natif externe ne peut ré-entrer dans une plage liftée que par l'octet de début patché. Les octets intermédiaires sont remplis de int3, donc toute branche native qui cible un octet intermédiaire est rejetée au moment du scan. Le code lifté qui sort d'une plage vers des octets hors de celle-ci devient `JMP_NATIVE` ou `CALL_NATIVE`.

Utilisez d'abord `--scan` pour trouver les candidats éligibles. Le scan classe les plages avec des trous, sans bloc terminé par `ret`, avec un corps plus court que 5 octets, ou avec des branches natives externes vers des octets intermédiaires comme quasi-éligibles plutôt qu'éligibles.

### Pipeline de construction

Comment `--ranges` ne lifte que les plages d'octets choisies et patche le shellcode natif sur place afin que le contrôle soit redirigé vers les stubs d'entrée VM ajoutés.```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]

Flux d'exécution

Le flux de contrôle à travers un blob mixte natif/VM, incluant l'entrée dans le dispatch de la VM lorsque le code natif atteint le début d'une plage patchée, et les chemins d'échappement natifs utilisés lorsque le code levé rebranche vers l'extérieur.```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:~
Les plages de type coroutine, celles sans `ret` qui sortent via un tail-jmp, sont documentées via `--coroutines` pour la sortie de `--scan`. Le drapeau ne modifie pas la génération de code pour l'instant. Le mode plages est le mieux adapté aux fonctions feuilles autonomes. Les fonctions auxiliaires qui dépendent d'un état de registre spécifique fourni par l'appelant ne peuvent pas être extraites de manière autonome ; l'API finit par être appelée avec des arguments quelconques.

## Mode superposé : `--pack --ranges A:B,C:D`

Exécutez la construction hybride, puis encapsulez le résultat avec pack. La VM externe déchiffre le blob hybride interne sur place et saute à son octet 0. À partir de là, l'exécution se déroule exactement comme en mode hybride autonome, sauf que l'intégralité du blob, y compris le bytecode de la plage choisie, est chiffrée au repos. Les deux couches s'assemblent proprement car le blob interne est une région PIC autonome.

### Pipeline de construction

Le mode superposé invoque le packager de manière récursive : d'abord une construction interne `--ranges`, puis un encapsulage externe `--pack` de cette construction.```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]

Flux d'exécution

Comment la VM du pack externe transmet le contrôle à la VM interne en mode range, chacune s'exécutant sur sa propre trame VMState indépendante.```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 interne et la VM externe ne partagent aucun état. Ce sont deux VM indépendantes qui se trouvent dans le même blob. Le seul rôle de la VM externe est de verrouiller la VM interne derrière une couche de déchiffrement.

## Mode détour : `--embed-into target.exe --at RVA`

Forme différente des autres. L'entrée est du shellcode brut, classiquement un blob VM déjà émis par mkPIVM dans un autre mode, bien que n'importe quels octets PIC fonctionnent. La sortie est un PE patché.

L'outil analyse `target.exe`, localise la RVA choisie dans une section exécutable, désassemble suffisamment d'instructions à cet endroit pour couvrir 5 octets, refuse si la séquence déplacée contient de l'adressage relatif à RIP ou un flux de contrôle relatif qui ne peut pas survivre au déplacement, puis ajoute une nouvelle section RWX contenant un wrapper et le blob VM. Le wrapper préserve l'état de l'appelant, transfère le contrôle au blob VM, restaure l'état, exécute les octets d'origine déplacés, puis revient à l'octet suivant le patch. Un `jmp rel32` de 5 octets à la RVA choisie pointe vers le wrapper.

Deux sous-modes pour la manière dont le wrapper transfère le contrôle à la VM :

### Sous-mode threadé, par défaut

Deux schémas suivent. Le premier est le pipeline de patchage PE au moment du build qui injecte la section du wrapper, corrige les références et écrit le jmp de 5 octets à la RVA choisie. Le second est le flux de contrôle à l'exécution lorsque le processus hôte finit par atteindre cette 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]

The input content for chunk 21 is empty. Please provide the Markdown text to translate.```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:~
Le sous-mode fileté est la forme appropriée pour les stagers et les beacons. Le thread principal de l'hôte ne se bloque jamais. Le thread de travail hérite du cycle de vie nécessaire au payload. Si le payload appelle `ExitProcess`, tout le processus meurt, mais pour un payload non terminal comme tout beacon C2, l'hôte s'exécute indéfiniment en parallèle avec lui.

### Sous-mode inline : `--detour-inline`

Même structure de wrapper, mais le wrapper effectue un appel direct `call vm_blob` au lieu de `CreateThread`. Le thread principal de l'hôte se bloque jusqu'au retour de la VM. Utile lorsque la cible ne dispose pas de `CreateThread` dans son IAT, ou comme solution de repli lorsque le chemin de base-reloc fileté ne peut pas s'appliquer à une cible particulière.```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]

Scan mode : --scan

Aucun fichier de sortie. Construit le CFG à partir du shellcode d'entrée et imprime les candidats --ranges sur stderr. Chaque candidat est classé comme éligible, coroutine, quasi-manqué (near-miss) ou interne, où interne signifie masqué par un candidat éligible plus grand qui le couvre déjà. Utilisez ceci pour choisir les arguments de plage avant de relancer l'outil en mode hybride.```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:~
## Axes de polymorphisme par graine

Listés approximativement par ordre d'impact sur la signature statique.

* Famille de chiffrement. L'une des ARX, LcgSub, SBoxAdd, FeistelByte. Choisit à la fois le chiffrement du bytecode utilisé à la compilation et le déchiffrement inline émis dans le chemin de récupération du dispatcher. Le chiffrement et le déchiffrement correspondent par construction.
* Disposition des emplacements de registres. Le VMState contient des emplacements `reg_count`, dimensionnés de 24 à 32 par graine. Les 16 GPR architecturaux plus les 4 registres Tmp reçoivent une nouvelle permutation des indices d'emplacements à chaque graine.
* Mappage opcode-vers-gestionnaire. Chaque famille de codec se voit attribuer un octet d'opcode aléatoire à chaque graine. La table de gestionnaires à 256 entrées est indexée par opcode ; le bytecode encodé référence les opcodes mappés.
* Topologie du dispatcher. Threaded ou central, choisi par graine.
* Stratégie d'auto-localisation du prologue. `call $+5; pop`, `lea reg, [rip+0]`, ou un mélange jmp/call.
* Registres temporaires des gestionnaires. Le gestionnaire BR_CC permute ses 6 rôles temporaires dans le pool des registres volatils. Le gestionnaire Store fait de même.
* Densité de gadgets factices. De 0 à 3, contrôle l'émission de déchets entre les gestionnaires.
* Décisions de la passe d'obfuscation IR. L'injection d'IR mort et les prédicats opaques utilisent chacun leur propre sous-RNG afin que leurs décisions soient déterministes par graine sans perturber les autres axes.
* État initial du chiffrement du bytecode. 64 bits aléatoires par graine.

## Charges utiles testées

Cet outil a été validé sur plusieurs échantillons de shellcode issus de Cobalt Strike, MSF, Sliver et de nombreux autres.

## Compilation

Nécessite Visual Studio 2022, CMake 3.21 ou plus récent, et vcpkg. Le projet CMake récupère Zydis et fmt via le mode manifeste de vcpkg.```
cmake -S . -B build
cmake --build build --config Release --target mkpivm

Limites connues

  • Le mode Range pour les stagers Cobalt ne fonctionne pas en tant que feuille autonome. Les fonctions d'assistance du stager dépendent de l'état des registres fourni par l'appelant, que le runner ne fournit pas. La virtualisation complète ou --pack sont les voies qui aboutissent réellement.
  • Le détour fileté x86 exige que la cible ne possède pas d'ASLR ou accepte des entrées de relocalisation de base ajoutées, que l'outil émet lorsqu'elles sont présentes. Si le répertoire de données BASERELOC de la cible est malformé ou absent, l'outil retombe en mode inline.
  • Le lifter ne couvre pas actuellement les mouvements de registres SSE/AVX, les opérations atomiques, CMPXCHG, RDMSR, ni les instructions privilégiées. Le mode pack est la solution de contournement pour les shellcodes qui les utilisent.
  • Les signatures Authenticode sur la sortie de --embed-into sont invalidées. La somme de contrôle du PE est mise à zéro.
  • Les blobs de sortie nécessitent du RWX au moment de l'exécution, car le déchiffrement sur place réécrit dans les pages du blob lui-même. La plupart des chargeurs qui exécutent du shellcode allouent de toute façon du RWX. Il n'est pas prévu de passer en RX-only, car ce compromis achète du RX au prix d'un parcours du PEB et d'un appel à VirtualAlloc, ce qui constitue probablement une signature pire que la page RWX qu'il remplace.
  • La virtualisation des payloads sans stager via le mode par défaut n'est pas encore prise en charge, car ils sont horriblement compliqués à envelopper dans la VM ; préférez --pack + --ranges.

Notes

  • Ce projet est en grande partie une recherche de preuve de concept. S'il est bien accueilli, je l'étendrai comme demandé et les contributions sont les bienvenues. Cependant, il semblait stable pour les échantillons testés.
  • Si votre shellcode ne fonctionne pas et que vous ne voulez pas le déposer dans une Issue, je ne peux malheureusement pas vous aider. Cela a été testé sur Sliver, Cobalt Strike 4.12, MSF, Havoc et quelques autres échantillons non divulgués.
  • Lors de mes tests, l'injection de la VM dans des processus vivants a très bien fonctionné. Cependant, en ce qui concerne l'intégration dans des PE, cela n'a pas été testé avec des logiciels commerciaux comme MS Word, seulement des tests synthétiques, mais ça fonctionne probablement. Si ce n'est pas le cas, je corrigerai.
  • En date du 20/05/2026 (cela semble avoir pris fin maintenant), il semble que le dépôt ait été envahi de bots. Donc, c'est merveilleux. Veuillez ignorer tous les comptes Github vides.
  • J'ai vu un post généré par IA à propos de mkPIVM, affirmant que ses inconvénients étaient :
    • 1). L'absence de support Linux, ce qui est juste, et je vais bientôt l'ajouter.
    • 2). Pas d'interface graphique ? Pardon ?
    • Je ne sais pas, j'ai trouvé ça drôle.

Contribution

  • C'est vraiment de la bonne came, sans blague. Si vous voulez proposer de nouvelles idées, corriger vos propres bugs, soumettre des Issues pour que je les corrige, et ainsi de suite, allez-y.
Télécharger l’outil