您的位置:首页
ASM(四) 利用Method 组件动态注入方法逻辑
2017-07-03 18:31
239 查看
这篇继续结合样例来深入了解下Method组件动态变更方法字节码的实现。通过前面一篇,知道ClassVisitor 的visitMethod()方法能够返回一个MethodVisitor的实例。
那么我们也基本能够知道,同ClassVisitor改变类成员一样,MethodVIsistor假设须要改变方法成员,注入逻辑,也能够通过继承MethodVisitor,来编写一个MethodXXXAdapter来实现对于方法逻辑的注入。通过以下的两个样例来介绍下无状态注入和有状态注入方法逻辑的实现。
样例主要參考官方文档介绍,大家依照这个思路能够扩展很多其它种场景的应用。
一、无状态注入
先看一个样例,也是比較常见的一种场景,我们须要给以下这个类的全部方法注入一个计时的逻辑。
源代码例如以下:
我们目标的class 字节码例如以下:
通过查看字节码结构能够知道,首先我们须要添加一个field给Time类。然后在除了构造器以外的方法注入计时逻辑的字节码。我们先以第一个方法myCount()为例,用javap工具查看字节码信息例如以下:
从方法的偏移量0 到 7 是我们的 timer -=System.currentTimeMillis();相应的字节码实现。24 到31 是timer += System.currentTimeMillis();的字节码实现。
基本能够判定,我们须要再方法刚进入的时候先生成timer -= System.currentTimeMillis();的字节码。然后在方法返回return 指令或者是athrow指令之前生成timer+= System.currentTimeMillis()的字节码。
timer +=System.currentTimeMillis()我们能够通过visitCode(方法開始是通过此方法的调用)方法中加入ASM提供的字节码指令生成的几个方法来实现:
timer -=System.currentTimeMillis()须要通过visitInsn(int opcode)方法来完毕,遍历全部的操作码来推断我们当前的指令是否是return 或者athrow 。假设是那么前插入我们须要的指令。再继续调用下一层mv.visitInsn(opcode)。
代码例如以下:
那么最后还剩下。须要在class中生成一个timer的属性,如前面ClassVisitor的介绍一样,须要在ClassVisitor 的适配子类中的visitEnd()方法中插入我们的FieldVisitor。
至此。我们的字节码已经创建和生成完成,为了健壮性考虑。我们仅仅要再加上是否是Interface的推断。由于接口是没有方法实现体的。而且还要推断,构造器方法中不加入timer计时逻辑。这里我们把须要注入逻辑的Class的name通过參数owner传递给MethodVisitor。总体Adapter方法例如以下:
二、有状态注入
这里的有状态是相对于无状态来说的。刚才的样例中是对于方法绝对偏移量的一种逻辑注入。
简单来说就是注入的逻辑不依赖前一个指令的操作或者指令的參数。
对于Class文件的全部方法都是同样的逻辑注入。可是假设考虑一种情况,那就是当前须要注入的字节码指令依赖于前面指令的运行结果状态。那么我们就必须存储前面这个指令的状态。
以下这个样例来源于自官方文档中的举例。考虑例如以下方法:
这里再补充一下,我们不须要处理StacckMapFrame以及像前一部分须要计算局部变量表和操作数栈的size,那是由于我们没有添加额外的属性,而且演示样例中也没有无条件跳转语句等,须要验证的操作。但假设我们要实现更复杂的情况,还须要覆盖visitMaxs方法、visitFrame visitLable方法等。
(保证移除指令不会影响其它指令的正常跳转。须要调用visitInsn()方法)
事实上我个人认为。处理有状态的字节码指令移除、加入、转移还是须要注意各种字节码指令的情况。
字节码指令的顺序,上下文,栈信息对于编写一段健壮的ASM 逻辑注入代码都很关键。
有时候事实上还是建议先去把注入前后情况的class文件分析一遍,再进行编码。
那么我们也基本能够知道,同ClassVisitor改变类成员一样,MethodVIsistor假设须要改变方法成员,注入逻辑,也能够通过继承MethodVisitor,来编写一个MethodXXXAdapter来实现对于方法逻辑的注入。通过以下的两个样例来介绍下无状态注入和有状态注入方法逻辑的实现。
样例主要參考官方文档介绍,大家依照这个思路能够扩展很多其它种场景的应用。
一、无状态注入
先看一个样例,也是比較常见的一种场景,我们须要给以下这个类的全部方法注入一个计时的逻辑。
源代码例如以下:
package asm.core.methord; /** * Created by yunshen.ljy on 2015/6/29. */ public class Time { public void myCount() throws Exception { int i = 5; int j = 10; System.out.println(j - i); } public void myDeal() { try { int[] myInt = { 1, 2, 3, 4, 5 }; int f = myInt[10]; System.out.println(f); } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } } }
我们目标的class 字节码例如以下:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package asm.core.methord; public class Time { public static long timer; public Time() { } public void myCount() throws Exception { timer -= System.currentTimeMillis(); byte i = 5; byte j = 10; System.out.println(j - i); timer += System.currentTimeMillis(); } public void myDeal() { timer -= System.currentTimeMillis(); try { int[] e = new int[]{1, 2, 3, 4, 5}; int f = e[10]; System.out.println(f); } catch (ArrayIndexOutOfBoundsException var3) { var3.printStackTrace(); } timer += System.currentTimeMillis(); } }
通过查看字节码结构能够知道,首先我们须要添加一个field给Time类。然后在除了构造器以外的方法注入计时逻辑的字节码。我们先以第一个方法myCount()为例,用javap工具查看字节码信息例如以下:
public void myCount() throws java.lang.Exception; descriptor: ()V flags: ACC_PUBLIC Code: stack=7, locals=3, args_size=1 0: getstatic #18 // Field timer:J 3: invokestatic #24 // Method java/lang/System.currentTimeMillis:()J 6: lsub 7: putstatic #18 // Field timer:J 10: iconst_5 11: istore_1 12: bipush 10 14: istore_2 15: getstatic #28 // Field java/lang/System.out:Ljava/io/PrintStream; 18: iload_2 19: iload_1 20: isub 21: invokevirtual #34 // Method java/io/PrintStream.println:(I)V 24: getstatic #18 // Field timer:J 27: invokestatic #24 // Method java/lang/System.currentTimeMillis:()J 30: ladd 31: putstatic #18 // Field timer:J 34: return LocalVariableTable: Start Length Slot Name Signature 10 25 0 this Lasm/core/methord/Time; 12 23 1 i I 15 20 2 j I LineNumberTable: line 8: 10 line 9: 12 line 10: 15 line 11: 24 Exceptions: throws java.lang.Exception
从方法的偏移量0 到 7 是我们的 timer -=System.currentTimeMillis();相应的字节码实现。24 到31 是timer += System.currentTimeMillis();的字节码实现。
基本能够判定,我们须要再方法刚进入的时候先生成timer -= System.currentTimeMillis();的字节码。然后在方法返回return 指令或者是athrow指令之前生成timer+= System.currentTimeMillis()的字节码。
timer +=System.currentTimeMillis()我们能够通过visitCode(方法開始是通过此方法的调用)方法中加入ASM提供的字节码指令生成的几个方法来实现:
@Override public void visitCode() { mv.visitCode(); mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J"); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitInsn(Opcodes.LSUB); mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J"); }
timer -=System.currentTimeMillis()须要通过visitInsn(int opcode)方法来完毕,遍历全部的操作码来推断我们当前的指令是否是return 或者athrow 。假设是那么前插入我们须要的指令。再继续调用下一层mv.visitInsn(opcode)。
代码例如以下:
@Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J"); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitInsn(Opcodes.LADD); mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J"); } mv.visitInsn(opcode); }
那么最后还剩下。须要在class中生成一个timer的属性,如前面ClassVisitor的介绍一样,须要在ClassVisitor 的适配子类中的visitEnd()方法中插入我们的FieldVisitor。
@Override public void visitEnd() { if (!isInterface) { FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null); if (fv != null) { fv.visitEnd(); } } cv.visitEnd(); }
至此。我们的字节码已经创建和生成完成,为了健壮性考虑。我们仅仅要再加上是否是Interface的推断。由于接口是没有方法实现体的。而且还要推断,构造器方法中不加入timer计时逻辑。这里我们把须要注入逻辑的Class的name通过參数owner传递给MethodVisitor。总体Adapter方法例如以下:
package asm.core.methord;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* Created by yunshen.ljy on 2015/6/29.
*/
public class AddTimerAdapter extends ClassVisitor {
private String owner;
private boolean isInterface;
public AddTimerAdapter(ClassVisitor cv) {
super(Opcodes.ASM4, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (!isInterface && mv != null && !name.equals("<init>")) {
mv = new AddTimerMethodAdapter(mv);
}
return mv;
}
@Override public void visitEnd() { if (!isInterface) { FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null); if (fv != null) { fv.visitEnd(); } } cv.visitEnd(); }
class AddTimerMethodAdapter extends MethodVisitor {
public AddTimerMethodAdapter(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
}
@Override
public void visitCode() {
// mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LSUB);
mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
}
@Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J"); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitInsn(Opcodes.LADD); mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J"); } mv.visitInsn(opcode); }
@Override
public void visitMaxs(int maxStack, int maxLocals) {
// 手动挡须要计算栈空间,这里两个long型变量的操作须要4个slot
mv.visitMaxs(maxStack + 4, maxLocals);
}
}
}
二、有状态注入
这里的有状态是相对于无状态来说的。刚才的样例中是对于方法绝对偏移量的一种逻辑注入。
简单来说就是注入的逻辑不依赖前一个指令的操作或者指令的參数。
对于Class文件的全部方法都是同样的逻辑注入。可是假设考虑一种情况,那就是当前须要注入的字节码指令依赖于前面指令的运行结果状态。那么我们就必须存储前面这个指令的状态。
以下这个样例来源于自官方文档中的举例。考虑例如以下方法:
public void myCount(){ int i = 5; int j = 10; System.out.println(j - i); System.out.println(j + i); System.out.println(j + 0); }这里我们知道j+0 或者j-0 的输出结果都是j。那么假设我们要让上面的代码去掉+0 以及-0这两种操作。也就是须要变成例如以下的方法:
public void myCount(){ byte i = 5; byte j = 10; System.out.println(j - i); System.out.println(j + i); System.out.println(j); }通过查看原方法的字节码信息例如以下:
0: iconst_5 1: istore_1 2: bipush 10 4: istore_2 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: iload_2 9: iload_1 10: isub 11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 14: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 17: iload_2 18: iload_1 19: iadd 20: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 23: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 26: iload_2 27: iconst_0 28: iadd 29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 32: return能够发现iadd 指令是iconst_0 的后置指令。可是我们不能单纯得推断当前字节码指令时iadd或者iconst_0 就直接remove。当然remove的实现方式MethodVisitor 同ClassVisitor的适配器实现方式相近,都是通过不继续调用mv.visitInsn(opcode);方法的方式。但这里我们须要标记iconst_0指令的状态。iconst_0指令运行时标记一个状态,在下一条指令运行的时候推断状态值,假设下一条命令是iadd那么就直接return掉方法来移除指令。官方实现很的优雅,这里加了一些凝视。方便理解实现。
package asm.core.methord; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; /** * Created by yunshen.ljy on 2015/7/1. */ public class RemoveAddZeroAdapter extends MethodVisitor { private static int SEEN_ICONST_0 = 1; protected final static int SEEN_NOTHING = 0; protected int state; public RemoveAddZeroAdapter(MethodVisitor mv) { super(Opcodes.ASM4, mv); } @Override public void visitInsn(int opcode) { // 是否检測到前一个指令是ICONST_0 if (state == SEEN_ICONST_0) { // 而且当前指令时iadd if (opcode == Opcodes.IADD) { // 又一次初始化指令状态 state = SEEN_NOTHING; // 移除指令序列 return; } } visitInsn(); // 假设当前指令是ICONST_0 记录指令状态。而且直接返回(移除) if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0; return; } // 继续訪问下一条指令 mv.visitInsn(opcode); } protected void visitInsn() { // 假设最后訪问的是SEEN_ICONST_0指令,那么还原指令(由于刚才被移除了) if (state == SEEN_ICONST_0) { mv.visitInsn(Opcodes.ICONST_0); } state = SEEN_NOTHING; } }
这里再补充一下,我们不须要处理StacckMapFrame以及像前一部分须要计算局部变量表和操作数栈的size,那是由于我们没有添加额外的属性,而且演示样例中也没有无条件跳转语句等,须要验证的操作。但假设我们要实现更复杂的情况,还须要覆盖visitMaxs方法、visitFrame visitLable方法等。
(保证移除指令不会影响其它指令的正常跳转。须要调用visitInsn()方法)
事实上我个人认为。处理有状态的字节码指令移除、加入、转移还是须要注意各种字节码指令的情况。
字节码指令的顺序,上下文,栈信息对于编写一段健壮的ASM 逻辑注入代码都很关键。
有时候事实上还是建议先去把注入前后情况的class文件分析一遍,再进行编码。
相关文章推荐
- ASM(四) 利用Method 组件动态注入方法逻辑
- ASM(三) 利用Method组件动态生成方法的字节码
- 利用instrument+Attach API+javassist动态改变方法逻辑
- ASM(六) 利用TreeApi 动态生成以及转换方法字节码
- 利用cglib与asm包的method拦截(动态代理)
- Sql Injection脚本注入终极利用方法
- Asp.net Mvc 的积木,谈Asp.net MVC中视图逻辑模块化和controller组件的使用方法
- 利用.net反射动态调用指定程序集的中的方法
- 利用反射来动态创建实例和调用方法
- [转载]利用FLV组件制作的播放器,动态获取变量,控制FLV文件
- Unity Application Block 1.0系列(4): 方法调用注入(Method Call Injection )
- 利用数学方法来大大降低一个逻辑判断实现的难度的例子
- 利用JS实现:页面组件的动态显示、隐藏
- 在C#中利用VC的DLL和组件的方法
- 利用反射动态加载Assembly动态执行类型方法
- 利用.net反射动态调用指定程序集的中的方法
- 利用反射动态调用组件或类成员
- 利用反射来动态创建实例和调用方法(上)
- 利用数学方法来大大降低一个逻辑判断实现的难度的例子
- MSSQL注入PUBLIC权限下的xp_dirtree再度利用方法