病毒分析
点击放大


<p align="center">
<img src="./images/mkpivm.png" width="30%" height="30%">
<br>
<a href="./research/mkpi.pdf">Read</a> the research paper (written for 1.0.0).
<p align="center">
mkPI ha fully undetected since the release of this project.
</p>
</p>
mkPIVM is a polymorphic position-independent shellcode for Windows x86 and x64 (Linux soon).
Feed it raw shellcode. It emits another raw blob: a small machine that interprets a lifted, encrypted-at-rest of your original instructions. The output is itself position-independent code and runs anywhere the original shellcode would, from a remote-thread loader to a code ca. Every per-seed knob independently: cipher family, register slot layout, opcode-to-handler permutation, dispatcher topology, junk-gadget pattern, IR obfuscation insertion points. Two builds from the same input share fewer than a hundred coincidental bytes out of tens of kilobytes.
Why: nati is signature-trivial. Wrapping it in a per-instance VM with a per-instance cipher lea useful at rest, and lifting the instructions to bytecode puts another wall between disk bytes and any disassembler that knows what x86 looks like. As far as I can tell from a literature sweep, no public tool ships exactly this pipeline: raw PIC in, raw polymorphic VM PIC out. So, I mentioned that in the research paper it demanded. To be honest, if I am right about no one having done this (publicly) before, and I am pretty confident, I am surprised. _Nonetheless, enjoy._
Your PIVM is hot and ready. That's the simplest path. Several other modes vary how aggressively the original instructions get , whether the output is a standalone blob or a patched PE, and whether the lift runs at all.
I got the receipts. You can see a video of mkPIVM in action below, fully a Meterpreter stager ( btw), injecting into explorer.exe, and us capturing a callback. Of course this is just an example, and mkPIVM can be applied to much more, assuming the instructions in the shellcode are supported. If they aren't, make an Issue, send me the shellcode, I got you.
See it here. Hosted in ./media, can't embed sadly.
Here is the report for that exact sample (as of 08/14/2026).
<img src="./images/vt.png">
...and the packed , not e, notably higher entropy.
<img src="./images/packed.png">
Here is a normal Cobalt Strike beacon's results for comparison.
<img src="./images/normal_beacon.png">
There was careful attention paid to the entropy telemetry of the output of this tool, which results in shellcode of entropy less than typical Windows WinAPI DLLs (outside of packing mode), such as ntdll.dll or kernel32.dll. The entropy comparison is about...
| File | Bytes | Entropy |
|------|-------|---------|
| 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 |
| Mode | Flags | What changes |
|------|-------|--------------|
| Default | none | Lift the whole input. E . |
| Packer | --pack | Don't lift. Wrap input as encrypted data, decrypt at runtime, jump in. |
| Hybrid | --ranges A:B,... | Lift only the chosen byte ranges. Rest stays native. |
| Stacked | --pack --ranges A:B | Build the hybrid blob, then pack-wrap it. |
| Detour | --embed-into PE --at RVA | Take a pre-built blob, embed into a PE, patch a jmp at the chosen RVA. |
| Scan | --scan | Print eligible --ranges candidates from the input's CFG, then exit. |
| RX | --rx | PAGE_EXECUTE_READ blob. Data island stays encrypted at rest; in-blob PEB walker resol, decrypts in-place at state_init. |
| RX w/ Loader | --rx --rx-loader-vp | Like --rx but your loader passes in as the blob's first arg. No PEB walker. |
Every mode honors --seed, --arch, --input-format, and --format. See the per-mode sections below for the build pipeline and the runtime flow.
The lifter walks the whole CFG and lowers every instruction to a custom IR. The IR goes through two obfuscation passes, then through codecs that encode each insn into the per-seed bytecode shape. The block table, handler table, and data island are encrypted with the same per-byte stream cipher as the bytecode. At runtime the prologue decrypts those three regions in place and the dispatcher loop fetches bytecode bytes one at a time, decrypting and dispatching to a handler that does the work.
Steps the build performs to turn raw shellcode into the emitted blob, end to end. _All graphs below apply to the 1.0.0 release, these ha since then, but the idea is the same._
What the emitted blob does when the host loader calls into it at byte 0, from prologue through dispatcher fetches and the termination paths.
The runtime nonce trick is the key anti-memory-scan move: the handler table sits in memory XOR'd with a value derived from rdtsc XOR cipher_init, so two processes loading the same blob hold byte-different handler tables. The dispatcher unmasks at lookup time with one extra XOR.
--packThe opposite tradeoff. The lifter does not run. The original shellcode goes into the data island encrypted, and the IR is a single synthetic JMP_NATIVE imm=0. The prologue intentionally skips the data-island decrypt that default mode runs eagerly. Instead, the first time the lone JMP_NATI fires, it decrypts the data island in place, sets a marker byte, then transfers control to byte 0 of the now-plaintext shellcode. The shellcode runs natively from there.
Works on any shellcode, regardless of lifter co. Useful for stageless cobalt, exotic syscall-hea, anything too large or weird to . Loses the per-instruction defense but keeps the full per-seed on the wrapper.
How --pack wraps the original shellcode as an encrypted data island behind a one-instruction synthetic IR program.
What the pack wrapper does on first entry, including the gated lazy decrypt of the data island and the single synthetic JMP_NATIVE that hands control to the now-plaintext shellcode.
The deferred data-island decrypt is pack-mode-only. In default and hybrid mode the lifted code does VM LOAD/STORE on data-island bytes mid-execution and needs them plaintext from the start, so the prologue handles it eagerly. In pack mode there is exactly one consumer of the data island, the nati after the synthetic JMP_NATIVE, so the decrypt can wait until then.
--ranges A:B,C:DTargeted . Pick byte ranges in the input that should be lifted. E else stays as the original nati in the output. At each range start the lifter patches a 5-byte jmp rel32 to a stub appended after the nati region. External native code may only re-enter a lifted range through the patched start byte. Mid-range bytes are int3-filled, so any nati that targets a mid-range byte is rejected at scan time. Lifted code that exits a range to bytes outside it becomes JMP_NATIVE or CALL_NATIVE.
Use --scan first to find eligible candidates. Scan classifies ranges with gaps, with no ret-terminated block, with body shorter than 5 bytes, or with external nati into mid-range bytes as near-miss rather than eligible.
How --ranges lifts only the chosen byte ranges and patches the nati in place so control redirects into the appended stubs.
Control flow through a mixed nati blob, including the entry into when native code hits a patched range start and the nati paths used when lifted code branches back out.
Coroutine-style ranges, the ones with no ret that exit via a tail-jmp, are documented via --coroutines for --scan output. The flag does not change codegen at present. Range mode is best fit for self-contained leaf functions. Helper functions that depend on a specific caller-supplied register state cannot be lifted standalone; the API ends up called with garbage args.
--pack --ranges A:B,C:DRun the hybrid build, then pack-wrap the result. The outer the inner hybrid blob in place and jumps to byte 0 of it. From there execution proceeds exactly like standalone hybrid mode, except the entire blob including the chosen-range bytecode is encrypted at rest. The two layers compose cleanly because the inner blob is a self-contained PIC region.
How stacked mode recursively invokes the packager: an inner --ranges build first, then an outer --pack wrap of that build.
How the outer pack control to the inner range-mode VM, each running on its own independent frame.
The inner VM and outer no state. They are two independent VMs that happen to live in the same blob. The outer one's only job is to gate the inner one behind a decryption layer.
--embed-into target.exe --at RVADifferent shape from the others. Input is raw shellcode, con a VM blob already emitted by mkPIVM in another mode, though any PIC bytes will work. Output is a patched PE.
The tool parses target.exe, locates the chosen RVA in an executable section, disassembles enough instructions there to cover 5 bytes, refuses if the displaced run contains RIP-relati or relati flow that cannot survi moved, then adds a fresh RWX section containing a wrapper plus the VM blob. The wrapper preser state, transfers to the VM blob, restores state, executes the displaced original bytes, and jumps back to the byte after the patch. A 5-byte jmp rel32 at the chosen R at the wrapper.
Two sub-modes for how the wrapper transfers to the VM:
Two diagrams follow. The first is the build-time PE patching pipeline that injects the wrapper section, fixes up references, and writes the 5-byte jmp at the chosen RVA. The second is the runtime control flow when the host process e reaches that RVA.
The threaded sub-mode is the right shape for stagers and beacons. Host main ne. The worker thread inherits whate the payload needs. If the payload calls ExitProcess, the whole process dies, but for a non-terminating payload like every C2 beacon, the host runs forever in parallel with it.
--detour-inlineSame wrapper structure but the wrapper does a direct call instead of CreateThread. Host main thread blocks until the . Useful when the target lacks CreateThread in its IAT, or as a fallback when the threaded base-reloc path cannot apply against a particular target.
--scanNo output file. Builds the CFG from the input shellcode and prints --ranges candidates to stderr. Each candidate is classified as eligible, coroutine, near-miss, or internal, where internal means shadowed by a larger eligible candidate that already covers it. Use this to pick range arguments before in the tool again in hybrid mode.
Listed roughly in order of how much they affect the static signature.
* Cipher family. One of ARX, LcgSub, SBoxAdd, FeistelByte. Picks both the bytecode encryption used at build time and the inline decrypt emitted in the dispatcher fetch path. Encrypt and decrypt match by construction.
* Register slot layout. The contains reg_count slots, sized 24 to 32 per seed. The 16 architectural GPRs plus 4 Tmp regs get a fresh permutation of slot indices each seed.
* Opcode-to-handler mapping. Each codec family is assigned a random opcode byte each seed. The 256-entry handler table indexes by opcode; the encoded bytecode references the mapped opcodes.
* Dispatcher topology. Threaded or central, picked per seed.
* Prologue self-locate strategy. call $+5; pop, lea reg, [rip+0], or a jmp/call shuffle.
* Handler register temporaries. BR_CC handler permutes its 6 temporary roles across the pool. Store handler does the same.
* Junk gadget density. 0 to 3, controls inter-handler garbage emission.
* IR obfuscation pass decisions. Dead-IR injection and opaque predicates each use their own sub-RNG so their decisions are deterministic per seed without disturbing other axes.
* Bytecode encryption initial state. Random 64-bit per seed.
This tool was against several Cobalt Strike, MSF, Sliver, and numerous other shellcode samples.
Requires Studio 2022, CMake 3.21 or newer, and vcpkg. The CMake project pulls Zydis and fmt through vcpkg's manifest mode.
* Range mode for cobalt stagers does not work as a standalone leaf. The stager's helper functions depend on caller-supplied register state that the runner does not provide. Full or --pack are the routes that actually beacon.
* x86 threaded detour requires the target to either lack ASLR or accept added base relocation entries, which the tool emits when present. If the target's BASERELOC data dir is malformed or absent, the tool falls back to inline mode.
* The lifter does not currently cover SSE/A moves, atomics, CMPXCHG, RDMSR, or pri instructions. Pack mode is the workaround for shellcodes using those.
* Authenticode signatures on --embed-into output are in. The PE checksum is zeroed.
* Output blobs require RWX at runtime because in-place decryption writes back to the blob's own pages. Most loaders that run shellcode allocate RWX anyway. There is no plan to move to RX-only because the trade buys RX at the cost of a PEB walk and a call, which is probably a worse signature than the RWX page it replaces.
* stageless payloads mode is not supported yet, as they are horribly complicated to wrap in the --pack + --ranges.
开发语言:C++
Star 数:387
该项目属于病毒分析方向的开源资源,代码结构清晰,适合安全研究人员学习参考。项目围绕Generate polymorphic, position-independent virtual machines (PIVMs) from arbitrary x86/x64 shellcode.展开,提供了可运行的实现,对理解相关安全机制、工具原理与攻防技术具有较高的研究价值。
从技术实现角度看,此类项目的核心在于对底层原理的深入理解与工程化落地。开发者在研究过程中可以结合官方文档与社区讨论,梳理项目的初始化流程、核心模块职责与数据流转,逐步掌握其中的技术细节与设计思路。
1. 下载源码压缩包,解压到本地环境。
2. 根据项目类型准备对应的运行环境与依赖。
3. 按照 README 中的步骤完成配置与部署。
4. 在隔离的测试环境中运行验证,避免影响生产系统。
本资源来源于开源社区,仅供安全研究、技术学习与合法授权范围内的攻防演练使用,请勿用于任何非法或侵害他人权益的用途。使用本项目产生的一切后果由使用者自行承担。
| 资源标题 | mkPIVM - Generate polymorphic, position-independent virtual |
| 所属分类 | 病毒分析 |
| 发布作者 | admin |
| 发布时间 | 2026-10-05 23:21:02 |
| 下载权限 | 免费下载 |
| 附件数量 | 2 个下载链接 |
| 浏览次数 | 369 |
| 讨论数量 | 0 |
| 主题状态 | 正常讨论中 |