diff options
| author | Mason Reed <mason@vector35.com> | 2024-10-23 20:47:31 -0400 |
|---|---|---|
| committer | Mason Reed <mason@vector35.com> | 2024-10-24 17:03:11 -0400 |
| commit | e151425087b6c0ee8c4d099bfc2d6d1268201db0 (patch) | |
| tree | 25160cf81aa6d650cf1497a078acc50f0062757c /plugins | |
| parent | 0f53c21d6a7abbfd48fd59ec7c15586f02777b7e (diff) | |
Add WARP integration
https://github.com/Vector35/warp/
Diffstat (limited to 'plugins')
23 files changed, 2664 insertions, 0 deletions
diff --git a/plugins/warp/.gitignore b/plugins/warp/.gitignore new file mode 100644 index 00000000..04d953d3 --- /dev/null +++ b/plugins/warp/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +!bin/
\ No newline at end of file diff --git a/plugins/warp/CMakeLists.txt b/plugins/warp/CMakeLists.txt new file mode 100644 index 00000000..acfbf41e --- /dev/null +++ b/plugins/warp/CMakeLists.txt @@ -0,0 +1,128 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +project(warp_ninja) + +if(NOT BN_API_BUILD_EXAMPLES AND NOT BN_INTERNAL_BUILD) + if(NOT BN_API_PATH) + # If we have not already defined the API source directory try and find it. + find_path( + BN_API_PATH + NAMES binaryninjaapi.h + # List of paths to search for the clone of the api + HINTS ../../.. ../../binaryninja/api/ binaryninjaapi binaryninja-api $ENV{BN_API_PATH} + REQUIRED + ) + endif() + set(CARGO_STABLE_VERSION 1.77.0) + add_subdirectory(${BN_API_PATH} binaryninjaapi) +endif() + +file(GLOB_RECURSE PLUGIN_SOURCES CONFIGURE_DEPENDS + ${PROJECT_SOURCE_DIR}/Cargo.toml + ${PROJECT_SOURCE_DIR}/src/*.rs) + +if(CMAKE_BUILD_TYPE MATCHES Debug) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/debug) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target) +else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --release) +endif() + +if(FORCE_COLORED_OUTPUT) + set(CARGO_OPTS ${CARGO_OPTS} --color always) +endif() + +set(CARGO_FEATURES "") +set(OUTPUT_FILE_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}) +set(OUTPUT_PDB_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) +set(OUTPUT_FILE_PATH ${BN_CORE_PLUGIN_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}) +set(OUTPUT_PDB_PATH ${BN_CORE_PLUGIN_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) + +add_custom_target(${PROJECT_NAME} ALL DEPENDS ${OUTPUT_FILE_PATH}) +add_dependencies(${PROJECT_NAME} binaryninjaapi) +get_target_property(BN_API_SOURCE_DIR binaryninjaapi SOURCE_DIR) +list(APPEND CMAKE_MODULE_PATH "${BN_API_SOURCE_DIR}/cmake") +find_package(BinaryNinjaCore REQUIRED) + +set(BINJA_LIB_DIR ${BN_INSTALL_BIN_DIR}) + +set_property(TARGET ${PROJECT_NAME} PROPERTY OUTPUT_FILE_PATH ${OUTPUT_FILE_PATH}) + +# Add the whole api to the depends too +file(GLOB_RECURSE API_SOURCES CONFIGURE_DEPENDS + ${BN_API_SOURCE_DIR}/binaryninjacore.h + ${BN_API_SOURCE_DIR}/rust/src/*.rs + ${BN_API_SOURCE_DIR}/rust/binaryninjacore-sys/src/*.rs) + +find_program(RUSTUP_PATH rustup REQUIRED HINTS ~/.cargo/bin) +set(RUSTUP_COMMAND ${RUSTUP_PATH} run ${CARGO_STABLE_VERSION} cargo) + +if(APPLE) + if(UNIVERSAL) + if(CMAKE_BUILD_TYPE MATCHES Debug) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release/${OUTPUT_FILE_NAME}) + endif() + + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=aarch64-apple-darwin ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=x86_64-apple-darwin ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=aarch64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=x86_64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND lipo -create ${AARCH64_LIB_PATH} ${X86_64_LIB_PATH} -output ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + else() + if(CMAKE_BUILD_TYPE MATCHES Debug) + set(LIB_PATH ${PROJECT_BINARY_DIR}/target/debug/${OUTPUT_FILE_NAME}) + else() + set(LIB_PATH ${PROJECT_BINARY_DIR}/target/release/${OUTPUT_FILE_NAME}) + endif() + + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${LIB_PATH} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + endif() +elseif(WIN32) + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_PDB_NAME} ${OUTPUT_PDB_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) +else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) +endif() diff --git a/plugins/warp/Cargo.toml b/plugins/warp/Cargo.toml new file mode 100644 index 00000000..5527eca3 --- /dev/null +++ b/plugins/warp/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "warp_ninja" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["lib", "cdylib"] + +[dependencies] +binaryninja = { path = "../../rust", features = ["rayon"] } +binaryninjacore-sys = { path = "../../rust/binaryninjacore-sys" } +warp = { git = "https://github.com/Vector35/warp/", rev = "dc51fd5" } +log = "0.4" +arboard = "3.4" +rayon = "1.10" +dashmap = "6.1" +walkdir = "2.5" +fastbloom = "0.7" +# For sigem +env_logger = "0.11.5" +clap = { version = "4.5.16", features = ["derive"] } +ar = { git = "https://github.com/mdsteele/rust-ar" } +tempdir = "0.3.7" +serde_json = "1.0.132" + +[build-dependencies] +cc = "1.1.28" + +[dev-dependencies] +criterion = "0.5.1" +insta = { version = "1.38.0", features = ["yaml"] } + +[profile.release] +panic = "abort" +lto = true +debug = "full" + +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 + +[[bench]] +name = "guid" +harness = false + +[[bench]] +name = "convert" +harness = false + +[[bench]] +name = "function" +harness = false
\ No newline at end of file diff --git a/plugins/warp/benches/convert.rs b/plugins/warp/benches/convert.rs new file mode 100644 index 00000000..e916d5df --- /dev/null +++ b/plugins/warp/benches/convert.rs @@ -0,0 +1,39 @@ +use binaryninja::binaryview::BinaryViewExt; +use binaryninja::headless::Session; +use binaryninja::types::Conf; +use criterion::{criterion_group, criterion_main, Criterion}; +use std::path::PathBuf; +use warp_ninja::convert::from_bn_type; + +pub fn type_conversion_benchmark(c: &mut Criterion) { + let session = Session::new(); + let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); + for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") { + let entry = entry.expect("Failed to read directory entry"); + let path = entry.path(); + if path.is_file() { + if let Some(bv) = session.load(path.to_str().unwrap()) { + let functions = bv.functions(); + c.bench_function("type conversion all functions", |b| { + b.iter(|| { + for func in &functions { + from_bn_type(&bv, func.function_type(), u8::MAX); + } + }) + }); + + let types = bv.types(); + c.bench_function("type conversion all types", |b| { + b.iter(|| { + for ty in &types { + from_bn_type(&bv, ty.type_object().clone(), u8::MAX); + } + }) + }); + } + } + } +} + +criterion_group!(benches, type_conversion_benchmark); +criterion_main!(benches); diff --git a/plugins/warp/benches/function.rs b/plugins/warp/benches/function.rs new file mode 100644 index 00000000..c6f6d70d --- /dev/null +++ b/plugins/warp/benches/function.rs @@ -0,0 +1,42 @@ +use binaryninja::binaryview::BinaryViewExt; +use binaryninja::headless::Session; +use criterion::{criterion_group, criterion_main, Criterion}; +use rayon::prelude::*; +use warp_ninja::build_function; +use warp_ninja::cache::FunctionCache; + +pub fn function_benchmark(c: &mut Criterion) { + let session = Session::new(); + let bv = session.load(env!("TEST_BIN_LIBRARY_OBJ")).unwrap(); + let functions = bv.functions(); + assert_eq!(functions.len(), 6); + let mut function_iter = functions.into_iter(); + let first_function = function_iter.next().unwrap(); + + c.bench_function("signature first function", |b| { + b.iter(|| { + let _ = build_function(&first_function, &first_function.low_level_il().unwrap()); + }) + }); + + c.bench_function("signature all functions", |b| { + b.iter(|| { + for func in &functions { + let _ = build_function(&func, &func.low_level_il().unwrap()); + } + }) + }); + + let cache = FunctionCache::default(); + c.bench_function("signature all functions rayon", |b| { + b.iter(|| { + functions + .par_iter() + .map_with(cache.clone(), |par_cache, func| par_cache.function(&func)) + .collect::<Vec<_>>() + }) + }); +} + +criterion_group!(benches, function_benchmark); +criterion_main!(benches); diff --git a/plugins/warp/benches/guid.rs b/plugins/warp/benches/guid.rs new file mode 100644 index 00000000..577f80b5 --- /dev/null +++ b/plugins/warp/benches/guid.rs @@ -0,0 +1,22 @@ +use binaryninja::binaryview::BinaryViewExt; +use binaryninja::headless::Session; +use criterion::{criterion_group, criterion_main, Criterion}; +use warp_ninja::function_guid; + +pub fn guid_benchmark(c: &mut Criterion) { + let session = Session::new(); + let bv = session.load(env!("TEST_BIN_LIBRARY_OBJ")).unwrap(); + let functions = bv.functions(); + assert_eq!(functions.len(), 6); + let mut function_iter = functions.into_iter(); + let first_function = function_iter.next().unwrap(); + + c.bench_function("function guid", |b| { + b.iter(|| { + function_guid(&first_function, &vec![]); + }) + }); +} + +criterion_group!(benches, guid_benchmark); +criterion_main!(benches); diff --git a/plugins/warp/build.rs b/plugins/warp/build.rs new file mode 100644 index 00000000..fe4d31f8 --- /dev/null +++ b/plugins/warp/build.rs @@ -0,0 +1,67 @@ +use std::path::PathBuf; +use std::process::Command; + +fn compile_rust(file: PathBuf) -> bool { + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let rustc = std::env::var_os("RUSTC").unwrap(); + let rustc = rustc.to_str().unwrap(); + let mut rustc = rustc.split('\x1f'); + let mut cmd = Command::new(rustc.next().unwrap()); + cmd.args(rustc) + .arg("--crate-type=rlib") + .arg("--out-dir") + .arg(out_dir) + .arg(file); + cmd.status().expect("failed to invoke rustc").success() +} + +fn main() { + let link_path = std::env::var_os("BINARYNINJADIR").expect("BINARYNINJADIR specified"); + let out_dir = std::env::var_os("OUT_DIR").expect("OUT_DIR specified"); + let out_dir_path = PathBuf::from(out_dir); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(not(target_os = "windows"))] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } + + // Copy all binaries to OUT_DIR for unit tests. + let bin_dir: PathBuf = "fixtures/bin".into(); + if let Ok(entries) = std::fs::read_dir(bin_dir) { + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + let file_name = path.file_name().unwrap(); + let dest_path = out_dir_path.join(file_name); + std::fs::copy(&path, &dest_path).expect("failed to copy binary to OUT_DIR"); + } + } + } + + // Compile all .c files in fixtures/src directory for unit tests. + let src_dir: PathBuf = "fixtures/src".into(); + if let Ok(entries) = std::fs::read_dir(src_dir) { + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + match path.extension().map(|s| s.to_str().unwrap()) { + Some("c") => { + cc::Build::new() + .file(&path) + .compile(path.file_stem().unwrap().to_str().unwrap()); + } + Some("rs") => { + compile_rust(path); + } + _ => {} + } + } + } +} diff --git a/plugins/warp/fixtures/src/library.c b/plugins/warp/fixtures/src/library.c new file mode 100644 index 00000000..66a84ad5 --- /dev/null +++ b/plugins/warp/fixtures/src/library.c @@ -0,0 +1,33 @@ +#include <stdio.h> + +#include "library.h" + +int myFunction(int x) +{ + printf("%d\n", x); + return x; +} + +int recursiveFunc(int x); +int otherFunction(int x) +{ + x += 5; + if (x < 10) return otherFunction(x); + return recursiveFunc(x); +} + +int recursiveFunc(int x) +{ + if (x <= 0) return 0; + return x + otherFunction(x - 1); +} + +struct MyStruct myFunction2(int x) +{ + printf("MyStruct %d\n", x); + struct MyStruct myStruct; + myStruct.a = recursiveFunc(x); + myStruct.b = x * 10; + myStruct.c = "my struct"; + return myStruct; +}
\ No newline at end of file diff --git a/plugins/warp/fixtures/src/library.h b/plugins/warp/fixtures/src/library.h new file mode 100644 index 00000000..ec7a6e63 --- /dev/null +++ b/plugins/warp/fixtures/src/library.h @@ -0,0 +1,11 @@ +#pragma once + +struct MyStruct { + int a; + int b; + const char* c; + struct MyStruct* d; +}; + +int myFunction(int x); +struct MyStruct myFunction2(int x);
\ No newline at end of file diff --git a/plugins/warp/fixtures/src/simple.c b/plugins/warp/fixtures/src/simple.c new file mode 100644 index 00000000..6e7fbd43 --- /dev/null +++ b/plugins/warp/fixtures/src/simple.c @@ -0,0 +1,15 @@ +#include <stdio.h> + +#include "library.h" + +int simple() { + printf("This is main!\n"); + + printf("calling myFunction\n"); + int returnVal = myFunction(55); + + printf("calling myFunction2\n"); + struct MyStruct myStruct = myFunction2(returnVal); + + return myStruct.b; +}
\ No newline at end of file diff --git a/plugins/warp/src/bin/sigem.rs b/plugins/warp/src/bin/sigem.rs new file mode 100644 index 00000000..f0551ccc --- /dev/null +++ b/plugins/warp/src/bin/sigem.rs @@ -0,0 +1,176 @@ +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use ar::Archive; +use clap::{arg, Parser}; +use rayon::prelude::*; + +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use serde_json::json; +use warp::signature::Data; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Path of the binary/BNDB to generate signatures of + #[arg(index = 1)] + binary: PathBuf, + + /// The signature output file + #[arg(index = 2)] + output: Option<PathBuf>, + + /// Should we overwrite output file + /// + /// NOTE: If the file exists we will exit early to prevent wasted effort. + /// NOTE: If the file is created while we are running it will still be overwritten. + #[arg(short, long)] + overwrite: Option<bool>, + + /// The external debug information file to use + #[arg(short, long)] + debug_info: Option<PathBuf>, +} + +fn main() { + let args = Args::parse(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + // If no output file was given, just prepend binary with extension sbin + let output_file = args.output.unwrap_or(args.binary.with_extension("sbin")); + + if output_file.exists() && !args.overwrite.unwrap_or(false) { + log::info!("Output file already exists, skipping... {:?}", output_file); + return; + } + + log::debug!("Starting Binary Ninja session..."); + let _headless_session = binaryninja::headless::Session::new(); + + log::info!("Creating functions for {:?}...", args.binary); + let start = std::time::Instant::now(); + let data = data_from_file(&args.binary).expect("Failed to read data"); + log::info!("Functions created in {:?}", start.elapsed()); + + // TODO: Add a way to override the symbol type to make it a different function symbol. + // TODO: Right now the consumers must dictate that. + // TODO: The binja_warp consumer sets this to library function fwiw + + if !data.functions.is_empty() { + std::fs::write(&output_file, data.to_bytes()).expect("Failed to write functions to file"); + log::info!( + "{} functions written to {:?}...", + data.functions.len(), + output_file + ); + } else { + log::warn!("No functions found for binary {:?}...", args.binary); + } +} + +fn data_from_view(view: &BinaryView) -> Data { + let mut data = Data::default(); + + let functions = view + .functions() + .par_iter() + .filter(|f| !f.symbol().short_name().as_str().contains("sub_") || f.has_user_annotations()) + .filter_map(|f| { + let llil = f.low_level_il().ok()?; + warp_ninja::cache::cached_function(&f, &llil) + }) + .collect::<Vec<_>>(); + + data.functions = functions; + data +} + +fn data_from_archive<R: Read>(mut archive: Archive<R>) -> Option<Data> { + // TODO: I feel like this is a hack... + let temp_dir = tempdir::TempDir::new("tmp_archive").ok()?; + // Iterate through the entries in the ar file and make a temp dir with them + let mut entry_files = Vec::new(); + while let Some(entry) = archive.next_entry() { + match entry { + Ok(mut entry) => { + let name = String::from_utf8_lossy(entry.header().identifier()).to_string(); + // Write entry data to a temp directory + let output_path = temp_dir.path().join(name); + let mut output_file = + File::create(&output_path).expect("Failed to create entry file"); + std::io::copy(&mut entry, &mut output_file).expect("Failed to read entry data"); + entry_files.push(output_path); + } + Err(e) => { + log::error!("Failed to read archive entry: {}", e); + } + } + } + + // Create the data. + // TODO: into_par_iter will corrupt the heap frequently, you should basically always restrict + // TODO: With RAYON_NUM_THREADS (set to like... 1) + let entry_data = entry_files + .into_par_iter() + .filter_map(|path| { + log::debug!("Creating data for ENTRY {:?}...", path); + data_from_file(&path) + }) + .collect::<Vec<_>>(); + + // TODO: Cloning here is unnecessary + Some(Data { + functions: entry_data + .iter() + .flat_map(|d| d.functions.to_owned()) + .collect(), + types: entry_data.iter().flat_map(|d| d.types.to_owned()).collect(), + }) +} + +// TODO: Pass settings. +fn data_from_file(path: &Path) -> Option<Data> { + // TODO: Add external debug info files. + // TODO: Support IDB's through debug info + let settings_json = json!({ + "analysis.linearSweep.autorun": true, + "analysis.signatureMatcher.autorun": false, + "analysis.plugins.WARPMatcher": true, + }); + + match path.extension() { + Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => { + let archive_file = File::open(path).expect("Failed to open archive file"); + let archive = Archive::new(archive_file); + data_from_archive(archive) + } + _ => { + let path_str = path.to_str().unwrap(); + let view = + binaryninja::load_with_options(path_str, true, Some(settings_json.to_string()))?; + Some(data_from_view(&view)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_data_from_file() { + env_logger::init(); + // TODO: Store oracles here to get more out of this test. + let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); + let _headless_session = binaryninja::headless::Session::new(); + for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") { + let entry = entry.expect("Failed to read directory entry"); + let path = entry.path(); + if path.is_file() { + let result = data_from_file(&path); + assert!(result.is_some()); + } + } + } +} diff --git a/plugins/warp/src/cache.rs b/plugins/warp/src/cache.rs new file mode 100644 index 00000000..79e974b4 --- /dev/null +++ b/plugins/warp/src/cache.rs @@ -0,0 +1,270 @@ +use binaryninja::architecture::Architecture; +use binaryninja::binaryview::{BinaryView, BinaryViewBase, BinaryViewExt}; +use binaryninja::function::Function as BNFunction; +use binaryninja::llil; +use binaryninja::llil::{FunctionMutability, NonSSA, NonSSAVariant}; +use binaryninja::rc::Guard; +use binaryninja::rc::Ref as BNRef; +use dashmap::try_result::TryResult; +use dashmap::DashMap; +use std::collections::HashSet; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::OnceLock; +use warp::signature::function::constraints::FunctionConstraint; +use warp::signature::function::{Function, FunctionGUID}; + +use crate::convert::from_bn_symbol; +use crate::{build_function, function_guid}; + +pub static FUNCTION_CACHE: OnceLock<DashMap<ViewID, FunctionCache>> = OnceLock::new(); +pub static GUID_CACHE: OnceLock<DashMap<ViewID, GUIDCache>> = OnceLock::new(); + +pub fn cached_function<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + function: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, +) -> Option<Function> { + let view = function.view(); + let view_id = ViewID::from(view.as_ref()); + let function_cache = FUNCTION_CACHE.get_or_init(Default::default); + match function_cache.get(&view_id) { + Some(cache) => cache.function(function, llil), + None => { + let cache = FunctionCache::default(); + let function = cache.function(function, llil); + function_cache.insert(view_id, cache); + function + } + } +} + +pub fn cached_call_site_constraints(function: &BNFunction) -> HashSet<FunctionConstraint> { + let view = function.view(); + let view_id = ViewID::from(view); + let guid_cache = GUID_CACHE.get_or_init(Default::default); + match guid_cache.get(&view_id) { + Some(cache) => cache.call_site_constraints(function), + None => { + let cache = GUIDCache::default(); + let constraints = cache.call_site_constraints(function); + guid_cache.insert(view_id, cache); + constraints + } + } +} + +pub fn cached_adjacency_constraints(function: &BNFunction) -> HashSet<FunctionConstraint> { + let view = function.view(); + let view_id = ViewID::from(view); + let guid_cache = GUID_CACHE.get_or_init(Default::default); + match guid_cache.get(&view_id) { + Some(cache) => cache.adjacency_constraints(function), + None => { + let cache = GUIDCache::default(); + let constraints = cache.adjacency_constraints(function); + guid_cache.insert(view_id, cache); + constraints + } + } +} + +pub fn cached_function_guid<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + function: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, +) -> Option<FunctionGUID> { + let view = function.view(); + let view_id = ViewID::from(view); + let guid_cache = GUID_CACHE.get_or_init(Default::default); + match guid_cache.get(&view_id) { + Some(cache) => cache.function_guid(function, llil), + None => { + let cache = GUIDCache::default(); + let guid = cache.function_guid(function, llil); + guid_cache.insert(view_id, cache); + guid + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct FunctionCache { + pub cache: DashMap<FunctionID, Option<Function>>, +} + +impl FunctionCache { + pub fn function<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + &self, + function: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, + ) -> Option<Function> { + let function_id = FunctionID::from(function); + match self.cache.try_get_mut(&function_id) { + TryResult::Present(function) => function.value().to_owned(), + TryResult::Absent => { + let function = build_function(function, llil); + self.cache.insert(function_id, function.clone()); + function + } + TryResult::Locked => build_function(function, llil), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct GUIDCache { + pub cache: DashMap<FunctionID, Option<FunctionGUID>>, +} + +impl GUIDCache { + pub fn call_site_constraints(&self, function: &BNFunction) -> HashSet<FunctionConstraint> { + let view = function.view(); + let func_id = FunctionID::from(function); + let func_start = function.start(); + let mut constraints = HashSet::new(); + for call_site in &function.call_sites() { + for cs_ref in &view.get_code_refs(call_site.address) { + let cs_ref_func = cs_ref.function(); + let cs_ref_func_id = FunctionID::from(cs_ref_func); + if cs_ref_func_id != func_id { + if let Some(cs_ref_func_llil) = cs_ref_func.low_level_il_if_available() { + // Function references another function, constrain on the pattern. + // TODO: If function is trivial thunk we should _also_ insert the tailcall target as a constraint. + let call_site_offset: i64 = func_start as i64 - call_site.address as i64; + constraints.insert(self.function_constraint( + cs_ref_func, + &cs_ref_func_llil, + call_site_offset, + )); + } + } + } + } + constraints + } + + pub fn adjacency_constraints(&self, function: &BNFunction) -> HashSet<FunctionConstraint> { + let view = function.view(); + let func_id = FunctionID::from(function); + let func_start = function.start(); + let mut constraints = HashSet::new(); + + let mut func_addr_constraint = |func_start_addr| { + // NOTE: We could potentially have dozens of functions all at the same start address. + for curr_func in &view.functions_at(func_start_addr) { + let curr_func_id = FunctionID::from(curr_func.as_ref()); + if curr_func_id != func_id { + // NOTE: We have to get the llil here for the function which is problematic for running + // NOTE: within a workflow (before analysis has finished) + if let Some(curr_func_llil) = curr_func.low_level_il_if_available() { + // Function adjacent to another function, constrain on the pattern. + let curr_addr_offset = (func_start_addr as i64) - func_start as i64; + constraints.insert(self.function_constraint( + &curr_func, + &curr_func_llil, + curr_addr_offset, + )); + } + } + } + }; + + let mut before_func_start = func_start; + for _ in 0..2 { + before_func_start = view.function_start_before(before_func_start); + func_addr_constraint(before_func_start); + } + + let mut after_func_start = func_start; + for _ in 0..2 { + after_func_start = view.function_start_after(after_func_start); + func_addr_constraint(after_func_start); + } + + constraints + } + + /// Construct a function constraint, must pass the offset at which it is located. + pub fn function_constraint<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + &self, + function: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, + offset: i64, + ) -> FunctionConstraint { + let guid = self.function_guid(function, llil); + let symbol = from_bn_symbol(&function.symbol()); + FunctionConstraint { + guid, + symbol: Some(symbol), + offset, + } + } + + pub fn function_guid<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + &self, + function: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, + ) -> Option<FunctionGUID> { + let function_id = FunctionID::from(function); + match self.cache.try_get_mut(&function_id) { + TryResult::Present(function_guid) => function_guid.value().to_owned(), + TryResult::Absent => { + let function_guid = function_guid(function, llil); + self.cache.insert(function_id, function_guid); + function_guid + } + TryResult::Locked => function_guid(function, llil), + } + } +} + +/// A unique view ID, used for caching. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ViewID(u64); + +impl From<&BinaryView> for ViewID { + fn from(value: &BinaryView) -> Self { + let mut hasher = DefaultHasher::new(); + hasher.write_u64(value.original_image_base()); + hasher.write(value.view_type().to_bytes()); + hasher.write_u64(value.entry_point()); + hasher.write(value.file().filename().to_bytes()); + Self(hasher.finish()) + } +} + +impl From<BNRef<BinaryView>> for ViewID { + fn from(value: BNRef<BinaryView>) -> Self { + Self::from(value.as_ref()) + } +} + +impl From<Guard<'_, BinaryView>> for ViewID { + fn from(value: Guard<'_, BinaryView>) -> Self { + Self::from(value.as_ref()) + } +} + +/// A unique function ID, used for caching. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct FunctionID(u64); + +impl From<&BNFunction> for FunctionID { + fn from(value: &BNFunction) -> Self { + let mut hasher = DefaultHasher::new(); + hasher.write_u64(value.start()); + hasher.write_u64(value.lowest_address()); + hasher.write_u64(value.highest_address()); + Self(hasher.finish()) + } +} + +impl From<BNRef<BNFunction>> for FunctionID { + fn from(value: BNRef<BNFunction>) -> Self { + Self::from(value.as_ref()) + } +} + +impl From<Guard<'_, BNFunction>> for FunctionID { + fn from(value: Guard<'_, BNFunction>) -> Self { + Self::from(value.as_ref()) + } +} diff --git a/plugins/warp/src/convert.rs b/plugins/warp/src/convert.rs new file mode 100644 index 00000000..cbb47858 --- /dev/null +++ b/plugins/warp/src/convert.rs @@ -0,0 +1,688 @@ +use std::collections::HashSet; + +use binaryninja::architecture::Architecture as BNArchitecture; +use binaryninja::architecture::ArchitectureExt; +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use binaryninja::callingconvention::CallingConvention as BNCallingConvention; +use binaryninja::rc::Ref as BNRef; +use binaryninja::symbol::{Symbol as BNSymbol, SymbolType as BNSymbolType}; +use binaryninja::types::{ + BaseStructure as BNBaseStructure, Conf as BNConf, EnumerationBuilder as BNEnumerationBuilder, + FunctionParameter as BNFunctionParameter, MemberAccess as BNMemberAccess, MemberAccess, + MemberScope as BNMemberScope, NamedTypeReference, NamedTypeReference as BNNamedTypeReference, + NamedTypeReferenceClass, StructureBuilder as BNStructureBuilder, + StructureMember as BNStructureMember, +}; +use binaryninja::types::{ + StructureType as BNStructureType, Type as BNType, TypeClass as BNTypeClass, +}; + +use warp::r#type::class::array::ArrayModifiers; +use warp::r#type::class::function::{Location, RegisterLocation}; +use warp::r#type::class::pointer::PointerAddressing; +use warp::r#type::class::structure::StructureMemberModifiers; +use warp::r#type::class::{ + ArrayClass, BooleanClass, CallingConvention, CharacterClass, EnumerationClass, + EnumerationMember, FloatClass, FunctionClass, FunctionMember, IntegerClass, PointerClass, + ReferrerClass, StructureClass, StructureMember, TypeClass, +}; +use warp::r#type::guid::TypeGUID; +use warp::r#type::Type; +use warp::symbol::class::SymbolClass; +use warp::symbol::{Symbol, SymbolModifiers}; + +pub fn from_bn_symbol(raw_symbol: &BNSymbol) -> Symbol { + // TODO: Use this? + let _is_export = raw_symbol.external(); + let symbol_name = raw_symbol.raw_name().to_string(); + match raw_symbol.sym_type() { + BNSymbolType::ImportAddress => { + todo!() + } + BNSymbolType::Data => { + Symbol::new( + symbol_name, + // TODO: Data? + SymbolClass::Data, + SymbolModifiers::default(), + ) + } + BNSymbolType::Symbolic => { + todo!() + } + BNSymbolType::LocalLabel => { + todo!() + } + // BN External is our Exported + BNSymbolType::External => Symbol::new( + symbol_name, + // TODO: Data? + SymbolClass::Data, + SymbolModifiers::Exported, + ), + BNSymbolType::ImportedData => { + Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External) + } + BNSymbolType::LibraryFunction | BNSymbolType::Function => Symbol::new( + symbol_name, + SymbolClass::Function, + SymbolModifiers::default(), + ), + // BN Imported is our External + BNSymbolType::ImportedFunction => Symbol::new( + symbol_name, + SymbolClass::Function, + SymbolModifiers::External, + ), + } +} + +pub fn to_bn_symbol_at_address(view: &BinaryView, symbol: &Symbol, addr: u64) -> BNRef<BNSymbol> { + let is_external = symbol.modifiers.contains(SymbolModifiers::External); + let _is_exported = symbol.modifiers.contains(SymbolModifiers::Exported); + let symbol_type = match symbol.class { + SymbolClass::Function if is_external => BNSymbolType::ImportedFunction, + // TODO: We should instead make it a Function, however due to the nature of the imports we are setting them to library for now. + SymbolClass::Function => BNSymbolType::LibraryFunction, + SymbolClass::Data if is_external => BNSymbolType::ImportedData, + SymbolClass::Data => BNSymbolType::Data, + }; + let raw_name = symbol.name.as_str(); + let mut symbol_builder = BNSymbol::builder(symbol_type, &symbol.name, addr); + // Demangle symbol name (short is with simplifications). + if let Some(arch) = view.default_arch() { + if let Ok((_, full_name_list)) = + binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false) + { + let full_name = full_name_list.join("::"); + symbol_builder = symbol_builder.full_name(&full_name); + } + if let Ok((_, short_name_list)) = + binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false) + { + let short_name = short_name_list.join("::"); + symbol_builder = symbol_builder.short_name(&short_name); + } + } + symbol_builder.create() +} + +pub fn from_bn_type(view: &BinaryView, raw_ty: BNRef<BNType>, confidence: u8) -> Type { + from_bn_type_internal(view, &mut HashSet::new(), raw_ty, confidence) +} + +fn from_bn_type_internal( + view: &BinaryView, + visited_refs: &mut HashSet<String>, + raw_ty: BNRef<BNType>, + confidence: u8, +) -> Type { + let bytes_to_bits = |val| val * 8; + let raw_ty_bit_width = bytes_to_bits(raw_ty.width()); + let type_class = match raw_ty.type_class() { + BNTypeClass::VoidTypeClass => TypeClass::Void, + BNTypeClass::BoolTypeClass => { + let bool_class = BooleanClass { width: None }; + TypeClass::Boolean(bool_class) + } + BNTypeClass::IntegerTypeClass => { + let signed = raw_ty.is_signed().contents; + let width = Some(raw_ty_bit_width as u16); + if signed && width == Some(8) { + // NOTE: if its an i8, its a char. + let char_class = CharacterClass { width: None }; + TypeClass::Character(char_class) + } else { + let int_class = IntegerClass { width, signed }; + TypeClass::Integer(int_class) + } + } + BNTypeClass::FloatTypeClass => { + let float_class = FloatClass { + width: Some(raw_ty_bit_width as u16), + }; + TypeClass::Float(float_class) + } + // TODO: Union????? + BNTypeClass::StructureTypeClass => { + let raw_struct = raw_ty.get_structure().unwrap(); + + let mut members = raw_struct + .members() + .unwrap() + .into_iter() + .map(|raw_member| { + let bit_offset = bytes_to_bits(raw_member.offset); + let mut modifiers = StructureMemberModifiers::empty(); + // If this member is not public mark it as internal. + modifiers.set( + StructureMemberModifiers::Internal, + !matches!(raw_member.access, MemberAccess::PublicAccess), + ); + StructureMember { + name: Some(raw_member.name), + offset: bit_offset, + ty: from_bn_type_internal( + view, + visited_refs, + raw_member.ty.contents, + raw_member.ty.confidence, + ), + modifiers, + } + }) + .collect::<Vec<_>>(); + + // Add base structures as flattened members + if let Ok(base_structs) = raw_struct.base_structures() { + let base_to_member_iter = base_structs.iter().map(|base_struct| { + let bit_offset = bytes_to_bits(base_struct.offset); + let mut modifiers = StructureMemberModifiers::empty(); + modifiers.set(StructureMemberModifiers::Flattened, true); + let base_struct_ty = from_bn_type_internal( + view, + visited_refs, + BNType::named_type(&base_struct.ty), + 255, + ); + StructureMember { + name: base_struct_ty.name.to_owned(), + offset: bit_offset, + ty: base_struct_ty, + modifiers, + } + }); + members.extend(base_to_member_iter); + } + + // TODO: Check if union + let struct_class = StructureClass::new(members); + TypeClass::Structure(struct_class) + } + BNTypeClass::EnumerationTypeClass => { + let raw_enum = raw_ty.get_enumeration().unwrap(); + + let enum_ty_signed = raw_ty.is_signed().contents; + let enum_ty = Type::builder::<String, _>() + .class(TypeClass::Integer(IntegerClass { + width: Some(raw_ty_bit_width as u16), + signed: enum_ty_signed, + })) + .build(); + + let members = raw_enum + .members() + .into_iter() + .map(|raw_member| EnumerationMember { + name: Some(raw_member.name), + constant: raw_member.value, + }) + .collect(); + + let enum_class = EnumerationClass::new(enum_ty, members); + TypeClass::Enumeration(enum_class) + } + BNTypeClass::PointerTypeClass => { + let raw_child_ty = raw_ty.target().unwrap(); + let ptr_class = PointerClass { + width: Some(raw_ty_bit_width as u16), + child_type: from_bn_type_internal( + view, + visited_refs, + raw_child_ty.contents, + raw_child_ty.confidence, + ), + // TODO: Handle addressing. + addressing: PointerAddressing::Absolute, + }; + TypeClass::Pointer(ptr_class) + } + BNTypeClass::ArrayTypeClass => { + let length = raw_ty.count(); + let raw_member_ty = raw_ty.element_type().unwrap(); + let array_class = ArrayClass { + length: Some(length), + member_type: from_bn_type_internal( + view, + visited_refs, + raw_member_ty.contents, + raw_member_ty.confidence, + ), + modifiers: ArrayModifiers::empty(), + }; + TypeClass::Array(array_class) + } + BNTypeClass::FunctionTypeClass => { + let in_members = raw_ty + .parameters() + .unwrap() + .into_iter() + .map(|raw_member| { + // TODO: Location... + let _location = Location::Register(RegisterLocation); + FunctionMember { + name: Some(raw_member.name), + ty: from_bn_type_internal( + view, + visited_refs, + raw_member.t.contents, + raw_member.t.confidence, + ), + // TODO: Just omit location for now? + // TODO: Location should be optional... + locations: vec![], + } + }) + .collect(); + + let mut out_members = Vec::new(); + if let Ok(return_ty) = raw_ty.return_value() { + out_members.push(FunctionMember { + name: None, + ty: from_bn_type_internal( + view, + visited_refs, + return_ty.contents, + return_ty.confidence, + ), + locations: vec![], + }); + } + + let calling_convention = raw_ty + .calling_convention() + .map(|bn_cc| from_bn_calling_convention(bn_cc.contents)) + .ok(); + + let func_class = FunctionClass { + calling_convention, + in_members, + out_members, + }; + TypeClass::Function(func_class) + } + BNTypeClass::VarArgsTypeClass => TypeClass::Void, + BNTypeClass::ValueTypeClass => { + // What the is this. + TypeClass::Void + } + BNTypeClass::NamedTypeReferenceClass => { + let raw_ntr = raw_ty.get_named_type_reference().unwrap(); + let ref_id_str = raw_ntr.id().to_string(); + let raw_ntr_ty = raw_ntr.target(view); + if raw_ntr_ty.is_none() || !visited_refs.insert(ref_id_str.clone()) { + let ref_class = ReferrerClass::new(None, Some(raw_ntr.name().to_string())); + TypeClass::Referrer(ref_class) + } else { + use dashmap::DashMap; + use std::sync::Arc; + use std::sync::OnceLock; + static REF_CACHE: OnceLock<Arc<DashMap<String, TypeClass>>> = OnceLock::new(); + let ref_cache = REF_CACHE.get_or_init(|| Arc::new(DashMap::new())); + // Check the cache first before proceeding + if let Some(cached_type) = ref_cache.get(&ref_id_str) { + cached_type.value().to_owned() + } else { + let ntr_ty = + from_bn_type_internal(view, visited_refs, raw_ntr_ty.unwrap(), confidence); + visited_refs.remove(&ref_id_str); + // NOTE: The GUID here must always equal the same for any given type for this to work effectively. + let ntr_guid = TypeGUID::from(&ntr_ty); + let ref_class = ReferrerClass::new(Some(ntr_guid), ntr_ty.name); + let ntr_ty_class = TypeClass::Referrer(ref_class); + ref_cache.insert(ref_id_str, ntr_ty_class.clone()); + ntr_ty_class + } + } + } + BNTypeClass::WideCharTypeClass => { + let char_class = CharacterClass { + width: Some(raw_ty_bit_width as u16), + }; + TypeClass::Character(char_class) + } + }; + + let name = raw_ty.registered_name().map(|n| n.name().to_string()).ok(); + + Type { + name, + class: Box::new(type_class), + confidence, + // TODO: Fill these out... + modifiers: vec![], + alignment: Default::default(), + // TODO: Filling this out is... weird. + // TODO: we _do_ want this for networked types (this is the only way we can update type is if we fill this out) + ancestors: vec![], + } +} + +pub fn from_bn_calling_convention<A: BNArchitecture>( + raw_cc: BNRef<BNCallingConvention<A>>, +) -> CallingConvention { + // NOTE: Currently calling convention just stores the name. + CallingConvention::new(raw_cc.name().as_str()) +} + +pub fn to_bn_calling_convention<A: BNArchitecture>( + arch: &A, + calling_convention: &CallingConvention, +) -> BNRef<BNCallingConvention<A>> { + for cc in &arch.calling_conventions() { + if cc.name().as_str() == calling_convention.name { + return cc.clone(); + } + } + arch.get_default_calling_convention().unwrap() +} + +pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> { + let bits_to_bytes = |val: u64| (val / 8); + let addr_size = arch.address_size() as u64; + match ty.class.as_ref() { + TypeClass::Void => BNType::void(), + TypeClass::Boolean(_) => BNType::bool(), + TypeClass::Integer(c) => { + let width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(4); + BNType::int(width as usize, c.signed) + } + TypeClass::Character(c) => match c.width { + Some(w) => BNType::wide_char(bits_to_bytes(w as _) as usize), + None => BNType::char(), + }, + TypeClass::Float(c) => { + let width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(4); + BNType::float(width as usize) + } + TypeClass::Pointer(ref c) => { + let child_type = to_bn_type(arch, &c.child_type); + let ptr_width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(addr_size); + // TODO: Child type confidence + let constant = ty.is_const(); + let volatile = ty.is_volatile(); + // TODO: If the pointer is to a null terminated array of chars, make it a pointer to char + // TODO: Addressing mode + BNType::pointer_of_width(&child_type, ptr_width as usize, constant, volatile, None) + } + TypeClass::Array(c) => { + let member_type = to_bn_type(arch, &c.member_type); + // TODO: How to handle DST array (length is None) + BNType::array(&member_type, c.length.unwrap_or(0)) + } + TypeClass::Structure(c) => { + let builder = BNStructureBuilder::new(); + // TODO: Structure type class? + // TODO: Alignment + // TODO: Other modifiers? + let mut base_structs: Vec<BNBaseStructure> = Vec::new(); + for member in &c.members { + let member_type = BNConf::new(to_bn_type(arch, &member.ty), u8::MAX); + let member_name = member.name.to_owned().unwrap_or("field_OFFSET".into()); + let member_offset = bits_to_bytes(member.offset); + let member_access = if member + .modifiers + .contains(StructureMemberModifiers::Internal) + { + BNMemberAccess::PrivateAccess + } else { + BNMemberAccess::PublicAccess + }; + // TODO: Member scope + let member_scope = BNMemberScope::NoScope; + if member + .modifiers + .contains(StructureMemberModifiers::Flattened) + { + // Add member as a base structure to inherit its fields. + match member.ty.class.as_ref() { + TypeClass::Referrer(c) => { + // We only support base structures with a referrer right now. + let base_struct_ntr_name = + c.name.to_owned().unwrap_or("base_UNKNOWN".into()); + let base_struct_ntr = match c.guid { + Some(guid) => BNNamedTypeReference::new_with_id( + NamedTypeReferenceClass::UnknownNamedTypeClass, + guid.to_string(), + base_struct_ntr_name.into(), + ), + None => BNNamedTypeReference::new( + NamedTypeReferenceClass::UnknownNamedTypeClass, + base_struct_ntr_name.into(), + ), + }; + base_structs.push(BNBaseStructure::new( + base_struct_ntr, + member_offset, + member.ty.size().unwrap_or(0), + )) + } + _ => { + log::error!( + "Adding base {:?} with invalid ty: {:?}", + ty.name, + member.ty + ); + } + } + } else { + builder.insert_member( + &BNStructureMember::new( + member_type, + member_name, + member_offset, + member_access, + member_scope, + ), + false, + ); + } + } + builder.set_base_structures(base_structs); + BNType::structure(&builder.finalize()) + } + TypeClass::Enumeration(c) => { + let builder = BNEnumerationBuilder::new(); + for member in &c.members { + // TODO: Add default name? + let member_name = member.name.to_owned().unwrap_or("enum_VAL".into()); + let member_value = member.constant; + builder.insert(member_name, member_value); + } + // TODO: Warn if enumeration has no size. + let width = bits_to_bytes(c.member_type.size().unwrap()) as _; + let signed = matches!(*c.member_type.class, TypeClass::Integer(c) if c.signed); + BNType::enumeration(&builder.finalize(), width, signed) + } + TypeClass::Union(c) => { + let builder = BNStructureBuilder::new(); + builder.set_structure_type(BNStructureType::UnionStructureType); + for member in &c.members { + let member_type = BNConf::new(to_bn_type(arch, &member.ty), u8::MAX); + let member_name = member.name.to_owned(); + // TODO: Member access + let member_access = BNMemberAccess::PublicAccess; + // TODO: Member scope + let member_scope = BNMemberScope::NoScope; + let structure_member = BNStructureMember::new( + member_type, + member_name, + 0, // Union members all exist at 0 right? + member_access, + member_scope, + ); + builder.insert_member(&structure_member, false); + } + BNType::structure(&builder.finalize()) + } + TypeClass::Function(c) => { + let return_type = if !c.out_members.is_empty() { + // TODO: WTF + to_bn_type(arch, &c.out_members[0].ty) + } else { + BNType::void() + }; + let params: Vec<_> = c + .in_members + .iter() + .map(|member| { + let member_type = to_bn_type(arch, &member.ty); + let name = member.name.clone(); + // TODO: Location AND fix default param name + BNFunctionParameter::new(member_type, name.unwrap_or("param_IDK".into()), None) + }) + .collect(); + // TODO: Variable arguments + let variable_args = false; + // If we have a calling convention we run the extended function type creation. + match c.calling_convention.as_ref() { + Some(cc) => { + let calling_convention = to_bn_calling_convention(arch, cc); + BNType::function_with_options( + &return_type, + ¶ms, + variable_args, + &BNConf::new(calling_convention, u8::MAX), + BNConf::new(0, 0), + ) + } + None => BNType::function(&return_type, ¶ms, variable_args), + } + } + TypeClass::Referrer(c) => { + let ntr = match c.guid { + Some(guid) => { + let guid_str = guid.to_string(); + let ntr_name = c.name.to_owned().unwrap_or(guid_str.clone()); + NamedTypeReference::new_with_id( + NamedTypeReferenceClass::UnknownNamedTypeClass, + guid_str, + ntr_name.into(), + ) + } + None => match c.name.as_ref() { + Some(ntr_name) => NamedTypeReference::new( + NamedTypeReferenceClass::UnknownNamedTypeClass, + ntr_name.into(), + ), + None => { + log::error!("Referrer with no reference! {:?}", c); + NamedTypeReference::new( + NamedTypeReferenceClass::UnknownNamedTypeClass, + "AHHHHHH".into(), + ) + } + }, + }; + BNType::named_type(&ntr) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use binaryninja::binaryview::BinaryViewExt; + use binaryninja::headless::Session; + use std::path::PathBuf; + use std::sync::OnceLock; + use warp::r#type::guid::TypeGUID; + + static INIT: OnceLock<Session> = OnceLock::new(); + + fn get_session<'a>() -> &'a Session { + INIT.get_or_init(|| Session::new()) + } + + #[test] + fn type_conversion() { + let session = get_session(); + let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); + for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") { + let entry = entry.expect("Failed to read directory entry"); + let path = entry.path(); + if path.is_file() { + if let Some(bv) = session.load(path.to_str().unwrap()) { + let types_len = bv.types().len(); + let converted_types: Vec<_> = bv + .types() + .iter() + .map(|t| { + let ty = from_bn_type(&bv, t.type_object().clone(), u8::MAX); + (TypeGUID::from(&ty), ty) + }) + .collect(); + assert_eq!(types_len, converted_types.len()); + } + } + } + } + + #[test] + fn check_for_leaks() { + let session = get_session(); + let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); + for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") { + let entry = entry.expect("Failed to read directory entry"); + let path = entry.path(); + if path.is_file() { + if let Some(inital_bv) = session.load(path.to_str().unwrap()) { + let types_len = inital_bv.types().len(); + let converted_types: Vec<_> = inital_bv + .types() + .iter() + .map(|t| { + let ty = from_bn_type(&inital_bv, t.type_object().clone(), u8::MAX); + (TypeGUID::from(&ty), ty) + }) + .collect(); + assert_eq!(types_len, converted_types.len()); + // Hold on to a reference to the core to prevent view getting dropped in worker thread. + let core_ref = inital_bv + .functions() + .iter() + .next() + .map(|f| f.unresolved_stack_adjustment_graph()); + // Drop the file and view. + inital_bv.file().close(); + std::mem::drop(inital_bv); + let initial_memory_info = binaryninja::memory_info(); + if let Some(second_bv) = session.load(path.to_str().unwrap()) { + let types_len = second_bv.types().len(); + let converted_types: Vec<_> = second_bv + .types() + .iter() + .map(|t| { + let ty = from_bn_type(&second_bv, t.type_object().clone(), u8::MAX); + (TypeGUID::from(&ty), ty) + }) + .collect(); + assert_eq!(types_len, converted_types.len()); + // Hold on to a reference to the core to prevent view getting dropped in worker thread. + let core_ref = second_bv + .functions() + .iter() + .next() + .map(|f| f.unresolved_stack_adjustment_graph()); + // Drop the file and view. + second_bv.file().close(); + std::mem::drop(second_bv); + let final_memory_info = binaryninja::memory_info(); + for info in initial_memory_info { + let initial_count = info.1; + if let Some(&final_count) = final_memory_info.get(&info.0) { + assert!( + final_count <= initial_count, + "{}: final objects {} vs initial objects {}", + info.0, + final_count, + initial_count + ); + } + } + } + } + } + } + } +} diff --git a/plugins/warp/src/lib.rs b/plugins/warp/src/lib.rs new file mode 100644 index 00000000..ae14ae80 --- /dev/null +++ b/plugins/warp/src/lib.rs @@ -0,0 +1,161 @@ +use binaryninja::architecture::Architecture; +use binaryninja::basicblock::BasicBlock as BNBasicBlock; +use binaryninja::binaryview::BinaryViewExt; +use binaryninja::function::{Function as BNFunction, NativeBlock}; +use binaryninja::llil; +use binaryninja::llil::{ExprInfo, FunctionMutability, NonSSA, NonSSAVariant, VisitorAction}; +use binaryninja::rc::Ref as BNRef; +use warp::signature::basic_block::{BasicBlock, BasicBlockGUID}; +use warp::signature::function::constraints::FunctionConstraints; +use warp::signature::function::{Function, FunctionGUID}; + +use crate::cache::{ + cached_adjacency_constraints, cached_call_site_constraints, cached_function_guid, +}; +use crate::convert::{from_bn_symbol, from_bn_type}; + +pub mod cache; +pub mod convert; +mod matcher; +/// Only used when compiled for cdylib target. +mod plugin; + +pub fn build_function<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + func: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, +) -> Option<Function> { + let bn_fn_ty = func.function_type(); + Some(Function { + guid: cached_function_guid(func, llil)?, + symbol: from_bn_symbol(&func.symbol()), + // TODO: Confidence should be derived from function type. + ty: from_bn_type(&func.view(), bn_fn_ty, 255), + constraints: FunctionConstraints { + // NOTE: Adding adjacent only works if analysis is complete. + adjacent: cached_adjacency_constraints(func), + call_sites: cached_call_site_constraints(func), + // TODO: Add caller sites (when adjacent and call sites are minimal) + // NOTE: Adding caller sites only works if analysis is complete. + caller_sites: Default::default(), + }, + // TODO: We need more than one entry block. + entry: entry_basic_block_guid(func, llil).map(BasicBlock::new), + }) +} + +pub fn entry_basic_block_guid<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + func: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, +) -> Option<BasicBlockGUID> { + // NOTE: This is not actually the entry point. This is the highest basic block. + let first_basic_block = sorted_basic_blocks(func).into_iter().next()?; + basic_block_guid(&first_basic_block, llil) +} + +/// Basic blocks sorted from high to low. +pub fn sorted_basic_blocks(func: &BNFunction) -> Vec<BNRef<BNBasicBlock<NativeBlock>>> { + let mut basic_blocks = func + .basic_blocks() + .iter() + .map(|bb| bb.clone()) + .collect::<Vec<_>>(); + basic_blocks.sort_by_key(|f| f.raw_start()); + basic_blocks +} + +pub fn function_guid<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + func: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, +) -> Option<FunctionGUID> { + // TODO: Sort the basic blocks. + let basic_blocks = sorted_basic_blocks(func); + let basic_block_guids = basic_blocks + .iter() + .filter_map(|bb| basic_block_guid(bb, llil)) + .collect::<Vec<_>>(); + Some(FunctionGUID::from_basic_blocks(&basic_block_guids)) +} + +pub fn basic_block_guid<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + basic_block: &BNBasicBlock<NativeBlock>, + llil: &llil::Function<A, M, NonSSA<V>>, +) -> Option<BasicBlockGUID> { + let func = basic_block.function(); + let view = func.view(); + let arch = func.arch(); + let max_instr_len = arch.max_instr_len(); + // TODO: Add all the hacks here to remove stuff like function prolog... + // TODO mov edi, edi on windows x86 + // TODO: Ugh i really dislike the above and REALLY don't wanna do that. + // TODO: The above invalidates our "all function bytes" approach. + // TODO: Could we keep the bytes and just zero mask them? At least then we don't completely get rid of them. + + let basic_block_range = basic_block.raw_start()..basic_block.raw_end(); + let mut basic_block_bytes = Vec::with_capacity(basic_block_range.count()); + for instr_addr in basic_block.into_iter() { + let mut instr_bytes = view.read_vec(instr_addr, max_instr_len); + if let Some(instr_info) = arch.instruction_info(&instr_bytes, instr_addr) { + let instr_len = instr_info.len(); + instr_bytes.truncate(instr_len); + if let Some(instr_llil) = llil.instruction_at(instr_addr) { + if instr_llil.visit_tree(&mut |_expr, expr_info| match expr_info { + ExprInfo::ConstPtr(_) | ExprInfo::ExternPtr(_) => VisitorAction::Halt, + _ => VisitorAction::Descend, + }) == VisitorAction::Halt + { + // Found a variant instruction, mask off entire instruction. + instr_bytes.fill(0); + } + } + // Add the instructions bytes to the functions bytes + basic_block_bytes.extend(instr_bytes); + } + } + + Some(BasicBlockGUID::from(basic_block_bytes.as_slice())) +} + +#[cfg(test)] +mod tests { + use crate::cache::cached_function_guid; + use crate::convert::from_bn_type; + use binaryninja::binaryview::BinaryViewExt; + use binaryninja::headless::Session; + use std::path::PathBuf; + use std::sync::OnceLock; + use warp::r#type::guid::TypeGUID; + + static INIT: OnceLock<Session> = OnceLock::new(); + + fn get_session<'a>() -> &'a Session { + // TODO: This is not shared between other test modules, should still be fine (mutex in core now). + INIT.get_or_init(|| Session::new()) + } + + #[test] + fn insta_signatures() { + let session = get_session(); + let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); + for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") { + let entry = entry.expect("Failed to read directory entry"); + let path = entry.path(); + if path.is_file() { + if let Some(path_str) = path.to_str() { + if path_str.ends_with("library.o") { + if let Some(inital_bv) = session.load(path_str) { + let mut functions = inital_bv + .functions() + .iter() + .map(|f| { + cached_function_guid(&f, &f.low_level_il().unwrap()).unwrap() + }) + .collect::<Vec<_>>(); + functions.sort_by_key(|guid| guid.guid); + insta::assert_debug_snapshot!(functions); + } + } + } + } + } + } +} diff --git a/plugins/warp/src/matcher.rs b/plugins/warp/src/matcher.rs new file mode 100644 index 00000000..ae49f00a --- /dev/null +++ b/plugins/warp/src/matcher.rs @@ -0,0 +1,432 @@ +use binaryninja::architecture::{Architecture as BNArchitecture, Architecture}; +use binaryninja::backgroundtask::BackgroundTask; +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use binaryninja::function::{Function as BNFunction, FunctionUpdateType}; +use binaryninja::llil; +use binaryninja::llil::{FunctionMutability, NonSSA, NonSSAVariant}; +use binaryninja::platform::Platform; +use binaryninja::rc::Guard; +use binaryninja::rc::Ref as BNRef; +use dashmap::DashMap; +use fastbloom::BloomFilter; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::hash::{DefaultHasher, Hasher}; +use std::path::PathBuf; +use std::sync::OnceLock; +use walkdir::{DirEntry, WalkDir}; +use warp::r#type::class::TypeClass; +use warp::r#type::guid::TypeGUID; +use warp::r#type::Type; +use warp::signature::basic_block::BasicBlock; +use warp::signature::function::{Function, FunctionGUID}; +use warp::signature::Data; + +use crate::cache::{cached_call_site_constraints, cached_function_guid, FunctionID}; +use crate::convert::to_bn_type; +use crate::entry_basic_block_guid; +use crate::plugin::on_matched_function; + +pub const TRIVIAL_LLIL_THRESHOLD: usize = 8; + +pub static PLAT_MATCHER_CACHE: OnceLock<DashMap<PlatformID, Matcher>> = OnceLock::new(); + +pub fn cached_function_match<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + function: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, +) { + let platform = function.platform(); + let platform_id = PlatformID::from(platform.as_ref()); + let matcher_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default); + match matcher_cache.get(&platform_id) { + Some(matcher) => matcher.match_function(function, llil), + None => { + let matcher = Matcher::from_platform(platform); + matcher.match_function(function, llil); + matcher_cache.insert(platform_id, matcher); + } + } +} + +pub struct Matcher { + pub matched_functions: DashMap<FunctionID, Function>, + pub functions: DashMap<FunctionGUID, Vec<Function>>, + pub types: DashMap<TypeGUID, Type>, + pub named_types: DashMap<String, Type>, + /// This is used to fast-fail on functions not in the dataset. + /// NOTE: This can only handle one basic block classification, right now that is the entry block. + basic_block_filter: BloomFilter, +} + +impl Matcher { + /// Create a matcher from the platforms signature subdirectory. + pub fn from_platform(platform: BNRef<Platform>) -> Self { + let platform_name = platform.name().to_string(); + let task = BackgroundTask::new( + format!("Getting platform matcher data... {}", platform_name), + false, + ) + .unwrap(); + // Get core signatures for the given platform + let core_dir = binaryninja::install_directory().unwrap(); + let root_core_sig_dir = core_dir.join("signatures"); + let plat_core_sig_dir = root_core_sig_dir.join(&platform_name); + let mut data = get_data_from_dir(&plat_core_sig_dir); + + // Get user signatures for the given platform + let user_dir = binaryninja::user_directory().unwrap(); + let root_user_sig_dir = user_dir.join("signatures"); + let plat_user_sig_dir = root_user_sig_dir.join(&platform_name); + let user_data = get_data_from_dir(&plat_user_sig_dir); + + data.extend(user_data); + + // TODO: If a user signature has the same name as a core signature, remove the core signature. + + task.set_progress_text("Gathering entry blocks for matcher filtering..."); + + // Get entry_blocks for filtering. + let entry_blocks = data + .iter() + .flat_map(|(_, data)| data.functions.iter().map(|function| &function.entry)) + .collect::<Vec<_>>(); + // TODO: We need to disable this if we get a None basic block, as it will then fail to match all cases. + let basic_block_filter = BloomFilter::with_false_pos(0.1).items(entry_blocks); + + task.set_progress_text("Gathering matcher functions..."); + + // TODO: Merge like functions, right now we just hope and pray. + + // Get functions for comprehensive matching. + let functions = data + .iter() + .flat_map(|(_, data)| { + data.functions.iter().fold(DashMap::new(), |map, func| { + #[allow(clippy::unwrap_or_default)] + map.entry(func.guid) + .or_insert_with(Vec::new) + .push(func.clone()); + map + }) + }) + .map(|(guid, mut funcs)| { + funcs.sort_by_key(|f| f.symbol.name.to_owned()); + funcs.dedup_by_key(|f| f.symbol.name.to_owned()); + (guid, funcs) + }) + .collect(); + + task.set_progress_text("Gathering matcher types..."); + + let types = data + .iter() + .flat_map(|(_, data)| { + data.types.iter().fold(DashMap::new(), |map, comp_ty| { + map.insert(comp_ty.guid, comp_ty.ty.clone()); + map + }) + }) + .collect(); + + task.set_progress_text("Gathering matcher named types..."); + + // TODO: We store a duplicate lookup for named references. + let named_types = data + .iter() + .flat_map(|(_, data)| { + data.types.iter().fold(DashMap::new(), |map, comp_ty| { + if let Some(ty_name) = &comp_ty.ty.name { + map.insert(ty_name.to_owned(), comp_ty.ty.clone()); + } + map + }) + }) + .collect(); + + task.finish(); + + log::debug!("Loaded signatures: {:?}", data.keys()); + + Self { + matched_functions: Default::default(), + functions, + basic_block_filter, + types, + named_types, + } + } + + pub fn add_type_to_view<A: BNArchitecture>(&self, view: &BinaryView, arch: &A, ty: &Type) { + fn inner_add_type_to_view<A: BNArchitecture>( + matcher: &Matcher, + view: &BinaryView, + arch: &A, + visited_refs: &mut HashSet<String>, + ty: &Type, + ) { + let ty_id_str = TypeGUID::from(ty).to_string(); + if view.get_type_by_id(&ty_id_str).is_some() { + // Type already added. + return; + } + // Type not already added to the view. + // Verify all nested types are added before adding type. + match ty.class.as_ref() { + TypeClass::Pointer(c) => { + inner_add_type_to_view(matcher, view, arch, visited_refs, &c.child_type) + } + TypeClass::Array(c) => { + inner_add_type_to_view(matcher, view, arch, visited_refs, &c.member_type) + } + TypeClass::Structure(c) => { + for member in &c.members { + inner_add_type_to_view(matcher, view, arch, visited_refs, &member.ty) + } + } + TypeClass::Enumeration(c) => { + inner_add_type_to_view(matcher, view, arch, visited_refs, &c.member_type) + } + TypeClass::Union(c) => { + for member in &c.members { + inner_add_type_to_view(matcher, view, arch, visited_refs, &member.ty) + } + } + TypeClass::Function(c) => { + for out_member in &c.out_members { + inner_add_type_to_view(matcher, view, arch, visited_refs, &out_member.ty) + } + for in_member in &c.in_members { + inner_add_type_to_view(matcher, view, arch, visited_refs, &in_member.ty) + } + } + TypeClass::Referrer(c) => { + // Check to see if the referrer has been added to the view. + let mut resolved = false; + if let Some(ref_guid) = c.guid { + // NOTE: We do not need to check for cyclic reference here because + // NOTE: GUID references are unable to be referenced. + if view.get_type_by_id(ref_guid.to_string()).is_none() { + // Add the ref to the view if it is in the Matcher types + if let Some(ref_ty) = matcher.types.get(&ref_guid) { + inner_add_type_to_view(matcher, view, arch, visited_refs, &ref_ty); + resolved = true; + } + } + } + + if let Some(ref_name) = &c.name { + // Only try and resolve by name if not already visiting. + if !resolved + && visited_refs.insert(ref_name.to_string()) + && view.get_type_by_name(ref_name).is_none() + { + // Add the ref to the view if it is in the Matcher types + if let Some(ref_ty) = matcher.named_types.get(ref_name) { + inner_add_type_to_view(matcher, view, arch, visited_refs, &ref_ty); + } + // No longer visiting type. + visited_refs.remove(ref_name); + } + } + } + _ => {} + } + // All nested types _should_ be added now, we can add this type. + let ty_name = ty.name.to_owned().unwrap_or_else(|| ty_id_str.clone()); + view.define_auto_type_with_id(ty_name, ty_id_str, &to_bn_type(arch, ty)); + } + inner_add_type_to_view(self, view, arch, &mut HashSet::new(), ty) + } + + pub fn match_function<A: Architecture, M: FunctionMutability, V: NonSSAVariant>( + &self, + function: &BNFunction, + llil: &llil::Function<A, M, NonSSA<V>>, + ) { + let function_id = FunctionID::from(function); + if let Some(matched_function) = self.matched_functions.get(&function_id) { + // Skip computing the match for already matched function. + // We do still need to apply the match data through analysis updates. + return on_matched_function(function, &matched_function); + } + + let on_new_match = |matched: &Function| { + // We also want to resolve the types here. + if let TypeClass::Function(c) = matched.ty.class.as_ref() { + // Recursively go through the function type and resolve the uuids + let view = function.view(); + let arch = function.arch(); + for out_member in &c.out_members { + self.add_type_to_view(&view, &arch, &out_member.ty); + } + for in_member in &c.in_members { + self.add_type_to_view(&view, &arch, &in_member.ty); + } + } else { + // This should never happen. + log::error!( + "Matched function is not of function type class... 0x{:x}", + function.start() + ); + } + on_matched_function(function, matched); + + // We matched on the function, great! Now make sure we don't do this again :3 + self.matched_functions + .insert(function_id, matched.to_owned()); + // Also mark this for updates. + // TODO: Does this do anything? + function.mark_updates_required(FunctionUpdateType::UserFunctionUpdate); + }; + + // TODO: Expand this check to be less broad. + let is_function_trivial = { llil.instruction_count() < TRIVIAL_LLIL_THRESHOLD }; + + // Check to see if the functions entry block is even in the dataset + let entry_block = entry_basic_block_guid(function, llil).map(BasicBlock::new); + if self.basic_block_filter.contains(&entry_block) { + // Build the full function guid now + if let Some(warp_func_guid) = cached_function_guid(function, llil) { + if let Some(matched) = self.functions.get(&warp_func_guid) { + if matched.len() == 1 && !is_function_trivial { + on_new_match(&matched[0]); + } else if let Some(matched_function) = + self.match_function_from_constraints(function, &matched) + { + log::info!( + "Found best matching function `{}`... 0x{:x}", + matched_function.symbol.name, + function.start() + ); + on_new_match(matched_function); + } else { + log::error!( + "Failed to find matching function `{}`... 0x{:x}", + matched.len(), + function.start() + ); + } + } + } + } + } + + pub fn match_function_from_constraints<'a>( + &self, + function: &BNFunction, + matched_functions: &'a [Function], + ) -> Option<&'a Function> { + // TODO: To prevent invoking adjacent constraint function analysis, we must call call_site constraints specifically. + let call_sites = cached_call_site_constraints(function); + + // NOTE: We are only matching with call_sites for now, as adjacency requires we run after all analysis has completed. + if call_sites.is_empty() { + return None; + } + + // Check call site guids + let mut highest_guid_count = 0; + let mut matched_guid_func = None; + let call_site_guids = call_sites + .iter() + .filter_map(|c| c.guid) + .collect::<HashSet<_>>(); + for matched in matched_functions { + let matched_call_site_guids = matched + .constraints + .call_sites + .iter() + .filter_map(|c| c.guid) + .collect::<HashSet<_>>(); + let common_guid_count = call_site_guids + .intersection(&matched_call_site_guids) + .count(); + match common_guid_count.cmp(&highest_guid_count) { + Ordering::Equal => { + // Multiple matches with same count, don't match on ONE of them. + matched_guid_func = None; + } + Ordering::Greater => { + highest_guid_count = common_guid_count; + matched_guid_func = Some(matched); + } + Ordering::Less => {} + } + } + + // Check call site symbol names + let mut highest_symbol_count = 0; + let mut matched_symbol_func = None; + let call_site_symbol_names = call_sites + .into_iter() + .filter_map(|c| Some(c.symbol?.name)) + .collect::<HashSet<_>>(); + for matched in matched_functions { + let matched_call_site_symbol_names = matched + .constraints + .call_sites + .iter() + .filter_map(|c| Some(c.symbol.to_owned()?.name)) + .collect::<HashSet<_>>(); + let common_symbol_count = call_site_symbol_names + .intersection(&matched_call_site_symbol_names) + .count(); + match common_symbol_count.cmp(&highest_symbol_count) { + Ordering::Equal => { + // Multiple matches with same count, don't match on ONE of them. + matched_symbol_func = None; + } + Ordering::Greater => { + highest_symbol_count = common_symbol_count; + matched_symbol_func = Some(matched); + } + Ordering::Less => {} + } + } + + match highest_guid_count.cmp(&highest_symbol_count) { + Ordering::Less => matched_symbol_func, + Ordering::Greater => matched_guid_func, + Ordering::Equal => None, + } + } +} + +fn get_data_from_dir(dir: &PathBuf) -> HashMap<PathBuf, Data> { + let data_from_entry = |entry: DirEntry| { + let path = entry.path(); + let contents = std::fs::read(path).ok()?; + Data::from_bytes(&contents) + }; + + WalkDir::new(dir) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter_map(|e| Some((e.clone().into_path(), data_from_entry(e)?))) + .collect() +} + +/// A unique platform ID, used for caching. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct PlatformID(u64); + +impl From<&Platform> for PlatformID { + fn from(value: &Platform) -> Self { + let mut hasher = DefaultHasher::new(); + hasher.write(value.name().to_bytes()); + Self(hasher.finish()) + } +} + +impl From<BNRef<Platform>> for PlatformID { + fn from(value: BNRef<Platform>) -> Self { + Self::from(value.as_ref()) + } +} + +impl From<Guard<'_, Platform>> for PlatformID { + fn from(value: Guard<'_, Platform>) -> Self { + Self::from(value.as_ref()) + } +} diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs new file mode 100644 index 00000000..5bca373b --- /dev/null +++ b/plugins/warp/src/plugin.rs @@ -0,0 +1,152 @@ +use log::LevelFilter; + +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use binaryninja::command::{Command, FunctionCommand}; +use binaryninja::function::Function; +use binaryninja::rc::Ref; +use binaryninja::tags::TagType; +use warp::signature::function::Function as WarpFunction; + +use crate::build_function; +use crate::cache::{ViewID, FUNCTION_CACHE, GUID_CACHE}; +use crate::convert::{to_bn_symbol_at_address, to_bn_type}; +use crate::matcher::{PlatformID, PLAT_MATCHER_CACHE}; + +mod apply; +mod copy; +mod create; +mod find; +mod types; +mod workflow; + +// TODO: This icon is a little much +const TAG_ICON: &str = "🌏"; +const TAG_NAME: &str = "WARP"; + +fn get_warp_tag_type(view: &BinaryView) -> Ref<TagType> { + view.get_tag_type(TAG_NAME) + .unwrap_or_else(|| view.create_tag_type(TAG_NAME, TAG_ICON)) +} + +// What happens to the function when it is matched. +// TODO: add user: bool +// TODO: Rename to markup_function or something. +pub fn on_matched_function(function: &Function, matched: &WarpFunction) { + let view = function.view(); + view.define_auto_symbol(&to_bn_symbol_at_address( + &view, + &matched.symbol, + function.symbol().address(), + )); + function.set_auto_type(&to_bn_type(&function.arch(), &matched.ty)); + // TODO: Add metadata. (both binja metadata and warp metadata) + function.add_tag( + &get_warp_tag_type(&view), + matched.guid.to_string(), + None, + true, + None, + ); +} + +struct DebugFunction; + +impl FunctionCommand for DebugFunction { + fn action(&self, _view: &BinaryView, func: &Function) { + if let Ok(llil) = func.low_level_il() { + if let Some(function) = build_function(func, &llil) { + log::info!("{:#?}", function); + } + } + } + + fn valid(&self, _view: &BinaryView, _func: &Function) -> bool { + true + } +} + +struct DebugCache; + +impl Command for DebugCache { + fn action(&self, view: &BinaryView) { + let function_cache = FUNCTION_CACHE.get_or_init(Default::default); + let view_id = ViewID::from(view); + if let Some(cache) = function_cache.get(&view_id) { + log::info!("View functions: {}", cache.cache.len()); + } + + let function_guid_cache = GUID_CACHE.get_or_init(Default::default); + if let Some(cache) = function_guid_cache.get(&view_id) { + log::info!("View function guids: {}", cache.cache.len()); + } + + let plat_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default); + if let Some(plat) = view.default_platform() { + let platform_id = PlatformID::from(plat); + if let Some(cache) = plat_cache.get(&platform_id) { + log::info!("Platform functions: {}", cache.functions.len()); + log::info!("Platform types: {}", cache.types.len()); + log::info!( + "Platform matched functions: {}", + cache.matched_functions.len() + ); + } + } + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginInit() -> bool { + binaryninja::logger::init(LevelFilter::Debug).unwrap(); + + workflow::insert_matcher_workflow(); + + binaryninja::command::register( + "WARP\\Apply Signature File Types", + "Load all types from a signature file and ignore functions", + types::LoadTypesCommand {}, + ); + + binaryninja::command::register( + "WARP\\Debug Cache", + "Debug cache sizes... because...", + DebugCache {}, + ); + + binaryninja::command::register_for_function( + "WARP\\Debug Signature", + "Print the entire signature for the function", + DebugFunction {}, + ); + + binaryninja::command::register_for_function( + "WARP\\Copy Pattern", + "Copy the computed pattern for the function", + copy::CopyFunctionGUID {}, + ); + + binaryninja::command::register( + "WARP\\Find Function From GUID", + "Locate the function in the view using a GUID", + find::FindFunctionFromGUID {}, + ); + + binaryninja::command::register( + "WARP\\Generate Signature File", + "Generates a signature file containing all binary view functions", + create::CreateSignatureFile {}, + ); + + binaryninja::command::register( + "WARP\\Apply Signature File", + "Applies a signature file to the current view", + apply::ApplySignatureFile {}, + ); + + true +} diff --git a/plugins/warp/src/plugin/apply.rs b/plugins/warp/src/plugin/apply.rs new file mode 100644 index 00000000..56c09d77 --- /dev/null +++ b/plugins/warp/src/plugin/apply.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; +use std::time::Instant; + +use crate::cache::cached_function_guid; +use crate::plugin::on_matched_function; +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use rayon::prelude::*; +use warp::signature::function::{Function, FunctionGUID}; + +pub struct ApplySignatureFile; + +// TODO: All this should do is insert data into the Matcher. this is leftover code. +impl Command for ApplySignatureFile { + fn action(&self, view: &BinaryView) { + // TODO: Start bulk modification + // TODO: view.begin_bulk_modify_symbols(); + let Some(file) = + binaryninja::interaction::get_open_filename_input("Apply Signature File", "*.sbin") + else { + return; + }; + + // TODO: signature files also need to store type information. + + let Ok(data) = std::fs::read(&file) else { + log::error!("Could not read signature file: {:?}", file); + return; + }; + + let Some(data) = warp::signature::Data::from_bytes(&data) else { + log::error!("Could not get data from signature file: {:?}", file); + return; + }; + + // TODO: Turn Vec<Function> to HashSet so that functions with the same symbol and type get eliminated. + let data_functions: HashMap<FunctionGUID, Vec<Function>> = + data.functions + .into_iter() + .fold(HashMap::new(), |mut acc, func| { + #[allow(clippy::unwrap_or_default)] + acc.entry(func.guid).or_insert_with(Vec::new).push(func); + acc + }); + + let background_task = binaryninja::backgroundtask::BackgroundTask::new( + format!("Applying signatures from {:?}", file), + true, + ) + .unwrap(); + + let funcs = view.functions(); + let start = Instant::now(); + + background_task + .set_progress_text(format!("Building {} patterns to lookup...", funcs.len())); + + // TODO: Redo this. + let single_matched = funcs + .par_iter() + .filter_map(|func| { + let llil = func.low_level_il_if_available()?; + let pattern = cached_function_guid(&func, &llil)?; + Some((func, data_functions.get(&pattern)?)) + }) + .filter(|(_, sig)| sig.len() == 1) + .collect::<Vec<_>>(); + + background_task.set_progress_text(format!("Applying {} matches...", single_matched.len())); + for (func, matched) in single_matched { + on_matched_function(&func, &matched[0]); + } + + log::info!("Signature application took {:?}", start.elapsed()); + + background_task.finish(); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/copy.rs b/plugins/warp/src/plugin/copy.rs new file mode 100644 index 00000000..ee91fb4a --- /dev/null +++ b/plugins/warp/src/plugin/copy.rs @@ -0,0 +1,35 @@ +use binaryninja::binaryview::BinaryView; +use binaryninja::command::FunctionCommand; +use binaryninja::function::Function; + +use crate::cache::cached_function_guid; + +pub struct CopyFunctionGUID; + +impl FunctionCommand for CopyFunctionGUID { + fn action(&self, _view: &BinaryView, func: &Function) { + let Ok(llil) = func.low_level_il() else { + log::error!("Could not get low level il for copied function"); + return; + }; + if let Some(guid) = cached_function_guid(func, &llil) { + log::info!( + "Function GUID for {}... {}", + func.symbol().short_name().to_string(), + guid + ); + if let Ok(mut clipboard) = arboard::Clipboard::new() { + let _ = clipboard.set_text(guid.to_string()); + } + } else { + log::error!( + "Failed to create GUID for function... 0x{:0x}", + func.start() + ); + } + } + + fn valid(&self, _view: &BinaryView, _func: &Function) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/create.rs b/plugins/warp/src/plugin/create.rs new file mode 100644 index 00000000..2b1ad60e --- /dev/null +++ b/plugins/warp/src/plugin/create.rs @@ -0,0 +1,71 @@ +use crate::cache::cached_function; +use crate::convert::from_bn_type; +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use rayon::prelude::*; +use std::io::Write; +use std::thread; +use std::time::Instant; +use warp::r#type::ComputedType; + +pub struct CreateSignatureFile; + +// TODO: Prompt the user to add the newly created signature file to the signature blacklist (so that it doesn't keep getting applied) + +impl Command for CreateSignatureFile { + fn action(&self, view: &BinaryView) { + let mut signature_dir = binaryninja::user_directory().unwrap().join("signatures/"); + // TODO: This needs to split out each platform into its own bucket... + if let Some(default_plat) = view.default_platform() { + // If there is a default platform, put the signature in there. + signature_dir.push(default_plat.name().to_string()); + } + let view = view.to_owned(); + thread::spawn(move || { + let background_task = binaryninja::backgroundtask::BackgroundTask::new( + format!("Generating {} signatures... ", view.functions().len()), + true, + ) + .unwrap(); + + let start = Instant::now(); + + let mut data = warp::signature::Data::default(); + data.functions.par_extend( + view.functions() + .par_iter() + .filter_map(|func| cached_function(&func, func.low_level_il().ok()?.as_ref())), + ); + data.types.extend(view.types().iter().map(|ty| { + let ref_ty = ty.type_object().to_owned(); + ComputedType::new(from_bn_type(&view, ref_ty, u8::MAX)) + })); + + // And type generation :3 + log::info!("Signature generation took {:?}", start.elapsed()); + + if let Some(sig_file_name) = binaryninja::interaction::get_text_line_input( + "Signature File", + "Create Signature File", + ) { + let save_file = signature_dir.join(sig_file_name + ".sbin"); + log::info!("Saving to signatures to {:?}...", &save_file); + // TODO: Should we overwrite? Prompt user. + if let Ok(mut file) = std::fs::File::create(&save_file) { + match file.write_all(&data.to_bytes()) { + Ok(_) => log::info!("Signature file saved successfully."), + Err(e) => log::error!("Failed to write data to signature file: {:?}", e), + } + } else { + log::error!("Could not create signature file: {:?}", save_file); + } + } + + background_task.finish(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/find.rs b/plugins/warp/src/plugin/find.rs new file mode 100644 index 00000000..7f089c2f --- /dev/null +++ b/plugins/warp/src/plugin/find.rs @@ -0,0 +1,57 @@ +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use rayon::prelude::*; +use std::thread; +use warp::signature::function::FunctionGUID; + +use crate::cache::cached_function_guid; + +pub struct FindFunctionFromGUID; + +impl Command for FindFunctionFromGUID { + fn action(&self, view: &BinaryView) { + let Some(guid_str) = binaryninja::interaction::get_text_line_input( + "Function GUID", + "Find Function from GUID", + ) else { + return; + }; + + let Ok(searched_guid) = guid_str.parse::<FunctionGUID>() else { + log::error!("Failed to parse function guid... {}", guid_str); + return; + }; + + log::info!("Searching functions for GUID... {}", searched_guid); + let funcs = view.functions(); + thread::spawn(move || { + let background_task = binaryninja::backgroundtask::BackgroundTask::new( + format!("Searching functions for GUID... {}", searched_guid), + true, + ) + .unwrap(); + + // TODO: While background_task has not finished. + let matched = funcs + .par_iter() + .filter_map(|func| { + Some(( + func.clone(), + cached_function_guid(&func, func.low_level_il_if_available()?.as_ref())?, + )) + }) + .filter(|(_func, guid)| guid.eq(&searched_guid)) + .collect::<Vec<_>>(); + + for (func, _) in matched { + log::info!("Match found at function... 0x{:0x}", func.start()); + } + + background_task.finish(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/types.rs b/plugins/warp/src/plugin/types.rs new file mode 100644 index 00000000..b8718a63 --- /dev/null +++ b/plugins/warp/src/plugin/types.rs @@ -0,0 +1,52 @@ +use crate::convert::to_bn_type; +use binaryninja::binaryview::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use std::time::Instant; + +pub struct LoadTypesCommand; + +impl Command for LoadTypesCommand { + fn action(&self, view: &BinaryView) { + let Some(file) = binaryninja::interaction::get_open_filename_input( + "Apply Signature File Types", + "*.sbin", + ) else { + return; + }; + + let Ok(data) = std::fs::read(&file) else { + log::error!("Could not read signature file: {:?}", file); + return; + }; + + let Some(data) = warp::signature::Data::from_bytes(&data) else { + log::error!("Could not get data from signature file: {:?}", file); + return; + }; + + let view = view.to_owned(); + std::thread::spawn(move || { + let background_task = binaryninja::backgroundtask::BackgroundTask::new( + format!("Applying {} types...", data.types.len()), + true, + ) + .unwrap(); + + let start = Instant::now(); + for comp_ty in data.types { + let ty_id = comp_ty.guid.to_string(); + let ty_name = comp_ty.ty.name.to_owned().unwrap_or_else(|| ty_id.clone()); + // TODO: Using arch here is problematic. + let arch = view.default_arch().unwrap(); + view.define_auto_type_with_id(ty_name, ty_id, &to_bn_type(&arch, &comp_ty.ty)); + } + + log::info!("Type application took {:?}", start.elapsed()); + background_task.finish(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs new file mode 100644 index 00000000..da8473f0 --- /dev/null +++ b/plugins/warp/src/plugin/workflow.rs @@ -0,0 +1,38 @@ +use crate::matcher::cached_function_match; +use binaryninja::llil; +use binaryninja::workflow::{Activity, AnalysisContext, Workflow}; + +const MATCHER_ACTIVITY_NAME: &str = "analysis.plugins.WARPMatcher"; +// NOTE: runOnce is off because previously matched functions need info applied. +const MATCHER_ACTIVITY_CONFIG: &str = r#"{ + "name": "analysis.plugins.WARPMatcher", + "title" : "WARP Matcher", + "description": "This analysis step applies WARP info to matched functions...", + "eligibility": { + "auto": { "default": true }, + "runOnce": false + } +}"#; + +pub fn insert_matcher_workflow() { + let matcher_activity = |ctx: &AnalysisContext| { + let function = ctx.function(); + if function.has_user_annotations() { + // User has touched the function, stop trying to match on it! + return; + } + + if let Some(llil) = unsafe { ctx.llil_function::<llil::NonSSA<llil::RegularNonSSA>>() } { + cached_function_match(&function, &llil); + } + }; + + let meta_workflow = Workflow::new_from_copy("core.function.metaAnalysis"); + let activity = Activity::new_with_action(MATCHER_ACTIVITY_CONFIG, matcher_activity); + meta_workflow.register_activity(&activity).unwrap(); + meta_workflow.insert( + "core.function.runFunctionRecognizers", + [MATCHER_ACTIVITY_NAME], + ); + meta_workflow.register().unwrap(); +} diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__insta_signatures.snap b/plugins/warp/src/snapshots/warp_ninja__tests__insta_signatures.snap new file mode 100644 index 00000000..442fce25 --- /dev/null +++ b/plugins/warp/src/snapshots/warp_ninja__tests__insta_signatures.snap @@ -0,0 +1,39 @@ +--- +source: src/lib.rs +expression: functions +--- +[ + FunctionGUID { + guid: 623a8338-34d6-5a6e-8c4e-36a1a071117e, + }, + FunctionGUID { + guid: 6cd81a21-6967-5c90-b73e-5a810f835a84, + }, + FunctionGUID { + guid: 905fa3b0-3571-58ed-b81f-7cf62bdcfe49, + }, + FunctionGUID { + guid: 9a3e480c-5ebd-5278-8e33-4a6e982167fb, + }, + FunctionGUID { + guid: a25a06fb-fb60-542c-9b11-4c286dbc607b, + }, + FunctionGUID { + guid: b1c5fbad-2657-5231-9f7b-b15a0d3b3bb6, + }, + FunctionGUID { + guid: bcba7769-cc5a-5cdf-be50-d95b7a74ef60, + }, + FunctionGUID { + guid: dd833eb1-f99b-5c58-a289-998dba19e69a, + }, + FunctionGUID { + guid: f631b282-0174-5bdd-846d-c0514f2539e1, + }, + FunctionGUID { + guid: fa2d7ebf-d187-5592-bfc1-ee41614437b3, + }, + FunctionGUID { + guid: fa2d7ebf-d187-5592-bfc1-ee41614437b3, + }, +] |
