
Read the research paper (written for 1.0.0).
mkPIVM は、Windows x86 および x64(まもなく Linux 対応)向けの、ポリモーフィックな位置独立シェルコード仮想化ツールです。
生のシェルコードを入力として受け取り、別の生のブロブを出力します。それは、元の命令のリフティング(lifting)され、暗号化された形で保存されたバージョンを解釈する、小さな仮想マシンです。出力自体も位置独立コードであり、リモートスレッドローダーからコードケイブデトゥアまで、元のシェルコードが動作するあらゆる場所で実行できます。シードごとの各パラメータは独立に変化します: 暗号ファミリ、レジスタスロットレイアウト、オペコードからハンドラへの置換、ディスパッチャのトポロジ、ジャンクガジェットパターン、IR難読化の挿入ポイント。同じ入力からの2つのビルドは、数万バイトのうち偶然一致するバイトが100未満です。
理由: ネイティブシェルコードはシグネチャ検出されやすいものです。それをインスタンスごとのVMとインスタンスごとの暗号で包めば、静止状態で有用なものは何も残らず、命令をバイトコードにリフティングすることで、ディスク上のバイトとx86の構造を知っているあらゆる逆アセンブラとの間にもう1つの壁ができます。私が文献調査で確認した限り、このパイプラインを正確に提供する公開ツールはありません: 生のPICを受け取り、生のポリモーフィックVM PICを出力する。そこで、そのことを要求された研究論文に記載しました。正直なところ、これまでに誰も(公に)これをやっていないという私の認識が正しければ、そして私はかなり自信がありますが、驚きです。それでも、お楽しみください。
mkpivm.exe shellcode.bin --arch x64 -o out.bin
Your PIVMは熱々で準備完了です。それが最も簡単な道です。他のいくつかのモードは、元の命令がどの程度積極的に仮想化されるか、出力がスタンドアロンのblobかパッチ適用済みのPEか、そしてリフトが実行されるかどうかさえも変えます。
# Showcase
証拠はあります。以下で、mkPIVMがMeterpreterステージャーを完全に仮想化し(ちなみにバニラです)、explorer.exeに注入して、コールバックをキャプチャする様子をビデオで見ることができます。もちろんこれは一例にすぎず、シェルコード内の命令がサポートされていれば、mkPIVMはもっと多くのことに適用できます。サポートされていない場合は、Issueを作成してシェルコードを送ってください。対応します。
こちらで[見る](https://github.com/D7EAD/mkPIVM/raw/refs/heads/main/media/mkpivm-showcase.mp4)。./media にホストされています。残念ながら埋め込めません。
こちらが、その正確に仮想化されたサンプルのVirusTotalレポートです(06/04/2026時点)。
<img src="https://assets.kitploit.com/production/public/readmes/7466/3f4a6dbe2777c02fb32f71749a5b1fa2aa324cb0dac2f77d5681bbe3a4e17a81.png">
...そして、パックされたバージョンは、仮想化すらされておらず、エントロピーが著しく高くなっています。
<img src="https://assets.kitploit.com/production/public/readmes/7466/47f68372e31a2c24fa49eb71648adbdf8944977db90d645df337125c1bdfa8e3.png">
こちらは比較用の、通常のCobalt Strikeビーコンの結果です。
<img src="https://assets.kitploit.com/production/public/readmes/7466/76e2107589cd2b1b65c846a671aed173aa5e6d89630f5e9d283d99c546a2d566.png">
このツールの出力のエントロピーテレメトリには細心の注意が払われており、その結果、シェルコードのエントロピーは、パッキングモード以外では、ntdll.dllやkernel32.dllなどの典型的なWindows WinAPI DLLよりも低くなっています。エントロピーの比較は次のとおりです。
| ファイル | バイト | エントロピー |
|------|-------|---------|
| `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** |
## モード早見表
| モード | フラグ | 変更内容 |
|------|-------|--------------|
| Default | none | 入力全体をリフトする。すべてが仮想化される。 |
| Packer | `--pack` | リフトしない。入力を暗号化データとしてラップし、実行時に復号してジャンプインする。 |
| Hybrid | `--ranges A:B,...` | 選択されたバイト範囲のみをリフトする。残りはネイティブのまま。 |
| Stacked | `--pack --ranges A:B` | ハイブリッドblobを構築し、その後パックでラップする。 |
| Detour | `--embed-into PE --at RVA` | 事前にビルドしたblobを取得し、PEに埋め込み、選択したRVAにjmpをパッチする。 |
| Scan | `--scan` | 入力のCFGから有効な`--ranges`候補を出力して終了する。 |
| RX | `--rx` | PAGE_EXECUTE_READ blob。データアイランドは保存時は暗号化されたまま。blob内のPEBウォーカーがVirtualProtectを解決し、state_initでその場で復号する。 |
| RX w/ Loader | `--rx --rx-loader-vp` | `--rx` と同じだが、ローダーがVirtualProtectをblobの最初の引数として渡す。PEBウォーカーは不要。 |
すべてのモードは `--seed`、`--arch`、`--input-format`、`--format` を尊重します。ビルドパイプラインとランタイムフローについては、以下の各モードのセクションを参照してください。
## デフォルトの仮想化
リフターはCFG全体を走査し、すべての命令をカスタムIRに変換します。IRは2つの難読化パスを通過し、その後、各insnをシードごとのバイトコード形状にエンコードするコーデックを通過します。ブロックテーブル、ハンドラテーブル、データアイランドは、バイトコードと同じ1バイト単位のストリーム暗号で暗号化されます。実行時にはプロローグがこれら3つの領域をその場で復号し、ディスパッチャループがバイトコードバイトを1つずつ取得し、復号して処理を実行するハンドラにディスパッチします。
### ビルドパイプライン
生のシェルコードを、出力される仮想化blobに変換するためにビルドが実行する手順を、最初から最後まで示します。_以下のすべてのグラフは1.0.0リリースに適用されます。それ以降これらは変更されていますが、考え方は同じです。_```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]
ホストローダーがバイト0で呼び出したときの、生成されたblobの動作。プロローグからディスパッチャのフェッチ、各種終了パスまで。```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]
ランタイムノンスのトリックは、メモリスキャン対策の要となる手法です。ハンドラテーブルは、`rdtsc XOR cipher_init`から導出された値とXORされた状態でメモリ上に存在するため、同じブロブをロードした2つのプロセスは、バイト単位で異なるハンドラテーブルを保持します。ディスパッチャは、ルックアップ時に追加のXORを1回行ってマスクを解除します。
## パッカーモード: `--pack`
逆のトレードオフです。リフターは実行されません。元のシェルコードは暗号化された状態でデータアイランドに格納され、IRは単一の合成`JMP_NATIVE imm=0`になります。プロローグは、デフォルトモードが即座に実行するデータアイランドの復号を意図的にスキップします。代わりに、その唯一のJMP_NATIVEハンドラが最初に発火したとき、データアイランドをその場で復号し、マーカーバイトを設定してから、制御を今や平文となったシェルコードのバイト0へ移します。シェルコードはそこからネイティブに実行されます。
あらゆるシェルコードで動作し、リフターのカバレッジとは無関係です。ステージレスのCobalt、特殊なsyscall多用ペイロード、仮想化するには大きすぎる、または奇妙すぎるものなどに有用です。命令ごとの仮想化防御は失われますが、ラッパー上の完全なシード単位のVMポリモーフィズムは維持されます。
### ビルドパイプライン
`--pack`が、元のシェルコードを単一命令の合成IRプログラムの背後にある暗号化データアイランドとしてどのようにラップするか。```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]
パックラッパーが最初のエントリ時に行う処理。これには、データアイランドのゲート付き遅延復号と、制御を平文化されたシェルコードへ引き渡す単一の合成JMP_NATIVEが含まれる。```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]
遅延データアイランド復号はパックモード専用です。デフォルトモードとハイブリッドモードでは、リフトされたコードが実行途中でデータアイランドのバイトに対してVM LOAD/STOREを実行し、最初から平文である必要があるため、プロローグが即座に処理します。パックモードでは、データアイランドの消費者は合成JMP_NATIVEの後のネイティブエスケープの1つだけなので、復号はその時点まで待機できます。
## ハイブリッドモード: `--ranges A:B,C:D`
対象を絞った仮想化。入力内でリフトすべきバイト範囲を選択します。それ以外は、出力内で元のネイティブバイトのまま残ります。各範囲の開始位置で、リフターは5バイトの `jmp rel32` を、ネイティブシェルコード領域の後に追加された `vm_entry_K` スタブへのパッチに置き換えます。外部のネイティブコードは、パッチされた開始バイトを通じてのみリフトされた範囲に再入できます。範囲中間のバイトはint3で埋められるため、範囲中間のバイトをターゲットとするネイティブブランチはスキャン時に拒否されます。範囲外のバイトへ抜けるリフトされたコードは、`JMP_NATIVE` または `CALL_NATIVE` になります。
まず `--scan` を使用して、対象となる候補を見つけます。スキャンは、ギャップがある範囲、`ret` で終了するブロックがない範囲、本体が5バイト未満の範囲、または範囲中間のバイトへの外部ネイティブブランチがある範囲を、対象ではなくニアミスとして分類します。
### ビルドパイプライン
`--ranges` が選択したバイト範囲のみをリフトし、ネイティブシェルコードをその場でパッチして、制御が追加されたVMエントリスタブにリダイレクトされる仕組み。```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]
ネイティブとVMが混在するblobを通過する制御フロー。ネイティブコードがパッチされた範囲の開始位置に到達したときのVMディスパッチへの入口と、リフトされたコードが分岐して戻るときに使用されるネイティブエスケープパスを含みます。```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
コルーチンスタイルのレンジ(`ret` を持たず、テールジャンプで終了するもの)は、`--scan` 出力の `--coroutines` によって文書化されます。このフラグは現時点ではコード生成を変更しません。レンジモードは自己完結型のリーフ関数に最適です。呼び出し元が指定する特定のレジスタ状態に依存するヘルパー関数は、単独ではリフトできません。そのようなAPIは結局ガベージ引数で呼び出されることになります。
## スタックモード: `--pack --ranges A:B,C:D`
ハイブリッドビルドを実行し、その結果をパックラップします。外部VMは内部のハイブリッドブロブをその場で復号し、そのバイト0にジャンプします。そこからの実行は、選択したレンジのバイトコードを含むブロブ全体が保存時に暗号化される点を除けば、スタンドアロンハイブリッドモードとまったく同じように進行します。内部ブロブが自己完結型のPIC領域であるため、2つの層はきれいに構成されます。
### ビルドパイプライン
スタックモードがパッケージャを再帰的に呼び出す方法: 最初に内部の `--ranges` ビルドを行い、次にそのビルドを外部の `--pack` でラップします。```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]
外側の pack VM が内側の range-mode VM に制御を引き渡す仕組み。それぞれが独立した VMState フレーム上で実行されます。```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
内部VMと外部VMは状態を共有しません。それらは同じブロブ内に存在する独立した2つのVMです。外部VMの唯一の役割は、復号レイヤーの背後に内部VMをゲートすることです。
## Detourモード: `--embed-into target.exe --at RVA`
他のものとは形が異なります。入力は生のシェルコードで、通常は別のモードでmkPIVMが出力したVMブロブですが、任意のPICバイトでも機能します。出力はパッチ適用済みPEです。
このツールは`target.exe`を解析し、実行可能セクション内の選択されたRVAを特定し、そこから5バイトをカバーするのに十分な命令を逆アセンブルし、移動後も生存できないRIP相対アドレッシングまたは相対制御フローが置き換え対象の命令列に含まれている場合は拒否し、その後、ラッパーとVMブロブを含む新しいRWXセクションを追加します。ラッパーは呼び出し元の状態を保存し、VMブロブに転送し、状態を復元し、置き換えられた元のバイトを実行し、パッチの直後のバイトにジャンプして戻ります。選択されたRVAにある5バイトの`jmp rel32`はラッパーを指します。
ラッパーがVMに転送する方法には2つのサブモードがあります:
### Threadedサブモード(デフォルト)
2つの図が続きます。1つ目は、ラッパーセクションを注入し、参照を修正し、選択したRVAに5バイトのjmpを書き込むビルド時のPEパッチ適用パイプラインです。2つ目は、ホストプロセスが最終的にその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]
入力コンテンツが含まれていないため、翻訳するテキストがありません。```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]
スレッド化サブモードは、ステージャーやビーコンに適した形です。ホストのメインスレッドは決してブロックされません。ワーカースレッドは、ペイロードが必要とするライフサイクルをそのまま受け継ぎます。ペイロードが `ExitProcess` を呼び出すとプロセス全体が終了しますが、すべてのC2ビーコンと同様に終了しないペイロードの場合、ホストはペイロードと並行して永久に実行され続けます。
### インラインサブモード: `--detour-inline`
ラッパー構造は同じですが、ラッパーは `CreateThread` の代わりに直接 `call vm_blob` を実行します。ホストのメインスレッドは、VMが戻るまでブロックされます。ターゲットのIATに `CreateThread` が存在しない場合や、スレッド化されたベースリロケーションパスが特定のターゲットに適用できない場合のフォールバックとして有用です。```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出力ファイルはありません。入力シェルコードから CFG を構築し、--ranges の候補を stderr に出力します。各候補は eligible(適格)、coroutine(コルーチン)、near-miss(ニアミス)、internal(内部)のいずれかに分類されます。internal とは、より大きな eligible 候補にすでに覆われている状態を意味します。これを使用して、ハイブリッドモードでツールを再実行する前に range 引数を選択します。```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]
## シードごとの多態化軸
静的シグネチャに与える影響の大きい順に概ね並んでいます。
* 暗号ファミリ。ARX、LcgSub、SBoxAdd、FeistelByte のいずれか。ビルド時に使用するバイトコード暗号化と、ディスパッチャのフェッチ経路で生成されるインライン復号の両方を選択します。暗号化と復号は構造上一致します。
* レジスタスロットレイアウト。VMState には `reg_count` 個のスロットが含まれ、サイズはシードごとに 24 から 32 です。16 個のアーキテクチャ GPR と 4 個の Tmp レジスタは、シードごとに新しいスロットインデックスの順列を取得します。
* オペコードからハンドラへのマッピング。各コーデックファミリには、シードごとにランダムなオペコードバイトが割り当てられます。256 エントリのハンドラテーブルはオペコードでインデックス付けされ、エンコードされたバイトコードはマッピングされたオペコードを参照します。
* ディスパッチャのトポロジ。スレッド型または中央型で、シードごとに選択されます。
* プロローグの自己位置特定戦略。`call $+5; pop`、`lea reg, [rip+0]`、または jmp/call シャッフル。
* ハンドラの一時レジスタ使用。BR_CC ハンドラは、揮発性レジスタプール全体で 6 つの一時的な役割を並べ替えます。Store ハンドラも同様です。
* ジャンクガジェット密度。0 から 3 で、ハンドラ間のジャンクコード生成を制御します。
* IR 難読化パスの決定。デッド IR の注入と不透明述語はそれぞれ独自のサブ乱数生成器を使用するため、他の軸に影響を与えることなくシードごとに決定が決定的になります。
* バイトコード暗号化の初期状態。シードごとにランダムな 64 ビット。
## テスト済みペイロード
このツールは、複数の Cobalt Strike、MSF、Sliver、およびその他多数のシェルコードサンプルに対して検証されました。
## ビルド
Visual Studio 2022、CMake 3.21 以降、および vcpkg が必要です。CMake プロジェクトは、vcpkg のマニフェストモードを通じて Zydis と fmt を取得します。```
cmake -S . -B build
cmake --build build --config Release --target mkpivm
--pack が実際にビーコンする経路です。--embed-into 出力のAuthenticode署名は無効になります。PEチェックサムはゼロにされます。VirtualAlloc呼び出しのコストでRXを購入するものであり、おそらく置き換えるRWXページよりも悪いシグネチャになるからです。--pack + --ranges を推奨します。これは本当にクールなものです。現実を見ましょう。新しいアイデアを提供したい、自分のバグを修正したい、私に修正してもらうためのIssueを提出したい、などがあれば、どうぞご自由に。