feat: Initial implementation of IAT obfuscation feature

- Initial IAT obfuscation feature, Ryujin can now obfuscate the IAT for the configured procedures.
- New basic block context generation.
- Bug fixes.
 - Improved obfuscation logic for better organization.
This commit is contained in:
keowu
2025-06-07 21:55:58 -03:00
parent 5b004bff54
commit 351756a1b4
5 changed files with 127 additions and 7 deletions

View File

@@ -1,4 +1,4 @@
#pragma once;
#pragma once
struct RyujinInstruction {

View File

@@ -136,7 +136,7 @@ bool Ryujin::run(const RyujinObfuscatorConfig& config) {
proc.basic_blocks = rybb.createBasicBlocks(ucOpcodes, proc.size, proc.address);
//Is time to obfuscate ?
RyujinObfuscationCore obc(config, proc);
RyujinObfuscationCore obc(config, proc, reinterpret_cast<uintptr_t>(m_mappedPE.get()));
obc.Run();
//TODO: Custom passes support

View File

@@ -1,8 +1,10 @@
#include "RyujinObfuscationCore.hh"
RyujinObfuscationCore::RyujinObfuscationCore(const RyujinObfuscatorConfig& config, const RyujinProcedure& proc) {
RyujinObfuscationCore::RyujinObfuscationCore(const RyujinObfuscatorConfig& config, const RyujinProcedure& proc, uintptr_t ProcImageBase) {
m_proc = proc;
m_config = config;
m_ProcImageBase = ProcImageBase;
if (!extractUnusedRegisters())
throw std::exception("No registers avaliable for obfuscation...");
@@ -121,13 +123,128 @@ void RyujinObfuscationCore::addPaddingSpaces() {
}
void RyujinObfuscationCore::obfuscateIat() {
/*
Unexpected Ryujin requires at least one unused register in the current procedure to deobfuscate the IAT during runtime
*/
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) {
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);
// Call to an invalid IAT in the list of basic blocks
if (block_info.first == -1 || block_info.second == -1) continue;
// Retrieving the original opcodes where the opcodes have already been updated and obfuscated
auto& data = m_proc.basic_blocks[block_info.first].opcodes[block_info.second];
// Retrieving the "INSTRUCTION" from the basic block for our IAT call related to this context we're working with
auto orInstr = m_proc.basic_blocks[block_info.first].instructions.back(); // A call [IAT] will always be the last entry
/*
Let's calculate the IAT address that stores the resolved address for the given CALL
*/
// Calculating the VA of the next instruction after the "CALL" to the IAT in the original section
const uintptr_t next_instruction_address = orInstr.addressofinstruction + orInstr.instruction.info.length;
// Calculating the target address of the IAT using the memory immediate from the original instruction
const uint32_t iat_target_rva = (next_instruction_address + orInstr.instruction.operands->mem.disp.value) - m_ProcImageBase;
// A new vector to store our corrected IAT
std::vector<ZyanU8> new_iat_call;
unsigned char pebRecoverModuleBase[24]{
0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, // mov rax, gs:60h
0x48, 0x8B, 0x40, 0x10, // mov rax, qword ptr ds:[rax+0x10]
0x48, 0x05, 0x00, 0x00, 0x00, 0x00, // add rax, 22228h
0x48, 0x8B, 0x00, //mov rax, qword ptr ds:[rax] -> Recover the IAT jump value stored in the data segment
0xFF, 0xD0//call rax
};
std::memcpy(&*(pebRecoverModuleBase + 15), &iat_target_rva, sizeof(uint32_t));
new_iat_call.insert(new_iat_call.end(), std::begin(pebRecoverModuleBase), std::end(pebRecoverModuleBase));
// Replacing opcodes of the call in question with the new ones
data.assign(new_iat_call.begin(), new_iat_call.end());
std::printf("[OK] Obfuscating IAT CALL: %s\n", instr.instruction.text);
}
}
}
return;
}
void RyujinObfuscationCore::updateBasicBlocksContext() {
auto new_obfuscated_opcodes = getProcessedProc().getUpdateOpcodes();
auto bb = new RyujinBasicBlockerBuilder(ZYDIS_MACHINE_MODE_LONG_64, ZydisStackWidth_::ZYDIS_STACK_WIDTH_64);
m_obfuscated_bb = bb->createBasicBlocks(new_obfuscated_opcodes.data(), static_cast<size_t>(new_obfuscated_opcodes.size()), m_proc.address);
}
BOOL RyujinObfuscationCore::Run() {
//Add padding spaces
addPaddingSpaces();
//Update basic blocks view based on the new obfuscated
this->updateBasicBlocksContext();
//Obfuscate IAT for the configured procedures
if (m_config.m_isIatObfuscation) {
//First obfuscate IAT
obfuscateIat();
//Update our basic blocks context to rely 1-1 for the new obfuscated opcodes.
this->updateBasicBlocksContext();
}
/*
if (config.m_isIatObfuscation) todoAction();
if (config.m_isVirtualized) todoAction();
if (config.m_isJunkCode) todoAction();
@@ -258,7 +375,7 @@ void RyujinObfuscationCore::applyRelocationFixupsToInstructions(uintptr_t imageB
}
//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) {
else if (instruction.instruction.info.meta.category == ZYDIS_CATEGORY_CALL && instruction.instruction.operands->type == ZYDIS_OPERAND_TYPE_MEMORY && !m_config.m_isIatObfuscation) {
// References for the vector's data and size with the obfuscated opcodes
auto size = new_opcodes.size();

View File

@@ -17,14 +17,18 @@ private:
const int MAX_PADDING_SPACE_INSTR = 15;
std::vector<ZydisRegister> m_unusedRegisters;
std::vector<RyujinBasicBlock> m_obfuscated_bb;
uintptr_t m_ProcImageBase;
RyujinProcedure m_proc;
RyujinObfuscatorConfig m_config;
BOOL extractUnusedRegisters();
void updateBasicBlocksContext();
void addPaddingSpaces();
void obfuscateIat();
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:
RyujinObfuscationCore(const RyujinObfuscatorConfig& config, const RyujinProcedure& proc);
RyujinObfuscationCore(const RyujinObfuscatorConfig& config, const RyujinProcedure& proc, uintptr_t ProcImageBase);
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 isIgnoreOriginalCodeRemove = false);
BOOL Run();

View File

@@ -17,7 +17,6 @@ auto main() -> int {
config.m_isIatObfuscation = TRUE;
std::vector<std::string> procsToObfuscate{
"main",
"mainCRTStartup",
"invoke_main",
"sum",
"__scrt_common_main",