libafl-fuzz: Introduce Support for QEMU mode (#2481)

* libafl-fuzz: simplify Makefile.toml

* Re-introduce support for old AFL++ forkserver

* clippy

* libafl-fuzz: add support for QEMU mode

* libafl-fuzz: simplify Makefile
This commit is contained in:
Aarnav 2024-08-13 14:13:59 +02:00 committed by GitHub
parent 799c634fef
commit 2287afc59b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 260 additions and 73 deletions

View File

@ -13,27 +13,32 @@ LLVM_CONFIG = { value = "llvm-config-18", condition = { env_not_set = [
"LLVM_CONFIG",
] } }
AFL_VERSION = "db23931e7c1727ddac8691a6241c97b2203ec6fc"
AFL_DIR_NAME = { value = "./AFLplusplus-${AFL_VERSION}" }
AFL_CC_PATH = { value = "${AFL_DIR_NAME}/afl-clang-fast" }
AFL_DIR = { value = "./AFLplusplus" }
AFL_CC_PATH = { value = "${AFL_DIR}/afl-clang-fast" }
CC = { value = "clang" }
[tasks.build_afl]
script_runner = "@shell"
script = '''
if [ ! -d "$AFL_DIR_NAME" ]; then
if [ -f "v${AFL_VERSION}.zip" ]; then
rm v${AFL_VERSION}.zip
fi
wget https://github.com/AFLplusplus/AFLplusplus/archive/${AFL_VERSION}.zip
unzip ${AFL_VERSION}.zip
cd ${AFL_DIR_NAME}
if [ ! -d "$AFL_DIR" ]; then
git clone https://github.com/AFLplusplus/AFLplusplus.git
cd ${AFL_DIR}
git checkout ${AFL_VERSION}
LLVM_CONFIG=${LLVM_CONFIG} make
cd frida_mode
LLVM_CONFIG=${LLVM_CONFIG} make
cd ../..
fi
'''
[tasks.build_qemuafl]
script_runner = "@shell"
script = '''
cd ${AFL_DIR}/qemu_mode
./build_qemu_support.sh
cd ../..
'''
dependencies = ["build_afl"]
# Test
[tasks.test]
linux_alias = "test_unix"
@ -43,14 +48,22 @@ windows_alias = "unsupported"
[tasks.test_unix]
script_runner = "@shell"
script = "echo done"
dependencies = ["build_afl", "test_instr", "test_cmplog", "test_frida"]
dependencies = ["build_afl", "test_instr", "test_cmplog", "test_frida", "test_qemu"]
[tasks.build_libafl_fuzz]
script_runner = "@shell"
script = "cargo build --profile ${PROFILE}"
[tasks.test_instr]
script_runner = "@shell"
script = '''
cargo build --profile ${PROFILE}
AFL_PATH=${AFL_DIR_NAME} ${AFL_CC_PATH} ./test/test-instr.c -o ./test/out-instr
LIBAFL_DEBUG_OUTPUT=1 AFL_CORES=1 AFL_STATS_INTERVAL=1 timeout 5 ${FUZZER} -i ./test/seeds -o ./test/output ./test/out-instr || true
AFL_PATH=${AFL_DIR} ${AFL_CC_PATH} ./test/test-instr.c -o ./test/out-instr
export LIBAFL_DEBUG_OUTPUT=1
export AFL_CORES=1
export AFL_STATS_INTERVAL=1
timeout 5 ${FUZZER} -i ./test/seeds -o ./test/output ./test/out-instr || true
test -n "$( ls ./test/output/fuzzer_main/queue/id:000002* 2>/dev/null )" || {
echo "No new corpus entries found"
exit 1
@ -72,46 +85,50 @@ test -d "./test/output/fuzzer_main/crashes" || {
exit 1
}
'''
dependencies = ["build_afl"]
dependencies = ["build_afl", "build_libafl_fuzz"]
[tasks.test_cmplog]
script_runner = "@shell"
script = '''
cargo build --profile ${PROFILE}
# cmplog TODO: AFL_BENCH_UNTIL_CRASH=1 instead of timeout 15s
AFL_LLVM_CMPLOG=1 AFL_PATH=${AFL_DIR_NAME} ${AFL_CC_PATH} ./test/test-cmplog.c -o ./test/out-cmplog
AFL_LLVM_CMPLOG=1 AFL_PATH=${AFL_DIR} ${AFL_CC_PATH} ./test/test-cmplog.c -o ./test/out-cmplog
AFL_CORES=1 timeout 5 ${FUZZER} -Z -l 3 -m 0 -V30 -i ./test/seeds_cmplog -o ./test/output-cmplog -c 0 ./test/out-cmplog || true
test -n "$( ls -A ./test/output-cmplog/fuzzer_main/crashes/)" || {
echo "No crashes found"
exit 1
}
'''
dependencies = ["build_afl"]
dependencies = ["build_afl", "build_libafl_fuzz"]
[tasks.test_frida]
script_runner = "@shell"
script = '''
cargo build --profile ${PROFILE}
${CC} -no-pie ./test/test-instr.c -o ./test/out-frida
AFL_PATH=${AFL_DIR_NAME} AFL_CORES=1 AFL_STATS_INTERVAL=1 timeout 5 ${FUZZER} -m 0 -V07 -O -i ./test/seeds-frida -o ./test/output-frida -- ./test/out-frida || true
export AFL_PATH=${AFL_DIR}
export AFL_CORES=1
export AFL_STATS_INTERVAL=1
timeout 5 ${FUZZER} -m 0 -V07 -O -i ./test/seeds_frida -o ./test/output-frida -- ./test/out-frida || true
test -n "$( ls ./test/output-frida/fuzzer_main/queue/id:000002* 2>/dev/null )" || {
echo "No new corpus entries found for FRIDA mode"
exit 1
}
${CC} ./test/test-cmpcov.c -o ./test/out-frida-cmpcov
AFL_PATH=${AFL_DIR_NAME} LIBAFL_DEBUG_OUTPUT=1 AFL_DEBUG=1 AFL_CORES=1 AFL_FRIDA_VERBOSE=1 timeout 10 ${FUZZER} -m 0 -V07 -O -c 0 -l 3 -i ./test/seeds-frida -o ./test/output-frida-cmpcov -- ./test/out-frida-cmpcov || true
AFL_FRIDA_VERBOSE=1 timeout 10 ${FUZZER} -m 0 -V07 -O -c 0 -l 3 -i ./test/seeds_frida -o ./test/output-frida-cmpcov -- ./test/out-frida-cmpcov || true
test -n "$( ls ./test/output-frida-cmpcov/fuzzer_main/queue/id:000003* 2>/dev/null )" || {
echo "No new corpus entries found for FRIDA cmplog mode"
exit 1
}
export AFL_FRIDA_PERSISTENT_ADDR=0x`nm ./test/out-frida | grep -Ei "T _main|T main" | awk '{print $1}'`
AFL_PATH=${AFL_DIR_NAME} AFL_STATS_INTERVAL=1 AFL_CORES=1 timeout 5 ${FUZZER} -m 0 -V07 -O -i ./test/seeds-frida -o ./test/output-frida-persistent -- ./test/out-frida || true
# TODO: change it to id:000003* once persistent mode is fixed
timeout 5 ${FUZZER} -m 0 -V07 -O -i ./test/seeds_frida -o ./test/output-frida-persistent -- ./test/out-frida || true
test -n "$( ls ./test/output-frida-persistent/fuzzer_main/queue/id:000002* 2>/dev/null )" || {
echo "No new corpus entries found for FRIDA persistent mode"
exit 1
}
RUNTIME_PERSISTENT=`grep execs_done ./test/output-frida-persistent/fuzzer_main/fuzzer_stats | awk '{print$3}'`
RUNTIME=`grep execs_done ./test/output-frida/fuzzer_main/fuzzer_stats | awk '{print$3}'`
test -n "$RUNTIME" -a -n "$RUNTIME_PERSISTENT" && {
@ -125,8 +142,44 @@ test -n "$RUNTIME" -a -n "$RUNTIME_PERSISTENT" && {
} || {
echo "we got no data on executions performed? weird!"
}
unset AFL_FRIDA_PERSISTENT_ADDR
'''
dependencies = ["build_afl"]
dependencies = ["build_afl", "build_libafl_fuzz"]
[tasks.test_qemu]
script_runner = "@shell"
script = '''
${CC} -pie -fPIE ./test/test-instr.c -o ./test/out-qemu
${CC} -o ./test/out-qemu-cmpcov ./test/test-cmpcov.c
export AFL_PATH=${AFL_DIR}
export AFL_CORES=1
export AFL_STATS_INTERVAL=1
timeout 5 ${FUZZER} -m 0 -V07 -Q -i ./test/seeds_qemu -o ./test/output-qemu -- ./test/out-qemu || true
test -n "$( ls ./test/output-qemu/fuzzer_main/queue/id:000002* 2>/dev/null )" || {
echo "No new corpus entries found for QEMU mode"
exit 1
}
export AFL_ENTRYPOINT=`printf 1 | AFL_DEBUG=1 ${AFL_DIR}/afl-qemu-trace ./test/out-qemu 2>&1 >/dev/null | awk '/forkserver/{print $4; exit}'`
timeout 5 ${FUZZER} -m 0 -V2 -Q -i ./test/seeds_qemu -o ./test/output-qemu-entrypoint -- ./test/out-qemu || true
test -n "$( ls ./test/output-qemu-entrypoint/fuzzer_main/queue/id:000002* 2>/dev/null )" || {
echo "No new corpus entries found for QEMU mode with AFL_ENTRYPOINT"
exit 1
}
unset AFL_ENTRYPOINT
export AFL_PRELOAD=${AFL_DIR}/libcompcov.so
export AFL_COMPCOV_LEVEL=2
timeout 5 ${FUZZER} -V07 -Q -i ./test/seeds_qemu -o ./test/output-qemu-cmpcov -- ./test/out-qemu-cmpcov || true
test -n "$( ls ./test/output-qemu-cmpcov/fuzzer_main/queue/id:000002* 2>/dev/null )" || {
echo "No new corpus entries found for QEMU mode"
exit 1
}
'''
dependencies = ["build_afl", "build_qemuafl","build_libafl_fuzz"]
[tasks.clean]
linux_alias = "clean_unix"
@ -136,14 +189,12 @@ windows_alias = "unsupported"
[tasks.clean_unix]
script_runner = "@shell"
script = '''
rm -rf AFLplusplus-${AFL_VERSION}
rm ${AFL_VERSION}.zip
rm -rf AFLplusplus
rm -rf ./test/out-instr
rm -rf ./test/output
rm -rf ./test/cmplog-output
rm -rf ./test/output-frida
rm -rf ./test/output-frida-cmpcov
rm -rf ./test/output-frida-persistent
rm -rf ./test/output-frida*
rm -rf ./test/output-cmplog
rm -rf ./test/output-qemu*
rm ./test/out-*
'''

View File

@ -117,7 +117,8 @@ pub fn check_binary(opt: &mut Opt, shmem_env_var: &str) -> Result<(), Error> {
));
}
if opt.forkserver_cs || opt.qemu_mode || opt.frida_mode && is_instrumented(&mmap, shmem_env_var)
if (opt.forkserver_cs || opt.qemu_mode || opt.frida_mode)
&& is_instrumented(&mmap, shmem_env_var)
{
return Err(Error::illegal_argument(
"Instrumentation found in -Q/-O mode",

View File

@ -1,4 +1,4 @@
use std::{borrow::Cow, marker::PhantomData, path::PathBuf, time::Duration};
use std::{borrow::Cow, env, marker::PhantomData, path::PathBuf, time::Duration};
use libafl::{
corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus},
@ -426,12 +426,26 @@ fn base_executor<'a>(
if let Some(kill_signal) = opt.kill_signal {
executor = executor.kill_signal(kill_signal);
}
if opt.is_persistent {
if opt.is_persistent || opt.qemu_mode {
executor = executor.shmem_provider(shmem_provider);
}
if let Some(harness_input_type) = &opt.harness_input_type {
executor = executor.parse_afl_cmdline([harness_input_type]);
}
if opt.qemu_mode {
let exec = opt.executable.display().to_string();
executor = executor.program(
find_afl_binary("afl-qemu-trace", Some(opt.executable.clone()))
.expect("to find afl-qemu-trace"),
);
// we skip all libafl-fuzz arguments.
let (skip, _) = env::args()
.enumerate()
.find(|i| i.1 == exec)
.expect("invariant; should never occur");
let args = env::args().skip(skip);
executor = executor.args(args);
}
executor
}

View File

@ -0,0 +1 @@
00000

View File

@ -0,0 +1 @@
00000

View File

@ -53,12 +53,24 @@ const FS_NEW_ERROR: i32 = 0xeffe0000_u32 as i32;
const FS_NEW_VERSION_MIN: u32 = 1;
const FS_NEW_VERSION_MAX: u32 = 1;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_ENABLED: i32 = 0x80000001_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_NEW_OPT_MAPSIZE: i32 = 1_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_MAPSIZE: i32 = 0x40000000_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_SHDMEM_FUZZ: i32 = 0x01000000_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_NEW_OPT_SHDMEM_FUZZ: i32 = 2_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_NEW_OPT_AUTODICT: i32 = 0x00000800_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_AUTODICT: i32 = 0x10000000_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_ERROR_MAP_SIZE: i32 = 1_u32 as i32;
@ -280,6 +292,10 @@ impl Drop for Forkserver {
}
}
const fn fs_opt_get_mapsize(x: i32) -> i32 {
((x & 0x00fffffe) >> 1) + 1
}
#[allow(clippy::fn_params_excessive_bools)]
impl Forkserver {
/// Create a new [`Forkserver`]
@ -343,7 +359,6 @@ impl Forkserver {
};
let mut command = Command::new(target);
// Setup args, stdio
command
.args(args)
@ -627,7 +642,10 @@ pub struct ForkserverExecutorBuilder<'a, SP> {
crash_exitcode: Option<i8>,
}
impl<'a, SP> ForkserverExecutorBuilder<'a, SP> {
impl<'a, SP> ForkserverExecutorBuilder<'a, SP>
where
SP: ShMemProvider,
{
/// Builds `ForkserverExecutor`.
/// This Forkserver will attempt to provide inputs over shared mem when `shmem_provider` is given.
/// Else this forkserver will pass the input to the target via `stdin`
@ -814,27 +832,47 @@ impl<'a, SP> ForkserverExecutorBuilder<'a, SP> {
report_error_and_exit(version_status & 0x0000ffff)?;
}
let keep = version_status;
let version: u32 = version_status as u32 - 0x41464c00_u32;
if (0x41464c00..=0x41464cff).contains(&version_status) {
match version {
0 => {
return Err(Error::unknown("Fork server version is not assigned, this should not happen. Recompile target."));
}
FS_NEW_VERSION_MIN..=FS_NEW_VERSION_MAX => {
// good, do nothing
}
_ => {
return Err(Error::unknown(
"Fork server version is not supported. Recompile the target.",
));
}
if Self::is_old_forkserver(version_status) {
log::info!("Old fork server model is used by the target, this still works though.");
self.initialize_old_forkserver(version_status, &map, &mut forkserver)?;
} else {
self.initialize_forkserver(version_status, &map, &mut forkserver)?;
}
Ok((forkserver, input_file, map))
}
fn is_old_forkserver(version_status: i32) -> bool {
!(0x41464c00..0x41464cff).contains(&version_status)
}
/// Intialize forkserver > v4.20c
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
fn initialize_forkserver(
&mut self,
status: i32,
map: &Option<SP::ShMem>,
forkserver: &mut Forkserver,
) -> Result<(), Error> {
let keep = status;
let version: u32 = status as u32 - 0x41464c00_u32;
match version {
0 => {
return Err(Error::unknown("Fork server version is not assigned, this should not happen. Recompile target."));
}
FS_NEW_VERSION_MIN..=FS_NEW_VERSION_MAX => {
// good, do nothing
}
_ => {
return Err(Error::unknown(
"Fork server version is not supported. Recompile the target.",
));
}
}
let xored_version_status = (version_status as u32 ^ 0xffffffff) as i32;
let xored_status = (status as u32 ^ 0xffffffff) as i32;
let send_len = forkserver.write_ctl(xored_version_status)?;
let send_len = forkserver.write_ctl(xored_status)?;
if send_len != 4 {
return Err(Error::unknown("Writing to forkserver failed.".to_string()));
}
@ -852,29 +890,13 @@ impl<'a, SP> ForkserverExecutorBuilder<'a, SP> {
}
if status & FS_NEW_OPT_MAPSIZE == FS_NEW_OPT_MAPSIZE {
// When 0, we assume that map_size was filled by the user or const
/* TODO autofill map size from the observer
if map_size > 0 {
self.map_size = Some(map_size as usize);
}
*/
let (read_len, mut map_size) = forkserver.read_st()?;
let (read_len, fsrv_map_size) = forkserver.read_st()?;
if read_len != 4 {
return Err(Error::unknown(
"Failed to read map size from forkserver".to_string(),
));
}
if map_size % 64 != 0 {
map_size = ((map_size + 63) >> 6) << 6;
}
// TODO set AFL_MAP_SIZE
assert!(self.map_size.is_none() || map_size as usize <= self.map_size.unwrap());
// we'll use this later when we truncate the observer
self.map_size = Some(map_size as usize);
self.set_map_size(fsrv_map_size);
}
if status & FS_NEW_OPT_SHDMEM_FUZZ != 0 {
@ -921,14 +943,111 @@ impl<'a, SP> ForkserverExecutorBuilder<'a, SP> {
return Err(Error::unknown("Reading from forkserver failed".to_string()));
}
if aflx != version_status {
if aflx != keep {
return Err(Error::unknown(format!(
"Error in forkserver communication ({:x}=>{:x})",
keep, aflx
"Error in forkserver communication ({aflx:?}=>{keep:?})",
)));
}
Ok(())
}
Ok((forkserver, input_file, map))
/// Intialize old forkserver. < v4.20c
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
fn initialize_old_forkserver(
&mut self,
status: i32,
map: &Option<SP::ShMem>,
forkserver: &mut Forkserver,
) -> Result<(), Error> {
if status & FS_OPT_ENABLED == FS_OPT_ENABLED && status & FS_OPT_MAPSIZE == FS_OPT_MAPSIZE {
let fsrv_map_size = fs_opt_get_mapsize(status);
self.set_map_size(fsrv_map_size);
}
// Only with SHMEM or AUTODICT we can send send_status back or it breaks!
// If forkserver is responding, we then check if there's any option enabled.
// We'll send 4-bytes message back to the forkserver to tell which features to use
// The forkserver is listening to our response if either shmem fuzzing is enabled or auto dict is enabled
// <https://github.com/AFLplusplus/AFLplusplus/blob/147654f8715d237fe45c1657c87b2fe36c4db22a/instrumentation/afl-compiler-rt.o.c#L1026>
if status & FS_OPT_ENABLED == FS_OPT_ENABLED
&& (status & FS_OPT_SHDMEM_FUZZ == FS_OPT_SHDMEM_FUZZ
|| status & FS_OPT_AUTODICT == FS_OPT_AUTODICT)
{
let mut send_status = FS_OPT_ENABLED;
if (status & FS_OPT_SHDMEM_FUZZ == FS_OPT_SHDMEM_FUZZ) && map.is_some() {
log::info!("Using SHARED MEMORY FUZZING feature.");
send_status |= FS_OPT_SHDMEM_FUZZ;
self.uses_shmem_testcase = true;
}
if (status & FS_OPT_AUTODICT == FS_OPT_AUTODICT) && self.autotokens.is_some() {
log::info!("Using AUTODICT feature");
send_status |= FS_OPT_AUTODICT;
}
if send_status != FS_OPT_ENABLED {
// if send_status is not changed (Options are available but we didn't use any), then don't send the next write_ctl message.
// This is important
let send_len = forkserver.write_ctl(send_status)?;
if send_len != 4 {
return Err(Error::unknown("Writing to forkserver failed.".to_string()));
}
if (send_status & FS_OPT_AUTODICT) == FS_OPT_AUTODICT {
let (read_len, dict_size) = forkserver.read_st()?;
if read_len != 4 {
return Err(Error::unknown(
"Reading from forkserver failed.".to_string(),
));
}
if !(2..=0xffffff).contains(&dict_size) {
return Err(Error::illegal_state(
"Dictionary has an illegal size".to_string(),
));
}
log::info!("Autodict size {dict_size:x}");
let (rlen, buf) = forkserver.read_st_size(dict_size as usize)?;
if rlen != dict_size as usize {
return Err(Error::unknown("Failed to load autodictionary".to_string()));
}
if let Some(t) = &mut self.autotokens {
t.parse_autodict(&buf, dict_size as usize);
}
}
}
} else {
log::warn!("Forkserver Options are not available.");
}
Ok(())
}
#[allow(clippy::cast_sign_loss)]
fn set_map_size(&mut self, fsrv_map_size: i32) {
// When 0, we assume that map_size was filled by the user or const
/* TODO autofill map size from the observer
if fsrv_map_size > 0 {
self.map_size = Some(fsrv_map_size as usize);
}
*/
let mut map_size = fsrv_map_size;
if map_size % 64 != 0 {
map_size = ((map_size + 63) >> 6) << 6;
}
// TODO set AFL_MAP_SIZE
assert!(self.map_size.is_none() || map_size as usize <= self.map_size.unwrap());
// we'll use this later when we truncate the observer
self.map_size = Some(map_size as usize);
}
/// Use autodict?