什么是Java字节码? Java作为一门跨平台开发语言。其跨平台的特性与自身独特的JVM(Java Virtual Machine)机制相关。而字节码,便是一种可以被Java虚拟器加载和执行的指令。
使用javac命令可以将.java文件编译为字节码文件,以.class做后缀。
除了Java之外,Scala、Groovyc、Kotlin等语言均可被编译器编译为字节码文件(.class),从而被JVM识别执行。
类加载机制 字节码文件经过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 { public String classPath; public ClassLoaderTest (String classPath) { this .classPath=classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte [] classByte=loadClassData(name); return defineClass(name,classByte,0 , classByte.length); } 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
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(); } }
运行后发现成功打印字符串
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 ; if (_loadedExternalExtensionFunctions != null ) { ret = _loadedExternalExtensionFunctions.get(name); } if (ret == null ) { ret = super .loadClass(name); } return ret; } Class defineClass (final byte [] b) { return defineClass(null , b, 0 , b.length); } }
可以看到TransletClassLoader
对defineClass()
进行了重写。并且此处没有使用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 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); }
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 ; if ((cl=(Class)classes.get(class_name)) == null ) { 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 ; if (class_name.indexOf("$$BCEL$$" ) >= 0 ) clazz = createClass(class_name); else { 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 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 ; if (class_name.indexOf("$$BCEL$$" ) >= 0 ) clazz = createClass(class_name); else { 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 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(); } }