8.1.4 破解方法

8.1.4 破解方法

整个破解的过程可以分为若干个阶段。在最初的几个阶段中,破解者的任务主要是分析程序,试图弄明白程序的内部结构及其行为模式。而在后面几个阶段,破解者则会使用前几个阶段中获得的信息,按他的需要对程序进行修改。我们可以把整个破解过程分为下列几个阶段。

①黑盒阶段;

②动态分析阶段;

③静态分析阶段;

④编辑阶段;

⑤自动化阶段。

在黑盒分析阶段,破解者会充当“占卜者”,给程序输入一些数据,并且观察程序的输出,然后根据程序的行为作出某些推论。在动态分析阶段,破解者开始分析程序的内部行为,虽然这时他仍会执行程序,但这时他会记录在输入不同数据之后,程序都会执行哪些不同的部分。接着,为了更好地理解程序,他又会进行静态分析,这时就开始了直接分析代码阶段。

如果破解者的目的只是破坏程序中的机密性,那就可以到此结束了。因为这时他可能已经找到程序中的密钥,或者搞清楚了程序中他所关心的算法。总之,任务已经胜利完成了。但如果他要破坏的是程序的完整性,那破解者还需要执行编辑阶段的任务,即根据之前分析得到的信息对程序进行修改:他可能会把检查软件使用许可的代码刪掉,也可能抹掉程序中用来标识购买副本的用户的指纹。

在实际破解时,上述的前4个阶段是互相交织在一起,并由破解者手工完成的。破解者可能需要不断地进行各种尝试,多次运行程序,并不断地重复这一过程。每重复一次,破解者对程序的理解就更进一步,直到破解者自认为达到目标为止。

在最后这个阶段里,破解者将编写一个自动化的脚本把这次破解所获得的经验固化下来。这样在下次再遇到类似的问题时,他就可以直接使用这个脚本而不必再亲自动手分析一遍了。

1.动态分析——破解和调试的对比

破解和调试非常相像。事实上,就破解者的角度来看,软件使用许可代码就是程序中的一个需要去掉的漏洞!

尽管如此,调试和破解之间还是存在一些区别的。当调试程序时,你是按着“编辑源码—编译—测试”这样一个循环进行工作的:首先编辑源码,然后把它编译成二进制代码,最后测试编译生成的代码中是否还有漏洞。你将不断地重复这一过程,直到程序达到你的要求为止,程序调试过程如图8.4所示。

图8.4 程序调试过程

然而对于破解者而言,他是遵循“定位保护代码—修改—测试”这个循环来进行破解的,定位保护模式如图8.5所示。

图8.5 定位保护模式

在“定位保护代码”这个阶段,破解者试图找到那些通过修改就能解除软件保护(比如,使检查软件使用许可的代码永远不会被执行到)的代码在程序中的位置,并在接下来的“修改二进制代码”阶段对代码进行一系列的修改,使程序能按照他的意愿运行,对二进制代码进行的修改包括:添加新的指令,去掉旧有的指令,以及对原有指令进行修改。最后,破解者还要在“测试”阶段检查他的修改是否已经奏效,以及相关的修改是否影响到了程序的正常功能。

之前我们已经提到过,你手中有的是各种各样的测试单元能对程序进行测试,但在破解者的手中可没有这些。对于破解者来说,这是一个很厉害的限制条件——这将迫使他不得不小心翼翼地对代码进行修改,尽量使有关修改不破坏程序的正常功能。如果修改仅限于一个很小范围的话(比如,只把检查激活码是否正确的那个条件转移指令改成一个无条件转移指令),那问题还不大。但如果对程序的修改是在一个较大范围内进行的,由于破解者缺乏对程序内部实现的深入理解,又没有各种测试单元对程序的功能进行测试,因此要保证修改绝对不会影响到程序的正常功能还是有一定难度的。

软件保护技术也是从破解循环入手,设法增加破解者在各个阶段的阻力。比如,你可以增加程序中各个部分的相互依赖程度,使破解者不能只改动一两个跳转就完成破解。你也可以通过增加程序运行时的不确定性,即让程序在每次运行时都动态地选择不同的执行路径,使破解者不能轻易地就找到保护代码的所在。最后,你还可以在程序中加入额外的代码,使得破解者在测试阶段不得不编写更多的测试单元对程序进行测试。

2.动态分析——利用黑盒分析的结果

我们知道,即使是一个静态链接且不带调试符号的二进制可执行文件也会泄露一些信息给破解者——至少程序要有一个入口点(entry point)吧!这样从理论上讲,破解者就可以通过单步(single-step)跟踪对程序进行分析。

一种常用的攻击方法是通过分析程序对系统调用/库函数的使用情况来推断程序内部结构。比如在一个去除程序中软件使用许可代码的破解案例中,破解者可能首先就要找弹出“请输入激活码”这个对话框的库函数。在Windows系统中,破解者马上就会在Get DlgItemlnt()函数(这个函数的作用是把你输入对话框的激活码转换成一个int型整数)上设置一个断点。当这个断点被触发时,他就会去检查程序的函数调用栈,找出是哪段代码调用了GetDlgItemlnt()这个函数。而检查激活码是否正确的代码位置离这段代码应该就不远了。

若程序是静态链接并且不带调试符号的,那么破解者就不能通过库函数的名字来找库函数了。但是程序总要使用系统调用吧!破解者还是可以通过系统调用来找到库函数。比如,printf()函数需要在某个地方调用write()这个系统调用。那么破解者就可以在write()函数上设置一个断点,等这个断点被命中时,他就又可以去检查函数调用栈,找出printf()函数的所在位置了。

3.静态分析

静态分析分为多个档次,有些髙手能像读源码一样读机器码(一般这里所说的机器码就是指x86处理器的汇编代码)。因此他们只要有一个反汇编器就能开始逆向分析。

若有好的工具,静态分析的效率可能会更高一些。尽管现在的反编译器性能还不尽如人意——它只能处理Java之类在设计时就支持反编译的语言。而对于那些不支持反编译的语言生成的代码,反编译器即使是在处理带调试符号的可执行文件时,也只能比较好地反编译特定编译器产生的二进制代码。尽管如此,反编译器仍不失为一个有用工具,比如Van Emmerik和Waddington就描述过一个方法:借助于反汇编器和手工修改代码,他们把一个没有源码的程序中某个函数的一部分挖了出来,放到Boomerang编译器中,并把它成功地反编译了出来。尽管在整个反编译过程中要手工对代码进行不少修改,但是Emmerik和Waddington还是认为“用Boomerang编译器比手工反编译要安全得多。因为完全手工进行反编译的话,出错的概率实在是太大了!”

有时即使只使用简单的模式匹配算法,在二进制代码中搜索一些信息也很管用!比如在破解Skype的VoIP客户端软件时,破解者就是通过在二进制代码中搜索符合使用hash技术进行防篡改检查代码的特征这一招找到软件中的保护代码的。同样,我们也可以使用模式匹配的方法从二进制代码中找出printf()之类的库函数。

4.编译

发现源码中有漏洞时,你只要再回到“编辑源码—编译—测试”这个开发循环中的“编辑源码”阶段就可以了,因为你手里是有源码的!但对于破解者来说,在“定位—修改—测试”这个循环中,他手里只有二进制代码,修改代码也不是这么容易就能完成的。如果原来的指令比新指令要大或者一样大,修改代码还比较容易,因此把“branch-if-less-than”结构的代码改成“branch-never”结构(就像去掉软件使用许可检查代码那样)还不会引发什么额外的问题(如果原有的代码比新的指令更大的话,我们可以直接用NOP指令填充多余的空间)。但是如果原有指令比新的指令要小的话,麻烦就来了,特别是在静态链接的可执行文件中,所有转移指令的目标地址都是写死在程序里的,如果你的修改使得部分代码的位置必须进行调整,那改动量可就大了,如果你的反汇编器能自动对这些改动进行纠正,这个问题还不算太大。可是即使是最好的反汇编器偶尔也会出错。

5.自动化

破解的最后一步(某些破解者甚至都不会进行这一步)是自动化。根据破解软件所获得的经验,破解者会编写一个工具(大多数情况下是写一个脚本)。使用这个工具(通常都不需要运行被破解的程序),可以自动把程序中所使用的软件保护代码去除掉。根据破解者的水平以及软件中保护措施的强度,这类脚本的通用性也有好有坏,有的只能破解某个特定的软件,而有的则可以对付所有使用某类保护技术的程序。

这里有必要说一下“真正的”破解者和“脚本小子”的区別:“真正的”破解者是指那些自己动手分析软件,破解并最终编写脚本的人,而“脚本小子”只是那些使用脚本的人,一般他们甚至都不会编程,最多只是从网上下载一些脚本,用这些脚本破解他们所喜欢的游戏罢了。