本教程安装LibAFL使用的是Ubuntu 22.04 操作系统
1. 安装
1.1 Rust 安装
Rust的安装,参照Rust官网:https://www.rust-lang.org/tools/install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
1.2 LLVM安装
直接apt安装,安装的应该是LLVM 14,LibAFL说是在LLVM 11-15之间就行。
sudo apt update
sudo apt-get install llvm clang
1.3 LibAFL安装
cargo install cargo-make
git clone https://github.com/AFLplusplus/LibAFL
cd LibAFL
cargo build --release
自此安装完毕。
2. 基本使用
现在来使用这个LibAFL已经写好的模糊测试器(libfuzzer)来测试libpng。主要参考官方的这个教程:https://github.com/AFLplusplus/LibAFL/tree/main/fuzzers/libfuzzer_libpng
这个模糊测试器在LibAFL/fuzzers/libfuzzer_libpng目录下,先把他build起来。
cd fuzzers/libfuzzer_libpng
cargo build --release
这个操作会生成两个编译器(libafl_cc和libafl_cxx)的wrapper,需要使用他们来编译程序。他们会出现在target/release的文件夹下。
然后下载libpng,并解压
wget https://deac-fra.dl.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.xz
tar -xvf libpng-1.6.37.tar.xz
然后使用libafl_cc编译器来编译libpng,
cd libpng-1.6.37
./configure --enable-shared=no --with-pic=yes --enable-hardware-optimizations=yes
make CC="$(pwd)/../target/release/libafl_cc" CXX="$(pwd)/../target/release/libafl_cxx" -j `nproc`
然后可以发现编译后的静态库在这个目录下libpng-1.6.37/.libs/libpng16.a
因为我们测试的是libpng,它是一个库,所以还需要编译一个harness,来调用libpng的库。harness.cc位于fuzzers/libfuzzer_libpng下。
./target/release/libafl_cxx ./harness.cc libpng-1.6.37/.libs/libpng16.a -I libpng-1.6.37/ -o fuzzer_libpng -lz -lm
开始测试,先在一个终端运行下面的程序,它会开启一个tcp端口(1337),等待fuzzer 客户端连接,这个端口是本地的,目的只是用来初始化的握手。后续的通信是通过shared map。目前需要在libfuzzer_libpng的目录下运行,才可以访问到libpng的语料库。
./fuzzer_libpng
然后在另外开启另外一个终端,运行下面命令
./fuzzer_libpng
再切回到原来的终端,会发现开始跑模糊测试了。
这是第一个终端的界面,需要在另外一个终端执行./fuzzer_libpng才会出现下面的情况
另一个终端的界面
3. libfuzz_libpng是如何构造的
进入src目录下,会发现有lib.rs和bin目录,bin目录存放的是编译器wrapper的代码,也就是负责插桩的代码。lib.rs是构建fuzzer的代码。
3.1 libafl_cc.rs
简单来看就是个clang的包装器,加了个-fsanitize-coverage=trace-pc-guard选项。
下面是chatgpt对这部分代码的解释,感觉说的没啥问题。
这是一个Rust语言编写的程序,主要目的是作为一个编译器的包装器(wrapper)来调用Clang编译器,并在编译时链接静态库并进行覆盖率分析。
程序接受命令行参数作为输入,然后根据参数执行不同的操作。如果命令行参数的数量少于2,则程序会抛出一个panic异常。否则,程序会尝试解析命令行参数并使用Clang编译器进行编译。如果编译成功,则程序以编译器的返回代码(exit code)退出,否则程序也会抛出一个panic异常。
具体来说,程序首先使用Rust标准库中的env模块获取命令行参数,并检查是否至少传入了一个参数。接下来,程序通过调用ClangWrapper::new()方法创建一个ClangWrapper对象,然后根据包装器(wrapper)的名称来判断要使用C++编译器还是C编译器。如果包装器的名称以"cc"结尾,则使用C编译器,否则如果名称以"++"、"pp"或"xx"结尾,则使用C++编译器。如果无法确定应该使用哪种编译器,则程序会抛出一个panic异常。
然后,程序使用ClangWrapper对象的方法来添加链接静态库、设置覆盖率分析等编译选项,并运行编译器进行编译。如果编译器成功完成编译,则程序使用编译器的返回代码(exit code)退出。
总之,这个程序主要是作为一个包装器(wrapper)来调用Clang编译器,以便在编译时添加一些额外的选项。在这种情况下,它被用于编译fuzz测试。
use std::env;
use libafl_cc::{ClangWrapper, CompilerWrapper};
pub fn main() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
let mut dir = env::current_exe().unwrap();
let wrapper_name = dir.file_name().unwrap().to_str().unwrap();
let is_cpp = match wrapper_name[wrapper_name.len()-2..].to_lowercase().as_str() {
"cc" => false,
"++" | "pp" | "xx" => true,
_ => panic!("Could not figure out if c or c++ wrapper was called. Expected {dir:?} to end with c or cxx"),
};
dir.pop();
let mut cc = ClangWrapper::new();
if let Some(code) = cc
.cpp(is_cpp)
// silence the compiler wrapper output, needed for some configure scripts.
.silence(true)
.parse_args(&args)
.expect("Failed to parse the command line")
.link_staticlib(&dir, "libfuzzer_libpng")
.add_arg("-fsanitize-coverage=trace-pc-guard")
.run()
.expect("Failed to run the wrapped compiler")
{
std::process::exit(code);
}
} else {
panic!("LibAFL CC: No Arguments given");
}
}
3.2 lib.rs
lib.rs代码有点多,分段来看,首先导入库的部分,继承了LibAFL里很多有用的组件。
//! A libfuzzer-like fuzzer with llmp-multithreading support and restarts
//! The example harness is built for libpng.
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
use core::time::Duration;
#[cfg(feature = "crash")]
use std::ptr;
use std::{env, path::PathBuf};
use libafl::{
bolts::{
current_nanos,
rands::StdRand,
tuples::{tuple_list, Merge},
AsSlice,
},
corpus::{Corpus, InMemoryCorpus, OnDiskCorpus},
events::{setup_restarting_mgr_std, EventConfig, EventRestarter},
executors::{inprocess::InProcessExecutor, ExitKind, TimeoutExecutor},
feedback_or, feedback_or_fast,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
fuzzer::{Fuzzer, StdFuzzer},
inputs::{BytesInput, HasTargetBytes},
monitors::MultiMonitor,
mutators::{
scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator},
token_mutations::Tokens,
},
observers::{HitcountsMapObserver, StdMapObserver, TimeObserver},
schedulers::{
powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, StdWeightedScheduler,
},
stages::{calibrate::CalibrationStage, power::StdPowerMutationalStage},
state::{HasCorpus, HasMetadata, StdState},
Error,
};
use libafl_targets::{libfuzzer_initialize, libfuzzer_test_one_input, EDGES_MAP, MAX_EDGES_NUM};
再看下主函数,主要是调用fuzz函数,传入了三个参数,分别是语料库的路径、崩溃文件目录以及随机种子。
pub fn libafl_main() {
// Registry the metadata types used in this fuzzer
// Needed only on no_std
//RegistryBuilder::register::<Tokens>();
println!(
"Workdir: {:?}",
env::current_dir().unwrap().to_string_lossy().to_string()
);
fuzz(
&[PathBuf::from("./corpus")],
PathBuf::from("./crashes"),
1337,
)
.expect("An error occurred while fuzzing");
}
fuzz函数中,首先创建了MultiMonitor来打印调试信息。
let monitor = MultiMonitor::new(|s| println!("{s}"));
RestartingManager让目标程序在模糊测试过程中崩溃时重新启动程序。
let (state, mut restarting_mgr) =
match setup_restarting_mgr_std(monitor, broker_port, EventConfig::AlwaysUnique) {
Ok(res) => res,
Err(err) => match err {
Error::ShuttingDown => {
return Ok(());
}
_ => {
panic!("Failed to setup the restarter: {err}");
}
},
};
edges_observer使用覆盖率映射表来观察程序的执行情况
let edges_observer = unsafe {
HitcountsMapObserver::new(StdMapObserver::from_mut_ptr(
"edges",
EDGES_MAP.as_mut_ptr(),
MAX_EDGES_NUM,
))
};
判断输入是否有趣。主要是基于覆盖率和执行的时间来进行判断。
let time_observer = TimeObserver::new("time");
let map_feedback = MaxMapFeedback::new_tracking(&edges_observer, true, false);
let calibration = CalibrationStage::new(&map_feedback);
let mut feedback = feedback_or!(
// New maximization map feedback linked to the edges observer and the feedback state
map_feedback,
// Time feedback, this one does not need a feedback state
TimeFeedback::with_observer(&time_observer)
);
判断输入是否是最终想要的,即是不是PoC。
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
如果重启失败了,需要重新创建状态。
let mut state = state.unwrap_or_else(|| {
StdState::new(
// RNG
StdRand::with_seed(current_nanos()),
// Corpus that will be evolved, we keep it in memory for performance
InMemoryCorpus::new(),
// Corpus in which we store solutions (crashes in this example),
// on disk so the user can get them after stopping the fuzzer
OnDiskCorpus::new(objective_dir).unwrap(),
// States of the feedbacks.
// The feedbacks can report the data that should persist in the State.
&mut feedback,
// Same for objective feedbacks
&mut objective,
)
.unwrap()
});
创建png字典。主要是libpng需要合法的png图片来作为输入。
if state.metadata().get::<Tokens>().is_none() {
state.add_metadata(Tokens::from([
vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header
"IHDR".as_bytes().to_vec(),
"IDAT".as_bytes().to_vec(),
"PLTE".as_bytes().to_vec(),
"IEND".as_bytes().to_vec(),
]));
}
构造一个具有多阶段的变异器。
let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations()));
let power = StdPowerMutationalStage::new(mutator);
let mut stages = tuple_list!(calibration, power);
从语料库获取种子的调度器(种子调度)
let scheduler = IndexesLenTimeMinimizerScheduler::new(StdWeightedScheduler::with_schedule(
&mut state,
&edges_observer,
Some(PowerSchedule::FAST),
));
把前面的组件组装为1个fuzzer
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
构造harness的wrapper,不太理解这段代码,推测是libfuzzer的特性所以需要这么一段。
let mut harness = |input: &BytesInput| {
let target = input.target_bytes();
let buf = target.as_slice();
#[cfg(feature = "crash")]
if buf.len() > 4 && buf[4] == 0 {
unsafe {
eprintln!("Crashing (for testing purposes)");
let addr = ptr::null_mut();
*addr = 1;
}
}
libfuzzer_test_one_input(buf);
ExitKind::Ok
};
构建一个超时的执行器,也就是在给定的时间内执行程序。
let mut executor = TimeoutExecutor::new(
InProcessExecutor::new(
&mut harness,
tuple_list!(edges_observer, time_observer),
&mut fuzzer,
&mut state,
&mut restarting_mgr,
)?,
// 10 seconds timeout
Duration::new(10, 0),
);
// The actual target run starts here.
// Call LLVMFUzzerInitialize() if present.
let args: Vec<String> = env::args().collect();
if libfuzzer_initialize(&args) == -1 {
println!("Warning: LLVMFuzzerInitialize failed with -1");
}
处理下初始语料库为空的情况
// In case the corpus is empty (on first run), reset
if state.must_load_initial_inputs() {
state
.load_initial_inputs(&mut fuzzer, &mut executor, &mut restarting_mgr, corpus_dirs)
.unwrap_or_else(|_| panic!("Failed to load initial corpus at {:?}", &corpus_dirs));
println!("We imported {} inputs from disk.", state.corpus().count());
}
迭代执行
let iters = 1_000_000;
fuzzer.fuzz_loop_for(
&mut stages,
&mut executor,
&mut state,
&mut restarting_mgr,
iters,
)?;
总的来说有点像搭积木的感觉,但是目前对每个积木怎么搭的还不是很了解,后续再看看别的fuzzer是怎么实现的。