轉:http://cjnetwork.iteye.com/blog/851544
源程序加密解決方案
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,例如:
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類為例子,
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)生成類,返回出去。
攔截新類加載請求
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
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裝入應用程序,運行。整體上的思路如下:
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。。。。。。。。
- 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解密運行的源程序,其堆棧是很長的,如果應用程序中,出現異常,答應異常或日志記錄將會變得很麻煩,會記錄很多無用的堆棧信息。
備注:文中提到的應用程序,指需要被加密的程序。