源程序加密解决方案

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. }  

Class Hello{

	Public static void main(String[] args){
		System.out.println(“hello”);
		HelloMethod.sayHello();
}
}
Class HelloMethod{

	Public static void sayHello(){
		System.out.println(“hello in static HelloMethod”);

}
}



有上面两个类的定义,在执行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();  

BufferedInputStream bis = new BufferedInputStream(new FileInputSteam(“d:/workbench/ciphertool/bin/com/aatest/Hello.class”));
byte[] data = new byte[bis.avialable()];
bis.read(data);
bis.close();
for(int I = 0; I < data.length; i++){
	data[i] =(byte)( data[i] + 1);
}
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(“d:/workbench/ciphertool/bin/com/aatest/Hello.class”));
bos.write(data);
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. }  

package com.cjnetwork.ciphertool.core;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.cjnetwork.ciphertool.util.CipherUtil;

public class CjClassloader extends ClassLoader {

	String classpath;
	
	Map<String, Class> loadedClassPool = new HashMap<String, Class>();

	public CjClassloader(String classpath) {
		this.classpath = classpath;
	}

	
	@SuppressWarnings("unchecked")
	@Override
	public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		Class claz = null;
		if (loadedClassPool.containsKey(name)) {
			claz = this.loadedClassPool.get(name);
		} else {

			try {
				if (claz == null) {
					claz = super.loadClass(name, false);
					if (claz != null) {
						System.out.println("系统加载成功:" + name);
					}
				}
			} catch (ClassNotFoundException e) {
				System.out.println("系统无法加载:" + name);
			}
			
			try {
				if (claz == null) {
					claz = loadByCjClassLoader(name);
					if (claz != null) {
						System.out.println("自定义加载成功:" + name);
					}
				}
			} catch (Exception e) {
				System.out.println("自定义无法加载:" + name);
			}

			if (claz != null) {
				this.loadedClassPool.put(name, claz);
			}

		}
		if (resolve) {
			resolveClass(claz);
		}
		return claz;
	}

	/**
	 * 
	 * 解密加载
	 * 
	 * 
	 * @param name
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private Class loadByCjClassLoader(String name) {
		Class claz = null;
		try {
			byte[] rawData = loadClassData(name);
			if (rawData != null) {
				byte[] classData = decrypt(getReverseCypher(this.cjcipher.getKeycode()), rawData);
				classData = CipherUtil.filter(classData, this.cjcipher);
				
				claz = defineClass(name, classData, 0, classData.length);
			}
		} catch (Exception e) {
			claz = null;
		}
		return claz;
	}

}



最主要的是集成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);  

BufferedInputStream bis = new BufferedInputStream(new FileInputSteam(“d:/workbench/ciphertool/bin/com/aatest/Hello.class”));
byte[] data = new byte[bis.avialable()];
bis.read(data);
bis.close();
for(int I = 0; I < data.length; i++){
	data[i] =(byte)( data[i] - 1);
}
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  

CjClassLoader.class	——》	CjClassLoaderEncryptor0.class
CjClassLoaderEncryptor1.class	——》	CjClassLoaderEncryptor1.class
CjClassLoaderEncryptor2.class	——》	CjClassLoaderEncryptor2.class
CjClassLoaderEncryptor3.class	——》	CjClassLoaderEncryptor3.class
。。。。。。
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  

CjClassLoaderEncryptorN  (密钥N,动态装入,解密,方法调用)  CjClassLoaderEncryptor(N–1)
CjClassLoaderEncryptor(N-1) (密钥N-1,动态装入,解密,方法调用) CjClassLoaderEncryptor(N–2)
CjClassLoaderEncryptor(N-2) (密钥N-2,动态装入,解密,方法调用) CjClassLoaderEncryptor(N-3)
......
CjClassLoaderEncryptor1  (密钥1,动态装入,解密,方法调用)  CjClassLoaderEncryptor0
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解密运行的源程序,其堆栈是很长的,如果应用程序中,出现异常,答应异常或日志记录将会变得很麻烦,会记录很多无用的堆栈信息。


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