使用JVMTI实现SpringBoot的jar加密,防止反编译

 更新时间:2023年08月31日 15:06:24   作者:完美明天cxp  
这篇文章主要介绍了使用JVMTI实现SpringBoot的jar加密,防止反编译问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

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,运行时不需要的可以不用

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java数组的基本操作方法整理

    Java数组的基本操作方法整理

    这篇文章主要介绍了Java数组的基本操作方法整理,是Java入门学习中的基础知识,需要的朋友可以参考下
    2015-08-08
  • java图片验证码生成教程详解

    java图片验证码生成教程详解

    这篇文章主要为大家详细介绍了java图片验证码生成教程,从简单到复杂,从本地到前后台,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • java/word+fusionchart生成图表深入分析

    java/word+fusionchart生成图表深入分析

    本文将以java/word+fusionchart生成图表问题深入解析,有兴趣的朋友可以参考下
    2012-11-11
  • SpringBoot整合JavaMail实现发邮件的项目实践

    SpringBoot整合JavaMail实现发邮件的项目实践

    本文主要介绍了SpringBoot整合JavaMail实现发邮件的项目实践,详细阐述了使用SpringBoot和JavaMail发送邮件的步骤,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • IDEA中设置Tab健为4个空格的方法

    IDEA中设置Tab健为4个空格的方法

    这篇文章给大家介绍了代码缩进用空格还是Tab?(IDEA中设置Tab健为4个空格)的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-03-03
  • 基于AspectJ注解方式实现AOP

    基于AspectJ注解方式实现AOP

    这篇文章主要介绍了基于AspectJ注解方式实现AOP,使用AspectJ的注解可以更方便地编写和管理切面逻辑,而Spring AOP也是使用了AspectJ提供的注解来实现切面编程,需要的朋友可以参考下
    2023-09-09
  • SpringBoot通过自定义注解实现参数校验

    SpringBoot通过自定义注解实现参数校验

    实现参数校验说实话方式还挺多,个人使用过直接在Controller代码里面写、AOP+自定义注解、ConstraintValidator。本文主要和大家讲的是ConstraintValidator实现,感兴趣的可以了解一下
    2022-12-12
  • IDEA连接MySQL提示serverTimezone的问题及解决方法

    IDEA连接MySQL提示serverTimezone的问题及解决方法

    很多朋友私聊小编,使用IDEA软件连接MySQL数据库时总是提示Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' property manually.的错误,小编就不一一回复大家了,下面小编把我的解决方法分享到脚本之家平台,需要的朋友参考下吧
    2021-05-05
  • Java8的Stream()与ParallelStream()的区别说明

    Java8的Stream()与ParallelStream()的区别说明

    这篇文章主要介绍了Java8的Stream()与ParallelStream()的区别说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • springcloud中Feign超时提示Read timed out executing POST的问题及解决方法

    springcloud中Feign超时提示Read timed out executing

    Feign接口调用分两层,Ribbon的调用和Hystrix调用,理论上设置Ribbon的时间即可,但是Ribbon的超时时间和Hystrix的超时时间需要结合起来,这篇文章给大家介绍springcloud之Feign超时提示Read timed out executing POST问题及解决方法,感兴趣的朋友一起看看吧
    2024-01-01

最新评论