动态编程
动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如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去加载后进行调用。