著者:arata-nvm
はじめに
弊社のFuzzing Farmチームでは、オープンソースソフトウェアを主な調査対象として、さまざまな手法によりアプリケーションのバグを見つけています。今回のブログ記事をはじめとして、Fuzzing Farmチームでの活動を、「ファジングの活用」「ファジングのパフォーマンス計測」「1-day exploitの開発」「0-day exploitの開発」の4つのパートに分けて紹介していきます。
パート1の記事では、ファジングを利用して実世界のプログラムで脆弱性を見つけるまでの一例をご紹介します。
Fuzzing Farmチームの活動の一環として、弊社では、弊社が開発しているファジングフレームワークであるfuzzufを活用し、さまざまなプロダクトのバグを見つけています。本記事ではGEGLと呼ばれる画像処理ライブラリのバグを発見し、修正するまでの流れを説明していきます。
GEGLとは
GEGLとは、GNOMEプロジェクトで開発が進められている画像処理のためのライブラリです。画像編集に広く用いられているGIMPのほか、GNOME内の一部のプログラムにおいてもGELGが使用されています。
GEGLの最大の特徴は、画像処理の各工程をDAGというデータ構造を用いて表現していることです。DAGとは、Directed Acyclic Graphの略で、閉路のない有向グラフのことです。GEGLはDAGをXMLとして受け取り、記述されている通りに画像を加工します。例えば、以下のようなXMLをGEGLに渡します。すると、GEGLはin.png
という画像を読み込み、ガウシアンぼかしを適用した画像を出力します。
<?xml version='1.0' encoding='UTF-8'?>
<gegl>
<node operation='gegl:gaussian-blur'>
<params>
<param name='std-dev-x'>0.999</param>
<param name='std-dev-y'>0.999</param>
</params>
</node>
<node operation='gegl:load'>
<params>
<param name='path'>in.png</param>
</params>
</node>
</gegl>
詳解GEGL
先程述べたXMLのフォーマットについて、もう少し詳しく説明しましょう。
GEGLでは、一つ一つの画像処理の工程を「オペレーション」と呼んでいます。これらは、他の画像処理のツールでは「フィルター」などと呼ばれている概念です。例えば以下のようなオペレーションが存在します(括弧内はGEGLにおける識別子)。
- 切り抜き(
gegl:crop
) - ガンマ補正(
gegl:gamma
) - 反転(
gegl:invert
)
また、それぞれのオペレーションにはパラメータを与えることができます。例えば切り抜きではx
、y
、width
、height
といったパラメータが使用でき、どの範囲で画像を切り抜くのかを指定できます。GEGLでは、以上のオペレーションとパラメータを、あわせて一つのDAGノードとして扱っています。GEGLがサポートするオペレーションのリストと詳細はGEGLの公式ページにまとめられています。例えば以下のノードは、座標(10, 10)から幅100px、高さ100pxで画像を切り抜くことを表現しています。
<node operation='gegl:crop'>
<params>
<param name='x'>10</param>
<param name='y'>10</param>
<param name='width'>100</param>
<param name='height'>100</param>
</params>
</node>
同じ深さで順番にノードを並べることで、複数のノードの組み合わせを表現できます。例えば以下のDAGでは、画像の読み込みと縮小という一連の流れを表現しています。
<?xml version='1.0' encoding='UTF-8'?>
<gegl>
<node operation='gegl:scale-ratio'>
<params>
<param name='sampler'>cubic</param>
<param name='x'>0.5</param>
<param name='y'>0.5</param>
</params>
</node>
<node operation='gegl:load'>
<params>
<param name="path">data/grid.png</param>
</params>
</node>
</gegl>
また、以下のようにノードをネストすることで、一部にのみノードの効果を適用できます。この例ではチェッカーボードの生成と重ね合わせを適用しています。
<?xml version='1.0' encoding='UTF-8'?>
<gegl>
<node operation='svg:src-over'>
(中略)
<node operation='gegl:checkerboard'>
<params>
<param name='color1'>rgb(0.0, 0.0, 0.0)</param>
<param name='color2'>rgb(1.0, 1.0, 1.0)</param>
<param name='x'>32</param>
<param name='y'>32</param>
<param name='format'>YA float</param>
</params>
</node>
</node>
(中略)
<node operation='gegl:checkerboard'>
<params>
<param name='color1'>rgb(0.0, 0.0, 0.0)</param>
<param name='color2'>rgb(1.0, 1.0, 1.0)</param>
<param name='x'>32</param>
<param name='y'>32</param>
</params>
</node>
</gegl>
このように、GEGLは各オペレーションの設定、組み合わせ方などを細かく決めることが可能で、非常に高い表現力を持っていることがわかります。
GEGLのファジング
弊社が開発しているファジングフレームワークであるfuzzufを使用して、GEGLのファジングを実施しました。fuzzufの詳細についてはこちらのブログ記事をご覧ください。
GEGLの計装
今回は、fuzzufのAFLモードを利用します。そのために、まずはGEGLを計装してビルドします。GEGLのビルド方法は詳細にドキュメント化されているため、GEGLの公式ページを参照しながら進めます。今回はコミットIDがa566b738331757cf25118af5bdc65218ae5eb3b2
のバージョンを利用しました。
まずは、GEGLが依存しているパッケージをインストールします。
$ sudo apt update
$ sudo apt install build-essential pkg-config python3 python3-pip \\
ninja-build git libglib2.0-dev libjson-glib-dev libpng-dev libgegl-dev
$ sudo pip3 install meson
次に、AFLに含まれるafl-gcc
とafl-g++
を使用してGEGLをビルドします。
$ git clone --depth 1 <https://gitlab.gnome.org/GNOME/gegl> && cd gegl
$ CC=afl-gcc CXX=afl-g++ meson _build
$ ASAN_OPTIONS=detect_leaks=0 AFL_USE_ASAN=1 ninja -C _build
$ export BABL_PATH=/usr/lib/x86_64-linux-gnu/babl-0.1
$ export GEGL_PATH=/usr/lib/x86_64-linux-gnu/gegl-0.4
ビルドが完了すると、_build/bin/gegl
に計装済みのバイナリが配置されます。
コーパスの収集
ファジングを開始するにあたって、コーパスを収集する必要があります。Googleなどの検索エンジンを活用してコーパスを集める方法もありますが、今回はGEGLのテスト用に用意されたXMLファイル群を使用しました。GEGLが適切にXMLを認識するには、XMLに含まれるオペレーション名やパラメータ名が正しいものでなければならないため、この方法が最善であると判断しました。
ハーネスの作成
ここまでの準備で、先ほどビルドしたGEGLのバイナリに対してファジングできるようになりました。しかしながら、GEGLには今回のファジングで関係ない機能の初期化や、コマンドライン引数のパースなどが存在するため、そのままファジングするとパフォーマンス上の問題があります。
そこで、GEGLのエントリポイントに相当する関数から主要な処理のみを抽出し、以下のコードをハーネスとして使うことにしました。
gint main(gint argc, gchar **argv) {
GeglNode *gegl = NULL;
gchar *script = NULL;
GError *err = NULL;
gchar *path_root = NULL;
// [a]
gegl_init(NULL, NULL);
gegl_path_smooth_init();
path_root = g_get_current_dir ();
// [b]
g_file_get_contents (argv[1], &script, NULL, &err);
if (err != NULL) {
return 1;
}
// [c]
gegl = gegl_node_new_from_xml (script, path_root);
if (!gegl) {
return 1;
}
// [d]
GeglNode *output = gegl_node_new_child (gegl,
"operation", "gegl:save",
"path", "out.png",
NULL);
gegl_node_connect_from (output, "input", gegl, "output");
gegl_node_process (output);
// [e]
g_object_unref (output);
g_object_unref (gegl);
g_free (script);
g_clear_error (&err);
g_free (path_root);
gegl_exit ();
return 0;
}
このコードについて簡単に説明します。まず、[a]でGEGL内部の初期化を行います。GEGLの各オペレーションはバイナリには含まれておらず、特定のディレクトリ(環境変数GEGL_PATH
以下)にある共有ライブラリに格納されています。この初期化処理でそれらのオペレーションを読み込み、画像処理で使用できるようにします。次に[b][c]でXMLファイルの内容を文字列として読み込み、その文字列をパースしてGeglNode型のデータとして格納します。そして[d]でout.png
ファイルを画像の出力先として指定したのち、実際に画像処理を行います。最後に[e]で、これまでに確保したメモリをfreeしてプログラムを終了します。
ファジング
これで、ようやくファジングを始められます。デフォルトのオプションでは、タイムアウトでfuzzufが終了してしまうため、--exec_timelimit_ms 10000
を渡すことで、タイムアウトを10秒に設定しています。
$ ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=0 \
fuzzuf afl -i ./corpus -o ./out \
--exec_timelimit_ms 10000 \
-- _build/bin/gegl @@
トリアージ
2週間程度ファジングを回した結果、計134個のunique crashが見つかりました。AFLTriage()
を使用して見つかったクラッシュをトリアージした結果、GEGLには以下のような脆弱性があることがわかりました。
- ヒープバッファオーバーフロー(2個)
- 整数オーバーフロー(2個)
- リソース消費によるDoS(1個)
- スタックバッファオーバーフロー(1個)
- スタックバッファアンダーフロー(1個)
- NULLポインタ参照(10個)
クラッシュの解析
今回の記事では、発見したクラッシュのうち、簡単な例としてNULLポインタ参照の脆弱性について解説します。そのために、この脆弱性のRoot-Causeについて少しだけ説明します。以下のコードは、該当脆弱性を含んでいたgegl_path_parse_string
関数です。
void
gegl_path_parse_string (GeglPath *vector,
const gchar *path)
{
GeglPathPrivate *priv = GEGL_PATH_GET_PRIVATE (vector);
const gchar *p = path;
InstructionInfo *previnfo = NULL; // [3]
gdouble x0, y0, x1, y1, x2, y2;
while (*p)
{
gchar type = *p;
InstructionInfo *info = lookup_instruction_info(type);
if (!info && ((type>= '0' && type <= '9') || type == '-')) // [1]
{
if (previnfo->type == 'M') // [2]
{
info = lookup_instruction_info(type = 'L');
}
else if (previnfo->type == 'm')
{
info = lookup_instruction_info(type = 'l');
}
else if (previnfo->type == ' ')
g_warning ("EEEK");
}
// 中略
if (*p)
p++;
}
gegl_path_dirty (vector);
}
この関数は、GEGLが受け取ったXMLファイルにパス文字列が含まれていた場合に呼び出され、その文字列をパースします。パス文字列とは、複数の直線で構成される図形を記述するための文字列です。SVGのパスをイメージすると分かりやすいと思います。
ここで、次のようなXMLファイルをGEGLに渡してみます。
<gegl:fill-path d='0'/>
すると、gegl_path_parse_string
関数には引数path
として文字列”0”
が渡されることになります。while文の最初のループでtype
には文字’0’
が代入されるので、[1]のif文の条件式はtrue
と評価されます。次に、[2]でprevinfo
の参照が外されますが、[3]ではprevinfo
がNULL
に初期化されており、その後変更されていません。結果として、NULLポインタ参照が発生してプログラムがクラッシュします。
脆弱性の修正
previnfo
がNULLで初期化されて、NULLポインタ参照が発生するため、参照を外す前にNULLチェックをするように修正します。以下のコードから分かる通り、修正は非常にシンプルです。
// 中略
if (previnfo && previnfo->type == 'M')
// 中略
else if (previnfo && previnfo->type == 'm')
// 中略
else if (!previnfo || previnfo->type == ' ')
// 中略
おわりに
今回の記事では、弊社のFuzzing Farmチームの活動の一つとして実施した、GEGLのファジングとその脆弱性について解説しました。GEGLは20年以上開発され、広く使用されているライブラリですが、ファジングの対象としてはあまりメジャーではありません。今回ファジングを通して、これまで発見されていなかった脆弱性を見つけることができました。
今後も様々なソフトウェアに対してファジングを実施し、開発者と連携しつつ、アプリケーションに潜む脅威を減らす活動を継続していきます。
次回は、ファジングに関連して、ファザーのパフォーマンス計測に関する理論的な話題を取り上げる予定です。お楽しみに!