【问题标题】:In Java is it possible to change or modify an enum itself and thus to corrupt an enum singleton?在 Java 中是否可以更改或修改枚举本身,从而破坏枚举单例?
【发布时间】:2018-07-07 13:21:17
【问题描述】:

是否可以在运行时以某种方式更改枚举本身?例如。使用反射。问题不在于改变枚举常量的状态。即将更改枚举的常量集或删除任何常量。

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

public enum Color {

  RED, GREEN, BLUE;

}

我为什么要问?

我知道这个问题有点恶意。但即使是 Joshua Bloch 在谈到 (1) 实现单例时也提到了“巧妙的攻击”,并推荐了枚举单例模式。如果我们可以修改一个枚举,那么对这种模式的攻击是否可行?

我试图解决它并部分管理它。我将发布我的结果作为答案 - 在此 advice 之后。


(1) 请参阅What is an efficient way to implement a singleton pattern in Java? 链接到 effective_java_reloaded.pdf,第 31 页。

【问题讨论】:

  • 在这个问题中查看我新编辑的答案。我还添加了使用覆盖方法添加枚举实例的示例。

标签: java reflection enums singleton


【解决方案1】:

我开始分析使用javap -c 反汇编枚举Color。以下是摘录:

 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        

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

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

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

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

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

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

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

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

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

列出枚举常量

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

显示:

GREEN:1
RED:0
BLUE:2

显然顺序已经改变了。

因为可以给数组赋值,所以赋值null也是有效的。

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

列出枚举常量显示:

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

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

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

我们看到最后的变化更加严重:

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)

ReflectEnum.java:48 行包含上述Color.valueOf("GREEN") 的打印语句。此枚举常量未设置为 null。所以它彻底打破了ColorvalueOf方法。

Enum.valueOf(Color.class, "BLUE") 仍然解析枚举常量Color.BLUE

由于枚举数组被声明为static final,我跟随Change private static final field using Java reflection创建并设置了一个新的枚举数组。

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

但执行 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)

编辑 1(在Radiodef 的 cmets 之后):

我添加了以下两个方法后...

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

我试过这个:

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

这表明:

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

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


编辑 2(在GotoFinal 的 cmets 之后)
根据How to create an instance of enum using reflection in java? 上GotoFinal 的answer,可以在运行时创建更多枚举实例。然后应该可以将一个枚举单例实例替换为另一个。我有以下枚举单例:

  public enum Singleton {

    INSTANCE("The one and only");

    private String description;

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

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

  }

重用代码 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);
  }

正在执行

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

显示:

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

总结:

  • 可以在运行时修改枚举并将枚举数组替换为另一个。但至少将枚举常量设置为null 会破坏 VM 中定义的枚举的一致性。虽然这可以根据 GotoFinal 的回答来解决。

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

【讨论】:

  • 对于IllegalAccessException,您可以尝试使用this code 清除缓存的字段访问器。您也可以在首次使用 Field 实例之前删除 final 修饰符。
  • @Radiodef 我尝试了链接答案中提到的方法clearFieldAccessors,但仍然抛出了IllegalAccessException。尽管如此,还是感谢您的提示。
  • 这里有一个例子表明它可以工作:ideone.com/JhBCb2。如果您发布完整代码,我也许可以帮助您找到问题。
  • @Radiodef 谢谢你的例子。我更新了我的答案。
  • 可以创建新的枚举常量而没有任何“更大”的问题,ofc 有很多反射。我的例子:blog.gotofinal.com/java/diorite/breakingjava/2017/06/24/… 因为我不会将整个代码放入评论中
【解决方案2】:

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

所以你有多种方法可以打破枚举:
1. 使用反射将 Color.RED 等字段的值更改为其他枚举值。
2.通过修改ENUM$VALUES字段,使用反射来改变.values。
3. 通过在Class 类中编辑T[] enumConstants,使用反射来更改MyEnum.class.getEnumConstants() 返回的内容。
4. 与Class 类中的Map&lt;String, T&gt; enumConstantDirectory 类似——用于Enum.valueOf(MyEnum.class, name)MyEnum.valueOf(name)
5.使用反射添加新的枚举常量,如我上面链接的答案中所述。

所以对于

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

是的,有可能。

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

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

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

现在还使用反射来更新 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);

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

您不能只添加/删除诸如使Color.WHITE 工作或使Color.RED 消失的字段(但您可以将其设置为空)

同样使用 unsafe 应该可以声明新类来创建抽象枚举类的新实例。
我使用 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();
}

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

您可以使用它来打破单例模式而不会出现任何问题,只需将其设置为 null,或者您甚至可以尝试使用 unsafe 在运行时实现它自己的版本并替换它的实例,或者只是创建多个实例它。
这就是为什么我更喜欢带有私有构造函数和持有者类的简单类 - 如果有人想破坏它,他们无论如何都会这样做。但至少你不会将 enum 用于它不是为它设计的东西,所以代码更容易阅读。

【讨论】:

  • 非常好的示例如何在运行时更改实例以及如何立即提供功能 - 而功能可以根据运行的系统动态采用。 PS:我已经为这个答案投票了。不能再投票了;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-08
  • 2020-04-21
  • 1970-01-01
相关资源
最近更新 更多