源程序加密解决方案 

1. 概述: 
Java源程序的加密,有如下两种: 
1使用混淆器对源码进行混淆,降低反编译工具的作用 
2基于classloader的自定义加密、解密运行 

1.1. 混淆器加密 
1.2. 自定义classloader加密 
1.2.1. 原理 
原理:java虚拟机的动态加载机制,为classloader加密方案提供了理论基础。在jvm装载运行程序,初始的时候,只装在了必要的类,如java.lang.String等,而应用程序的类并没有一次性装入内存。Jvm解释执行应用程序的过程中,如果发现有未装载的类,则会调用装载正在执行的那个类的classloader来装载,这个过程是一层一层向上,直到顶层的classloader。Jvm启动的时候会装入ExtClassloader,而ExtClassloader又会装载AppClassloader,例如: 
Java代码   收藏代码
  1. Class Hello{  
  2.   
  3.     Public static void main(String[] args){  
  4.         System.out.println(“hello”);  
  5.         HelloMethod.sayHello();  
  6. }  
  7. }  
  8. Class HelloMethod{  
  9.   
  10.     Public static void sayHello(){  
  11.         System.out.println(“hello in static HelloMethod”);  
  12.   
  13. }  
  14. }  


有上面两个类的定义,在执行Hello类的main方法的时候,首先会委托装载Hello类的classloader来装载HelloMethod类,即jvm会委托AppClassloader来装载,但是在AppClassloader的实现的时候,会首先委托装载AppClassloader的classloader来装载,如果上层的classloader无法装载,才会由AppClassloader来装载HelloMethod类。这种模式叫做双亲委托模式。在jvm的所有classloader中都是如此,首先由父classloader加载,失败由自身加载。 

Java虚拟机的这种特性,使得我们可以自定义一个classloader,然后由这个classloader来装载应用程序的启动类,然后在启动应用程序,那么当应用程序中有未装载的类的时候,java徐机器逐层向上请求classloader装载新类,那么首先被请求的就是装在应用程序的classloader,即我们自定义的classloader,我们完全可以首先调用自己的加载方法来加载类,如果加载不成功,可以请求父classloader来加载,因为来请求加载的类是完全有可能是系统的类。 







在我们使用自定义的classloader的时候,装载自己的程序,那么就可以对装入的字节码进行一定的操作,比如解密。在调用自定义的装载器classloader的时候,首先是要装入被加密之后的文件,通常情况下仍旧已.class为扩展名,在调用defineClass之前对装入的数据解密。 
1.2.2. Classloader的两个重要方法 
protected Class defineClass(String name, byte[] classData, int offset, int length); 
最原子的操作,在调用自定义的classloader加载新类的时候,首先根据自定义规则找到加载的类所存放的位置,然后将数据一byte[]类型读入,进行解密运算时候,调用该方法,以生成一个Class。这是一个比较核心的方法,这个方法是被抽象的Classloader定义为protected访问标记的,只有继承了Classloader这个类才能使用。 

Class loadClass(String name, boolean resolve); 
Java虚拟机,在装载新类,递归向上查找并调用的方法,在自定义classloader中需要重写,就是判断是否能够自己装载,如果能则自己装载,否则交由系统装载。 
2. 源程序加密解决方案 

2.1. 自定义classloader加密 
加密和解密要是对应的,即使用加密之后的数据,经过解密是需要能够得到原来的数据。 

2.1.1. 加密应用程序 
为了简单,在这里才用一种简单的加密方法,把得到的需要加密的数据,以字节取,每一个字节加1,对应的解密就是每一个减1。 
还是以Hello、HelloMethod类为例子, 
Java代码   收藏代码
  1. BufferedInputStream bis = new BufferedInputStream(new FileInputSteam(“d:/workbench/ciphertool/bin/com/aatest/Hello.class”));  
  2. byte[] data = new byte[bis.avialable()];  
  3. bis.read(data);  
  4. bis.close();  
  5. for(int I = 0; I < data.length; i++){  
  6.     data[i] =(byte)( data[i] + 1);  
  7. }  
  8. BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(“d:/workbench/ciphertool/bin/com/aatest/Hello.class”));  
  9. bos.write(data);  
  10. bos.close();  

将加密对象取出,加密,然后存盘。 
2.1.2. 解密运行应用程序 
在自定义的classloader接收到加载新类请求的时候,首先读入加密之后的文件,然后解密,最后调用defineClass(name, classData, offset, length)生成类,返回出去。 


拦截新类加载请求 

Java代码   收藏代码
  1. package com.cjnetwork.ciphertool.core;  
  2.   
  3. import java.io.BufferedInputStream;  
  4. import java.io.File;  
  5. import java.io.FileInputStream;  
  6. import java.lang.reflect.Method;  
  7. import java.util.ArrayList;  
  8. import java.util.HashMap;  
  9. import java.util.List;  
  10. import java.util.Map;  
  11. import java.util.jar.JarEntry;  
  12. import java.util.jar.JarFile;  
  13.   
  14. import com.cjnetwork.ciphertool.util.CipherUtil;  
  15.   
  16. public class CjClassloader extends ClassLoader {  
  17.   
  18.     String classpath;  
  19.       
  20.     Map<String, Class> loadedClassPool = new HashMap<String, Class>();  
  21.   
  22.     public CjClassloader(String classpath) {  
  23.         this.classpath = classpath;  
  24.     }  
  25.   
  26.       
  27.     @SuppressWarnings("unchecked")  
  28.     @Override  
  29.     public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {  
  30.         Class claz = null;  
  31.         if (loadedClassPool.containsKey(name)) {  
  32.             claz = this.loadedClassPool.get(name);  
  33.         } else {  
  34.   
  35.             try {  
  36.                 if (claz == null) {  
  37.                     claz = super.loadClass(name, false);  
  38.                     if (claz != null) {  
  39.                         System.out.println("系统加载成功:" + name);  
  40.                     }  
  41.                 }  
  42.             } catch (ClassNotFoundException e) {  
  43.                 System.out.println("系统无法加载:" + name);  
  44.             }  
  45.               
  46.             try {  
  47.                 if (claz == null) {  
  48.                     claz = loadByCjClassLoader(name);  
  49.                     if (claz != null) {  
  50.                         System.out.println("自定义加载成功:" + name);  
  51.                     }  
  52.                 }  
  53.             } catch (Exception e) {  
  54.                 System.out.println("自定义无法加载:" + name);  
  55.             }  
  56.   
  57.             if (claz != null) {  
  58.                 this.loadedClassPool.put(name, claz);  
  59.             }  
  60.   
  61.         }  
  62.         if (resolve) {  
  63.             resolveClass(claz);  
  64.         }  
  65.         return claz;  
  66.     }  
  67.   
  68.     /** 
  69.      *  
  70.      * 解密加载 
  71.      *  
  72.      *  
  73.      * @param name 
  74.      * @return 
  75.      */  
  76.     @SuppressWarnings("unchecked")  
  77.     private Class loadByCjClassLoader(String name) {  
  78.         Class claz = null;  
  79.         try {  
  80.             byte[] rawData = loadClassData(name);  
  81.             if (rawData != null) {  
  82.                 byte[] classData = decrypt(getReverseCypher(this.cjcipher.getKeycode()), rawData);  
  83.                 classData = CipherUtil.filter(classData, this.cjcipher);  
  84.                   
  85.                 claz = defineClass(name, classData, 0, classData.length);  
  86.             }  
  87.         } catch (Exception e) {  
  88.             claz = null;  
  89.         }  
  90.         return claz;  
  91.     }  
  92.   
  93. }  


最主要的是集成Classloader,并重写Class loadClass(String name, Boolean resolve)方法,在这个方法中,可以根据需要自己加载需要的文件,并解析生成Class。 


解密并返回Class 
Java代码   收藏代码
  1. BufferedInputStream bis = new BufferedInputStream(new FileInputSteam(“d:/workbench/ciphertool/bin/com/aatest/Hello.class”));  
  2. byte[] data = new byte[bis.avialable()];  
  3. bis.read(data);  
  4. bis.close();  
  5. for(int I = 0; I < data.length; i++){  
  6.     data[i] =(byte)( data[i] - 1);  
  7. }  
  8. Class claz = defineClass(“Hello”, data, 0, data.length);  

2.2. 加密自定义classloader 
采用以上的方法,就可以将应用程序加密,使得被加密的程序不能被反编译,因为加密之后的class文件已经不是jvm定义的标准class文件,只能通过解密运行程序解密,才能运行。 

如果只做到这一步,对于java源程序加密还没有完成。虽然应用程序无法直接反编译,但是自定义的classloader是没有被加密的,它自身是可以被反编译的。理论上,如果得到真正的class文件(即jvm标准的class文件),是可以反编译的java文件,在这里,假设得到class文件就得到了java文件。 
如果攻击者将自定义的classloader反编译,得到源码,则攻击者可以再自定义解密运行的同事,将得到的应用程序的字节码存储到本地,那么,攻击者就相当于是跳过了源程序加密解密。例如攻击者在代码Class claz = defineClass(name, classData, offset, length);这句代码前,将classData存储到本地,即攻击者可以再解密运行应用程序的同时,将应用程序的字节码保存,就达到了破解应用程序源代码的效果。 

为了描述方便,实例化一个 自定义的classloader,叫做CjClassLoader 
这一个漏洞在于,CjClassLoader没有加密,攻击者可以在其中嵌入导出应用程序代码,那么,要解决这个问题,加密CjClassLoader就成了保护应用程序源代码的关键。 
试想,如果加密、解密运行程序中,没有CjClassLoader.class文件,或是CjClassLoader.class文件本身也是经过加密的,CjClassLoader类的获得也是通过自己书写的方法动态获取,那么攻击者无法获取到CjClassLoader.class文件,相当于无法获取到CjClassLoader.java文件,那么也就无法再其中加入到处应用程序类文件的代码,那么被加密的应用程序可以认为是安全的。 
假设将CjClassLoader.class加密后生成CjClassLoaderEncryptor0.class,那么CjClassLoader是安全了,但理论上攻击者还是可以通过反编译CjClassLoaderEncryptor0来获取CjClassLoader的源码,那么保护CjClassLoaderEncryptor0又成了保护应用程序的关键,注意在CjClassLoaderEncryptor0中存在解密CjClassLoader的密钥,即将密钥硬编码到CjClassLoaderEncryptor0中,这样做是为了防止攻击者直接获取密钥,直接破解最里面一层的加密,至于什么是最里面一层,请继续看后文。 
那么如何CjClassLoaderEncryptor0.class的安全性呢,我们同样采取加密的方式,即将CjClassLoaderEncryptor0.class加密,生成CjClassLoaderEncryptor1.class,在解密运行的时候首先动态的生成CjClassLoaderEncryptor1.class,在由CjClassLoaderEncryptor1所定义的类动态的装入CjClassLoaderEncryptor0.class,并且解密生成CjClassLoader,最后使用CjClassLoader装入应用程序,运行。整体上的思路如下: 
Java代码   收藏代码
  1. CjClassLoader.class ——》 CjClassLoaderEncryptor0.class  
  2. CjClassLoaderEncryptor1.class   ——》 CjClassLoaderEncryptor1.class  
  3. CjClassLoaderEncryptor2.class   ——》 CjClassLoaderEncryptor2.class  
  4. CjClassLoaderEncryptor3.class   ——》 CjClassLoaderEncryptor3.class  
  5. 。。。。。。  
  6. CjClassLoaderEncryptorN.class   ——》 CjClassLoaderEncryptorN.class  

这样的一级一级加密,我们称CjClassLoaderEncryptorN.class为最外层,成CjClassLoaderEncryptor0.class为最里层。除去最外层没有加密,里面的每一层都是加密之后的数据,都是不能直接为jvm所识别的字节码,都是需要通过后一级的解密程序解密之后才能为jvm所识别。 

系统装在CjClassLoaderEncryptorN.class,生成CjClassLoaderEncryptorN类,使用反射机制,调用CjClassLoaderEncryptorN类中的方法,这个方法可以动态的装入CjClassLoaderEncryptor(N-1).class,并利用CjClassLoaderEncryptorN中的密钥,解密CjClassLoaderEncryptor(N – 1),然后生成CjClassLoaderEncryptor( N – 1)类,最后调用CjClassLoaderEncryptor(N – 1)中的方法。而CjClassLoaderEncryptor( N – 1)类中的方法,可以动态装入CjClassLoaderEncryptor(N- 2).class文件,并利用CjClassLoaderEncryptor(N – 1)中的密钥,解密CjClassLoaderEncryptor(N – 2),然后生成CjClassLoaderEncryptor(N – 2)类,最后调用方法,被调用的方法可以动态的装入CjClassLoaderEncryptor(N – 3).class。。。。。。。。 
Java代码   收藏代码
  1. CjClassLoaderEncryptorN  (密钥N,动态装入,解密,方法调用)  CjClassLoaderEncryptor(N–1)  
  2. CjClassLoaderEncryptor(N-1) (密钥N-1,动态装入,解密,方法调用) CjClassLoaderEncryptor(N–2)  
  3. CjClassLoaderEncryptor(N-2) (密钥N-2,动态装入,解密,方法调用) CjClassLoaderEncryptor(N-3)  
  4. ......  
  5. CjClassLoaderEncryptor1  (密钥1,动态装入,解密,方法调用)  CjClassLoaderEncryptor0  
  6. CjClassLoaderEncryptor0  (密钥0,动态装入,解密,方法调用)  CjClassLoader  

最后使用CjClassLoader解密装载应用程序。 


通过这样一个过程的加密CjClassLoader,可以达到保护加密程序本身的目的,这种保护在理论上是可破,但在实际操作中将会变得困难,因为密钥是通过硬编码的方式存储在下一层的封装器中,即CjClassLoaderEncryptor(N-1).class的密钥是放在CjClassLoaderEncryptorN.class中,如果存在CjClassLoaderEncryptor1000.class,那么加密过程将会变得非常复杂。 

当然动态生成CjClassLoaderEncryptorN.class的工作,虽然内置了应编码(解密CjClassLoaderEncryptor(N-1)的密钥),但是这样一个过程,是不需要手动实现,利用程序自动生成即可。目前,这个版本的实现中是采用了动态生成CjClassLoaderEncryptorN.java文件,然后调用javac 命令,编译生成class文件。 

请记住,这个过程是理论上不安全的,但如果需要加密的应用程序非常的重要,那么可以将加密、解密运行自身的CjClassLoader加密次数增加,以达到更加安全的目的。 
2.3. 隐藏自定义classloader 
通过上述加密CjClassLoader的方案,可以使得CjClassLoader变得相对安全,但似乎还是有一个问题,即解密运行程序本身的main方法中,会动态的装入CjClassLoaderEncryptorN,然后通过层层调用,最终获取到CjClassLoader类,然后使用CjClassLoader解密装载应用程序,这段代码是没有加密的,攻击者可以不考虑CjClassLoaderEncryptorN开始的层层调用,只需要在最终获取的CjClassLoader解密应用程序之前,将CjClassLoader本地化,即可以获得未经加密的CjClassLoader,这样,就不安全了。 
解决这个问题,可以将这段代码中动态获取CjClassLoader类,修改为动态获取CjClassLoader中的Class loadClass(String name, Boolean resovle)方法,然后直接使用获取到的方法,开始加载应用程序。 


如此,攻击者就没有办法直接获取到解密之后的CjClassLoader,保护了加密、解密程序。 

2.4. 隐藏加密、解密方法 
在上述的实现中,CjClassLoader中加密、解密应用程序的方法是被放置于CipherUtil.class文件中,而这个文件是没有被加密的,攻击者是可以直接获取到应用程序加密和解密的方法的,这给应用程序带来了不安全性,是的攻击者不利用解密程序的繁琐解密过程,而自定调用CipherUtil.class中的方法,解密应用程序。 

解决这个问题,可以将CipherUtil.class中的加密和解密方法封装到CjClassloader中,因为CjClassloader是没有办法直接得到,所以认为加密解密所用到的方法是安全的。最终在程序中调用的时候不是直接得到CjClassloader类,都是通过CjClassloaderEncrytorN的层层方法调用,而直接获取到需要使用的方法。例如,我们可以在CjClassloaderEncrytorN类中封装了一个Method getEncrytMethod(),如此的方法,这个方法会去调用CjClassloaderEncrytor(N-1)中的同名方法,如此一直调用,直到CjClassloaderEncrytor0.class中,在这个类中直接反射获得CjClassloader中的加密方法,当然这个是比较特殊的,因为在CjClassloaderEncrytor0中时候反射获取CjClassloader中方法的时候,这个反射是需要带参数的,但这个带参数获取也是简单的。 


3. bug 

异常堆栈过长 
经过这种一层一层的CjClassLoader解密运行的源程序,其堆栈是很长的,如果应用程序中,出现异常,答应异常或日志记录将会变得很麻烦,会记录很多无用的堆栈信息。 


备注:文中提到的应用程序,指需要被加密的程序。 


转载于:http://cjnetwork.iteye.com/blog/851544?page=2#comments

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐