
Genera macchine virtuali polimorfiche e indipendenti dalla posizione (PIVMs) da shellcode x86/x64 arbitrario.
Leggi l'articolo di ricerca (scritto per la 1.0.0).
mkPIVM è un virtualizzatore di shellcode polimorfo e indipendente dalla posizione per Windows x86 e x64 (Linux presto).
Dagli in pasto shellcode grezzo. Emette un altro blob grezzo: una piccola macchina virtuale che interpreta una versione "lifted" e cifrata a riposo delle tue istruzioni originali. L'output è esso stesso codice indipendente dalla posizione e gira ovunque girerebbe lo shellcode originale, da un loader a thread remoto a un detour in una code cave. Ogni parametro per-seed varia in modo indipendente: famiglia di cifrari, layout degli slot dei registri, permutazione opcode-gestore, topologia del dispatcher, pattern di gadget spazzatura, punti di inserimento dell'offuscamento IR. Due build dallo stesso input condividono meno di un centinaio di byte coincidenti su decine di kilobyte.
Perché: lo shellcode nativo è banale a livello di firme. Avvolgerlo in una VM per istanza con un cifrario per istanza non lascia nulla di utile a riposo, e il lifting delle istruzioni in bytecode aggiunge un'altra barriera tra i byte su disco e qualsiasi disassemblatore che conosca l'aspetto di x86. Per quanto posso dire da una rassegna della letteratura, nessuno strumento pubblico offre esattamente questa pipeline: PIC grezzo in ingresso, PIC di VM polimorfo grezzo in uscita. Quindi, l'ho menzionato nell'articolo di ricerca che lo richiedeva. A essere onesti, se ho ragione sul fatto che nessuno lo abbia fatto (pubblicamente) prima, e ne sono abbastanza sicuro, sono sorpreso. Ciononostante, goditelo.
mkpivm.exe shellcode.bin --arch x64 -o out.bin
La tua PIVM è calda e pronta. Questo è il percorso più semplice. Diverse altre modalità variano in base a quanto aggressivamente le istruzioni originali vengono virtualizzate, se l'output è un blob autonomo o un PE patchato, e se il lift viene eseguito o meno.
# Vetrina
Ho le prove. Puoi vedere un video di mkPIVM in azione qui sotto, in cui virtualizza completamente uno stager di Meterpreter (vanilla, tra l'altro), inietta in explorer.exe e catturiamo una callback. Ovviamente questo è solo un esempio, e mkPIVM può essere applicato a molto di più, supponendo che le istruzioni nello shellcode siano supportate. Se non lo sono, apri una Issue, mandami lo shellcode, ci penso io.
Guardalo [qui](https://github.com/D7EAD/mkPIVM/raw/refs/heads/main/media/mkpivm-showcase.mp4). Ospitato in ./media, purtroppo non posso incorporarlo.
Ecco il report VirusTotal per quell'esatto campione virtualizzato (al 06/04/2026).
<img src="https://assets.kitploit.com/production/public/readmes/7466/3f4a6dbe2777c02fb32f71749a5b1fa2aa324cb0dac2f77d5681bbe3a4e17a81.png">
...e la versione impacchettata, nemmeno virtualizzata, con un'entropia notevolmente più alta.
<img src="https://assets.kitploit.com/production/public/readmes/7466/47f68372e31a2c24fa49eb71648adbdf8944977db90d645df337125c1bdfa8e3.png">
Ecco, per confronto, i risultati di un normale beacon Cobalt Strike.
<img src="https://assets.kitploit.com/production/public/readmes/7466/76e2107589cd2b1b65c846a671aed173aa5e6d89630f5e9d283d99c546a2d566.png">
È stata prestata attenzione alla telemetria dell'entropia dell'output di questo strumento, che produce shellcode con entropia inferiore a quella delle tipiche DLL WinAPI di Windows (al di fuori della modalità packing), come ntdll.dll o kernel32.dll. Il confronto dell'entropia è il seguente...
| File | Byte | Entropia |
|------|-------|---------|
| `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** |
## Modalità a colpo d'occhio
| Modalità | Flag | Cosa cambia |
|------|-------|--------------|
| Default | nessuno | Esegue il lift dell'intero input. Tutto virtualizzato. |
| Packer | `--pack` | Niente lift. Avvolge l'input come dati cifrati, decifra a runtime e salta dentro. |
| Hybrid | `--ranges A:B,...` | Esegue il lift solo degli intervalli di byte scelti. Il resto rimane nativo. |
| Stacked | `--pack --ranges A:B` | Costruisce il blob ibrido e poi lo impacchetta. |
| Detour | `--embed-into PE --at RVA` | Prende un blob pre-costruito, lo incorpora in un PE e applica una patch jmp all'RVA scelta. |
| Scan | `--scan` | Stampa i candidati `--ranges` ammissibili dalla CFG dell'input, quindi esce. |
| RX | `--rx` | Blob PAGE_EXECUTE_READ. La data island rimane cifrata a riposo; il PEB walker interno al blob risolve VirtualProtect e decifra in-place in state_init. |
| RX w/ Loader | `--rx --rx-loader-vp` | Come `--rx`, ma il tuo loader passa VirtualProtect come primo argomento del blob. Nessun PEB walker. |
Ogni modalità rispetta `--seed`, `--arch`, `--input-format` e `--format`. Vedi le sezioni relative a ciascuna modalità qui sotto per la pipeline di build e il flusso a runtime.
## Virtualizzazione predefinita
Il lifter attraversa l'intera CFG e traduce ogni istruzione in un IR personalizzato. L'IR passa attraverso due passaggi di offuscamento, poi attraverso codec che codificano ogni istruzione nella forma bytecode per seed. La tabella dei blocchi, la tabella degli handler e la data island sono cifrate con lo stesso stream cipher per byte del bytecode. A runtime, il prologo decifra queste tre regioni in place e il loop del dispatcher recupera i byte del bytecode uno alla volta, decifrandoli e inviandoli a un handler che svolge il lavoro.
### Pipeline di build
Passaggi che la build esegue per trasformare lo shellcode grezzo nel blob virtualizzato emesso, dall'inizio alla fine. _Tutti i grafici qui sotto si riferiscono alla release 1.0.0; da allora sono cambiati, ma l'idea è la stessa._```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]
Ciò che il blob emesso fa quando il loader host lo chiama al byte 0, dal prologo attraverso i fetch del dispatcher e i vari percorsi di terminazione.```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]
Il trucco del nonce runtime è la mossa anti-memory-scan chiave: la tabella degli handler risiede in memoria sottoposta a XOR con un valore derivato da `rdtsc XOR cipher_init`, quindi due processi che caricano lo stesso blob hanno tabelle handler byte-diverse. Il dispatcher rimuove la maschera al momento del lookup con un'ulteriore XOR.
## Modalità packer: `--pack`
Il compromesso opposto. Il lifter non viene eseguito. La shellcode originale finisce nell'isola di dati cifrata e l'IR è un singolo `JMP_NATIVE imm=0` sintetico. Il prologo salta intenzionalmente la decifratura dell'isola di dati che la modalità predefinita esegue subito. Invece, la prima volta che il singolo handler JMP_NATIVE scatta, decifra l'isola di dati sul posto, imposta un byte marcatore e poi trasferisce il controllo al byte 0 della shellcode ora in chiaro. La shellcode viene eseguita nativamente da lì.
Funziona su qualsiasi shellcode, indipendentemente dalla copertura del lifter. Utile per Cobalt senza stager, payload esotici ricchi di syscall, qualsiasi cosa troppo grande o strana da virtualizzare. Perde la difesa di virtualizzazione per singola istruzione ma mantiene l'intero polimorfismo VM per-seed sul wrapper.
### Pipeline di build
Come `--pack` avvolge la shellcode originale come un'isola di dati cifrata dietro un programma IR sintetico a singola istruzione.```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]
Ciò che il wrapper del pack fa al primo ingresso, inclusa la decrittazione lazy controllata dell'isola di dati e il singolo JMP_NATIVE sintetico che cede il controllo allo shellcode ora in chiaro.```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]
La decrittazione differita dell'isola di dati è esclusiva della modalità pack. Nella modalità predefinita e in quella ibrida, il codice liftato esegue VM LOAD/STORE sui byte dell'isola di dati nel corso dell'esecuzione e li richiede in chiaro fin dall'inizio, quindi il prologo la gestisce subito. Nella modalità pack c'è esattamente un consumatore dell'isola di dati, l'uscita verso il codice nativo dopo il JMP_NATIVE sintetico, quindi la decrittazione può attendere fino a quel momento.
## Modalità ibrida: `--ranges A:B,C:D`
Virtualizzazione mirata. Scegli gli intervalli di byte nell'input che devono essere liftati. Tutto il resto rimane come byte nativi originali nell'output. All'inizio di ogni intervallo il lifter scrive un `jmp rel32` di 5 byte verso uno stub `vm_entry_K` aggiunto dopo la regione dello shellcode nativo. Il codice nativo esterno può rientrare in un intervallo liftato solo attraverso il byte iniziale patchato. I byte interni all'intervallo sono riempiti con int3, quindi qualsiasi ramo nativo che punti a un byte interno viene rifiutato in fase di scansione. Il codice liftato che esce da un intervallo verso byte esterni diventa `JMP_NATIVE` o `CALL_NATIVE`.
Usa prima `--scan` per trovare i candidati idonei. La scansione classifica come near-miss, piuttosto che idonei, gli intervalli con lacune, senza blocco terminato da `ret`, con corpo più corto di 5 byte o con rami nativi esterni verso byte interni all'intervallo.
### Pipeline di build
Come `--ranges` lifta solo gli intervalli di byte scelti e applica una patch allo shellcode nativo in loco, così il controllo viene reindirizzato negli stub di ingresso VM aggiunti.```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]
Il flusso di controllo attraverso un blob misto nativo e VM, inclusa l'entrata nella dispatch della VM quando il codice nativo raggiunge l'inizio di un intervallo patchato e i percorsi di uscita nativa usati quando il codice sollevato esce tramite ramificazione.```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
I range in stile coroutine, quelli senza `ret` che escono tramite un tail-jmp, sono documentati tramite `--coroutines` per l'output di `--scan`. Il flag al momento non modifica il codegen. La modalità range è la più adatta per funzioni foglia autocontenute. Le funzioni helper che dipendono da uno specifico stato dei registri fornito dal chiamante non possono essere estratte singolarmente; l'API finisce per essere chiamata con argomenti spazzatura.
## Modalità stacked: `--pack --ranges A:B,C:D`
Esegui la build ibrida, quindi applica il pack-wrap al risultato. La VM esterna decripta il blob ibrido interno in place e salta al suo byte 0. Da lì l'esecuzione procede esattamente come nella modalità ibrida standalone, tranne per il fatto che l'intero blob, incluso il bytecode dell'intervallo selezionato, è crittografato a riposo. I due livelli si compongono in modo pulito perché il blob interno è una regione PIC autocontenuta.
### Pipeline di build
Come la modalità stacked invoca ricorsivamente il packager: prima una build interna con `--ranges`, poi un wrap esterno con `--pack` di quella build.```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]
Come la VM del pack esterno passa il controllo alla VM interna in modalità range, ciascuna in esecuzione sul proprio frame VMState indipendente.```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
La VM interna e la VM esterna non condividono alcuno stato. Sono due VM indipendenti che si trovano nello stesso blob. L'unico compito della VM esterna è proteggere quella interna dietro un livello di decrittazione.
## Detour mode: `--embed-into target.exe --at RVA`
Forma diversa rispetto alle altre. L'input è shellcode grezzo, tipicamente un blob VM già prodotto da mkPIVM in un'altra modalità, anche se qualsiasi byte PIC funzionerà. L'output è un PE patchato.
Il tool analizza `target.exe`, individua la RVA scelta in una sezione eseguibile, disassembla abbastanza istruzioni per coprire 5 byte, rifiuta se il tratto spostato contiene indirizzamento relativo a RIP o flusso di controllo relativo che non sopravviverebbe allo spostamento, quindi aggiunge una nuova sezione RWX contenente un wrapper e il blob VM. Il wrapper preserva lo stato del chiamante, trasferisce il controllo al blob VM, ripristina lo stato, esegue i byte originali spostati e salta di nuovo al byte successivo alla patch. Un `jmp rel32` di 5 byte alla RVA scelta punta al wrapper.
Due sottomodalità per il modo in cui il wrapper trasferisce il controllo alla VM:
### Sottomodalità threaded, predefinita
Seguono due diagrammi. Il primo è la pipeline di patching del PE in fase di build che inietta la sezione wrapper, corregge i riferimenti e scrive il jmp di 5 byte alla RVA scelta. Il secondo è il flusso di controllo a runtime quando il processo host raggiunge infine tale 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]
Possiamo sfruttare le vulnerabilità in vari servizi e strumenti:
Possiamo effettuare l'escalation dei privilegi in vari modi:
pwd - Mostra la directory di lavoro correntewhoami - Mostra l'utente corrente del sistemals - Elenca i filecd - Cambia directorycat - Visualizza il contenuto dei fileifconfig - Mostra le interfacce di reteiwconfig - Mostra le interfacce wirelessping - Verifica la connettività tra due nodinmap - Scansiona le porte di un sistemanetstat - Visualizza le connessioni di retearp - Visualizza e modifica la tabella ARPsudo - Esegue comandi come superutentesu - Cambia utenteclear - Pulisce il terminaleman - Mostra il manuale di un comandochmod - Modifica i permessi di un filechown - Modifica il proprietario di un filemkdir - Crea una directoryrmdir - Rimuove una directorytouch - Crea un file vuotocp - Copia un filemv - Sposta o rinomina un filerm - Rimuove un filefind - Trova un filegrep - Cerca il contenuto di un filelocate - Localizza un filetop - Visualizza i processi in esecuzione e le risorse di sistemaps - Visualizza i processi in esecuzione sul sistemauname - Visualizza le informazioni sul kernel Linuxhistory - Visualizza la cronologia dei comandi```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]La sottomodalità threaded è la forma giusta per stager e beacon. Il main dell'host non si blocca mai. Il thread worker eredita qualsiasi ciclo di vita richieda il payload. Se il payload chiama `ExitProcess`, l'intero processo termina, ma per un payload che non termina, come ogni beacon C2, l'host rimane in esecuzione per sempre in parallelo ad esso.
### Sottomodalità inline: `--detour-inline`
Stessa struttura del wrapper, ma il wrapper esegue un `call vm_blob` diretto invece di `CreateThread`. Il thread principale dell'host resta bloccato finché la VM non restituisce il controllo. Utile quando il target non ha `CreateThread` nella sua IAT, o come fallback quando il percorso base-reloc basato su thread non può essere applicato a un target specifico.```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]
--scanNessun file di output. Costruisce il CFG dallo shellcode di input e stampa i candidati --ranges su stderr. Ogni candidato viene classificato come eligible, coroutine, near-miss o internal, dove internal significa oscurato da un candidato eligible più grande che già lo copre. Usalo per selezionare gli argomenti di range prima di invocare nuovamente lo strumento in modalità ibrida.```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]
## Assi di polimorfismo per seed
Elencati approssimativamente in ordine di quanto influenzano la firma statica.
* Famiglia di cifrario. Una tra ARX, LcgSub, SBoxAdd, FeistelByte. Seleziona sia la cifratura del bytecode usata in fase di build sia la decifratura inline emessa nel percorso di fetch del dispatcher. Cifratura e decifratura coincidono per costruzione.
* Layout degli slot dei registri. Il VMState contiene `reg_count` slot, dimensionati da 24 a 32 per seed. I 16 GPR architetturali più 4 registri Tmp ricevono una nuova permutazione degli indici di slot per ogni seed.
* Mappatura opcode-handler. A ogni famiglia di codec viene assegnato un byte di opcode casuale per ogni seed. La tabella degli handler a 256 voci indicizza per opcode; il bytecode codificato fa riferimento agli opcode mappati.
* Topologia del dispatcher. Threaded o centralizzata, scelta per seed.
* Strategia di auto-localizzazione del prologo. `call $+5; pop`, `lea reg, [rip+0]`, o uno shuffle jmp/call.
* Registri temporanei degli handler. L'handler BR_CC permuta i suoi 6 ruoli temporanei attraverso il pool volatile. Anche l'handler Store fa lo stesso.
* Densità dei junk gadget. Da 0 a 3, controlla l'emissione di spazzatura tra gli handler.
* Decisioni del pass di offuscamento IR. L'iniezione di IR morto e i predicati opachi usano ciascuno il proprio sub-RNG, così le loro decisioni sono deterministiche per seed senza disturbare gli altri assi.
* Stato iniziale della cifratura del bytecode. 64 bit casuali per seed.
## Payload testati
Questo strumento è stato validato contro diversi campioni di shellcode di Cobalt Strike, MSF, Sliver e molti altri.
## Compilazione
Richiede Visual Studio 2022, CMake 3.21 o successivo e vcpkg. Il progetto CMake recupera Zydis e fmt tramite la modalità manifest di vcpkg.```
cmake -S . -B build
cmake --build build --config Release --target mkpivm
--pack sono le strade che fanno effettivamente beacon.--embed-into vengono invalidate. Il checksum PE viene azzerato.VirtualAlloc, che probabilmente è una firma peggiore della pagina RWX che sostituisce.--pack + --ranges.