代码混淆/程序保护(对抗反汇编)原理与实践

来源:互联网 发布:注册淘宝药店 编辑:程序博客网 时间:2024/06/06 18:38

所谓对抗反汇编技术,就是在程序中使用特殊构造的代码或数据,让反汇编工具产生不正确的指令。恶意代码使用该技术,可以一定程度上阻碍相似性检测算法和启发式反病毒检测。

一、反汇编算法的局限性

反汇编软件在拿到一堆机器码时,采用什么样的思维和算法来“断词断句”,又基于哪些假设,都会决定最终的解析结果。运用不同的反汇编器,同样的字节码会被“翻译”出完全不同的指令序列。而对抗反汇编技术就是利用了反汇编器算法的天生漏洞来实现的。现有的反汇编算法有两种,各自皆有不同的局限性。

1、线性反汇编

线性反汇编容易实现,也最容易出错。来看它的算法原理:

char buffer[BUF_SIZE]int position=0;while(position<BUF_SIZE){    x86_insn_t insn;    int size=x86_disasm(buf,BUF_SIZE,0,position,&insn);    if(size!=0){        char disassembly_line[1024];        x86_format_insn(&insn,disassembly_line,1024,intel_syntax);        printf("%s\n",disassembly_line);        position += size;    }else{        //invalid(unrecognized instruction)        position++;    }}

可以看出,该算法优先将字节码翻译成指令。然而二进制文件中,既有代码,也有数据。恶意代码编写者就可以利用这个漏洞,植入能够组成多字节指令机器码的数据字节。这样,无关的一些数据就会被“翻译”成call指令、jmp指令等等,干扰分析。

2、面向代码流反汇编

IDApro等软件运用的就是这种更先进的反汇编算法。它并不盲目地反汇编整个缓冲区,也不假设仅有指令而无数据,相反,它会随着代码逻辑,边走边“翻译”,建立一个需要反汇编的地址队列。

然而即便如此,它也有自身的局限性。例如,条件分支中,它优先处理false分支,往往会把作者有意植入的一些数据字节“翻译”成指令,产生错误结果。

当IDA反汇编出错时,需要我们手动将数据和指令互相转换。C键可以将光标处的数据转换成代码,D键可以将光标处代码转换成数据。

二、对抗反汇编技术

1、相同目标的跳转指令

jz loc_512之后紧接着jnz loc_512,等价于jmp指令。然而反汇编器每次只反汇编一条指令,所以意识不到这一点,仍然继续反汇编jnz的false分支。例如:

image

IDA反汇编出来的错误结果:

74 03            jz  short near ptr loc_4011C4+175 01            jnz short near ptr loc_4011C4+1                  loc_4011C4:     ...E8 58 C3 90 90   call near ptr 90D0D521h

由于总是优先处理false分支,而没有来得及查看跳转地址究竟在哪儿,从而意识不到跳转地址是同一个,并且就在附近。

E8以及之后的字节被翻译成Call指令是完全错误的,因为根本不会被执行。实际上E8是植入的流氓字节,它掩盖了58 C3作为pop eax,ret这两条真实的指令。

手工修正,将E8修正为数据,将loc_4011C5处转换成指令:

74 03            jz  short near ptr loc_4011C575 01            jnz short near ptr loc_4011C5E8               db 0E8h                 loc_4011C5:  ...58               pop    eaxC3               ret

2、永真条件的跳转指令

另一种常见的技术是由跳转条件恒定的跳转指令构成的。例如:

image

与上面一种方法类似,通过手动将eax清零(xor eax,eax)使得jz总是无条件执行。反汇编器总是先处理false分支,因而E9这个流氓字节又被错误地翻译成了jmp指令,掩盖了其后的真实指令pop,ret

3、无效的反汇编指令

前面所讲的技巧是插入流氓字节,用其掩盖随后的真实指令,这些流氓字节可以被忽略。

然而也有一种情况,流氓字节不能被忽略,并且甚至还参与到代码执行中。例如:

image

这串代码写得真是巧妙,加黑的字节被当作两个指令的一部分,执行了两次。顺着代码跳转逻辑,我们发现,实际上最终会落到E8之后的Real Code中,E8本身不会被执行。然而反汇编器还是把E8翻译成call指令。

这种情况下,需要用IDA Python脚本将这些重叠执行、无法取舍的指令变成nop指令,留下实际影响指令(即 xor eax,eax)

三、混淆控制流图

混淆控制流是一种对抗反汇编的方法,可以使分析人员忽视一些代码,通过模糊函数与其他函数掉调用关系达到隐藏函数的目的。这种情况下,交叉引用可能失灵,为分析带来困难。常见的方法有:

1、刻意使用函数指针

刻意使用函数指针,大大降低了反汇编器自动推导出的信息,从而隐藏了交叉函数引用。例如:

image
image

函数sub_4011D0中有三处调用了上一个函数,然而只有第一个地方被IDA识别出,并添加交叉引用。这种情况下,第二处、第三处的调用函数行为久不容易被发觉,也因此丢失了很多调用函数的原型信息。

2、滥用ret

ret <==> pop + jmp。无缘无故出现的ret指令,会令反汇编器不知所措,甚至识别不出函数的存在。例如:

image

add指令的作用是将栈顶esp存放的值变为0x004011CA。接着,使用流氓指令ret,实质是call了0x004011CA处的函数。然而,IDA却没有识别到0x004011CA处是一个函数。

四、反汇编案例实践

分析Lab15-01.exe。它是一个命令行程序,添加了代码混淆。只有输入正确的password,才会显示“Good Job!”

1、处理混淆代码片段

(1)第一处0x40100E

image

出现了利用永真跳转条件,植入流氓字节E8,误导反汇编器解析成call指令的情况。上图中标为红色的交叉引用也印证了这一点。

image

上图,E8和其后的字节被解析成了call指令,从而掩盖了真实的代码。手动修改,将E8转为数据;将E8之后的字节转换为代码。随后碰到db就转换为指令,使得指令对齐,中间没有数据。

调整后的正确汇编代码如下:

image

此时,交叉引用恢复了颜色。

(2)第二处0x40101F

同理,修正。

(3)第三处0x401033

(4)第四处0x401047

原来的问题代码:

image

修正后,发现原来在4B到50之间隐藏的一些字符串(如“Good Job!”)显露了出来:

image

“Good Job!”这样的字符串对逆向分析非常有帮助。而对抗反汇编技术恰恰是通过流氓字节来将其隐藏。

(5)第五处0x40105E

修正后,又暴露出了一些有用的字符串:

image

2、分析password值

选中0x401000到0x401077ret之间的代码,按p键,强制转换为函数,可以基本判定这就是主函数,接受输入参数,进而比对是否为password值。

image

上图中,三个jnz分支都跳转到一个地址,可以推出此地址就是处理password值不相等的分支。

image

重点关注这三段代码,发现:

第一个函数比对的是输入参数argv的第一个字母。如果和70h相等,就跳转到第二个函数loc_401024继续比较。

第二个函数比对的是输入参数argv的第三个字母。如果和71h相等,就跳转到第三个函数loc_401038继续比较。

第三个函数比对的是输入参数argv的第二个字母。如果和64h相等,就跳转到打印“Good Job!”的函数。

由此得出password/flag值为pdq。

3、运行验证

image

五、小结

学习两种反汇编算法的不同与缺陷,以及漏洞利用。

对抗反汇编技术主要针对面向代码流的反汇编器,构造一些条件指令,植入无关字节,达到保护真实指令的目的。

混淆控制流用于隐藏函数信息、函数调用关系,给分析带来困难。

这种技术有其两面性。用在正的方面,可作为软件保护,防止破解;用在反的方面,可以为病毒木马增加保护,干扰分析。

通过反汇编一个实例,学习了代码混淆/程序保护的原理和方法,加深了对汇编语言、逆向分析的理解,收获颇丰。


参考资料《恶意代码分析与实战》

0 0
原创粉丝点击