Java謎題5:球(ball)-解決方案

jdon發表於2019-09-20

如果你不能投球,那就沒什麼樂趣了。但擴充套件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安全漏洞。

相關文章