diff --git a/fuzzers/qemu_tmin/.gitignore b/fuzzers/qemu_tmin/.gitignore new file mode 100644 index 0000000000..eb0ade05d1 --- /dev/null +++ b/fuzzers/qemu_tmin/.gitignore @@ -0,0 +1,7 @@ +libpng-* +libpng_harness +libpng_harness_crashing +zlib-* +crashes +target +output diff --git a/fuzzers/qemu_tmin/Cargo.toml b/fuzzers/qemu_tmin/Cargo.toml new file mode 100644 index 0000000000..051f6bf103 --- /dev/null +++ b/fuzzers/qemu_tmin/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "qemu_tmin" +version = "0.11.1" +authors = ["Andrea Fioraldi ", "Dominik Maier ", "WorksButNotTested"] +edition = "2021" + +[profile.release] +#lto = true +#codegen-units = 1 +#opt-level = 3 +debug = true + +[features] +default = ["std"] +std = [] +be = ["libafl_qemu/be"] +arm = ["libafl_qemu/arm"] +x86_64 = ["libafl_qemu/x86_64"] +i386 = ["libafl_qemu/i386"] +aarch64 = ["libafl_qemu/aarch64"] +mips = ["libafl_qemu/mips"] +ppc = ["libafl_qemu/ppc", "be"] + +[build-dependencies] +vergen = { version = "8.2.1", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] } + +[dependencies] +clap = { version = "4.3.0", features = ["derive", "string"]} +libafl = { path = "../../libafl/" } +libafl_bolts = { path = "../../libafl_bolts/" } +libafl_qemu = { path = "../../libafl_qemu/", features = ["usermode"] } +log = {version = "0.4.20" } +rangemap = { version = "1.3" } diff --git a/fuzzers/qemu_tmin/Makefile.toml b/fuzzers/qemu_tmin/Makefile.toml new file mode 100644 index 0000000000..d654a7000d --- /dev/null +++ b/fuzzers/qemu_tmin/Makefile.toml @@ -0,0 +1,323 @@ +[env] +PROFILE = { value = "release", condition = {env_not_set = ["PROFILE"]} } +PROFILE_DIR = {value = "release", condition = {env_not_set = ["PROFILE_DIR"] }} +CROSS_CC = "x86_64-linux-gnu-gcc" +CROSS_CXX = "x86_64-linux-gnu-g++" +CROSS_CFLAGS = "" +TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/x86_64" +LIBPNG_ARCH = "x86_64" +LIBPNG_OPTIMIZATIONS = "yes" +FEATURE = "x86_64" +#LIBAFL_DEBUG_OUTPUT = "1" +#CUSTOM_QEMU_DIR= "~/qemu-libafl-bridge" + +[env.arm] +CROSS_CC = "arm-linux-gnueabi-gcc" +CROSS_CXX = "arm-linux-gnueabi-g++" +CROSS_CFLAGS = "" +TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/arm" +LIBPNG_ARCH = "arm" +LIBPNG_OPTIMIZATIONS = "yes" +FEATURE = "arm" + +[env.aarch64] +CROSS_CC = "aarch64-linux-gnu-gcc" +CROSS_CXX = "aarch64-linux-gnu-g++" +CROSS_CFLAGS = "" +TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/aarch64" +LIBPNG_ARCH = "aarch64" +LIBPNG_OPTIMIZATIONS = "yes" +FEATURE = "aarch64" + +[env.x86_64] +CROSS_CC = "x86_64-linux-gnu-gcc" +CROSS_CXX = "x86_64-linux-gnu-g++" +CROSS_CFLAGS = "" +TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/x86_64" +LIBPNG_ARCH = "x86_64" +LIBPNG_OPTIMIZATIONS = "yes" +FEATURE = "x86_64" + +[env.i386] +CROSS_CC = "x86_64-linux-gnu-gcc" +CROSS_CXX = "x86_64-linux-gnu-g++" +CROSS_CFLAGS = "-m32" +TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/i386" +LIBPNG_ARCH = "i386" +LIBPNG_OPTIMIZATIONS = "yes" +FEATURE = "i386" + +[env.mips] +CROSS_CC = "mipsel-linux-gnu-gcc" +CROSS_CXX = "mipsel-linux-gnu-g++" +CROSS_CFLAGS = "" +TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/mips" +LIBPNG_ARCH = "mips" +LIBPNG_OPTIMIZATIONS = "yes" +FEATURE = "mips" + +[env.ppc] +CROSS_CC = "powerpc-linux-gnu-gcc" +CROSS_CXX = "powerpc-linux-gnu-g++" +CROSS_CFLAGS = "" +TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/ppc" +LIBPNG_ARCH = "ppc" +LIBPNG_OPTIMIZATIONS = "no" +FEATURE = "ppc" + +[tasks.unsupported] +script_runner="@shell" +script=''' +echo "Qemu fuzzer not supported on windows/mac" +''' + + +[tasks.target_dir] +condition = { files_not_exist = [ "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}" ] } +script_runner="@shell" +script=''' +mkdir ${CARGO_MAKE_CRATE_TARGET_DIRECTORY} +''' + +[tasks.deps_dir] +dependencies = ["target_dir"] +condition = { files_not_exist = [ "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/" ] } +script_runner="@shell" +script=''' +mkdir ${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/ +''' + +[tasks.arch_target_dir] +dependencies = ["target_dir"] +condition = { files_not_exist = [ "${TARGET_DIR}" ] } +script_runner="@shell" +script=''' +mkdir ${TARGET_DIR} +''' + +[tasks.zlib] +linux_alias = "zlib_unix" +mac_alias = "unsupported" +windows_alias = "unsupported" + +[tasks.zlib_unix_wget] +dependencies = ["deps_dir"] +condition = { files_not_exist = [ "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/zlib-1.2.13" ] } +script_runner="@shell" +# NOTE: There's no specific reason we're using an old version of zlib, +# but newer versions get moved to fossils/ after a while. +script=''' +wget \ + -O "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/zlib-1.2.13.tar.gz" \ + https://zlib.net/fossils/zlib-1.2.13.tar.gz + +tar \ + zxvf ${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/zlib-1.2.13.tar.gz \ + -C ${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/ +''' + +[tasks.zlib_unix] +dependencies = ["arch_target_dir", "zlib_unix_wget" ] +condition = { files_not_exist = [ "${TARGET_DIR}/build-zlib/libz.a" ] } +script_runner="@shell" +script=''' +rm -rf ${TARGET_DIR}/build-zlib/ + +mkdir ${TARGET_DIR}/build-zlib/ + +cd ${TARGET_DIR}/build-zlib/ && \ + CC=$CROSS_CC \ + CFLAGS=${CROSS_CFLAGS} \ + ${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/zlib-1.2.13/configure \ + --prefix=./zlib + +make install +''' + +[tasks.libpng] +linux_alias = "libpng_unix" +mac_alias = "unsupported" +windows_alias = "unsupported" + +[tasks.libpng_unix_wget] +dependencies = ["deps_dir"] +condition = { files_not_exist = [ "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/libpng-1.6.37" ] } +script_runner="@shell" +script=''' +wget \ + -O "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/libpng-1.6.37.tar.xz" \ + https://deac-fra.dl.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.xz + +tar \ + -xvf "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/libpng-1.6.37.tar.xz" \ + -C ${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/ +''' + +[tasks.libpng_unix] +dependencies = [ "arch_target_dir", "zlib", "libpng_unix_wget" ] +condition = { files_not_exist = [ "${TARGET_DIR}/build-png/.libs/libpng16.a" ] } +script_runner="@shell" +script=''' +rm -rf ${TARGET_DIR}/build-png/ + +mkdir ${TARGET_DIR}/build-png/ + +cd ${TARGET_DIR}/build-png/ && \ + CC=$CROSS_CC \ + CFLAGS="${CROSS_CFLAGS} -I"${TARGET_DIR}/build-zlib/zlib/lib"" \ + LDFLAGS=-L"${TARGET_DIR}/build-zlib/zlib/lib" \ + ${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/deps/libpng-1.6.37/configure \ + --enable-shared=no \ + --with-pic=yes \ + --enable-hardware-optimizations=${LIBPNG_OPTIMIZATIONS} \ + --host=${LIBPNG_ARCH} \ + +make +''' + +[tasks.build] +linux_alias = "build_unix" +mac_alias = "build_unix" +windows_alias = "unsupported" + +[tasks.build_unix] +command = "cargo" +args = [ + "build", + "--profile", + "${PROFILE}", + "--features", "${FEATURE}", + "--target-dir", "${TARGET_DIR}" +] + +[tasks.fuzzer] +dependencies = ["build"] +script_runner="@shell" +script=''' +rm -f ${TARGET_DIR}/${PROFILE_DIR}/qemu_tmin-${CARGO_MAKE_PROFILE} +mv ${TARGET_DIR}/${PROFILE_DIR}/qemu_tmin ${TARGET_DIR}/${PROFILE_DIR}/qemu_tmin-${CARGO_MAKE_PROFILE} +''' + +[tasks.harness] +linux_alias = "harness_unix" +mac_alias = "unsupported" +windows_alias = "unsupported" + +[tasks.harness_unix] +script_runner="@shell" +script=''' +${CROSS_CXX} \ + ./harness.cc \ + $CROSS_CFLAGS \ + "${TARGET_DIR}/build-png/.libs/libpng16.a" \ + "${TARGET_DIR}/build-zlib/libz.a" \ + -I"${TARGET_DIR}/build-png" \ + -I"${TARGET_DIR}/build-zlib/zlib/lib" \ + -L"${TARGET_DIR}/build-zlib/zlib/lib" \ + -o"${TARGET_DIR}/libpng-harness-${CARGO_MAKE_PROFILE}" \ + -lm \ + -static +''' +dependencies = [ "libpng" ] + +[tasks.run] +linux_alias = "run_unix" +mac_alias = "unsupported" +windows_alias = "unsupported" + +[tasks.run_unix] +command = "${TARGET_DIR}/${PROFILE_DIR}/qemu_tmin-${CARGO_MAKE_PROFILE}" +args = [ + "--input", "./corpus", + "--output", "./output", + "--iterations", "10000", + "--", + "${TARGET_DIR}/libpng-harness-${CARGO_MAKE_PROFILE}", +] +dependencies = [ "harness", "fuzzer" ] + +[tasks.test] +linux_alias = "test_unix" +mac_alias = "unsupported" +windows_alias = "unsupported" + +[tasks.test_unix] +dependencies = [ "all" ] +# Tidy up after we've run our tests so we don't hog all the disk space +command = "cargo" +args = [ + "make", + "clean", +] + +[tasks.clean] +linux_alias = "clean_unix" +mac_alias = "clean_unix" +windows_alias = "unsupported" + +[tasks.clean_unix] +# Disable default `clean` definition +clear = true +script_runner="@shell" +script=''' +rm -rf ${CARGO_MAKE_CRATE_TARGET_DIRECTORY} +cargo clean +''' + +[tasks.arm] +command = "cargo" +args = [ + "make", + "-p", "arm", + "run", +] + +[tasks.aarch64] +command = "cargo" +args = [ + "make", + "-p", "aarch64", + "run", +] + +[tasks.x86_64] +command = "cargo" +args = [ + "make", + "-p", "x86_64", + "run", +] + +[tasks.i386] +command = "cargo" +args = [ + "make", + "-p", "i386", + "run", +] + +[tasks.mips] +command = "cargo" +args = [ + "make", + "-p", "mips", + "run", +] + +[tasks.ppc] +command = "cargo" +args = [ + "make", + "-p", "ppc", + "run", +] + +[tasks.all] +dependencies = [ + "arm", + "aarch64", + "x86_64", + "i386", + "mips", + "ppc" +] diff --git a/fuzzers/qemu_tmin/README.md b/fuzzers/qemu_tmin/README.md new file mode 100644 index 0000000000..992be8ed47 --- /dev/null +++ b/fuzzers/qemu_tmin/README.md @@ -0,0 +1,38 @@ +# qemu_tmin + +This folder contains a test case minimizer using QEMU instrumentation. This fuzzer also distributes the test cases in +the input corupus evenly across the selected cores. + +The following architectures are supported: +* arm +* aarch64 +* i386 +* x86_64 +* mips +* ppc + +## Prerequisites +```bash +sudo apt install \ + gcc-arm-linux-gnueabi \ + g++-arm-linux-gnueabi \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + gcc \ + g++ \ + gcc-mipsel-linux-gnu \ + g++-mipsel-linux-gnu \ + gcc-powerpc-linux-gnu \ + g++-powerpc-linux-gnu +``` + +## Run + +Defaults to `x86_64` architecture +```bash +cargo make run +``` + +```bash +cargo make +``` diff --git a/fuzzers/qemu_tmin/build.rs b/fuzzers/qemu_tmin/build.rs new file mode 100644 index 0000000000..16317b1560 --- /dev/null +++ b/fuzzers/qemu_tmin/build.rs @@ -0,0 +1,47 @@ +use vergen::EmitBuilder; + +#[macro_export] +macro_rules! assert_unique_feature { + () => {}; + ($first:tt $(,$rest:tt)*) => { + $( + #[cfg(all(not(any(doc, feature = "clippy")), feature = $first, feature = $rest))] + compile_error!(concat!("features \"", $first, "\" and \"", $rest, "\" cannot be used together")); + )* + assert_unique_feature!($($rest),*); + } +} + +fn main() { + EmitBuilder::builder() + .all_build() + .all_cargo() + .all_git() + .all_rustc() + .all_sysinfo() + .emit() + .unwrap(); + + assert_unique_feature!("arm", "aarch64", "i386", "x86_64", "mips", "ppc"); + + let cpu_target = if cfg!(feature = "x86_64") { + "x86_64".to_string() + } else if cfg!(feature = "arm") { + "arm".to_string() + } else if cfg!(feature = "aarch64") { + "aarch64".to_string() + } else if cfg!(feature = "i386") { + "i386".to_string() + } else if cfg!(feature = "mips") { + "mips".to_string() + } else if cfg!(feature = "ppc") { + "ppc".to_string() + } else { + println!("cargo:warning=No architecture specified defaulting to x86_64..."); + println!("cargo:rustc-cfg=feature=\"x86_64\""); + println!("cargo:rustc-cfg=feature=\"64bit\""); + "x86_64".to_string() + }; + + println!("cargo:rustc-env=CPU_TARGET={cpu_target}"); +} diff --git a/fuzzers/qemu_tmin/corpus/not_kitty.png b/fuzzers/qemu_tmin/corpus/not_kitty.png new file mode 100644 index 0000000000..eff7c1707b Binary files /dev/null and b/fuzzers/qemu_tmin/corpus/not_kitty.png differ diff --git a/fuzzers/qemu_tmin/corpus/not_kitty_alpha_long.png b/fuzzers/qemu_tmin/corpus/not_kitty_alpha_long.png new file mode 100644 index 0000000000..92134133bf Binary files /dev/null and b/fuzzers/qemu_tmin/corpus/not_kitty_alpha_long.png differ diff --git a/fuzzers/qemu_tmin/corpus/not_kitty_gamma.png b/fuzzers/qemu_tmin/corpus/not_kitty_gamma.png new file mode 100644 index 0000000000..939d9d29a9 Binary files /dev/null and b/fuzzers/qemu_tmin/corpus/not_kitty_gamma.png differ diff --git a/fuzzers/qemu_tmin/corpus/not_kitty_icc.png b/fuzzers/qemu_tmin/corpus/not_kitty_icc.png new file mode 100644 index 0000000000..f0c7804d99 Binary files /dev/null and b/fuzzers/qemu_tmin/corpus/not_kitty_icc.png differ diff --git a/fuzzers/qemu_tmin/harness.cc b/fuzzers/qemu_tmin/harness.cc new file mode 100644 index 0000000000..d9e149d5cb --- /dev/null +++ b/fuzzers/qemu_tmin/harness.cc @@ -0,0 +1,193 @@ +// libpng_read_fuzzer.cc +// Copyright 2017-2018 Glenn Randers-Pehrson +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that may +// be found in the LICENSE file https://cs.chromium.org/chromium/src/LICENSE + +// Last changed in libpng 1.6.35 [July 15, 2018] + +// The modifications in 2017 by Glenn Randers-Pehrson include +// 1. addition of a PNG_CLEANUP macro, +// 2. setting the option to ignore ADLER32 checksums, +// 3. adding "#include " which is needed on some platforms +// to provide memcpy(). +// 4. adding read_end_info() and creating an end_info structure. +// 5. adding calls to png_set_*() transforms commonly used by browsers. + +#include +#include +#include + +#include + +#define PNG_INTERNAL +#include "png.h" + +#define PNG_CLEANUP \ + if (png_handler.png_ptr) { \ + if (png_handler.row_ptr) \ + png_free(png_handler.png_ptr, png_handler.row_ptr); \ + if (png_handler.end_info_ptr) \ + png_destroy_read_struct(&png_handler.png_ptr, &png_handler.info_ptr, \ + &png_handler.end_info_ptr); \ + else if (png_handler.info_ptr) \ + png_destroy_read_struct(&png_handler.png_ptr, &png_handler.info_ptr, \ + nullptr); \ + else \ + png_destroy_read_struct(&png_handler.png_ptr, nullptr, nullptr); \ + png_handler.png_ptr = nullptr; \ + png_handler.row_ptr = nullptr; \ + png_handler.info_ptr = nullptr; \ + png_handler.end_info_ptr = nullptr; \ + } + +struct BufState { + const uint8_t *data; + size_t bytes_left; +}; + +struct PngObjectHandler { + png_infop info_ptr = nullptr; + png_structp png_ptr = nullptr; + png_infop end_info_ptr = nullptr; + png_voidp row_ptr = nullptr; + BufState *buf_state = nullptr; + + ~PngObjectHandler() { + if (row_ptr) { png_free(png_ptr, row_ptr); } + if (end_info_ptr) + png_destroy_read_struct(&png_ptr, &info_ptr, &end_info_ptr); + else if (info_ptr) + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + else + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + delete buf_state; + } +}; + +void user_read_data(png_structp png_ptr, png_bytep data, size_t length) { + BufState *buf_state = static_cast(png_get_io_ptr(png_ptr)); + if (length > buf_state->bytes_left) { png_error(png_ptr, "read error"); } + memcpy(data, buf_state->data, length); + buf_state->bytes_left -= length; + buf_state->data += length; +} + +static const int kPngHeaderSize = 8; + +// Entry point for LibFuzzer. +// Roughly follows the libpng book example: +// http://www.libpng.org/pub/png/book/chapter13.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < kPngHeaderSize) { return 0; } + + std::vector v(data, data + size); + if (png_sig_cmp(v.data(), 0, kPngHeaderSize)) { + // not a PNG. + return 0; + } + + PngObjectHandler png_handler; + png_handler.png_ptr = nullptr; + png_handler.row_ptr = nullptr; + png_handler.info_ptr = nullptr; + png_handler.end_info_ptr = nullptr; + + png_handler.png_ptr = + png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_handler.png_ptr) { return 0; } + + png_handler.info_ptr = png_create_info_struct(png_handler.png_ptr); + if (!png_handler.info_ptr) { + PNG_CLEANUP + return 0; + } + + png_handler.end_info_ptr = png_create_info_struct(png_handler.png_ptr); + if (!png_handler.end_info_ptr) { + PNG_CLEANUP + return 0; + } + + png_set_crc_action(png_handler.png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); +#ifdef PNG_IGNORE_ADLER32 + png_set_option(png_handler.png_ptr, PNG_IGNORE_ADLER32, PNG_OPTION_ON); +#endif + + // Setting up reading from buffer. + png_handler.buf_state = new BufState(); + png_handler.buf_state->data = data + kPngHeaderSize; + png_handler.buf_state->bytes_left = size - kPngHeaderSize; + png_set_read_fn(png_handler.png_ptr, png_handler.buf_state, user_read_data); + png_set_sig_bytes(png_handler.png_ptr, kPngHeaderSize); + + if (setjmp(png_jmpbuf(png_handler.png_ptr))) { + PNG_CLEANUP + return 0; + } + + // Reading. + png_read_info(png_handler.png_ptr, png_handler.info_ptr); + + // reset error handler to put png_deleter into scope. + if (setjmp(png_jmpbuf(png_handler.png_ptr))) { + PNG_CLEANUP + return 0; + } + + png_uint_32 width, height; + int bit_depth, color_type, interlace_type, compression_type; + int filter_type; + + if (!png_get_IHDR(png_handler.png_ptr, png_handler.info_ptr, &width, &height, + &bit_depth, &color_type, &interlace_type, &compression_type, + &filter_type)) { + PNG_CLEANUP + return 0; + } + + // This is going to be too slow. + if (width && height > 100000000 / width) { + PNG_CLEANUP +#ifdef HAS_DUMMY_CRASH + #if defined(__aarch64__) || defined(__arm__) + asm volatile(".word 0xf7f0a000\n"); + #else + asm("ud2"); + #endif +#endif + return 0; + } + + // Set several transforms that browsers typically use: + png_set_gray_to_rgb(png_handler.png_ptr); + png_set_expand(png_handler.png_ptr); + png_set_packing(png_handler.png_ptr); + png_set_scale_16(png_handler.png_ptr); + png_set_tRNS_to_alpha(png_handler.png_ptr); + + int passes = png_set_interlace_handling(png_handler.png_ptr); + + png_read_update_info(png_handler.png_ptr, png_handler.info_ptr); + + png_handler.row_ptr = + png_malloc(png_handler.png_ptr, + png_get_rowbytes(png_handler.png_ptr, png_handler.info_ptr)); + + for (int pass = 0; pass < passes; ++pass) { + for (png_uint_32 y = 0; y < height; ++y) { + png_read_row(png_handler.png_ptr, + static_cast(png_handler.row_ptr), nullptr); + } + } + + png_read_end(png_handler.png_ptr, png_handler.end_info_ptr); + + PNG_CLEANUP + return 0; +} + +int main() { + uint8_t buf[10] = {0}; + LLVMFuzzerTestOneInput(buf, 10); +} diff --git a/fuzzers/qemu_tmin/src/fuzzer.rs b/fuzzers/qemu_tmin/src/fuzzer.rs new file mode 100644 index 0000000000..285b95accc --- /dev/null +++ b/fuzzers/qemu_tmin/src/fuzzer.rs @@ -0,0 +1,295 @@ +//! A libfuzzer-like fuzzer using qemu for binary-only coverage +//! +#[cfg(feature = "i386")] +use core::mem::size_of; +use core::time::Duration; +use std::{env, fs::DirEntry, io, path::PathBuf, ptr::addr_of_mut}; + +use clap::{builder::Str, Parser}; +use libafl::{ + corpus::{CorpusId, InMemoryCorpus, OnDiskCorpus}, + events::{launcher::Launcher, EventConfig, EventRestarter}, + executors::{ExitKind, TimeoutExecutor}, + fuzzer::StdFuzzer, + inputs::{BytesInput, HasTargetBytes}, + monitors::MultiMonitor, + mutators::{havoc_mutations, StdScheduledMutator}, + observers::{HitcountsMapObserver, VariableMapObserver}, + prelude::LlmpRestartingEventManager, + schedulers::QueueScheduler, + stages::{MapEqualityFactory, StagesTuple, StdTMinMutationalStage}, + state::StdState, + Error, +}; +use libafl_bolts::{ + core_affinity::Cores, + current_nanos, + rands::StdRand, + shmem::{ShMemProvider, StdShMemProvider}, + tuples::tuple_list, + AsSlice, +}; +use libafl_qemu::{ + edges::{edges_map_mut_slice, MAX_EDGES_NUM}, + elf::EasyElf, + emu::Emulator, + ArchExtras, CallingConvention, GuestAddr, GuestReg, MmapPerms, QemuEdgeCoverageHelper, + QemuExecutor, QemuHooks, Regs, +}; + +#[derive(Default)] +pub struct Version; + +impl From for Str { + fn from(_: Version) -> Str { + let version = [ + ("Architecture:", env!("CPU_TARGET")), + ("Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP")), + ("Describe:", env!("VERGEN_GIT_DESCRIBE")), + ("Commit SHA:", env!("VERGEN_GIT_SHA")), + ("Commit Date:", env!("VERGEN_RUSTC_COMMIT_DATE")), + ("Commit Branch:", env!("VERGEN_GIT_BRANCH")), + ("Rustc Version:", env!("VERGEN_RUSTC_SEMVER")), + ("Rustc Channel:", env!("VERGEN_RUSTC_CHANNEL")), + ("Rustc Host Triple:", env!("VERGEN_RUSTC_HOST_TRIPLE")), + ("Rustc Commit SHA:", env!("VERGEN_RUSTC_COMMIT_HASH")), + ("Cargo Target Triple", env!("VERGEN_CARGO_TARGET_TRIPLE")), + ] + .iter() + .map(|(k, v)| format!("{k:25}: {v}\n")) + .collect::(); + + format!("\n{version:}").into() + } +} + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[command( + name = format!("qemu_tmin-{}",env!("CPU_TARGET")), + version = Version::default(), + about, + long_about = "Tool for test case minimization using QEMU instrumentation" +)] +pub struct FuzzerOptions { + #[arg(long, help = "Output directory")] + output: String, + + #[arg(long, help = "Input directory")] + input: String, + + #[arg(long, help = "Timeout in seconds", default_value_t = 1_u64)] + timeout: u64, + + #[arg(long = "port", help = "Broker port", default_value_t = 1337_u16)] + port: u16, + + #[arg(long, help = "Cpu cores to use", default_value = "all", value_parser = Cores::from_cmdline)] + cores: Cores, + + #[arg( + long, + help = "Number of iterations for minimization", + default_value_t = 1024_usize + )] + iterations: usize, + + #[clap(short, long, help = "Enable output from the fuzzer clients")] + verbose: bool, + + #[arg(last = true, help = "Arguments passed to the target")] + args: Vec, +} + +pub const MAX_INPUT_SIZE: usize = 1048576; // 1MB + +pub fn fuzz() { + let mut options = FuzzerOptions::parse(); + + let corpus_dir = PathBuf::from(options.input); + + let corpus_files = corpus_dir + .read_dir() + .expect("Failed to read corpus dir") + .collect::, io::Error>>() + .expect("Failed to read dir entry"); + + let num_files = corpus_files.len(); + let num_cores = options.cores.ids.len(); + let files_per_core = (num_files as f64 / num_cores as f64).ceil() as usize; + + let program = env::args().next().unwrap(); + log::debug!("Program: {program:}"); + + options.args.insert(0, program); + log::debug!("ARGS: {:#?}", options.args); + + env::remove_var("LD_LIBRARY_PATH"); + let env: Vec<(String, String)> = env::vars().collect(); + let emu = Emulator::new(&options.args, &env).unwrap(); + + let mut elf_buffer = Vec::new(); + let elf = EasyElf::from_file(emu.binary_path(), &mut elf_buffer).unwrap(); + + let test_one_input_ptr = elf + .resolve_symbol("LLVMFuzzerTestOneInput", emu.load_addr()) + .expect("Symbol LLVMFuzzerTestOneInput not found"); + log::debug!("LLVMFuzzerTestOneInput @ {test_one_input_ptr:#x}"); + + emu.entry_break(test_one_input_ptr); + + for m in emu.mappings() { + log::debug!( + "Mapping: 0x{:016x}-0x{:016x}, {}", + m.start(), + m.end(), + m.path().unwrap_or("") + ); + } + + let pc: GuestReg = emu.read_reg(Regs::Pc).unwrap(); + log::debug!("Break at {pc:#x}"); + + let ret_addr: GuestAddr = emu.read_return_address().unwrap(); + log::debug!("Return address = {ret_addr:#x}"); + + emu.set_breakpoint(ret_addr); + + let input_addr = emu + .map_private(0, MAX_INPUT_SIZE, MmapPerms::ReadWrite) + .unwrap(); + log::debug!("Placing input at {input_addr:#x}"); + + let stack_ptr: GuestAddr = emu.read_reg(Regs::Sp).unwrap(); + + let reset = |buf: &[u8], len: GuestReg| -> Result<(), String> { + unsafe { + emu.write_mem(input_addr, buf); + emu.write_reg(Regs::Pc, test_one_input_ptr)?; + emu.write_reg(Regs::Sp, stack_ptr)?; + emu.write_return_address(ret_addr)?; + emu.write_function_argument(CallingConvention::Cdecl, 0, input_addr)?; + emu.write_function_argument(CallingConvention::Cdecl, 1, len)?; + emu.run(); + Ok(()) + } + }; + + let mut harness = |input: &BytesInput| { + let target = input.target_bytes(); + let mut buf = target.as_slice(); + let mut len = buf.len(); + if len > MAX_INPUT_SIZE { + buf = &buf[0..MAX_INPUT_SIZE]; + len = MAX_INPUT_SIZE; + } + let len = len as GuestReg; + reset(buf, len).unwrap(); + ExitKind::Ok + }; + + let mut run_client = |state: Option<_>, mut mgr: LlmpRestartingEventManager<_, _>, core_id| { + let core_idx = options + .cores + .position(core_id) + .expect("Failed to get core index"); + let files = corpus_files + .iter() + .skip(files_per_core * core_idx) + .take(files_per_core) + .map(|x| x.path()) + .collect::>(); + + if files.is_empty() { + mgr.send_exiting()?; + Err(Error::ShuttingDown)? + } + + let mut state = state.unwrap_or_else(|| { + StdState::new( + StdRand::with_seed(current_nanos()), + OnDiskCorpus::new(PathBuf::from(options.output.clone())).unwrap(), + InMemoryCorpus::new(), + &mut (), + &mut (), + ) + .unwrap() + }); + + let minimizer = StdScheduledMutator::new(havoc_mutations()); + + let edges_observer = unsafe { + HitcountsMapObserver::new(VariableMapObserver::from_mut_slice( + "edges", + edges_map_mut_slice(), + addr_of_mut!(MAX_EDGES_NUM), + )) + }; + + let feedback_factory = MapEqualityFactory::with_observer(&edges_observer); + + let mut stages = tuple_list!(StdTMinMutationalStage::new( + minimizer, + feedback_factory, + options.iterations + )); + + let scheduler = QueueScheduler::new(); + + let mut fuzzer = StdFuzzer::new(scheduler, (), ()); + + let mut hooks = QemuHooks::new(&emu, tuple_list!(QemuEdgeCoverageHelper::default())); + + let executor = QemuExecutor::new( + &mut hooks, + &mut harness, + tuple_list!(edges_observer), + &mut fuzzer, + &mut state, + &mut mgr, + ) + .expect("Failed to create QemuExecutor"); + + let mut executor = TimeoutExecutor::new(executor, Duration::from_secs(options.timeout)); + + state.load_initial_inputs_by_filenames_forced( + &mut fuzzer, + &mut executor, + &mut mgr, + &files, + )?; + + stages.perform_all( + &mut fuzzer, + &mut executor, + &mut state, + &mut mgr, + CorpusId::from(0_usize), + )?; + + log::debug!("Processed {} inputs from disk.", files.len()); + + mgr.send_exiting()?; + Err(Error::ShuttingDown)? + }; + + match Launcher::builder() + .shmem_provider(StdShMemProvider::new().expect("Failed to init shared memory")) + .broker_port(options.port) + .configuration(EventConfig::from_build_id()) + .monitor(MultiMonitor::new(|s| println!("{s}"))) + .run_client(&mut run_client) + .cores(&options.cores) + .stdout_file(if options.verbose { + None + } else { + Some("/dev/null") + }) + .build() + .launch() + { + Ok(()) => (), + Err(Error::ShuttingDown) => println!("Run finished successfully."), + Err(err) => panic!("Failed to run launcher: {err:?}"), + } +} diff --git a/fuzzers/qemu_tmin/src/main.rs b/fuzzers/qemu_tmin/src/main.rs new file mode 100644 index 0000000000..bc1e80f767 --- /dev/null +++ b/fuzzers/qemu_tmin/src/main.rs @@ -0,0 +1,13 @@ +//! A libfuzzer-like fuzzer using qemu for binary-only coverage +#[cfg(target_os = "linux")] +mod fuzzer; + +#[cfg(target_os = "linux")] +pub fn main() { + fuzzer::fuzz(); +} + +#[cfg(not(target_os = "linux"))] +pub fn main() { + panic!("qemu-user and libafl_qemu is only supported on linux!"); +}