Spring boot custom classloader encrypts and protects class files

background

Recently, we have encrypted the key business code for the company's framework to prevent the easy restoration of engineering code through JD GUI and other decompiler tools. The configuration of relevant confusion schemes is complex and there are many problems for springboot projects. Therefore, this scheme is not absolutely safe for encrypting class files and decrypting and loading them through custom classloder, Just increase the difficulty of decompilation, and prevent gentleman but not villain. The overall encryption protection flow chart is shown in the figure below

maven plug-in encryption

The user-defined maven plug-in is used to encrypt the compiled specified class file. The encrypted class file is copied to the specified path. Here, it is saved to resource/coreclass and the source class file is deleted. Simple DES symmetric encryption is used for encryption

 @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 filter
            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);
                    //Encrypted class file saving path
                    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);
        }
    }

matters needing attention

1. The encrypted file is also a class file. In order to prevent repeated encryption in recursive search, it is necessary to record the encrypted class name to prevent duplication

2. When deleting the source file, the compilation may be occupied, execute system Delete after GC ()

3. The configuration node in the form of list for user-defined plug-ins can be mapped using list < string >

The plug-in configuration is shown in the figure

Custom classloader

Creating a CustomClassLoader inherits from ClassLoader. Rewriting the findClass method only handles loading encrypted class files. Other classes are handled by the default loader. It should be noted that the default processing cannot call super The finclass method has no problem in idea debugging. When it is run in a jar package, it will report that the dependent class in the encrypted class cannot be loaded (ClassNoDefException/ClassNotFoundException). There is no problem with the class loader using the context of the current thread (thread. Currentthread() getContextClassLoader())

public class CustomClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clz = findLoadedClass(name);
        //First query whether this class has been loaded. If it has been loaded, the loaded class will be returned directly. If not, load a new class.
        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 {
            //Delegate to parent class load
            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;
        }
    }


}

Hide classloader

The flaw of the classloader encrypted class file processing scheme is that the custom class loader is completely exposed. The original class file can be obtained by analyzing the decryption process, so we need to hide the contents of the classloader

1. Delete the source file of classloader during compilation (implemented by maven custom plug-in)

2. After Base64 encoding the contents of the classloder, split the contents, find multiple system startup injection points and write them to the loader Key file (the path and file name written during splitting need base64 encryption to avoid global search), for example

    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. Dynamically compile the content (string) of the classloader through GroovyClassLoader to obtain the object and delete the loader Key file

pom files add dynamic compilation dependency

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

Get the contents of the file and compile the code as follows (pay attention to utf-8 processing in writing / reading to prevent garbled code)

public class CustomCompile {
    private static Object Compile(String source){
        Object instance = null;
        try{
            // compiler
            CompilerConfiguration config = new CompilerConfiguration();
            config.setSourceEncoding("UTF-8");
            // Set the parent ClassLoader of this GroovyClassLoader as the loader of the current thread (default)
            GroovyClassLoader groovyClassLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config);
            Class<?> clazz = groovyClassLoader.parseClass(source);
            // Create instance
            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);
    }
}

Protected manual shelling

Because the relevant class files that need to be encrypted are loaded through customerclassloder, and the displayed class type cannot be obtained, our actual business class can only be called through reflection methods, such as business tool class LicenseUtil. After encryption, the class is LicenseCoreUtil. We need to reflect and call the methods in LicenseUtil, such as

@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);
    }
}

In order to prevent reflection calls from losing more performance as the number of calls increases, a third-party plug-in reflectasm is used to increase the dependency of pom

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

reflectasm uses the MethodAccess quick location method and calls it at the bytecode level. The code of CoreLoader is as follows

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;
    }
}

summary

Custom classloder is not a perfect solution for code encryption protection, but it is the smallest in terms of transformation workload and impact on the project. It only needs to protect the key core logic methods, which will not affect the system operation logic and create bug s. Theoretically, the smaller the split of classloder, the more hidden the system startup injection points, The higher the cost of cracking, please forgive me if there are deficiencies

Keywords: Spring Boot

Added by pdub56 on Sat, 19 Feb 2022 23:09:54 +0200