一个基于覆盖率引导的模糊测试工具,用于纯 Ruby 代码和 Ruby C 扩展。
Ruzzy 深受 Google 的 Atheris(一个 Python 模糊测试器)启发。Ruzzy 使用 libFuzzer(或 LibAFL)作为其覆盖率检测和模糊测试引擎。在对 C 扩展进行模糊测试时,Ruzzy 还支持 AddressSanitizer 和 UndefinedBehaviorSanitizer。如果您想了解更多关于 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 环境变量会在编译 Ruzzy C 扩展时覆盖 make 命令。这会告诉 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 脚本:一个追踪器脚本和一个模糊测试驱动。由于 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。这些标志有助于模糊测试过程。它们启用了有用的功能,例如地址消毒器和改进的堆栈跟踪信息。更多信息请参阅 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)。
macOS 上的 Ruzzy 需要 Homebrew 安装的 LLVM(Apple Clang 不包含 libFuzzer)和非系统 Ruby(位于 /usr/bin/ruby 的系统 Ruby 受 SIP 保护,会在 Ruby 启动前剥离 DYLD_* 环境变量)。
brew install llvm ruby
任何非系统 Ruby 都可以(brew、rbenv、asdf),但 shim 类型的版本管理器请参阅下面的注意事项。
使用 Homebrew 的 Clang 路径和适用于 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'
在 macOS 上,Ruzzy::ASAN_PATH 和 Ruzzy::UBSAN_PATH 解析为 .dylib 文件。
asdf、rbenv)会剥离 DYLD_* 环境变量。**这些 shim 使用 #!/usr/bin/env bash,而 /usr/bin/env 受 SIP 保护,因此 macOS 会在 Ruby 启动前剥离 DYLD_INSERT_LIBRARIES。要么使用 Homebrew Ruby(没有 shim),要么调用已安装 Ruby 二进制的绝对路径:
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIES 注入 ASan dylib 会导致进程在启动时挂起。如果您看到 Ruzzy 在启动时挂起,请更新 Homebrew LLVM(brew upgrade llvm)。使用 Ruzzy 发现的漏洞:
toml gem: #76toml-rb gem: #150ox gem: #351, #410Marshal garbage collector crash: #20941redcarpet gem: #813可以在本地或使用本仓库提供的 Dockerfile 进行开发。
您可以使用以下命令构建 Ruzzy Docker 镜像:
docker build --tag ruzzy .
然后,您可以使用以下命令进入容器:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
我们使用 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 会自动发布到 RubyGems。
要发布新版本,请运行以下命令:
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) | 用 Ruby 分支覆盖率检测包装 harness_script,然后 require 它。纯 Ruby 模糊测试必需。 |
Ruzzy.dummy | 对捆绑的玩具驱动进行模糊测试(堆释放后使用演示)。 |
Ruzzy.dummy_test_one_input(data) | 玩具驱动本身。 |
| 常量 | 描述 |
|---|
Ruzzy::ASAN_PATH | ASan + fuzzer 包装器的路径。与 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 | 跨越完整双精度范围的浮点数 | Float |
consume_float_in_range(min, max) | 在 [min, max] 范围内的浮点数 | Float |
consume_probability | 在 [0.0, 1.0] 范围内的浮点数 | Float |
pick_value_in_list(list) | 从 list 中随机选取一个元素 | 元素 |