背景
最近针对公司框架进行要害业务代码进行加密解决,避免通过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<String>来映射
插件应用配置如图所示
自定义classloader
创立CustomClassLoader继承自ClassLoader,重写findClass办法只解决装载加密后的class文件,其余class交有默认加载器解决,须要留神的是默认解决不能调用super.finclass办法,在idea调试没问题,打成jar包运行就会报加密的class中的依赖class无奈加载(ClassNoDefException/ClassNotFoundException),这里应用的是以后线程的上下文的类加载器就没有问题(Thread.currentThread().getContextClassLoader())
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加密防止全局搜寻),例如
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中的办法,例如
@Componentpublic 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的拆分越小,系统启动注入点暗藏的越多,那破解的老本就会越高,如果有不足之处还请见谅