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 使用
參考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