
Фаззер, управляемый покрытием, для чистого кода Ruby и C-расширений Ruby
Фаззер с управлением покрытием для чистого Ruby-кода и C-расширений Ruby.
Ruzzy во многом вдохновлён Atheris от Google — фаззером для Python. Ruzzy использует libFuzzer (или LibAFL) для инструментирования покрытия и в качестве движка фаззинга. Ruzzy также поддерживает AddressSanitizer и UndefinedBehaviorSanitizer при фаззинге C-расширений. Если вы хотите узнать больше о том, что вдохновило 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 переопределяет команду make при компиляции C-расширения Ruzzy. Она указывает 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 для AddressSanitizerRuzzy::UBSAN_PATH для UndefinedBehaviorSanitizerДавайте в качестве примера отфаззим небольшой Ruby-скрипт. Фаззинг чистого Ruby-кода требует двух Ruby-скриптов: скрипт-трассировщик и фаззинг-обвязку (fuzzing harness). Скрипт-трассировщик необходим из-за особенности реализации интерпретатора 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) # Your fuzzing target would go here
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. Эти флаги помогают в процессе фаззинга: они включают полезные функции, такие как AddressSanitizer, и улучшают информацию в стек-трейсах. Подробнее см. AddressSanitizerFlags.
Далее нам нужна фаззинг-обвязка для msgpack. Следующий код может быть знаком тем, у кого есть опыт работы с 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)
Назовём этот файл 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.
Чтобы фаззить собственную цель, измените test_one_input lambda так, чтобы она вызывала вашу функцию.
Модуль Ruzzy предоставляет точки входа верхнего уровня.
Ruzzy::FuzzedDataProvider разделяет сырые байты фаззера на типизированные значения Ruby.
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)
Когда данные исчерпаны, все методы возвращают значения по умолчанию (0, "", false, min).
Ruzzy на macOS требует LLVM, установленный через Homebrew (Apple Clang не включает libFuzzer), и несистемный Ruby (системный Ruby в /usr/bin/ruby защищён SIP, из-за чего переменные окружения DYLD_* удаляются до запуска Ruby).
brew install llvm ruby
Подойдёт любой несистемный Ruby (brew, rbenv, asdf), но для менеджеров версий на основе шим см. предостережения ниже.
Используйте пути к Clang из Homebrew и подходящие для 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
Используйте DYLD_INSERT_LIBRARIES вместо LD_PRELOAD:
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
Ruzzy::ASAN_PATH и Ruzzy::UBSAN_PATH на macOS указывают на файлы .dylib.
asdf, rbenv) удаляют переменные окружения DYLD_*. Эти шимы используют #!/usr/bin/env bash, а /usr/bin/env защищён SIP, поэтому macOS удаляет DYLD_INSERT_LIBRARIES до запуска Ruby. Либо используйте Homebrew Ruby (у которого нет шима), либо вызывайте абсолютный путь к установленному бинарнику Ruby:
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIES с dylib ASan процесс зависает во время запуска. Если Ruzzy зависает при старте, обновите Homebrew LLVM (brew upgrade llvm).Ошибки, найденные с помощью Ruzzy:
toml: #76toml-rb: #150ox: #351, #410Marshal: #20941redcarpet: #813Разработку можно вести локально или с помощью Dockerfile, предоставленного в этом репозитории.
Вы можете собрать Docker-образ Ruzzy следующей командой:
docker build --tag ruzzy .
Затем вы можете войти в контейнер с помощью следующей команды:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
Для компиляции C-расширений Ruzzy мы используем rake-compiler.
Вы можете скомпилировать C-расширения внутри контейнера следующей командой:
rake compile
Для тестирования Ruby-кода мы используем модульные тесты rake.
Вы можете запустить тесты внутри контейнера следующей командой:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
Для линтинга Ruby-кода мы используем rubocop.
Вы можете запустить rubocop внутри контейнера следующей командой:
rubocop
Ruzzy автоматически публикуется на RubyGems при пуше нового git-тега.
Чтобы выпустить новую версию, выполните следующие команды:
git tag vX.X.X
git push --tags
| Метод | Описание |
|---|
Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS) | Фаззит test_one_input (proc/lambda, принимающий сырые байты). |
Ruzzy.trace(harness_script) | Оборачивает harness_script инструментированием покрытия ветвей Ruby, затем подключает его через require. Требуется для фаззинга чистого Ruby. |
Ruzzy.dummy | Фаззит встроенную игрушечную обвязку (демо heap-use-after-free). |
Ruzzy.dummy_test_one_input(data) | Сама игрушечная обвязка. |
| Константа | Описание |
|---|
Ruzzy::ASAN_PATH | Путь к обёртке ASan + фаззера. Используйте с LD_PRELOAD (Linux) / DYLD_INSERT_LIBRARIES (macOS). |
Ruzzy::UBSAN_PATH | Аналогично, для UBSan. |
Ruzzy::EXT_PATH | Путь к каталогу сборки ext/cruzzy. |
Ruzzy::DEFAULT_ARGS | Аргументы по умолчанию, передаваемые фаззеру. |
| Метод | Описание | Возвращаемое значение |
|---|
remaining_bytes | Количество ещё не потреблённых байтов | Integer |
consume_bytes(count) | Потребляет до count сырых байтов | бинарная String |
consume_random_length_string(max_length) | Потребляет строку переменной длины; завершается на \ + не-\ байте | String |
consume_remaining_bytes | Потребляет все оставшиеся байты | бинарная String |
consume_remaining_as_string | Псевдоним для consume_remaining_bytes | бинарная String |
consume_uint(count) | Беззнаковое целое из count байтов | Integer |
consume_int(count) | Знаковое целое (в дополнительном коде) из count байтов | Integer |
consume_int_in_range(min, max) | Целое, равномерно распределённое в диапазоне [min, max] | Integer |
consume_bool | Логическое значение из одного байта (младший бит) | true/false |
consume_float | Число с плавающей запятой, покрывающее весь диапазон double | Float |
consume_float_in_range(min, max) | Число с плавающей запятой в диапазоне [min, max] | Float |
consume_probability | Число с плавающей запятой в диапазоне [0.0, 1.0] | Float |
pick_value_in_list(list) | Случайный элемент из list | элемент |