Java字节码的动态加载
yearnxyl Lv2

什么是Java字节码?

Java作为一门跨平台开发语言。其跨平台的特性与自身独特的JVM(Java Virtual Machine)机制相关。而字节码,便是一种可以被Java虚拟器加载和执行的指令。

使用javac命令可以将.java文件编译为字节码文件,以.class做后缀。

除了Java之外,Scala、Groovyc、Kotlin等语言均可被编译器编译为字节码文件(.class),从而被JVM识别执行。

image

类加载机制

字节码文件经过JVM中ClassLoader的加载,在内存中生成Class类对象

ClassLoader就是一个“加载器”,告诉Java虚拟机如何加载这个类。

在Java中进行类加载时会调用三个方法:ClassLoader#loadClass()ClassLoader#findClass()ClassLoader#defineClass()

  • loadClass:从已加载的类缓存、父加载器等位置寻找类(采用双亲委派机制)
  • findClass:根据名称或位置加载.class字节码
  • defineClass:把字节码转换成Class类对象

详细的类加载机制和双亲委派可参考:https://www.cnblogs.com/hollischuang/p/14260801.html

ClassLoader加载字节码文件

这里我们尝试创建一个自定义ClassLoader来加载本地的字节码文件:

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
package org.example;

import java.io.FileInputStream;
import java.io.IOException;

public class ClassLoaderTest extends ClassLoader{ //继承ClassLoader
public String classPath;
public ClassLoaderTest (String classPath){
this.classPath=classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classByte=loadClassData(name);
//将字节码交给defineClass()方法,在内存中创建CLass类对象
return defineClass(name,classByte,0, classByte.length);
}
//loadClassData用来读取字节码文件
private byte[] loadClassData(String className){
String classFilePath=classPath+className.replace(".","/")+".class";
try(FileInputStream fis=new FileInputStream(classFilePath)){
byte[] buffer=new byte[fis.available()];
fis.read(buffer);
return buffer;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoaderTest classLoaderTest=new ClassLoaderTest("./Serialization/src/main/java/");
Class cls=classLoaderTest.findClass("org.example.Hello");
cls.newInstance();
}
}

执行代码,发现成功加载Hello.class

image

ClassLoader#define()直接加载字节码

从上面的自定义ClassLoader代码中可以看到,findClass()将字节码文件以byte的形式读取。之后交给defineClass()处理生成Class类。

我们可以尝试跳过findClass()方法,直接将字节码给defineClass()

首先编写Hello.java,并将其编译为Hello.class

1
2
3
4
5
6
7
package org.example;

public class Hello {
public Hello(){
System.out.println("Hello World");
}
}

因为字节码是byte形式的,为了方便我们这里将其读取为base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;

import java.io.*;
import java.util.Base64;

public class test {
public static void main(String[] args) throws Exception {
try (FileInputStream fis = new FileInputStream("./Serialization/src/main/java/org/example/Hello.class")) {
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
String bytes2base64 = Base64.getEncoder().encodeToString(buffer);
System.out.println(bytes2base64);
}

}
}

将读取到的base64字符串进行解码,并传递给ClassLoader#defineClass处理。

这里有个需要注意的点:defineClass()方法的第一个参数应当为类的全称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

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

public class TmpClassLoader {
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[] bytes= Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQARb3JnL2V4YW1wbGUvSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAAEAAQABQAMAAYAAQALAAAAAgAM");
Class Hello= (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(),"org.example.Hello",bytes,0,bytes.length);
Hello.newInstance();
}
}

运行后发现成功打印字符串

image

TemplatesImpl类加载字节码

PS:有一个巨巨巨坑的点,环境要是JDK8。JDK9之后采取模块化设计,某些包可能被模块限制在某个范围内,导致不能直接导入这些包中的类

ClassLoader#defineClass()是protected修饰,无法直接在外部进行利用。因此,这里引入一个新的类:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

TemplatesImpl中存在内部类TransletClassLoader

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
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);
}
}

可以看到TransletClassLoaderdefineClass()进行了重写。并且此处没有使用protected修饰符。

TemplatesImpl中存在如下利用链

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

其中getOutputProperties()newTransformer()方法均为public修饰。因此可以调用这两个方法,来加载字节码

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
package org.example;

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

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

public class TemplatesImplStu {
static void SetFieldValue(String FieldName,Object object,Object value)throws Exception{
Field field=object.getClass().getDeclaredField(FieldName);
field.setAccessible(true);
field.set(object,value);
}
public static void main(String[] args)throws Exception {

TemplatesImpl templates=new TemplatesImpl();
byte[] bytes= Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABVUZW1wbGF0ZXNJbXBsQ2xzLmphdmEMAA4ADwcAGwwAHAAdAQAMSGVsbG8gV29ybGQhBwAeDAAfACABABBUZW1wbGF0ZXNJbXBsQ2xzAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAwABAAcACAACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAADAALAAAABAABAAwAAQAHAA0AAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABEACwAAAAQAAQAMAAEADgAPAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAATAAQAFAAMABUAAQAQAAAAAgAR");
SetFieldValue("_name",templates,"name");
SetFieldValue("_bytecodes",templates,new byte[][]{bytes});
SetFieldValue("_tfactory",templates,new TransformerFactoryImpl());
templates.newTransformer();
}

}

代码中的三个SetFieldValue()均是为了成功的走通整条链而设置的内部成员变量。比较简单,就不分析了。

需要注意的是TemplatesImpl对加载的字节码有要求。字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类。

TemplatesImpl 类是Xalan中的一个类,用于表示编译后的 XSLT 样式表。
AbstractTranslet 类是Xalan 中的一个基类,它为XSLT样式表的转换提供了基本的支持。
TemplatesImpl 作为编译后的样式表,需要能够与 AbstractTranslet 类进行交互,以便在运行时执行 XSLT 转换。这就是为什么 TemplatesImpl 类对应的类必须是 AbstractTranslet 的子类。

因此需要构造一个特殊的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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 TemplatesImplCls extends AbstractTranslet {
public void transform(DOM var1, SerializationHandler[] var2) throws TransletException {
}

public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
}

public TemplatesImplCls() {
System.out.println("Hello World!");
}
}

这里需要注意的是:TemplatesImplCls类没有package。这是因为TemplatesImpl#TransletClassLoader#defineClass()中调用的defineClass()第一个参数是null。

1
2
3
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}

image

BCEL ClassLoader加载字节码

PS:BCEL ClassLoader在jdk1.8.0_251被移除

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

在BCEL中存在一个ClassLoader:com.sun.org.apache.bcel.internal.util.ClassLoader ,其重写了loadClass()方法

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
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;
}

简单来分析一下

首先判断class_name中是否有$$BCEL$$,若存在则执行createClass(),若不存在则使用类加载器直接生成JavaClass类。(JavaClass为BCEL中的自定义类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}

createClass()会将$$BCEL$$后的内容解码,并生成JavaClass类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

最后JavaClass经过处理,变为Java原生Class类。

我们尝试将之前的Hello.class读取为BCEL字节码并加载

这里用到了两个类:Repository和Utility。Repository可以将字节码转换为JavaClass类对象。Utility可以将JavaClass类对象转换为BCEL字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;

import com.sun.org.apache.bcel.internal.Repository;
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.util.ClassLoader;

public class BcelStu {
public static void main(String[] args) throws Exception{
//类加载器寻找要编码的类
JavaClass jcls= Repository.lookupClass(Hello.class);
//对类进行编码,第二个参数表示是否在编码时进行压缩
String code= "$$BCEL$$"+Utility.encode(jcls.getBytes(),true);
System.out.println(code);
Object obj=new ClassLoader().loadClass(code).newInstance();
}
}

image