提到Java代码加密,常见方式是使用代码混淆工具,如proguard。混淆是一种逻辑层面的加密,被混淆的代码仍可以反编译,但由于命名与程序流程上的等效替换,使得程序的可读性变的很差,导致代码难以被理解和盗用。但若有方法使代码根本无法被反编译,效果显然优于逻辑上的加密,而一种可以实现的方式就是字节码加密。
Java代码的实际运行与源代码(*.java)关系不大,只依赖于编译后的字节码文件(*.class)。class文件的内容有非常紧凑和严格的约定,使JVM可以识别和执行代码功能;反编译工具也是利用这种约定的结构将字节码反向解析成源码。只要破坏class文件的结构,就能使这个文件完全失效,变得不可能被反编译。
当然,这样加密的class文件也无法被JVM正常加载,不过java的类加载机制是支持自定义,这就给解密留出了空间。可以用自定义的类加载器实现“先解密,后加载”,使JVM能“正常”执行被加密的class文件。以下就来实现上述字节码加密策略。
首先写一个加密器接口,方便实现各种字节码加密算法,两个接口方法就是将一个字节数组进行某种变换与逆变换:
以下是一个最简单的实现:
这个加密器只是将首字节改为一个自定义的特殊字节magicIndex,以下是用字节码查看工具查看某个java类字节码的加密结果:
这么简单的“算法”也能加密?以下是用Eclipse的反编译插件JD查看加密文件的结果:
这印证了字节码文件的内容是具有严格约束的结构,相差一个字节就能破坏整个文件。如使用稍复杂的加密算法,反编译就变的完全不可能了。
接下来写一个jar包加密方法,逻辑很简单,就是将class文件从原包提取出来,加密后写到新包里去
类似的,解密方法从一个Jar包中获取一个(加密后的)class文件,并解密成JVM可识别的字节码。这里作了一点小变化,把多种加密器写在一个Map(CipherConfig.cipherMap)里,用加密文件的首字节作为标识:
如果是从文件系统(而不是Jar包)加载,还要更简单一点,因为不需要通过ZipEntry迭代器来定位文件名。
下一步是怎样调用解密方法,这涉及Java的类加载机制。通常自定义类加载都是通过继承java.lang.ClassLoader类或其子类,重写findClass(name)方法,再利用反射机制调用方法。但此处我们无从了解一个发布了的加密的项目中,各类的加载时机和顺序,也不知道类之间的引用关系(这些是由JVM的执行机制确定的),因此通过通常的方式难以实现让一个加密的Jar包在JVM上运行起来。为解决这个问题,这里采取一种听起来有点“吓人”的方法,那就是修改java系统类源码。下面先给出实现方法。
自定义的类都是通过sun.misc.Lancher$AppClassLoader.loadClass(name)这个方法来加载。该方法的调用时机由JVM底层决定,返回值被直接加载到JVM内存。其源码如下:
最后一行本质上就是调用底层原生方法(后续文章还会涉及)将字节码加载到内存。如果对应类名的字节码文件是加密的,这个方法将无法解析,并抛出ClassNotFoundException异常。修改的方式很简单,当默认加载失败后,尝试用之前自定义的解密方法解密加载:
以上类修改以后,将编译产生的bin/sun/misc/Launcher$AppClassLoader.class文件(注意命名空间必须与系统相同)替换掉jre\lib\rt.jar包中的对应文件,就能正常运行加密后的Jar包了。如将loadFromJar方法写在自定义的其他类中(如工具类),那么这个类也要一同添加到rt.jar。并且命名空间也必须为sun.misc,否则系统将无法加载这个类本身。关于重写系统类的命名空间问题,后续还会讨论。
修改系统类的“代价”是程序发布时需要捆绑一个自定义的JRE,这看起来让人“不太舒服”,但仔细分析的话其实利大于弊。第一,增强了程序的独立性和完整性,不会产生JVM不兼容问题,而在没有安装Java的终端,附带一个JVM则是必不可少的;第二,进一步增强加密强度,因为用通用的JRE无法运行加密代码;第三,修改系统类甚至JVM源码有助于了解Java虚拟机底层原理,而自定义系统类和JVM也使得开发者对代码的控制能力大为。唯一的不足可能是捆绑JRE会增加程序的总体积,不过通过一些不算复杂的优化措施(可以搜索“绿色JRE”或“JRE瘦身”等关键字来了解),大部分程序所必须的JRE都可以压缩到10MB以内,加上应用后续文章将讨论的“轻客户端模式”,这一点空间上的代价完全是可以接收的。
如果有对轻客户端感兴趣的同学,也可以加入我们的群:291694807。本群主要用于讨论轻客户端,使用技术包括qt、flex、java、vc等。
也可以关注我的微博 http://weibo.com/liuxue9527