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.
This commit is contained in:
keowu
2025-06-01 12:58:04 -03:00
parent 3f8bced350
commit caf7a199db
5 changed files with 323 additions and 11 deletions

View File

@@ -6,7 +6,6 @@ class RyujinProcedure {
public:
std::string name;
uintptr_t imagebase;
uintptr_t address;
uintptr_t size;
std::vector<RyujinBasicBlock> basic_blocks;

View File

@@ -94,7 +94,7 @@ bool Ryujin::run(const RyujinObfuscatorConfig& config) {
return FALSE;
}
std::vector<RyujinProcedure> processed_procs;
std::vector<RyujinObfuscationCore> 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<unsigned char> tempValued = processed_procs.front().getUpdateOpcodes();
std::vector<unsigned char> tempValued;
for (auto& obc : processed_procs) {
//Fix relocations
tempValued = obc.getProcessedProc().getUpdateOpcodes();
//Fix relocations
obc.applyRelocationFixupsToInstructions(reinterpret_cast<uintptr_t>(imgDos), peSections.getRyujinSectionVA(), tempValued);
//Destructing class
obc.~RyujinObfuscationCore();
}
//Process new opcodes
peSections.ProcessOpcodesNewSection(tempValued);

View File

@@ -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<uint32_t>(i);
return 0;
}
std::vector<uint8_t> 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<uint8_t, uint8_t> 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<uint8_t> result;
// First tries as a short jump (2 bytes)
const int short_length = 2;
const int64_t short_disp = static_cast<int64_t>(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<uint8_t>(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<int64_t>(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<uint32_t>(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<unsigned char>& 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<size_t>(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<int32_t>(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<uint32_t>(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<void*>(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<77>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<void*>(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() {
}

View File

@@ -2,22 +2,30 @@
#include <Windows.h>
#include <vector>
#include <set>
#include <cstdint>
#include <unordered_map>
#include <asmjit/asmjit.h>
#include <Zydis/Zydis.h>
#include <Zydis/SharedTypes.h>
#include "../Models/RyujinProcedure.hh"
#include "../Models/RyujinObfuscatorConfig.hh"
#include "../RyujinCore/BasicBlockerBuilder.hh"
class RyujinObfuscationCore {
private:
const int MAX_PADDING_SPACE_INSTR = 50;
std::vector<ZydisRegister> m_unusedRegisters;
std::vector<RyujinBasicBlock> m_obfuscated_bb;
RyujinProcedure m_proc;
BOOL extractUnusedRegisters();
void addPaddingSpaces();
std::vector<uint8_t> 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<unsigned char>& new_opcodes);
BOOL Run();
RyujinProcedure getProcessedProc();
~RyujinObfuscationCore();

View File

@@ -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;
}