X86指令混淆之函数分析和代码块粉碎
字数 1083 2025-08-24 20:49:31

X86指令混淆之函数分析和代码块粉碎教学文档

一、指令混淆概述

指令混淆是一种通过改变代码结构而不改变其功能的技术,主要用于增加逆向分析的难度。本文档将详细介绍基于X86架构的函数分析和代码块粉碎技术。

二、函数分析技术

2.1 基本概念

函数分析是指从二进制代码中识别出函数的起始地址和大小,类似于IDA的sub_xxx标注功能。由于不同编译器生成的函数特征各异,准确识别函数具有挑战性。

2.2 函数识别算法

2.2.1 基本规则

  1. 向下跳转记录

    • 记录所有jcc指令跳转目的地址
    • 保留跳转目的地址最大的一个
  2. ret结尾识别

    • 遇到ret指令时比较当前地址与最大跳转地址
    • 当前地址>跳转地址:函数结束
    • 当前地址<跳转地址:从跳转地址继续分析
  3. 其他结尾判断条件

    • 向上跳转的无条件jmp指令
    • nop指令(0x90)
    • 连续两个以上int3指令(0xCC)
    • add [eax], al指令(0x00,0x00....)
    • 第一条指令是无条件jmp
    • 下一条指令是另一个函数的开始特征
  4. Call指令处理

    • 遇到call立即数指令,将目的地址加入待分析集合
  5. 其他启发式规则

    • 根据编译器特定特征
    • 利用调试信息(如果存在)

2.2.2 代码实现

FunctionNode X86Analysis::AnalysisFunction(DWORD64 begin, DWORD bufferSize, 
                                          map<DWORD64, FunctionNode>* functionMap, 
                                          map<DWORD64, FunctionNode>* excludeMap, 
                                          DWORD64 pc) {
    ud_t ud;
    ud_init(&ud);
    ud_set_mode(&ud, 32);
    ud_set_syntax(&ud, UD_SYN_INTEL);
    ud_set_input_buffer(&ud, (uint8_t*)begin, bufferSize);
    ud_set_pc(&ud, pc);
    
    InstrFlowNode jcc_max, jcc_flow;
    memset(&jcc_max, 0, sizeof(jcc_max));
    
    while(ud_disassemble(&ud)) {
        jcc_flow = GetInstrNode(&ud);
        
        if(jcc_flow.isInvalid()) {
            // 处理无效指令(可能花指令)
            FunctionNode function;
            function.memoryFileAddress = begin;
            function.loadImageAddress = pc;
            function.size = begin - (jcc_flow.memoryFileAddress - jcc_flow.insnLen);
            return function;
        }
        
        if(jcc_flow.isJcc && (jcc_flow.operatorType == UD_OP_JIMM)) {
            if(jcc_flow.jmpMemoryFileAddress > jcc_max.jmpMemoryFileAddress) {
                jcc_max = jcc_flow;
            }
        }
        
        switch(jcc_flow.type) {
            case UD_Ijmp:
                // 处理无条件跳转
                break;
            case UD_Icall:
                // 处理call指令
                break;
            case UD_Iret:
                // 处理ret指令
                break;
            case UD_Inop:
            case UD_Iint3:
                // 处理nop和int3
                break;
            case UD_Iadd:
                // 处理add指令
                break;
            default:
                // 其他指令
        }
        
        // 检查下一条指令是否是函数开始特征
        DWORD64 ptr = jcc_flow.memoryFileAddress + jcc_flow.insnLen;
        if(LookNextBegin(ptr)) {
            // 添加到待分析集合
            if((excludeMap != nullptr) && (!excludeMap->empty())) {
                if(excludeMap->find(ptr) == excludeMap->end()) {
                    FunctionNode node(ptr, jcc_flow.loadImageAddress + jcc_flow.insnLen);
                    functionMap->operator[](ptr) = node;
                }
            } else {
                FunctionNode node(ptr, jcc_flow.loadImageAddress + jcc_flow.insnLen);
                functionMap->operator[](ptr) = node;
            }
            return FunctionNode(begin, pc, jcc_flow.memoryFileAddress + jcc_flow.insnLen - begin);
        }
    }
    return FunctionNode(begin, pc, bufferSize);
}

三、代码块粉碎技术

3.1 基本思路

  1. 以函数为单位进行混淆
  2. 抽取函数所有指令
  3. 申请新空间并随机乱序放置指令
  4. 增加指令保证执行顺序不变
  5. 修复跳转关系和重定位表

3.2 实现步骤

3.2.1 指令预处理

vector<InstrFlowNode> instrbox = this->m_Analysis.InstrExtract(
    function.memoryFileAddress, 
    function.size, 
    function.loadImageAddress);

3.2.2 指令混淆块生成

class ObfuscateInstr {
public:
    DWORD64 memoryAddress = 0;  // 混淆块在新空间的地址
    DWORD64 virtulAddress = 0;   // 混淆块的VA
    DWORD prexCodeSize = 0;      // 原始功能指令前面的指令长度
    char* code = nullptr;        // 混淆块数据
    DWORD size = 0;              // 混淆块大小
};

vector<ObfuscateInstr> chunkbox;
x86::Gp registers[] = {x86::eax, x86::ebx, x86::ecx, x86::edx, x86::esi, x86::edi};
bool first = true;

for(auto instr : instrbox) {
    CodeHolder code;
    code.init(CodeInfo(ArchInfo::kIdX86));
    x86::Assembler assember(&code);
    
    // 原始功能指令前面
    if(!first) {
        assember.pop(registers[index]);
        assember.popfd();  // 保护标志位
    } else {
        first = false;
    }
    
    DWORD prexCodeSize = code.sectionById(0)->buffer().size();
    
    // 处理原始功能指令
    if((instr.isJcc || instr.isCall) && (instr.operatorType == UD_OP_JIMM)) {
        // 处理跳转和call指令
        char* new_jcc = new char[6];
        DWORD jcc_padding = 0xAAAAAAAA;  // 跳转偏移占位
        WORD jcc_opcode = this->jcc_long_opcode[instr.type];
        // 特殊处理call和jmp指令
        // ...
        assember.embed(new_jcc, insn_len);
        delete[] new_jcc;
    } else {
        assember.embed((char*)instr.memoryFileAddress, instr.insnLen);
    }
    
    // 原始功能指令后面
    assember.pushfd();  // 保护标志位
    index = this->GetRandomKey() % sizeof(registers)/sizeof(x86::Gp);
    assember.mov(x86::dword_ptr(x86::esp, -4), registers[index]);
    assember.add(x86::esp, -4);
    
    Label label = assember.newLabel();
    assember.call(label);
    assember.bind(label);
    assember.pop(registers[index]);
    
    int num = -(13 + prexCodeSize + insn_len);
    assember.add(registers[index], num);
    assember.add(registers[index], 0xdeadbeaf);  // 占位,需要修复
    assember.push(registers[index]);
    assember.ret();
    
    // 保存混淆块
    CodeBuffer& buffer = code.sectionById(0)->buffer();
    ObfuscateInstr instrchunk;
    instrchunk.code = new char[buffer.size()];
    ::memcpy(instrchunk.code, buffer.data(), buffer.size());
    instrchunk.size = buffer.size();
    instrchunk.prexCodeSize = prexCodeSize;
    chunkbox.push_back(instrchunk);
}

3.2.3 随机分配指令块

DWORD buffer_index = 0;
DWORD buffer_size = function.size * 100;  // 申请100倍原始函数空间
char* buffer = new char[buffer_size];

map<DWORD64, DWORD64> orign_chunk_map;  // 原始地址到混淆后地址的映射
vector<int> indexTable;  // chunkbox的索引表

// 随机分配指令块
while(!indexTable.empty()) {
    int key = this->GetRandomKey() % indexTable.size();
    int index = indexTable[key];
    
    memcpy(buffer + buffer_index, chunkbox[index].code, chunkbox[index].size);
    DWORD64 addr = (DWORD64)buffer + buffer_index;
    DWORD64 va = VirtulAddress + (addr - (DWORD64)buffer);
    
    chunkbox[index].memoryAddress = addr;
    chunkbox[index].virtulAddress = va;
    orign_chunk_map.insert(pair<DWORD64, DWORD64>(
        instrbox[index].memoryFileAddress, addr));
    
    buffer_index += chunkbox[index].size;
    
    // 添加随机垃圾数据
    int junk_size = this->GetRandomKey() % 16 + 6;
    this->GetRandomBytes(buffer + buffer_index, junk_size);
    buffer_index += junk_size;
    
    // 从索引表中移除已处理的指令块
    auto iter = indexTable.begin();
    iter += key;
    indexTable.erase(iter);
}

3.2.4 修复指令执行顺序和跳转

DWORD offset_flag = 0xdeadbeaf;
DWORD jcc_flag = 0xAAAAAAAA;

for(int i = 0; i < chunkbox.size(); i++) {
    char* begin = (char*)chunkbox[i].memoryAddress;
    char* end = (char*)chunkbox[i].memoryAddress + chunkbox[i].size;
    
    DWORD offset;
    if(i < chunkbox.size() - 1) {
        offset = chunkbox[i+1].memoryAddress - chunkbox[i].memoryAddress;
    } else {
        // 最后一条指令随机跳转到前面的指令块
        int k = this->GetRandomKey() % (chunkbox.size() - 1);
        offset = chunkbox[k].memoryAddress - chunkbox[i].memoryAddress;
    }
    
    // 修复相邻指令执行顺序
    char* ptr = this->SearchBytes(begin, end, (char*)&offset_flag, sizeof(DWORD));
    memcpy(ptr, &offset, 4);
    
    // 修复jcc指令跳转偏移
    if((instrbox[i].isJcc || instrbox[i].isCall) && 
       (instrbox[i].operatorType == UD_OP_JIMM)) {
        char* ptr = this->SearchBytes(begin, end, (char*)&jcc_flag, sizeof(DWORD));
        DWORD64 addr = 0;
        
        if((instrbox[i].type == UD_Icall) || 
           (orign_chunk_map.count(instrbox[i].jmpMemoryFileAddress) <= 0)) {
            // 处理call或跳转到函数外的情况
            addr = instrbox[i].jmpLoadImageAddress;
            DWORD64 va = VirtulAddress + ((DWORD64)begin + chunkbox[i].prexCodeSize - (DWORD64)buffer);
            offset = addr - va - 5;  // jmp/call指令长度是5
        } else {
            // 处理函数内跳转
            addr = orign_chunk_map[instrbox[i].jmpMemoryFileAddress] + chunkbox[i].prexCodeSize;
            if(instrbox[i].type == UD_Ijmp) {
                offset = addr - ((DWORD64)begin + chunkbox[i].prexCodeSize) - 5;
            } else {
                offset = addr - ((DWORD64)begin + chunkbox[i].prexCodeSize) - 6;
            }
        }
        memcpy(ptr, &offset, 4);
    }
}

3.2.5 重定位修复

// 保存新的重定位项
for(auto relocInstr : relocInstrBox) {
    DWORD index = relocInstr.index;
    DWORD64 relocVa = chunkbox[index].virtulAddress + chunkbox[index].prexCodeSize + relocInstr.off;
    DWORD rva = relocVa - this->m_pefile.getOptionHeader()->ImageBase;
    DWORD orignRva = instrbox[index].loadImageAddress + relocInstr.off - 
                     this->m_pefile.getOptionHeader()->ImageBase;
    
    WORD typeOffset = rva % 0x1000;  // 新的typeOffset
    DWORD newPage = rva - typeOffset;
    typeOffset |= ((WORD)(relocInstr.type << 12));
    
    RelocFixer fixer;
    fixer.orignRva = orignRva;
    fixer.newPage = newPage;
    fixer.typeOffset = typeOffset;
    relocFixBox.push_back(fixer);
}

3.2.6 修改原函数入口

char* begin = (char*)function.memoryFileAddress;
char opcode[] = {0xe9, 00, 00, 00, 00, 0xc3};  // jmp imm + ret

DWORD64 firstCodeVa = VirtulAddress + (chunkbox[0].memoryAddress - (DWORD64)buffer);
DWORD jmpoffset = firstCodeVa - function.loadImageAddress - 5;
memcpy(opcode+1, &jmpoffset, 4);
memcpy(begin, opcode, 6);

begin += 6;
this->GetRandomBytes(begin, function.size - 6);  // 剩余空间填充随机数据

四、PE文件处理

4.1 段合并与新增

  1. 找到reloc段的前一个段
  2. 向下合并reloc段
  3. 创建新的text段存放混淆代码
  4. 在新text段后面创建reloc段
  5. 修复重定位信息

4.2 重定位处理

混淆前必须扫描记录函数的所有重定位信息,混淆过程中将原始重定位信息和新重定位信息关联,以便后续修复。

五、总结

本文介绍了一种基于X86架构的函数分析和代码块粉碎技术,主要包括:

  1. 函数识别算法:通过多种启发式规则识别二进制文件中的函数
  2. 指令粉碎技术:将函数指令随机乱序并保证执行顺序不变
  3. 跳转修复:维护原始跳转关系并在混淆后修复
  4. 重定位处理:正确处理混淆后的重定位信息
  5. PE文件处理:合理组织段结构以容纳混淆代码

该技术可以有效增加逆向分析的难度,阻止IDA等工具的F5伪代码分析功能,但需要注意这只是一种基础的混淆方法,高级逆向工程师仍然可以通过动态分析等手段还原原始逻辑。

X86指令混淆之函数分析和代码块粉碎教学文档 一、指令混淆概述 指令混淆是一种通过改变代码结构而不改变其功能的技术,主要用于增加逆向分析的难度。本文档将详细介绍基于X86架构的函数分析和代码块粉碎技术。 二、函数分析技术 2.1 基本概念 函数分析是指从二进制代码中识别出函数的起始地址和大小,类似于IDA的sub_ xxx标注功能。由于不同编译器生成的函数特征各异,准确识别函数具有挑战性。 2.2 函数识别算法 2.2.1 基本规则 向下跳转记录 : 记录所有jcc指令跳转目的地址 保留跳转目的地址最大的一个 ret结尾识别 : 遇到ret指令时比较当前地址与最大跳转地址 当前地址>跳转地址:函数结束 当前地址 <跳转地址:从跳转地址继续分析 其他结尾判断条件 : 向上跳转的无条件jmp指令 nop指令(0x90) 连续两个以上int3指令(0xCC) add [ eax ], al指令(0x00,0x00....) 第一条指令是无条件jmp 下一条指令是另一个函数的开始特征 Call指令处理 : 遇到call立即数指令,将目的地址加入待分析集合 其他启发式规则 : 根据编译器特定特征 利用调试信息(如果存在) 2.2.2 代码实现 三、代码块粉碎技术 3.1 基本思路 以函数为单位进行混淆 抽取函数所有指令 申请新空间并随机乱序放置指令 增加指令保证执行顺序不变 修复跳转关系和重定位表 3.2 实现步骤 3.2.1 指令预处理 3.2.2 指令混淆块生成 3.2.3 随机分配指令块 3.2.4 修复指令执行顺序和跳转 3.2.5 重定位修复 3.2.6 修改原函数入口 四、PE文件处理 4.1 段合并与新增 找到reloc段的前一个段 向下合并reloc段 创建新的text段存放混淆代码 在新text段后面创建reloc段 修复重定位信息 4.2 重定位处理 混淆前必须扫描记录函数的所有重定位信息,混淆过程中将原始重定位信息和新重定位信息关联,以便后续修复。 五、总结 本文介绍了一种基于X86架构的函数分析和代码块粉碎技术,主要包括: 函数识别算法:通过多种启发式规则识别二进制文件中的函数 指令粉碎技术:将函数指令随机乱序并保证执行顺序不变 跳转修复:维护原始跳转关系并在混淆后修复 重定位处理:正确处理混淆后的重定位信息 PE文件处理:合理组织段结构以容纳混淆代码 该技术可以有效增加逆向分析的难度,阻止IDA等工具的F5伪代码分析功能,但需要注意这只是一种基础的混淆方法,高级逆向工程师仍然可以通过动态分析等手段还原原始逻辑。