diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 86d0785ca5..2d55ff37df 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -199,6 +199,14 @@ jobs: uses: baptiste0928/cargo-install@v1.3.0 with: crate: cargo-make + - name: install wasm-pack + uses: baptiste0928/cargo-install@v1.3.0 + with: + crate: wasm-pack + - name: install chrome + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable - uses: actions/checkout@v3 with: submodules: true # recursively checkout submodules diff --git a/fuzzers/baby_fuzzer_wasm/.cargo/config.toml b/fuzzers/baby_fuzzer_wasm/.cargo/config.toml new file mode 100644 index 0000000000..f4e8c002fc --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/fuzzers/baby_fuzzer_wasm/.gitignore b/fuzzers/baby_fuzzer_wasm/.gitignore new file mode 100644 index 0000000000..6a1ebb73fc --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +wasm-pack.log +.idea/ diff --git a/fuzzers/baby_fuzzer_wasm/Cargo.toml b/fuzzers/baby_fuzzer_wasm/Cargo.toml new file mode 100644 index 0000000000..157469cfd2 --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "baby_fuzzer_wasm" +version = "0.1.0" +authors = ["Addison Crump "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +js-sys = "0.3" +wasm-bindgen = "0.2.63" + +libafl = { path = "../../libafl", default-features = false } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +[dependencies.web-sys] +version = "0.3" +features = ['console', 'Window', 'Performance', 'PerformanceTiming'] + +[dev-dependencies] +wasm-bindgen-test = "0.3.13" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 diff --git a/fuzzers/baby_fuzzer_wasm/Makefile.toml b/fuzzers/baby_fuzzer_wasm/Makefile.toml new file mode 100644 index 0000000000..38324abd36 --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/Makefile.toml @@ -0,0 +1,29 @@ +[env] +FUZZER_NAME="fuzzer" +PROJECT_DIR = { script = ["pwd"] } + +[tasks.unsupported] +script_runner="@shell" +script=''' +echo "Cargo-make not integrated yet on this" +''' + +# Fuzzer +[tasks.build] +command = "wasm-pack" +args = ["build", "--target", "web"] + +# Test +[tasks.test] +linux_alias = "test_unix" +mac_alias = "test_unix" +windows_alias = "unsupported" + +[tasks.test_unix] +command = "wasm-pack" +args = ["test", "--chrome", "--headless"] + +# Clean +[tasks.clean] +command = "cargo" +args = ["clean"] \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_wasm/README.md b/fuzzers/baby_fuzzer_wasm/README.md new file mode 100644 index 0000000000..9dfc4db169 --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/README.md @@ -0,0 +1,12 @@ +# libafl-wasm + +A brief demo demonstrating libafl's compatibility with WASM, and how to do it. + +In this example, the entire LibAFL harness and target are present in a WASM binary, which is then loaded by [the example +webpage](pkg/index.html). To run this example, do `cargo make build`, then open [the example webpage](pkg/index.html) in +your browser (via something like `python3 -m http.server`). The fuzzer will execute until finding a solution and will +write the fuzzer log to your console. + +In a real fuzzing campaign, you would likely need to also create a LibAFL Corpus implementation which was backed by +JavaScript, and restart the fuzzing campaign by re-invoking the fuzzer and providing the associated corpora. This is +not demonstrated in this barebones example. \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_wasm/pkg/.gitignore b/fuzzers/baby_fuzzer_wasm/pkg/.gitignore new file mode 100644 index 0000000000..f59ec20aab --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/pkg/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_wasm/pkg/index.html b/fuzzers/baby_fuzzer_wasm/pkg/index.html new file mode 100644 index 0000000000..f7077f9d6c --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/pkg/index.html @@ -0,0 +1,14 @@ + + + + + libafl_wasm test + + + + + \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_wasm/pkg/package.json b/fuzzers/baby_fuzzer_wasm/pkg/package.json new file mode 100644 index 0000000000..9a87bd8141 --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/pkg/package.json @@ -0,0 +1,15 @@ +{ + "name": "baby_fuzzer_wasm", + "collaborators": [ + "Addison Crump " + ], + "version": "0.1.0", + "files": [ + "baby_fuzzer_wasm_bg.wasm", + "baby_fuzzer_wasm.js", + "baby_fuzzer_wasm.d.ts" + ], + "module": "baby_fuzzer_wasm.js", + "types": "baby_fuzzer_wasm.d.ts", + "sideEffects": false +} \ No newline at end of file diff --git a/fuzzers/baby_fuzzer_wasm/src/lib.rs b/fuzzers/baby_fuzzer_wasm/src/lib.rs new file mode 100644 index 0000000000..30b5c99a96 --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/src/lib.rs @@ -0,0 +1,134 @@ +mod utils; + +use libafl::{ + bolts::{current_nanos, rands::StdRand, tuples::tuple_list, AsSlice}, + corpus::{Corpus, InMemoryCorpus}, + events::SimpleEventManager, + executors::{ExitKind, InProcessExecutor}, + feedbacks::{CrashFeedback, MaxMapFeedback}, + generators::RandPrintablesGenerator, + inputs::{BytesInput, HasTargetBytes}, + monitors::SimpleMonitor, + mutators::{havoc_mutations, StdScheduledMutator}, + observers::StdMapObserver, + schedulers::QueueScheduler, + stages::StdMutationalStage, + state::{HasSolutions, StdState}, + Fuzzer, StdFuzzer, +}; +use wasm_bindgen::prelude::*; +use web_sys::{Performance, Window}; + +use crate::utils::set_panic_hook; + +// defined for internal use by libafl +#[no_mangle] +pub extern "C" fn external_current_millis() -> u64 { + let window: Window = web_sys::window().expect("should be in browser to run this demo"); + let performance: Performance = window + .performance() + .expect("should be in browser to run this demo"); + let now = performance.now() as u64; + now +} + +#[wasm_bindgen] +pub fn fuzz() { + set_panic_hook(); + + let mut signals = [0u8; 64]; + let signals_ptr = signals.as_mut_ptr(); + let signals_set = |i: usize| unsafe { + *signals_ptr.offset(i as isize) += 1; + }; + + // The closure that we want to fuzz + let mut harness = |input: &BytesInput| { + let target = input.target_bytes(); + let buf = target.as_slice(); + signals_set(0); + if !buf.is_empty() && buf[0] == b'a' { + signals_set(1); + if buf.len() > 1 && buf[1] == b'b' { + signals_set(2); + #[allow(clippy::manual_assert)] + if buf.len() > 2 && buf[2] == b'c' { + // WASM cannot handle traps: https://webassembly.github.io/spec/core/intro/overview.html + // in a "real" fuzzing campaign, you should prefer to setup trap handling in JS, + // but we do not do this for demonstration purposes + return ExitKind::Crash; + } + } + } + ExitKind::Ok + }; + + // Create an observation channel using the signals map + let observer = + unsafe { StdMapObserver::from_mut_ptr("signals", signals.as_mut_ptr(), signals.len()) }; + + // Feedback to rate the interestingness of an input + let mut feedback = MaxMapFeedback::new(&observer); + + // A feedback to choose if an input is a solution or not + let mut objective = CrashFeedback::new(); + + // create a State from scratch + let mut state = StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we keep it in memory for performance + InMemoryCorpus::new(), + // In a "real" fuzzing campaign, you should stash solutions in a JS array instead + InMemoryCorpus::new(), + // 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(); + + // The Monitor trait define how the fuzzer stats are reported to the user + let monitor = SimpleMonitor::new(|s| { + web_sys::console::log_1(&s.into()); + }); + + // The event manager handle the various events generated during the fuzzing loop + // such as the notification of the addition of a new item to the corpus + let mut mgr = SimpleEventManager::new(monitor); + + // A queue policy to get testcasess from the corpus + let scheduler = QueueScheduler::new(); + + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + // Create the executor for an in-process function with just one observer + let mut executor = InProcessExecutor::new( + &mut harness, + tuple_list!(observer), + &mut fuzzer, + &mut state, + &mut mgr, + ) + .expect("Failed to create the Executor"); + + // Generator of printable bytearrays of max size 32 + let mut generator = RandPrintablesGenerator::new(32); + + // Generate 8 initial inputs + state + .generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 8) + .expect("Failed to generate the initial corpus"); + + // Setup a mutational stage with a basic bytes mutator + let mutator = StdScheduledMutator::new(havoc_mutations()); + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + + while state.solutions().is_empty() { + fuzzer + .fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr) + .expect("Error in the fuzzing loop"); + } +} diff --git a/fuzzers/baby_fuzzer_wasm/src/utils.rs b/fuzzers/baby_fuzzer_wasm/src/utils.rs new file mode 100644 index 0000000000..b1d7929dc9 --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/fuzzers/baby_fuzzer_wasm/tests/web.rs b/fuzzers/baby_fuzzer_wasm/tests/web.rs new file mode 100644 index 0000000000..9a44961b3f --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/tests/web.rs @@ -0,0 +1,14 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use baby_fuzzer_wasm::fuzz; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn test_fuzz() { + fuzz(); +} diff --git a/fuzzers/baby_fuzzer_wasm/webdriver.json b/fuzzers/baby_fuzzer_wasm/webdriver.json new file mode 100644 index 0000000000..035b45deb5 --- /dev/null +++ b/fuzzers/baby_fuzzer_wasm/webdriver.json @@ -0,0 +1,15 @@ +{ + "moz:firefoxOptions": { + "prefs": { + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true + }, + "args": [] + }, + "goog:chromeOptions": { + "args": [ + "--use-fake-device-for-media-stream", + "--use-fake-ui-for-media-stream" + ] + } +} \ No newline at end of file