In Java is it possible to change or modify an enum itself and thus to corrupt an enum singleton?

后端 未结 2 1046
抹茶落季
抹茶落季 2021-01-19 12:33

Is it possible to change an enum itself at run-time somehow? E.g. using reflection. The question is not about to change the state of an enum constant. It\'s about to change

2条回答
  •  既然无缘
    2021-01-19 12:58

    I started my analysis disassembling the enum Color using javap -c. Here is an excerpt from:

     static {};
        Code:
           0: new           #1                  // class playground/Color
           3: dup           
           4: ldc           #14                 // String RED
           6: iconst_0      
           7: invokespecial #15                 // Method "":(Ljava/lang/String;I)V
          10: putstatic     #19                 // Field RED:Lplayground/Color;
          13: new           #1                  // class playground/Color
          16: dup           
          17: ldc           #21                 // String GREEN
          19: iconst_1      
          20: invokespecial #15                 // Method "":(Ljava/lang/String;I)V
          23: putstatic     #22                 // Field GREEN:Lplayground/Color;
          26: new           #1                  // class playground/Color
          29: dup           
          30: ldc           #24                 // String BLUE
          32: iconst_2      
          33: invokespecial #15                 // Method "":(Ljava/lang/String;I)V
          36: putstatic     #25                 // Field BLUE:Lplayground/Color;
          39: iconst_3      
          40: anewarray     #1                  // class playground/Color
          43: dup           
          44: iconst_0      
          45: getstatic     #19                 // Field RED:Lplayground/Color;
          48: aastore       
          49: dup           
          50: iconst_1      
          51: getstatic     #22                 // Field GREEN:Lplayground/Color;
          54: aastore       
          55: dup           
          56: iconst_2      
          57: getstatic     #25                 // Field BLUE:Lplayground/Color;
          60: aastore       
          61: putstatic     #27                 // Field ENUM$VALUES:[Lplayground/Color;
          64: return        
    

    At index 61 we see the three enum constants being assigned to a static array with name ENUM$VALUES.

    Listing all static fields by reflection ...

    Field[] declaredFields = Color.class.getDeclaredFields();
    for (Field field : declaredFields) {
      if (Modifier.isStatic(field.getModifiers())) {
        System.out.println(field.getName() + ": " + field.getType());
      }
    }
    

    shows the enum constants and reveals the array:

    RED: class playground.ReflectEnum$Color
    GREEN: class playground.ReflectEnum$Color
    BLUE: class playground.ReflectEnum$Color
    ENUM$VALUES: class [Lplayground.ReflectEnum$Color;
    

    I defined the following method to get the enum array:

      protected static > E[] getEnumsArray(Class ec) throws Exception {
        Field field = ec.getDeclaredField("ENUM$VALUES");
        field.setAccessible(true);
        return (E[]) field.get(ec);
      }
    

    Using it it's possible to change the order of the enum constants:

    Color[] colors = getEnumsArray(Color.class);
    colors[0] = Color.GREEN;
    colors[1] = Color.RED;
    colors[2] = Color.BLUE;
    

    Listing the enum constants

    for (Color color : Color.values()) {
      System.out.println(action + ":" + color.ordinal());
    }
    

    shows:

    GREEN:1
    RED:0
    BLUE:2
    

    Obviously the order has been changed.

    Since it is possible to assign values to the array it is also valid to assign null.

    Color[] colors = getEnumsArray(Color.class);
    colors[0] = Color.GREEN;
    colors[1] = Color.RED;
    colors[2] = null;
    

    Listing the enum constants shows:

    GREEN:1
    RED:0
    Exception in thread "main" java.lang.NullPointerException
        at playground.ReflectEnum.main(ReflectEnum.java:57)
    

    And if we try look up the enum constants by their names

    System.out.println(Color.valueOf("GREEN"));
    System.out.println(Color.valueOf("RED"));
    System.out.println(Color.valueOf("BLUE"));
    

    we see that the last change broke even more:

    Exception in thread "main" java.lang.NullPointerException
        at java.lang.Class.enumConstantDirectory(Class.java:3236)
        at java.lang.Enum.valueOf(Enum.java:232)
        at playground.Color.valueOf(Color.java:1)
        at playground.ReflectEnum.main(ReflectEnum.java:48)
    

    The line ReflectEnum.java:48 contains the above print statement of Color.valueOf("GREEN"). This enum constant was not set to null. So it broke the valueOf method of Color completely.

    But Enum.valueOf(Color.class, "BLUE") still resolves the enum constant Color.BLUE.

    Since the enum array is declared as static final I followed Change private static final field using Java reflection to create and set a new enum array.

      protected static > void setEnumsArray(Class ec, E... e) throws Exception {
        Field field = ec.getDeclaredField("ENUM$VALUES");
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        field.setAccessible(true);
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(ec, e);
      }
    

    But executing setEnumsArray(Color.class, Color.BLUE, Color.GREEN, Color.RED, Color.BLUE) fails with

    Exception in thread "main" java.lang.IllegalAccessException: Can not set static final [Lplayground.Color; field playground.Color.ENUM$VALUES to [Lplayground.Color;
        at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
        at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
        at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)
        at java.lang.reflect.Field.set(Field.java:758)
        at playground.ReflectEnum.setEnumsArray(ReflectEnum.java:76)
        at playground.ReflectEnum.main(ReflectEnum.java:37)
    

    EDIT 1 (after comments of Radiodef):

    After I added the following two methods ...

      protected static Field getEnumsArrayField(Class ec) throws Exception {
        Field field = ec.getDeclaredField("ENUM$VALUES");
        field.setAccessible(true);
        return field;
      }
    
      protected static void clearFieldAccessors(Field field) throws ReflectiveOperationException {
        Field fa = Field.class.getDeclaredField("fieldAccessor");
        fa.setAccessible(true);
        fa.set(field, null);
    
        Field ofa = Field.class.getDeclaredField("overrideFieldAccessor");
        ofa.setAccessible(true);
        ofa.set(field, null);
    
        Field rf = Field.class.getDeclaredField("root");
        rf.setAccessible(true);
        Field root = (Field) rf.get(field);
        if (root != null) {
          clearFieldAccessors(root);
        }
    

    i tried this:

    System.out.println(Arrays.toString((Object[]) getEnumsArrayField(Color.class).get(null)));
    clearFieldAccessors(getEnumsArrayField(Color.class));
    setEnumsArray(Color.class, Color.BLUE, Color.GREEN, Color.RED, Color.BLUE);
    System.out.println(Arrays.toString(Color.values()));
    

    This shows:

    [RED, GREEN, BLUE]
    [BLUE, GREEN, RED, BLUE]
    

    The enum array has been replaced by another one.


    EDIT 2 (after comments of GotoFinal)
    According to the answer of GotoFinal on How to create an instance of enum using reflection in java? it's possible to create further enum instances at run-time. Then it should be possible to replace an enum singleton instance by another one. I have the following enum singleton:

      public enum Singleton {
    
        INSTANCE("The one and only");
    
        private String description;
    
        private Singleton(String description) {
          this.description = description;
        }
    
        @Override
        public String toString() {
          return description;
        }
    
      }
    

    Reusing the code GotoFinal has shown I defined the following method:

      protected static Singleton createEnumValue(String name, int ordinal, String description) throws Exception {
        Class monsterClass = Singleton.class;
        Constructor constructor = monsterClass.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
    
        Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor");
        constructorAccessorField.setAccessible(true);
        sun.reflect.ConstructorAccessor ca = (sun.reflect.ConstructorAccessor) constructorAccessorField.get(constructor);
        if (ca == null) {
          Method acquireConstructorAccessorMethod = Constructor.class.getDeclaredMethod("acquireConstructorAccessor");
          acquireConstructorAccessorMethod.setAccessible(true);
          ca = (sun.reflect.ConstructorAccessor) acquireConstructorAccessorMethod.invoke(constructor);
        }
        Singleton enumValue = (Singleton) ca.newInstance(new Object[] { name, ordinal, description });
        return enumValue;
      }
    
     protected static > void setFinalField(Class ec, Field field, E e) throws NoSuchFieldException, IllegalAccessException {
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        field.setAccessible(true);
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(ec, e);
      }
    

    Now executing

    System.out.println(Singleton.INSTANCE.toString());
    // setting INSTANCE = theNewOne
    Singleton theNewOne = createEnumValue(Singleton.INSTANCE.name(), Singleton.INSTANCE.ordinal(), "The new one!");
    setFinalField(Singleton.class, Singleton.class.getDeclaredField(Singleton.INSTANCE.name()), theNewOne);
    System.out.println(Singleton.INSTANCE.toString());
    // setting enum array = [theNewOne]
    clearFieldAccessors(getEnumsArrayField(Singleton.class));
    setEnumsArray(Singleton.class, theNewOne);
    System.out.println(Arrays.toString(Singleton.values()));
    

    shows:

    The one and only
    The new one!
    [The new one!]
    

    Summarizing:

    • It's possible to modify an enum at run-time and to replace the enum array by another one. But at least setting an enum constant to null breaks the consistency of defined enums within the VM. Though this can be fixed according to GotoFinal's answer.

    • When implementing a singleton using enum it's possible to replace the only one instance by another enum instance - at least for some JDKs/JREs knowing their implementations. If the enum constructor has arguments they can be exploited by the new created enum instance to plant malicious behaviour.

提交回复
热议问题