Skip to content

Commit

Permalink
Support JavaScript crash triaging for Jazzer.js and jsfuzz via casr-l…
Browse files Browse the repository at this point in the history
…ibfuzzer (#176)
  • Loading branch information
PaDarochek authored Dec 12, 2023
1 parent 9181c77 commit 4644ee0
Show file tree
Hide file tree
Showing 27 changed files with 1,474 additions and 31 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/amd64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ jobs:
- name: Run tests
run: |
sudo apt update && sudo apt install -y gdb pip curl python3.10-dev llvm \
openjdk-17-jdk
openjdk-17-jdk ca-certificates gnupg
pip3 install atheris
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
export NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update && sudo apt install -y nodejs
sudo npm install -g jsfuzz
sudo npm install --save-dev @jazzer.js/core
curl https://sh.rustup.rs -o rustup.sh && chmod +x rustup.sh && \
./rustup.sh -y && rm rustup.sh
rustup install nightly
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ jobs:
- name: Install Dependences
run: |
sudo apt update && sudo apt install -y gdb pip curl python3.10-dev llvm \
openjdk-17-jdk
openjdk-17-jdk ca-certificates gnupg
pip3 install atheris
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
export NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update && sudo apt install -y nodejs
sudo npm install -g jsfuzz
sudo npm install --save-dev @jazzer.js/core
curl https://sh.rustup.rs -o rustup.sh && chmod +x rustup.sh && \
./rustup.sh -y && rm rustup.sh
rustup install nightly
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ Cargo.lock
*/Cargo.lock
*/tests/tmp_tests_casr
*.swp
node_modules
*/node_modules/*
.vscode
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ ASAN reports or `casr-ubsan` to analyze UBSAN reports. Try `casr-gdb` to get
reports from gdb. Use `casr-python` to analyze python reports and get report
from [Atheris](https://github.com/google/atheris). Use `casr-java` to analyze
java reports and get report from
[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer).
[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).

Crash report contains many useful information: severity (like [exploitable](https://github.com/jfoote/exploitable))
for x86, x86\_64, arm32, aarch64, rv32g, rv64g architectures,
Expand All @@ -44,7 +47,8 @@ Triage is based on stack trace comparison from [gdb-command](https://github.com/
`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)
/[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer)).
/[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer)/[Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js)/
[jsfuzz](https://github.com/fuzzitdev/jsfuzz)).
`casr-dojo` allows to upload new and unique CASR reports to
[DefectDojo](https://github.com/DefectDojo/django-DefectDojo) (available with
`dojo` feature).
Expand Down Expand Up @@ -75,6 +79,7 @@ and program languages:
* Go
* Python
* Java
* JavaScript

It could be built with `exploitable` feature for severity estimation crashes
collected from gdb. To save crash reports as json use `serde` feature.
Expand Down Expand Up @@ -137,6 +142,10 @@ Create report from java:

$ casr-java -o java.casrep -- java casr/tests/casr_tests/java/Test1.java

Create report from JavaScript:

$ casr-js -o js.casrep -- node casr/tests/casr_tests/js/test_casr_js.js

View report:

$ casr-cli casr/tests/casr_tests/casrep/test_clustering_san/load_fuzzer_crash-120697a7f5b87c03020f321c8526adf0f4bcc2dc.casrep
Expand Down Expand Up @@ -176,8 +185,16 @@ Triage libFuzzer crashes with casr-libfuzzer:
Triage Atheris crashes with casr-libfuzzer:

$ unzip casr/tests/casr_tests/python/ruamel.zip
$ cp casr/tests/casr_tests/python/yaml_fuzzer.py .
$ casr-libfuzzer -i casr/tests/casr_tests/casrep/atheris_crashes_ruamel_yaml -o casr/tests/tmp_tests_casr/casr_libfuzzer_atheris_out -- ./yaml_fuzzer.py
$ casr-libfuzzer -i casr/tests/casr_tests/casrep/atheris_crashes_ruamel_yaml -o casr/tests/tmp_tests_casr/casr_libfuzzer_atheris_out -- casr/tests/casr_tests/python/yaml_fuzzer.py

Triage Jazzer.js crashes with casr-libfuzzer (Jazzer.js installation [guide](https://github.com/CodeIntelligenceTesting/jazzer.js#quickstart)):

$ unzip casr/tests/casr_tests/js/xml2js.zip -d xml2js
$ mkdir -p casr/tests/tmp_tests_casr/xml2js_fuzzer_out
$ cp casr/tests/casr_tests/js/test_casr_libfuzzer_jazzer_js_xml2js.js casr/tests/tmp_tests_casr/xml2js_fuzzer_out/xml2js_fuzzer.js
$ sudo npm install xml2js
$ sudo npm install --save-dev @jazzer.js/core
$ casr-libfuzzer -i ./xml2js -o casr/tests/tmp_tests_casr/xml2js_fuzzer_out/out -- npx jazzer casr/tests/tmp_tests_casr/xml2js_fuzzer_out/xml2js_fuzzer.js

Upload new and unique CASR reports to
[DefectDojo](https://github.com/DefectDojo/django-DefectDojo):
Expand All @@ -195,7 +212,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`, or `casr-java`.
are present), `casr-python`, `casr-java`, or `casr-js`.
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`.
Expand All @@ -208,7 +225,9 @@ If you use [AFL++](https://github.com/AFLplusplus/AFLplusplus), the pipeline
`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)),
(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
by `casr-libfuzzer`.

Expand Down
14 changes: 14 additions & 0 deletions casr/src/bin/casr-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,16 @@ fn build_tree_report(
tree.collapse_item(row);
}

if !report.js_report.is_empty() {
row = tree
.insert_container_item("JsReport".to_string(), Placement::After, row)
.unwrap();
report.js_report.iter().for_each(|e| {
tree.insert_item(e.clone(), Placement::LastChild, row);
});
tree.collapse_item(row);
}

if !report.source.is_empty() {
row = tree
.insert_container_item("Source".to_string(), Placement::After, row)
Expand Down Expand Up @@ -629,6 +639,10 @@ fn build_slider_report(
select.add_item("RustReport", report.rust_report.join("\n"));
}

if !report.js_report.is_empty() {
select.add_item("JsReport", report.js_report.join("\n"));
}

if !report.source.is_empty() {
select.add_item("Source", report.source.join("\n"));
}
Expand Down
3 changes: 1 addition & 2 deletions casr/src/bin/casr-cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,7 @@ fn main() -> Result<()> {
.value_parser(clap::value_parser!(u32).range(1..))
)
.get_matches();

init_ignored_frames!("cpp", "rust", "python", "go", "java");
init_ignored_frames!("cpp", "rust", "python", "go", "java", "js");

// Get number of threads
let jobs = if let Some(jobs) = matches.get_one::<u32>("jobs") {
Expand Down
2 changes: 1 addition & 1 deletion casr/src/bin/casr-java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ fn main() -> Result<()> {
)
.get_matches();

init_ignored_frames!("java"); //TODO
init_ignored_frames!("java", "cpp"); //TODO
if let Some(path) = matches.get_one::<PathBuf>("ignore") {
util::add_custom_ignored_frames(path)?;
}
Expand Down
170 changes: 170 additions & 0 deletions casr/src/bin/casr-js.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use casr::util;
use libcasr::{
exception::Exception, init_ignored_frames, js::*, report::CrashReport, stacktrace::*,
};

use anyhow::{bail, Result};
use clap::{Arg, ArgAction, ArgGroup};
use regex::Regex;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() -> Result<()> {
let matches = clap::Command::new("casr-js")
.version(clap::crate_version!())
.about("Create CASR reports (.casrep) from JavaScript crash 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).range(0..))
)
.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("ARGS")
.action(ArgAction::Set)
.num_args(1..)
.last(true)
.help("Add \"-- <path> <arguments>\" to run"),
)
.get_matches();

init_ignored_frames!("js", "cpp");
if let Some(path) = matches.get_one::<PathBuf>("ignore") {
util::add_custom_ignored_frames(path)?;
}
// Get program args.
let argv: Vec<&str> = if let Some(argvs) = matches.get_many::<String>("ARGS") {
argvs.map(|s| s.as_str()).collect()
} else {
bail!("Wrong arguments for starting program");
};

// Get stdin for target program.
let stdin_file = util::stdin_from_matches(&matches)?;

// Get timeout
let timeout = *matches.get_one::<u64>("timeout").unwrap();

// Run program.
let mut js_cmd = Command::new(argv[0]);
if let Some(ref file) = stdin_file {
js_cmd.stdin(std::fs::File::open(file)?);
}
if argv.len() > 1 {
js_cmd.args(&argv[1..]);
}
let js_result = util::get_output(&mut js_cmd, timeout, true)?;

let js_stderr = String::from_utf8_lossy(&js_result.stderr);

// Create report.
let mut report = CrashReport::new();
// Set executable path.
report.executable_path = argv[0].to_string();
let mut path_to_tool = PathBuf::new();
path_to_tool.push(argv[0]);
if argv.len() > 1 {
let fpath = Path::new(argv[0]);
if let Some(fname) = fpath.file_name() {
path_to_tool = if fname == fpath.as_os_str() {
let Ok(full_path_to_tool) = which::which(fname) else {
bail!("{} is not found in PATH", argv[0]);
};
full_path_to_tool
} else {
fpath.to_path_buf()
};
if !path_to_tool.exists() {
bail!("Could not find the tool in the specified path {}", argv[0]);
}
let fname = fname.to_string_lossy();
if (fname == "node" || fname == "jsfuzz") && argv[1].ends_with(".js") {
report.executable_path = argv[1].to_string();
} else if argv.len() > 2
&& fname == "npx"
&& argv[1] == "jazzer"
&& argv[2].ends_with(".js")
{
report.executable_path = argv[2].to_string();
}
}
}
report.proc_cmdline = argv.join(" ");
let _ = report.add_os_info();
let _ = report.add_proc_environ();

// Get JS report.
let js_stderr_list: Vec<String> = js_stderr.split('\n').map(|l| l.to_string()).collect();
let re = Regex::new(r"^(?:.*Error:(?:\s+.*)?|Thrown at:)$").unwrap();
if let Some(start) = js_stderr_list.iter().position(|x| re.is_match(x)) {
report.js_report = js_stderr_list[start..].to_vec();
report
.js_report
.retain(|x| !x.is_empty() && (x.trim().starts_with("at") || x.contains("Error:")));
let report_str = report.js_report.join("\n");
report.stacktrace = JsStacktrace::extract_stacktrace(&report_str)?;
if let Some(exception) = JsException::parse_exception(&report.js_report[0]) {
report.execution_class = exception;
}
} else {
// Call casr-san with absolute path to interpreter/fuzzer
let mut modified_argv = argv.clone();
modified_argv[0] = path_to_tool.to_str().unwrap_or(argv[0]);
return util::call_casr_san(&matches, &modified_argv, "casr-js");
}

if let Ok(crash_line) = JsStacktrace::parse_stacktrace(&report.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;
}
}
}

//Output report
util::output_report(&report, &matches, &argv)
}
7 changes: 6 additions & 1 deletion casr/src/bin/casr-libfuzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::path::{Path, PathBuf};
fn main() -> Result<()> {
let matches = clap::Command::new("casr-libfuzzer")
.version(clap::crate_version!())
.about("Triage crashes found by libFuzzer based fuzzer (C/C++/go-fuzz/Atheris/Jazzer)")
.about("Triage crashes found by libFuzzer based fuzzer (C/C++/go-fuzz/Atheris/Jazzer/Jazzer.js/jsfuzz)")
.term_width(90)
.arg(
Arg::new("log-level")
Expand Down Expand Up @@ -128,6 +128,11 @@ fn main() -> Result<()> {
"casr-python"
} else if argv[0].ends_with("jazzer") || argv[0].ends_with("java") {
"casr-java"
} else if argv[0].ends_with("node")
|| argv.len() > 1 && argv[0].ends_with("npx") && argv[1] == "jazzer"
|| argv[0].ends_with("jsfuzz")
{
"casr-js"
} else {
let sym_list = util::symbols_list(Path::new(argv[0]))?;
if sym_list.contains("__asan") || sym_list.contains("runtime.go") {
Expand Down
2 changes: 1 addition & 1 deletion casr/src/bin/casr-python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fn main() -> Result<()> {
)
.get_matches();

init_ignored_frames!("python");
init_ignored_frames!("python", "cpp");
if let Some(path) = matches.get_one::<PathBuf>("ignore") {
util::add_custom_ignored_frames(path)?;
}
Expand Down
12 changes: 12 additions & 0 deletions casr/tests/casr_tests/js/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"targets": [
{
"cflags": [ "-fexceptions -fsanitize=address,fuzzer-no-link -O0 -g -fPIC" ],
"cflags_cc": [ "-fexceptions -fsanitize=address,fuzzer-no-link -O0 -g -fPIC" ],
"include_dirs" : ["<!@(node -p \"require('node-addon-api').include\")"],
"target_name": "native",
"sources": [ "native.cpp" ],
'defines': [ 'NAPI_CPP_EXCEPTIONS' ]
}
]
}
Binary file added casr/tests/casr_tests/js/crashes.zip
Binary file not shown.
Loading

0 comments on commit 4644ee0

Please sign in to comment.