Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a clone of a static constructor with Javassist

Tags:

java

javassist

It seems that Javassist's API allows us to create an exactly copy of the class initializer (ie., static constructor) declared in a class:

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);
}

however, that copy also includes (public/private) static final fields. for example, the static constructor of the following class:

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

  private String name;

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

is in fact:

static {
  Example.ex1 = "__NAME__";
}

and therefore, an exactly copy of the static constructor will also include that call to the final field "name".

Is there any way of creating a copy of a static constructor which does not include calls to final fields?

-- Thanks

like image 775
josecampos Avatar asked Feb 21 '16 16:02

josecampos


1 Answers

Introduction

Being your drive resetting the static state of the class but removing the final fields the key to do it is the ExprEditor class. This class basically allows you to easily transform certain operations using Javassist's highlevel API instead of you having to bother with all the bytecode.

Even though we'll do all this work in the highlevel API I'll still dump a bit of bytecode so we can see the changes at that level as well.

Working base

Let's grab your Example class but with a twist:

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;
 }
}

I've added a static field that is not final, so we can change it and we should be able to reset it. I also added a static block with some code, in this case it's only a System.out but keep in mind that other classes may have code that is not meant to run more than once and you might find yourself debugging strange behaviors (but I'm sure you're probably aware of that).

To test our modification I've also created a test class with the following code:

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[] {});
   }
}

If we run this class out of the box, we get the following output:

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

The main goal is to make this print Paulo again (yes, sometimes I can be way too self centered I know! :P)

Let's start

The first question we have to ask ourselves is what is happening in the static initializer? For that we'll use javap to get Example's class bytecode, with the following command:

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

Quick note on the switches if you're not used to them.

  • c: shows the bytecode
  • l: shows the local variable table
  • v: be verbose shows line table, exception table, etc
  • p: include private methods

The code for the initializer is (I clipped everything else out):

 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

Looking at this code we see that our target is stackframe 9 where a putstatic is done into the field ex1 that we actually know is final, we are only interested in changing writes to those fields, reads should still be made.

So now let's run your injector as you coded it and check the bytecode again. Bellow the NEW_NAME() method bytecode:

 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 is still there, as expected.

Curiosity: Did you know that the bytecode verifier does not check for illegal assignments regarding final keyword. This means that you can already run this method without "problems", dodgy right? I say "problems" because if you're expecting to have that final variable with some kind of permanent state you'll be in a lot of troubles :)

Ok, but back on track let's finally rewrite your injector to do what you want to. Here's your code with my modification:

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("...);
    }
  }
}

After we've cloned the static constructor we instrument with an ExprEditor that edits field accesses. So whenever we find a field access that is a writting to a static field and the modifiers are final, we replace the code by " { } " which basically translates to "do nothing".

When running the new injector and checking the bytecode we get the following:

  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

As you can see, stackframe 9 no longer is a putstatic but rather astore_1, actually javassist injected 3 new stackframes, from 9 to 11:

         9: astore_1
        10: aconst_null
        11: astore_0

And now if we run the Test class again we get the following output:

Class inited
Paulo
Jose
Class inited
Paulo

Ending notes

Keep in mind that even though in this sandbox kind of scenario things work, when doing this kind of magic in the real world it can backfire due to unexpected situations... It's very possible that you might need to create a smarter ExprEditor, to handle more scenarios, but your base point will be this.

If you could actually implement resetState() methods that would be a better option, but I'm pretty sure you probably won't be able to do it and that's why you are looking into bytecode solutions.

Sorry for the long post, but I wanted to guide you through all my thought process. Hope you find it helpful.

like image 95
pabrantes Avatar answered Sep 30 '22 10:09

pabrantes