Javaでシングルトンを実装する場合はEnum型(列挙型)が良いらしい

f:id:ts0818:20191030231019p:plain

Higman Sims Graph drawing, based on the construction of Paul R. Hafner: "On the Graphs of Hoffman-Singleton and Higman-Sims", The Electronic Journal of combinatorics 11 (2004). 

ファイル:Higman Sims Graph.svg - Wikipedia

⇧ 「ホフマン–シングルトングラフ」を元にしとるそうな。

ホフマン–シングルトングラフとは、50個の頂点と175個の辺からなる7-正則グラフである。これは(50,7,0,1)-強正則グラフであり一意である。このグラフはアラン・ホフマンとロバート・シングルトンによって、ムーアグラフの分類の過程で構成された。またホフマン–シングルトングラフは知られているムーアグラフの中でもっとも頂点数が多いグラフである。 次数7のムーアグラフであることから、内周は5であり、(7,5)-ケージとなる。

ホフマン–シングルトングラフ - Wikipedia

う~ん、目的は分かりませんが、綺麗ですね~、お美しい。あ、どうもボクです。

というわけで、今回も、関係ない話で開幕ですが、ここからは、Java の話ですから~、レッツトライ~。 

 

シングルトンを盗めるはずらしいんだけど...

「Effective Java 第3版」を読んでいて、掲載されているプログラミング通りやってるんだけど、どうやっても不可能な気がしてならんって個所がでてきたんだけどね...

どういうことかというと、

qiita.com

⇧  上記サイト様にありますが、「12章 89項」にある「89.インスタンス制御に対しては、readResolve より enum 型を選ぶべし」って件なのですが。

この章で言及してるのは、「シリアライズ」についてなんですが、Javaの場合、「Serializable」インターフェイスを implements すれば「シリアライズ」を実現できるらしいんですが、あんまりオススメではなく、

なんかを使えるんであれば、使ってくれということらしい。

何で、「Serializable」「JSON」「Protocol Buffers」なんかが必要かというと、Javaのようなオブジェクトなんかの情報を、外部のシステムと連携したい時に、オブジェクトの形のままだと連携できないからと。

逆に、ある形のデータを、Javaのようなオブジェクトの形のデータに整形してあげることを、「デシリアライズ」っていうらしい。

他のサーバサイド言語とかでも、オブジェクトとかの概念があれば、同様の仕組みがあるんではないかと。

OWSAP(Open Web Application Security Project)によりますと、

What is Deserialization?

Serialization is the process of turning some object into a data format that can be restored later. People often serialize objects in order to save them to storage, or to send as part of communications.

Deserialization is the reverse of that process, taking data structured from some format, and rebuilding it into an object. Today, the most popular data format for serializing data is JSON. Before that, it was XML.

However, many programming languages offer a native capability for serializing objects. These native formats usually offer more features than JSON or XML, including customizability of the serialization process.

Unfortunately, the features of these native deserialization mechanisms can be repurposed for malicious effect when operating on untrusted data. Attacks against deserializers have been found to allow denial-of-service, access control, and remote code execution (RCE) attacks.

Deserialization · OWASP Cheat Sheet Series

⇧  標準で用意されてる、デシリアライズ化のプログラミングだと、抜け穴があるらしく攻撃されることが判明してるらしい。 

 

で、このデシリアライズによる脆弱性を付いた攻撃は、

graneed.hatenablog.com

⇧  上記サイト様が参考になるかと。

IPAによりますと、

www.ipa.go.jp

⇧  上記がセキュリティ系のまとめなのかな?

Javaに関しては、

www.ipa.go.jp

⇧  このへんかしら。

 

ちなみに、Wikipediaさんによりますと、

JSONは、

JavaScript Object NotationJSON、ジェイソン)は軽量なデータ記述言語の1つである。構文はJavaScriptにおけるオブジェクトの表記法をベースとしているが、JSONJavaScript専用のデータ形式では決してなく、様々なソフトウェアやプログラミング言語間におけるデータの受け渡しに使えるよう設計されている。

プロセス間通信、マシン間通信においても、疎結合にするため、JSONで情報を受け渡しすることもある。

JavaScript Object Notation - Wikipedia

Protocol Buffersは、 

Protocol Buffersプロトコルバッファー)はインタフェース定義言語 (IDL) で構造を定義する通信永続化での利用を目的としたシリアライズフォーマットであり、Googleにより開発されている。オリジナルのGoogle実装はC++JavaPythonによるものであり、フリーソフトウェアとしてオープンソースライセンスで公開されている。また、ActionScriptC言語C#ClojureCommon LispD言語ErlangGoHaskellJavaScriptLuaMATLABMercuryObjective-COCamlPerlPHPR言語RubyScala.NET Frameworkなどの実装が利用可能である

Protocol Buffers - Wikipedia

ってな説明であると。まぁ、これ以外にも、いろんな技術はあると思いますが、他にどんな技術があるのか分からんので割愛。

 

試してみる

はい、すみません、脱線しました。  

ちょっと、Effective Java 第3版の12章89頁にある例題を検証してみるんですが、

の3つのクラスが必要になるんで、作成しちゃいましょう。

Java 10だと上手くいかないということで、Java 11 環境(AdoptOpenJDK)で試してみるということで。(Eclipse 2019-09を使っているので、デフォルトのJDKは、AdoptOpenJDK 11 になっています。)

Eclipseを起動で、適当な「Java プロジェクト」を作成し、上記のクラスファイルを作成で。(Java 9 から導入されたモジュールは作成なしで。)

f:id:ts0818:20191102103216p:plain

んで、今回、Elvis.javaシリアライズ化した情報が欲しいので、用意するために、Apache Commons の外部APIを使うので、ダウンロードします。

今回、ダウンロードするのは、「Apache Common Lang」ってAPIです。

入手する方法としては、

のどっちかからが多いんではないかと。

まぁ、今回は、Maven Repositry のほうからダウンロードということで。

https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 で、

f:id:ts0818:20191102104916p:plain

⇧  「3.9」のリンクをクリック。

画面遷移したらば、「Files」の「jar」のリンクをクリックで、jarファイルをダウンロードが始まり、

f:id:ts0818:20191102105034p:plain

「この種類のファイルはコンピュータに損害を与える可能性があります。~」って表示されるので、「保存」で。 

f:id:ts0818:20191102105251p:plain

ダウンロードが完了します。

f:id:ts0818:20191102105501p:plain

そしたらば、Eclipseに作成したJavaプロジェクトに、「lib」ディレクトリを作成し、ダウンロードした jarファイルを配置します。

f:id:ts0818:20191102105728p:plain

f:id:ts0818:20191102105809p:plain

「lib」ディレクトリに、ドラッグ&ドロップで。

f:id:ts0818:20191102110121p:plain

f:id:ts0818:20191102110241p:plain

f:id:ts0818:20191102110320p:plain

そしたらば、プロジェクトを選択した状態で右クリックし、「ビルド・パス(B)」>「ビルド・パスの構成(C)...」で。

f:id:ts0818:20191103113826p:plain

「ライブラリー(L)」タグで、「クラスパス」を選択した状態で、「外部 JAR の追加(X)...」を選択。

f:id:ts0818:20191103114037p:plain

ダウンロードして、「C:\Eclipse_2019-09\pleiades-2019-09-java-win-64bit-jre_20191007\pleiades\workspace\work_00\TestDeserializationAttacks\lib\commons-lang3-3.9.jar」に配置してた、jarファイルを指定します。(ファイルのパスはご自身の環境のものに合わせてください。)

f:id:ts0818:20191103114344p:plain

そしたらば、「適用して閉じる」で。

f:id:ts0818:20191103114601p:plain

参照ライブラリーに追加されてればOK。

f:id:ts0818:20191103114722p:plain

まずは、Elvis.javaシリアライズ化した値が必要なので、一旦、シリアライズ化した値を出すために、「Elvis.java」、「ElvisImpersonator.java」のコーディング。

package dto;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Arrays;

public class Elvis implements Serializable {
 public static final Elvis INSTANCE = new Elvis();

 private Elvis() {
 }

 private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };

 public void printFavorites() {
  System.out.println(Arrays.toString(favoriteSongs));
 }

 private Object readResolve() throws ObjectStreamException {
  return INSTANCE;
 }
}
package main;

import org.apache.commons.lang3.SerializationUtils;

import dto.Elvis;

public class ElvisImpersonator {

 public static void main(String[] args) {
  // TODO 自動生成されたメソッド・スタブ
  Elvis elvis = Elvis.INSTANCE;
  byte[] elvisBytes = SerializationUtils.serialize(elvis);
  StringBuilder sb = new StringBuilder();
  for (int index = 0; index < elvisBytes.length; index++) {
   if (index != 0) {
    sb.append(", ");
   }
   sb.append(String.format("0x%02x", elvisBytes[index]));
   if ((index +1) % 9 == 0) {
    sb.append("\n");
   }
  }
  System.out.println(sb.toString());
 }
}

んで実行。

f:id:ts0818:20191103123917p:plain

f:id:ts0818:20191103124020p:plain

⇧  コンソールに表示された文字列をすべてコピーしときます。

では、「ElvisImpersonator.java」、「ElvisStealer.java」を編集します。

ElvisStealer.java

package dto;

import java.io.Serializable;

public class ElvisStealer implements Serializable {
	public 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;
}

シリアライズ化していた値を、byte[]の変数serializedFormの要素として使用します。一部、(byte)でキャストの必要あり。

ElvisImpersonator.java

package main;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;

import dto.Elvis;
import dto.ElvisStealer;

public class ElvisImpersonator {

 // Byte stream could not have come from real Elvis instance!
 private static final byte[] serializedForm = new byte[] {
   (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x09, 0x64
   , 0x74, 0x6f, 0x2e, 0x45, 0x6c, 0x76, 0x69, 0x73, (byte) 0xdd
   , 0x7d, (byte) 0xbf, 0x02, (byte) 0xc3, 0x23, (byte) 0xf6, (byte) 0x98, 0x02, 0x00
   , 0x01, 0x5b, 0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72
   , 0x69, 0x74, 0x65, 0x53, 0x6f, 0x6e, 0x67, 0x73, 0x74
   , 0x00, 0x13, 0x5b, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f
   , 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x53, 0x74, 0x72, 0x69
   , 0x6e, 0x67, 0x3b, 0x78, 0x70, 0x75, 0x72, 0x00, 0x13
   , 0x5b, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x6c, 0x61
   , 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67
   , 0x3b, (byte) 0xad, (byte) 0xd2, 0x56, (byte) 0xe7, (byte) 0xe9, 0x1d, 0x7b, 0x47
   , 0x02, 0x00, 0x00, 0x78, 0x70, 0x00, 0x00, 0x00, 0x02
   , 0x74, 0x00, 0x09, 0x48, 0x6f, 0x75, 0x6e, 0x64, 0x20
   , 0x44, 0x6f, 0x67, 0x74, 0x00, 0x10, 0x48, 0x65, 0x61
   , 0x72, 0x74, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x20, 0x48
   , 0x6f, 0x74, 0x65, 0x6c };

 public static void main(String[] args) {
  // TODO 自動生成されたメソッド・スタブ
  //  Elvis elvis = Elvis.INSTANCE;
  //  byte[] elvisBytes = SerializationUtils.serialize(elvis);
  //  StringBuilder sb = new StringBuilder();
  //  for (int index = 0; index < elvisBytes.length; index++) {
  //   if (index != 0) {
  //    sb.append(", ");
  //   }
  //   sb.append(String.format("0x%02x", elvisBytes[index]));
  //   if ((index +1) % 9 == 0) {
  //    sb.append("\n");
  //   }
  //  }
  //  System.out.println(sb.toString());

  // Initializes ElvisStealer.impersonator and returns
  // the real Elvis (which is Elvis.INSTANCE)
  Elvis elvis = (Elvis) deserialize(serializedForm);
  Elvis impersonator = ElvisStealer.impersonator;

  elvis.printFavorites();
  impersonator.printFavorites();
 }

 // Returns the object with the specified serialized form
 private static Object deserialize(byte[] sf) {
  try {
   InputStream is = new ByteArrayInputStream(sf);
   ObjectInputStream ois = new ObjectInputStream(is);
   return ois.readObject();
  } catch (Exception e) {
   throw new IllegalArgumentException(e);
  }
 }
}

んで、実行すると。

f:id:ts0818:20191103125629p:plain

NullPointerExecption が発生する。

f:id:ts0818:20191103125716p:plain

デバッグ実行で、様子を見てみると、

f:id:ts0818:20191103130044p:plain

⇧  ElvisStealer.impersonator が、null になってると。

自分が思うに、ElvisStealer.javaのメンバ変数 impersonator は初期化が行われていない気がするから、どうやっても null にしかならない気がするんだけど、何かJavaの仕組みがよく分からん...

Elvisのインスタンスは普通に作成されてるけど、インスタンスを盗めていない...「Deserialization attack」攻撃が上手くいってないってことだと思うけど、「Effective Java 第3版」の意図とは合致しませんな...

シリアライズ化が良くないのかな?

github.com

⇧  上記サイト様の、Util.serialize() メソッド使えば上手くいくのかな?


まぁ、「Deserialization attack」攻撃が上手くいかないから、「Elvis.java」をEnum型にする意味が薄れてしまうんですが、Effective Java は推奨してるようなので、

package dto;

import java.util.Arrays;

public enum Elvis {
  INSTANCE;
  private String[] favoriteSongs =
      {"Hound Dog", "Heartbreak Hotel"};

  public void printFavorites() {
      System.out.println(Arrays.toString(favoriteSongs));
  }
}    

ってな感じにしたほうが良いようです。

で実行すると、「ElvisStealer.java」のデシリアライズ時にエラーになるから期待した通りの動きになっているのではないかと。

Exception in thread "main" java.lang.IllegalArgumentException: java.io.InvalidClassException: cannot bind non-enum descriptor to an enum class
	at main.ElvisImpersonator.deserialize(ElvisImpersonator.java:63)
	at main.ElvisImpersonator.main(ElvisImpersonator.java:49)
Caused by: java.io.InvalidClassException: cannot bind non-enum descriptor to an enum class
	at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:679)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at main.ElvisImpersonator.deserialize(ElvisImpersonator.java:61)
	... 1 more

というわけで、モヤモヤ感が半端ないですが...

Javaに詳しい人が、解決してブログとかにアップしてくれることを祈るばかりですかね、他力本願寺~。

今回はこのへんで。