由于Java字节码具有更高的抽象层次,所以更容易被反编译。本节介绍几种常用的保护Java字节码不被反编译的方法。通常,这些方法并不能绝对防止程序被反编译,只是增加了反编译的难度,因为这些方法都有各自的使用环境和弱点。隔离Java程序最简单的方法就是阻止用户访问Java类程序。这个方法是最基本的方法,实现方法有很多种。例如,开发者可以将关键的Java类放在服务器端,客户端可以通过访问服务器的相关接口来获取服务,而不是直接访问Class文件。这样一来,黑客就没有办法【反编译Class文件了。目前,通过接口提供服务的标准和协议越来越多,如HTTP、WebService、RPC等。但是,有很多应用并不适合这种保护方式,例如Java程序不能对于在单台机器上运行的程序来说是隔离的。这种保护方式如图1所示。加密Class文件为了防止Class文件被直接反编译,很多开发者对一些关键的Class文件进行了加密,比如注册码和序列号管理相关的类。在使用这些加密类之前,程序首先需要对这些类进行解密,然后将这些类加载到JVM中。这些类别的解密可以通过硬件或使用软件来完成。开发者在实现时,往往通过自定义ClassLoader类来完成加密类的加载(注意,出于安全原因,Applet不支持自定义ClassLoader)。自定义ClassLoader首先找到加密的类,然后解密,最后将解密后的类加载到JVM中。在这种保护方式中,自定义的ClassLoader是一个非常关键的类。由于它本身没有加密,因此可能是黑客的第一个目标。如果相关的解密密钥和算法被泄露,加密类也可以很容易地被解密。这种保护模式的示意图如图2所示。Convertingtonativecode将程序转换为nativecode也是一种有效的防止反编译的方法。因为本机代码通常很难反编译。开发人员可以选择将整个应用程序转换为原生代码,也可以选择转换关键模块。如果只转换模块的关键部分,Java程序在使用这些模块时需要使用JNI技术进行调用。当然,在利用这种技术保护Java程序的同时,也牺牲了Java的跨平台特性。针对不同的平台,我们需要维护不同版本的原生代码,这会增加软件支持和维护的工作量。但是,对于一些关键模块,有时往往需要这种解决方案。为了保证这些本地代码不被修改和替换,通常需要对这些代码进行数字签名。在使用这些本地代码之前,往往需要对这些本地代码进行鉴权,以保证这些代码没有被黑客篡改过。如果签名检查通过,则调用相关的JNI方法。这种保护模式的示意图如图3所示。代码混淆代码混淆是对Class文件进行重组和处理,使处理后的代码和预处理后的代码完成相同的功能(语义)。但是混淆后的代码很难[反编译,即[反编译后得到的代码非常难以理解和晦涩,因此[反编译者很难获得程序的真实语义。理论上,如果黑客有足够的时间,混淆后的代码还是可以被破解的,目前甚至有人在开发反混淆工具。但从实际情况来看,由于混淆技术的多元化发展和混淆理论的成熟,混淆后的Java代码仍然可以防止[反编译。下面我们将详细介绍混淆技术,因为混淆是保护Java程序的一项重要技术。图4是代码混淆图。几种技术总结以上几种技术的应用环境不同,各有优缺点。表1是相关特性的比较。混淆技术介绍表1不同保护技术对比表到目前为止,对于Java程序的保护,混淆技术仍然是最基本的保护方法。Java混淆工具也有很多,包括商业的、免费的和开源的。Sun还提供了自己的混淆工具。大多是对Class文件进行混淆,也有少数工具是先处理源码再处理Class,加大了混淆的力度。目前商业上成功的混淆工具有JProof的1stBarrier系列、Eastridge的JShrink和4thpass.com的SourceGuard。主要的混淆技术按照混淆目标可以分为词法混淆、数据混淆、控制混淆和防止转换。符号混淆Class中有很多与程序执行本身无关的信息,比如方法名、变量名等。这些符号的名称往往具有一定的含义。例如,如果调用了一个方法getKeyLength(),那么这个方法很可能用于返回Key的长度。符号混淆就是把信息打乱,把信息变成无意义的表示,比如把所有的变量从vairant_001开始编号;所有方法都从method_001开始。这会给反编译带来一定的困难。对于私有函数和局部变量,通常可以改变它们的符号而不影响程序的运行。但是对于一些接口名称、公共函数、成员变量等,如果其他外部模块需要引用这些符号,我们往往需要保留这些名称,否则外部模块无法找到这些名称的方法和变量。因此,大多数混淆工具都提供了丰富的符号混淆选项,供用户选择是否进行符号混淆以及如何进行符号混淆。数据混淆数据混淆是对程序使用的数据进行混淆。混淆的方法也有很多,主要分为改变数据存储和编码(StoreandEncodeTransform)和改变数据访问(AccessTransform)。更改数据存储和编码可能会破坏程序使用数据存储的方式。例如,将一个有10个成员的数组拆分成10个变量,并打乱这些变量的名字;将二维数组转化为一维数组等。对于一些复杂的数据结构,我们会打乱它的数据结构,比如将一个复杂的类替换为多个类等。另外一种方式就是改变数据访问方式。例如,当访问一个数组的下标时,我们可以进行某些计算。图5是一个示例。在混淆实践中,这两种方法通常结合使用。在打乱数据存储的同时,也打乱了数据访问的方式。对数据进行混淆后,程序的语义变得复杂,增加了反编译的难度。控制混淆控制混淆就是对程序的控制流进行混淆处理,使程序的控制流更加难以进行负面影响。有时程序的性能和混淆程度之间存在权衡。控制混淆的技术是最复杂、花样最多的。这些技术可分为以下几类:添加混淆控制通过添加额外的、复杂的控制流,可以隐藏程序的原始语义。比如对于顺序执行的两条语句A和B,我们可以添加一个控制条件来判断B的执行,这样就增加了反汇编的难度。但是所有的干扰控制都不应该影响B的执行。图6显示了向该示例添加混淆控制的三种方法。控制流重组控制流重组也是一种重要的混淆方法。例如,一个程序调用一个方法,经过混淆后,方法代码可以嵌入到调用程序中。反之,程序中的一段代码也可以变成函数调用。另外,对于一个循环的控制流程,可以拆分多个循环的控制流程,或者将循环转化为递归过程。这种方法最为复杂,研究者也较多。预防性混淆这种混淆通常是为一些特殊的反编译器设计的。一般来说,这些技术都是利用反编译器的弱点或漏洞来设计混淆方案。比如有的反编译器不反编译Return后面的指令,有的混淆方案只是把代码放在Return语句后面。这种混淆的有效性因不同的反编译器而异。一个好的混淆工具通常会结合使用这些混淆技术。案例研究在实践中,保护大型Java程序通常需要这些方法的组合,而不是单一方法。这是因为每种方法都有其弱点和应用环境。这些方法的综合运用,使得对Java程序的保护更加有效。此外,我们还经常需要用到其他相关的安全技术,如安全认证、数字签名、PKI等。本文给出的例子是一个Java应用程序,是SCJP(SunCertificateJavaProgrammer)的模拟考试软件。该应用程序自带大量练习题,所有练习题都被加密并存储在文件中。由于其自带的题库是软件的核心部分,所以题库的接入和获取就成了一个非常核心的类。一旦这些相关类被反编译,所有的题库都会被破解。现在,让我们考虑如何保护这些题库和相关类。在这个例子中,我们考虑了包括本机代码和混淆技术在内的综合保护技术。由于软件主要分布在Windows上,转换为原生代码后,只需要维护一个版本的原生代码。另外,混淆对Java程序也很有效,适用于这种独立发布的应用系统。在具体方案中,我们将程序分为两部分,一部分是本地代码编写的题库访问的模块,另一部分是Java开发的其他模块。这样,可以更高程度的保护话题管理模块不被反编译。对于Java开发的模块,我们还是要用到混淆技术。有关协议的示意图,请参见图7。图7SCJP保护技术方案图对于题目管理模块,由于程序主要在Windows下使用,因此采用C++开发题库接入模块,并提供一定的接入接口。为了保护访问题库的接口,我们还增加了一个初始化接口,用于每次使用访问题库的接口前进行初始化。它的接口主要分为两类:初始化接口在使用题库模块之前,我们必须先调用初始化接口。调用该接口时,客户端需要提供一个随机数作为参数。题库管理模块和客户端使用这个随机数按照一定的算法同时生成相同的SessionKey,加密后用于对所有输入输出的数据进行加密。这样,只有授权(有效)的客户端才能连接到正确的连接并生成正确的SessionKey以访问题库信息。不法客户很难生成正确的SessionKey,因此无法获取题库信息。如果需要建立更高级别的保密性,也可以采用双向认证技术。数据访问接口认证完成后,客户端就可以正常访问题库数据了。但是,输入和输出数据由SessionKey加密。因此,只有正确的题库管理模块才能使用题库管理模块。图8是题库管理模块与其他部分交互过程的时序图。来源|www.cnblogs.com/dartagnan/
