diff --git a/fuzzers/others/libafl-fuzz/Makefile.toml b/fuzzers/others/libafl-fuzz/Makefile.toml index b8f9598a0f..3914df5578 100644 --- a/fuzzers/others/libafl-fuzz/Makefile.toml +++ b/fuzzers/others/libafl-fuzz/Makefile.toml @@ -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-* ''' diff --git a/fuzzers/others/libafl-fuzz/src/executor.rs b/fuzzers/others/libafl-fuzz/src/executor.rs index f4685d7c1a..daff6e279a 100644 --- a/fuzzers/others/libafl-fuzz/src/executor.rs +++ b/fuzzers/others/libafl-fuzz/src/executor.rs @@ -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", diff --git a/fuzzers/others/libafl-fuzz/src/fuzzer.rs b/fuzzers/others/libafl-fuzz/src/fuzzer.rs index f9a360bfd1..dce415b7d4 100644 --- a/fuzzers/others/libafl-fuzz/src/fuzzer.rs +++ b/fuzzers/others/libafl-fuzz/src/fuzzer.rs @@ -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 } diff --git a/fuzzers/others/libafl-fuzz/test/seeds_frida/init b/fuzzers/others/libafl-fuzz/test/seeds_frida/init new file mode 100644 index 0000000000..5c0ac06f55 --- /dev/null +++ b/fuzzers/others/libafl-fuzz/test/seeds_frida/init @@ -0,0 +1 @@ +00000 diff --git a/fuzzers/others/libafl-fuzz/test/seeds_qemu/init b/fuzzers/others/libafl-fuzz/test/seeds_qemu/init new file mode 100644 index 0000000000..5c0ac06f55 --- /dev/null +++ b/fuzzers/others/libafl-fuzz/test/seeds_qemu/init @@ -0,0 +1 @@ +00000 diff --git a/libafl/src/executors/forkserver.rs b/libafl/src/executors/forkserver.rs index f35d84355b..cdc0edebd7 100644 --- a/libafl/src/executors/forkserver.rs +++ b/libafl/src/executors/forkserver.rs @@ -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, } -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, + 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, + 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 + // + 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?