feat: Initial implementation of Ryujin MiniVM virtualization

- Ryujin can now locate candidate instructions, convert them into VM bytecode, and insert the MiniVM entry point to enable execution of these bytecodes.
- Minor bug fixes.
This commit is contained in:
keowu
2025-06-15 18:23:55 -03:00
parent b1b309d32f
commit 3a3a92f7ca
3 changed files with 427 additions and 46 deletions

View File

@@ -133,37 +133,6 @@ void RyujinObfuscationCore::obfuscateIat() {
*/
if (m_unusedRegisters.size() == 0) return;
auto findBlockId = [&](ZyanU8 uopcode, ZyanI64 value) -> std::pair<int, int> {
int block_id = 0;
int opcode_id = 0;
for (auto& block : m_proc.basic_blocks) {
opcode_id = 0;
for (auto& opcode : block.opcodes) {
auto data = opcode.data();
auto size = opcode.size();
if (data[0] == uopcode) //0xFF ?
if (std::memcmp(&*(data + 2), &value, sizeof(uint32_t)) == 0) // Is it the same memory immediate?
return std::make_pair(block_id, opcode_id);
opcode_id++;
}
block_id++;
}
return std::make_pair(-1, -1);
};
for (auto& block : m_obfuscated_bb) {
for (auto& instr : block.instructions) {
@@ -171,7 +140,7 @@ void RyujinObfuscationCore::obfuscateIat() {
if (instr.instruction.info.meta.category == ZYDIS_CATEGORY_CALL && instr.instruction.operands->type == ZYDIS_OPERAND_TYPE_MEMORY) {
// Finding the block info related to the obfuscated opcode
auto block_info = findBlockId(instr.instruction.info.opcode, instr.instruction.operands->mem.disp.value);
auto block_info = findBlockId(instr.instruction.info.opcode, instr.instruction.operands->mem.disp.value, 2, sizeof(uint32_t));
// Call to an invalid IAT in the list of basic blocks
if (block_info.first == -1 || block_info.second == -1) continue;
@@ -438,7 +407,388 @@ void RyujinObfuscationCore::insertJunkCode() {
void RyujinObfuscationCore::insertVirtualization() {
//TODO
/*
1 - Converter instru<72><75>es do procedimento e seus basic blocks para o bytecode da VM(cada instru<72><75>o gera 8 bytes de bytecode).
2 - Substiuir a instru<72><75>o por uma call para a rotina de interpreta<74><61>o da VM e passar os bytecodes via RCX. (levar em considera<72><61>o o salvamento dos contextos de registradore e stack)
3 - Ser capaz de continuar a execu<63><75>o sem problemas integrando a rotina da VM com o c<>digo original a ser executado e n<>o ofuscado
4 - Essa rotina deve inserir a stub e bytecode da vm apenas. ap<61>s isso teremos um processamento antes de salvar e corrigir reloca<63><61>es
para sermos capazes de encontrar o padr<64>o da rotina de virtualiza<7A><61>o e colocar o endere<72>o real do interpretador da VM para que a mesma funcione.
Basicamente essa <20> uma single-vm que:
Analisa a instru<72><75>o em quest<73>o. extrair seu opcode e mapear para o da vm, extrair seus immediatos e armazenala em um unico conjunto
exemplo:
0x48 -> mov -> bytecode
rbx -> bytecode
10 -> valor
Exemplo de sa<73>da:
0x112210
Que sera atribuido ao valor de rcx:
push rcx
mov rcx, 112210h
call vmentry(mas um valor simbolico visto que n<>o seria inserido o offset immediato aqui)
-> rax resultado vai no registrado em quest<73>o que continuaria o fluxo de execu<63><75>o ou receberia o resultado, nesse exemplo: rbx
pop rcx
Dessa forma o c<>digo continuaria
*/
// <20> uma instru<72><75>o candidata a ser virtualizada pela minivm ??
auto isValidToSRyujinMiniVm = [&](RyujinInstruction instr) {
return instr.instruction.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && instr.instruction.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE;
};
// Vamos mapear o registrador do Zydis para o ASMJIT
auto mapZydisToAsmjitGp = [&](ZydisRegister zydisReg) -> asmjit::x86::Gp {
switch (zydisReg) {
// RAX family
case ZYDIS_REGISTER_AL:
case ZYDIS_REGISTER_AH:
case ZYDIS_REGISTER_AX:
case ZYDIS_REGISTER_EAX:
case ZYDIS_REGISTER_RAX: return asmjit::x86::rax;
// RBX family
case ZYDIS_REGISTER_BL:
case ZYDIS_REGISTER_BH:
case ZYDIS_REGISTER_BX:
case ZYDIS_REGISTER_EBX:
case ZYDIS_REGISTER_RBX: return asmjit::x86::rbx;
// RCX family
case ZYDIS_REGISTER_CL:
case ZYDIS_REGISTER_CH:
case ZYDIS_REGISTER_CX:
case ZYDIS_REGISTER_ECX:
case ZYDIS_REGISTER_RCX: return asmjit::x86::rcx;
// RDX family
case ZYDIS_REGISTER_DL:
case ZYDIS_REGISTER_DH:
case ZYDIS_REGISTER_DX:
case ZYDIS_REGISTER_EDX:
case ZYDIS_REGISTER_RDX: return asmjit::x86::rdx;
// RSI family
case ZYDIS_REGISTER_SIL:
case ZYDIS_REGISTER_SI:
case ZYDIS_REGISTER_ESI:
case ZYDIS_REGISTER_RSI: return asmjit::x86::rsi;
// RDI family
case ZYDIS_REGISTER_DIL:
case ZYDIS_REGISTER_DI:
case ZYDIS_REGISTER_EDI:
case ZYDIS_REGISTER_RDI: return asmjit::x86::rdi;
// RBP family
case ZYDIS_REGISTER_BPL:
case ZYDIS_REGISTER_BP:
case ZYDIS_REGISTER_EBP:
case ZYDIS_REGISTER_RBP: return asmjit::x86::rbp;
// RSP family
case ZYDIS_REGISTER_SPL:
case ZYDIS_REGISTER_SP:
case ZYDIS_REGISTER_ESP:
case ZYDIS_REGISTER_RSP: return asmjit::x86::rsp;
// R8 family
case ZYDIS_REGISTER_R8B:
case ZYDIS_REGISTER_R8W:
case ZYDIS_REGISTER_R8D:
case ZYDIS_REGISTER_R8: return asmjit::x86::r8;
// R9 family
case ZYDIS_REGISTER_R9B:
case ZYDIS_REGISTER_R9W:
case ZYDIS_REGISTER_R9D:
case ZYDIS_REGISTER_R9: return asmjit::x86::r9;
// R10 family
case ZYDIS_REGISTER_R10B:
case ZYDIS_REGISTER_R10W:
case ZYDIS_REGISTER_R10D:
case ZYDIS_REGISTER_R10: return asmjit::x86::r10;
// R11 family
case ZYDIS_REGISTER_R11B:
case ZYDIS_REGISTER_R11W:
case ZYDIS_REGISTER_R11D:
case ZYDIS_REGISTER_R11: return asmjit::x86::r11;
// R12 family
case ZYDIS_REGISTER_R12B:
case ZYDIS_REGISTER_R12W:
case ZYDIS_REGISTER_R12D:
case ZYDIS_REGISTER_R12: return asmjit::x86::r12;
// R13 family
case ZYDIS_REGISTER_R13B:
case ZYDIS_REGISTER_R13W:
case ZYDIS_REGISTER_R13D:
case ZYDIS_REGISTER_R13: return asmjit::x86::r13;
// R14 family
case ZYDIS_REGISTER_R14B:
case ZYDIS_REGISTER_R14W:
case ZYDIS_REGISTER_R14D:
case ZYDIS_REGISTER_R14: return asmjit::x86::r14;
// R15 family
case ZYDIS_REGISTER_R15B:
case ZYDIS_REGISTER_R15W:
case ZYDIS_REGISTER_R15D:
case ZYDIS_REGISTER_R15: return asmjit::x86::r15;
default: break;
}
};
// Vamos traduzir uma instru<72><75>o para o bytecode da MiniVm do Ryujin
auto translateToMiniVmBytecode = [&](ZydisRegister reg, ZyanU8 op, ZyanU64 value) {
ZyanU64 miniVmByteCode = 0;
switch (reg) {
case ZYDIS_REGISTER_EAX:
case ZYDIS_REGISTER_RAX: {
miniVmByteCode = 0x33; // reg = RAX
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_RBX: {
miniVmByteCode = 0x34; // reg = RBX
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_RCX: {
miniVmByteCode = 0x35; // reg = RCX
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_RDX: {
miniVmByteCode = 0x36; // reg = RDX
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_RSI: {
miniVmByteCode = 0x37; // reg = RSI
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_RDI: {
miniVmByteCode = 0x38; // reg = RDI
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_RBP: {
miniVmByteCode = 0x39; // reg = RBP
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_RSP: {
miniVmByteCode = 0x40; // reg = RSP
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R8: {
miniVmByteCode = 0x41; // reg = R8
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R9: {
miniVmByteCode = 0x42; // reg = R9
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R10: {
miniVmByteCode = 0x43; // reg = R10
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R11: {
miniVmByteCode = 0x44; // reg = R11
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R12: {
miniVmByteCode = 0x45; // reg = R12
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R13: {
miniVmByteCode = 0x46; // reg = R13
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R14: {
miniVmByteCode = 0x47; // reg = R14
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
case ZYDIS_REGISTER_R15: {
miniVmByteCode = 0x48; // reg = R15
miniVmByteCode <<= 8;
miniVmByteCode |= op; // OP TYPE
miniVmByteCode <<= 8;
miniVmByteCode |= value; // valor
break;
}
default: break;
}
return miniVmByteCode;
};
// Inicializando o runtime do asmjit
asmjit::JitRuntime runtime;
for (auto& block : m_proc.basic_blocks) {
for (auto& instr : block.instructions) {
// Vector para armazenarmos os opcodes da MiniVm do Ryujin
std::vector<ZyanU8> minivm_enter;
// Operand type
ZyanU8 opType = 0;
// Encontrando o block info para o opcode atual
auto block_info = findBlockId(instr.instruction.info.opcode, instr.instruction.operands[1].imm.value.u, 2, sizeof(unsigned char));
// Caso n<>o encontremos
if (block_info.first == -1 || block_info.second == -1) continue;
// Recuperando os opcodes originais desta instru<72><75>o ao qual trabalhamos
auto& data = m_proc.basic_blocks[block_info.first].opcodes[block_info.second];
// Verificando por operands candidatos a serem virtualizados pela minivm
if (instr.instruction.info.mnemonic == ZYDIS_MNEMONIC_ADD && isValidToSRyujinMiniVm(instr)) opType = 1;
else if (instr.instruction.info.mnemonic == ZYDIS_MNEMONIC_SUB && isValidToSRyujinMiniVm(instr)) opType = 2;
else if (instr.instruction.info.mnemonic == ZYDIS_MNEMONIC_IMUL && isValidToSRyujinMiniVm(instr)) opType = 3;
else if (instr.instruction.info.mnemonic == ZYDIS_MNEMONIC_DIV && isValidToSRyujinMiniVm(instr)) opType = 4;
//Existe um VM Operator novo ?
if (opType != 0) {
// Inicializando para o asmjit gerar as instru<72><75>es de nossa minivm
asmjit::CodeHolder code;
code.init(runtime.environment());
asmjit::x86::Assembler a(&code);
a.push(asmjit::x86::rcx);
a.mov(asmjit::x86::rcx, translateToMiniVmBytecode(instr.instruction.operands[0].reg.value, opType, instr.instruction.operands[1].imm.value.u));
a.mov(asmjit::x86::rax, 0x8888888888888888); // Endere<72>o a ser substituido pelo endere<72>o da nossa minivm
a.call(asmjit::x86::rax);
a.mov(mapZydisToAsmjitGp(instr.instruction.operands[0].reg.value), asmjit::x86::rax);
a.pop(asmjit::x86::rcx);
auto& opcodeBuffer = code.sectionById(0)->buffer();
const auto pOpcodeBuffer = opcodeBuffer.data();
minivm_enter.reserve(opcodeBuffer.size());
// Armazenando cada opcocde individual no nosso vector da minivm
for (auto i = 0; i < opcodeBuffer.size(); ++i) minivm_enter.push_back(static_cast<ZyanU8>(pOpcodeBuffer[i]));
// Sobrescrevendo opcodes antigos pelos novos
data.assign(minivm_enter.begin(), minivm_enter.end());
std::printf("[!] Inserting a new MiniVm on %s\n", instr.instruction.text);
}
}
}
}
@@ -458,21 +808,21 @@ BOOL RyujinObfuscationCore::Run() {
//Update basic blocks view based on the new obfuscated
this->updateBasicBlocksContext();
//Obfuscate IAT for the configured procedures
if (m_config.m_isIatObfuscation) {
if (m_config.m_isVirtualized) {
// Obfuscate IAT
obfuscateIat();
// Insert Virtualization
insertVirtualization();
//Update our basic blocks context to rely 1-1 for the new obfuscated opcodes.
this->updateBasicBlocksContext();
}
if (m_config.m_isVirtualized) {
//Obfuscate IAT for the configured procedures
if (m_config.m_isIatObfuscation) {
// Insert Virtualization
insertVirtualization();
// Obfuscate IAT
obfuscateIat();
//Update our basic blocks context to rely 1-1 for the new obfuscated opcodes.
this->updateBasicBlocksContext();

View File

@@ -32,6 +32,37 @@ private:
std::vector<uint8_t> fix_branch_near_far_short(uint8_t original_opcode, uint64_t jmp_address, uint64_t target_address);
uint32_t findOpcodeOffset(const uint8_t* data, size_t dataSize, const void* opcode, size_t opcodeSize);
inline std::pair<int, int> findBlockId(ZyanU8 uopcode, ZyanI64 value, ZyanU8 idx, ZyanU8 szData) {
int block_id = 0;
int opcode_id = 0;
for (auto& block : m_proc.basic_blocks) {
opcode_id = 0;
for (auto& opcode : block.opcodes) {
auto data = opcode.data();
auto size = opcode.size();
if (data[0] == uopcode) //0xFF ?
if (std::memcmp(&*(data + idx), &value, szData) == 0) // Is it the same memory immediate?
return std::make_pair(block_id, opcode_id);
opcode_id++;
}
block_id++;
}
return std::make_pair(-1, -1);
};
public:
RyujinObfuscationCore(const RyujinObfuscatorConfig& config, const RyujinProcedure& proc, uintptr_t ProcImageBase);
void applyRelocationFixupsToInstructions(uintptr_t imageBase, DWORD virtualAddress, std::vector<unsigned char>& new_opcodes);

View File

@@ -11,17 +11,17 @@ auto main() -> int {
RyujinObfuscatorConfig config;
config.m_isIgnoreOriginalCodeRemove = FALSE;
config.m_isJunkCode = TRUE;
config.m_isJunkCode = FALSE;
config.m_isRandomSection = FALSE;
config.m_isVirtualized = TRUE;
config.m_isIatObfuscation = TRUE;
config.m_isEncryptObfuscatedCode = FALSE;
std::vector<std::string> procsToObfuscate{
"main",
"invoke_main",
"sum",
"__scrt_common_main",
"j___security_init_cookie"
//"main"
//"invoke_main",
"sum"
// "__scrt_common_main",
// "j___security_init_cookie"
};
config.m_strProceduresToObfuscate.assign(procsToObfuscate.begin(), procsToObfuscate.end());