javassist介绍

Javassist是一个开源的分析、编辑和创建Java字节码的类库,Java 字节码存储在称为类文件的二进制文件中。每个类文件包含一个 Java 类或接口。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

学习一下javassist主要的几个类

javassist几个重要的类

下面关于类的资料基本是网上copy的

ClassPool

ClassPool:一个基于哈希表(Hashtable)实现的CtClass对象容器,其中键名是类名称,值是表示该类的CtClass对象

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
ClassPool		getDefault()			返回默认的类池。比如 ClassPool classPool = 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

CtClass:表示一个类,一个CtClass(编译时类)对象可以处理一个class文件,这些CtClass对象可以从ClassPool获得。

一般都是对CtClass类对象进行操作,比如添加方法,添加成员属性

1
CtClass ctClass = classPool.makeClass("cc2.classdemo2");

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
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

CtMethod:表示类中的方法。超类为CtBehavior,很多有用的方法都在CtBehavior

也就是关于添加方法的一系列操作

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

CtConstructor

CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数。

也就是创建有参或者无参函数

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

ClassClassPath

定义在 java.lang.Class 中获取类文件的搜索路径。

构造方法

1
2
ClassClassPath(java.lang.Class<?> c)	
创建一个搜索路径。

比如下面的代码,是在默认系统搜索路径获取demo对象

1
2
3
ClassPool classPool = ClassPool.getDefault();
// 获取目标类 cc2.demo
CtClass ctClass = classPool.get("cc2.demo");

但是我们可以通过insertClassPath修改这个搜索路径

如下

1
2
classPool.insertClassPath(new ClassClassPath(test_javassist3.class.getClass()));
CtClass ctClass = classPool.get("cc1.cc1_test");

功能一:动态生成类

也就是通过javassist上面的几个类以及对应的方法可以构造一个恶意的类,其中的方法,属性,接口等等我们都可以自己定义

动态生成类需要以下几个步骤(重点为前3步或者前4步)

  1. 获取默认类池ClassPool classPool = ClassPool.getDefault();
  2. 创建一个自定义类CtClass ctClass = classPool.makeClass();
  3. 添加实现接口/属性/构造方法/普通方法
  4. 写入磁盘
  5. 进行验证(也就是调用该类的方法)

简单的动态生成一个含有calcmethod(弹计算器)方法的类classdemo

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
package cc2;

import javafx.util.converter.LocalDateStringConverter;
import javassist.*;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

public class test_javassist {
public static void main(String[] args) throws CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
// 1. 获取默认类池
ClassPool classPool = ClassPool.getDefault();
// 2. 创建一个自定义类classdemo
CtClass ctClass = classPool.makeClass("cc2.classdemo");
// 3. 添加一个方法
CtMethod ctMethod = CtNewMethod.make("public void calcmethod(){java.lang.Runtime.getRuntime().exec(\"calc\");}",ctClass);
// 将该方法添加给ctClass
ctClass.addMethod(ctMethod);

// 4. 写入磁盘
// 转换为字节流
byte[] bytes = ctClass.toBytecode();
// 使用writefile指定绝对路径写入磁盘
ctClass.writeFile("D:\\projects\\java\\java1\\src\\main\\java");


// 5. 验证
// 调用classdemo的方法
// 获取javassist的Classloader
ClassLoader loader = new Loader(classPool);
// 加载该class文件
Class<?> aClass = loader.loadClass("cc2.classdemo");
// 调用该方法
aClass.getDeclaredMethod("calcmethod").invoke(aClass.newInstance());

}
}

运行结果

在指定目录下生成classdemo.class

image-20221023231659650

并且加载该类并调用calcmethod方法成功

image-20221023231453520

然后更加丰富一点,在这个基础上添加接口,属性等等

比如下面这个,我们新建了一个classdemo2.class,实现了Serializable接口,新增了id和name属性,新增了无参和有参构造方法,并且通过实例化,反射调用getname和getid方法。

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
77
78
79
80
81
package cc2;

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

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class test_javassist {
public static void main(String[] args) throws CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, NotFoundException {
// 获取默认类池
ClassPool classPool = ClassPool.getDefault();
// 创建一个自定义类classdemo
CtClass ctClass = classPool.makeClass("cc2.classdemo2");
// 实现序列化接口
ctClass.setInterfaces(new CtClass[]{classPool.makeInterface("java.io.Serializable")});

//添加一个int类型的成员变量
CtField id = new CtField(CtClass.intType, "id", ctClass);
id.setModifiers(AccessFlag.PUBLIC);
ctClass.addField(id);

// 添加一个String类型的成员变量
CtField name = new CtField(classPool.get("java.lang.String"), "name", ctClass);
// 将id设置为public
name.setModifiers(AccessFlag.PUBLIC);
// 将该name属性添加给atClass
ctClass.addField(name);

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

// 添加一个方法
CtMethod ctMethod = CtNewMethod.make("public void calcmethod(){java.lang.Runtime.getRuntime().exec(\"calc\");}",ctClass);
// 将该方法添加给ctClass
ctClass.addMethod(ctMethod);

//添加get类方法
CtMethod getid_method = CtNewMethod.make("public int getid(){return this.id;}", ctClass);
ctClass.addMethod(getid_method);
//添加get类方法
CtMethod getname_method = CtNewMethod.make("public String getname(){return this.name;}", ctClass);
ctClass.addMethod(getname_method);

// 写入磁盘
// 转换为字节流
byte[] bytes = ctClass.toBytecode();
// 方法一:使用writefile指定绝对路径写入磁盘
// ctClass.writeFile("D:\\projects\\java\\java1\\src\\main\\java");
// 方法二:使用FileOutputStream写入磁盘
// System.getProperties("user.dir") 定位到的当前用户目录("user.dir")(即工程根目录)
File file = new File(new File(System.getProperty("user.dir"), "/src/main/java/cc2/"),"classdemo2.class"); // 这个和上面的make不同,这里是类的位置以及文件名
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(bytes);
fileOutputStream.close();

// 验证
// 调用classdemo的方法
// 获取javassist的Classloader
ClassLoader loader = new Loader(classPool);
// 加载该class文件
Class<?> aClass = loader.loadClass("cc2.classdemo2");
// 通过无参构造创建对象,反射调用该方法
// aClass.getDeclaredMethod("calcmethod").invoke(aClass.newInstance());
//通过有参构造方法创建对象,反射调用该方法
Constructor<?> constructor = aClass.getConstructor(int.class, String.class);
//创建对象
Object o = constructor.newInstance(555, "Sk1y");
//反射调用
System.out.println(aClass.getDeclaredMethod("getid").invoke(o));
System.out.println(aClass.getDeclaredMethod("getname").invoke(o));
}
}

运行结果

image-20221024084732743

功能二:动态获取类方法

一般步骤

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

首先整一个测试类demo.java

1
2
3
4
5
6
7
8
package cc2;

public class demo {
public void hello(){
System.out.println("hello world!!!");
}
}

编译

image-20221024091218990

test_javassist2.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cc2;

import javassist.*;

public class test_javassist2 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException {
// 获取默认类池
ClassPool classPool = ClassPool.getDefault();
// 获取目标类 cc2.demo
CtClass ctClass = classPool.get("cc2.demo");
// 获取类的方法hello
CtMethod hello = ctClass.getDeclaredMethod("hello");
// 插入任意代码,这里插入的是calc
hello.insertBefore("{java.lang.Runtime.getRuntime().exec(\"calc\");}");

// 转换为class对象
Class aClass = ctClass.toClass();
// 实例化demo对象
demo o = (demo)aClass.newInstance(); //强制转换,创建demo类对象
// 调用hello方法,发现会调用calc
o.hello();

}
}

运行结果,调用hello方法,会弹计算器

image-20221024092714149

功能三:动态获取类信息

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
package cc2;

import javassist.*;

import java.lang.reflect.Array;
import java.util.Arrays;

public class test_javassist3 {
public static void main(String[] args) throws NotFoundException {
ClassPool classPool = ClassPool.getDefault();
// System.out.println(classPool);
classPool.insertClassPath(new ClassClassPath(test_javassist3.class.getClass()));
CtClass ctClass = classPool.get("cc2.demo");
System.out.println(ctClass.getName()); //获取类名
System.out.println(ctClass.getSimpleName()); //获取类名
System.out.println(ctClass.getSuperclass().getName()); //超类
System.out.println(Arrays.toString(ctClass.getInterfaces())); //获取接口
for (CtConstructor constructor : ctClass.getConstructors()) {
System.out.println(constructor);
}
for (CtMethod method : ctClass.getMethods()) {
System.out.println(method);
}
}
}

运行结果

image-20221024094652669

总结

个人感觉javassist很神奇,通常我们生成class文件,是通过对java文件进行编译生成的,但是有了javassist之后,就可以直接动态生成class文件。

除此之外,动态获取类方法,把一个安全的类方法,通过insertBefore可以插入任意恶意代码,进行命令执行。不过限制在于需要有javassist这个依赖。

在学习过程中,看到了nice0e3师傅关于免杀的一些想法,学到了学到了🐂,文章链接在参考链接3

参考链接

  1. Java之Javassist动态编程 - Zh1z3ven - 博客园 (cnblogs.com)
  2. (2条消息) Java动态编程之javassist_Ricky_Fung的博客-CSDN博客
  3. Java安全之Javassist动态编程 - nice_0e3 - 博客园 (cnblogs.com)