
純粋な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オプションを参照してください。