8.1.3 破解是如何进行的
破解者是从何处开始着手破解的?刚开始时他们知道些什么?或者对你来说更重要的——他们不知道些什么?我们先从编译型语言(如C语言)开始讨论吧。
对于用这类语言开发的软件,作为一个软件开发者,你是把一个二进制可执行文件和一本用户手册提供给你的客户(或者潜在的破解者)的,这个二进制可执行文件通常都是不带调试符号的(不过我们即将分析的程序却是例外)——它不带符号名,也不含有任何类型信息,更没有标识哪里是函数的起始位置,哪里是函数的结束位置之类的信息。(再强调一遍,从本质上来说)它就是一个黑盒子。
我们再进一步假设:破解者刚开始进行破解时也不掌握任何其他关于程序的信息。特别是他没有关于程序内部是如何设计的文档(开发人员当然是有这些文档的),也没有程序员或者测试人员用来测试软件的各种测试单元。当然随着破解的深入,他可能会根据破解时所获得的信息自己编写一些文档,也可能会编写一些测试单元,但是在一开始,他确实一无所有!
当然在有些情况下,破解者开始破解时,可能就已经掌握一些信息了。比如,他在开发商那里有个线人,能给他提供有关的信息,甚至他自己可能就是开发商的一个雇员,熟知软件的整个研发过程。又比如在软件开发商的官网上有一些文档可供下载,这些文档中的内容也可能给破解者一点提示……但所有这些都不在我们的讨论范围内,我们假设破解者现在还是什么也没有。
1.静态与动态,带调试符号与不带调试符号的对比
绝对的黑盒子是不存在的!至少,每个程序都要告诉操作系统应该从哪里开始执行它。程序能告诉你多少信息取决于它是动态链接的还是静态链接的,以及它是否带调试符号的。如果一个程序是静态链接的,那么它就会把所有它将要使用的库函数都复制到它自身中去。同样,如果一个程序是不带调试符号的,那它就会删掉所有不必要的调试符号(变量名和函数名)。为了帮助大家更好地理解这一点,将动态链接与静态链接的对比用图来表示,如图8.3所示。
静态链接文件会把诸如printf()、scanf()以及time()之类执行时需要使用的库函数全部复制到自己体内,所以比起动态链接文件(执行时从系统库中动态加载库函数的可执行文件)来文件体积就明显大很多。在图8.3中,对于动态链接文件而言,无论是否带调试符号,它都必须要带上库函数,否则在执行时就无法找到相关的库函数。对于我们这个演示程序而言,由于它是带调试符号的,所以它是含有变量名符号的,而对于静态链接又不带调试符号的可执行文件而言,它是什么符号都没有的——库函数已经在编译时被复制到可执行文件中了,不需要在运行时使用库函数的函数名来寻找库函数,因而所有的符号都将被刪除。
图8.3 动态链接与静态链接的对比
黑客从静态链接且不带调试符号的可执行文件中能获取的信息是最少的,但是反过来说,这也使它的文件体积远大于动态链接的且不带调试符号的可执行文件!所以软件的开发者们一般都会以动态链接的且不带调试符号的可执行文件的形式发布软件,这样做的原因不仅在于这样软件的文件体积会小一些,而且它还使程序能够动态链接上用户系统中那些与系统紧密相关的文件,从而确保所有的系统升级都能马上作用于他们的程序。从软件安全的角度去看,这当然不是很理想。因为你马上就会看见,即使我们把play()函数的符号名从可执行文件中去掉,只要程序还是动态链接的,由于整个可执行文件中只有play()函数调用了time()函数,破解者仍然能很快就从time()函数出发找到play()函数。
2.与平台无关的分发形式
Java或C#一类的很多语言都不会把源码编译成某种处理器专用的机器码,而是把源码编译成与平台无关的形式。对于Java来说,毎个X.java文件都会被编译成一个X.class文件。在这个.class文件中包含有一个符号表,其中记录了所有方法及方法中所有字节码指令。由于设计思想使然,.class文件与源码是同构的——字节码中包含了所有类型信息,而且符号表中也包含了所有方法的完整签名。因而,一个反编译器可以轻松地把.class文件近乎完美地还原成Java源码。
Java的.class文件还是动态链接的,所以当一个Java程序调用System.out.println()方法(你可以把它认为是Java中的printf()函数)时,你可以很方便地在字节码中找到它。一般地,Java可执行文件是动态链接且带调试符号的。此外相对于一般的C程序而言,Java程序更为依赖于标准库中提供的类,因而即使没有Java程序的源码或者文档,破解者还是可以很方便地通过分析库方法的被使用情况来确定程序的各个部分各自的作用。