『Java安全』初试字节码框架ASM_使用asm修改class文件

ASM介绍

asm4-guide.pdf

ASM是操纵分析字节码的一个框架,ASM库的目标是生成、转换和分析编译的
的Java类,以字节数组的形式表示(因为它们存储在磁盘上并加载到
在Java虚拟机中加载)。为此目的,ASM提供了一些工具来读、写和转换这些字节数组。
为此,ASM提供了一些工具,通过使用比字节更高层次的概念来读取、写入和转换这些字节数组。

jdk内置了一个asm,如果没有去maven库下载,本文操作采用6.2版本

        <!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>6.2</version>
        </dependency>

asm库有三个重要的核心方法:

  • ClassReader:解析class字节码
  • ClassWriter:构建class类
  • ClassVisitor:抽象类,用来定义实现要对class进行的操作

用asm可以很方便的对一个class文件完成改造,也可以生成一个新的class

先写一个Test.class用作例子
在这里插入图片描述

ClassReader 与 ClassVisitor

代码实现

ClassReader类用来解析字节码,支持传入类名、class字节码流

package asm;

import org.objectweb.asm.*;
import java.io.FileInputStream;

public class Reader {
    public static void main(String[] args) throws Exception {
        FileInputStream fis = new FileInputStream("src\\main\\java\\asm\\Test.class");
        ClassReader cr = new ClassReader(fis);
        System.out.println("ClassName:" + cr.getClassName() + "\nSuperName:" + cr.getSuperName() + "\nInterfaces:" + cr.getInterfaces());
    }
}

创建好ClassReader后会自动解析class
在这里插入图片描述
通过ClassReader只能获取简单的class文件信息,要获取变量、方法就要用到accept方法
在这里插入图片描述
而accept方法需要传入ClassVisitor类,这是一个抽象类,我们可以在里面定义要访问获取哪些内容:
在这里插入图片描述
写一个子类继承它,实现功能。

构造器直接调用ClassVisitor的,这里要传入一个api编号,由于我们下载的版本号是ASM6,因此就填入ASM6版本的api编号,在Opcodes操作码中已经有存储为Opcode.ASMx`。
在这里插入图片描述
接着要重写方法,ClassReader.accept调用时会按以下顺序执行这些方法,如同名字所示这些方法是用来获取对应内容的,如果要获取方法就在ClassVisitor重写visitMethod方法,然后获取name参数即可
在这里插入图片描述

读取类信息、变量、方法的例子

编写一个ClassInfoPrinter.java获取类的信息

package asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class ClassInfoPrinter extends ClassVisitor {
    public ClassInfoPrinter() {
        super(Opcodes.ASM6);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println("Version:" + version + " Access:" + access + " Name:" + name + " SuperName:" + superName + " Interfaces"+ interfaces);
        System.out.println();
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        System.out.println("Field Access: " + access  + " Name:" +  name + " Descriptor:"  + descriptor + " Value:" + value);
        return null;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("Method Access: " + access  + " Name:" +  name + " Descriptor:"  + descriptor);
        return null;
    }
}

调用ClassReader.accept即可,第二个参数为0,参数主要是跳过解析某些部分
在这里插入图片描述
观察输出,我们要的信息全部输出了
在这里插入图片描述

读取方法中变量的例子

AdviceAdapter已经不用了

  • 先写一个ClassVisitor的子类、重写visitMethod拿到方法数组
  • 再写一个MethodVisitor的子类、重写visitLocalVariable方法,然后在里面就能获取

MethodInfoPrinter.java

package asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class MethodInfoPrinter extends ClassVisitor {
    public MethodInfoPrinter() {
        super(Opcodes.ASM6);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if(name.equals("main")){
            System.out.println("Method: " + name);
            return new MyMethodVisitor();
        }
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }
}

// 重写MethodVisitor,待上面return的时候调用
class MyMethodVisitor extends MethodVisitor{
    public MyMethodVisitor() {
        super(Opcodes.ASM6);
    }

    @Override
    public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {
        System.out.println(name + " " + descriptor + " " +index);
    }
}

ReadMethod.java

package asm;

import org.objectweb.asm.ClassReader;
import java.io.FileInputStream;

public class ReadMethod {
    public static void main(String[] args) throws Exception{
        FileInputStream fis = new FileInputStream("src\\main\\java\\asm\\Test.class");
        ClassReader cr = new ClassReader(fis);
        System.out.println("ClassName:" + cr.getClassName() + "\nSuperName:" + cr.getSuperName() + "\nInterfaces:" + cr.getInterfaces());
        System.out.println();
        cr.accept(new MethodInfoPrinterBy(), 0);
    }
}

在这里插入图片描述
与javap效果一致
在这里插入图片描述

ClassReader解析原理

利用ASM读取class

对于ClassReader的构造器:传入文件输入流那么就读取解析、传入类名就先用类加载器加载该类然后再分别读取解析
在这里插入图片描述
在这里插入图片描述

对于ClassReader的accept方法:通过位移量定位然后解析
在这里插入图片描述

功能总结

ClassReader读取二进制类数据,ClassVisitor配置获取的属性,ClassReader.accept通过属性对类数据执行操作。

我们想要完成的操作统统定义在ClassVisitor里面

ClassWriter

代码实现

ClassWriter这个类是ClassVisitor的子类,用来直接在二进制层面修改或生成class文件
在这里插入图片描述
它的构造器有两个,传入一个int配置选项,ClassReader是可选的参数。两个构造器对应了:在原class文件修改或直接新建class文件在这里插入图片描述
int配置参数一般设置为CLASSWRITER.COMPUTE_FRAMES,因为修改class文件可能会导致stack和local参数改变导致jvm无法识别,配置该参数会自动计算

在这里插入图片描述

之前注意到:ClassVisitor有两个构造器(其实第一个是特殊情况)
在这里插入图片描述
如果要进行修改,第二个参数ClassVisitor就设置为ClassWriter用于存储,然后具体修改的值在新建的ClassVisitor用return返回
在这里插入图片描述
然后设置accept的选项为扩大frame在这里插入图片描述
最后将ClassWriter转化成byteArray写入文件即可
在这里插入图片描述

修改类信息、变量、方法的例子

将Test.class的类变量hello==1修改为hi==10

首先重写一个ClassVisitor,查找hello变量

ClassInfoModifier.java

package asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;

public class ClassInfoModifier extends ClassVisitor {
    public ClassInfoModifier(ClassWriter cw) {
        super(Opcodes.ASM6, cw);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        if(name.equals("hello")){
            return super.visitField(access, "hi", descriptor, signature, 10);
        }
        return super.visitField(access, name, descriptor, signature, value);
    }
}

然后编写主方法Write.java

package asm;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class Writer {
    public static void main(String[] args) throws Exception {
        FileInputStream fis = new FileInputStream("src\\main\\java\\asm\\Test.class");
        ClassReader cr = new ClassReader(fis);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
        cr.accept(new ClassInfoModifier(cw), ClassReader.EXPAND_FRAMES);
        FileOutputStream fos = new FileOutputStream(new File("src\\main\\java\\asm\\newTest.class"));
        fos.write(cw.toByteArray());

    }
}

生成了newTest.class,反编译发现已经修改成功!

在这里插入图片描述

方法中添加java代码

这部分参考其他师傅的笔记:

关于java字节码框架ASM的学习
ASM库的介绍和使用

ASM写HelloWorld

Java用ASM写一个HelloWorld程序

package asm;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.FileOutputStream;

public class WriteHelloWorld {
    public static void main(String[] args) throws Exception {
        ClassWriter cw = new ClassWriter(0);
        // 编辑class信息
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "HelloWorld", null, "java/lang/Object", null);
        // 编辑主方法
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        // 获取静态属性,该属性类型是PrintStream
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        // 加载字符串常量
        mv.visitLdcInsn("HelloWorld!");
        // 调用静态属性System.out的print方法
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        // 返回
        mv.visitInsn(Opcodes.RETURN);
        // 设置栈大小、变量表大小(stack=2,locals=1)
        mv.visitMaxs(2, 1);
        // 结束方法和类
        mv.visitEnd();
        cw.visitEnd();
        // 保存
        FileOutputStream fos = new FileOutputStream("src\\main\\java\\asm\\HelloWorld.class");
        fos.write(cw.toByteArray());
    }
}

之后java -classpath class文件目录 HelloWorld即可运行
在这里插入图片描述

功能总结

修改class文件较复杂,大致分为以下步骤:

  1. 新建ClassReader解析class
  2. 新建ClassWriter,传入ClassReader并且参数设置为ClassWriter.COMPUTE_FRAMES
  3. 重写一个ClassVisitor,要修改的值通过return super返回
  4. 调用ClassReader的accept方法,传入重写的ClassVisitor并且选项设置为ClassReader.EXPAND_FRAMES
  5. 新建FileOutputStream文件输出流
  6. ClassWriter.toByteArray写入文件

后记

找到一位师傅写的一系列asm笔记,可以学习一下

https://www.jianshu.com/p/d713f1722b37

欢迎关注我的CSDN :@Ho1aAs
版权属于:Ho1aAs
本文链接:https://blog.csdn.net/Xxy605/article/details/123132137
版权声明:本文为原创,转载时须注明出处及本声明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值