代码混淆之道(一)

本文原创作者:penguin_wwy,
本文属i春秋的原创奖励计划,未经许可禁止转载!


代码混淆是软件保护技术的一种,而且是最重要却又最难以捉摸的一类(这话不是我说的,是Christian Collberg和Jasvir Nagra说的)。
说它难以捉摸是因为很难明确定义,很难设计出切实有效的混淆算法,也很难针对混淆算法的质量进行评估。
说它很重要是因为如果能在它定义的问题上取得一致,并设计出强壮的算法,我们就可以解决许多安全和密码学上的实际问题。

一、程序分析之路
有过逆向经验的盆友们都会运用各种各样的工具对程序进行分析,比如利用OD对程序进行调试、IDA分析函数的控制流程、分析函数间的调用关系,这些都是分析程序的方法。
破解者使用的程序分析技术大致可以分为两大类:静态分析——通过分析代码获取信息;动态分析——通过运行程序收集信息。
对于程序的静态分析又包括但不限于以下几种方法:
1、控制流分析,也就是画出程序运行的流程图
2、数据流分析,获得变量使用的保守信息
3、数据依赖分析,变量间的依赖关系(赋值、使用、判断顺序等等)
4、别名分析,在程序运行的某个时间上多个变量必然或可能指向同一个内存地址
5、切片,对目标变量的影响
6、抽象解析,定性而不是定量的判读(比如偶数A和B,那么 (A + B)% 2 == 0必然成立 )
动态分析包括但不限于调试、剖分、trace等等手段
此外反汇编、反编译是对无源码程序分析的必经之路。以上这些是破解者常用的甚至必须的对针对软件进行攻击的手段。

二、代码混淆的价值
所谓混淆,就是针对需要被保护的程序P,经过混淆后转换为P',P和P'的行为保持一致,但是攻击者很难从P'中获取想要的信息。这里的很难指攻击者在分析P'时需要付出比P多的资源(时间、人力)才能从P'中获得和P一样的信息,这个可以是验证码、敏感的个人信息、软件核心运行流程等等。
代码混淆同样也分为针对静态分析的静态混淆和针对动态分析的动态混淆。
静态混淆在程序执行前代码便固定下来了,典型的比如压扁控制流、复杂化控制流、不透明谓词等。动态混淆自然是执行时代码仍在变化,比如代码自修改、程序状态机等等。

三、一种简单的混淆算法——OBFLDK算法
该算法基本思路就是把程序中的一个无条件转移指令替换成调用一个跳转函数的指令。在正常的思维中函数执行完成后应该返回到函数调用位置的下一条指令
代码混淆之道(一)_第1张图片 
而之所以调用函数会返回到函数调用的下一条指令,是因为在call指令执行的时候,next指令的地址被保存起来,也就是函数的返回地址。那么当函数返回的时候,返回地址就会被写入到PC寄存器中,从而开始执行函数调用的下一条指令。如果我们修改了函数的返回地址,那不就意味着我们可以控制函数返回之后下一条被执行指令的地址了吗
代码混淆之道(一)_第2张图片 
可以看到,我们通过一个jump_function保持原有的代码code2——>code3的执行顺序不变,而在紧接jump_function的地方,也就是默认在函数调用后被执行的位置插入的是垃圾代码。那么原有的CFG(控制流图)由
code1——>code2——>code3
变成
code1——>code2——>jump_function——>垃圾代码——>code3
而这段垃圾代码在程序当中是永远不会被执行的,也就是说我们可以把它写成任何东西都不会影响到程序本身的正确性


下面我们来做一个简单的例子,为了方便我们计算返回地址的位置,我们可以借助宇宙第一IDE——Vistual Studio的反汇编功能

首先准备一个灰常简单的函数

int setFunc(int *val) {
        *val += 5;
        return 0;
}

然后我们开始对他进行修改,在return 0之前插入我们的跳转函数和垃圾代码

int setFunc(int *val) {
        *val += 5;
        jmpFunc();
                 
        getEIP();
        _asm {
                sub                eax, 05h
                jmp                eax
        };
        return 0;
}


jmpFunc就是跳转函数,这条函数会将返回地址设置为return 0所在的位置

int jmpFunc() {
        _asm {
                mov                eax, [ebp+04h]        //取出返回地址
                add                eax, 0ah                //定位到return 0
                mov                [ebp+04h], eax        //写回返回地址
        }
        return 0;
}


_asm是关键字,花括号内跟随的是汇编代码。
根据x86的调用规则,函数的返回地址会在保存在栈上EBP+4的位置,把返回地址保存到eax寄存器,然后根据计算将返回地址定位到return 0
计算过程为设置好断点开始debug,然后alt+8打开VS的反汇编窗口
代码混淆之道(一)_第3张图片 
上一个箭头就是此时eax保存的地址,下一个就是我们希望跳转的位置,计算一下差10个字节,所以给eax加上0ah

主函数

int main()
{
        int a = 10;
        setFunc(&a);
        char *str = "this is good";
        printf("%s\n", str);
        getchar();
        return 0;
}


如果执行成功会输出this is good
至于中间的垃圾代码,我们可以看到是一个死循环
void getEIP() {
        _asm {
                xor                eax, eax
                mov                eax, [ebp+4]
        }
}

代码混淆之道(一)_第4张图片 
执行成功

进一步的,我们说过这段垃圾代码可以是任何东西,我们可以在垃圾代码所在的位置上填充任何东西。而由于这些代码紧接在函数调用之后,所以IDA这类的反汇编工具会把这些字节识别为可执行代码
用010Edit打开我们编译生成的可执行程序,然后找到那段垃圾代码
代码混淆之道(一)_第5张图片 
一共10字节,把他们换成hello world的大写字母
代码混淆之道(一)_第6张图片 
程序可以正常执行,丢到IDA中,找到对应的位置
代码混淆之道(一)_第7张图片 
从D0到D9位置就是我们大写的hello world,但是IDA把它们全都识别为可执行代码。。。WTF!

你可能感兴趣的:(编程语言)