2013年2月19日火曜日

少し前に話題になっていたJavaの脆弱性

先月あたりに話題になっていたJavaの脆弱性について解説した記事を眺めてみたのでメモ。 POCへのリンクもあるので文章で書かれたことがどういうコードになるのかがなんとなくわかりました。

CERT/CC Blog: Anatomy of Java Exploits

自分が理解できた(つもり)のは以下のとおり。

  • 背景
    • Javaには信用できない(untrusted)コードの実行を制限する機能が存在する
      • 一番重要なのは SecurityManager クラス。 System.{set,get}SecurityManagerで操作できる
    • SecurityManagerはデスクトップ環境などでは通常nullに設定されていて、特に実行を制限しない
    • アプレットなどではセキュリティポリシーにしたがってファイルアクセスなどを制限するよう設定されている
      • 許可されていないコードを実行しようとするとSecurityExceptionが発生して終了する

クラスローダやリフレクションAPIなどを信用できないコードから実行することは制限されていますが、 脆弱性を利用するとこれらの制限を回避できてしまうようです。

以下の2つを達成することで、任意のコードを実行できるようになるようです。

  1. sun.org.mozilla.javascript.internal.{Context, GeneratedClassLoader} クラスへのハンドルを取得する
  2. 上記クラスを使いdefineClassで動的にセキュリティ機能を無効化する処理を行うクラスを定義する
    • System.securityManagerをnullにする
  • sun.org.mozilla.javascript.internal.{Context, GeneratedClassLoader} クラス
    • セキュリティ機能によるアクセス制御が行われているがprivate宣言されていない
    • アクセス制御が行われていない com.sun.jmx.mbeanserver.MBeanInstantiator クラスの findClass を経由して取得できる
  • java.lang.invoke.MethodHandles.Lookup クラス
    • 直接クラスローダーを作成しようとするとSecurityManagerに阻止されるので、このクラスを経由する
    • コンストラクタやメソッドのハンドルを取得できる
    • 通常、リフレクションAPIはセキュリティ機能により呼び出し元(caller)が信用できるかどうかを確認している
    • java.lang.invoke.MethodHandles.Lookupは脆弱性により信用できる呼び出し元だと判断されてしまう

2013年2月7日木曜日

[Clojure]clojure.asmでHello World

Clojureはバイトコードを作成するために ASM というライブラリ(Javaバイトコード操作用フレームワーク)を 利用しているようです。

(JVMの)Clojureを利用できる環境では(たぶん)ASMが利用できる(clojure.asm)ので、 勉強がてらこのライブラリを利用してHelloWorldプログラムを作成してみます。

(import '[clojure.asm ClassWriter Opcodes])

(def target-name "Hello")

(def cw (ClassWriter. ClassWriter/COMPUTE_MAXS))

;; public class Hello extends java.lang.Object { ...
(.visit cw
        Opcodes/V1_5       ; バージョン
        Opcodes/ACC_PUBLIC ; アクセス修飾子
        target-name        ; クラス名
        nil                ; シグネチャ
        "java/lang/Object" ; 親クラス
        nil)               ; インターフェース名の配列

;; MethodWriter
;; public static void main(java.lang.String []){ ...
(def mw (.visitMethod cw
                      (bit-or Opcodes/ACC_PUBLIC
                              Opcodes/ACC_STATIC) ; アクセス修飾子
                      "main"                      ; メソッド名
                      "([Ljava/lang/String;)V"    ; ディスクリプタ(引数と戻り値の型)
                      nil                         ; シグネチャ
                      nil))                       ; 例外名の配列

;; メソッドの内容
(.visitCode mw)
(.visitFieldInsn mw
                 Opcodes/GETSTATIC
                 "java/lang/System"
                 "out"
                 "Ljava/io/PrintStream;")
(.visitLdcInsn mw "Hello World!")
(.visitMethodInsn mw
                  Opcodes/INVOKEVIRTUAL
                  "java/io/PrintStream"
                  "println"
                  "(Ljava/lang/String;)V")
(.visitInsn mw Opcodes/RETURN)
(.visitMaxs mw 0 0)
(.visitEnd mw)

(.visitEnd cw)

;; バイトコードを取得
(def bytecode (.toByteArray cw))

;; classファイルを作成
(with-open [out (java.io.FileOutputStream. (str target-name ".class"))]
  (.write out bytecode))

上記のコードを実行すると Hello.class という名前でクラスファイルが作成されるので、 javaコマンドで実行してみます。

> java Hello
Hello World!

javapコマンドでバイトコードを逆アセンブルしてみると、 Clojureで書いたコードと対応してるように見えます。

javap -c Hello.class 
public class Hello {
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #14                 // String Hello World!
       5: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return        
}

Clojureのコンパイル時の動作を詳しく知りたい場合、このライブラリを理解しておいたほうが良さそうです。

2013年2月6日水曜日

[Clojure]文字列からReaderを作る

文字列を直接指定することはできないので、 getBytes メソッドで配列を取得して ByteArrayInputStreamを作成することで、指定した文字列を読み込むことのできるReaderを作成できます。

(->> (.getBytes "hoge")
     java.io.ByteArrayInputStream.
     java.io.InputStreamReader.
     java.io.BufferedReader.
     .readLine)
;; => "hoge"

(with-open [r (clojure.java.io/reader (.getBytes "Hello\nWorld\n"))]
  (list (.readLine r)
        (.readLine r)))
;; => ("Hello" "World")

2013年2月4日月曜日

[Clojure]リフレクションを使ってインターフェースの関係を図にしてみる

clojure.langあたりのコードを少し読もうとしてみましたが、各クラスやインターフェースの関係が分かりにくいので 図にしてみようと思いました。

clojure.reflectを使ってインターフェースの継承関係を取得し、DOTファイルを作って図にしてみます。

(require '[clojure.reflect :as r])

(defn class-found? [klass]
  (try (Class/forName (.getName klass))
       (catch Exception _ nil)
       (catch Error _ nil)))

(defn clj-class-name [klass]
  (let [name (.getName klass)]
    (if-let [clj-name (re-find #"clojure\.lang\.(.+)" name)]
      (nth clj-name 1)
      (.getName klass))))

(defn clj-class-name? [name]
  (.startsWith name "clojure.lang"))

(defn class-name-list []
  (let [f (doto (.getDeclaredField ClassLoader "classes")
            (.setAccessible true))
        loader (.getClassLoader clojure.lang.RT)]
    (try
      (for [klass (vec (.get f loader))
            :when (class-found? klass)]
        (.getName klass))
      (finally
        (.setAccessible f false)))))

(defn clojure-interface-list []
  (->> (class-name-list)
       (filter clj-class-name?)
       (map #(Class/forName %))
       (filter #(.isInterface %))))

(defn parent-class-set [klass]
  (:bases (r/reflect klass)))

(defn print-interface-tree []
  (printf "digraph \"interface-tree\" {\n")
  (doseq [klass (clojure-interface-list)]
    (let [name (clj-class-name klass)]
      (doseq [parent (parent-class-set klass)]
        (printf "\"%s\" -> \"%s\";\n" name (clj-class-name parent)))))
  (printf "}\n"))

;; (print-interface-tree)

print-interface-tree 関数を実行すると repl に DOTファイルの内容が出力されるので、 DOTファイルとして保存します。

DOTファイルをPNG画像にするには以下のコマンドを実行すれば良いです。

dot -Tpng interface-tree.dot > interface-tree.png

シーケンスに関連する部分が多いようです。

2013年2月1日金曜日

[Clojure]LazySeqで嵌ったのでメモ

ClojureでCSVファイルを読み込んでいたら OutOfMemoryError に出会ってしまったので 戒めの意味をこめてメモ。


先頭2行がヘッダで残りの行がデータ、というExcelチックなファイルを読み込んでみます。

まず、読み込む対象となる大きなCVSファイルを作成します。

(require '[clojure.data.csv :as csv])

;; ファイル名
(def LARGE-CSV-FILE "large-file.csv")

;; ファイル作成
(with-open [w (clojure.java.io/writer LARGE-CSV-FILE)]
  (.write w "ID1, ID2, ID3, ID4\n")
  (.write w "a, b, c, d\n")
  (doseq [x (range 5000000)]
    (.write w (format "%s, %s, %s, %s\n" (rand) (rand) (rand) (rand)))))

CSVファイルを読み込んで指定した関数を実行する補助関数を定義します。
clojure.data.csv/read-csv はCSVファイルの内容を表すLazySeqを返すので、 指定された関数に渡される際にはまだ実際の読み込み処理は行われていません。

;; CSVを表すLazySeqを引数として関数を実行する関数
(defn call-with-large-csv [f]
  (with-open [r (clojure.java.io/reader LARGE-CSV-FILE)]
    (f (csv/read-csv r))))

CSVファイルを表すLazySeqから、先頭行を取得してみます。

(call-with-large-csv
  (fn [csv] (first csv)))
;; => ["ID1" " ID2" " ID3" " ID4"]

この処理は即座に終了します。 LazySeqのおかげで、実際に読み込まれたのは500万行あるCSVファイルのうち1行だけ(のはず)です。

次に、 ヘッダを表す先頭の2行を取得してみます。

(call-with-large-csv
  (fn [csv] (take 2 csv)))
;; => IOException Stream closed  java.io.BufferedReader.ensureOpen

この処理は正常に終了しません。 「ストリームが既にクローズされています」という例外が発生してしまいます。

これは、take が返す値がLazySeqであるために起こります。
補助関数(call-with-large-csv)の外側でLazySeqの要素が必要(replで表示したい)になると、 その時になってから実際の読み込み処理が行われますが、このときにはもう(with-openによって)入力ストリームはクローズしています。

この処理を正常に行うには、実際の読み込み処理をストリームがオープンしている間に行わなければなりません。 読み込み処理を行いたいタイミングで、doallマクロで囲んだり、ベクタに変換したりしましょう。

(call-with-large-csv
  (fn [csv] (doall (take 2 csv))))
;; => (["ID1" " ID2" " ID3" " ID4"] ["a" " b" " c" " d"])

(call-with-large-csv
  (fn [csv] (vec (take 2 csv))))
;; => [["ID1" " ID2" " ID3" " ID4"] ["a" " b" " c" " d"]]

次に、ファイルの最終行を取得してみます。

(call-with-large-csv
  (fn [csv] (last csv)))
;; => ["0.9335203181407478" " 0.19775692522243427" " 0.21165896344265756" " 0.9433273177661132"]

多少時間はかかりますが、正常に終了します。

同様に、doseqで各行に対して処理を行うふりをしても、処理は正常に終了します。

(call-with-large-csv
  (fn [csv]
    (doseq [x csv] nil)))
;; => nil

doseqで処理を行った後で、先頭行を取得してみます。

(call-with-large-csv
  (fn [csv]
    (doseq [x csv] nil)
    (first csv)))
;; => java.lang.OutOfMemoryError: Java heap space

この処理はメモリが足りないと言われて異常終了します。
処理の最後にLazySeqの先頭要素が必要なので、CSVファイルの内容を保持しておくために doseq中にGCでメモリを回収できないからだと思われます。

以下のようにdoseqよりも前に先頭行を取得しておけば、CSVファイル(LazySeq)の内容すべてを 保持する必要はないため、メモリが足りなくなることなく正常に終了します。

(call-with-large-csv
  (fn [csv]
    (let [header (first csv)]
      (doseq [x csv] nil)
      header)))
;; => ["ID1" " ID2" " ID3" " ID4"]

次に、先頭行だけでなく、先頭の2行(ヘッダ)を取得してみます。

;; 先頭2行のヘッダを取得する関数
(defn get-headers [csv]
  (take 2 csv))

(call-with-large-csv
  (fn [csv]
    (let [headers (get-headers csv)]
      (doseq [x csv] nil)
      headers)))
;; => java.lang.OutOfMemoryError: Java heap space

この処理は異常終了します。 原因は get-headers が返す値がLazySeqであり、doseqの後までCSVの内容を保持しておく必要があるためかと思います。

私はここで悩みました。直接take関数などを書いていればすぐに気づいたかもしれませんが、 「ヘッダを取得」という関数を作成したつもりでいたために返値がLazySeqであるということを忘れていました。

この場合も、処理を正常に終了させるためには、あらかじめdoallなどでLazySeqの要素を計算し、 巨大なデータを保持しなくても良いようにします。

(call-with-large-csv
  (fn [csv]
    (let [headers (doall (get-headers csv))]
      (doseq [x csv] nil)
      headers)))
;; => (["ID1" " ID2" " ID3" " ID4"] ["a" " b" " c" " d"])

ファイルに限らず、LazySeqで巨大なデータを扱うときは、処理の途中でGCできるかどうか気を付けようと思います。