10.1.5 资源与代码加密

10.1.5 资源与代码加密

代码加密是现代保护系统的核心。随着计算机硬件技术的迅猛发展,代码加密保护逐渐成为保护系统最可靠、最有效的保护手段,保护系统可以利用强大的处理器运算能力来膨胀被保护的代码,将一条原本非常简单的指令膨胀为成千上万条指令的组合,从而尽可能提高分析和破解代码的成本。在现代保护系统中,代码保护成为保护系统的核心,是加密与解密最激烈的“战场”。软件的安全性将严重依赖代码复杂化后被分析者理解的难度。因此,经过多年发展,在代码加密技术方面出现了相当多的技术,这里我们将着重介绍其中的部分常用技术。代码加密的最终目的都是将原始的代码转换为等价的、极其复杂的、更多的代码,这要求加密后的代码与加密前的代码在执行结果上尽可能等价。这不仅是加密的本质,也是我们以后要了解的解密的基础。

代码加密技术分为局部代码加密和全局代码加密,其区别在于:局部加密一般针对的是单条或者为数不多的几条指令,而全局加密是通过全局考虑程序的代码布局转而进行加密的技术。下面我们将介绍各种代码加密技术。

1.代码变形

代码变形技术是指将一条或多条指令转变为与执行结果等价的一条或多条其他指令。代码变形也分为局部变形和全局变形两种形式。局部变形一般只考虑一条代码的变形,而全局变形是将两条或者多条代码结合起来考虑变形。我们先来看一条指令的变形。

选取一条普通的指令,示例如图10.10所示。

图10.10 代码示例(a)

这是一条简单的赋值指令,目的是将CPU寄存器eax的内容设定为12345678h。如果我们要把这条代码复杂化,都有什么办法呢?我们分析图10.11所示这段代码。

图10.11 代码示例(b)

这是两条代码的组合,这个组合的功能是先将12345678h压入栈,然后弹出到CPU的eax寄存器。不难看出,这个过程的运行结果也是将12345678h放到寄存器eax中,因此这两条指令是等价的,可以置换。但是,我们能明显看出后面的代码比前面的代码复杂,因为后面是通过两个过程才实现了为eax赋值的目的。我们还可以进一步将这个过程复杂化,代码如图10.12所示。

图10.12 代码示例(c)

这段代码最终也实现了向eax赋值的目的,但是很明显,它已经变得复杂了。而且,在这段代码中又出现了类似“mov eax,12345678h”的代码“mov eax,1234h”,我们可以将这段代码用其他的等价代码替换,变成如图10.13所示代码。

图10.13 代码示例(d)

这段代码同样是等价的。这样进行下去,我们可以将一条简单的指令膨胀成任何数量的指令,如10 000条。那么,这会带来什么效果呢?

很明显,其结果就是:我们为了理解这个简单的赋值过程需要阅读这10 000条代码,最后才确定,原来只是做了一条指令的工作。这就是代码变形,甚至是所有代码加密的核心思路。因为人类永远无法设计出人类不能理解的代码,所以只能这样做。

单条代码的变形很容易理解。下面我们来看看多条代码,也就是全局代码的变形。

假设有如图10.14所示代码:

图10.14 代码示例(e)

这是两条简单的数据传送代码,但是这两条代码是有序列关联的,其中第二条代码的执行依赖于第一条代码指令的结果。当然,我们很容易想到可以将代码逐条变形,但是这么复杂的程序有时候不如同时变形两条代码。例如,可以用图10.15的代码进行等价替换。

图10.15 代码示例(f)

我们看到,这种变形和第一种变形是不一样的。在这种变形中,我们同时考虑了两条指令的执行,包括执行顺序和最终结果。我们无法看到单独的“mov cax,ebx”指令的等价替换,或者“mov ecx,eax”指令的等价替换,但整体执行的结果却是等价的,这就是全局代码变形的特点。这种特点将使通过变形后的代码推导出变形前的代码的操作更加困难。

上面的两种模式,就是基于代码指令变形的基本思路。

为了让读者了解代码变形在代码保护上的效果,我们可以看下面这样一个例子,如图10.16所示。

图10.16 代码变形效果图(a)

这是Win License代码虚拟机的入口代码,是典型的使用代码变形技术的例子。由于篇幅所限,下面直接给出我们关心的结束部分的代码,如图10.17所示。

图10.17 代码变形效果图(b)

0056899A处的call指令编号为313,说明从入口到这里执行了313条指令序列。我们用自动化技术执行代码简化,可以了解这300多行指令究竟有何作用,如图10.18所示。

图10.18 代码变形效果图(c)

我们不得不惊讶,前面的300多行指令,其目的不外乎就是模拟pushfd及pushad指令以保护环境而已,这充分展现了代码变形的威力。

2.花指令

花指令是代码保护中一种很简单的技巧。其原理是在原始的代码中插入一段无用的或者能够干扰调试器反汇编引擎的代码,这段代码本身没有任何功能性的作用,只是作为扰乱代码分析的手段。我们来看图10.19的代码。

图10.19 代码示例(a)

假设这是两条原始代码,我们可以通过在这两条代码中插入花指令使代码的分析复杂化。例如,可以将代码变成如10.20的代码。

图10.20 代码示例(b)

其区别就在于多了一条“mov eax,eax”指令。我们知道,这条指令没有任何用处,但是它增加了我们理解原始代码的难度。例如,至少在第二段代码中,我们不得不阅读三条指令。又如,我们可以将代码变成如图10.21的代码。

图10.21 代码示例(c)

大家也许会发现,这段代码变得很奇怪,怎么多出了一个“call”?但是,这段代码与图10.20的代码是等价的。这就是花指令的另外一个目的:扰乱调试器的反汇编引擎。大多数调试器的反汇编引擎都是静态工作的,也就是说,调试器的反汇编引擎只能通过顺序反汇编,反汇编完上一条指令时,根据指令长度来判断下一条指令的位置,但是它很难准确地侦测到代码运行时可能对代码执行流程造成的影响。在这段代码中,当0100368A处的代码被执行后,CPU会因为代码的控制而转入0100368D处。代码的执行流程如图10.22所示。

图10.22 代码示例(d)

0100368C处的代码永远不会被执行,但是调试器很难事先知道这一点,所以,这有效干扰了我们对代码的理解。

3.代码乱序

代码乱序的思路是非常容易理解的。代码指令一般都是按照一定序列执行的,如图10.23所示的代码。

图10.23 代码示例(a)

我们可以一眼看出其中的代码序列,并且可以连贯起来从整体上理解。代码乱序的意思就是,通过一种或者多种方法打乱这种指令的排列方式,以干扰大脑的直观分析能力。但是,为了保证执行结果的相同,代码乱序的主要目的是破坏我们的这种直观感受,代码的真实执行顺序是不能改变的。例如,我们可以将图10.23的代码变换为图10.24的等价代码。

图10.24 代码示例(b)

通过观察上面的代码可以发现,将原来代码序列中的指令拆分,并打乱其顺序,然后用jmp指令将它们的执行流程连接起来。这样处理后,这两段代码在执行结果上就是一样的。但是我们很容易发现,阅读和理解第二段代码的难度要比第一段高。即便是在上面这种我们可以一眼看到所有指令的情况下,观察第二段代码时我们的头脑中还是要有一个逻辑跟踪的过程。保护系统中的指令乱序后往往跨度很大,远远超出了我们能够同时观察的视野,所以,在这种情况下,还要考验我们的临时记忆能力。

我们来看某保护系统的实际代码,如图10.25所示,其中所有跳转都超出了调试器的可视范围。

图10.25 代码示例(c)

4.多分支

多分支技术是一种利用不同的条件跳转指令将程序执行流程复杂化的技术。在上一节中,我们体会到代码顺序上的改变会使我们对代码的理解变得复杂,但是代码乱序技术对代码的执行流程是没有改变的,所以要还原乱序的代码并不困难。然而,多分支技术对代码程序的执行流程却是有改变的,我们来看如图10.26所示代码。

图10.26 代码示例(d)

这里有一段由4条指令组成的指令序列,我们可以通过如图10.27所示的方式将其变形。

图10.27 代码示例(e)

观察这段代码我们发现,在0100368A处多了一个条件跳转,而且我们并不关心这个条件跳转在执行的时候到底会不会被触发。也就是说,当指令执行到0100368A处时,执行流程有可能跳转到0100368F处继续执行,也有可能接着执行0100368C处的代码,这样,这段代码的执行流程就出现了不确定性,需要在代码执行时才能够确定代码的执行流程。但可以肯定的是,这段代码的执行结果和第一段代码始终相同,这就是多分支的核心思想。如果在分析时无法确定代码的具体执行流程,就会大大增加分析和理解代码的难度。我们可以发现,在第二段代码中,实际上是在原有代码的基础上增加了一个代码分支。那么,我们就可以在这种情况下做出更大的改变。例如,用不同的代码替换两个分支处的代码,将其修改为如图10.28所示的形式。

图10.28 代码示例(f)

如果不能判断0100368A处跳转代码的两个目的地的代码是否等价,那么我们就不得不同时分析0100368C处的代码和0100368F处的代码,这就增加了代码分析量。因此,这是一种非常有效的干扰代码分析的手段。

5.call链

call链是作者在开发ZProtect时想到的一种专门针对call指令的加密方法,在这里与读者分享。call链的思想在于,在一个正常的PE程序中可以找出非常多的call指令。如图10.29所示,用Olly Dbg的“All Commands..”查找“Call Any”,可以找到许多指令。

图10.29 查找的call指令

我们知道,call指令在调用子程序时会将call指令后面的地址压入栈顶,这样我们就可以同时抽取许多不同的call指令,然后让它们相互调用,最后根据压入栈的返回地址在事先保存的原始call指令的目标地址表中找到call指令的原始目标地址,从而进入这个目标地址。例如,我们可以构建如图10.30所示代码。

图10.30 代码示例

这是一个call链,当所有call指令都被执行后,栈里面的数据如图10.31所示。

图10.31 call指令执行后的栈的数据

根据入栈的顺序和数量,我们可以找出最初被调用的那个call指令,然后转入那个call指令最初的目标地址。当程序代码中有许多call指令经过这样的处理以后,会对静态分析工具(如IDA)造成非常大的干扰。