返回
Featured image of post java 动态加载字节码

java 动态加载字节码

go!

Java 动态加载字节码

几种常见的动态加载字节码方式。

何为字节码?

狭义上 Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在 .class 文件中。

更广义一些:所有能够恢复成一个类并在 JVM 虚拟机里加载的字节序列都可成为字节码。

URLClassLoader

ClassLoader 是什么呢?它就是一个“加载器”,告诉 Java 虚拟机如何加载这个类。Java 默认的 ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime

URLClassLoader 实际上是平时默认使用的 AppClassLoader 的父类,所以,解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。

正常情况下,Java 会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

  • URL未以斜杠 / 结尾,则认为是一个 JAR 文件,使用 JarLoader 来寻找类,即为在 Jar 包中寻 找.class文件
  • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻 找.class文件
  • URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

也就是说,当使用 http 协议时会用到Loader来寻找类。

Demo:

package demo;

import java.net.URL;
import java.net.URLClassLoader;


public class TestClassLoader {
    public static void main(String[] args) throws Exception{
        URL[] urls = {new URL("http://localhost:8000/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class c = loader.loadClass("Hello");
        c.newInstance();

    }
}

Hello 类:

public class Hello {
    public Hello() {
        System.out.println("my test~");
    }
}

可看到成功请求 HTTP 服务器托管的 class 文件,并加载执行。

ClassLoader#defineClass

无论是加载远程 class 文件,还是本地 class 或 jar 文件,整个加载过程会经历下列三个方法调用:

  • loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机 制),在前面没有找到的情况下,执行 findClass
  • findClass 的作用是根据基础 URL 指定的方式来加载类的字节码,就比如会在本地文件系统、jar 包或远程 http 服务器上读取字节码,然后交给 defineClass
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类

综上,defineClass决定了如何将一段字节流转变成一个 Java 类,Java 默认的 ClassLoader#defineClass是一个 native 方法,逻辑在 JVM 的 C 语言代码中。

Demo:

package demo;

import java.lang.reflect.Method;
import java.util.Base64;


public class TestClassLoader {
    public static void main(String[] args) throws Exception{
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAHgoABgAQCQARABIIABMKABQAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAHTEhlbGxvOwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAHAAgHABgMABkAGgEACG15IHRlc3R+BwAbDAAcAB0BAAVIZWxsbwEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJp" +
                "bmc7KVYAIQAFAAYAAAAAAAEAAQAHAAgAAQAJAAAAPwACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAIACgAAAA4AAwAAAAIABAADAAwABAALAAAADAABAAAADQAMAA0AAAABAA4AAAACAA8=") ;
        Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
        hello.newInstance();

    }
}

需要注意的是:

  • defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造 函数,初始化代码才能被执行。而且,即使将初始化代码放在类的 static 块中,在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。
  • ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得不使用反射的形式来调用。
  • 在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是常用的一个攻击链 TemplatesImpl 的基石。

TemplatesImpl

虽然大部分上层开发者不会直接使用到 defineClass 方法,但是 Java 底层还是有一些类用到了它,这就是 TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类TransletClassLoader

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 方法,并且这里没有显式地声明其定义域。Java 中默认情况下,如果一个 方法没有显式声明作用域,其作用域为 default。所以也就是说这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。

现在追踪看下如何调用到 defineClass 方法。

先关注 TemplatesImpl 的 getOutputProperties 方法:

跟进 newTransformer()

发现在 new TransformerImpl对象时会进入到getTransletInstance()中,继续跟进:

getTransletInstance()中,如果在_name不等于 null 且_class等于 null 时会进入到defineTransletClasses()中。继续往下看,打断点发现_transletIndex为 0,也就是说会对_class数组中的第一个类进行实例化,并且会强制转换为AbstractTranslet

现在问题是_class数组是如何来的?跟进defineTransletClasses()方法:

发现是通过循环遍历 _bytecodes数组并调用defineClass 去加载,最终生成_class数组。需要注意的是:_tfactory不为 null,并且因为加载完类后会强制类型转换为AbstractTranslet,也就是说加载的类必须为AbstractTranslet的子类。

综上整个调用用链如下:

TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()

构造需要注意:

  • _bytecodes[]中加载的类需为AbstractTranslet的子类
  • _name不为 null
  • _tfactory不为 null

Demo:

package demo;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;


public class TestClassLoader {
    public static void main(String[] args) throws Exception{
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQALAoABgAdCQAeAB8IACAKACEAIgcAIwcAJAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi" +
                "9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQATTFRlc3RUZW1wbGF0ZXNJbXBsOwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAlAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYBAApTb3VyY2VGaWxlAQAWVGVzdFRlbXBsYXRlc0ltcGwuamF2YQwAGQAaBwAmDAAnACgBABZteSB0ZXN0IFRlbXBsYXRlc0ltcGx+BwApDAAqACsBABFUZXN0VGVtcGxhdGVzSW1wbAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lz" +
                "dGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAPwAAAAMAAAABsQAAAAIACgAAAAYAAQAAAAkACwAAACAAAwAAAAEADAANAAAAAAABAA4ADwABAAAAAQAQABEAAgASAAAABAABABMAAQAHABQAAgAJAAAASQAAAAQAAAABsQAAAAIACgAAAAYAAQAAAAoACwAAACoABAAAAAEADAANAAAAAAABAA4ADwABAAAAAQAVABYAAgAAAAEAFwAYAAMAEgAAAAQAAQATAAEAGQAaAAEACQAAAD8AAgABAAAADSq" +
                "3AAGyAAISA7YABLEAAAACAAoAAAAOAAMAAAAMAAQADQAMAA4ACwAAAAwAAQAAAA0ADAANAAAAAQAbAAAAAgAc");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "TestTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        obj.newTransformer();
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

构造要加载的特殊类:

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 TestTemplatesImpl extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
    public TestTemplatesImpl() {
        super();
        System.out.println("my test TemplatesImpl~");
    }
}

结果:

BCEL ClassLoader

BCEL 的全名应该是 Apache Commons BCEL,属于 Apache Commons 项目下的一个子项目。BCEL 库提供了一系列用于分析、创建、修改 Java Class 文件的 API。

与 Commons Collections 相比,其功能不及 CC,但特殊的一点是,它被包含在了原生的 JDK 中com.sun.org.apache.bcel

BCEL 这个包中有个类:com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()方法。

ClassLoader#loadClass()中,其会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行 decode。具体的算法这里就不详述,总之就是加载 BCEL 编码的"字节码"得到相应的类。

BCEL 提供了两个类 Repository 和 Utility:

  • Repository 用于将一个Java Class 先转换成原生字节码,当然也可以直接使用 javac 命令来编译 java 文件生成字节码
  • Utility 用于将原生的字节码转换成BCEL格式的字节码

Demo:

package demo;

import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.util.ClassLoder


public class TestBCEL {
    public static void main(String []args) throws Exception {
        JavaClass cls = Repository.lookupClass(demo.Evil.class);
        String code = Utility.encode(cls.getBytes(), true);
        System.out.println(code);
        new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
    }

}

Evil:

package demo;

public class Evil {
    static {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (Exception e) {}
    }
}

BCEL ClassLoader 在 Fastjson 等漏洞的利用链构造时都有被用到,其实这个类和前面的 TemplatesImpl 都出自于同一个第三方库,Apache Xalan。但在 Java 8u251 的更新中,这个 ClassLoader 被移除。

参考

参考自 phith0n 的文章。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy