Java插桩工具
简介
Javassist (JAVA programming Assistant,Java编程助手) 是一个用Java编辑字节码的类库。它使Java程序可以在运行时定义新类,并在JVM加载它时修改类文件。
与其他类似的字节码编辑器不同,Javassist提供两个级别的API:源代码级别和字节码级别。
如果使用源代码级API,则可以在不了解Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表进行设计。甚至可以以源文本的形式指定插入的字节码。Javassist可以即时对其进行编译。
另一方面,字节码级API允许用户像其他编辑器一样直接编辑类文件。
我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassist。
类似的技术还有:bcel,asm等,他们相对于Javassit,偏向底层,效率较高,但编码难度更高(需要了解JVM指令)。
Javassist是Jboss的一个子项目,其特点是简单:不需要了解底层JVM指令,直接用Java代码编写,容易理解,并且现在生成代码效率和以上两种技术相差已经很小。目前,最新版本 3.27.0-GA (2020年03月19日).
目录
├── build.xml
├── javassist.jar
├── License.html
├── pom.xml
├── Readme.html
├── README.md
├── sample
│ ├── duplicate
│ ├── evolve
│ ├── hotswap
│ ├── preproc
│ ├── reflect
│ ├── rmi
│ ├── Test.java
│ └── vector
├── src
│ ├── main
│ └── test
└── tutorial
├── brown.css
├── tutorial2.html
├── tutorial3.html
└── tutorial.html
- sample 样例
- src 源代码
- tutorial 教程
- Readme.html 自述文件。
- License.html 许可证文件
- tutorial 教程目录
- javassist.jar jar文件(类文件)
- src 源代码目录
源代码级别
ClassPool
CtClass对象的容器。
- getDefault (): 返回默认的
ClassPool
,单例模式,一般通过该方法创建我们的ClassPool; - appendClassPath(ClassPath cp), insertClassPath(ClassPath cp) : 将一个
ClassPath
加到类搜索路径的末尾位置或插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类问题; - importPackage(String packageName):导入包;
- makeClass(String classname):创建一个空类,没有变量和方法,后序通过CtClass的函数进行添加;
- get(String classname)、getCtClass(String classname) : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
CtClass
类
- debugDump;String类型,如果生成。class文件,保存在这个目录下。
- setName(String name):给类重命名;
- setSuperclass(CtClass clazz):设置父类;
- addField(CtField f, Initializer init):添加字段(属性),初始值见CtField;
- addMethod(CtMethod m):添加方法(函数);
- toBytecode(): 返回修改后的字节码。需要注意的是一旦调用该方法,则无法继续修改CtClass;
- toClass(): 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的
toClass
方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的CtClass; - writeFile(String directoryName):根据CtClass生成
.class
文件; - defrost():解冻类,用于使用了toclass()、toBytecode、writeFile(),类已经被JVM加载,Javassist冻结CtClass后;
- detach():避免内存溢出,从ClassPool中移除一些不需要的CtClass。
Loader
类加载器
- loadClass(String name):加载类
CtField
字段
- CtField(CtClass type, String name, CtClass declaring) :构造函数,添加字段类型,名称,所属的类;
- CtField.Initializer constant():CtClass使用addField时初始值的设置;
- setModifiers(int mod):设置访问级别,一般使用Modifier调用常量。
CtMethod
方法
- insertBefore(String src):在方法的起始位置插入代码;
- insertAfter(String src):在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
- insertAt(int lineNum, String src):在指定的位置插入代码;
- addCatch(String src, CtClass exceptionType):将方法内语句作为try的代码块,插入catch代码块src;
- setBody(String src):将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
- setModifiers(int mod):设置访问级别,一般使用Modifier调用常量;
- invoke(Object obj, Object... args):反射调用字节码生成类的方法。
- 对于setBody $0代表this $1、$2、...代表方法的第几个参数
setBody("{$0.name = $1;}");
$符号含义
$符号含义
使用步骤
使用步骤
举例
新建Java项目,导入javassist.jar。
项目结构1
项目结构
代码1
Main无所谓,是我建项目时选择了Hello World模板
Test.java
package bupt.edu.cn; import javassist.*; import java.lang.reflect.Method; public class Test { public static void createStudent() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("E:\\Workspace\\IDEA_workspace\\JavassistTest\\src\\bupt.edu.cn.Student"); // 字段名为name CtField param = new CtField(pool.get("java.lang.String"),"name", cc); // 访问级别是 private param.setModifiers(Modifier.PRIVATE); // 初始值是 "Frankyu" cc.addField(param, CtField.Initializer.constant("Frankyu")); // 生成 getter、setter 方法 cc.addMethod(CtNewMethod.setter("setName", param)); cc.addMethod(CtNewMethod.getter("getName", param)); // 添加无参的构造函数 CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{name = \"yubo\";}"); cc.addConstructor(cons); // 5. 添加有参的构造函数 cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc); // $0=this / $1,$2,$3... 代表方法参数 cons.setBody("{$0.name = $1;}"); cc.addConstructor(cons); // 6. 创建一个名为printName方法,无参数,无返回值,输出name值 CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(name);}"); cc.addMethod(ctMethod); //这里会将这个创建的类对象编译为.class文件 cc.writeFile(""); } public static void usingStudent() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.getCtClass("E:\\Workspace\\IDEA_workspace\\JavassistTest\\src\\bupt.edu.cn.Student"); // 实例化 Object student = cc.toClass().newInstance(); // 设置值 Method setName = student.getClass().getMethod("setName", String.class); setName.invoke(student, "junjie"); // 输出值 Method execute = student.getClass().getMethod("printName"); execute.invoke(student); } public static void main(String[] args) throws Exception{ createStudent(); usingStudent(); } }
注意:.class文件的绝对路径需要修改
Student.class
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package E:\Workspace\IDEA_workspace\JavassistTest\src\bupt.edu.cn; public class Student { private String name = "Frankyu"; public void setName(String var1) { this.name = var1; } public String getName() { return this.name; } public Student() { this.name = "yubo"; } public Student(String var1) { this.name = var1; } public void printName() { System.out.println(this.name); } }
结果
项目结构2
项目结构
代码2
Student.java
package bupt.edu.cn; import java.util.Random; public class Student { public int getAge() { return age; } public void setAge(int age) { this.age = age; } private int age; public int getGrade() { return grade; } public void setGrade(int grade) { this.grade = grade; } private int grade; @Override public String toString() { return "Student{" + "age=" + age + ", grade=" + grade + '}'; } public void display() { System.out.println(this.toString()); } public void learn() throws Exception{ // long start = System.currentTimeMillis(); Random rand = new Random(); int time = rand.nextInt(1000)+3000; Thread.sleep(time); // long end = System.currentTimeMillis(); // System.out.println(end-start); } public static void main(String[] args) { Student s = new Student(); s.display(); try{ s.learn(); } catch (Exception e){ } } }
Monitor.java
import bupt.edu.cn.Student; import javassist.*; public class Monitor { final static ClassPool pool = ClassPool.getDefault(); final static String classname = "bupt.edu.cn.Student"; public void studentLearnMonitor() throws Exception{ CtClass ss = pool.getCtClass(classname); CtClass.debugDump="./dump"; String methodname = "learn"; CtMethod learn_ori = ss.getDeclaredMethod(methodname); //拷贝一份learn方法 CtMethod learn_cp = CtNewMethod.copy(learn_ori,learn_ori.getName()+"_cp",ss,null); //添加拷贝后的方法 ss.addMethod(learn_cp); //修改learn方法:原代码前后添加时间 String src = "{"+ "long start = System.currentTimeMillis();" + learn_ori.getName()+"_cp($$);"+ "long end = System.currentTimeMillis();"+ "System.out.println(end-start);"+ "}"; learn_ori.setBody(src); ss.toClass(); //生成.class文件,主要用于调试,查看是否有代码片段被忽略 //ss.writeFile(); Student s = new Student(); s.learn(); } public void studentDisplayMonitor() throws Exception{ CtClass ss = pool.getCtClass(classname); CtClass.debugDump="./dump"; //添加字段name CtField param = new CtField(pool.get("java.lang.String"),"name", ss); // 访问级别是 private param.setModifiers(Modifier.PRIVATE); // 初始值是 "Frankyu" ss.addField(param, CtField.Initializer.constant("Frankyu")); String methodname = "display"; CtMethod display = ss.getDeclaredMethod(methodname); String src = "{"+ "System.out.println($0.age);" + "$0.name=\"frankyu\";" + "System.out.println($0.name);"+ "}"; display.insertBefore(src); if(true){ System.out.println("Hello javassist"); } src ="if(true){" + "System.out.println(\"Hello javassist\");" + "}"; display.insertAt(7,src); src = "{"+ "System.out.println($0.grade);"+ "}"; display.insertAfter(src); ss.toClass(); //ss.writeFile(); Student s = new Student(); s.display(); } public static void main(String[] args) throws Exception{ Monitor m = new Monitor(); // 由于类冻结问题,两个方法不可同时调用 // m.studentLearnMonitor(); m.studentDisplayMonitor(); } }
studentLearnMonitor对learn方法插桩,通过拷贝的方式,使start和end属于同一个代码块,解决不同代码块之间变量无法使用问题。
studentDisplayMonitor对display方法插桩,演示了添加变量,访问变量,修改变量,方法前插,后插,任意位置插。
learn插桩结果
display插桩结果
调用studentDisplayMonitor时,dump目录下,Student.class部分代码
public void display() { System.out.println(this.age); this.name = "frankyu"; System.out.println(this.name); if (true) { System.out.println("Hello javassist"); } System.out.println(this.toString()); Object var2 = null; System.out.println(this.grade); }
调用studentLearnMonitor时,dump目录下,Student.class部分代码
public void learn() throws Exception { long var1 = System.currentTimeMillis(); this.learn_cp(); long var3 = System.currentTimeMillis(); System.out.println(var3 - var1); }
字节码级别
ClassFile
类
- getFields():返回字段列表;
- addField(FieldInfo finfo):添加字段;
- addMethod(MethodInfo minfo) :添加方法;
- getMethod(String name):根据方法名返回MethodInfo对象;
FieldInfo
字段
- setAccessFlags(int acc):设置访问级别,通过类AccessFlag调用它的常量PUBLIC等。
MethodInfo
方法
- getCodeAttribute():返回CodeAttribute对象。
CodeAttribute
代码属性
- iterator():返回代码迭代器
CodeIterator
代码指令迭代器
void begin()
移到第一个指令处.void move(int index)
移到指定索引处boolean hasNext()
如果存在指令的话,返回trueint next()
返回下一个指令的索引
需要注意的是,此方法并不会返回下一个指令的操作码int byteAt(int index)
返回指定索引处的无符号8bit位长值.int u16bitAt(int index)
返回指定索引处的无符号16bit位长值.int write(byte[] code, int index)
在指定索引处写入字节数组.void insert(int index, byte[] code)
在指定索引处写入字节数组,其他字节码的offset等将会自适应更改。
举例
参考
更多内容查看:网络安全-自学笔记
喜欢本文的请动动小手点个赞,收藏一下,有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系