From caf7a199dbc6e05892a86f9e7cf9daa362a4df77 Mon Sep 17 00:00:00 2001 From: keowu Date: Sun, 1 Jun 2025 12:58:04 -0300 Subject: [PATCH] feat: Fixing full obfuscated opcode relocations, Obfuscated section improvements and much more - Now after processing the obfuscated opcodes the ryujin can fix all the relocations for the obfuscated code turning this code valid again after obfuscating considering all possible relocation cases. - The section generator was been improved as well - The logic to handle obfuscated opcodes and function is also improved. - Some unused field for our logic has been removed. --- .../Ryujin/Models/RyujinProcedure.hh | 1 - RyujinConsole/RyujinConsole/Ryujin/Ryujin.cc | 21 +- .../RyujinCore/RyujinObfuscationCore.cc | 302 +++++++++++++++++- .../RyujinCore/RyujinObfuscationCore.hh | 8 + .../Ryujin/Utils/RyujinPESections.cc | 2 - 5 files changed, 323 insertions(+), 11 deletions(-) diff --git a/RyujinConsole/RyujinConsole/Ryujin/Models/RyujinProcedure.hh b/RyujinConsole/RyujinConsole/Ryujin/Models/RyujinProcedure.hh index db9f0fd..a4dae5a 100644 --- a/RyujinConsole/RyujinConsole/Ryujin/Models/RyujinProcedure.hh +++ b/RyujinConsole/RyujinConsole/Ryujin/Models/RyujinProcedure.hh @@ -6,7 +6,6 @@ class RyujinProcedure { public: std::string name; - uintptr_t imagebase; uintptr_t address; uintptr_t size; std::vector basic_blocks; diff --git a/RyujinConsole/RyujinConsole/Ryujin/Ryujin.cc b/RyujinConsole/RyujinConsole/Ryujin/Ryujin.cc index fd958af..551eb64 100644 --- a/RyujinConsole/RyujinConsole/Ryujin/Ryujin.cc +++ b/RyujinConsole/RyujinConsole/Ryujin/Ryujin.cc @@ -94,7 +94,7 @@ bool Ryujin::run(const RyujinObfuscatorConfig& config) { return FALSE; } - std::vector processed_procs; + std::vector processed_procs; for (auto& proc : m_ryujinProcedures) { @@ -138,17 +138,18 @@ bool Ryujin::run(const RyujinObfuscatorConfig& config) { //Is time to obfuscate ? RyujinObfuscationCore obc(config, proc); obc.Run(); - processed_procs.push_back(obc.getProcessedProc()); - obc.~RyujinObfuscationCore(); //TODO: Custom passes support + //Storing processed procs + processed_procs.push_back(obc); + //Clean up opcodes delete[] ucOpcodes; } - //More obfuscation + //Remove old code and jump to the new code region if (config.m_isIgnoreOriginalCodeRemove) todoAction(); //Add section @@ -159,10 +160,18 @@ bool Ryujin::run(const RyujinObfuscatorConfig& config) { peSections.AddNewSection(m_strInputFilePath, chSectionName); //Get New Opcodes - todo: improve, this only works for the first procedure - std::vector tempValued = processed_procs.front().getUpdateOpcodes(); + std::vector tempValued; + for (auto& obc : processed_procs) { - //Fix relocations + tempValued = obc.getProcessedProc().getUpdateOpcodes(); + //Fix relocations + obc.applyRelocationFixupsToInstructions(reinterpret_cast(imgDos), peSections.getRyujinSectionVA(), tempValued); + + //Destructing class + obc.~RyujinObfuscationCore(); + + } //Process new opcodes peSections.ProcessOpcodesNewSection(tempValued); diff --git a/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.cc b/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.cc index b2b93f0..5457bec 100644 --- a/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.cc +++ b/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.cc @@ -97,9 +97,8 @@ void RyujinObfuscationCore::addPaddingSpaces() { code.init(runtime.environment()); asmjit::x86::Assembler a(&code); - for (auto i = 0; i < 50; i++) { + for (auto i = 0; i < MAX_PADDING_SPACE_INSTR; i++) a.nop(); - } code.flatten(); @@ -138,6 +137,305 @@ BOOL RyujinObfuscationCore::Run() { return TRUE; } +uint32_t RyujinObfuscationCore::findOpcodeOffset(const uint8_t* data, size_t dataSize, const void* opcode, size_t opcodeSize) { + + if (opcodeSize == 0 || dataSize < opcodeSize) return 0; + + for (size_t i = 0; i <= dataSize - opcodeSize; ++i) if (std::memcmp(data + i, opcode, opcodeSize) == 0) return static_cast(i); + + return 0; +} + +std::vector RyujinObfuscationCore::fix_branch_near_far_short(uint8_t original_opcode, uint64_t jmp_address, uint64_t target_address) { + + // Mapping short opcodes to near + static const std::unordered_map SHORT_TO_NEAR = { + + { 0x70, 0x80 }, { 0x71, 0x81 }, { 0x72, 0x82 }, { 0x73, 0x83 }, + { 0x74, 0x84 }, { 0x75, 0x85 }, { 0x76, 0x86 }, { 0x77, 0x87 }, + { 0x78, 0x88 }, { 0x79, 0x89 }, { 0x7A, 0x8A }, { 0x7B, 0x8B }, + { 0x7C, 0x8C }, { 0x7D, 0x8D }, { 0x7E, 0x8E }, { 0x7F, 0x8F } + + }; + + std::vector result; + + // First tries as a short jump (2 bytes) + const int short_length = 2; + const int64_t short_disp = static_cast(target_address) - (jmp_address + short_length); + + if (short_disp >= -128 && short_disp <= 127) { + + // Keeps it as a short jump + result.push_back(original_opcode); + result.push_back(static_cast(short_disp)); + + return result; + } + + // Converts to a near jump (6 bytes) + auto it = SHORT_TO_NEAR.find(original_opcode); + if (it == SHORT_TO_NEAR.end()) throw new std::exception("[X] RyujinObfuscationCore::fix_branch_offset_cpp: Branch opcode is not suported to regenerate a branch"); + + const uint8_t near_opcode = it->second; + const int near_length = 6; + const int64_t near_disp = static_cast(target_address) - (jmp_address + near_length); + + // Checks for 32-bit overflow + if (near_disp < INT32_MIN || near_disp > INT32_MAX) throw std::exception("[X] Offset exceeds the limit of a 32-bit signed integer."); + + // Packs the displacement (little-endian) + result.push_back(0x0F); + result.push_back(near_opcode); + + const uint32_t raw_disp = static_cast(near_disp); + result.push_back((raw_disp >> 0) & 0xFF); + result.push_back((raw_disp >> 8) & 0xFF); + result.push_back((raw_disp >> 16) & 0xFF); + result.push_back((raw_disp >> 24) & 0xFF); + + return result; +} + +void RyujinObfuscationCore::applyRelocationFixupsToInstructions(uintptr_t imageBase, DWORD virtualAddress, std::vector& new_opcodes) { + + /* + Creating a new basic block for our obfuscated code + */ + auto bb = new RyujinBasicBlockerBuilder(ZYDIS_MACHINE_MODE_LONG_64, ZydisStackWidth_::ZYDIS_STACK_WIDTH_64); + m_obfuscated_bb = bb->createBasicBlocks(new_opcodes.data(), static_cast(new_opcodes.size()), imageBase + virtualAddress); + + //The current block id that we're working with + int block_id = 0; + + for (auto& block : m_proc.basic_blocks) { + + for (auto& instruction : block.instructions) { + + //Fixing all Call to a immediate(No IAT) values from our obfuscated opcodes -> CALL IMM + if (instruction.instruction.info.meta.category == ZYDIS_CATEGORY_CALL && instruction.instruction.operands->type == ZYDIS_OPERAND_TYPE_IMMEDIATE) { + + //References for the data and size of the vector with the obfuscated opcodes + auto size = new_opcodes.size(); + auto data = new_opcodes.data(); + + //Getting the immediate value of the original "CALL" + const uint32_t immediateValue = instruction.instruction.operands[0].imm.value.u; + + /* + Creating a signature for the opcode from the original section so that we can + scan the obfuscated region using the correct instruction offset and recalculate its displacement. + */ + unsigned char ucOpcodeSignature[5]{ instruction.instruction.info.opcode }; + std::memcpy(&*(ucOpcodeSignature + 1), &immediateValue, sizeof(immediateValue)); + + //Finding the offset of the "CALL" using the opcode signature in the obfuscated section + const uint32_t offset = findOpcodeOffset(data, size, &ucOpcodeSignature, 5); + + //Calculating the VA (Virtual Address) of the "CALL" in the obfuscated section + uint32_t obfuscated_call_va = imageBase + virtualAddress + offset; + + /* + Calculating the new immediate offset to fix the relocation of the new obfuscated "CALL" instruction + */ + // Calculate address of the next instruction (CALL instruction + its length) + const uintptr_t next_instruction_address = instruction.addressofinstruction + instruction.instruction.info.length; + + // Get the relative displacement from the first operand (signed 32-bit integer) + const int32_t displacement = static_cast(instruction.instruction.operands[0].imm.value.s); + + // Calculate absolute target address + const uintptr_t target_address = next_instruction_address + displacement; + + //Calculating the new immediate value for the "CALL" instruction using the VA addresses of the obfuscated section + uint32_t new_immediate_reloc = static_cast(target_address) - (obfuscated_call_va + instruction.instruction.info.length); //length == 5 + + //Fixing the relocation of the "CALL" instruction in the obfuscated region + std::memcpy(&*(data + offset + 1), &new_immediate_reloc, sizeof(uint32_t)); + + std::printf("[OK] Fixing CALL IMM -> %s from 0x%X to 0x%X\n", instruction.instruction.text, immediateValue, new_immediate_reloc); + + } + //Fixing all Call to a memory(IAT) values from our obfuscated opcodes -> CALL [MEMORY] + else if (instruction.instruction.info.meta.category == ZYDIS_CATEGORY_CALL && instruction.instruction.operands->type == ZYDIS_OPERAND_TYPE_MEMORY) { + + // References for the vector's data and size with the obfuscated opcodes + auto size = new_opcodes.size(); + auto data = new_opcodes.data(); + + // Obtaining the memory immediate value for the "CALL" + const uint32_t memmory_immediate = instruction.instruction.operands->mem.disp.value; + + // Creating a signature to search for the offset in the obfuscated opcodes + unsigned char ucOpcodeSignature[6]{ 0xFF, 0x15 }; + std::memcpy(&*(ucOpcodeSignature + 2), &memmory_immediate, sizeof(memmory_immediate)); + + // Finding the offset of the "CALL" memory using the opcode signature in the obfuscated section + const uint32_t offset = findOpcodeOffset(data, size, &ucOpcodeSignature, 6); + + // If we don't find the signature, it might not be an IAT... requiring future handling. + if (offset == 0) { + + std::printf("[X] Invalid IAT call or call to a custom address detected.....\n"); + + continue; + } + + // Calculating the VA (Virtual Address) of the "CALL" instruction to the IAT in the obfuscated section + const uintptr_t obfuscated_call_iat_va = ((imageBase + virtualAddress + offset)); + + // Calculating the VA of the next instruction after the "CALL" to the IAT in the original section + const uintptr_t next_instruction_address = instruction.addressofinstruction + instruction.instruction.info.length; + + // Calculating the target address of the IAT using the memory immediate from the original instruction + const uintptr_t iat_target_address = next_instruction_address + memmory_immediate; + + // Calculating new RIP (Instruction Pointer) for the obfuscated instruction + uintptr_t new_rip = obfuscated_call_iat_va + instruction.instruction.info.length; + + // Calculating the displacement from the new position to the IAT address + const uint32_t new_memory_immediate_iat = iat_target_address - new_rip; + + // Fixing the relocation of the "CALL" instruction in the obfuscated region to the new memory immediate + std::memcpy(&*(data + offset + 2), &new_memory_immediate_iat, sizeof(uint32_t)); + + std::printf("[OK] Fixing IAT Call -> %s from 0x%X to 0x%X\n", instruction.instruction.text, obfuscated_call_iat_va, new_memory_immediate_iat); + + } + // Searching for MOV and LEA instructions that have the second operand as memory-relative + else if ((instruction.instruction.info.mnemonic == ZYDIS_MNEMONIC_LEA || instruction.instruction.info.mnemonic == ZYDIS_MNEMONIC_MOV) && instruction.instruction.operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY) { + + const ZydisDecodedOperandMem* mem = &instruction.instruction.operands[1].mem; + + //Looking for: lea reg, [MEMORY] and mov reg, [MEMORY] + if (mem->base == ZYDIS_REGISTER_RIP && mem->index == ZYDIS_REGISTER_NONE && mem->disp.has_displacement) { + + // References for data and vector size with obfuscated opcodes + auto size = new_opcodes.size(); + auto data = new_opcodes.data(); + + // Getting the memory immediate offset value to build the signature + const uint32_t memmory_immediate_offset = mem->disp.value; + + // Creating a signature to search for the offset in the obfuscated opcodes + unsigned char ucOpcodeSignature[7]{ 0 }; + std::memcpy(&ucOpcodeSignature, reinterpret_cast(instruction.addressofinstruction), 3); // 3 BYTES do opcode relativo ao LEA ou MOV + std::memcpy(&*(ucOpcodeSignature + 3), &memmory_immediate_offset, sizeof(memmory_immediate_offset)); + + // Finding the offset of the "LEA" or "MOV" that uses memory-relative addressing + const ZyanI64 offset = findOpcodeOffset(data, size, &ucOpcodeSignature, 7); + + // If we don't find any offset, there may be an issue or bug. + if (offset == 0) { + + std::printf("[X] Invalid lea reference or uknown lea detected.....\n"); + + continue; + } + + // Retrieving the instruction address in the original section + const uintptr_t original_address = instruction.addressofinstruction; + + // Calculating new address in the obfuscated section + const uintptr_t obfuscated_va_address = ((imageBase + virtualAddress + offset)); + + /* + Calculating new displacement for the immediate value + */ + // Calculating the address of the instruction following the original instruction + const uintptr_t original_rip = original_address + instruction.instruction.info.length; + + // Calculating the original target address of the original instruction + const uintptr_t target_original = original_rip + memmory_immediate_offset; + + // Calculating the address of the instruction following the obfuscated instruction + const uintptr_t new_obfuscated_rip = obfuscated_va_address + instruction.instruction.info.length; + + // New memory immediate value for the instruction + const uintptr_t new_memory_immediate = target_original - new_obfuscated_rip; + + // Fixing the immediate value for the "LEA" or "MOV" instruction with the corrected relative immediate value + std::memcpy(&*(data + offset + 3), &new_memory_immediate, sizeof(uint32_t)); // 3 bytes for the size of the LEA or MOV opcode + + std::printf("[OK] Fixing -> %s - from %X to %X\n", instruction.instruction.text, mem->disp.value, new_memory_immediate); + + } + + } + else if (instruction.instruction.info.meta.category == ZYDIS_CATEGORY_COND_BR || instruction.instruction.info.meta.category == ZYDIS_CATEGORY_UNCOND_BR) { + + // References for data and vector size with obfuscated opcodes + auto size = new_opcodes.size(); + auto data = new_opcodes.data(); + + /* + Finding the address of the currently analyzed branch instruction within the set of obfuscated basic blocks + */ + uintptr_t obfuscated_jmp_address = 0; + auto basic_block_obfuscated_ctx = m_obfuscated_bb.at(block_id); + for (auto& inst : basic_block_obfuscated_ctx.instructions) + + if (inst.instruction.info.opcode == instruction.instruction.info.opcode && inst.instruction.operands[0].imm.value.u == instruction.instruction.operands[0].imm.value.u) { + obfuscated_jmp_address = inst.addressofinstruction; + break; + } + + /* + Based on the branch's destination address, we’ll search for the block ID in our vector so that we can + synchronize both the obfuscated and original blocks to work on a fix. + */ + auto address_branch = instruction.addressofinstruction + instruction.instruction.info.length + instruction.instruction.operands[0].imm.value.u; + uint32_t local_block_id = 0; + for (auto& block : m_proc.basic_blocks) { + + if (address_branch >= block.start_address && address_branch <= block.end_address) + break; + + local_block_id++; + } + + //Calculating our new branch immediate offset + auto basic_block_original = m_proc.basic_blocks.at(local_block_id); + auto basic_block_obfuscated = m_obfuscated_bb.at(local_block_id); + + /* + Normally, obfuscated and deobfuscated blocks are 1-to-1, and we just need to get the address relative + to the first instruction of the block in question so we can determine the jump address for the new obfuscated region. + */ + auto obfuscated_target_address = basic_block_obfuscated.instructions.at(0).addressofinstruction; + + /* + Let's fix our new branch. Previously it was a "near" jump, but now it will be "far" depending on the jump length. + This procedure will perform the calculation and generate a far or near branch depending on the need + and the output of the obfuscated code. + */ + auto corrected = fix_branch_near_far_short(instruction.instruction.info.opcode, obfuscated_jmp_address, obfuscated_target_address); + + // Creating a signature for the original branch in the obfuscated opcode to be fixed + unsigned char ucSignature[2]{ 0, 0 }; + std::memcpy(ucSignature, reinterpret_cast(instruction.addressofinstruction), 2); + // Finding the correct offset of the opcode to apply the patch + const uint32_t offset = findOpcodeOffset(data, size, &ucSignature, 2); + + // Clearing the branch so we can insert the new branch with the corrected opcode and its offset + std::memset(&*(data + offset), 0x90, 9); // Equivalent to -> branch + offset and possibly some add reg, value -> we have space because "addPaddingSpaces" into this section. + + // Patching the cleared region with the new branch, now fully fixed and with the newly calculated jump displacement + std::memcpy(&*(data + offset), corrected.data(), corrected.size()); + + std::printf("[OK] Fixing %s -> %X -> id: %X\n", instruction.instruction.text, instruction.instruction.operands[0].imm.value.u, block_id); + + } + + } + + //Increment block index + block_id++; + + } + +} + RyujinObfuscationCore::~RyujinObfuscationCore() { } \ No newline at end of file diff --git a/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.hh b/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.hh index dbc2262..d670ef7 100644 --- a/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.hh +++ b/RyujinConsole/RyujinConsole/Ryujin/RyujinCore/RyujinObfuscationCore.hh @@ -2,22 +2,30 @@ #include #include #include +#include +#include #include #include #include #include "../Models/RyujinProcedure.hh" #include "../Models/RyujinObfuscatorConfig.hh" +#include "../RyujinCore/BasicBlockerBuilder.hh" class RyujinObfuscationCore { private: + const int MAX_PADDING_SPACE_INSTR = 50; std::vector m_unusedRegisters; + std::vector m_obfuscated_bb; RyujinProcedure m_proc; BOOL extractUnusedRegisters(); void addPaddingSpaces(); + std::vector fix_branch_near_far_short(uint8_t original_opcode, uint64_t jmp_address, uint64_t target_address); public: RyujinObfuscationCore(const RyujinObfuscatorConfig& config, const RyujinProcedure& proc); + uint32_t findOpcodeOffset(const uint8_t* data, size_t dataSize, const void* opcode, size_t opcodeSize); + void applyRelocationFixupsToInstructions(uintptr_t imageBase, DWORD virtualAddress, std::vector& new_opcodes); BOOL Run(); RyujinProcedure getProcessedProc(); ~RyujinObfuscationCore(); diff --git a/RyujinConsole/RyujinConsole/Ryujin/Utils/RyujinPESections.cc b/RyujinConsole/RyujinConsole/Ryujin/Utils/RyujinPESections.cc index dc2c8c8..010fd6b 100644 --- a/RyujinConsole/RyujinConsole/Ryujin/Utils/RyujinPESections.cc +++ b/RyujinConsole/RyujinConsole/Ryujin/Utils/RyujinPESections.cc @@ -145,8 +145,6 @@ BOOL RyujinPESections::FinishNewSection(const std::string& strOutputFilePath) { auto bSucess = RyujinUtils::SaveBuffer(strOutputFilePath, m_ucResizedPE, m_szNewSec); if (!bSucess) return FALSE; - - delete[] m_ucModifiedPeMap; return TRUE; } \ No newline at end of file