SpringBoot自定义classloader加密保护class文件

2024-09-06 13:18

本文主要是介绍SpringBoot自定义classloader加密保护class文件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

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

maven插件加密

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

 @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来映射

插件使用配置如图所示

自定义classloader

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

public class CustomClassLoader extends ClassLoader {@Overrideprotected 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加密避免全局搜索),例如

    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文件增加动态编译依赖

        <dependency><groupId>org.codehaus.groovy</groupId><artifactId>groovy-all</artifactId><version>2.4.13</version></dependency>

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

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中的方法,例如

@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增加依赖

        <dependency><groupId>com.esotericsoftware</groupId><artifactId>reflectasm</artifactId><version>1.11.0</version></dependency>

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

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文件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1142109

相关文章

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Java中的工具类命名方法

《Java中的工具类命名方法》:本文主要介绍Java中的工具类究竟如何命名,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java中的工具类究竟如何命名?先来几个例子几种命名方式的比较到底如何命名 ?总结Java中的工具类究竟如何命名?先来几个例子JD

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依