在Java中是否可以更改或修改枚举本身,从而破坏枚举单例?

LuC*_*Cio 4 java reflection singleton enums

是否有可能以某种方式在运行时更改枚举?例如使用反射.问题不是要改变枚举常量的状态.它将要更改枚举的常量集或删除任何常量.

关于以下枚举,是否可以添加颜色WHITE或删除颜色RED或更改其顺序?

public enum Color {

  RED, GREEN, BLUE;

}
Run Code Online (Sandbox Code Playgroud)

我为什么这么问?

我知道这个问题有点恶意.但即使是约书亚布洛赫在谈论(1)关于实施单身人士并推荐了枚举单身人士模式时也提到了" 巧妙制造的攻击 ".如果我们可以修改枚举,那么对这种模式的攻击是否可行?

我试图解决它并部分管理它.我将发布我的结果作为答案 - 遵循这个建议.


(1)请参阅在Java中实现单例模式的有效方法是什么?其中包含指向effective_java_reloaded.pdf的链接,第31页.

LuC*_*Cio 5

我开始分析Color使用enum 来拆解enum javap -c.以下摘录自:

 static {};
    Code:
       0: new           #1                  // class playground/Color
       3: dup           
       4: ldc           #14                 // String RED
       6: iconst_0      
       7: invokespecial #15                 // Method "<init>":(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 "<init>":(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 "<init>":(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        
Run Code Online (Sandbox Code Playgroud)

在索引61处,我们看到三个枚举常量被分配给具有名称的静态数组ENUM$VALUES.

通过反射列出所有静态字段......

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

显示枚举常量并显示数组:

RED: class playground.ReflectEnum$Color
GREEN: class playground.ReflectEnum$Color
BLUE: class playground.ReflectEnum$Color
ENUM$VALUES: class [Lplayground.ReflectEnum$Color;
Run Code Online (Sandbox Code Playgroud)

我定义了以下方法来获取枚举数组:

  protected static <E extends Enum<E>> E[] getEnumsArray(Class<E> ec) throws Exception {
    Field field = ec.getDeclaredField("ENUM$VALUES");
    field.setAccessible(true);
    return (E[]) field.get(ec);
  }
Run Code Online (Sandbox Code Playgroud)

使用它可以改变枚举常量的顺序:

Color[] colors = getEnumsArray(Color.class);
colors[0] = Color.GREEN;
colors[1] = Color.RED;
colors[2] = Color.BLUE;
Run Code Online (Sandbox Code Playgroud)

列出枚举常量

for (Color color : Color.values()) {
  System.out.println(action + ":" + color.ordinal());
}
Run Code Online (Sandbox Code Playgroud)

说明:

GREEN:1
RED:0
BLUE:2
Run Code Online (Sandbox Code Playgroud)

显然订单已经改变了.

由于可以为数组赋值,因此分配也是有效的null.

Color[] colors = getEnumsArray(Color.class);
colors[0] = Color.GREEN;
colors[1] = Color.RED;
colors[2] = null;
Run Code Online (Sandbox Code Playgroud)

列出枚举常量显示:

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

如果我们尝试按名称查找枚举常量

System.out.println(Color.valueOf("GREEN"));
System.out.println(Color.valueOf("RED"));
System.out.println(Color.valueOf("BLUE"));
Run Code Online (Sandbox Code Playgroud)

我们看到最后的变化更加突破:

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)
Run Code Online (Sandbox Code Playgroud)

该行ReflectEnum.java:48包含上述打印声明Color.valueOf("GREEN").此枚举常量未设置为null.所以它打破了完全的valueOf方法Color.

Enum.valueOf(Color.class, "BLUE")仍然解决了枚举常数Color.BLUE.

由于枚举数组声明为static final我遵循使用Java反射更改私有静态最终字段以创建和设置新的枚举数组.

  protected static <E extends Enum<E>> void setEnumsArray(Class<E> 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);
  }
Run Code Online (Sandbox Code Playgroud)

但执行setEnumsArray(Color.class, Color.BLUE, Color.GREEN, Color.RED, Color.BLUE)失败了

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)
Run Code Online (Sandbox Code Playgroud)

编辑1(在Radiodef的评论之后):

之后我添加了以下两种方法......

  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);
    }
Run Code Online (Sandbox Code Playgroud)

我试过这个:

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()));
Run Code Online (Sandbox Code Playgroud)

由此可见:

[RED, GREEN, BLUE]
[BLUE, GREEN, RED, BLUE]
Run Code Online (Sandbox Code Playgroud)

枚举数组已被另一个替换.


编辑2(在GotoFinal的评论之后)
根据GotoFinal关于如何在java中使用反射创建枚举实例答案可以在运行时创建更多枚举实例.然后应该可以用另一个实例替换枚举单例实例.我有以下枚举单例:

  public enum Singleton {

    INSTANCE("The one and only");

    private String description;

    private Singleton(String description) {
      this.description = description;
    }

    @Override
    public String toString() {
      return description;
    }

  }
Run Code Online (Sandbox Code Playgroud)

重用代码GotoFinal已经显示我定义了以下方法:

  protected static Singleton createEnumValue(String name, int ordinal, String description) throws Exception {
    Class<Singleton> 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 <E extends Enum<E>> void setFinalField(Class<E> 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);
  }
Run Code Online (Sandbox Code Playgroud)

现在执行

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()));
Run Code Online (Sandbox Code Playgroud)

说明:

The one and only
The new one!
[The new one!]
Run Code Online (Sandbox Code Playgroud)

总结:

  • 可以在运行时修改枚举,并用另一个修改枚举数组.但至少设置一个枚举常量来null打破VM中定义的枚举的一致性.虽然这可以根据GotoFinal的答案修复.

  • 当使用enum它实现单例时,可以用另一个枚举实例替换唯一的一个实例 - 至少对于知道它们的实现的一些JDK/JRE.如果枚举构造函数具有参数,则新创建的枚举实例可以利用这些参数来植入恶意行为.

  • 有可能创建新的枚举常量而没有任何"更大"的问题,并且有很多反射.我的例子:https://blog.gotofinal.com/java/diorite/breakingjava/2017/06/24/dynamic-enum.html因为我不能将整个代码放到评论中 (2认同)

Got*_*nal 5

是的,您甚至可以毫无问题地向枚举添加新值,正如我已经在此处说明的那样:https : //stackoverflow.com/a/51244909/4378853

因此,您可以通过多种方法来中断枚举:
1.使用反射将诸如Color.RED之类的字段值更改为其他枚举值。
2.使用反射通过修改ENUM$VALUES字段来更改值。
3.使用反射MyEnum.class.getEnumConstants()通过T[] enumConstantsClass类中进行编辑来更改返回的内容。
4. Map<String, T> enumConstantDirectoryClass类中类似-用于Enum.valueOf(MyEnum.class, name)MyEnum.valueOf(name)
5.使用反射添加新的枚举常量,如上面我的链接答案所述。

因此对于

关于以下枚举,是否可以添加白色或删除红色或更改其顺序?

对的,这是可能的。

Class<Color> enumClass = Color.class;
// first we need to find our constructor, and make it accessible
Constructor<?> constructor = enumClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);

// this is this same code as in constructor.newInstance, but we just skipped all that useless enum checks ;)
Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor");
constructorAccessorField.setAccessible(true);
// sun.reflect.ConstructorAccessor -> iternal class, we should not use it, if you need use it, it would be better to actually not import it, but use it only via reflections. (as package may change, and will in java 9)
ConstructorAccessor ca = (ConstructorAccessor) constructorAccessorField.get(constructor);
if (ca == null) {
    Method acquireConstructorAccessorMethod = Constructor.class.getDeclaredMethod("acquireConstructorAccessor");
    acquireConstructorAccessorMethod.setAccessible(true);
    ca = (ConstructorAccessor) acquireConstructorAccessorMethod.invoke(constructor);
}
// note that real constructor contains 2 additional parameters, name and ordinal
Color enumValue = (Color) ca.newInstance(new Object[]{"WHITE", 3});// you can call that using reflections too, reflecting reflections are best part of java ;)


static void makeAccessible(Field field) throws Exception {
    field.setAccessible(true);
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~ Modifier.FINAL);
}
Run Code Online (Sandbox Code Playgroud)

然后只需将该字段更改为包含我们新字段的新值即可:

Field valuesField = Color.class.getDeclaredField("$VALUES");
makeAccessible(valuesField);
// just copy old values to new array and add our new field.
Color[] oldValues = (Color[]) valuesField.get(null);
Color[] newValues = new Color[oldValues.length + 1];
System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);
newValues[oldValues.length] = enumValue;
valuesField.set(null, newValues);
Run Code Online (Sandbox Code Playgroud)

现在还使用反射来更新Class类中的两个字段。

Field enumConstantDirectoryField = Class.class.getDeclaredField("enumConstantDirectory");
enumConstantDirectoryField.setAccessible(true);
enumConstantDirectoryField.set(Color.class, null);
Field enumConstantsField = Class.class.getDeclaredField("enumConstants");
enumConstantsField.setAccessible(true);
enumConstantsField.set(Color.class, null);
Run Code Online (Sandbox Code Playgroud)

要更改订单,只需使用与$VALUES字段编辑相同的代码,但只需将其设置为所需的任何代码即可。

您不仅可以添加/删除诸如make Color.WHITEwork或make Color.REDmissing之类的字段(但可以将其设置为null)

同样使用不安全的方法,应该可以声明新的类来创建抽象枚举类的新实例。
我使用javassist库来减少生成新类所需的代码:

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(MyEnum.VALUE.getSomething()); // prints 5

        ClassPool classPool = ClassPool.getDefault();
        CtClass enumCtClass = classPool.getCtClass(MyEnum.class.getName());
        CtClass ctClass = classPool.makeClass("com.example.demo.MyEnum$2", enumCtClass);

        CtMethod getSomethingCtMethod = new CtMethod(CtClass.intType, "getSomething", new CtClass[0], ctClass);
        getSomethingCtMethod.setBody("{return 3;}");
        ctClass.addMethod(getSomethingCtMethod);

        Constructor<?> unsafeConstructor = Unsafe.class.getDeclaredConstructors()[0];
        unsafeConstructor.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeConstructor.newInstance();

        MyEnum newInstance = (MyEnum) unsafe.allocateInstance(ctClass.toClass());
        Field singletonInstance = MyEnum.class.getDeclaredField("VALUE");
        makeAccessible(singletonInstance);
        singletonInstance.set(null, newInstance);

        System.out.println(MyEnum.VALUE.getSomething()); // prints 3
    }

    static void makeAccessible(Field field) throws Exception {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~ Modifier.FINAL);
    }
}

enum MyEnum {
    VALUE {
        @Override
        public int getSomething() {
            return 5;
        }
    };

    public abstract int getSomething();
}
Run Code Online (Sandbox Code Playgroud)

这将先打印5,然后打印3。请注意,这无法枚举不包含子类的类-因此,如果没有任何覆盖的方法,则枚举将被声明为最终类。

而且,您可以使用它来打破单例模式而没有任何问题,只需将其设置为null,或者甚至可以尝试在运行时使用不安全的方法来实现自己的版本并替换其实例,或者只是创建它的多个实例。
这就是为什么我只喜欢带有私有构造函数和拥有者类的简单类-如果有人想打破它,他们还是会这么做。但是至少您不对没有设计用途的东西使用枚举,因此代码更易于阅读。