Java谜题5:球(ball)-解决方案


如果你不能投球,那就没什么乐趣了。但扩展Throwable也使它实现了可序列化,这就是真正有趣的开始。使用序列化,我们可以创建一个球,该球应该被捕获的次数与序列化数据声明的次数相同。
这场比赛似乎破坏了乐趣。你不能抛出它;但更重要的是你不能序列化它。如果您试图直接将球序列化,它也将尝试序列化它所附加到的游戏,从而导致NotSerializableException。
请注意,从技术上讲,从球到其比赛的参考只是一个类似于 this$0的字段。我说的“附加”是指指定给那个字段。因此,这个问题相当于序列化一个具有不可序列化的正常字段的对象。
问题在于我们根本不需要序列化游戏和球。我们只需要将球反序列化,并将其附加到游戏中。该Game实例不需要直接来自序列化流。我们可以在它的位置放置替代品,并覆盖readResolve,在它附着到球之前用一个game替换它。

在实践中作弊
有多种方法可以创建包含附加到替代游戏的球的原始数据(字节数组)。我们创建了一个类似于ball(ba)的类。我们给它和ball一样的serialversionuid和我们期望的caught值。它附加到我们的替代实现readResolve(Player)上。我们将其序列化,并将ba类的名称替换为ball类。将byte[]转换为一个String并返回,允许我们使用string.replace。
选择ba这个名称是为了让play.player$ba的长度与game.game$ball的长度相同。否则,直接用一个替换另一个会损坏流。

package play;
 
import game.Game;
import game.Game.Ball;
 
import java.io.*;
 
public class Player implements Serializable {
 
    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(new Player().new Ba());
        byte[] bytes = new String(bos.toByteArray(), "ISO-8859-1")
                .replace(
"play.Player$Ba", "game.Game$Ball")
                .getBytes(
"ISO-8859-1");
        Ball ball = (Ball) new ObjectInputStream(new ByteArrayInputStream(bytes))
                .readObject();
        ball.caught();
    }
 
    class Ba implements Serializable {
        static final long serialVersionUID = -7172046060844866133L;
 
        private long caught = -1;
    }
 
    Object readResolve() {
        return new Game();
    }
}

这就是发生的情况:

  • 在调用时readObject(),首先Ball获取反序列化,然后caught设置为-1。
  • Ball.this$0反序列化的值是一个实例Player。
  • 在Player分配给该字段之前(因为它的类型错误,它将失败),readResolve调用其方法,创建一个新Game的score0
  • 这Game被分配给Ball.this$0,readObject()返回Ball。
  • ball.catched()是用catched=-1和这个$0.score==0来调用的,你作弊就被抓住了!

结论
创建具有对不可序列化对象的引用的可序列化对象是一个坏主意,因为您无法对它们进行序列化。但是,你仍然可以反序列化它们。
Java序列化充满了令人讨厌的意外可能性。关于这一点你可以做一系列的谜题。但是,如果你真的陷入此种境地,那么你需要做的就是查看过去几年的JDK安全漏洞。