编程语言
首页 > 编程语言> > 使用Javassist创建静态构造函数的克隆

使用Javassist创建静态构造函数的克隆

作者:互联网

似乎Javassist的API允许我们创建类中声明的类初始化器(即静态构造函数)的完全副本:

CtClass cc = ...;
CtConstructor staticConstructor = cc.getClassInitializer();
if (staticConstructor != null) {
  CtConstructor staticConstructorClone = new CtConstructor(staticConstructor, cc, null);
  staticConstructorClone.getMethodInfo().setName(__NEW_NAME__);
  staticConstructorClone.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
  cc.addConstructor(staticConstructorClone);
}

但是,该副本还包括(公共/私人)静态最终字段.例如,以下类的静态构造函数:

public class Example {
  public static final Example ex1 = new Example("__EX_1__");

  private String name;

  private Example(String name) {
    this.name = name;
  }
}

实际上是:

static {
  Example.ex1 = "__NAME__";
}

因此,静态构造函数的完全副本还将包括对最终字段“name”的调用.

有没有办法创建一个静态构造函数的副本,其中不包含对最终字段的调用?

– 谢谢

解决方法:

介绍

作为您的驱动器重置类的静态状态但删除最后的字段,执行它的关键是ExprEditor类.这个类基本上允许您使用Javassist的高级API轻松转换某些操作,而不必使用所有字节码.

即使我们将在高级API中完成所有这些工作,我仍然会转储一些字节码,因此我们也可以看到该级别的更改.

工作基地

让我们抓住你的Example类,但有一个转折点:

public class Example {
 public static final Example ex1 = new Example("__EX_1__");
 public static String DEFAULT_NAME = "Paulo"; // <-- change 1

 private String name;

 static {
     System.out.println("Class inited");  // <-- change 2
 }

 public Example(String name) {
     this.name = name;
 }
}

我添加了一个非最终的静态字段,因此我们可以更改它,我们应该可以重置它.我还添加了一个带有一些代码的静态块,在这种情况下它只是一个System.out但请记住,其他类可能有不多次运行的代码,你可能会发现自己调试了奇怪的行为(但我’我相信你可能已经意识到了这一点.

为了测试我们的修改,我还使用以下代码创建了一个测试类:

public class Test {

   public static void main(String[] args) throws Throwable {
    System.out.println(Example.DEFAULT_NAME);
    Example.DEFAULT_NAME = "Jose";
    System.out.println(Example.DEFAULT_NAME);
    try {
        reset();
    } catch (Throwable t) {
        System.out.println("Problems calling reset, maybe not injected?");
    }
    System.out.println(Example.DEFAULT_NAME);
   }

   private static void reset() throws Throwable {
    Method declaredMethod = Example.class.getDeclaredMethod("__NEW_NAME__", new Class[] {});
    declaredMethod.invoke(null, new Object[] {});
   }
}

如果我们开箱即用这个类,我们得到以下输出:

Class inited
Paulo
Jose
Problems calling reset, maybe not injected?
Jose

主要目标是再次制作这个印刷保罗(是的,有时我可能会过于自我中心,我知道!:P)

开始吧

我们要问自己的第一个问题是静态初始化器中发生了什么?为此,我们将使用javap获取Example的类字节码,并使用以下命令:

javap -c -l -v -p Example.class

如果您不熟悉开关,请快速记下开关.

> c:显示字节码
> l:显示局部变量表
> v:详细显示行表,异常表等
> p:包含私有方法

初始化程序的代码是(我剪切了所有其他内容):

 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: new           #1                  // class test7/Example
         3: dup
         4: ldc           #13                 // String __EX_1__
         6: invokespecial #15                 // Method "<init>":(Ljava/lang/String;)V
         9: putstatic     #19                 // Field ex1:Ltest7/Example;
        12: ldc           #21                 // String Paulo
        14: putstatic     #23                 // Field DEFAULT_NAME:Ljava/lang/String;
        17: getstatic     #25                 // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc           #31                 // String Class inited
        22: invokevirtual #33                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return
      LineNumberTable:
        line 4: 0
        line 5: 12
        line 10: 17
        line 11: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

看看这段代码,我们看到我们的目标是stackframe 9,其中putstatic进入我们实际知道的最终字段ex1,我们只对更改这些字段的写入感兴趣,仍应进行读取.

所以现在让我们在编码时运行你的注入器并再次检查字节码.对NEW_NAME()方法字节码进行波纹处理:

 public static void __NEW_NAME__();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: new           #1                  // class test7/Example
         3: dup
         4: ldc           #13                 // String __EX_1__
         6: invokespecial #15                 // Method "<init>":(Ljava/lang/String;)V
         9: putstatic     #19                 // Field ex1:Ltest7/Example;
        12: ldc           #21                 // String Paulo
        14: putstatic     #23                 // Field DEFAULT_NAME:Ljava/lang/String;
        17: getstatic     #25                 // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc           #31                 // String Class inited
        22: invokevirtual #33                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return
      LineNumberTable:
        line 4: 0
        line 5: 12
        line 10: 17
        line 11: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

正如预期的那样,Stackframe 9仍然存在.

好奇心:您是否知道字节码验证程序不会检查有关最终关键字的非法分配.这意味着您可以在没有“问题”的情况下运行此方法,狡猾吧?我说“问题”,因为如果你期望拥有某种永久状态的最终变量,你会遇到很多麻烦:)

好的,但回到正轨让我们最终重写你的注射器来做你想做的事.这是我修改的代码:

public class Injector {

 public static void main(String[] args) throws Throwable {
    CtClass cc = ClassPool.getDefault().get(Example.class.getName());
    CtConstructor staticConstructor = cc.getClassInitializer();
    if (staticConstructor != null) {
        CtConstructor staticConstructorClone = new CtConstructor(staticConstructor, cc, null);
        staticConstructorClone.getMethodInfo().setName("__NEW_NAME__");
        staticConstructorClone.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
        cc.addConstructor(staticConstructorClone);

        // Here's the trick :-)

        staticConstructorClone.instrument(new ExprEditor() {

            @Override
            public void edit(FieldAccess f) throws CannotCompileException {
                try {
                    if (f.isStatic() && f.isWriter() && Modifier.isFinal(f.getField().getModifiers())) {
                        System.out.println("Found field");
                        f.replace("{  }");
                    }
                } catch (NotFoundException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        });

        cc.writeFile("...);
    }
  }
}

在我们用一个编辑字段访问的ExprEditor克隆静态构造函数instrument之后.因此,每当我们发现一个字段访问是写入静态字段并且修饰符是最终的时,我们将代码替换为“{}”,它基本上转换为“什么都不做”.

运行新的注入器并检查字节码时,我们得到以下结果:

  public static void __NEW_NAME__();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=0
         0: new           #1                  // class test7/Example
         3: dup
         4: ldc           #13                 // String __EX_1__
         6: invokespecial #15                 // Method "<init>":(Ljava/lang/String;)V
         9: astore_1
        10: aconst_null
        11: astore_0
        12: ldc           #21                 // String Paulo
        14: putstatic     #23                 // Field DEFAULT_NAME:Ljava/lang/String;
        17: getstatic     #25                 // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc           #31                 // String Class inited
        22: invokevirtual #33                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return
      LineNumberTable:
        line 4: 0
        line 5: 12
        line 10: 17
        line 11: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

正如你所看到的,stackframe 9不再是一个putstatic而是astore_1,实际上javassist注入了3个新的stackframes,从9到11:

         9: astore_1
        10: aconst_null
        11: astore_0

现在如果我们再次运行Test类,我们得到以下输出:

Class inited
Paulo
Jose
Class inited
Paulo

结尾笔记

请记住,即使在这个沙箱中,某种情况下工作正常,但在现实世界中进行这种魔术时,由于意外情况,它可能会适得其反……你很可能需要创建一个更智能的ExprEditor来处理更多场景,但你的基点将是这个.

如果你真的可以实现resetState()方法,那将是一个更好的选择,但我很确定你可能无法做到这一点,这就是为什么你要研究字节码解决方案.

对不起,很长的帖子,但我想引导你完成我所有的思考过程.希望你觉得它有用.

标签:java,javassist
来源: https://codeday.me/bug/20190623/1270156.html