Jar 反編譯保護

JAR包反編譯

使用 JD-GUI 打開JAR包,就能獲取JAR包的源碼, 這樣如果對於想保護源碼的開發者或廠商來說是一件煩惱的事情;

JAR包反編譯保護

方案大概有以下幾種:
1. 使用 ProGuard 工具進行代碼混淆, 但有點耐心的人還是能夠分析出來的;【不做介紹】
2. 對class進行加密,再自定義ClassLoader進行解密,但自定義的ClassLoader本身就很容易成爲突破口;【不做介紹】
3. 對class進行加密,使用 JVM Tool Interface 對JAR包進行解密,這樣不容易被輕易突破, 但也能夠進行突破如果能遇上反編譯大牛的話,能逆向動態庫,看懂彙編那就另說了,當然還可以對so(dll)進行再次加殼(加密)操作,有興趣的同學可以去Google下,推薦個關於安全的論壇看雪

JVM Tool Interface 介紹

JVM工具接口(JVM TI)是開發和監視工具使用的編程接口; 可以通用編寫一個Agent進行對JVM進行一些監控,比如:監控函數入口、線程、堆棧等操作,當然也可以編寫類加載HOOK對class進行解密操作。

文檔: 《JVM Tool Interface 1.2》

JVM Tool Interface 使用

參考IBM《JVMTI 和 Agent實現》, 這邊就不過多進行介紹。

Encrypt

加密方法這邊通過簡單的位置互換和異或操作來達到目的, 當然也可以使用加密算法,對稱加密或非對稱加密;

將JAR包中指定的類進行加密,生成一個新的JAR包

package com.tool;

import java.io.*;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
 * Created by Borey.Zhu on 2017/9/8.
 */
public class Encrypt {

    native byte[] encrypt(byte[] _buf);

    static {
        System.loadLibrary("encrypt");
    }

    // 需要加密類包的前綴
    private static String encryptClassPrefix = "com/borey/";

    public static void main(String []args){

        int argc = args.length;
        if (0 == argc){
            System.out.println("usage:\n\t java -Djava.library.path=./ -cp jvm-ti.jar  com.tool.Encrypt  [*.jar]");
            System.exit(0);
        }
        for (String package_name : args) {
            String encryptedJar = encryptJarFile(package_name);
            System.out.println(String.format("Encrypt Jar Package: %s  -->  %s", package_name, encryptedJar));
        }
    }

    /**
     * @todo  加密jar包返回加密後的jar 包路徑
     *
     * @param jarSrcName: Jar 包路徑
     *
     * @return  encrypt jar 路徑
     * */
    public static String encryptJarFile(String jarSrcName){
        String jarEncrypt_path = null;
        try{
            if (!jarSrcName.endsWith(".jar")){
                System.err.println(String.format("Error: %s is not a jar package!", jarSrcName));;
            }
            Encrypt encrypt = new Encrypt();

            // 生成新的 加密jar包
            jarEncrypt_path = jarSrcName.substring(0, jarSrcName.length() - 4) + "-encrypted.jar";
            JarOutputStream encryptJar = new JarOutputStream(new FileOutputStream(jarEncrypt_path));


            byte[] buf = new byte[2048];
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            // 讀取 jar 包
            JarFile jf_src = new JarFile(jarSrcName);

            for (Enumeration<JarEntry> enumeration = jf_src.entries(); enumeration.hasMoreElements();) {
                JarEntry    entry = enumeration.nextElement();
                InputStream input = jf_src.getInputStream(entry);
                int len;

                while ( (len = input.read(buf, 0, 2048)) != -1){
                    output.write(buf, 0, len);
                }

                String className = entry.getName();
                byte[] classBytes = output.toByteArray();


                if (className.endsWith(".class") && className.startsWith(encryptClassPrefix)){
                    classBytes = encrypt.encrypt(classBytes);
                    System.out.println(String.format("Encrypted [package]:%s, [class]:%s ", jarSrcName, className));
                }else{
                    System.out.println(String.format("Appended  [package]:%s, [class]:%s ", jarSrcName, className));
                }

                // 將處理完後的class 寫入新的 Jar 包中
                encryptJar.putNextEntry(new JarEntry(className));
                encryptJar.write(classBytes);
                output.reset();
            }

            encryptJar.close();
            jf_src.close();

        }catch (IOException e){
            e.printStackTrace();
        }

        return jarEncrypt_path;
    }
}

加密算法採用CPP編寫

extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tool_Encrypt_encrypt(
    JNIEnv * jni_env, 
    jobject  arg, 
    jbyteArray _buf){
    jbyte * jbuf = jni_env->GetByteArrayElements(_buf, 0);
    jsize length = jni_env->GetArrayLength( _buf);

    jbyte *dbuf = (jbyte *)malloc(length);
    int index = 0;
    for( ;index < length-1; ){
        dbuf[index]  = jbuf[index+1] ^ 0x07;
        dbuf[index+1]= jbuf[index  ] ^ 0x08;
        index += 2;
    }
    if ( (0 == index  && 1 == length) || length-1 == index){    // size 1 || size 2(index-1) + 1
        dbuf[index] = jbuf[index] ^ 0x09;
    }
    jbyteArray dst = jni_env->NewByteArray(length);
    jni_env->SetByteArrayRegion(dst, 0, length, dbuf);
    free(dbuf);
    return dst;
} 

Decrypt

解密利用 JVM TI來完成, 通過編寫一個Agent,capability 設置 can_generate_all_class_hook_events 爲 1, 再註冊一個ClassFileLoadHook函數指針來進行回調,每當JVM進行類加載時,都會進行調用該方法;

void JvmTIAgent::AddCapability() const throw(AgentException)
{
    // 創建一個新的環境
    jvmtiCapabilities caps;
    memset(&caps, 0, sizeof(caps));
    // 可以生成 方法進入事件
    //caps.can_generate_method_entry_events = 1;
    // 可以對每個加載要類生成 ClassFileLoadHook 事件
    caps.can_generate_all_class_hook_events = 1;

    // 設置當前環境
    jvmtiError error = m_jvmti->AddCapabilities(&caps);
    CheckException(error);
}

void JvmTIAgent::RegisterEvent() const throw(AgentException)
{
    // 創建一個新的回調函數
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    // 設置方法進入函數指針
    //callbacks.MethodEntry = &JvmTIAgent::HandleMethodEntry;
    // 設置類加載方法函數指針
    callbacks.ClassFileLoadHook = &JvmTIAgent::HandleClassFileLoadHook;

    // 設置回調函數
    jvmtiError error;
    error = m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
    CheckException(error);

    // 開啓事件監聽
    error = m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);
    error = m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, 0);
    CheckException(error);
}

void JNICALL JvmTIAgent::HandleClassFileLoadHook(
    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)
{
    try{
        // class 文件長度賦值
        *new_class_data_len = class_data_len;
        jvmtiError error;
        // 申請 新的class 字符空間
        error = m_jvmti->Allocate(class_data_len, new_class_data);
        CheckException(error);
        unsigned char *pNewClass = *new_class_data;
        unsigned int selfPackageLen = strlen(g_SelfJavaPackageName);
        if (strlen(name) > selfPackageLen && 0 == strncmp(name, g_SelfJavaPackageName, selfPackageLen)){
            // 進行 class 解密 :規則(奇數異或 0x07, 偶數 異或 0x08, 並換位, 最後一位奇數異或0x09)
            int index = 0;
            for(; index < class_data_len-1 ; ){
                *pNewClass++ = class_data[index+1] ^ 0x08;
                *pNewClass++ = class_data[index]   ^ 0x07;
                index += 2; 
            }

            if ( (0 == index  && 1 == class_data_len) ||  class_data_len-1 == index ){  // size 1 || size 2n + 1
                *pNewClass = class_data[index] ^ 0x09;
            }
            cout << "[JVMTI Agent] Decrypt class (" << name << ") finished !" <<endl;
        }else{
            memcpy(pNewClass, class_data, class_data_len);
        }

    }catch(AgentException& e) {
        cerr << "Error when enter HandleClassFileLoadHook: " << e.what() << " [" << e.ErrCode() << "]";
    }    
}

JVM TI Example

代碼 github

[注意] jvm-tool-cpp 目錄下的 makefile 中 INCLUDE 參數修改爲本地的JDK頭文件的所在的目錄

  • 編譯
cd jvmti && make

這裏寫圖片描述

  • 使用JD-GUI 對JAR進行反編譯

這裏寫圖片描述
這裏寫圖片描述

  • 運行JAR包
java -Djava.library.path=./ -cp jvm-ti.jar com.borey.JvmTITest

這裏寫圖片描述

  • 對JAR包進行加密
java -Djava.library.path=./ -cp jvm-ti.jar  com.tool.Encrypt jvm-ti.jar 

這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述

  • 正常運行加密後的JAR包,ClassFormatError錯誤
java -Djava.library.path=./ -cp jvm-ti-encrypted.jar com.borey.JvmTITest

這裏寫圖片描述

  • 運行帶Agent加密後的JAR包
java -Djava.library.path=./ -agentpath:./libagent.so=show -cp jvm-ti-encrypted.jar com.borey.JvmTITest

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章