因为看到在缩短payload的时候会用到,赶紧来学习一下,参考文章:https://www.yishuifengxiao.com/2023/04/04/javassist%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8%E7%AC%94%E8%AE%B0/

这是官方文档:http://www.javassist.org/tutorial/tutorial.html

javassist介绍

Javassist 是一个开源的分析、编辑和创建Java字节码的类库.;其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类。

使用前导入jar包

<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>

Javassist中最为重要的是ClassPool,CtClass, CtMethod以及CtField这几个类.

  • ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象
  • CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
  • CtMethod: 表示类中的方法
  • CtField: 表示类中的字段
  • CtConstructor:可读写的类构造方法对象

ClassPool相关方法

  • getDefault: 返回默认的ClassPool是单例模式的,一般通过该方法创建我们的ClassPool
  • appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  • toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClasstoClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  • get , getCtClass: 根据类路径名获取该类的CtClass对象,用于后续的编辑。

ClassPool对象的创建

// 获取ClassPool对象, 使用系统默认类路径
ClassPool pool = new ClassPool(true);
// 效果与 new ClassPool(true) 一致,只不过返回的是默认的单例模式
ClassPool pool1 = ClassPool.getDefault();

为减少ClassPool可能导致的内存消耗; 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象。

// 从ClassPool中删除CtClass对象
ctClass.detach();
// 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出
ClassPool pool2 = new ClassPool(true);

CtClass相关方法

  • freeze: 冻结一个类,使其不可修改;
  • isFrozen : 判断一个类是否已被冻结;
  • prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  • defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  • detach : 将该class从ClassPool中删除;
  • writeFile : 根据CtClass生成 .class 文件;
  • toClass : 通过类加载器加载该CtClass。
  • setInterfaces: 添加父接口
  • setSuperclass: 添加父类

获取CtClass

CtClass ctClass = classPool.get("org.clown.ssist.Student");//未获取到类或抛异常
// 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常
CtClass ctClass1 = pool.getOrNull("org.clown.ssist.Student");
ctClass.freeze();//冻结类,即不能修改
System.out.println(ctClass.isFrozen());//检查是否冻结,即不可修改

创建CtClass

// 复制一个类
CtClass ctClass2 = pool.getAndRename("org.clown.ssist.Student", "org.clown.ssist.Teacher");
// 创建一个新类
CtClass ctClass3 = pool.makeClass("org.clown.ssist.Student");
// 通过class文件创建一个新类
CtClass ctClass4 = classPool.makeClass(new FileInputStream(new File("target/classes/org/clown/ssist/Student.class")));

创建一个类然后写入

//创建新类并写入
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("Temp.Hello");
ctClass.writeFile();

image-20240907150758908

然后就会根据名称保存到对应的目录下,将类持久化了到文件中

CtClass基础信息

就是一些类的各种基础信息,类全名、类方法、类字段等

// 类名
String simpleName = ctClass.getSimpleName();
// 类全名
String name = ctClass.getName();
// 包名
String packageName = ctClass.getPackageName();
// 接口
CtClass[] interfaces = ctClass.getInterfaces();
// 继承类
CtClass superclass = ctClass.getSuperclass();
// 获取类方法
CtMethod ctMethod = ctClass.getDeclaredMethod("getName()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
// 获取类字段
CtField ctField = ctClass.getField("name");
// 判断数组类型
ctClass.isArray();
// 判断原生类型
ctClass.isPrimitive();
// 判断接口类型
ctClass.isInterface();
// 判断枚举类型
ctClass.isEnum();
// 判断注解类型
ctClass.isAnnotation();
// 冻结一个类,使其不可修改
ctClass.freeze ()
// 判断一个类是否已被冻结
ctClass.isFrozen()
// 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用
ctClass.prune()
//解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用prune方法
ctClass.defrost()

对CtClass进行操作

// 添加接口
ctClass.addInterface(...);
// 添加构造器
ctClass.addConstructor(...);
// 添加字段
ctClass.addField(...);
// 添加方法
ctClass.addMethod(...);

CtClass编译

// 获取字节码文件 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
Class clazz = ctClass.toClass();
// 类的字节码文件
ClassFile classFile = ctClass.getClassFile();
// 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常
byte[] bytes = ctClass.toBytecode();

CtMethod相关方法

CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

CtNewMethod有点类似一个工具类,里面的方法都是静态方法,比如生成一个新的CtMethod

image-20240906003402632

  • insertBefore : 在方法的起始位置插入代码;

  • insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;

  • insertAt : 在指定的位置插入代码;

  • setBody: 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;

  • make : 创建一个新的方法,本质就是调用CtNewMethod#make

    image-20240906003533328

CtMethod属性获取

CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
// 方法名
String methodName = ctMethod.getName();
// 返回类型
CtClass returnType = ctMethod.getReturnType();
// 方法参数, 通过此种方式得到方法参数列表
// 格式: com.kawa.TestService.getOrder(java.lang.String,java.util.List)
ctMethod.getLongName();
// 方法签名 格式: (Ljava/lang/String;Ljava/util/List;Lcom/test/Order;)Ljava/lang/Integer;
ctMethod.getSignature();

// 获取方法参数名称, 可以通过这种方式得到方法真实参数名称
List<String> argKeys = new ArrayList<>();
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int len = ctMethod.getParameterTypes().length;
// 非静态的成员函数的第一个参数是this
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = pos; i < len; i++) {
argKeys.add(attr.variableName(i));
}

CtMethod方法体修改

// 在方法体前插入代码块
ctMethod.insertBefore("");
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");
// 添加参数
ctMethod.addParameter(CtClass);
// 设置方法名
ctMethod.setName("newName");
// 设置方法体 $0=this / $1,$2,$3... 代表方法参数
ctMethod.setBody("{$0.name = $1;}");
//创建一个新的方法
ctMethod.make("kawa",CtClass);

异常块添加

在方法中加入try catch块, 需要注意的是, 必须在插入的代码中, 加入return值$e代表异常信息.插入的代码片段必须以throw或return语句结束

CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);
// 等同于添加如下代码:
try {
// the original method body
} catch (java.io.IOException e) {
System.out.println(e);
throw e;
}

类搜索路径

我们前面获取的ClassPool他有自己的类搜索路径,如果程序运行在JBoss或Tomcat等web服务器上,可能会找不到用户自己的类,我们需要手动添加一个类搜索路径。

下面是各种添加类搜素路径的各种方式

/*通过ClassClassPath添加路径*/
// 将classpath插入到指定classpath之前
pool.insertClassPath(new ClassClassPath(Student.getClass()));
// 将classpath添加到指定classpath之后
pool.appendClassPath(new ClassClassPath(this.getClass()));

该方式添加的时候,比如上面的Student.class,可以将class所在的整个jar添加到搜索路径

/*指定目录添加搜索路径*/
// 将一个目录作为classpath
pool.insertClassPath("/xxx/lib");
/*通过url指定搜索路径*/
ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.sample.com", 80, "/out/", "com.test");
pool.insertClassPath(cp);

上述代码将http://www.sample.com:80/out添加到类搜索路径。并且这个URL只能搜索`com.test`包里面的类。

/*通过ByteArrayPath添加搜索路径*/
ClassPool cp = ClassPool.getDefault();
byte[] buf = 字节数组;
String name = 类名;
cp.insertClassPath(new ByteArrayClassPath(name, buf));
CtClass cc = cp.get(name);
/*通过输入流加载class*/
ClassPool cp = ClassPool.getDefault();
InputStream ins = class文件对应的输入流;
CtClass cc = cp.makeClass(ins);

读写字节码

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");

ClassPoolgetDefault()方法将会查找系统默认的路径来搜索test.Rectable对象,然后将获取到的CtClass对象赋值给cc变量,如果对象没有被找到,那么get()方法就会创建出一个默认的CtClass对象,然后放入到HashTable中,同时将当前创建的对象返回。

byte[] b = cc.toBytecode(); //直接获取字节码
Class clazz = cc.toClass(); //获取Class

toClass()方法调用使得当前线程中的context class loader加载此CtClass类,然后生成java.lang.Class对象。

对类的相关操作

主要还是学一下具体的使用,太深入的东西先不看,参考文章:https://blog.csdn.net/weixin_54902210/article/details/129562446

这里基于前面创建的Hello类进行操作

ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("Temp.Hello");
ctClass.writeFile();

添加属性

package org.clown.ssist;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.Modifier;

public class demo2 {
public static void main(String[] args) throws Exception{
//1.创建Hello类
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Temp.Hello");

//2.添加属性
CtField name = new CtField(classPool.get("java.lang.String"), "name", ctClass);
name.setModifiers(Modifier.PUBLIC);//设置属性为public
ctClass.addField(name,CtField.Initializer.constant("Sentiment"));//给name变量初始化值Sentiment
ctClass.writeFile();
}
}

image-20240907163059547

赋值也可以这样

ctClass.addField(name,"name=\"Sentiment\"");

但这种赋值偏向于用构造器等进行初始化

添加方法

方法可以设置的返回类型

public static CtClass booleanType;
public static CtClass charType;
public static CtClass byteType;
public static CtClass shortType;
public static CtClass intType;
public static CtClass longType;
public static CtClass floatType;
public static CtClass doubleType;
public static CtClass voidType;

这里不支持直接用String,是因为在java字节码中,参数和返回类型的String一般都是用常量池中字符串的索引值,要设置String类型的话就和前面一样用classPool.getCtClass(“java.lang.String”)

package org.clown.ssist;

import javassist.*;

public class demo2 {
public static void main(String[] args) throws Exception{
//1.创建Hello类
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Temp.Hello");

//2.添加属性
CtField name = new CtField(classPool.get("java.lang.String"), "name", ctClass);
name.setModifiers(Modifier.PUBLIC);//设置属性为public
ctClass.addField(name,CtField.Initializer.constant("Sentiment"));//给name变量初始化值Sentiment

//3.添加方法
CtMethod test = new CtMethod(CtClass.voidType, "Test", new CtClass[]{CtClass.intType, CtClass.charType}, ctClass);//分别是返回类型,方法名,方法参数,要添加的方法的CtClass
ctClass.setModifiers(Modifier.PUBLIC);//设置方法为public
ctClass.addMethod(test);
ctClass.writeFile();


}
}

image-20240907170022235

设置方法体

package org.clown.ssist;

import javassist.*;

public class demo2 {
public static void main(String[] args) throws Exception{
//1.创建Hello类
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Temp.Hello");

//2.添加属性
CtField name = new CtField(classPool.get("java.lang.String"), "name", ctClass);
name.setModifiers(Modifier.PUBLIC);//设置属性为public
ctClass.addField(name,CtField.Initializer.constant("Sentiment"));//给name变量初始化值Sentiment
// ctClass.writeFile();

//3.添加方法
CtMethod test = new CtMethod(CtClass.voidType, "Test", new CtClass[]{CtClass.intType, CtClass.charType}, ctClass);//分别是返回类型,方法名,方法参数,要添加的方法的CtClass
ctClass.setModifiers(Modifier.PUBLIC);//设置方法为public
ctClass.addMethod(test);
// ctClass.writeFile();

//4.设置方法体
test.setBody("System.out.println(\"Hello World\");");
ctClass.writeFile();

}
}

image-20240907170209069

方法体前后插入代码

test.insertBefore("System.out.println(\"我在前面插入:\"+$1);");
test.insertAfter("System.out.println(\"我在后面插入了:\"+$2);");

image-20240907170453863

添加构造器

CtConstructor cons = new CtConstructor(new CtClass[]{classPool.getCtClass("java.lang.String")}, ctClass);//分别是参数列表,要添加的CtClass
cons.setBody("{$0.name = $1;}");//设置name=var1,也就是第一个参数
ctClass.addConstructor(cons);
ctClass.writeFile();

image-20240907171307942

无参构造去掉中间的参数即可

修改已有类

用ClassPool获取CtClass之后进行修改,我们对我们我们前面创建的Hello.class进行修改

package org.clown.ssist;


import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

public class demo3 {
public static void main(String[] args) throws Exception {
//对已有类进行修改
System.out.println(System.getProperty("user.dir"));
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath("D:\\CTF\\Java\\JavaCode\\JavassistLearn");
CtClass ctClass = classPool.getCtClass("Temp.Hello");
CtConstructor constructor = ctClass.getConstructors()[0];
constructor.setBody("{System.out.println(\"changing\");}");
ctClass.writeFile();
}
}

image-20240907174508054

可以看到成功修改

这里添加类路径的时候要注意在包名的上一层,不然会找不到类,因为get的时候用完整包名会自动添加上路径进行搜索

比如上面的添加路径为:D:\CTF\Java\JavaCode\JavassistLearn,搜索时就是这样:D:\CTF\Java\JavaCode\JavassistLearn\Temp\Hello.class

一些特殊变量

就是我们前面使用的$1,$0那些

标识符 作用
$0、$1、$2、 $3等 this和方法参数(1-N是方法参数的顺序)
$args 方法参数数组,类型为Object[]
$$ 所有方法参数,例如:m($$)相当于m($1,$2,…)
$cflow(…) control flow 变量
$r 返回结果的类型,在强制转换表达式中使用。
$w 包装器类型,在强制转换表达式中使用。
$_ 方法的返回值
$sig 类型为java.lang.Class的参数类型对象数组
$type 类型为java.lang.Class的返回值类型
$class 类型为java.lang.Class的正在修改的类