一个基于覆盖率引导的模糊测试工具,用于纯 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.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) | 玩具驱动本身。 |