diff options
Diffstat (limited to 'plugins/warp/src')
46 files changed, 5485 insertions, 3411 deletions
diff --git a/plugins/warp/src/bin/sigem.rs b/plugins/warp/src/bin/sigem.rs deleted file mode 100644 index a50b4ae2..00000000 --- a/plugins/warp/src/bin/sigem.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::collections::HashSet; -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::binary_view::{BinaryView, BinaryViewExt}; -use binaryninja::function::Function as BNFunction; -use binaryninja::rc::Guard as BNGuard; -use binaryninja::settings::Settings; -use serde_json::{json, Value}; -use walkdir::WalkDir; -use warp::signature::Data; -use warp_ninja::cache::{cached_type_references, register_cache_destructor}; - -#[derive(Parser, Debug)] -#[command(about, long_about)] -/// A simple CLI utility to generate WARP signature files headlessly using Binary Ninja. -/// -/// NOTE: This requires a headless compatible Binary Ninja, make sure it's in your path. -struct Args { - /// Path to create signatures from, this can be: - /// - A binary (that can be opened with Binary Ninja) - /// - A directory (all files will be merged) - /// - An archive (with ext: a, lib, rlib) - /// - A BNDB - /// - A Signature file (sbin) - #[arg(index = 1, verbatim_doc_comment)] - path: PathBuf, - - /// The signature output file - /// - /// NOTE: If not specified the output will be the input path with the sbin extension - /// as an example `mylib.a` will output `mylib.sbin`. - #[arg(index = 2)] - output: Option<PathBuf>, - - /// Should we overwrite output file - /// - /// NOTE: If the file exists we will exit early to prevent wasted effort. - #[arg(short, long)] - overwrite: Option<bool>, - - /// The external debug information file to use - #[arg(short, long)] - debug_info: Option<PathBuf>, - // TODO: Add a file filter and default to filter out files starting with "." -} - -fn default_settings(bn_settings: &Settings) -> Value { - // TODO: Make these settings configurable through the CLI - let mut settings = json!({ - "analysis.linearSweep.autorun": false, - "analysis.signatureMatcher.autorun": false, - "analysis.mode": "full", - // The reason we need to do this is a little unfortunate. - // Basically some of the COFF's have really low image bases that confuses - // Analysis and also our basic block GUID when a constant value points to a low address section. - // TODO: This might not exist, we should set this based on the view. - "loader.imageBase": 0x1000000, - }); - - // If WARP is enabled we must turn it off to prevent matching on other stuff. - if bn_settings.contains("analysis.warp.matcher") { - settings["analysis.warp.matcher"] = json!(false); - settings["analysis.warp.guid"] = json!(false); - } - - settings -} - -fn main() { - let args = Args::parse(); - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - - // TODO: After analysis finishes for a file we should save off the bndb to another directory called the bndb cache - // TODO: This cache should be used before opening a file for first analysis. - - // TODO: We should resolve the path to something sensible in cases where user is passing CWD. - // If no output file was given, just prepend binary with extension sbin - let output_file = args - .output - .unwrap_or(args.path.to_owned()) - .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().expect("Failed to initialize session"); - - // Adjust the amount of worker threads so that we can actually free BinaryViews. - let worker_count = rayon::current_num_threads() * 4; - log::debug!("Adjusting Binary Ninja worker count to {}...", worker_count); - binaryninja::worker_thread::set_worker_thread_count(worker_count); - - // Make sure caches are flushed when the views get destructed. - register_cache_destructor(); - - let bn_settings = Settings::new(); - let settings = default_settings(&bn_settings); - - log::info!("Creating functions for {:?}...", args.path); - let start = std::time::Instant::now(); - let data = data_from_file(&settings, &args.path) - .expect("Failed to read data, check your license and Binary Ninja version!"); - 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.path); - } -} - -fn data_from_view(view: &BinaryView) -> Data { - let mut data = Data::default(); - let is_function_named = |f: &BNGuard<BNFunction>| { - !f.symbol().short_name().to_string_lossy().contains("sub_") || f.has_user_annotations() - }; - - data.functions = view - .functions() - .iter() - .filter(is_function_named) - .filter_map(|f| { - let llil = f.low_level_il().ok()?; - Some(warp_ninja::cache::cached_function(&f, &llil)) - }) - .collect::<Vec<_>>(); - - if let Some(ref_ty_cache) = cached_type_references(view) { - let referenced_types = ref_ty_cache - .cache - .iter() - .filter_map(|t| t.to_owned()) - .collect::<Vec<_>>(); - - data.types.extend(referenced_types); - } - - data -} - -fn data_from_archive<R: Read>(settings: &Value, 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: HashSet<PathBuf> = HashSet::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); - if !entry_files.contains(&output_path) { - 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.insert(output_path); - } else { - log::debug!("Skipping already inserted entry: {}", name); - } - } - Err(e) => { - log::error!("Failed to read archive entry: {}", e); - } - } - } - - // Create the data. - let entry_data = entry_files - .into_par_iter() - .filter_map(|path| { - log::debug!("Creating data for ENTRY {:?}...", path); - data_from_file(settings, &path) - }) - .collect::<Vec<_>>(); - - Some(Data::merge(entry_data)) -} - -fn data_from_directory(settings: &Value, dir: PathBuf) -> Option<Data> { - let files = WalkDir::new(dir) - .into_iter() - .filter_map(|e| { - let path = e.ok()?.into_path(); - if path.is_file() { - Some(path) - } else { - None - } - }) - .collect::<Vec<_>>(); - - let unmerged_data = files - .into_par_iter() - .filter_map(|path| { - log::info!("Creating data for FILE {:?}...", path); - data_from_file(settings, &path) - }) - .collect::<Vec<_>>(); - - if !unmerged_data.is_empty() { - Some(Data::merge(unmerged_data)) - } else { - None - } -} - -fn data_from_file(settings: &Value, path: &Path) -> Option<Data> { - 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(settings, archive) - } - Some(ext) if ext == "sbin" => { - let contents = std::fs::read(path).ok()?; - Data::from_bytes(&contents) - } - _ if path.is_dir() => data_from_directory(settings, path.into()), - _ => { - let path_str = path.to_str().unwrap(); - let view = binaryninja::load_with_options(path_str, true, Some(settings.to_string()))?; - let data = data_from_view(&view); - view.file().close(); - Some(data) - } - } -} - -#[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().expect("Failed to initialize session"); - let bn_settings = Settings::new(); - let settings = default_settings(&bn_settings); - 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(&settings, &path); - assert!(result.is_some()); - } - } - } -} diff --git a/plugins/warp/src/cache.rs b/plugins/warp/src/cache.rs index cc8dfded..09ad1444 100644 --- a/plugins/warp/src/cache.rs +++ b/plugins/warp/src/cache.rs @@ -1,29 +1,18 @@ -use crate::convert::{from_bn_symbol, from_bn_type_internal}; -use crate::{build_function, function_guid}; +pub mod container; +pub mod function; +pub mod guid; +pub mod type_reference; + +pub use function::*; +pub use guid::*; +pub use type_reference::*; + use binaryninja::binary_view::{BinaryView, BinaryViewExt}; -use binaryninja::confidence::MAX_CONFIDENCE; use binaryninja::function::Function as BNFunction; -use binaryninja::low_level_il::function::{FunctionMutability, LowLevelILFunction, NonSSA}; -use binaryninja::low_level_il::LowLevelILRegularFunction; use binaryninja::rc::Guard; use binaryninja::rc::Ref as BNRef; -use binaryninja::symbol::Symbol as BNSymbol; -use binaryninja::types::NamedTypeReference as BNNamedTypeReference; use binaryninja::ObjectDestructor; -use dashmap::mapref::one::Ref; -use dashmap::DashMap; -use std::collections::HashSet; use std::hash::{DefaultHasher, Hash, Hasher}; -use std::sync::OnceLock; -use warp::r#type::ComputedType; -use warp::signature::function::constraints::FunctionConstraint; -use warp::signature::function::{Function, FunctionGUID}; - -pub static MATCHED_FUNCTION_CACHE: OnceLock<DashMap<ViewID, MatchedFunctionCache>> = - OnceLock::new(); -pub static FUNCTION_CACHE: OnceLock<DashMap<ViewID, FunctionCache>> = OnceLock::new(); -pub static GUID_CACHE: OnceLock<DashMap<ViewID, GUIDCache>> = OnceLock::new(); -pub static TYPE_REF_CACHE: OnceLock<DashMap<ViewID, TypeRefCache>> = OnceLock::new(); pub fn register_cache_destructor() { pub static mut CACHE_DESTRUCTOR: CacheDestructor = CacheDestructor; @@ -34,351 +23,6 @@ pub fn register_cache_destructor() { }; } -pub fn cached_function_match<F>(function: &BNFunction, f: F) -> Option<Function> -where - F: Fn() -> Option<Function>, -{ - let view = function.view(); - let view_id = ViewID::from(view.as_ref()); - let function_id = FunctionID::from(function); - let function_cache = MATCHED_FUNCTION_CACHE.get_or_init(Default::default); - match function_cache.get(&view_id) { - Some(cache) => cache.get_or_insert(&function_id, f).to_owned(), - None => { - let cache = MatchedFunctionCache::default(); - let matched = cache.get_or_insert(&function_id, f).to_owned(); - function_cache.insert(view_id, cache); - matched - } - } -} - -pub fn try_cached_function_match(function: &BNFunction) -> Option<Function> { - let view = function.view(); - let view_id = ViewID::from(view); - let function_id = FunctionID::from(function); - let function_cache = MATCHED_FUNCTION_CACHE.get_or_init(Default::default); - function_cache - .get(&view_id)? - .get(&function_id)? - .value() - .to_owned() -} - -pub fn cached_function(function: &BNFunction, llil: &LowLevelILRegularFunction) -> 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<F>( - function: &BNFunction, - filter: F, -) -> HashSet<FunctionConstraint> -where - F: Fn(&BNFunction) -> bool, -{ - 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, filter), - None => { - let cache = GUIDCache::default(); - let constraints = cache.adjacency_constraints(function, filter); - guid_cache.insert(view_id, cache); - constraints - } - } -} - -pub fn cached_function_guid<M: FunctionMutability>( - function: &BNFunction, - llil: &LowLevelILFunction<M, NonSSA>, -) -> 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 - } - } -} - -pub fn try_cached_function_guid(function: &BNFunction) -> Option<FunctionGUID> { - let view = function.view(); - let view_id = ViewID::from(view); - let guid_cache = GUID_CACHE.get_or_init(Default::default); - guid_cache.get(&view_id)?.try_function_guid(function) -} - -pub fn cached_type_reference( - view: &BinaryView, - visited_refs: &mut HashSet<TypeRefID>, - type_ref: &BNNamedTypeReference, -) -> Option<ComputedType> { - let view_id = ViewID::from(view); - let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default); - match type_ref_cache.get(&view_id) { - Some(cache) => cache.cached_type_reference(view, visited_refs, type_ref), - None => { - let cache = TypeRefCache::default(); - let ntr = cache.cached_type_reference(view, visited_refs, type_ref); - type_ref_cache.insert(view_id, cache); - ntr - } - } -} - -pub fn cached_type_references(view: &BinaryView) -> Option<Ref<ViewID, TypeRefCache>> { - let view_id = ViewID::from(view); - let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default); - type_ref_cache.get(&view_id) -} - -#[derive(Clone, Debug, Default)] -pub struct MatchedFunctionCache { - pub cache: DashMap<FunctionID, Option<Function>>, -} - -impl MatchedFunctionCache { - pub fn get_or_insert<F>( - &self, - function_id: &FunctionID, - f: F, - ) -> Ref<'_, FunctionID, Option<Function>> - where - F: FnOnce() -> Option<Function>, - { - self.cache.get(function_id).unwrap_or_else(|| { - self.cache.insert(*function_id, f()); - self.cache.get(function_id).unwrap() - }) - } - - pub fn get(&self, function_id: &FunctionID) -> Option<Ref<'_, FunctionID, Option<Function>>> { - self.cache.get(function_id) - } -} - -#[derive(Clone, Debug, Default)] -pub struct FunctionCache { - pub cache: DashMap<FunctionID, Function>, -} - -impl FunctionCache { - pub fn function(&self, function: &BNFunction, llil: &LowLevelILRegularFunction) -> Function { - let function_id = FunctionID::from(function); - match self.cache.get(&function_id) { - Some(function) => function.value().to_owned(), - None => { - let function = build_function(function, llil); - self.cache.insert(function_id, function.clone()); - function - } - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct GUIDCache { - pub cache: DashMap<FunctionID, 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 func_platform = function.platform(); - let mut constraints = HashSet::new(); - for call_site in &function.call_sites() { - for cs_ref_addr in view.code_refs_from_addr(call_site.address, Some(function)) { - match view.function_at(&func_platform, cs_ref_addr) { - Some(cs_ref_func) => { - // Call site is a function, constrain on it. - let cs_ref_func_id = FunctionID::from(cs_ref_func.as_ref()); - if cs_ref_func_id != func_id { - let call_site_offset: i64 = - call_site.address.wrapping_sub(func_start) as i64; - // TODO: If the function is thunk we should also insert the called function. - constraints - .insert(self.function_constraint(&cs_ref_func, call_site_offset)); - } - } - None => { - // We could be dealing with an extern symbol, get the symbol as a constraint. - let call_site_offset: i64 = - call_site.address.wrapping_sub(func_start) as i64; - if let Some(call_site_sym) = view.symbol_by_address(cs_ref_addr) { - constraints.insert( - self.function_constraint_from_symbol( - &call_site_sym, - call_site_offset, - ), - ); - } - } - } - } - } - constraints - } - - pub fn adjacency_constraints<F>( - &self, - function: &BNFunction, - filter: F, - ) -> HashSet<FunctionConstraint> - where - F: Fn(&BNFunction) -> bool, - { - 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 && filter(curr_func.as_ref()) { - // NOTE: For this to work the GUID has to have already been cached. If not it will just be the symbol. - // 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_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(&self, function: &BNFunction, offset: i64) -> FunctionConstraint { - let guid = self.try_function_guid(function); - let symbol = from_bn_symbol(&function.symbol()); - FunctionConstraint { - guid, - symbol: Some(symbol), - offset, - } - } - - /// Construct a function constraint from a symbol, typically used for extern function call sites, must pass the offset at which it is located. - pub fn function_constraint_from_symbol( - &self, - symbol: &BNSymbol, - offset: i64, - ) -> FunctionConstraint { - let symbol = from_bn_symbol(symbol); - FunctionConstraint { - guid: None, - symbol: Some(symbol), - offset, - } - } - - pub fn function_guid<M: FunctionMutability>( - &self, - function: &BNFunction, - llil: &LowLevelILFunction<M, NonSSA>, - ) -> FunctionGUID { - let function_id = FunctionID::from(function); - match self.cache.get(&function_id) { - Some(function_guid) => function_guid.value().to_owned(), - None => { - let function_guid = function_guid(function, llil); - self.cache.insert(function_id, function_guid); - function_guid - } - } - } - - pub fn try_function_guid(&self, function: &BNFunction) -> Option<FunctionGUID> { - let function_id = FunctionID::from(function); - self.cache - .get(&function_id) - .map(|function_guid| function_guid.value().to_owned()) - } -} - -#[derive(Clone, Debug, Default)] -pub struct TypeRefCache { - pub cache: DashMap<TypeRefID, Option<ComputedType>>, -} - -impl TypeRefCache { - /// NOTE: No self-referential type must be used on this function. - pub fn cached_type_reference( - &self, - view: &BinaryView, - visited_refs: &mut HashSet<TypeRefID>, - type_ref: &BNNamedTypeReference, - ) -> Option<ComputedType> { - let ntr_id = TypeRefID::from(type_ref); - match self.cache.get(&ntr_id) { - Some(cache) => cache.to_owned(), - None => match type_ref.target(view) { - Some(raw_ty) => { - let computed_ty = ComputedType::new(from_bn_type_internal( - view, - visited_refs, - &raw_ty, - MAX_CONFIDENCE, - )); - self.cache - .entry(ntr_id) - .insert(Some(computed_ty)) - .to_owned() - } - None => self.cache.entry(ntr_id).insert(None).to_owned(), - }, - } - } -} - /// A unique view ID, used for caching. #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct ViewID(u64); @@ -430,48 +74,11 @@ impl From<Guard<'_, BNFunction>> for FunctionID { } } -/// A unique named type reference ID, used for caching. -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct TypeRefID(u64); - -impl From<&BNNamedTypeReference> for TypeRefID { - fn from(value: &BNNamedTypeReference) -> Self { - let mut hasher = DefaultHasher::new(); - hasher.write(value.id().as_bytes()); - Self(hasher.finish()) - } -} - -impl From<BNRef<BNNamedTypeReference>> for TypeRefID { - fn from(value: BNRef<BNNamedTypeReference>) -> Self { - Self::from(value.as_ref()) - } -} - -impl From<Guard<'_, BNNamedTypeReference>> for TypeRefID { - fn from(value: Guard<'_, BNNamedTypeReference>) -> Self { - Self::from(value.as_ref()) - } -} - pub struct CacheDestructor; impl ObjectDestructor for CacheDestructor { fn destruct_view(&self, view: &BinaryView) { - // Clear caches as the view is no longer alive. - let view_id = ViewID::from(view); - if let Some(cache) = MATCHED_FUNCTION_CACHE.get() { - cache.remove(&view_id); - } - if let Some(cache) = FUNCTION_CACHE.get() { - cache.remove(&view_id); - } - if let Some(cache) = GUID_CACHE.get() { - cache.remove(&view_id); - } - if let Some(cache) = TYPE_REF_CACHE.get() { - cache.remove(&view_id); - } + clear_type_ref_cache(view); log::debug!("Removed WARP caches for {:?}", view.file().filename()); } } diff --git a/plugins/warp/src/cache/container.rs b/plugins/warp/src/cache/container.rs new file mode 100644 index 00000000..59b31bad --- /dev/null +++ b/plugins/warp/src/cache/container.rs @@ -0,0 +1,28 @@ +use crate::container::Container; +use dashmap::DashMap; +use std::ops::Deref; +use std::sync::{Arc, OnceLock, RwLock}; + +pub static CONTAINER_CACHE: OnceLock<DashMap<String, Arc<RwLock<Box<dyn Container>>>>> = + OnceLock::new(); + +pub fn for_cached_containers(f: impl Fn(&dyn Container)) { + let containers_cache = CONTAINER_CACHE.get_or_init(Default::default); + for container in containers_cache.iter() { + if let Ok(guarded_container) = container.read() { + f(guarded_container.deref().as_ref()); + } + } +} + +// TODO: The static lifetime here is a little wierd... (we need it to Box) +pub fn add_cached_container(container: impl Container + 'static) { + let containers_cache = CONTAINER_CACHE.get_or_init(Default::default); + let container_name = container.to_string(); + containers_cache.insert(container_name, Arc::new(RwLock::new(Box::new(container)))); +} + +pub fn cached_containers() -> Vec<Arc<RwLock<Box<dyn Container>>>> { + let containers_cache = CONTAINER_CACHE.get_or_init(Default::default); + containers_cache.iter().map(|c| c.clone()).collect() +} diff --git a/plugins/warp/src/cache/function.rs b/plugins/warp/src/cache/function.rs new file mode 100644 index 00000000..2fef05fd --- /dev/null +++ b/plugins/warp/src/cache/function.rs @@ -0,0 +1,28 @@ +use binaryninja::function::{Function as BNFunction, FunctionUpdateType}; +use warp::signature::function::Function; + +/// Inserts a function match into the cache. +/// +/// IMPORTANT: This will mark the function as needing updates, if you intend to fill in functions with +/// no match (i.e. `None`), then you must change this function to prevent marking that as needing updates. +/// However, it's perfectly valid to remove a match and need to update the function still, so be careful. +pub fn insert_cached_function_match(function: &BNFunction, matched_function: Option<Function>) { + // NOTE: If we expect to run match_function multiple times on a function, we should move this elsewhere. + // Mark the function as needing updates so that reanalysis occurs on the function, and we apply the match. + function.mark_updates_required(FunctionUpdateType::FullAutoFunctionUpdate); + match matched_function { + Some(matched_function) => { + function.store_metadata("warp_matched_function", &matched_function.to_bytes(), false); + } + None => { + function.remove_metadata("warp_matched_function"); + } + } +} + +// TODO: This does allocations, and for every reanalysis. +pub fn try_cached_function_match(function: &BNFunction) -> Option<Function> { + let metadata = function.query_metadata("warp_matched_function")?; + let raw_metadata = metadata.get_raw()?; + Function::from_bytes(&raw_metadata) +} diff --git a/plugins/warp/src/cache/guid.rs b/plugins/warp/src/cache/guid.rs new file mode 100644 index 00000000..f1788a33 --- /dev/null +++ b/plugins/warp/src/cache/guid.rs @@ -0,0 +1,193 @@ +use crate::cache::FunctionID; +use crate::convert::from_bn_symbol; +use crate::function_guid; +use binaryninja::binary_view::BinaryViewExt; +use binaryninja::function::Function as BNFunction; +use binaryninja::low_level_il::function::{FunctionMutability, LowLevelILFunction, NonSSA}; +use binaryninja::symbol::Symbol as BNSymbol; +use std::collections::HashSet; +use uuid::Uuid; +use warp::signature::constraint::Constraint; +use warp::signature::function::FunctionGUID; + +pub fn cached_function_guid<M: FunctionMutability>( + function: &BNFunction, + lifted_il: &LowLevelILFunction<M, NonSSA>, +) -> FunctionGUID { + let cached_guid = try_cached_function_guid(function); + if let Some(cached_guid) = cached_guid { + return cached_guid; + } + + let function_guid = function_guid(function, lifted_il); + function.store_metadata( + "warp_function_guid", + &function_guid.as_bytes().to_vec(), + false, + ); + function_guid +} + +pub fn try_cached_function_guid(function: &BNFunction) -> Option<FunctionGUID> { + let metadata = function.query_metadata("warp_function_guid")?; + let raw_metadata = metadata.get_raw()?; + let uuid = Uuid::from_slice(raw_metadata.as_slice()).ok()?; + Some(FunctionGUID::from(uuid)) +} + +pub fn cached_constraints<F>(function: &BNFunction, filter: F) -> HashSet<Constraint> +where + F: Fn(&BNFunction) -> bool, +{ + // TODO: Implied constraints, symbol name, image offset + let cs_constraints = cached_call_site_constraints(function); + let adj_constraints = cached_adjacency_constraints(function, filter); + cs_constraints.union(&adj_constraints).cloned().collect() +} + +pub fn cached_call_site_constraints(function: &BNFunction) -> HashSet<Constraint> { + let cache = ConstraintBuilder; + cache.call_site_constraints(function) +} + +pub fn cached_adjacency_constraints<F>(function: &BNFunction, filter: F) -> HashSet<Constraint> +where + F: Fn(&BNFunction) -> bool, +{ + let cache = ConstraintBuilder; + cache.adjacency_constraints(function, filter) +} + +#[derive(Clone, Debug, Default)] +pub struct ConstraintBuilder; + +impl ConstraintBuilder { + pub fn call_site_constraints(&self, function: &BNFunction) -> HashSet<Constraint> { + let view = function.view(); + let func_id = FunctionID::from(function); + let func_start = function.start(); + let func_platform = function.platform(); + let mut constraints = HashSet::new(); + for call_site in &function.call_sites() { + for cs_ref_addr in view.code_refs_from_addr(call_site.address, Some(function)) { + match view.function_at(&func_platform, cs_ref_addr) { + Some(cs_ref_func) => { + // Call site is a function, constrain on it. + let cs_ref_func_id = FunctionID::from(cs_ref_func.as_ref()); + if cs_ref_func_id != func_id { + let call_site_offset: i64 = + call_site.address.wrapping_sub(func_start) as i64; + // TODO: If the function is thunk we should also insert the called function. + constraints.extend( + self.related_function_constraint(&cs_ref_func, call_site_offset), + ); + } + } + None => { + // We could be dealing with an extern symbol, get the symbol as a constraint. + let call_site_offset: i64 = + call_site.address.wrapping_sub(func_start) as i64; + if let Some(call_site_sym) = view.symbol_by_address(cs_ref_addr) { + constraints.insert( + self.related_symbol_constraint(&call_site_sym, call_site_offset), + ); + } + } + } + } + } + constraints + } + + pub fn adjacency_constraints<F>(&self, function: &BNFunction, filter: F) -> HashSet<Constraint> + where + F: Fn(&BNFunction) -> bool, + { + 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 && filter(curr_func.as_ref()) { + // NOTE: For this to work the GUID has to have already been cached. If not it will just be the symbol. + // Function adjacent to another function, constrain on the pattern. + let curr_addr_offset = (func_start_addr as i64) - func_start as i64; + constraints + .extend(self.related_function_constraint(&curr_func, 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 related_function_constraint( + &self, + function: &BNFunction, + offset: i64, + ) -> Vec<Constraint> { + let mut constraints = vec![]; + if let Some(guid) = try_cached_function_guid(function) { + let guid_constraint = Constraint::from_function(&guid, Some(offset)); + constraints.push(guid_constraint); + } + let symbol_constraint = self.related_symbol_constraint(&function.symbol(), offset); + constraints.push(symbol_constraint); + constraints + } + + /// Construct a symbol constraint, must pass the offset at which it is located. + pub fn related_symbol_constraint(&self, symbol: &BNSymbol, offset: i64) -> Constraint { + let mut symbol = from_bn_symbol(symbol); + symbol.name = clean_symbol_name(&symbol.name); + Constraint::from_symbol(&symbol, Some(offset)) + } +} + +/// Cleans various internal symbol prefixes and suffixes for consistency. +/// +/// This is very important for getting matching symbol constraints. +/// +/// Examples: +/// - "__imp__RemoveDirectoryW@4" -> "RemoveDirectoryW" +/// - "__free_base" -> "free_base" +/// - "__impl__free_base" -> "free_base" +/// - "j___free_base" -> "free_base" +/// - "j_free_base" -> "free_base" +/// - "_free_base" -> "free_base" +pub fn clean_symbol_name(symbol_name: &str) -> String { + // Handle MSVC-style imported symbols + let without_imp = symbol_name.strip_prefix("__imp__").unwrap_or(symbol_name); + + // Handle jump thunk prefix + let without_jump = without_imp.strip_prefix("j_").unwrap_or(without_imp); + + // Strip all remaining leading underscores + let mut result = without_jump; + while result.starts_with('_') { + result = &result[1..]; + } + + // Remove stdcall decoration (@N suffix) + match result.find('@') { + Some(pos) => result[..pos].to_string(), + None => result.to_string(), + } +} diff --git a/plugins/warp/src/cache/type_reference.rs b/plugins/warp/src/cache/type_reference.rs new file mode 100644 index 00000000..0b07c139 --- /dev/null +++ b/plugins/warp/src/cache/type_reference.rs @@ -0,0 +1,105 @@ +use crate::cache::ViewID; +use crate::convert::from_bn_type_internal; +use binaryninja::binary_view::BinaryView; +use binaryninja::confidence::MAX_CONFIDENCE; +use binaryninja::rc::Guard; +use binaryninja::rc::Ref as BNRef; +use binaryninja::types::NamedTypeReference as BNNamedTypeReference; +use dashmap::mapref::one::Ref; +use dashmap::DashMap; +use std::collections::HashSet; +use std::hash::{DefaultHasher, Hasher}; +use std::sync::OnceLock; +use warp::r#type::ComputedType; + +pub static TYPE_REF_CACHE: OnceLock<DashMap<ViewID, TypeRefCache>> = OnceLock::new(); + +pub fn clear_type_ref_cache(view: &BinaryView) { + let view_id = ViewID::from(view); + if let Some(cache) = TYPE_REF_CACHE.get() { + cache.remove(&view_id); + } +} + +pub fn cached_type_reference( + view: &BinaryView, + visited_refs: &mut HashSet<TypeRefID>, + type_ref: &BNNamedTypeReference, +) -> Option<ComputedType> { + let view_id = ViewID::from(view); + let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default); + match type_ref_cache.get(&view_id) { + Some(cache) => cache.cached_type_reference(view, visited_refs, type_ref), + None => { + let cache = TypeRefCache::default(); + let ntr = cache.cached_type_reference(view, visited_refs, type_ref); + type_ref_cache.insert(view_id, cache); + ntr + } + } +} + +pub fn cached_type_references(view: &BinaryView) -> Option<Ref<ViewID, TypeRefCache>> { + let view_id = ViewID::from(view); + let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default); + type_ref_cache.get(&view_id) +} + +#[derive(Clone, Debug, Default)] +pub struct TypeRefCache { + pub cache: DashMap<TypeRefID, Option<ComputedType>>, +} + +impl TypeRefCache { + /// NOTE: No self-referential type must be used on this function. + pub fn cached_type_reference( + &self, + view: &BinaryView, + visited_refs: &mut HashSet<TypeRefID>, + type_ref: &BNNamedTypeReference, + ) -> Option<ComputedType> { + let ntr_id = TypeRefID::from(type_ref); + match self.cache.get(&ntr_id) { + Some(cache) => cache.to_owned(), + None => match type_ref.target(view) { + Some(raw_ty) => { + let computed_ty = ComputedType::new(from_bn_type_internal( + view, + visited_refs, + &raw_ty, + MAX_CONFIDENCE, + )); + self.cache + .entry(ntr_id) + .insert(Some(computed_ty)) + .to_owned() + } + None => self.cache.entry(ntr_id).insert(None).to_owned(), + }, + } + } +} + +/// A unique named type reference ID, used for caching. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct TypeRefID(u64); + +impl From<&BNNamedTypeReference> for TypeRefID { + fn from(value: &BNNamedTypeReference) -> Self { + let mut hasher = DefaultHasher::new(); + hasher.write(value.id().as_bytes()); + Self(hasher.finish()) + } +} + +impl From<BNRef<BNNamedTypeReference>> for TypeRefID { + fn from(value: BNRef<BNNamedTypeReference>) -> Self { + Self::from(value.as_ref()) + } +} + +impl From<Guard<'_, BNNamedTypeReference>> for TypeRefID { + fn from(value: Guard<'_, BNNamedTypeReference>) -> Self { + Self::from(value.as_ref()) + } +} diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs new file mode 100644 index 00000000..8d720021 --- /dev/null +++ b/plugins/warp/src/container.rs @@ -0,0 +1,266 @@ +use crate::container::disk::NAMESPACE_DISK_SOURCE; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; +use std::hash::Hash; +use std::io; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use thiserror::Error; +use uuid::Uuid; +use warp::r#type::guid::TypeGUID; +use warp::r#type::{ComputedType, Type}; +use warp::signature::function::{Function, FunctionGUID}; +use warp::target::Target; + +pub mod disk; +pub mod memory; +pub mod network; + +pub type ContainerResult<T> = Result<T, ContainerError>; + +#[derive(Debug, Error, PartialEq, Eq, Hash)] +pub enum ContainerError { + #[error("source {0} was not found")] + SourceNotFound(SourceId), + #[error("source {0} is not writable")] + SourceNotWritable(SourceId), + #[error("source with path {0} already exists")] + SourceAlreadyExists(SourcePath), + #[error("source with path {0} cannot be created in container")] + CannotCreateSource(SourcePath), + #[error("operation failed due to corrupted data: {0}")] + CorruptedData(&'static str), + #[error("failed io operation: {0}")] + FailedIO(io::ErrorKind), + #[error("source {0} does not have an available path")] + SourcePathUnavailable(SourceId), +} + +/// Represents the ID for a single container source. +/// +/// A [`SourceId`] can be used in multiple separate containers, but **must** be unique in a container. +/// +/// A source is used to relate types and functions separate from the container. This allows +/// type name lookups and for containers which are bandwidth sensitive to exist. +/// +/// An example of a bandwidth-sensitive container would be a container that pulls functions over +/// the network instead of from memory. +/// +/// This type is marked `repr(transparent)` to the underlying `[u8; 16]` type, so it is safe to use in FFI. +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Copy)] +pub struct SourceId(Uuid); + +impl SourceId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +impl From<Uuid> for SourceId { + fn from(value: Uuid) -> Self { + Self(value) + } +} + +impl FromStr for SourceId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Uuid::parse_str(s).map(Into::into) + } +} + +impl Display for SourceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Represents a unique path to a source. +/// +/// This is used when first creating a source for a container, the path is given to the container +/// as otherwise the user has no control over source creation and where the source is ultimately located. +/// +/// While the underlying type is a [`PathBuf`], a source path can be really anything, the [`PathBuf`] +/// just provides an easier way to join segments for nested source locations. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct SourcePath(PathBuf); + +impl SourcePath { + pub fn new(path: PathBuf) -> Self { + Self(path) + } + + pub fn new_with_str(value: &str) -> Self { + Self(PathBuf::from(value)) + } + + pub fn to_source_id(&self) -> SourceId { + // TODO: This path is not relative to the disk container is it? + // TODO: The path here should be relative to the container I think? + // TODO: The above is important so that the id is the same across users. + let value: Vec<u8> = self.to_string().into_bytes(); + SourceId(Uuid::new_v5(&NAMESPACE_DISK_SOURCE, &value)) + } +} + +impl AsRef<PathBuf> for SourcePath { + fn as_ref(&self) -> &PathBuf { + &self.0 + } +} + +impl AsRef<Path> for SourcePath { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +impl Display for SourcePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +} + +/// Storage for WARP information. +/// +/// Containers are made up of sources, see [`SourceId`] for more details. +pub trait Container: Send + Sync + Display + Debug { + /// Available container sources. + /// + /// NOTE: Due to the nature of some containers, this list of sources may be incomplete. Do not + /// rely on this list to retrieve data, instead prefer: + /// - [Container::sources_with_type_guid] + /// - [Container::sources_with_type_guids] + /// - [Container::sources_with_function_guid] + /// - [Container::sources_with_function_guids] + fn sources(&self) -> ContainerResult<Vec<SourceId>>; + + /// Create a new source in the container or add the existing source at the given path to known sources. + /// + /// The returned [`SourceId`] can be used to add, query and remove information from the source. + /// + /// NOTE: Adding a source does **NOT** mean that it, and the data associated with it, has been + /// persisted, you **MUST** call [`Container::commit_source`] to persist the created source. + /// + /// NOTE: Adding a source does **NOT** mean that you can write to it, use [`Container::is_source_writable`] + /// to verify the permissions of the source. + fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId>; + + /// Flush changes made to a source. + /// + /// Because writing to a source can require file or network operations, we let the container + /// offer the ability to hold off performing that operation until the data needs to be committed. + fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool>; + + /// Whether the source can be written to. + /// + /// The source must be mutable to perform the following: + /// - [Container::add_types] + /// - [Container::add_computed_types] + /// - [Container::remove_types] + /// - [Container::add_functions] + /// - [Container::remove_functions] + fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool>; + + /// Whether the source has uncommitted changes or not. + /// + /// NOTE: This is **NOT** whether the source has been committed at all, rather a flag to indicate + /// that a source has uncommitted changes. + fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool>; + + /// Retrieve the [`SourcePath`] for the given source. + /// + /// NOTE: This does not have to be a filesystem path, its representation is dictated + /// by the implementation. + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath>; + + // TODO: Note about commit_source + fn add_types(&mut self, source: &SourceId, types: &[Type]) -> ContainerResult<()> { + let computed_types: Vec<_> = types.iter().cloned().map(ComputedType::new).collect(); + self.add_computed_types(source, &computed_types) + } + + // TODO: Note about commit_source + fn add_computed_types( + &mut self, + source: &SourceId, + types: &[ComputedType], + ) -> ContainerResult<()>; + + // TODO: Note about commit_source + fn remove_types(&mut self, source: &SourceId, guids: &[TypeGUID]) -> ContainerResult<()>; + + // TODO: Note about commit_source + fn add_functions( + &mut self, + target: &Target, + source: &SourceId, + functions: &[Function], + ) -> ContainerResult<()>; + + // TODO: Note about commit_source + fn remove_functions( + &mut self, + target: &Target, + source: &SourceId, + functions: &[Function], + ) -> ContainerResult<()>; + + /// Get the sources that contain a type with the given [`TypeGUID`]. + fn sources_with_type_guid(&self, guid: &TypeGUID) -> ContainerResult<Vec<SourceId>>; + + /// Plural version of [`Container::sources_with_type_guid`]. + /// + /// Each source will have a list of the containing GUID's so that when looking up a source, you give + /// it only the GUID's that it knows about, for networking this means cutting down traffic significantly. + fn sources_with_type_guids( + &self, + guids: &[TypeGUID], + ) -> ContainerResult<HashMap<TypeGUID, Vec<SourceId>>>; + + /// Retrieve all [`TypeGUID`]'s with the given name. + fn type_guids_with_name(&self, source: &SourceId, name: &str) + -> ContainerResult<Vec<TypeGUID>>; + + fn type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<Option<Type>>; + + fn has_type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<bool> { + Ok(self.type_with_guid(source, guid)?.is_some()) + } + + /// Get the sources that contain functions with the given [`FunctionGUID`]. + fn sources_with_function_guid( + &self, + target: &Target, + guid: &FunctionGUID, + ) -> ContainerResult<Vec<SourceId>>; + + // TODO: Allocating with Vec is not good. + /// Plural version of [`Container::sources_with_function_guid`]. + /// + /// Each source will have a list of the containing GUID's so that when looking up a source you give + /// it only the GUID's that it knows about, for networking this means cutting down traffic significantly. + fn sources_with_function_guids( + &self, + target: &Target, + guids: &[FunctionGUID], + ) -> ContainerResult<HashMap<FunctionGUID, Vec<SourceId>>>; + + fn functions_with_guid( + &self, + target: &Target, + source: &SourceId, + guid: &FunctionGUID, + ) -> ContainerResult<Vec<Function>>; + + fn has_function_with_guid( + &self, + target: &Target, + source: &SourceId, + guid: &FunctionGUID, + ) -> ContainerResult<bool> { + Ok(!self.functions_with_guid(target, source, guid)?.is_empty()) + } +} diff --git a/plugins/warp/src/container/disk.rs b/plugins/warp/src/container/disk.rs new file mode 100644 index 00000000..ca685a5f --- /dev/null +++ b/plugins/warp/src/container/disk.rs @@ -0,0 +1,402 @@ +use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; +use std::collections::HashMap; +use std::fmt::{Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::path::PathBuf; +use uuid::{uuid, Uuid}; +use walkdir::{DirEntry, WalkDir}; +use warp::chunk::{Chunk, ChunkKind, CompressionType}; +use warp::r#type::chunk::TypeChunk; +use warp::r#type::guid::TypeGUID; +use warp::r#type::{ComputedType, Type}; +use warp::signature::chunk::SignatureChunk; +use warp::signature::function::{Function, FunctionGUID}; +use warp::target::Target; +use warp::{WarpFile, WarpFileHeader}; + +pub const NAMESPACE_DISK_SOURCE: Uuid = uuid!("ea89e8ab-a27a-432b-8fbd-77b026cd5f41"); + +// TODO: How to support remote projects? I.e. collaboration? +pub struct DiskContainer { + pub name: String, + pub sources: HashMap<SourceId, DiskContainerSource>, +} + +impl DiskContainer { + pub fn new(name: String, sources: HashMap<SourceId, DiskContainerSource>) -> Self { + Self { name, sources } + } + + pub fn new_from_dir(dir_path: PathBuf) -> Self { + let source_from_entry = |entry: DirEntry| { + let path = SourcePath(entry.into_path()); + let source_id = path.to_source_id(); + let path_ext = path.0.extension().unwrap_or_default().to_str(); + match (DiskContainerSource::new_from_path(path.clone()), path_ext) { + (Ok(source), _) => Some((source_id, source)), + (Err(err), Some("warp")) => { + log::error!("Failed to load source '{}' from disk: {}", path, err); + None + } + // We don't care to show errors loading for non-warp files. + (Err(_), _) => None, + } + }; + + // TODO: For now, any file that does not have the "warp" extension will be filtered out. + // TODO: cont. in the future we might want to remove this for convenience. + let name = dir_path.to_string_lossy().to_string(); + let sources = WalkDir::new(dir_path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter(|e| e.path().extension().is_some_and(|e| e == "warp")) + .filter_map(source_from_entry) + .collect(); + + Self::new(name, sources) + } +} + +impl Container for DiskContainer { + fn sources(&self) -> ContainerResult<Vec<SourceId>> { + Ok(self.sources.keys().copied().collect()) + } + + fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> { + // Disk sources have there source id computed from the path. + let source_id = path.to_source_id(); + if self.sources.contains_key(&source_id) { + return Err(ContainerError::SourceAlreadyExists(path)); + } + // NOTE: We let anyone add a file from anywhere on the file system because of this. + match path.0.exists() { + true => { + let disk_source = DiskContainerSource::new_from_path(path.clone())?; + self.sources.insert(source_id, disk_source); + Ok(source_id) + } + false => { + let file = WarpFile::new(WarpFileHeader::new(), vec![]); + let disk_source = DiskContainerSource::new(path, file); + self.sources.insert(source_id, disk_source); + Ok(source_id) + } + } + } + + fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> { + let disk_source = self + .sources + .get_mut(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + + disk_source.commit_to_disk() + } + + fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool> { + let _disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + // TODO: I think this should be up to the container. (cant write to bundled files) + Ok(true) + } + + fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> { + let disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(disk_source.uncommitted) + } + + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { + let disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(disk_source.path.clone()) + } + + fn add_computed_types( + &mut self, + source: &SourceId, + types: &[ComputedType], + ) -> ContainerResult<()> { + let disk_source = self + .sources + .get_mut(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + + disk_source.add_computed_types(types) + } + + // TODO: I believe any remove has to happen immediately, i.e. we cant add an uncommitted for this? + fn remove_types(&mut self, source: &SourceId, _guids: &[TypeGUID]) -> ContainerResult<()> { + let _disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + + // TODO: Do this. + Err(ContainerError::SourceNotWritable(*source)) + } + + fn add_functions( + &mut self, + target: &Target, + source: &SourceId, + functions: &[Function], + ) -> ContainerResult<()> { + let disk_source = self + .sources + .get_mut(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + + disk_source.add_functions(target.clone(), functions) + } + + // TODO: I believe any remove has to happen immediately, i.e. we cant add an uncommitted for this? + fn remove_functions( + &mut self, + _target: &Target, + source: &SourceId, + _functions: &[Function], + ) -> ContainerResult<()> { + let _disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + + // TODO: Do this. + Err(ContainerError::SourceNotWritable(*source)) + } + + fn sources_with_type_guid(&self, guid: &TypeGUID) -> ContainerResult<Vec<SourceId>> { + let sources = self + .sources + .iter() + .filter(|(_, source)| source.has_type_with_guid(guid)) + .map(|(id, _)| *id) + .collect(); + Ok(sources) + } + + fn sources_with_type_guids<'a>( + &'a self, + guids: &'a [TypeGUID], + ) -> ContainerResult<HashMap<TypeGUID, Vec<SourceId>>> { + let mut result: HashMap<TypeGUID, Vec<SourceId>> = HashMap::new(); + for (source_id, source) in &self.sources { + guids + .iter() + .filter(|guid| source.has_type_with_guid(guid)) + .for_each(|guid| result.entry(*guid).or_default().push(*source_id)); + } + Ok(result) + } + + fn type_guids_with_name( + &self, + source: &SourceId, + name: &str, + ) -> ContainerResult<Vec<TypeGUID>> { + let disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(disk_source.type_guids_with_name(name)) + } + + fn type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<Option<Type>> { + let disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(disk_source.type_with_guid(guid)) + } + + fn sources_with_function_guid( + &self, + target: &Target, + guid: &FunctionGUID, + ) -> ContainerResult<Vec<SourceId>> { + let sources = self + .sources + .iter() + .filter(|(_, source)| source.has_function_with_guid(target, guid)) + .map(|(id, _)| *id) + .collect(); + Ok(sources) + } + + fn sources_with_function_guids<'a>( + &self, + target: &Target, + guids: &[FunctionGUID], + ) -> ContainerResult<HashMap<FunctionGUID, Vec<SourceId>>> { + let mut result: HashMap<FunctionGUID, Vec<SourceId>> = HashMap::new(); + for (source_id, source) in &self.sources { + guids + .iter() + .filter(|guid| source.has_function_with_guid(target, guid)) + .for_each(|guid| result.entry(*guid).or_default().push(*source_id)); + } + Ok(result) + } + + fn functions_with_guid( + &self, + target: &Target, + source: &SourceId, + guid: &FunctionGUID, + ) -> ContainerResult<Vec<Function>> { + let disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(disk_source.functions_with_guid(target, guid)) + } +} + +impl Display for DiskContainer { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Debug for DiskContainer { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DiskContainer") + .field("name", &self.name) + .field("sources", &self.sources) + .finish() + } +} + +pub struct DiskContainerSource { + pub path: SourcePath, + file: WarpFile<'static>, + uncommitted: bool, +} + +impl DiskContainerSource { + pub fn new(path: SourcePath, file: WarpFile<'static>) -> Self { + Self { + path, + file, + uncommitted: false, + } + } + + pub fn new_from_path(path: SourcePath) -> ContainerResult<Self> { + // TODO: To keep the lifetime out of DiskContainerSource we do not allow mapping file to memory. + let contents = std::fs::read(&path).map_err(|e| ContainerError::FailedIO(e.kind()))?; + let file = WarpFile::from_owned_bytes(contents).ok_or(ContainerError::CorruptedData( + "file data failed to validate", + ))?; + Ok(Self::new(path, file)) + } + + fn add_computed_types(&mut self, types: &[ComputedType]) -> ContainerResult<()> { + let type_chunk = TypeChunk::new_with_computed(types).ok_or( + ContainerError::CorruptedData("type chunk failed to validate"), + )?; + let chunk = Chunk::new(ChunkKind::Type(type_chunk), CompressionType::None); + self.file.chunks.push(chunk); + self.uncommitted = true; + Ok(()) + } + + fn add_functions(&mut self, target: Target, functions: &[Function]) -> ContainerResult<()> { + let signature_chunk = SignatureChunk::new(functions).ok_or( + ContainerError::CorruptedData("signature chunk failed to validate"), + )?; + let chunk = Chunk::new_with_target( + ChunkKind::Signature(signature_chunk), + CompressionType::None, + target, + ); + self.file.chunks.push(chunk); + self.uncommitted = true; + Ok(()) + } + + fn commit_to_disk(&mut self) -> ContainerResult<bool> { + let file = self.file.to_bytes(); + std::fs::write(&self.path, file).map_err(|e| ContainerError::FailedIO(e.kind()))?; + self.uncommitted = false; + Ok(true) + } + + fn type_guids_with_name(&self, name: &str) -> Vec<TypeGUID> { + let mut found: Vec<TypeGUID> = Vec::new(); + for chunk in &self.file.chunks { + if let ChunkKind::Type(tc) = &chunk.kind { + found.extend( + tc.raw_type_with_name(name) + .into_iter() + .map(|t| TypeGUID::from(t.guid())), + ); + } + } + found + } + + fn type_with_guid(&self, guid: &TypeGUID) -> Option<Type> { + self.file.chunks.iter().find_map(|chunk| { + if let ChunkKind::Type(tc) = &chunk.kind { + tc.type_with_guid(guid) + } else { + None + } + }) + } + + // TODO: When we support reading lazily instead of all in memory. + fn has_type_with_guid(&self, guid: &TypeGUID) -> bool { + self.type_with_guid(guid).is_some() + } + + fn functions_with_guid(&self, target: &Target, guid: &FunctionGUID) -> Vec<Function> { + let mut found: Vec<Function> = Vec::new(); + for chunk in &self.file.chunks { + if chunk.header.target != *target { + continue; + } + if let ChunkKind::Signature(sc) = &chunk.kind { + found.extend(sc.functions_with_guid(guid)); + } + } + found + } + + // TODO: When we support reading lazily instead of all in memory. + fn has_function_with_guid(&self, target: &Target, guid: &FunctionGUID) -> bool { + // TODO: How about we dont clone. + !self.functions_with_guid(target, guid).is_empty() + } +} + +impl Hash for DiskContainerSource { + fn hash<H: Hasher>(&self, state: &mut H) { + self.path.hash(state); + } +} + +impl Display for DiskContainerSource { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path) + } +} + +impl Debug for DiskContainerSource { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DiskContainerSource") + .field("path", &self.path) + .field("file_header", &self.file.header) + .field("file_chunks", &self.file.chunks.len()) + .finish() + } +} diff --git a/plugins/warp/src/container/memory.rs b/plugins/warp/src/container/memory.rs new file mode 100644 index 00000000..cf53390e --- /dev/null +++ b/plugins/warp/src/container/memory.rs @@ -0,0 +1,307 @@ +use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; +use std::collections::HashMap; +use std::fmt::Display; +use warp::r#type::guid::TypeGUID; +use warp::r#type::{ComputedType, Type}; +use warp::signature::function::{Function, FunctionGUID}; +use warp::target::Target; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct MemoryContainer { + sources: HashMap<SourceId, MemorySource>, +} + +impl MemoryContainer { + pub fn new() -> Self { + MemoryContainer::default() + } + + pub fn with_source(mut self, id: SourceId, source: MemorySource) -> Self { + self.sources.insert(id, source); + self + } + + pub fn with_source_function( + mut self, + id: SourceId, + guid: FunctionGUID, + func: Function, + ) -> Self { + self.sources + .entry(id) + .or_default() + .functions + .entry(guid) + .or_default() + .push(func); + self + } + + pub fn with_source_type(mut self, id: SourceId, guid: TypeGUID, ty: Type) -> Self { + self.sources.entry(id).or_default().types.insert(guid, ty); + self + } +} + +impl Container for MemoryContainer { + fn sources(&self) -> ContainerResult<Vec<SourceId>> { + todo!() + } + + fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> { + Err(ContainerError::CannotCreateSource(path)) + } + + fn commit_source(&mut self, _source: &SourceId) -> ContainerResult<bool> { + Ok(false) + } + + fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool> { + let memory_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(memory_source.writable) + } + + fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> { + let _memory_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + // NOTE: Memory containers do not have a notion of uncommitted data. + Ok(false) + } + + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { + Err(ContainerError::SourcePathUnavailable(*source)) + } + + fn add_computed_types( + &mut self, + source: &SourceId, + types: &[ComputedType], + ) -> ContainerResult<()> { + let memory_source = self + .sources + .get_mut(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + match memory_source.writable { + true => { + for ty in types { + memory_source.types.insert(ty.guid, ty.ty.clone()); + } + Ok(()) + } + false => Err(ContainerError::SourceNotWritable(*source)), + } + } + + fn remove_types(&mut self, source: &SourceId, guids: &[TypeGUID]) -> ContainerResult<()> { + let memory_source = self + .sources + .get_mut(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + match memory_source.writable { + true => { + for guid in guids { + memory_source.types.remove(guid); + } + Ok(()) + } + false => Err(ContainerError::SourceNotWritable(*source)), + } + } + + fn add_functions( + &mut self, + _target: &Target, + source: &SourceId, + functions: &[Function], + ) -> ContainerResult<()> { + let memory_source = self + .sources + .get_mut(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + match memory_source.writable { + true => { + for function in functions { + memory_source + .functions + .entry(function.guid) + .or_default() + .push(function.clone()); + } + Ok(()) + } + false => Err(ContainerError::SourceNotWritable(*source)), + } + } + + fn remove_functions( + &mut self, + _target: &Target, + source: &SourceId, + functions: &[Function], + ) -> ContainerResult<()> { + let memory_source = self + .sources + .get_mut(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + match memory_source.writable { + true => { + for function in functions { + if let Some(src_funcs) = memory_source.functions.get_mut(&function.guid) { + src_funcs.retain(|f| f != function); + if src_funcs.is_empty() { + memory_source.functions.remove(&function.guid); + } + } + } + Ok(()) + } + false => Err(ContainerError::SourceNotWritable(*source)), + } + } + + fn sources_with_type_guid(&self, guid: &TypeGUID) -> ContainerResult<Vec<SourceId>> { + let sources = self + .sources + .iter() + .filter(|(_, source)| source.has_type_with_guid(guid)) + .map(|(id, _)| *id) + .collect(); + Ok(sources) + } + + fn sources_with_type_guids( + &self, + guids: &[TypeGUID], + ) -> ContainerResult<HashMap<TypeGUID, Vec<SourceId>>> { + let mut result: HashMap<TypeGUID, Vec<SourceId>> = HashMap::new(); + for (source_id, source) in &self.sources { + guids + .iter() + .filter(|guid| source.has_type_with_guid(guid)) + .for_each(|guid| result.entry(*guid).or_default().push(*source_id)); + } + Ok(result) + } + + fn type_guids_with_name( + &self, + source: &SourceId, + name: &str, + ) -> ContainerResult<Vec<TypeGUID>> { + let memory_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(memory_source.type_guids_with_name(name)) + } + + fn type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<Option<Type>> { + let memory_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(memory_source.type_with_guid(guid)) + } + + fn sources_with_function_guid( + &self, + _target: &Target, + guid: &FunctionGUID, + ) -> ContainerResult<Vec<SourceId>> { + let sources = self + .sources + .iter() + .filter(|(_, source)| source.has_function_with_guid(guid)) + .map(|(id, _)| *id) + .collect(); + Ok(sources) + } + + fn sources_with_function_guids( + &self, + _target: &Target, + guids: &[FunctionGUID], + ) -> ContainerResult<HashMap<FunctionGUID, Vec<SourceId>>> { + let mut result: HashMap<FunctionGUID, Vec<SourceId>> = HashMap::new(); + for (source_id, source) in &self.sources { + guids + .iter() + .filter(|guid| source.has_function_with_guid(guid)) + .for_each(|guid| result.entry(*guid).or_default().push(*source_id)); + } + Ok(result) + } + + fn functions_with_guid( + &self, + _target: &Target, + source: &SourceId, + guid: &FunctionGUID, + ) -> ContainerResult<Vec<Function>> { + let memory_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(memory_source.functions_with_guid(guid)) + } +} + +impl Display for MemoryContainer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("MemoryContainer") + } +} + +/// An in-memory store of functions. +/// +/// This is typically an overlay on top of a container source. +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct MemorySource { + pub writable: bool, + pub functions: HashMap<FunctionGUID, Vec<Function>>, + pub types: HashMap<TypeGUID, Type>, + pub named_types: HashMap<String, Vec<TypeGUID>>, +} + +impl MemorySource { + pub fn type_guids_with_name(&self, name: &str) -> Vec<TypeGUID> { + // TODO: The function here is a little goofy. + // TODO: This is cloned. + self.named_types.get(name).cloned().unwrap_or_default() + } + + pub fn type_with_guid(&self, guid: &TypeGUID) -> Option<Type> { + // TODO: This is cloned. + self.types.get(guid).cloned() + } + + pub fn functions_with_guid(&self, guid: &FunctionGUID) -> Vec<Function> { + // TODO: The function here is a little goofy. + // TODO: This is cloned. + self.functions.get(guid).cloned().unwrap_or_default() + } + + pub fn has_type_with_guid(&self, guid: &TypeGUID) -> bool { + self.type_with_guid(guid).is_some() + } + + pub fn has_function_with_guid(&self, guid: &FunctionGUID) -> bool { + !self.functions_with_guid(guid).is_empty() + } +} + +impl Default for MemorySource { + fn default() -> Self { + Self { + writable: true, + functions: HashMap::new(), + types: HashMap::new(), + named_types: HashMap::new(), + } + } +} diff --git a/plugins/warp/src/container/network.rs b/plugins/warp/src/container/network.rs new file mode 100644 index 00000000..ffbe6108 --- /dev/null +++ b/plugins/warp/src/container/network.rs @@ -0,0 +1,13 @@ +pub struct NetworkContainer {} + +// TODO: The container is populated as the user is navigating a binary +// TODO: We need to have a few helper functions here to post and pull +// TODO: Then in the interface we operate off the network cache +// TODO: The network cache could just be a disk container? Or disk container sources? +// TODO: We should also store the cache on the filesystem for a certain time, will need to timestamp +// TODO: When we commit we need to actually POST i believe. +// TODO: There needs to be a setting that adjusts the sweep size of functions at the cursor. +// TODO: Probably need a callback or something to tell the network containers to refresh from the network. +// TODO: The network container should never instantiate itself, unless its gurenteed to not have any data in it? + +// TODO: Need to PUSH chunks and PULL chunks diff --git a/plugins/warp/src/convert.rs b/plugins/warp/src/convert.rs index b6c2f7b4..8272768b 100644 --- a/plugins/warp/src/convert.rs +++ b/plugins/warp/src/convert.rs @@ -1,678 +1,54 @@ -use std::collections::HashSet; +pub mod symbol; +pub mod types; -use binaryninja::architecture::Architecture as BNArchitecture; -use binaryninja::architecture::ArchitectureExt; -use binaryninja::binary_view::{BinaryView, BinaryViewExt}; -use binaryninja::calling_convention::CoreCallingConvention as BNCallingConvention; -use binaryninja::confidence::{Conf as BNConf, MAX_CONFIDENCE}; -use binaryninja::rc::Ref as BNRef; -use binaryninja::symbol::{Symbol as BNSymbol, SymbolType as BNSymbolType}; -use binaryninja::types::{ - BaseStructure as BNBaseStructure, 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 binaryninja::function::Comment as BNComment; +use binaryninja::function::Function as BNFunction; +use binaryninja::platform::Platform; +use binaryninja::rc::Ref; +pub use symbol::*; +pub use types::*; +use warp::signature::comment::FunctionComment; +use warp::target::Target; -use crate::cache::{cached_type_reference, TypeRefID}; -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::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_lossy().to_string(); - match raw_symbol.sym_type() { - BNSymbolType::ImportAddress => { - Symbol::new( - symbol_name, - SymbolClass::Function, - // TODO: External = symbolic i guess - SymbolModifiers::External, - ) - } - BNSymbolType::Data => { - Symbol::new( - symbol_name, - // TODO: Data? - SymbolClass::Data, - SymbolModifiers::default(), - ) - } - BNSymbolType::Symbolic => { - Symbol::new( - symbol_name, - SymbolClass::Function, - // TODO: External = symbolic i guess - SymbolModifiers::External, - ) - } - BNSymbolType::LocalLabel => { - // TODO: This is a placeholder for another symbol. - Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External) - } - BNSymbolType::External => Symbol::new( - symbol_name, - // TODO: External data? - SymbolClass::Function, - SymbolModifiers::External, - ), - BNSymbolType::ImportedData => { - Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External) - } - BNSymbolType::LibraryFunction | BNSymbolType::Function => Symbol::new( - symbol_name, - SymbolClass::Function, - SymbolModifiers::default(), - ), - BNSymbolType::ImportedFunction => Symbol::new( - symbol_name, - SymbolClass::Function, - // TODO: Exported? - 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 Some((full_name, _)) = - binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false) - { - symbol_builder = symbol_builder.full_name(full_name); - } - if let Some((short_name, _)) = - binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false) - { - symbol_builder = symbol_builder.short_name(short_name); - } - } - symbol_builder.create() -} - -pub fn from_bn_type(view: &BinaryView, raw_ty: &BNType, confidence: u8) -> Type { - from_bn_type_internal(view, &mut HashSet::new(), raw_ty, confidence) -} - -pub fn from_bn_type_internal( - view: &BinaryView, - visited_refs: &mut HashSet<TypeRefID>, - raw_ty: &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() - .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 - let base_to_member_iter = raw_struct.base_structures().into_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), - MAX_CONFIDENCE, - ); - 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.ty.contents, - raw_member.ty.confidence, - ), - // TODO: Just omit location for now? - // TODO: Location should be optional... - locations: vec![], - } - }) - .collect(); - - let mut out_members = Vec::new(); - if let Some(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)); - - 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 = TypeRefID::from(raw_ntr.as_ref()); - let mut ref_class = ReferrerClass::new(None, Some(raw_ntr.name().to_string())); - if visited_refs.insert(ref_id) { - // This ntr is NOT self-referential, meaning we can deduce a type GUID. - if let Some(computed_ty) = cached_type_reference(view, visited_refs, &raw_ntr) { - // NOTE: The GUID here must always equal the same for any given type for this to work effectively. - ref_class.guid = Some(computed_ty.guid); - } - visited_refs.remove(&ref_id); - } - TypeClass::Referrer(ref_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()); - - 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 bn_comment_to_comment(func: &BNFunction, bn_comment: BNComment) -> FunctionComment { + let offset = (bn_comment.addr as i64) - (func.start() as i64); + FunctionComment { + offset, + text: bn_comment.comment, } } -pub fn from_bn_calling_convention(raw_cc: BNRef<BNCallingConvention>) -> 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> { - for cc in &arch.calling_conventions() { - if cc.name().as_str() == calling_convention.name { - return cc.clone(); - } +pub fn comment_to_bn_comment(func: &BNFunction, comment: FunctionComment) -> BNComment { + BNComment { + addr: comment + .offset + .checked_add_unsigned(func.start()) + .unwrap_or_default() as u64, + comment: comment.text, } - 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) +pub fn platform_to_target(platform: &Platform) -> Target { + let arch_name = platform.arch().name(); + let platform_name = platform.name(); + // We do not want to populate the platform if we are actually only the architecture. + if arch_name == platform_name { + Target { + architecture: Some(arch_name), + platform: None, } - 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 mut 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, - ), - None => BNNamedTypeReference::new( - NamedTypeReferenceClass::UnknownNamedTypeClass, - base_struct_ntr_name, - ), - }; - 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.base_structures(&base_structs); - BNType::structure(&builder.finalize()) - } - TypeClass::Enumeration(c) => { - let mut 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 usize; - let signed = matches!(*c.member_type.class, TypeClass::Integer(c) if c.signed); - // TODO: Passing width like this is weird. - BNType::enumeration(&builder.finalize(), width.try_into().unwrap(), signed) - } - TypeClass::Union(c) => { - let mut builder = BNStructureBuilder::new(); - builder.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_opts( - &return_type, - ¶ms, - variable_args, - BNConf::new(calling_convention, u8::MAX), - BNConf::new(0, 0), - ) - } - None => BNType::function(&return_type, params, 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, - ) - } - None => match c.name.as_ref() { - Some(ntr_name) => NamedTypeReference::new( - NamedTypeReferenceClass::UnknownNamedTypeClass, - ntr_name, - ), - None => { - log::error!("Referrer with no reference! {:?}", c); - NamedTypeReference::new( - NamedTypeReferenceClass::UnknownNamedTypeClass, - "AHHHHHH", - ) - } - }, - }; - BNType::named_type(&ntr) + } else { + Target { + architecture: Some(arch_name), + platform: Some(platform_name), } } } -#[cfg(test)] -mod tests { - use super::*; - use binaryninja::binary_view::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().expect("Failed to initialize session")) - } - - #[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(|qualified_name_and_type| { - let ty = from_bn_type(&bv, &qualified_name_and_type.ty, u8::MAX); - (TypeGUID::from(&ty), ty) - }) - .collect(); - assert_eq!(types_len, converted_types.len()); - } - } - } - } - - #[ignore] - #[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.ty, 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.ty, 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 - ); - } - } - } - } - } - } +pub fn target_to_platform(target: Target) -> Option<Ref<Platform>> { + // First try using the platform, then try using the arch. + match Platform::by_name(&target.platform.unwrap()) { + None => Platform::by_name(&target.architecture.unwrap()).map(|platform| platform), + Some(platform) => Some(platform), } } diff --git a/plugins/warp/src/convert/symbol.rs b/plugins/warp/src/convert/symbol.rs new file mode 100644 index 00000000..0107cff5 --- /dev/null +++ b/plugins/warp/src/convert/symbol.rs @@ -0,0 +1,91 @@ +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::rc::Ref as BNRef; +use binaryninja::symbol::Symbol as BNSymbol; +use binaryninja::symbol::SymbolType as BNSymbolType; +use warp::symbol::{Symbol, SymbolClass, SymbolModifiers}; + +pub fn from_bn_symbol(raw_symbol: &BNSymbol) -> Symbol { + // TODO: Use this? + let _is_export = raw_symbol.external(); + let raw_symbol_name = raw_symbol.raw_name(); + let symbol_name = raw_symbol_name.to_string_lossy(); + match raw_symbol.sym_type() { + BNSymbolType::ImportAddress => { + Symbol::new( + symbol_name, + SymbolClass::Function, + // TODO: External = symbolic i guess + SymbolModifiers::External, + ) + } + BNSymbolType::Data => { + Symbol::new( + symbol_name, + // TODO: Data? + SymbolClass::Data, + SymbolModifiers::default(), + ) + } + BNSymbolType::Symbolic => { + Symbol::new( + symbol_name, + SymbolClass::Function, + // TODO: External = symbolic i guess + SymbolModifiers::External, + ) + } + BNSymbolType::LocalLabel => { + // TODO: This is a placeholder for another symbol. + Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External) + } + BNSymbolType::External => Symbol::new( + symbol_name, + // TODO: External data? + SymbolClass::Function, + SymbolModifiers::External, + ), + BNSymbolType::ImportedData => { + Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External) + } + BNSymbolType::LibraryFunction | BNSymbolType::Function => Symbol::new( + symbol_name, + SymbolClass::Function, + SymbolModifiers::default(), + ), + BNSymbolType::ImportedFunction => Symbol::new( + symbol_name, + SymbolClass::Function, + // TODO: Exported? + 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, + _ => 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 Some((full_name, _)) = + binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false) + { + symbol_builder = symbol_builder.full_name(full_name); + } + if let Some((short_name, _)) = + binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false) + { + symbol_builder = symbol_builder.short_name(short_name); + } + } + symbol_builder.create() +} diff --git a/plugins/warp/src/convert/types.rs b/plugins/warp/src/convert/types.rs new file mode 100644 index 00000000..1dfe5fa9 --- /dev/null +++ b/plugins/warp/src/convert/types.rs @@ -0,0 +1,516 @@ +use crate::cache::{cached_type_reference, TypeRefID}; +use binaryninja::architecture::Architecture as BNArchitecture; +use binaryninja::architecture::ArchitectureExt; +use binaryninja::binary_view::BinaryView; +use binaryninja::calling_convention::CoreCallingConvention as BNCallingConvention; +use binaryninja::confidence::Conf as BNConf; +use binaryninja::confidence::MAX_CONFIDENCE; +use binaryninja::rc::Ref as BNRef; +use binaryninja::types::BaseStructure as BNBaseStructure; +use binaryninja::types::EnumerationBuilder as BNEnumerationBuilder; +use binaryninja::types::FunctionParameter as BNFunctionParameter; +use binaryninja::types::MemberAccess as BNMemberAccess; +use binaryninja::types::MemberScope as BNMemberScope; +use binaryninja::types::NamedTypeReference as BNNamedTypeReference; +use binaryninja::types::StructureBuilder as BNStructureBuilder; +use binaryninja::types::StructureMember as BNStructureMember; +use binaryninja::types::StructureType as BNStructureType; +use binaryninja::types::Type as BNType; +use binaryninja::types::TypeClass as BNTypeClass; +use binaryninja::types::{NamedTypeReference, NamedTypeReferenceClass}; +use std::collections::HashSet; +use warp::r#type::class::array::ArrayModifiers; +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::{Type, TypeModifiers}; + +pub fn from_bn_type(view: &BinaryView, raw_ty: &BNType, confidence: u8) -> Type { + from_bn_type_internal(view, &mut HashSet::new(), raw_ty, confidence) +} + +pub fn from_bn_type_internal( + view: &BinaryView, + visited_refs: &mut HashSet<TypeRefID>, + raw_ty: &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() + .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, BNMemberAccess::PublicAccess), + ); + StructureMember { + name: Some(raw_member.name), + offset: bit_offset, + ty: Box::new(from_bn_type_internal( + view, + visited_refs, + &raw_member.ty.contents, + raw_member.ty.confidence, + )), + modifiers, + } + }) + .collect::<Vec<_>>(); + + // Add base structures as flattened members + let base_to_member_iter = raw_struct.base_structures().into_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), + MAX_CONFIDENCE, + ); + StructureMember { + name: base_struct_ty.name.to_owned(), + offset: bit_offset, + ty: Box::new(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: Box::new(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: Box::new(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: Box::new(from_bn_type_internal( + view, + visited_refs, + &raw_member.ty.contents, + raw_member.ty.confidence, + )), + // TODO: Just omit location for now? + // TODO: Location should be optional... + location: None, + } + }) + .collect(); + + let mut out_members = Vec::new(); + if let Some(return_ty) = raw_ty.return_value() { + out_members.push(FunctionMember { + name: None, + ty: Box::new(from_bn_type_internal( + view, + visited_refs, + &return_ty.contents, + return_ty.confidence, + )), + location: None, + }); + } + + let calling_convention = raw_ty + .calling_convention() + .map(|bn_cc| from_bn_calling_convention(bn_cc.contents)); + + 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 = TypeRefID::from(raw_ntr.as_ref()); + let mut ref_class = ReferrerClass::new(None, Some(raw_ntr.name().to_string())); + if visited_refs.insert(ref_id) { + // This ntr is NOT self-referential, meaning we can deduce a type GUID. + if let Some(computed_ty) = cached_type_reference(view, visited_refs, &raw_ntr) { + // NOTE: The GUID here must always equal the same for any given type for this to work effectively. + ref_class.guid = Some(computed_ty.guid); + } + visited_refs.remove(&ref_id); + } + TypeClass::Referrer(ref_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()); + + Type { + name, + class: type_class, + confidence, + // TODO: Fill these out... + modifiers: TypeModifiers::empty(), + metadata: 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(raw_cc: BNRef<BNCallingConvention>) -> 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> { + 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 { + 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 mut 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 { + 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, + ), + None => BNNamedTypeReference::new( + NamedTypeReferenceClass::UnknownNamedTypeClass, + base_struct_ntr_name, + ), + }; + 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.base_structures(&base_structs); + BNType::structure(&builder.finalize()) + } + TypeClass::Enumeration(c) => { + let mut 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 usize; + let signed = matches!(c.member_type.class, TypeClass::Integer(c) if c.signed); + // TODO: Passing width like this is weird. + BNType::enumeration(&builder.finalize(), width.try_into().unwrap(), signed) + } + TypeClass::Union(c) => { + let mut builder = BNStructureBuilder::new(); + builder.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_opts( + &return_type, + ¶ms, + variable_args, + BNConf::new(calling_convention, u8::MAX), + BNConf::new(0, 0), + ) + } + None => BNType::function(&return_type, params, 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::TypedefNamedTypeClass, + &guid_str, + ntr_name, + ) + } + None => match c.name.as_ref() { + Some(ntr_name) => NamedTypeReference::new( + NamedTypeReferenceClass::UnknownNamedTypeClass, + ntr_name, + ), + None => { + log::error!("Referrer with no reference! {:?}", c); + NamedTypeReference::new( + NamedTypeReferenceClass::UnknownNamedTypeClass, + "AHHHHHH", + ) + } + }, + }; + BNType::named_type(&ntr) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use binaryninja::binary_view::BinaryViewExt; + use binaryninja::headless::Session; + use std::path::PathBuf; + use warp::r#type::guid::TypeGUID; + + #[test] + fn type_conversion() { + let session = Session::new().expect("Failed to initialize 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) { + let types_len = bv.types().len(); + let converted_types: Vec<_> = bv + .types() + .iter() + .map(|qualified_name_and_type| { + let ty = from_bn_type(&bv, &qualified_name_and_type.ty, u8::MAX); + (TypeGUID::from(&ty), ty) + }) + .collect(); + assert_eq!(types_len, converted_types.len()); + } + } + } + } +} diff --git a/plugins/warp/src/lib.rs b/plugins/warp/src/lib.rs index 6d147bdc..aab26932 100644 --- a/plugins/warp/src/lib.rs +++ b/plugins/warp/src/lib.rs @@ -1,7 +1,5 @@ -use crate::cache::{ - cached_adjacency_constraints, cached_call_site_constraints, cached_function_guid, -}; -use crate::convert::{from_bn_symbol, from_bn_type}; +use crate::cache::{cached_constraints, cached_function_guid}; +use crate::convert::{bn_comment_to_comment, from_bn_symbol, from_bn_type}; use binaryninja::architecture::{ Architecture, ImplicitRegisterExtend, Register as BNRegister, RegisterInfo, }; @@ -9,25 +7,55 @@ use binaryninja::basic_block::BasicBlock as BNBasicBlock; use binaryninja::binary_view::{BinaryView, BinaryViewExt}; use binaryninja::confidence::MAX_CONFIDENCE; use binaryninja::function::{Function as BNFunction, NativeBlock}; -use binaryninja::low_level_il::expression::{ExpressionHandler, LowLevelILExpressionKind}; +use binaryninja::low_level_il::expression::{ + ExpressionHandler, LowLevelILExpression, LowLevelILExpressionKind, ValueExpr, +}; use binaryninja::low_level_il::function::{FunctionMutability, LowLevelILFunction, NonSSA}; use binaryninja::low_level_il::instruction::{ InstructionHandler, LowLevelILInstruction, LowLevelILInstructionKind, }; use binaryninja::low_level_il::{LowLevelILRegisterKind, VisitorAction}; -use binaryninja::rc::Ref as BNRef; +use binaryninja::rc::{Ref as BNRef, Ref}; use std::ops::Range; use std::path::PathBuf; use warp::signature::basic_block::BasicBlockGUID; -use warp::signature::function::constraints::FunctionConstraints; use warp::signature::function::{Function, FunctionGUID}; +use binaryninja::tags::TagType; +use binaryninja::variable::RegisterValueType; +/// Re-export the warp crate that is used, this is useful for consumers of this crate. +pub use warp; + pub mod cache; +pub mod container; pub mod convert; -mod matcher; +pub mod matcher; +pub mod processor; +pub mod report; + /// Only used when compiled for cdylib target. mod plugin; +// TODO: Make this 4kb +/// If the address is within this range before or after a relocatable region, we will assume the address to be relocatable. +const ADDRESS_RELOCATION_THRESHOLD: u64 = 0x10000; + +const TAG_ICON: &str = "🌐"; +const TAG_NAME: &str = "WARP"; + +fn get_warp_tag_type(view: &BinaryView) -> Ref<TagType> { + view.tag_type_by_name(TAG_NAME) + .unwrap_or_else(|| view.create_tag_type(TAG_NAME, TAG_ICON)) +} + +const INCLUDE_TAG_ICON: &str = "🚀"; +const INCLUDE_TAG_NAME: &str = "WARP: Selected Function"; + +fn get_warp_include_tag_type(view: &BinaryView) -> Ref<TagType> { + view.tag_type_by_name(INCLUDE_TAG_NAME) + .unwrap_or_else(|| view.create_tag_type(INCLUDE_TAG_NAME, INCLUDE_TAG_ICON)) +} + pub fn core_signature_dir() -> PathBuf { // Get core signatures for the given platform let install_dir = binaryninja::install_directory(); @@ -45,22 +73,34 @@ pub fn user_signature_dir() -> PathBuf { pub fn build_function<M: FunctionMutability>( func: &BNFunction, - llil: &LowLevelILFunction<M, NonSSA>, + lifted_il: &LowLevelILFunction<M, NonSSA>, ) -> Function { - let bn_fn_ty = func.function_type(); + let comments = func + .comments() + .iter() + .map(|c| bn_comment_to_comment(func, c)) + .collect(); Function { - guid: cached_function_guid(func, llil), + guid: cached_function_guid(func, lifted_il), symbol: from_bn_symbol(&func.symbol()), - ty: from_bn_type(&func.view(), &bn_fn_ty, MAX_CONFIDENCE), - constraints: FunctionConstraints { - // NOTE: Adding adjacent only works if analysis is complete. - // NOTE: We do not filter out adjacent functions here. - adjacent: cached_adjacency_constraints(func, |_| true), - 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(), + // Currently we only store the type if its a user type. + // TODO: In the future we might want to make this configurable. + ty: match func.has_user_type() || func.has_explicitly_defined_type() { + true => Some(from_bn_type( + &func.view(), + &func.function_type(), + MAX_CONFIDENCE, + )), + false => None, }, + // NOTE: Adding adjacent only works if analysis is complete. + // NOTE: We do not filter out adjacent functions here. + constraints: cached_constraints(func, |_| true), + comments, + // TODO: Gather relevant variables (only user?). + // TODO: Will need MLIL SSA for this to locate def sites. + // TODO: Add this info in a second pass? + variables: vec![], } } @@ -71,20 +111,21 @@ pub fn sorted_basic_blocks(func: &BNFunction) -> Vec<BNRef<BNBasicBlock<NativeBl .iter() .map(|bb| bb.clone()) .collect::<Vec<_>>(); + // NOTE: start_index is actually the address with [`NativeBlock`]. basic_blocks.sort_by_key(|f| f.start_index()); basic_blocks } pub fn function_guid<M: FunctionMutability>( func: &BNFunction, - llil: &LowLevelILFunction<M, NonSSA>, + lifted_il: &LowLevelILFunction<M, NonSSA>, ) -> FunctionGUID { // TODO: We might want to make this configurable, or otherwise _not_ retrieve from the view here. let relocatable_regions = relocatable_regions(&func.view()); let basic_blocks = sorted_basic_blocks(func); let basic_block_guids = basic_blocks .iter() - .map(|bb| basic_block_guid(&relocatable_regions, bb, llil)) + .map(|bb| basic_block_guid(&relocatable_regions, bb, lifted_il)) .collect::<Vec<_>>(); FunctionGUID::from_basic_blocks(&basic_block_guids) } @@ -92,30 +133,60 @@ pub fn function_guid<M: FunctionMutability>( pub fn basic_block_guid<M: FunctionMutability>( relocatable_regions: &[Range<u64>], basic_block: &BNBasicBlock<NativeBlock>, - llil: &LowLevelILFunction<M, NonSSA>, + lifted_il: &LowLevelILFunction<M, NonSSA>, ) -> BasicBlockGUID { let func = basic_block.function(); + // TODO: We really should never consult another IL, no guarantee that it exists. + let low_level_il = func.low_level_il(); let view = func.view(); let arch = func.arch(); let max_instr_len = arch.max_instr_len(); + // NOTE: Whenever you make a change here, prefer being "additive", that is, make a smaller change that + // only increases the masked contents, instead of making a larger change that could *remove* masked + // contents. The reason is that we assume any change that is purely additive to increase the ability + // to match previously "unmatchable" functions, whereas the latter would take away. This is not always + // the case, but it is generally a good rule to follow. let basic_block_range = basic_block.start_index()..basic_block.end_index(); 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) { instr_bytes.truncate(instr_info.length); - if let Some(instr_llil) = llil.instruction_at(instr_addr) { - // If instruction is blacklisted don't include the bytes. - if !is_blacklisted_instruction(&instr_llil) { - if is_variant_instruction(relocatable_regions, &instr_llil) { - // Found a variant instruction, mask off entire instruction. + + // Find variant and blacklisted instructions using lifted il. + if let Some(lifted_il_instr) = lifted_il.instruction_at(instr_addr) { + // If instruction is blacklisted, don't include the bytes. + if is_blacklisted_instruction(&lifted_il_instr) { + continue; + } + + if is_variant_instruction(relocatable_regions, &lifted_il_instr) { + // Found a variant instruction, mask off the entire instruction. + instr_bytes.fill(0); + } + } + + // TODO: We cannot access the values of expression in lifted IL, we have to go and consult low level IL. + // TODO: But because of some extremely annoying simplifications that are happening at LLIL, namely + // TODO: Folding of expressions into other instructions, we cannot use only LLIL. Therefor + // TODO: We only put the checks that require the expr value here. + // TODO: This still has the issue of, some (if (rax + 44) => 28) expression being masked, + // TODO: But the only way to remove that is to not consult LLIL at all and have the values + // TODO: Available at lifted IL, I have not found a good way to do this without making + // TODO: A "mapped llil" or having some simple data flow, the simple data flow is the most attractive + // TODO: "solution", but it would require + if let Ok(llil) = &low_level_il { + if let Some(low_level_instr) = llil.instruction_at(instr_addr) { + if is_computed_variant_instruction(relocatable_regions, &low_level_instr) { + // Found a computed variant instruction, mask off the entire instruction. instr_bytes.fill(0); } - // Add the instructions bytes to the basic blocks bytes - basic_block_bytes.extend(instr_bytes); } } + + // Add the instruction bytes to the basic blocks bytes + basic_block_bytes.extend(instr_bytes); } } @@ -127,8 +198,8 @@ pub fn basic_block_guid<M: FunctionMutability>( /// Blacklisted instructions will make an otherwise identical function GUID fail to match. /// /// Example: NOPs and useless moves are blacklisted to allow for hot-patchable functions. -pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>( - instr: &LowLevelILInstruction<A, M, NonSSA<RegularNonSSA>>, +pub fn is_blacklisted_instruction<M: FunctionMutability>( + instr: &LowLevelILInstruction<M, NonSSA>, ) -> bool { match instr.kind() { LowLevelILInstructionKind::Nop(_) => true, @@ -138,13 +209,13 @@ pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>( if op.dest_reg() == source_op.source_reg() => { match op.dest_reg() { - LowLevelILRegister::ArchReg(r) => { - // If this register has no implicit extend then we can safely assume it's a NOP. + LowLevelILRegisterKind::Arch(r) => { + // If this register has no implicit extend, we can safely assume it's a NOP. // Ex. on x86_64 we don't want to remove `mov edi, edi` as it will zero the upper 32 bits. // Ex. on x86 we do want to remove `mov edi, edi` as it will not have a side effect like above. matches!(r.info().implicit_extend(), ImplicitRegisterExtend::NoExtend) } - LowLevelILRegister::Temp(_) => false, + LowLevelILRegisterKind::Temp(_) => false, } } _ => false, @@ -154,24 +225,22 @@ pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>( } } -pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>( +pub fn is_variant_instruction<M: FunctionMutability>( relocatable_regions: &[Range<u64>], - instr: &LowLevelILInstruction<A, M, NonSSA<RegularNonSSA>>, + instr: &LowLevelILInstruction<M, NonSSA>, ) -> bool { - let is_variant_expr = |expr: &LowLevelILExpressionKind<A, M, NonSSA<RegularNonSSA>>| { - match expr { + let is_variant_expr = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| { + match expr.kind() { LowLevelILExpressionKind::ConstPtr(op) if is_address_relocatable(relocatable_regions, op.value()) => { // Constant Pointer must be in a section for it to be relocatable. - // NOTE: We cannot utilize segments here as there will be a zero based segment. true } LowLevelILExpressionKind::Const(op) if is_address_relocatable(relocatable_regions, op.value()) => { // Constant value must be in a section for it to be relocatable. - // NOTE: We cannot utilize segments here as there will be a zero based segment. true } LowLevelILExpressionKind::ExternPtr(_) => true, @@ -181,7 +250,7 @@ pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>( // Visit instruction expressions looking for variant expression, [VisitorAction::Halt] means variant. instr.visit_tree(&mut |expr| { - if is_variant_expr(&expr.kind()) { + if is_variant_expr(expr) { // Found a variant expression. VisitorAction::Halt } else { @@ -191,18 +260,87 @@ pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>( }) == VisitorAction::Halt } -/// If the address is inside any of the given ranges we will assume the address to be relocatable. +/// NOTE: This will only work at LLIL, **NOT** lifted IL. You must do this in a second pass. +/// +/// This was previously done inside `is_variant_instruction` but had to be moved to access expr value. +pub fn is_computed_variant_instruction<M: FunctionMutability>( + relocatable_regions: &[Range<u64>], + instr: &LowLevelILInstruction<M, NonSSA>, +) -> bool { + let is_expr_constant = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| match expr.kind() { + LowLevelILExpressionKind::Const(_) => true, + _ => false, + }; + + let is_variant_observed_expr = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| { + match expr.kind() { + // TODO: Skip problematic expressions like IF? + LowLevelILExpressionKind::Add(op) | LowLevelILExpressionKind::Sub(op) => { + // For now, we limit to only expressions that contain some constant; this keeps add expressions + // with two registers with known values from being marked variant. + let constant_expressed = + is_expr_constant(&op.left()) || is_expr_constant(&op.right()); + // NOTE: Lifted IL does not have the value ever, we must consult Low Level IL. + // If the expression value is known, we check to see if it's a relocatable address. + let expr_value = expr.value(); + match expr_value.state { + RegisterValueType::EntryValue + | RegisterValueType::ConstantValue + | RegisterValueType::ConstantPointerValue + | RegisterValueType::ExternalPointerValue + | RegisterValueType::StackFrameOffset + | RegisterValueType::ReturnAddressValue + | RegisterValueType::ImportedAddressValue + if constant_expressed + && is_address_relocatable( + relocatable_regions, + expr_value.value as u64, + ) => + { + // Concrete arithmetic operation with a relocatable result. + true + } + _ => false, + } + } + _ => false, + } + }; + + // Visit instruction expressions looking for an observed variant expression, [VisitorAction::Halt] means variant. + instr.visit_tree(&mut |expr| { + if is_variant_observed_expr(expr) { + // Found a variant expression. + VisitorAction::Halt + } else { + // Keep looking for an observed variant expression. + VisitorAction::Descend + } + }) == VisitorAction::Halt +} + +/// If the address is inside any of the given ranges, we will assume the address to be relocatable. pub fn is_address_relocatable(relocatable_regions: &[Range<u64>], address: u64) -> bool { relocatable_regions .iter() - .any(|range| range.contains(&address)) + .any(|range| { + // Check if the address is within the range itself + (range.contains(&address)) + // Check if the address is within the threshold **AFTER** the range + // NOTE: The address must at least be larger than the threshold itself, for lower image-based binaries. + || (address > range.end && address > ADDRESS_RELOCATION_THRESHOLD && address <= range.end + ADDRESS_RELOCATION_THRESHOLD) + // Check if the address is within the threshold **BEFORE** the range + // NOTE: The address must at least be larger than the threshold itself, for lower image-based binaries. + || (address < range.start && address > ADDRESS_RELOCATION_THRESHOLD && address >= range.start.saturating_sub(ADDRESS_RELOCATION_THRESHOLD)) + }) } // TODO: This might need to be configurable, in that case we better remove this function. /// Get the relocatable regions of the view. /// -/// Currently, this is all the sections, however this might be refined later. +/// Currently, this is all the sections, however, this might be refined later. pub fn relocatable_regions(view: &BinaryView) -> Vec<Range<u64>> { + // NOTE: We cannot use segments here as there will be a zero-based segment. view.sections() .iter() .map(|s| Range { @@ -211,41 +349,3 @@ pub fn relocatable_regions(view: &BinaryView) -> Vec<Range<u64>> { }) .collect() } - -#[cfg(test)] -mod tests { - use crate::cache::cached_function_guid; - use binaryninja::binary_view::BinaryViewExt; - use binaryninja::headless::Session; - use std::path::PathBuf; - use std::sync::OnceLock; - - 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().expect("Failed to initialize session")) - } - - #[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() { - let view = session.load(&path).expect("Failed to load view"); - let mut functions = view - .functions() - .iter() - .map(|f| cached_function_guid(&f, &f.low_level_il().unwrap())) - .collect::<Vec<_>>(); - functions.sort_by_key(|guid| guid.guid); - let snapshot_name = - format!("snapshot_{}", path.file_stem().unwrap().to_string_lossy()); - insta::assert_debug_snapshot!(snapshot_name, functions); - } - } - } -} diff --git a/plugins/warp/src/matcher.rs b/plugins/warp/src/matcher.rs index 0ce258ad..c6ab7f9f 100644 --- a/plugins/warp/src/matcher.rs +++ b/plugins/warp/src/matcher.rs @@ -1,363 +1,282 @@ +use crate::cache::cached_constraints; +use crate::container::{Container, SourceId}; +use crate::convert::to_bn_type; use binaryninja::architecture::Architecture as BNArchitecture; use binaryninja::binary_view::{BinaryView, BinaryViewExt}; use binaryninja::function::Function as BNFunction; -use binaryninja::platform::Platform; -use binaryninja::rc::Guard; -use binaryninja::rc::Ref as BNRef; -use dashmap::DashMap; +use binaryninja::settings::{QueryOptions, Settings as BNSettings}; use serde_json::json; use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; -use std::hash::{DefaultHasher, Hash, Hasher}; -use std::path::PathBuf; -use std::sync::OnceLock; -use walkdir::{DirEntry, WalkDir}; +use std::collections::HashSet; +use std::hash::Hash; use warp::r#type::class::TypeClass; -use warp::r#type::guid::TypeGUID; use warp::r#type::Type; -use warp::signature::function::{Function, FunctionGUID}; -use warp::signature::Data; - -use crate::cache::{ - cached_adjacency_constraints, cached_call_site_constraints, cached_function_match, - try_cached_function_guid, -}; -use crate::convert::to_bn_type; -use crate::plugin::on_matched_function; -use crate::{core_signature_dir, user_signature_dir}; - -pub static PLAT_MATCHER_CACHE: OnceLock<DashMap<PlatformID, Matcher>> = OnceLock::new(); - -pub fn cached_function_matcher(function: &BNFunction) { - 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), - None => { - let matcher = Matcher::from_platform(platform); - matcher.match_function(function); - matcher_cache.insert(platform_id, matcher); - } - } -} - -// TODO: Maybe just clear individual platforms? This works well enough either way. -pub fn invalidate_function_matcher_cache() { - let matcher_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default); - matcher_cache.clear(); -} +use warp::signature::function::Function; -#[derive(Debug, Default, Clone)] +/// A matcher represents a specific configuration for identify functions using WARP. A matcher +/// does not store/own any WARP information directly, instead the matcher is given a [`Container`] +/// that holds all of that information. +/// +/// The separation of the WARP information from the [`Matcher`] allows a greater degree of control and +/// provides a clean interface for further logic to be built on top of. A matcher instance, unlike +/// a typical [`Container`] implementation, is cheap to create. +#[derive(Debug, Clone, Copy)] pub struct Matcher { - // TODO: Storing the settings here means that they are effectively global. - // TODO: If we want scoped or view settings they must be moved out. pub settings: MatcherSettings, - pub functions: DashMap<FunctionGUID, Vec<Function>>, - pub types: DashMap<TypeGUID, Type>, - pub named_types: DashMap<String, Type>, } 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(); + pub fn new(settings: MatcherSettings) -> Self { + Matcher { settings } + } - // Get core and user signatures. - // TODO: Separate each file into own bucket for filtering? - let plat_core_sig_dir = core_signature_dir().join(&platform_name); - let mut data = get_data_from_dir(&plat_core_sig_dir); - let plat_user_sig_dir = user_signature_dir().join(&platform_name); - let user_data = get_data_from_dir(&plat_user_sig_dir); + pub fn match_function_from_constraints<'a>( + &self, + function: &BNFunction, + matched_functions: &'a [Function], + ) -> Option<&'a Function> { + let function_len = function.highest_address() - function.lowest_address(); + let is_function_trivial = { function_len < self.settings.trivial_function_len }; + let is_function_allowed = { + function_len >= self.settings.minimum_function_len + && function_len < self.settings.maximum_function_len.unwrap_or(u64::MAX) + }; - data.extend(user_data); - let merged_data = Data::merge(data.values().cloned().collect::<Vec<_>>()); - log::debug!("Loaded signatures: {:?}", data.keys()); - Matcher::from_data(merged_data) - } + // Function isn't allowed, or no matches so stop early. + if !is_function_allowed || matched_functions.is_empty() { + return None; + } - pub fn from_data(data: Data) -> Self { - let functions = data.functions.into_iter().fold( - DashMap::new(), - |map: DashMap<FunctionGUID, Vec<_>>, func| { - map.entry(func.guid).or_default().push(func); - map - }, - ); - let types = data - .types - .iter() - .map(|ty| (ty.guid, ty.ty.clone())) - .collect(); - let named_types = data - .types - .into_iter() - .filter_map(|ty| ty.ty.name.to_owned().map(|name| (name, ty.ty))) - .collect(); + // If we have a single possible match than that must be our function. + // We must also not be a trivial function, as those will likely be artifacts of an incomplete dataset + if matched_functions.len() == 1 && !is_function_trivial { + return matched_functions.first(); + } + // Filter out adjacent functions which are trivial, this helps avoid false positives. + // NOTE: If the user sets `trivial_function_adjacent_allowed` to true we will always match. + // TODO: Expand on this more later. We might want to match on adjacent functions smaller than this. + let adjacent_function_filter = |adj_func: &BNFunction| { + let adj_func_len = adj_func.highest_address() - adj_func.lowest_address(); + adj_func_len >= self.settings.trivial_function_len + || self.settings.trivial_function_adjacent_allowed + }; - Self { - // NOTE: Settings will be retrieved from global state every time this is called. - settings: MatcherSettings::global(), - functions, - types, - named_types, + // TODO: When the highest count has two matches we return None. Need to alert the user. + // "common" being the intersection between the observed and matched. + let constraints = cached_constraints(function, adjacent_function_filter); + let mut highest_count = 0; + let mut matched_func = None; + for matched in matched_functions { + let common_count = constraints.intersection(&matched.constraints).count(); + match common_count.cmp(&highest_count) { + Ordering::Equal => matched_func = None, + Ordering::Greater => { + highest_count = common_count; + matched_func = Some(matched); + } + Ordering::Less => {} + } } - } - pub fn extend_with_matcher(&mut self, matcher: Matcher) { - self.functions.extend(matcher.functions); - self.types.extend(matcher.types); - self.named_types.extend(matcher.named_types); + // If we have a match below the minimum threshold, ignore. + match highest_count.cmp(&self.settings.minimum_matched_constraints) { + Ordering::Equal => matched_func, + Ordering::Greater => matched_func, + Ordering::Less => None, + } } - pub fn add_type_to_view<A: BNArchitecture>(&self, view: &BinaryView, arch: &A, ty: &Type) { + // TODO: I would really like for WARP types to be added in a seperate type container, so that we don't + // TODO: just add them as system or user types. + pub fn add_type_to_view<A: BNArchitecture>( + &self, + container: &dyn Container, + source: &SourceId, + view: &BinaryView, + arch: &A, + ty: &Type, + ) where + Self: Sized, + { fn inner_add_type_to_view<A: BNArchitecture>( - matcher: &Matcher, + container: &dyn Container, + source: &SourceId, view: &BinaryView, arch: &A, visited_refs: &mut HashSet<String>, ty: &Type, ) { - let ty_id_str = TypeGUID::from(ty).to_string(); - if view.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) - } + match &ty.class { + TypeClass::Pointer(c) => inner_add_type_to_view( + container, + source, + view, + arch, + visited_refs, + &c.child_type, + ), + TypeClass::Array(c) => inner_add_type_to_view( + container, + source, + 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) + inner_add_type_to_view( + container, + source, + view, + arch, + visited_refs, + &member.ty, + ) } } - TypeClass::Enumeration(c) => { - inner_add_type_to_view(matcher, view, arch, visited_refs, &c.member_type) - } + TypeClass::Enumeration(c) => inner_add_type_to_view( + container, + source, + 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) + inner_add_type_to_view( + container, + source, + 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) + inner_add_type_to_view( + container, + source, + 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) + inner_add_type_to_view( + container, + source, + 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; + let mut resolved_ty = None; 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 by themselves. if view.type_by_id(&ref_guid.to_string()).is_none() { // Add the referrer 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 Ok(Some(ref_ty)) = container.type_with_guid(source, &ref_guid) { + inner_add_type_to_view( + container, + source, + view, + arch, + visited_refs, + &ref_ty, + ); + resolved_ty = Some(ref_ty); } } } if let Some(ref_name) = &c.name { // Only try and resolve by name if not already visiting. - if !resolved + if resolved_ty.is_none() && visited_refs.insert(ref_name.to_string()) && view.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); + let type_guids = container + .type_guids_with_name(source, ref_name) + .unwrap_or_default(); + // TODO: What happens if we have more than one? + if type_guids.len() == 1 { + // TODO: What happens if we cant get the guid? + if let Ok(Some(ref_ty)) = + container.type_with_guid(source, &type_guids[0]) + { + inner_add_type_to_view( + container, + source, + view, + arch, + visited_refs, + &ref_ty, + ); + resolved_ty = Some(ref_ty); + } } // No longer visiting type. visited_refs.remove(ref_name); } } - // All nested types _should_ be added now, we can add this type. - // TODO: Do we want to make unnamed types visible? I think we should, but some people might be opposed. - 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(&self, function: &BNFunction) { - // Call this the first time you matched on the function. - let resolve_new_types = |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 referrers - 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); - } - } - }; - - if let Some(matched_function) = cached_function_match(function, || { - // We have yet to match on this function. - let function_len = function.highest_address() - function.lowest_address(); - let is_function_trivial = { function_len < self.settings.trivial_function_len }; - let is_function_allowed = { - function_len > self.settings.minimum_function_len - && function_len < self.settings.maximum_function_len.unwrap_or(u64::MAX) - }; - let warp_func_guid = try_cached_function_guid(function)?; - match self.functions.get(&warp_func_guid) { - _ if !is_function_allowed => None, - Some(matched) if matched.len() == 1 && !is_function_trivial => { - resolve_new_types(&matched[0]); - Some(matched[0].to_owned()) - } - Some(matched) => { - let matched_on = self.match_function_from_constraints(function, &matched)?; - resolve_new_types(matched_on); - Some(matched_on.to_owned()) - } - None => None, - } - }) { - on_matched_function(function, &matched_function); - } - } - - pub fn match_function_from_constraints<'a>( - &self, - function: &BNFunction, - matched_functions: &'a [Function], - ) -> Option<&'a Function> { - // Filter out adjacent functions which are trivial, this helps avoid false positives. - // NOTE: If the user sets `trivial_function_adjacent_allowed` to true we will always match. - // TODO: Expand on this more later. We might want to match on adjacent functions smaller than this. - let adjacent_function_filter = |adj_func: &BNFunction| { - let adj_func_len = adj_func.highest_address() - adj_func.lowest_address(); - adj_func_len > self.settings.trivial_function_len - || self.settings.trivial_function_adjacent_allowed - }; - - let call_sites = cached_call_site_constraints(function); - let adjacent = cached_adjacency_constraints(function, adjacent_function_filter); - - // "common" being the intersection between the observed and matched. - fn find_highest_common_count<'a, F, T>( - observed_items: &HashSet<T>, - matched_functions: &'a [Function], - extract_items: F, - ) -> (usize, Option<&'a Function>) - where - F: Fn(&Function) -> HashSet<T>, - T: Hash + Eq, - { - let mut highest_count = 0; - let mut matched_func = None; - for matched in matched_functions { - let matched_items = extract_items(matched); - let common_count = observed_items.intersection(&matched_items).count(); - match common_count.cmp(&highest_count) { - Ordering::Equal => matched_func = None, - Ordering::Greater => { - highest_count = common_count; - matched_func = Some(matched); + // Adds the ref'd type to the view. + match (c.guid, &c.name, resolved_ty) { + (Some(guid), Some(name), Some(ref_ty)) => { + view.define_auto_type_with_id( + name, + &guid.to_string(), + &to_bn_type(arch, &ref_ty), + ); + } + (Some(_guid), Some(_name), None) => { + // TODO: Got name and guid but no type? Do we add a bare NTR? + } + (Some(_guid), None, _) => { + // TODO: How would we reference this type without a name??? + } + (None, Some(_name), _) => { + // TODO: Cyclic type reference if no guid, so... dont define? + } + (None, None, _) => { + // TODO: What?!?!? + } } - Ordering::Less => {} } + TypeClass::Void + | TypeClass::Boolean(_) + | TypeClass::Integer(_) + | TypeClass::Character(_) + | TypeClass::Float(_) => {} } - (highest_count, matched_func) - } - - let call_site_guids: HashSet<_> = call_sites.iter().filter_map(|c| c.guid).collect(); - let call_site_symbol_names: HashSet<_> = call_sites - .into_iter() - .filter_map(|c| c.symbol.map(|s| s.name)) - .collect(); - let adjacent_guids: HashSet<_> = adjacent.iter().filter_map(|c| c.guid).collect(); - let adjacent_symbol_names: HashSet<_> = adjacent - .into_iter() - .filter_map(|c| c.symbol.map(|s| s.name)) - .collect(); - - // Ordered from the lowest confidence to the highest confidence constraint. - let checked_constraints = [ - find_highest_common_count(&adjacent_symbol_names, matched_functions, |matched| { - matched - .constraints - .adjacent - .iter() - .filter_map(|c| c.symbol.to_owned().map(|s| s.name)) - .collect() - }), - find_highest_common_count(&adjacent_guids, matched_functions, |matched| { - matched - .constraints - .adjacent - .iter() - .filter_map(|c| c.guid) - .collect() - }), - find_highest_common_count(&call_site_symbol_names, matched_functions, |matched| { - matched - .constraints - .call_sites - .iter() - .filter_map(|c| c.symbol.to_owned().map(|s| s.name)) - .collect() - }), - find_highest_common_count(&call_site_guids, matched_functions, |matched| { - matched - .constraints - .call_sites - .iter() - .filter_map(|c| c.guid) - .collect() - }), - ]; - // If there is a tie, the last one wins, which should be call_site guid. - checked_constraints - .into_iter() - .max_by_key(|&(count, _)| count) - .filter(|&(count, _)| count >= self.settings.minimum_matched_constraints) - .and_then(|(_, func)| func) + // TODO: Some refs likely need to ommitted because they are just that, refs to another type. + // let guid = TypeGUID::from(ty); + // let name = ty.name.clone().unwrap_or(guid.to_string()); + // view.define_auto_type_with_id(name, &guid.to_string(), &to_bn_type(arch, ty)); + } + inner_add_type_to_view(container, source, view, arch, &mut HashSet::new(), ty) } } -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() -} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MatcherSettings { /// Any function under this length will be required to constrain. /// - /// This is set to [MatcherSettings::DEFAULT_TRIVIAL_FUNCTION_LEN] by default. + /// This is set to [MatcherSettings::TRIVIAL_FUNCTION_LEN_DEFAULT] by default. pub trivial_function_len: u64, /// Any function under this length will not match. /// @@ -367,13 +286,15 @@ pub struct MatcherSettings { /// /// This is set to [MatcherSettings::MAXIMUM_FUNCTION_LEN_DEFAULT] by default. pub maximum_function_len: Option<u64>, - /// For a successful constrained function match the number of matches must be above this. + /// For a successful constrained function match, the number of matches must be above this. /// - /// This is set to [MatcherSettings::DEFAULT_TRIVIAL_FUNCTION_LEN] by default. + /// This is set to [MatcherSettings::MINIMUM_MATCHED_CONSTRAINTS_DEFAULT] by default. pub minimum_matched_constraints: usize, - /// For a successful constrained function match the number of matches must be above this. + /// When function constraints are checked, if this is enabled, functions can match based off trivial adjacent functions. + /// + /// Any function under `trivial_function_len` will be considered trivial. /// - /// This is set to [MatcherSettings::DEFAULT_TRIVIAL_FUNCTION_LEN] by default. + /// This is set to [MatcherSettings::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_DEFAULT] by default. pub trivial_function_adjacent_allowed: bool, } @@ -395,17 +316,15 @@ impl MatcherSettings { /// /// Call this once when you initialize so that the settings exist. /// - /// NOTE: If you are using this as a library then just modify the MatcherSettings directly + /// NOTE: If you are using this as a library, then modify the [`MatcherSettings`] directly /// in the matcher instance, that way you don't need to round-trip through Binary Ninja. - pub fn register() { - let bn_settings = binaryninja::settings::Settings::new(); - + pub fn register(bn_settings: &mut BNSettings) { let trivial_function_len_props = json!({ "title" : "Trivial Function Length", "type" : "number", "default" : Self::TRIVIAL_FUNCTION_LEN_DEFAULT, "description" : "Functions below this length in bytes will be required to match on constraints.", - "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + "ignore" : [] }); bn_settings.register_setting_json( Self::TRIVIAL_FUNCTION_LEN_SETTING, @@ -417,7 +336,7 @@ impl MatcherSettings { "type" : "number", "default" : Self::MINIMUM_FUNCTION_LEN_DEFAULT, "description" : "Functions below this length will not be matched.", - "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + "ignore" : [] }); bn_settings.register_setting_json( Self::MINIMUM_FUNCTION_LEN_SETTING, @@ -429,7 +348,7 @@ impl MatcherSettings { "type" : "number", "default" : Self::MAXIMUM_FUNCTION_LEN_DEFAULT, "description" : "Functions above this length will not be matched. A value of 0 will disable this check.", - "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + "ignore" : [] }); bn_settings.register_setting_json( Self::MAXIMUM_FUNCTION_LEN_SETTING, @@ -441,7 +360,7 @@ impl MatcherSettings { "type" : "number", "default" : Self::MINIMUM_MATCHED_CONSTRAINTS_DEFAULT, "description" : "When function constraints are checked the amount of constraints matched must be at-least this.", - "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + "ignore" : [] }); bn_settings.register_setting_json( Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING, @@ -453,7 +372,7 @@ impl MatcherSettings { "type" : "boolean", "default" : Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_DEFAULT, "description" : "When function constraints are checked if this is enabled functions can match based off trivial adjacent functions.", - "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + "ignore" : [] }); bn_settings.register_setting_json( Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING, @@ -461,26 +380,32 @@ impl MatcherSettings { ); } - pub fn global() -> Self { + /// Retrieve matcher settings from [`BNSettings`]. + pub fn from_settings(bn_settings: &BNSettings, query_opts: &mut QueryOptions) -> Self { let mut settings = MatcherSettings::default(); - let bn_settings = binaryninja::settings::Settings::new(); if bn_settings.contains(Self::TRIVIAL_FUNCTION_LEN_SETTING) { settings.trivial_function_len = - bn_settings.get_integer(Self::TRIVIAL_FUNCTION_LEN_SETTING); + bn_settings.get_integer_with_opts(Self::TRIVIAL_FUNCTION_LEN_SETTING, query_opts); } if bn_settings.contains(Self::MINIMUM_FUNCTION_LEN_SETTING) { settings.minimum_function_len = - bn_settings.get_integer(Self::MINIMUM_FUNCTION_LEN_SETTING); + bn_settings.get_integer_with_opts(Self::MINIMUM_FUNCTION_LEN_SETTING, query_opts); } if bn_settings.contains(Self::MAXIMUM_FUNCTION_LEN_SETTING) { - match bn_settings.get_integer(Self::MAXIMUM_FUNCTION_LEN_SETTING) { + match bn_settings.get_integer_with_opts(Self::MAXIMUM_FUNCTION_LEN_SETTING, query_opts) + { 0 => settings.maximum_function_len = None, len => settings.maximum_function_len = Some(len), } } if bn_settings.contains(Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING) { - settings.minimum_matched_constraints = - bn_settings.get_integer(Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING) as usize; + settings.minimum_matched_constraints = bn_settings + .get_integer_with_opts(Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING, query_opts) + as usize; + } + if bn_settings.contains(Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING) { + settings.trivial_function_adjacent_allowed = bn_settings + .get_bool_with_opts(Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING, query_opts); } settings } @@ -498,27 +423,3 @@ impl Default for MatcherSettings { } } } - -/// 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().as_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 index 4adab293..f8c4c0c5 100644 --- a/plugins/warp/src/plugin.rs +++ b/plugins/warp/src/plugin.rs @@ -1,186 +1,40 @@ use crate::cache::register_cache_destructor; +use std::time::Instant; +use crate::cache::container::add_cached_container; +use crate::container::disk::DiskContainer; use crate::matcher::MatcherSettings; use crate::plugin::render_layer::HighlightRenderLayer; -use binaryninja::binary_view::{BinaryView, BinaryViewExt}; -use binaryninja::command::{Command, FunctionCommand}; -use binaryninja::function::{Function, FunctionUpdateType}; +use crate::plugin::settings::PluginSettings; +use crate::{core_signature_dir, user_signature_dir}; +use binaryninja::background_task::BackgroundTask; +use binaryninja::command::{ + register_command, register_command_for_function, register_command_for_project, +}; use binaryninja::logger::Logger; -use binaryninja::rc::Ref; -use binaryninja::tags::TagType; -use binaryninja::ObjectDestructor; +use binaryninja::settings::Settings; use log::LevelFilter; -use warp::signature::function::constraints::FunctionConstraint; -use warp::signature::function::Function as WarpFunction; -mod add; -mod copy; mod create; -mod find; +mod debug; +mod ffi; +mod file; +mod function; mod load; +mod project; mod render_layer; -mod types; +mod settings; 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.tag_type_by_name(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(); - // TODO: Using user symbols here is problematic - // TODO: For one they queue up a bunch of main thread actions - // TODO: Secondly by queueing up those main thread actions if you attempt to save the file - // TODO: Before the undo actions are done completing - view.define_user_symbol(&to_bn_symbol_at_address( - &view, - &matched.symbol, - function.symbol().address(), - )); - function.set_user_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, - ); - // Seems to be the only way to get the analysis update to work correctly. - function.mark_updates_required(FunctionUpdateType::FullAutoFunctionUpdate); -} - -struct DebugFunction; - -impl FunctionCommand for DebugFunction { - fn action(&self, _view: &BinaryView, func: &Function) { - if let Ok(llil) = func.low_level_il() { - log::info!("{:#?}", build_function(func, &llil)); - } - } - - fn valid(&self, _view: &BinaryView, _func: &Function) -> bool { - true - } -} - -struct DebugMatcher; - -impl FunctionCommand for DebugMatcher { - fn action(&self, _view: &BinaryView, function: &Function) { - let Ok(llil) = function.low_level_il() else { - log::error!("No LLIL for function 0x{:x}", function.start()); - return; - }; - let platform = function.platform(); - // Build the matcher every time this is called to make sure we aren't in a bad state. - let matcher = Matcher::from_platform(platform); - let func = build_function(function, &llil); - // TODO: Clean this up. - if let Some(possible_matches) = matcher.functions.get(&func.guid) { - let print_constraint = |prefix: &str, constraint: &FunctionConstraint| { - log::info!( - " {} {} ({})", - prefix, - constraint - .to_owned() - .symbol - .map(|s| s.name) - .unwrap_or("*".to_string()), - constraint - .guid - .map(|g| g.to_string()) - .unwrap_or("*".to_string()) - ); - }; - for possible_match in possible_matches.value() { - log::info!("{} ({})", possible_match.symbol.name, possible_match.guid); - for constraint in &possible_match.constraints.call_sites { - print_constraint("CS", constraint); - } - for constraint in &possible_match.constraints.call_sites { - print_constraint("ADJ", constraint); - } - } - } else { - log::error!( - "No possible matches found for the function 0x{:x}", - function.start() - ); - }; - } - - fn valid(&self, _view: &BinaryView, _function: &Function) -> bool { - true - } -} - -struct DebugCache; - -impl Command for DebugCache { - fn action(&self, view: &BinaryView) { - let view_id = ViewID::from(view); - let function_cache = FUNCTION_CACHE.get_or_init(Default::default); - if let Some(cache) = function_cache.get(&view_id) { - log::info!("View functions: {}", cache.cache.len()); - } - - let matched_function_cache = MATCHED_FUNCTION_CACHE.get_or_init(Default::default); - if let Some(cache) = matched_function_cache.get(&view_id) { - log::info!("View matched 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 settings: {:?}", cache.settings); - } - } - } - - fn valid(&self, _view: &BinaryView) -> bool { - true - } -} - -struct DebugInvalidateCache; - -impl Command for DebugInvalidateCache { - fn action(&self, view: &BinaryView) { - invalidate_function_matcher_cache(); - let destructor = cache::CacheDestructor {}; - destructor.destruct_view(view); - log::info!("Invalidated all WARP caches..."); - } - - fn valid(&self, _view: &BinaryView) -> bool { - true - } -} - #[no_mangle] #[allow(non_snake_case)] pub extern "C" fn CorePluginInit() -> bool { Logger::new("WARP").with_level(LevelFilter::Debug).init(); - // Register our matcher settings. - MatcherSettings::register(); + // Register our matcher and plugin settings globally. + let mut global_bn_settings = Settings::new(); + MatcherSettings::register(&mut global_bn_settings); + PluginSettings::register(&mut global_bn_settings); // Make sure caches are flushed when the views get destructed. register_cache_destructor(); @@ -190,70 +44,93 @@ pub extern "C" fn CorePluginInit() -> bool { workflow::insert_workflow(); - binaryninja::command::register_command( + let plugin_settings = PluginSettings::from_settings(&global_bn_settings); + // We want to load all the bundled directories into the container cache. + let background_task = BackgroundTask::new("Loading WARP files...", false); + let start = Instant::now(); + if plugin_settings.load_bundled_files { + let core_disk_container = DiskContainer::new_from_dir(core_signature_dir()); + log::debug!("{:#?}", core_disk_container); + add_cached_container(core_disk_container); + } + if plugin_settings.load_user_files { + let user_disk_container = DiskContainer::new_from_dir(user_signature_dir()); + log::debug!("{:#?}", user_disk_container); + add_cached_container(user_disk_container); + } + log::info!("Loading bundled files took {:?}", start.elapsed()); + background_task.finish(); + + register_command( "WARP\\Run Matcher", "Run the matcher manually", workflow::RunMatcher {}, ); - binaryninja::command::register_command( + register_command( "WARP\\Debug\\Cache", "Debug cache sizes... because...", - DebugCache {}, + debug::DebugCache {}, ); - binaryninja::command::register_command( + register_command( "WARP\\Debug\\Invalidate Caches", "Invalidate all WARP caches", - DebugInvalidateCache {}, + debug::DebugInvalidateCache {}, ); - binaryninja::command::register_command_for_function( + register_command_for_function( "WARP\\Debug\\Function Signature", "Print the entire signature for the function", - DebugFunction {}, - ); - - binaryninja::command::register_command_for_function( - "WARP\\Debug\\Function Matcher", - "Print all possible matches for the function", - DebugMatcher {}, + debug::DebugFunction {}, ); - binaryninja::command::register_command( - "WARP\\Debug\\Apply Signature File Types", - "Load all types from a signature file and ignore functions", - types::LoadTypes {}, - ); - - binaryninja::command::register_command( - "WARP\\Load Signature File", + register_command( + "WARP\\Load File", "Load file into the matcher, this does NOT kick off matcher analysis", load::LoadSignatureFile {}, ); - binaryninja::command::register_command_for_function( - "WARP\\Copy Function GUID", + register_command_for_function( + "WARP\\Function\\Include", + "Add current function to the list of functions to add to the signature file", + function::IncludeFunction {}, + ); + + register_command_for_function( + "WARP\\Function\\Copy GUID", "Copy the computed GUID for the function", - copy::CopyFunctionGUID {}, + function::CopyFunctionGUID {}, ); - binaryninja::command::register_command( - "WARP\\Find Function From GUID", + register_command( + "WARP\\Function\\Find GUID", "Locate the function in the view using a GUID", - find::FindFunctionFromGUID {}, + function::FindFunctionFromGUID {}, + ); + + register_command( + "WARP\\Create\\From Current View", + "Creates a signature file containing all selected functions", + create::CreateFromCurrentView {}, + ); + + register_command( + "WARP\\Create\\From File(s)", + "Creates a signature file containing all selected functions", + create::CreateFromFiles {}, ); - binaryninja::command::register_command( - "WARP\\Generate Signature File", - "Generates a signature file containing all binary view functions", - create::CreateSignatureFile {}, + register_command( + "WARP\\Show Report", + "Creates a report for the selected file, displaying info on functions and types", + file::ShowFileReport {}, ); - binaryninja::command::register_command_for_function( - "WARP\\Add Function Signature to File", - "Stores the signature for the function in the signature file", - add::AddFunctionSignature {}, + register_command_for_project( + "WARP\\Create\\From Project", + "Create signature files from select project files", + project::CreateSignatures {}, ); true diff --git a/plugins/warp/src/plugin/add.rs b/plugins/warp/src/plugin/add.rs deleted file mode 100644 index 2f76c8b5..00000000 --- a/plugins/warp/src/plugin/add.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::cache::{cached_function, cached_type_references}; -use crate::matcher::invalidate_function_matcher_cache; -use crate::user_signature_dir; -use binaryninja::binary_view::BinaryView; -use binaryninja::command::FunctionCommand; -use binaryninja::function::Function; -use std::thread; - -pub struct AddFunctionSignature; - -impl FunctionCommand for AddFunctionSignature { - fn action(&self, view: &BinaryView, func: &Function) { - let func_plat_name = func.platform().name().to_string(); - let signature_dir = user_signature_dir().join(func_plat_name); - let view = view.to_owned(); - let func = func.to_owned(); - thread::spawn(move || { - let Ok(llil) = func.low_level_il() else { - log::error!("Could not get low level IL for function."); - return; - }; - - // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API. - // If we did need to save signature files to a project than this would need to change. - let Some(save_file) = rfd::FileDialog::new() - .add_filter("Signature Files", &["sbin"]) - .set_file_name("user.sbin") - .set_directory(signature_dir) - .save_file() - else { - return; - }; - - let mut data = warp::signature::Data::default(); - if let Ok(file_bytes) = std::fs::read(&save_file) { - // If the file we are adding the function to already has data we should preserve it! - log::info!("Signature file already exists, preserving data..."); - let Some(file_data) = warp::signature::Data::from_bytes(&file_bytes) else { - log::error!("Could not get data from signature file: {:?}", save_file); - return; - }; - data = file_data; - }; - - // Now add our function to the data. - data.functions.push(cached_function(&func, &llil)); - - if let Some(ref_ty_cache) = cached_type_references(&view) { - let referenced_types = ref_ty_cache - .cache - .iter() - .filter_map(|t| t.to_owned()) - .collect::<Vec<_>>(); - - data.types.extend(referenced_types); - } - - match std::fs::write(&save_file, data.to_bytes()) { - Ok(_) => { - log::info!("Signature file saved successfully."); - // Force rebuild platform matcher. - invalidate_function_matcher_cache(); - } - Err(e) => log::error!("Failed to write data to signature file: {:?}", e), - } - }); - } - - fn valid(&self, _view: &BinaryView, _func: &Function) -> bool { - true - } -} diff --git a/plugins/warp/src/plugin/copy.rs b/plugins/warp/src/plugin/copy.rs deleted file mode 100644 index b9b985fc..00000000 --- a/plugins/warp/src/plugin/copy.rs +++ /dev/null @@ -1,29 +0,0 @@ -use binaryninja::binary_view::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; - }; - let guid = cached_function_guid(func, &llil); - log::info!( - "Function GUID for {:?}... {}", - func.symbol().short_name(), - guid - ); - if let Ok(mut clipboard) = arboard::Clipboard::new() { - let _ = clipboard.set_text(guid.to_string()); - } - } - - 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 index 1dd83e6e..9ebcba2d 100644 --- a/plugins/warp/src/plugin/create.rs +++ b/plugins/warp/src/plugin/create.rs @@ -1,94 +1,232 @@ -use crate::cache::{cached_function, cached_type_references}; -use crate::matcher::invalidate_function_matcher_cache; -use crate::user_signature_dir; +use crate::processor::{ + new_processing_state_background_thread, CompressionTypeField, FileDataKindField, + IncludedFunctionsField, SaveReportToDiskField, WarpFileProcessor, +}; +use crate::report::{ReportGenerator, ReportKindField}; +use crate::{user_signature_dir, INCLUDE_TAG_NAME}; +use binaryninja::background_task::BackgroundTask; use binaryninja::binary_view::{BinaryView, BinaryViewExt}; use binaryninja::command::Command; -use binaryninja::function::Function; -use binaryninja::rc::Guard; -use rayon::prelude::*; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering::Relaxed; +use binaryninja::interaction::form::{Form, FormInputField}; +use binaryninja::interaction::{MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon}; +use binaryninja::rc::Ref; +use std::path::PathBuf; use std::thread; -use std::time::Instant; +use warp::chunk::Chunk; +use warp::WarpFile; -pub struct CreateSignatureFile; +pub struct SaveFileField; -// TODO: Prompt the user to add the newly created signature file to the signature blacklist (so that it doesn't keep getting applied) +impl SaveFileField { + pub fn field(view: &BinaryView) -> FormInputField { + let default_name = view + .file() + .filename() + .split('/') + .last() + .unwrap_or("file") + .to_string(); + let signature_dir = user_signature_dir(); + let default_file_path = signature_dir.join(&default_name).with_extension("warp"); + FormInputField::SaveFileName { + prompt: "File Path".to_string(), + // TODO: This is called extension but is really a filter. + extension: Some("*.warp".to_string()), + default_name: Some(default_name), + default: Some(default_file_path.to_string_lossy().to_string()), + value: None, + } + } -impl Command for CreateSignatureFile { - fn action(&self, view: &BinaryView) { - let is_function_named = |f: &Guard<Function>| { - !f.symbol().short_name().to_string_lossy().contains("sub_") || f.has_user_annotations() - }; - let mut signature_dir = user_signature_dir(); - if let Some(default_plat) = view.default_platform() { - // If there is a default platform, put the signature in there. - // TODO: We should instead use the platform of the function. - signature_dir.push(default_plat.name().to_string()); + pub fn from_form(form: &Form) -> Option<PathBuf> { + let field = form.get_field_with_name("File Path")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct OpenFileField; + +impl OpenFileField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "Input File Path".to_string(), + extension: None, + default: None, + value: None, } - let view = view.to_owned(); - thread::spawn(move || { - let total_functions = view.functions().len(); - let done_functions = AtomicUsize::default(); - let background_task = binaryninja::background_task::BackgroundTask::new( - &format!("Generating signatures... ({}/{})", 0, total_functions), - true, - ); + } + + pub fn from_form(form: &Form) -> Option<PathBuf> { + let field = form.get_field_with_name("Input File Path")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct CreateFromCurrentView; - let start = Instant::now(); +impl CreateFromCurrentView { + pub fn execute(view: Ref<BinaryView>, external_file: bool) -> Option<()> { + // Prompt the user first so that they can go do other things and not worry about a popup. + let mut form = Form::new("Create From View"); - let mut data = warp::signature::Data::default(); - data.functions.par_extend( - view.functions() - .par_iter() - .inspect(|_| { - done_functions.fetch_add(1, Relaxed); - background_task.set_progress_text(&format!( - "Generating signatures... ({}/{})", - done_functions.load(Relaxed), - total_functions - )) - }) - .filter(is_function_named) - .filter(|f| !f.analysis_skipped()) - .filter_map(|func| { - let llil = func.low_level_il().ok()?; - Some(cached_function(&func, &llil)) - }), + if external_file { + form.add_field(OpenFileField::field()); + } + + form.add_field(SaveFileField::field(&view)); + + let fd_field = FileDataKindField::default(); + form.add_field(fd_field.to_field()); + + let compression_field = CompressionTypeField::default(); + form.add_field(compression_field.to_field()); + + let mut included_field = IncludedFunctionsField::default(); + // If the view has the include tag, we better set the default to the selected functions. + if view.tag_type_by_name(INCLUDE_TAG_NAME).is_some() { + included_field = IncludedFunctionsField::Selected; + } + form.add_field(included_field.to_field()); + + let report_field = ReportKindField::default(); + form.add_field(report_field.to_field()); + let report_to_disk_field = SaveReportToDiskField::default(); + form.add_field(report_to_disk_field.to_field()); + + if !form.prompt() { + return None; + } + let compression_type = CompressionTypeField::from_form(&form).unwrap_or_default(); + let file_path = SaveFileField::from_form(&form)?; + let file_data_kind = FileDataKindField::from_form(&form).unwrap_or_default(); + let file_included_functions = IncludedFunctionsField::from_form(&form).unwrap_or_default(); + let report_kind = ReportKindField::from_form(&form).unwrap_or_default(); + let save_report_to_disk = SaveReportToDiskField::from_form(&form).unwrap_or_default(); + let open_file_path = OpenFileField::from_form(&form); + + // If we already have a file, prompt the user if they want to add the data. + let mut existing_chunks = Vec::new(); + if file_path.exists() { + let prompt_result = binaryninja::interaction::show_message_box( + "Keep existing file data?", + "The file already exists. Do you want to keep the existing data?", + MessageBoxButtonSet::YesNoCancelButtonSet, + MessageBoxIcon::QuestionIcon, ); - if let Some(ref_ty_cache) = cached_type_references(&view) { - let referenced_types = ref_ty_cache - .cache - .iter() - .filter_map(|t| t.to_owned()) - .collect::<Vec<_>>(); + match prompt_result { + MessageBoxButtonResult::NoButton => { + // User wants to overwrite the file. + } + MessageBoxButtonResult::YesButton | MessageBoxButtonResult::OKButton => { + // User wants to keep the existing data. + let data = std::fs::read(&file_path).ok()?; + let existing_file = WarpFile::from_owned_bytes(data)?; + existing_chunks.extend(existing_file.chunks); + } + MessageBoxButtonResult::CancelButton => { + log::info!( + "User cancelled signature file creation, no operations were performed." + ); + return None; + } + } + } + + let processor = WarpFileProcessor::new() + .with_compression_type(compression_type) + .with_file_data(file_data_kind) + .with_included_functions(file_included_functions); - data.types.extend(referenced_types); + let file = match open_file_path { + None => { + // We are processing the current view. NOT an external file. + // Reference path is just used for the state tracking. Does not need to be readable. + let reference_path = file_path.clone(); + processor.process_view(reference_path, &view) + } + Some(open_file_path) => { + // This thread will show the state in a background task. + let background_task = BackgroundTask::new("Processing started...", true); + new_processing_state_background_thread(background_task.clone(), processor.state()); + let file = processor.process(open_file_path); + background_task.finish(); + file } + }; + + if let Err(err) = file { + binaryninja::interaction::show_message_box( + "Error", + &format!("Failed to create signature file: {}", err), + MessageBoxButtonSet::OKButtonSet, + MessageBoxIcon::ErrorIcon, + ); + log::error!("Failed to create signature file: {}", err); + return None; + } - log::info!("Signature generation took {:?}", start.elapsed()); - background_task.finish(); + let mut file = file.unwrap(); + // Add back the existing chunks if the user selected to keep them. + file.chunks.extend(existing_chunks); + // TODO: Make merging optional? + file.chunks = Chunk::merge(&file.chunks, compression_type.into()); + + if std::fs::write(&file_path, file.to_bytes()).is_err() { + log::error!("Failed to write data to signature file!"); + } - // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API. - // If we did need to save signature files to a project than this would need to change. - let Some(save_file) = rfd::FileDialog::new() - .add_filter("Signature Files", &["sbin"]) - .set_file_name(format!("{}.sbin", view.file().filename())) - .set_directory(signature_dir) - .save_file() - else { - return; - }; + // Show a report of the generate signatures, if desired. + let report_generator = ReportGenerator::new(); + if let Some(report_string) = report_generator.report(&report_kind, &file) { + if save_report_to_disk == SaveReportToDiskField::Yes { + let report_ext = report_generator + .report_extension(&report_kind) + .unwrap_or_default(); + let report_path = file_path.with_extension(report_ext); + let _ = std::fs::write(report_path, &report_string); + } - match std::fs::write(&save_file, data.to_bytes()) { - Ok(_) => { - log::info!("Signature file saved successfully."); - // Force rebuild platform matcher. - invalidate_function_matcher_cache(); + match report_kind { + ReportKindField::None => {} + ReportKindField::Html => { + view.show_html_report("Generated WARP File", report_string.as_str(), ""); + } + ReportKindField::Markdown => { + view.show_markdown_report("Generated WARP File", report_string.as_str(), ""); + } + ReportKindField::Json => { + view.show_plaintext_report("Generated WARP File", report_string.as_str()); } - Err(e) => log::error!("Failed to write data to signature file: {:?}", e), } + } + + Some(()) + } +} + +impl Command for CreateFromCurrentView { + fn action(&self, view: &BinaryView) { + let view = view.to_owned(); + thread::spawn(move || { + CreateFromCurrentView::execute(view, false); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} + +pub struct CreateFromFiles; + +impl Command for CreateFromFiles { + fn action(&self, view: &BinaryView) { + let view = view.to_owned(); + thread::spawn(move || { + CreateFromCurrentView::execute(view, true); }); } diff --git a/plugins/warp/src/plugin/debug.rs b/plugins/warp/src/plugin/debug.rs new file mode 100644 index 00000000..fc4e5817 --- /dev/null +++ b/plugins/warp/src/plugin/debug.rs @@ -0,0 +1,48 @@ +use crate::cache::container::for_cached_containers; +use crate::{build_function, cache}; +use binaryninja::binary_view::BinaryView; +use binaryninja::command::{Command, FunctionCommand}; +use binaryninja::function::Function; +use binaryninja::ObjectDestructor; + +pub struct DebugFunction; + +impl FunctionCommand for DebugFunction { + fn action(&self, _view: &BinaryView, func: &Function) { + if let Ok(lifted_il) = func.lifted_il() { + log::info!("{:#?}", build_function(func, &lifted_il)); + } + } + + fn valid(&self, _view: &BinaryView, _func: &Function) -> bool { + true + } +} + +pub struct DebugCache; + +impl Command for DebugCache { + fn action(&self, _view: &BinaryView) { + for_cached_containers(|c| { + log::info!("Container: {:#?}", c); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} + +pub struct DebugInvalidateCache; + +impl Command for DebugInvalidateCache { + fn action(&self, view: &BinaryView) { + let destructor = cache::CacheDestructor {}; + destructor.destruct_view(view); + log::info!("Invalidated all WARP caches..."); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/ffi.rs b/plugins/warp/src/plugin/ffi.rs new file mode 100644 index 00000000..d78ca3c5 --- /dev/null +++ b/plugins/warp/src/plugin/ffi.rs @@ -0,0 +1,205 @@ +mod container; +mod function; + +use binaryninjacore_sys::{ + BNBasicBlock, BNBinaryView, BNFunction, BNLowLevelILFunction, BNPlatform, +}; +use std::ffi::c_char; +use std::sync::{Arc, RwLock}; +use uuid::Uuid; + +use binaryninja::basic_block::{BasicBlock, BasicBlockType}; +use binaryninja::function::{Function, NativeBlock}; + +use crate::cache::cached_function_guid; +use crate::container::{Container, SourceId}; +use crate::convert::platform_to_target; +use crate::plugin::workflow::run_matcher; +use crate::{ + basic_block_guid, is_blacklisted_instruction, is_computed_variant_instruction, + is_variant_instruction, relocatable_regions, +}; +use binaryninja::binary_view::BinaryView; +use binaryninja::low_level_il::function::{LowLevelILFunction, Mutable, NonSSA}; +use binaryninja::low_level_il::instruction::LowLevelInstructionIndex; +use binaryninja::platform::Platform; +use binaryninja::string::BnString; +use warp::r#type::guid::TypeGUID; +use warp::signature::basic_block::BasicBlockGUID; +use warp::signature::constraint::{Constraint, ConstraintGUID, UNRELATED_OFFSET}; +use warp::signature::function::FunctionGUID; + +/// [`SourceId`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI. +pub type BNWARPSource = SourceId; + +/// [`BasicBlockGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI. +pub type BNWARPBasicBlockGUID = BasicBlockGUID; + +/// [`ConstraintGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI. +pub type BNWARPConstraintGUID = ConstraintGUID; + +/// [`FunctionGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI. +pub type BNWARPFunctionGUID = FunctionGUID; + +/// [`TypeGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI. +pub type BNWARPTypeGUID = TypeGUID; + +pub type BNWARPTarget = warp::target::Target; +pub type BNWARPFunction = warp::signature::function::Function; +pub type BNWARPContainer = RwLock<Box<dyn Container>>; + +// TODO: Some sort of callback for loading functions +// TODO: Be able to run matcher for a specific file +// TODO: Generate signatures for a file, return what? + +#[repr(C)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct BNWARPConstraint { + guid: BNWARPConstraintGUID, + offset: i64, +} + +impl From<BNWARPConstraint> for Constraint { + fn from(constraint: BNWARPConstraint) -> Self { + Constraint { + guid: constraint.guid, + offset: match constraint.offset { + UNRELATED_OFFSET => None, + _ => Some(constraint.offset), + }, + } + } +} + +impl From<Constraint> for BNWARPConstraint { + fn from(constraint: Constraint) -> Self { + BNWARPConstraint { + guid: constraint.guid, + offset: constraint.offset.unwrap_or(UNRELATED_OFFSET), + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPUUIDGetString(uuid: *const Uuid) -> *mut c_char { + let uuid_str = (*uuid).to_string(); + // NOTE: Leak the uuid string to be freed by BNFreeString + BnString::into_raw(uuid_str.into()) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPUUIDEqual(a: *const Uuid, b: *const Uuid) -> bool { + (*a) == (*b) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPRunMatcher(view: *mut BNBinaryView) { + let view = BinaryView::from_raw(view); + run_matcher(&view) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPGetBasicBlockGUID( + basic_block: *mut BNBasicBlock, + result: *mut BNWARPBasicBlockGUID, +) -> bool { + let basic_block = unsafe { BasicBlock::from_raw(basic_block, NativeBlock::new()) }; + if basic_block.block_type() != BasicBlockType::Native { + return false; + } + let function = basic_block.function(); + match function.lifted_il() { + Ok(lifted_il) => { + let relocatable_regions = relocatable_regions(&function.view()); + *result = basic_block_guid(&relocatable_regions, &basic_block, &lifted_il); + true + } + Err(_) => false, + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPGetAnalysisFunctionGUID( + analysis_function: *mut BNFunction, + result: *mut BNWARPFunctionGUID, +) -> bool { + let function = unsafe { Function::from_raw(analysis_function) }; + match function.lifted_il() { + Ok(lifted_il) => { + *result = cached_function_guid(&function, &lifted_il); + true + } + Err(_) => false, + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPIsLiftedInstructionVariant( + analysis_function: *mut BNLowLevelILFunction, + index: LowLevelInstructionIndex, +) -> bool { + let lifted_il: LowLevelILFunction<Mutable, NonSSA> = + unsafe { LowLevelILFunction::from_raw(analysis_function) }; + match lifted_il.instruction_from_index(index) { + Some(instr) => { + let relocatable_regions = relocatable_regions(&lifted_il.function().view()); + is_variant_instruction(&relocatable_regions, &instr) + } + None => false, + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPIsLowLevelInstructionComputedVariant( + analysis_function: *mut BNLowLevelILFunction, + index: LowLevelInstructionIndex, +) -> bool { + let llil: LowLevelILFunction<Mutable, NonSSA> = + unsafe { LowLevelILFunction::from_raw(analysis_function) }; + match llil.instruction_from_index(index) { + Some(instr) => { + let relocatable_regions = relocatable_regions(&llil.function().view()); + is_computed_variant_instruction(&relocatable_regions, &instr) + } + None => false, + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPIsLiftedInstructionBlacklisted( + analysis_function: *mut BNLowLevelILFunction, + index: LowLevelInstructionIndex, +) -> bool { + let lifted_il: LowLevelILFunction<Mutable, NonSSA> = + unsafe { LowLevelILFunction::from_raw(analysis_function) }; + match lifted_il.instruction_from_index(index) { + Some(instr) => is_blacklisted_instruction(&instr), + None => false, + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeUUIDList(uuids: *mut Uuid, count: usize) { + let sources_ptr = std::ptr::slice_from_raw_parts_mut(uuids, count); + let _ = unsafe { Box::from_raw(sources_ptr) }; +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPGetTarget(platform: *mut BNPlatform) -> *mut BNWARPTarget { + let platform = Platform::from_raw(platform); + Arc::into_raw(Arc::new(platform_to_target(&platform))) as *mut BNWARPTarget +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewTargetReference(target: *mut BNWARPTarget) -> *mut BNWARPTarget { + Arc::increment_strong_count(target); + target +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeTargetReference(target: *mut BNWARPTarget) { + if target.is_null() { + return; + } + Arc::decrement_strong_count(target); +} diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs new file mode 100644 index 00000000..21ad4dc0 --- /dev/null +++ b/plugins/warp/src/plugin/ffi/container.rs @@ -0,0 +1,412 @@ +use crate::cache::container::cached_containers; +use crate::container::SourcePath; +use crate::convert::{from_bn_type, to_bn_type}; +use crate::plugin::ffi::{ + BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource, BNWARPTarget, BNWARPTypeGUID, +}; +use binaryninja::architecture::CoreArchitecture; +use binaryninja::binary_view::BinaryView; +use binaryninja::rc::Ref; +use binaryninja::string::BnString; +use binaryninja::types::Type; +use binaryninjacore_sys::{BNArchitecture, BNBinaryView, BNType}; +use std::ffi::{c_char, CStr}; +use std::mem::ManuallyDrop; +use std::sync::Arc; + +#[no_mangle] +pub unsafe extern "C" fn BNWARPGetContainers(count: *mut usize) -> *mut *mut BNWARPContainer { + // NOTE: Leak the arc pointers to be freed by BNWARPFreeContainerList + let boxed_raw_containers: Box<[_]> = + cached_containers().into_iter().map(Arc::into_raw).collect(); + *count = boxed_raw_containers.len(); + let leaked_raw_containers = Box::into_raw(boxed_raw_containers); + leaked_raw_containers as *mut *mut BNWARPContainer +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetName(container: *mut BNWARPContainer) -> *const c_char { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null(); + }; + let name = container.to_string(); + // NOTE: Leak the container name to be freed by BNFreeString + BnString::into_raw(name.into()) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetSources( + container: *mut BNWARPContainer, + count: *mut usize, +) -> *mut BNWARPSource { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.write() else { + return std::ptr::null_mut(); + }; + + // NOTE: Leak the sources to be freed by BNWARPFreeSourceList + let boxed_sources: Box<[_]> = container.sources().unwrap_or_default().into_boxed_slice(); + *count = boxed_sources.len(); + Box::into_raw(boxed_sources) as *mut BNWARPSource +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerAddSource( + container: *mut BNWARPContainer, + source_path: *const c_char, + result: *mut BNWARPSource, +) -> bool { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(mut container) = arc_container.write() else { + return false; + }; + + let source_path_cstr = unsafe { CStr::from_ptr(source_path) }; + let source_path_str = source_path_cstr.to_str().unwrap(); + let source_path = SourcePath::new_with_str(source_path_str); + + match container.add_source(source_path) { + Ok(source) => { + // NOTE: Leak the source to be freed by BNFreeString + *result = source; + true + } + Err(_) => false, + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerCommitSource( + container: *mut BNWARPContainer, + source: *const BNWARPSource, +) -> bool { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(mut container) = arc_container.write() else { + return false; + }; + + let source = unsafe { *source }; + + container + .commit_source(&source) + .is_ok_and(|committed| committed) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerIsSourceUncommitted( + container: *mut BNWARPContainer, + source: *const BNWARPSource, +) -> bool { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return false; + }; + + let source = unsafe { *source }; + + container + .is_source_uncommitted(&source) + .is_ok_and(|uncommitted| uncommitted) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerIsSourceWritable( + container: *mut BNWARPContainer, + source: *const BNWARPSource, +) -> bool { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return false; + }; + + let source = unsafe { *source }; + + container + .is_source_writable(&source) + .is_ok_and(|writable| writable) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetSourcePath( + container: *mut BNWARPContainer, + source: *const BNWARPSource, +) -> *const c_char { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null(); + }; + + let source = unsafe { *source }; + + match container.source_path(&source) { + Ok(path) => { + let path = path.to_string(); + // NOTE: Leak the source path to be freed by BNFreeString + BnString::into_raw(path.into()) + } + Err(_) => std::ptr::null(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerAddFunctions( + container: *mut BNWARPContainer, + target: *mut BNWARPTarget, + source: *const BNWARPSource, + functions: *mut *mut BNWARPFunction, + count: usize, +) -> bool { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(mut container) = arc_container.write() else { + return false; + }; + + let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) }; + + let source = unsafe { *source }; + + let functions_ptr = std::slice::from_raw_parts(functions, count); + // TODO: We have to clone the objects here to make the type checker happy. + // TODO: See about avoiding this later. + let functions: Vec<_> = functions_ptr + .iter() + .map(|&f| unsafe { ManuallyDrop::new(Arc::from_raw(f)).as_ref().clone() }) + .collect(); + container + .add_functions(&target, &source, &functions) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerAddTypes( + view: *mut BNBinaryView, + container: *mut BNWARPContainer, + source: *const BNWARPSource, + types: *mut *mut BNType, + count: usize, +) -> bool { + let view = unsafe { BinaryView::from_raw(view) }; + + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(mut container) = arc_container.write() else { + return false; + }; + + let source = unsafe { *source }; + + let types_ptr = std::slice::from_raw_parts(types, count); + let types: Vec<_> = types_ptr + .iter() + .map(|&t| Type::from_raw(t)) + .map(|ty| from_bn_type(&view, &ty, 255)) + .collect(); + container.add_types(&source, &types).is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerRemoveFunctions( + container: *mut BNWARPContainer, + target: *mut BNWARPTarget, + source: *const BNWARPSource, + functions: *mut *mut BNWARPFunction, + count: usize, +) -> bool { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(mut container) = arc_container.write() else { + return false; + }; + + let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) }; + + let source = unsafe { *source }; + + let functions_ptr = std::slice::from_raw_parts(functions, count); + // TODO: We have to clone the objects here to make the type checker happy. + // TODO: See about avoiding this later. + let functions: Vec<_> = functions_ptr + .iter() + .map(|&f| unsafe { ManuallyDrop::new(Arc::from_raw(f)).as_ref().clone() }) + .collect(); + container + .remove_functions(&target, &source, &functions) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerRemoveTypes( + container: *mut BNWARPContainer, + source: *const BNWARPSource, + guids: *mut BNWARPTypeGUID, + count: usize, +) -> bool { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(mut container) = arc_container.write() else { + return false; + }; + + let source = unsafe { *source }; + + let guids = std::slice::from_raw_parts(guids, count); + container.remove_types(&source, &guids).is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetSourcesWithFunctionGUID( + container: *mut BNWARPContainer, + target: *mut BNWARPTarget, + guid: *const BNWARPFunctionGUID, + count: *mut usize, +) -> *mut BNWARPSource { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null_mut(); + }; + + let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) }; + + let guid = unsafe { *guid }; + + // NOTE: Leak the sources to be freed by BNWARPFreeSourceList + let boxed_sources: Box<[_]> = container + .sources_with_function_guid(&target, &guid) + .unwrap_or_default() + .into_boxed_slice(); + *count = boxed_sources.len(); + Box::into_raw(boxed_sources) as *mut BNWARPSource +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetSourcesWithTypeGUID( + container: *mut BNWARPContainer, + guid: *const BNWARPTypeGUID, + count: *mut usize, +) -> *mut BNWARPSource { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null_mut(); + }; + + let guid = unsafe { *guid }; + + // NOTE: Leak the sources to be freed by BNWARPFreeSourceList + let boxed_sources: Box<[_]> = container + .sources_with_type_guid(&guid) + .unwrap_or_default() + .into_boxed_slice(); + *count = boxed_sources.len(); + Box::into_raw(boxed_sources) as *mut BNWARPSource +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetFunctionsWithGUID( + container: *mut BNWARPContainer, + target: *mut BNWARPTarget, + source: *const BNWARPSource, + guid: *const BNWARPFunctionGUID, + count: *mut usize, +) -> *mut *mut BNWARPFunction { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null_mut(); + }; + + let source = unsafe { *source }; + + let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) }; + + let guid = unsafe { *guid }; + + // NOTE: Leak the functions to be freed by BNWARPFreeFunctionList + let raw_boxed_functions: Box<[_]> = container + .functions_with_guid(&target, &source, &guid) + .unwrap_or_default() + .into_iter() + .map(Arc::new) + .map(Arc::into_raw) + .collect(); + *count = raw_boxed_functions.len(); + Box::into_raw(raw_boxed_functions) as *mut *mut BNWARPFunction +} + +// TODO: Swap arch to Target? +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetTypeWithGUID( + arch: *mut BNArchitecture, + container: *mut BNWARPContainer, + source: *const BNWARPSource, + guid: *const BNWARPTypeGUID, +) -> *mut BNType { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null_mut(); + }; + + // NOTE: to convert the type, we must have an architecture. + let arch = CoreArchitecture::from_raw(arch); + + let source = unsafe { *source }; + + let guid = unsafe { *guid }; + + let Some(ty) = container.type_with_guid(&source, &guid).unwrap_or_default() else { + return std::ptr::null_mut(); + }; + let function_type = to_bn_type(&arch, &ty); + // NOTE: The type ref has been pre-incremented for the caller. + unsafe { Ref::into_raw(function_type) }.handle +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerGetTypeGUIDsWithName( + container: *mut BNWARPContainer, + source: *const BNWARPSource, + name: *const c_char, + count: *mut usize, +) -> *mut BNWARPTypeGUID { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null_mut(); + }; + + let source = unsafe { *source }; + + let name_cstr = unsafe { CStr::from_ptr(name) }; + let name = name_cstr.to_str().unwrap(); + + // NOTE: Leak the guids to be freed by BNWARPFreeTypeGUIDList + let boxed_guids = container + .type_guids_with_name(&source, name) + .unwrap_or_default() + .into_boxed_slice(); + *count = boxed_guids.len(); + Box::into_raw(boxed_guids) as *mut BNWARPTypeGUID +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewContainerReference( + container: *mut BNWARPContainer, +) -> *mut BNWARPContainer { + Arc::increment_strong_count(container); + container +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeContainerReference(container: *mut BNWARPContainer) { + if container.is_null() { + return; + } + Arc::decrement_strong_count(container); +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeContainerList( + containers: *mut *mut BNWARPContainer, + count: usize, +) { + let containers_ptr = std::ptr::slice_from_raw_parts_mut(containers, count); + let containers = unsafe { Box::from_raw(containers_ptr) }; + for container in containers { + BNWARPFreeContainerReference(container); + } +} diff --git a/plugins/warp/src/plugin/ffi/function.rs b/plugins/warp/src/plugin/ffi/function.rs new file mode 100644 index 00000000..3db8f307 --- /dev/null +++ b/plugins/warp/src/plugin/ffi/function.rs @@ -0,0 +1,228 @@ +use crate::build_function; +use crate::cache::{insert_cached_function_match, try_cached_function_match}; +use crate::convert::{to_bn_symbol_at_address, to_bn_type}; +use crate::plugin::ffi::{BNWARPConstraint, BNWARPFunction, BNWARPFunctionGUID}; +use binaryninja::function::Function; +use binaryninja::rc::Ref; +use binaryninja::string::BnString; +use binaryninjacore_sys::{BNFunction, BNSymbol, BNType}; +use std::ffi::c_char; +use std::mem::ManuallyDrop; +use std::sync::Arc; +use warp::signature::comment::FunctionComment; + +#[repr(C)] +pub struct BNWarpFunctionComment { + pub text: *mut c_char, + pub offset: i64, +} + +impl BNWarpFunctionComment { + /// Leaks the text string to be freed with BNWARPFreeFunctionComment + pub fn from_owned(value: &FunctionComment) -> Self { + let text = BnString::into_raw(BnString::new(&value.text)); + Self { + text, + offset: value.offset, + } + } + + pub unsafe fn free_raw(value: &Self) { + unsafe { BnString::free_raw(value.text) } + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPGetFunction( + analysis_function: *mut BNFunction, +) -> *mut BNWARPFunction { + let function = Function::from_raw(analysis_function); + let Ok(lifted_il) = function.lifted_il() else { + return std::ptr::null_mut(); + }; + let function = build_function(&function, &lifted_il); + Arc::into_raw(Arc::new(function)) as *mut BNWARPFunction +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPGetMatchedFunction( + analysis_function: *mut BNFunction, +) -> *mut BNWARPFunction { + let function = Function::from_raw(analysis_function); + match try_cached_function_match(&function) { + Some(matched_function) => { + let arc_matched_function = Arc::new(matched_function); + // NOTE: Freed by BNWARPFreeFunctionReference + Arc::into_raw(arc_matched_function) as *mut BNWARPFunction + } + None => std::ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionApply( + function: *mut BNWARPFunction, + analysis_function: *mut BNFunction, +) { + let analysis_function = Function::from_raw(analysis_function); + match function.is_null() { + false => { + // Set the matched function to `function` and return previous. + let matched_function = ManuallyDrop::new(Arc::from_raw(function)); + insert_cached_function_match( + &analysis_function, + Some(matched_function.as_ref().clone()), + ) + } + true => { + // We are removing the previous match and returning it. + insert_cached_function_match(&analysis_function, None) + } + }; +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionGetGUID( + function: *mut BNWARPFunction, +) -> BNWARPFunctionGUID { + // We do not own function so we should not drop. + let function = ManuallyDrop::new(Arc::from_raw(function)); + function.guid +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionGetSymbol( + function: *mut BNWARPFunction, + analysis_function: *mut BNFunction, +) -> *mut BNSymbol { + let analysis_function = Function::from_raw(analysis_function); + // We do not own function so we should not drop. + let function = ManuallyDrop::new(Arc::from_raw(function)); + let view = analysis_function.view(); + let address = analysis_function.symbol().address(); + let function_symbol = to_bn_symbol_at_address(&view, &function.symbol, address); + // NOTE: The symbol ref has been pre-incremented for the caller. + Ref::into_raw(function_symbol).handle +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionGetSymbolName(function: *mut BNWARPFunction) -> *mut c_char { + // We do not own function so we should not drop. + let function = ManuallyDrop::new(Arc::from_raw(function)); + let bn_name = BnString::new(&function.symbol.name); + // NOTE: The symbol name string to be freed by BNFreeString + BnString::into_raw(bn_name) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionGetType( + function: *mut BNWARPFunction, + analysis_function: *mut BNFunction, +) -> *mut BNType { + let analysis_function = Function::from_raw(analysis_function); + // We do not own function so we should not drop. + let function = ManuallyDrop::new(Arc::from_raw(function)); + match &function.ty { + Some(func_ty) => { + let arch = analysis_function.arch(); + let function_type = to_bn_type(&arch, func_ty); + // NOTE: The type ref has been pre-incremented for the caller. + unsafe { Ref::into_raw(function_type) }.handle + } + None => std::ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionGetConstraints( + function: *mut BNWARPFunction, + count: *mut usize, +) -> *mut BNWARPConstraint { + // We do not own function so we should not drop. + let function = ManuallyDrop::new(Arc::from_raw(function)); + let raw_constraints: Box<[BNWARPConstraint]> = function + .constraints + .clone() + .into_iter() + .map(Into::into) + .collect(); + *count = raw_constraints.len(); + let raw_constraints_ptr = Box::into_raw(raw_constraints); + raw_constraints_ptr as *mut BNWARPConstraint +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionGetComments( + function: *mut BNWARPFunction, + count: *mut usize, +) -> *mut BNWarpFunctionComment { + // We do not own function so we should not drop. + let function = ManuallyDrop::new(Arc::from_raw(function)); + let raw_comments: Box<[_]> = function + .comments + .iter() + .map(BNWarpFunctionComment::from_owned) + .collect(); + *count = raw_comments.len(); + let raw_comments_ptr = Box::into_raw(raw_comments); + raw_comments_ptr as *mut BNWarpFunctionComment +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFunctionsEqual( + function_a: *mut BNWARPFunction, + function_b: *mut BNWARPFunction, +) -> bool { + // We do not own function so we should not drop. + let function_a = ManuallyDrop::new(Arc::from_raw(function_a)); + // We do not own function so we should not drop. + let function_b = ManuallyDrop::new(Arc::from_raw(function_b)); + function_a.eq(&function_b) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewFunctionReference( + function: *mut BNWARPFunction, +) -> *mut BNWARPFunction { + Arc::increment_strong_count(function); + function +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeFunctionReference(function: *mut BNWARPFunction) { + if function.is_null() { + return; + } + Arc::decrement_strong_count(function); +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeFunctionList(functions: *mut *mut BNWARPFunction, count: usize) { + let functions_ptr = std::ptr::slice_from_raw_parts_mut(functions, count); + let functions = Box::from_raw(functions_ptr); + for function in functions { + // NOTE: The functions themselves should also be arc. + BNWARPFreeFunctionReference(function); + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeFunctionCommentList( + comments: *mut BNWarpFunctionComment, + count: usize, +) { + let comments_ptr = std::ptr::slice_from_raw_parts_mut(comments, count); + let comments = Box::from_raw(comments_ptr); + for comment in &comments { + BNWarpFunctionComment::free_raw(comment) + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeConstraintList( + constraints: *mut BNWARPConstraint, + count: usize, +) { + let constraints_ptr = std::ptr::slice_from_raw_parts_mut(constraints, count); + let _constraints = unsafe { Box::from_raw(constraints_ptr) }; +} diff --git a/plugins/warp/src/plugin/file.rs b/plugins/warp/src/plugin/file.rs new file mode 100644 index 00000000..ec61bd78 --- /dev/null +++ b/plugins/warp/src/plugin/file.rs @@ -0,0 +1,41 @@ +use crate::report::ReportGenerator; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; + +pub struct ShowFileReport; + +impl Command for ShowFileReport { + fn action(&self, view: &BinaryView) { + let view = view.to_owned(); + std::thread::spawn(move || { + let Some(path) = + binaryninja::interaction::get_open_filename_input("Select file to show", "*.warp") + else { + return; + }; + + let Ok(bytes) = std::fs::read(&path) else { + log::error!("Failed to read file: {:?}", path); + return; + }; + + let Some(file) = warp::WarpFile::from_bytes(&bytes) else { + log::error!("Failed to parse file: {:?}", path); + return; + }; + + let report_generator = ReportGenerator::new(); + if let Some(html_string) = report_generator.html_report(&file) { + view.show_html_report( + &format!("WARP File: {}", path.to_string_lossy()), + html_string.as_str(), + "", + ); + } + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/find.rs b/plugins/warp/src/plugin/find.rs deleted file mode 100644 index accc015d..00000000 --- a/plugins/warp/src/plugin/find.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::cache::try_cached_function_guid; -use binaryninja::binary_view::{BinaryView, BinaryViewExt}; -use binaryninja::command::Command; -use binaryninja::function::Function as BNFunction; -use binaryninja::rc::Guard as BNGuard; -use rayon::prelude::*; -use std::thread; -use warp::signature::function::FunctionGUID; - -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::background_task::BackgroundTask::new( - &format!("Searching functions for GUID... {}", searched_guid), - false, - ); - - // Only run this for functions which have already generated a GUID. - let matched = funcs - .par_iter() - .filter(|func| { - try_cached_function_guid(func).is_some_and(|guid| guid == searched_guid) - }) - .collect::<Vec<BNGuard<BNFunction>>>(); - - if matched.is_empty() { - log::info!("No matches found for GUID... {}", searched_guid); - } else { - 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/function.rs b/plugins/warp/src/plugin/function.rs new file mode 100644 index 00000000..130e8582 --- /dev/null +++ b/plugins/warp/src/plugin/function.rs @@ -0,0 +1,133 @@ +use crate::cache::{cached_function_guid, try_cached_function_guid}; +use crate::{get_warp_include_tag_type, INCLUDE_TAG_NAME}; +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::command::{Command, FunctionCommand}; +use binaryninja::function::Function; +use binaryninja::rc::Guard; +use rayon::iter::ParallelIterator; +use std::thread; +use warp::signature::function::FunctionGUID; + +pub struct IncludeFunction; + +impl FunctionCommand for IncludeFunction { + fn action(&self, view: &BinaryView, func: &Function) { + let sym_name = func.symbol().short_name(); + let sym_name_str = sym_name.to_string_lossy(); + let should_add_tag = func.function_tags(None, Some(INCLUDE_TAG_NAME)).is_empty(); + let insert_tag_type = get_warp_include_tag_type(view); + match should_add_tag { + true => { + log::info!( + "Including selected function '{}' at 0x{:x}", + sym_name_str, + func.start() + ); + func.add_tag(&insert_tag_type, "", None, false, None); + } + false => { + log::info!( + "Removing included function '{}' at 0x{:x}", + sym_name_str, + func.start() + ); + func.remove_tags_of_type(&insert_tag_type, None, false, None); + } + } + } + + fn valid(&self, _view: &BinaryView, _func: &Function) -> bool { + // TODO: Only allow if the function is named? + true + } +} + +pub struct CopyFunctionGUID; + +impl FunctionCommand for CopyFunctionGUID { + fn action(&self, _view: &BinaryView, func: &Function) { + let Ok(lifted_il) = func.lifted_il() else { + log::error!("Could not get lifted il for copied function"); + return; + }; + let guid = cached_function_guid(func, &lifted_il); + log::info!( + "Function GUID for {:?}... {}", + func.symbol().short_name(), + guid + ); + if let Ok(mut clipboard) = arboard::Clipboard::new() { + let _ = clipboard.set_text(guid.to_string()); + } + } + + fn valid(&self, _view: &BinaryView, _func: &Function) -> bool { + true + } +} + +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(); + let view = view.to_owned(); + thread::spawn(move || { + let background_task = BackgroundTask::new( + &format!("Searching functions for GUID... {}", searched_guid), + false, + ); + + // Only run this for functions which have already generated a GUID. + let matched: Vec<Guard<Function>> = funcs + .par_iter() + .filter(|func| { + try_cached_function_guid(func).is_some_and(|guid| guid == searched_guid) + }) + .collect(); + + if matched.is_empty() { + log::info!("No matches found for GUID... {}", searched_guid); + } else { + for func in &matched { + // Also navigate the user, as that is probably what they want. + if matched.len() == 1 { + let current_view = view.file().current_view(); + if view + .file() + .navigate_to(¤t_view, func.start()) + .is_err() + { + log::error!( + "Failed to navigate to found function 0x{:0x} in view {}", + func.start(), + current_view + ); + } + } + 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/load.rs b/plugins/warp/src/plugin/load.rs index c86a16cb..c0319ea5 100644 --- a/plugins/warp/src/plugin/load.rs +++ b/plugins/warp/src/plugin/load.rs @@ -1,54 +1,162 @@ -use crate::matcher::{Matcher, PlatformID, PLAT_MATCHER_CACHE}; +use crate::cache::container::add_cached_container; +use crate::container::disk::{DiskContainer, DiskContainerSource}; +use crate::container::{ContainerError, SourcePath}; +use crate::convert::platform_to_target; +use crate::plugin::workflow::run_matcher; use binaryninja::binary_view::{BinaryView, BinaryViewExt}; use binaryninja::command::Command; +use binaryninja::interaction::{ + show_message_box, Form, FormInputField, MessageBoxButtonResult, MessageBoxButtonSet, + MessageBoxIcon, +}; +use binaryninja::rc::Ref; +use std::collections::HashMap; +use std::path::PathBuf; +use std::thread; +use warp::WarpFile; + +pub struct LoadFileField; + +impl LoadFileField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "File Path".to_string(), + // TODO: This is called extension but is really a filter. + extension: Some("*.warp".to_string()), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option<PathBuf> { + let field = form.get_field_with_name("File Path")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct RunMatcherField; + +impl RunMatcherField { + pub fn field() -> FormInputField { + FormInputField::Choice { + prompt: "Rerun Initial Matcher".to_string(), + choices: vec!["No".to_string(), "Yes".to_string()], + default: Some(1), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<bool> { + let field = form.get_field_with_name("Rerun Initial Matcher")?; + let field_value = field.try_value_index()?; + match field_value { + 1 => Some(true), + _ => Some(false), + } + } +} + pub struct LoadSignatureFile; -impl Command for LoadSignatureFile { - fn action(&self, view: &BinaryView) { - let Some(platform) = view.default_platform() else { - log::error!("Default platform must be set to load signature!"); - return; - }; +impl LoadSignatureFile { + pub fn read_file( + view: &BinaryView, + path: SourcePath, + ) -> Result<WarpFile<'static>, ContainerError> { + let contents = std::fs::read(&path).map_err(|e| ContainerError::FailedIO(e.kind()))?; + let mut file = WarpFile::from_owned_bytes(contents).ok_or( + ContainerError::CorruptedData("file data failed to validate"), + )?; - // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API. - // If we did need to load signature files from a project than this would need to change. - let Some(file) = rfd::FileDialog::new() - .add_filter("Signature Files", &["sbin"]) - .set_file_name(format!("{}.sbin", view.file().filename())) - .pick_file() - else { - return; - }; + let view_target = view + .default_platform() + .map(|p| platform_to_target(&p)) + .unwrap_or_default(); + let file_has_target = file + .chunks + .iter() + .find(|c| c.header.target == view_target) + .is_some(); + + if !file_has_target { + // File does not contain a view target, alert user if they would like to override the file chunks to the view target. + let text = format!( + "Attempting to load WARP file with no target `{:?}`, continue loading anyways?", + &view_target + ); + let res = show_message_box( + "Override file target?", + &text, + MessageBoxButtonSet::YesNoButtonSet, + MessageBoxIcon::WarningIcon, + ); + if res != MessageBoxButtonResult::YesButton { + return Err(ContainerError::CorruptedData( + "User does not want to load file", + )); + } + + // Take all the chunks and convert them to the target, so we load them. + // If we do not do this, the user will be surprised when they get no new matches. + for chunk in &mut file.chunks { + chunk.header.target = view_target.clone(); + } + } - let Ok(data) = std::fs::read(&file) else { - log::error!("Could not read signature file: {:?}", file); + Ok(file) + } + + pub fn execute(view: Ref<BinaryView>) { + let mut form = Form::new("Load Signature File"); + form.add_field(LoadFileField::field()); + // let fd_field = FileDataKindField::default(); + // form.add_field(fd_field.to_field()); + form.add_field(RunMatcherField::field()); + if !form.prompt() { return; - }; + } - let Some(data) = warp::signature::Data::from_bytes(&data) else { - log::error!("Could not get data from signature file: {:?}", file); + let Some(file_path) = LoadFileField::from_form(&form) else { return; }; + // TODO: Decide what to pull using the file data kind. + // let _file_data_kind = FileDataKindField::from_form(&form).unwrap_or_default(); + let rerun_matcher = RunMatcherField::from_form(&form).unwrap_or(false); + + let source_file_path = SourcePath::new(file_path.clone()); - let new_matcher = Matcher::from_data(data); - log::info!( - "Loading signature file with {} functions and {} types...", - new_matcher.functions.len(), - new_matcher.types.len() - ); - let platform_id = PlatformID::from(platform.as_ref()); - let matcher_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default); - match matcher_cache.get_mut(&platform_id) { - Some(mut matcher) => matcher.extend_with_matcher(new_matcher), - None => { - // We still must uphold `from_platform` in case we are running this before the matcher workflow - // is kicked off. Other-wise we only will have the `new_matcher` data. - let mut matcher = Matcher::from_platform(platform); - matcher.extend_with_matcher(new_matcher); - matcher_cache.insert(platform_id, matcher); + let file = match LoadSignatureFile::read_file(&view, source_file_path.clone()) { + Ok(file) => file, + Err(e) => { + log::error!("Failed to read signature file: {}", e); + return; } + }; + + let container_source = DiskContainerSource::new(source_file_path.clone(), file); + log::info!("Loading container source: '{}'", container_source.path); + let mut map = HashMap::new(); + map.insert(source_file_path.to_source_id(), container_source); + let container = DiskContainer::new("Loaded signatures".to_string(), map); + // TODO: See notes in the matcher about doing this, we really need to load it into an existing container. + add_cached_container(container); + + if rerun_matcher { + thread::spawn(move || { + run_matcher(&view); + }); } } +} + +impl Command for LoadSignatureFile { + fn action(&self, view: &BinaryView) { + let view = view.to_owned(); + thread::spawn(move || { + LoadSignatureFile::execute(view); + }); + } fn valid(&self, _view: &BinaryView) -> bool { true diff --git a/plugins/warp/src/plugin/project.rs b/plugins/warp/src/plugin/project.rs new file mode 100644 index 00000000..ea5a5600 --- /dev/null +++ b/plugins/warp/src/plugin/project.rs @@ -0,0 +1,132 @@ +use binaryninja::background_task::BackgroundTask; +use binaryninja::command::ProjectCommand; +use binaryninja::interaction::{Form, FormInputField}; +use binaryninja::project::Project; +use binaryninja::rc::Ref; +use regex::Regex; +use std::thread; +use std::time::Instant; + +use crate::processor::{ + new_processing_state_background_thread, FileDataKindField, FileFilterField, WarpFileProcessor, +}; +use crate::report::{ReportGenerator, ReportKindField}; + +pub struct CreateSignaturesForm { + form: Form, +} + +impl CreateSignaturesForm { + pub fn new(_project: &Project) -> CreateSignaturesForm { + let mut form = Form::new("Create Signature File"); + form.add_field(Self::file_data_field()); + form.add_field(Self::file_filter_field()); + form.add_field(Self::generated_report_field()); + // TODO: Threads (we run the analysis in the background) + Self { form } + } + + pub fn file_data_field() -> FormInputField { + FileDataKindField::default().to_field() + } + + pub fn file_data_kind(&self) -> FileDataKindField { + FileDataKindField::from_form(&self.form).unwrap_or_default() + } + + pub fn file_filter_field() -> FormInputField { + FileFilterField::to_field() + } + + pub fn file_filter(&self) -> Option<Regex> { + FileFilterField::from_form(&self.form) + } + + pub fn generated_report_field() -> FormInputField { + ReportKindField::default().to_field() + } + + pub fn generated_report_kind(&self) -> ReportKindField { + ReportKindField::from_form(&self.form).unwrap_or_default() + } + + pub fn prompt(&mut self) -> bool { + self.form.prompt() + } +} + +pub struct CreateSignatures; + +impl CreateSignatures { + pub fn execute(project: Ref<Project>) { + let mut form = CreateSignaturesForm::new(&project); + if !form.prompt() { + return; + } + let file_data_kind = form.file_data_kind(); + let report_kind = form.generated_report_kind(); + + let mut processor = WarpFileProcessor::new().with_file_data(file_data_kind); + + // This thread will show the state in a background task. + let background_task = BackgroundTask::new("Processing started...", true); + new_processing_state_background_thread(background_task.clone(), processor.state()); + + if let Some(filter) = form.file_filter() { + processor = processor.with_file_filter(filter); + } + + let start = Instant::now(); + match processor.process_project(&project) { + Ok(warp_file) => { + // Print the processor string into the description of the file, so we know how it was generated. + let processor_str = format!("{:#?}", &processor); + + // TODO: File name needs to be configurable. + if project + .create_file( + &warp_file.to_bytes(), + None, + "generated.warp", + &processor_str, + ) + .is_err() + { + log::error!("Failed to create project file!"); + } + + let report = ReportGenerator::new(); + if let Some(generated) = report.report(&report_kind, &warp_file) { + let ext = report.report_extension(&report_kind).unwrap_or_default(); + let file_name = format!("report.{}", ext); + if project + .create_file(&generated.into_bytes(), None, &file_name, "Warp file") + .is_err() + { + log::error!("Failed to create project file!"); + } + } + } + Err(e) => { + log::error!("Failed to process project: {}", e); + } + } + log::info!("Processing project files took: {:?}", start.elapsed()); + + // Tells the processing state thread to finish. + background_task.finish(); + } +} + +impl ProjectCommand for CreateSignatures { + fn action(&self, project: &Project) { + let project = project.to_owned(); + thread::spawn(move || { + CreateSignatures::execute(project); + }); + } + + fn valid(&self, _view: &Project) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/render_layer.rs b/plugins/warp/src/plugin/render_layer.rs index cdac71f2..d7984303 100644 --- a/plugins/warp/src/plugin/render_layer.rs +++ b/plugins/warp/src/plugin/render_layer.rs @@ -1,56 +1,94 @@ -use crate::{is_blacklisted_instruction, is_variant_instruction, relocatable_regions}; +use crate::{ + is_blacklisted_instruction, is_computed_variant_instruction, is_variant_instruction, + relocatable_regions, +}; use binaryninja::basic_block::BasicBlock; use binaryninja::disassembly::DisassemblyTextLine; +use binaryninja::flowgraph::FlowGraph; use binaryninja::function::{HighlightColor, HighlightStandardColor, NativeBlock}; -use binaryninja::low_level_il::instruction::LowLevelInstructionIndex; +use binaryninja::low_level_il::LowLevelILRegularFunction; use binaryninja::render_layer::{register_render_layer, RenderLayer}; -pub struct HighlightRenderLayer {} +// TODO: Add a render layer to show basic block GUID's? +// TODO: Add a render layer to show constraints for current function? + +pub struct HighlightRenderLayer { + blacklist: HighlightColor, + variant: HighlightColor, + computed_variant: HighlightColor, +} impl HighlightRenderLayer { pub fn register() { register_render_layer( "WARP Highlight Layer", - HighlightRenderLayer {}, + // TODO: Make the highlight colors configurable. + HighlightRenderLayer { + blacklist: HighlightColor::StandardHighlightColor { + color: HighlightStandardColor::BlackHighlightColor, + alpha: 155, + }, + variant: HighlightColor::StandardHighlightColor { + color: HighlightStandardColor::RedHighlightColor, + alpha: 155, + }, + computed_variant: HighlightColor::StandardHighlightColor { + color: HighlightStandardColor::OrangeHighlightColor, + alpha: 155, + }, + }, Default::default(), ); } + + /// Highlights the lines that are variant or blacklisted. + pub fn highlight_lines( + &self, + lifted_il: &LowLevelILRegularFunction, + llil: &LowLevelILRegularFunction, + lines: &mut [DisassemblyTextLine], + ) { + let relocatable_regions = relocatable_regions(&lifted_il.function().view()); + for line in lines { + // We use address here instead of index since it's more reliable for other IL's. + if let Some(lifted_il_instr) = lifted_il.instruction_at(line.address) { + if is_blacklisted_instruction(&lifted_il_instr) { + line.highlight = self.blacklist; + } else if is_variant_instruction(&relocatable_regions, &lifted_il_instr) { + line.highlight = self.variant; + } + } + + if let Some(llil_instr) = llil.instruction_at(line.address) { + if is_computed_variant_instruction(&relocatable_regions, &llil_instr) { + line.highlight = self.computed_variant; + } + } + } + } } impl RenderLayer for HighlightRenderLayer { - fn apply_to_llil_block( + fn apply_to_flow_graph(&self, graph: &mut FlowGraph) { + if let (Some(lifted_il), Some(llil)) = (graph.lifted_il(), graph.low_level_il()) { + for node in &graph.nodes() { + let mut new_lines = node.lines().to_vec(); + self.highlight_lines(&lifted_il, &llil, &mut new_lines); + node.set_lines(new_lines); + } + } + } + + fn apply_to_block( &self, block: &BasicBlock<NativeBlock>, mut lines: Vec<DisassemblyTextLine>, ) -> Vec<DisassemblyTextLine> { - // Highlight any LLIL instruction that will be masked by WARP. + // Highlight any instruction that WARP will mask. let function = block.function(); - // TODO: We might need to make relocatable regions configurable. - let relocatable_regions = relocatable_regions(&function.view()); - let Ok(llil) = function.low_level_il() else { - // Don't even think this is possible but _shrug_. - return lines; - }; - - for line in &mut lines { - let llil_instr_idx = LowLevelInstructionIndex(line.instruction_index); - if let Some(llil_instr) = llil.instruction_from_index(llil_instr_idx) { - if is_blacklisted_instruction(&llil_instr) { - // We have a blacklisted instruction, highlight it as orange! - line.highlight = HighlightColor::StandardHighlightColor { - color: HighlightStandardColor::OrangeHighlightColor, - alpha: 155, - }; - } else if is_variant_instruction(&relocatable_regions, &llil_instr) { - // We have a variant instruction, highlight it as red! - line.highlight = HighlightColor::StandardHighlightColor { - color: HighlightStandardColor::RedHighlightColor, - alpha: 155, - }; - } - } + if let (Ok(lifted_il), Ok(llil)) = (function.lifted_il(), function.low_level_il()) { + self.highlight_lines(&lifted_il, &llil, &mut lines); } - lines } } diff --git a/plugins/warp/src/plugin/settings.rs b/plugins/warp/src/plugin/settings.rs new file mode 100644 index 00000000..6cd2e0cc --- /dev/null +++ b/plugins/warp/src/plugin/settings.rs @@ -0,0 +1,129 @@ +use binaryninja::settings::Settings as BNSettings; +use serde_json::json; +use std::string::ToString; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PluginSettings { + /// Whether to load bundled WARP files on startup. Turn this off if you want to manually load them. + /// + /// This is set to [PluginSettings::LOAD_BUNDLED_FILES_DEFAULT] by default. + pub load_bundled_files: bool, + /// Whether to load user WARP files on startup. Turn this off if you want to manually load them. + /// + /// This is set to [PluginSettings::LOAD_USER_FILES_DEFAULT] by default. + pub load_user_files: bool, + /// The WARP server to use. + /// + /// This is set to [PluginSettings::SERVER_URL_DEFAULT] by default. + pub server_url: String, + /// The API key to use for the selected WARP server, if not specified, you will be unable to push data and may be rate-limited. + /// + /// This is set to [PluginSettings::SERVER_API_KEY_DEFAULT] by default. + pub server_api_key: String, + /// Whether to allow networked WARP requests. Turning this off will not disable local WARP functionality. + /// + /// This is set to [PluginSettings::ENABLE_SERVER_DEFAULT] by default. + pub enable_server: bool, +} + +impl PluginSettings { + pub const LOAD_BUNDLED_FILES_DEFAULT: bool = true; + pub const LOAD_BUNDLED_FILES_SETTING: &'static str = "analysis.warp.loadBundledFiles"; + pub const LOAD_USER_FILES_DEFAULT: bool = true; + pub const LOAD_USER_FILES_SETTING: &'static str = "analysis.warp.loadUserFiles"; + pub const SERVER_URL_DEFAULT: &'static str = "https://warp.binary.ninja"; + pub const SERVER_URL_SETTING: &'static str = "analysis.warp.serverUrl"; + pub const SERVER_API_KEY_DEFAULT: &'static str = ""; + pub const SERVER_API_KEY_SETTING: &'static str = "analysis.warp.serverApiKey"; + pub const ENABLE_SERVER_DEFAULT: bool = true; + pub const ENABLE_SERVER_SETTING: &'static str = "network.enableWARP"; + + pub fn register(bn_settings: &mut BNSettings) { + let load_bundled_files_prop = json!({ + "title" : "Load Bundled Files", + "type" : "boolean", + "default" : Self::LOAD_BUNDLED_FILES_DEFAULT, + "description" : "Whether to load bundled WARP files on startup. Turn this off if you want to manually load them.", + "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + }); + bn_settings.register_setting_json( + Self::LOAD_BUNDLED_FILES_SETTING, + &load_bundled_files_prop.to_string(), + ); + let load_user_files_prop = json!({ + "title" : "Load User Files", + "type" : "boolean", + "default" : Self::LOAD_USER_FILES_DEFAULT, + "description" : "Whether to load user WARP files on startup. Turn this off if you want to manually load them.", + "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + }); + bn_settings.register_setting_json( + Self::LOAD_USER_FILES_SETTING, + &load_user_files_prop.to_string(), + ); + let server_url_prop = json!({ + "title" : "Server URL", + "type" : "string", + "default" : Self::SERVER_URL_DEFAULT, + "description" : "The WARP server to use.", + "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + }); + bn_settings.register_setting_json(Self::SERVER_URL_SETTING, &server_url_prop.to_string()); + let server_api_key_prop = json!({ + "title" : "Server API Key", + "type" : "string", + "default" : Self::SERVER_API_KEY_DEFAULT, + "description" : "The API key to use for the selected WARP server, if not specified you will be unable to push data, and may be rate limited.", + "ignore" : ["SettingsProjectScope", "SettingsResourceScope"], + "hidden": true + }); + bn_settings.register_setting_json( + Self::SERVER_API_KEY_SETTING, + &server_api_key_prop.to_string(), + ); + let server_enabled_prop = json!({ + "title" : "Enable WARP", + "type" : "boolean", + "default" : Self::ENABLE_SERVER_DEFAULT, + "description" : "Whether or not to allow networked WARP requests. Turning this off will not disable local WARP functionality.", + "ignore" : ["SettingsProjectScope", "SettingsResourceScope"] + }); + bn_settings.register_setting_json( + Self::ENABLE_SERVER_SETTING, + &server_enabled_prop.to_string(), + ); + } + + /// Retrieve plugin settings from [`BNSettings`]. + pub fn from_settings(bn_settings: &BNSettings) -> Self { + let mut settings = PluginSettings::default(); + if bn_settings.contains(Self::LOAD_BUNDLED_FILES_SETTING) { + settings.load_bundled_files = bn_settings.get_bool(Self::LOAD_BUNDLED_FILES_SETTING); + } + if bn_settings.contains(Self::LOAD_USER_FILES_SETTING) { + settings.load_user_files = bn_settings.get_bool(Self::LOAD_USER_FILES_SETTING); + } + if bn_settings.contains(Self::SERVER_URL_SETTING) { + settings.server_url = bn_settings.get_string(Self::SERVER_URL_SETTING); + } + if bn_settings.contains(Self::SERVER_API_KEY_SETTING) { + settings.server_url = bn_settings.get_string(Self::SERVER_API_KEY_SETTING); + } + if bn_settings.contains(Self::ENABLE_SERVER_SETTING) { + settings.enable_server = bn_settings.get_bool(Self::ENABLE_SERVER_SETTING); + } + settings + } +} + +impl Default for PluginSettings { + fn default() -> Self { + Self { + load_bundled_files: PluginSettings::LOAD_BUNDLED_FILES_DEFAULT, + load_user_files: PluginSettings::LOAD_USER_FILES_DEFAULT, + server_url: PluginSettings::SERVER_URL_DEFAULT.to_string(), + server_api_key: PluginSettings::SERVER_API_KEY_DEFAULT.to_string(), + enable_server: PluginSettings::ENABLE_SERVER_DEFAULT, + } + } +} diff --git a/plugins/warp/src/plugin/types.rs b/plugins/warp/src/plugin/types.rs deleted file mode 100644 index 41e03cd3..00000000 --- a/plugins/warp/src/plugin/types.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::convert::to_bn_type; -use binaryninja::binary_view::{BinaryView, BinaryViewExt}; -use binaryninja::command::Command; -use std::time::Instant; - -pub struct LoadTypes; - -impl Command for LoadTypes { - fn action(&self, view: &BinaryView) { - // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API. - // If we did need to load signature files from a project than this would need to change. - let Some(file) = rfd::FileDialog::new() - .add_filter("Signature Files", &["sbin"]) - .set_file_name(format!("{}.sbin", view.file().filename())) - .pick_file() - 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 Some(arch) = view.default_arch() else { - log::error!("Could not get default architecture"); - return; - }; - - let view = view.to_owned(); - std::thread::spawn(move || { - let background_task = binaryninja::background_task::BackgroundTask::new( - &format!("Applying {} types...", data.types.len()), - true, - ); - - 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()); - 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 index f5763116..487fccfe 100644 --- a/plugins/warp/src/plugin/workflow.rs +++ b/plugins/warp/src/plugin/workflow.rs @@ -1,16 +1,40 @@ -use crate::cache::cached_function_guid; -use crate::matcher::cached_function_matcher; +use crate::cache::container::for_cached_containers; +use crate::cache::{ + cached_function_guid, insert_cached_function_match, try_cached_function_guid, + try_cached_function_match, +}; +use crate::convert::{ + comment_to_bn_comment, platform_to_target, to_bn_symbol_at_address, to_bn_type, +}; +use crate::matcher::{Matcher, MatcherSettings}; +use crate::{get_warp_tag_type, relocatable_regions}; use binaryninja::background_task::BackgroundTask; use binaryninja::binary_view::{BinaryView, BinaryViewExt}; use binaryninja::command::Command; +use binaryninja::settings::{QueryOptions, Settings}; use binaryninja::workflow::{Activity, AnalysisContext, Workflow}; +use itertools::Itertools; +use std::collections::HashMap; use std::time::Instant; +use warp::signature::function::{Function, FunctionGUID}; +use warp::target::Target; + +pub const APPLY_ACTIVITY_NAME: &str = "analysis.warp.apply"; +const APPLY_ACTIVITY_CONFIG: &str = r#"{ + "name": "analysis.warp.apply", + "title" : "WARP Apply Matched", + "description": "This analysis step applies WARP info to matched functions...", + "eligibility": { + "auto": {}, + "runOnce": false + } +}"#; pub const MATCHER_ACTIVITY_NAME: &str = "analysis.warp.matcher"; const MATCHER_ACTIVITY_CONFIG: &str = r#"{ "name": "analysis.warp.matcher", "title" : "WARP Matcher", - "description": "This analysis step applies WARP info to matched functions...", + "description": "This analysis step attempts to find matching WARP functions after the initial analysis is complete...", "eligibility": { "auto": {}, "runOnce": true @@ -24,7 +48,7 @@ const GUID_ACTIVITY_CONFIG: &str = r#"{ "description": "This analysis step generates the GUID for all analyzed functions...", "eligibility": { "auto": {}, - "runOnce": true + "runOnce": false } }"#; @@ -33,19 +57,8 @@ pub struct RunMatcher; impl Command for RunMatcher { fn action(&self, view: &BinaryView) { let view = view.to_owned(); - // TODO: Check to see if the GUID cache is empty and ask the user if they want to regenerate the guids. std::thread::spawn(move || { - let undo_id = view.file().begin_undo_actions(true); - let background_task = BackgroundTask::new("Matching on functions...", false); - let start = Instant::now(); - view.functions() - .iter() - .for_each(|function| cached_function_matcher(&function)); - log::info!("Function matching took {:?}", start.elapsed()); - background_task.finish(); - view.file().commit_undo_actions(&undo_id); - // Now we want to trigger re-analysis. - view.update_analysis(); + run_matcher(&view); }); } @@ -54,44 +67,157 @@ impl Command for RunMatcher { } } +pub fn run_matcher(view: &BinaryView) { + // Alert the user if we have no actual regions (one comes from the synthetic section). + let regions = relocatable_regions(view); + if regions.len() <= 1 { + log::warn!( + "No relocatable regions found, for best results please define sections for the binary!" + ); + } + + // Then we want to actually find matching functions. + let background_task = BackgroundTask::new("Matching on WARP functions...", true); + let start = Instant::now(); + + // Build matcher + let view_settings = Settings::new(); + let mut query_opts = QueryOptions::new_with_view(view); + let matcher_settings = MatcherSettings::from_settings(&view_settings, &mut query_opts); + let matcher = Matcher::new(matcher_settings); + + // TODO: Par iter this? Using dashmap + let functions_by_target_and_guid: HashMap<(FunctionGUID, Target), Vec<_>> = view + .functions() + .iter() + .filter_map(|f| { + let guid = try_cached_function_guid(&f)?; + let target = platform_to_target(&f.platform()); + Some(((guid, target), f.to_owned())) + }) + .into_group_map(); + + // TODO: Par iter this? Using dashmap + let guids_by_target: HashMap<Target, Vec<FunctionGUID>> = functions_by_target_and_guid + .keys() + .map(|(guid, target)| (target.clone(), *guid)) + .into_group_map(); + + // TODO: Target gets cloned a lot. + // TODO: Containers might both match on the same function. What should we do? + for_cached_containers(|container| { + if background_task.is_cancelled() { + return; + } + + for (target, guids) in &guids_by_target { + let function_guid_with_sources = container + .sources_with_function_guids(target, guids) + .unwrap_or_default(); + + for (guid, sources) in &function_guid_with_sources { + let matched_functions: Vec<Function> = sources + .iter() + .flat_map(|source| { + container + .functions_with_guid(target, source, guid) + .unwrap_or_default() + }) + .collect(); + + let functions = functions_by_target_and_guid + .get(&(*guid, target.clone())) + .expect("Function guid not found"); + + for function in functions { + // Match on all the possible functions + if let Some(matched_function) = + matcher.match_function_from_constraints(function, &matched_functions) + { + // We were able to find a match, add it to the match cache and then mark the function + // as requiring updates; this is so that we know about it in the applier activity. + insert_cached_function_match(function, Some(matched_function.clone())); + } + } + } + } + }); + + if background_task.is_cancelled() { + log::info!("Matcher was cancelled by user, you may run it again by running the 'Run Matcher' command."); + } + + log::info!("Function matching took {:?}", start.elapsed()); + background_task.finish(); + + // Now we want to trigger re-analysis. + view.update_analysis(); +} + pub fn insert_workflow() { + // "Hey look, it's a plier" ~ Josh 2025 + let apply_activity = |ctx: &AnalysisContext| { + let view = ctx.view(); + let function = ctx.function(); + if let Some(matched_function) = try_cached_function_match(&function) { + view.define_auto_symbol(&to_bn_symbol_at_address( + &view, + &matched_function.symbol, + function.symbol().address(), + )); + if let Some(func_ty) = &matched_function.ty { + function.set_auto_type(&to_bn_type(&function.arch(), func_ty)); + } + // TODO: How to clear the comments? They are just persisted. + // TODO: Also they generate an undo action, i hate implicit undo actions so much. + for comment in matched_function.comments { + let bn_comment = comment_to_bn_comment(&function, comment); + function.set_comment_at(bn_comment.addr, &bn_comment.comment); + } + function.add_tag( + &get_warp_tag_type(&view), + &matched_function.guid.to_string(), + None, + false, + None, + ); + } + }; + let matcher_activity = |ctx: &AnalysisContext| { let view = ctx.view(); - let undo_id = view.file().begin_undo_actions(true); - let background_task = BackgroundTask::new("Matching on functions...", false); - let start = Instant::now(); - view.functions() - .iter() - .for_each(|function| cached_function_matcher(&function)); - log::info!("Function matching took {:?}", start.elapsed()); - background_task.finish(); - view.file().commit_undo_actions(&undo_id); - // Now we want to trigger re-analysis. - view.update_analysis(); + run_matcher(&view); }; let guid_activity = |ctx: &AnalysisContext| { let function = ctx.function(); - // TODO: Returning RegularNonSSA means we cant modify the il (the lifting code was written just for lifted il, that needs to be fixed) - if let Some(llil) = unsafe { ctx.llil_function() } { - cached_function_guid(&function, &llil); + if let Some(lifted_il) = unsafe { ctx.lifted_il_function() } { + cached_function_guid(&function, &lifted_il); } }; let old_function_meta_workflow = Workflow::instance("core.function.metaAnalysis"); let function_meta_workflow = old_function_meta_workflow.clone_to("core.function.metaAnalysis"); let guid_activity = Activity::new_with_action(GUID_ACTIVITY_CONFIG, guid_activity); + let apply_activity = Activity::new_with_action(APPLY_ACTIVITY_CONFIG, apply_activity); function_meta_workflow .register_activity(&guid_activity) .unwrap(); + // Because we are going to impact analysis with application we must make sure the function update is triggered to continue to update analysis. + // TODO: need to ask why i cant do core.function.update like in the rtti plugin. + function_meta_workflow + .register_activity_with_subactivities::<Vec<String>>(&apply_activity, vec![]) + .unwrap(); function_meta_workflow.insert("core.function.runFunctionRecognizers", [GUID_ACTIVITY_NAME]); + function_meta_workflow.insert("core.function.generateMediumLevelIL", [APPLY_ACTIVITY_NAME]); function_meta_workflow.register().unwrap(); let old_module_meta_workflow = Workflow::instance("core.module.metaAnalysis"); let module_meta_workflow = old_module_meta_workflow.clone_to("core.module.metaAnalysis"); let matcher_activity = Activity::new_with_action(MATCHER_ACTIVITY_CONFIG, matcher_activity); + // Matcher activity must have core.module.update as subactivity otherwise analysis will sometimes never retrigger. module_meta_workflow - .register_activity(&matcher_activity) + .register_activity_with_subactivities(&matcher_activity, vec!["core.module.update"]) .unwrap(); module_meta_workflow.insert( "core.module.deleteUnusedAutoFunctions", diff --git a/plugins/warp/src/processor.rs b/plugins/warp/src/processor.rs new file mode 100644 index 00000000..4b973753 --- /dev/null +++ b/plugins/warp/src/processor.rs @@ -0,0 +1,829 @@ +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::atomic::{AtomicBool, AtomicUsize}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use ar::Archive; +use dashmap::DashMap; +use rayon::iter::IntoParallelIterator; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use regex::Regex; +use serde_json::{json, Value}; +use tempdir::TempDir; +use thiserror::Error; +use walkdir::WalkDir; + +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::function::Function as BNFunction; +use binaryninja::interaction::{Form, FormInputField}; +use binaryninja::project::file::ProjectFile; +use binaryninja::project::Project; +use binaryninja::rc::{Guard, Ref}; + +use warp::chunk::{Chunk, ChunkKind, CompressionType}; +use warp::r#type::chunk::TypeChunk; +use warp::signature::chunk::SignatureChunk; +use warp::signature::function::Function; +use warp::target::Target; +use warp::{WarpFile, WarpFileHeader}; + +use crate::cache::cached_type_references; +use crate::convert::platform_to_target; +use crate::{build_function, INCLUDE_TAG_ICON, INCLUDE_TAG_NAME}; + +#[derive(Error, Debug)] +pub enum ProcessingError { + #[error("Failed to open archive: {0}")] + ArchiveOpen(std::io::Error), + + #[error("Failed to read archive entry: {0}")] + ArchiveRead(std::io::Error), + + #[error("Binary view load error: {0}")] + BinaryViewLoad(PathBuf), + + #[error("Existing data load error: {0}")] + ExistingDataLoad(PathBuf), + + #[error("Temporary directory creation failed: {0}")] + TempDirCreation(std::io::Error), + + #[error("Failed to read file: {0}")] + FileRead(std::io::Error), + + #[error("Failed to create chunk, possibly too large")] + ChunkCreationFailed, + + #[error("Failed to retrieve path to project file: {0:?}")] + NoPathToProjectFile(Ref<ProjectFile>), + + #[error("Processing state has been poisoned")] + StatePoisoned, + + #[error("Processing has been cancelled")] + Cancelled, +} + +#[derive(Debug, Clone, Default)] +pub struct FileFilterField; + +impl FileFilterField { + pub fn to_field() -> FormInputField { + FormInputField::TextLine { + prompt: "File Filter".to_string(), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option<Regex> { + let field = form.get_field_with_name("File Filter")?; + let field_value = field.try_value_string()?; + + // TODO: This is pretty absurd but whatever. + let pattern = if field_value.contains(['*', '.', '[', '(']) { + // Assume it's a regex if it contains meta-characters. + field_value + } else { + // Treat it as a substring + format!(".*{}.*", regex::escape(&field_value)) + }; + + Regex::new(&pattern).ok() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum FileDataKindField { + Symbols, + Signatures, + Types, + #[default] + All, +} + +impl FileDataKindField { + pub fn to_field(&self) -> FormInputField { + FormInputField::Choice { + prompt: "File Data".to_string(), + choices: vec![ + "Symbols".to_string(), + "Signatures".to_string(), + "Types".to_string(), + "All".to_string(), + ], + default: Some(match self { + Self::Symbols => 0, + Self::Signatures => 1, + Self::Types => 2, + Self::All => 3, + }), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("File Data")?; + let field_value = field.try_value_index()?; + match field_value { + 3 => Some(Self::All), + 2 => Some(Self::Types), + 1 => Some(Self::Signatures), + 0 => Some(Self::Symbols), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum IncludedFunctionsField { + Selected, + #[default] + Annotated, + All, +} + +impl IncludedFunctionsField { + pub fn to_field(&self) -> FormInputField { + // If the user has selected any functions, change the default value of the included functions field. + FormInputField::Choice { + prompt: "Included Functions".to_string(), + choices: vec![ + format!("Selected {}", INCLUDE_TAG_ICON), + "Annotated".to_string(), + "All".to_string(), + ], + default: Some(match self { + Self::Selected => 0, + Self::Annotated => 1, + Self::All => 2, + }), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("Included Functions")?; + let field_value = field.try_value_index()?; + match field_value { + 2 => Some(Self::All), + 1 => Some(Self::Annotated), + 0 => Some(Self::Selected), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum SaveReportToDiskField { + No, + #[default] + Yes, +} + +impl SaveReportToDiskField { + pub fn to_field(&self) -> FormInputField { + FormInputField::Checkbox { + prompt: "Save Report to Disk".to_string(), + default: Some(true), + value: false, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("Save Report to Disk")?; + let field_value = field.try_value_int()?; + match field_value { + 1 => Some(Self::Yes), + _ => Some(Self::No), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum CompressionTypeField { + None, + #[default] + Zstd, +} + +impl CompressionTypeField { + pub fn to_field(&self) -> FormInputField { + FormInputField::Choice { + prompt: "Compression Type".to_string(), + choices: vec!["None".to_string(), "Zstd".to_string()], + default: Some(match self { + Self::None => 0, + Self::Zstd => 1, + }), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("Compression Type")?; + let field_value = field.try_value_index()?; + match field_value { + 1 => Some(Self::Zstd), + _ => Some(Self::None), + } + } +} + +impl From<CompressionTypeField> for CompressionType { + fn from(field: CompressionTypeField) -> Self { + match field { + CompressionTypeField::None => CompressionType::None, + CompressionTypeField::Zstd => CompressionType::Zstd, + } + } +} + +pub fn new_processing_state_background_thread( + task: Ref<BackgroundTask>, + state: Arc<ProcessingState>, +) { + std::thread::spawn(move || { + let start = Instant::now(); + while !task.is_finished() { + std::thread::sleep(Duration::from_millis(100)); + // Check if the user wants to cancel the processing. + if task.is_cancelled() { + state.cancel(); + } + + let total = state.total_files(); + let processed = state.files_with_state(ProcessingFileState::Processed); + let unprocessed = state.files_with_state(ProcessingFileState::Unprocessed); + let analyzing = state.files_with_state(ProcessingFileState::Analyzing); + let processing = state.files_with_state(ProcessingFileState::Processing); + let completion = (processed as f64 / total as f64) * 100.0; + let elapsed = start.elapsed().as_secs_f32(); + let text = format!( + "Processing {} files... {{{}|{}|{}|{}}} ({:.2}%) [{:.2}s]", + total, unprocessed, analyzing, processing, processed, completion, elapsed + ); + task.set_progress_text(&text); + } + }); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ProcessingFileState { + /// File is yet to be processed. + Unprocessed, + /// File is being analyzed by Binary Ninja. + Analyzing, + /// File is currently generating WARP data. + /// TODO: (AtomicUsize) for the total and done functions, we can then write to it with Relaxed when processing. + Processing, + /// File is done being processed. + Processed, +} + +#[derive(Debug, Default)] +pub struct ProcessingState { + pub cancelled: AtomicBool, + pub files: DashMap<PathBuf, ProcessingFileState>, + pub total_functions: AtomicUsize, +} + +impl ProcessingState { + pub fn is_cancelled(&self) -> bool { + self.cancelled.load(Relaxed) + } + + pub fn cancel(&self) { + self.cancelled.store(true, Relaxed) + } + + pub fn increment_functions(&self) { + self.total_functions.fetch_add(1, Relaxed); + } + + pub fn total_files(&self) -> usize { + self.files.len() + } + + pub fn files_with_state(&self, state: ProcessingFileState) -> usize { + self.files.iter().filter(|f| *f.value() == state).count() + } + + pub fn set_file_state(&self, path: PathBuf, state: ProcessingFileState) { + self.files.insert(path, state); + } +} + +/// Create a new [`WarpFile`] from files, projects, and directories. +#[derive(Debug, Clone)] +pub struct WarpFileProcessor { + /// The Binary Ninja settings to use when analyzing the binaries. + analysis_settings: Value, + /// For any function without an LLIL, request analysis to be run, waiting for analysis to + /// complete to include in the analysis. + request_analysis: bool, + // TODO: Project cache path, so we save to a project instead of some temp path. + // TODO: Databases will require regenerating LLIL in some cases, so we must support generating the LLIL. + /// The path to a folder to intake and output analysis artifacts. + cache_path: Option<PathBuf>, + file_data: FileDataKindField, + included_functions: IncludedFunctionsField, + compression_type: CompressionTypeField, + /// Regex pattern used to filter out files. + file_filter: Option<Regex>, + /// Processor state, this is shareable between threads, so the processor and the consumer can + /// read / write to the state, use this if you want to show a progress indicator. + state: Arc<ProcessingState>, +} + +impl WarpFileProcessor { + pub fn new() -> Self { + Self { + analysis_settings: json!({ + "analysis.linearSweep.autorun": false, + "analysis.signatureMatcher.autorun": false, + "analysis.mode": "full", + // Disable warp when opening views. + "analysis.warp.guid": false, + "analysis.warp.matcher": false, + "analysis.warp.apply": false, + }), + request_analysis: true, + cache_path: None, + file_data: Default::default(), + included_functions: Default::default(), + compression_type: Default::default(), + file_filter: None, + state: Arc::new(ProcessingState::default()), + } + } + + /// Retrieve a thread-safe shared reference to the [`ProcessingState`]. + pub fn state(&self) -> Arc<ProcessingState> { + self.state.clone() + } + + pub fn with_analysis_settings(mut self, analysis_settings: Value) -> Self { + self.analysis_settings = analysis_settings; + self + } + + pub fn with_request_analysis(mut self, request_analysis: bool) -> Self { + self.request_analysis = request_analysis; + self + } + + pub fn with_cache_path(mut self, cache_path: PathBuf) -> Self { + self.cache_path = Some(cache_path); + self + } + + pub fn with_file_data(mut self, file_data: FileDataKindField) -> Self { + self.file_data = file_data; + self + } + + pub fn with_included_functions(mut self, included_functions: IncludedFunctionsField) -> Self { + self.included_functions = included_functions; + self + } + + pub fn with_compression_type(mut self, compression_type: CompressionTypeField) -> Self { + self.compression_type = compression_type; + self + } + + pub fn with_file_filter(mut self, file_filter: Regex) -> Self { + self.file_filter = Some(file_filter); + self + } + + pub fn file_filter(&self, path: &Path) -> bool { + match (&self.file_filter, path.to_str()) { + (Some(filter), Some(path)) => filter.is_match(path), + _ => true, + } + } + + /// Place a call to this in places to interrupt when canceled. + fn check_cancelled(&self) -> Result<(), ProcessingError> { + match self.state.is_cancelled() { + true => Err(ProcessingError::Cancelled), + false => Ok(()), + } + } + + pub fn process(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + match path.extension() { + Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => self.process_archive(path), + Some(ext) if ext == "warp" => self.process_warp_file(path), + _ if path.is_dir() => self.process_directory(&path), + // TODO: process_database? + _ => self.process_file(path), + } + } + + pub fn process_project(&self, project: &Project) -> Result<WarpFile<'static>, ProcessingError> { + let filter_project_file = |file: &Guard<ProjectFile>| { + let path = project_file_path(file); + self.file_filter(&path) + }; + + let files: Vec<_> = project + .files() + .iter() + .filter(filter_project_file) + .map(|f| f.to_owned()) + .collect(); + + // Inform the state of the new unprocessed project files. + for project_file in &files { + // NOTE: We use the on disk path here because the downstream file state uses that. + if let Some(path) = project_file.path_on_disk() { + self.state + .set_file_state(path, ProcessingFileState::Unprocessed); + } + } + + let unmerged_files: Result<Vec<_>, _> = files + .par_iter() + .map(|file| { + self.check_cancelled()?; + self.process_project_file(file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + log::error!("Project file processing error: {:?}", e); + None + } + }) + .collect(); + + let unmerged_chunks: Vec<_> = unmerged_files? + .iter() + .flat_map(|f| f.chunks.clone()) + .collect(); + let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into()); + Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks)) + } + + pub fn process_project_file( + &self, + project_file: &ProjectFile, + ) -> Result<WarpFile<'static>, ProcessingError> { + let file_name = project_file.name(); + let extension = file_name.split('.').last(); + let path = project_file + .path_on_disk() + .ok_or_else(|| ProcessingError::NoPathToProjectFile(project_file.to_owned()))?; + match extension { + Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => self.process_archive(path), + Some("warp") => self.process_warp_file(path), + _ => self.process_file(path), + } + } + + pub fn process_warp_file(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + let contents = std::fs::read(&path).map_err(ProcessingError::FileRead)?; + let file = WarpFile::from_owned_bytes(contents) + .ok_or(ProcessingError::ExistingDataLoad(path.clone())); + + // Inform the state of the new processed warp file. + self.state + .set_file_state(path, ProcessingFileState::Processed); + + file + } + + pub fn process_file(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + // Inform the state of the new analyzing file. + self.state + .set_file_state(path.clone(), ProcessingFileState::Analyzing); + + // Load the view, either from the cache or from the given path. + // Using the cache can speed up the processing, especially for larger binaries. + let settings_str = self.analysis_settings.to_string(); + let view = match &self.cache_path { + Some(cache_path) => { + // Processor is caching analysis, try and find our file in the cache. + let file_cache_path = cache_path + .join(path.file_name().unwrap()) + .with_extension("bndb"); + if file_cache_path.exists() { + // TODO: Update analysis and wait option + log::debug!("Analysis database found in cache: {:?}", file_cache_path); + binaryninja::load_with_options(&file_cache_path, true, Some(settings_str)) + } else { + log::debug!("No database found in cache: {:?}", file_cache_path); + binaryninja::load_with_options(&path, true, Some(settings_str)) + } + } + None => { + // Processor is not caching analysis + binaryninja::load_with_options(&path, true, Some(settings_str)) + } + } + .ok_or(ProcessingError::BinaryViewLoad(path.clone()))?; + + // Analysis is complete, if needed, save the database to cache. + if let Some(cache_path) = &self.cache_path { + // Before we process the view we should cache the analysis database. + // Only cache the analysis database if there has been a change. + // TODO: What if there is multiple paths with the same name? + // TODO: We need more context than just the path, likely we need a processing path stack. + let file_cache_path = cache_path + .join(path.file_name().unwrap()) + .with_extension("bndb"); + // TODO: We should also update the cache if analysis has changed! + if !view.file().is_database_backed() { + // Update the cache. + log::debug!("Saving analysis database to {:?}", file_cache_path); + if !view.file().create_database(&file_cache_path) { + // TODO: We might want to error here... + log::warn!("Failed to save analysis database to {:?}", file_cache_path); + } + } else { + log::debug!( + "Analysis database unchanged, skipping save to {:?}", + file_cache_path + ); + } + } + + // Process the view + let warp_file = self.process_view(path, &view); + // Close the view manually, see comment in [`BinaryView`]. + view.file().close(); + warp_file + } + + pub fn process_directory(&self, path: &Path) -> Result<WarpFile<'static>, ProcessingError> { + // Collect all files in the directory + let files = WalkDir::new(path) + .into_iter() + .filter_map(|e| { + let path = e.ok()?.into_path(); + if path.is_file() && self.file_filter(&path) { + Some(path) + } else { + None + } + }) + .collect::<Vec<_>>(); + + // Inform the state of the new unprocessed files. + for entry_file in &files { + self.state + .set_file_state(entry_file.clone(), ProcessingFileState::Unprocessed); + } + + // Process all the files. + let unmerged_files: Result<Vec<_>, _> = files + .into_par_iter() + .inspect(|path| log::debug!("Processing file: {:?}", path)) + .map(|path| { + self.check_cancelled()?; + self.process(path) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + log::error!("Directory file processing error: {:?}", e); + None + } + }) + .collect(); + + let unmerged_chunks: Vec<_> = unmerged_files? + .iter() + .flat_map(|f| f.chunks.clone()) + .collect(); + let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into()); + Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks)) + } + + pub fn process_archive(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + // Open the archive. + let archive_file = File::open(&path).map_err(ProcessingError::ArchiveOpen)?; + let mut archive = Archive::new(archive_file); + + // Create a temp directory to store the archive entries. + let temp_dir = TempDir::new("tmp_archive").map_err(ProcessingError::TempDirCreation)?; + + // TODO: Use the file_filter? We would need to normalize the path then. + // Iterate through the entries in the ar file and make a temp dir with them + let mut entry_files: HashSet<PathBuf> = HashSet::new(); + while let Some(entry) = archive.next_entry() { + let mut entry = entry.map_err(ProcessingError::ArchiveRead)?; + // NOTE: The entry name here may resemble a full path, on unix this is fine, but + // on Windows this will prevent a file from being created, so we "normalize" the file name. + let name = String::from_utf8_lossy(entry.header().identifier()).to_string(); + // Normalize file name for Windows compatibility + let normalized_name = name + .replace(':', "_") + .replace('/', "_") + .replace('\\', "_") + .split_whitespace() + .collect::<Vec<_>>() + .join("_"); + let output_path = temp_dir.path().join(&normalized_name); + if !entry_files.contains(&output_path) { + let mut output_file = + File::create(&output_path).map_err(ProcessingError::TempDirCreation)?; + std::io::copy(&mut entry, &mut output_file).map_err(ProcessingError::FileRead)?; + entry_files.insert(output_path); + } else { + log::debug!("Skipping already inserted entry: {}", normalized_name); + } + } + + // Inform the state of the new unprocessed files. + for entry_file in &entry_files { + self.state + .set_file_state(entry_file.clone(), ProcessingFileState::Unprocessed); + } + + // TODO: Par iter? + // Process all the entries. + let unmerged_files: Result<Vec<_>, _> = entry_files + .into_par_iter() + .inspect(|path| log::debug!("Processing entry: {:?}", path)) + .map(|path| { + self.check_cancelled()?; + self.process_file(path) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + log::error!("Archive file processing error: {:?}", e); + None + } + }) + .collect(); + + let unmerged_chunks: Vec<_> = unmerged_files? + .iter() + .flat_map(|f| f.chunks.clone()) + .collect(); + let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into()); + Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks)) + } + + pub fn process_view( + &self, + path: PathBuf, + view: &BinaryView, + ) -> Result<WarpFile<'static>, ProcessingError> { + self.state + .set_file_state(path.clone(), ProcessingFileState::Processing); + + let mut chunks = Vec::new(); + if self.file_data != FileDataKindField::Types { + let mut signature_chunks = self.create_signature_chunks(view)?; + for (target, signature_chunk) in signature_chunks.drain() { + let chunk = Chunk::new_with_target( + ChunkKind::Signature(signature_chunk), + self.compression_type.into(), + target, + ); + chunks.push(chunk) + } + } + + if self.file_data != FileDataKindField::Signatures { + chunks.push(Chunk::new( + ChunkKind::Type(self.create_type_chunk(view)?), + self.compression_type.into(), + )); + } + + self.state + .set_file_state(path, ProcessingFileState::Processed); + + Ok(WarpFile::new(WarpFileHeader::new(), chunks)) + } + + /// Create signature chunks for each unique [`Target`]. + /// + /// A [`Target`] in Binary Ninja is a [`Platform`], so we just fill in that information. + pub fn create_signature_chunks( + &self, + view: &BinaryView, + ) -> Result<HashMap<Target, SignatureChunk<'static>>, ProcessingError> { + let is_function_named = |f: &Guard<BNFunction>| { + self.included_functions == IncludedFunctionsField::All + || view.symbol_by_address(f.start()).is_some() + || f.has_user_annotations() + }; + let is_function_tagged = |f: &Guard<BNFunction>| { + self.included_functions != IncludedFunctionsField::Selected + || !f.function_tags(None, Some(INCLUDE_TAG_NAME)).is_empty() + }; + // TODO: is_function_blacklisted (use tag) + + // TODO: Move this background task to use the ProcessingState. + let view_functions = view.functions(); + let total_functions = view_functions.len(); + let done_functions = AtomicUsize::default(); + let background_task = BackgroundTask::new( + &format!("Generating signatures... ({}/{})", 0, total_functions), + true, + ); + + // Create all of the "built" functions, for the chunk. + // NOTE: This does a bit of filtering to remove undesired functions, look at this if + // a desired function is not in the created chunk. + // TODO: Make this interruptable. with background_task.is_cancelled. + let start = Instant::now(); + let built_functions: DashMap<Target, Vec<Function>> = view_functions + .par_iter() + .inspect(|_| { + done_functions.fetch_add(1, Relaxed); + background_task.set_progress_text(&format!( + "Generating signatures... ({}/{}) [{}s]", + done_functions.load(Relaxed), + total_functions, + start.elapsed().as_secs_f32() + )) + }) + .filter(is_function_tagged) + .filter(is_function_named) + .filter(|f| !f.analysis_skipped()) + .filter_map(|func| { + let lifted_il = func.lifted_il().ok()?; + let target = platform_to_target(&func.platform()); + let mut built_function = build_function(&func, &lifted_il); + // User asked to only save symbols, so we will remove the function type. + if self.file_data == FileDataKindField::Symbols { + built_function.ty = None; + } + Some((target, built_function)) + }) + .fold( + DashMap::new, + |acc: DashMap<Target, Vec<Function>>, (target, function)| { + acc.entry(target).or_default().push(function); + acc + }, + ) + .reduce(DashMap::new, |acc, other| { + other.into_iter().for_each(|(key, value)| { + acc.entry(key).or_default().extend(value); + }); + acc + }); + + let chunks: Result<HashMap<Target, SignatureChunk<'static>>, ProcessingError> = + built_functions + .into_iter() + .map(|(target, functions)| { + Ok(( + target, + SignatureChunk::new(&functions) + .ok_or(ProcessingError::ChunkCreationFailed)?, + )) + }) + .collect(); + + background_task.finish(); + chunks + } + + // TODO: Add a background task here. + pub fn create_type_chunk( + &self, + view: &BinaryView, + ) -> Result<TypeChunk<'static>, ProcessingError> { + let mut referenced_types = Vec::new(); + if let Some(ref_ty_cache) = cached_type_references(view) { + referenced_types = ref_ty_cache + .cache + .iter() + .filter_map(|t| t.to_owned()) + .collect::<Vec<_>>(); + } + TypeChunk::new_with_computed(&referenced_types).ok_or(ProcessingError::ChunkCreationFailed) + } +} + +fn project_file_path(file: &ProjectFile) -> PathBuf { + // Recurse up the folders to build a string like /foldera/folderb/myfile + let mut path = PathBuf::new(); + // Add file name + path.push(file.name()); + // Recursively add parent folder names + let mut current = file.folder(); + while let Some(folder) = current { + path = PathBuf::from(folder.name()).join(path); + current = folder.parent(); + } + path +} diff --git a/plugins/warp/src/report.rs b/plugins/warp/src/report.rs new file mode 100644 index 00000000..8ba9cc8b --- /dev/null +++ b/plugins/warp/src/report.rs @@ -0,0 +1,185 @@ +use binaryninja::interaction::{Form, FormInputField}; +use minijinja::Environment; +use serde::Serialize; +use warp::chunk::{Chunk, ChunkKind}; +use warp::r#type::guid::TypeGUID; +use warp::WarpFile; + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum ReportKindField { + None, + #[default] + Html, + Markdown, + Json, +} + +impl ReportKindField { + pub fn to_field(&self) -> FormInputField { + FormInputField::Choice { + prompt: "Generated Report".to_string(), + choices: vec![ + "None".to_string(), + "HTML".to_string(), + "Markdown".to_string(), + "JSON".to_string(), + ], + default: Some(match self { + Self::None => 0, + Self::Html => 1, + Self::Markdown => 2, + Self::Json => 3, + }), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("Generated Report")?; + let field_value = field.try_value_index()?; + match field_value { + 3 => Some(Self::Json), + 2 => Some(Self::Markdown), + 1 => Some(Self::Html), + _ => Some(Self::None), + } + } +} + +#[derive(Debug, Clone)] +pub struct ReportGenerator { + environment: Environment<'static>, +} + +impl ReportGenerator { + pub fn new() -> Self { + let mut environment = Environment::new(); + // Remove trailing lines for blocks, this is required for Markdown tables. + environment.set_trim_blocks(true); + minijinja_embed::load_templates!(&mut environment); + Self { environment } + } + + pub fn report(&self, kind: &ReportKindField, file: &WarpFile) -> Option<String> { + match kind { + ReportKindField::None => None, + ReportKindField::Html => self.html_report(file), + ReportKindField::Markdown => self.markdown_report(file), + ReportKindField::Json => self.json_report(file), + } + } + + pub fn report_extension(&self, kind: &ReportKindField) -> Option<&'static str> { + match kind { + ReportKindField::None => None, + ReportKindField::Html => Some("html"), + ReportKindField::Markdown => Some("md"), + ReportKindField::Json => Some("json"), + } + } + + pub fn html_report(&self, file: &WarpFile) -> Option<String> { + let data = FileReportData::new(file); + let tmpl = self.environment.get_template("file.html").ok()?; + tmpl.render(data).ok() + } + + pub fn markdown_report(&self, file: &WarpFile) -> Option<String> { + let data = FileReportData::new(file); + let tmpl = self.environment.get_template("file.md").ok()?; + tmpl.render(data).ok() + } + + pub fn json_report(&self, file: &WarpFile) -> Option<String> { + let data = FileReportData::new(file); + let tmpl = self.environment.get_template("file.json").ok()?; + tmpl.render(data).ok() + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct FileReportData { + pub title: String, + // pub header: WarpFileHeader, + pub chunks: Vec<ChunkReportData>, +} + +impl FileReportData { + pub fn new(file: &WarpFile) -> Self { + Self { + title: "Warp File Report".to_string(), + // header: file.header.clone(), + chunks: file + .chunks + .iter() + .map(|chunk| ChunkReportData::new(chunk)) + .collect(), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct ChunkReportData { + pub title: String, + // pub header: ChunkHeader, + pub target: String, + pub total_item_count: usize, + /// View into a (possible subset) of chunk items. + pub item_view: Vec<ItemReportData>, +} + +impl ChunkReportData { + pub fn new(chunk: &Chunk) -> Self { + // TODO: Set a limit for the number of items so we dont construct 10000000 items in the report. + let items: Vec<_> = match &chunk.kind { + ChunkKind::Signature(sc) => sc + .raw_functions() + .map(|f| ItemReportData { + name: f.symbol().and_then(|s| s.name().map(|n| n.to_string())), + guid: f.guid().to_string(), + note: None, + }) + .collect(), + ChunkKind::Type(tc) => tc + .raw_types() + .map(|t| ItemReportData { + name: t.type_().and_then(|s| s.name().map(|n| n.to_string())), + guid: TypeGUID::from(t.guid()).to_string(), + note: None, + }) + .collect(), + }; + + let chunk_type = match &chunk.kind { + ChunkKind::Signature(_) => "Signature".to_string(), + ChunkKind::Type(_) => "Type".to_string(), + }; + + let size_in_kb = chunk.header.size as f64 / 1024.0; + let formatted_size = format!("{:.1}kb", size_in_kb); + + // For the target show the platform, or the architecture if available. + let target = chunk + .header + .target + .platform + .clone() + .or_else(|| chunk.header.target.architecture.clone()) + .unwrap_or_else(|| "None".to_string()); + + Self { + title: format!("{} Chunk ({})", chunk_type, formatted_size), + target, + // header: chunk.header.clone(), + total_item_count: items.len(), + item_view: items, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct ItemReportData { + pub guid: String, + pub name: Option<String>, + pub note: Option<String>, +} diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__ctype.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__ctype.snap deleted file mode 100644 index 0cda5d3a..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__ctype.snap +++ /dev/null @@ -1,126 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 00c71b63-3039-5aa3-94f7-1737530239d9, - }, - FunctionGUID { - guid: 06ff982d-b167-5dce-9689-71a631820da6, - }, - FunctionGUID { - guid: 0d6d3685-7a8a-5d1c-8b7a-fe14871218ff, - }, - FunctionGUID { - guid: 0df54a49-5267-52f7-9665-70a8e66ad0dd, - }, - FunctionGUID { - guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18, - }, - FunctionGUID { - guid: 15d686e8-3e80-5632-9ce7-17c3510f8238, - }, - FunctionGUID { - guid: 18cfce71-47bc-5595-89cb-06e0563b211d, - }, - FunctionGUID { - guid: 1b6aa5a3-ac7f-542d-a0ab-bae8f142d8d8, - }, - FunctionGUID { - guid: 1e794537-6289-59e7-bef9-0c72f3989db8, - }, - FunctionGUID { - guid: 29690354-fa27-54d1-a8be-18535b19b1c3, - }, - FunctionGUID { - guid: 33e6bc5f-eeb6-5a07-810e-f04a2dba36cf, - }, - FunctionGUID { - guid: 3b992cd7-1721-5f30-b59c-84ea682be808, - }, - FunctionGUID { - guid: 3ba4d0cb-9c07-5904-aafe-34e051b89be5, - }, - FunctionGUID { - guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904, - }, - FunctionGUID { - guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c, - }, - FunctionGUID { - guid: 53b484f3-751a-505d-beae-acc9d69c261d, - }, - FunctionGUID { - guid: 5b7cc78f-3b2f-5b82-9726-37c26ad087b4, - }, - FunctionGUID { - guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c, - }, - FunctionGUID { - guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9, - }, - FunctionGUID { - guid: 6db530d1-8ea4-568e-a089-c61164851f04, - }, - FunctionGUID { - guid: 7cae2466-6b19-5fff-869e-9a9717043df4, - }, - FunctionGUID { - guid: 7cae2466-6b19-5fff-869e-9a9717043df4, - }, - FunctionGUID { - guid: 7f0e055b-1e83-5f7b-a1fd-6a0a3d0b74f5, - }, - FunctionGUID { - guid: 88d9b26b-3884-58d6-b164-435d8d888e5c, - }, - FunctionGUID { - guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd, - }, - FunctionGUID { - guid: 8ff1fc25-6912-5772-8fa3-19e1b996aea7, - }, - FunctionGUID { - guid: 951d5c60-d457-5b62-98e7-ade4a37d7cbe, - }, - FunctionGUID { - guid: 95bf9d56-3b11-515f-960f-07abf69aed95, - }, - FunctionGUID { - guid: 9e30f7d4-0a31-50cf-93f8-490a9e8c300b, - }, - FunctionGUID { - guid: a7acb567-eb98-53e4-a843-5c60a8c59f19, - }, - FunctionGUID { - guid: ae6f9d26-872c-5b6e-925e-40c0ec95c702, - }, - FunctionGUID { - guid: b8b2746b-20aa-5ad2-bcfd-ab5448d952eb, - }, - FunctionGUID { - guid: bc4d18a4-96d5-5322-ba99-fcee83648701, - }, - FunctionGUID { - guid: c7bd1444-8f5c-5bc6-87c3-48f010b00455, - }, - FunctionGUID { - guid: cacf9fe4-7518-5b22-ae3a-ab775dea4c9a, - }, - FunctionGUID { - guid: cdb3f650-974f-5686-b81f-10b6544680f4, - }, - FunctionGUID { - guid: d344f254-4903-581d-ad93-713c9ff2be2e, - }, - FunctionGUID { - guid: d5456209-db22-53a1-906d-1ecb3254ab2d, - }, - FunctionGUID { - guid: ec40932b-8293-5ffb-9d31-04c282b6530d, - }, - FunctionGUID { - guid: faedbe75-8451-525b-83da-7a57ec96166e, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__fptostr.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__fptostr.snap deleted file mode 100644 index 5628853f..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__fptostr.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 9d4dba66-7106-5215-8e64-a47279f67dfe, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__mbslen.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__mbslen.snap deleted file mode 100644 index 01ca9ec5..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__mbslen.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18, - }, - FunctionGUID { - guid: 1e794537-6289-59e7-bef9-0c72f3989db8, - }, - FunctionGUID { - guid: 29690354-fa27-54d1-a8be-18535b19b1c3, - }, - FunctionGUID { - guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904, - }, - FunctionGUID { - guid: 6a7eb6dd-be3e-50dd-ae91-cea8f3eb4f06, - }, - FunctionGUID { - guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9, - }, - FunctionGUID { - guid: 6b0d368e-83d9-55fc-9683-5812e651a49b, - }, - FunctionGUID { - guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd, - }, - FunctionGUID { - guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257, - }, - FunctionGUID { - guid: f034945d-32e2-5d86-8a86-3761cc54f419, - }, - FunctionGUID { - guid: f552d8ee-3064-50ed-af18-4e60f095c9ea, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__memicmp.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__memicmp.snap deleted file mode 100644 index 8f83530e..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__memicmp.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: cf6e80f2-69aa-5757-a06f-fb2e4866ab14, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__strnicm.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__strnicm.snap deleted file mode 100644 index 9d661016..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__strnicm.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 7c350a2d-2282-5625-943e-009f317b8d2c, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__wctype.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__wctype.snap deleted file mode 100644 index 2d9f4e73..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__wctype.snap +++ /dev/null @@ -1,117 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 0a9027e0-fc87-5f83-a9da-ea7b1eef0761, - }, - FunctionGUID { - guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18, - }, - FunctionGUID { - guid: 1b434f0e-26b8-539b-9679-33d96a683190, - }, - FunctionGUID { - guid: 1b434f0e-26b8-539b-9679-33d96a683190, - }, - FunctionGUID { - guid: 1e34d278-fd31-5cdf-98dc-395cb29419a7, - }, - FunctionGUID { - guid: 1e34d278-fd31-5cdf-98dc-395cb29419a7, - }, - FunctionGUID { - guid: 1e794537-6289-59e7-bef9-0c72f3989db8, - }, - FunctionGUID { - guid: 23ba7382-bd48-5d47-a1f0-79ad5db87730, - }, - FunctionGUID { - guid: 23ba7382-bd48-5d47-a1f0-79ad5db87730, - }, - FunctionGUID { - guid: 29690354-fa27-54d1-a8be-18535b19b1c3, - }, - FunctionGUID { - guid: 33ce8a60-e3fa-5941-9ca6-09707bb1f579, - }, - FunctionGUID { - guid: 33ce8a60-e3fa-5941-9ca6-09707bb1f579, - }, - FunctionGUID { - guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904, - }, - FunctionGUID { - guid: 607a10c5-8943-5dec-b888-f5210f08b311, - }, - FunctionGUID { - guid: 607a10c5-8943-5dec-b888-f5210f08b311, - }, - FunctionGUID { - guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9, - }, - FunctionGUID { - guid: 6cebba56-9929-5f3d-9dd3-fc56f8e84593, - }, - FunctionGUID { - guid: 75b0aba8-582a-5085-abbb-cebf55d2498d, - }, - FunctionGUID { - guid: 75b0aba8-582a-5085-abbb-cebf55d2498d, - }, - FunctionGUID { - guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd, - }, - FunctionGUID { - guid: 90cb6aa0-f402-5874-ae2d-e10752ad6462, - }, - FunctionGUID { - guid: 90cb6aa0-f402-5874-ae2d-e10752ad6462, - }, - FunctionGUID { - guid: 9dd8ec46-e680-5fbe-b28b-30505b505fa3, - }, - FunctionGUID { - guid: 9dd8ec46-e680-5fbe-b28b-30505b505fa3, - }, - FunctionGUID { - guid: b08dcc9b-b766-56e0-a9af-1d24cc6c6574, - }, - FunctionGUID { - guid: b08dcc9b-b766-56e0-a9af-1d24cc6c6574, - }, - FunctionGUID { - guid: b098f8a1-ecdd-5938-8da3-bb37935c6b7e, - }, - FunctionGUID { - guid: b2339a04-7dcf-5791-b74e-806220d37f56, - }, - FunctionGUID { - guid: b2339a04-7dcf-5791-b74e-806220d37f56, - }, - FunctionGUID { - guid: b3723a76-a709-510c-9d93-97dc42b163a3, - }, - FunctionGUID { - guid: b3723a76-a709-510c-9d93-97dc42b163a3, - }, - FunctionGUID { - guid: cc259c7e-0ada-5680-8ad7-b077d60bd2a1, - }, - FunctionGUID { - guid: cc259c7e-0ada-5680-8ad7-b077d60bd2a1, - }, - FunctionGUID { - guid: eed08c42-3616-5c27-82ad-719db5cce36e, - }, - FunctionGUID { - guid: eed08c42-3616-5c27-82ad-719db5cce36e, - }, - FunctionGUID { - guid: f6535880-21d4-5272-b2c0-5e63cbc208e7, - }, - FunctionGUID { - guid: f6535880-21d4-5272-b2c0-5e63cbc208e7, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atof.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atof.snap deleted file mode 100644 index 4ff8251c..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atof.snap +++ /dev/null @@ -1,390 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d, - }, - FunctionGUID { - guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d, - }, - FunctionGUID { - guid: 06a9339a-7249-50f3-9c3d-131d3248d560, - }, - FunctionGUID { - guid: 06a9339a-7249-50f3-9c3d-131d3248d560, - }, - FunctionGUID { - guid: 06a9339a-7249-50f3-9c3d-131d3248d560, - }, - FunctionGUID { - guid: 06a9339a-7249-50f3-9c3d-131d3248d560, - }, - FunctionGUID { - guid: 07c4bac9-cf7d-5e00-833d-d23ba44c07d9, - }, - FunctionGUID { - guid: 0916139e-a5db-51c8-be24-2c6fb20a9e3a, - }, - FunctionGUID { - guid: 0c7b078d-dafb-5be7-8b10-a25b1e691b33, - }, - FunctionGUID { - guid: 0e6feb85-ff6f-5b86-bd2c-26fb9829241c, - }, - FunctionGUID { - guid: 0e6feb85-ff6f-5b86-bd2c-26fb9829241c, - }, - FunctionGUID { - guid: 11592e15-a2a6-51b3-bda2-534cc0434df0, - }, - FunctionGUID { - guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18, - }, - FunctionGUID { - guid: 1b1669c2-4a81-5300-99f7-14cba1cc7b24, - }, - FunctionGUID { - guid: 1b969932-96ce-5a6f-b91c-82e5f7b7f33d, - }, - FunctionGUID { - guid: 1c037c1e-9af1-59af-b19e-122fdf885148, - }, - FunctionGUID { - guid: 1de88f85-a19c-5454-9122-ddc80f7509b4, - }, - FunctionGUID { - guid: 1de88f85-a19c-5454-9122-ddc80f7509b4, - }, - FunctionGUID { - guid: 1e794537-6289-59e7-bef9-0c72f3989db8, - }, - FunctionGUID { - guid: 1eee5e05-95ef-5f99-8713-504c2003183c, - }, - FunctionGUID { - guid: 2833f544-9acb-552d-aa4b-df7d51dbab64, - }, - FunctionGUID { - guid: 29690354-fa27-54d1-a8be-18535b19b1c3, - }, - FunctionGUID { - guid: 31a8e73e-74fe-5a33-b0ab-aa1b136e021d, - }, - FunctionGUID { - guid: 364c5864-0cd1-5527-8ca0-2378d5b4c0a5, - }, - FunctionGUID { - guid: 3c38a678-9c4e-527a-b543-3075b345d532, - }, - FunctionGUID { - guid: 3c38a678-9c4e-527a-b543-3075b345d532, - }, - FunctionGUID { - guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904, - }, - FunctionGUID { - guid: 3eb2a4c5-84f5-5687-92a0-35ef66c8b746, - }, - FunctionGUID { - guid: 3efd6327-6038-53ed-904c-a800bee86d2e, - }, - FunctionGUID { - guid: 40358a63-7e16-52da-9270-434c7d444b10, - }, - FunctionGUID { - guid: 40358a63-7e16-52da-9270-434c7d444b10, - }, - FunctionGUID { - guid: 40358a63-7e16-52da-9270-434c7d444b10, - }, - FunctionGUID { - guid: 410b1cab-55ff-5931-a894-54842e56bee2, - }, - FunctionGUID { - guid: 41fd6d24-0f8b-5848-9873-65b98bd370ab, - }, - FunctionGUID { - guid: 4345609a-e619-5f33-84ca-7cc1ccdfa2d0, - }, - FunctionGUID { - guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c, - }, - FunctionGUID { - guid: 45770948-1c56-5b24-bd38-f5a559ae8d22, - }, - FunctionGUID { - guid: 45770948-1c56-5b24-bd38-f5a559ae8d22, - }, - FunctionGUID { - guid: 479b6feb-0dff-5c33-8f30-fd0c563b9abc, - }, - FunctionGUID { - guid: 4b60a419-4ad1-5808-b41f-d341868c9234, - }, - FunctionGUID { - guid: 4ba8a311-60f3-57d4-a17a-c0cc93f5c2a4, - }, - FunctionGUID { - guid: 4f0bdeb9-691c-52f8-8541-6c834f3fbe64, - }, - FunctionGUID { - guid: 4f645f3c-3e89-5b5d-a185-9f1a25e94ed2, - }, - FunctionGUID { - guid: 52b2a610-5304-5e4f-8b79-9b975e9efb86, - }, - FunctionGUID { - guid: 53b484f3-751a-505d-beae-acc9d69c261d, - }, - FunctionGUID { - guid: 53b484f3-751a-505d-beae-acc9d69c261d, - }, - FunctionGUID { - guid: 56c82b1f-380c-52fe-ae70-a39962e5ffdc, - }, - FunctionGUID { - guid: 5779439c-a22f-5d33-8868-4e1832ad49e9, - }, - FunctionGUID { - guid: 5b55a8c8-1d3a-5825-a0d9-917f434e07af, - }, - FunctionGUID { - guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c, - }, - FunctionGUID { - guid: 65be15d9-ef02-58a4-af63-b40ddb3e56b7, - }, - FunctionGUID { - guid: 669a32f2-f1e9-5b38-98aa-9eb8431ebc4e, - }, - FunctionGUID { - guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9, - }, - FunctionGUID { - guid: 6c01cd47-89a2-59f3-ac6a-cd7806fb8f79, - }, - FunctionGUID { - guid: 72452dc5-da9c-54fc-82de-032aab1780a4, - }, - FunctionGUID { - guid: 7995ca73-04fe-5b30-9e88-1e69197007c1, - }, - FunctionGUID { - guid: 7995ca73-04fe-5b30-9e88-1e69197007c1, - }, - FunctionGUID { - guid: 7c43c697-22ef-57a9-a43a-2214e627749d, - }, - FunctionGUID { - guid: 7cbd1cef-0134-5a29-a8c6-82697f5717fe, - }, - FunctionGUID { - guid: 7d4d1429-5fef-5c8b-9074-93e9d97ab57e, - }, - FunctionGUID { - guid: 82e98c80-859c-5ab3-83ab-b597fc520a3f, - }, - FunctionGUID { - guid: 84429ba2-9f72-585f-8cda-31e8ef4983fd, - }, - FunctionGUID { - guid: 86ab6ab4-45e5-5c08-9a47-05344ea503ab, - }, - FunctionGUID { - guid: 86ab6ab4-45e5-5c08-9a47-05344ea503ab, - }, - FunctionGUID { - guid: 87371a48-38c9-5e0e-81db-5e2d6da49202, - }, - FunctionGUID { - guid: 879b1d59-7301-5cb7-911e-db05292bee8f, - }, - FunctionGUID { - guid: 88d9b26b-3884-58d6-b164-435d8d888e5c, - }, - FunctionGUID { - guid: 89f0e45b-c1f6-5aeb-94a8-26e72bb66e6c, - }, - FunctionGUID { - guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd, - }, - FunctionGUID { - guid: 8c736b66-b63f-5000-a060-2e60a1b8952a, - }, - FunctionGUID { - guid: 8c736b66-b63f-5000-a060-2e60a1b8952a, - }, - FunctionGUID { - guid: 8c736b66-b63f-5000-a060-2e60a1b8952a, - }, - FunctionGUID { - guid: 8dca0c9d-bdb7-56a1-9832-431aec16a68f, - }, - FunctionGUID { - guid: 9a9e3287-e407-5526-885b-1b5ee78a5300, - }, - FunctionGUID { - guid: 9e4c3806-b291-569c-a687-0bcaa48e248b, - }, - FunctionGUID { - guid: 9ef21651-3eaf-5940-8e72-104bda834e85, - }, - FunctionGUID { - guid: a0c07bfa-a027-5c19-9da4-d2d35d7beb40, - }, - FunctionGUID { - guid: a0c07bfa-a027-5c19-9da4-d2d35d7beb40, - }, - FunctionGUID { - guid: a23505e4-e7cd-5543-897c-46b97c1eaef3, - }, - FunctionGUID { - guid: a482559e-6a69-505e-b692-147deeced692, - }, - FunctionGUID { - guid: a482559e-6a69-505e-b692-147deeced692, - }, - FunctionGUID { - guid: a5b947d4-a7e0-5072-86b3-240474c16595, - }, - FunctionGUID { - guid: a603d813-2b33-567d-80c8-06e31562a400, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: adadfe23-cb3f-5c9f-ae8d-1dd2fd922367, - }, - FunctionGUID { - guid: b098f8a1-ecdd-5938-8da3-bb37935c6b7e, - }, - FunctionGUID { - guid: b098f8a1-ecdd-5938-8da3-bb37935c6b7e, - }, - FunctionGUID { - guid: b0aca296-adc3-5b46-a6b1-e82ef2100ead, - }, - FunctionGUID { - guid: b9111cf4-dcff-52a5-8755-2dedea6d5c72, - }, - FunctionGUID { - guid: bbb9b018-a0cd-5f98-b941-367f8707434d, - }, - FunctionGUID { - guid: bbb9b018-a0cd-5f98-b941-367f8707434d, - }, - FunctionGUID { - guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257, - }, - FunctionGUID { - guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257, - }, - FunctionGUID { - guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203, - }, - FunctionGUID { - guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203, - }, - FunctionGUID { - guid: c44bdef1-d78a-57ec-bff5-4e7d32ff4337, - }, - FunctionGUID { - guid: c71f4559-f0af-54dc-a224-e1f341311988, - }, - FunctionGUID { - guid: c71f4559-f0af-54dc-a224-e1f341311988, - }, - FunctionGUID { - guid: c74ed160-24fe-50d7-ba1c-9751c4599420, - }, - FunctionGUID { - guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c, - }, - FunctionGUID { - guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c, - }, - FunctionGUID { - guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83, - }, - FunctionGUID { - guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83, - }, - FunctionGUID { - guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83, - }, - FunctionGUID { - guid: cf4808fe-2fea-517d-8f85-f88ddc96cc78, - }, - FunctionGUID { - guid: cf4808fe-2fea-517d-8f85-f88ddc96cc78, - }, - FunctionGUID { - guid: cfa4374b-0fc7-575d-9ba1-4a37d2de0406, - }, - FunctionGUID { - guid: d72f40b9-0557-5695-aa4d-468515ec4b8e, - }, - FunctionGUID { - guid: d8ba616a-1747-587c-9265-cd4e26ae35c8, - }, - FunctionGUID { - guid: db957206-4cca-5631-893d-1d3dc500c804, - }, - FunctionGUID { - guid: dd480d6b-04f7-5fb2-a7b2-236e1d78b2b0, - }, - FunctionGUID { - guid: e1b1e1df-fdb9-59d6-b966-5b7f926b5649, - }, - FunctionGUID { - guid: e480dd82-0b7d-5c75-ac67-ba8c0bb755cb, - }, - FunctionGUID { - guid: e4ba95bb-0221-5dbd-9884-13a39c4f9d55, - }, - FunctionGUID { - guid: e8290b3d-cf56-5c1b-8315-abce0f135559, - }, - FunctionGUID { - guid: e8290b3d-cf56-5c1b-8315-abce0f135559, - }, - FunctionGUID { - guid: ecca9a9d-1722-5344-b68f-54f4527c0d78, - }, - FunctionGUID { - guid: ed23805d-3729-590d-83a3-3b0803a56870, - }, - FunctionGUID { - guid: eefbc8a9-f4f1-5725-9a4f-9dcd04ea3478, - }, - FunctionGUID { - guid: f37337a6-4cf0-55b4-a0da-25a15e3d9bcb, - }, - FunctionGUID { - guid: f37cf37b-b19d-5575-b2c7-a6604d3faa69, - }, - FunctionGUID { - guid: f3d57592-4cb1-5356-9591-1f9327c58565, - }, - FunctionGUID { - guid: fb20fa7c-c796-58e4-a2f9-7a128baaf5b1, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atoldbl.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atoldbl.snap deleted file mode 100644 index 5dc305cc..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atoldbl.snap +++ /dev/null @@ -1,192 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d, - }, - FunctionGUID { - guid: 07b3f23f-333b-5c13-873f-5f1973cee8e2, - }, - FunctionGUID { - guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18, - }, - FunctionGUID { - guid: 1de88f85-a19c-5454-9122-ddc80f7509b4, - }, - FunctionGUID { - guid: 1e794537-6289-59e7-bef9-0c72f3989db8, - }, - FunctionGUID { - guid: 22c32840-7bb0-5d80-858c-55dd04553db0, - }, - FunctionGUID { - guid: 29690354-fa27-54d1-a8be-18535b19b1c3, - }, - FunctionGUID { - guid: 29690354-fa27-54d1-a8be-18535b19b1c3, - }, - FunctionGUID { - guid: 30f44555-ef86-5e85-96c3-6153f4bcc11e, - }, - FunctionGUID { - guid: 364c5864-0cd1-5527-8ca0-2378d5b4c0a5, - }, - FunctionGUID { - guid: 39cd0d51-d688-58e2-a757-ac868ff8635e, - }, - FunctionGUID { - guid: 3c83a5f5-215d-5d25-97c8-ffe65f73907c, - }, - FunctionGUID { - guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904, - }, - FunctionGUID { - guid: 41fd6d24-0f8b-5848-9873-65b98bd370ab, - }, - FunctionGUID { - guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c, - }, - FunctionGUID { - guid: 45770948-1c56-5b24-bd38-f5a559ae8d22, - }, - FunctionGUID { - guid: 4738dcb9-abab-5157-baef-8edf38d9fabc, - }, - FunctionGUID { - guid: 48551d5f-2e1f-557b-85a3-a122d3ce07cf, - }, - FunctionGUID { - guid: 4ba8a311-60f3-57d4-a17a-c0cc93f5c2a4, - }, - FunctionGUID { - guid: 4f645f3c-3e89-5b5d-a185-9f1a25e94ed2, - }, - FunctionGUID { - guid: 5779439c-a22f-5d33-8868-4e1832ad49e9, - }, - FunctionGUID { - guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c, - }, - FunctionGUID { - guid: 61d32a04-c077-5985-b241-c326dd5b1864, - }, - FunctionGUID { - guid: 6444ff14-38c5-520e-b9e3-6e3ae15215c4, - }, - FunctionGUID { - guid: 6589669f-6b11-569e-9adf-ca3c6d6bc62e, - }, - FunctionGUID { - guid: 6590d978-d2b0-510c-b5a6-a81361800609, - }, - FunctionGUID { - guid: 67f303c6-d2e4-55e1-8a5f-5492da69e69f, - }, - FunctionGUID { - guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9, - }, - FunctionGUID { - guid: 6ef93fa8-909e-596c-9454-74b89e8cabb7, - }, - FunctionGUID { - guid: 7995ca73-04fe-5b30-9e88-1e69197007c1, - }, - FunctionGUID { - guid: 7c43c697-22ef-57a9-a43a-2214e627749d, - }, - FunctionGUID { - guid: 86ab6ab4-45e5-5c08-9a47-05344ea503ab, - }, - FunctionGUID { - guid: 88d9b26b-3884-58d6-b164-435d8d888e5c, - }, - FunctionGUID { - guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd, - }, - FunctionGUID { - guid: 8c736b66-b63f-5000-a060-2e60a1b8952a, - }, - FunctionGUID { - guid: 8c736b66-b63f-5000-a060-2e60a1b8952a, - }, - FunctionGUID { - guid: 8c736b66-b63f-5000-a060-2e60a1b8952a, - }, - FunctionGUID { - guid: 8ccf8b37-65c5-525a-b96a-64f26d711c75, - }, - FunctionGUID { - guid: 9676fc90-ecba-595b-8d24-5df4e66ac683, - }, - FunctionGUID { - guid: 9891b84c-000b-529a-8c5b-d762120eff9d, - }, - FunctionGUID { - guid: a030f6f1-e29b-5e4a-8f83-56e6c6c51c15, - }, - FunctionGUID { - guid: a23505e4-e7cd-5543-897c-46b97c1eaef3, - }, - FunctionGUID { - guid: a45d9322-0d45-5dc9-b17b-32b843416c4b, - }, - FunctionGUID { - guid: a482559e-6a69-505e-b692-147deeced692, - }, - FunctionGUID { - guid: a603d813-2b33-567d-80c8-06e31562a400, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5, - }, - FunctionGUID { - guid: b9e4b6b0-47c3-54ca-9a01-56916ea0db8e, - }, - FunctionGUID { - guid: bae57993-863a-5c22-b8bb-92b862148af3, - }, - FunctionGUID { - guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257, - }, - FunctionGUID { - guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203, - }, - FunctionGUID { - guid: c5766cb0-afbc-5701-ad2f-23d980f4bc15, - }, - FunctionGUID { - guid: cb8ebff6-30e8-5a8a-9c1b-ec5f26041d65, - }, - FunctionGUID { - guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c, - }, - FunctionGUID { - guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83, - }, - FunctionGUID { - guid: cf4808fe-2fea-517d-8f85-f88ddc96cc78, - }, - FunctionGUID { - guid: e3b83b64-6ba1-5fe5-97da-5db987d79b07, - }, - FunctionGUID { - guid: eefbc8a9-f4f1-5725-9a4f-9dcd04ea3478, - }, - FunctionGUID { - guid: efcd394f-9201-57f7-aa2d-fc89ad355326, - }, - FunctionGUID { - guid: efcd394f-9201-57f7-aa2d-fc89ad355326, - }, - FunctionGUID { - guid: f25e7750-c61c-5ed8-a43d-959c3ba3a5b4, - }, -] diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atox.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atox.snap deleted file mode 100644 index c9960a24..00000000 --- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atox.snap +++ /dev/null @@ -1,186 +0,0 @@ ---- -source: plugins/warp/src/lib.rs -expression: functions ---- -[ - FunctionGUID { - guid: 0244fa6b-6425-5aa9-ad87-2378cc4992e7, - }, - FunctionGUID { - guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d, - }, - FunctionGUID { - guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d, - }, - FunctionGUID { - guid: 067d6811-472a-5e4a-b65f-3760df0b7bd5, - }, - FunctionGUID { - guid: 0916139e-a5db-51c8-be24-2c6fb20a9e3a, - }, - FunctionGUID { - guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18, - }, - FunctionGUID { - guid: 1de88f85-a19c-5454-9122-ddc80f7509b4, - }, - FunctionGUID { - guid: 1de88f85-a19c-5454-9122-ddc80f7509b4, - }, - FunctionGUID { - guid: 1e794537-6289-59e7-bef9-0c72f3989db8, - }, - FunctionGUID { - guid: 283e5e36-a239-5f0c-8937-f48a4b1d86e0, - }, - FunctionGUID { - guid: 29690354-fa27-54d1-a8be-18535b19b1c3, - }, - FunctionGUID { - guid: 31a8e73e-74fe-5a33-b0ab-aa1b136e021d, - }, - FunctionGUID { - guid: 33717d5d-a15f-5c10-bc4a-d85cce963aca, - }, - FunctionGUID { - guid: 39d556f9-435a-5105-b56c-6558ddc63fc0, - }, - FunctionGUID { - guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904, - }, - FunctionGUID { - guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024, - }, - FunctionGUID { - guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024, - }, - FunctionGUID { - guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024, - }, - FunctionGUID { - guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024, - }, - FunctionGUID { - guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c, - }, - FunctionGUID { - guid: 46eae5e0-61e2-5316-9e4d-030dd2240567, - }, - FunctionGUID { - guid: 4ba8a311-60f3-57d4-a17a-c0cc93f5c2a4, - }, - FunctionGUID { - guid: 56c82b1f-380c-52fe-ae70-a39962e5ffdc, - }, - FunctionGUID { - guid: 5779439c-a22f-5d33-8868-4e1832ad49e9, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 580affd8-2657-50b2-9b65-3327422894de, - }, - FunctionGUID { - guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c, - }, - FunctionGUID { - guid: 65cb411a-0ae9-546f-9293-3abaa3fd1c31, - }, - FunctionGUID { - guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9, - }, - FunctionGUID { - guid: 7995ca73-04fe-5b30-9e88-1e69197007c1, - }, - FunctionGUID { - guid: 7995ca73-04fe-5b30-9e88-1e69197007c1, - }, - FunctionGUID { - guid: 88d9b26b-3884-58d6-b164-435d8d888e5c, - }, - FunctionGUID { - guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd, - }, - FunctionGUID { - guid: 9ef21651-3eaf-5940-8e72-104bda834e85, - }, - FunctionGUID { - guid: a23505e4-e7cd-5543-897c-46b97c1eaef3, - }, - FunctionGUID { - guid: a482559e-6a69-505e-b692-147deeced692, - }, - FunctionGUID { - guid: a482559e-6a69-505e-b692-147deeced692, - }, - FunctionGUID { - guid: a603d813-2b33-567d-80c8-06e31562a400, - }, - FunctionGUID { - guid: a8a9d162-b637-5ce6-82f8-a8ac0fb740dc, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: bdf90a04-d375-59b1-8a92-b962b3f914ae, - }, - FunctionGUID { - guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203, - }, - FunctionGUID { - guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203, - }, - FunctionGUID { - guid: c66dd704-899b-5d58-9d4a-136063f5f552, - }, - FunctionGUID { - guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c, - }, - FunctionGUID { - guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c, - }, - FunctionGUID { - guid: f674b84a-7a05-5ca0-aac4-5b5598683087, - }, - FunctionGUID { - guid: f762c527-0674-52ce-b59f-49bf4e13d878, - }, -] diff --git a/plugins/warp/src/templates/file.html b/plugins/warp/src/templates/file.html new file mode 100644 index 00000000..7ea465ef --- /dev/null +++ b/plugins/warp/src/templates/file.html @@ -0,0 +1,37 @@ +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{{ title }}</title> +</head> +<body> +<h1>{{ title }}</h1> + +{% for chunk in chunks %} +<section> + <h2>{{ chunk.title }}</h2> + <p>Target: {{ chunk.target }}</p> + <p>Total items: {{ chunk.total_item_count }}</p> + + <table> + <thead> + <tr> + <th>GUID</th> + <th>Name</th> + <th>Note</th> + </tr> + </thead> + <tbody> + {% for item in chunk.item_view %} + <tr> + <td>{{ item.guid }}</td> + <td>{{ item.name or 'N/A' }}</td> + <td>{{ item.note or 'N/A' }}</td> + </tr> + {% endfor %} + </tbody> + </table> +</section> +{% endfor %} +</body> +</html>
\ No newline at end of file diff --git a/plugins/warp/src/templates/file.json b/plugins/warp/src/templates/file.json new file mode 100644 index 00000000..764d35cd --- /dev/null +++ b/plugins/warp/src/templates/file.json @@ -0,0 +1,21 @@ +{ + "title": "{{ title }}", + "chunks": [ + {% for chunk in chunks %} + { + "title": "{{ chunk.title }}", + "target": "{{ chunk.target }}", + "total_item_count": {{ chunk.total_item_count }}, + "item_view": [ + {% for item in chunk.item_view %} + { + "guid": "{{ item.guid }}", + "name": "{{ item.name or 'N/A' }}", + "note": "{{ item.note or 'N/A' }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] +}
\ No newline at end of file diff --git a/plugins/warp/src/templates/file.md b/plugins/warp/src/templates/file.md new file mode 100644 index 00000000..433cb8e9 --- /dev/null +++ b/plugins/warp/src/templates/file.md @@ -0,0 +1,15 @@ +# {{ title }} + +{% for chunk in chunks %} +## {{ chunk.title }} + +Target: {{ chunk.target }} + +Total items: {{ chunk.total_item_count }} + +| GUID | Name | Note | +|--------------|--------------|--------------| +{% for item in chunk.item_view -%} +| {{ item.guid }} | {{ item.name or 'N/A' }} | {{ item.note or 'N/A' }} | +{% endfor %} +{% endfor %} |
