Java安全之Javassist动态编程
2022-04-27 22:19:12 # Java安全

动态编程

动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如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中最为重要的是ClassPoolCtClassCtMethod 以及 CtField这几个类。

ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。

CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。

CtMethods:表示类中的方法。

CtFields :表示类中的字段。

ClassPool

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ClassPool		getDefault()			返回默认的类池。

ClassPath insertClassPath(String pathname) 在搜索路径的开头插入目录或jar(或zip)文件。

ClassPath insertClassPath(ClassPath cp) ClassPath在搜索路径的开头插入一个对象。

java.lang.ClassLoader getClassLoader() 获取类加载器

CtClass get(java.lang.String classname) 从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用。

ClassPath appendClassPath(ClassPath cp) 将ClassPath对象附加到搜索路径的末尾。

CtClass makeClass(java.lang.String classname) 创建一个新的public

CtClass

1
2
3
4
5
6
7
8
9
10
11
12
13
void	setSuperclass(CtClass clazz)	更改超类,除非此对象表示接口。

java.lang.Class<?> toClass(java.lang.invoke.MethodHandles.Lookup lookup)
将此类转换为java.lang.Class对象。

byte[] toBytecode() 将该类转换为类文件。

void writeFile() 将由此CtClass 对象表示的类文件写入当前目录。

void writeFile(java.lang.String directoryName) 将由此CtClass 对象表示的类文件写入本地磁盘。

CtConstructor makeClassInitializer() 制作一个空的类初始化程序(静态构造函数)。

CtMethod

1
2
3
4
5
6
7
void	insertBefore (java.lang.String src)	
在正文的开头插入字节码。
void insertAfter (java.lang.String src)
在正文的末尾插入字节码。
void setBody (CtMethod src, ClassMap map)
从另一个方法复制方法体。

CtConstructor

1
2
3
4
5
6
7
void	setBody(java.lang.String src)	
设置构造函数主体。
void setBody(CtConstructor src, ClassMap map)
从另一个构造函数复制一个构造函数主体。
CtMethod toMethod(java.lang.String name, CtClass declaring)
复制此构造函数并将其转换为方法。

CtField

CtFields :表示类中的字段。

动态生成类

大致有如下几个步骤

  1. 获取默认类池ClassPool classPool = ClassPool.getDefault();
  2. 创建一个自定义类CtClass ctClass = classPool.makeClass();
  3. 添加实现接口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
    2
    CtMethod ctMethod = CtNewMethod.make("public void calcDemo(){java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}", ctClass);
    ctClass.addMethod(ctMethod);
  1. 写入磁盘

    这里写入磁盘可以用如下两种方法

    • javassist自带的ctClass.writeFile();可指定绝对路径写入
    • 也可转换为byte流通过FileOutputStream等写入磁盘
  2. 进行验证:调用方法or属性赋值

  3. tips:

    • 这里注意javassist.CannotCompileException异常: 因为同个 Class 是不能在同个 ClassLoader 中加载两次的,所以在输出 CtClass 的时候需要注意下,可以使用javassist自带的classloader解决此问题
    • 反射时newInstance()抛出了java.lang.InstantiationException异常可能是因为没有写无参构造
    • 如果已经加载了通过javassist生成的类,即便是通过反射(如class.forName())或者new都不是加载一个"新类",只有换一个ClassLoader加载才会是生成一个"新类"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.sec.test2;

import javassist.*;
import javassist.bytecode.AccessFlag;

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

public class JavassistDemo01 {
public static void main(String[] args) {

JavassistDemo01 a = new JavassistDemo01();
a.makeClass0();
}

public void makeClass0(){
//获取默认类池
ClassPool classPool = ClassPool.getDefault();
//创建一个类ClassDemo
CtClass ctClass = classPool.makeClass("com.sec.test2.ClassDemo");
//让该类实现序列化接口
ctClass.setInterfaces(new CtClass[]{classPool.makeInterface("java.io.Serializable")});
try {
//新建一个int类型名为id的成员变量
CtField id = new CtField(CtClass.intType, "id", ctClass);
//将id设置为public
id.setModifiers(AccessFlag.PUBLIC);
//将该id属性"赋值"给ClassDemo
ctClass.addField(id);

//添加无参构造方法
CtConstructor ctConstructor = CtNewConstructor.make("public ClassDemo(){};", ctClass);
ctClass.addConstructor(ctConstructor);

//添加有参构造方法
CtConstructor ctConstructor1 = CtNewConstructor.make("public ClassDemo(int id){this.id = id;}", ctClass);
ctClass.addConstructor(ctConstructor1);

//添加普通方法1
CtMethod ctMethod = CtNewMethod.make("public void calcDemo(){java.lang.Runtime.getRuntime().exec(\"cmd.exe /c calc.exe\");}", ctClass);
ctClass.addMethod(ctMethod);

//添加普通方法2
CtMethod ctMethod1 = CtNewMethod.make("public void hello(){System.out.println(\"Hello Javassist!!!\");}", ctClass);
ctClass.addMethod(ctMethod1);

//将class文件写入磁盘
//转换成字节流
byte[] bytes = ctClass.toBytecode();
//写入磁盘
File classPath = new File(new File(System.getProperty("user.dir"), "/src/main/java/com/sec/test2"), "ClassDemo.class");
FileOutputStream fos = new FileOutputStream(classPath);
fos.write(bytes);
fos.close();

//验证-调用方法
//注意这里可能会抛javassist.CannotCompileException异常因为同个 Class 是不能在同个 ClassLoader 中加载两次的,所以在输出 CtClass 的时候需要注意下
//需要通过一个未加载该class的classloader加载即可,为此javassist内置了一个classloader

//获取javassist的classloader
ClassLoader loader = new Loader(classPool);
System.out.println("loading");
//通过该classloader加载才是新的一个class
Class<?> clazz = loader.loadClass("com.sec.test2.ClassDemo");

//反射调用hello
clazz.getDeclaredMethod("hello").invoke(clazz.newInstance());
//反射调用calc
clazz.getDeclaredMethod("calcDemo").invoke(clazz.newInstance());

} catch (Exception e){
System.out.println(e);
}
}
}

动态获取类方法

  1. 获取默认类池ClassPool classPool = ClassPool.getDefault();
  2. 获取目标类CtClass cc = cp.get();
  3. 获取类的方法CtMethod m = cc.getDeclaredMethod();
  4. 插入任意代码m.insertBefore("{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}");
  5. 转换为class对象Class c = cc.toClass();
  6. 反射调用对象JavassistDemo j= (JavassistDemo)c.newInstance();
  7. 执行方法j.hello();

tips:

  1. 如果目标类未加载过,可以直接调用toClass()方法之后new一个该类的对象即可调用该类。
  2. 如果目标类已加载过,就需要用上面的方法,通过javassist的ClassLoader去加载后进行调用。

参考

https://www.cnblogs.com/CoLo/p/15383642.html