From a680ce5eab7a63723f10a0e39b6bc312df79a7fb Mon Sep 17 00:00:00 2001 From: headshog <124502670+headshog@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:38:22 +0300 Subject: [PATCH] Support C# crash triage for SharpFuzz (#207) --- .github/workflows/amd64.yml | 3 + .github/workflows/coverage.yaml | 3 + .gitignore | 2 + README.md | 27 +- casr/Cargo.toml | 1 + casr/src/bin/casr-afl.rs | 64 +-- casr/src/bin/casr-cluster.rs | 2 +- casr/src/bin/casr-core.rs | 3 +- casr/src/bin/casr-csharp.rs | 162 ++++++++ casr/src/bin/casr-gdb.rs | 3 +- casr/src/bin/casr-java.rs | 3 +- casr/src/bin/casr-js.rs | 3 +- casr/src/bin/casr-libfuzzer.rs | 36 +- casr/src/bin/casr-python.rs | 3 +- casr/src/bin/casr-san.rs | 3 +- casr/src/bin/casr-ubsan.rs | 3 +- .../afl-out-sharpfuzz/afl_main-worker/cmdline | 3 + ...2,src:000001,time:0,execs:9,op:havoc,rep:1 | 1 + ...,src:000001,time:0,execs:12,op:havoc,rep:3 | Bin 0 -> 11 bytes ...,src:000001,time:3,execs:27,op:havoc,rep:3 | 1 + .../afl_main-worker/fuzzer_stats | 43 +++ .../afl-out-sharpfuzz/afl_s01-worker/cmdline | 3 + ...,src:000001,time:0,execs:10,op:havoc,rep:1 | 1 + ...,src:000001,time:1,execs:12,op:havoc,rep:1 | Bin 0 -> 4 bytes ...,src:000001,time:2,execs:24,op:havoc,rep:8 | 1 + .../afl_s01-worker/fuzzer_stats | 43 +++ .../test_casr_afl_csharp.cs | 19 + .../test_casr_afl_csharp.csproj | 15 + .../test_casr_afl_csharp_module.cs | 21 + .../test_casr_afl_csharp_module.csproj | 9 + .../test_casr_csharp/test_casr_csharp.cs | 16 + .../test_casr_csharp/test_casr_csharp.csproj | 10 + casr/tests/tests.rs | 363 ++++++++++++++---- docs/usage.md | 99 +++-- libcasr/src/cpp.rs | 7 +- libcasr/src/csharp.rs | 3 +- libcasr/src/execution_class.rs | 2 +- libcasr/src/js.rs | 4 +- libcasr/src/lib.rs | 1 + libcasr/src/rust.rs | 4 +- 40 files changed, 818 insertions(+), 172 deletions(-) create mode 100644 casr/src/bin/casr-csharp.rs create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/cmdline create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:9,op:havoc,rep:1 create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000001,sig:02,src:000001,time:0,execs:12,op:havoc,rep:3 create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000002,sig:02,src:000001,time:3,execs:27,op:havoc,rep:3 create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/fuzzer_stats create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/cmdline create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:10,op:havoc,rep:1 create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000001,sig:02,src:000001,time:1,execs:12,op:havoc,rep:1 create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000002,sig:02,src:000001,time:2,execs:24,op:havoc,rep:8 create mode 100644 casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/fuzzer_stats create mode 100644 casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.cs create mode 100644 casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.csproj create mode 100644 casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.cs create mode 100644 casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.csproj create mode 100644 casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.cs create mode 100644 casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj diff --git a/.github/workflows/amd64.yml b/.github/workflows/amd64.yml index 48c7fe84..bd264b87 100644 --- a/.github/workflows/amd64.yml +++ b/.github/workflows/amd64.yml @@ -29,6 +29,9 @@ jobs: sudo apt update && sudo apt install -y nodejs sudo npm install -g jsfuzz sudo npm install --save-dev @jazzer.js/core + wget -q https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + dpkg -i packages-microsoft-prod.deb && rm packages-microsoft-prod.deb + sudo apt update && sudo apt install -y --no-install-recommends dotnet-sdk-8.0 curl https://sh.rustup.rs -o rustup.sh && chmod +x rustup.sh && \ ./rustup.sh -y && rm rustup.sh rustup install nightly diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 9d806814..12f23c35 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -27,6 +27,9 @@ jobs: sudo apt update && sudo apt install -y nodejs sudo npm install -g jsfuzz sudo npm install --save-dev @jazzer.js/core + wget -q https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + dpkg -i packages-microsoft-prod.deb && rm packages-microsoft-prod.deb + sudo apt update && sudo apt install -y --no-install-recommends dotnet-sdk-8.0 curl https://sh.rustup.rs -o rustup.sh && chmod +x rustup.sh && \ ./rustup.sh -y && rm rustup.sh rustup install nightly diff --git a/.gitignore b/.gitignore index a89614ff..b3cec92b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ Cargo.lock */target */Cargo.lock */tests/tmp_tests_casr +*/tests/casr_tests/csharp/*/bin +*/tests/casr_tests/csharp/*/obj *.swp node_modules */node_modules/* diff --git a/README.md b/README.md index df689455..4903e4b8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ java reports and get report from to analyze JavaScript reports and get report from [Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js) or [jsfuzz](https://github.com/fuzzitdev/jsfuzz). +Use `casr-csharp` to analyze C# reports and get report from +[Sharpfuzz](https://github.com/Metalnem/sharpfuzz). Crash report contains many useful information: severity (like [exploitable](https://github.com/jfoote/exploitable)) for x86, x86\_64, arm32, aarch64, rv32g, rv64g architectures, @@ -44,7 +46,8 @@ stored in JSON format. `casr-cli` is meant to provide TUI for viewing reports and converting them into SARIF report. Reports triage (deduplication, clustering) is done by `casr-cluster`. Triage is based on stack trace comparison from [gdb-command](https://github.com/anfedotoff/gdb-command). -`casr-afl` is used to triage crashes found by [AFL++](https://github.com/AFLplusplus/AFLplusplus). +`casr-afl` is used to triage crashes found by [AFL++](https://github.com/AFLplusplus/AFLplusplus) +and AFL-based fuzzer [Sharpfuzz](https://github.com/Metalnem/sharpfuzz). `casr-libfuzzer` can triage crashes found by [libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) based fuzzer (C/C++/[go-fuzz](https://github.com/dvyukov/go-fuzz)/[Atheris](https://github.com/google/atheris) @@ -81,6 +84,7 @@ and program languages: * Python * Java * JavaScript +* C# It could be built with `exploitable` feature for severity estimation crashes collected from gdb. To save crash reports as json use `serde` feature. @@ -161,6 +165,10 @@ Create report from JavaScript: $ casr-js -o js.casrep -- node casr/tests/casr_tests/js/test_casr_js.js +Create report from C#: + + $ casr-csharp -o csharp.casrep -- dotnet run --project casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj + View report: $ casr-cli casr/tests/casr_tests/casrep/test_clustering_san/load_fuzzer_crash-120697a7f5b87c03020f321c8526adf0f4bcc2dc.casrep @@ -193,6 +201,15 @@ Triage crashes after AFL++ fuzzing with casr-afl: $ # You may also additionally generate crash reports for uninstrumented binary with casr-gdb $ casr-afl -i casr/tests/casr_tests/casrep/afl-out-xlnt -o casr/tests/tmp_tests_casr/casr_afl_out -- /tmp/load_sydr @@ +Triage crashes after Sharpfuzz fuzzing with casr-afl: + + $ cp -r casr/tests/casr_tests/csharp/test_casr_afl_csharp /tmp/test_casr_afl_csharp + $ cp -r casr/tests/casr_tests/csharp/test_casr_afl_csharp_module /tmp/test_casr_afl_csharp_module + $ dotnet publish /tmp/test_casr_afl_csharp/test_casr_afl_csharp.csproj -c Debug -o /tmp/test_casr_afl_csharp/bin + $ casr-afl -i casr/tests/casr_tests/casrep/afl-out-sharpfuzz -o casr/tests/tmp_tests_casr/casr_afl_csharp_out + $ # You may force your own run arguments using --ignore-cmdline + $ casr-afl --ignore-cmdline -i casr/tests/casr_tests/casrep/afl-out-sharpfuzz -o casr/tests/tmp_tests_casr/casr_afl_csharp_out -- dotnet run --no-build --project /tmp/test_casr_afl_csharp/test_casr_afl_csharp.csproj @@ + Triage libFuzzer crashes with casr-libfuzzer: $ casr-libfuzzer -t 30 -i casr/tests/casr_tests/casrep/libfuzzer_crashes_xlnt -o casr/tests/tmp_tests_casr/casr_libfuzzer_out -- casr/tests/casr_tests/bin/load_fuzzer @@ -227,7 +244,7 @@ Upload new and unique CASR reports to When you have crashes from fuzzing you may do the following steps: 1. Create reports for all crashes via `casr-san`, `casr-gdb` (if no sanitizers - are present), `casr-python`, `casr-java`, or `casr-js`. + are present), `casr-python`, `casr-java`, `casr-js`, or `casr-csharp`. 2. Deduplicate collected crash reports via `casr-cluster -d`. 3. Cluster deduplicated crash reports via `casr-cluster -c`. 4. Create reports and deduplicate them for all UBSAN errors via `casr-ubsan`. @@ -235,15 +252,15 @@ When you have crashes from fuzzing you may do the following steps: [DefectDojo](https://github.com/DefectDojo/django-DefectDojo) with `casr-dojo`. -If you use [AFL++](https://github.com/AFLplusplus/AFLplusplus), the pipeline +If you use [AFL++](https://github.com/AFLplusplus/AFLplusplus) or AFL-based +fuzzer [Sharpfuzz](https://www.llvm.org/docs/LibFuzzer.html), the pipeline (without `casr-ubsan` and `casr-dojo`) could be done automatically by `casr-afl`. If you use [libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) based fuzzer (C/C++/[go-fuzz](https://github.com/dvyukov/go-fuzz)/[Atheris](https://github.com/google/atheris) /[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer)/[Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js)/ -[jsfuzz](https://github.com/fuzzitdev/jsfuzz)), -the pipeline (without `casr-ubsan` and `casr-dojo`) could be done automatically +[jsfuzz](https://github.com/fuzzitdev/jsfuzz)), the pipeline (without `casr-ubsan` and `casr-dojo`) could be done automatically by `casr-libfuzzer`. ## Contributing diff --git a/casr/Cargo.toml b/casr/Cargo.toml index ab09d2bf..204323fe 100644 --- a/casr/Cargo.toml +++ b/casr/Cargo.toml @@ -52,3 +52,4 @@ required-features = ["dojo"] [dev-dependencies] lazy_static = "1.4" +copy_dir = "0.1.3" diff --git a/casr/src/bin/casr-afl.rs b/casr/src/bin/casr-afl.rs index 5cee25bd..2c0f158b 100644 --- a/casr/src/bin/casr-afl.rs +++ b/casr/src/bin/casr-afl.rs @@ -15,7 +15,7 @@ use std::path::{Path, PathBuf}; fn main() -> Result<()> { let matches = clap::Command::new("casr-afl") .version(clap::crate_version!()) - .about("Triage crashes found by AFL++") + .about("Triage crashes found by AFL++/Sharpfuzz") .term_width(90) .arg( Arg::new("log-level") @@ -40,7 +40,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("input") @@ -100,23 +100,23 @@ fn main() -> Result<()> { .required(false) .num_args(1..) .last(true) - .help("Add \"-- ./gdb_fuzz_target \" to generate additional crash reports with casr-gdb (e.g., test whether program crashes without sanitizers)"), + .help("Add \"-- ./gdb_fuzz_target \" to generate additional crash reports with casr-gdb \ + (for compiled binaries, e.g., test whether program crashes without sanitizers), \"-- dotnet \" \ + or \"-- mono \" to triage C# crashes with additional options") ) .get_matches(); // Init log. util::initialize_logging(&matches); - let casr_san = util::get_path("casr-san")?; - let casr_gdb = util::get_path("casr-gdb")?; - - let mut gdb_args = if let Some(argv) = matches.get_many::("ARGS") { + let mut is_casr_gdb = true; + let mut args = if let Some(argv) = matches.get_many::("ARGS") { argv.cloned().collect() } else { Vec::new() }; - if gdb_args.is_empty() && matches.get_flag("ignore-cmdline") { + if args.is_empty() && matches.get_flag("ignore-cmdline") { bail!("ARGS is empty, but \"ignore-cmdline\" option is provided."); } @@ -130,11 +130,10 @@ fn main() -> Result<()> { // Get crashes from one node. let mut crash_info = casr::triage::CrashInfo { - casr_tool: casr_gdb.clone(), ..Default::default() }; crash_info.target_args = if matches.get_flag("ignore-cmdline") { - gdb_args.clone() + args.clone() } else { let cmdline_path = path.join("cmdline"); if let Ok(cmdline) = fs::read_to_string(&cmdline_path) { @@ -144,6 +143,15 @@ fn main() -> Result<()> { continue; } }; + crash_info.casr_tool = if !crash_info.target_args.is_empty() + && (crash_info.target_args[0].ends_with("dotnet") + || crash_info.target_args[0].ends_with("mono")) + { + is_casr_gdb = false; + util::get_path("casr-csharp")? + } else { + util::get_path("casr-gdb")? + }; crash_info.at_index = crash_info .target_args .iter() @@ -151,22 +159,26 @@ fn main() -> Result<()> { .position(|s| s.contains("@@")) .map(|x| x + 1); - if let Some(target) = crash_info.target_args.first() { - match util::symbols_list(Path::new(target)) { - Ok(list) => { - if list.contains("__asan") { - crash_info.casr_tool = casr_san.clone() + // When we triage crashes for binaries, use casr-san. + if is_casr_gdb { + if let Some(target) = crash_info.target_args.first() { + match util::symbols_list(Path::new(target)) { + Ok(list) => { + if list.contains("__asan") { + crash_info.casr_tool = util::get_path("casr-san")?.clone() + } + } + Err(e) => { + error!("{e}"); + continue; } } - Err(e) => { - error!("{e}"); - continue; - } + } else { + error!("Cmdline is empty. Path: {:?}", path.join("cmdline")); + continue; } - } else { - error!("Cmdline is empty. Path: {:?}", path.join("cmdline")); - continue; } + // Push crash paths. for crash in path .read_dir()? @@ -183,10 +195,10 @@ fn main() -> Result<()> { } } - if matches.get_flag("ignore-cmdline") { - gdb_args = Vec::new(); + if matches.get_flag("ignore-cmdline") || !is_casr_gdb { + args = Vec::new(); } - // Generate reports - fuzzing_crash_triage_pipeline(&matches, &crashes, &gdb_args) + // Generate reports. + fuzzing_crash_triage_pipeline(&matches, &crashes, &args) } diff --git a/casr/src/bin/casr-cluster.rs b/casr/src/bin/casr-cluster.rs index 83971af2..ae465a23 100644 --- a/casr/src/bin/casr-cluster.rs +++ b/casr/src/bin/casr-cluster.rs @@ -694,7 +694,7 @@ fn main() -> Result<()> { .value_parser(clap::value_parser!(u32).range(1..)) ) .get_matches(); - init_ignored_frames!("cpp", "rust", "python", "go", "java", "js"); + init_ignored_frames!("cpp", "rust", "python", "go", "java", "js", "csharp"); // Get number of threads let jobs = if let Some(jobs) = matches.get_one::("jobs") { diff --git a/casr/src/bin/casr-core.rs b/casr/src/bin/casr-core.rs index 7f42e272..4854970b 100644 --- a/casr/src/bin/casr-core.rs +++ b/casr/src/bin/casr-core.rs @@ -302,6 +302,7 @@ fn main() -> Result<()> { // Save report. if let Ok(mut file) = OpenOptions::new() .create(true) + .truncate(true) .write(true) .open(&report_path) { @@ -319,7 +320,7 @@ fn check_lock() -> Result { project_dir.push("Casr.lock"); let file = OpenOptions::new() .create(true) - .write(true) + .append(true) .open(project_dir)?; let fd = file.as_raw_fd(); flock(fd, FlockArg::LockExclusive).unwrap(); diff --git a/casr/src/bin/casr-csharp.rs b/casr/src/bin/casr-csharp.rs new file mode 100644 index 00000000..ce26ac2d --- /dev/null +++ b/casr/src/bin/casr-csharp.rs @@ -0,0 +1,162 @@ +use casr::util; +use libcasr::{ + csharp::*, exception::Exception, init_ignored_frames, report::CrashReport, stacktrace::*, +}; + +use anyhow::{bail, Result}; +use clap::{Arg, ArgAction, ArgGroup}; +use regex::Regex; +use std::path::PathBuf; +use std::process::Command; + +fn main() -> Result<()> { + let matches = clap::Command::new("casr-csharp") + .version(clap::crate_version!()) + .about("Create CASR reports (.casrep) from C# reports") + .term_width(90) + .arg( + Arg::new("output") + .short('o') + .long("output") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .value_name("REPORT") + .help( + "Path to save report. Path can be a directory, then report name is generated", + ), + ) + .arg( + Arg::new("stdout") + .action(ArgAction::SetTrue) + .long("stdout") + .help("Print CASR report to stdout"), + ) + .group( + ArgGroup::new("out") + .args(["stdout", "output"]) + .required(true), + ) + .arg( + Arg::new("stdin") + .long("stdin") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .value_name("FILE") + .help("Stdin file for program"), + ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .default_value("0") + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") + .value_parser(clap::value_parser!(u64)) + ) + .arg( + Arg::new("ignore") + .long("ignore") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .value_name("FILE") + .help("File with regular expressions for functions and file paths that should be ignored"), + ) + .arg( + Arg::new("strip-path") + .long("strip-path") + .env("CASR_STRIP_PATH") + .action(ArgAction::Set) + .value_name("PREFIX") + .help("Path prefix to strip from stacktrace and crash line"), + ) + .arg( + Arg::new("ARGS") + .action(ArgAction::Set) + .num_args(1..) + .last(true) + .required(true) + .help("Add \"-- \" to run"), + ) + .get_matches(); + + init_ignored_frames!("csharp", "cpp"); + if let Some(path) = matches.get_one::("ignore") { + util::add_custom_ignored_frames(path)?; + } + // Get program args. + let argv: Vec<&str> = if let Some(argvs) = matches.get_many::("ARGS") { + argvs.map(|s| s.as_str()).collect() + } else { + bail!("Wrong arguments for starting program"); + }; + + // Check that args are valid. + let Some(pos) = argv + .iter() + .position(|x| x.ends_with(".dll") || x.ends_with(".exe") || x.ends_with(".csproj")) + else { + bail!("dotnet/mono target is not specified by .dll, .exe or .csproj executable."); + }; + + // Get stdin for target program. + let stdin_file = util::stdin_from_matches(&matches)?; + + // Get timeout. + let timeout = *matches.get_one::("timeout").unwrap(); + + // Run program. + let mut csharp_cmd = Command::new(argv[0]); + if let Some(ref file) = stdin_file { + csharp_cmd.stdin(std::fs::File::open(file)?); + } + if argv.len() > 1 { + csharp_cmd.args(&argv[1..]); + } + let csharp_result = util::get_output(&mut csharp_cmd, timeout, true)?; + + let csharp_stderr = String::from_utf8_lossy(&csharp_result.stderr); + + // Create report. + let mut report = CrashReport::new(); + // Set executable path (for C# .dll, .csproj (dotnet) or .exe (mono) file). + report.executable_path = argv.get(pos).unwrap().to_string(); + report.proc_cmdline = argv.join(" "); + let _ = report.add_os_info(); + let _ = report.add_proc_environ(); + + // Get C# report. + let csharp_stderr_list: Vec = + csharp_stderr.split('\n').map(|l| l.to_string()).collect(); + let re = Regex::new(r"^Unhandled [Ee]xception(?::\n|\. ).*").unwrap(); + if let Some(start) = csharp_stderr_list.iter().position(|x| re.is_match(x)) { + let end = csharp_stderr_list[start..] + .iter() + .rposition(|x| !x.is_empty()) + .unwrap() + + 1; + report.csharp_report = csharp_stderr_list[start..end].to_vec(); + let report_str = report.csharp_report.join("\n"); + report.stacktrace = CSharpStacktrace::extract_stacktrace(&report_str)?; + if let Some(exception) = CSharpException::parse_exception(&report_str) { + report.execution_class = exception; + } + } + + let stacktrace = CSharpStacktrace::parse_stacktrace(&report.stacktrace)?; + if let Ok(crash_line) = stacktrace.crash_line() { + report.crashline = crash_line.to_string(); + if let CrashLine::Source(debug) = crash_line { + if let Some(sources) = CrashReport::sources(&debug) { + report.source = sources; + } + } + } + + if let Some(path) = matches.get_one::("strip-path") { + util::strip_paths(&mut report, &stacktrace, path); + } + + //Output report. + util::output_report(&report, &matches, &argv) +} diff --git a/casr/src/bin/casr-gdb.rs b/casr/src/bin/casr-gdb.rs index 013423b8..5436ddf8 100644 --- a/casr/src/bin/casr-gdb.rs +++ b/casr/src/bin/casr-gdb.rs @@ -69,7 +69,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("ignore") @@ -92,6 +92,7 @@ fn main() -> Result<()> { .action(ArgAction::Set) .num_args(1..) .last(true) + .required(true) .help("Add \"-- ./binary \" to run executable"), ) .get_matches(); diff --git a/casr/src/bin/casr-java.rs b/casr/src/bin/casr-java.rs index 2f544657..917119ea 100644 --- a/casr/src/bin/casr-java.rs +++ b/casr/src/bin/casr-java.rs @@ -64,7 +64,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("ignore") @@ -87,6 +87,7 @@ fn main() -> Result<()> { .action(ArgAction::Set) .num_args(1..) .last(true) + .required(true) .help("Add \"-- \" to run"), ) .get_matches(); diff --git a/casr/src/bin/casr-js.rs b/casr/src/bin/casr-js.rs index 8ac68fad..1762ea1b 100644 --- a/casr/src/bin/casr-js.rs +++ b/casr/src/bin/casr-js.rs @@ -52,7 +52,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("ignore") @@ -75,6 +75,7 @@ fn main() -> Result<()> { .action(ArgAction::Set) .num_args(1..) .last(true) + .required(true) .help("Add \"-- \" to run"), ) .get_matches(); diff --git a/casr/src/bin/casr-libfuzzer.rs b/casr/src/bin/casr-libfuzzer.rs index 76b970c2..bb58cfd4 100644 --- a/casr/src/bin/casr-libfuzzer.rs +++ b/casr/src/bin/casr-libfuzzer.rs @@ -39,7 +39,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("input") @@ -98,6 +98,7 @@ fn main() -> Result<()> { .action(ArgAction::Set) .num_args(1..) .last(true) + .required(true) .help("Add \"-- ./fuzz_target \""), ) .get_matches(); @@ -114,15 +115,16 @@ fn main() -> Result<()> { } else { bail!("Invalid fuzz target arguments"); }; - let at_index = if let Some(idx) = argv.iter().skip(1).position(|s| s.contains("@@")) { - idx + 1 + + // Get gdb args. + let gdb_args = if let Some(argv) = matches.get_one::("casr-gdb-args") { + shell_words::split(argv)? } else { - argv.push("@@"); - argv.len() - 1 + Vec::new() }; + // Get tool. let mut envs = HashMap::new(); - let tool = if argv[0].ends_with(".py") { envs.insert("LD_PRELOAD".to_string(), util::get_atheris_lib()?); "casr-python" @@ -142,7 +144,19 @@ fn main() -> Result<()> { "casr-gdb" } }; - let tool = util::get_path(tool)?; + let tool_path = util::get_path(tool)?; + + if !gdb_args.is_empty() && tool != "casr-gdb" && tool != "casr-san" { + bail!("casr-gdb-args option is provided with incompatible tool. This option can be used with casr-san or casr-gdb."); + } + + // Get input file argument index. + let at_index = if let Some(idx) = argv.iter().skip(1).position(|s| s.contains("@@")) { + idx + 1 + } else { + argv.push("@@"); + argv.len() - 1 + }; // Get all crashes. let crashes: HashMap = fs::read_dir(input_dir)? @@ -159,18 +173,12 @@ fn main() -> Result<()> { target_args: argv.iter().map(|x| x.to_string()).collect(), envs: envs.clone(), at_index: Some(at_index), - casr_tool: tool.clone(), + casr_tool: tool_path.clone(), }, ) }) .collect(); - let gdb_args = if let Some(argv) = matches.get_one::("casr-gdb-args") { - shell_words::split(argv)? - } else { - Vec::new() - }; - // Generate reports fuzzing_crash_triage_pipeline(&matches, &crashes, &gdb_args) } diff --git a/casr/src/bin/casr-python.rs b/casr/src/bin/casr-python.rs index b7ace1bf..bdfa0b63 100644 --- a/casr/src/bin/casr-python.rs +++ b/casr/src/bin/casr-python.rs @@ -56,7 +56,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("ignore") @@ -79,6 +79,7 @@ fn main() -> Result<()> { .action(ArgAction::Set) .num_args(1..) .last(true) + .required(true) .help("Add \"-- \" to run"), ) .get_matches(); diff --git a/casr/src/bin/casr-san.rs b/casr/src/bin/casr-san.rs index bb20f660..29be269e 100644 --- a/casr/src/bin/casr-san.rs +++ b/casr/src/bin/casr-san.rs @@ -72,7 +72,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("ignore") @@ -95,6 +95,7 @@ fn main() -> Result<()> { .action(ArgAction::Set) .num_args(1..) .last(true) + .required(true) .help("Add \"-- ./binary \" to run executable"), ) .get_matches(); diff --git a/casr/src/bin/casr-ubsan.rs b/casr/src/bin/casr-ubsan.rs index 00d1b483..42200ec5 100644 --- a/casr/src/bin/casr-ubsan.rs +++ b/casr/src/bin/casr-ubsan.rs @@ -238,7 +238,7 @@ fn main() -> Result<()> { .default_value("0") .value_name("SECONDS") .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") - .value_parser(clap::value_parser!(u64).range(0..)) + .value_parser(clap::value_parser!(u64)) ) .arg( Arg::new("input") @@ -287,6 +287,7 @@ fn main() -> Result<()> { .required(false) .num_args(1..) .last(true) + .required(true) .help("Add \"-- \" to run"), ) .get_matches(); diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/cmdline b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/cmdline new file mode 100644 index 00000000..c2b6ed28 --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/cmdline @@ -0,0 +1,3 @@ +/usr/bin/dotnet +/tmp/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll +@@ diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:9,op:havoc,rep:1 b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:9,op:havoc,rep:1 new file mode 100644 index 00000000..ea69c037 --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:9,op:havoc,rep:1 @@ -0,0 +1 @@ +dadxxxx diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000001,sig:02,src:000001,time:0,execs:12,op:havoc,rep:3 b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000001,sig:02,src:000001,time:0,execs:12,op:havoc,rep:3 new file mode 100644 index 0000000000000000000000000000000000000000..f10969ae0e7e24bd04e96af19c3e3468f0e1f1ea GIT binary patch literal 11 ScmYeA_aeWjfSG}zf(rl~)C0c& literal 0 HcmV?d00001 diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000002,sig:02,src:000001,time:3,execs:27,op:havoc,rep:3 b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000002,sig:02,src:000001,time:3,execs:27,op:havoc,rep:3 new file mode 100644 index 00000000..c972f284 --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/crashes/id:000002,sig:02,src:000001,time:3,execs:27,op:havoc,rep:3 @@ -0,0 +1 @@ +baaaaa€Exxxx diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/fuzzer_stats b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/fuzzer_stats new file mode 100644 index 00000000..c25403fb --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_main-worker/fuzzer_stats @@ -0,0 +1,43 @@ +start_time : 1710252309 +last_update : 1710252326 +run_time : 17 +fuzzer_pid : 1465 +cycles_done : 143 +cycles_wo_finds : 143 +time_wo_finds : 0 +execs_done : 187893 +execs_per_sec : 10919.57 +execs_ps_last_min : 0.00 +corpus_count : 2 +corpus_favored : 1 +corpus_found : 0 +corpus_imported : 0 +corpus_variable : 0 +max_depth : 1 +cur_item : 2 +pending_favs : 0 +pending_total : 0 +stability : 100.00% +bitmap_cvg : 0.01% +saved_crashes : 3 +saved_hangs : 0 +last_find : 0 +last_crash : 1710252309 +last_hang : 0 +execs_since_crash : 187866 +exec_timeout : 10000 +slowest_exec_ms : 0 +peak_rss_mb : 35 +cpu_affinity : 0 +edges_found : 6 +total_edges : 65536 +var_byte_count : 0 +havoc_expansion : 5 +auto_dict_entries : 0 +testcache_size : 21 +testcache_count : 2 +testcache_evict : 0 +afl_banner : /build_test/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll#afl_main +afl_version : ++4.09c +target_mode : shmem_testcase default +command_line : /usr/local/bin/afl-fuzz -T /build_test/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll#afl_main -M afl_main-worker -i /fuzz/test-out/corpus -t 10000 -o /fuzz/test-out/aflplusplus -- /usr/bin/dotnet /build_test/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll @@ diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/cmdline b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/cmdline new file mode 100644 index 00000000..c2b6ed28 --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/cmdline @@ -0,0 +1,3 @@ +/usr/bin/dotnet +/tmp/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll +@@ diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:10,op:havoc,rep:1 b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:10,op:havoc,rep:1 new file mode 100644 index 00000000..c2c357e3 --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000000,sig:02,src:000001,time:0,execs:10,op:havoc,rep:1 @@ -0,0 +1 @@ +bdax \ No newline at end of file diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000001,sig:02,src:000001,time:1,execs:12,op:havoc,rep:1 b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000001,sig:02,src:000001,time:1,execs:12,op:havoc,rep:1 new file mode 100644 index 0000000000000000000000000000000000000000..d21b57de118e99975df8dde5fbb919dc125520e3 GIT binary patch literal 4 LcmZQzNT~n-0Ym`Z literal 0 HcmV?d00001 diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000002,sig:02,src:000001,time:2,execs:24,op:havoc,rep:8 b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000002,sig:02,src:000001,time:2,execs:24,op:havoc,rep:8 new file mode 100644 index 00000000..daeedb43 --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/crashes/id:000002,sig:02,src:000001,time:2,execs:24,op:havoc,rep:8 @@ -0,0 +1 @@ +ba&&&&€ \ No newline at end of file diff --git a/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/fuzzer_stats b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/fuzzer_stats new file mode 100644 index 00000000..e134b762 --- /dev/null +++ b/casr/tests/casr_tests/casrep/afl-out-sharpfuzz/afl_s01-worker/fuzzer_stats @@ -0,0 +1,43 @@ +start_time : 1710252309 +last_update : 1710252326 +run_time : 17 +fuzzer_pid : 1466 +cycles_done : 494 +cycles_wo_finds : 494 +time_wo_finds : 0 +execs_done : 197874 +execs_per_sec : 11390.40 +execs_ps_last_min : 0.00 +corpus_count : 2 +corpus_favored : 1 +corpus_found : 0 +corpus_imported : 0 +corpus_variable : 0 +max_depth : 1 +cur_item : 1 +pending_favs : 0 +pending_total : 0 +stability : 100.00% +bitmap_cvg : 0.01% +saved_crashes : 3 +saved_hangs : 0 +last_find : 0 +last_crash : 1710252309 +last_hang : 0 +execs_since_crash : 197850 +exec_timeout : 10000 +slowest_exec_ms : 0 +peak_rss_mb : 36 +cpu_affinity : 1 +edges_found : 6 +total_edges : 65536 +var_byte_count : 0 +havoc_expansion : 5 +auto_dict_entries : 0 +testcache_size : 4 +testcache_count : 1 +testcache_evict : 0 +afl_banner : /build_test/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll#afl_s01 +afl_version : ++4.09c +target_mode : shmem_testcase default +command_line : /usr/local/bin/afl-fuzz -T /build_test/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll#afl_s01 -S afl_s01-worker -i /fuzz/test-out/corpus -t 10000 -o /fuzz/test-out/aflplusplus -a binary -p explore -- /usr/bin/dotnet /build_test/test_casr_afl_csharp/bin/test_casr_afl_csharp.dll @@ diff --git a/casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.cs b/casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.cs new file mode 100644 index 00000000..25de400b --- /dev/null +++ b/casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.cs @@ -0,0 +1,19 @@ +using SharpFuzz; + +public class Program { + public static void Main(string[] args) { + Fuzzer.OutOfProcess.Run(stream => { + using (var reader = new StreamReader(args[0])) { + if (reader.BaseStream.Length < 4) + return; + + string buffer = reader.ReadLine(); + f1(buffer); + } + }); + } + + public static void f1(string s) { + Module.f2(s); + } +} diff --git a/casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.csproj b/casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.csproj new file mode 100644 index 00000000..23b42045 --- /dev/null +++ b/casr/tests/casr_tests/csharp/test_casr_afl_csharp/test_casr_afl_csharp.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + disable + + + + + + + + diff --git a/casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.cs b/casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.cs new file mode 100644 index 00000000..b3c64eb1 --- /dev/null +++ b/casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; + +public static class Module { + public static void f2(string s) { + if (s[0] == 'b') + Console.WriteLine("Path 1T"); + else + throw new IndexOutOfRangeException("Index out of range"); + + if (s[1] == 'a') + Console.WriteLine("Path 1T"); + else + throw new ArgumentException("Parameter cannot be null"); + + if (s[2] == 'd') + Console.WriteLine("Path 1T"); + else + throw new System.IO.IOException("IO error"); + } +} diff --git a/casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.csproj b/casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.csproj new file mode 100644 index 00000000..740c693b --- /dev/null +++ b/casr/tests/casr_tests/csharp/test_casr_afl_csharp_module/test_casr_afl_csharp_module.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.cs b/casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.cs new file mode 100644 index 00000000..ac610b61 --- /dev/null +++ b/casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.cs @@ -0,0 +1,16 @@ +using System; + +public class Program { + public static void Main(string[] args) + { + f1(); + } + + public static void f1() { + f2(); + } + + public static void f2() { + throw new ArgumentException("Parameter cannot be null"); + } +} diff --git a/casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj b/casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj new file mode 100644 index 00000000..206b89a9 --- /dev/null +++ b/casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/casr/tests/tests.rs b/casr/tests/tests.rs index b29d9a20..fb08fd4d 100644 --- a/casr/tests/tests.rs +++ b/casr/tests/tests.rs @@ -1,7 +1,9 @@ +extern crate copy_dir; extern crate lazy_static; extern crate regex; extern crate serde_json; +use copy_dir::copy_dir; use regex::Regex; use serde_json::Value; use std::env; @@ -22,6 +24,7 @@ lazy_static::lazy_static! { static ref EXE_CASR_PYTHON: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-python")); static ref EXE_CASR_JAVA: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-java")); static ref EXE_CASR_JS: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-js")); + static ref EXE_CASR_CSHARP: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-csharp")); static ref EXE_CASR_GDB: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-gdb")); static ref PROJECT_DIR: RwLock<&'static str> = RwLock::new(env!("CARGO_MANIFEST_DIR")); } @@ -3864,18 +3867,7 @@ fn test_casr_ubsan() { let test_dir = abs_path("tests/tmp_tests_casr/test_casr_ubsan"); let _ = fs::remove_dir_all(&test_dir); - - let output = Command::new("cp") - .args(["-r", &work_dir, &test_dir]) - .output() - .expect("failed to copy dir"); - - assert!( - output.status.success(), - "Stdout {}.\n Stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _ = copy_dir(work_dir, &test_dir).unwrap(); let paths = [ abs_path("tests/tmp_tests_casr/test_casr_ubsan/test_ubsan.cpp"), @@ -4375,17 +4367,7 @@ fn test_casr_san_python_df() { let work_dir = abs_path("tests/casr_tests/python"); let test_dir = abs_path("tests/tmp_tests_casr/test_casr_san_python_df"); - let output = Command::new("cp") - .args(["-r", &work_dir, &test_dir]) - .output() - .expect("failed to copy dir"); - - assert!( - output.status.success(), - "Stdout {}.\n Stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _ = copy_dir(work_dir, &test_dir).unwrap(); let paths = [ abs_path("tests/tmp_tests_casr/test_casr_san_python_df/cpp_module.cpp"), @@ -4482,17 +4464,7 @@ fn test_casr_san_atheris_df() { let work_dir = abs_path("tests/casr_tests/python"); let test_dir = abs_path("tests/tmp_tests_casr/test_casr_san_atheris_df"); - let output = Command::new("cp") - .args(["-r", &work_dir, &test_dir]) - .output() - .expect("failed to copy dir"); - - assert!( - output.status.success(), - "Stdout {}.\n Stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _ = copy_dir(work_dir, &test_dir).unwrap(); let paths = [ abs_path("tests/tmp_tests_casr/test_casr_san_atheris_df/cpp_module.cpp"), @@ -4593,17 +4565,7 @@ fn test_casr_python_call_san_df() { let work_dir = abs_path("tests/casr_tests/python"); let test_dir = abs_path("tests/tmp_tests_casr/test_casr_python_call_san_df"); - let output = Command::new("cp") - .args(["-r", &work_dir, &test_dir]) - .output() - .expect("failed to copy dir"); - - assert!( - output.status.success(), - "Stdout {}.\n Stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _ = copy_dir(work_dir, &test_dir).unwrap(); let paths = [ abs_path("tests/tmp_tests_casr/test_casr_python_call_san_df/cpp_module.cpp"), @@ -4961,19 +4923,9 @@ fn test_casr_js_native() { // Copy files to tmp dir let work_dir = abs_path("tests/casr_tests/js"); let test_dir = abs_path("tests/tmp_tests_casr/test_casr_js_native"); - let _ = std::fs::remove_dir_all(&test_dir); - let output = Command::new("cp") - .args(["-r", &work_dir, &test_dir]) - .output() - .expect("failed to copy dir"); - - assert!( - output.status.success(), - "Stdout {}.\n Stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _ = std::fs::remove_dir_all(&test_dir); + let _ = copy_dir(work_dir, &test_dir).unwrap(); let paths = [ abs_path("tests"), @@ -5096,19 +5048,9 @@ fn test_casr_js_native_jsfuzz() { // Copy files to tmp dir let work_dir = abs_path("tests/casr_tests/js"); let test_dir = abs_path("tests/tmp_tests_casr/test_casr_js_native_jsfuzz"); - let _ = std::fs::remove_dir_all(&test_dir); - - let output = Command::new("cp") - .args(["-r", &work_dir, &test_dir]) - .output() - .expect("failed to copy dir"); - assert!( - output.status.success(), - "Stdout {}.\n Stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _ = std::fs::remove_dir_all(&test_dir); + let _ = copy_dir(work_dir, &test_dir).unwrap(); let paths = [ abs_path("tests"), @@ -5231,19 +5173,9 @@ fn test_casr_js_native_jazzer() { // Copy files to tmp dir let work_dir = abs_path("tests/casr_tests/js"); let test_dir = abs_path("tests/tmp_tests_casr/test_casr_js_native_jazzer"); - let _ = std::fs::remove_dir_all(&test_dir); - - let output = Command::new("cp") - .args(["-r", &work_dir, &test_dir]) - .output() - .expect("failed to copy dir"); - assert!( - output.status.success(), - "Stdout {}.\n Stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _ = std::fs::remove_dir_all(&test_dir); + let _ = copy_dir(work_dir, &test_dir).unwrap(); let paths = [ abs_path("tests"), @@ -5735,3 +5667,272 @@ fn test_casr_libfuzzer_jazzer_js_xml2js() { assert!(storage.values().all(|x| *x > 1)); } + +#[test] +#[cfg(target_arch = "x86_64")] +fn test_casr_csharp() { + let paths = [ + abs_path("tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.cs"), + abs_path("tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj"), + abs_path("tests/tmp_tests_casr/test_casr_csharp"), + abs_path("tests/tmp_tests_casr/test_casr_csharp/test_casr_csharp.cs"), + abs_path("tests/tmp_tests_casr/test_casr_csharp/test_casr_csharp.csproj"), + ]; + let _ = std::fs::create_dir_all(&paths[2]); + let _ = fs::copy(&paths[0], &paths[3]); + let _ = fs::copy(&paths[1], &paths[4]); + let Ok(dotnet_path) = which::which("dotnet") else { + panic!("No dotnet is found."); + }; + + let output = Command::new(*EXE_CASR_CSHARP.read().unwrap()) + .args([ + "--stdout", + "--", + &dotnet_path.to_str().unwrap(), + "run", + "--project", + &paths[4], + ]) + .output() + .expect("failed to start casr-csharp"); + + assert!( + output.status.success(), + "Stdout {}.\n Stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let report: Result = serde_json::from_slice(&output.stdout); + if let Ok(report) = report { + let severity_type = report["CrashSeverity"]["Type"].as_str().unwrap(); + let severity_desc = report["CrashSeverity"]["ShortDescription"] + .as_str() + .unwrap() + .to_string(); + + assert_eq!(3, report["Stacktrace"].as_array().unwrap().iter().count()); + assert_eq!(severity_type, "NOT_EXPLOITABLE"); + assert_eq!(severity_desc, "System.ArgumentException"); + assert!(report["CrashLine"] + .as_str() + .unwrap() + .contains("test_casr_csharp.cs:14")); + } else { + panic!("Couldn't parse json report file."); + } +} + +#[test] +#[cfg(target_arch = "x86_64")] +fn test_casr_afl_csharp() { + use std::collections::HashMap; + + let paths = [ + abs_path("tests/casr_tests/casrep/afl-out-sharpfuzz"), + abs_path("tests/tmp_tests_casr/casr_afl_csharp_out"), + abs_path("tests/casr_tests/csharp/test_casr_afl_csharp"), + abs_path("tests/casr_tests/csharp/test_casr_afl_csharp_module"), + abs_path("/tmp/test_casr_afl_csharp"), + abs_path("/tmp/test_casr_afl_csharp_module"), + ]; + + let _ = fs::remove_dir_all(&paths[1]); + let _ = fs::create_dir(abs_path("tests/tmp_tests_casr")); + let _ = copy_dir(&paths[2], &paths[4]).unwrap(); + let _ = copy_dir(&paths[3], &paths[5]).unwrap(); + let Ok(dotnet_path) = which::which("dotnet") else { + panic!("No dotnet is found."); + }; + + let _ = Command::new(dotnet_path.to_str().unwrap()) + .args([ + "publish", + "-o", + &format!("{}/bin", &paths[4]), + &format!("{}/test_casr_afl_csharp.csproj", &paths[4]), + ]) + .output() + .expect("dotnet publish crashed"); + + let bins = Path::new(*EXE_CASR_AFL.read().unwrap()).parent().unwrap(); + let mut output = Command::new(*EXE_CASR_AFL.read().unwrap()); + output.args(["-i", &paths[0], "-o", &paths[1]]).env( + "PATH", + format!("{}:{}", bins.display(), std::env::var("PATH").unwrap()), + ); + + let output = output.output().expect("casr-afl crashed"); + + assert!( + output.status.success(), + "Stdout {}.\n Stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let res = String::from_utf8_lossy(&output.stderr); + + assert!(!res.is_empty()); + + let re = Regex::new(r"Number of reports after deduplication: (?P\d+)").unwrap(); + let unique_cnt = re + .captures(&res) + .unwrap() + .name("unique") + .map(|x| x.as_str()) + .unwrap() + .parse::() + .unwrap(); + + assert_eq!(unique_cnt, 3, "Invalid number of deduplicated reports"); + + let re = Regex::new(r"Number of clusters: (?P\d+)").unwrap(); + let clusters_cnt = re + .captures(&res) + .unwrap() + .name("clusters") + .map(|x| x.as_str()) + .unwrap() + .parse::() + .unwrap(); + + assert_eq!(clusters_cnt, 3, "Invalid number of clusters"); + + let mut storage: HashMap = HashMap::new(); + for entry in fs::read_dir(&paths[1]).unwrap() { + let e = entry.unwrap().path(); + let fname = e.file_name().unwrap().to_str().unwrap(); + if fname.starts_with("cl") && e.is_dir() { + for file in fs::read_dir(e).unwrap() { + let mut e = file.unwrap().path(); + if e.is_file() && e.extension().is_some() && e.extension().unwrap() == "casrep" { + e = e.with_extension(""); + } + let fname = e.file_name().unwrap().to_str().unwrap(); + if let Some(v) = storage.get_mut(fname) { + *v += 1; + } else { + storage.insert(fname.to_string(), 1); + } + } + } + } + + assert!(storage.values().all(|x| *x > 1)); + let _ = fs::remove_dir_all(&paths[4]); + let _ = fs::remove_dir_all(&paths[5]); +} + +#[test] +#[cfg(target_arch = "x86_64")] +fn test_casr_afl_csharp_ignore_cmd() { + use std::collections::HashMap; + + let paths = [ + abs_path("tests/casr_tests/casrep/afl-out-sharpfuzz"), + abs_path("tests/tmp_tests_casr/casr_afl_csharp_ignore_cmd_out"), + abs_path("tests/casr_tests/csharp/test_casr_afl_csharp"), + abs_path("tests/casr_tests/csharp/test_casr_afl_csharp_module"), + abs_path("tests/tmp_tests_casr/test_casr_afl_csharp"), + abs_path("tests/tmp_tests_casr/test_casr_afl_csharp_module"), + ]; + + let _ = fs::remove_dir_all(&paths[1]); + let _ = fs::create_dir(abs_path("tests/tmp_tests_casr")); + let _ = copy_dir(&paths[2], &paths[4]).unwrap(); + let _ = copy_dir(&paths[3], &paths[5]).unwrap(); + let Ok(dotnet_path) = which::which("dotnet") else { + panic!("No dotnet is found."); + }; + + let _ = Command::new(dotnet_path.to_str().unwrap()) + .args([ + "build", + &format!("{}/test_casr_afl_csharp.csproj", &paths[4]), + ]) + .output() + .expect("dotnet build crashed"); + + let bins = Path::new(*EXE_CASR_AFL.read().unwrap()).parent().unwrap(); + let mut output = Command::new(*EXE_CASR_AFL.read().unwrap()); + output + .args([ + "--ignore-cmdline", + "-i", + &paths[0], + "-o", + &paths[1], + "--", + &dotnet_path.to_str().unwrap(), + "run", + "--no-build", + "--project", + &format!("{}/test_casr_afl_csharp.csproj", &paths[4]), + "@@", + ]) + .env( + "PATH", + format!("{}:{}", bins.display(), std::env::var("PATH").unwrap()), + ); + + let output = output.output().expect("casr-afl crashed"); + + assert!( + output.status.success(), + "Stdout {}.\n Stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let res = String::from_utf8_lossy(&output.stderr); + + assert!(!res.is_empty()); + + let re = Regex::new(r"Number of reports after deduplication: (?P\d+)").unwrap(); + let unique_cnt = re + .captures(&res) + .unwrap() + .name("unique") + .map(|x| x.as_str()) + .unwrap() + .parse::() + .unwrap(); + + assert_eq!(unique_cnt, 3, "Invalid number of deduplicated reports"); + + let re = Regex::new(r"Number of clusters: (?P\d+)").unwrap(); + let clusters_cnt = re + .captures(&res) + .unwrap() + .name("clusters") + .map(|x| x.as_str()) + .unwrap() + .parse::() + .unwrap(); + + assert_eq!(clusters_cnt, 3, "Invalid number of clusters"); + + let mut storage: HashMap = HashMap::new(); + for entry in fs::read_dir(&paths[1]).unwrap() { + let e = entry.unwrap().path(); + let fname = e.file_name().unwrap().to_str().unwrap(); + if fname.starts_with("cl") && e.is_dir() { + for file in fs::read_dir(e).unwrap() { + let mut e = file.unwrap().path(); + if e.is_file() && e.extension().is_some() && e.extension().unwrap() == "casrep" { + e = e.with_extension(""); + } + let fname = e.file_name().unwrap().to_str().unwrap(); + if let Some(v) = storage.get_mut(fname) { + *v += 1; + } else { + storage.insert(fname.to_string(), 1); + } + } + } + } + + assert!(storage.values().all(|x| *x > 1)); + let _ = fs::remove_dir_all(&paths[4]); + let _ = fs::remove_dir_all(&paths[5]); +} diff --git a/docs/usage.md b/docs/usage.md index 9c04cad7..8a3a23b7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,8 +9,11 @@ java reports and get report from [Jazzer](https://github.com/CodeIntelligenceTesting/jazzer). Use `casr-js` to analyze JavaScript reports and get report from [Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js) or -[jsfuzz](https://github.com/fuzzitdev/jsfuzz). `casr-afl` is used -to triage crashes found by [AFL++](https://github.com/AFLplusplus/AFLplusplus). +[jsfuzz](https://github.com/fuzzitdev/jsfuzz). +Use `casr-csharp` to analyze C# reports and get report from +[Sharpfuzz](https://github.com/Metalnem/sharpfuzz). `casr-afl` can triage +crashes found by [AFL++](https://github.com/AFLplusplus/AFLplusplus) and +AFL-based fuzzer [Sharpfuzz](https://github.com/Metalnem/sharpfuzz). `casr-libfuzzer` can triage crashes found by [libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) (libFuzzer, go-fuzz, Atheris, Jazzer, Jazzer.js, jsfuzz). `casr-dojo` allows to upload new and @@ -22,10 +25,10 @@ SARIF report. Reports triage (deduplication, clustering) is done by `casr-cluste Create CASR reports (.casrep) from gdb execution - Usage: casr-gdb [OPTIONS] <--stdout|--output > [-- ...] + Usage: casr-gdb [OPTIONS] <--stdout|--output > -- ... Arguments: - [ARGS]... Add "-- ./binary " to run executable + ... Add "-- ./binary " to run executable Options: -o, --output Path to save report. Path can be a directory, then report @@ -49,10 +52,10 @@ Example: Create CASR reports (.casrep) from AddressSanitizer reports - Usage: casr-san [OPTIONS] <--stdout|--output > [-- ...] + Usage: casr-san [OPTIONS] <--stdout|--output > -- ... Arguments: - [ARGS]... Add "-- ./binary " to run executable + ... Add "-- ./binary " to run executable Options: -o, --output Path to save report. Path can be a directory, then report @@ -87,10 +90,10 @@ ASAN stacktrace or Rust backtrace to analyze. If environment variable Triage errors found by UndefinedBehaviorSanitizer and create CASR reports (.casrep) - Usage: casr-ubsan [OPTIONS] --input ... --output [-- ...] + Usage: casr-ubsan [OPTIONS] --input ... --output -- ... Arguments: - [ARGS]... Add "-- " to run + ... Add "-- " to run Options: -l, --log-level Logging level [default: info] [possible values: info, @@ -124,10 +127,10 @@ deduplication to remove equal ubsan errors, then run report generation. Create CASR reports (.casrep) from python reports - Usage: casr-python [OPTIONS] <--stdout|--output > [-- ...] + Usage: casr-python [OPTIONS] <--stdout|--output > -- ... Arguments: - [ARGS]... Add "-- " to run + ... Add "-- " to run Options: -o, --output Path to save report. Path can be a directory, then report @@ -150,10 +153,10 @@ Example: Create CASR reports (.casrep) from java reports - Usage: casr-java [OPTIONS] <--stdout|--output > [-- ...] + Usage: casr-java [OPTIONS] <--stdout|--output > -- ... Arguments: - [ARGS]... Add "-- " to run + ... Add "-- " to run Options: -o, --output Path to save report. Path can be a directory, then report @@ -179,10 +182,10 @@ Run casr-java: Create CASR reports (.casrep) from JavaScript crash reports - Usage: casr-js [OPTIONS] <--stdout|--output > [-- ...] + Usage: casr-js [OPTIONS] <--stdout|--output > -- ... Arguments: - [ARGS]... Add "-- " to run + ... Add "-- " to run Options: -o, --output Path to save report. Path can be a directory, then report @@ -202,6 +205,31 @@ Run casr-js: $ casr-js -o js.casrep -- node casr/tests/casr_tests/js/test_casr_js.js +## casr-csharp + +Create CASR reports (.casrep) from C# reports + + Usage: casr-csharp [OPTIONS] <--stdout|--output > -- ... + + Arguments: + ... Add "-- " to run + + Options: + -o, --output Path to save report. Path can be a directory, then report name + is generated + --stdout Print CASR report to stdout + --stdin Stdin file for program + -t, --timeout Timeout (in seconds) for target execution, 0 value means that + timeout is disabled [default: 0] + --ignore File with regular expressions for functions and file paths that + should be ignored + -h, --help Print help + -V, --version Print version + +Run casr-csharp: + + $ casr-csharp -o csharp.casrep -- dotnet run --project casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj + ## casr-core Analyze coredump for security goals and provide detailed report with severity estimation @@ -408,17 +436,18 @@ Convert reports to SARIF report: ## casr-afl -Triage crashes found by AFL++ +Triage crashes found by AFL++/Sharpfuzz Usage: casr-afl [OPTIONS] --input --output [-- ...] Arguments: [ARGS]... Add "-- ./gdb_fuzz_target " to generate additional crash reports - with casr-gdb (e.g., test whether program crashes without sanitizers) + with casr-gdb (for compiled binaries, e.g., test whether program crashes + without sanitizers), "-- dotnet " or "-- mono " to + triage C# crashes with additional options Options: - -l, --log-level Logging level [default: info] [possible values: info, - debug] + -l, --log-level Logging level [default: info] [possible values: info, debug] -j, --jobs Number of parallel jobs for generating CASR reports [default: half of cpu cores] -t, --timeout Timeout (in seconds) for target execution, 0 value means @@ -426,8 +455,8 @@ Triage crashes found by AFL++ -i, --input AFL++ work directory -o, --output Output directory with triaged reports -f, --force-remove Remove output project directory if it exists - --ignore-cmdline Force usage to run target instead of searching for - cmdline files in AFL fuzzing directory + --ignore-cmdline Force usage to run target instead of searching for cmdline files + in AFL fuzzing directory --no-cluster Do not cluster CASR reports -h, --help Print help -V, --version Print version @@ -436,12 +465,15 @@ Triage crashes found by AFL++ instances, `casr-afl` generates crash reports depending on target binary. For binary with ASAN `casr-san` is used, otherwise `casr-gdb`. On the next step report deduplication is done by `casr-cluster`. Finally, reports are traiged into -clusters. Crash reports contain many useful information: severity (like [exploitable](https://github.com/jfoote/exploitable)), OS and package versions, command line, stack trace, register values, -disassembly, and even source code fragment where crash appeared. +clusters. Crash reports contain many useful information: severity +(like [exploitable](https://github.com/jfoote/exploitable)), OS and package +versions, command line, stack trace, register values, disassembly, and even +source code fragment where crash appeared. `casr-afl` also provides integration with AFL-based +fuzzer [Sharpfuzz](https://github.com/Metalnem/sharpfuzz). **NOTE:** `casr-gdb` and `casr-san` should be in PATH to make `casr-afl` work. -Example (Ubuntu 20.04+): +AFL++ Example (Ubuntu 20.04+): $ cp casr/tests/casr_tests/bin/load_afl /tmp/load_afl $ cp casr/tests/casr_tests/bin/load_sydr /tmp/load_sydr @@ -516,15 +548,32 @@ you can estimate crash severity for program built without sanitizers. You can set environment variable `RUST_BACKTRACE=(1|full)` for `casr-afl`. This variable may be used by [casr-san](#casr-san). +Sharpfuzz example: + + $ cp -r casr/tests/casr_tests/csharp/test_casr_afl_csharp /tmp/test_casr_afl_csharp + $ cp -r casr/tests/casr_tests/csharp/test_casr_afl_csharp_module /tmp/test_casr_afl_csharp_module + $ dotnet publish /tmp/test_casr_afl_csharp/test_casr_afl_csharp.csproj -c Debug -o /tmp/test_casr_afl_csharp/bin + $ casr-afl -i casr/tests/casr_tests/casrep/afl-out-sharpfuzz -o casr/tests/tmp_tests_casr/casr_afl_csharp_out + +Sharpfuzz example (with --ignore-cmdline): + + $ cp -r casr/tests/casr_tests/csharp/test_casr_afl_csharp /tmp/test_casr_afl_csharp + $ cp -r casr/tests/casr_tests/csharp/test_casr_afl_csharp_module /tmp/test_casr_afl_csharp_module + $ dotnet publish /tmp/test_casr_afl_csharp/test_casr_afl_csharp.csproj -c Debug -o /tmp/test_casr_afl_csharp/bin + $ casr-afl --ignore-cmdline -i casr/tests/casr_tests/casrep/afl-out-sharpfuzz -o casr/tests/tmp_tests_casr/casr_afl_csharp_out -- dotnet run --no-build --project /tmp/test_casr_afl_csharp/test_casr_afl_csharp.csproj @@ + +**NOTE:** if you run `casr-afl` for Sharpfuzz pipeline using `--ignore-cmdline` with `dotnet run`, build +your project before (via `dotnet build` or `dotnet publish`) and specify `--no-build` option for `dotnet run`. + ## casr-libfuzzer Triage crashes found by libFuzzer based fuzzer (C/C++/go-fuzz/Atheris/Jazzer/Jazzer.js/jsfuzz) - Usage: casr-libfuzzer [OPTIONS] --output [-- ...] + Usage: casr-libfuzzer [OPTIONS] --output -- ... Arguments: - [ARGS]... Add "-- ./fuzz_target " + ... Add "-- ./fuzz_target " Options: -l, --log-level diff --git a/libcasr/src/cpp.rs b/libcasr/src/cpp.rs index 42de96f6..fc94853b 100644 --- a/libcasr/src/cpp.rs +++ b/libcasr/src/cpp.rs @@ -15,12 +15,9 @@ impl Exception for CppException { .collect(); let rexception = Regex::new(r"terminate called after throwing an instance of (.+)").unwrap(); - let Some(pos) = stderr_list + let pos = stderr_list .iter() - .position(|line| rexception.is_match(line)) - else { - return None; - }; + .position(|line| rexception.is_match(line))?; let instance = rexception .captures(&stderr_list[pos]) .unwrap() diff --git a/libcasr/src/csharp.rs b/libcasr/src/csharp.rs index 194f50f5..dabf71f4 100644 --- a/libcasr/src/csharp.rs +++ b/libcasr/src/csharp.rs @@ -137,9 +137,10 @@ impl Exception for CSharpException { .split_once(": ")?; Some(ExecutionClass { + severity: "NOT_EXPLOITABLE".to_string(), short_description: exception.to_string(), description: message.to_string(), - ..ExecutionClass::default() + explanation: "".to_string(), }) } } diff --git a/libcasr/src/execution_class.rs b/libcasr/src/execution_class.rs index ab69db5e..91f6e76a 100644 --- a/libcasr/src/execution_class.rs +++ b/libcasr/src/execution_class.rs @@ -204,7 +204,7 @@ impl Default for ExecutionClass { severity: "UNDEFINED".to_string(), short_description: "Undefined".to_string(), description: "Undefined class".to_string(), - explanation: "The is no execution class for this type of exception".to_string(), + explanation: "There is no execution class for this type of exception".to_string(), } } } diff --git a/libcasr/src/js.rs b/libcasr/src/js.rs index 31b5167e..6287b51f 100644 --- a/libcasr/src/js.rs +++ b/libcasr/src/js.rs @@ -12,9 +12,7 @@ pub struct JsException; impl Exception for JsException { fn parse_exception(stderr: &str) -> Option { let rexception = Regex::new(r"(?m)^.*?(\S*Error):(?:\s+(.*))?$").unwrap(); - let Some(captures) = rexception.captures(stderr) else { - return None; - }; + let captures = rexception.captures(stderr)?; let error_type = captures.get(1).unwrap().as_str(); let message = if let Some(message) = captures.get(2) { message.as_str() diff --git a/libcasr/src/lib.rs b/libcasr/src/lib.rs index 08476134..0dc1d90f 100644 --- a/libcasr/src/lib.rs +++ b/libcasr/src/lib.rs @@ -17,6 +17,7 @@ //! * Python //! * Java //! * JavaScript +//! * C# //! //! It could be built with `exploitable` feature for severity estimation crashes //! collected from gdb. To save crash reports as json (.casrep/.sarif) use `serde` feature. diff --git a/libcasr/src/rust.rs b/libcasr/src/rust.rs index b6689b38..5ba76b66 100644 --- a/libcasr/src/rust.rs +++ b/libcasr/src/rust.rs @@ -13,9 +13,7 @@ pub struct RustPanic; impl Exception for RustPanic { fn parse_exception(stderr: &str) -> Option { let rexception = Regex::new(r"thread '.+?' panicked at (?:'(.*)'|.+?:\n(.*))").unwrap(); - let Some(captures) = rexception.captures(stderr) else { - return None; - }; + let captures = rexception.captures(stderr)?; let message = if let Some(message) = captures.get(1) { message.as_str() } else {