原理

二进制分析的时候经常遇到经过混淆的函数,所以一直想自己学习实现一个简单的指令乱序demo,也不需要太复杂(其实就是自己菜而已)。能阻止IDA F5党(就是我)就可以了(能去混淆的师傅除外),常见的指令乱序是把一段代码根据jcc指令划分成若干基本块jcc指令通俗来说就是常见的跳转指令诸如jz,jnz,jmp....此类。基本块的概念参考IDA截图,像这种loc_xxx就能看作基本块。

更直观一点就是下面这张图,代码被划分成块,执行流程被分析的明明白白

划分完基本块之后再打乱或者隐藏各个基本块之间的直接联系,使静态反编译工具无法分析执行流程。
更无法通过F5看伪代码。

最简单最原始的做法就是增加新的代码块A,找出所有jcc指令,修改该指令跳转到A,
再通过A跳转到正确的代码块,代码块A可以根据数学公式实现一些运算,动态计算出跳转地址,模糊控制流
这种做法也被大牛们叫做控制流程平坦化,代码块A也叫做控制分发器,负责分发指令跳转。

当然这只是最简单最基本的控制流程平坦化,去混淆也很容易,几乎可以静态将代码打回原形
我没有采用上面的方法,我的基本想法是以函数为单位进行混淆,比如有函数F,抽取出F函数的所有指令,
申请一个新的空间将每条指令随机乱序放置在新的空间,再增加指令保证两条指令的执行顺序和原始函数一致
可以采用上面说的复杂算法计算出下一条指令的地址也可以使用直观的跳转指令进行链接
实现每条指令空间顺序上的随机乱序,但是执行顺序不变,空间上相邻的两条指令之间也可以生成一些大小随机的花指令进行干扰。

最后修复跳转关系和重定位表。这样就完成了对一个函数的“粉碎”。
使用工具:自己撸的一个PE操作类,反汇编引擎使用的udis86,汇编引擎使用的asmjit

asmjit

udis86


函数分析

函数分析的意思是,给定一个代码块,识别出函数的起始地址和大小,类似IDA以sub_xxx标注出函数的功能
如图

正确识别出函数是很困难的事情,因为每个编译器生成的函数特征可能都不一样,比如某些函数以ret指令结尾,
有些函数根本没有ret指令,有些函数也不是以push xxx开头。所以只能尽可能加入较多的函数特征

连IDA这种级别的反编译器都不可能百分百识别出代码和数据有些编译器把部分数据和代码混合编译在一起,
比如delphi。或者编程者故意插入了导致某些反编译结果出错的花指令,这种情况是无法分析函数的。

参考了玩命的关于代码数据识别的文章,自己再总结了一些规则,得出能识别大部分函数的算法,
暂时没有加入识别某些delphi函数的规则,这类函数代码和数据混杂在了一起。

基本算法如下

1.jmp immediate(立即数)

  • 反编译过程中记录遇到的所有jcc指令跳转目的地址,每次都和新遇到的jcc指令目的地址比较,记录下跳转目的地址最大的一个
    ### 2.ret结尾识别
  • 遇到ret指令则比较前面保留的最大跳转地址和当前地址,如果当前地址大于跳转地址则函数结束,如果小于跳转目的址则从跳转地址开始继续分析
    ### 3.其他情况结尾判断
  • 如果遇到向上跳转的无条件jmp指令则函数结束
  • 如果找到nop (0x90)则函数结束
  • 如果找到至少连续两个以上int3 (0xCC)则函数结束
  • 如果找到add [eax], al (0x00,0x00....)则函数结束
  • 函数第一条指令是无条件jmp则函数结束,并把jmp指令目标地址加入待分析函数地址集合
  • 如果下一条指令是另一个函数的开始,(比如遇到指令push esp,mov ebp, esp) 则函数结束,并把下一条指令地址加入待分析函数地址集合
    ### 4.Call immediate(立即数)
  • 遇到call立即数指令则把目的地址加入待分析函数起始地址集合
    ### 5.其他
  • 如果程序有调试信息也可以根据调试信息来区分指令数据和函数,这里的规则肯定代表全部,可以根据每个编译器的不同加入自定义规则
  • 基本思路是从pe文件入口点开始使用上面的算法启发式分析,遇到函数调用就把调用目地地址加入待分析集合,重复以此

部分代码如下

/***
* 指令流节点
*/
typedef struct _Instr_Flow_Node
{
    bool isJmpDown;//是否向下跳
    DWORD64 jmpRange;//跳转范围大小
    ud_mnemonic_code type;//指令类型
    ud_type operatorType;// 操作数类型   1.跳转立即数 2.寄存器 3.内存地址
    bool isJcc = false;//是否是jcc类型的指令
    bool isCall = false;//是否是Call类型的指令
    DWORD64 loadImageAddress;//当前指令虚拟内存
    DWORD64 memoryFileAddress;//当前指令文件内存
    DWORD64 jmpLoadImageAddress;//跳转目的地虚拟内存
    DWORD64 jmpMemoryFileAddress;//跳转目的地文件内存
    DWORD insnLen;//指令长度

    //jcc立即数跳转类型的跳转偏移量
    struct
    {
        union
        {
            int8_t sbyte;
            int16_t sword;
            int32_t sdword;
        };
    } jmpOffset;


    bool operator < (const _Instr_Flow_Node & node) const
    {
        return this->memoryFileAddress < node.memoryFileAddress;    //  < 升序
    }

    bool isInvalid()
    {
        return this->type == UD_Iinvalid;
    }

} InstrFlowNode;
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())
        {
            //遇到无效指令(可能花指令),则停止分析该函数,返回前面分析完成的部分,可能函数长度为0
            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)
            {
                //记录CFG流图中跳转目标地址最大的跳转指令
                jcc_max = jcc_flow;
            }
        }

        switch (jcc_flow.type)
        {

        case UD_Ijmp:
        {
            //如果无条件跳转目标地址小于函数起始或者当前跳转指令是函数第一条指令则视为结束
            //并把目标地址加入预分析函数节点
            if (jcc_flow.operatorType == UD_OP_JIMM)
            {

                if ((jcc_flow.jmpMemoryFileAddress < begin) || (jcc_flow.memoryFileAddress == begin))
                {
                    if (functionMap != nullptr)
                    {
                        //如果排除map中不存在已经分析过的函数则插入节点
                        if ((excludeMap != nullptr) && (!excludeMap->empty()))
                        {
                            if (excludeMap->find(jcc_flow.jmpMemoryFileAddress) == excludeMap->end())
                            {
                                FunctionNode node(jcc_flow.jmpMemoryFileAddress, jcc_flow.jmpLoadImageAddress);
                                functionMap->operator[](node.memoryFileAddress) = node;
                            }

                        }
                        else
                        {
                            FunctionNode node(jcc_flow.jmpMemoryFileAddress, jcc_flow.jmpLoadImageAddress);
                            functionMap->operator[](node.memoryFileAddress) = node;
                        }


                    }

                    return FunctionNode
                    (
                        begin,
                        pc,
                        jcc_flow.memoryFileAddress + jcc_flow.insnLen - begin
                    );
                }
            }
            break;
        }
        case UD_Icall:
        {
demo.rar (1.493 MB) 下载附件
点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖