Custom Secure Class Loader


 

Custom Secure Class Loader

自定义安全类加载器

0x00. 简介

近年来,初创企业和企业都在使用云服务。然而,它带来了安全问题。在这一点上,开发的代码与数据是不同的。关键数据应加密存储。另一方面,开发的代码大多是脆弱地安装在服务器上。大多数Java项目都是直接在服务器上运行后缀为jarwar的扩展文件。这些文件的层次结构中包含字节码文件。然而,有些反编译器可从字节码文件中提取原始Java代码,这对于专利性较强或者企业私有的代码而言是极为不利的。

对于这种情况,我们应该像加密关键数据一样加密重要的代码,将它们存储在云数据库中,并在运行时解密,以保护知识产权。这样,即使云系统被入侵,私密代码仍然是安全的,因为加密密钥不会存储在云系统中。

0x01. 编译方

假设存在非常重要的代码如下,使用IntelliJ IDEA或者javac.exe对其进行编译得到字节码文件,使用IntelliJ IDEA Decompiler或者其他反编译工具很容易就可以得到源码,下面将其放置到E:\\crypto中。

package org.jordon.vm;
public class CoreClass {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

使用javax.crypto.Cipher工具包中的AES-128对编译后的CoreClass类的字节码文件进行加密,此处使用固定密钥进行测试,实际工程中应使用随机密钥。加密后的字节码文件放置到E:\crypto\org\jordon\vm中.

public class ClassEncryption {

    public static void main(String[] args) throws Exception{
        String path = "E:\\crypto";
        String classname = "CoreClass";
        String algorithm = "AES";
        // 16 x 8 = 128 位密钥
        byte[] key = {75, 110, -45, -61, 101, -103, -26, -25, 55, -70, 19, 51, 66, -91, -35, 19}; 
        System.out.println(Arrays.toString(key));
        encrypt(path, classname, algorithm, key);
    }

    private static void encrypt(String path, String classname, String algorithm, byte[] key) throws Exception{
        Path file = Paths.get(path + "\\" + classname + ".class");
        byte[] content = Files.readAllBytes(file);
        Cipher encryption = Cipher.getInstance(algorithm);
        encryption.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, 0, key.length, algorithm));
        byte[] encryptedContent = encryption.doFinal(content);
        writeToFile(path, classname, encryptedContent);
    }

    public static void writeToFile(String path, String filename, byte[] content) throws Exception{
        FileOutputStream out = new FileOutputStream(path + "\\encrypted\\org\\jordon\\vm\\" + filename + ".class");
        out.write(content);
        out.close();
    }
}

0x02. 自定义安全的类加载器

ClassLoader的加载逻辑遵循双亲委派模型,即如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。这样做的好处是

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行
public Class<?> loadClass(String name)throws ClassNotFoundException {
        return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
        // 首先判断该类型是否已经被加载
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
            try {
                if (parent != null) {
                     //如果存在父类加载器,就委派给父类加载器加载
                    c = parent.loadClass(name, false);
                } else {
                //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
             // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写findClass方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

public class MyClassLoader extends ClassLoader {
    private String root;
    private static final String algorithm = "AES";
    private static final byte[] key = {75, 110, -45, -61, 101, -103, -26, -25, 55, -70, 19, 51, 66, -91, -35, 19};

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try {
            Path file = Paths.get(fileName);
            byte[] encryptedContent = Files.readAllBytes(file);

            Cipher decryption = Cipher.getInstance(algorithm);
            decryption.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, 0, key.length, algorithm));

            return decryption.doFinal(encryptedContent);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private void setRoot(String root) {
        this.root = root;
    }
    
    public static void main(String[] args)  {
        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\crypto\\encrypted");
        try {
            Class<?> testClass = classLoader.loadClass("org.jordon.vm.CoreClass");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
            Method m = testClass.getMethod("main", String[].class);
            m.invoke(null, new Object[] {null});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出中打印了类加载器的名称以及对其中的main方法的调用结果

org.jordon.vm.MyClassLoader@677327b6
hello world!

对于一般的类加载器而言,只需在loadClassData中直接返回Files.readAllBytes(Paths.get(fileName))即可,并不需要经过解密这一操作。上述示例中使用对称加密算法AES进行加解密,也可以使用其他对称加密算法保证加解密运行的高效性。

0x0. 参考

  • 《深入理解Java虚拟机》(第二版,周志明著,机械工业出版社)
  • 纯洁的微笑: [jvm系列(一):java类的加载机制](

0x0. 相关文章