我用java寫了一個程序如下:
| 12345678910111213141516171819202122232425 | package com.monkey.demo;// App.javapublic class App{  static public void main( String args[] ) throws Exception {    System.out.PRintln( "This is your application." );    System.out.print( "Args: " );    for (int a=0; a<args.length; ++a)      System.out.print( args[a]+" " );    System.out.println( "" );        new App().new AppChild().print();        new Foo();    new Bar();  }    public class AppChild{	  public void print(){		  System.out.println("haha ....");	  }  }} | 
然后編譯生成.class文件后,我發布出去了,別人拿到我的.class文件拖到JD-GUI里面看到的是這樣:

玩毛線,看源代碼一樣,當然你也可以使用ProGuard混淆,不過別人有點耐心還是能分析出來的。另外你還可以修改class文件里面的某些字段,這些字段對運行沒有影響,但是能導致別人無法反編譯。這里我們暫且不討論這種方式,分別討論下使用ClassLoader和jvmti對class文件加密解密的方案。
Java運行時裝入字節碼的機制隱含地意味著可以對字節碼進行修改。JVM每次裝入類文件時都需要一個稱為ClassLoader的對象,這個對象負責把新的類裝入正在運行的JVM。JVM給ClassLoader一個包含了待裝入類(比如java.lang.Object)名字的字符串,然后由ClassLoader負責找到類文件,裝入原始數據,并把它轉換成一個Class對象。
所以我們可以通過自定義一個ClassLoader,然后先對class進行解密之后,再加載到JVM。大致流程如下:
| 1234567 | // 首先創建一個ClassLoader對象ClassLoader myClassLoader = new myClassLoader();// 利用定制ClassLoader對象裝入類文件// 并把它轉換成Class對象Class myClass = myClassLoader.loadClass( "mypackage.MyClass" );// 最后,創建該類的一個實例Object newInstance = myClass.newInstance(); | 
在創建自定義的ClassLoader時,只需覆蓋其中的一個,即loadClass,獲取加密后的文件數據解密加載。
| 1234567891011121314151617181920212223242526272829 | public Class loadClass( String name, boolean resolve )      throws ClassNotFoundException {	try {	  // 我們要創建的Class對象	   Class clasz = null;	  // 如果類已經在系統緩沖之中,不必再次裝入它	  clasz = findLoadedClass( name );	  if (clasz != null)	    return clasz;	  // 下面是定制部分	  byte classData[] = /* 解密加密后的字節數據 */;	  if (classData != null) {	    // 成功讀取字節碼數據,現在把它轉換成一個Class對象	    clasz = defineClass( name, classData, 0, classData.length );	  }	  // 如果上面沒有成功,嘗試用默認的ClassLoader裝入它	  if (clasz == null)	    clasz = findSystemClass( name );	  //如有必要,則裝入相關的類	  if (resolve && clasz != null)	    resolveClass( clasz );	  // 把類返回給調用者	  return clasz;	} catch( IOException ie ) {	  throw new ClassNotFoundException( ie.toString() );	} catch( GeneralSecurityException gse ) {	  throw new ClassNotFoundException( gse.toString() );	}} | 
上面是一個簡單的loadClass實現,其中涉及到如下幾個方法:
findLoadedClass: 檢查當前要加載的類是否已經加載。defineClass: 獲得原始類文件字節碼數據后,調用defineClass轉換成一個Class對象。findSystemClass: 提供默認ClassLoader支持。resolveClass: 當JVM想要裝入的不僅包括指定的類,而且還包括該類引用的所有其他類時,它會把loadClass的resolve參數設置成true。這時,必須在返回剛剛裝入的Class對象給調用者之前調用resolveClass。直接使用java自帶加密算法,比如DES。
| 123456789 | //DES算法要求有一個可信任的隨機數源SecureRandom sr = new SecureRandom();//為選擇的DES算法生成一個KeyGenerator對象KeyGenerator kg = KeyGenerator.getInstance( "DES" );kg.init( sr );// 生成密匙SecretKey key = kg.generateKey();// 獲取密匙數據byte rawKeyData[] = key.getEncoded(); | 
| 123456789101112131415161718 | // DES算法要求有一個可信任的隨機數源SecureRandom sr = new SecureRandom();byte rawKeyData[] = /* 用某種方法獲得密匙數據 */;// 從原始密匙數據創建DESKeySpec對象DESKeySpec dks = new DESKeySpec( rawKeyData );// 創建一個密匙工廠,然后用它把DESKeySpec轉換成一個SecretKey對象SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );SecretKey key = keyFactory.generateSecret( dks );// Cipher對象實際完成加密操作Cipher cipher = Cipher.getInstance( "DES" );// 用密匙初始化Cipher對象cipher.init( Cipher.ENCRYPT_MODE, key, sr );// 現在,獲取數據并加密byte data[] = /* 用某種方法獲取數據 */// 正式執行加密操作byte encryptedData[] = cipher.doFinal( data );// 進一步處理加密后的數據doSomething( encryptedData ); | 
| 12345678910111213141516171819 | // DES算法要求有一個可信任的隨機數源SecureRandom sr = new SecureRandom();byte rawKeyData[] = /* 用某種方法獲取原始密匙數據 */;// 從原始密匙數據創建一個DESKeySpec對象DESKeySpec dks = new DESKeySpec( rawKeyData );// 創建一個密匙工廠,然后用它把DESKeySpec對象轉換成// 一個SecretKey對象SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );SecretKey key = keyFactory.generateSecret( dks );// Cipher對象實際完成解密操作Cipher cipher = Cipher.getInstance( "DES" );// 用密匙初始化Cipher對象cipher.init( Cipher.DECRYPT_MODE, key, sr );// 現在,獲取數據并解密byte encryptedData[] = /* 獲得經過加密的數據 */// 正式執行解密操作byte decryptedData[] = cipher.doFinal( encryptedData );// 進一步處理解密后的數據doSomething( decryptedData ); | 
這里寫了一個簡單的例子,代碼在github。
首先生成密鑰:
| 123 | javac FileUtil.java javac GenerateKey.javajava GenerateKey key.data | 
然后加密class:
| 12 | javac EncryptClasses.javajava EncryptClasses key.data App.class Foo.class Bar.class | 
運行加密后的應用:
| 12 | javac MyClassLoader.java -Xlint:unchecked java MyClassLoader key.data App | 
總的來說ClassLoader在類非常多的情況還是比較麻煩,而且這樣一來自定義的ClassLoader本身就成為了突破口。下面介紹另外一種加密保護的方案。
jvmti(JVMTM Tool Interface)是JDK提供的一套用于開發JVM監控,問題定位與性能調優工具的通用變成接口。通過JVMTI,我們可以開發各式各樣的JVMTI Agent。這個Agent的表現形式是一個以c/c++語言編寫的動態共享庫。
JVMTI Agent原理: java啟動或運行時,動態加載一個外部基于JVM TI編寫的dynamic module到Java進程內,然后觸發JVM源生線程Attach Listener來執行這個dynamic module的回調函數。在函數體內,你可以獲取各種各樣的VM級信息,注冊感興趣的VM事件,甚至控制VM的行為。
這里我們只需要監控class的加載信息,而jvmti也提供了這樣的接口,通過下面的方式我們就能監控到class的加載:
| 123456789101112131415161718192021222324252627282930313233 | JNIEXPORT jint JNICALLAgent_OnLoad(    JavaVM *vm,    char *options,    void *reserved){	......	//設置事件回調    jvmtiEventCallbacks callbacks;    (void)memset(&callbacks,0, sizeof(callbacks));     callbacks.ClassFileLoadHook = &MyClassFileLoadHook;    error = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));    ......}void JNICALLMyClassFileLoadHook(    jvmtiEnv *jvmti_env,    JNIEnv* jni_env,    jclass class_being_redefined,    jobject loader,    const char* name,   //class名字    jobject protection_domain,    jint class_data_len,  //class文件數據長度    const unsigned char* class_data,   //class文件數據    jint* new_class_data_len,   //新的class文件數據長度    unsigned char** new_class_data   //新的class文件數據){	......} | 
通過這樣的方式就能監控到class的加載然后再對其進行解密。
這里簡單的通過遍歷class文件,然后對每個字節進行一個異或處理,具體的加密方法可以自己擴展:
| 12345678910111213141516171819 | extern"C" JNIEXPORT jbyteArray JNICALL Java_Encrypt_encrypt(    JNIEnv * _env,     jobject _obj,    jbyteArray _buf){    jsize len =_env->GetArrayLength(_buf);       unsigned char* dst = (unsigned char*)_env->GetByteArrayElements(_buf, 0); 	 	for (int i = 0; i < len; ++i) 	{ 		dst[i] = dst[i] ^ 0x07; 	}     _env->SetByteArrayRegion(_buf, 0, len, (jbyte *)dst);    return _buf;} | 
在運行jar文件的時候,加載我們的jvmti agent動態庫進行動態解密:
| 12345678910111213141516171819202122232425262728293031 | void JNICALLMyClassFileLoadHook(    jvmtiEnv *jvmti_env,    JNIEnv* jni_env,    jclass class_being_redefined,    jobject loader,    const char* name,    jobject protection_domain,    jint class_data_len,    const unsigned char* class_data,    jint* new_class_data_len,    unsigned char** new_class_data){    *new_class_data_len = class_data_len;    jvmti_env->Allocate(class_data_len, new_class_data);     unsigned char* my_data = *new_class_data;    if(name&&strncmp(name,"com/monkey/",11)==0){        for (int i = 0; i < class_data_len; ++i)        {            my_data[i] = class_data[i] ^ 0x07;        }    }else{        for (int i = 0; i < class_data_len; ++i)        {            my_data[i] = class_data[i];        }    }} | 
這里寫了一個簡單的例子,代碼在github。
首先加密jar包:
| 12 | javac Encrypt.javajava -Djava.library.path=. -cp . Encrypt -src jardemo.jar | 

然后會得到一個jardemo_encrypt.jar文件,如果現在直接去運行該文件的話肯定是會出錯的,所以要做解密。
先編譯生成一個解密的動態庫libdecrypt.dylib。然后運行:
| 1 | java -jar -agentlib:decrypt jardemo_encrypt.jar | 

總的來說,使用jvmti提供的監控api,方便了我們直接對class的操作,所以第二個方案更好一些,當然其中具體使用怎么樣的加密,以及如何去保證加密不被破解就需要各位發揮自己的空間了。
原文地址:http://www.alonemonkey.com/2016/05/25/encrypt-jar-class/
參考:http://blog.csdn.net/yczz/article/details/39034223
https://www.ibm.com/developerworks/cn/java/l-secureclass/
| 
 
 | 
新聞熱點
疑難解答