Ghostscript脆弱性とImageMagick/GraphicsMagick、そしてGoogle Project Zero

Ghostscriptの脆弱性が、Google Project ZeroのTavis Ormandy氏により公開されました(CVE番号はまだ無し)。openwallのoss-securityメーリングリストにもクロスポストされたので、こちらを見た方も多いでしょう。

基本的な情報は以下のJPCERTアナウンスから出ているので、ここではもうちっと細かいTopicsをいくつかまとめます。

概要

PostScriptは、単なるドキュメントファイルの一型式ではなく、描画コマンドなども利用できるプログラミング言語です。そのためGhostscriptには、-dSAFERという「キケンなこと」ができないようにするモードがあります。ところが、この-dSAFERを付けていても任意コード実行が可能な穴があった……というのが今回の脆弱性のキモです。

脆弱性のパッチは、開発元のArtifex Softwareから既に提供されています。正式な次バージョンのリリースは9月になるようです。

ただ、上記Project ZeroのTavis氏の発言によると、この修正は不十分であり、また現在Fuzzerを回しておりまだまだ見つかりそうだとのことから、さらなる追加修正が入りそうです。

ImageMagickも対象である

今回の脆弱性はGhostscriptの脆弱性ですが、ImageMagickもGhostscriptに依存しているため影響を受けます。そのためWebアプリでユーザに画像をアップロードしてもらう等、画像処理をImageMagickで行うプログラムではすべて対応が必要です。

実際、以下のようにImageMagickで細工したPSファイル(VU-332928.ps)を扱うとLinuxコマンドが実行されており、脆弱性の影響を受けることが分かります。
なお以下の例では、idコマンドを実行しているので、uid等が表示されています。もちろん、もっとヒドいこともできます。

$ convert VU-332928.ps test.png
uid=1000(ozuma) gid=1000(ozuma) groups=1000(ozuma),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
convert: `%s' (%d) "gs" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pngalpha" -dTextAlphaBits=4 -dGraphicsAlphaBits=4 "-r72x72" -g612x792  "-sOutputFile=/tmp/magick-GrMdTtj4--0000001" "-f/tmp/magick-4q5WILKb" "-f/tmp/magick-th3RM7bj" -c showpage @ error/utility.c/SystemCommand/1890.
convert: Postscript delegate failed `VU-332928.ps': そのようなファイルやディレクトリはありません @ error/ps.c/ReadPSImage/832.
convert: no images defined `test.png' @ error/convert.c/ConvertImageCommand/3046.
$ 

気をつけないといけないのは、この脆弱性はPSファイルを読み込んでパースした時点で発生するということです。具体的には、ImageMagickでconvertをしていなくても、identifyでファイルタイプ判別のみに利用していても、脆弱性の対象となります。下記でも、ファイルに仕込んだidコマンドが実行されてしまっています。

$ identify VU-332928.ps 
uid=1000(ozuma) gid=1000(ozuma) groups=1000(ozuma),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
identify: `%s' (%d) "gs" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pngalpha" -dTextAlphaBits=4 -dGraphicsAlphaBits=4 "-r72x72" -g612x792  "-sOutputFile=/tmp/magick-ItbgeaDQ--0000001" "-f/tmp/magick-59biIgLS" "-f/tmp/magick-KUgSVrTU" -c showpage @ error/utility.c/SystemCommand/1890.
identify: Postscript delegate failed `VU-332928.ps': そのようなファイルやディレクトリはありません @ error/ps.c/ReadPSImage/832.
$

また、例えばPythonのライブラリであるPythonMagickでは、Image()メソッドでファイルを読み込むだけで発動します。つまり以下のように、ファイルを読み込むだけで何もしないスクリプト脆弱性の影響を受けます。

#!/usr/bin/python

# coding=UTF-8
import PythonMagick
img = PythonMagick.Image("VU-332928.ps")
ozuma@ubuntu17:~$ ./vul.py VU-332928.ps 
uid=1000(ozuma) gid=1000(ozuma) groups=1000(ozuma),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),118(lpadmin),128(sambashare)
Error: /ioerror in --showpage--
Operand stack:
   --nostringval--   1   true
Execution stack:
   %interp_exit   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   --nostringval--   --nostringval--   false   1   %stopped_push   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   1874   1   4   %oparray_pop   --nostringval--   --nostringval--
Dictionary stack:
   --dict:1215/1684(ro)(G)--   --dict:0/20(G)--   --dict:78/200(L)--   --dict:88/91(L)--
Current allocation mode is local
Last OS error: Broken pipe
GPL Ghostscript 9.21: Unrecoverable error, exit code 1
$

ImageMagickでの対策は、JPCERT等からアナウンスされている通り、policy.xmlで処理を禁止するファイルを指定することです。

具体的には、以下のようにPS/EPS/PDF/XPSを禁止することになります。ちなみにPS2PS3とは、PostScript Level 2とLevel 3です。

<policy domain="coder" rights="none" pattern="PS" />
<policy domain="coder" rights="none" pattern="PS2" />
<policy domain="coder" rights="none" pattern="PS3" />
<policy domain="coder" rights="none" pattern="EPS" />
<policy domain="coder" rights="none" pattern="PDF" />
<policy domain="coder" rights="none" pattern="XPS" />

GraphicsMagickでの対策

GraphicsMagickも内部でGhostscriptを利用しているため本脆弱性の影響を受けます。以下のように、identifyするだけでidコマンドが実行できてしまっています。convertも同様に脆弱性の影響を受けます。

$ ./gm identify VU-332928.ps 
uid=1000(ozuma) gid=1000(ozuma) groups=1000(ozuma),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
./gm identify: "gs" "-q" "-dBATCH" "-dSAFER" "-dMaxBitmap=50000000" "-dNOPAUSE" "-sDEVICE=pnmraw" "-dTextAlphaBits=4" "-dGraphicsAlphaBits=4" "-r72x72" "-g612x792" "-sOutputFile=/tmp/gm8OJn56" "--" "/tmp/gmBWNNro" "-c" "quit" (child process quit due to signal 13).
./gm identify: Postscript delegate failed (VU-332928.ps).
./gm identify: Request did not return an image.
$

さてImageMagickと対比されることで有名なGraphicsMagickですが、policy.xmlに除外ファイルを書けば済むImageMagickと違い、こちらの対策は厄介です。
なぜなら、GraphicsMagickには、ImageMagickのようなpolicy.xmlファイルによる除外機能が無く、除外ファイルを一括指定できないためです(この辺、素直にImageMagickの設計をパクればいいのに……ってみんな思ってる。たぶん)。

GraphicsMagickでは、代わりにdelegates.mgkというファイル修正で対策が行えます。これは、GraphicsMagickのメーリングリストで、メンテナのBob Friesenhahn氏も話題に挙げています。

具体的には、以下のパスにあるdelegates.mgkファイルを修正します。

lib/GraphicsMagick-X.X.XX/config/delegates.mgk
 (X.X.XXの部分はバージョン番号)

まずは現在の状態を確認するために、-listで見てみましょう。

$ ./gm convert -list delegate
Path: /home/ozuma/local/GraphicsMagick-1.3.30/lib/GraphicsMagick-1.3.30/config/delegates.mgk

Delegate             Command
-------------------------------------------------------------------------------
     cgm =>          "ralcgm" -d ps < "%i" > "%o" 2>/dev/null
   dcraw =>          "dcraw" -c -w "%i" > "%o"
     dot =>          "dot" -Tps "%i" -o "%o"
     dvi =>          "dvips" -q -o "%o" "%i"
     eps<=>pdf       "gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE
                      -sDEVICE=pdfwrite "-sOutputFile=%o" -- "%i" -c quit
     eps<=>ps        "gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE
                      -sDEVICE=ps2write "-sOutputFile=%o" -- "%i" -c quit
     fig =>          "fig2dev" -L ps "%i" "%o"
     hpg =>          "hp2xx" -q -m eps -f `basename "%o"` "%i" && /usr/bin/mv
                      -f `basename "%o"` "%o"
    hpgl =>          "hp2xx" -q -m eps -f `basename "%o"` "%i" && /usr/bin/mv
                      -f `basename "%o"` "%o"
     htm =>          "html2ps" -U -o "%o" "%i"
    html =>          "html2ps" -U -o "%o" "%i"
    ilbm =>          "ilbmtoppm" "%i" > "%o"
    mpeg =>          "mpeg2decode" -q -b "%i" -f -o3 "%u%%05d"; gm convert
                      -temporary "%u*.ppm" "miff:%o" ; rm -f "%u"*.ppm 
     pdf<=>eps       "gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE
                      -sDEVICE=epswrite "-sOutputFile=%o" -- "%i" -c quit
     pdf<=>ps        "gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE
                      -sDEVICE=ps2write "-sOutputFile=%o" -- "%i" -c quit
     pnm<= ilbm      "ppmtoilbm" -24if "%i" > "%o"
     pnm<= launch    "gimp" "%i"
     pnm<= win       "gm" display -immutable "%i"
      ps<=>eps       "gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE
                      -sDEVICE=epswrite "-sOutputFile=%o" -- "%i" -c quit
      ps<=>pdf       "gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE
                      -sDEVICE=pdfwrite "-sOutputFile=%o" -- "%i" -c quit
      ps<= print     "no -c -s" "%i"
   shtml =>          "html2ps" -U -o "%o" "%i"

このように、各形式に対して実行するコマンドテーブルが用意されています。ここから、Ghostscriptの実行コマンドである"gs"を含むものを全部消してやればいいわけです。

delegates.mgkの中から、以下のようにgsコマンドが使われている部分をすべて削除します。

...(省略)...
  <!-- Read monochrome Postscript, EPS, and PDF  -->
  <delegate decode="gs-mono" stealth="True" command='"gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE -sDEVICE=pbmraw -dTextAlphaBits=%u -dGraphicsAlphaBits=%u -r%s %s "-sOutputFile=%s" -- "%s" -c quit' />

  <!-- Read grayscale Postscript, EPS, and PDF  -->
  <delegate decode="gs-gray" stealth="True" command='"gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE -sDEVICE=pgmraw -dTextAlphaBits=%u -dGraphicsAlphaBits=%u -r%s %s "-sOutputFile=%s" -- "%s" -c quit' />

  <!-- Read colormapped Postscript, EPS, and PDF  -->
  <delegate decode="gs-palette" stealth="True" command='"gs" -q -dBATCH -dSAFER -dMaxBitmap=50000000 -dNOPAUSE -sDEVICE=pcx256 -dTextAlphaBits=%u -dGraphicsAlphaBits=%u -r%s %s "-sOutputFile=%s" -- "%s" -c quit' />
...(省略)...

修正できたら、もう一度gm convert -list delegateを実行して、gsコマンドが使われない状態であることを確認してください。
なお、これはImageMagickもそうですが、そもそもPS/EPS/PDF/XPSファイルが扱えなくなってしまう問題が発生しますので、サービスによっては多大な影響が発生する点に注意してください。

その他の対策としては、そもそもGhostscriptをアンインストールしてしまう、gsコマンドの実行権限を落としてしまう、などの荒技も一応考えられますね。

ファイル形式をチェックしてファイル入力時点でブロック

保険的対策として、画像処理をするプログラムに渡す前に、ファイル形式をチェックしてPS/EPS/PDF/XPSならばエラーとして弾く、というのも有効です。なお、別の入力経路があるかもしれないことと、ファイル形式を偽装する攻撃手法があるので、あくまで保険的対策です。

しかしこの際、どのようにファイル形式をチェックするかは注意が必要です。

まず、当然のことながら拡張子で判断してはいけません。拡張子を.jpgとした悪意のあるPSファイルをアップロードされたら、チェックをすり抜けて試合終了です。

じゃぁImageMagickのidentifyを利用して画像形式を判別して……と思いつきそうですが、冒頭に記載した通り本脆弱性はidentifyしただけで発動します。また、今後も出るであろうGhostscript+PSファイルの脆弱性も、PostScriptファイルの性質からして、おそらくidentifyするだけで発動します。

ではどうすればいいか……正攻法の一つには、WAF(Web Application Firewall)の利用が挙げられます。例えばSaaS型WAFのScutumは対応が早く、8月23日時点で対応していました。

一方、プログラマとして対応する場合は、例えばPythonには、mimetypesというモジュールがあり、このguess_type()メソッドを利用することでMIME Typeが判別できます。これで「application/postscript」等を叩き落とせばいいですね。(2018/08/27追記:mimetypesモジュールは、拡張子を見るだけなのでダメでした。ご指摘いただいた id:penult さん、ありがとうございます。)
一方、プログラマとして対応する場合は、fileコマンドを使ったり、magicファイル(ややこしいですが、ImageMagickとは関係なく、ファイルタイプ判別のmagicファイルのこと)を利用する言語ごとのファイル形式判別ライブラリを使うと良いでしょう。例えばPythonには、python-magicというモジュールがあります。

なお、このように特定のファイル形式を叩き落とす場合は、ブラックリスト型ではなくホワイトリスト型の設計とするのが基本です。例えば画像アップローダのサービスならば、はじめから受け付ける画像形式を決めて(例えばjpg/png/gifのみ等)、それ以外は全てエラーとする、とすべきです。意図しない画像形式がアップロードされた時の誤作動を防ぐことができます。

今回のゼロデイ公開について

Google Project Zeroは、いわゆる「90日ルール」を設定しています。これは、発見した脆弱性をベンダへ通知した後に90日のdeadlineを設定し、修正されない場合は強制的に脆弱性を公開するというものです。

実際にどう運用されているかは、チケットを見てみると分かりやすいかと思います。

上記ポストの最後に、以下の文があります。

This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available, the bug report will become visible to the public.

さて今回、Ghostscriptを開発しているArtifex Software社は、Google Project Zeroから報告を受けたけど90日以内に直せなかったのでしょうか? これは事実関係がどこにも書かれていないので推測でしかありませんが、「そもそも報告を受けていない」と捉えるのが自然です。

Artifex Software社は現在既にパッチをリリースしていますが、そのアナウンスの最後に以下のような文章を掲示しています。

Artifex takes security issues very seriously and strongly encourages responsible and coordinated disclosure of vulnerabilities. Developers should be given the opportunity to fix security problems in advance of public disclosure.

(意訳)Artifex Softwareは、セキュリティ情報を非常に重要なものとして取り扱っています。頼むからいきなりZero-Day公開しないで、ちゃんと猶予をくれ。

https://artifex.com/news/ghostscript-security-resolved/

さて、今回のゼロデイ公開は、Google Project Zeroの以下のポストです。

こちらの"Reported"は、2018-08-21になっています。うーん、90日ルールはどこへ行ってしまったのでしょうか。

Google Project Zeroは、マネージャーのParisa Tabriz氏が先日のBlack Hat USA 2018でKeyNoteを努めました。セキュリティに対する熱い思いを語り、Google Project Zeroの理念を語っていた姿に感動した私は、正直「むむむ?」というフクザツな気分です。

脆弱性を事前通知したのかどうか、はっきりしないため誤解があったらすみません。

(何かあれば加筆する)