feat: Handle multiple procedures at once, remove and redirect original code after obfuscation, and fix bugs

- Now Ryujin removes all the original procedure code after obfuscation.
- Now Ryujin redirects the original procedure to the correct obfuscated location for execution.
- Now Ryujin can handle multiple procedures at once and their relocation, organizing each obfuscated procedure sequentially in the new section.
- Fixed the bug in "RyujinObfuscationCore::fix_branch_near_far_short" that could break in some unexpected branching cases.
This commit is contained in:
keowu
2025-06-03 20:58:00 -03:00
parent caf7a199db
commit a02c72a5e5
6 changed files with 87 additions and 23 deletions

View File

@@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13 VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RyujinConsole", "RyujinConsole\RyujinConsole.vcxproj", "{1DC1BB2C-6B3E-4084-8F26-76852C709BB4}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RyujinConsole", "RyujinConsole\RyujinConsole.vcxproj", "{1DC1BB2C-6B3E-4084-8F26-76852C709BB4}"
EndProject EndProject

View File

@@ -159,22 +159,31 @@ bool Ryujin::run(const RyujinObfuscatorConfig& config) {
RyujinPESections peSections; RyujinPESections peSections;
peSections.AddNewSection(m_strInputFilePath, chSectionName); peSections.AddNewSection(m_strInputFilePath, chSectionName);
//Get New Opcodes - todo: improve, this only works for the first procedure uintptr_t offsetVA = 0;
std::vector<unsigned char> tempValued; std::vector<unsigned char> opcodesWithRelocsFixed;
for (auto& obc : processed_procs) { for (auto& obc : processed_procs) {
tempValued = obc.getProcessedProc().getUpdateOpcodes(); auto tempValued = obc.getProcessedProc().getUpdateOpcodes();
//Fix relocations //Fix relocations
obc.applyRelocationFixupsToInstructions(reinterpret_cast<uintptr_t>(imgDos), peSections.getRyujinSectionVA(), tempValued); obc.applyRelocationFixupsToInstructions(reinterpret_cast<uintptr_t>(imgDos), peSections.getRyujinSectionVA() + offsetVA, tempValued);
//Removendo e adicionando um salto no procedimento original e removendo opcodes originais para um salto ao novo c<>digo ofuscado
obc.removeOldOpcodeRedirect(peSections.mappedPeDiskBaseAddress(), peSections.getRyujinMappedPeSize(), reinterpret_cast<uintptr_t>(imgDos) + peSections.getRyujinSectionVA() + offsetVA);
//Destructing class //Destructing class
obc.~RyujinObfuscationCore(); obc.~RyujinObfuscationCore();
//Inserindo procedures na lista de opcodes corrigidos
opcodesWithRelocsFixed.insert(opcodesWithRelocsFixed.end(), tempValued.begin(), tempValued.end());
// Incrementando o offset com o tamanho dos opcodes em quest<73>o
offsetVA += tempValued.size();
} }
//Process new opcodes //Process new opcodes
peSections.ProcessOpcodesNewSection(tempValued); peSections.ProcessOpcodesNewSection(opcodesWithRelocsFixed);
//Save output file //Save output file
peSections.FinishNewSection(m_strOutputFilePath); peSections.FinishNewSection(m_strOutputFilePath);

View File

@@ -97,8 +97,7 @@ void RyujinObfuscationCore::addPaddingSpaces() {
code.init(runtime.environment()); code.init(runtime.environment());
asmjit::x86::Assembler a(&code); asmjit::x86::Assembler a(&code);
for (auto i = 0; i < MAX_PADDING_SPACE_INSTR; i++) for (auto i = 0; i < MAX_PADDING_SPACE_INSTR; i++) a.nop();
a.nop();
code.flatten(); code.flatten();
@@ -148,7 +147,6 @@ uint32_t RyujinObfuscationCore::findOpcodeOffset(const uint8_t* data, size_t dat
std::vector<uint8_t> RyujinObfuscationCore::fix_branch_near_far_short(uint8_t original_opcode, uint64_t jmp_address, uint64_t target_address) { 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 = { static const std::unordered_map<uint8_t, uint8_t> SHORT_TO_NEAR = {
{ 0x70, 0x80 }, { 0x71, 0x81 }, { 0x72, 0x82 }, { 0x73, 0x83 }, { 0x70, 0x80 }, { 0x71, 0x81 }, { 0x72, 0x82 }, { 0x73, 0x83 },
@@ -160,31 +158,34 @@ std::vector<uint8_t> RyujinObfuscationCore::fix_branch_near_far_short(uint8_t or
std::vector<uint8_t> result; std::vector<uint8_t> result;
// First tries as a short jump (2 bytes) // Tries to handle as a short jump (2 bytes)
const int short_length = 2; const int short_length = 2;
const int64_t short_disp = static_cast<int64_t>(target_address) - (jmp_address + short_length); const int64_t short_disp = static_cast<int64_t>(target_address) - (jmp_address + short_length);
if (short_disp >= -128 && short_disp <= 127) { if (short_disp >= -128 && short_disp <= 127) {
// Keeps it as a short jump
result.push_back(original_opcode); result.push_back(original_opcode);
result.push_back(static_cast<uint8_t>(short_disp)); result.push_back(static_cast<uint8_t>(short_disp));
return result; return result;
} }
// Converts to a near jump (6 bytes) // If it is not a conditional jump, returns the original
auto it = SHORT_TO_NEAR.find(original_opcode); 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"); if (it == SHORT_TO_NEAR.end()) {
result.push_back(original_opcode);
return result; // Does not apply conversion
}
// Handles as a near jump (6 bytes)
const uint8_t near_opcode = it->second; const uint8_t near_opcode = it->second;
const int near_length = 6; const int near_length = 6;
const int64_t near_disp = static_cast<int64_t>(target_address) - (jmp_address + near_length); 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."); 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(0x0F);
result.push_back(near_opcode); result.push_back(near_opcode);
@@ -436,6 +437,44 @@ void RyujinObfuscationCore::applyRelocationFixupsToInstructions(uintptr_t imageB
} }
void RyujinObfuscationCore::removeOldOpcodeRedirect(uintptr_t newMappedPE, std::size_t szMapped, uintptr_t newObfuscatedAddress) {
/*
Creating signatures to search for the opcode in the PE mapped from disk.
We will use findOpcodeOffset to find the exact offset of the procedure's start
in the unmapped region with the SEC_IMAGE flag.
*/
unsigned char ucSigature[10]{ 0 };
std::memcpy(ucSigature, reinterpret_cast<void*>(m_proc.address), 10);
auto offsetz = findOpcodeOffset(reinterpret_cast<unsigned char*>(newMappedPE), szMapped, &ucSigature, 10);
/*
Removing all the opcodes from the original procedure and replacing them with NOP instructions.
*/
std::memset(reinterpret_cast<void*>(newMappedPE + offsetz), 0x90, m_proc.size);
/*
Creating a new JMP opcode in such a way that it can be added to the old region that was completely replaced by NOP,
thus redirecting execution to the new obfuscated code.
*/
unsigned char ucOpcodeJmp[5]{
0xE9, 0, 0, 0, 0, //JMP imm
};
/*
Calculating the new displacement between the original code region and the target obfuscated opcode,
calculating the relative immediate offset.
*/
const uint32_t offset = newObfuscatedAddress - (m_proc.address + 5);
//Replacing the jump opcode with the new relative immediate displacement value.
std::memcpy(&*(ucOpcodeJmp + 1), &offset, sizeof(uint32_t));
//Inserting the new jump opcode into the original cleaned function to redirect execution to the fully obfuscated code.
std::memcpy(reinterpret_cast<void*>(newMappedPE + offsetz), ucOpcodeJmp, 5);
}
RyujinObfuscationCore::~RyujinObfuscationCore() { RyujinObfuscationCore::~RyujinObfuscationCore() {
} }

View File

@@ -14,18 +14,19 @@
class RyujinObfuscationCore { class RyujinObfuscationCore {
private: private:
const int MAX_PADDING_SPACE_INSTR = 50; const int MAX_PADDING_SPACE_INSTR = 15;
std::vector<ZydisRegister> m_unusedRegisters; std::vector<ZydisRegister> m_unusedRegisters;
std::vector<RyujinBasicBlock> m_obfuscated_bb; std::vector<RyujinBasicBlock> m_obfuscated_bb;
RyujinProcedure m_proc; RyujinProcedure m_proc;
BOOL extractUnusedRegisters(); BOOL extractUnusedRegisters();
void addPaddingSpaces(); void addPaddingSpaces();
std::vector<uint8_t> fix_branch_near_far_short(uint8_t original_opcode, uint64_t jmp_address, uint64_t target_address); 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);
public: public:
RyujinObfuscationCore(const RyujinObfuscatorConfig& config, const RyujinProcedure& proc); 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); void applyRelocationFixupsToInstructions(uintptr_t imageBase, DWORD virtualAddress, std::vector<unsigned char>& new_opcodes);
void removeOldOpcodeRedirect(uintptr_t newMappedPE, std::size_t szMapped, uintptr_t newObfuscatedAddress);
BOOL Run(); BOOL Run();
RyujinProcedure getProcessedProc(); RyujinProcedure getProcessedProc();
~RyujinObfuscationCore(); ~RyujinObfuscationCore();

View File

@@ -31,11 +31,21 @@ public:
return m_ucResizedPE; return m_ucResizedPE;
} }
uintptr_t mappedPeDiskBaseAddress() {
return reinterpret_cast<uintptr_t>(m_dosHeader);
}
uintptr_t getRyujinSectionSize() { uintptr_t getRyujinSectionSize() {
return m_szNewSec; return m_szNewSec;
} }
uintptr_t getRyujinMappedPeSize() {
return m_szFile;
}
BOOL AddNewSection(const std::string& strInputFilePath, char chSectionName[8]); BOOL AddNewSection(const std::string& strInputFilePath, char chSectionName[8]);
BOOL ProcessOpcodesNewSection(std::vector<unsigned char>& opcodeData); BOOL ProcessOpcodesNewSection(std::vector<unsigned char>& opcodeData);

View File

@@ -5,7 +5,7 @@ auto main() -> int {
std::cout << "Hello World!\n"; std::cout << "Hello World!\n";
std::unique_ptr<Ryujin> ryujin = std::make_unique<Ryujin>("C:\\Users\\Keowu\\Documents\\GitHub\\MoFei\\x64\\Debug\\DemoObfuscation.exe", "C:\\Users\\Keowu\\Documents\\GitHub\\MoFei\\x64\\Debug\\DemoObfuscation.pdb", "C:\\Users\\Keowu\\Documents\\GitHub\\MoFei\\x64\\Debug\\DemoObfuscation2.exe"); std::unique_ptr<Ryujin> ryujin = std::make_unique<Ryujin>("C:\\Users\\Keowu\\Documents\\GitHub\\Ryujin\\RyujinConsole\\x64\\Debug\\DemoObfuscation.exe", "C:\\Users\\Keowu\\Documents\\GitHub\\Ryujin\\RyujinConsole\\x64\\Debug\\DemoObfuscation.pdb", "C:\\Users\\Keowu\\Documents\\GitHub\\Ryujin\\RyujinConsole\\x64\\Debug\\DemoObfuscation.obfuscated.exe");
ryujin.get()->listRyujinProcedures(); ryujin.get()->listRyujinProcedures();
@@ -15,7 +15,12 @@ auto main() -> int {
config.m_isRandomSection = FALSE; config.m_isRandomSection = FALSE;
config.m_isVirtualized = FALSE; config.m_isVirtualized = FALSE;
std::vector<std::string> procsToObfuscate{ std::vector<std::string> procsToObfuscate{
"main" "main",
"mainCRTStartup",
"invoke_main",
"sum",
"__scrt_common_main",
"j___security_init_cookie"
}; };
config.m_strProceduresToObfuscate.assign(procsToObfuscate.begin(), procsToObfuscate.end()); config.m_strProceduresToObfuscate.assign(procsToObfuscate.begin(), procsToObfuscate.end());