来自《Effective Java》的 ElvisStealer

4
这里是一个类窃取引用单例副本的参考,同时单例正在被反序列化。
public class ElvisStealer implements Serializable {

    static Elvis impersonator;
    private Elvis payload;

    private Object readResolve() {
        // Save a reference to the "unresolved" Elvis instance
        impersonator = payload;
        // Return an object of correct type for favorites field
        return new String[] { "A Fool Such as I" };
    }
    
    private static final long serialVersionUID = 0;
    
}

我的问题是:

  1. 在哪里以及如何获取Elvis对象引用的副本?这部分没有提供太多信息。

// Save a reference to the "unresolved" Elvis instance impersonator = payload;

换句话说,子对象的 readResolve 如何访问父对象引用?

  1. 为使ElvisStealer生效,我必须通过将类型为 String [] 的单例实例字段(在该情况下)替换为ElvisStealer实例来修改序列化数据。在书中,单例包含一个非 transient String [] 实例字段,并且在序列化流中将该字段替换为ElvisStealer实例。然后,当那个字段被反序列化时, ObjectInputStream 看到这个字段是ElvisStealer类型的,并从ElvisStealer类调用 readResolve 。我的问题是:为什么JVM在解析这样的字段时不会出错,明知道应该是String而不是ElvisStealer,其次,为什么JVM调用ElvisStealer类中的 readResolve ,明知道应该是 String [] ,而不是ElvisStealer。

  2. 为什么ElvisStealer除了静态字段外还包含一个类型为Elvis的实例字段?难道静态字段不够吗?

1个回答

2

反序列化始终会从ObjectInputStream中提取出字节并新创建实例。

在此步骤之后,您将拥有如下的所有新实例:

Elvis(new).favoriteSongs = ElvisStealer(new)
ElvisStealer(new).payload = Elvis(new)   // same elvis, circular reference

在第二步中,反序列化使用那些实例的readResolve方法将初步反序列化的对象“解析”为它们的最终形式。但是它从内部的ElvisStealer开始。
Elvis(new).favoriteSongs = ElvisStealer(new).readResolve()
=> Elvis(new).favoriteSongs = String[] { .... }

下一步是解决Elvis实例。
result = Elvis(new).readResolve()
=> result = Elvis(INSTANCE)

正确的类型(String[]而不是实际上无效的ElvisStealer)只需要在readResolve步骤之后出现。

有这个中间的“无效”阶段是有用的。您可以在writeReplace方法中声明,您想要序列化的是另一个不同的对象,然后在该对象的readResolve方法中编写代码,生成定位正确的对象。

例如,在您拥有以下内容时:

class ComplexThing implements Serializable {
    private Object writeReplace() throws ObjectStreamException {
        return new SimpleHiddenReplacement();
    }
}
private class SimpleHiddenReplacement implements Serializable {
    private Object readResolve() throws ObjectStreamException {
        return new ComplexThing();
    }
}

你可以将 ComplexThing 传递给 ObjectOutputStream,并从 ObjectInputStream 中获取一个 ComplexThing,但在幕后,这些流操作的字节实际上是 SimpleHiddenReplacement 的表示形式。
Elvis stealing 攻击会在 readResolve 方法(即未解析它)有机会替换(解析)它之前窃取新创建的 Elvis

ElvisStealer(new).payload = Elvis(new)。这发生在哪里?我只看到了一个 payload 变量的声明,但它没有以任何方式初始化。

书中说:

首先,编写一个“stealer”类,具有 [..] 引用“隐藏”在其中的序列化单例的实例字段。在序列化流中,使用 stealer 的实例替换单例的非瞬态字段。

这个设置以一个精心制作的 byte[] serializedForm 的形式出现。它是一个假的 Elvis 对象的序列化形式,与普通的序列化 Elvis 不同,它包含一个带有对 Elvis 对象的反向引用的 stealer。

其次,静态的 impersonator 不应该足够吗?

不,序列化不会处理静态变量,只有实例字段。这种攻击依赖于反序列化对变量的初始化,而反序列化之所以这样做是因为序列化形式中有一个值。


谢谢,这解决了第二个问题。回到第一个和第三个问题。你写道:“ElvisStealer(new).payload = Elvis(new) //同一个Elvis,循环引用”。这在书中发生在哪里?我只看到了payload变量的声明,但它没有以任何方式初始化。其次,静态变量impersonator不应该足够吗?而不是初始化payload,可以使用用于初始化payload的值来初始化impersonator吗? - ctomek
@ctomek在问题中添加了一点内容。那有帮助吗? - zapl
啊,好的,我错过了在序列化流中初始化“payload”的部分。我只看到这种机制将原始的“String”实例变量替换为“ElvisStealer”对象,并且我不知道何时在代码中初始化“payload”。但是它不是在代码中初始化的,而是在序列化流中,通过将原始的“Elvis”单例粘贴为“payload”值来初始化。是这样吗?我感到困惑,因为书中没有清楚地描述。 - ctomek
结束这个话题之前,请确认我的描述是否正确。;) - ctomek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接