trailofbits/ruzzy
GitHub: trailofbits/ruzzy
Ruzzy 是 Trail of Bits 开发的面向 Ruby 代码和 Ruby C 扩展的覆盖率引导模糊测试工具,可用于自动发现安全漏洞和内存错误。
Stars: 113 | Forks: 7
# Ruzzy
[](https://github.com/trailofbits/ruzzy/actions/workflows/test.yml)
[](https://rubygems.org/gems/ruzzy)
一个面向纯 Ruby 代码和 Ruby [C 扩展](https://ruby-doc.org/3.3.0/extension_rdoc.html)的覆盖率引导模糊测试工具。
Ruzzy 深受 Google 的 [Atheris](https://github.com/google/atheris)(Python 模糊测试工具)启发。与 Atheris 一样,Ruzzy 使用 [libFuzzer](https://llvm.org/docs/LibFuzzer.html) 进行覆盖率插桩和模糊测试引擎。在模糊测试 C 扩展时,Ruzzy 还支持 [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html) 和 [UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html)。如果您想了解更多关于 Ruzzy 设计的灵感来源,请参阅我们的论文:[Design and Implementation of a Coverage-Guided Ruby Fuzzer](https://dl.acm.org/doi/10.1145/3675741.3675749)。
目录:
- [安装](#installing)
- [使用](#using)
- [快速开始](#getting-started)
- [模糊测试纯 Ruby 代码](#fuzzing-pure-ruby-code)
- [模糊测试 Ruby C 扩展](#fuzzing-ruby-c-extensions)
- [API](#api)
- [FuzzedDataProvider](#fuzzeddataprovider)
- [漏洞案例](#trophy-case)
- [开发](#developing)
- [编译](#compiling)
- [测试](#testing)
- [代码检查](#linting)
- [发布](#releasing)
- [延伸阅读](#further-reading)
# 安装
目前,Ruzzy 仅支持 Linux x86-64 和 AArch64/ARM64。如果您想在 Mac 或 Windows 上运行 Ruzzy,可以构建 [`Dockerfile`](https://github.com/trailofbits/ruzzy/blob/main/Dockerfile) 和/或使用[开发环境](#developing)。Ruzzy 需要较新版本的 `clang`(已测试回溯到 `14.0.0`),建议使用[最新版本](https://github.com/llvm/llvm-project/releases)。
使用以下命令安装 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` 环境变量在编译 Ruzzy C 扩展时覆盖 `make` 命令。这告诉 `make` 在编译扩展时遵守后续的环境变量。
- 其余环境变量在编译期间用于确保使用正确的 `clang` 二进制文件。这确保我们拥有最新的 `clang` 功能,这对于正确的模糊测试是必需的。
如果安装时遇到问题,可以运行以下命令获取调试输出:
```
RUZZY_DEBUG=1 gem install --verbose ruzzy
```
# 使用
## 快速开始
Ruzzy 包含一个[示例程序](https://llvm.org/docs/LibFuzzer.html#toy-example)来演示其工作原理。首先,设置以下环境变量:
```
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
```
然后可以使用以下命令运行示例:
```
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
```
_`LD_PRELOAD` 的原因与 [Atheris](https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#option-a-sanitizerlibfuzzer-preloads) 相同。但是,与 `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.7.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`](https://github.com/trailofbits/ruzzy/blob/main/ext/dummy/dummy.c) 了解此违规发生的原因。
您可以使用以下命令重新运行崩溃用例:
```
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
```
以下 sanitizer 可用:
- `Ruzzy::ASAN_PATH` 用于 [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html)
- `Ruzzy::UBSAN_PATH` 用于 [UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html)
## 模糊测试纯 Ruby 代码
让我们以一个小的 Ruby 脚本为例进行模糊测试。模糊测试纯 Ruby 代码需要两个 Ruby 脚本:tracer 脚本和模糊测试 harness。由于 Ruby 解释器的实现细节,tracer 脚本是必需的。除了它是必要的这一事实外,不需要了解此交互的细节。
首先,tracer 脚本,我们称之为 `test_tracer.rb`:
```
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
```
接下来,模糊测试 harness,我们称之为 `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 ': unhandled exception
from /var/lib/gems/3.1.0/gems/ruzzy-0.7.0/lib/ruzzy.rb:15:in `c_fuzz'
from /var/lib/gems/3.1.0/gems/ruzzy-0.7.0/lib/ruzzy.rb:15:in `fuzz'
from /app/ruzzy/bin/test_harness.rb:35:in `'
from bin/test_tracer.rb:7:in `require_relative'
from bin/test_tracer.rb:7:in `'
...
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` 以调用您的目标函数。
## 模糊测试 Ruby C 扩展
让我们以 [`msgpack-ruby`](https://github.com/msgpack/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`。这些标志有助于模糊测试过程。它们启用了有用的功能,如 address sanitizer 和改进的堆栈跟踪信息。欲了解更多详细信息,请参阅 [AddressSanitizerFlags](https://github.com/google/sanitizers/wiki/AddressSanitizerFlags)。
接下来,我们需要一个用于 `msgpack` 的模糊测试 harness。对于有 [libFuzzer 经验](https://llvm.org/docs/LibFuzzer.html#fuzz-target)的人来说,以下内容可能很熟悉:
```
# 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 options](https://llvm.org/docs/LibFuzzer.html#options)。
要模糊测试您自己的目标,请修改 `test_one_input` `lambda` 以调用您的目标函数。
# API
## FuzzedDataProvider
`Ruzzy::FuzzedDataProvider` 将原始模糊测试字节[拆分](https://github.com/google/fuzzing/blob/master/docs/split-inputs.md)为类型化的 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)
```
| 方法 | 描述 | 返回值 |
|--------|-------------|---------|
| `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` | 从一个字节(LSB)读取布尔值 | `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` 中随机选择一个元素 | 元素 |
当数据耗尽时,所有方法都返回默认值(`0`、`""`、`false`、`min`)。
# 漏洞案例
使用 Ruzzy 发现的漏洞:
- `toml` gem:[#76](https://github.com/jm/toml/issues/76)
- `toml-rb` gem:[#150](https://github.com/emancu/toml-rb/issues/150)
- `ox` gem:[#351](https://github.com/ohler55/ox/issues/351)
- Ruby `Marshal` 垃圾回收器崩溃:[#20941](https://bugs.ruby-lang.org/issues/20941)
- XML 解析器差异:[REXML vs. Nokogiri](https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/)
# 开发
开发可以在本地进行,也可以使用本仓库中提供的 `Dockerfile`。
您可以使用以下命令构建 Ruzzy Docker 镜像:
```
docker build --tag ruzzy .
```
然后,可以使用以下命令进入容器:
```
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
```
## 编译
我们使用 [`-compiler`](https://github.com/rake-compiler/rake-compiler) 来编译 Ruzzy 的 C 扩展。
您可以使用以下命令在容器中编译 C 扩展:
```
rake compile
```
## 测试
我们使用 `rake` 单元测试来测试 Ruby 代码。
您可以使用以下命令在容器中运行测试:
```
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
```
## 代码检查
我们使用 `rubocop` 来检查 Ruby 代码。
您可以使用以下命令在容器中运行 `rubocop`:
```
rubocop
```
## 发布
当推送新的 git 标签时,Ruzzy 会自动[发布](https://github.com/trailofbits/ruzzy/actions/workflows/release.yml)到 [RubyGems](https://rubygems.org/gems/ruzzy)。
要发布新版本,请运行以下命令:
```
git tag vX.X.X
```
```
git push --tags
```
# 延伸阅读
- Ruby C 扩展
- 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
- 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
- 覆盖率
- 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
理解这些选项并非必要,但如果您感兴趣可以点击此处查看。
### `ASAN_OPTIONS` 1. 内存分配失败常见且影响较小(DoS),因此暂时跳过。 2. 与 Python 一样,Ruby 解释器[会泄漏数据](https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#leak-detection),因此暂时忽略这些。 3. Ruby 建议[禁用 sigaltstack](https://github.com/ruby/ruby/blob/master/doc/contributing/building_ruby.md#building-with-address-sanitizer)。标签:AddressSanitizer, Clang, C扩展, fuzzing, libFuzzer, LLVM, pocsuite3, Ruby, Ruby安全, UndefinedBehaviorSanitizer, 二进制安全, 安全测试, 攻击性安全, 知识库, 覆盖引导测试, 软件测试