前言
最近也是在学java的基础语法,然后去编写了一些代码,把基础语法大概掌握了%40了吧,虽然记不住,敲多了应该没有问题,然后也是准备学习java安全的基础了,然后java安全入门根据https://cn-sec.com/archives/1505525.html链接有,一个一个学就ok了,千万要把基础学好,然后理解到,牢记基础是神
md就是不喜欢打ctf,不喜欢坐牢,md,该提升下自己的耐心了,写这篇文章都要写吐了,虽然很多图片是抄的,内容抄的,还是自己打一遍加深印象,不如前几天剪视频有意思
正文
静态和动态加载
(1)静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
上面这段代码里,使用new的方式创建对象且没有定义Dog类,使用javac进行编译时,由于找不到Dog类,所以会报错
(2)动态加载:运行时加载需要的类,如果运行时不用该类,即使不存在该类,则不报错,降低了依赖性。
使用了反射机制来创建对象且没有定义Dog类,javac编译通过且运行时只有当需要Dog类时才会报错:
类与字节码
我们知道java能够运行是Java会先通过编译器将源代码转换为Java二进制代码(字节码),并将其保存在文件中(通常是.class文件),之后通过Java虚拟机(JVM)的解释器来执行这段代码。
动态字节码加载就是动态类加载,因为在 JVM 解释执行的过程中,类加载器ClassLoader会将Java字节码中的Class加载到内存中,将字节码转换成JAVA类。只要编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行。
类加载与双亲委派
类加载的加载阶段任务是“负责将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成。”
常见类加载器
JDK中内置的ClassLoader常见的有:BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLClassLoader。
ClassLoader是一个抽象类
除了启动类加载器BootstrapClassLoader是用C/C++实现的,是JVM虚拟机自身的一部分之外。其它的所有类加载器都是由Java实现,独立于虚拟机,并且全部继承自抽象类java.lang.ClassLoader。URLClassLoader是ExtensionClassLoader、AppClassLoader的父类。类图关系如下:
三大类加载器的区别
BootstrapClassLoader:即Bootstrap ClassLoader,启动类加载器/根加载器。负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。由于是C/C++代码实现的,故启动类加载器无法被 Java 程序直接引用。
ExtClassLoader:即Extension ClassLoader,扩展类加载器,位于sun.misc.Launcher$ExtClassLoader:
负责将 <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实例的父级。双亲委派模型如下图所示:
双亲委派使得 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 对象。
概括就是
(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)则不进行初始化,不调用静态代码块。
执行第一句弹计算器,执行第二句不弹计算器,第二句后添加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协议
代码实现
如果我们能够控制目标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 对象来实例化对象、调用方法等操作。
反正我是这样理解的
大佬你好厉害,能不能带带我
师傅佬