freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java反序列化基础篇-类加载器
2022-06-08 14:38:37
所属地 香港

博客地址:https://drun1baby.github.io/

Java反序列化基础篇-05-类的动态加载

0x01 前言

这篇文章/笔记的话,打算从类加载器,双亲委派到代码块的加载顺序这样来讲。最后才是加载字节码。

0x02 类加载器及双亲委派

  • 说类加载器有些师傅可能没听过,但是说 Java ClassLoader,相信大家耳熟能详。

1. 类加载器有什么用

  • 加载 Class 文件

以这段简单代码为例

Student student = new Student();

我们知道,Student 本身其实是一个抽象类,是通过 new 这个操作,将其实例化的,类加载器做的便是这个工作。

ClassLoader 的工作如图所示
image

加载器也分多种加载器,每个加载器负责不同的功能。

主要分为这四种加载器

  1. 虚拟机自带的加载器

  2. 启动类(根)加载器

  3. 扩展类加载器

  4. 应用程序加载器

2. 几种加载器

引导类加载器

引导类加载器(BootstrapClassLoader),底层原生代码是 C++ 语言编写,属于 JVM 一部分。

不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为javajavaxsun等开头的类)。

扩展类加载器(ExtensionsClassLoader)

扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。

App类加载器(AppClassLoader)

App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。

3. 双亲委派机制

  • 在 Java 开发当中,双亲委派机制是从安全角度出发的。

我们这里以代码先来感受一下,双亲委派机制确实牛逼。

从报错的角度感受双亲委派机制

  • 尽量别尝试,看看就好了。要不然整个文件夹挺乱的,如果想上手尝试一下的话,我建议是新建一个项目,不要把其他的文件放一起。

新建一个 java.lang的文件夹,在其中新建 String.java的文件。

String.java

package java.lang;

// 双亲委派的错误代码
public class String {

    public String toString(){
        return "hello";
 }

    public static void main(String[] args) {
        String s = new String();
 s.toString();
 }
}

看着是不是没有问题,没有错误吧?
我们自己定义了一个java.lang的文件夹,并在文件夹中定义了 String.class,还定义了 String 这个类的 toString 方法。我们跑一下程序。(这里如果把 Stirng 类放到其他文件夹会直接报错,原因也是和下面一样的)

  • 结果居然报错了!而且非常离谱

image

我这不是已经定义了 main 方法吗??为什么还会报错,这里就提到双亲委派机制了,双亲委派机制是从安全角度出发的。

首先,我们要知道 Java 的类加载器是分很多层的,如图。

image

我们的类加载器在被调用时,也就是在 new class 的时候,它是以这么一个顺序去找的 BOOT ---> EXC ----> APP

如果 BOOT 当中没有,就去 EXC 里面找,如果 EXC 里面没有,就去 APP 里面找。

  • 所以我们之前报错的程序当中,定义的java.lang.String在 BOOT 当中是有的,所以我们自定义 String 时,会报错,如果要修改的话,是需要去 rt.jar 里面修改的,这里就不展开了。

从正确的角度感受双亲委派机制

前文提到我们新建的java.lang.String报错了,是因为我们定义的 String 和 BOOT 包下面的 String 冲突了,所以才会报错,我们这里定义一个 BOOT 和 EXC 都没有的对象试一试。

在其他的文件夹下,新建 Student.java

Student.java

package src.DynamicClassLoader;

// 双亲委派的正确代码
public class Student {

    public String toString(){
        return "Hello";
 }

    public static void main(String[] args) {
        Student student = new Student();

 System.out.println(student.getClass().getClassLoader());
 System.out.println(student.toString());
 }
}

并把加载器打印出来

image
我们定义的 Student 类在 APP 加载器中找到了。

0x03 各场景下代码块加载顺序

  • 这里的代码块主要指的是这四种

    • 静态代码块:static{}

    • 构造代码块:{}

    • 无参构造器:ClassName()

    • 有参构造器:ClassName(String name)

场景一、实例化对象

这里有两个文件,分别介绍一下用途:

  • Person.java:一个普普通通的类,里面有静态代码块、构造代码块、无参构造器、有参构造器、静态成员变量、普通成员变量、静态方法。

  • Main.java:启动类

Person.java

package src.DynamicClassLoader;

// 存放代码块
public class Person {
    public static int staticVar;
 public int instanceVar;

 static {
        System.out.println("静态代码块");
 }

    {
        System.out.println("构造代码块");
 }

    Person(){
        System.out.println("无参构造器");
 }
    Person(int instanceVar){
        System.out.println("有参构造器");
 }

    public static void staticAction(){
        System.out.println("静态方法");
 }
}

Main.java

package src.DynamicClassLoader;

// 代码块的启动器
public class Main {
    public static void main(String[] args) {
        Person person = new Person();
 }
}

运行结果如图

image

  • 结论:

通过new关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同,调用不同的构造器。

场景二、调用静态方法

直接调用类的静态方法

Person.java 不变,修改 Main.java 启动器即可。

Main.java

package src.DynamicClassLoader;

// 代码块的启动器
public class Main {
    public static void main(String[] args) {
        Person.staticAction();
 }
}

image

  • 结论:

不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法

场景三、对类中的静态成员变量赋值

Main.java

package src.DynamicClassLoader;

// 代码块的启动器
public class Main {
    public static void main(String[] args) {
 		Person.staticVar = 1;
 	}
}

image

  • 结论:

在对静态成员变量赋值前,会调用静态代码块

场景四、使用 class 获取类

package src.DynamicClassLoader;

// 代码块的启动器
public class Main {
    public static void main(String[] args) {
 		Class c = Person.class;
 	}
}

// 空屁
  • 结论:

利用class关键字获取类,并不会加载类,也就是什么也不会输出。

场景五、使用 forName 获取类

  • 这里要抛出异常一下。

我们写三种forName的方法调用。
修改 Main.java

package src.DynamicClassLoader;

// 代码块的启动器
public class Main {
    public static void main(String[] args) throws ClassNotFoundException{
 		Class.forName("src.DynamicClassLoader.Person");
 	}
}
// 静态代码块
package src.DynamicClassLoader;

// 代码块的启动器
public class Main {
    public static void main(String[] args) throws ClassNotFoundException{
 	Class.forName("src.DynamicClassLoader.Person", true, ClassLoader.getSystemClassLoader());
 }
}
// 静态代码块
package src.DynamicClassLoader;

// 代码块的启动器
public class Main {
    public static void main(String[] args) throws ClassNotFoundException{
 	Class.forName("src.DynamicClassLoader.Person", false, ClassLoader.getSystemClassLoader());
 }
}
//没有输出
  • 结论:

Class.forName(className)Class.forName(className, true, ClassLoader.getSystemClassLoader())等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为false,那么就不会调用静态代码块

场景六、使用 ClassLoader.loadClass() 获取类

Main.java

package com.xiinnn.i.test;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.xiinnn.i.test.Person", false, ClassLoader.getSystemClassLoader());
    }
}
//没有输出
  • 结论:

ClassLoader.loadClass()方法不会进行类的初始化,当然,如果后面再使用newInstance()进行初始化,那么会和场景一、实例化对象一样的顺序加载对应的代码块。

# web安全 # Java代码审计 # JAVA安全 # Java类加载器 # 类的动态加载
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录