ソフトウェアテストアドベントカレンダーの17日目です.
前回の記事はnihonbusonさんのWACATE合宿レポートでした.WACATEというワークショップがあるということを初めて知りました.
さて前置きが長くなりましたが,今回ご紹介するのはファジングです.SQAよりもセキュリティの方が馴染み深いテストかもしれませんが,できるだけ分かりやすく解説していきます.
Fuzzing(ファジング)とは
情報処理推進機構(IPA)によると,ファジングとは**「検査対象のソフトウェアに対して,問題を起こしそうなデータを入力し,その応答や挙動を監視する手法」です.入力するデータのことをfuzz(ファズ)**と呼んだり,テストケースと呼んだりします.
ファズの入力方法は,ソフトウェアによって様々です.例えば,bashコマンドのように標準入力からファイルや文字列を入力する場合やHTTPサーバやFTPサーバのようにTCP/IP通信でデータを入力する場合,さらに画像や音声ライブラリではメディアデータを入力します.
以上のようにファズを大量に入力することでファジングが行われます.長い時間をかけて順番に入力するツールを**ファザー(fuzzer)**と呼びますが,これまでに多くのファザーが公開されています.
ソフトウェアテストとの関連性
次にソフトウェアテストとの関連性について説明します.これを述べないと,セキュリティのアドカレンダーが無かったからこっちに来た,ってことがバレてしまうのでここでしっかりと関連性を述べます.
VLATESによると,ソフトウェアテストとは「対象のソフトウェアが意図通り動作することを実証したり,バグや想定外の動作を検出したりするテスト」です。
ということは,ファジングは後者の「バグや想定外の動作を検出したりするテスト」に相当します.
ファジングの流れ
以下の図のような感じでファジングは実施されます.
- 正常データをベースにファズを生成します.
- 対象のソフトウェアに対してファズを入力します.
- ソフトウェアの挙動や正常データの入力による死活監視を行います.
- 再び 1. に戻ります.
毎回ファズを動的に生成せずに,先にファズを大量に生成してそれを順々に入力するというタイプもあります.
ファジングの機能分解:GIDAC
以下のようにファジングを5つの機能で分けます.
- Fuzz Generation:ファズ生成
- Fuzz Input:ファズ入力
- Bug Detection:バグ検出
- Automation:自動化
- Coverage:カバレッジ
これらの頭文字を合わせて**GIDAC(ジダック)**と呼ぶことにします.
ファズ生成(G)
効率的にバグを検出するためにファズの生成手法は重要です.なぜならば,その手法によって検出可能なバグが決定されるためです.
例えば,30文字以上で桁あふれを引き起こすプログラムに対して,29文字以内のファズを延々と入力していてもバグは検出できません.また,HTTPサーバのような文法が決まっている入力を必要とするプログラムでは,文法を無視したファズを入力してもプログラムの浅い部分しかテストできません.
ファズ入力(I)
ソフトウェアには「入出力(I/O)」という共通した概念がある一方,その方法は前述のように様々です.独自でファザーを開発するには,入出力(特に入力)について注意する必要があります.
バグ検出(D)
テスト対象にファズを入力後,ファザーはテスト対象の挙動を監視し異常状態を検出します.例えば,Bashコマンドでは異常終了(SIGILL/SIGSEGV)を検出することによってバグの検出が可能です.また,HTTPサーバやibusのようにプロセスまたはデーモン化する場合は,ファズ入力直後に正常データを送信して死活監視することでバグの状態異常を検出します.
自動化(A)
大規模開発では,十分なテストを行うために生成すべきファズが億兆を超えます.そのため,すべての入力をヒトの手で行うことは不可能であり,自動化という手法に頼らざるを得ません.
カバレッジ(C)
ファジングに限らずソフトウェアテスト全体で言えることですが,テストによって網羅されたプログラムの範囲を把握することは大切です.ただし,ブラックボックステスト(後述)のようにカバレッジの測定が困難な場合は,テストケース実施件数をカバレッジの代わりにします.
ファジングの種類
以下のように3種類に分類されます.
ブラックボックスFuzzing
- ファズ入力後,外部からテスト対象の挙動を確認
- プログラムの内部処理に関しては一切考慮しない
- バグの例:httpdがファズ受信後に再起動/ポートが閉じてサービスが強制停止
グレイボックスFuzzing
- ファズ入力後,外部からの挙動確認に加え内部処理も確認
- ファズが通過したプログラム内のパスを追跡
- カバレッジを計測可能のため,カバレッジガイドファジングとも呼ばれている
- バグの例:bmp2tiffにファズを入力するとsegfaultが発生
ホワイトボックスFuzzing
- 動的シンボリック実行,SMT, SAT 等
ファザーの動作確認
以下のファザーをUbuntu16.04(x64)上で動作させます.
- boofuzz:ブラックボックス
- radamsa:ブラックボックス
- AFL:グレイボックス
boofuzz
boo(ブー)は,Pythonベースのブラックボックスファザーです.Sulley(サリー)というプロジェクトから派生したファザーで,親プロジェクトと同様,モンスターズインクにちなんで名付けられました.それでは,インストールから説明していきます。
インストール方法
# git clone https://github.com/jtpereyda/boofuzz.git
# cd ./boofuzz
# python setup.py install
ファザーの実行&ステータス
# python ftp-simple.py
# firefox http://localhost/:26000
独自ファザーの作成
自分でファザーを作成するには,boofuzz/examples/にあるサンプルを例にするか,もしくはソースコードを読んで各APIの動作を理解する必要があります.今回,説明用にファザーを作成しようと試みたのですが,時間がなかったため割愛します.
radamsa
radamsa(ラダムサ)は,正常データをランダムな文字列や乱数によって置換することでファズを作成する便利なツールです.scapy(スカパイ)と組合せるとさらに強力なファザーを作成できます.ただし,今回は時間の都合上組合せません.
インストール
# git clone https://gitlab.com/akihe/radamsa.git
# cd ./radamsa
# make
Fuzzing対象のTCPサーバ
80番ポートをオープンし"die"と送信されたらAbort(中止)するTCPサーバをビルドします.
# gcc -O3 -trigraphs -o tcp-server tcp-server.c
以下、TCPサーバのサンプルです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main(void) {
int srv_sock, cli_sock;
int len, inlen;
char buf[32];
struct sockaddr_in server;
struct sockaddr_in client;
server.sin_family = AF_INET;
server.sin_port = htons(80);
server.sin_addr.s_addr = INADDR_ANY;
srv_sock = socket(AF_INET, SOCK_STREAM, 0);
bind(srv_sock, (struct sockaddr *)&server, sizeof(server));
listen(srv_sock, 1);
while (1) {
len = sizeof(client);
cli_sock = accept(srv_sock, (struct sockaddr *)&client, &len);
memset(buf, 0, sizeof(buf));
write(cli_sock, "hello> ", 7);
inlen = read(cli_sock, buf, sizeof(buf));
if (strcmp(buf, "die\n") == 0) abort();
shutdown(cli_sock, 2);
close(cli_sock);
}
close(srv_sock);
return 0;
}
ファザーの実行
# echo 'di' > valid-case.txt
# ./tcp-server &
# radamsa -o 127.0.0.1:80 -n inf valid-case.txt
# tcpdump -i lo
GIDACを用いた考察
radamsaが提供する機能はファズ生成(G)と一部のファズ入力(I)です.メインは,専らファズ生成で,その他の機能は完全にサポートされていません.サポート外の機能は,別のツールに頼るか自分で作成する必要があります.このときに,scapyという自由にパケットを生成できるツールを使うことで解決できます.
ただし,booもradamsaもブラックボックスファザーのため,カバレッジ(C)に関してはテストケースの実行件数や実施時間で代用するしか方法はありません.
カバレッジが取れないと,ファズがプログラムの深い部分に届いているのかわからないため不安ですよね.それを解消してくれるのが,グレイボックスファジングなのです.
AFL
グレイボックスでは,ブラックボックスと異なりカバレッジを測定できるため,ファズの生成アルゴリズムを改良すれば,プログラムの深い部分にファズを届けることが可能です.
広く知られているツールとして,AFL, LibFuzzer, HonggFuzz等があり,3者とも遺伝的アルゴリズムをファズ生成アルゴリズムに改良することで効率的なバグ検出を実現しています.
具体的には,発見した通過パスを分類し,良性のテストケースを変異させて次のファズを生成するという流れです.これらのツールが見つけた脆弱性(CVE)は非常に多く,まだ歴史が浅いのにも関わらず数千件以上に上ります.
インストール
# git clone https://github.com/google/AFL.git
# cd ./AFL
# make
# make install
CVE-2014-9330の再現
AFLを用いてCVE-2014-9330を検出します.
IPAの解説によると,libtiff
ライブラリのbmp2tif
のtif_packbits.c
に存在する,範囲外の読み取りを誘発するディメンションに関する処理に不備が,整数オーバーフローの発生要因だったとのことです.
準備
以下のように,libtiff(3.8.2)をダウンロード&Fuzzing用にビルドします.
# wget http://download.osgeo.org/libtiff/old/tiff-3.8.2.tar.gz
# tar xvzf tiff-3.8.2.tar.gz
# cd tiff-3.8.2
# export CC=afl-gcc
# export CXX=afl-g++
# ./configure --disable-shared
# make
# ls ./tools
# cp /opt/afl-2.52b/testcases/images/bmp/not_kitty.bmp ./input
ファジングの実行
以下のように,ファジングを実行してしばらく待ちます.
# afl-fuzz -i ./input/ -o ./output/ -- ./bmp2tiff @@ /dev/null
実行時,上図のようにステータス画面が表示されますが,細かいことにはこだわらないでください.大事なのは右上の"total paths"と"uniq crashes",そして"uniq hangs"です."total paths"は,ファズが通過したプログラムのパスの識別数(パスカバレッジ??)です."uniq crashes"と"uniq hangs"はユニークなバグの検出件数です.その他のステータスの説明については,AFLのReadmeを参照してください.ここでは省略します.
結果
8分51秒で767,000回のテストケース実行を行い,209個のパスを発見し,4件のクラッシュと13件のハングを検出しました../out/crash/
にクラッシュを引き起こしたファズが保存されています.それを用いて簡単に再現確認ができます.
# cat ./out/crash/id0000* | ./bmp2tiff /dev/null
Segmentation fault
再びGIDACを用いた考察
以上のように,CVE-2014-9330を簡単に再現することができました.ただし,今回実施したのはコマンドラインのアプリでした.整理すると,AFLはファズ生成(G)に遺伝的アルゴリズムを採用し,ファズ入力(I)は標準入力です.また,バグ検出(D)はテスト対象の終了ステータスで,自動化(A)もカバレッジ(C)も実現しています.
皆さんが開発されているような複雑なソフトウェアに対してファジングする場合,ファズ入力(I)の部分を改良する必要があります.
最後に
ファジングの意味とブラックボックスとグレイボックスの違いをまとめます.
ファジングとは
- ソフトウェアテストのバグ検出のための手法
- 世の中には多くのツールが存在する
- ブラックボックス/グレイボックス/ホワイトボックスと分類できる
ブラックボックスファジングとは
- 様々なソフトウェアに対して比較的簡単にファジングできる
- カバレッジを計測ができないため,プログラムの深い部分までテストできたという根拠が示せない
グレイボックスファジングとは
- カバレッジを計測ができるので,プログラムの深い部分までテストすることができる
- 様々なソフトウェアでファジングを行うためにはファズ入力を改良する必要がある