简介

传统的 AFL、libFuzzer 和 honggfuzz 主要支持用 C/C++ 语言开发的项目。但是现在随着 Rust、Go 等新兴编译型语言的兴起,相应的出现了对这些语言开发的项目进行 Fuzz 的需求。因此,人们在基于传统的 Fuzz 思想的基础上,逐步为这些语言构建了 Fuzz 工具。本文所要学习的 cargo-fuzz 正是如此。

Rust环境配置

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
#然后选1,将以下命令写入
source $HOME/.cargo/env

cargo-fuzz

cargo-fuzz 是 Rust 代码模糊测试的推荐工具。不过,cargo-fuzz 本身并不是一个 fuzzer,而是一个调用 fuzzer 的工具。目前,它仅仅支持通过 libfuzzer-sys crate 调用 LibFuzzer。

环境搭建

由于 libFuzzer 需要 LLVM sanitizer 支持,所以我们需要将 Rust 编译器切换至 nightly 版本

# 安装nightly版本
rustup install nightly

# 设置为默认编译器
rustup default nightly

安装 cargo-fuzz 工具:

cargo install cargo-fuzz

基本用法

接下来我们将对 Rust 的 URL 解析库  rust-url 进行 Fuzzing 来学习如何使用 cargo-fuzz。

环境搭建:

首先,克隆 rust-url 项目并切换指定提交:

git clone https://github.com/servo/rust-url.git
cd rust-url

git checkout bfa167b4e0253642b6766a7aa74a99df60a94048

初始化 Fuzz 环境

cargo fuzz init

查看现有的 fuzz 目标

❯ cargo fuzz list
fuzz_target_1

默认生成了 fuzz_target_1 文件。

配置测试目标:

编辑默认生成的测试文件fuzz-targets/fuzz_target_1.rs,修改为以下内容。

#![no_main] //禁用默认 main 函数入口
extern crate libfuzzer_sys; //导入 libfuzzer_sys,
use libfuzzer_sys::fuzz_target;
extern crate url; //引入 url crate

//模糊测试接口函数
fuzz_target!(|data: &[u8]| {
    // 忽略非 URF-8 输入
    if let Ok(s) = std::str::from_utf8(data) {
	    // 测试目标函数
        let _ = url::Url::parse(s);
    }
});

fuzz_target!是 cargo-fuzz 提供的宏,用于定义 Fuzz 入口。libFuzzer 会持续调用它,并传入生成的数据。类似于 C/C++ 中的LLVMTestOneInput

构建语料:

在 fuzz 目录下创建corpus/fuzz_target_1目录,然后将语料存放在corpus/fuzz_target_1目录下。

cd fuzz
mkdir -p corpus/fuzz_target_1       
mv seed.txt corpus/fuzz_target_1       

开始 Fuzzing:

通过cargo fuzz run运行目标,自动编译带插桩的目标。

cargo fuzz run fuzz_target_1

在跑了一会之后程序就直接 crash 了,并将 crash 样本保存到了artifacts/fuzz_target_1中。

根据调用栈跟踪发现具体问题在#18 ... rust-url/src/parser.rs:287:46,错误信息为:

thread '<unnamed>' panicked at ... attempt to subtract with overflow

说明某个地方发生了x - y的运算,其中x < y,在 release 模式下没有检查,但是我们通过 ASan 捕捉到了。

复现 crash:

 cargo fuzz run fuzz_target_1 artifacts/fuzz_target_1/crash-7336c41a88a07fc4343d1cea44ddc092589caff9

cargo fuzz 的其它常用参数:

  • cargo fuzz add target:创建新的模糊测试目标。
  • cargo fuzz fmt target input:打印测试用例的输入。
  • cargo fuz tmin target input:缩小crash。
  • cargo fuzz cmin target:缩小输入语料库。
  • cargo fuzz coverage:生成覆盖率信息。
  • 可以设置环境变量RUST_BACKTRACE=1来决定是否打印调用栈。

libFuzzer支持

通过--可以使用 libFuzzer 的参数:

cargo fuzz run target_name -- -max_len=512 -only_ascii=1

参数:

  • -ignore_crashes=1:即使遇到崩溃也继续 Fuzz。
  • -s,--sanitizer:选择 Sanitizer。
  • -max_len=<len>:限制输入最大长度。
  • -runs=<number>:限制运行次数。
  • -max_total_time=<sec>:限制总运行时间(秒)。
  • -timeout=<sec>:单次运行超时时间(秒)。
  • -only_ascii=1:仅生成 ASCII 输入。
  • -dict=<file>:使用字典文件。
  • -c, --careful:开启额外的安全检查。
  • --no-cfg-fuzzing:禁用默认的cfg(fuzzing)编译配置。

测量代码覆盖率

环境搭建

# 安装 LLVM 工具集(用于 coverage)
rustup component add --toolchain nightly llvm-tools-preview
cargo install cargo-binutils
cargo install rustfilt

基本使用

测量代码覆盖率需要通过 cargo-fuzz,所以又回到了 cargo-fuzz。

#手动插桩目标项目
RUSTFLAGS="-C instrument-coverage" cargo build --release
#测量覆盖率
cargo fuzz coverage fuzz_target_1

这里 cargo fuzz 会自动测试corpus/fuzz_target_1目录下的语料。

最后在fuzz/coverage/fuzz_target_1目录下生成了覆盖率文件coverage.profdata

可视化覆盖率信息

生成 HTML 格式覆盖率报告:

cargo cov -- show fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 \
  --format=html \
  --Xdemangler=rustfilt \
  --instr-profile=fuzz/coverage/fuzz_target_1/coverage.profdata \
  > coverage.html

终端查看:

cargo cov -- show --use-color \
  --instr-profile=fuzz/coverage/fuzz_target_1/coverage.profdata \
  fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 \
  --show-regions --show-instantiations

参考

rust-fuzz/trophy-case: 🏆 Collection of bugs uncovered by fuzzing Rust code afl_stat - Rust 设置 - Rust Fuzz Book rust-fuzz/afl.rs: 🐇 Fuzzing Rust code with American Fuzzy Lop