动态编程
动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。
那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术,在Java中有如下几种方式:
反射
这个搞Java的应该比较熟悉,原理也就是通过在运行时获得类型信息然后做相应的操作。
动态编译
动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。
调用JavaScript引擎
Java 6加入了对Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本。这个脚本API允许你为脚本语言提供Java支持。
动态生成字节码
这种技术通过操作Java字节码的方式在JVM中生成新类或者对已经加载的类动态添加元素。
方式:
ASM:直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。
Javassit :提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。
什么是Javassist
Javassist
是一个开源的分析、编辑和创建Java字节码的类库,Java 字节码存储在称为类文件的二进制文件中。每个类文件包含一个 Java 类或接口。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。而个人感觉在安全中最重要的就是在使用Javassist
时我们可以像写Java代码一样直接插入Java代码片段,让我们不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist
的API即可实现字节码编辑,类似于可以达到任意代码执行的效果。
Javassist的使用
在Javassist中最为重要的是ClassPool
,CtClass
,CtMethod
以及 CtField
这几个类。
ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。
CtMethods:表示类中的方法。
CtFields :表示类中的字段。
ClassPool
常用方法:
1 | ClassPool getDefault() 返回默认的类池。 |
CtClass
1 | void setSuperclass(CtClass clazz) 更改超类,除非此对象表示接口。 |
CtMethod
1 | void insertBefore (java.lang.String src) |
CtConstructor
1 | void setBody(java.lang.String src) |
CtField
CtFields :表示类中的字段。
动态生成类
大致有如下几个步骤
- 获取默认类池
ClassPool classPool = ClassPool.getDefault();
- 创建一个自定义类
CtClass ctClass = classPool.makeClass();
- 添加实现接口or属性or构造方法or普通方法
添加接口
1
ctClass.setInterfaces(new CtClass[]{classPool.makeInterface("java.io.Serializable")});
添加属性
1
2
3
4
5
6//新建一个int类型名为id的成员变量
CtField id = new CtField(CtClass.intType, "id", ctClass);
//将id设置为public
id.setModifiers(AccessFlag.PUBLIC);
//将该id属性"赋值"给ClassDemo
ctClass.addField(id);添加构造方法(有参)
1
2
3//添加有参构造方法
CtConstructor ctConstructor1 = CtNewConstructor.make("public ClassDemo(int id){this.id = id;}", ctClass);
ctClass.addConstructor(ctConstructor1);添加方法
1
2CtMethod ctMethod = CtNewMethod.make("public void calcDemo(){java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}", ctClass);
ctClass.addMethod(ctMethod);
写入磁盘
这里写入磁盘可以用如下两种方法
- javassist自带的
ctClass.writeFile();
可指定绝对路径写入 - 也可转换为byte流通过
FileOutputStream
等写入磁盘
- javassist自带的
进行验证:调用方法or属性赋值
tips:
- 这里注意
javassist.CannotCompileException
异常: 因为同个 Class 是不能在同个 ClassLoader 中加载两次的,所以在输出 CtClass 的时候需要注意下,可以使用javassist
自带的classloader解决此问题 - 反射时
newInstance()
抛出了java.lang.InstantiationException
异常可能是因为没有写无参构造 - 如果已经加载了通过javassist生成的类,即便是通过反射(如
class.forName()
)或者new
都不是加载一个"新类"
,只有换一个ClassLoader加载才会是生成一个"新类"
- 这里注意
1 | package com.sec.test2; |
动态获取类方法
- 获取默认类池
ClassPool classPool = ClassPool.getDefault();
- 获取目标类
CtClass cc = cp.get();
- 获取类的方法
CtMethod m = cc.getDeclaredMethod();
- 插入任意代码
m.insertBefore("{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}");
- 转换为class对象
Class c = cc.toClass();
- 反射调用对象
JavassistDemo j= (JavassistDemo)c.newInstance();
- 执行方法
j.hello();
tips:
- 如果目标类未加载过,可以直接调用
toClass()
方法之后new
一个该类的对象即可调用该类。 - 如果目标类已加载过,就需要用上面的方法,通过javassist的ClassLoader去加载后进行调用。