本教程安装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 statemap_feedback,// Time feedback, this one does not need a feedback stateTimeFeedback::with_observer(&time_observer));
判断输入是否是最终想要的,即是不是PoC。
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
如果重启失败了,需要重新创建状态。
let mut state = state.unwrap_or_else(|| {StdState::new(// RNGStdRand::with_seed(current_nanos()),// Corpus that will be evolved, we keep it in memory for performanceInMemoryCorpus::new(),// Corpus in which we store solutions (crashes in this example),// on disk so the user can get them after stopping the fuzzerOnDiskCorpus::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 timeoutDuration::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), resetif 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是怎么实现的。