From 02cfbba539f0f28cbd8f61c4108fa4bc9ca5a733 Mon Sep 17 00:00:00 2001 From: Jesse Hertz Date: Tue, 25 Oct 2022 16:13:08 -0400 Subject: [PATCH 1/5] initial commit for public contribution. still needs revision, not ready for merging yet --- fuzzers/jif/README.md | 19 + fuzzers/jif/allowlist.txt | 4 + fuzzers/jif/args.gn | 17 + fuzzers/jif/chromium_patches.diff | 78 +++ fuzzers/jif/jif.cc | 594 +++++++++++++++++++++++ fuzzers/jif/libjif/Cargo.toml | 34 ++ fuzzers/jif/libjif/README.md | 4 + fuzzers/jif/libjif/src/bin/libafl_cc.rs | 431 ++++++++++++++++ fuzzers/jif/libjif/src/bin/libafl_cxx.rs | 5 + fuzzers/jif/libjif/src/js.rs | 219 +++++++++ fuzzers/jif/libjif/src/lib.rs | 521 ++++++++++++++++++++ fuzzers/jif/libjif/src/mutators.rs | 350 +++++++++++++ fuzzers/jif/libjif/src/rvf.rs | 61 +++ fuzzers/jif/make.sh | 29 ++ 14 files changed, 2366 insertions(+) create mode 100644 fuzzers/jif/README.md create mode 100644 fuzzers/jif/allowlist.txt create mode 100644 fuzzers/jif/args.gn create mode 100644 fuzzers/jif/chromium_patches.diff create mode 100644 fuzzers/jif/jif.cc create mode 100644 fuzzers/jif/libjif/Cargo.toml create mode 100644 fuzzers/jif/libjif/README.md create mode 100644 fuzzers/jif/libjif/src/bin/libafl_cc.rs create mode 100644 fuzzers/jif/libjif/src/bin/libafl_cxx.rs create mode 100644 fuzzers/jif/libjif/src/js.rs create mode 100644 fuzzers/jif/libjif/src/lib.rs create mode 100644 fuzzers/jif/libjif/src/mutators.rs create mode 100644 fuzzers/jif/libjif/src/rvf.rs create mode 100755 fuzzers/jif/make.sh diff --git a/fuzzers/jif/README.md b/fuzzers/jif/README.md new file mode 100644 index 0000000000..f1597e81c5 --- /dev/null +++ b/fuzzers/jif/README.md @@ -0,0 +1,19 @@ +# JIF - Javascript Injection Fuzzer + +## building + +* setup a chromium repo: https://www.chromium.org/developers/how-tos/get-the-code + * note that this will take several hours +* `mv jif $root/chromium/src/headless/jif` +* `cd $root/chromium/src` +* `python3 tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Upload Mac ASan' out/jif` +* apply patches in chromium_patches.diff +* `cp args.gn $root/chromium/src/out/jif/` (modify this so the fixed path points to your directory!) +* modify fixed path in `libafl_cc.rs` so it points to the correct place +* `./make.sh` (first time will take several hours, after that about 1m) + +## running + +* `cd $root/chromium/src/out/jif` +* `./jif --cores 0-3 --broker-port 1337 --harness harness.js -i corpus -x dict -o out` +* to see arguments, run `./jif --help` \ No newline at end of file diff --git a/fuzzers/jif/allowlist.txt b/fuzzers/jif/allowlist.txt new file mode 100644 index 0000000000..20f8f43821 --- /dev/null +++ b/fuzzers/jif/allowlist.txt @@ -0,0 +1,4 @@ +# Enable instrumentation for a whole folder +src:*/blink/* +# Enable instrumentation for all functions in those files +fun:* \ No newline at end of file diff --git a/fuzzers/jif/args.gn b/fuzzers/jif/args.gn new file mode 100644 index 0000000000..e6de5c5c8f --- /dev/null +++ b/fuzzers/jif/args.gn @@ -0,0 +1,17 @@ +dcheck_always_on = false +enable_nacl = false +ffmpeg_branding = "Chrome" +is_asan = false +is_component_build = true +is_debug = false +optimize_for_fuzzing = true +pdf_enable_xfa = true +proprietary_codecs = true +use_goma = false +use_libfuzzer = false +use_external_fuzzing_engine = true +sanitizer_coverage_flags = "trace-pc-guard,trace-cmp" +clang_base_path = "/Users/jhertz/jif/chromium/src/headless/jif/libjif/llvm/" +clang_use_chrome_plugins = false +mac_deployment_target="10.14.0" +mac_sdk_path="/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" # must be 12.3 or higher, use xcrun --show-sdk-path \ No newline at end of file diff --git a/fuzzers/jif/chromium_patches.diff b/fuzzers/jif/chromium_patches.diff new file mode 100644 index 0000000000..f525f688df --- /dev/null +++ b/fuzzers/jif/chromium_patches.diff @@ -0,0 +1,78 @@ +diff --git a/chrome/BUILD.gn b/chrome/BUILD.gn +index aee9abd99a..dc3beb026e 100644 +--- a/chrome/BUILD.gn ++++ b/chrome/BUILD.gn +@@ -1105,6 +1105,8 @@ if (is_win) { + ":chrome_framework", + ":chrome_framework_create_bundle", + ":chrome_framework_shared_library", ++ "//headless:jif", # JIF ++ "//headless:headless_shared_sources", # JIF + ] + + sources = [ +diff --git a/testing/libfuzzer/unittest_main.cc b/testing/libfuzzer/unittest_main.cc +index 01a7af3253..27f2ec0c5a 100644 +--- a/testing/libfuzzer/unittest_main.cc ++++ b/testing/libfuzzer/unittest_main.cc +@@ -32,7 +32,7 @@ std::vector readFile(std::string path) { + size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize) { + return 0; + } +- ++/** + int main(int argc, char **argv) { + if (argc == 1) { + std::cerr +@@ -57,3 +57,4 @@ int main(int argc, char **argv) { + LLVMFuzzerTestOneInput(v.data(), v.size()); + } + } ++**/ +\ No newline at end of file + +diff --git a/headless/BUILD.gn b/headless/BUILD.gn +index 2a44aec23d..4ea02324d5 100644 +--- a/headless/BUILD.gn ++++ b/headless/BUILD.gn +@@ -18,6 +18,9 @@ import("//tools/grit/grit_rule.gni") + import("//tools/grit/repack.gni") + import("//tools/v8_context_snapshot/v8_context_snapshot.gni") + ++ ++ ++ + if (headless_use_policy) { + assert(headless_use_prefs, + "'headless_use_policy' requires 'headless_use_prefs'.") +@@ -298,6 +301,7 @@ source_set("headless_shared_sources") { + visibility = [ + ":headless_non_renderer", + ":headless_renderer", ++ ":jif", # JIF + ] + defines = [] + +@@ -1063,3 +1067,22 @@ executable("headless_example") { + + configs += [ ":headless_defines_config" ] + } ++ ++ ++# JIF ++ ++import("//testing/libfuzzer/fuzzer_test.gni") ++ ++ fuzzer_test("jif") { ++ sources = [ "jif/jif.cc" ] ++ deps = [ ++ ":headless_shell_lib", ++ "//headless:headless_shared_sources", ++ "//headless:headless_non_renderer", ++ "//content", ++ "//sandbox", ++ "//skia", # we need this to override font render hinting in headless build ++ "//ui/gfx/geometry", ++ ] ++ libs = ["jif/libjif/target/release/libjif.a"] ++ } \ No newline at end of file diff --git a/fuzzers/jif/jif.cc b/fuzzers/jif/jif.cc new file mode 100644 index 0000000000..418db05325 --- /dev/null +++ b/fuzzers/jif/jif.cc @@ -0,0 +1,594 @@ +// jif.cc +// Heavily based off headless_example.cc from chromium project: https://chromium.googlesource.com/chromium/src/+/master/headless/app/headless_example.cc +// Original copyright notice follows: + +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This is the C++ Executor +// It provides three external C functions: +// LLVMFuzzerTestOneInput - the main entry point for the fuzzer +// LLVMFuzzerInitialize - called once before any other fuzzer function +// get_js_coverage - called to get the JS coverage data + +#include +#include +#include +#include +#include +#include + +#include + +#include "base/bind.h" +#include "base/base64.h" +#include "base/command_line.h" +#include "base/memory/weak_ptr.h" +#include "base/task/thread_pool.h" +#include "base/values.h" +#include "build/build_config.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "headless/public/devtools/domains/page.h" +#include "headless/public/devtools/domains/runtime.h" +#include "headless/public/devtools/domains/profiler.h" +#include "headless/public/headless_browser.h" +#include "headless/public/headless_devtools_client.h" +#include "headless/public/headless_devtools_target.h" +#include "headless/public/headless_web_contents.h" + +#include "ui/gfx/geometry/size.h" +#include "v8/include/v8.h" + +bool VERBOSE_MODE = std::getenv("JIF_VERBOSE") != nullptr; +size_t MAX_TESTCASE_LENGTH = 1024 * 10; + + +// This class contains the main application logic, i.e., waiting for a page to +// load and printing its DOM. Note that browser initialization happens outside +// this class. +class JIF : public headless::HeadlessWebContents::Observer, + public headless::page::Observer { + public: + JIF(headless::HeadlessBrowser* browser, + headless::HeadlessWebContents* web_contents); + ~JIF() override; + + // headless::HeadlessWebContents::Observer implementation: + void DevToolsTargetReady() override; + + // headless::page::Observer implementation: + void OnLoadEventFired( + const headless::page::LoadEventFiredParams& params) override; + + // Tip: Observe headless::inspector::ExperimentalObserver::OnTargetCrashed to + // be notified of renderer crashes. + + + void OnDomFetched(std::unique_ptr result); + + void OnJavascriptDialogOpening( + const headless::page::JavascriptDialogOpeningParams& params) override; + + // The headless browser instance. Owned by the headless library. See main(). + headless::HeadlessBrowser* browser_; + // Our tab. Owned by |browser_|. + headless::HeadlessWebContents* web_contents_; + // The DevTools client used to control the tab. + std::unique_ptr devtools_client_; + std::unique_ptr devtools_client_js; + // A helper for creating weak pointers to this class. + base::WeakPtrFactory weak_factory_{this}; +}; + +namespace { +JIF* jif_global = NULL; +std::mutex headless_startup_mutex; +std::mutex devtools_ready_mutex; +std::mutex page_loaded_mutex; +std::mutex js_load_mutex; +std::mutex harness_done_mutex; +std::mutex precise_coverage_started_mutex; +std::mutex precise_coverage_completed_mutex; +std::mutex precise_coverage_stopped_mutex; +std::condition_variable precise_coverage_started_cv; +std::condition_variable precise_coverage_completed_cv; +std::condition_variable precise_coverage_stopped_cv; +std::condition_variable headless_startup_cv; +std::condition_variable devtools_ready_cv; +std::condition_variable page_loaded_cv; +std::condition_variable js_load_cv; +std::condition_variable harness_done_cv; +std::string processed_test_case = ""; +headless::HeadlessWebContents* web_contents_js = nullptr; +scoped_refptr task_runner = nullptr; + +bool alert_called = false; +bool initialized = false; + +char* js_coverage = NULL; + +} + + +JIF::JIF(headless::HeadlessBrowser* browser, + headless::HeadlessWebContents* web_contents) + : browser_(browser), + web_contents_(web_contents), + devtools_client_(headless::HeadlessDevToolsClient::Create()), + devtools_client_js(headless::HeadlessDevToolsClient::Create()) { + web_contents_->AddObserver(this); +} + +JIF::~JIF() { + // Note that we shut down the browser last, because it owns objects such as + // the web contents which can no longer be accessed after the browser is gone. + devtools_client_->GetPage()->RemoveObserver(this); + web_contents_->GetDevToolsTarget()->DetachClient(devtools_client_.get()); + web_contents_->RemoveObserver(this); + browser_->Shutdown(); +} + +std::string base64_encode(std::string input){ + std::string output; + std::vector bytes(input.begin(), input.end()); + return base::Base64Encode(bytes); +} + +// This method is called when the tab is ready for DevTools inspection. +void JIF::DevToolsTargetReady() { + + printf("DevTools Target Ready\n"); + + // lock the mutex and set jif_global, then notify the condition variable + std::lock_guard lk(devtools_ready_mutex); + + // Attach our DevTools client to the tab so that we can send commands to it + // and observe events. + web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); + web_contents_js->GetDevToolsTarget()->AttachClient(devtools_client_js.get()); + + // Start observing events from DevTools's page domain. This lets us get + // notified when the page has finished loading. Note that it is possible + // the page has already finished loading by now. See + // HeadlessShell::DevToolTargetReady for how to handle that case correctly. + devtools_client_->GetPage()->AddObserver(this); + devtools_client_->GetPage()->Enable(); + + devtools_ready_cv.notify_all(); +} + +void JIF::OnLoadEventFired( + const headless::page::LoadEventFiredParams& params) { + + if(VERBOSE_MODE) { + printf("Loaded. DOM:\n"); + + devtools_client_->GetRuntime()->Evaluate( + "(document.doctype ? new " + "XMLSerializer().serializeToString(document.doctype) + '\\n' : '') + " + "document.documentElement.outerHTML", + base::BindOnce(&JIF::OnDomFetched, + weak_factory_.GetWeakPtr())); + } + + page_loaded_cv.notify_all(); + + +} + +void JIF::OnJavascriptDialogOpening(const headless::page::JavascriptDialogOpeningParams& params){ + printf("Javascript Dialog Opening\n"); + devtools_client_->GetPage()->HandleJavaScriptDialog(true); + + alert_called = true; +} + +void JIF::OnDomFetched( + std::unique_ptr result) { + // Make sure the evaluation succeeded before reading the result. + if (result->HasExceptionDetails()) { + LOG(ERROR) << "Failed to serialize document: " + << result->GetExceptionDetails()->GetText(); + } else { + printf("%s\n", result->GetResult()->GetValue()->GetString().c_str()); + } +} + + + +void OnNavigation(std::unique_ptr result) { + +} + +void LoadURL(std::string url) { + + // Navigate to the given URL. + auto page = jif_global->devtools_client_->GetPage(); + CHECK(page); + page->Navigate(url, base::BindOnce(&OnNavigation)); + // onNavigation should unlock the mutex and notify the condition variable +} + +// This function is called by the headless library after the browser has been +// initialized. It runs on the UI thread. +void OnHeadlessBrowserStarted(headless::HeadlessBrowser* browser) { + + + + printf("starting browser!\n"); + // In order to open tabs, we first need a browser context. It corresponds to a + // user profile and contains things like the user's cookies, local storage, + // cache, etc. + headless::HeadlessBrowserContext::Builder context_builder = + browser->CreateBrowserContextBuilder(); + + // Here we can set options for the browser context. As an example we enable + // incognito mode, which makes sure profile data is not written to disk. + context_builder.SetIncognitoMode(true); + + // set locale info to en-us + context_builder.SetAcceptLanguage("en-us"); + + // Construct the context and set it as the default. The default browser + // context is used by the Target.createTarget() DevTools command when no other + // context is given. + headless::HeadlessBrowserContext* browser_context = context_builder.Build(); + browser->SetDefaultBrowserContext(browser_context); + + + // Open a tab (i.e., HeadlessWebContents) in the newly created browser + // context. + headless::HeadlessWebContents::Builder tab_builder( + browser_context->CreateWebContentsBuilder()); + + // We can set options for the opened tab here. In this example we are just + // setting the initial URL to navigate to. + tab_builder.SetInitialURL(GURL("about:blank")); + + // Create an instance of the example app, which will wait for the page to load + // and print its DOM. + headless::HeadlessWebContents* web_contents = tab_builder.Build(); + + //second webcontents for inputs + web_contents_js = tab_builder.Build(); + + + // lock the mutex and set jif_global, then notify the condition variable + std::lock_guard lk(headless_startup_mutex); + jif_global = new JIF(browser, web_contents); + + headless_startup_cv.notify_all(); + + +} + +// convert a string to a data:text/html url where the input string is the base64 encoded content +// of the html file +std::string stringToDataUrl(std::string html_string) { + std::string data_url = "data:text/html;base64,"; + // convert html_string to a span + data_url.append(base64_encode(html_string)); + return data_url; +} + +//TODO: exception handling lol +std::string ReadFile(std::string path){ + std::ifstream t(path); + std::stringstream buffer; + buffer << t.rdbuf(); + return buffer.str(); +} + + + + +// OnHarnessLoaded +void OnHarnessLoaded(std::unique_ptr result) { + // Make sure the evaluation succeeded before reading the result. + if (result->HasExceptionDetails()) { + LOG(FATAL) << "Failed to evaluate function: " + << result->GetExceptionDetails()->GetText(); + } else { + printf("Harness Loaded\n"); + } + js_load_cv.notify_all(); +} + +// loadJSFunctionUsingEvaluate +// This function is called by the harness to load a javascript function into the page +// using the Evaluate command. +void LoadHarness(std::string function) { + + printf("Entering load harness\n"); + + + CHECK(!initialized); + + //base64 encode the function + std::string encoded_function = base64_encode(function); + + // Evaluate the given function in the page. + + // can we just Evaluate the string? + jif_global->devtools_client_js->GetRuntime()->Evaluate( + "eval(atob('" + encoded_function + "'))", + base::BindOnce(&OnHarnessLoaded)); + + // block until the function is loaded + std::unique_lock lk(js_load_mutex); + js_load_cv.wait(lk); + + printf("Harness Loaded\n"); +} + +// OnHarnessFinished is called when the harness finishes. +void OnHarnessFinished(std::unique_ptr result) { + // Make sure the evaluation succeeded before reading the result. + if (result->HasExceptionDetails()) { + LOG(WARNING) << " Evaluation failed: " + << result->GetExceptionDetails()->GetText(); + processed_test_case = ""; + } else { + if(result->GetResult()->HasValue()){ + processed_test_case = result->GetResult()->GetValue()->GetString(); + } + else{ + LOG(FATAL) << "Harness did not return a value, curious"; + } + } + + harness_done_cv.notify_all(); +} + +void OnPreciseCoverageStarted(std::unique_ptr result) { + precise_coverage_started_cv.notify_all(); +} + +extern "C" const char* get_js_coverage(){ + if(js_coverage != NULL) + return (const char*) js_coverage; + return ""; +} + +void OnPreciseCoverageCompleted(std::unique_ptr result) { + if(result){ + std::ostringstream os; + os << *(result->Serialize()); + std::string json = os.str(); + //if(VERBOSE_MODE) + // std::cout << json << std::endl; + // save the coverage to the global + CHECK(js_coverage == NULL); + auto buffer = (char*)malloc(json.length() + 1); //please do not provide a coverage string of length INT_MAX + CHECK(buffer); + strcpy(buffer, json.c_str()); + js_coverage = buffer; + } else{ + js_coverage = NULL; + } + precise_coverage_completed_cv.notify_all(); +} + +void OnPreciseCoverageStopped(void) { + precise_coverage_stopped_cv.notify_all(); +} + +std::string RunTestCaseThroughHarness(std::string test_case){ + + if(js_coverage != NULL){ + free(js_coverage); + js_coverage = NULL; + } + + // strip any non-printable characters from the test case: + std::string ascii_test_case = ""; + for(int i = 0; i < test_case.length(); i++){ + if(isprint(test_case[i])){ + ascii_test_case.push_back(test_case[i]); + } + } + + //base64 encode the test case + std::string encoded_test_case = base64_encode(ascii_test_case); + std::string payload = "harness(atob('" + encoded_test_case + "'));"; + + + jif_global->devtools_client_js->GetProfiler()->Enable(); + + jif_global->devtools_client_js->GetProfiler()->StartPreciseCoverage( + // use builder to set options + headless::profiler::StartPreciseCoverageParams::Builder() + .SetCallCount(true) + .SetDetailed(true) + .Build(), + base::BindOnce(&OnPreciseCoverageStarted)); + + // wait for the precise coverage to start + std::unique_lock lk(precise_coverage_started_mutex); + precise_coverage_started_cv.wait(lk); + + // run test case through harness + + processed_test_case = ""; + + //printf("running through eval: %s\n", payload.c_str()); + jif_global->devtools_client_js->GetRuntime()->Evaluate( + payload, + base::BindOnce(base::BindOnce(&OnHarnessFinished))); + + // wait for the harness to finish + std::unique_lock lk2(harness_done_mutex); + harness_done_cv.wait(lk2); + + if(processed_test_case == ""){ + if(VERBOSE_MODE){ + printf("Harness returned the empty string. The triggering input was: %s\n", encoded_test_case.c_str()); + } + return ""; + } + + + + // take precise coverage + jif_global->devtools_client_js->GetProfiler()->TakePreciseCoverage( + base::BindOnce(&OnPreciseCoverageCompleted)); + + // wait for the precise coverage to finish + std::unique_lock lk3(precise_coverage_completed_mutex); + precise_coverage_completed_cv.wait(lk3); + + // BUG: if we stop precise coverage, all future calls to start precise coverage fail + // so we never do! + + return processed_test_case; +} + + +// initialize signal logic +// TODO: do we only need this bc we're using a timeout executor? +void InitializeSignals(){ + // mask off SIGALARM + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGALRM); + sigprocmask(SIG_BLOCK, &mask, NULL); + +} + + +// LibFuzzer initilization function (LLVMInitialize) +extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) { + + CHECK(!initialized); + + // disable JIT + static const char kJitless[] = "--jitless"; + v8::V8::SetFlagsFromString(kJitless); + + // Create a headless browser instance. There can be one of these per process + // and it can only be initialized once. + headless::HeadlessBrowser::Options::Builder builder(*argc, (const char **) *argv); + + + // Here you can customize browser options. + builder.SetWindowSize(gfx::Size(800, 600)); + builder.SetSingleProcessMode(true); + //disable the GPU (not sure how to do this) + //builder.SetGLImplementation("desktop-gl-core-profile"); + builder.SetDisableSandbox(true); + + // Pass control to the headless library. It will bring up the browser and + // invoke the given callback on the browser UI thread. Note: if you need to + // pass more parameters to the callback, you can add them to the Bind() call + // below. + + printf("starting headless main\n"); + + //create a new std::thread which calls HeadlessBrowserMain + std::thread headless_browser_main_thread(headless::HeadlessBrowserMain, + builder.Build(), base::BindOnce(&OnHeadlessBrowserStarted) ); + + headless_browser_main_thread.detach(); + + printf("blocking on headless_startup_mutex\n"); + + // wait on the condition variable headless_startup_cv until the headless_browser_main_thread + // is ready and has set jif_global to a valid JIF instance + std::unique_lock lk(headless_startup_mutex); + headless_startup_cv.wait(lk, []{return jif_global != nullptr;}); + + printf("woke up. blocking on devtools_ready_mutex\n"); + + // okay that worked, now we have to wait until DevTools is ready, same deal + // as above + std::unique_lock lk2(devtools_ready_mutex); + devtools_ready_cv.wait(lk2); + + //finally set up the task runner + task_runner = content::GetUIThreadTaskRunner({}); // doesnt seem to make a perf difference whether we use UI or IO + + + // initialize the harness + // read the harness from argv + std::string harness_file = ""; + // argv will contain "--harness ABC.js" + // we want to read the ABC.js file + for(int i = 0; i < *argc; i++){ + if(strcmp((*argv)[i], "--harness") == 0){ + harness_file = (*argv)[i+1]; //TODO: bounds checking + break; + } + } + + //TODO: real error checking/handling + + CHECK(harness_file != ""); + + std::string harness = ReadFile(harness_file); + CHECK(harness != ""); + + LoadHarness(harness); + + //initialize signals + InitializeSignals(); + + initialized = true; + printf("done with init\n"); + + return 0; +} + + + + +// Entrypoint for LibFuzzer/LibAFL +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + + + CHECK(initialized); + alert_called = false; + + // make sure we have a valid example, not just a nullptr + if(!size || !data) + return 0; + if(VERBOSE_MODE) + printf("Pre-Harness: %s\n", data); + + //check length here + + // run the test case through the harness + std::string result = RunTestCaseThroughHarness(std::string(reinterpret_cast(data), size)); + + + if(VERBOSE_MODE) + printf("Post-Harness: %s\n", result.c_str()); + + // create data string URI from the processed data + std::string url = stringToDataUrl(result); + + if(url.length() > MAX_TESTCASE_LENGTH){ + if(VERBOSE_MODE) printf("Testcase too long skipping. Encoded length %lu. Original: %s\n",url.length(), data); + return 0; + } + // load the url on the task runner + + task_runner->PostTask(FROM_HERE, + base::BindOnce(&LoadURL, url)); + + // wait on mutex and condition variable page_loaded_mutex and page_loaded_cv + std::unique_lock lk2(page_loaded_mutex); + page_loaded_cv.wait(lk2); + + // okay, check the global variable to see if alert was called + if(alert_called){ + printf("[!] XSS DETECTED: WE CALLED ALERT\n"); + printf("[!] XSS Payload: %s\n", data); + return 42; // this is the magic number for JIF to stop fuzzing + } + + + return 0; +} + diff --git a/fuzzers/jif/libjif/Cargo.toml b/fuzzers/jif/libjif/Cargo.toml new file mode 100644 index 0000000000..c2e91a3bcc --- /dev/null +++ b/fuzzers/jif/libjif/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "libjif" +version = "0.0.1" +authors = ["jhertz"] +edition = "2021" + +[profile.release] +lto = true +codegen-units = 1 +opt-level = 3 +debug = true + +[build-dependencies] +cc = { version = "1.0", features = ["parallel"] } +which = { version = "4.0.2" } +num_cpus = "1.0" + +[dependencies] + +# previous rev: 7b345 0fe81 +libafl = { path = "../../../libafl", features = ["std", "derive", "llmp_compression", "rand_trait", "cmin", "prelude"], default-features = false } +libafl_targets = { path = "../../../libafl_targets", features = ["sancov_pcguard_hitcounts", "sancov_cmplog", "libfuzzer"] } + +libafl_cc = { path = "../../../libafl_cc"} + +mimalloc = { version = "*", default-features = false } +structopt = "0.3.25" +serde_json = "1.0.83" +serde = "1.0.143" +atomic-counter = "1.0.1" + +[lib] +name = "jif" +crate-type = ["staticlib"] diff --git a/fuzzers/jif/libjif/README.md b/fuzzers/jif/libjif/README.md new file mode 100644 index 0000000000..5710bc9212 --- /dev/null +++ b/fuzzers/jif/libjif/README.md @@ -0,0 +1,4 @@ +# Overview + +LibJif is the Rust component of JIF, based heavily off StdFuzzer: https://github.com/AFLplusplus/StdFuzzer +It also sources a custom version of libafl_cc, which is already in this repository \ No newline at end of file diff --git a/fuzzers/jif/libjif/src/bin/libafl_cc.rs b/fuzzers/jif/libjif/src/bin/libafl_cc.rs new file mode 100644 index 0000000000..63cb951315 --- /dev/null +++ b/fuzzers/jif/libjif/src/bin/libafl_cc.rs @@ -0,0 +1,431 @@ +//! Uses LLVM compiler Wrapper from `LibAFL` + +use libafl_cc::{ + clang::{CLANGXX_PATH, CLANG_PATH, OUT_DIR}, + CompilerWrapper, Error, LLVMPasses, LIB_EXT, LIB_PREFIX, +}; + +use std::{ + convert::Into, + env, + path::{Path, PathBuf}, + string::String, + vec::Vec, +}; + +/// Wrap Clang +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug)] +pub struct ClangWrapper { + is_silent: bool, + optimize: bool, + wrapped_cc: String, + wrapped_cxx: String, + + name: String, + is_cpp: bool, + linking: bool, + x_set: bool, + bit_mode: u32, + need_libafl_arg: bool, + has_libafl_arg: bool, + + parse_args_called: bool, + base_args: Vec, + cc_args: Vec, + link_args: Vec, + passes: Vec, + passes_args: Vec, +} + +#[allow(clippy::match_same_arms)] // for the linking = false wip for "shared" +#[allow(clippy::too_many_lines)] +#[allow(clippy::case_sensitive_file_extension_comparisons)] +impl CompilerWrapper for ClangWrapper { + fn parse_args(&mut self, args: &[S]) -> Result<&'_ mut Self, Error> + where + S: AsRef, + { + let mut new_args: Vec = vec![]; + if args.is_empty() { + return Err(Error::InvalidArguments( + "The number of arguments cannot be 0".to_string(), + )); + } + + if self.parse_args_called { + return Err(Error::Unknown( + "CompilerWrapper::parse_args cannot be called twice on the same instance" + .to_string(), + )); + } + self.parse_args_called = true; + + if args.len() == 1 { + return Err(Error::InvalidArguments( + "LibAFL Compiler wrapper - no commands specified. Use me as compiler.".to_string(), + )); + } + + self.name = args[0].as_ref().to_string(); + // Detect C++ compiler looking at the wrapper name + self.is_cpp = self.is_cpp || self.name.ends_with("++"); + + // Sancov flag + // new_args.push("-fsanitize-coverage=trace-pc-guard".into()); + + let mut linking = true; + // Detect stray -v calls from ./configure scripts. + if args.len() > 1 && args[1].as_ref() == "-v" { + linking = false; + } + + let mut suppress_linking = 0; + + for arg in &args[1..] { + //TODO: refactor this into a match guard + if arg.as_ref().starts_with('@') + && arg.as_ref().ends_with(".rsp") + && arg.as_ref() != "@./jif.rsp" + { + // we are linking! + suppress_linking += 1; + self.has_libafl_arg = true; + } + match arg.as_ref() { + //XXX: refactor this + "-Wl,--no-call-graph-profile-sort" | "-Wl,-u,__sanitizer_options_link_helper" => { + continue; + } + //"-Wunknown-warning-option" | "-Werror,-Wunknown-warning-option" => { + "-Werror" => { + continue; + } + "-Wl,-dead_strip" => { + continue; + } + "--libafl-no-link" => { + suppress_linking += 1; + self.has_libafl_arg = true; + continue; + } + "--libafl" => { + suppress_linking += 1337; + self.has_libafl_arg = true; + continue; + } + "-fsanitize=fuzzer-no-link" => { + suppress_linking += 1; + self.has_libafl_arg = true; + continue; + } + "-fsanitize=fuzzer" => { + suppress_linking += 1337; + self.has_libafl_arg = true; + continue; + } + "-x" => self.x_set = true, + "-m32" => self.bit_mode = 32, + "-m64" => self.bit_mode = 64, + "-c" | "-S" | "-E" => linking = false, + "-shared" => { + linking = false; // TODO dynamic list? + new_args.push("-undefined".into()); + new_args.push("dynamic_lookup".into()); + } + "-Wl,-z,defs" | "-Wl,--no-undefined" | "--no-undefined" => continue, + _ => (), + }; + + new_args.push(arg.as_ref().to_string()); + } + + //println!("{:?}", suppress_linking); + + if linking && suppress_linking > 0 && suppress_linking < 1337 { + linking = false; + println!("adding no-link-rt"); + new_args.push("-force_load".into()); + new_args.push( + PathBuf::from(OUT_DIR) + .join(format!("{}no-link-rt.{}", LIB_PREFIX, LIB_EXT)) + .into_os_string() + .into_string() + .unwrap(), + ); + + new_args.push("-undefined".into()); + new_args.push("dynamic_lookup".into()); + } + + self.linking = linking; + + if self.optimize { + new_args.push("-g".into()); + new_args.push("-O3".into()); + new_args.push("-funroll-loops".into()); + } + + // Fuzzing define common among tools + new_args.push("-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1".into()); + + // Libraries needed by libafl on Windows + #[cfg(windows)] + if linking { + new_args.push("-lws2_32".into()); + new_args.push("-lBcrypt".into()); + new_args.push("-lAdvapi32".into()); + } + // required by timer API (timer_create, timer_settime) + #[cfg(target_os = "linux")] + if linking { + new_args.push("-lrt".into()); + } + // MacOS has odd linker behavior sometimes + #[cfg(target_vendor = "apple")] + if linking { + new_args.push("-undefined".into()); + new_args.push("dynamic_lookup".into()); + } + + self.base_args = new_args; + Ok(self) + } + + fn add_arg(&mut self, arg: S) -> &'_ mut Self + where + S: AsRef, + { + self.base_args.push(arg.as_ref().to_string()); + self + } + + fn add_cc_arg(&mut self, arg: S) -> &'_ mut Self + where + S: AsRef, + { + self.cc_args.push(arg.as_ref().to_string()); + self + } + + fn add_link_arg(&mut self, arg: S) -> &'_ mut Self + where + S: AsRef, + { + self.link_args.push(arg.as_ref().to_string()); + self + } + + fn link_staticlib(&mut self, dir: &Path, name: S) -> &'_ mut Self + where + S: AsRef, + { + if cfg!(target_vendor = "apple") { + //self.add_link_arg("-force_load".into())?; + } else { + self.add_link_arg("-Wl,--whole-archive"); + } + self.add_link_arg( + dir.join(format!("{}{}.{}", LIB_PREFIX, name.as_ref(), LIB_EXT)) + .into_os_string() + .into_string() + .unwrap(), + ); + if cfg!(target_vendor = "apple") { + self + } else { + self.add_link_arg("-Wl,-no-whole-archive") + } + } + + fn command(&mut self) -> Result, Error> { + let mut args = vec![]; + if self.is_cpp { + args.push(self.wrapped_cxx.clone()); + } else { + args.push(self.wrapped_cc.clone()); + } + args.extend_from_slice(self.base_args.as_slice()); + if self.need_libafl_arg && !self.has_libafl_arg { + return Ok(args); + } + + if !self.passes.is_empty() { + args.push("-fno-experimental-new-pass-manager".into()); + } + for pass in &self.passes { + args.push("-Xclang".into()); + args.push("-load".into()); + args.push("-Xclang".into()); + args.push(pass.path().into_os_string().into_string().unwrap()); + } + for passes_arg in &self.passes_args { + args.push("-mllvm".into()); + args.push(passes_arg.into()); + } + if self.linking { + if self.x_set { + args.push("-x".into()); + args.push("none".into()); + } + + args.extend_from_slice(self.link_args.as_slice()); + + if cfg!(unix) { + args.push("-pthread".into()); + args.push("-ldl".into()); + } + } else { + args.extend_from_slice(self.cc_args.as_slice()); + } + + Ok(args) + } + + fn is_linking(&self) -> bool { + self.linking + } + + fn silence(&mut self, value: bool) -> &'_ mut Self { + self.is_silent = value; + self + } + + fn is_silent(&self) -> bool { + self.is_silent + } +} + +impl Default for ClangWrapper { + /// Create a new Clang Wrapper + #[must_use] + fn default() -> Self { + Self::new() + } +} + +impl ClangWrapper { + /// Create a new Clang Wrapper + #[must_use] + pub fn new() -> Self { + Self { + optimize: true, + wrapped_cc: CLANG_PATH.into(), + wrapped_cxx: CLANGXX_PATH.into(), + name: "".into(), + is_cpp: false, + linking: false, + x_set: false, + bit_mode: 0, + need_libafl_arg: false, + has_libafl_arg: false, + parse_args_called: false, + base_args: vec![], + cc_args: vec![], + link_args: vec![], + passes: vec![], + passes_args: vec![], + is_silent: false, + } + } + + /// Sets the wrapped `cc` compiler + pub fn wrapped_cc(&mut self, cc: String) -> &'_ mut Self { + self.wrapped_cc = cc; + self + } + + /// Sets the wrapped `cxx` compiler + pub fn wrapped_cxx(&mut self, cxx: String) -> &'_ mut Self { + self.wrapped_cxx = cxx; + self + } + + /// Disable optimizations + pub fn dont_optimize(&mut self) -> &'_ mut Self { + self.optimize = false; + self + } + + /// Set cpp mode + pub fn cpp(&mut self, value: bool) -> &'_ mut Self { + self.is_cpp = value; + self + } + + /// Add LLVM pass + pub fn add_pass(&mut self, pass: LLVMPasses) -> &'_ mut Self { + self.passes.push(pass); + self + } + + /// Add LLVM pass arguments + pub fn add_passes_arg(&mut self, arg: S) -> &'_ mut Self + where + S: AsRef, + { + self.passes_args.push(arg.as_ref().to_string()); + self + } + + /// Set if linking + pub fn linking(&mut self, value: bool) -> &'_ mut Self { + self.linking = value; + self + } + + /// Set if it needs the --libafl arg to add the custom arguments to clang + pub fn need_libafl_arg(&mut self, value: bool) -> &'_ mut Self { + self.need_libafl_arg = value; + self + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_clang_version() { + if let Err(res) = ClangWrapper::new() + .parse_args(&["my-clang", "-v"]) + .unwrap() + .run() + { + println!("Ignored error {:?} - clang is probably not installed.", res); + } + } +} + +pub fn main() { + let args: Vec = 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" | "ng" => false, + "++" | "pp" | "xx" => true, + _ => panic!("Could not figure out if c or c++ wrapper was called. Expected {:?} to end with c or cxx", dir), + }; + + 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") + .add_arg("-fsanitize-coverage=trace-pc-guard,trace-cmp") + // TODO: write the allowlist to a file in /tmp and pass it to the compiler wrapper here + .add_arg("-fsanitize-coverage-allowlist=/Users/jhertz/jif/chromium/src/headless/jif/allowlist.txt") + .add_pass(LLVMPasses::CmpLogRtn) + .run() + .expect("Failed to run the wrapped compiler") + { + std::process::exit(code); + } + } else { + panic!("LibAFL CC: No Arguments given"); + } +} diff --git a/fuzzers/jif/libjif/src/bin/libafl_cxx.rs b/fuzzers/jif/libjif/src/bin/libafl_cxx.rs new file mode 100644 index 0000000000..dabd22971a --- /dev/null +++ b/fuzzers/jif/libjif/src/bin/libafl_cxx.rs @@ -0,0 +1,5 @@ +pub mod libafl_cc; + +fn main() { + libafl_cc::main(); +} diff --git a/fuzzers/jif/libjif/src/js.rs b/fuzzers/jif/libjif/src/js.rs new file mode 100644 index 0000000000..f8c9390c0d --- /dev/null +++ b/fuzzers/jif/libjif/src/js.rs @@ -0,0 +1,219 @@ +use libafl::bolts::tuples::Named; +use libafl::events::EventFirer; +use libafl::executors::ExitKind; +use libafl::feedbacks::Feedback; +use libafl::inputs::Input; +use libafl::observers::Observer; +use libafl::observers::ObserversTuple; +use libafl::state::HasClientPerfMonitor; +use libafl::state::HasMetadata; +use libafl::state::HasNamedMetadata; +use libafl::Error; +use libafl::SerdeAny; +use serde::{Deserialize, Serialize}; + +use std::cmp::max; +use std::ffi::CStr; +use std::os::raw::c_char; + +// This module contains everything needed to do parse chrome's JS "block coverage" +// and provide an Oberserver and Feedback for LibAFL + +#[derive(Debug, Serialize, Deserialize)] +pub struct JSObserver { + name: String, + js_coverage: String, +} + +extern "C" { + fn get_js_coverage() -> *const c_char; +} + +impl Observer for JSObserver { + fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + // we don't currently do much here? + Ok(()) + } + + fn post_exec( + &mut self, + _state: &mut S, + _input: &I, + _exit_kind: &ExitKind, + ) -> Result<(), Error> { + unsafe { + let js_coverage = get_js_coverage(); + if js_coverage.is_null() { + self.js_coverage = "".to_string(); + } else { + self.js_coverage = CStr::from_ptr(js_coverage).to_string_lossy().into_owned(); + } + } + Ok(()) + } +} + +impl Named for JSObserver { + fn name(&self) -> &str { + &self.name + } +} + +impl JSObserver { + pub fn new(name: &str) -> Self { + Self { + name: name.to_owned(), + js_coverage: String::new(), + } + } +} + +#[derive(Debug)] +pub struct JSFeedback { + name: String, +} + +impl Feedback + for JSFeedback +{ + fn is_interesting( + &mut self, + state: &mut S, + _manager: &mut EM, + _input: &I, + observers: &OT, + _exit_kind: &ExitKind, + ) -> Result + where + EM: EventFirer, + OT: ObserversTuple, + { + let observer = observers.match_name::(&self.name).unwrap(); + let novel = state + .named_metadata_mut() + .get_mut::(&self.name) + .unwrap() + .add_coverage(&observer.js_coverage); + Ok(novel) + } + + fn init_state(&mut self, state: &mut S) -> Result<(), Error> { + state.add_named_metadata(JSMapState::new(&self.name), &self.name); + Ok(()) + } +} + +impl JSFeedback { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + } + } +} + +impl Named for JSFeedback { + fn name(&self) -> &str { + &self.name + } +} + +#[derive(Debug, Serialize, Deserialize, SerdeAny)] +pub struct JSMapState { + name: String, + coverage_map: Vec, + current_map: Vec, +} + +impl Named for JSMapState { + fn name(&self) -> &str { + &self.name + } +} + +impl JSMapState { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + coverage_map: Vec::new(), + current_map: Vec::new(), + } + } + + // add_coverage all the coverage from a JSON string into the current map + // it returns true if the current map is then novel (along with merging the maps) + pub fn add_coverage(&mut self, coverage: &str) -> bool { + if coverage.is_empty() { + return false; + } + self.current_map = Vec::::new(); + let json: serde_json::Value = serde_json::from_str(coverage).unwrap(); + for v in json["result"].as_array().unwrap().iter() { + for f in v["functions"].as_array().unwrap().iter() { + if f["isBlockCoverage"].as_bool().unwrap() { + for r in f["ranges"].as_array().unwrap().iter() { + // r has three properties we're interested in, start end and count + let count = r["count"].as_u64().unwrap(); + let end = r["endOffset"].as_u64().unwrap(); + let start = r["startOffset"].as_u64().unwrap(); + self.add_range_to_current( + start.try_into().unwrap(), + end.try_into().unwrap(), + count, + ); + } + } + } + } + let novel = compare_maps(&mut self.current_map, &mut self.coverage_map); + if novel { + println!("found a novel input!"); + self.coverage_map = merge_maps(&mut self.coverage_map, &mut self.current_map); + } + novel + } + + // adds the range to the current map, including blanking part of it if the count is 0 + // also implements wraparound to 1 addition + pub fn add_range_to_current(&mut self, start: usize, end: usize, count: u64) { + if self.current_map.len() < end { + self.current_map.resize(end, 0); + } + + for i in start..end { + if count == 0 { + self.current_map[i] = 0; + } else { + self.current_map[i] = self.current_map[i].wrapping_add((count % 255) as u8); + if self.current_map[i] == 0 { + self.current_map[i] = 1; + } + } + } + } +} + +pub fn merge_maps(coverage_map: &mut Vec, current_map: &mut Vec) -> Vec { + let size = max(coverage_map.len(), current_map.len()); + let mut merged_map = Vec::new(); + merged_map.resize(size, 0); + coverage_map.resize(size, 0); + current_map.resize(size, 0); + for i in 0..size { + merged_map[i] = max(coverage_map[i], current_map[i]); + } + merged_map +} + +// returns true if the current map is novel compared to the coverage map +pub fn compare_maps(current_map: &mut Vec, coverage_map: &mut Vec) -> bool { + if current_map.len() > coverage_map.len() { + return true; + } + current_map.resize(coverage_map.len(), 0); + for i in 0..current_map.len() { + if current_map[i] > coverage_map[i] { + return true; + } + } + false +} diff --git a/fuzzers/jif/libjif/src/lib.rs b/fuzzers/jif/libjif/src/lib.rs new file mode 100644 index 0000000000..b0c5354e16 --- /dev/null +++ b/fuzzers/jif/libjif/src/lib.rs @@ -0,0 +1,521 @@ +// LibJIF provides a rust static library which can get linked into chrome (or another browser that meets +// the contract of LibJIF) and implements a feedback driven XSS fuzzer using grimoire and cmplog. + +use mimalloc::MiMalloc; +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +use core::time::Duration; +use libafl::events::EventRestarter; +use libafl::prelude::Cores; +use libafl::prelude::GeneralizationStage; +use libafl::prelude::GeneralizedInput; +use libafl::prelude::SkippableStage; +use libafl::prelude::LlmpRestartingEventManager; +use libafl::Evaluator; +use libafl::prelude::TokenInsert; +use libafl::{ + bolts::{ + current_nanos, + launcher::Launcher, + rands::StdRand, + shmem::{ShMemProvider, StdShMemProvider}, + tuples::tuple_list, + AsSlice, + }, + corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus}, + events::EventConfig, + executors::{inprocess::InProcessExecutor, ExitKind, TimeoutExecutor}, + feedback_or, + feedbacks::MaxMapFeedback, + fuzzer::{Fuzzer, StdFuzzer}, + inputs::HasTargetBytes, + monitors::MultiMonitor, + mutators::mutations::{ + BytesDeleteMutator, CrossoverInsertMutator, CrossoverReplaceMutator, SpliceMutator, + }, + mutators::{ + scheduled::StdScheduledMutator, + token_mutations::{I2SRandReplace, Tokens}, + GrimoireExtensionMutator, GrimoireRandomDeleteMutator, GrimoireRecursiveReplacementMutator, + GrimoireStringReplacementMutator, + }, + observers::{HitcountsMapObserver, StdMapObserver, TimeObserver}, + schedulers::QueueScheduler, + stages::{calibrate::CalibrationStage, StdMutationalStage, TracingStage}, + state::{HasCorpus, HasMaxSize, HasMetadata, StdState}, + Error, +}; +use std::{env, fs, io::Read, net::SocketAddr, path::PathBuf}; +use structopt::StructOpt; + +use libafl_targets::{ + libfuzzer_initialize, libfuzzer_test_one_input, CmpLogObserver, CMPLOG_MAP, EDGES_MAP, + MAX_EDGES_NUM, +}; + +mod js; +mod mutators; +mod rvf; +use crate::js::JSFeedback; +use crate::js::JSMapState; +use crate::js::JSObserver; +use crate::rvf::ReturnValueFeedback; +use mutators::TagCopyMutator; +use mutators::TagCrossoverMutator; +use mutators::TagDeleteMutator; +use mutators::TagTokenMutator; + +use atomic_counter::AtomicCounter; +use atomic_counter::RelaxedCounter; + +const NUM_ITERATIONS: usize = 10_000; + +/// Parses a millseconds int into a [`Duration`], used for commandline arg parsing +fn timeout_from_millis_str(time: &str) -> Result { + Ok(Duration::from_millis(time.parse()?)) +} + +#[derive(Debug, StructOpt)] +#[structopt( + name = "jif", + about = "JIF: Javascript Injection Fuzzer", + author = "jhertz" +)] +struct Opt { + #[structopt( + short, + long, + parse(try_from_str = Cores::from_cmdline), + help = "Spawn a client in each of the provided cores. Broker runs in the 0th core. 'all' to select all available cores. 'none' to run a client without binding to any core. eg: '1,2-4,6' selects the cores 1,2,3,4,6.", + name = "CORES" + )] + cores: Cores, + + #[structopt( + short = "p", + long, + help = "Choose the broker TCP port, default is 1337", + name = "PORT" + )] + broker_port: u16, + + #[structopt( + parse(try_from_str), + short = "a", + long, + help = "Specify a remote broker", + name = "REMOTE" + )] + remote_broker_addr: Option, + + #[structopt( + parse(try_from_str), + short, + long, + help = "Set an initial corpus directory", + name = "INPUT" + )] + input: PathBuf, + + #[structopt( + short, + long, + parse(try_from_str), + help = "Set the output directory, default is ./out", + name = "OUTPUT", + default_value = "./out" + )] + output: PathBuf, + + #[structopt( + help = "Path for the JS file with the harness to run inputs through", + name = "HARNESS", + long = "harness", + parse(from_os_str) + )] + harness: PathBuf, + + #[structopt( + parse(try_from_str = timeout_from_millis_str), + short, + long, + help = "Set the exeucution timeout in milliseconds, default is 1000", + name = "TIMEOUT", + default_value = "1000" + )] + timeout: Duration, + + #[structopt( + parse(from_os_str), + short = "x", + long, + help = "Feed the fuzzer with an user-specified list of tokens (often called \"dictionary\"", + name = "TOKENS", + multiple = true + )] + tokens: Vec, + + #[structopt( + help = "File to run instead of doing fuzzing loop", + name = "REPRO", + long = "repro", + parse(from_os_str) + )] + repro_file: Option, + + // several new flags, -g for grimoire -b for bytes -t for tags + #[structopt( + help = "Use grimoire mutator", + name = "GRIMOIRE", + long = "grimoire", + short = "g" + )] + grimoire: bool, + + #[structopt( + help = "Use bytes mutator", + name = "BYTES", + long = "bytes", + short = "b" + )] + bytes: bool, + + #[structopt( + help = "Use tags mutator", + name = "TAGS", + long = "tags", + short = "t" + )] + tags: bool, + + #[structopt( + help = "Use cmplog mutator", + name = "CMPLOG", + long = "cmplog", + short = "c" + )] + cmplog: bool, +} + + +/// The main fn, `no_mangle` as it is a C symbol +#[allow(clippy::too_many_lines)] +#[no_mangle] +pub extern "C" fn main() { + let _args: Vec = env::args().collect(); + let workdir = env::current_dir().unwrap(); + let opt = Opt::from_args(); + let cores = opt.cores; + let broker_port = opt.broker_port; + let remote_broker_addr = opt.remote_broker_addr; + let input_dir = opt.input; + let output_dir = opt.output; + let token_files = opt.tokens; + let timeout_ms = opt.timeout; + let repro_file = opt.repro_file; + + let use_grimoire = opt.grimoire; + let use_bytes = opt.bytes; + let use_tags = opt.tags; + let use_cmplog = opt.cmplog; + + if !use_grimoire && !use_bytes && !use_tags && !use_cmplog { + panic!("Must specify at least one mutator"); + } + + println!("Workdir: {:?}", workdir.to_string_lossy().to_string()); + + let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory"); + + let monitor = MultiMonitor::new(|s| println!("{}", s)); + + let iteration_counter = RelaxedCounter::new(0); + + let mut run_client = |state: Option>, + mut mgr: LlmpRestartingEventManager<_, _, _, _>, + _core_id| { + let repro_file = repro_file.clone(); + + // Create an observation channel using the coverage map + let edges = unsafe { &mut EDGES_MAP[0..MAX_EDGES_NUM] }; + let edges_observer = HitcountsMapObserver::new(StdMapObserver::new("edges", edges)); + + // Create an observation channel to keep track of the execution time + let time_observer = TimeObserver::new("time"); + + // Create the Cmp observer + let cmplog = unsafe { &mut CMPLOG_MAP }; + let cmplog_observer = CmpLogObserver::new("cmplog", cmplog, true); + + // feedback from js + let js_observer = JSObserver::new("js"); + let js_feedback = JSFeedback::new("js"); + let _js_mapstate = JSMapState::new("js"); + + // Feedback to rate the interestingness of an input + let mut feedback = feedback_or!( + MaxMapFeedback::new_tracking(&edges_observer, true, true), + js_feedback + ); + + // A feedback to choose if an input is a solution or not + + let mut objective = ReturnValueFeedback::new(); + + let mut crashdir = output_dir.clone(); + crashdir.push("solutions"); + let mut corpdir = output_dir.clone(); + corpdir.push("corpus"); + + let generalization = GeneralizationStage::new(&edges_observer); //TODO: investigate using a multimapobserver + let generalization = SkippableStage::new(generalization, |_s| { + use_grimoire.into() + }); + let mut state = match state { + Some(state) => state, + None => StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we use a cached disk version + CachedOnDiskCorpus::new(corpdir, 40_000).unwrap(), + // Corpus in which we store solutions (XSS) + OnDiskCorpus::new(crashdir).unwrap(), + // States of the feedbacks. + // They are the data related to the feedbacks that you want to persist in the State. + &mut feedback, + &mut objective, + ) + .unwrap(), + }; + + //set max size of a test case to 1024 + state.set_max_size(1024); + + // Create a dictionary if not existing + if state.metadata().get::().is_none() { + for tokens_file in &token_files { + state.add_metadata(Tokens::from_file(tokens_file)?); + } + } + + // The actual target run starts here. + // Call LLVMFUzzerInitialize() if present. + let args: Vec = env::args().collect(); + assert_ne!( + libfuzzer_initialize(&args), + -1, + "Error: LLVMFuzzerInitialize failed with -1" + ); + + // if repro_file isnt empty, read the file to a buf + // and then run the input through libfuzzer_test_one_input + // and return + if let Some(repro_file) = repro_file { + println!("Running repro file: {:?}", repro_file); + let repro_file = repro_file.to_str().unwrap(); + let repro_buf = fs::read(repro_file).unwrap(); + println!("Repro file size: {}", repro_buf.len()); + if libfuzzer_test_one_input(&repro_buf) == 42 { + println!("XSS Detected in repro file"); + } + println!("Done running repro file, exiting"); + return Ok(()); + } + + let max_map_feedback = MaxMapFeedback::new_tracking(&edges_observer, true, false); + let calibration = CalibrationStage::new(&max_map_feedback); + + // Setup a randomic Input2State stage + let i2s = + SkippableStage::new( + StdMutationalStage::new(StdScheduledMutator::new(tuple_list!(I2SRandReplace::new()))), + |_s| { + use_cmplog.into() + }, + ); + + // mutations + let byte_mutations = tuple_list!( + SpliceMutator::new(), + CrossoverInsertMutator::new(), + CrossoverReplaceMutator::new(), + BytesDeleteMutator::new(), + BytesDeleteMutator::new(), + BytesDeleteMutator::new(), + BytesDeleteMutator::new(), + BytesDeleteMutator::new(), + TokenInsert::new(), + ); + + let tag_mutations = tuple_list!( + TagCopyMutator::new(), + TagDeleteMutator::new(), + TagDeleteMutator::new(), + TagDeleteMutator::new(), + TagDeleteMutator::new(), + TagCrossoverMutator::new(), + TagTokenMutator::new(), + ); + + // note: right now if you dont use byte or tag mutations, you wont get any default tokens inserted? + let grimoire_mutations = StdScheduledMutator::new(tuple_list!( + GrimoireExtensionMutator::new(), + GrimoireRecursiveReplacementMutator::new(), + GrimoireStringReplacementMutator::new(), + // give more probability to avoid large inputs + GrimoireRandomDeleteMutator::new(), + GrimoireRandomDeleteMutator::new(), + )); + + + let byte_mutational_stage = SkippableStage::new( + StdMutationalStage::new(StdScheduledMutator::new(byte_mutations)), + |_s| { + use_bytes.into() + } + ); + let tag_mutational_stage = SkippableStage::new( + StdMutationalStage::new(StdScheduledMutator::new(tag_mutations)), + |_s| { + use_tags.into() + } + ); + + let grim_mutational_stage = SkippableStage::new( + StdMutationalStage::new(grimoire_mutations), + |_s| { + use_grimoire.into() + } + ); + + // A minimization+queue policy to get testcases from the corpus + let scheduler = QueueScheduler::new(); + + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + // The wrapped harness function, calling out to the LLVM-style harness + let mut harness = |input: &GeneralizedInput| { + iteration_counter.inc(); + let target = input.target_bytes(); + let buf = target.as_slice(); + let rv = libfuzzer_test_one_input(buf); + if rv == 42 { + return ExitKind::Oom; + } // for now, use OOM to mean XSS, this is a hack + ExitKind::Ok + }; + + + // TODO: try without timeout executor + // Create the executor for an in-process function with one observer for edge coverage and one for the execution time + let mut executor = TimeoutExecutor::new( + InProcessExecutor::new::, _, _>( + &mut harness, + tuple_list!(edges_observer, time_observer, js_observer), + &mut fuzzer, + &mut state, + &mut mgr, + )?, + timeout_ms, + ); + + // Secondary harness due to mut ownership + let mut harness = |input: &GeneralizedInput| { + iteration_counter.inc(); + let target = input.target_bytes(); + let buf = target.as_slice(); + let rv = libfuzzer_test_one_input(buf); + if rv == 42 { + return ExitKind::Oom; + } // for now, use OOM to mean XSS, this is a hack + ExitKind::Ok + }; + + + // Setup a tracing stage in which we log comparisons + let tracing = SkippableStage::new(TracingStage::new(InProcessExecutor::new( + &mut harness, + tuple_list!(cmplog_observer), + &mut fuzzer, + &mut state, + &mut mgr, + )?), |_s| { + use_cmplog.into() + }); + + + + // The order of the stages matter! + let mut stages = tuple_list!( + calibration, + tracing, + i2s, + byte_mutational_stage, + tag_mutational_stage, + generalization, // should this be in a different position? + grim_mutational_stage, + ); + + // In case the corpus is empty (on first run), reset + if state.corpus().count() < 1 { + println!("Loading from {:?}", &input_dir); + let mut initial_inputs = vec![]; + for entry in fs::read_dir(input_dir.as_path()).unwrap() { + let path = entry.unwrap().path(); + let attr = fs::metadata(&path); + if attr.is_err() { + continue; + } + let attr = attr.unwrap(); + + if attr.is_file() && attr.len() > 0 { + println!("Loading file {:?} ...", &path); + let mut file = fs::File::open(path).expect("no file found"); + let mut buffer = vec![]; + file.read_to_end(&mut buffer).expect("buffer overflow"); + let input = GeneralizedInput::new(buffer); + initial_inputs.push(input); + } + } + assert!( + !initial_inputs.is_empty(), + "Failed to load any inputs from {:?}", + &input_dir + ); + + for input in initial_inputs { + fuzzer + .evaluate_input(&mut state, &mut executor, &mut mgr, input) + .unwrap(); + } + } + + // run the fuzzer for NUM_ITERATIONS + while iteration_counter.get() < NUM_ITERATIONS { + fuzzer.fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr)?; + } + + println!("restarting fuzzer"); + // save state + mgr.on_restart(&mut state)?; + std::process::exit(0); + }; + + match Launcher::builder() + .shmem_provider(shmem_provider) + .configuration(EventConfig::from_name("default")) + .monitor(monitor) + .run_client(&mut run_client) + .cores(&cores) + .broker_port(broker_port) + .remote_broker_addr(remote_broker_addr) + .stdout_file(Some("/dev/null")) + .build() + .launch() + { + Ok(_) | Err(Error::ShuttingDown) => (), + Err(e) => panic!("{:?}", e), + }; +} diff --git a/fuzzers/jif/libjif/src/mutators.rs b/fuzzers/jif/libjif/src/mutators.rs new file mode 100644 index 0000000000..f889b57ac3 --- /dev/null +++ b/fuzzers/jif/libjif/src/mutators.rs @@ -0,0 +1,350 @@ +// TODO: comments on this file + +use libafl::{ + bolts::{rands::Rand, tuples::Named}, + corpus::Corpus, + inputs::{HasBytesVec, Input}, + mutators::{MutationResult, Mutator}, + prelude::Tokens, + state::{HasCorpus, HasMetadata, HasRand}, + Error, +}; + +/// Bytes delete mutation for inputs with a bytes vector +#[derive(Default, Debug)] +pub struct TagDeleteMutator; + +impl Mutator for TagDeleteMutator +where + I: Input + HasBytesVec, + S: HasRand, +{ + fn mutate( + &mut self, + state: &mut S, + input: &mut I, + _stage_idx: i32, + ) -> Result { + let size = input.bytes().len(); + if size <= 2 { + return Ok(MutationResult::Skipped); + } + + // we want to delete a tag, so we find offsets for < and > and delete one tag + // first, collect all the offsets for < and > + let mut starts = Vec::new(); + let mut ends = Vec::new(); + for (i, b) in input.bytes().iter().enumerate() { + if *b == b'<' { + starts.push(i); + } else if *b == b'>' { + ends.push(i); + } + } + + if starts.is_empty() || ends.is_empty() { + return Ok(MutationResult::Skipped); + } + // pick an offset randomly + let idx: usize = state.rand_mut().below(starts.len().try_into().unwrap()) as usize; + let start = starts[idx]; + // bounds check + if idx >= ends.len() { + return Ok(MutationResult::Skipped); + } + let end = ends[idx]; + + if start > end || start > input.bytes().len() || end > input.bytes().len() { + return Ok(MutationResult::Skipped); + } + + //println!("original input: {}", String::from_utf8_lossy(input.bytes())); + input.bytes_mut().drain(start..=end); + //println!("trimmed input: {}", String::from_utf8_lossy(input.bytes())); + + Ok(MutationResult::Mutated) + } +} + +impl Named for TagDeleteMutator { + fn name(&self) -> &str { + "TagDeleteMutator" + } +} + +impl TagDeleteMutator { + /// Creates a new [`BytesDeleteMutator`]. + #[must_use] + pub fn new() -> Self { + Self + } +} + +// TagExpandMutator +// chooses a random tag within the test case and inserts it to a random location in the buffer +// that is after a > + +/// Bytes delete mutation for inputs with a bytes vector +#[derive(Default, Debug)] +pub struct TagCopyMutator; + +impl Mutator for TagCopyMutator +where + I: Input + HasBytesVec, + S: HasRand, +{ + fn mutate( + &mut self, + state: &mut S, + input: &mut I, + _stage_idx: i32, + ) -> Result { + let size = input.bytes().len(); + if size <= 2 { + return Ok(MutationResult::Skipped); + } + + // we want to copy a tag to + // first, collect all the offsets for < and > + let mut starts = Vec::new(); + let mut ends = Vec::new(); + for (i, b) in input.bytes().iter().enumerate() { + if *b == b'<' { + starts.push(i); + } else if *b == b'>' { + ends.push(i); + } + } + + if starts.is_empty() || ends.is_empty() { + return Ok(MutationResult::Skipped); + } + + // pick an offset randomly + let idx: usize = state.rand_mut().below(starts.len().try_into().unwrap()) as usize; + let start = starts[idx]; + // bounds check + if idx >= ends.len() { + return Ok(MutationResult::Skipped); + } + let end = ends[idx]; + + if start > end || start > input.bytes().len() || end > input.bytes().len() { + return Ok(MutationResult::Skipped); + } + let the_tag = input.bytes()[start..=end].to_vec(); + + // now we need to find a > to insert the tag after + let insertion_point = + ends[state.rand_mut().below(ends.len().try_into().unwrap()) as usize] + 1; + + //println!("original input: {}", String::from_utf8_lossy(input.bytes())); + // splice the tag in at insertion_point + input + .bytes_mut() + .splice(insertion_point..insertion_point, the_tag); + //println!("expanded input: {}", String::from_utf8_lossy(input.bytes())); + Ok(MutationResult::Mutated) + } +} + +impl Named for TagCopyMutator { + fn name(&self) -> &str { + "TagCopyMutator" + } +} + +impl TagCopyMutator { + /// Creates a new [`TagCopyMutator`]. + #[must_use] + pub fn new() -> Self { + Self + } +} + +// TagCrossoverMutator +// chooses a random tag within the test case and inserts it to a random location in the buffer +// that is after a > + +/// Bytes delete mutation for inputs with a bytes vector +#[derive(Default, Debug)] +pub struct TagCrossoverMutator; + +impl Mutator for TagCrossoverMutator +where + I: Input + HasBytesVec, + S: HasRand + HasCorpus, +{ + fn mutate( + &mut self, + state: &mut S, + input: &mut I, + _stage_idx: i32, + ) -> Result { + let size = input.bytes().len(); + if size <= 2 { + return Ok(MutationResult::Skipped); + } + + // we want to copy a tag to + // first, collect all the offsets for < and > + let mut starts = Vec::new(); + let mut ends = Vec::new(); + for (i, b) in input.bytes().iter().enumerate() { + if *b == b'<' { + starts.push(i); + } else if *b == b'>' { + ends.push(i); + } + } + + if starts.is_empty() || ends.is_empty() { + return Ok(MutationResult::Skipped); + } + + // this should be a tag from a diff input + + // We don't want to use the testcase we're already using for splicing + let count = state.corpus().count(); + let idx = state.rand_mut().below(count as u64) as usize; + if let Some(cur) = state.corpus().current() { + if idx == *cur { + return Ok(MutationResult::Skipped); + } + } + let mut other_testcase = state.corpus().get(idx)?.borrow_mut().clone(); + let other_bytes = other_testcase.load_input()?.bytes(); + + // pick a tag in other_bytes + let mut other_starts = Vec::new(); + let mut other_ends = Vec::new(); + for (i, b) in other_bytes.iter().enumerate() { + if *b == b'<' { + other_starts.push(i); + } else if *b == b'>' { + other_ends.push(i); + } + } + + if other_starts.is_empty() || other_ends.is_empty() { + return Ok(MutationResult::Skipped); + } + + // pick an offset randomly + let the_rand = state.rand_mut(); + let idx: usize = the_rand.below(other_starts.len().try_into().unwrap()) as usize; + let start = other_starts[idx]; + // bounds check + if idx >= other_ends.len() { + return Ok(MutationResult::Skipped); + } + let end = other_ends[idx]; + + if start > end || start > other_bytes.len() || end > other_bytes.len() { + return Ok(MutationResult::Skipped); + } + let the_tag = other_bytes[start..=end].to_vec(); + + // now we need to find a > to insert the tag after + let insertion_point = ends[the_rand.below(ends.len().try_into().unwrap()) as usize] + 1; + + // splice the tag in at insertion_point + //println!("original input: {}", String::from_utf8_lossy(input.bytes())); + input + .bytes_mut() + .splice(insertion_point..insertion_point, the_tag); + //println!("expanded input: {}", String::from_utf8_lossy(input.bytes())); + Ok(MutationResult::Mutated) + } +} + +impl Named for TagCrossoverMutator { + fn name(&self) -> &str { + "TagCrossoverMutator" + } +} + +impl TagCrossoverMutator { + /// Creates a new [`TagCopyMutator`]. + #[must_use] + pub fn new() -> Self { + Self + } +} + +// TagTokenMutator +// chooses a random token and inserts it to a random location in the buffer +// that is after a > +// idk what will happen if the token is not a tag + +/// Bytes delete mutation for inputs with a bytes vector +#[derive(Default, Debug)] +pub struct TagTokenMutator; + +impl Mutator for TagTokenMutator +where + I: Input + HasBytesVec, + S: HasRand + HasCorpus + HasMetadata, +{ + fn mutate( + &mut self, + state: &mut S, + input: &mut I, + _stage_idx: i32, + ) -> Result { + let size = input.bytes().len(); + if size <= 2 { + return Ok(MutationResult::Skipped); + } + + // we want to copy a tag to + // first, collect all the offsets for < and > + let mut starts = Vec::new(); + let mut ends = Vec::new(); + for (i, b) in input.bytes().iter().enumerate() { + if *b == b'<' { + starts.push(i); + } else if *b == b'>' { + ends.push(i); + } + } + + if starts.is_empty() || ends.is_empty() { + return Ok(MutationResult::Skipped); + } + + let tokens = state.metadata().clone(); + let tokens = tokens.get::(); + if tokens.is_none() { + return Ok(MutationResult::Skipped); + } + let tokens = tokens.unwrap(); + let the_token = + tokens.tokens()[state.rand_mut().below(tokens.len() as u64) as usize].clone(); + + // now we need to find a > to insert the tag after + let insertion_point = ends[state.rand_mut().below(ends.len() as u64) as usize] + 1; + + // splice the tag in at insertion_point + //println!("original input: {}", String::from_utf8_lossy(input.bytes())); + input + .bytes_mut() + .splice(insertion_point..insertion_point, the_token); + //println!("expanded input: {}", String::from_utf8_lossy(input.bytes())); + Ok(MutationResult::Mutated) + } +} + +impl Named for TagTokenMutator { + fn name(&self) -> &str { + "TagTokenMutator" + } +} + +impl TagTokenMutator { + /// Creates a new [`TagTokenMutator`]. + #[must_use] + pub fn new() -> Self { + Self + } +} diff --git a/fuzzers/jif/libjif/src/rvf.rs b/fuzzers/jif/libjif/src/rvf.rs new file mode 100644 index 0000000000..5f2841b53f --- /dev/null +++ b/fuzzers/jif/libjif/src/rvf.rs @@ -0,0 +1,61 @@ +use libafl::prelude::EventFirer; +use libafl::prelude::ExitKind; +use libafl::prelude::Feedback; +use libafl::prelude::HasClientPerfMonitor; +use libafl::prelude::Input; +use libafl::prelude::Named; +use libafl::prelude::ObserversTuple; +use libafl::Error; +use serde::{Deserialize, Serialize}; + +/// A [`ReturnValueFeedback`] reports as interesting if `LLVMTestOneInput == 42` +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ReturnValueFeedback {} + +impl Feedback for ReturnValueFeedback +where + I: Input, + S: HasClientPerfMonitor, +{ + #[allow(clippy::wrong_self_convention)] + fn is_interesting( + &mut self, + _state: &mut S, + _manager: &mut EM, + _input: &I, + _observers: &OT, + exit_kind: &ExitKind, + ) -> Result + where + EM: EventFirer, + OT: ObserversTuple, + { + if let ExitKind::Oom = exit_kind { + //HACK: we need to add a new ExitKind for XSS + Ok(true) + } else { + Ok(false) + } + } +} + +impl Named for ReturnValueFeedback { + #[inline] + fn name(&self) -> &str { + "ReturnValueFeedback" + } +} + +impl ReturnValueFeedback { + /// Creates a new [`ReturnValueFeedback`] + #[must_use] + pub fn new() -> Self { + Self {} + } +} + +impl Default for ReturnValueFeedback { + fn default() -> Self { + Self::new() + } +} diff --git a/fuzzers/jif/make.sh b/fuzzers/jif/make.sh new file mode 100755 index 0000000000..52d2048fcd --- /dev/null +++ b/fuzzers/jif/make.sh @@ -0,0 +1,29 @@ +set -e + +export MACOSX_DEPLOYMENT_TARGET=10.14.0 +export LIBAFL_EDGES_MAP_SIZE=3000000 + #8888888 + +CLANG_FOLDER=`llvm-config --bindir` + +pushd libjif +cargo build --release + +echo "[+] Setting up libAFL LLVM environment" + +# if llvm dir doesnt exist, create it and copy stuff in +if [ ! -d "llvm" ]; then + echo "[+] Creating LLVM dir" + mkdir llvm + cp -r $CLANG_FOLDER/../* llvm +fi + +# copy the libafl cc's in place to build chrome +cp target/release/libafl_cc llvm/bin/clang +cp target/release/libafl_cxx llvm/bin/clang++ +popd +# build jif +ninja -v -C ../../out/jif jif + + + From f4cefec329254aba439c99fea52754e6a679c789 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 17 Nov 2022 11:16:59 +0100 Subject: [PATCH 2/5] fmt --- fuzzers/jif/libjif/src/bin/libafl_cc.rs | 10 +-- fuzzers/jif/libjif/src/js.rs | 28 +++--- fuzzers/jif/libjif/src/lib.rs | 111 +++++++++--------------- fuzzers/jif/libjif/src/rvf.rs | 12 +-- 4 files changed, 62 insertions(+), 99 deletions(-) diff --git a/fuzzers/jif/libjif/src/bin/libafl_cc.rs b/fuzzers/jif/libjif/src/bin/libafl_cc.rs index 63cb951315..6db336f25e 100644 --- a/fuzzers/jif/libjif/src/bin/libafl_cc.rs +++ b/fuzzers/jif/libjif/src/bin/libafl_cc.rs @@ -1,10 +1,5 @@ //! Uses LLVM compiler Wrapper from `LibAFL` -use libafl_cc::{ - clang::{CLANGXX_PATH, CLANG_PATH, OUT_DIR}, - CompilerWrapper, Error, LLVMPasses, LIB_EXT, LIB_PREFIX, -}; - use std::{ convert::Into, env, @@ -13,6 +8,11 @@ use std::{ vec::Vec, }; +use libafl_cc::{ + clang::{CLANGXX_PATH, CLANG_PATH, OUT_DIR}, + CompilerWrapper, Error, LLVMPasses, LIB_EXT, LIB_PREFIX, +}; + /// Wrap Clang #[allow(clippy::struct_excessive_bools)] #[derive(Debug)] diff --git a/fuzzers/jif/libjif/src/js.rs b/fuzzers/jif/libjif/src/js.rs index f8c9390c0d..946db04116 100644 --- a/fuzzers/jif/libjif/src/js.rs +++ b/fuzzers/jif/libjif/src/js.rs @@ -1,21 +1,17 @@ -use libafl::bolts::tuples::Named; -use libafl::events::EventFirer; -use libafl::executors::ExitKind; -use libafl::feedbacks::Feedback; -use libafl::inputs::Input; -use libafl::observers::Observer; -use libafl::observers::ObserversTuple; -use libafl::state::HasClientPerfMonitor; -use libafl::state::HasMetadata; -use libafl::state::HasNamedMetadata; -use libafl::Error; -use libafl::SerdeAny; +use std::{cmp::max, ffi::CStr, os::raw::c_char}; + +use libafl::{ + bolts::tuples::Named, + events::EventFirer, + executors::ExitKind, + feedbacks::Feedback, + inputs::Input, + observers::{Observer, ObserversTuple}, + state::{HasClientPerfMonitor, HasMetadata, HasNamedMetadata}, + Error, SerdeAny, +}; use serde::{Deserialize, Serialize}; -use std::cmp::max; -use std::ffi::CStr; -use std::os::raw::c_char; - // This module contains everything needed to do parse chrome's JS "block coverage" // and provide an Oberserver and Feedback for LibAFL diff --git a/fuzzers/jif/libjif/src/lib.rs b/fuzzers/jif/libjif/src/lib.rs index b0c5354e16..c9d15b55a4 100644 --- a/fuzzers/jif/libjif/src/lib.rs +++ b/fuzzers/jif/libjif/src/lib.rs @@ -6,14 +6,8 @@ use mimalloc::MiMalloc; static GLOBAL: MiMalloc = MiMalloc; use core::time::Duration; -use libafl::events::EventRestarter; -use libafl::prelude::Cores; -use libafl::prelude::GeneralizationStage; -use libafl::prelude::GeneralizedInput; -use libafl::prelude::SkippableStage; -use libafl::prelude::LlmpRestartingEventManager; -use libafl::Evaluator; -use libafl::prelude::TokenInsert; +use std::{env, fs, io::Read, net::SocketAddr, path::PathBuf}; + use libafl::{ bolts::{ current_nanos, @@ -24,50 +18,48 @@ use libafl::{ AsSlice, }, corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus}, - events::EventConfig, + events::{EventConfig, EventRestarter}, executors::{inprocess::InProcessExecutor, ExitKind, TimeoutExecutor}, feedback_or, feedbacks::MaxMapFeedback, fuzzer::{Fuzzer, StdFuzzer}, inputs::HasTargetBytes, monitors::MultiMonitor, - mutators::mutations::{ - BytesDeleteMutator, CrossoverInsertMutator, CrossoverReplaceMutator, SpliceMutator, - }, mutators::{ + mutations::{ + BytesDeleteMutator, CrossoverInsertMutator, CrossoverReplaceMutator, SpliceMutator, + }, scheduled::StdScheduledMutator, token_mutations::{I2SRandReplace, Tokens}, GrimoireExtensionMutator, GrimoireRandomDeleteMutator, GrimoireRecursiveReplacementMutator, GrimoireStringReplacementMutator, }, observers::{HitcountsMapObserver, StdMapObserver, TimeObserver}, + prelude::{ + Cores, GeneralizationStage, GeneralizedInput, LlmpRestartingEventManager, SkippableStage, + TokenInsert, + }, schedulers::QueueScheduler, stages::{calibrate::CalibrationStage, StdMutationalStage, TracingStage}, state::{HasCorpus, HasMaxSize, HasMetadata, StdState}, - Error, + Error, Evaluator, }; -use std::{env, fs, io::Read, net::SocketAddr, path::PathBuf}; -use structopt::StructOpt; - use libafl_targets::{ libfuzzer_initialize, libfuzzer_test_one_input, CmpLogObserver, CMPLOG_MAP, EDGES_MAP, MAX_EDGES_NUM, }; +use structopt::StructOpt; mod js; mod mutators; mod rvf; -use crate::js::JSFeedback; -use crate::js::JSMapState; -use crate::js::JSObserver; -use crate::rvf::ReturnValueFeedback; -use mutators::TagCopyMutator; -use mutators::TagCrossoverMutator; -use mutators::TagDeleteMutator; -use mutators::TagTokenMutator; - -use atomic_counter::AtomicCounter; -use atomic_counter::RelaxedCounter; +use atomic_counter::{AtomicCounter, RelaxedCounter}; +use mutators::{TagCopyMutator, TagCrossoverMutator, TagDeleteMutator, TagTokenMutator}; + +use crate::{ + js::{JSFeedback, JSMapState, JSObserver}, + rvf::ReturnValueFeedback, +}; const NUM_ITERATIONS: usize = 10_000; @@ -135,7 +127,7 @@ struct Opt { parse(from_os_str) )] harness: PathBuf, - + #[structopt( parse(try_from_str = timeout_from_millis_str), short, @@ -181,12 +173,7 @@ struct Opt { )] bytes: bool, - #[structopt( - help = "Use tags mutator", - name = "TAGS", - long = "tags", - short = "t" - )] + #[structopt(help = "Use tags mutator", name = "TAGS", long = "tags", short = "t")] tags: bool, #[structopt( @@ -198,7 +185,6 @@ struct Opt { cmplog: bool, } - /// The main fn, `no_mangle` as it is a C symbol #[allow(clippy::too_many_lines)] #[no_mangle] @@ -269,9 +255,7 @@ pub extern "C" fn main() { corpdir.push("corpus"); let generalization = GeneralizationStage::new(&edges_observer); //TODO: investigate using a multimapobserver - let generalization = SkippableStage::new(generalization, |_s| { - use_grimoire.into() - }); + let generalization = SkippableStage::new(generalization, |_s| use_grimoire.into()); let mut state = match state { Some(state) => state, None => StdState::new( @@ -327,12 +311,9 @@ pub extern "C" fn main() { let calibration = CalibrationStage::new(&max_map_feedback); // Setup a randomic Input2State stage - let i2s = - SkippableStage::new( - StdMutationalStage::new(StdScheduledMutator::new(tuple_list!(I2SRandReplace::new()))), - |_s| { - use_cmplog.into() - }, + let i2s = SkippableStage::new( + StdMutationalStage::new(StdScheduledMutator::new(tuple_list!(I2SRandReplace::new()))), + |_s| use_cmplog.into(), ); // mutations @@ -368,27 +349,20 @@ pub extern "C" fn main() { GrimoireRandomDeleteMutator::new(), )); - let byte_mutational_stage = SkippableStage::new( StdMutationalStage::new(StdScheduledMutator::new(byte_mutations)), - |_s| { - use_bytes.into() - } + |_s| use_bytes.into(), ); let tag_mutational_stage = SkippableStage::new( StdMutationalStage::new(StdScheduledMutator::new(tag_mutations)), - |_s| { - use_tags.into() - } + |_s| use_tags.into(), ); - let grim_mutational_stage = SkippableStage::new( - StdMutationalStage::new(grimoire_mutations), - |_s| { + let grim_mutational_stage = + SkippableStage::new(StdMutationalStage::new(grimoire_mutations), |_s| { use_grimoire.into() - } - ); - + }); + // A minimization+queue policy to get testcases from the corpus let scheduler = QueueScheduler::new(); @@ -407,7 +381,6 @@ pub extern "C" fn main() { ExitKind::Ok }; - // TODO: try without timeout executor // Create the executor for an in-process function with one observer for edge coverage and one for the execution time let mut executor = TimeoutExecutor::new( @@ -433,19 +406,17 @@ pub extern "C" fn main() { ExitKind::Ok }; - // Setup a tracing stage in which we log comparisons - let tracing = SkippableStage::new(TracingStage::new(InProcessExecutor::new( - &mut harness, - tuple_list!(cmplog_observer), - &mut fuzzer, - &mut state, - &mut mgr, - )?), |_s| { - use_cmplog.into() - }); - - + let tracing = SkippableStage::new( + TracingStage::new(InProcessExecutor::new( + &mut harness, + tuple_list!(cmplog_observer), + &mut fuzzer, + &mut state, + &mut mgr, + )?), + |_s| use_cmplog.into(), + ); // The order of the stages matter! let mut stages = tuple_list!( diff --git a/fuzzers/jif/libjif/src/rvf.rs b/fuzzers/jif/libjif/src/rvf.rs index 5f2841b53f..6fa21d1fa1 100644 --- a/fuzzers/jif/libjif/src/rvf.rs +++ b/fuzzers/jif/libjif/src/rvf.rs @@ -1,11 +1,7 @@ -use libafl::prelude::EventFirer; -use libafl::prelude::ExitKind; -use libafl::prelude::Feedback; -use libafl::prelude::HasClientPerfMonitor; -use libafl::prelude::Input; -use libafl::prelude::Named; -use libafl::prelude::ObserversTuple; -use libafl::Error; +use libafl::{ + prelude::{EventFirer, ExitKind, Feedback, HasClientPerfMonitor, Input, Named, ObserversTuple}, + Error, +}; use serde::{Deserialize, Serialize}; /// A [`ReturnValueFeedback`] reports as interesting if `LLVMTestOneInput == 42` From e1504acc49091093752ce24252139be811ef42c7 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 17 Nov 2022 11:27:05 +0100 Subject: [PATCH 3/5] fix compilation on latest LibAFL --- fuzzers/jif/libjif/src/js.rs | 22 +++++++++++++--------- fuzzers/jif/libjif/src/lib.rs | 12 ++---------- fuzzers/jif/libjif/src/mutators.rs | 18 +++++++++--------- fuzzers/jif/libjif/src/rvf.rs | 15 ++++++++------- 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/fuzzers/jif/libjif/src/js.rs b/fuzzers/jif/libjif/src/js.rs index 946db04116..3739f28d1c 100644 --- a/fuzzers/jif/libjif/src/js.rs +++ b/fuzzers/jif/libjif/src/js.rs @@ -5,8 +5,8 @@ use libafl::{ events::EventFirer, executors::ExitKind, feedbacks::Feedback, - inputs::Input, observers::{Observer, ObserversTuple}, + prelude::UsesInput, state::{HasClientPerfMonitor, HasMetadata, HasNamedMetadata}, Error, SerdeAny, }; @@ -25,8 +25,11 @@ extern "C" { fn get_js_coverage() -> *const c_char; } -impl Observer for JSObserver { - fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { +impl Observer for JSObserver +where + S: UsesInput, +{ + fn pre_exec(&mut self, _state: &mut S, _input: &S::Input) -> Result<(), Error> { // we don't currently do much here? Ok(()) } @@ -34,7 +37,7 @@ impl Observer for JSObserver { fn post_exec( &mut self, _state: &mut S, - _input: &I, + _input: &S::Input, _exit_kind: &ExitKind, ) -> Result<(), Error> { unsafe { @@ -69,20 +72,21 @@ pub struct JSFeedback { name: String, } -impl Feedback - for JSFeedback +impl Feedback for JSFeedback +where + S: UsesInput + HasClientPerfMonitor + HasMetadata + HasNamedMetadata, { fn is_interesting( &mut self, state: &mut S, _manager: &mut EM, - _input: &I, + _input: &S::Input, observers: &OT, _exit_kind: &ExitKind, ) -> Result where - EM: EventFirer, - OT: ObserversTuple, + EM: EventFirer, + OT: ObserversTuple, { let observer = observers.match_name::(&self.name).unwrap(); let novel = state diff --git a/fuzzers/jif/libjif/src/lib.rs b/fuzzers/jif/libjif/src/lib.rs index c9d15b55a4..1cf8f2cc24 100644 --- a/fuzzers/jif/libjif/src/lib.rs +++ b/fuzzers/jif/libjif/src/lib.rs @@ -120,14 +120,6 @@ struct Opt { )] output: PathBuf, - #[structopt( - help = "Path for the JS file with the harness to run inputs through", - name = "HARNESS", - long = "harness", - parse(from_os_str) - )] - harness: PathBuf, - #[structopt( parse(try_from_str = timeout_from_millis_str), short, @@ -219,7 +211,7 @@ pub extern "C" fn main() { let iteration_counter = RelaxedCounter::new(0); let mut run_client = |state: Option>, - mut mgr: LlmpRestartingEventManager<_, _, _, _>, + mut mgr: LlmpRestartingEventManager<_, _>, _core_id| { let repro_file = repro_file.clone(); @@ -384,7 +376,7 @@ pub extern "C" fn main() { // TODO: try without timeout executor // Create the executor for an in-process function with one observer for edge coverage and one for the execution time let mut executor = TimeoutExecutor::new( - InProcessExecutor::new::, _, _>( + InProcessExecutor::new::, _, _>( &mut harness, tuple_list!(edges_observer, time_observer, js_observer), &mut fuzzer, diff --git a/fuzzers/jif/libjif/src/mutators.rs b/fuzzers/jif/libjif/src/mutators.rs index f889b57ac3..930ac4241a 100644 --- a/fuzzers/jif/libjif/src/mutators.rs +++ b/fuzzers/jif/libjif/src/mutators.rs @@ -5,7 +5,7 @@ use libafl::{ corpus::Corpus, inputs::{HasBytesVec, Input}, mutators::{MutationResult, Mutator}, - prelude::Tokens, + prelude::{Tokens, UsesInput}, state::{HasCorpus, HasMetadata, HasRand}, Error, }; @@ -14,10 +14,10 @@ use libafl::{ #[derive(Default, Debug)] pub struct TagDeleteMutator; -impl Mutator for TagDeleteMutator +impl Mutator for TagDeleteMutator where I: Input + HasBytesVec, - S: HasRand, + S: HasRand + UsesInput, { fn mutate( &mut self, @@ -88,10 +88,10 @@ impl TagDeleteMutator { #[derive(Default, Debug)] pub struct TagCopyMutator; -impl Mutator for TagCopyMutator +impl Mutator for TagCopyMutator where I: Input + HasBytesVec, - S: HasRand, + S: HasRand + UsesInput, { fn mutate( &mut self, @@ -170,10 +170,10 @@ impl TagCopyMutator { #[derive(Default, Debug)] pub struct TagCrossoverMutator; -impl Mutator for TagCrossoverMutator +impl Mutator for TagCrossoverMutator where I: Input + HasBytesVec, - S: HasRand + HasCorpus, + S: HasRand + HasCorpus, { fn mutate( &mut self, @@ -281,10 +281,10 @@ impl TagCrossoverMutator { #[derive(Default, Debug)] pub struct TagTokenMutator; -impl Mutator for TagTokenMutator +impl Mutator for TagTokenMutator where I: Input + HasBytesVec, - S: HasRand + HasCorpus + HasMetadata, + S: HasRand + HasCorpus + HasMetadata, { fn mutate( &mut self, diff --git a/fuzzers/jif/libjif/src/rvf.rs b/fuzzers/jif/libjif/src/rvf.rs index 6fa21d1fa1..50b3493943 100644 --- a/fuzzers/jif/libjif/src/rvf.rs +++ b/fuzzers/jif/libjif/src/rvf.rs @@ -1,5 +1,7 @@ use libafl::{ - prelude::{EventFirer, ExitKind, Feedback, HasClientPerfMonitor, Input, Named, ObserversTuple}, + prelude::{ + EventFirer, ExitKind, Feedback, HasClientPerfMonitor, Named, ObserversTuple, UsesInput, + }, Error, }; use serde::{Deserialize, Serialize}; @@ -8,23 +10,22 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ReturnValueFeedback {} -impl Feedback for ReturnValueFeedback +impl Feedback for ReturnValueFeedback where - I: Input, - S: HasClientPerfMonitor, + S: HasClientPerfMonitor + UsesInput, { #[allow(clippy::wrong_self_convention)] fn is_interesting( &mut self, _state: &mut S, _manager: &mut EM, - _input: &I, + _input: &S::Input, _observers: &OT, exit_kind: &ExitKind, ) -> Result where - EM: EventFirer, - OT: ObserversTuple, + EM: EventFirer, + OT: ObserversTuple, { if let ExitKind::Oom = exit_kind { //HACK: we need to add a new ExitKind for XSS From a77f77111245e5d269a32724eb3d5878021d5b91 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 17 Nov 2022 11:36:14 +0100 Subject: [PATCH 4/5] move to clap 4 --- fuzzers/jif/libjif/Cargo.toml | 6 ++-- fuzzers/jif/libjif/src/lib.rs | 66 +++++++++++++---------------------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/fuzzers/jif/libjif/Cargo.toml b/fuzzers/jif/libjif/Cargo.toml index c2e91a3bcc..341039eb86 100644 --- a/fuzzers/jif/libjif/Cargo.toml +++ b/fuzzers/jif/libjif/Cargo.toml @@ -24,9 +24,9 @@ libafl_targets = { path = "../../../libafl_targets", features = ["sancov_pcguard libafl_cc = { path = "../../../libafl_cc"} mimalloc = { version = "*", default-features = false } -structopt = "0.3.25" -serde_json = "1.0.83" -serde = "1.0.143" +clap = { version = "4.0", features = ["derive"] } +serde_json = "1.0" +serde = "1.0" atomic-counter = "1.0.1" [lib] diff --git a/fuzzers/jif/libjif/src/lib.rs b/fuzzers/jif/libjif/src/lib.rs index 1cf8f2cc24..adb031ba56 100644 --- a/fuzzers/jif/libjif/src/lib.rs +++ b/fuzzers/jif/libjif/src/lib.rs @@ -8,6 +8,7 @@ static GLOBAL: MiMalloc = MiMalloc; use core::time::Duration; use std::{env, fs, io::Read, net::SocketAddr, path::PathBuf}; +use clap::{self, Parser}; use libafl::{ bolts::{ current_nanos, @@ -48,7 +49,6 @@ use libafl_targets::{ libfuzzer_initialize, libfuzzer_test_one_input, CmpLogObserver, CMPLOG_MAP, EDGES_MAP, MAX_EDGES_NUM, }; -use structopt::StructOpt; mod js; mod mutators; @@ -68,60 +68,47 @@ fn timeout_from_millis_str(time: &str) -> Result { Ok(Duration::from_millis(time.parse()?)) } -#[derive(Debug, StructOpt)] -#[structopt( +#[derive(Debug, Parser)] +#[command( name = "jif", about = "JIF: Javascript Injection Fuzzer", author = "jhertz" )] struct Opt { - #[structopt( + #[arg( short, long, - parse(try_from_str = Cores::from_cmdline), + value_parser = Cores::from_cmdline, help = "Spawn a client in each of the provided cores. Broker runs in the 0th core. 'all' to select all available cores. 'none' to run a client without binding to any core. eg: '1,2-4,6' selects the cores 1,2,3,4,6.", name = "CORES" )] cores: Cores, - #[structopt( - short = "p", + #[arg( + short = 'p', long, help = "Choose the broker TCP port, default is 1337", name = "PORT" )] broker_port: u16, - #[structopt( - parse(try_from_str), - short = "a", - long, - help = "Specify a remote broker", - name = "REMOTE" - )] + #[arg(short = 'a', long, help = "Specify a remote broker", name = "REMOTE")] remote_broker_addr: Option, - #[structopt( - parse(try_from_str), - short, - long, - help = "Set an initial corpus directory", - name = "INPUT" - )] + #[arg(short, long, help = "Set an initial corpus directory", name = "INPUT")] input: PathBuf, - #[structopt( + #[arg( short, long, - parse(try_from_str), help = "Set the output directory, default is ./out", name = "OUTPUT", default_value = "./out" )] output: PathBuf, - #[structopt( - parse(try_from_str = timeout_from_millis_str), + #[arg( + value_parser = timeout_from_millis_str, short, long, help = "Set the exeucution timeout in milliseconds, default is 1000", @@ -130,49 +117,46 @@ struct Opt { )] timeout: Duration, - #[structopt( - parse(from_os_str), - short = "x", + #[arg( + short = 'x', long, help = "Feed the fuzzer with an user-specified list of tokens (often called \"dictionary\"", - name = "TOKENS", - multiple = true + name = "TOKENS" )] tokens: Vec, - #[structopt( + #[arg( help = "File to run instead of doing fuzzing loop", name = "REPRO", - long = "repro", - parse(from_os_str) + long = "repro" )] repro_file: Option, // several new flags, -g for grimoire -b for bytes -t for tags - #[structopt( + #[arg( help = "Use grimoire mutator", name = "GRIMOIRE", long = "grimoire", - short = "g" + short = 'g' )] grimoire: bool, - #[structopt( + #[arg( help = "Use bytes mutator", name = "BYTES", long = "bytes", - short = "b" + short = 'b' )] bytes: bool, - #[structopt(help = "Use tags mutator", name = "TAGS", long = "tags", short = "t")] + #[arg(help = "Use tags mutator", name = "TAGS", long = "tags", short = 't')] tags: bool, - #[structopt( + #[arg( help = "Use cmplog mutator", name = "CMPLOG", long = "cmplog", - short = "c" + short = 'c' )] cmplog: bool, } @@ -183,7 +167,7 @@ struct Opt { pub extern "C" fn main() { let _args: Vec = env::args().collect(); let workdir = env::current_dir().unwrap(); - let opt = Opt::from_args(); + let opt = Opt::parse(); let cores = opt.cores; let broker_port = opt.broker_port; let remote_broker_addr = opt.remote_broker_addr; From 4043a2b8e5e5e84b361531a9d96f57b50bb53613 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 17 Nov 2022 11:37:57 +0100 Subject: [PATCH 5/5] fix testcase --- fuzzers/jif/libjif/src/bin/libafl_cc.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fuzzers/jif/libjif/src/bin/libafl_cc.rs b/fuzzers/jif/libjif/src/bin/libafl_cc.rs index 6db336f25e..b9a1b1d23f 100644 --- a/fuzzers/jif/libjif/src/bin/libafl_cc.rs +++ b/fuzzers/jif/libjif/src/bin/libafl_cc.rs @@ -383,6 +383,10 @@ impl ClangWrapper { #[cfg(test)] mod tests { + use libafl_cc::CompilerWrapper; + + use super::ClangWrapper; + #[test] fn test_clang_version() { if let Err(res) = ClangWrapper::new()