
Ein abdeckungsgesteuerter Fuzzer für reinen Ruby-Code und Ruby-C-Erweiterungen.
Ein coverage-gesteuerter Fuzzer für reinen Ruby-Code und Ruby-C-Erweiterungen.
Ruzzy ist stark von Googles Atheris inspiriert, einem Python-Fuzzer. Ruzzy verwendet libFuzzer (oder LibAFL) für seine Coverage-Instrumentierung und als Fuzzing-Engine. Ruzzy unterstützt beim Fuzzing von C-Erweiterungen außerdem AddressSanitizer und UndefinedBehaviorSanitizer. Wenn Sie mehr über die Inspiration hinter Ruzzy erfahren möchten, lesen Sie unser Paper: Design and Implementation of a Coverage-Guided Ruby Fuzzer.
Inhaltsverzeichnis:
Ruzzy unterstützt Linux (x86-64, AArch64/ARM64) und macOS (Apple Silicon). Unter Windows können Sie das Dockerfile erstellen und/oder die Entwicklungsumgebung nutzen. Ruzzy benötigt eine aktuelle Version von clang (getestet ab 14.0.0), vorzugsweise die neueste Version. Informationen zur macOS-spezifischen Einrichtung finden Sie unter Hinweise für macOS-Benutzer.
Installieren Sie Ruzzy mit dem folgenden Befehl:
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
Hier passiert einiges; schauen wir uns das einmal genauer an:
MAKE überschreibt den make-Befehl beim Kompilieren der Ruzzy-C-Erweiterung. Dadurch wird make angewiesen, die nachfolgenden Umgebungsvariablen beim Kompilieren der Erweiterung zu berücksichtigen.clang-Binaries zum Einsatz kommen. So wird gewährleistet, dass die neuesten clang-Funktionen verfügbar sind, die für ein ordnungsgemäßes Fuzzing erforderlich sind.Sollten beim Installieren Probleme auftreten, können Sie den folgenden Befehl ausführen, um Debug-Ausgaben zu erhalten:
RUZZY_DEBUG=1 gem install --verbose ruzzy
Wenn Ihr Fuzzing-Ziel für reinen Ruby-Code stark auf Regexps setzt, installieren Sie regexp_parser:
gem install regexp_parser
Nach der Installation nutzt Ruzzy diese Funktionalität automatisch, um Regexps, auf die es beim Fuzzing stößt, zu sampeln (d. h. zu lösen). Dadurch kann der Fuzzer an Regexp-Bedingungen vorbeigelangen und zusätzliche Coverage freischalten.
Ruzzy enthält ein Toy-Beispiel, um zu demonstrieren, wie es funktioniert. Setzen Sie zunächst die folgende Umgebungsvariable:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
ASAN_OPTIONSAnschließend können Sie das Beispiel mit dem folgenden Befehl ausführen:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
LD_PRELOAD wird aus denselben Gründen benötigt wie bei Atheris. Anders als ASAN_OPTIONS sollten Sie es jedoch wahrscheinlich nicht per export setzen, da es andere Programme stören kann.
Es sollte schnell einen Absturz wie den folgenden erzeugen:
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=
Wir können sehen, dass die Eingabe ("HI") korrekt gefunden wurde, die eine Speicherverletzung verursacht hat. Weitere Informationen dazu, warum diese Verletzung aufgetreten ist, finden Sie in dummy.c.
Sie können den Absturzfall mit dem folgenden Befehl erneut ausführen:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
Die folgenden Sanitizer sind verfügbar:
Ruzzy::ASAN_PATH für AddressSanitizerRuzzy::UBSAN_PATH für UndefinedBehaviorSanitizerFuzzen wir als Beispiel ein kleines Ruby-Skript. Das Fuzzing von reinem Ruby-Code erfordert zwei Ruby-Skripte: ein Tracer-Skript und eine Fuzzing-Harness. Das Tracer-Skript ist aufgrund eines Implementierungsdetails des Ruby-Interpreters erforderlich.
Zuerst das Tracer-Skript; nennen wir es test_tracer.rb:
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
Als Nächstes die Fuzzing-Harness; nennen wir sie 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) # Your fuzzing target would go here
return 0
end
Ruzzy.fuzz(test_one_input)
Sie können diese Datei ausführen und mit dem folgenden Befehl das Fuzzing starten:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
Es sollte schnell einen Absturz wie den folgenden erzeugen:
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==
Wir können sehen, dass die Eingabe ("FUZZ") korrekt gefunden wurde, die eine Ausnahme verursacht hat.
Um Ihr eigenes Ziel zu fuzzen, ändern Sie das test_one_input-lambda, sodass es Ihre Zielfunktion aufruft.
Fuzzen wir als Beispiel die Bibliothek msgpack-ruby. Installieren Sie zunächst das 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
Zusätzlich zu den Umgebungsvariablen, die beim Kompilieren von Ruzzy verwendet werden, geben wir CFLAGS und CXXFLAGS an. Diese Flags unterstützen den Fuzzing-Prozess. Sie aktivieren nützliche Funktionen wie einen Address Sanitizer und verbesserte Stack-Trace-Informationen. Weitere Informationen finden Sie unter AddressSanitizerFlags.
Als Nächstes benötigen wir eine Fuzzing-Harness für msgpack. Das Folgende dürfte denen bekannt vorkommen, die Erfahrung mit libFuzzer haben:
# frozen_string_literal: true
require 'msgpack'
require 'ruzzy'
test_one_input = lambda do |data|
begin
MessagePack.unpack(data)
rescue Exception
# We're looking for memory corruption, not Ruby exceptions
end
return 0
end
Ruzzy.fuzz(test_one_input)
Nennen wir diese Datei fuzz_msgpack.rb. Sie können diese Datei ausführen und mit dem folgenden Befehl das Fuzzing starten:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
libFuzzer-Optionen können wie folgt an das Ruby-Skript übergeben werden:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
Weitere Informationen finden Sie unter libFuzzer-Optionen.
Um Ihr eigenes Ziel zu fuzzen, ändern Sie das test_one_input-lambda, sodass es Ihre Zielfunktion aufruft.
Das Modul Ruzzy stellt die zentralen Einstiegspunkte bereit.
| Methode | Beschreibung |
|---|---|
Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS) | Fuzzt test_one_input (ein proc/lambda, das rohe Bytes entgegennimmt). |
Ruzzy.trace(harness_script) | Umhüllt harness_script mit Ruby-Branch-Coverage-Instrumentierung und lädt es anschließend per require. Erforderlich für reines Ruby-Fuzzing. |
Ruzzy.dummy | Fuzzt die mitgelieferte Beispiel-Harness (Heap-use-after-free-Demo). |
Ruzzy.dummy_test_one_input(data) | Die Beispiel-Harness selbst. |
| Konstante | Beschreibung |
|---|---|
Ruzzy::ASAN_PATH | Pfad zum ASan- und Fuzzer-Wrapper. Verwendung mit LD_PRELOAD (Linux) / DYLD_INSERT_LIBRARIES (macOS). |
Ruzzy::UBSAN_PATH | Dasselbe für UBSan. |
Ruzzy::EXT_PATH | Pfad zum Build-Verzeichnis ext/cruzzy. |
Ruzzy::DEFAULT_ARGS | Standardargumente, die an den Fuzzer übergeben werden. |
Ruzzy::FuzzedDataProvider zerteilt rohe Fuzzer-Bytes in typisierte Ruby-Werte.
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)
| Methode | Beschreibung | Rückgabe |
|---|---|---|
remaining_bytes | Anzahl der verbleibenden, noch nicht konsumierten Bytes | Integer |
consume_bytes(count) | Konsumiert bis zu count rohe Bytes | binäre String |
consume_random_length_string(max_length) | Konsumiert eine Zeichenkette variabler Länge; endet bei \ + Nicht-\-Byte | String |
consume_remaining_bytes | Konsumiert alle verbleibenden Bytes | binäre String |
consume_remaining_as_string | Alias für consume_remaining_bytes | binäre String |
consume_uint(count) | Vorzeichenlose Ganzzahl aus count Bytes | Integer |
consume_int(count) | Vorzeichenbehaftete (Zweierkomplement-)Ganzzahl aus count Bytes | Integer |
consume_int_in_range(min, max) | Gleichverteilte Ganzzahl in [min, max] | Integer |
consume_bool | Boolescher Wert aus einem Byte (LSB) | true/false |
consume_float | Float über den gesamten Double-Bereich | Float |
consume_float_in_range(min, max) | Float in [min, max] | Float |
consume_probability | Float in [0.0, 1.0] | Float |
pick_value_in_list(list) | Zufälliges Element aus list |
Alle Methoden geben Standardwerte (0, "", false, min) zurück, wenn die Daten erschöpft sind.
Ruzzy erfordert unter macOS ein per Homebrew installiertes LLVM (Apple Clang enthält kein libFuzzer) sowie ein Nicht-System-Ruby (das System-Ruby unter /usr/bin/ruby ist SIP-geschützt, wodurch die DYLD_*-Umgebungsvariablen entfernt werden, bevor Ruby startet).
brew install llvm ruby
Jedes Nicht-System-Ruby funktioniert (brew, rbenv, asdf); beachten Sie jedoch die Einschränkungen unten für Shim-basierte Versionsmanager.
Verwenden Sie die Homebrew-Clang-Pfade und die für macOS geeigneten Linker-Flags:
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
Verwenden Sie DYLD_INSERT_LIBRARIES anstelle von LD_PRELOAD:
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
Ruzzy::ASAN_PATH und Ruzzy::UBSAN_PATH verweisen unter macOS auf .dylib-Dateien.
asdf, rbenv) entfernen DYLD_*-Umgebungsvariablen. Diese Shims verwenden #!/usr/bin/env bash, und /usr/bin/env ist SIP-geschützt, weshalb macOS DYLD_INSERT_LIBRARIES entfernt, bevor Ruby startet. Verwenden Sie entweder Homebrew-Ruby (das keinen Shim hat) oder rufen Sie den absoluten Pfad zur installierten Ruby-Binärdatei auf:
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIES auf die ASan-dylib den Prozess beim Start aufhängt. Wenn Ruzzy beim Start hängen bleibt, aktualisieren Sie Homebrew-LLVM (brew upgrade llvm).Mit Ruzzy gefundene Fehler:
Die Entwicklung kann lokal oder mithilfe des in diesem Repository bereitgestellten Dockerfile erfolgen.
Sie können das Ruzzy-Docker-Image mit dem folgenden Befehl erstellen:
docker build --tag ruzzy .
Anschließend können Sie mit dem folgenden Befehl eine Shell im Container öffnen:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
Wir verwenden rake-compiler, um die C-Erweiterungen von Ruzzy zu kompilieren.
Sie können die C-Erweiterungen im Container mit dem folgenden Befehl kompilieren:
rake compile
Wir verwenden rake-Unit-Tests, um den Ruby-Code zu testen.
Sie können die Tests im Container mit dem folgenden Befehl ausführen:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
Wir verwenden rubocop, um den Ruby-Code zu linten.
Sie können rubocop im Container mit dem folgenden Befehl ausführen:
rubocop
Ruzzy wird automatisch auf RubyGems veröffentlicht, wenn ein neuer Git-Tag gepusht wird.
Um eine neue Version zu veröffentlichen, führen Sie die folgenden Befehle aus:
git tag vX.X.X
git push --tags
| Element |