
純粋なRubyコードおよびRuby C拡張向けのカバレッジガイドファザーです。
Ruzzyは、GoogleのAtheris(Pythonファザー)に大きく触発されています。Ruzzyは、カバレッジの計装とファジングエンジンにlibFuzzer(またはLibAFL)を使用しています。また、C拡張のファジング時にはAddressSanitizerおよびUndefinedBehaviorSanitizerもサポートしています。Ruzzyの背後にある着想について詳しく知りたい場合は、論文「Design and Implementation of a Coverage-Guided Ruby Fuzzer」をご覧ください。
目次:
RuzzyはLinux(x86-64、AArch64/ARM64)およびmacOS(Apple Silicon)をサポートしています。Windowsでは、Dockerfileをビルドするか、開発環境を使用できます。Ruzzyには、比較的新しいバージョンのclang(14.0.0までテスト済み)が必要です。できれば最新リリースを使用してください。macOS固有の設定については、macOSユーザー向けの注意を参照してください。
次のコマンドでRuzzyをインストールします:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
gem install ruzzy
ここでは多くの設定を行っていますので、分解して説明します:
MAKE環境変数は、Ruzzy C拡張をコンパイルする際のmakeコマンドを上書きします。これにより、拡張機能のコンパイル時に後続の環境変数が尊重されるようにmakeに指示します。clangバイナリを使用するために使用されます。これにより、適切なファジングに必要な最新のclang機能が確実に使用されます。インストールで問題が発生した場合は、次のコマンドを実行してデバッグ出力を取得できます:
RUZZY_DEBUG=1 gem install --verbose ruzzy
純粋なRubyファジングターゲットが正規表現を多用する場合は、regexp_parserをインストールしてください:
gem install regexp_parser
インストール後、Ruzzyはこの機能を自動的に使用して、ファジング中に遭遇した正規表現をサンプリング(解決)します。これにより、ファザーは正規表現条件を通過して、追加のカバレッジを獲得できます。
Ruzzyには、その動作を示すおもちゃの例が含まれています。最初に、次の環境変数を設定します:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
ASAN_OPTIONS次に、次のコマンドで例を実行できます:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
LD_PRELOADは、Atherisと同じ理由で必要です。ただし、ASAN_OPTIONSとは異なり、exportすることはおそらく避けるべきです。他のプログラムに干渉する可能性があるためです。
次のようなクラッシュがすぐに発生するはずです:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2527961537
...
==45==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c0009bab80 at pc 0xffff99ea1b44 bp 0xffffce8a67d0 sp 0xffffce8a67c8
...
SUMMARY: AddressSanitizer: heap-use-after-free /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/ext/dummy/dummy.c:18:24 in _c_dummy_test_one_input
...
==45==ABORTING
MS: 4 EraseBytes-CopyPart-CopyPart-ChangeBit-; base unit: 410e5346bca8ee150ffd507311dd85789f2e171e
0x48,0x49,
HI
artifact_prefix='./'; Test unit written to ./crash-253420c1158bc6382093d409ce2e9cff5806e980
Base64: SEk=
メモリ違反を引き起こす入力("HI")が正しく見つかったことがわかります。詳細については、dummy.cを参照して、この違反が発生した理由を確認してください。
次のコマンドでクラッシュケースを再実行できます:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
以下のサニタイザーが利用可能です:
Ruzzy::ASAN_PATH:AddressSanitizer用Ruzzy::UBSAN_PATH:UndefinedBehaviorSanitizer用例として、小さなRubyスクリプトをファジングしてみましょう。純粋なRubyコードのファジングには、トレーサースクリプトとファジングハーネスの2つのRubyスクリプトが必要です。トレーサースクリプトは、Rubyインタプリタの実装の詳細のために必要です。
まず、トレーサースクリプトです。test_tracer.rbという名前にしましょう:
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
次に、ファジングハーネスです。test_harness.rbという名前にしましょう:
# frozen_string_literal: true
require 'ruzzy'
def fuzzing_target(input)
if input.length == 4
if input[0] == 'F'
if input[1] == 'U'
if input[2] == 'Z'
if input[3] == 'Z'
raise
end
end
end
end
end
end
test_one_input = lambda do |data|
fuzzing_target(data) # ここにファジングターゲットを記述
return 0
end
Ruzzy.fuzz(test_one_input)
次のコマンドでこのファイルを実行し、ファジングを開始できます:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
次のようなクラッシュがすぐに発生するはずです:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2311041000
...
/app/ruzzy/bin/test_harness.rb:12:in `block in <top (required)>': unhandled exception
from /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/lib/ruzzy.rb:15:in `c_fuzz'
from /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/lib/ruzzy.rb:15:in `fuzz'
from /app/ruzzy/bin/test_harness.rb:35:in `<top (required)>'
from bin/test_tracer.rb:7:in `require_relative'
from bin/test_tracer.rb:7:in `<main>'
...
SUMMARY: libFuzzer: fuzz target exited
MS: 1 CopyPart-; base unit: 24b4b428cf94c21616893d6f94b30398a49d27cc
0x46,0x55,0x5a,0x5a,
FUZZ
artifact_prefix='./'; Test unit written to ./crash-aea2e3923af219a8956f626558ef32f30a914ebc
Base64: RlVaWg==
例外を発生させる入力("FUZZ")が正しく見つかったことがわかります。
独自のターゲットをファジングするには、test_one_input lambdaを変更して、ターゲット関数を呼び出します。
例として、msgpack-rubyライブラリをファジングしてみましょう。まず、gemをインストールします:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
CFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
CXXFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
gem install msgpack
Ruzzyのコンパイル時に使用した環境変数に加えて、CFLAGSとCXXFLAGSを指定しています。これらのフラグはファジングプロセスを支援します。アドレスサニタイザーや改善されたスタックトレース情報などの便利な機能を有効にします。詳細については、AddressSanitizerFlagsを参照してください。
次に、msgpack用のファジングハーネスが必要です。以下は、libFuzzerに詳しい方にはおなじみかもしれません:
# frozen_string_literal: true
require 'msgpack'
require 'ruzzy'
test_one_input = lambda do |data|
begin
MessagePack.unpack(data)
rescue Exception
# メモリ破損を探しているため、Rubyの例外は無視
end
return 0
end
Ruzzy.fuzz(test_one_input)
このファイルをfuzz_msgpack.rbという名前で保存しましょう。次のコマンドでこのファイルを実行し、ファジングを開始できます:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
libFuzzerオプションは、次のようにRubyスクリプトに渡すことができます:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
詳細については、libFuzzerオプションを参照してください。
独自のターゲットをファジングするには、test_one_input lambdaを変更して、ターゲット関数を呼び出します。
Ruzzyモジュールは、トップレベルのエントリポイントを公開します。
Ruzzy::FuzzedDataProviderは、生のファザーバイトを分割して、型付きのRuby値に変換します。
test_one_input = lambda do |data|
fdp = Ruzzy::FuzzedDataProvider.new(data)
name = fdp.consume_random_length_string(50)
age = fdp.consume_int_in_range(0, 150)
score = fdp.consume_float_in_range(0.0, 100.0)
role = fdp.pick_value_in_list(['admin', 'user', 'guest'])
User.new(name: name, age: age, score: score, role: role).validate!
end
Ruzzy.fuzz(test_one_input)
すべてのメソッドは、データが尽きた場合にデフォルト値(0、""、false、min)を返します。
macOS上のRuzzyには、HomebrewでインストールしたLLVM(Apple ClangにはlibFuzzerが含まれていません)と、システム以外のRuby(/usr/bin/rubyのシステムRubyはSIPで保護されており、Ruby起動前にDYLD_*環境変数が削除されます)が必要です。
brew install llvm ruby
システム以外のRuby(brew、rbenv、asdf)はどれでも動作しますが、以下の注意事項をシムベースのバージョンマネージャーについて参照してください。
Homebrew ClangのパスとmacOSに適したリンカーフラグを使用します:
MAKE="make --environment-overrides V=1" \
CC="$(brew --prefix llvm)/bin/clang" \
CXX="$(brew --prefix llvm)/bin/clang++" \
LDSHARED="$(brew --prefix llvm)/bin/clang -dynamic -bundle -undefined dynamic_lookup" \
LDSHAREDXX="$(brew --prefix llvm)/bin/clang++ -dynamic -bundle -undefined dynamic_lookup" \
gem install ruzzy
LD_PRELOADの代わりにDYLD_INSERT_LIBRARIESを使用します:
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
Ruzzy::ASAN_PATHおよびRuzzy::UBSAN_PATHは、macOSでは.dylibファイルに解決されます。
asdf、rbenv) はDYLD_*環境変数を削除します。 これらのシムは#!/usr/bin/env bashを使用し、/usr/bin/envはSIPで保護されているため、macOSはRuby起動前にDYLD_INSERT_LIBRARIESを削除します。Homebrew Ruby(シムなし)を使用するか、インストールされているRubyバイナリへの絶対パスを呼び出します:
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIESでASan dylibをロードすると、起動時にプロセスがハングするバグがあります。Ruzzyが起動時にハングする場合は、Homebrew LLVMを更新してください(brew upgrade llvm)。Ruzzyを使用して発見されたバグ:
toml gem: #76toml-rb gem: #150ox gem: #351、#410Marshalガベージコレクタークラッシュ: #20941redcarpet gem: #813開発はローカルで行うか、このリポジトリに用意されているDockerfileを使用して行うことができます。
次のコマンドでRuzzy Dockerイメージをビルドできます:
docker build --tag ruzzy .
次に、次のコマンドでコンテナにシェルアクセスできます:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
RuzzyのC拡張をコンパイルするために、rake-compilerを使用しています。
コンテナ内で次のコマンドを使用してC拡張をコンパイルできます:
rake compile
Rubyコードのテストには、rakeユニットテストを使用しています。
コンテナ内で次のコマンドを使用してテストを実行できます:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
Rubyコードのリントには、rubocopを使用しています。
コンテナ内で次のコマンドを使用してrubocopを実行できます:
rubocop
新しいgitタグがプッシュされると、Ruzzyは自動的にRubyGemsにリリースされます。
新しいバージョンをリリースするには、次のコマンドを実行します:
git tag vX.X.X
git push --tags
| メソッド | 説明 |
|---|
Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS) | test_one_input(生のバイトを受け取るproc/lambda)をファジングします。 |
Ruzzy.trace(harness_script) | harness_scriptをRubyのブランチカバレッジ計装でラップし、その後requireします。純粋なRubyファジングに必要です。 |
Ruzzy.dummy | バンドルされているおもちゃのハーネス(heap-use-after-freeデモ)をファジングします。 |
Ruzzy.dummy_test_one_input(data) | おもちゃのハーネス自体。 |
| 定数 | 説明 |
|---|
Ruzzy::ASAN_PATH | ASan + ファザーラッパーへのパス。LD_PRELOAD(Linux) / DYLD_INSERT_LIBRARIES(macOS)とともに使用します。 |
Ruzzy::UBSAN_PATH | 同上、UBSan用。 |
Ruzzy::EXT_PATH | ext/cruzzyビルドディレクトリへのパス。 |
Ruzzy::DEFAULT_ARGS | ファザーに渡されるデフォルトの引数。 |
| メソッド | 説明 | 戻り値 |
|---|
remaining_bytes | 未消費の残りバイト数 | Integer |
consume_bytes(count) | 最大countバイトの生のバイトを消費 | バイナリString |
consume_random_length_string(max_length) | 可変長の文字列を消費;\ + 非\バイトで終了 | String |
consume_remaining_bytes | 残りのすべてのバイトを消費 | バイナリString |
consume_remaining_as_string | consume_remaining_bytesのエイリアス | バイナリString |
consume_uint(count) | countバイトからの符号なし整数 | Integer |
consume_int(count) | countバイトからの符号付き(2の補数)整数 | Integer |
consume_int_in_range(min, max) | [min, max]の範囲で一様分布する整数 | Integer |
consume_bool | 1バイト(LSB)からのブール値 | true/false |
consume_float | 完全な倍精度範囲にわたる浮動小数点数 | Float |
consume_float_in_range(min, max) | [min, max]の範囲の浮動小数点数 | Float |
consume_probability | [0.0, 1.0]の範囲の浮動小数点数 | Float |
pick_value_in_list(list) | listからランダムな要素 | 要素 |