
Un fuzzer guidato dalla copertura per codice Ruby puro ed estensioni C di Ruby.
Un fuzzer guidato dalla copertura per codice Ruby puro ed estensioni C di Ruby.
Ruzzy è fortemente ispirato ad Atheris di Google, un fuzzer per Python. Ruzzy usa libFuzzer (o LibAFL) per la strumentazione della copertura e il motore di fuzzing. Ruzzy supporta anche AddressSanitizer e UndefinedBehaviorSanitizer quando si esegue il fuzzing di estensioni C. Se vuoi saperne di più sull'ispirazione alla base di Ruzzy, consulta il nostro articolo: Design and Implementation of a Coverage-Guided Ruby Fuzzer.
Indice dei contenuti:
Ruzzy supporta Linux (x86-64, AArch64/ARM64) e macOS (Apple Silicon). Su Windows, puoi creare il Dockerfile e/o usare l'ambiente di sviluppo. Ruzzy richiede una versione recente di clang (testata fino alla 14.0.0), preferibilmente l'ultima release. Per la configurazione specifica di macOS, vedi note per gli utenti macOS.
Installa Ruzzy con il seguente comando:
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
C'è molto da spiegare qui, quindi analizziamolo:
MAKE sovrascrive il comando make durante la compilazione dell'estensione C di Ruzzy. Questo dice a make di rispettare le successive variabili d'ambiente durante la compilazione dell'estensione.clang corretti. Questo assicura di avere le funzionalità più recenti di clang, necessarie per un corretto fuzzing.Se riscontri problemi durante l'installazione, puoi eseguire il seguente comando per ottenere un output di debug:
RUZZY_DEBUG=1 gem install --verbose ruzzy
Se il tuo target di fuzzing in Ruby puro fa un uso intensivo delle regexp, installa regexp_parser:
gem install regexp_parser
Una volta installato, Ruzzy userà automaticamente questa funzionalità per campionare (cioè risolvere) le regexp che incontra durante il fuzzing. Questo permette al fuzzer di superare le condizioni basate su regexp e sbloccare ulteriore copertura.
Ruzzy include un esempio giocattolo per dimostrare come funziona. Per prima cosa, imposta la seguente variabile d'ambiente:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
ASAN_OPTIONSPuoi quindi eseguire l'esempio con il seguente comando:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
LD_PRELOAD è richiesto per gli stessi motivi di Atheris. Tuttavia, a differenza di ASAN_OPTIONS, probabilmente non vorrai esportarlo con export poiché potrebbe interferire con altri programmi.
Dovrebbe produrre rapidamente un crash come il seguente:
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=
Possiamo vedere che ha trovato correttamente l'input ("HI") che ha prodotto una violazione di memoria. Per maggiori informazioni, vedi dummy.c per capire perché si è verificata questa violazione.
Puoi rieseguire il caso di crash con il seguente comando:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
I seguenti sanitizer sono disponibili:
Ruzzy::ASAN_PATH per AddressSanitizerRuzzy::UBSAN_PATH per UndefinedBehaviorSanitizerFacciamo il fuzzing di un piccolo script Ruby come esempio. Il fuzzing di codice Ruby puro richiede due script Ruby: uno script tracer e un harness di fuzzing. Lo script tracer è necessario a causa di un dettaglio implementativo dell'interprete Ruby.
Prima, lo script tracer, chiamiamolo test_tracer.rb:
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
Poi, l'harness di fuzzing, chiamiamolo 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)
Puoi eseguire questo file e iniziare il fuzzing con il seguente comando:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
Dovrebbe produrre rapidamente un crash come il seguente:
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==
Possiamo vedere che ha trovato correttamente l'input ("FUZZ") che ha prodotto un'eccezione.
Per fare il fuzzing del tuo target, modifica la lambda test_one_input per chiamare la tua funzione target.
Facciamo il fuzzing della libreria msgpack-ruby come esempio. Per prima cosa, installa la gemma:
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
Oltre alle variabili d'ambiente usate per compilare Ruzzy, specifichiamo CFLAGS e CXXFLAGS. Questi flag aiutano nel processo di fuzzing. Abilitano funzionalità utili come l'address sanitizer e migliori informazioni sullo stack trace. Per maggiori informazioni vedi AddressSanitizerFlags.
Successivamente, abbiamo bisogno di un harness di fuzzing per msgpack. Quanto segue potrebbe essere familiare a chi ha esperienza con 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)
Chiamiamo questo file fuzz_msgpack.rb. Puoi eseguire questo file e iniziare il fuzzing con il seguente comando:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
Le opzioni di libFuzzer possono essere passate allo script Ruby in questo modo:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
Vedi opzioni di libFuzzer per maggiori informazioni.
Per fare il fuzzing del tuo target, modifica la lambda test_one_input per chiamare la tua funzione target.
Il modulo Ruzzy espone i punti di ingresso di livello superiore.
Ruzzy::FuzzedDataProvider divide i byte grezzi del fuzzer in valori Ruby tipizzati.
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)
Tutti i metodi restituiscono valori predefiniti (0, "", false, min) quando i dati sono esauriti.
Ruzzy su macOS richiede LLVM installato tramite Homebrew (Apple Clang non include libFuzzer) e un Ruby non di sistema (il Ruby di sistema in /usr/bin/ruby è protetto da SIP, che rimuove le variabili d'ambiente DYLD_* prima che Ruby venga avviato).
brew install llvm ruby
Qualsiasi Ruby non di sistema funziona (brew, rbenv, asdf), ma vedi le avvertenze qui sotto per i gestori di versioni basati su shim.
Usa i percorsi di Clang di Homebrew e i flag di linker appropriati per 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
Usa DYLD_INSERT_LIBRARIES invece di LD_PRELOAD:
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
Ruzzy::ASAN_PATH e Ruzzy::UBSAN_PATH puntano a file .dylib su macOS.
asdf, rbenv) rimuovono le variabili d'ambiente DYLD_*. Questi shim usano #!/usr/bin/env bash e /usr/bin/env è protetto da SIP, quindi macOS rimuove DYLD_INSERT_LIBRARIES prima che Ruby venga avviato. Usa Ruby di Homebrew (che non ha shim) oppure invoca il percorso assoluto del binario Ruby installato:
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIES della dylib ASan blocca il processo durante l'avvio. Se Ruzzy si blocca all'avvio, aggiorna LLVM di Homebrew (brew upgrade llvm).Bug trovati usando Ruzzy:
toml: #76toml-rb: #150ox: #351, #410Marshal: #20941redcarpet: #813Lo sviluppo può essere fatto localmente, oppure usando il Dockerfile fornito in questo repository.
Puoi creare l'immagine Docker di Ruzzy con il seguente comando:
docker build --tag ruzzy .
Poi, puoi entrare nella shell del container usando il seguente comando:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
Usiamo rake-compiler per compilare le estensioni C di Ruzzy.
Puoi compilare le estensioni C all'interno del container con il seguente comando:
rake compile
Usiamo i test unitari rake per testare il codice Ruby.
Puoi eseguire i test all'interno del container con il seguente comando:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
Usiamo rubocop per il linting del codice Ruby.
Puoi eseguire rubocop all'interno del container con il seguente comando:
rubocop
Ruzzy viene rilasciato automaticamente su RubyGems quando viene pushato un nuovo tag git.
Per rilasciare una nuova versione esegui i seguenti comandi:
git tag vX.X.X
git push --tags
| Metodo | Descrizione |
|---|
Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS) | Esegue il fuzzing di test_one_input (una proc/lambda che accetta byte grezzi). |
Ruzzy.trace(harness_script) | Avvolge harness_script con la strumentazione di copertura dei rami di Ruby, quindi lo carica con require. Necessario per il fuzzing di puro Ruby. |
Ruzzy.dummy | Esegue il fuzzing dell'harness giocattolo incluso (demo di heap-use-after-free). |
Ruzzy.dummy_test_one_input(data) | L'harness giocattolo stesso. |
| Costante | Descrizione |
|---|
Ruzzy::ASAN_PATH | Percorso del wrapper ASan + fuzzer. Da usare con LD_PRELOAD (Linux) / DYLD_INSERT_LIBRARIES (macOS). |
Ruzzy::UBSAN_PATH | Idem, per UBSan. |
Ruzzy::EXT_PATH | Percorso della directory di build di ext/cruzzy. |
Ruzzy::DEFAULT_ARGS | Argomenti predefiniti passati al fuzzer. |
| Metodo | Descrizione | Restituisce |
|---|
remaining_bytes | Numero di byte non consumati rimanenti | Integer |
consume_bytes(count) | Consuma fino a count byte grezzi | String binaria |
consume_random_length_string(max_length) | Consuma una stringa a lunghezza variabile; termina su \ + byte diverso da \ | String |
consume_remaining_bytes | Consuma tutti i byte rimanenti | String binaria |
consume_remaining_as_string | Alias di consume_remaining_bytes | String binaria |
consume_uint(count) | Intero senza segno da count byte | Integer |
consume_int(count) | Intero con segno (complemento a due) da count byte | Integer |
consume_int_in_range(min, max) | Intero distribuito uniformemente in [min, max] | Integer |
consume_bool | Booleano da un byte (LSB) | true/false |
consume_float | Float che copre l'intero intervallo del double | 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) | Elemento casuale da list | elemento |