使用JVMTI实现SpringBoot的jar加密,防止反编译
作者:完美明天cxp
1.背景
ToB项目私有化部署,携带有项目jar包,防止别人下载jar,反编译出源码
2.JVMTI解释
JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,它处于整个 JPDA(Java Platform Debugger Architecture) 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。
通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。
3.使用JVMTI思路
在SpringBoot项目打包后,通过dll动态链接库(so共享对象)加密生成新的jar,在启动jar时,让jvm启动时调用jvmti提供的Agent_OnLoad方法,用c++去实现,去解密对应的class文件。
利用c++编译型语言特性,无法反编译出对应的加密解密方法。
4.实现源码
c++加密实现和jvm启动监听,需要jni.h,jvmti.h,jni_md.h三个头文件,分别在java环境变量下include和include/linux(win32)下
// c++加密头文件 #include <jni.h> #ifndef _Included_com_cxp_demo_encrypt_ByteCodeEncryptor #define _Included_com_cxp_demo_encrypt_ByteCodeEncryptor #ifdef __cplusplus extern "C" { #endif JNIEXPORT jbyteArray JNICALL Java_com_cxp_demo_encrypt_ByteCodeEncryptor_encrypt(JNIEnv *, jclass, jbyteArray); #ifdef __cplusplus } #endif #endif #ifndef CONST_HEADER_H_ #define CONST_HEADER_H_ const int k = 4; const int compare_length = 18; const char* package_prefix= "com/cxp/demo"; #endif
// c++加密解密源码 #include <iostream> #include <jni.h> #include <jvmti.h> #include <jni_md.h> #include "demo_bytecode_encryptor.h" void encode(char *str) { unsigned int m = strlen(str); for (int i = 0; i < m; i++) { str[i] = str[i] + k; } } void decode(char *str) { unsigned int m = strlen(str); for (int i = 0; i < m; i++) { str[i] = str[i] - k; } } extern"C" JNIEXPORT jbyteArray JNICALL Java_com_cxp_demo_encrypt_ByteCodeEncryptor_encrypt(JNIEnv * env, jclass cla, jbyteArray text) { char* dst = (char*)env->GetByteArrayElements(text, 0); encode(dst); env->SetByteArrayRegion(text, 0, strlen(dst), (jbyte *)dst); return text; } void JNICALL ClassDecryptHook( 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* _data = *new_class_data; if (name && strncmp(name, package_prefix, compare_length) == 0 && strstr(name, "BySpringCGLIB") == NULL) { for (int i = 0; i < class_data_len; i++) { _data[i] = class_data[i]; } decode((char*)_data); } else { for (int i = 0; i < class_data_len; i++) { _data[i] = class_data[i]; } } } JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) { jvmtiEnv *jvmti; jint ret = vm->GetEnv((void **)&jvmti, JVMTI_VERSION); if (JNI_OK != ret) { printf("ERROR: Unable to access JVMTI!\n"); return ret; } jvmtiCapabilities capabilities; (void)memset(&capabilities, 0, sizeof(capabilities)); capabilities.can_generate_all_class_hook_events = 1; capabilities.can_tag_objects = 1; capabilities.can_generate_object_free_events = 1; capabilities.can_get_source_file_name = 1; capabilities.can_get_line_numbers = 1; capabilities.can_generate_vm_object_alloc_events = 1; jvmtiError error = jvmti->AddCapabilities(&capabilities); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Unable to AddCapabilities JVMTI!\n"); return error; } jvmtiEventCallbacks callbacks; (void)memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &ClassDecryptHook; error = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks)); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Unable to SetEventCallbacks JVMTI!\n"); return error; } error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Unable to SetEventNotificationMode JVMTI!\n"); return error; } return JNI_OK; }
java加密逻辑:
// JNI调用c++代码 public class ByteCodeEncryptor { static { String currentPath = ByteCodeEncryptor.class.getResource("").getPath().split("SpringBootJarEncryptor.jar!")[0]; if (currentPath.startsWith("file:")) { currentPath = currentPath.substring(5); } String dllPath; String os = System.getProperty("os.name"); if (os.toLowerCase().startsWith("win")) { dllPath = currentPath + "SpringBootJarEncryptor.dll"; } else { dllPath = currentPath + "SpringBootJarEncryptor.so"; } System.load(dllPath); } public native static byte[] encrypt(byte[] text); }
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; // 加密jar代码 public class JarEncryptor { public static void encrypt(String fileName, String dstName) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; File srcFile = new File(fileName); File dstFile = new File(dstName); FileOutputStream dstFos = new FileOutputStream(dstFile); JarOutputStream dstJar = new JarOutputStream(dstFos); JarFile srcJar = new JarFile(srcFile); for (Enumeration<JarEntry> enumeration = srcJar.entries(); enumeration.hasMoreElements(); ) { JarEntry entry = enumeration.nextElement(); InputStream is = srcJar.getInputStream(entry); int len; while ((len = is.read(buf, 0, buf.length)) != -1) { bos.write(buf, 0, len); } byte[] bytes = bos.toByteArray(); String name = entry.getName(); if (name.startsWith("com/cxp/demo") && name.endsWith(".class")) { try { bytes = ByteCodeEncryptor.encrypt(bytes); } catch (Exception e) { e.printStackTrace(); } } JarEntry ne = new JarEntry(name); dstJar.putNextEntry(ne); dstJar.write(bytes); bos.reset(); } srcJar.close(); dstJar.close(); dstFos.close(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { if (args == null || args.length == 0) { System.out.println("please input parameter"); return; } if (args[0].endsWith(".jar")) { JarEncryptor.encrypt(args[0], args[0].substring(0, args[0].lastIndexOf(".")) + "_encrypted.jar"); } else { File file = new File(args[0]); if (file.isDirectory()) { String[] list = file.list(); if (list != null) { for (String jarFilePath : list) { if (jarFilePath.endsWith(".jar")) { JarEncryptor.encrypt(args[0] + "/" + jarFilePath, args[0] + "_encrypted/" + jarFilePath); } } } } else { System.out.println("this is not a folder or folder is empty"); } } } }
实现步骤:
1.先把c++打成dll(so)动态链接库
2.通过java调用dll文件,加密jar
3.启动加密后的jar,加上启动参数-agentpath:*.dll
gradle.build打包:
task copyJar(type: Copy) { delete "$buildDir/libs/lib" from configurations.runtime into "$buildDir/libs/lib" } jar { enabled = true dependsOn copyJar archivesBaseName = "app" archiveVersion = "" manifest { attributes 'Main-Class': "主类全限定名", 'Class-Path': configurations.runtime.files.collect { "lib/$it.name" }.join(' ') } }
Dockerfile文件:
# 加密jar RUN mkdir ./build/libs/lib_encrypted RUN java -jar ./encrypt/SpringBootJarEncryptor.jar ./build/libs/app.jar RUN java -jar ./encrypt/SpringBootJarEncryptor.jar ./build/libs/lib RUN rm -r ./build/libs/lib RUN rm ./build/libs/app.jar RUN mv ./build/libs/lib_encrypted ./build/libs/lib RUN mv ./build/libs/app_encrypted.jar ./build/libs/app.jar # --- java --- FROM java8 # 提取启动需要的资源 COPY --from=builder /build/libs/app.jar /application/app.jar COPY --from=builder /build/libs/lib /application/lib COPY --from=builder /encrypt /encrypt COPY --from=builder /start.sh start.sh ENTRYPOINT ["sh", "start.sh"]
启动命令:
java -agentpath:./SpringBootJarEncryptor.dll -jar app.jar
5.踩坑
java使用jni调用c++函数,c++被调用的函数名要与java方法全限定名一致,如:com.cxp.demo.encrypt.ByteCodeEncryptor#encrypt=》Java_com_cxp_demo_encrypt_ByteCodeEncryptor_encrypt,注意java和c++数据类型的对应关系
JVMTI只能作用Java原生启动类加载器,而SpringBoot有自己启动类加载器,读取SpringBoot规定的原文件路径(BOOT-INF/classes)和依赖路径(BOOT-INF/lib),所以SpringBoot要打包成普通jar方式,bootjar是SpringBoot默认的打包方式,改为jar打包方式无法打入依赖包,只能把依赖包打到同文件夹下,修改MANIFEST.MF文件的CLass-Path属性,把依赖包添加进去。可以看下SpringBoot的启动jar和普通jar区别。
c++在linux下打成so库,可以自行搜索下,推荐使用cmake
java引入dll动态库,只能放在java的classpath下或绝对路径
引包方式改为可以打出依赖包的方式,如compile,运行时不需要的可以不用
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。