
Un fuzzer guiado por cobertura para código Ruby puro y extensiones C de Ruby
# Ruzzy
[](https://github.com/trailofbits/ruzzy/actions/workflows/test.yml)
[](https://rubygems.org/gems/ruzzy)
Un fuzzer guiado por cobertura para código Ruby puro y [extensiones C](https://ruby-doc.org/3.3.0/extension_rdoc.html) de Ruby.
Ruzzy está fuertemente inspirado en [Atheris](https://github.com/google/atheris), un fuzzer de Python. Ruzzy utiliza [libFuzzer](https://llvm.org/docs/LibFuzzer.html) (o [LibAFL](https://github.com/trailofbits/ruzzy/blob/main/Dockerfile.LibAFL)) para su instrumentación de cobertura y su motor de fuzzing. Ruzzy también es compatible con [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html) y [UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html) al fuzzear extensiones C. Si desea obtener más información sobre la inspiración detrás de Ruzzy, consulte nuestro artículo: [Design and Implementation of a Coverage-Guided Ruby Fuzzer](https://dl.acm.org/doi/10.1145/3675741.3675749).
Tabla de contenidos:
- [Instalación](#installing)
- [Uso](#using)
- [Primeros pasos](#getting-started)
- [Fuzzing de código Ruby puro](#fuzzing-pure-ruby-code)
- [Fuzzing de extensiones C de Ruby](#fuzzing-ruby-c-extensions)
- [API](#api)
- [Ruzzy](#ruzzy)
- [FuzzedDataProvider](#fuzzeddataprovider)
- [Notas para usuarios de macOS](#notes-for-macos-users)
- [Casos de éxito](#trophy-case)
- [Desarrollo](#developing)
- [Compilación](#compiling)
- [Pruebas](#testing)
- [Linting](#linting)
- [Publicación](#releasing)
- [Lecturas adicionales](#further-reading)
# Instalación
Ruzzy es compatible con Linux (x86-64, AArch64/ARM64) y macOS (Apple Silicon). En Windows, puede compilar el [`Dockerfile`](https://github.com/trailofbits/ruzzy/blob/main/Dockerfile) y/o utilizar el [entorno de desarrollo](#developing). Ruzzy requiere una versión reciente de `clang` (probada hasta `14.0.0`), preferiblemente la [última versión](https://github.com/llvm/llvm-project/releases). Para la configuración específica de macOS, consulte las [notas para usuarios de macOS](#notes-for-macos-users).
Instale Ruzzy con el siguiente comando:
```bash
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
```
Hay mucho en juego aquí, así que vamos a desglosarlo:
- La variable de entorno `MAKE` sobrescribe el comando `make` al compilar la extensión C de Ruzzy. Esto le indica a `make` que respete las variables de entorno posteriores al compilar la extensión.
- El resto de las variables de entorno se utilizan durante la compilación para asegurarnos de que estamos usando los binarios `clang` adecuados. Esto garantiza que tengamos las funciones más recientes de `clang`, necesarias para un fuzzing adecuado.
Si tiene problemas con la instalación, puede ejecutar el siguiente comando para obtener información de depuración:
```bash
RUZZY_DEBUG=1 gem install --verbose ruzzy
```
Si su objetivo de fuzzing en Ruby puro hace un uso intensivo de regexps, instale [`regexp_parser`](https://github.com/ammar/regexp_parser):
```
gem install regexp_parser
```
Una vez instalado, Ruzzy utilizará automáticamente esta funcionalidad para muestrear (es decir, resolver) las regexps que encuentre durante el fuzzing. Esto permite que el fuzzer avance más allá de las condiciones de regexp y desbloquee cobertura adicional.
# Uso
## Primeros pasos
Ruzzy incluye un [ejemplo de juguete](https://llvm.org/docs/LibFuzzer.html#toy-example) para demostrar cómo funciona. Primero, configure la siguiente variable de entorno:
```bash
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
```
<details>
<summary>Comprender estas opciones no es necesario, pero si tiene curiosidad, haga clic aquí.</summary>
### `ASAN_OPTIONS`
1. Los fallos de asignación de memoria son comunes y de bajo impacto (DoS), así que omitámoslos por ahora.
1. Al igual que Python, el intérprete de Ruby [pierde memoria](https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#leak-detection), así que ignóralos por ahora.
1. Ruby recomienda [deshabilitar sigaltstack](https://github.com/ruby/ruby/blob/master/doc/contributing/building_ruby.md#building-with-address-sanitizer).
</details>
Puede ejecutar el ejemplo con el siguiente comando:
```bash
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
```
_`LD_PRELOAD` es necesario por las mismas razones [que en Atheris](https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#option-a-sanitizerlibfuzzer-preloads). Sin embargo, a diferencia de `ASAN_OPTIONS`, probablemente no quiera `exportarlo`, ya que puede interferir con otros programas._
Debería producir rápidamente un fallo como el siguiente:
```
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=
```
Podemos ver que encontró correctamente la entrada (`"HI"`) que produjo una violación de memoria. Para más información, consulte [`dummy.c`](https://github.com/trailofbits/ruzzy/blob/main/ext/dummy/dummy.c) para ver por qué ocurrió esta violación.
Puede volver a ejecutar el caso de fallo con el siguiente comando:
```bash
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
```
Los siguientes sanitizadores están disponibles:
- `Ruzzy::ASAN_PATH` para [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html)
- `Ruzzy::UBSAN_PATH` para [UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html)
## Fuzzing de código Ruby puro
Fuzzeemos un pequeño script de Ruby como ejemplo. El fuzzing de código Ruby puro requiere dos scripts de Ruby: un script de trazado (tracer) y un harness de fuzzing. El script de trazado es necesario debido a un detalle de implementación del intérprete de Ruby.
Primero, el script de trazado, llamémoslo `test_tracer.rb`:
```ruby
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
```
A continuación, el harness de fuzzing, llamémoslo `test_harness.rb`:
```ruby
# 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)
```
Puede ejecutar este archivo y comenzar a fuzzear con el siguiente comando:
```bash
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
```
Debería producir rápidamente un fallo como el siguiente:
```
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==
```
Podemos ver que encontró correctamente la entrada (`"FUZZ"`) que produjo una excepción.
Para fuzzear su propio objetivo, modifique el `lambda` `test_one_input` para que llame a su función objetivo.
## Fuzzing de extensiones C de Ruby
Fuzzeemos la biblioteca [`msgpack-ruby`](https://github.com/msgpack/msgpack-ruby) como ejemplo. Primero, instale la gema:
```bash
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
```
Además de las variables de entorno utilizadas al compilar Ruzzy, estamos especificando `CFLAGS` y `CXXFLAGS`. Estas banderas ayudan en el proceso de fuzzing. Habilitan funcionalidades útiles como un address sanitizer y una mejor información de stack trace. Para más información, consulte [AddressSanitizerFlags](https://github.com/google/sanitizers/wiki/AddressSanitizerFlags).
A continuación, necesitamos un harness de fuzzing para `msgpack`. Lo siguiente puede resultar familiar para quienes tienen [experiencia con libFuzzer](https://llvm.org/docs/LibFuzzer.html#fuzz-target):
```ruby
# 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)
```
Llamemos a este archivo `fuzz_msgpack.rb`. Puede ejecutar este archivo y comenzar a fuzzear con el siguiente comando:
```bash
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
```
Las opciones de libFuzzer se pueden pasar al script de Ruby de la siguiente manera:
```bash
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
```
Consulte [opciones de libFuzzer](https://llvm.org/docs/LibFuzzer.html#options) para obtener más información.
Para fuzzear su propio objetivo, modifique el `lambda` `test_one_input` para que llame a su función objetivo.
# API
## Ruzzy
El módulo `Ruzzy` expone los puntos de entrada de nivel superior.
| Método | Descripción |
|--------|-------------|
| `Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS)` | Fuzzea `test_one_input` (un `proc`/`lambda` que recibe bytes sin procesar). |
| `Ruzzy.trace(harness_script)` | Envuelve `harness_script` con instrumentación de cobertura de ramas de Ruby y luego hace `require` del mismo. Necesario para fuzzing de Ruby puro. |
| `Ruzzy.dummy` | Fuzzea el harness de juguete incluido (demo de heap-use-after-free). |
| `Ruzzy.dummy_test_one_input(data)` | El propio harness de juguete. |
| Constante | Descripción |
|----------|-------------|
| `Ruzzy::ASAN_PATH` | Ruta al wrapper de ASan + fuzzer. Úsela con `LD_PRELOAD` (Linux) / `DYLD_INSERT_LIBRARIES` (macOS). |
| `Ruzzy::UBSAN_PATH` | Igual, para UBSan. |
| `Ruzzy::EXT_PATH` | Ruta al directorio de compilación `ext/cruzzy`. |
| `Ruzzy::DEFAULT_ARGS` | Argumentos predeterminados pasados al fuzzer. |
## FuzzedDataProvider
`Ruzzy::FuzzedDataProvider` [divide](https://github.com/google/fuzzing/blob/master/docs/split-inputs.md) los bytes sin procesar del fuzzer en valores Ruby tipados.
```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)
```
| Método | Descripción | Retorna |
|--------|-------------|---------|
| `remaining_bytes` | Número de bytes no consumidos restantes | `Integer` |
| `consume_bytes(count)` | Consume hasta `count` bytes sin procesar | `String` binario |
| `consume_random_length_string(max_length)` | Consume una cadena de longitud variable; termina en `\` + byte distinto de `\` | `String` |
| `consume_remaining_bytes` | Consume todos los bytes restantes | `String` binario |
| `consume_remaining_as_string` | Alias de `consume_remaining_bytes` | `String` binario |
| `consume_uint(count)` | Entero sin signo a partir de `count` bytes | `Integer` |
| `consume_int(count)` | Entero con signo (complemento a dos) a partir de `count` bytes | `Integer` |
| `consume_int_in_range(min, max)` | Entero distribuido uniformemente en [`min`, `max`] | `Integer` |
| `consume_bool` | Booleano a partir de un byte (LSB) | `true`/`false` |
| `consume_float` | Flotante que abarca todo el rango de double | `Float` |
| `consume_float_in_range(min, max)` | Flotante en [`min`, `max`] | `Float` |
| `consume_probability` | Flotante en [0.0, 1.0] | `Float` |
| `pick_value_in_list(list)` | Elemento aleatorio de `list` | elemento |
Todos los métodos devuelven valores predeterminados (`0`, `""`, `false`, `min`) cuando los datos se agotan.
# Notas para usuarios de macOS
Ruzzy en macOS requiere LLVM instalado mediante Homebrew (Apple Clang no incluye libFuzzer) y un Ruby que no sea el del sistema (el Ruby del sistema en `/usr/bin/ruby` está protegido por SIP, lo que elimina las variables de entorno `DYLD_*` antes de que Ruby se inicie).
## Requisitos previos
```bash
brew install llvm ruby
```
Cualquier Ruby que no sea el del sistema funciona (`brew`, `rbenv`, `asdf`), pero consulte las [limitaciones](#caveats) a continuación para los gestores de versiones basados en shims.
## Instalación
Utilice las rutas de Clang de Homebrew y las banderas de enlazado apropiadas para macOS:
```bash
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
```
## Ejecución
Use `DYLD_INSERT_LIBRARIES` en lugar de `LD_PRELOAD`:
```bash
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
```
`Ruzzy::ASAN_PATH` y `Ruzzy::UBSAN_PATH` se resuelven a archivos `.dylib` en macOS.
## Limitaciones
- **Los shims de gestores de versiones (`asdf`, `rbenv`) eliminan las variables de entorno `DYLD_*`.** Estos shims usan `#!/usr/bin/env bash` y `/usr/bin/env` está protegido por SIP, por lo que macOS elimina `DYLD_INSERT_LIBRARIES` antes de que Ruby se inicie. Utilice el Ruby de Homebrew (que no tiene shim) o invoque la ruta absoluta al binario de Ruby instalado:
```bash
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
```
- **Se requiere LLVM reciente.** Algunas versiones antiguas de Homebrew LLVM (particularmente 19.x) tienen un error por el cual `DYLD_INSERT_LIBRARIES` con la dylib de ASan bloquea el proceso durante el arranque. Si Ruzzy se bloquea al iniciar, actualice Homebrew LLVM (`brew upgrade llvm`).
# Casos de éxito
Errores encontrados usando Ruzzy:
- gema `toml`: [#76](https://github.com/jm/toml/issues/76)
- gema `toml-rb`: [#150](https://github.com/emancu/toml-rb/issues/150)
- gema `ox`: [#351](https://github.com/ohler55/ox/issues/351), [#410](https://github.com/ohler55/ox/issues/410)
- Fallo del recolector de basura de Ruby `Marshal`: [#20941](https://bugs.ruby-lang.org/issues/20941)
- Diferencial de analizadores XML: [REXML vs. Nokogiri](https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/)
- gema `redcarpet`: [#813](https://github.com/vmg/redcarpet/issues/813)
# Desarrollo
El desarrollo se puede realizar localmente o usando el `Dockerfile` proporcionado en este repositorio.
Puede construir la imagen Docker de Ruzzy con el siguiente comando:
```bash
docker build --tag ruzzy .
```
Luego, puede acceder al contenedor con el siguiente comando:
```
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
```
## Compilación
Usamos [`rake-compiler`](https://github.com/rake-compiler/rake-compiler) para compilar las extensiones C de Ruzzy.
Puede compilar las extensiones C dentro del contenedor con el siguiente comando:
```bash
rake compile
```
## Pruebas
Usamos pruebas unitarias de `rake` para probar el código Ruby.
Puede ejecutar las pruebas dentro del contenedor con el siguiente comando:
```bash
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
```
## Linting
Usamos `rubocop` para hacer linting del código Ruby.
Puede ejecutar `rubocop` dentro del contenedor con el siguiente comando:
```bash
rubocop
```
## Publicación
Ruzzy se [publica](https://github.com/trailofbits/ruzzy/actions/workflows/release.yml) automáticamente en [RubyGems](https://rubygems.org/gems/ruzzy) cuando se empuja una nueva etiqueta git.
Para publicar una nueva versión, ejecute los siguientes comandos:
```bash
git tag vX.X.X
```
```bash
git push --tags
```
# Lecturas adicionales
- Extensiones C de Ruby
- https://guides.rubygems.org/gems-with-extensions/
- https://www.rubyguides.com/2018/03/write-ruby-c-extension/
- https://rubyreferences.github.io/rubyref/advanced/extensions.html
- https://silverhammermba.github.io/emberb/c/
- https://ruby-doc.org/3.3.0/extension_rdoc.html
- https://ruby-doc.org/3.3.0/stdlibs/mkmf/MakeMakefile.html
- https://github.com/flavorjones/ruby-c-extensions-explained
- https://github.com/ruby/ruby/blob/v3_3_0/lib/mkmf.rb
- Fuzzing de Ruby
- https://github.com/twistlock/kisaten
- https://github.com/richo/afl-ruby
- https://github.com/krypt/FuzzBert
- https://z2-2z.github.io/2024/jan/16/fuzzing-ruby-c-extensions-with-coverage-and-asan.html
- https://bsidessf2018.sched.com/event/E6jC/fuzzing-ruby-and-c-extensions
- Atheris
- https://github.com/google/atheris/blob/master/native_extension_fuzzing.md
- https://security.googleblog.com/2020/12/how-atheris-python-fuzzer-works.html
- https://github.com/google/atheris/blob/2.3.0/setup.py
- https://github.com/google/atheris/blob/2.3.0/src/native/core.cc
- https://github.com/google/atheris/blob/2.3.0/src/native/tracer.cc
- https://github.com/google/atheris/blob/2.3.0/src/native/counters.cc
- https://github.com/google/atheris/blob/2.3.0/src/instrument_bytecode.py
- Cobertura
- https://calabi-yau.space/blog/sanitizer-coverage-interface.html
- https://carstein.github.io/2020/05/21/writing-simple-fuzzer-4.html
- https://h0mbre.github.io/Fuzzing-Like-A-Caveman-5/
- https://github.com/mirrorer/afl/blob/master/docs/technical_details.txt
- https://lcamtuf.coredump.cx/afl/historical_notes.txt
- https://www.code-intelligence.com/blog/the-magic-behind-feedback-based-fuzzing
- https://blog.includesecurity.com/2024/04/coverage-guided-fuzzing-extending-instrumentation/
- https://git.sr.ht/~myrrc/ba-thesis/blob/master/thesis.pdf
- https://www.politesi.polimi.it/bitstream/10589/173614/3/2021_04_Frighetto.pdf
- https://wcventure.github.io/FuzzingPaper/Paper/SP18_ColLAFL.pdf
- https://www.ndss-symposium.org/wp-content/uploads/2020/02/24422.pdf
- https://mboehme.github.io/paper/ICSE22.pdf
- https://www.usenix.org/system/files/raid2019-wang-jinghan.pdf