背景

最近针对公司框架进行关键业务代码进行加密处理,防止通过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 {

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

@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文件的更多相关文章

  1. Tomcat自定义classLoader加密解密

    class很好反编译,所以需要对class文件先进行加密,然后使用自己的classloader进行解密并加载. [步骤] 大概分两步: 1.对class文件进行加密 2.写解密class文件并加载的c ...

  2. 自定义ClassLoader加载class文件

    package com.yd.wmsc.util; public class Test { public void say(){ System.out.println("Say Hello& ...

  3. JAVA 利用JNI加密class文件/自定义ClassLoader 类

    利用 JNI 对bytecode 加密.不影响java程序员的正常开发.09年的时候写的,现在拿出来晒晒————————————————————————————混淆才是王道,如果混淆再加密就更酷了.. ...

  4. Java代码加密与反编译(二):用加密算法DES修改classLoader实现对.class文件加密

    Java代码加密与反编译(二):用加密算法DES修改classLoader实现对.class文件加密 二.利用加密算法DES实现java代码加密 传统的C/C++自动带有保护机制,但java不同,只要 ...

  5. [破解] DRM-内容数据版权加密保护技术学习(上):视频文件打包实现

    1. DRM介绍: DRM,英文全称Digital Rights Management, 可以翻译为:内容数字版权加密保护技术. DRM技术的工作原理是,首先建立数字节目授权中心.编码压缩后的数字节目 ...

  6. springboot自定义静态文件目录,解决jar打包后修改页面等静态文件的问题

    1.问题 springboot开发时候,一般将文件放在resources目录,但是发布后想修订文件或是开发时候修改了文件内容一般需重新打包或者重启动才能达到效果: 2.原因 将资源文件打包入jar包, ...

  7. 解决自定义classloader后无法使用maven install

    @上篇博客中探讨了web项目利用自定义classloader进行解密,利用的是编译后的文件直接运行程序一切正常 今天博主在探讨加密后进行混淆时,打包程序报程序包org.apache.catalina. ...

  8. 使用自定义 classloader 的正确姿势

    详细的原理就不多说了,网上一大把, 但是, 看了很多很多, 即使看了jdk 源码, 说了罗里吧嗦, 还是不很明白: 到底如何正确自定义ClassLoader, 需要注意什么 ExtClassLoade ...

  9. .Net加密保护工具分析介绍

    本文主要介绍一些dotNet加密保护工具的原理以及就其脱壳进行简单探讨. remotesoft protector.maxtocode..Net Reactor.Cliprotector.themid ...

随机推荐

  1. react中label标签的作用

    当我们点击输入内容触发input焦点的时候,就需要使用到label标签里的htmlFor属性来扩大点击的区域 代码如下:

  2. HDU 2018 母牛的故事 (递归入门)

    原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=2018 思路分析: 问题分析:这道题的递推关系非常类似斐波那契数列,由题意不难得到以下函数递推式: 对于 ...

  3. leetcode 718. 最长重复子数组

    问题描述 给两个整数数组 A 和 B ,返回两个数组中公共的.长度最长的子数组的长度. 示例: 输入: A: [1,2,3,2,1] B: [3,2,1,4,7] 输出:3 解释: 长度最长的公共子数 ...

  4. Solon Web 开发,十三、WebSocket

    Solon Web 开发 一.开始 二.开发知识准备 三.打包与运行 四.请求上下文 五.数据访问.事务与缓存应用 六.过滤器.处理.拦截器 七.视图模板与Mvc注解 八.校验.及定制与扩展 九.跨域 ...

  5. JSON串、JSON对象、Java对象的相互转换

    对象类型转换2: com.alibaba.fastjson.JSONObject时经常会用到它的转换方法,包括Java对象转成JSON串.JSON对象,JSON串转成java对象.JSON对象,JSO ...

  6. MacBookPro2021 M1-MAX电脑问题锦集

    MacBook2021 M1-MAXPro电脑问题锦集 问题1: 开启硬盘加密,开机闪屏 问题详述: 在系统偏好设置中,打开安全与隐私,在弹出窗口中切换到第二个页签(文件保险箱),启用文件保险箱功能, ...

  7. 539. Minimum Time Difference

    Given a list of 24-hour clock time points in "Hour:Minutes" format, find the minimum minut ...

  8. T-SQL的游标和fetch

    很多时候我们sql操作会得到一个结果集合,当我们需要依次查看集合内的内容时,我们便需要游标特性了. 所以,sql的游标类似c++里容器的迭代器,下面举个例子来辅助理解: DECLARE vend_cu ...

  9. tep0.9.5支持自定义扩展request

    tep0.9.5更新了以下内容: 自定义request请求日志 Allure报告添加request描述 猴子补丁扩展request fixtures支持多层级目录 FastAPI替代Flask 升级t ...

  10. golang中的定时器

    1.  timer 定时器,时间到了执行,只执行一次 package main import ( "fmt" "time" ) func main() { // ...