
Generiere polymorphe, positionsunabhängige virtuelle Maschinen (PIVMs) aus beliebigem x86/x64-Shellcode.
Lesen Sie das Forschungspapier (geschrieben für 1.0.0).
mkPIVM ist ein polymorpher, positionsunabhängiger Shellcode-Virtualisierer für Windows x86 und x64 (Linux bald).
Gib ihm rohen Shellcode. Er gibt einen weiteren rohen Blob aus: eine kleine virtuelle Maschine, die eine angehobene, im Ruhezustand verschlüsselte Version deiner ursprünglichen Befehle interpretiert. Die Ausgabe ist selbst positionsunabhängiger Code und läuft überall, wo der ursprüngliche Shellcode laufen würde, von einem Remote-Thread-Loader bis zu einem Code-Cave-Umweg. Jeder pro-Seed-Regler variiert unabhängig: Chiffrefamilie, Register-Slot-Layout, Opcode-zu-Handler-Permutation, Dispatcher-Topologie, Junk-Gadget-Muster, Einfügepunkte für IR-Obfuskation. Zwei Builds aus derselben Eingabe teilen weniger als hundert zufällig übereinstimmende Bytes aus zig Kilobyte.
Warum: Nativer Shellcode ist signatur-trivial. Ihn in eine pro Instanz einzigartige VM mit einer pro Instanz einzigartigen Chiffre zu hüllen, hinterlässt im Ruhezustand nichts Nützliches, und das Anheben der Befehle zu Bytecode stellt eine weitere Wand zwischen die Bytes auf der Platte und jeden Disassembler, der weiß, wie x86 aussieht. Soweit ich aus einer Literaturdurchsicht sagen kann, liefert kein öffentliches Tool exakt diese Pipeline: roher PIC rein, roher polymorpher VM-PIC raus. Also erwähnte ich das in dem Forschungspapier, das es verlangte. Um ehrlich zu sein, wenn ich richtig liege, dass das vorher niemand (öffentlich) getan hat, und ich bin ziemlich zuversichtlich, dann bin ich überrascht. Nichtsdestotrotz, viel Spaß.
mkpivm.exe shellcode.bin --arch x64 -o out.bin
Deine PIVM ist heiß und bereit. Das ist der einfachste Weg. Mehrere andere Modi variieren, wie aggressiv die ursprünglichen Instruktionen virtualisiert werden, ob die Ausgabe ein eigenständiger Blob oder eine gepatchte PE ist und ob der Lift überhaupt läuft.
# Showcase
Ich habe die Belege. Du kannst unten ein Video von mkPIVM in Aktion sehen: Es virtualisiert einen Meterpreter-Stager (übrigens Vanilla) vollständig, injiziert in explorer.exe, und wir fangen einen Callback ab. Natürlich ist das nur ein Beispiel, und mkPIVM kann auf viel mehr angewendet werden, vorausgesetzt, die Instruktionen im Shellcode werden unterstützt. Falls nicht, erstelle ein Issue, schick mir den Shellcode, ich helfe dir.
Siehe [hier](https://github.com/D7EAD/mkPIVM/raw/refs/heads/main/media/mkpivm-showcase.mp4). Liegt in ./media, kann leider nicht eingebettet werden.
Hier ist der VirusTotal-Bericht für genau dieses virtualisierte Sample (Stand 06/04/2026).
<img src="https://assets.kitploit.com/production/public/readmes/7466/3f4a6dbe2777c02fb32f71749a5b1fa2aa324cb0dac2f77d5681bbe3a4e17a81.png">
...und die gepackte Version, nicht einmal virtualisiert, mit deutlich höherer Entropie.
<img src="https://assets.kitploit.com/production/public/readmes/7466/47f68372e31a2c24fa49eb71648adbdf8944977db90d645df337125c1bdfa8e3.png">
Hier sind die Ergebnisse eines normalen Cobalt-Strike-Beacons zum Vergleich.
<img src="https://assets.kitploit.com/production/public/readmes/7466/76e2107589cd2b1b65c846a671aed173aa5e6d89630f5e9d283d99c546a2d566.png">
Es wurde sorgfältig auf die Entropie-Telemetrie der Ausgabe dieses Tools geachtet, was zu Shellcode führt, dessen Entropie geringer ist als die typischer Windows-WinAPI-DLLs (außerhalb des Packing-Modus), wie z. B. ntdll.dll oder kernel32.dll. Der Entropievergleich sieht ungefähr so aus...
| Datei | Bytes | 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** |
## Modi auf einen Blick
| Modus | Flags | Was sich ändert |
|------|-------|--------------|
| Standard | none | Lifte die gesamte Eingabe. Alles wird virtualisiert. |
| Packer | `--pack` | Nicht liften. Eingabe als verschlüsselte Daten verpacken, zur Laufzeit entschlüsseln, hineinspringen. |
| Hybrid | `--ranges A:B,...` | Nur die gewählten Bytebereiche liften. Der Rest bleibt nativ. |
| Stacked | `--pack --ranges A:B` | Den Hybrid-Blob erstellen und dann pack-verpacken. |
| Detour | `--embed-into PE --at RVA` | Einen vorgefertigten Blob nehmen, in eine PE einbetten, einen jmp an der gewählten RVA patchen. |
| Scan | `--scan` | Geeignete `--ranges`-Kandidaten aus dem CFG der Eingabe ausgeben und dann beenden. |
| RX | `--rx` | PAGE_EXECUTE_READ-Blob. Data Island bleibt im Ruhezustand verschlüsselt; PEB-Walker im Blob löst VirtualProtect auf und entschlüsselt an Ort und Stelle bei state_init. |
| RX mit Loader | `--rx --rx-loader-vp` | Wie `--rx`, aber dein Loader übergibt VirtualProtect als erstes Argument des Blobs. Kein PEB-Walker. |
Jeder Modus beachtet `--seed`, `--arch`, `--input-format` und `--format`. Siehe die Abschnitte zu den einzelnen Modi unten für die Build-Pipeline und den Laufzeitablauf.
## Standard-Virtualisierung
Der Lifter durchläuft das gesamte CFG und senkt jede Instruktion auf ein benutzerdefiniertes IR herab. Das IR durchläuft zwei Obfuscation-Durchgänge und dann Codecs, die jede insn in die pro Seed definierte Bytecode-Form kodieren. Die Blocktabelle, Handler-Tabelle und das Data Island sind mit derselben Byte-für-Byte-Stromchiffre verschlüsselt wie der Bytecode. Zur Laufzeit entschlüsselt der Prolog diese drei Bereiche an Ort und Stelle, und die Dispatcher-Schleife holt Bytecode-Bytes einzeln, entschlüsselt sie und leitet sie an einen Handler weiter, der die Arbeit erledigt.
### Build-Pipeline
Schritte, die der Build ausführt, um rohen Shellcode in den erzeugten virtualisierten Blob zu verwandeln, von Anfang bis Ende. _Alle Grafiken unten beziehen sich auf das 1.0.0-Release; sie haben sich seitdem geändert, aber die Idee ist dieselbe._```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]
Was der emittierte Blob tut, wenn der Host-Loader ihn an Byte 0 aufruft, vom Prolog über die Dispatcher-Fetches bis zu den verschiedenen Beendigungspfaden.```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]
Der Runtime-Nonce-Trick ist der entscheidende Anti-Memory-Scan-Schachzug: Die Handler-Tabelle liegt im Speicher, XOR-verknüpft mit einem Wert, der aus `rdtsc XOR cipher_init` abgeleitet wird, sodass zwei Prozesse, die dasselbe Blob laden, byteweise unterschiedliche Handler-Tabellen enthalten. Der Dispatcher demaskiert sie bei der Lookup-Zeit mit einem zusätzlichen XOR.
## Packer-Modus: `--pack`
Der umgekehrte Kompromiss. Der Lifter wird nicht ausgeführt. Der ursprüngliche Shellcode wird verschlüsselt in die Dateninsel gelegt, und die IR ist ein einziges synthetisches `JMP_NATIVE imm=0`. Der Prolog überspringt bewusst die Entschlüsselung der Dateninsel, die der Standardmodus sofort ausführt. Stattdessen entschlüsselt der einzige JMP_NATIVE-Handler beim ersten Auslösen die Dateninsel an Ort und Stelle, setzt ein Markierungsbyte und übergibt dann die Kontrolle an Byte 0 des nun im Klartext vorliegenden Shellcodes. Der Shellcode wird von dort nativ ausgeführt.
Funktioniert mit jedem Shellcode, unabhängig von der Lifter-Abdeckung. Nützlich für stageless Cobalt, exotische syscall-lastige Payloads und alles, was zu groß oder zu ungewöhnlich zum Virtualisieren ist. Verliert die Pro-Instruktion-Virtualisierungsverteidigung, behält aber die vollständige VM-Polymorphie pro Seed auf dem Wrapper.
### Build-Pipeline
Wie `--pack` den ursprünglichen Shellcode als verschlüsselte Dateninsel hinter einem synthetischen IR-Programm mit einer einzigen Instruktion verpackt.```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]
Was der Pack-Wrapper beim ersten Eintritt tut, einschließlich der gesteuerten verzögerten Entschlüsselung der Dateninsel und des einzigen synthetischen JMP_NATIVE, das die Kontrolle an den nun im Klartext vorliegenden Shellcode übergibt.```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]
Die verzögerte Data-Island-Entschlüsselung ist nur im Pack-Modus verfügbar. Im Standard- und Hybridmodus führt der geliftete Code VM LOAD/STORE auf Data-Island-Bytes mitten in der Ausführung aus und benötigt diese von Anfang an im Klartext, daher behandelt der Prolog dies eager. Im Pack-Modus gibt es genau einen Konsumenten der Data-Island, den nativen Escape nach dem synthetischen JMP_NATIVE, sodass die Entschlüsselung bis dahin warten kann.
## Hybridmodus: `--ranges A:B,C:D`
Gezielte Virtualisierung. Wählen Sie Byte-Bereiche in der Eingabe aus, die geliftet werden sollen. Alles andere bleibt als die ursprünglichen nativen Bytes in der Ausgabe erhalten. An jedem Bereichsstart patcht der Lifter einen 5-Byte-`jmp rel32` zu einem `vm_entry_K`-Stub, der nach dem nativen Shellcode-Bereich angehängt wird. Externer nativer Code kann einen gelifteten Bereich nur über das gepatchte Startbyte wieder betreten. Mid-Range-Bytes werden mit int3 gefüllt, sodass jeder native Zweig, der auf ein Mid-Range-Byte zielt, beim Scan abgelehnt wird. Gelifteter Code, der einen Bereich zu Bytes außerhalb davon verlässt, wird zu `JMP_NATIVE` oder `CALL_NATIVE`.
Verwenden Sie zuerst `--scan`, um geeignete Kandidaten zu finden. Der Scan klassifiziert Bereiche mit Lücken, ohne `ret`-terminierten Block, mit einem Körper kürzer als 5 Bytes oder mit externen nativen Verzweigungen in Mid-Range-Bytes als Near-Miss statt als geeignet.
### Build-Pipeline
Wie `--ranges` nur die ausgewählten Byte-Bereiche liftet und den nativen Shellcode an Ort und Stelle patcht, sodass die Steuerung in die angehängten VM-Eintritts-Stubs umgeleitet wird.```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]
Kontrollfluss durch einen gemischten Blob aus nativem und VM-Code, einschließlich des Einstiegs in den VM-Dispatch, wenn nativer Code auf den Anfang eines gepatchten Bereichs trifft, sowie der Ausstiegspfade zum nativen Code, die verwendet werden, wenn gelifteter Code wieder in den nativen Code zurückverzweigt.```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
Coroutine-artige Bereiche, diejenigen ohne `ret`, die über einen tail-jmp enden, werden über `--coroutines` für die `--scan`-Ausgabe dokumentiert. Das Flag ändert derzeit nichts am Codegen. Der Range-Modus ist am besten für eigenständige Blattfunktionen geeignet. Helper-Funktionen, die von einem bestimmten, vom Aufrufer bereitgestellten Registerzustand abhängen, können nicht eigenständig geliftet werden; die API wird dann mit Müll-Argumenten aufgerufen.
## Stacked-Modus: `--pack --ranges A:B,C:D`
Führe den Hybrid-Build aus und umhülle das Ergebnis anschließend mit Pack-Wrap. Die äußere VM entschlüsselt das innere Hybrid-Blob direkt an Ort und Stelle und springt zu dessen Byte 0. Von dort aus läuft die Ausführung genau wie im eigenständigen Hybrid-Modus ab, nur dass das gesamte Blob einschließlich des Bytecode des gewählten Bereichs im Ruhezustand verschlüsselt ist. Die beiden Ebenen fügen sich sauber zusammen, da das innere Blob eine eigenständige PIC-Region ist.
### Build-Pipeline
Wie der Stacked-Modus den Packager rekursiv aufruft: Zuerst ein innerer Build mit `--ranges`, dann ein äußerer `--pack`-Wrap dieses Builds.```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]
Wie die äußere pack VM die Kontrolle an die innere range-mode VM übergibt, wobei jede auf ihrem eigenen unabhängigen VMState-Frame läuft.```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
Die innere VM und die äußere VM teilen keinen Zustand. Es sind zwei unabhängige VMs, die zufällig im selben Blob existieren. Die einzige Aufgabe der äußeren besteht darin, die innere hinter einer Entschlüsselungsschicht abzuschirmen.
## Detour-Modus: `--embed-into target.exe --at RVA`
Anders geformt als die anderen. Die Eingabe ist roher Shellcode, üblicherweise ein VM-Blob, der bereits von mkPIVM in einem anderen Modus erzeugt wurde, obwohl beliebige PIC-Bytes funktionieren. Die Ausgabe ist eine gepatchte PE.
Das Tool parst `target.exe`, lokalisiert die gewählte RVA in einem ausführbaren Abschnitt, disassembliert dort genügend Anweisungen, um 5 Bytes abzudecken, und lehnt ab, wenn die verschobene Sequenz RIP-relative Adressierung oder relativen Kontrollfluss enthält, der eine Verschiebung nicht übersteht, und fügt dann einen neuen RWX-Abschnitt hinzu, der einen Wrapper plus den VM-Blob enthält. Der Wrapper sichert den Aufruferzustand, übergibt die Kontrolle an den VM-Blob, stellt den Zustand wieder her, führt die verschobenen ursprünglichen Bytes aus und springt zurück zu dem Byte nach dem Patch. Ein 5-Byte-`jmp rel32` an der gewählten RVA zeigt auf den Wrapper.
Zwei Untermodi dafür, wie der Wrapper an die VM übergibt:
### Threaded-Untermodus, der Standard
Es folgen zwei Diagramme. Das erste ist die PE-Patching-Pipeline zur Buildzeit, die den Wrapper-Abschnitt injiziert, Referenzen korrigiert und den 5-Byte-Jmp an der gewählten RVA schreibt. Das zweite ist der Kontrollfluss zur Laufzeit, wenn der Hostprozess schließlich diese RVA erreicht.```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]
No input content was provided to translate. Please provide the chunk text.```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]
Die Thread-Submodus ist die richtige Form für Stager und Beacons. Der Hauptthread des Hosts blockiert nie. Der Worker-Thread erbt den Lebenszyklus, den der Payload benötigt. Wenn der Payload `ExitProcess` aufruft, stirbt der gesamte Prozess, aber für einen nicht-terminierenden Payload wie jeden C2-Beacon läuft der Host für immer parallel dazu weiter.
### Inline-Submodus: `--detour-inline`
Gleiche Wrapper-Struktur, aber der Wrapper führt einen direkten `call vm_blob` anstelle von `CreateThread` aus. Der Hauptthread des Hosts blockiert, bis die VM zurückkehrt. Nützlich, wenn dem Ziel `CreateThread` in seiner IAT fehlt, oder als Fallback, wenn der Thread-Base-Reloc-Pfad auf ein bestimmtes Ziel nicht angewendet werden kann.```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]
--scanKeine Ausgabedatei. Erstellt den CFG aus dem eingegebenen Shellcode und gibt --ranges-Kandidaten an stderr aus. Jeder Kandidat wird als eligible, coroutine, near-miss oder internal klassifiziert, wobei internal bedeutet, dass er von einem größeren eligible-Kandidaten verdeckt wird, der ihn bereits abdeckt. Verwenden Sie dies, um range-Argumente auszuwählen, bevor Sie das Tool erneut im Hybridmodus aufrufen.```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]
## Polymorphie-Achsen pro Seed
Grob in der Reihenfolge aufgelistet, wie stark sie die statische Signatur beeinflussen.
* Chiffre-Familie. Eine von ARX, LcgSub, SBoxAdd, FeistelByte. Wählt sowohl die beim Erstellen verwendete Bytecode-Verschlüsselung als auch die Inline-Entschlüsselung, die im Dispatcher-Fetch-Pfad emittiert wird. Verschlüsselung und Entschlüsselung stimmen konstruktionsbedingt überein.
* Register-Slot-Layout. Der VMState enthält `reg_count` Slots – je nach Seed zwischen 24 und 32. Die 16 architektonischen GPRs plus 4 Tmp-Regs erhalten pro Seed eine neue Permutation der Slot-Indizes.
* Opcode-zu-Handler-Zuordnung. Jeder Codec-Familie wird pro Seed ein zufälliges Opcode-Byte zugewiesen. Die Handler-Tabelle mit 256 Einträgen wird über den Opcode indiziert; der kodierte Bytecode referenziert die abgebildeten Opcodes.
* Dispatcher-Topologie. Threaded oder zentral, pro Seed gewählt.
* Prologue-Selbstlokalisierungsstrategie. `call $+5; pop`, `lea reg, [rip+0]` oder ein jmp/call-Shuffle.
* Handler-Register-Temporärvariablen. Der BR_CC-Handler permutiert seine 6 temporären Rollen über den Pool flüchtiger Register. Der Store-Handler macht dasselbe.
* Junk-Gadget-Dichte. 0 bis 3, steuert die Ausgabe von Junk-Code zwischen den Handlern.
* Entscheidungen des IR-Obfuskationspasses. Dead-IR-Injektion und opake Prädikate verwenden jeweils ihr eigenes Sub-RNG, sodass ihre Entscheidungen pro Seed deterministisch sind, ohne andere Achsen zu beeinflussen.
* Anfangszustand der Bytecode-Verschlüsselung. Zufällige 64 Bit pro Seed.
## Getestete Payloads
Dieses Tool wurde gegen mehrere Cobalt Strike-, MSF-, Sliver- und zahlreiche weitere Shellcode-Beispiele validiert.
## Erstellen
Erfordert Visual Studio 2022, CMake 3.21 oder neuer und vcpkg. Das CMake-Projekt bezieht Zydis und fmt über den Manifest-Modus von vcpkg.```
cmake -S . -B build
cmake --build build --config Release --target mkpivm
--pack sind die Wege, die tatsächlich Beacon-Verbindungen aufbauen.--embed-into-Ausgaben werden ungültig. Die PE-Prüfsumme wird auf null gesetzt.VirtualAlloc-Aufrufs erkauft, was wahrscheinlich eine schlechtere Signatur ist als die RWX-Seite, die er ersetzt.--pack + --ranges.Das ist cooler Scheiß, mal im Ernst. Wenn du neue Ideen beitragen, deine eigenen Bugs beheben, Issues zur Behebung einreichen möchtest, und so weiter und so fort: nur zu.