
Un fuzzer guidé par la couverture pour le code Ruby pur et les extensions C de Ruby
Un fuzzer guidé par la couverture pour le code Ruby pur et les extensions C de Ruby.
Ruzzy est fortement inspiré par Atheris de Google, un fuzzer pour Python. Ruzzy utilise libFuzzer (ou LibAFL) pour son instrumentation de couverture et son moteur de fuzzing. Ruzzy prend également en charge AddressSanitizer et UndefinedBehaviorSanitizer lors du fuzzing d'extensions C. Si vous souhaitez en savoir plus sur ce qui a inspiré Ruzzy, consultez notre article : Design and Implementation of a Coverage-Guided Ruby Fuzzer.
Table des matières :
Ruzzy prend en charge Linux (x86-64, AArch64/ARM64) et macOS (Apple Silicon). Sous Windows, vous pouvez construire le Dockerfile et/ou utiliser l'environnement de développement. Ruzzy nécessite une version récente de clang (testée jusqu'à 14.0.0), de préférence la dernière version. Pour une configuration spécifique à macOS, voir notes pour les utilisateurs de macOS.
Installez Ruzzy avec la commande suivante :
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
Il y a beaucoup de choses ici, alors décomposons-les :
MAKE remplace la commande make lors de la compilation de l'extension C de Ruzzy. Cela indique à make de respecter les variables d'environnement suivantes lors de la compilation de l'extension.clang. Cela nous assure de disposer des dernières fonctionnalités de clang, nécessaires pour un fuzzing correct.Si vous rencontrez des problèmes d'installation, vous pouvez exécuter la commande suivante pour obtenir une sortie de débogage :
RUZZY_DEBUG=1 gem install --verbose ruzzy
Si votre cible de fuzzing en Ruby pur utilise massivement des expressions régulières, installez regexp_parser :
gem install regexp_parser
Une fois installé, Ruzzy utilisera automatiquement cette fonctionnalité pour échantillonner (c'est-à-dire résoudre) les expressions régulières qu'il rencontre lors du fuzzing. Cela permet au fuzzer de dépasser les conditions liées aux expressions régulières et de débloquer une couverture supplémentaire.
Ruzzy inclut un exemple jouet pour démontrer son fonctionnement. Tout d'abord, définissez la variable d'environnement suivante :
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
ASAN_OPTIONSVous pouvez ensuite exécuter l'exemple avec la commande suivante :
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
LD_PRELOAD est requis pour les mêmes raisons qu'Atheris. Cependant, contrairement à ASAN_OPTIONS, vous ne voulez probablement pas l'exporter car cela pourrait interférer avec d'autres programmes.
Il devrait rapidement produire un crash comme celui-ci :
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=
Nous pouvons voir qu'il a correctement trouvé l'entrée ("HI") qui a produit une violation mémoire. Pour plus d'informations, voir dummy.c pour comprendre pourquoi cette violation s'est produite.
Vous pouvez ré-exécuter le cas de crash avec la commande suivante :
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
Les sanitizers suivants sont disponibles :
Ruzzy::ASAN_PATH pour AddressSanitizerRuzzy::UBSAN_PATH pour UndefinedBehaviorSanitizerFuzzeons un petit script Ruby comme exemple. Le fuzzing de code Ruby pur nécessite deux scripts Ruby : un script de trace et un harnais de fuzzing. Le script de trace est nécessaire en raison d'un détail d'implémentation de l'interpréteur Ruby.
Tout d'abord, le script de trace, appelons-le test_tracer.rb :
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
Ensuite, le harnais de fuzzing, appelons-le 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)
Vous pouvez exécuter ce fichier et commencer le fuzzing avec la commande suivante :
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
Il devrait rapidement produire un crash comme celui-ci :
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==
Nous pouvons voir qu'il a correctement trouvé l'entrée ("FUZZ") qui a produit une exception.
Pour fuzzer votre propre cible, modifiez le lambda test_one_input pour appeler votre fonction cible.
Fuzzeons la bibliothèque msgpack-ruby comme exemple. Tout d'abord, installez la gemme :
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
En plus des variables d'environnement utilisées lors de la compilation de Ruzzy, nous spécifions CFLAGS et CXXFLAGS. Ces drapeaux aident au processus de fuzzing. Ils activent des fonctionnalités utiles comme un détecteur d'adresses et une meilleure information de trace de pile. Pour plus d'informations, voir AddressSanitizerFlags.
Ensuite, nous avons besoin d'un harnais de fuzzing pour msgpack. Ce qui suit peut être familier à ceux qui ont de l'expérience avec libFuzzer :
# 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)
Appelons ce fichier fuzz_msgpack.rb. Vous pouvez exécuter ce fichier et commencer le fuzzing avec la commande suivante :
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
Les options de libFuzzer peuvent être passées au script Ruby comme ceci :
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
Voir options de libFuzzer pour plus d'informations.
Pour fuzzer votre propre cible, modifiez le lambda test_one_input pour appeler votre fonction cible.
Le module Ruzzy expose les points d'entrée de plus haut niveau.
| Méthode | Description |
|---|---|
Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS) | Fuzze test_one_input (un proc/lambda prenant des octets bruts). |
Ruzzy.trace(harness_script) | Enveloppe harness_script avec l'instrumentation de couverture de branches de Ruby, puis require celle-ci. Requis pour le fuzzing de Ruby pur. |
Ruzzy.dummy | Fuzze le harnais jouet fourni (démo heap-use-after-free). |
Ruzzy.dummy_test_one_input(data) | Le harnais jouet lui-même. |
| Constante | Description |
|---|---|
Ruzzy::ASAN_PATH | Chemin vers le wrapper ASan + fuzzer. À utiliser avec LD_PRELOAD (Linux) / DYLD_INSERT_LIBRARIES (macOS). |
Ruzzy::UBSAN_PATH | Idem, pour UBSan. |
Ruzzy::EXT_PATH | Chemin vers le répertoire de construction ext/cruzzy. |
Ruzzy::DEFAULT_ARGS | Arguments par défaut passés au fuzzer. |
Ruzzy::FuzzedDataProvider divise les octets bruts du fuzzer en valeurs Ruby typées.
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)
| Méthode | Description | Retourne |
|---|---|---|
remaining_bytes | Nombre d'octets non consommés restants | Integer |
consume_bytes(count) | Consomme jusqu'à count octets bruts | String binaire |
consume_random_length_string(max_length) | Consomme une chaîne de longueur variable ; se termine sur \ + octet non-\ | String |
consume_remaining_bytes | Consomme tous les octets restants | String binaire |
consume_remaining_as_string | Alias pour consume_remaining_bytes | String binaire |
consume_uint(count) | Entier non signé à partir de count octets | Integer |
consume_int(count) | Entier signé (complément à deux) à partir de count octets | Integer |
consume_int_in_range(min, max) | Entier distribué uniformément dans [min, max] | Integer |
consume_bool | Booléen à partir d'un octet (LSB) | true/false |
consume_float | Flottant couvrant toute la plage des doubles | Float |
consume_float_in_range(min, max) | Flottant dans [min, max] | Float |
consume_probability | Flottant dans [0.0, 1.0] | Float |
pick_value_in_list(list) | Élément aléatoire de list |
Toutes les méthodes retournent des valeurs par défaut (0, "", false, min) lorsque les données sont épuisées.
Ruzzy sur macOS nécessite LLVM installé via Homebrew (Apple Clang n'inclut pas libFuzzer) et un Ruby non système (le Ruby système à /usr/bin/ruby est protégé par SIP, ce qui supprime les variables d'environnement DYLD_* avant le démarrage de Ruby).
brew install llvm ruby
Tout Ruby non système fonctionne (brew, rbenv, asdf), mais voir les mises en garde ci-dessous pour les gestionnaires de versions basés sur des shims.
Utilisez les chemins Clang de Homebrew et les drapeaux de liaison appropriés pour 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
Utilisez DYLD_INSERT_LIBRARIES au lieu de LD_PRELOAD :
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
Ruzzy::ASAN_PATH et Ruzzy::UBSAN_PATH se résolvent en fichiers .dylib sur macOS.
asdf, rbenv) suppriment les variables d'environnement DYLD_*. Ces shims utilisent #!/usr/bin/env bash et /usr/bin/env est protégé par SIP, donc macOS supprime DYLD_INSERT_LIBRARIES avant le démarrage de Ruby. Utilisez soit le Ruby de Homebrew (qui n'a pas de shim), soit invoquez le chemin absolu vers le binaire Ruby installé :
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIES avec la dylib ASan bloque le processus au démarrage. Si vous voyez Ruzzy se bloquer au lancement, mettez à jour LLVM de Homebrew (brew upgrade llvm).Bugs trouvés en utilisant Ruzzy :
Le développement peut être fait localement, ou en utilisant le Dockerfile fourni dans ce dépôt.
Vous pouvez construire l'image Docker de Ruzzy avec la commande suivante :
docker build --tag ruzzy .
Ensuite, vous pouvez ouvrir un shell dans le conteneur en utilisant la commande suivante :
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
Nous utilisons rake-compiler pour compiler les extensions C de Ruzzy.
Vous pouvez compiler les extensions C dans le conteneur avec la commande suivante :
rake compile
Nous utilisons des tests unitaires rake pour tester le code Ruby.
Vous pouvez exécuter les tests dans le conteneur avec la commande suivante :
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
Nous utilisons rubocop pour lint le code Ruby.
Vous pouvez exécuter rubocop dans le conteneur avec la commande suivante :
rubocop
Pour publier une nouvelle version, exécutez les commandes suivantes :
git tag vX.X.X
git push --tags
| élément |