java_web学习之路(二):JavaByteCode从烧0到1


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

    }
}

可以看到运行之后成功触发并弹出了了计算器

image-20220324195124837

注意一点,在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造 函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中(在本系列文章第一篇 中进行过说明),在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目 标机器上执行任意代码,需要想办法调用构造函数。

TemplatesImpl加载字节码

我们上面也说过,defineClass方法是一个native方法,由JVM的底层c语言实现,所以一般上层开发者没人会闲的无聊使用他,但是TemplatesImpl类中有使用到defineClass方法,成为我们重视的对象。

我们去TemplatesImpl类中搜寻defineClass方法,可以看到在一个内部类TransletClassLoader中重写了defineClass方法。

接着我们去寻找调用,在defineTransletClasses方法中我们看到这里调用了defineClass。

image-20220324202449534

我们往上走,找可以利用的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();
        }
    }
}

文章作者: m1yuu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 m1yuu !
  目录