背景
最近针对公司框架进行要害业务代码进行加密解决,避免通过 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 中的办法,例如
@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 的拆分越小,系统启动注入点暗藏的越多,那破解的老本就会越高,如果有不足之处还请见谅