rust

RustでlibFuzzerを使ったファジングを行う

はじめに

以前afl.rsを使ったファジングの手順を紹介したのですが、今回はlibFuzzerベースのcargo-fuzzを使ったファジングの手順を紹介します。

前提条件

項目 説明
検証日 2017.08.18
OS macOS 10.11.6
Rust rustc 1.21.0-nightly (52a330969 2017-07-27)
cargo-fuzz 0.4.3

現在はx86-64 Linuxとx86-64 macOSのみサポートしている模様

手順

今回は例として、 woothee-rustプロジェクトを使って説明します。

まずは cargo-fuzz 自体をインストールします。

$ cargo install --force cargo-fuzz

インストールが終わったら、ターゲットとなるRustプロジェクトのディレクトリルートでcargo fuzz initを実行し、ファジングテストのセットアップを行います。

$ cd woothee-rust
$ cargo fuzz init
$ tree
.
├── Cargo.toml
├── LICENSE
├── README.md
├── benches
│   └── benchmark.rs
├── build.rs
├── fuzz
│   ├── Cargo.toml
│   └── fuzz_targets
│       └── fuzz_target_1.rs
├── rustfmt.toml
├── src
│   ├── dataset.rs
│   ├── lib.rs
│   ├── parser.rs
│   └── woothee.rs
├── templates
│   ├── dataset.tmpl
│   └── tests.tmpl
└── tests
    ├── appliance.rs
    ├── blank.rs
    ├── crawler.rs
    ├── crawler_google.rs
    ├── crawler_nonmajor.rs
    ├── misc.rs
    ├── mobilephone_au.rs
    ├── mobilephone_docomo.rs
    ├── mobilephone_misc.rs
    ├── mobilephone_softbank.rs
    ├── mobilephone_willcom.rs
    ├── pc_lowpriority.rs
    ├── pc_misc.rs
    ├── pc_windows.rs
    ├── smartphone_android.rs
    ├── smartphone_ios.rs
    └── smartphone_misc.rs

6 directories, 31 files

ディレクトリルートにfuzzディレクトリが新たに作成され、ファジングに必要な最低限のファイルができあがります。
fuzz/fuzz_targets/fuzz_target_1.rs が実行の起点になるファイルになります。はじめは以下のような内容になっています。

#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate woothee;

fuzz_target!(|data: &[u8]| {
    // fuzzed code goes here
});

これを以下のように変更します。

#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate woothee;
use woothee::parser::Parser;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        let parser = Parser::new();
        let _ = parser.parse(s);
    }
});

これでテストの準備ができました。実行してみましょう。

$ cargo fuzz run fuzz_target_1
   Compiling arbitrary v0.1.0
   Compiling utf8-ranges v1.0.0
   Compiling regex-syntax v0.4.1
   Compiling void v1.0.2
     Running `rustc --crate-name arbitrary /Users/hattori-h/.cargo/registry/src/github.com-1ecc6299db9ec823/arbitrary-0.1.0/src/lib.rs --crate-type lib --emit=dep-info,link -C debuginfo=2 -C metadata=ed02362ebbd857c6 -C extra-filename=-ed02362ebbd857c6 --out-dir /Users/hattori-h/work/woothee-rust.new/fuzz/target/x86_64-apple-darwin/debug/deps --target x86_64-apple-darwin -L dependency=/Users/hattori-h/work/woothee-rust.new/fuzz/target/x86_64-apple-darwin/debug/deps -L dependency=/Users/hattori-h/work/woothee-rust.new/fuzz/target/debug/deps --cap-lints allow -Cpasses=sancov -Cllvm-args=-sanitizer-coverage-level=3 -Zsanitizer=address -Cpanic=abort`
   :
   : ビルド
   :
   Compiling woothee-fuzz v0.0.1 (file:///Users/hattori-h/work/woothee-rust.tmp/fuzz)
     Running `rustc --crate-name fuzz_target_1 fuzz/fuzz_targets/fuzz_target_1.rs --crate-type bin --emit=dep-info,link -C debuginfo=2 -C metadata=61313f395163699b -C extra-filename=-61313f395163699b --out-dir /Users/hattori-h/work/woothee-rust.tmp/fuzz/target/x86_64-apple-darwin/debug/deps --target x86_64-apple-darwin -L dependency=/Users/hattori-h/work/woothee-rust.tmp/fuzz/target/x86_64-apple-darwin/debug/deps -L dependency=/Users/hattori-h/work/woothee-rust.tmp/fuzz/target/debug/deps --extern woothee=/Users/hattori-h/work/woothee-rust.tmp/fuzz/target/x86_64-apple-darwin/debug/deps/libwoothee-c8ac09f4115f38f8.rlib --extern libfuzzer_sys=/Users/hattori-h/work/woothee-rust.tmp/fuzz/target/x86_64-apple-darwin/debug/deps/liblibfuzzer_sys-881433e46785513a.rlib -Cpasses=sancov -Cllvm-args=-sanitizer-coverage-level=3 -Zsanitizer=address -Cpanic=abort -L native=/Users/hattori-h/work/woothee-rust.tmp/fuzz/target/x86_64-apple-darwin/debug/build/libfuzzer-sys-111c832b98e6acbd/out`
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `fuzz/target/x86_64-apple-darwin/debug/fuzz_target_1 -artifact_prefix=/Users/hattori-h/work/woothee-rust.tmp/fuzz/artifacts/fuzz_target_1/ /Users/hattori-h/work/woothee-rust.tmp/fuzz/corpus/fuzz_target_1`
INFO: Seed: 2815031522
INFO: Loaded 0 modules (0 guards):
Loading corpus dir: /Users/hattori-h/work/woothee-rust.tmp/fuzz/corpus/fuzz_target_1
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0  READ units: 1
#1  INITED cov: 16670 corp: 1/1b exec/s: 0 rss: 34Mb
#2  NEW    cov: 16679 corp: 2/3b exec/s: 0 rss: 34Mb L: 2 MS: 1 InsertByte-
#7  NEW    cov: 16681 corp: 3/4b exec/s: 0 rss: 34Mb L: 1 MS: 1 ChangeByte-
#19 NEW    cov: 16755 corp: 4/5b exec/s: 0 rss: 34Mb L: 1 MS: 3 ChangeByte-EraseBytes-ChangeBinInt-
#37 NEW    cov: 16831 corp: 5/10b exec/s: 0 rss: 34Mb L: 5 MS: 1 CMP- DE: "\x00\x00\x00\x00"-
#38 NEW    cov: 16859 corp: 6/15b exec/s: 0 rss: 34Mb L: 5 MS: 2 CMP-ChangeBit- DE: "\x00\x00\x00\x00"-
#39 NEW    cov: 16876 corp: 7/24b exec/s: 0 rss: 34Mb L: 9 MS: 3 CMP-ChangeBit-PersAutoDict- DE: "\x00\x00\x00\x00"-"\x00\x00\x00\x00"-
#48 NEW    cov: 16892 corp: 8/43b exec/s: 0 rss: 35Mb L: 19 MS: 2 ChangeByte-InsertRepeatedBytes-
#53 NEW    cov: 16894 corp: 9/62b exec/s: 0 rss: 35Mb L: 19 MS: 2 PersAutoDict-ChangeBinInt- DE: "\x00\x00\x00\x00"-
#90 NEW    cov: 16897 corp: 10/69b exec/s: 0 rss: 35Mb L: 7 MS: 4 PersAutoDict-ChangeByte-ChangeASCIIInt-EraseBytes- DE: "\x00\x00\x00\x00"-
#94 NEW    cov: 16898 corp: 11/72b exec/s: 0 rss: 36Mb L: 3 MS: 3 CopyPart-ShuffleBytes-InsertByte-
#95 NEW    cov: 16903 corp: 12/94b exec/s: 0 rss: 36Mb L: 22 MS: 4 CopyPart-ShuffleBytes-InsertByte-CrossOver-
#97 NEW    cov: 16904 corp: 13/99b exec/s: 0 rss: 36Mb L: 5 MS: 1 CopyPart-
#110    NEW    cov: 16917 corp: 14/101b exec/s: 0 rss: 36Mb L: 2 MS: 4 ChangeBit-InsertByte-ShuffleBytes-ChangeBit-
   :
   : ファジングテストが続く
   :

Ctrl+C等で停止するまでテスト実行が継続されます。

テスト結果

今回のテストではpanicは検出できませんでしたが、panic等が発生した場合は fuzz/artifactsディレクトリ以下にテストで検出したデータが格納されます。
panicは検出できなかったのですが、実行時間がかかるパターンを検出しました。

$ ls fuzz/artifacts/fuzz_target_1/
slow-unit-3cbe49cc498d80637afa7f06153cca9c725934d0  slow-unit-e09e5f665aca223dc631b0f9c5ae49badb814b88

slow-unix-XXXに実行時間がかかったデータが格納されます。(これを使えば後ほど再現させることが可能です。)

その他

テスト追加

別のファジングテストを追加したい場合は cargo fuzz addを実行します。

$ cargo fuzz add test2
$ cargo fuzz list
fuzz_target_1
test2

fuzz/Cargo.tomlにもビルドターゲットが追加されるので、上記の場合だとcargo fuzz run test2とすれば新しく追加したテストが実行できます。

コーパスの指定

ファジングテストで使用するデータの元となるデータを指定することができます。woothee-rustの場合、ユーザーエージェントを指定することが多いので、それに類するデータを指定することで少し現実に近い形でテストすることもできます。

サンプルデータは以下のようなデータにしてみます。

Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_4; ja-jp) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1

別のコーパスとして使いたいので以下のように準備します。

$ mkdir mycorpus
$ echo "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_4; ja-jp) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1" > mycorpus/sample

コーパスを指定して実行します。またデータサイズも大きなものにして実行してみます。

$ cargo fuzz run fuzz_target_1 mycorpus -- -max_len=1024
    :
    :
INFO: Seed: 364705934
INFO: Loaded 0 modules (0 guards):
Loading corpus dir: mycorpus
#0  READ units: 1
#1  INITED cov: 16517 corp: 1/126b exec/s: 0 rss: 27Mb
#2  NEW    cov: 16599 corp: 2/253b exec/s: 0 rss: 27Mb L: 127 MS: 1 InsertByte-
#4  NEW    cov: 16606 corp: 3/319b exec/s: 0 rss: 27Mb L: 66 MS: 3 InsertByte-ChangeByte-EraseBytes-

指定したコーパス用のディレクトリを確認すると様々なデータが生成されているのがわかります。

$ ls -ltr mycorpus/
total 464
-rw-r--r--  1    126  8 19 22:15 sample
-rw-r--r--  1    732  8 19 22:18 e633414fee54285eceb763e861f4d74407822c97
-rw-r--r--  1     68  8 19 22:18 b2756e02a3ba92cc0f58f27be52ee3b602080f8f
-rw-r--r--  1   1024  8 19 22:18 795176a87e869f449b3a6430fc103c8fa0549ed9
-rw-r--r--  1    127  8 19 22:18 741b1197abb7ac8be3193da96ccc8f234d7f5cbf
-rw-r--r--  1    175  8 19 22:18 57995eafdffdba30a485177485cefd291b693861
-rw-r--r--  1     66  8 19 22:18 4334a4c31b0fb6865b1f83c0bc687901b766e7c1
-rw-r--r--  1     67  8 19 22:18 2e7db98cd25debfc333c4b6cada66f2c4bda2e87
-rw-r--r--  1    731  8 19 22:18 1b59508611eb56f8116308d9eac0f4b075c551ab
-rw-r--r--  1    613  8 19 22:18 d965544c1c37e7f58876c3bd887344f872453c91
    :

他にはコーパスを圧縮するcminサブコマンドを使えます。(テストを圧縮する?tminというサブコマンドもあるのですが、実行時エラーになったので結果どうなるのかわかりませんでした。)

$ cargo fuzz cmin fuzz_target_1 mycorpus
INFO: Seed: 723930214
INFO: Loaded 0 modules (0 guards):
INFO: -max_len is not provided, using 1048576
=== Merging extra 64 units
#64 MIN0   cov: 18970 units: 39 exec/s: 0 rss: 39Mb
#103    MIN1   cov: 4486 units: 36 exec/s: 0 rss: 41Mb
=== Merge: written 36 units