入门知识,高手勿读
一、 概述
花指令是对抗反汇编的有效手段之一,正常代码添加了花指令之后,可以破坏静态反汇编的过程,使反汇编的结果出现错误。错误的反汇编结果会造成破解者的分析工作大量增加,进而使之不能理解程序的结构和算法,也就很难破解程序,从而达到病毒或软件保护的目的。
二、花指令分类
[2.1]可执行式花指令
顾名思义,可执行式花指令指的是能够正常运行的但又不改变原始程序逻辑性的一组无用指令。这类花指令有如下特点:①可以正常运行;②不改变任何寄存器的值;③反汇编器可以正确反汇编该指令。
例如这样几组花指令就属于该类别:PUSHEAX&POPEAX;NOP:INCEAX&DECEAX等等。这种类别的花指令组合形式很多,常常用在病毒代码的变形引擎中,病毒在传播时通过变形引擎随机产生一组该类别花指令并插入到病毒正常代码中,可以改变病毒的特征码,从而起到变形的作用。
[2.2]不可执行式花指令(垃圾指令)
本文主要就是讲这个方面,是指被插入到原始代码中但又不改变原始程序逻辑性的一组无用字节。这类花指令有如下特点:①不可以正常运行;②不改变任何寄存器的值;③反汇编器可能会错误反汇编这些字节。
根据反汇编的工作原理,只有当花指令同正常指令的开始几个字节被反汇编器识别成一条指令时,才能有效破坏反汇编的结果。因此,插入的花指令应当是一些不完整的指令,被插入的不完整指令可以是随机选择的。正因为不可执行花指令有这些特点,该类花指令才能应用到软件保护中。
Cullen等人指出为了能够有效“迷惑"静态反汇编工具,同时保证代码的正确运行,花指令必须满足两个基本特征,即:
1)垃圾数据必须是某个合法指令的一部分:
2)程序运行时,花指令必须位于实际不可执行的代码路径。
三、不可执行花指令的成功来自反汇编算法的缺陷
当前静态分析中采用的反汇编算法主要可以分为2类:线性扫描算法与行进递归算法。
[3.1]线性扫描反汇编算法
线性扫描算法p1从程序的入口点开始反汇编,然后对整个代码段进行扫描,反汇编扫描过程中所遇到的每条指令。线性扫描算法的缺点在于在冯诺依曼体系结构下,无法区分数据与代码,从而导致将代码段中嵌入的数据误解释为指令的操作码,以致最后得到错误的反汇编结果。
[3.2]行进递归反拒绾算法
相比线性扫描算法,行进递归算法通过程序的控制流来确定反汇编的下一条指令,遇到非控制转移指令时顺序进行反汇编,而遇到控制转移指令时则从转移地址处开始进行反汇编。行进递归算法的缺点在于准确确定间接转移目的地址的难度较大。
四、简单不可执行花指令
实际编程中不要简单花指令,否则容易被去除或被认为是病毒。
下面是的最典型形式:
jmp Label1
db thunkcode1;垃圾数据
Label1:
……
解析:
1.Jmp可以用call,ret,loop等替换
2.该垃圾数据通常是一条多字节指令的操作码,例如在thunkcode1处放入0e8h,由于0e8h是call指令的操作码,因此对0e8h进行解码时就会将它后面的4个字节,也就是maliciouscode的前4个字节看作是调用目标地址,从而造成反汇编过程中的错误,达到隐藏恶意代码的目的。
五、稍复杂的花指令
[5.1]多节形式
典型形式的条件跳转混淆可能仅仅造成几条指令的反汇编错误,但是下面的多重顺序嵌套的条件跳转混淆则能够使更多指令的反汇编得到错误的结果。
举例:
JMP Label1
Db thunkcode1
Label1:
……
JMP Label2
Db thunkcode2
Label2:
……
[5.2]多层乱序
在上面的形式中,可以简单地将条件跳转到跳转目的地址之间的所有字节进行填充来破解混淆,于是有了条件混淆的一种新的形式,即多层乱序嵌套:
JMP Label1
Db thunkcode1
Label2:
……
JMP Label3
Db thunkcode3
Label1:
…….
JMP Label2
Db thunkcode2
Label3:
……
一、概述
“jmp/call/ret+垃圾数据”这样的花指令已经是相当“老掉牙”了,OD的插件对付它们基本是“秒杀”,所以本文想说点“有点创意”的花指令,至少能对付OD的插件。
二、创意的核心
[2.1]花指令因何被发现
“jmp/call/ret+垃圾数据”的方法之所以容易被检测,原因是“意图太明显”,具体说任何一种检测手段,都是基于以下两点:
①检测跳转结构的固定形态特征,当然这不是绝对的,如果检测机制没能预见到这种形态特征,此点可以忽略,本文后边会提到一些这样的情况。
②用可靠的方法直接证明,垃圾数据部分在任何情况下都不会被执行。应该说该点是对“不可执行花指令”的无敌检测手段,然而却难在“可靠”二字上,目前已知的实践,只是做到“可供参考”的程度。
[2.2]创意的策略
通过[2.1]的分析,我们知道要想编写“有创意的花指令”,火力应集中在①上,因此我们通过以下策略对抗:
① 采用新的固定结构,许多插件还没有检测,比如下文提到的xor-cmp的结构。
② 采用“伪条件跳转”替代“强制跳转”
三、一步一步学创意
[3.1]一次幼稚的实践——互补条件跳转
首先,我们来尝试一种用“伪条件跳转”替代“强制跳转”的方法,称作“互补条件跳转法”,即用两个互补的条件跳转指令替代一个强制跳转指令。比如,用jz和jnz跳转到同一地址,和一个jmp是等价的。
举例:
……
Jz Label
Jnz Label
Db thunkcode;垃圾数据
Label:
……
除了jz和jnz,还有许多互补跳转指令,如表1。
经过测试,只有一少部分OD插件没能识别这类花指令,究其原因还是这种方法形态太过固定,一旦形态特征广为人知,检测就相当容易。
[3.2]上面方法的改进版——用随机值获得确定性标志位
上面的方面采用两个互补的条件跳转还是明显,连普通程序都不会使用。所以,我们应该试图只用一个条件跳转。具体方法是这样的:
通过某些隐蔽的手法,使得某个标志位有确定性的值,然后利用这个确定值,进行确定性条件跳转。比如下面的手法
Xor reg,value1
Cmp reg,value1
Jnz Label
Db thunkcode
Label:
……
该手法的优点是,无论value为何值,经过了xor reg,value1和cmp reg,value这两句指令,,ZF位一定不会置位,所以jnz一定跳转。该方法有相当多的变体,该方法的核心是使用随机值,使得某标志位有确定性值。
经过测试,OD的大部分插件都不能检测,这类花指令变种较多,插件编写容易遗漏或者未预见到某些形态,但是这种方法的形态特征还是相对固定的,只是某些还未广泛熟知,但随着时间推移,该方法的效力会越来越弱。
[3.3]利用API返回确定值
大部分API函数的返回值是不确定的,只要有方法使得API返回确定值,那么后面接续的条件跳转,就是等价的。比如说CreateFile返回文件句柄,句柄可能是任意值且有不确定性,然而我们可以使得函数CreateFile返回值是确定的。
1) 令CreateFile返回错误码,比如故意向CreateFile传入错误参数,还可以使用类似inc esp,使得堆栈不4字节对齐,即使传入正确参数,也会返回错误码。
2) 由于句柄值实际上是句柄表索引,所以CreatFile返回的正确句柄值都是4的倍数,我们把句柄值取4的模,得0是一定的。
类似的方法可以说是数不胜数,所以OD的插件想通过检测固定形态的方法,是无法穷举的。所以实际检测结果我不必说了吧。
任何技术都有被攻破的一天,没有包打天下的方法。再强的花指令也是有可能被清除的,这时我们也要想办法应对。所以我们经常需要检测花指令是否被清除,加以应对。
一、花指令被清除后的痕迹
我们来看一下,下面一段简单花指令被清除后的结果:
代码:
jmp @F
byte 08eH
@@:
ret
清除后:
二、检测
从上文我们可以看到,花指令被替换指令NOP,这样就不必重构代码,就能够使代码正常运行。显然,我们只要NOP就行了。
举例:
.386
.model stdcall,flat
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
.data
Context byte "Junkcode has been cleaned",0
Tip byte "Tip",0
.code
start:
JunkCode:
jmp @F
byte 8eH
@@:
mov al,byte ptr[$-1]
cmp al,90H
jnz Pro_End
invoke MessageBox,NULL,addr Context,addr Tip,MB_OK
Pro_End:
invoke ExitProcess,0
end start