Ruzzy
Ruzzy は、libFuzzer をベースにした Ruby 用のカバレッジガイド型ファザーです。純粋な Ruby コードと Ruby C 拡張の両方をファジングでき、メモリ破壊や未定義動作を検出するためのサニタイザをサポートしています。
使用するタイミング
Ruzzy は現在、Ruby 向けの唯一のプロダクションレディなカバレッジガイド型ファザーです。
Ruzzy を選ぶべきケース:
- Ruby アプリケーションまたはライブラリのファジング
- Ruby C 拡張のメモリ安全性問題のテスト
- Ruby コードにカバレッジガイド型ファジングが必要な場合
- ネイティブ拡張を持つ Ruby gem を扱う場合
クイックスタート
環境を設定します:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
付属のサンプル例でテストします:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
これにより、Ruzzy が正しく動作していることを示すクラッシュがすぐに見つかるはずです。
インストール
プラットフォームサポート
Ruzzy は Linux x86-64 および AArch64/ARM64 をサポートしています。macOS または Windows の場合は、Dockerfile または 開発環境 を使用してください。
前提条件
- Linux x86-64 または AArch64/ARM64
- 最新バージョンの clang(14.0.0 以降でテスト済み、最新リリース推奨)
- gem がインストールされた Ruby
インストールコマンド
clang コンパイラフラグを指定して 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:後続の環境変数を優先するように make を上書きしますCC、CXX、LDSHARED、LDSHAREDXX:最新の機能を利用するために適切な clang バイナリを使用します
インストールのトラブルシューティング
インストールが失敗した場合は、デバッグ出力を有効にします:
RUZZY_DEBUG=1 gem install --verbose ruzzy
インストールの確認
クイックスタートのサンプル例を実行してインストールを確認します(クイックスタートセクションを参照)。
ハーネスの作成
純粋な Ruby コードのファジング
純粋な Ruby のファジングには、Ruby インタプリタの実装の詳細により、2 つのスクリプトが必要です。
トレーサースクリプト(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
Ruby C 拡張のファジング
C 拡張はトレーサー不要で、1 つのハーネスファイルでファジングできます。
msgpack 用のハーネス例(fuzz_msgpack.rb):
# 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)
実行:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
ハーネスのルール
| すべきこと | してはいけないこと |
|---|---|
| C 拡張をテストする場合は Ruby 例外をキャッチする | Ruby 例外でファザーをクラッシュさせる |
| test_one_input ラムダから 0 を返す | 他の値を返す |
| ハーネスを決定論的に保つ | 乱数や時間ベースのロジックを使用する |
| 純粋な Ruby にはトレーサースクリプトを使用する | 純粋な Ruby コードでトレーサーを省略する |
関連情報: 詳細なハーネス作成テクニック、複雑な入力のパターン、高度な戦略については、fuzz-harness-writing テクニックスキルを参照してください。
コンパイル
サニタイザ付き gem のインストール
ファジング用に C 拡張を含む 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 <gem-name>
ビルドフラグ
| フラグ | 目的 |
|---|---|
-fsanitize=address,fuzzer-no-link | AddressSanitizer とファザー計測を有効化 |
-fno-omit-frame-pointer | スタックトレースの品質を向上 |
-fno-common | サニタイザとの互換性を向上 |
-fPIC | 共有ライブラリ用の位置独立コード |
-g | デバッグシンボルを含める |
キャンペーンの実行
環境設定
ファジングキャンペーンを実行する前に、ASAN_OPTIONS を設定します:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
オプションの説明:
allocator_may_return_null=1:影響の小さい一般的な割り当て失敗(DoS)をスキップdetect_leaks=0:Ruby インタプリタはデータをリークするため、当面はリーク検出を無効化use_sigaltstack=0:Ruby は ASan で sigaltstack を無効にすることを推奨
基本実行
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby harness.rb
注意: LD_PRELOAD はサニタイザ注入のために必要です。ASAN_OPTIONS とは異なり、他のプログラムに干渉する可能性があるためエクスポートしないでください。
コーパスを使用する場合
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby harness.rb /path/to/corpus
libFuzzer オプションの受け渡し
すべての libFuzzer オプションを引数として渡せます:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby harness.rb /path/to/corpus -max_len=1024 -timeout=10
詳細は libFuzzer のオプション を参照してください。
クラッシュの再現
クラッシュファイルを渡して再実行します:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby harness.rb ./crash-253420c1158bc6382093d409ce2e9cff5806e980
出力の解釈
| 出力 | 意味 |
|---|---|
INFO: Running with entropic power schedule | ファジングキャンペーンが開始 |
ERROR: AddressSanitizer: heap-use-after-free | メモリ破壊を検出 |
SUMMARY: libFuzzer: fuzz target exited | Ruby 例外が発生 |
artifact_prefix='./'; Test unit written to ./crash-* | クラッシュ入力が保存されました |
Base64: ... | クラッシュ入力の Base64 エンコード |
サニタイザの統合
AddressSanitizer(ASan)
Ruzzy にはプリコンパイル済みの AddressSanitizer ライブラリが含まれています:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby harness.rb
ASan は以下の検出に使用します:
- ヒープバッファオーバーフロー
- スタックバッファオーバーフロー
- Use-after-free
- Double-free
- メモリリーク(Ruzzy ではデフォルトで無効)
UndefinedBehaviorSanitizer(UBSan)
Ruzzy には UBSan も含まれています:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::UBSAN_PATH') \
ruby harness.rb
UBSan は以下の検出に使用します:
- 符号付き整数オーバーフロー
- NULL ポインタ参照外し
- アライメントされていないメモリアクセス
- ゼロ除算
よくあるサニタイザの問題
| 問題 | 解決策 |
|---|---|
| Ruby インタプリタのリーク警告 | ASAN_OPTIONS=detect_leaks=0 を使用 |
| Sigaltstack の競合 | ASAN_OPTIONS=use_sigaltstack=0 を使用 |
| 割り当て失敗の大量出力 | ASAN_OPTIONS=allocator_may_return_null=1 を使用 |
| LD_PRELOAD がツールに干渉する | エクスポートせず、ruby コマンドとインラインで設定 |
関連情報: 詳細なサニタイザ設定、一般的な問題、高度なフラグについては、address-sanitizer および undefined-behavior-sanitizer テクニックスキルを参照してください。
実際の例
例:msgpack-ruby
msgpack MessagePack パーサーのメモリ破壊ファジング。
サニタイザ付きでインストール:
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
ハーネス(fuzz_msgpack.rb):
# 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)
実行:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
例:純粋な 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'
require_relative 'my_parser'
test_one_input = lambda do |data|
begin
MyParser.parse(data)
rescue StandardError
# 不正な入力による予期される例外
end
return 0
end
Ruzzy.fuzz(test_one_input)
実行:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| インストール失敗 | clang のバージョンまたはパスが間違っている | clang のパスを確認し、clang 14.0.0+ を使用 |
cannot open shared object file | LD_PRELOAD が設定されていない | ruby コマンドとインラインで LD_PRELOAD を設定 |
| ファザーがすぐに終了する | コーパスディレクトリがない | コーパスディレクトリを作成するか、引数として渡す |
| カバレッジが進まない | 純粋な Ruby にはトレーサーが必要 | 純粋な Ruby コードにはトレーサースクリプトを使用 |
| リーク検出の大量出力 | Ruby インタプリタのリーク | ASAN_OPTIONS=detect_leaks=0 を設定 |
| インストールのデバッグが必要 | コンパイルエラー | RUZZY_DEBUG=1 gem install --verbose ruzzy を使用 |
関連スキル
テクニックスキル
| スキル | ユースケース |
|---|---|
| fuzz-harness-writing | 効果的なハーネス作成のための詳細ガイダンス |
| address-sanitizer | ファジング中のメモリエラー検出 |
| undefined-behavior-sanitizer | C 拡張における未定義動作の検出 |
| libfuzzer | libFuzzer オプションの理解(Ruzzy は libFuzzer 上に構築) |
関連ファザー
| スキル | 検討するタイミング |
|---|---|
| libfuzzer | Ruby C 拡張のコードを C/C++ で直接ファジングする場合 |
| aflpp | Ruby インタプリタを計測して Ruby をファジングする代替アプローチ |
リソース
主要な外部リソース
Ruzzy の紹介:カバレッジガイド型 Ruby ファザー Ruzzy を発表する Trail of Bits 公式ブログ記事。動機、アーキテクチャ、初期結果をカバー。
Ruzzy GitHub リポジトリ ソースコード、追加の例、開発手順。
libFuzzer ドキュメント Ruzzy は libFuzzer 上に構築されているため、libFuzzer のオプションと動作を理解することが重要です。
Ruby C 拡張のファジング コンパイルフラグと例を含む C 拡張ファジングの詳細ガイド。
純粋な Ruby コードのファジング 純粋な Ruby ファジングに必要なトレーサーパターンの詳細ガイド。