服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - SpringBoot详细讲解通过自定义classloader加密保护class文件

SpringBoot详细讲解通过自定义classloader加密保护class文件

2022-11-22 11:20code2roc Java教程

这篇文章主要介绍了SpringBoot通过自定义classloader加密class文件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

背景

最近针对公司框架进行关键业务代码进行加密处理,防止通过jd-gui等反编译工具能够轻松还原工程代码,相关混淆方案配置使用比较复杂且针对springboot项目问题较多,所以针对class文件加密再通过自定义的classloder进行解密加载,此方案并不是绝对安全,只是加大反编译的困难程度,防君子不防小人,整体加密保护流程图如下图所示

SpringBoot详细讲解通过自定义classloader加密保护class文件

maven插件加密

使用自定义maven插件对编译后指定的class文件进行加密,加密后的class文件拷贝到指定路径,这里是保存到resource/coreclass下,删除源class文件,加密使用的是简单的DES对称加密

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@Parameter(name = "protectClassNames", defaultValue = "")
private List<String> protectClassNames;
@Parameter(name = "noCompileClassNames", defaultValue = "")
private List<String> noCompileClassNames;
private List<String> protectClassNameList = new ArrayList<>();
private void protectCore(File root) throws IOException {
       if (root.isDirectory()) {
           for (File file : root.listFiles()) {
               protectCore(file);
           }
       }
       String className = root.getName().replace(".class", "");
       if (root.getName().endsWith(".class")) {
           //class筛选
           boolean flag = false;
           if (protectClassNames!=null && protectClassNames.size()>0) {
               for (String item : protectClassNames) {
                   if (className.equals(item)) {
                       flag = true;
                   }
               }
           }
           if(noCompileClassNames.contains(className)){
               boolean deleteResult = root.delete();
               if(!deleteResult){
                   System.gc();
                   deleteResult = root.delete();
               }
               System.out.println("【noCompile-deleteResult】:" + deleteResult);
           }
           if (flag && !protectClassNameList.contains(className)) {
               protectClassNameList.add(className);
               System.out.println("【protectCore】:" + className);
               FileOutputStream fos = null;
               try {
                   final byte[] instrumentBytes = doProtectCore(root);
                   //加密后的class文件保存路径
                   String folderPath = output.getAbsolutePath() + "\\" + "classes";
                   File  folder = new File(folderPath);
                   if(!folder.exists()){
                       folder.mkdir();
                   }
                   folderPath = output.getAbsolutePath() + "\\" + "classes"+ "\\" + "coreclass" ;
                   folder = new File(folderPath);
                   if(!folder.exists()){
                       folder.mkdir();
                   }
                   String filePath = output.getAbsolutePath() + "\\" + "classes" + "\\" + "coreclass" + "\\" + className + ".class";
                   System.out.println("【filePath】:" + filePath);
                   File protectFile = new File(filePath);
                   if (protectFile.exists()) {
                       protectFile.delete();
                   }
                   protectFile.createNewFile();
                   fos = new FileOutputStream(protectFile);
                   fos.write(instrumentBytes);
                   fos.flush();
               } catch (MojoExecutionException e) {
                   System.out.println("【protectCore-exception】:" + className);
                   e.printStackTrace();
               } finally {
                   if (fos != null) {
                       fos.close();
                   }
                   if(root.exists()){
                       boolean deleteResult = root.delete();
                       if(!deleteResult){
                           System.gc();
                           deleteResult = root.delete();
                       }
                       System.out.println("【protectCore-deleteResult】:" + deleteResult);
                   }
               }
           }
       }
   }
   private byte[] doProtectCore(File clsFile) throws MojoExecutionException {
       try {
           FileInputStream inputStream = new FileInputStream(clsFile);
           byte[] content = ProtectUtil.encrypt(inputStream);
           inputStream.close();
           return content;
       } catch (Exception e) {
           throw new MojoExecutionException("doProtectCore error", e);
       }
   }

注意事项

1.加密后的文件也是class文件,为了防止在递归查找中重复加密,需要对已经加密后的class名称记录防止重复

2.在删除源文件时可能出现编译占用的情况,执行System.gc()后方可删除

3.针对自定义插件的列表形式的configuration节点可以使用List来映射

插件使用配置如图所示

SpringBoot详细讲解通过自定义classloader加密保护class文件

自定义classloader

创建CustomClassLoader继承自ClassLoader,重写findClass方法只处理装载加密后的class文件,其他class交有默认加载器处理,需要注意的是默认处理不能调用super.finclass方法,在idea调试没问题,打成jar包运行就会报加密的class中的依赖class无法加载(ClassNoDefException/ClassNotFoundException),这里使用的是当前线程的上下文的类加载器就没有问题(Thread.currentThread().getContextClassLoader())

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clz = findLoadedClass(name);
        //先查询有没有加载过这个类。如果已经加载,则直接返回加载好的类。如果没有,则加载新的类。
        if (clz != null) {
            return clz;
        }
        String[] classNameList = name.split("\\.");
        String classFileName = classNameList[classNameList.length - 1];
        if (classFileName.endsWith("MethodAccess") || !classFileName.endsWith("CoreUtil")) {
            return Thread.currentThread().getContextClassLoader().loadClass(name);
        }
        ClassLoader parent = this.getParent();
        try {
            //委派给父类加载
            clz = parent.loadClass(name);
        } catch (Exception e) {
            //log.warn("parent load class fail:"+ e.getMessage(),e);
        }
        if (clz != null) {
            return clz;
        } else {
            byte[] classData = null;
            ClassPathResource classPathResource = new ClassPathResource("coreclass/" + classFileName + ".class");
            InputStream is = null;
            try {
                is = classPathResource.getInputStream();
                classData = DESEncryptUtil.decryptFromByteV2(FileUtil.convertStreamToByte(is), "xxxxxxx");
            } catch (Exception e) {
                e.printStackTrace();
                throw new ProtectClassLoadException("getClassData error");
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                clz = defineClass(name, classData, 0, classData.length);
            }
            return clz;
        }
    }
}

隐藏classloader

classloader加密class文件处理方案的漏洞在于自定义类加载器是完全暴露的,只需进行分析解密流程就能获取到原始class文件,所以我们需要对classloder的内容进行隐藏

1.把classloader的源文件在编译期间进行删除(maven自定义插件实现)

2.将classloder的内容进行base64编码后拆分内容寻找多个系统启动注入点写入到loader.key文件中(拆分时写入的路径和文件名需要进行base64加密避免全局搜索),例如

?
1
2
3
4
5
6
7
8
9
10
private static void init() {
    String source = "dCA9IG5hbWUuc3BsaXQoIlxcLiIpOwogICAgICAgIFN0cmluZyBjbGFzc0ZpbGVOYW1lID0gY2xhc3NOYW1lTGlzdFtjbGFzc05hbWVMaXN0Lmxlbmd0aCAtIDFdOwogICAgICAgIGlmIChjbGFzc0ZpbGVOYW1lLmVuZHNXaXRoKCJNZXRob2RBY2Nlc3MiKSB8fCAhY2xhc3NGaWxlTmFtZS5lbmRzV2l0aCgiQ29yZVV0aWwiKSkgewogICAgICAgICAgICByZXR1cm4gVGhyZWFkLmN1cnJlbnRUaHJlYWQoKS5nZXRDb250ZXh0Q2xhc3NMb2FkZXIoKS5sb2FkQ2xhc3MobmFtZSk7CiAgICAgICAgfQogICAgICAgIENsYXNzTG9hZGVyIHBhcmVudCA9IHRoaXMuZ2V0UGFyZW50KCk7CiAgICAgICAgdHJ5IHsKICAgICAgICAgICAgLy/lp5TmtL7nu5nniLbnsbvliqDovb0KICAgICAgICAgICAgY2x6ID0gcGFyZW50LmxvYWRDbGFzcyhuYW1lKTsKICAgICAgICB9IGNhdGNoIChFeGNlcHRpb24gZSkgewogICAgICAgICAgICAvL2xvZy53YXJuKCJwYXJlbnQgbG9hZCBjbGFzcyBmYWls77yaIisgZS5nZXRNZXNzYWdlKCksZSk7CiAgICAgICAgfQogICAgICAgIGlmIChjbHogIT0gbnVsbCkgewogICAgICAgICAgICByZXR1cm4gY2x6OwogICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgIGJ5dGVbXSBjbGFzc0RhdGEgPSBudWxsOwogICAgICAgICAgICBDbGFzc1BhdGhSZXNvdXJjZSBjbGFzc1BhdGhSZXNvdXJjZSA9IG5ldyBDbGFzc1BhdGhSZXNvdXJjZSgiY29yZWNsYXNzLyIgKyBjbGFzc0ZpbGVOYW1lICsgIi5jbGFzcyIpOwogICAgICAgICAgICBJbnB1dFN0cmVhbSBpcyA9IG51bGw7CiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICBpcyA9IGNsYXNzUGF0aFJlc291cmNlLmdldElucHV0U3RyZWFtKCk7CiAgICAgICAgICAgICAgICBjbGFzc0RhdGEgPSBERVNFbmNyeXB0VXRpbC5kZWNyeXB0RnJvbUJ5dGVWMihGaWxlVXRpbC5jb252ZXJ0U3RyZWFtVG9CeXRlKGlzKSwgIlNGQkRiRzkxWkZoaFltTmtNVEl6TkE9PSIpOwogICAgICAgICAgICB9IGNhdGNoIChFeGNlcHRpb24gZSkgewogICAgICAgICAgICAgICAgZS5wcmludFN0YWNrVHJhY2UoKTsKICAgICAgICAgICAgICAgIHRocm93IG5ldyBQc";
    String filePath = "";
    try{
        filePath = new String(Base64.decodeBase64("dGVtcGZpbGVzL2R5bmFtaWNnZW5zZXJhdGUvbG9hZGVyLmtleQ=="),"utf-8");
    }catch (Exception e){
        e.printStackTrace();
    }
    FileUtil.writeFile(filePath, source,true);
}

3.通过GroovyClassLoader对classloder的内容(字符串)进行动态编译获取到对象,删除loader.key文件

pom文件增加动态编译依赖

?
1
2
3
4
5
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.13</version>
</dependency>

获取文件内容进行编译代码如下(写入/读取注意utf-8处理防止乱码)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CustomCompile {
    private static Object Compile(String source){
        Object instance = null;
        try{
            // 编译器
            CompilerConfiguration config = new CompilerConfiguration();
            config.setSourceEncoding("UTF-8");
            // 设置该GroovyClassLoader的父ClassLoader为当前线程的加载器(默认)
            GroovyClassLoader groovyClassLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config);
            Class<?> clazz = groovyClassLoader.parseClass(source);
            // 创建实例
            instance = clazz.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
        return instance;
    }
    public static  ClassLoader getClassLoader(){
        String filePath = "tempfiles/dynamicgenserate/loader.key";
        String source = FileUtil.readFileContent(filePath);
        byte[] decodeByte = Base64.decodeBase64(source);
        String str = "";
        try{
            str = new String(decodeByte, "utf-8");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            FileUtil.deleteDirectory("tempfiles/dynamicgenserate/");
        }
        return (ClassLoader)Compile(str);
    }
}

被保护class手动加壳

因为相关需要加密的class文件都是通过customerclassloder加载的,获取不到显示的class类型,所以我们实际的业务类只能通过反射的方法进行调用,例如业务工具类LicenseUtil,加密后类为LicenseCoreUtil,我们在LicenseUtil的方法中需要反射调用,LicenseCoreUtil中的方法,例如

?
1
2
3
4
5
6
7
8
9
10
@Component
public class LicenseUtil {
    private String coreClassName = "com.haopan.frame.core.util.LicenseCoreUtil";
    public String getMachineCode() throws Exception {
        return (String) CoreLoader.getInstance().executeMethod(coreClassName, "getMachineCode");
    }
    public boolean checkLicense(boolean startCheck) {
        return (boolean)CoreLoader.getInstance().executeMethod(coreClassName, "checkLicense",startCheck);
    }
}

为了避免反射调用随着调用次数的增加损失较多的性能,使用了一个第三方的插件reflectasm,pom增加依赖

?
1
2
3
4
5
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>reflectasm</artifactId>
    <version>1.11.0</version>
</dependency>

reflectasm使用了MethodAccess快速定位方法并在字节码层面进行调用,CoreLoader的代码如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CoreLoader {
    private ClassLoader classLoader;
    private CoreLoader() {
        classLoader = CustomCompile.getClassLoader();
    }
    private static class SingleInstace {
        private static final CoreLoader instance = new CoreLoader();
    }
    public static CoreLoader getInstance() {
        return SingleInstace.instance;
    }
    public Object executeMethod(String className,String methodName, Object... args) {
        Object result = null;
        try {
            Class clz = classLoader.loadClass(className);
            MethodAccess access = MethodAccess.get(clz);
            result = access.invoke(clz.newInstance(), methodName, args);
        } catch (Exception e) {
            e.printStackTrace();
            throw  new ProtectClassLoadException("executeMethod error");
        }
        return result;
    }
}

总结

自定义classloder并不是一个完美的代码加密保护的解决方案,但就改造工作量与对项目的影响程度来说是最小的,只需要针对关键核心逻辑方法进行保护,不会对系统运行逻辑产生影响制造bug,理论上来说只要classloder的拆分越小,系统启动注入点隐藏的越多,那破解的成本就会越高,如果有不足之处还请见谅

到此这篇关于SpringBoot详细讲解通过自定义classloader加密保护class文件的文章就介绍到这了,更多相关SpringBoot自定义classloader内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/u013407099/article/details/123021968

延伸 · 阅读

精彩推荐
  • Java教程IDEA 开发多项目依赖的方法(图文)

    IDEA 开发多项目依赖的方法(图文)

    这篇文章主要介绍了IDEA 开发多项目依赖的方法(图文),本文讲一下关于使用IntelliJ IDEA基于Maven创建多模块项目的实际开发,非常具有实用价值,需要的朋友...

    yisun12345610242021-06-08
  • Java教程Java文件操作之IO流 File类的使用详解

    Java文件操作之IO流 File类的使用详解

    在java中提供有对于文件操作系统的支持,这个支持在java.io.File类中进行了定义,也就是说在整个java.io包中File类是唯一一个与文件本身操作有关的类(创建,删除...

    叶绿体不忘呼吸3612022-01-22
  • Java教程JAVA实现下载文件功能的两种方法

    JAVA实现下载文件功能的两种方法

    这篇文章主要介绍了JAVA实现下载文件功能的两种方法,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下 ...

    奋进的小土豆2462020-07-15
  • Java教程java设计模式之简单工厂模式

    java设计模式之简单工厂模式

    这篇文章主要为大家详细介绍了java设计模式之简单工厂模式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    jiayayao2952020-07-11
  • Java教程SpringCloud Config使用本地仓库及map注入

    SpringCloud Config使用本地仓库及map注入

    这篇文章主要介绍了SpringCloud Config使用本地仓库及map注入,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    一马平川15782020-09-10
  • Java教程listview点击无效的处理方法(推荐)

    listview点击无效的处理方法(推荐)

    下面小编就为大家带来一篇listview点击无效的处理方法(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    Java教程网2472020-09-22
  • Java教程HashMap实现保存两个key相同的数据

    HashMap实现保存两个key相同的数据

    这篇文章主要介绍了HashMap实现保存两个key相同的数据操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...

    Bee.F6602021-09-22
  • Java教程SpringBoot处理请求参数中包含特殊符号

    SpringBoot处理请求参数中包含特殊符号

    今天写代码遇到了一个问题,请求参数是个路径“D:/ExcelFile”,本文就详细的介绍一下该错误的解决方法,感兴趣的可以了解一下...

    Programmer18172021-09-22