D7EAD/mkPIVM

GitHub: D7EAD/mkPIVM

mkPIVM 通过生成多态虚拟机来包装 x86/x64 壳码,以增强隐蔽性并逃避安全检测。

Stars: 4 | Forks: 0


Read the research paper.

**mkPIVM** 是一款针对 Windows x86 和 x64 的多态位置无关壳码虚拟化工具。 输入原始壳码,它会输出另一段原始数据块:一个小型虚拟机,用于解释你原始指令的一个经提取、静态加密的版本。输出本身是位置无关的代码,可以在原始壳码运行的任何地方运行,从远程线程加载器到代码洞跳转。每个种子对应的所有参数独立变化:密码族、寄存器槽布局、操作码到处理器的置换、调度器拓扑、垃圾指令模式、IR 混淆插入点。来自同一输入的两次构建,在数十千字节中共享的巧合字节少于一百个。 原因:原生壳码的特征签名非常简单。将其包装在每实例的 VM 和每实例的密码中,使得静态下不留任何有用信息,而将指令提升为字节码,则在磁盘字节与任何懂得 x86 结构的反汇编器之间又增加了一道屏障。据我通过文献调研所知,没有公开工具提供完全相同的处理流程:原始 PIC 进入,原始多态 VM PIC 输出。因此,我在其要求的研究论文中提到了这一点。老实说,如果我是对的(我相当确信)之前没有人(公开地)做过这件事,我会感到惊讶。_尽管如此,请享用。_ ## 快速开始 ``` mkpivm.exe shellcode.bin --arch x64 -o out.bin ``` 你的 PIVM 已就绪。这是最简单的路径。还有其他几种模式,它们在虚拟化原始指令的激进程度、输出是独立的数据块还是打过补丁的 PE,以及是否运行提升等方面有所不同。 # 展示 我有证据为证。你可以在下面看到 mkPIVM 运行的视频,它完整虚拟化了一个 Meterpreter 载荷(原版的),注入到 explorer.exe 中,我们捕获了回调。当然,这只是一个示例,mkPIVM 可以应用到更多场景,前提是壳码中的指令受支持。如果不受支持,请提交 Issue,把壳码发给我,我来处理。 可以在这里查看视频。托管在 ./media,很遗憾无法嵌入。 这是那个虚拟化样本的 VirusTotal 报告(截至 2026 年 5 月 17 日)。 ...以及打包版本(甚至未虚拟化),熵值明显更高。 该工具的输出熵值受到了精心控制,使得生成的壳码熵值低于典型的 Windows WinAPI DLL(在非打包模式下),如 ntdll.dll 或 kernel32.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** | ## 模式概览 | 模式 | 标志 | 变化 | |------|-------|--------------| | 默认 | 无 | 提升整个输入。所有内容均被虚拟化。 | | 打包 | `--pack` | 不提升。将输入包装为加密数据,运行时解密,然后跳入执行。 | | 混合 | `--ranges A:B,...` | 仅提升选定的字节范围。其余部分保持原生。 | | 堆叠 | `--pack --ranges A:B` | 构建混合数据块,然后用打包模式包装它。 | | 跳转 | `--embed-into PE --at RVA` | 获取预构建的数据块,嵌入到 PE 中,在选定的 RVA 处打补丁跳转。 | | 扫描 | `--scan` | 打印输入 CFG 中符合条件的 `--ranges` 候选项,然后退出。 | 每种模式都支持 `--seed`、`--arch`、`--input-format` 和 `--format`。请参阅下方的各模式章节了解构建流程和运行时流程。 ## 默认虚拟化 提升器遍历整个 CFG 并将每条指令降低为自定义 IR。IR 经过两次混淆处理,然后通过编解码器将每条指令编码为每种子对应的字节码形式。块表、处理器表和数据岛使用与字节码相同的每字节流密码进行加密。在运行时,序言就地解密这三个区域,调度器循环每次获取一个字节码字节,解密后分派给执行实际工作的处理器。 ### 构建流程 构建执行的步骤,将原始壳码转换为发出的虚拟化数据块,端到端。 ``` 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 调用发出的数据块时,它所执行的操作,从序言到调度器获取以及各种终止路径。 ``` 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] ``` 运行时 nonce 技巧是关键的反内存扫描手段:处理器表存储在内存中,并与一个从 `rdtsc XOR cipher_init` 推导出的值进行异或,因此两个加载相同数据块的进程持有字节不同的处理器表。调度器在查找时通过一次额外的异或操作解掩码。 ## 打包模式:`--pack` 相反的权衡。不运行提升器。原始壳码作为加密数据存入数据岛,IR 是单条合成的 `JMP_NATIVE imm=0`。序言故意跳过默认模式中主动执行的数据岛解密。相反,当那条唯一的 JMP_NATIVE 处理器首次触发时,它就地解密数据岛,设置一个标记字节,然后将控制权转移到当前已明文的壳码的字节 0 处。壳码随后原生执行。 适用于任何壳码,不受提升器覆盖率限制。适用于无阶段的 Cobalt 载荷、涉及大量系统调用的奇特载荷、任何太大或太奇怪而无法虚拟化的载荷。失去了逐指令虚拟化的防御优势,但保留了包装器完整的每种子 VM 多态性。 ### 构建流程 `--pack` 如何将原始壳码包装为加密数据岛,置于一个单指令合成 IR 程序之后。 ``` 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。 ``` 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 之后的原生逃离路径,因此解密可以等到那时再进行。 ## 混合模式:`--ranges A:B,C:D` 目标虚拟化。在输入中选取需要提升的字节范围。输出中所有其他内容保持为原始原生字节。在每个范围的起始处,提升器打上一个 5 字节的 `jmp rel32` 补丁,跳转到附加在原生壳码区域之后的 `vm_entry_K` 桩代码。外部原生代码只能通过打过补丁的起始字节重新进入已提升的范围。范围内的中间字节用 int3 填充,因此任何指向范围中间字节的原生分支在扫描时都会被拒绝。从已提升范围跳出到范围外字节的代码会变成 `JMP_NATIVE` 或 `CALL_NATIVE`。 建议先使用 `--scan` 查找符合条件的候选项。扫描会将有间隔、没有 `ret` 结尾的块、主体短于 5 字节或有外部原生分支指向范围中间字节的范围分类为“接近符合”而不是“符合条件”。 ### 构建流程 `--ranges` 如何仅提升选定的字节范围,并就地修补原生壳码,使控制流重定向到附加的 VM 入口桩代码。 ``` 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 数据块的控制流,包括当原生代码命中打过补丁的范围起始处时进入 VM 调度,以及当提升的代码分支跳出时使用的原生逃离路径。 ``` 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 区域。 ### 构建流程 堆叠模式如何递归调用打包器:首先进行一次内部 `--ranges` 构建,然后对该构建进行一次外部 `--pack` 包装。 ``` 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] ``` ### 运行时流程 外部打包 VM 如何将控制权交给内部范围模式 VM,每个都在其独立的 VMState 帧上运行。 ``` 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 不共享状态。它们是恰好位于同一数据块中的两个独立 VM。外部 VM 的唯一职责是通过一个解密层来门控内部 VM。 ## 跳转模式:`--embed-into target.exe --at RVA` 与其他模式形式不同。输入是原始壳码,通常是 mkPIVM 在另一种模式下已发出的 VM 数据块,不过任何 PIC 字节都可以。输出是打过补丁的 PE。 工具解析 `target.exe`,在可执行节中定位选定的 RVA,反汇编足够覆盖 5 字节的指令,如果被替换的指令序列包含无法在移动后存活的 RIP 相对寻址或相对控制流,则拒绝,然后添加一个新的 RWX 节,包含一个包装器和 VM 数据块。包装器保存调用者状态,转移到 VM 数据块,恢复状态,执行被替换的原始字节,然后跳回补丁之后的字节。选定 RVA 处的 5 字节 `jmp rel32` 指向包装器。 包装器转移到 VM 有两种子模式: ### 线程子模式(默认) 下面有两个图。第一个是构建时 PE 打补丁流程,注入包装器节,修复引用,并在选定的 RVA 处写入 5 字节 jmp。第二个是当宿主进程最终到达该 RVA 时的运行时控制流。 ``` 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] ``` ``` 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` 相同的包装器结构,但包装器执行直接的 `call vm_blob` 而不是 `CreateThread`。宿主主线程阻塞直到 VM 返回。当目标的 IAT 中缺少 `CreateThread`,或作为线程化基础重定位路径无法应用于特定目标时的后备方案,非常有用。 ``` 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` 候选项打印到标准错误。每个候选项被分类为“符合条件”、“协程”、“接近符合”或“内部”,其中“内部”指被一个更大的、已覆盖它的符合条件候选项所遮蔽。用此模式在再次调用工具进入混合模式前选取范围参数。 ``` 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 ``` ## 已知限制 * 范围模式用于 Cobalt 载荷阶段器时,不能作为独立的叶函数工作。阶段器的辅助函数依赖于运行器未提供的调用者提供的寄存器状态。完整虚拟化或 `--pack` 是实际能够建立信标的途径。 * x86 线程化跳转要求目标要么缺乏 ASLR,要么接受添加的基础重定位条目,工具在存在时会发出。如果目标的 BASERELOC 数据目录格式错误或缺失,工具会回退到内联模式。 * 提升器目前不覆盖 SSE/AVX 寄存器移动、原子操作、CMPXCHG、RDMSR 或特权指令。对于使用这些指令的壳码,打包模式是解决方法。 * `--embed-into` 输出上的 Authenticode 签名会失效。PE 校验和被清零。 * 输出数据块在运行时需要 RWX 权限,因为就地解密会写回数据块自身的页面。大多数运行壳码的加载器无论如何都会分配 RWX。没有计划迁移到仅 RX,因为该权衡通过 PEB 遍历和 `VirtualAlloc` 调用来获得 RX,这可能比它替换的 RWX 页面具有更明显的特征。 * 通过默认模式虚拟化无阶段载荷目前不受支持,因为它们极其复杂地包装到 VM 中;推荐使用 `--pack + --ranges`。 # 备注 * 该项目主要是一个概念验证研究。如果反响良好,我将应要求扩展它并欢迎贡献。然而,对于测试样本它似乎是稳定的。 * 如果你的壳码无法工作,并且你不想将其放在 Issue 中,那么很遗憾我无法帮助你。这已在 Sliver、Cobalt Strike 4.12、MSF、Havoc 和一些其他未公开的样本上进行了测试。 * 在我的测试中,将 VM 注入到活动进程运行良好。然而,在嵌入 PE 方面,这并未在像 MS Word 这样的商业软件中进行测试,仅进行了合成测试,但可能有效。如果不行,我会修复。
标签:Bash脚本, DNS 反向解析, mkPIVM, PIVM, Shellcode虚拟化, Windows平台, x64架构, x86架构, 代码混淆, 代码解释, 位置无关代码, 加密, 反检测, 多态代码, 恶意软件, 指令提升, 漏洞扫描器, 网络安全, 虚拟机技术, 邮件伪造, 隐私保护