nnonkey k1n9的博客

当你为错过太阳而哭泣时,你也要再错过群星了——泰戈尔​

java安全之动态字节码加载基础

前言

最近也是在学java的基础语法,然后去编写了一些代码,把基础语法大概掌握了%40了吧,虽然记不住,敲多了应该没有问题,然后也是准备学习java安全的基础了,然后java安全入门根据https://cn-sec.com/archives/1505525.html链接有,一个一个学就ok了,千万要把基础学好,然后理解到,牢记基础是神
md就是不喜欢打ctf,不喜欢坐牢,md,该提升下自己的耐心了,写这篇文章都要写吐了,虽然很多图片是抄的,内容抄的,还是自己打一遍加深印象,不如前几天剪视频有意思

正文

静态和动态加载

(1)静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
2024-02-29T12:50:07.png
上面这段代码里,使用new的方式创建对象且没有定义Dog类,使用javac进行编译时,由于找不到Dog类,所以会报错
2024-02-29T12:50:33.png
(2)动态加载:运行时加载需要的类,如果运行时不用该类,即使不存在该类,则不报错,降低了依赖性。
2024-02-29T12:50:56.png
使用了反射机制来创建对象且没有定义Dog类,javac编译通过且运行时只有当需要Dog类时才会报错:
2024-02-29T12:51:10.png

类与字节码

我们知道java能够运行是Java会先通过编译器将源代码转换为Java二进制代码(字节码),并将其保存在文件中(通常是.class文件),之后通过Java虚拟机(JVM)的解释器来执行这段代码。

动态字节码加载就是动态类加载,因为在 JVM 解释执行的过程中,类加载器ClassLoader会将Java字节码中的Class加载到内存中,将字节码转换成JAVA类。只要编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行。
2024-02-29T12:54:19.png

类加载与双亲委派

类加载的加载阶段任务是“负责将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成。”

常见类加载器
JDK中内置的ClassLoader常见的有:BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLClassLoader。
ClassLoader是一个抽象类

除了启动类加载器BootstrapClassLoader是用C/C++实现的,是JVM虚拟机自身的一部分之外。其它的所有类加载器都是由Java实现,独立于虚拟机,并且全部继承自抽象类java.lang.ClassLoader。URLClassLoader是ExtensionClassLoader、AppClassLoader的父类。类图关系如下:

2024-02-29T12:59:03.png
三大类加载器的区别

BootstrapClassLoader:即Bootstrap ClassLoader,启动类加载器/根加载器。负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。由于是C/C++代码实现的,故启动类加载器无法被 Java 程序直接引用。

ExtClassLoader:即Extension ClassLoader,扩展类加载器,位于sun.misc.Launcher$ExtClassLoader:

2024-02-29T13:01:39.png
负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,库名通常以 javax 开头,开发者可以直接使用扩展类加载器。

AppClassLoader:即Application ClassLoader,应用程序类加载器,位于sun.misc.Launcher$AppClassLoader:

负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由BootstrapClassLoader、ExtClassLoader、AppClassLoader这三种类加载器互相配合从而实现类加载,如果有必要,还可以加入自己定义的类加载器。那么各个不同类加载器之间是怎么配合的呢?这就要说到双亲委派模型了。

还可以加入自己定义的类加载器。那么各个不同类加载器之间是怎么配合的呢?这就要说到双亲委派模型了。

双亲委派模型

双亲委派模型是JAVA设计者推荐的类加载器工作方式。所谓双亲委派就是每一个ClassLoader类的实例都有一个父级ClassLoader,当一个类加载器需要加载类时,首先它会把这个请求委派给父级ClassLoader先进行加载,每一层都是如此,一直递归到顶层。如果父级ClassLoader无法加载再自行加载。

这里的父级不是父类,不是平常说的继承关系,而是调用逻辑。

JVM 内置的 BootstrapClassLoader 自身没有父级ClassLoader,而它可以作为其他ClassLoader实例的父级。双亲委派模型如下图所示:

2024-02-29T13:08:11.png
双亲委派使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一,也避免了多份同样字节码的加载。双亲委派模型保证了Java程序的稳定运作

双亲委派模型的代码实现

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) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                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;
        }
    }
synchronized (getClassLoadingLock(name)):通过 synchronized 块获取一个类加载锁,以确保在多线程环境下对类加载操作的安全性。

Class<?> c = findLoadedClass(name):查看是否已经加载了指定名称的类,如果已经加载过则直接返回该类的 Class 对象。

if (c == null):如果还没有加载指定名称的类,则执行以下操作:

尝试从父类加载器(parent)中加载该类,如果父类加载器存在且成功加载,则将加载的 Class 对象赋给变量 c。

如果父类加载器无法加载指定类,则尝试从引导类加载器(bootstrap class loader)中加载该类。

如果仍然无法找到该类,则调用 findClass 方法来查找并加载该类。

在加载类的过程中,会记录一些性能统计信息,比如父类委托耗时、findClass 方法耗时以及加载类的次数等。

if (resolve):如果参数 resolve 为 true,表示需要解析类,则调用 resolveClass 方法来解析该类。

最后返回加载的 Class 对象。

概括就是
2024-02-29T13:25:13.png

(1)首先从底向上的检查类是否已经加载过,就是这段代码:

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) {
       // ClassNotFoundException thrown if class not found
       // from the non-null parent class loader
  }
(2)如果都没有加载过的话,那么就自顶向下的尝试加载该类。就是这段代码:
if (c == null) {
   // If still not found, then invoke findClass in order
   // to find the class.
   long t1 = System.nanoTime();
   c = findClass(name);
  ...
}

常见动态加载字节码的方法

上面的都是基础知识,下面进入正题

Class.forName()

说到动态类加载的方法,第一想到的肯定是Class.forName()。

Class.forName ("类的全限定路径")会调用静态代码块,因为它默认初始化。

若修改为Class.forName ("类的全限定路径",false,classLoader)则不进行初始化,不调用静态代码块。

2024-02-29T13:31:40.png
执行第一句弹计算器,执行第二句不弹计算器,第二句后添加evil2.newInstance()会弹计算器。因为第二次根本就没有把下面的类进行初始化,jvm无法运行

URLClassLoader加载字节码

JVM在加载阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象

JAVA提供了URLClassLoader这样一个类加载器,可以从本地文件,jar包中加载类,也可以从远程服务器上加载类,取决于URL的地址形式

URL以!/结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在Jar包中寻找.class文件。

URL以斜杠/结尾,且协议名是file ,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件

URL以斜杠/结尾,且协议名不是file,则使用最基础的Loader来寻找类,最常见的就是http协议
代码实现
2024-02-29T13:38:16.png
如果我们能够控制目标Java ClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。

这是因为 Java 的 ClassLoader 具有动态加载类的特性,可以从多种来源加载类文件,包括本地文件系统、网络等。如果能够控制目标 Java ClassLoader 的基础路径为一个 HTTP 服务器,那么就可以利用远程加载的方式来执行任意代码。

当 Java 程序尝试加载类时,ClassLoader 会首先查找指定名称的类是否已经加载过,如果没有则会尝试从其基础路径加载类文件。如果基础路径指向一个 HTTP 服务器,那么恶意攻击者可以将恶意的类文件上传到该 HTTP 服务器上,并构造恶意的类名让目标 Java 程序尝试加载。当目标程序尝试加载这个类时,ClassLoader 会从 HTTP 服务器下载并加载这个恶意的类文件,从而执行其中的恶意代码。

loadClass(),defineClass()

ClassLoader 中有几个重要的方法:loadClass()、findClass()、defineClass()。不管是调用哪个类加载器,都会调用到这三个方法。

loadClass(String classname),它完成整个类加载的过程,先从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,调用 findClass()。

findClass(),根据指定的方式来加载类的字节码,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass()。

defineClass(),处理前面传入的字节码,将其处理成真正的Java类。

我们要学会使用loadClass()、defineClass()加载字节码

我举一个例子

import java.io.File;
import java.nio.file.Files;

public class DynamicClassLoaderExample {
    public static void main(String[] args) throws Exception {
        // 读取字节码文件
        File file = new File("HelloWorld.class");
        byte[] classBytes = Files.readAllBytes(file.toPath());

        // 创建自定义类加载器
        ClassLoader classLoader = new ClassLoader() {
            @Override//重写方法
            public Class<?> findClass(String name) throws ClassNotFoundException {
                return defineClass(name, classBytes, 0, classBytes.length);
            }
        };

        // 加载并实例化类
        Class<?> clazz = classLoader.loadClass("HelloWorld");
        Object instance = clazz.newInstance();
    }
}

defineClass(name, classBytes, 0, classBytes.length) 方法的各个参数含义如下:

name:要加载类的名称,这个名称必须与字节码文件中定义的类名一致。
classBytes:包含类字节码的字节数组。
0:字节数组中要加载的起始偏移量。在这里我们从数组的开头开始加载整个字节码,所以偏移量为0。
classBytes.length:要加载的字节数,即字节码数组的长度。加载整个字节码文件。

在这个例子我们就是使用自己定义的加载器去加载我们指定的class文件,defineclass的参数我们是可以自己按照自己的需求去调整的

我们思考一下为什么最后都要实例化一个对象?

如果我们了解一个类加载的过程

静态代码块是在类的初始化阶段执行的,而并非在类加载阶段。因此,在调用defineClass()方法时,即使字节码被加载为 Java 类,但并不会触发其中的静态代码块。 defineClass(),其作用只是来加载字节码,将字节码转化为Java类,不进行初始化。所以我们需要实例化,在调用 newInstance() 方法时,会触发类的初始化过程,包括执行静态代码块和静态变量的初始化。
这样就可以把静态代码加载了

一个文件->javac编译把文件转为字节码->生成class文件->实例化加载器去加载我们指定的类文件->调用加载器中defineclass把字节码加载为转换为Java 类的实例对象

当我们编写一个 Java 源文件后,使用 javac 编译器将其编译为对应的字节码文件(.class 文件)。

接着,我们可以实例化一个自定义的类加载器,并使用该加载器去加载指定的类文件。

在加载过程中,类加载器会调用 defineClass() 方法,将字节码数据转换为 Java 类的实例对象,这样我们就可以通过获取到的 Class 对象来实例化对象、调用方法等操作。
反正我是这样理解的

本原创文章未经允许不得转载 | 当前页面:nnonkey k1n9的博客 » java安全之动态字节码加载基础

评论 2

  1. 大佬你好厉害,能不能带带我

    Aiwin 2024-03-01    回复